init
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
+25
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
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
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
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
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
Binary file not shown.
+34
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"
]
}
]
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+15
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
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
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
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
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
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
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
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
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
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
View File
@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;
Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

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

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

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
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
+25
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
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 })
}
}
+9
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
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
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
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>
);
}
+11
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>
);
}
+11
View File
@@ -0,0 +1,11 @@
"use client"
import ShowCaseInfo from "@/components/showcaseCard"
export default function ShowcaseCard() {
return (
<div>
<ShowCaseInfo />
</div>
)
}
+11
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>
);
}
+445
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>
);
}
+140
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>
)
}
+529
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>
);
}
+83
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>
);
}
+125
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