init
Some checks failed
Gitea Auto Deploy / Deploy-Container (push) Failing after 52s

This commit is contained in:
2026-04-13 18:05:27 +07:00
commit c77f4a2cb9
207 changed files with 18035 additions and 0 deletions

View 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
View 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
View 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
View File

@@ -0,0 +1,62 @@
# Firefly SrTools
![Next.js](https://img.shields.io/badge/Next.js-black?style=for-the-badge&logo=next.js&logoColor=white)
![Bun](https://img.shields.io/badge/Bun-%23000000.svg?style=for-the-badge&logo=bun&logoColor=white)
![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)
![License](https://img.shields.io/badge/License-MIT-blue?style=for-the-badge)
**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
```

1408
bun.lock Normal file

File diff suppressed because it is too large Load Diff

BIN
data/as.json.br Normal file

Binary file not shown.

BIN
data/avatar.json.br Normal file

Binary file not shown.

34
data/changelog.json Normal file
View 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

Binary file not shown.

BIN
data/metadata.json.br Normal file

Binary file not shown.

BIN
data/moc.json.br Normal file

Binary file not shown.

BIN
data/monster.json.br Normal file

Binary file not shown.

BIN
data/peak.json.br Normal file

Binary file not shown.

BIN
data/pf.json.br Normal file

Binary file not shown.

BIN
data/relic.json.br Normal file

Binary file not shown.

15
docker-compose.yml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

BIN
public/ff-srtool.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

BIN
public/ff-srtool.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
public/icon/IconJoy.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
public/icon/attack.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
public/icon/crit-rate.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/icon/defence.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
public/icon/effect-res.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/icon/elation.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
public/icon/fire-add.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/icon/fire.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

BIN
public/icon/hp.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/icon/ice-add.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
public/icon/ice.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/icon/imaginary.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
public/icon/knight.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/icon/mage.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/icon/memory.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
public/icon/physical.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 B

BIN
public/icon/priest.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
public/icon/quantum.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 B

BIN
public/icon/rogue.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
public/icon/shaman.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/icon/speed.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
public/icon/thunder.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

BIN
public/icon/warlock.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
public/icon/warrior.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/icon/wind-add.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
public/icon/wind.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

BIN
public/relics/BODY.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
public/relics/FOOT.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
public/relics/HAND.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/relics/HEAD.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
public/relics/NECK.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
public/relics/OBJECT.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
public/skilltree/MAGE.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

BIN
public/skilltree/ROGUE.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

13
script/auto-gen.bat Normal file
View 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

View 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
View 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 })
}
}

View 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
View 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
View 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
View File

@@ -0,0 +1,10 @@
"use client"
import AvatarInfo from "@/components/avatarInfo";
export default function Home() {
return (
<div className="w-full">
<AvatarInfo></AvatarInfo>
</div>
);
}

View File

@@ -0,0 +1,11 @@
"use client";
import RelicsInfo from "@/components/relicsInfo";
export default function RelicsInfoPage() {
return (
<div className="w-full">
<RelicsInfo></RelicsInfo>
</div>
);
}

View File

@@ -0,0 +1,11 @@
"use client"
import ShowCaseInfo from "@/components/showcaseCard"
export default function ShowcaseCard() {
return (
<div>
<ShowCaseInfo />
</div>
)
}

View File

@@ -0,0 +1,11 @@
"use client";
import SkillsInfo from "@/components/skillsInfo";
export default function SkillsInfoPage() {
return (
<div className="w-full">
<SkillsInfo></SkillsInfo>
</div>
);
}

View 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>
);
}

View 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>
)
}

View 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>
);
}

View 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>
);
}

View 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>
);
};

Some files were not shown because too many files have changed in this diff Show More