25
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: Gitea Auto Deploy
|
||||||
|
run-name: ${{ gitea.actor }} pushed code 🚀
|
||||||
|
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Deploy-Container:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out latest code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Stop and remove old containers
|
||||||
|
run: |
|
||||||
|
docker compose down || true
|
||||||
|
|
||||||
|
- name: Remove unused Docker resources
|
||||||
|
run: |
|
||||||
|
docker system prune -a --volumes -f
|
||||||
|
|
||||||
|
- name: Build and restart containers
|
||||||
|
run: |
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d --build
|
||||||
45
.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
.history/
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
test1.json
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
|
||||||
|
*.exe
|
||||||
55
Dockerfile
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
FROM oven/bun:canary-alpine AS base
|
||||||
|
|
||||||
|
FROM base AS deps
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json bun.lock ./
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# Rebuild the source code only when needed
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Next.js collects completely anonymous telemetry data about general usage.
|
||||||
|
# Learn more here: https://nextjs.org/telemetry
|
||||||
|
# Disable telemetry during the build
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
# Production image, copy all the files and run next
|
||||||
|
FROM base AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Disable telemetry
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder /app/messages ./messages
|
||||||
|
|
||||||
|
# Set the correct permission for prerender cache
|
||||||
|
RUN mkdir .next
|
||||||
|
RUN chown nextjs:bun .next
|
||||||
|
|
||||||
|
# Automatically leverage output traces to reduce image size
|
||||||
|
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||||
|
COPY --from=builder --chown=nextjs:bun /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:bun /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV PORT=3000
|
||||||
|
|
||||||
|
# Set hostname to localhost
|
||||||
|
ENV HOSTNAME=0.0.0.0
|
||||||
|
|
||||||
|
CMD ["bun", "server.js"]
|
||||||
62
README.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Firefly SrTools
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
**Firefly SrTools** is a modern, high-performance web toolkit designed to streamline workflows and provide essential services. This project is built upon the powerful synergy of the **Next.js** framework and the ultra-fast **Bun.js** runtime, ensuring lightning-fast development and deployment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Live Sites
|
||||||
|
|
||||||
|
The application is deployed and available at the following URLs:
|
||||||
|
|
||||||
|
| Role | URL |
|
||||||
|
| :--- | :--- |
|
||||||
|
| **Main Site** | [srtools.punklorde.org](https://srtools.punklorde.org) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
* **Optimized Performance:** Leveraging Bun for dependency management, scripting, and development server speed.
|
||||||
|
* **Next.js App Router:** Modern routing and data fetching using the latest Next.js architecture.
|
||||||
|
* **Server-Side Rendering (SSR):** Enhanced SEO and faster initial load times for improved user experience.
|
||||||
|
* **Robust Type Safety:** Built with TypeScript for reliable and maintainable code.
|
||||||
|
* **Modular Design:** Clean separation of concerns with reusable components and utilities.
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
* **Frontend Framework:** [Next.js 15](https://nextjs.org/) (React)
|
||||||
|
* **Runtime & Package Manager:** [Bun](https://bun.sh/)
|
||||||
|
* **Language:** TypeScript
|
||||||
|
* **Styling:** (Tailwind CSS, DaisyUI)
|
||||||
|
* **State Management:** (Zustand)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Ensure you have **Bun** installed on your system.
|
||||||
|
|
||||||
|
Install Bun (macOS, WSL, Linux):
|
||||||
|
```bash
|
||||||
|
curl -fsSL [https://bun.sh/install](https://bun.sh/install) | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
```bash
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
```bash
|
||||||
|
bun run build
|
||||||
|
bun run start
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
BIN
data/as.json.br
Normal file
BIN
data/avatar.json.br
Normal file
34
data/changelog.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"version": "4.1.6",
|
||||||
|
"date": "12/04/2026",
|
||||||
|
"type": "update",
|
||||||
|
"items": [
|
||||||
|
"Support RobinSR"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "4.1.5",
|
||||||
|
"date": "17/03/2026",
|
||||||
|
"type": "update",
|
||||||
|
"items": [
|
||||||
|
"New data for 4.1.5"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "4.0.5",
|
||||||
|
"date": "09/02/2026",
|
||||||
|
"type": "update",
|
||||||
|
"items": [
|
||||||
|
"New data for 4.0.5"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "3.8.5",
|
||||||
|
"date": "11/01/2026",
|
||||||
|
"type": "improvement",
|
||||||
|
"items": [
|
||||||
|
"Support skills_by_anchor_type"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
BIN
data/lightcone.json.br
Normal file
BIN
data/metadata.json.br
Normal file
BIN
data/moc.json.br
Normal file
BIN
data/monster.json.br
Normal file
BIN
data/peak.json.br
Normal file
BIN
data/pf.json.br
Normal file
BIN
data/relic.json.br
Normal file
15
docker-compose.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
services:
|
||||||
|
srtools:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: srtools
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3009:3000"
|
||||||
|
networks:
|
||||||
|
- srtools-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
srtools-network:
|
||||||
|
driver: bridge
|
||||||
13
eslint.config.mjs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
|
||||||
|
import nextTypescript from "eslint-config-next/typescript";
|
||||||
|
import { dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const eslintConfig = [...nextCoreWebVitals, ...nextTypescript, {
|
||||||
|
ignores: ["node_modules/**", ".next/**", "out/**", "build/**", "next-env.d.ts"]
|
||||||
|
}];
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
11
i18n/request.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { getRequestConfig } from "next-intl/server";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
|
export default getRequestConfig( async () => {
|
||||||
|
const locale = (await cookies()).get("MYNEXTAPP_LOCALE")?.value || "en";
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
messages: (await import(`../messages/${locale}.json`)).default
|
||||||
|
}
|
||||||
|
})
|
||||||
284
messages/en.json
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
{
|
||||||
|
"TabTitle": {
|
||||||
|
"title": "Firefly Tools",
|
||||||
|
"description": "Firefly tools by Firefly Shelter"
|
||||||
|
},
|
||||||
|
"DataPage": {
|
||||||
|
"skillType": "Skill Type",
|
||||||
|
"skillName": "Skill Name",
|
||||||
|
"character": "Character",
|
||||||
|
"id": "Id",
|
||||||
|
"path": "Path",
|
||||||
|
"rarity": "Rarity",
|
||||||
|
"element": "Element",
|
||||||
|
"technique": "Technique",
|
||||||
|
"talent": "Talent",
|
||||||
|
"basic": "Basic Attack",
|
||||||
|
"skill": "Skill",
|
||||||
|
"ultimate": "Ultimate",
|
||||||
|
"servant": "Servant",
|
||||||
|
"damage": "Damage",
|
||||||
|
"type": "Type",
|
||||||
|
"warrior": "The Destruction",
|
||||||
|
"knight": "The Preservation",
|
||||||
|
"mage": "The Erudition",
|
||||||
|
"priest": "The Abundance",
|
||||||
|
"rogue": "The Hunt",
|
||||||
|
"shaman": "The Harmony",
|
||||||
|
"warlock": "The Nihility",
|
||||||
|
"memory": "The Remembrance",
|
||||||
|
"elation": "The Elation",
|
||||||
|
"fire": "Fire",
|
||||||
|
"ice": "Ice",
|
||||||
|
"imaginary": "Imaginary",
|
||||||
|
"physical": "Physical",
|
||||||
|
"quantum": "Quantum",
|
||||||
|
"thunder": "Thunder",
|
||||||
|
"wind": "Wind",
|
||||||
|
"hp": "Hp",
|
||||||
|
"atk": "Atk",
|
||||||
|
"speed": "Speed",
|
||||||
|
"critRate": "Crit Rate",
|
||||||
|
"critDmg": "Crit Dmg",
|
||||||
|
"breakEffect": "Break Effect",
|
||||||
|
"effectRes": "Effect Res",
|
||||||
|
"energyRegenerationRate": "Energy Regeneration Rate",
|
||||||
|
"effectHitRate": "Effect Hit Rate",
|
||||||
|
"outgoingHealingBoost": "Outgoing Healing Boost",
|
||||||
|
"fireDmgBoost": "Fire damage boost",
|
||||||
|
"iceDmgBoost": "Ice damage Boost",
|
||||||
|
"imaginaryDmgBoost": "Imaginary damage boost",
|
||||||
|
"physicalDmgBoost": "Physical damage boost",
|
||||||
|
"quantumDmgBoost": "Quantum damage boost",
|
||||||
|
"thunderDmgBoost": "Thunder damage boost",
|
||||||
|
"windDmgBoost": "Wind damage boost",
|
||||||
|
"pursued": "Additional damage",
|
||||||
|
"true damage": "True damage",
|
||||||
|
"elationdamage": "Elation damage",
|
||||||
|
"follow-up": "Follow-up Damage",
|
||||||
|
"elemental damage": "Break and Super break damage",
|
||||||
|
"dot": "Damage over time ",
|
||||||
|
"qte": "QTE Skill",
|
||||||
|
"level": "Level",
|
||||||
|
"relics": "Relics",
|
||||||
|
"eidolons": "Eidolons",
|
||||||
|
"lightcones": "Lightcones",
|
||||||
|
"loadData": "Load data",
|
||||||
|
"exportData": "Export data",
|
||||||
|
"connectSetting": "Connection Setting",
|
||||||
|
"connected": "Connected",
|
||||||
|
"unconnected": "Unconnected",
|
||||||
|
"psConnection": "PS Connection",
|
||||||
|
"connectionType": "Connection Type",
|
||||||
|
"status": "Status",
|
||||||
|
"connectPs": "Connect PS",
|
||||||
|
"disconnect": "Disconnect",
|
||||||
|
"other": "Other",
|
||||||
|
"freeSr": "FreeSR",
|
||||||
|
"database": "Database",
|
||||||
|
"enka": "Enka",
|
||||||
|
"monsterSetting": "Monster Setting",
|
||||||
|
"serverUrl": "Server URL",
|
||||||
|
"privateType": "Private Type",
|
||||||
|
"local": "Local",
|
||||||
|
"server": "Server",
|
||||||
|
"username": "Username",
|
||||||
|
"password": "Password",
|
||||||
|
"placeholderServerUrl": "Enter server URL",
|
||||||
|
"placeholderUsername": "Enter username",
|
||||||
|
"placeholderPassword": "Enter password",
|
||||||
|
"connectedSuccess": "Connected to PS successfully",
|
||||||
|
"connectedFailed": "Failed to connect to PS",
|
||||||
|
"syncSuccess": "Synced data to PS successfully",
|
||||||
|
"syncFailed": "Failed to sync data to PS",
|
||||||
|
"sync": "Sync",
|
||||||
|
"importSetting": "Import Setting",
|
||||||
|
"profile": "Profile",
|
||||||
|
"default": "Default",
|
||||||
|
"copyProfiles": "Copy profiles",
|
||||||
|
"addNewProfile": "Add new profile",
|
||||||
|
"createNewProfile": "Create new profile",
|
||||||
|
"editProfile": "Edit profile",
|
||||||
|
"placeholderProfileName": "Enter profile name",
|
||||||
|
"profileName": "Profile name",
|
||||||
|
"create": "Create",
|
||||||
|
"update": "Update",
|
||||||
|
"characterInformation": "Character Information",
|
||||||
|
"skills": "Skills",
|
||||||
|
"showcaseCard": "Showcase Card",
|
||||||
|
"comingSoon": "Coming soon",
|
||||||
|
"characterName": "Character name",
|
||||||
|
"placeholderCharacter": "Enter character name",
|
||||||
|
"characterSettings": "Character Settings",
|
||||||
|
"levelConfiguration": "Level Configuration",
|
||||||
|
"characterLevel": "Character Level",
|
||||||
|
"max": "MAX",
|
||||||
|
"ultimateEnergy": "Ultimate Energy",
|
||||||
|
"currentEnergy": "Current Energy",
|
||||||
|
"setTo50": "Set to 50%",
|
||||||
|
"battleConfiguration": "Battle Configuration",
|
||||||
|
"useTechnique": "Use Technique",
|
||||||
|
"techniqueNote": "Enable pre-battle technique effects",
|
||||||
|
"enhancement": "Enhancement",
|
||||||
|
"enhancementLevel": "Enhancement Level",
|
||||||
|
"origin": "Origin",
|
||||||
|
"enhancedNote": "Higher enhanced unlock additional abilities",
|
||||||
|
"lightconeEquipment": "Lightcone Equipment",
|
||||||
|
"lightconeSettings": "Lightcone Settings",
|
||||||
|
"placeholderLevel": "Enter level",
|
||||||
|
"superimpositionRank": "Superimposition Rank",
|
||||||
|
"ranksNote": "Higher ranks provide stronger effects",
|
||||||
|
"changeLightcone": "Change Lightcone",
|
||||||
|
"removeLightcone": "Remove Lightcone",
|
||||||
|
"equipLightcone": "Equip Lightcone",
|
||||||
|
"noLightconeEquipped": "No Lightcone Equipped",
|
||||||
|
"equipLightconeNote": "Equip a lightcone to enhance your character's abilities",
|
||||||
|
"filter": "Filter",
|
||||||
|
"selectedCharacters": "Selected Characters",
|
||||||
|
"selectedProfiles": "Selected Profiles",
|
||||||
|
"clearAll": "Clear All",
|
||||||
|
"selectAll": "Select All",
|
||||||
|
"copy": "Copy",
|
||||||
|
"copied": "Copied",
|
||||||
|
"noAvatarSelected": "No avatar selected",
|
||||||
|
"noAvatarToCopySelected": "No avatar to copy selected",
|
||||||
|
"pleaseSelectAtLeastOneProfile": "Please select at least one profile",
|
||||||
|
"pleaseEnterUid": "Please enter UID",
|
||||||
|
"failedToFetchEnkaData": "Failed to fetch enka data",
|
||||||
|
"pleaseSelectAtLeastOneCharacter": "Please select at least one character",
|
||||||
|
"noDataToImport": "No data to import",
|
||||||
|
"pleaseSelectAFile": "Please select a file",
|
||||||
|
"fileMustBeAValidJsonFile": "File must be a valid json file",
|
||||||
|
"importEnkaDataSuccess": "Import Enka data success",
|
||||||
|
"importFreeSRDataSuccess": "Import FreeSR data success",
|
||||||
|
"importDatabaseSuccess": "Import database success",
|
||||||
|
"getData": "Get Data",
|
||||||
|
"import": "Import",
|
||||||
|
"freeSRImport": "FreeSR Import",
|
||||||
|
"onlySupportFreeSRJsonFile": "Only support FreeSR json file",
|
||||||
|
"pickAFile": "Pick a file",
|
||||||
|
"lightConeSetting": "LightCone Setting",
|
||||||
|
"relicMaker": "Relic Maker",
|
||||||
|
"pleaseSelectAllOptions": "Please select all options",
|
||||||
|
"relicSavedSuccessfully": "Relic saved successfully",
|
||||||
|
"mainSettings": "Main Settings",
|
||||||
|
"mainStat": "Main Stat",
|
||||||
|
"set": "Set",
|
||||||
|
"pleaseSelectASet": "Please select a set",
|
||||||
|
"effectBonus": "Effect Bonus",
|
||||||
|
"totalRoll": "Total Roll",
|
||||||
|
"randomizeStats": "Randomize Stats",
|
||||||
|
"randomizeRolls": "Randomize Rolls",
|
||||||
|
"selectASubStat": "Select a sub stat",
|
||||||
|
"selectASet": "Select a set",
|
||||||
|
"selectAMainStat": "Select a main stat",
|
||||||
|
"save": "Save",
|
||||||
|
"reset": "Reset",
|
||||||
|
"roll": "Roll",
|
||||||
|
"step": "Step",
|
||||||
|
"memoryOfChaos": "Memory of Chaos",
|
||||||
|
"pureFiction": "Pure Fiction",
|
||||||
|
"apocalypticShadow": "Apocalyptic Shadow",
|
||||||
|
"customEnemy": "Custom Enemy",
|
||||||
|
"simulatedUniverse": "Simulated Universe",
|
||||||
|
"floor": "Floor",
|
||||||
|
"side": "Side",
|
||||||
|
"wave": "Wave",
|
||||||
|
"stage": "Stage",
|
||||||
|
"useCycleCount": "Use cycle count?",
|
||||||
|
"useTurbulenceBuff": "Use turbulence buff?",
|
||||||
|
"firstHalfEnemies": "First half enemies",
|
||||||
|
"secondHalfEnemies": "Second half enemies",
|
||||||
|
"listEnemies": "List enemies",
|
||||||
|
"turbulenceBuff": "Turbulence Buff",
|
||||||
|
"noEventSelected": "No event selected",
|
||||||
|
"noTurbulenceBuff": "No Turbulence Buff",
|
||||||
|
"upper": "Upper",
|
||||||
|
"lower": "Lower",
|
||||||
|
"upperToLower": "Upper -> Lower",
|
||||||
|
"lowerToUpper": "Lower -> Upper",
|
||||||
|
"selectMOCEvent": "Select MOC Event",
|
||||||
|
"selectPFEvent": "Select PF Event",
|
||||||
|
"selectASEvent": "Select AS Event",
|
||||||
|
"selectCEEvent": "Select CE Event",
|
||||||
|
"selectEvent": "Select Event",
|
||||||
|
"selectFloor": "Select a Floor",
|
||||||
|
"selectSide": "Select a Side",
|
||||||
|
"selectBuff": "Select a Buff",
|
||||||
|
"selectStage": "Select a Stage",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next",
|
||||||
|
"noMonstersFound": "No monsters found",
|
||||||
|
"addNewWave": "Add New Wave",
|
||||||
|
"searchStage": "Search stage...",
|
||||||
|
"noStageFound": "No stage found",
|
||||||
|
"searchMonster": "Search monster...",
|
||||||
|
"changeRelic": "Change relic",
|
||||||
|
"deleteRelic": "Delete relic",
|
||||||
|
"deleteRelicConfirm": "Are you sure you want to delete relic in slot",
|
||||||
|
"setEffects": "Set Effects",
|
||||||
|
"details": "Details",
|
||||||
|
"normal": "Basic ATK",
|
||||||
|
"bpskill": "Skill",
|
||||||
|
"maze": "Technique",
|
||||||
|
"ultra": "Ultimate",
|
||||||
|
"servantskill": "Memosprite Skill",
|
||||||
|
"severaltalent": "Memosprite Talent",
|
||||||
|
"singleattack": "Single Attack",
|
||||||
|
"enhance": "Enhance",
|
||||||
|
"summon": "Summon",
|
||||||
|
"mazeattack": "Technique Attack",
|
||||||
|
"blast": "Blast",
|
||||||
|
"restore": "Restore",
|
||||||
|
"support": "Support",
|
||||||
|
"aoeattack": "AoE Attack",
|
||||||
|
"impair": "Impair",
|
||||||
|
"bounce": "Bounce",
|
||||||
|
"active": "Active",
|
||||||
|
"defence": "Defence",
|
||||||
|
"inactive": "Inactive",
|
||||||
|
"maxAll": "Max All",
|
||||||
|
"maxAllSuccess": "Successfully set skill level to max.",
|
||||||
|
"maxAllFailed": "Failed to set skill level to max.",
|
||||||
|
"noRelicEquipped": "No relic equipped",
|
||||||
|
"anomalyArbitration": "Anomaly Arbitration",
|
||||||
|
"normalMode": "Normal Mode",
|
||||||
|
"hardMode": "Hard Mode",
|
||||||
|
"selectPEAKEvent": "Select PEAK Event",
|
||||||
|
"mode": "Mode",
|
||||||
|
"selectMode": "Select a mode",
|
||||||
|
"rollBack": "Roll Back",
|
||||||
|
"upRoll": "Up Roll",
|
||||||
|
"downRoll": "Down Roll",
|
||||||
|
"actions": "Actions",
|
||||||
|
"avatars": "Avatars",
|
||||||
|
"quickView": "Quick View",
|
||||||
|
"extraSetting": "Extra Settings",
|
||||||
|
"disableCensorship": "Disable Censorship",
|
||||||
|
"hideUI": "Hide UI",
|
||||||
|
"theoryCraftMode": "Theorycraft Mode",
|
||||||
|
"cycleCount": "Cycle Count",
|
||||||
|
"pleaseSelectAllSubStats": "Please select all sub stats",
|
||||||
|
"subStatRollCountCannotBeZero": "Sub stat roll count cannot be zero",
|
||||||
|
"theoryCraft": "Theorycraft",
|
||||||
|
"multipathCharacter": "Multipath Character",
|
||||||
|
"mainPath": "Main Path",
|
||||||
|
"march7Path": "March 7 Path",
|
||||||
|
"challenge": "Challenge",
|
||||||
|
"skipNode": "Skip Node",
|
||||||
|
"disableSkip": "Disable skip",
|
||||||
|
"skipNode1": "Skip node 1",
|
||||||
|
"skipNode2": "Skip node 2",
|
||||||
|
"extraFeatures": "Extra Features",
|
||||||
|
"detailTheoryCraft": "Enabling this feature allows you to customize the cycle count and adjust enemy HP in the enemy settings.",
|
||||||
|
"detailSkipNode": "Enabling this feature allows you to skip (Node 1/Node 2) in Memory of Chaos or Pure Fiction.",
|
||||||
|
"detailChallengePeak": "Allows changing the Peak season in the current Anomaly.",
|
||||||
|
"detailHiddenUi": "Enabling this feature will hide the game UI.",
|
||||||
|
"detailDisableCensorship": "Enabling this feature will disable in-game censorship.",
|
||||||
|
"detailMultipathCharacter": "Allows changing the Path of certain characters.",
|
||||||
|
"trailblazer": "Trailblazer",
|
||||||
|
"listExtraEffect": "List Extra Effect",
|
||||||
|
"extra": "Extra",
|
||||||
|
"customLineup": "Custom Lineup"
|
||||||
|
}
|
||||||
|
}
|
||||||
283
messages/ja.json
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
{
|
||||||
|
"TabTitle": {
|
||||||
|
"title": "Firefly Tools",
|
||||||
|
"description": "Firefly tools by Firefly Shelter"
|
||||||
|
},
|
||||||
|
"DataPage": {
|
||||||
|
"skillType": "スキルタイプ",
|
||||||
|
"skillName": "スキル名",
|
||||||
|
"character": "キャラクター",
|
||||||
|
"id": "ID",
|
||||||
|
"path": "運命",
|
||||||
|
"rarity": "レアリティ",
|
||||||
|
"element": "属性",
|
||||||
|
"technique": "秘技",
|
||||||
|
"talent": "天賦",
|
||||||
|
"basic": "通常攻撃",
|
||||||
|
"skill": "スキル",
|
||||||
|
"ultimate": "アルティメット",
|
||||||
|
"servant": "サーバント",
|
||||||
|
"damage": "ダメージ",
|
||||||
|
"type": "タイプ",
|
||||||
|
"warrior": "壊滅",
|
||||||
|
"knight": "存護",
|
||||||
|
"mage": "知恵",
|
||||||
|
"priest": "豊穣",
|
||||||
|
"rogue": "巡狩",
|
||||||
|
"shaman": "調和",
|
||||||
|
"warlock": "虚無",
|
||||||
|
"memory": "記憶",
|
||||||
|
"elation": "愉悦",
|
||||||
|
"fire": "火",
|
||||||
|
"ice": "氷",
|
||||||
|
"imaginary": "虚数",
|
||||||
|
"physical": "物理",
|
||||||
|
"quantum": "量子",
|
||||||
|
"thunder": "雷",
|
||||||
|
"wind": "風",
|
||||||
|
"hp": "HP",
|
||||||
|
"atk": "攻撃",
|
||||||
|
"speed": "速度",
|
||||||
|
"critRate": "会心率",
|
||||||
|
"critDmg": "会心ダメージ",
|
||||||
|
"breakEffect": "撃破特効",
|
||||||
|
"effectRes": "効果抵抗",
|
||||||
|
"energyRegenerationRate": "EP回復効率",
|
||||||
|
"effectHitRate": "効果命中率",
|
||||||
|
"outgoingHealingBoost": "回復増加",
|
||||||
|
"fireDmgBoost": "火属性ダメージ強化",
|
||||||
|
"iceDmgBoost": "氷属性ダメージ強化",
|
||||||
|
"imaginaryDmgBoost": "幻影ダメージ強化",
|
||||||
|
"physicalDmgBoost": "物理ダメージ強化",
|
||||||
|
"quantumDmgBoost": "量子ダメージ強化",
|
||||||
|
"thunderDmgBoost": "雷属性ダメージ強化",
|
||||||
|
"windDmgBoost": "風属性ダメージ強化",
|
||||||
|
"pursued": "追加ダメージ",
|
||||||
|
"true damage": "確定ダメージ",
|
||||||
|
"elationdamage": "愉悦ダメージ",
|
||||||
|
"follow-up": "追撃ダメージ",
|
||||||
|
"elemental damage": "属性ダメージ",
|
||||||
|
"dot": "継続ダメージ",
|
||||||
|
"qte": "QTEスキル",
|
||||||
|
"level": "レベル",
|
||||||
|
"relics": "遺物",
|
||||||
|
"eidolons": "エイドロン",
|
||||||
|
"lightcones": "光円錐",
|
||||||
|
"loadData": "データ読み込み",
|
||||||
|
"exportData": "データ出力",
|
||||||
|
"connectSetting": "接続設定",
|
||||||
|
"connected": "接続済み",
|
||||||
|
"unconnected": "未接続",
|
||||||
|
"psConnection": "PS接続",
|
||||||
|
"connectionType": "接続タイプ",
|
||||||
|
"status": "ステータス",
|
||||||
|
"connectPs": "PS接続",
|
||||||
|
"disconnect": "切断",
|
||||||
|
"other": "その他",
|
||||||
|
"freeSr": "FreeSR",
|
||||||
|
"database": "データベース",
|
||||||
|
"enka": "Enka",
|
||||||
|
"monsterSetting": "モンスター設定",
|
||||||
|
"serverUrl": "サーバー URL",
|
||||||
|
"privateType": "プライベートタイプ",
|
||||||
|
"local": "ローカル",
|
||||||
|
"server": "サーバー",
|
||||||
|
"username": "ユーザー名",
|
||||||
|
"password": "パスワード",
|
||||||
|
"placeholderServerUrl": "サーバー URL を入力",
|
||||||
|
"placeholderUsername": "ユーザー名を入力",
|
||||||
|
"placeholderPassword": "パスワードを入力",
|
||||||
|
"connectedSuccess": "PS接続成功",
|
||||||
|
"connectedFailed": "PS接続失敗",
|
||||||
|
"syncSuccess": "PSへのデータ同期に成功",
|
||||||
|
"syncFailed": "PSへのデータ同期に失敗",
|
||||||
|
"sync": "同期",
|
||||||
|
"importSetting": "インポート設定",
|
||||||
|
"profile": "プロファイル",
|
||||||
|
"default": "デフォルト",
|
||||||
|
"copyProfiles": "プロファイルをコピー",
|
||||||
|
"addNewProfile": "プロファイルを追加",
|
||||||
|
"createNewProfile": "新しいプロファイルを作成",
|
||||||
|
"editProfile": "プロファイル編集",
|
||||||
|
"placeholderProfileName": "プロファイル名を入力",
|
||||||
|
"profileName": "プロファイル名",
|
||||||
|
"create": "作成",
|
||||||
|
"update": "更新",
|
||||||
|
"characterInformation": "キャラクター情報",
|
||||||
|
"skills": "スキル",
|
||||||
|
"showcaseCard": "ショーケースカード",
|
||||||
|
"comingSoon": "近日公開",
|
||||||
|
"characterName": "キャラクター名",
|
||||||
|
"placeholderCharacter": "キャラクター名を入力",
|
||||||
|
"characterSettings": "キャラクター設定",
|
||||||
|
"levelConfiguration": "レベル構成",
|
||||||
|
"characterLevel": "キャラクターレベル",
|
||||||
|
"max": "最大",
|
||||||
|
"ultimateEnergy": "アルティメットエネルギー",
|
||||||
|
"currentEnergy": "現在のエネルギー",
|
||||||
|
"setTo50": "50%に設定",
|
||||||
|
"battleConfiguration": "バトル構成",
|
||||||
|
"useTechnique": "秘技を使用",
|
||||||
|
"techniqueNote": "バトル前に秘技効果を有効化",
|
||||||
|
"enhancement": "強化",
|
||||||
|
"enhancementLevel": "強化レベル",
|
||||||
|
"origin": "由来",
|
||||||
|
"enhancedNote": "強化レベルに応じて追加スキル解放",
|
||||||
|
"lightconeEquipment": "光円錐装備",
|
||||||
|
"lightconeSettings": "光円錐の設定",
|
||||||
|
"placeholderLevel": "レベルを入力",
|
||||||
|
"superimpositionRank": "重ねランク",
|
||||||
|
"ranksNote": "ランクが高いほど効果強化",
|
||||||
|
"changeLightcone": "光円錐を変更",
|
||||||
|
"removeLightcone": "光円錐を外す",
|
||||||
|
"equipLightcone": "光円錐を装備",
|
||||||
|
"noLightconeEquipped": "光円錐が装備されていません",
|
||||||
|
"equipLightconeNote": "強化のため光円錐を装備してください",
|
||||||
|
"filter": "フィルター",
|
||||||
|
"selectedCharacters": "選択中のキャラクター",
|
||||||
|
"selectedProfiles": "選択中のプロファイル",
|
||||||
|
"clearAll": "全てクリア",
|
||||||
|
"selectAll": "全て選択",
|
||||||
|
"copy": "コピー",
|
||||||
|
"copied": "コピー済み",
|
||||||
|
"noAvatarSelected": "キャラクターが選択されていません",
|
||||||
|
"noAvatarToCopySelected": "コピーするキャラクター選択されていません",
|
||||||
|
"pleaseSelectAtLeastOneProfile": "プロファイルを少なくとも1つ選んでください",
|
||||||
|
"pleaseEnterUid": "UIDを入力してください",
|
||||||
|
"failedToFetchEnkaData": "Enka のデータ取得に失敗",
|
||||||
|
"pleaseSelectAtLeastOneCharacter": "キャラクターを最低1つ選択してください",
|
||||||
|
"noDataToImport": "インポートするデータがありません",
|
||||||
|
"pleaseSelectAFile": "ファイルを選択してください",
|
||||||
|
"fileMustBeAValidJsonFile": "有効な JSON ファイルである必要があります",
|
||||||
|
"importEnkaDataSuccess": "Enka データのインポート成功",
|
||||||
|
"importFreeSRDataSuccess": "FreeSR データのインポート成功",
|
||||||
|
"importDatabaseSuccess": "データベースのインポート成功",
|
||||||
|
"getData": "データ取得",
|
||||||
|
"import": "インポート",
|
||||||
|
"freeSRImport": "FreeSR インポート",
|
||||||
|
"onlySupportFreeSRJsonFile": "FreeSR JSON ファイルのみ対応",
|
||||||
|
"pickAFile": "ファイル選択",
|
||||||
|
"lightConeSetting": "ライトコーン設定",
|
||||||
|
"relicMaker": "遺物製作",
|
||||||
|
"pleaseSelectAllOptions": "すべてのオプションを選択してください",
|
||||||
|
"relicSavedSuccessfully": "遺物の保存に成功しました",
|
||||||
|
"mainSettings": "メイン設定",
|
||||||
|
"mainStat": "メインステータス",
|
||||||
|
"set": "セット",
|
||||||
|
"pleaseSelectASet": "セットを選択してください",
|
||||||
|
"effectBonus": "効果ボーナス",
|
||||||
|
"totalRoll": "総ロール数",
|
||||||
|
"randomizeStats": "ステータスランダム化",
|
||||||
|
"randomizeRolls": "ロール回数ランダム化",
|
||||||
|
"selectASubStat": "サブステータスを選択",
|
||||||
|
"selectASet": "セットを選択",
|
||||||
|
"selectAMainStat": "メインステータスを選んでください",
|
||||||
|
"save": "保存",
|
||||||
|
"reset": "リセット",
|
||||||
|
"roll": "ロール",
|
||||||
|
"step": "ステップ",
|
||||||
|
"memoryOfChaos": "混沌の記憶",
|
||||||
|
"pureFiction": "虚構叙事",
|
||||||
|
"apocalypticShadow": "末日の幻影",
|
||||||
|
"customEnemy": "カスタム敵",
|
||||||
|
"simulatedUniverse": "模擬宇宙",
|
||||||
|
"floor": "階層",
|
||||||
|
"side": "前半/後半",
|
||||||
|
"wave": "ウェーブ",
|
||||||
|
"stage": "ステージ",
|
||||||
|
"useCycleCount": "サイクル数を使用しますか?",
|
||||||
|
"useTurbulenceBuff": "乱気流バフを使用しますか?",
|
||||||
|
"firstHalfEnemies": "前半の敵",
|
||||||
|
"secondHalfEnemies": "後半の敵",
|
||||||
|
"turbulenceBuff": "乱気流バフ",
|
||||||
|
"noEventSelected": "イベントが選択されていません",
|
||||||
|
"noTurbulenceBuff": "乱気流バフがありません",
|
||||||
|
"upper": "上半",
|
||||||
|
"lower": "下半",
|
||||||
|
"upperToLower": "上半 -> 下半",
|
||||||
|
"lowerToUpper": "下半 -> 上半",
|
||||||
|
"selectMOCEvent": "MOC イベントを選択",
|
||||||
|
"selectPFEvent": "PF イベントを選択",
|
||||||
|
"selectASEvent": "AS イベントを選択",
|
||||||
|
"selectCEEvent": "CE イベントを選択",
|
||||||
|
"selectEvent": "イベントを選択",
|
||||||
|
"selectFloor": "階層を選択",
|
||||||
|
"selectSide": "前半/後半を選択",
|
||||||
|
"selectBuff": "バフを選択",
|
||||||
|
"selectStage": "ステージを選択",
|
||||||
|
"previous": "前へ",
|
||||||
|
"next": "次へ",
|
||||||
|
"noMonstersFound": "敵が見つかりません",
|
||||||
|
"addNewWave": "新しいウェーブを追加",
|
||||||
|
"searchStage": "ステージを検索...",
|
||||||
|
"noStageFound": "ステージが見つかりません",
|
||||||
|
"searchMonster": "敵を検索...",
|
||||||
|
"changeRelic": "遺物を変更",
|
||||||
|
"deleteRelic": "遺物を削除",
|
||||||
|
"deleteRelicConfirm": "この遺物を削除してもよろしいですか?",
|
||||||
|
"setEffects": "効果を設定",
|
||||||
|
"details": "詳細",
|
||||||
|
"normal": "通常攻撃",
|
||||||
|
"bpskill": "戦闘スキル",
|
||||||
|
"maze": "秘技",
|
||||||
|
"ultra": "必殺技",
|
||||||
|
"servantskill": "メモスプライトスキル",
|
||||||
|
"severaltalent": "メモスプライトの才能",
|
||||||
|
"singleattack": "単体攻撃",
|
||||||
|
"enhance": "強化",
|
||||||
|
"summon": "召喚",
|
||||||
|
"blast": "爆発",
|
||||||
|
"restore": "回復",
|
||||||
|
"support": "支援",
|
||||||
|
"aoeattack": "範囲攻撃",
|
||||||
|
"mazeattack": "迷宮秘技",
|
||||||
|
"impair": "弱体化",
|
||||||
|
"bounce": "弾跳",
|
||||||
|
"active": "アクティブ",
|
||||||
|
"inactive": "非アクティブ",
|
||||||
|
"defence": "防御",
|
||||||
|
"maxAll": "すべて最大化",
|
||||||
|
"maxAllSuccess": "スキルレベルを最大に設定しました。",
|
||||||
|
"maxAllFailed": "スキルレベルの最大設定に失敗しました。",
|
||||||
|
"noRelicEquipped": "遺物が装備されていません",
|
||||||
|
"anomalyArbitration": "異相の仲裁",
|
||||||
|
"normalMode": "通常モード",
|
||||||
|
"hardMode": "困難モード",
|
||||||
|
"selectPEAKEvent": "PEAK イベントを選択",
|
||||||
|
"mode": "モード",
|
||||||
|
"selectMode": "モードを選択",
|
||||||
|
"rollBack": "前の状態に戻る",
|
||||||
|
"upRoll": "サブステータスを増やす",
|
||||||
|
"downRoll": "サブステータスを減らす",
|
||||||
|
"actions": "アクション",
|
||||||
|
"avatars": "アバター",
|
||||||
|
"quickView": "クイックビュー",
|
||||||
|
"extraSetting": "追加設定",
|
||||||
|
"disableCensorship": "検閲を無効化",
|
||||||
|
"hideUI": "UIを非表示",
|
||||||
|
"theoryCraftMode": "シアリークラフトモード",
|
||||||
|
"cycleCount": "サイクル数",
|
||||||
|
"pleaseSelectAllSubStats": "すべてのサブステータスを選択してください",
|
||||||
|
"subStatRollCountCannotBeZero": "サブステータスの行数は0にできません",
|
||||||
|
"theoryCraft": "シアリークラフト",
|
||||||
|
"multipathCharacter": "複数運命キャラ",
|
||||||
|
"mainPath": "主人公の運命",
|
||||||
|
"march7Path": "三月なのかの運命",
|
||||||
|
"challenge": "挑戦",
|
||||||
|
"skipNode": "ノードをスキップ",
|
||||||
|
"disableSkip": "スキップ無効",
|
||||||
|
"skipNode1": "ノード1をスキップ",
|
||||||
|
"skipNode2": "ノード2をスキップ",
|
||||||
|
"extraFeatures": "追加機能",
|
||||||
|
"detailTheoryCraft": "この機能を有効にすると、サイクル数の調整や敵設定でHPの調整が可能になります。",
|
||||||
|
"detailSkipNode": "この機能を有効にすると、混沌の記憶または虚構叙事の(ノード1/ノード2)をスキップできます。",
|
||||||
|
"detailChallengePeak": "現在の異相における「頂」のシーズンを変更できます。",
|
||||||
|
"detailHiddenUi": "この機能を有効にすると、ゲームのUIを非表示にします。",
|
||||||
|
"detailDisableCensorship": "この機能を有効にすると、ゲーム内の検閲を無効にします。",
|
||||||
|
"detailMultipathCharacter": "一部キャラクターの運命を変更できます。",
|
||||||
|
"trailblazer": "開拓者",
|
||||||
|
"listExtraEffect": "追加効果一覧",
|
||||||
|
"extra": "追加",
|
||||||
|
"customLineup": "カスタム編成"
|
||||||
|
}
|
||||||
|
}
|
||||||
283
messages/ko.json
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
{
|
||||||
|
"TabTitle": {
|
||||||
|
"title": "Firefly Tools",
|
||||||
|
"description": "Firefly tools by Firefly Shelter"
|
||||||
|
},
|
||||||
|
"DataPage": {
|
||||||
|
"skillType": "스킬 유형",
|
||||||
|
"skillName": "스킬 이름",
|
||||||
|
"character": "캐릭터",
|
||||||
|
"id": "ID",
|
||||||
|
"path": "운명",
|
||||||
|
"rarity": "희귀도",
|
||||||
|
"element": "속성",
|
||||||
|
"technique": "비기",
|
||||||
|
"talent": "탈렌트",
|
||||||
|
"basic": "기본 공격",
|
||||||
|
"skill": "스킬",
|
||||||
|
"ultimate": "궁극기",
|
||||||
|
"servant": "서번트",
|
||||||
|
"damage": "데미지",
|
||||||
|
"type": "종류",
|
||||||
|
"warrior": "파괴",
|
||||||
|
"knight": "보호",
|
||||||
|
"mage": "박학",
|
||||||
|
"priest": "풍요",
|
||||||
|
"rogue": "사냥",
|
||||||
|
"shaman": "조화",
|
||||||
|
"warlock": "허무",
|
||||||
|
"memory": "기억",
|
||||||
|
"elation": "환락",
|
||||||
|
"fire": "화염",
|
||||||
|
"ice": "얼음",
|
||||||
|
"imaginary": "환상",
|
||||||
|
"physical": "물리",
|
||||||
|
"quantum": "양자",
|
||||||
|
"thunder": "번개",
|
||||||
|
"wind": "바람",
|
||||||
|
"hp": "HP",
|
||||||
|
"atk": "공격력",
|
||||||
|
"speed": "속도",
|
||||||
|
"critRate": "치명타 확률",
|
||||||
|
"critDmg": "치명타 피해",
|
||||||
|
"breakEffect": "파괴 피해",
|
||||||
|
"effectRes": "효과 저항",
|
||||||
|
"energyRegenerationRate": "에너지 회복속도",
|
||||||
|
"effectHitRate": "효과 적중률",
|
||||||
|
"outgoingHealingBoost": "치유 강화",
|
||||||
|
"fireDmgBoost": "화염 피해 증가",
|
||||||
|
"iceDmgBoost": "얼음 피해 증가",
|
||||||
|
"imaginaryDmgBoost": "환상 피해 증가",
|
||||||
|
"physicalDmgBoost": "물리 피해 증가",
|
||||||
|
"quantumDmgBoost": "양자 피해 증가",
|
||||||
|
"thunderDmgBoost": "번개 피해 증가",
|
||||||
|
"windDmgBoost": "바람 피해 증가",
|
||||||
|
"pursued": "추가 피해",
|
||||||
|
"true damage": "진실한 피해",
|
||||||
|
"elationdamage": "환락 피해",
|
||||||
|
"follow-up": "후속 피해",
|
||||||
|
"elemental damage": "파괴 및 초파괴 피해",
|
||||||
|
"dot": "시간 경과 피해",
|
||||||
|
"qte": "QTE 스킬",
|
||||||
|
"level": "레벨",
|
||||||
|
"relics": "유물",
|
||||||
|
"eidolons": "에이돌론",
|
||||||
|
"lightcones": "라이트콘",
|
||||||
|
"loadData": "데이터 불러오기",
|
||||||
|
"exportData": "데이터 내보내기",
|
||||||
|
"connectSetting": "연결 설정",
|
||||||
|
"connected": "연결됨",
|
||||||
|
"unconnected": "연결 안됨",
|
||||||
|
"psConnection": "PS 연결",
|
||||||
|
"connectionType": "연결 타입",
|
||||||
|
"status": "상태",
|
||||||
|
"connectPs": "PS 연결",
|
||||||
|
"disconnect": "연결 끊기",
|
||||||
|
"other": "기타",
|
||||||
|
"freeSr": "FreeSR",
|
||||||
|
"database": "데이터베이스",
|
||||||
|
"enka": "Enka",
|
||||||
|
"monsterSetting": "몬스터 설정",
|
||||||
|
"serverUrl": "서버 URL",
|
||||||
|
"privateType": "프라이빗 타입",
|
||||||
|
"local": "로컬",
|
||||||
|
"server": "서버",
|
||||||
|
"username": "사용자명",
|
||||||
|
"password": "비밀번호",
|
||||||
|
"placeholderServerUrl": "서버 URL 입력",
|
||||||
|
"placeholderUsername": "사용자명 입력",
|
||||||
|
"placeholderPassword": "비밀번호 입력",
|
||||||
|
"connectedSuccess": "PS에 성공적으로 연결됨",
|
||||||
|
"connectedFailed": "PS 연결 실패",
|
||||||
|
"syncSuccess": "PS에 데이터 동기화 성공",
|
||||||
|
"syncFailed": "PS에 데이터 동기화 실패",
|
||||||
|
"sync": "동기화",
|
||||||
|
"importSetting": "설정 가져오기",
|
||||||
|
"profile": "프로필",
|
||||||
|
"default": "기본",
|
||||||
|
"copyProfiles": "프로필 복사",
|
||||||
|
"addNewProfile": "새 프로필 추가",
|
||||||
|
"createNewProfile": "새 프로필 생성",
|
||||||
|
"editProfile": "프로필 편집",
|
||||||
|
"placeholderProfileName": "프로필 이름 입력",
|
||||||
|
"profileName": "프로필 이름",
|
||||||
|
"create": "생성",
|
||||||
|
"update": "업데이트",
|
||||||
|
"characterInformation": "캐릭터 정보",
|
||||||
|
"skills": "스킬",
|
||||||
|
"showcaseCard": "쇼케이스 카드",
|
||||||
|
"comingSoon": "곧 출시",
|
||||||
|
"characterName": "캐릭터 이름",
|
||||||
|
"placeholderCharacter": "캐릭터 이름 입력",
|
||||||
|
"characterSettings": "캐릭터 설정",
|
||||||
|
"levelConfiguration": "레벨 구성",
|
||||||
|
"characterLevel": "캐릭터 레벨",
|
||||||
|
"max": "최대",
|
||||||
|
"ultimateEnergy": "궁극 에너지",
|
||||||
|
"currentEnergy": "현재 에너지",
|
||||||
|
"setTo50": "50%로 설정",
|
||||||
|
"battleConfiguration": "전투 구성",
|
||||||
|
"useTechnique": "비기 사용",
|
||||||
|
"techniqueNote": "전투 전 비기 효과 활성화",
|
||||||
|
"enhancement": "강화",
|
||||||
|
"enhancementLevel": "강화 레벨",
|
||||||
|
"origin": "출처",
|
||||||
|
"enhancedNote": "강화 레벨이 높을수록 추가 효과 해제",
|
||||||
|
"lightconeEquipment": "라이트콘 장비",
|
||||||
|
"lightconeSettings": "라이트콘 설정",
|
||||||
|
"placeholderLevel": "레벨 입력",
|
||||||
|
"superimpositionRank": "중첩 등급",
|
||||||
|
"ranksNote": "등급이 높을수록 효과가 강함",
|
||||||
|
"changeLightcone": "라이트콘 변경",
|
||||||
|
"removeLightcone": "라이트콘 제거",
|
||||||
|
"equipLightcone": "라이트콘 장착",
|
||||||
|
"noLightconeEquipped": "라이트콘 미장착",
|
||||||
|
"equipLightconeNote": "캐릭터 능력 향상을 위해 장착하세요",
|
||||||
|
"filter": "필터",
|
||||||
|
"selectedCharacters": "선택된 캐릭터",
|
||||||
|
"selectedProfiles": "선택된 프로필",
|
||||||
|
"clearAll": "전체 지우기",
|
||||||
|
"selectAll": "전체 선택",
|
||||||
|
"copy": "복사",
|
||||||
|
"copied": "복사됨",
|
||||||
|
"noAvatarSelected": "캐릭터가 선택되지 않음",
|
||||||
|
"noAvatarToCopySelected": "복사할 캐릭터가 선택되지 않음",
|
||||||
|
"pleaseSelectAtLeastOneProfile": "프로필을 최소 하나 선택하세요",
|
||||||
|
"pleaseEnterUid": "UID를 입력해주세요",
|
||||||
|
"failedToFetchEnkaData": "Enka 데이터 가져오기 실패",
|
||||||
|
"pleaseSelectAtLeastOneCharacter": "캐릭터를 최소 하나 선택하세요",
|
||||||
|
"noDataToImport": "가져올 데이터가 없습니다",
|
||||||
|
"pleaseSelectAFile": "파일을 선택하세요",
|
||||||
|
"fileMustBeAValidJsonFile": "유효한 JSON 파일이어야 합니다",
|
||||||
|
"importEnkaDataSuccess": "Enka 데이터 가져오기 성공",
|
||||||
|
"importFreeSRDataSuccess": "FreeSR 데이터 가져오기 성공",
|
||||||
|
"importDatabaseSuccess": "데이터베이스 가져오기 성공",
|
||||||
|
"getData": "데이터 가져오기",
|
||||||
|
"import": "가져오기",
|
||||||
|
"freeSRImport": "FreeSR 가져오기",
|
||||||
|
"onlySupportFreeSRJsonFile": "FreeSR JSON 파일만 지원",
|
||||||
|
"pickAFile": "파일 선택",
|
||||||
|
"lightConeSetting": "라이트콘 설정",
|
||||||
|
"relicMaker": "유물 제작",
|
||||||
|
"pleaseSelectAllOptions": "모든 옵션을 선택해주세요",
|
||||||
|
"relicSavedSuccessfully": "유물 저장 성공",
|
||||||
|
"mainSettings": "메인 설정",
|
||||||
|
"mainStat": "주 속성",
|
||||||
|
"set": "세트",
|
||||||
|
"pleaseSelectASet": "세트 하나를 선택해주세요",
|
||||||
|
"effectBonus": "효과 보너스",
|
||||||
|
"totalRoll": "총 횟수",
|
||||||
|
"randomizeStats": "속성 랜덤화",
|
||||||
|
"randomizeRolls": "횟수 랜덤화",
|
||||||
|
"selectASubStat": "부 속성 선택",
|
||||||
|
"selectASet": "세트 선택",
|
||||||
|
"selectAMainStat": "주 속성 선택",
|
||||||
|
"save": "저장",
|
||||||
|
"reset": "초기화",
|
||||||
|
"roll": "굴리기",
|
||||||
|
"step": "단계",
|
||||||
|
"memoryOfChaos": "망각의 정원",
|
||||||
|
"pureFiction": "허구 이야기",
|
||||||
|
"apocalypticShadow": "종말의 환영",
|
||||||
|
"customEnemy": "커스텀 적",
|
||||||
|
"simulatedUniverse": "시뮬레이티드 유니버스",
|
||||||
|
"floor": "층수",
|
||||||
|
"side": "전반/후반",
|
||||||
|
"wave": "웨이브",
|
||||||
|
"stage": "스테이지",
|
||||||
|
"useCycleCount": "사이클 수 사용?",
|
||||||
|
"useTurbulenceBuff": "난류 버프 사용?",
|
||||||
|
"firstHalfEnemies": "전반 적",
|
||||||
|
"secondHalfEnemies": "후반 적",
|
||||||
|
"turbulenceBuff": "난류 버프",
|
||||||
|
"noEventSelected": "이벤트가 선택되지 않음",
|
||||||
|
"noTurbulenceBuff": "난류 버프가 없음",
|
||||||
|
"upper": "상반",
|
||||||
|
"lower": "하반",
|
||||||
|
"upperToLower": "상반 -> 하반",
|
||||||
|
"lowerToUpper": "하반 -> 상반",
|
||||||
|
"selectMOCEvent": "MOC 이벤트 선택",
|
||||||
|
"selectPFEvent": "PF 이벤트 선택",
|
||||||
|
"selectASEvent": "AS 이벤트 선택",
|
||||||
|
"selectCEEvent": "CE 이벤트 선택",
|
||||||
|
"selectEvent": "이벤트 선택",
|
||||||
|
"selectFloor": "층 선택",
|
||||||
|
"selectSide": "전반/후반 선택",
|
||||||
|
"selectBuff": "버프 선택",
|
||||||
|
"selectStage": "스테이지 선택",
|
||||||
|
"previous": "이전",
|
||||||
|
"next": "다음",
|
||||||
|
"noMonstersFound": "적을 찾을 수 없음",
|
||||||
|
"addNewWave": "새 웨이브 추가",
|
||||||
|
"searchStage": "스테이지 검색...",
|
||||||
|
"noStageFound": "스테이지를 찾을 수 없음",
|
||||||
|
"searchMonster": "적 검색...",
|
||||||
|
"changeRelic": "유물 변경",
|
||||||
|
"deleteRelic": "유물 삭제",
|
||||||
|
"deleteRelicConfirm": "이 슬롯의 유물을 삭제하시겠습니까?",
|
||||||
|
"setEffects": "효과 설정",
|
||||||
|
"details": "상세",
|
||||||
|
"normal": "기본 공격",
|
||||||
|
"bpskill": "스킬",
|
||||||
|
"maze": "전술",
|
||||||
|
"ultra": "필살기",
|
||||||
|
"servantskill": "메모스프라이트 스킬",
|
||||||
|
"severaltalent": "메모스프라이트 재능",
|
||||||
|
"singleattack": "단일 공격",
|
||||||
|
"enhance": "강화",
|
||||||
|
"summon": "소환",
|
||||||
|
"blast": "광역 공격",
|
||||||
|
"restore": "회복",
|
||||||
|
"support": "지원",
|
||||||
|
"aoeattack": "광역 공격",
|
||||||
|
"mazeattack": "미궁 비기",
|
||||||
|
"impair": "약화",
|
||||||
|
"bounce": "튕김",
|
||||||
|
"active": "활성",
|
||||||
|
"inactive": "비활성",
|
||||||
|
"defence": "방어",
|
||||||
|
"maxAll": "모두 최대치",
|
||||||
|
"maxAllSuccess": "스킬 레벨이 최대치로 설정되었습니다.",
|
||||||
|
"maxAllFailed": "스킬 레벨을 최대치로 설정하지 못했습니다.",
|
||||||
|
"noRelicEquipped": "성유물이 장착되지 않음",
|
||||||
|
"anomalyArbitration": "이상 중재",
|
||||||
|
"normalMode": "일반 모드",
|
||||||
|
"hardMode": "어려움 모드",
|
||||||
|
"selectPEAKEvent": "PEAK 이벤트 선택",
|
||||||
|
"mode": "모드",
|
||||||
|
"selectMode": "모드를 선택",
|
||||||
|
"rollBack": "이전 상태로 되돌리기",
|
||||||
|
"upRoll": "부옵션 추가",
|
||||||
|
"downRoll": "부옵션 감소",
|
||||||
|
"actions": "동작",
|
||||||
|
"avatars": "아바타",
|
||||||
|
"quickView": "빠른 조회",
|
||||||
|
"extraSetting": "추가 설정",
|
||||||
|
"disableCensorship": "검열 비활성화",
|
||||||
|
"hideUI": "UI 숨기기",
|
||||||
|
"theoryCraftMode": "Theory Craft 모드",
|
||||||
|
"cycleCount": "사이클 수",
|
||||||
|
"pleaseSelectAllSubStats": "모든 부옵션을 선택하세요",
|
||||||
|
"subStatRollCountCannotBeZero": "부옵션의 줄 수는 0일 수 없습니다",
|
||||||
|
"theoryCraft": "Theory Craft",
|
||||||
|
"multipathCharacter": "다중 운명 캐릭터",
|
||||||
|
"mainPath": "개척자 운명",
|
||||||
|
"march7Path": "삼월칠일 운명",
|
||||||
|
"challenge": "도전",
|
||||||
|
"skipNode": "노드 건너뛰기",
|
||||||
|
"disableSkip": "건너뛰기 비활성화",
|
||||||
|
"skipNode1": "노드 1 건너뛰기",
|
||||||
|
"skipNode2": "노드 2 건너뛰기",
|
||||||
|
"extraFeatures": "추가 기능",
|
||||||
|
"detailTheoryCraft": "이 기능을 활성화하면 사이클 수를 조정하고 적 설정에서 HP를 변경할 수 있습니다.",
|
||||||
|
"detailSkipNode": "이 기능을 활성화하면 혼돈의 기억 또는 허구 서사의 (노드 1/노드 2)를 건너뛸 수 있습니다.",
|
||||||
|
"detailChallengePeak": "현재 이형에서의 피크 시즌을 변경할 수 있습니다.",
|
||||||
|
"detailHiddenUi": "이 기능을 활성화하면 게임 UI가 숨겨집니다.",
|
||||||
|
"detailDisableCensorship": "이 기능을 활성화하면 게임 내 검열이 비활성화됩니다.",
|
||||||
|
"detailMultipathCharacter": "일부 캐릭터의 운명을 변경할 수 있습니다.",
|
||||||
|
"trailblazer": "개척자",
|
||||||
|
"listExtraEffect": "추가 효과 목록",
|
||||||
|
"extra": "추가",
|
||||||
|
"customLineup": "커스텀 편성"
|
||||||
|
}
|
||||||
|
}
|
||||||
283
messages/vi.json
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
{
|
||||||
|
"TabTitle": {
|
||||||
|
"title": "Firefly Tools",
|
||||||
|
"description": "Firefly tools by Firefly Shelter"
|
||||||
|
},
|
||||||
|
"DataPage": {
|
||||||
|
"skillType": "Loại kỹ năng",
|
||||||
|
"skillName": "Tên kỹ năng",
|
||||||
|
"character": "Nhân vật",
|
||||||
|
"id": "ID",
|
||||||
|
"path": "Vận mệnh",
|
||||||
|
"rarity": "Độ hiếm",
|
||||||
|
"element": "Nguyên tố",
|
||||||
|
"technique": "Bí kỹ",
|
||||||
|
"talent": "Thiên phú",
|
||||||
|
"basic": "Đánh thường",
|
||||||
|
"skill": "Kỹ năng",
|
||||||
|
"ultimate": "Tuyệt kỹ",
|
||||||
|
"servant": "Phụ trợ",
|
||||||
|
"damage": "Sát thương",
|
||||||
|
"type": "Loại",
|
||||||
|
"warrior": "Hủy Diệt",
|
||||||
|
"knight": "Bảo Hộ",
|
||||||
|
"mage": "Tri Thức",
|
||||||
|
"priest": "Trù phú",
|
||||||
|
"rogue": "Săn Bắn",
|
||||||
|
"shaman": "Hòa Hợp",
|
||||||
|
"warlock": "Hư Vô",
|
||||||
|
"memory": "Ký Ức",
|
||||||
|
"elation": "Vui vẻ",
|
||||||
|
"fire": "Hỏa",
|
||||||
|
"ice": "Băng",
|
||||||
|
"imaginary": "Ảo Ảnh",
|
||||||
|
"physical": "Vật Lý",
|
||||||
|
"quantum": "Lượng Tử",
|
||||||
|
"thunder": "Lôi",
|
||||||
|
"wind": "Phong",
|
||||||
|
"hp": "HP",
|
||||||
|
"atk": "Tấn công",
|
||||||
|
"speed": "Tốc độ",
|
||||||
|
"critRate": "Tỷ lệ bạo",
|
||||||
|
"critDmg": "ST bạo",
|
||||||
|
"breakEffect": "sát thương kích phá",
|
||||||
|
"effectRes": "Kháng hiệu ứng",
|
||||||
|
"energyRegenerationRate": "Tốc độ hồi năng lượng",
|
||||||
|
"effectHitRate": "Tỷ lệ trúng hiệu ứng",
|
||||||
|
"outgoingHealingBoost": "Tăng hồi phục",
|
||||||
|
"fireDmgBoost": "Tăng sát thương Hỏa",
|
||||||
|
"iceDmgBoost": "Tăng sát thương Băng",
|
||||||
|
"imaginaryDmgBoost": "Tăng sát thương Ảo Ảnh",
|
||||||
|
"physicalDmgBoost": "Tăng sát thương Vật Lý",
|
||||||
|
"quantumDmgBoost": "Tăng sát thương Lượng Tử",
|
||||||
|
"thunderDmgBoost": "Tăng sát thương Lôi",
|
||||||
|
"windDmgBoost": "Tăng sát thương Phong",
|
||||||
|
"pursued": "Sát thương thêm",
|
||||||
|
"true damage": "Sát thương chuẩn",
|
||||||
|
"elationdamage": "Sát thương vui vẻ",
|
||||||
|
"follow-up": "Sát thương phản kích",
|
||||||
|
"elemental damage": "Sát thương kích phá và siêu kích phá",
|
||||||
|
"dot": "Sát thương theo thời gian",
|
||||||
|
"qte": "Kỹ năng QTE",
|
||||||
|
"level": "Cấp độ",
|
||||||
|
"relics": "Thánh di vật",
|
||||||
|
"eidolons": "Tinh hồn",
|
||||||
|
"lightcones": "Nón ánh sáng",
|
||||||
|
"loadData": "Tải dữ liệu",
|
||||||
|
"exportData": "Xuất dữ liệu",
|
||||||
|
"connectSetting": "Cài đặt kết nối",
|
||||||
|
"connected": "Đã kết nối",
|
||||||
|
"unconnected": "Chưa kết nối",
|
||||||
|
"psConnection": "Kết nối PS",
|
||||||
|
"connectionType": "Loại kết nối",
|
||||||
|
"status": "Trạng thái",
|
||||||
|
"connectPs": "Kết nối PS",
|
||||||
|
"disconnect": "Ngắt kết nối",
|
||||||
|
"other": "Khác",
|
||||||
|
"freeSr": "FreeSR",
|
||||||
|
"database": "Database",
|
||||||
|
"enka": "Enka",
|
||||||
|
"monsterSetting": "Cài đặt quái",
|
||||||
|
"serverUrl": "Địa chỉ server",
|
||||||
|
"privateType": "Loại riêng tư",
|
||||||
|
"local": "Cục bộ",
|
||||||
|
"server": "Máy chủ",
|
||||||
|
"username": "Tên người dùng",
|
||||||
|
"password": "Mật khẩu",
|
||||||
|
"placeholderServerUrl": "Nhập địa chỉ server",
|
||||||
|
"placeholderUsername": "Nhập tên người dùng",
|
||||||
|
"placeholderPassword": "Nhập mật khẩu",
|
||||||
|
"connectedSuccess": "Kết nối PS thành công",
|
||||||
|
"connectedFailed": "Kết nối PS thất bại",
|
||||||
|
"syncSuccess": "Đồng bộ dữ liệu với PS thành công",
|
||||||
|
"syncFailed": "Đồng bộ dữ liệu với PS thất bại",
|
||||||
|
"sync": "Đồng bộ",
|
||||||
|
"importSetting": "Cài đặt nhập",
|
||||||
|
"profile": "Hồ sơ",
|
||||||
|
"default": "Mặc định",
|
||||||
|
"copyProfiles": "Sao chép hồ sơ",
|
||||||
|
"addNewProfile": "Thêm hồ sơ mới",
|
||||||
|
"createNewProfile": "Tạo hồ sơ mới",
|
||||||
|
"editProfile": "Chỉnh sửa hồ sơ",
|
||||||
|
"placeholderProfileName": "Nhập tên hồ sơ",
|
||||||
|
"profileName": "Tên hồ sơ",
|
||||||
|
"create": "Tạo",
|
||||||
|
"update": "Cập nhật",
|
||||||
|
"characterInformation": "Thông tin nhân vật",
|
||||||
|
"skills": "Kỹ năng",
|
||||||
|
"showcaseCard": "Thẻ trưng bày",
|
||||||
|
"comingSoon": "Sắp ra mắt",
|
||||||
|
"characterName": "Tên nhân vật",
|
||||||
|
"placeholderCharacter": "Nhập tên nhân vật",
|
||||||
|
"characterSettings": "Cài đặt nhân vật",
|
||||||
|
"levelConfiguration": "Cấu hình cấp độ",
|
||||||
|
"characterLevel": "Cấp độ nhân vật",
|
||||||
|
"max": "Tối đa",
|
||||||
|
"ultimateEnergy": "Năng lượng tuyệt kỹ",
|
||||||
|
"currentEnergy": "Năng lượng hiện tại",
|
||||||
|
"setTo50": "Đặt thành 50%",
|
||||||
|
"battleConfiguration": "Cấu hình trận đấu",
|
||||||
|
"useTechnique": "Dùng bí kỹ",
|
||||||
|
"techniqueNote": "Bật hiệu ứng bí kỹ trước trận",
|
||||||
|
"enhancement": "Cường hóa",
|
||||||
|
"enhancementLevel": "Cấp độ cường hóa",
|
||||||
|
"origin": "Nguyên gốc",
|
||||||
|
"enhancedNote": "Cường hóa cao mở thêm kỹ năng",
|
||||||
|
"lightconeEquipment": "Trang bị nón ánh sáng",
|
||||||
|
"lightconeSettings": "Cài đặt nón ánh sáng",
|
||||||
|
"placeholderLevel": "Nhập cấp độ",
|
||||||
|
"superimpositionRank": "Bậc chồng kỹ năng",
|
||||||
|
"ranksNote": "Bậc càng cao hiệu ứng càng mạnh",
|
||||||
|
"changeLightcone": "Thay đổi nón ánh sáng",
|
||||||
|
"removeLightcone": "Gỡ nón ánh sáng",
|
||||||
|
"equipLightcone": "Trang bị nón ánh sáng",
|
||||||
|
"noLightconeEquipped": "Chưa trang bị nón ánh sáng",
|
||||||
|
"equipLightconeNote": "Trang bị nón để tăng sức mạnh cho nhân vật",
|
||||||
|
"filter": "Lọc",
|
||||||
|
"selectedCharacters": "Nhân vật đã chọn",
|
||||||
|
"selectedProfiles": "Hồ sơ đã chọn",
|
||||||
|
"clearAll": "Xóa tất cả",
|
||||||
|
"selectAll": "Chọn tất cả",
|
||||||
|
"copy": "Sao chép",
|
||||||
|
"copied": "Đã sao chép",
|
||||||
|
"noAvatarSelected": "Chưa chọn nhân vật",
|
||||||
|
"noAvatarToCopySelected": "Chưa chọn nhân vật để sao chép",
|
||||||
|
"pleaseSelectAtLeastOneProfile": "Vui lòng chọn ít nhất một hồ sơ",
|
||||||
|
"pleaseEnterUid": "Vui lòng nhập UID",
|
||||||
|
"failedToFetchEnkaData": "Lấy dữ liệu Enka thất bại",
|
||||||
|
"pleaseSelectAtLeastOneCharacter": "Vui lòng chọn ít nhất một nhân vật",
|
||||||
|
"noDataToImport": "Không có dữ liệu để nhập",
|
||||||
|
"pleaseSelectAFile": "Vui lòng chọn một tệp",
|
||||||
|
"fileMustBeAValidJsonFile": "Tệp phải là tệp JSON hợp lệ",
|
||||||
|
"importEnkaDataSuccess": "Nhập dữ liệu Enka thành công",
|
||||||
|
"importFreeSRDataSuccess": "Nhập dữ liệu FreeSR thành công",
|
||||||
|
"importDatabaseSuccess": "Nhập cơ sở dữ liệu thành công",
|
||||||
|
"getData": "Lấy dữ liệu",
|
||||||
|
"import": "Nhập",
|
||||||
|
"freeSRImport": "Nhập FreeSR",
|
||||||
|
"onlySupportFreeSRJsonFile": "Chỉ hỗ trợ tệp JSON từ FreeSR",
|
||||||
|
"pickAFile": "Chọn tệp",
|
||||||
|
"lightConeSetting": "Cài đặt Nón Ánh Sáng",
|
||||||
|
"relicMaker": "Trình tạo Thánh Di Vật",
|
||||||
|
"pleaseSelectAllOptions": "Vui lòng chọn tất cả tùy chọn",
|
||||||
|
"relicSavedSuccessfully": "Lưu thánh di vật thành công",
|
||||||
|
"mainSettings": "Cài đặt chính",
|
||||||
|
"mainStat": "Chỉ số chính",
|
||||||
|
"set": "Bộ",
|
||||||
|
"pleaseSelectASet": "Vui lòng chọn một bộ",
|
||||||
|
"effectBonus": "Hiệu ứng cộng thêm",
|
||||||
|
"totalRoll": "Tổng số dòng",
|
||||||
|
"randomizeStats": "Ngẫu nhiên chỉ số",
|
||||||
|
"randomizeRolls": "Ngẫu nhiên số dòng",
|
||||||
|
"selectASubStat": "Chọn chỉ số phụ",
|
||||||
|
"selectASet": "Chọn một bộ",
|
||||||
|
"selectAMainStat": "Chọn chỉ số chính",
|
||||||
|
"save": "Lưu",
|
||||||
|
"reset": "Đặt lại toàn bộ",
|
||||||
|
"roll": "Số dòng",
|
||||||
|
"step": "Bước nhảy",
|
||||||
|
"memoryOfChaos": "Hồi ức hỗn độn",
|
||||||
|
"pureFiction": "Kể chuyện hư cấu",
|
||||||
|
"apocalypticShadow": "Ảo ảnh tận thế",
|
||||||
|
"customEnemy": "Kẻ địch tùy chỉnh",
|
||||||
|
"simulatedUniverse": "Vũ trụ mô phỏng",
|
||||||
|
"floor": "Tầng",
|
||||||
|
"side": "Nửa trận",
|
||||||
|
"wave": "Đợt",
|
||||||
|
"stage": "Màn",
|
||||||
|
"useCycleCount": "Dùng dếm chu kỳ?",
|
||||||
|
"useTurbulenceBuff": "Dùng buff hỗn loạn?",
|
||||||
|
"firstHalfEnemies": "Địch nửa đầu",
|
||||||
|
"secondHalfEnemies": "Địch nửa sau",
|
||||||
|
"turbulenceBuff": "Buff hỗn loạn",
|
||||||
|
"noEventSelected": "Không có sự kiện",
|
||||||
|
"noTurbulenceBuff": "Không có buff hỗn loạn",
|
||||||
|
"upper": "Nửa trên",
|
||||||
|
"lower": "Nửa dưới",
|
||||||
|
"upperToLower": "Nửa trên -> Nửa dưới",
|
||||||
|
"lowerToUpper": "Nửa dưới -> Nửa trên",
|
||||||
|
"selectMOCEvent": "Chọn sự kiện MOC",
|
||||||
|
"selectPFEvent": "Chọn sự kiện PF",
|
||||||
|
"selectASEvent": "Chọn sự kiện AS",
|
||||||
|
"selectCEEvent": "Chọn sự kiện CE",
|
||||||
|
"selectPEAKEvent": "Chọn sự kiện PEAK",
|
||||||
|
"selectEvent": "Chọn sự kiện",
|
||||||
|
"selectFloor": "Chọn tầng",
|
||||||
|
"selectSide": "Chọn nửa trận",
|
||||||
|
"selectBuff": "Chọn buff",
|
||||||
|
"selectStage": "Chọn màn",
|
||||||
|
"previous": "Trước",
|
||||||
|
"next": "Tiếp",
|
||||||
|
"noMonstersFound": "Không tìm thấy quái",
|
||||||
|
"addNewWave": "Thêm đợt mới",
|
||||||
|
"searchStage": "Tìm màn...",
|
||||||
|
"noStageFound": "Không tìm thấy màn",
|
||||||
|
"searchMonster": "Tìm quái...",
|
||||||
|
"changeRelic": "Thay đổi di vật",
|
||||||
|
"deleteRelic": "Xóa di vật",
|
||||||
|
"deleteRelicConfirm": "Bạn có chắc chắn muốn xóa di vật trong ô này không?",
|
||||||
|
"setEffects": "Thiết lập hiệu ứng",
|
||||||
|
"details": "Chi tiết",
|
||||||
|
"normal": "Đánh thường",
|
||||||
|
"bpskill": "Chiến kỹ",
|
||||||
|
"maze": "Bí kỹ",
|
||||||
|
"ultra": "Tuyệt kỹ",
|
||||||
|
"servantskill": "Kỹ năng vật triệu hồi",
|
||||||
|
"severaltalent": "Thiên phú vật triệu hồi",
|
||||||
|
"singleattack": "Tấn công đơn",
|
||||||
|
"enhance": "Cường hóa",
|
||||||
|
"summon": "Triệu hồi",
|
||||||
|
"blast": "Tấn công 3 mục tiêu",
|
||||||
|
"restore": "Hồi phục",
|
||||||
|
"support": "Hỗ trợ",
|
||||||
|
"aoeattack": "Tấn công đa mục tiêu",
|
||||||
|
"mazeattack": "Bí kỹ tấn công",
|
||||||
|
"impair": "Suy yếu",
|
||||||
|
"bounce": "Nảy bật",
|
||||||
|
"active": "Kích hoạt",
|
||||||
|
"inactive": "Không kích hoạt",
|
||||||
|
"defence": "Phòng thủ",
|
||||||
|
"maxAll": "Tối đa tất cả",
|
||||||
|
"maxAllSuccess": "Đã thiết lập cấp độ kỹ năng tối đa thành công.",
|
||||||
|
"maxAllFailed": "Thiết lập cấp độ kỹ năng tối đa thất bại.",
|
||||||
|
"noRelicEquipped": "Không có di vật",
|
||||||
|
"anomalyArbitration": "Trọng tài dị tướng",
|
||||||
|
"normalMode": "Chế độ thường",
|
||||||
|
"hardMode": "Chế độ khó",
|
||||||
|
"mode": "Chế độ",
|
||||||
|
"selectMode": "Chọn chế độ",
|
||||||
|
"rollBack": "Quay lại bước trước",
|
||||||
|
"upRoll": "Tăng dòng",
|
||||||
|
"downRoll": "Giảm dòng",
|
||||||
|
"actions": "Hành động",
|
||||||
|
"avatars": "Nhân vật",
|
||||||
|
"quickView": "Xem nhanh",
|
||||||
|
"extraSetting": "Cài đặt bổ sung",
|
||||||
|
"disableCensorship": "Tắt kiểm duyệt",
|
||||||
|
"hideUI": "Ẩn giao diện",
|
||||||
|
"theoryCraftMode": "Chế độ Theory Craft",
|
||||||
|
"cycleCount": "Số vòng",
|
||||||
|
"pleaseSelectAllSubStats": "Vui lòng chọn tất cả chỉ số phụ",
|
||||||
|
"subStatRollCountCannotBeZero": "Số dòng của chỉ số phụ không thể bằng 0",
|
||||||
|
"theoryCraft": "Theory Craft",
|
||||||
|
"multipathCharacter": "Nhân vật đa Vận Mệnh",
|
||||||
|
"mainPath": "Vận Mệnh Nhân Vật Chính",
|
||||||
|
"march7Path": "Vận Mệnh March 7",
|
||||||
|
"challenge": "Thử thách",
|
||||||
|
"skipNode": "Bỏ qua node",
|
||||||
|
"disableSkip": "Tắt bỏ qua",
|
||||||
|
"skipNode1": "Bỏ qua node 1",
|
||||||
|
"skipNode2": "Bỏ qua node 2",
|
||||||
|
"extraFeatures": "Tính năng bổ sung",
|
||||||
|
"detailTheoryCraft": "Khi bật tính năng này sẽ cho phép tùy chỉnh số cycle và trong mục kẻ địch tủy chỉnh sẽ cho phép điều chỉnh số hp.",
|
||||||
|
"detailSkipNode": "Khi bật tính năng này sẽ cho phép bỏ qua (node 1/node 2) của Hồi ức hỗn độn hoặc Kể chuyện hư cấu.",
|
||||||
|
"detailChallengePeak": "Cho phép thay đổi mùa Trọng tại dị tướng hiện tại.",
|
||||||
|
"detailHiddenUi": "Khi bật tính năng này sẽ ẩn giao diện của game.",
|
||||||
|
"detailDisableCensorship": "Khi bật tính năng này sẽ tắt kiểm duyệt của game.",
|
||||||
|
"detailMultipathCharacter": "Cho phép thay đổi Vận Mệnh của một vài nhân vật.",
|
||||||
|
"trailblazer": "Nhà khai phá",
|
||||||
|
"listExtraEffect": "Danh sách hiệu ứng bổ sung",
|
||||||
|
"extra": "Bổ sung",
|
||||||
|
"customLineup": "Đội hình tùy chỉnh"
|
||||||
|
}
|
||||||
|
}
|
||||||
283
messages/zh.json
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
{
|
||||||
|
"TabTitle": {
|
||||||
|
"title": "Firefly Tools",
|
||||||
|
"description": "Firefly tools by Firefly Shelter"
|
||||||
|
},
|
||||||
|
"DataPage": {
|
||||||
|
"skillType": "技能类型",
|
||||||
|
"skillName": "技能名称",
|
||||||
|
"character": "角色",
|
||||||
|
"id": "ID",
|
||||||
|
"path": "命运",
|
||||||
|
"rarity": "稀有度",
|
||||||
|
"element": "元素",
|
||||||
|
"technique": "秘技",
|
||||||
|
"talent": "天赋",
|
||||||
|
"basic": "普通攻击",
|
||||||
|
"skill": "技能",
|
||||||
|
"ultimate": "终结技",
|
||||||
|
"servant": "忆灵",
|
||||||
|
"damage": "伤害",
|
||||||
|
"type": "类型",
|
||||||
|
"warrior": "毁灭",
|
||||||
|
"knight": "守护",
|
||||||
|
"mage": "博学",
|
||||||
|
"priest": "丰饶",
|
||||||
|
"rogue": "狩猎",
|
||||||
|
"shaman": "同谐",
|
||||||
|
"warlock": "虚无",
|
||||||
|
"memory": "记忆",
|
||||||
|
"elation": "欢愉",
|
||||||
|
"fire": "火",
|
||||||
|
"ice": "冰",
|
||||||
|
"imaginary": "虚数",
|
||||||
|
"physical": "物理",
|
||||||
|
"quantum": "量子",
|
||||||
|
"thunder": "雷",
|
||||||
|
"wind": "风",
|
||||||
|
"hp": "生命值",
|
||||||
|
"atk": "攻击",
|
||||||
|
"speed": "速度",
|
||||||
|
"critRate": "暴击率",
|
||||||
|
"critDmg": "暴击伤害",
|
||||||
|
"breakEffect": "击破伤害",
|
||||||
|
"effectRes": "效果抗性",
|
||||||
|
"energyRegenerationRate": "能量恢复速率",
|
||||||
|
"effectHitRate": "效果命中率",
|
||||||
|
"outgoingHealingBoost": "治疗增强",
|
||||||
|
"fireDmgBoost": "火元素伤害增强",
|
||||||
|
"iceDmgBoost": "冰元素伤害增强",
|
||||||
|
"imaginaryDmgBoost": "虚数伤害增强",
|
||||||
|
"physicalDmgBoost": "物理伤害增强",
|
||||||
|
"quantumDmgBoost": "量子伤害增强",
|
||||||
|
"thunderDmgBoost": "雷元素伤害增强",
|
||||||
|
"windDmgBoost": "风元素伤害增强",
|
||||||
|
"pursued": "附加伤害",
|
||||||
|
"true damage": "真实伤害",
|
||||||
|
"elationdamage": "欢愉伤害",
|
||||||
|
"follow-up": "后续伤害",
|
||||||
|
"elemental damage": "击破与超击破伤害",
|
||||||
|
"dot": "持续伤害",
|
||||||
|
"qte": "QTE 技能",
|
||||||
|
"level": "等级",
|
||||||
|
"relics": "遗器",
|
||||||
|
"eidolons": "星魂",
|
||||||
|
"lightcones": "光锥",
|
||||||
|
"loadData": "加载数据",
|
||||||
|
"exportData": "导出数据",
|
||||||
|
"connectSetting": "连接设置",
|
||||||
|
"connected": "已连接",
|
||||||
|
"unconnected": "未连接",
|
||||||
|
"psConnection": "PS 连接",
|
||||||
|
"connectionType": "连接类型",
|
||||||
|
"status": "状态",
|
||||||
|
"connectPs": "连接 PS",
|
||||||
|
"disconnect": "断开连接",
|
||||||
|
"other": "其他",
|
||||||
|
"freeSr": "FreeSR",
|
||||||
|
"database": "数据库",
|
||||||
|
"enka": "Enka",
|
||||||
|
"monsterSetting": "怪物设置",
|
||||||
|
"serverUrl": "服务器 URL",
|
||||||
|
"privateType": "私有类型",
|
||||||
|
"local": "本地",
|
||||||
|
"server": "服务器",
|
||||||
|
"username": "用户名",
|
||||||
|
"password": "密码",
|
||||||
|
"placeholderServerUrl": "输入服务器 URL",
|
||||||
|
"placeholderUsername": "输入用户名",
|
||||||
|
"placeholderPassword": "输入密码",
|
||||||
|
"connectedSuccess": "成功连接 PS",
|
||||||
|
"connectedFailed": "连接 PS 失败",
|
||||||
|
"syncSuccess": "已成功与 PS 同步数据",
|
||||||
|
"syncFailed": "与 PS 同步数据失败",
|
||||||
|
"sync": "同步",
|
||||||
|
"importSetting": "导入设置",
|
||||||
|
"profile": "配置档",
|
||||||
|
"default": "默认",
|
||||||
|
"copyProfiles": "复制配置档",
|
||||||
|
"addNewProfile": "添加新配置档",
|
||||||
|
"createNewProfile": "创建新配置档",
|
||||||
|
"editProfile": "编辑配置档",
|
||||||
|
"placeholderProfileName": "输入配置档名称",
|
||||||
|
"profileName": "配置档名称",
|
||||||
|
"create": "创建",
|
||||||
|
"update": "更新",
|
||||||
|
"characterInformation": "角色信息",
|
||||||
|
"skills": "技能",
|
||||||
|
"showcaseCard": "展示卡",
|
||||||
|
"comingSoon": "敬请期待",
|
||||||
|
"characterName": "角色名称",
|
||||||
|
"placeholderCharacter": "输入角色名称",
|
||||||
|
"characterSettings": "角色设置",
|
||||||
|
"levelConfiguration": "等级配置",
|
||||||
|
"characterLevel": "角色等级",
|
||||||
|
"max": "最大",
|
||||||
|
"ultimateEnergy": "终极能量",
|
||||||
|
"currentEnergy": "当前能量",
|
||||||
|
"setTo50": "设为50%",
|
||||||
|
"battleConfiguration": "战斗配置",
|
||||||
|
"useTechnique": "使用秘术",
|
||||||
|
"techniqueNote": "启用战前秘术效果",
|
||||||
|
"enhancement": "强化",
|
||||||
|
"enhancementLevel": "强化等级",
|
||||||
|
"origin": "来源",
|
||||||
|
"enhancedNote": "强化更高可解锁额外技能",
|
||||||
|
"lightconeEquipment": "光锥装备",
|
||||||
|
"lightconeSettings": "光锥设置",
|
||||||
|
"placeholderLevel": "输入等级",
|
||||||
|
"superimpositionRank": "叠加等级",
|
||||||
|
"ranksNote": "更高等级提供更强效果",
|
||||||
|
"changeLightcone": "更换光锥",
|
||||||
|
"removeLightcone": "移除光锥",
|
||||||
|
"equipLightcone": "装备光锥",
|
||||||
|
"noLightconeEquipped": "未装备光锥",
|
||||||
|
"equipLightconeNote": "装备光锥以增强角色能力",
|
||||||
|
"filter": "筛选",
|
||||||
|
"selectedCharacters": "已选角色",
|
||||||
|
"selectedProfiles": "已选配置档",
|
||||||
|
"clearAll": "清除全部",
|
||||||
|
"selectAll": "全选",
|
||||||
|
"copy": "复制",
|
||||||
|
"copied": "已复制",
|
||||||
|
"noAvatarSelected": "未选择角色",
|
||||||
|
"noAvatarToCopySelected": "未选择要复制的角色",
|
||||||
|
"pleaseSelectAtLeastOneProfile": "请至少选择一个配置档",
|
||||||
|
"pleaseEnterUid": "请输入 UID",
|
||||||
|
"failedToFetchEnkaData": "获取 Enka 数据失败",
|
||||||
|
"pleaseSelectAtLeastOneCharacter": "请至少选择一个角色",
|
||||||
|
"noDataToImport": "无可导入数据",
|
||||||
|
"pleaseSelectAFile": "请选择一个文件",
|
||||||
|
"fileMustBeAValidJsonFile": "文件必须为有效 JSON",
|
||||||
|
"importEnkaDataSuccess": "导入 Enka 数据成功",
|
||||||
|
"importFreeSRDataSuccess": "导入 FreeSR 数据成功",
|
||||||
|
"importDatabaseSuccess": "导入数据库成功",
|
||||||
|
"getData": "获取数据",
|
||||||
|
"import": "导入",
|
||||||
|
"freeSRImport": "导入 FreeSR",
|
||||||
|
"onlySupportFreeSRJsonFile": "仅支持 FreeSR JSON 文件",
|
||||||
|
"pickAFile": "选择文件",
|
||||||
|
"lightConeSetting": "光锥设置",
|
||||||
|
"relicMaker": "遗器制作",
|
||||||
|
"pleaseSelectAllOptions": "请选择所有选项",
|
||||||
|
"relicSavedSuccessfully": "遗器已成功保存",
|
||||||
|
"mainSettings": "主设置",
|
||||||
|
"mainStat": "主属性",
|
||||||
|
"set": "套装",
|
||||||
|
"pleaseSelectASet": "请选择一个套装",
|
||||||
|
"effectBonus": "效果加成",
|
||||||
|
"totalRoll": "总次数",
|
||||||
|
"randomizeStats": "随机属性",
|
||||||
|
"randomizeRolls": "随机次数",
|
||||||
|
"selectASubStat": "选择副属性",
|
||||||
|
"selectASet": "选择套装",
|
||||||
|
"selectAMainStat": "选择主属性",
|
||||||
|
"save": "保存",
|
||||||
|
"reset": "重置",
|
||||||
|
"roll": "滚动",
|
||||||
|
"step": "步数",
|
||||||
|
"memoryOfChaos": "混沌之忆",
|
||||||
|
"pureFiction": "虚构叙事",
|
||||||
|
"apocalypticShadow": "末日幻影",
|
||||||
|
"customEnemy": "自定义敌人",
|
||||||
|
"simulatedUniverse": "模拟宇宙",
|
||||||
|
"floor": "层数",
|
||||||
|
"side": "上/下半场",
|
||||||
|
"wave": "波次",
|
||||||
|
"stage": "关卡",
|
||||||
|
"useCycleCount": "使用轮次数?",
|
||||||
|
"useTurbulenceBuff": "使用紊乱增益?",
|
||||||
|
"firstHalfEnemies": "上半场敌人",
|
||||||
|
"secondHalfEnemies": "下半场敌人",
|
||||||
|
"turbulenceBuff": "紊乱增益",
|
||||||
|
"noEventSelected": "未选择事件",
|
||||||
|
"noTurbulenceBuff": "未选择紊乱增益",
|
||||||
|
"upper": "上半",
|
||||||
|
"lower": "下半",
|
||||||
|
"upperToLower": "上半 -> 下半",
|
||||||
|
"lowerToUpper": "下半 -> 上半",
|
||||||
|
"selectMOCEvent": "选择 MOC 事件",
|
||||||
|
"selectPFEvent": "选择 PF 事件",
|
||||||
|
"selectASEvent": "选择 AS 事件",
|
||||||
|
"selectCEEvent": "选择 CE 事件",
|
||||||
|
"selectEvent": "选择事件",
|
||||||
|
"selectFloor": "选择层数",
|
||||||
|
"selectSide": "选择上/下半场",
|
||||||
|
"selectBuff": "选择 buff",
|
||||||
|
"selectStage": "选择关卡",
|
||||||
|
"previous": "上一页",
|
||||||
|
"next": "下一页",
|
||||||
|
"noMonstersFound": "未找到怪物",
|
||||||
|
"addNewWave": "添加新波次",
|
||||||
|
"searchStage": "搜索关卡...",
|
||||||
|
"noStageFound": "未找到关卡",
|
||||||
|
"searchMonster": "搜索怪物...",
|
||||||
|
"changeRelic": "更换遗物",
|
||||||
|
"deleteRelic": "删除遗物",
|
||||||
|
"deleteRelicConfirm": "确定要删除插槽中的遗物吗",
|
||||||
|
"setEffects": "设置效果",
|
||||||
|
"details": "详情",
|
||||||
|
"normal": "普通攻击",
|
||||||
|
"bpskill": "技能",
|
||||||
|
"maze": "技巧",
|
||||||
|
"ultra": "终结技",
|
||||||
|
"servantskill": "记灵技能",
|
||||||
|
"severaltalent": "记灵天赋",
|
||||||
|
"singleattack": "单体攻击",
|
||||||
|
"enhance": "强化",
|
||||||
|
"summon": "召唤",
|
||||||
|
"blast": "爆裂",
|
||||||
|
"restore": "恢复",
|
||||||
|
"support": "支援",
|
||||||
|
"aoeattack": "范围攻击",
|
||||||
|
"mazeattack": "迷宫秘技",
|
||||||
|
"impair": "削弱",
|
||||||
|
"bounce": "弹跳",
|
||||||
|
"active": "活跃",
|
||||||
|
"defence": "防御",
|
||||||
|
"inactive": "不活跃",
|
||||||
|
"maxAll": "全部最大化",
|
||||||
|
"maxAllSuccess": "技能等级已成功设置为最大。",
|
||||||
|
"maxAllFailed": "设置技能等级为最大失败。",
|
||||||
|
"noRelicEquipped": "未装备圣遗物",
|
||||||
|
"anomalyArbitration": "异相",
|
||||||
|
"normalMode": "普通模式",
|
||||||
|
"hardMode": "困难模式",
|
||||||
|
"selectPEAKEvent": "选择 PEAK 事件",
|
||||||
|
"mode": "模式",
|
||||||
|
"selectMode": "选择模式",
|
||||||
|
"rollBack": "回到之前的状态",
|
||||||
|
"upRoll": "增加副属性",
|
||||||
|
"downRoll": "减少副属性",
|
||||||
|
"actions": "操作",
|
||||||
|
"avatars": "头像",
|
||||||
|
"quickView": "快速预览",
|
||||||
|
"extraSetting": "额外设置",
|
||||||
|
"disableCensorship": "禁用审查",
|
||||||
|
"hideUI": "隐藏界面",
|
||||||
|
"theoryCraftMode": "Theory Craft 模式",
|
||||||
|
"cycleCount": "循环次数",
|
||||||
|
"pleaseSelectAllSubStats": "请选取所有副属性",
|
||||||
|
"subStatRollCountCannotBeZero": "副属性的行数不能为0",
|
||||||
|
"theoryCraft": "Theory Craft",
|
||||||
|
"multipathCharacter": "多命途角色",
|
||||||
|
"mainPath": "主角命途",
|
||||||
|
"march7Path": "三月七命途",
|
||||||
|
"challenge": "挑战",
|
||||||
|
"skipNode": "跳过节点",
|
||||||
|
"disableSkip": "禁用跳过",
|
||||||
|
"skipNode1": "跳过节点1",
|
||||||
|
"skipNode2": "跳过节点2",
|
||||||
|
"extraFeatures": "附加功能",
|
||||||
|
"detailTheoryCraft": "开启后可自定义循环数,并在敌人设置中调整生命值。",
|
||||||
|
"detailSkipNode": "开启后可跳过混沌回忆或虚构叙事的(节点1/节点2)。",
|
||||||
|
"detailChallengePeak": "允许更改当前异相中的「巅峰」赛季。",
|
||||||
|
"detailHiddenUi": "开启后将隐藏游戏界面。",
|
||||||
|
"detailDisableCensorship": "开启后将关闭游戏内的审查。",
|
||||||
|
"detailMultipathCharacter": "允许更改部分角色的命途。",
|
||||||
|
"trailblazer": "开拓者",
|
||||||
|
"listExtraEffect": "额外效果列表",
|
||||||
|
"extra": "额外",
|
||||||
|
"customLineup": "自定义阵容"
|
||||||
|
}
|
||||||
|
}
|
||||||
42
next.config.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import createNextIntlPlugin from "next-intl/plugin";
|
||||||
|
import type { NextConfig } from "next";
|
||||||
|
import bundleAnalyzer from "@next/bundle-analyzer";
|
||||||
|
|
||||||
|
const withBundleAnalyzer = bundleAnalyzer({
|
||||||
|
enabled: process.env.ANALYZE === "true",
|
||||||
|
});
|
||||||
|
|
||||||
|
const withNextIntl = createNextIntlPlugin();
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
reactStrictMode: false,
|
||||||
|
output: "standalone",
|
||||||
|
images: {
|
||||||
|
unoptimized: true,
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: "http",
|
||||||
|
hostname: "localhost",
|
||||||
|
pathname: "**",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "localhost",
|
||||||
|
pathname: "**",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "cdn.punklorde.org",
|
||||||
|
pathname: "**",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
compiler: {
|
||||||
|
styledComponents: true,
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
CDN_URL: "https://cdn.punklorde.org/asbres",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withBundleAnalyzer(withNextIntl(nextConfig));
|
||||||
51
package.json
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "firefly-tools",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@next/bundle-analyzer": "16.1.6",
|
||||||
|
"@tanstack/react-query": "^5.90.20",
|
||||||
|
"axios": "^1.13.4",
|
||||||
|
"fast-average-color": "^9.5.0",
|
||||||
|
"framer-motion": "^12.29.2",
|
||||||
|
"html2canvas-pro": "^1.6.6",
|
||||||
|
"lru-cache": "^11.2.5",
|
||||||
|
"lucide-react": "^0.563.0",
|
||||||
|
"next": "16.1.6",
|
||||||
|
"next-intl": "^4.8.2",
|
||||||
|
"prismjs": "^1.30.0",
|
||||||
|
"react": "19.2.4",
|
||||||
|
"react-dom": "19.2.4",
|
||||||
|
"react-select": "^5.10.2",
|
||||||
|
"react-simple-code-editor": "^0.14.1",
|
||||||
|
"react-toastify": "^11.0.5",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
|
"zod": "^4.3.6",
|
||||||
|
"zustand": "^5.0.11"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/node": "^25.1.0",
|
||||||
|
"@types/react": "19.2.6",
|
||||||
|
"@types/react-dom": "19.2.3",
|
||||||
|
"baseline-browser-mapping": "^2.9.19",
|
||||||
|
"daisyui": "^5.5.14",
|
||||||
|
"eslint": "^9.39.2",
|
||||||
|
"eslint-config-next": "16.1.6",
|
||||||
|
"tailwind-scrollbar": "^4.0.2",
|
||||||
|
"tailwindcss": "^4.1.18",
|
||||||
|
"ts-to-zod": "^5.1.0",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@types/react": "19.2.6",
|
||||||
|
"@types/react-dom": "19.2.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
postcss.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: ["@tailwindcss/postcss"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
BIN
public/ff-srtool.ico
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
public/ff-srtool.png
Normal file
|
After Width: | Height: | Size: 846 KiB |
BIN
public/icon/AbyssIcon01.webp
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/icon/AbyssIcon02.webp
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
public/icon/ChallengeBoss.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/icon/ChallengePeakIcon.webp
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
public/icon/ChallengeStory.webp
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/icon/IconJoy.webp
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/icon/MonsterIcon.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/icon/SimulatedUniverse.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/icon/attack.webp
Normal file
|
After Width: | Height: | Size: 958 B |
BIN
public/icon/break-effect.webp
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/icon/crit-damage.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/icon/crit-rate.webp
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/icon/defence.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/icon/effect-hit-rate.webp
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/icon/effect-res.webp
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/icon/elation.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
public/icon/energy-rate.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/icon/fire-add.webp
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/icon/fire.webp
Normal file
|
After Width: | Height: | Size: 744 B |
BIN
public/icon/healing-boost.webp
Normal file
|
After Width: | Height: | Size: 372 B |
BIN
public/icon/hp.webp
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/icon/ice-add.webp
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/icon/ice.webp
Normal file
|
After Width: | Height: | Size: 918 B |
BIN
public/icon/imaginary-add.webp
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/icon/imaginary.webp
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/icon/knight.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/icon/mage.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/icon/memory.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/icon/physical-add.webp
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
public/icon/physical.webp
Normal file
|
After Width: | Height: | Size: 1020 B |
BIN
public/icon/priest.webp
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
public/icon/quantum-add.webp
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
public/icon/quantum.webp
Normal file
|
After Width: | Height: | Size: 710 B |
BIN
public/icon/rogue.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/icon/shaman.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/icon/speed.webp
Normal file
|
After Width: | Height: | Size: 984 B |
BIN
public/icon/thunder-add.webp
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/icon/thunder.webp
Normal file
|
After Width: | Height: | Size: 872 B |
BIN
public/icon/warlock.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/icon/warrior.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/icon/wind-add.webp
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/icon/wind.webp
Normal file
|
After Width: | Height: | Size: 860 B |
BIN
public/relics/BODY.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
public/relics/FOOT.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
public/relics/HAND.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/relics/HEAD.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/relics/NECK.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/relics/OBJECT.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/skilltree/ELATION.webp
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
public/skilltree/KNIGHT.webp
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
public/skilltree/MAGE.webp
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
public/skilltree/MEMORY.webp
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
public/skilltree/PRIEST.webp
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
public/skilltree/ROGUE.webp
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
public/skilltree/SHAMAN.webp
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
public/skilltree/WARLOCK.webp
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
public/skilltree/WARRIOR.webp
Normal file
|
After Width: | Height: | Size: 51 KiB |
13
script/auto-gen.bat
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
@echo off
|
||||||
|
echo [INFO] Create folder src\zod if not exist...
|
||||||
|
if not exist src\zod (
|
||||||
|
mkdir src\zod
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [INFO] Start convert file .ts from src\types to Zod schemas...
|
||||||
|
for %%f in (src\types\*.ts) do (
|
||||||
|
echo [ZOD] Chuyển %%f -> src\zod\%%~nf.zod.ts
|
||||||
|
npx ts-to-zod src\types\%%~nxf src\zod\%%~nf.zod.ts
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [DONE] ✅ Done
|
||||||
25
src/app/api/data/[name]/route.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { getDataCache } from "@/lib/cache/cache"
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
req: Request,
|
||||||
|
{ params }: { params: Promise<{ name: string }> }
|
||||||
|
) {
|
||||||
|
const { name } = await params
|
||||||
|
|
||||||
|
const item = getDataCache(name)
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return new Response("Not found", { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Cache-Control": "public, max-age=3600"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === "br") {
|
||||||
|
headers["Content-Encoding"] = "br"
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(new Uint8Array(item.buf), { headers })
|
||||||
|
}
|
||||||
102
src/app/api/proxy/route.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import axios from 'axios'
|
||||||
|
import net from 'net'
|
||||||
|
|
||||||
|
function isPrivateHost(hostname: string): boolean {
|
||||||
|
if (
|
||||||
|
hostname === 'localhost' ||
|
||||||
|
hostname.startsWith('127.') ||
|
||||||
|
hostname.startsWith('10.') ||
|
||||||
|
hostname.startsWith('192.168.') ||
|
||||||
|
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname)
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (net.isIP(hostname)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(req.url)
|
||||||
|
const targetUrl = searchParams.get("url")
|
||||||
|
if (!targetUrl) {
|
||||||
|
return NextResponse.json({ error: "Missing url" }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(targetUrl)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return NextResponse.json({ error: "Failed to fetch image" }, { status: 500 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = await response.arrayBuffer()
|
||||||
|
|
||||||
|
return new NextResponse(buffer, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": response.headers.get("content-type") || "image/png",
|
||||||
|
"Cache-Control": "public, max-age=3600",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return NextResponse.json({ error: (e as Error)?.message }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json()
|
||||||
|
const { serverUrl, method, ...payload } = body
|
||||||
|
|
||||||
|
if (!serverUrl) {
|
||||||
|
return NextResponse.json({ error: 'Missing serverUrl' }, { status: 400 })
|
||||||
|
}
|
||||||
|
if (!method) {
|
||||||
|
return NextResponse.json({ error: 'Missing method' }, { status: 400 })
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = serverUrl.trim()
|
||||||
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||||
|
url = `http://${url}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = new URL(url)
|
||||||
|
if (isPrivateHost(parsed.hostname)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: `Connection to private/internal address (${parsed.hostname}) is not allowed` },
|
||||||
|
{ status: 403 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let response
|
||||||
|
|
||||||
|
switch (method.toUpperCase()) {
|
||||||
|
case 'GET':
|
||||||
|
const queryString = new URLSearchParams(payload as Record<string, string>).toString()
|
||||||
|
const fullUrl = queryString ? `${url}?${queryString}` : url
|
||||||
|
response = await axios.get(fullUrl)
|
||||||
|
break
|
||||||
|
case 'POST':
|
||||||
|
response = await axios.post(url, payload)
|
||||||
|
break
|
||||||
|
case 'PUT':
|
||||||
|
response = await axios.put(url, payload)
|
||||||
|
break
|
||||||
|
case 'DELETE':
|
||||||
|
response = await axios.delete(url, { data: payload })
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return NextResponse.json({ error: `Unsupported method: ${method}` }, { status: 405 })
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(response.data)
|
||||||
|
} catch (err: unknown) {
|
||||||
|
return NextResponse.json({ error: (err as Error)?.message || 'Proxy failed' }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/app/eidolons-info/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"use client"
|
||||||
|
import EidolonsInfo from "@/components/eidolonsInfo";
|
||||||
|
export default function EidolonsInfoPage() {
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<EidolonsInfo/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
src/app/globals.css
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
|
||||||
|
@plugin "daisyui" {
|
||||||
|
exclude: properties;
|
||||||
|
themes: winter --default, night --prefersdark, cupcake, coffee;
|
||||||
|
}
|
||||||
|
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@plugin 'tailwind-scrollbar' {
|
||||||
|
nocompatible: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--size-big: 4vw;
|
||||||
|
--size-medium: 3vw;
|
||||||
|
--size-small: 2vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #9ca3af;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
102
src/app/layout.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
import Header from "../components/header";
|
||||||
|
import { ClientThemeWrapper, ThemeProvider } from "@/components/themeController";
|
||||||
|
import Footer from "@/components/footer";
|
||||||
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
|
import { getLocale, getMessages } from "next-intl/server";
|
||||||
|
import { ToastContainer } from 'react-toastify';
|
||||||
|
import QueryProviderWrapper from "@/components/queryProvider";
|
||||||
|
import ClientDataFetcher from "@/components/clientDataFetcher";
|
||||||
|
import AvatarBar from "@/components/avatarBar";
|
||||||
|
import ActionBar from "@/components/actionBar";
|
||||||
|
|
||||||
|
|
||||||
|
const geistSans = Geist({
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Firefly SrTools",
|
||||||
|
description: "SrTools by Firefly Shelter",
|
||||||
|
icons: {
|
||||||
|
icon: "/ff-srtool.png",
|
||||||
|
shortcut: "/ff-srtool.ico",
|
||||||
|
apple: "/ff-srtool.png",
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: "Firefly SrTools",
|
||||||
|
description: "SrTools by Firefly Shelter",
|
||||||
|
url: "https://srtools.punklorde.org",
|
||||||
|
siteName: "Firefly SrTools",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: "https://srtools.punklorde.org/ff-srtool.png",
|
||||||
|
width: 312,
|
||||||
|
height: 312,
|
||||||
|
alt: "Firefly SrTools Logo",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
locale: "en_US",
|
||||||
|
type: "website",
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title: "Firefly SrTools",
|
||||||
|
description: "SrTools by Firefly Shelter",
|
||||||
|
images: ["https://srtools.punklorde.org/ff-srtool.png"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
const messages = await getMessages();
|
||||||
|
const locale = await getLocale()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html lang={locale} suppressHydrationWarning>
|
||||||
|
<body
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
|
suppressHydrationWarning
|
||||||
|
>
|
||||||
|
<NextIntlClientProvider messages={messages}>
|
||||||
|
<QueryProviderWrapper>
|
||||||
|
<ThemeProvider>
|
||||||
|
<ClientThemeWrapper>
|
||||||
|
<ClientDataFetcher >
|
||||||
|
<div className="min-h-screen w-full">
|
||||||
|
<Header />
|
||||||
|
<div className="grid grid-cols-12 w-full">
|
||||||
|
<div className="hidden sm:block md:col-span-4 lg:col-span-3 sticky top-0 self-start h-fit">
|
||||||
|
<AvatarBar />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-12 sm:col-span-8 lg:col-span-9">
|
||||||
|
<ActionBar />
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</ClientDataFetcher>
|
||||||
|
</ClientThemeWrapper>
|
||||||
|
</ThemeProvider>
|
||||||
|
</QueryProviderWrapper>
|
||||||
|
</NextIntlClientProvider>
|
||||||
|
<ToastContainer />
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/app/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
"use client"
|
||||||
|
import AvatarInfo from "@/components/avatarInfo";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<AvatarInfo></AvatarInfo>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/app/relics-info/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import RelicsInfo from "@/components/relicsInfo";
|
||||||
|
|
||||||
|
export default function RelicsInfoPage() {
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<RelicsInfo></RelicsInfo>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/app/showcase-card/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import ShowCaseInfo from "@/components/showcaseCard"
|
||||||
|
|
||||||
|
export default function ShowcaseCard() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ShowCaseInfo />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
11
src/app/skills-info/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import SkillsInfo from "@/components/skillsInfo";
|
||||||
|
|
||||||
|
export default function SkillsInfoPage() {
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<SkillsInfo></SkillsInfo>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
445
src/components/actionBar/index.tsx
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import ParseText from "../parseText";
|
||||||
|
import useLocaleStore from "@/stores/localeStore";
|
||||||
|
import { getNameChar } from "@/helper/getName";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import useModelStore from "@/stores/modelStore";
|
||||||
|
import useUserDataStore from "@/stores/userDataStore";
|
||||||
|
import { ModalConfig, RelicStore } from "@/types";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import useGlobalStore from "@/stores/globalStore";
|
||||||
|
import { connectToPS, syncDataToPS } from "@/helper";
|
||||||
|
import CopyImport from "../importBar/copy";
|
||||||
|
import useCopyProfileStore from "@/stores/copyProfile";
|
||||||
|
import AvatarBar from "../avatarBar";
|
||||||
|
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||||
|
import useDetailDataStore from "@/stores/detailDataStore";
|
||||||
|
|
||||||
|
export default function ActionBar() {
|
||||||
|
const router = useRouter()
|
||||||
|
const { avatarSelected } = useCurrentDataStore()
|
||||||
|
const { damageType, baseType } = useDetailDataStore()
|
||||||
|
const { setResetData } = useCopyProfileStore()
|
||||||
|
const transI18n = useTranslations("DataPage")
|
||||||
|
const { locale } = useLocaleStore()
|
||||||
|
const {
|
||||||
|
isOpenCreateProfile,
|
||||||
|
setIsOpenCreateProfile,
|
||||||
|
isOpenCopy,
|
||||||
|
setIsOpenCopy,
|
||||||
|
isOpenAvatars,
|
||||||
|
setIsOpenAvatars
|
||||||
|
} = useModelStore()
|
||||||
|
const { avatars, setAvatar } = useUserDataStore()
|
||||||
|
const [profileName, setProfileName] = useState("");
|
||||||
|
const [formState, setFormState] = useState("EDIT");
|
||||||
|
const [profileEdit, setProfileEdit] = useState(-1);
|
||||||
|
const { isConnectPS } = useGlobalStore()
|
||||||
|
|
||||||
|
const profileCurrent = useMemo(() => {
|
||||||
|
if (!avatarSelected) return null;
|
||||||
|
const avatar = avatars[avatarSelected.ID];
|
||||||
|
return avatar?.profileList[avatar.profileSelect] || null;
|
||||||
|
}, [avatarSelected, avatars]);
|
||||||
|
|
||||||
|
const listProfile = useMemo(() => {
|
||||||
|
if (!avatarSelected) return [];
|
||||||
|
const avatar = avatars[avatarSelected.ID];
|
||||||
|
return avatar?.profileList || [];
|
||||||
|
}, [avatarSelected, avatars]);
|
||||||
|
|
||||||
|
|
||||||
|
const handleUpdateProfile = () => {
|
||||||
|
if (!profileName.trim()) return;
|
||||||
|
if (formState === "CREATE" && avatarSelected && avatars[avatarSelected.ID]) {
|
||||||
|
const newListProfile = [...listProfile]
|
||||||
|
const newProfile = {
|
||||||
|
profile_name: profileName,
|
||||||
|
lightcone: null,
|
||||||
|
relics: {} as Record<string, RelicStore>
|
||||||
|
}
|
||||||
|
newListProfile.push(newProfile)
|
||||||
|
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: newListProfile.length - 1 })
|
||||||
|
toast.success("Profile created successfully")
|
||||||
|
} else if (formState === "EDIT" && profileCurrent && avatarSelected && profileEdit !== -1) {
|
||||||
|
const newListProfile = [...listProfile]
|
||||||
|
newListProfile[profileEdit].profile_name = profileName;
|
||||||
|
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile })
|
||||||
|
toast.success("Profile updated successfully")
|
||||||
|
}
|
||||||
|
handleCloseModal("update_profile_modal");
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleShow = (modalId: string) => {
|
||||||
|
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||||
|
if (modal) {
|
||||||
|
modal.showModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close modal handler
|
||||||
|
const handleCloseModal = (modalId: string) => {
|
||||||
|
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||||
|
if (modal) {
|
||||||
|
modal.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const actionMove = (path: string) => {
|
||||||
|
router.push(`/${path}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleProfileSelect = (profileId: number) => {
|
||||||
|
if (!avatarSelected) return;
|
||||||
|
if (avatars[avatarSelected.ID].profileSelect === profileId) return;
|
||||||
|
setAvatar({ ...avatars[avatarSelected.ID], profileSelect: profileId })
|
||||||
|
toast.success(`Profile changed to Profile: ${avatars[avatarSelected.ID].profileList[profileId].profile_name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteProfile = (profileId: number, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (!avatarSelected || profileId == 0) return;
|
||||||
|
if (window.confirm(`Are you sure you want to delete profile: ${avatars[avatarSelected.ID].profileList[profileId].profile_name}?`)) {
|
||||||
|
const newListProfile = [...listProfile]
|
||||||
|
newListProfile.splice(profileId, 1)
|
||||||
|
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: profileId - 1 })
|
||||||
|
toast.success(`Profile ${avatars[avatarSelected.ID].profileList[profileId].profile_name} deleted successfully`)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConnectOrSyncPS = async () => {
|
||||||
|
if (isConnectPS) {
|
||||||
|
const res = await syncDataToPS()
|
||||||
|
if (res.success) {
|
||||||
|
toast.success(transI18n("syncSuccess"))
|
||||||
|
} else {
|
||||||
|
toast.error(`${transI18n("syncFailed")}: ${res.message}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const res = await connectToPS()
|
||||||
|
if (res.success) {
|
||||||
|
toast.success(transI18n("connectedSuccess"))
|
||||||
|
} else {
|
||||||
|
toast.error(`${transI18n("connectedFailed")}: ${res.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalConfigs: ModalConfig[] = [
|
||||||
|
{
|
||||||
|
id: "update_profile_modal",
|
||||||
|
title: formState === "CREATE" ? transI18n("createNewProfile") : transI18n("editProfile"),
|
||||||
|
isOpen: isOpenCreateProfile,
|
||||||
|
onClose: () => {
|
||||||
|
setIsOpenCreateProfile(false)
|
||||||
|
handleCloseModal("update_profile_modal")
|
||||||
|
},
|
||||||
|
content: (
|
||||||
|
<div className="px-6 space-y-4">
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text text-primary font-semibold text-lg">
|
||||||
|
{transI18n("profileName")}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={transI18n("placeholderProfileName")}
|
||||||
|
className="input input-warning mt-1 w-full"
|
||||||
|
value={profileName}
|
||||||
|
onChange={(e) => setProfileName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="modal-action">
|
||||||
|
<button className="btn btn-success btn-sm sm:btn-md" onClick={handleUpdateProfile}>
|
||||||
|
{formState === "CREATE" ? transI18n("create") : transI18n("update")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "copy_profile_modal",
|
||||||
|
title: transI18n("copyProfiles").toUpperCase(),
|
||||||
|
isOpen: isOpenCopy,
|
||||||
|
onClose: () => {
|
||||||
|
setIsOpenCopy(false)
|
||||||
|
handleCloseModal("copy_profile_modal")
|
||||||
|
},
|
||||||
|
content: <CopyImport />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "avatars_modal",
|
||||||
|
title: transI18n("avatars").toUpperCase(),
|
||||||
|
isOpen: isOpenAvatars,
|
||||||
|
onClose: () => {
|
||||||
|
setIsOpenAvatars(false)
|
||||||
|
handleCloseModal("avatars_modal")
|
||||||
|
},
|
||||||
|
content: <AvatarBar onClose={() => { setIsOpenAvatars(false); handleCloseModal("avatars_modal") }} />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// Handle ESC key to close modal
|
||||||
|
useEffect(() => {
|
||||||
|
for (const item of modalConfigs) {
|
||||||
|
if (!item?.isOpen) {
|
||||||
|
handleCloseModal(item?.id || "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleEscKey = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
for (const item of modalConfigs) {
|
||||||
|
handleCloseModal(item?.id || "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleEscKey);
|
||||||
|
return () => window.removeEventListener('keydown', handleEscKey);
|
||||||
|
}, [isOpenCopy, isOpenCreateProfile, isOpenAvatars]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full px-4 pb-4 bg-base-200">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 items-center justify-items-center">
|
||||||
|
|
||||||
|
<div className="flex flex-col justify-center w-full">
|
||||||
|
<div className="flex flex-wrap items-center gap-2 ">
|
||||||
|
<div className="flex flex-wrap items-center h-full opacity-80 lg:hover:opacity-100 cursor-pointer text-base md:text-lg lg:text-xl">
|
||||||
|
{avatarSelected && (
|
||||||
|
<div className="flex flex-wrap items-center justify-start h-full w-full">
|
||||||
|
<Image
|
||||||
|
src={`${process.env.CDN_URL}/${damageType?.[avatarSelected?.DamageType]?.Icon}`}
|
||||||
|
alt={'damage type'}
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
className="h-10 w-10 object-contain"
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
/>
|
||||||
|
<p className="text-center font-bold text-lg">
|
||||||
|
{transI18n(avatarSelected.BaseType.toLowerCase())}
|
||||||
|
</p>
|
||||||
|
<div className="text-center font-bold text-lg">{" / "}</div>
|
||||||
|
<ParseText
|
||||||
|
locale={locale}
|
||||||
|
text={getNameChar(locale, transI18n, avatarSelected).toWellFormed()}
|
||||||
|
className={"font-bold text-lg"}
|
||||||
|
/>
|
||||||
|
<div className="text-center italic text-sm ml-2"> {`(${avatarSelected.ID})`}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<span className="text-base opacity-70 font-bold w-16">{transI18n("profile")}:</span>
|
||||||
|
<div className="dropdown dropdown-center md:dropdown-start">
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
className="btn btn-warning border-info btn-soft gap-1"
|
||||||
|
>
|
||||||
|
<span className="truncate max-w-48 font-bold">
|
||||||
|
{profileCurrent?.profile_name}
|
||||||
|
</span>
|
||||||
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box min-w-max w-full mt-1 border border-base-300 max-h-60 overflow-y-auto">
|
||||||
|
{listProfile.map((profile, index) => (
|
||||||
|
<li key={index} className="grid grid-cols-12">
|
||||||
|
<button
|
||||||
|
className={`col-span-8 btn btn-ghost`}
|
||||||
|
onClick={() => handleProfileSelect(index)}
|
||||||
|
>
|
||||||
|
<span className="flex-1 truncate text-left">
|
||||||
|
{profile.profile_name}
|
||||||
|
{index === 0 && (
|
||||||
|
<span className="text-xs text-base-content/60 ml-1">({transI18n("default")})</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
{index !== 0 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="col-span-2 flex items-center justify-center text-lg text-warning hover:bg-warning/20 z-20 w-full h-full"
|
||||||
|
onClick={() => {
|
||||||
|
setFormState("EDIT");
|
||||||
|
setProfileName(profile.profile_name)
|
||||||
|
setProfileEdit(index)
|
||||||
|
setIsOpenCreateProfile(true)
|
||||||
|
handleShow("update_profile_modal")
|
||||||
|
}}
|
||||||
|
title="Edit Profile"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4 " fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536M4 20h4l10.293-10.293a1 1 0 000-1.414l-2.586-2.586a1 1 0 00-1.414 0L4 16v4z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="col-span-2 flex items-center justify-center text-error hover:bg-error/20 z-20 w-full h-full text-lg"
|
||||||
|
onClick={(e) => {
|
||||||
|
handleDeleteProfile(index, e)
|
||||||
|
}}
|
||||||
|
title="Delete Profile"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4 " fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M9 7V4h6v3m5 0H4" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<li className="border-t border-base-300 mt-2 pt-2 z-10">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpenCopy(true)
|
||||||
|
setResetData(baseType, damageType)
|
||||||
|
handleShow("copy_profile_modal")
|
||||||
|
}}
|
||||||
|
className="btn btn-ghost flex justify-start px-3 py-2 h-full w-full hover:bg-base-200 cursor-pointer text-primary z-20"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
{transI18n("copyProfiles")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost flex justify-start px-3 py-2 h-full w-full hover:bg-base-200 cursor-pointer text-primary z-20"
|
||||||
|
onClick={() => {
|
||||||
|
|
||||||
|
setIsOpenCreateProfile(true)
|
||||||
|
setFormState("CREATE");
|
||||||
|
setProfileName("")
|
||||||
|
handleShow("update_profile_modal")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
{transI18n("addNewProfile")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className=" grid grid-cols-2 w-full sm:hidden gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpenAvatars(true)
|
||||||
|
handleShow("avatars_modal")
|
||||||
|
}}
|
||||||
|
className="col-span-1 btn btn-warning btn-sm w-full">
|
||||||
|
{transI18n("avatars")}
|
||||||
|
</button>
|
||||||
|
<div className="col-span-1 dropdown dropdown-center w-full">
|
||||||
|
<label tabIndex={0} className="btn btn-info btn-sm w-full">
|
||||||
|
{transI18n("actions")}
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
tabIndex={0}
|
||||||
|
className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box min-w-max w-full mt-1 border border-base-300 max-h-60 overflow-y-auto"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<button onClick={() => actionMove('')}>
|
||||||
|
{transI18n("characterInformation")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button onClick={() => actionMove('relics-info')}>
|
||||||
|
{transI18n("relics")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button onClick={() => actionMove('eidolons-info')}>
|
||||||
|
{transI18n("eidolons")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button onClick={() => actionMove('skills-info')}>
|
||||||
|
{transI18n("skills")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button onClick={() => actionMove('showcase-card')}>
|
||||||
|
{transI18n("showcaseCard")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm">
|
||||||
|
{isConnectPS ? transI18n("sync") : transI18n("connectPs")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden sm:grid grid-cols-3 gap-2 w-full">
|
||||||
|
<button className="btn btn-success btn-sm" onClick={() => actionMove("")}>
|
||||||
|
{transI18n("characterInformation")}
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-success btn-sm" onClick={() => actionMove("relics-info")}>
|
||||||
|
{transI18n("relics")}
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-success btn-sm" onClick={() => actionMove("eidolons-info")}>
|
||||||
|
{transI18n("eidolons")}
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-success btn-sm" onClick={() => actionMove("skills-info")}>
|
||||||
|
{transI18n("skills")}
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-success btn-sm" onClick={() => actionMove("showcase-card")}>
|
||||||
|
{transI18n("showcaseCard")}
|
||||||
|
</button>
|
||||||
|
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm">
|
||||||
|
{isConnectPS ? transI18n("sync") : transI18n("connectPs")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{modalConfigs.map(({ id, title, onClose, content }) => (
|
||||||
|
<dialog key={id} id={id} className="modal">
|
||||||
|
<div className="modal-box w-11/12 max-w-[90%] max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
|
||||||
|
<div className="sticky top-0 z-10">
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.1, rotate: 90 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
||||||
|
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
))}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
140
src/components/avatarBar/index.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
"use client"
|
||||||
|
import Image from "next/image"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import CharacterCard from "../card/characterCard"
|
||||||
|
import useLocaleStore from "@/stores/localeStore"
|
||||||
|
import { useTranslations } from "next-intl"
|
||||||
|
import useDetailDataStore from "@/stores/detailDataStore"
|
||||||
|
import useCurrentDataStore from '@/stores/currentDataStore';
|
||||||
|
import { calcRarity, getNameChar } from "@/helper"
|
||||||
|
|
||||||
|
export default function AvatarBar({ onClose }: { onClose?: () => void }) {
|
||||||
|
const {
|
||||||
|
avatarSearch,
|
||||||
|
mapAvatarElementActive,
|
||||||
|
mapAvatarPathActive,
|
||||||
|
setAvatarSearch,
|
||||||
|
setAvatarSelected,
|
||||||
|
setMapAvatarElementActive,
|
||||||
|
setMapAvatarPathActive,
|
||||||
|
setSkillIDSelected,
|
||||||
|
} = useCurrentDataStore()
|
||||||
|
const { mapAvatar, baseType, damageType } = useDetailDataStore()
|
||||||
|
const transI18n = useTranslations("DataPage")
|
||||||
|
const {locale} = useLocaleStore()
|
||||||
|
|
||||||
|
const listAvatar = useMemo(() => {
|
||||||
|
if (!mapAvatar || !locale || !transI18n) return []
|
||||||
|
|
||||||
|
let list = Object.values(mapAvatar)
|
||||||
|
|
||||||
|
if (avatarSearch) {
|
||||||
|
list = list.filter(item =>
|
||||||
|
getNameChar(locale, transI18n, item)
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(avatarSearch.toLowerCase())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const allElementFalse = !Object.values(mapAvatarElementActive).some(v => v)
|
||||||
|
const allPathFalse = !Object.values(mapAvatarPathActive).some(v => v)
|
||||||
|
|
||||||
|
list = list.filter(item =>
|
||||||
|
(allElementFalse || mapAvatarElementActive[item.DamageType]) &&
|
||||||
|
(allPathFalse || mapAvatarPathActive[item.BaseType])
|
||||||
|
)
|
||||||
|
|
||||||
|
list.sort((a, b) => {
|
||||||
|
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
|
||||||
|
if (r !== 0) return r
|
||||||
|
return b.ID - a.ID
|
||||||
|
})
|
||||||
|
|
||||||
|
return list
|
||||||
|
}, [mapAvatar, mapAvatarElementActive, mapAvatarPathActive, avatarSearch, locale, transI18n])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-flow-row h-full auto-rows-max w-full">
|
||||||
|
<div className="h-full rounded-lg mx-2 py-2">
|
||||||
|
<div className="container">
|
||||||
|
<div className="flex flex-col h-full gap-2">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<input type="text"
|
||||||
|
placeholder={transI18n("placeholderCharacter")}
|
||||||
|
className="input input-bordered input-primary w-full"
|
||||||
|
value={avatarSearch}
|
||||||
|
onChange={(e) => setAvatarSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-7 sm:grid-cols-4 lg:grid-cols-7 mb-1 mx-1 gap-2 w-full max-h-[17vh] min-h-[5vh] overflow-y-auto">
|
||||||
|
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
onClick={() => {
|
||||||
|
setMapAvatarElementActive({ ...mapAvatarElementActive, [key]: !mapAvatarElementActive[key] })
|
||||||
|
}}
|
||||||
|
className="hover:bg-gray-600 grid items-center justify-items-center cursor-pointer rounded-md shadow-lg"
|
||||||
|
style={{
|
||||||
|
backgroundColor: mapAvatarElementActive[key] ? "#374151" : "#6B7280"
|
||||||
|
}}>
|
||||||
|
<Image
|
||||||
|
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||||
|
alt={key}
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
|
||||||
|
width={200}
|
||||||
|
height={200} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-9 sm:grid-cols-5 lg:grid-cols-9 mb-1 mx-1 gap-2 overflow-y-auto w-full max-h-[17vh] min-h-[5vh]">
|
||||||
|
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
onClick={() => {
|
||||||
|
setMapAvatarPathActive({ ...mapAvatarPathActive, [key]: !mapAvatarPathActive[key] })
|
||||||
|
}}
|
||||||
|
className="hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-lg cursor-pointer"
|
||||||
|
style={{
|
||||||
|
backgroundColor: mapAvatarPathActive[key] ? "#374151" : "#6B7280"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
<Image
|
||||||
|
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
alt={key}
|
||||||
|
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
|
||||||
|
width={200}
|
||||||
|
height={200} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start h-full">
|
||||||
|
<ul className="grid grid-cols-3 sm:grid-cols-2 lg:grid-cols-3 gap-2 w-full h-[65vh] overflow-y-scroll overflow-x-hidden">
|
||||||
|
{listAvatar.map((item, index) => (
|
||||||
|
<div key={index} onClick={() => {
|
||||||
|
setAvatarSelected(item);
|
||||||
|
setSkillIDSelected(null)
|
||||||
|
if (onClose) onClose()
|
||||||
|
}}>
|
||||||
|
<CharacterCard data={item} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
529
src/components/avatarInfo/index.tsx
Normal file
@@ -0,0 +1,529 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import useUserDataStore from "@/stores/userDataStore";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import LightconeBar from '../lightconeBar'
|
||||||
|
import { calcPromotion, calcRarity, replaceByParam } from '@/helper';
|
||||||
|
import { getSkillTree } from '@/helper/getSkillTree';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import ParseText from '../parseText';
|
||||||
|
import useLocaleStore from '@/stores/localeStore';
|
||||||
|
import useModelStore from '@/stores/modelStore';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||||
|
import useDetailDataStore from "@/stores/detailDataStore";
|
||||||
|
import { getLocaleName } from "@/helper/getName";
|
||||||
|
export default function AvatarInfo() {
|
||||||
|
const { avatarSelected, setResetDataLightcone } = useCurrentDataStore()
|
||||||
|
const { avatars, setAvatars, setAvatar } = useUserDataStore()
|
||||||
|
const { isOpenLightcone, setIsOpenLightcone } = useModelStore()
|
||||||
|
const { mapLightCone, baseType } = useDetailDataStore()
|
||||||
|
const transI18n = useTranslations("DataPage")
|
||||||
|
const { locale } = useLocaleStore();
|
||||||
|
|
||||||
|
const lightcone = useMemo(() => {
|
||||||
|
if (!avatarSelected) return null;
|
||||||
|
const avatar = avatars[avatarSelected.ID];
|
||||||
|
return avatar?.profileList[avatar.profileSelect]?.lightcone || null;
|
||||||
|
}, [avatarSelected, avatars]);
|
||||||
|
|
||||||
|
const lightconeDetail = useMemo(() => {
|
||||||
|
if (!mapLightCone || !lightcone?.item_id) return null;
|
||||||
|
return mapLightCone?.[lightcone.item_id.toString()] || null;
|
||||||
|
}, [lightcone, mapLightCone]);
|
||||||
|
|
||||||
|
const handleShow = (modalId: string) => {
|
||||||
|
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||||
|
if (modal) {
|
||||||
|
setIsOpenLightcone(true);
|
||||||
|
modal.showModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close modal handler
|
||||||
|
const handleCloseModal = (modalId: string) => {
|
||||||
|
setIsOpenLightcone(false);
|
||||||
|
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||||
|
if (modal) {
|
||||||
|
modal.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle ESC key to close modal
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpenLightcone) {
|
||||||
|
handleCloseModal("action_detail_modal");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEscKey = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape' && isOpenLightcone) {
|
||||||
|
handleCloseModal("action_detail_modal");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleEscKey);
|
||||||
|
|
||||||
|
return () => window.removeEventListener('keydown', handleEscKey);
|
||||||
|
}, [isOpenLightcone]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-base-100 max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
|
||||||
|
{avatarSelected && avatars[avatarSelected?.ID.toString() || ""] && (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 w-full">
|
||||||
|
<div className="m-2 min-h-96">
|
||||||
|
<div className="container">
|
||||||
|
<div className="card bg-base-200 shadow-xl">
|
||||||
|
<div className="card-body">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="card-title text-2xl font-bold flex items-center gap-2">
|
||||||
|
<div className="w-2 h-8 bg-linear-to-b from-success to-success/50 rounded-full"></div>
|
||||||
|
{transI18n("characterSettings")}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Level Control */}
|
||||||
|
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10">
|
||||||
|
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<div className="w-2 h-6 bg-linear-to-b from-info to-info/50 rounded-full"></div>
|
||||||
|
{transI18n("levelConfiguration")}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-medium">{transI18n("characterLevel")}</span>
|
||||||
|
<span className="label-text-alt text-info font-mono">{avatars[avatarSelected?.ID.toString() || ""]?.level}/80</span>
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="80"
|
||||||
|
value={avatars[avatarSelected?.ID.toString() || ""]?.level}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newLevel = Math.min(80, Math.max(1, parseInt(e.target.value) || 1));
|
||||||
|
|
||||||
|
setAvatars({ ...avatars, [avatarSelected?.ID.toString() || ""]: { ...avatars[avatarSelected?.ID.toString() || ""], level: newLevel, promotion: calcPromotion(newLevel) } });
|
||||||
|
}}
|
||||||
|
className="input input-bordered w-full pr-16 font-mono"
|
||||||
|
placeholder={transI18n("placeholderLevel")}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
setAvatars({ ...avatars, [avatarSelected?.ID.toString() || ""]: { ...avatars[avatarSelected?.ID.toString() || ""], level: 80, promotion: calcPromotion(80) } });
|
||||||
|
}}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-base-content/60 cursor-pointer">
|
||||||
|
<span className="text-sm">{transI18n("max")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="80"
|
||||||
|
value={avatars[avatarSelected?.ID.toString() || ""]?.level}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newLevel = Math.min(80, Math.max(1, parseInt(e.target.value) || 1));
|
||||||
|
setAvatars({ ...avatars, [avatarSelected?.ID.toString() || ""]: { ...avatars[avatarSelected?.ID.toString() || ""], level: newLevel, promotion: calcPromotion(newLevel) } });
|
||||||
|
}}
|
||||||
|
className="range range-info range-sm w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Energy Control */}
|
||||||
|
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10">
|
||||||
|
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<div className="w-2 h-6 bg-linear-to-b from-warning to-warning/50 rounded-full"></div>
|
||||||
|
{transI18n("ultimateEnergy")}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-medium">{transI18n("currentEnergy")}</span>
|
||||||
|
<span className="label-text text-warning font-mono">
|
||||||
|
{Math.round(avatars[avatarSelected?.ID.toString() || ""]?.sp_value)}/{avatars[avatarSelected?.ID.toString() || ""]?.sp_max}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max={avatars[avatarSelected?.ID.toString() || ""]?.sp_max}
|
||||||
|
value={avatars[avatarSelected?.ID.toString() || ""]?.sp_value}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!avatars[avatarSelected?.ID.toString() || ""]?.can_change_sp) return
|
||||||
|
const newSpValue = Math.min(avatars[avatarSelected?.ID.toString() || ""]?.sp_max, Math.max(0, parseInt(e.target.value) || 0));
|
||||||
|
setAvatars({ ...avatars, [avatarSelected?.ID.toString() || ""]: { ...avatars[avatarSelected?.ID.toString() || ""], sp_value: newSpValue } });
|
||||||
|
}}
|
||||||
|
className="range range-warning range-sm w-full"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-sm text-base-content/60 mt-1">
|
||||||
|
<span>0%</span>
|
||||||
|
<span className="font-mono text-warning">{((avatars[avatarSelected?.ID.toString() || ""]?.sp_value / avatars[avatarSelected?.ID.toString() || ""]?.sp_max) * 100).toFixed(1)}%</span>
|
||||||
|
<span>100%</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-outline btn-warning"
|
||||||
|
onClick={() => {
|
||||||
|
if (!avatars[avatarSelected?.ID.toString() || ""]?.can_change_sp) return
|
||||||
|
const newSpValue = Math.ceil(avatars[avatarSelected?.ID.toString() || ""]?.sp_max / 2);
|
||||||
|
const newAvatar = { ...avatars[avatarSelected?.ID.toString() || ""], sp_value: newSpValue }
|
||||||
|
setAvatar(newAvatar)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{transI18n("setTo50")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Technique Toggle */}
|
||||||
|
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10">
|
||||||
|
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<div className="w-2 h-6 bg-linear-to-b from-accent to-accent/50 rounded-full"></div>
|
||||||
|
{transI18n("battleConfiguration")}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="grid grid-cols-1 gap-2 sm:grid-cols-2 label cursor-pointer">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<svg className="w-5 h-5 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
<span className="label-text font-medium">{transI18n("useTechnique")}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={avatars[avatarSelected?.ID.toString() || ""]?.techniques.length > 0 }
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!avatarSelected || avatarSelected?.MazeBuff?.length == 0) return
|
||||||
|
const techniques = e.target.checked ? avatarSelected?.MazeBuff : [];
|
||||||
|
const newAvatar = { ...avatars[avatarSelected?.ID.toString() || ""], techniques: techniques };
|
||||||
|
setAvatar(newAvatar);
|
||||||
|
}}
|
||||||
|
className="toggle toggle-accent"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="text-xs text-base-content/60 mt-1">
|
||||||
|
{transI18n("techniqueNote")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Enhancement Selection */}
|
||||||
|
{avatarSelected?.Enhanced && (
|
||||||
|
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10">
|
||||||
|
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<svg className="w-5 h-5 text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 0 0 4.486-6.336l-3.276 3.277a3.004 3.004 0 0 1-2.25-2.25l3.276-3.276a4.5 4.5 0 0 0-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437 1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008Z" />
|
||||||
|
</svg>
|
||||||
|
{transI18n("enhancement")}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-medium">{transI18n("enhancementLevel")}</span>
|
||||||
|
<span className="label-text-alt text-secondary font-mono">
|
||||||
|
{avatars[avatarSelected?.ID.toString() || ""]?.enhanced || transI18n("origin")}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={avatars[avatarSelected?.ID.toString() || ""]?.enhanced || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newAvatar = avatars[avatarSelected?.ID.toString() || ""]
|
||||||
|
if (newAvatar) {
|
||||||
|
newAvatar.enhanced = e.target.value
|
||||||
|
const skillTree = getSkillTree(avatarSelected, e.target.value)
|
||||||
|
if (skillTree) {
|
||||||
|
newAvatar.data.skills = skillTree
|
||||||
|
}
|
||||||
|
setAvatar(newAvatar)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="select select-bordered select-secondary"
|
||||||
|
>
|
||||||
|
<option value="">{transI18n("origin")}</option>
|
||||||
|
{Object.keys(avatarSelected?.Enhanced || {}).map((key) => (
|
||||||
|
<option key={key} value={key}>
|
||||||
|
{key}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="text-xs text-base-content/60 mt-1">
|
||||||
|
{transI18n("enhancedNote")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="m-2 min-h-96">
|
||||||
|
<div className="container">
|
||||||
|
<div className="card bg-base-200 shadow-xl">
|
||||||
|
<div className="card-body">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h2 className="card-title text-2xl font-bold flex items-center gap-2">
|
||||||
|
<div className="w-2 h-8 bg-linear-to-b from-primary to-secondary rounded-full"></div>
|
||||||
|
{transI18n("lightconeEquipment")}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{lightcone && lightconeDetail ? (
|
||||||
|
<div className="grid lg:grid-cols-3 gap-6">
|
||||||
|
<div className="lg:col-span-3">
|
||||||
|
{/* Level & Rank Controls */}
|
||||||
|
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10">
|
||||||
|
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
<div className="w-2 h-6 bg-linear-to-b from-accent to-accent/50 rounded-full"></div>
|
||||||
|
{transI18n("lightconeSettings")}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
{/* Level Input */}
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-medium">{transI18n("level")}</span>
|
||||||
|
<span className="label-text-alt text-primary font-mono">{lightcone.level}/80</span>
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="80"
|
||||||
|
value={lightcone.level || 1}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newLightconeLevel = Math.min(80, Math.max(1, parseInt(e.target.value) || 1))
|
||||||
|
const newLightcone = { ...lightcone, level: newLightconeLevel, promotion: calcPromotion(newLightconeLevel) }
|
||||||
|
const newAvatar = { ...avatars[avatarSelected.ID] }
|
||||||
|
newAvatar.profileList[newAvatar.profileSelect].lightcone = newLightcone
|
||||||
|
setAvatar(newAvatar)
|
||||||
|
}}
|
||||||
|
className="input input-bordered w-full pr-16 font-mono"
|
||||||
|
placeholder={transI18n("placeholderLevel")}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
const newLightcone = { ...lightcone, level: 80, promotion: calcPromotion(80) }
|
||||||
|
const newAvatar = { ...avatars[avatarSelected.ID] }
|
||||||
|
newAvatar.profileList[newAvatar.profileSelect].lightcone = newLightcone
|
||||||
|
setAvatar(newAvatar)
|
||||||
|
}}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-base-content/60 cursor-pointer">
|
||||||
|
<span className="text-sm">{transI18n("max")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="80"
|
||||||
|
value={lightcone.level || 1}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newLightconeLevel = Math.min(80, Math.max(1, parseInt(e.target.value) || 1))
|
||||||
|
const newLightcone = { ...lightcone, level: newLightconeLevel, promotion: calcPromotion(newLightconeLevel) }
|
||||||
|
const newAvatar = { ...avatars[avatarSelected.ID] }
|
||||||
|
newAvatar.profileList[newAvatar.profileSelect].lightcone = newLightcone
|
||||||
|
setAvatar(newAvatar)
|
||||||
|
}}
|
||||||
|
className="range range-primary range-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rank Selection */}
|
||||||
|
<div className="form-control space-y-3">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<span className="font-medium text-sm sm:text-base">{transI18n("superimpositionRank")}</span>
|
||||||
|
<span className="text-info font-mono text-lg sm:text-xl font-semibold">S{lightcone.rank}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rank Buttons */}
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="grid grid-cols-5 gap-1.5 sm:gap-2 md:grid-cols-3 xl:grid-cols-5 max-w-sm mx-auto sm:mx-0">
|
||||||
|
{[1, 2, 3, 4, 5].map((r) => (
|
||||||
|
<button
|
||||||
|
key={r}
|
||||||
|
onClick={() => {
|
||||||
|
const newLightcone = { ...lightcone, rank: r }
|
||||||
|
const newAvatar = { ...avatars[avatarSelected.ID] }
|
||||||
|
newAvatar.profileList[newAvatar.profileSelect].lightcone = newLightcone
|
||||||
|
setAvatar(newAvatar)
|
||||||
|
}}
|
||||||
|
className={`
|
||||||
|
btn btn-sm sm:btn-md font-mono font-semibold
|
||||||
|
transition-all duration-200 hover:scale-105
|
||||||
|
${lightcone.rank === r
|
||||||
|
? 'btn-primary shadow-lg'
|
||||||
|
: 'btn-outline btn-primary hover:btn-primary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
S{r}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Help Text */}
|
||||||
|
<div className="text-xs sm:text-sm text-base-content/60 text-center sm:text-left mt-2">
|
||||||
|
{transI18n("ranksNote")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex flex-wrap gap-3 mt-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (avatarSelected) {
|
||||||
|
setResetDataLightcone(avatarSelected, baseType)
|
||||||
|
handleShow("action_detail_modal")
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="btn btn-primary btn-lg flex-1 min-w-fit"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||||
|
</svg>
|
||||||
|
{transI18n("changeLightcone")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const newAvatar = { ...avatars[avatarSelected.ID] }
|
||||||
|
newAvatar.profileList[newAvatar.profileSelect].lightcone = null
|
||||||
|
setAvatar(newAvatar)
|
||||||
|
}}
|
||||||
|
className="btn btn-warning btn-lg flex-1 min-w-fit"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
|
</svg>
|
||||||
|
{transI18n("removeLightcone")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lightcone Image */}
|
||||||
|
<div className="lg:col-span-1">
|
||||||
|
<div className="">
|
||||||
|
<Image
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
width={904}
|
||||||
|
height={1260}
|
||||||
|
priority
|
||||||
|
src={`${process.env.CDN_URL}/${lightconeDetail?.Image?.ImagePath}`}
|
||||||
|
className="w-full h-full rounded-lg object-cover shadow-lg"
|
||||||
|
alt="Lightcone"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lightcone Info & Controls */}
|
||||||
|
<div className="lg:col-span-2 space-y-6">
|
||||||
|
{/* Basic Info */}
|
||||||
|
{lightconeDetail && (
|
||||||
|
<div className="bg-base-300 rounded-xl p-6 border border-base-content/10">
|
||||||
|
<div className="flex flex-wrap items-center gap-3 mb-4">
|
||||||
|
<h3 className="text-2xl font-bold text-base-content">
|
||||||
|
<ParseText
|
||||||
|
locale={locale}
|
||||||
|
text={getLocaleName(locale, lightconeDetail.Name)}
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
<div className="badge badge-outline badge-lg">
|
||||||
|
{transI18n(lightconeDetail.BaseType.toLowerCase())}
|
||||||
|
</div>
|
||||||
|
<div className="badge badge-outline badge-lg">
|
||||||
|
{calcRarity(lightconeDetail.Rarity) + "⭐"}
|
||||||
|
</div>
|
||||||
|
<div className="badge badge-outline badge-lg">
|
||||||
|
{"id: " + lightcone.item_id}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="prose prose-sm max-w-none">
|
||||||
|
<div
|
||||||
|
className="text-base-content/80 leading-relaxed"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: replaceByParam(
|
||||||
|
getLocaleName(locale, lightconeDetail.Skills.Desc),
|
||||||
|
lightconeDetail.Skills.Level[lightcone.rank.toString()]?.Param || []
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* No Lightcone Equipped State */
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div
|
||||||
|
onClick={() => handleShow("action_detail_modal")}
|
||||||
|
className="w-24 h-24 mx-auto mb-6 bg-base-300 rounded-full flex items-center justify-center cursor-pointer"
|
||||||
|
>
|
||||||
|
<svg className="w-12 h-12 text-base-content/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold mb-2">{transI18n("noLightconeEquipped")}</h3>
|
||||||
|
<p className="text-base-content/60 mb-6">{transI18n("equipLightconeNote")}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (avatarSelected) {
|
||||||
|
setResetDataLightcone(avatarSelected, baseType)
|
||||||
|
handleShow("action_detail_modal")
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="btn btn-success btn-lg"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
|
</svg>
|
||||||
|
{transI18n("equipLightcone")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<dialog id="action_detail_modal" className="modal backdrop-blur-sm">
|
||||||
|
<div className="modal-box w-11/12 max-w-5xl max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
|
||||||
|
<div className="sticky top-0 z-10">
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.1, rotate: 90 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
|
||||||
|
onClick={() => handleCloseModal("action_detail_modal")}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
<LightconeBar />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
83
src/components/card/characterCard.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
"use client";
|
||||||
|
import { getNameChar } from '@/helper';
|
||||||
|
import useLocaleStore from '@/stores/localeStore';
|
||||||
|
import { AvatarDetail } from '@/types';
|
||||||
|
import ParseText from '../parseText';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import useDetailDataStore from '@/stores/detailDataStore';
|
||||||
|
|
||||||
|
interface CharacterCardProps {
|
||||||
|
data: AvatarDetail
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CharacterCard({ data }: CharacterCardProps) {
|
||||||
|
const { locale } = useLocaleStore();
|
||||||
|
const transI18n = useTranslations("DataPage");
|
||||||
|
const { baseType, damageType } = useDetailDataStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
className="z-10 flex flex-col items-center rounded-xl shadow-xl
|
||||||
|
bg-linear-to-br from-base-300 via-base-100 to-warning/70
|
||||||
|
transform transition-transform duration-300 ease-in-out
|
||||||
|
hover:scale-105 cursor-pointer min-h-45 sm:min-h-45 md:min-h-52.5 lg:min-h-55 xl:min-h-60 2xl:min-h-65"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`w-full rounded-md bg-linear-to-br ${data.Rarity === "CombatPowerAvatarRarityType5"
|
||||||
|
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
|
||||||
|
: "from-purple-400 via-purple-600/70 to-purple-800/50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
|
||||||
|
<div className="relative w-full h-32 lg:h-26 xl:h-36">
|
||||||
|
<Image
|
||||||
|
width={376}
|
||||||
|
height={512}
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src={`${process.env.CDN_URL}/${data.Image.AvatarIconPath}`}
|
||||||
|
priority={true}
|
||||||
|
className="rounded-md w-full h-full object-contain"
|
||||||
|
alt="ALT"
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src={`${process.env.CDN_URL}/${damageType?.[data.DamageType].Icon}`}
|
||||||
|
className="absolute top-0 left-0 w-6 h-6 rounded-full"
|
||||||
|
alt={data.DamageType.toLowerCase()}
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src={`${process.env.CDN_URL}/${baseType?.[data.BaseType].Icon}`}
|
||||||
|
className="absolute top-0 right-0 w-6 h-6 rounded-full"
|
||||||
|
alt={data.BaseType.toLowerCase()}
|
||||||
|
style={{
|
||||||
|
boxShadow: "inset 0 0 8px 4px #9CA3AF"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ParseText
|
||||||
|
locale={locale}
|
||||||
|
text={getNameChar(locale, transI18n, data)}
|
||||||
|
className="
|
||||||
|
w-full px-0.5
|
||||||
|
my-1
|
||||||
|
text-center font-bold text-shadow-white
|
||||||
|
leading-tight
|
||||||
|
wrap-break-word
|
||||||
|
text-sm sm:text-base 2xl:text-lg
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
125
src/components/card/characterInfoCard.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { CharacterInfoCardType } from '@/types';
|
||||||
|
import useLocaleStore from '@/stores/localeStore';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import ParseText from '../parseText';
|
||||||
|
import useDetailDataStore from '@/stores/detailDataStore';
|
||||||
|
import { getLocaleName } from '@/helper/getName';
|
||||||
|
|
||||||
|
export default function CharacterInfoCard({ character, selectedCharacters, onCharacterToggle }: { character: CharacterInfoCardType, selectedCharacters: CharacterInfoCardType[], onCharacterToggle: (characterId: CharacterInfoCardType) => void }) {
|
||||||
|
const isSelected = selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id);
|
||||||
|
const { mapAvatar, mapLightCone, baseType, damageType } = useDetailDataStore();
|
||||||
|
const { locale } = useLocaleStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`bg-base-200/60 rounded-xl p-4 border cursor-pointer transition-all duration-200 ${isSelected
|
||||||
|
? 'border-blue-400 ring-2 ring-blue-400/50'
|
||||||
|
: 'border-base-300/50 hover:border-base-300 opacity-75'
|
||||||
|
}`}
|
||||||
|
|
||||||
|
onClick={() => onCharacterToggle(character)}
|
||||||
|
>
|
||||||
|
|
||||||
|
{/* Character Portrait */}
|
||||||
|
<div className="relative mb-4">
|
||||||
|
<div className="w-full h-48 rounded-lg overflow-hidden relative">
|
||||||
|
<Image
|
||||||
|
src={`${process.env.CDN_URL}/${mapAvatar?.[character.avatar_id.toString()]?.Image?.AvatarIconPath}`}
|
||||||
|
alt={getLocaleName(locale, mapAvatar?.[character.avatar_id.toString()]?.Name)}
|
||||||
|
width={376}
|
||||||
|
height={512}
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src={`${process.env.CDN_URL}/${damageType?.[mapAvatar?.[character.avatar_id.toString()]?.DamageType || ""].Icon}`}
|
||||||
|
className="absolute top-0 left-0 w-10 h-10 rounded-full"
|
||||||
|
alt={mapAvatar[character.avatar_id.toString()]?.DamageType.toLowerCase()}
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src={`${process.env.CDN_URL}/${baseType?.[mapAvatar?.[character.avatar_id.toString()]?.BaseType || ""].Icon}`}
|
||||||
|
className="absolute top-0 right-0 w-10 h-10 rounded-full"
|
||||||
|
alt={mapAvatar[character.avatar_id.toString()]?.BaseType.toLowerCase()}
|
||||||
|
style={{
|
||||||
|
boxShadow: "inset 0 0 8px 4px #9CA3AF"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Character Name and Level */}
|
||||||
|
<div className="w-full rounded-lg flex items-center justify-center mb-2">
|
||||||
|
<div className="text-center">
|
||||||
|
<ParseText className="text-lg font-bold"
|
||||||
|
text={getLocaleName(locale, mapAvatar[character.avatar_id.toString()]?.Name)}
|
||||||
|
locale={locale}
|
||||||
|
/>
|
||||||
|
<div className="text-base mb-1">Lv.{character.level} E{character.rank}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{character.relics.length > 0 && (
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
|
||||||
|
{character.relics.map((relic, index) => (
|
||||||
|
<div key={index} className="relative">
|
||||||
|
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50">
|
||||||
|
<Image
|
||||||
|
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
|
||||||
|
alt="Relic"
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
width={124}
|
||||||
|
height={124}
|
||||||
|
className="w-14 h-14 object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white text-xs px-1 rounded">
|
||||||
|
+{relic.level}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Light Cone */}
|
||||||
|
{character.lightcone.item_id && (
|
||||||
|
<div className="">
|
||||||
|
<div className="rounded-lg h-42 flex items-center justify-center">
|
||||||
|
<Image
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src={`${process.env.CDN_URL}/${mapLightCone?.[character.lightcone.item_id.toString()]?.Image?.ImagePath}`}
|
||||||
|
alt={getLocaleName(locale, mapLightCone?.[character.lightcone.item_id.toString()]?.Name)}
|
||||||
|
width={348}
|
||||||
|
height={408}
|
||||||
|
className="w-full h-full object-contain rounded-lg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-full rounded-lg flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-lg font-bold">
|
||||||
|
<ParseText
|
||||||
|
text={getLocaleName(locale, mapLightCone[character.lightcone.item_id.toString()]?.Name)}
|
||||||
|
locale={locale}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-base mb-1">Lv.{character.lightcone.level} S{character.lightcone.rank}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||