Refactor TypeScript files for improved readability and consistency
Gitea Auto Deploy / Deploy-Container (push) Successful in 3m21s

- Cleaned up imports and formatting in localeStore.ts and socketSettingStore.ts.
- Standardized interface definitions and formatting in attack.ts, avatar.ts, battle.ts, enemy.ts, entity.ts, error.ts, index.ts, lineup.ts, mics.ts, skill.ts, srtools.ts, team.ts, turn.ts, version.ts, and waveAndCycle.ts.
- Ensured consistent use of semicolons and spacing across all modified files.
- Updated tsconfig.json for consistency in formatting.
This commit is contained in:
2026-08-27 18:23:17 +07:00
parent b5976a4d82
commit cf9553d343
70 changed files with 4376 additions and 4376 deletions
+14 -14
View File
@@ -1,15 +1,15 @@
name: Gitea Auto Deploy name: Gitea Auto Deploy
run-name: ${{ gitea.actor }} pushed code 🚀 run-name: ${{ gitea.actor }} pushed code 🚀
on: [push] on: [push]
jobs: jobs:
Deploy-Container: Deploy-Container:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Deploy to Container - name: Deploy to Container
run: | run: |
docker compose up -d --build --remove-orphans docker compose up -d --build --remove-orphans
+56 -56
View File
@@ -1,56 +1,56 @@
FROM oven/bun:canary-alpine AS base FROM oven/bun:canary-alpine AS base
# Install dependencies only when needed # Install dependencies only when needed
FROM base AS deps FROM base AS deps
WORKDIR /app WORKDIR /app
# Install dependencies # Install dependencies
COPY package.json bun.lock ./ COPY package.json bun.lock ./
RUN bun install --frozen-lockfile RUN bun install --frozen-lockfile
# Rebuild the source code only when needed # Rebuild the source code only when needed
FROM base AS builder FROM base AS builder
WORKDIR /app WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
# Next.js collects completely anonymous telemetry data about general usage. # Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry # Learn more here: https://nextjs.org/telemetry
# Disable telemetry during the build # Disable telemetry during the build
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
RUN bun run build RUN bun run build
# Production image, copy all the files and run next # Production image, copy all the files and run next
FROM base AS runner FROM base AS runner
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
# Disable telemetry # Disable telemetry
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
RUN adduser --system --uid 1001 nextjs RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public COPY --from=builder /app/public ./public
COPY --from=builder /app/messages ./messages COPY --from=builder /app/messages ./messages
# Set the correct permission for prerender cache # Set the correct permission for prerender cache
RUN mkdir .next RUN mkdir .next
RUN chown nextjs:bun .next RUN chown nextjs:bun .next
# Automatically leverage output traces to reduce image size # Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing # 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/standalone ./
COPY --from=builder --chown=nextjs:bun /app/.next/static ./.next/static COPY --from=builder --chown=nextjs:bun /app/.next/static ./.next/static
USER nextjs USER nextjs
EXPOSE 3000 EXPOSE 3000
ENV PORT=3000 ENV PORT=3000
# Set hostname to localhost # Set hostname to localhost
ENV HOSTNAME=0.0.0.0 ENV HOSTNAME=0.0.0.0
CMD ["bun", "server.js"] CMD ["bun", "server.js"]
Binary file not shown.
Binary file not shown.
+15 -15
View File
@@ -1,15 +1,15 @@
services: services:
sranalysis: sranalysis:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: sranalysis container_name: sranalysis
restart: unless-stopped restart: unless-stopped
ports: ports:
- "3005:3000" - "3005:3000"
networks: networks:
- sranalysis-network - sranalysis-network
networks: networks:
sranalysis-network: sranalysis-network:
driver: bridge driver: bridge
+13 -13
View File
@@ -1,13 +1,13 @@
import nextCoreWebVitals from "eslint-config-next/core-web-vitals"; import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
import nextTypescript from "eslint-config-next/typescript"; import nextTypescript from "eslint-config-next/typescript";
import { dirname } from "path"; import { dirname } from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const eslintConfig = [...nextCoreWebVitals, ...nextTypescript, { const eslintConfig = [...nextCoreWebVitals, ...nextTypescript, {
ignores: ["node_modules/**", ".next/**", "out/**", "build/**", "next-env.d.ts"] ignores: ["node_modules/**", ".next/**", "out/**", "build/**", "next-env.d.ts"]
}]; }];
export default eslintConfig; export default eslintConfig;
+10 -10
View File
@@ -1,11 +1,11 @@
import { getRequestConfig } from "next-intl/server"; import { getRequestConfig } from "next-intl/server";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
export default getRequestConfig( async () => { export default getRequestConfig( async () => {
const locale = (await cookies()).get("MYNEXTAPP_LOCALE")?.value || "en"; const locale = (await cookies()).get("MYNEXTAPP_LOCALE")?.value || "en";
return { return {
locale, locale,
messages: (await import(`../messages/${locale}.json`)).default messages: (await import(`../messages/${locale}.json`)).default
} }
}) })
+111 -111
View File
@@ -1,111 +1,111 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly 分析器", "title": "Firefly 分析器",
"description": "Veritas 分析工具" "description": "Veritas 分析工具"
}, },
"DataAnalysisPage": { "DataAnalysisPage": {
"useSkill": "使用技能", "useSkill": "使用技能",
"totalDamage": "总伤害", "totalDamage": "总伤害",
"damagePerAV": "每行动值伤害", "damagePerAV": "每行动值伤害",
"totalAV": "总行动值", "totalAV": "总行动值",
"skillType": "技能类型", "skillType": "技能类型",
"skillName": "技能名称", "skillName": "技能名称",
"actionValue": "行动值", "actionValue": "行动值",
"character": "角色", "character": "角色",
"id": "ID", "id": "ID",
"path": "命途", "path": "命途",
"rarity": "稀有度", "rarity": "稀有度",
"damageByActionValue": "此行动值的伤害", "damageByActionValue": "此行动值的伤害",
"element": "属性", "element": "属性",
"totalTurn": "总回合", "totalTurn": "总回合",
"technique": "密技", "technique": "密技",
"talent": "天赋", "talent": "天赋",
"basic": "普攻", "basic": "普攻",
"skill": "战技", "skill": "战技",
"ultimate": "终结技", "ultimate": "终结技",
"servant": "忆灵", "servant": "忆灵",
"skillDamageBreakdown": "技能类别", "skillDamageBreakdown": "技能类别",
"skillUsageDistribution": "技能使用分布", "skillUsageDistribution": "技能使用分布",
"damageOverTime": "累计伤害", "damageOverTime": "累计伤害",
"damage": "伤害", "damage": "伤害",
"cumulativeDamage": "累计伤害量", "cumulativeDamage": "累计伤害量",
"characterInformation": "角色信息", "characterInformation": "角色信息",
"turnDetail": "轮次详情", "turnDetail": "轮次详情",
"damageDetails": "伤害详情", "damageDetails": "伤害详情",
"cycleCount": "轮次", "cycleCount": "轮次",
"chartInfo": "图表信息", "chartInfo": "图表信息",
"actionBar": "行动轴", "actionBar": "行动轴",
"lineupInfo": "阵容信息", "lineupInfo": "阵容信息",
"loadData": "加载战斗数据", "loadData": "加载战斗数据",
"exportData": "导出战斗数据", "exportData": "导出战斗数据",
"connectSetting": "连接设置", "connectSetting": "连接设置",
"connected": "已连接", "connected": "已连接",
"unconnected": "未连接", "unconnected": "未连接",
"socketConnection": "socket连接", "socketConnection": "socket连接",
"connectionType": "连接类型", "connectionType": "连接类型",
"status": "状态", "status": "状态",
"connect": "连接", "connect": "连接",
"checkGameConnect": "检查游戏连接", "checkGameConnect": "检查游戏连接",
"other": "其他", "other": "其他",
"host": "主机", "host": "主机",
"port": "端口", "port": "端口",
"hostPlaceHolder": "输入主机地址", "hostPlaceHolder": "输入主机地址",
"portPlaceHolder": "输入端口号", "portPlaceHolder": "输入端口号",
"noDamageDetail": "没有可用的伤害详情", "noDamageDetail": "没有可用的伤害详情",
"noCharactersInLineup": "队伍中没有角色", "noCharactersInLineup": "队伍中没有角色",
"noTurns": "尚未有回合", "noTurns": "尚未有回合",
"type": "类型 ", "type": "类型 ",
"warrior": "毁灭", "warrior": "毁灭",
"knight": "存护", "knight": "存护",
"mage": "智识", "mage": "智识",
"priest": "丰饶", "priest": "丰饶",
"rogue": "巡猎", "rogue": "巡猎",
"shaman": "同协", "shaman": "同协",
"warlock": "虚无", "warlock": "虚无",
"memory": "记忆", "memory": "记忆",
"elation": "欢愉", "elation": "欢愉",
"fire": "火", "fire": "火",
"ice": "冰", "ice": "冰",
"imaginary": "虚数", "imaginary": "虚数",
"physical": "物理", "physical": "物理",
"quantum": "量子", "quantum": "量子",
"thunder": "雷", "thunder": "雷",
"wind": "风", "wind": "风",
"cycle": "回合", "cycle": "回合",
"wave": "波次", "wave": "波次",
"hp": "生命值", "hp": "生命值",
"atk": "攻击力", "atk": "攻击力",
"speed": "速度", "speed": "速度",
"critRate": "暴击率", "critRate": "暴击率",
"critDmg": "暴击伤害", "critDmg": "暴击伤害",
"breakEffect": "击破特攻", "breakEffect": "击破特攻",
"effectRes": "效果抵抗", "effectRes": "效果抵抗",
"energyRegenerationRate": "能量回复效率", "energyRegenerationRate": "能量回复效率",
"effectHitRate": "效果命中", "effectHitRate": "效果命中",
"outgoingHealingBoost": "治疗量加成", "outgoingHealingBoost": "治疗量加成",
"fireDmgBoost": "火属性伤害提高", "fireDmgBoost": "火属性伤害提高",
"iceDmgBoost": "冰属性伤害提高", "iceDmgBoost": "冰属性伤害提高",
"imaginaryDmgBoost": "虚数属性伤害提高", "imaginaryDmgBoost": "虚数属性伤害提高",
"physicalDmgBoost": "物理属性伤害提高", "physicalDmgBoost": "物理属性伤害提高",
"quantumDmgBoost": "量子属性伤害提高", "quantumDmgBoost": "量子属性伤害提高",
"thunderDmgBoost": "雷属性伤害提高", "thunderDmgBoost": "雷属性伤害提高",
"windDmgBoost": "风属性伤害提高", "windDmgBoost": "风属性伤害提高",
"pursued": "附加伤害", "pursued": "附加伤害",
"true damage": "真实伤害", "true damage": "真实伤害",
"elation damage": "欢愉伤害", "elation damage": "欢愉伤害",
"follow-up": "追加攻击", "follow-up": "追加攻击",
"elemental damage": "击破与超击破伤害", "elemental damage": "击破与超击破伤害",
"dot": "持续伤害 ", "dot": "持续伤害 ",
"damagePerCycle": "每轮伤害", "damagePerCycle": "每轮伤害",
"damagePerCycleAndWave": "每轮/波伤害", "damagePerCycleAndWave": "每轮/波伤害",
"damagePerWave": "每波伤害", "damagePerWave": "每波伤害",
"lastTurn": "最后一回合", "lastTurn": "最后一回合",
"qte": "QTE技能", "qte": "QTE技能",
"mazenormal": "普通迷宫", "mazenormal": "普通迷宫",
"level": "等级", "level": "等级",
"relics": "遗器", "relics": "遗器",
"eidolons": "星魂", "eidolons": "星魂",
"lightcones": "光锥", "lightcones": "光锥",
"trailblazer": "开拓者" "trailblazer": "开拓者"
} }
} }
+111 -111
View File
@@ -1,111 +1,111 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firely Analytics", "title": "Firely Analytics",
"description": "Analytics tool for Veritas" "description": "Analytics tool for Veritas"
}, },
"DataAnalysisPage": { "DataAnalysisPage": {
"useSkill": "Use Ability", "useSkill": "Use Ability",
"totalDamage": "Total damage", "totalDamage": "Total damage",
"damagePerAV": "Damage/Action value", "damagePerAV": "Damage/Action value",
"totalAV": "Total action value", "totalAV": "Total action value",
"skillType": "Skill Type", "skillType": "Skill Type",
"skillName": "Skill Name", "skillName": "Skill Name",
"actionValue": "Action Value", "actionValue": "Action Value",
"character": "Character", "character": "Character",
"id": "Id", "id": "Id",
"path": "Path", "path": "Path",
"rarity": "Rarity", "rarity": "Rarity",
"element": "Element", "element": "Element",
"totalTurn": "Total Turn", "totalTurn": "Total Turn",
"technique": "Technique", "technique": "Technique",
"talent": "Talent", "talent": "Talent",
"basic": "Basic Attack", "basic": "Basic Attack",
"skill": "Skill", "skill": "Skill",
"ultimate": "Ultimate", "ultimate": "Ultimate",
"servant": "Servant", "servant": "Servant",
"skillDamageBreakdown": "Skill Damage Breakdown", "skillDamageBreakdown": "Skill Damage Breakdown",
"skillUsageDistribution": "Skill Usage Distribution", "skillUsageDistribution": "Skill Usage Distribution",
"damageOverTime": "Damage Over Time", "damageOverTime": "Damage Over Time",
"damage": "Damage", "damage": "Damage",
"cumulativeDamage": "Cumulative Damage", "cumulativeDamage": "Cumulative Damage",
"damageByActionValue": "Damage by Action Value", "damageByActionValue": "Damage by Action Value",
"characterInformation": "Character Information", "characterInformation": "Character Information",
"turnDetail": "Turn Details", "turnDetail": "Turn Details",
"damageDetails": "Damage Details", "damageDetails": "Damage Details",
"cycleCount": "Cycle Count", "cycleCount": "Cycle Count",
"chartInfo": "Chart info", "chartInfo": "Chart info",
"actionBar": "Action time line", "actionBar": "Action time line",
"lineupInfo": "Lineup info", "lineupInfo": "Lineup info",
"loadData": "Load data battle", "loadData": "Load data battle",
"exportData": "Export data battle", "exportData": "Export data battle",
"connectSetting": "Connection Setting", "connectSetting": "Connection Setting",
"connected": "Connected", "connected": "Connected",
"unconnected": "Unconnected", "unconnected": "Unconnected",
"socketConnection": "Socket Connection", "socketConnection": "Socket Connection",
"connectionType": "Connection Type", "connectionType": "Connection Type",
"status": "Status", "status": "Status",
"connect": "Connect", "connect": "Connect",
"checkGameConnect": "Check game connection", "checkGameConnect": "Check game connection",
"other": "Other", "other": "Other",
"host": "Host", "host": "Host",
"port": "Port", "port": "Port",
"hostPlaceHolder": "Enter Host", "hostPlaceHolder": "Enter Host",
"portPlaceHolder": "Enter port number", "portPlaceHolder": "Enter port number",
"noDamageDetail": "No damage details available", "noDamageDetail": "No damage details available",
"noCharactersInLineup": "No characters in lineup", "noCharactersInLineup": "No characters in lineup",
"noTurns": "No turns yet", "noTurns": "No turns yet",
"type": "Type", "type": "Type",
"warrior": "The Destruction", "warrior": "The Destruction",
"knight": "The Preservation", "knight": "The Preservation",
"mage": "The Erudition", "mage": "The Erudition",
"priest": "The Abundance", "priest": "The Abundance",
"rogue": "The Hunt", "rogue": "The Hunt",
"shaman": "The Harmony", "shaman": "The Harmony",
"warlock": "The Nihility", "warlock": "The Nihility",
"memory": "The Remembrance", "memory": "The Remembrance",
"elation": "The Elation", "elation": "The Elation",
"fire": "Fire", "fire": "Fire",
"ice": "Ice", "ice": "Ice",
"imaginary": "Imaginary", "imaginary": "Imaginary",
"physical": "Physical", "physical": "Physical",
"quantum": "Quantum", "quantum": "Quantum",
"thunder": "Thunder", "thunder": "Thunder",
"wind": "Wind", "wind": "Wind",
"cycle": "Cycle", "cycle": "Cycle",
"wave": "Wave", "wave": "Wave",
"hp": "Hp", "hp": "Hp",
"atk": "Atk", "atk": "Atk",
"speed": "Speed", "speed": "Speed",
"critRate": "Crit Rate", "critRate": "Crit Rate",
"critDmg": "Crit Dmg", "critDmg": "Crit Dmg",
"breakEffect": "Break Effect", "breakEffect": "Break Effect",
"effectRes": "Effect Res", "effectRes": "Effect Res",
"energyRegenerationRate": "Energy Regeneration Rate", "energyRegenerationRate": "Energy Regeneration Rate",
"effectHitRate": "Effect Hit Rate", "effectHitRate": "Effect Hit Rate",
"outgoingHealingBoost": "Outgoing Healing Boost", "outgoingHealingBoost": "Outgoing Healing Boost",
"fireDmgBoost": "Fire damage boost", "fireDmgBoost": "Fire damage boost",
"iceDmgBoost": "Ice damage Boost", "iceDmgBoost": "Ice damage Boost",
"imaginaryDmgBoost": "Imaginary damage boost", "imaginaryDmgBoost": "Imaginary damage boost",
"physicalDmgBoost": "Physical damage boost", "physicalDmgBoost": "Physical damage boost",
"quantumDmgBoost": "Quantum damage boost", "quantumDmgBoost": "Quantum damage boost",
"thunderDmgBoost": "Thunder damage boost", "thunderDmgBoost": "Thunder damage boost",
"windDmgBoost": "Wind damage boost", "windDmgBoost": "Wind damage boost",
"pursued": "Additional damage", "pursued": "Additional damage",
"true damage": "True damage", "true damage": "True damage",
"elation damage": "Elation Damage", "elation damage": "Elation Damage",
"follow-up": "Follow-up Damage", "follow-up": "Follow-up Damage",
"elemental damage": "Break and Super break damage", "elemental damage": "Break and Super break damage",
"dot": "Damage over time ", "dot": "Damage over time ",
"damagePerCycle": "Damage per Cycle", "damagePerCycle": "Damage per Cycle",
"damagePerCycleAndWave": "Damage per Cycle and Wave", "damagePerCycleAndWave": "Damage per Cycle and Wave",
"damagePerWave": "Damage per Wave", "damagePerWave": "Damage per Wave",
"lastTurn": "Last turn", "lastTurn": "Last turn",
"qte": "QTE Skill", "qte": "QTE Skill",
"mazenormal": "MazeNormal", "mazenormal": "MazeNormal",
"level": "Level", "level": "Level",
"relics": "Relics", "relics": "Relics",
"eidolons": "Eidolons", "eidolons": "Eidolons",
"lightcones": "Lightcones", "lightcones": "Lightcones",
"trailblazer": "Trailblazer" "trailblazer": "Trailblazer"
} }
} }
+111 -111
View File
@@ -1,111 +1,111 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Analytics", "title": "Firefly Analytics",
"description": "Veritasの分析ツール" "description": "Veritasの分析ツール"
}, },
"DataAnalysisPage": { "DataAnalysisPage": {
"useSkill": "スキルを使用", "useSkill": "スキルを使用",
"totalDamage": "総ダメージ", "totalDamage": "総ダメージ",
"damagePerAV": "ダメージ/行動値", "damagePerAV": "ダメージ/行動値",
"totalAV": "総行動値", "totalAV": "総行動値",
"skillType": "スキルタイプ", "skillType": "スキルタイプ",
"skillName": "スキル名", "skillName": "スキル名",
"actionValue": "行動値", "actionValue": "行動値",
"character": "キャラクター", "character": "キャラクター",
"id": "ID", "id": "ID",
"path": "運命", "path": "運命",
"rarity": "レアリティ", "rarity": "レアリティ",
"element": "属性", "element": "属性",
"totalTurn": "総ターン数", "totalTurn": "総ターン数",
"technique": "秘技", "technique": "秘技",
"talent": "天賦", "talent": "天賦",
"basic": "通常攻撃", "basic": "通常攻撃",
"skill": "スキル", "skill": "スキル",
"ultimate": "必殺技", "ultimate": "必殺技",
"servant": "召喚", "servant": "召喚",
"skillDamageBreakdown": "スキルダメージ内訳", "skillDamageBreakdown": "スキルダメージ内訳",
"skillUsageDistribution": "スキル使用分布", "skillUsageDistribution": "スキル使用分布",
"damageOverTime": "継続ダメージ", "damageOverTime": "継続ダメージ",
"damage": "ダメージ", "damage": "ダメージ",
"cumulativeDamage": "累積ダメージ", "cumulativeDamage": "累積ダメージ",
"damageByActionValue": "行動値別ダメージ", "damageByActionValue": "行動値別ダメージ",
"characterInformation": "キャラクター情報", "characterInformation": "キャラクター情報",
"turnDetail": "ターン詳細", "turnDetail": "ターン詳細",
"damageDetails": "ダメージ詳細", "damageDetails": "ダメージ詳細",
"cycleCount": "サイクル数", "cycleCount": "サイクル数",
"chartInfo": "チャート情報", "chartInfo": "チャート情報",
"actionBar": "アクションタイムライン", "actionBar": "アクションタイムライン",
"lineupInfo": "編成情報", "lineupInfo": "編成情報",
"loadData": "戦闘データを読み込む", "loadData": "戦闘データを読み込む",
"exportData": "戦闘データをエクスポート", "exportData": "戦闘データをエクスポート",
"connectSetting": "接続設定", "connectSetting": "接続設定",
"connected": "接続済み", "connected": "接続済み",
"unconnected": "未接続", "unconnected": "未接続",
"socketConnection": "ソケット接続", "socketConnection": "ソケット接続",
"connectionType": "接続タイプ", "connectionType": "接続タイプ",
"status": "ステータス", "status": "ステータス",
"connect": "接続する", "connect": "接続する",
"checkGameConnect": "ゲーム接続を確認", "checkGameConnect": "ゲーム接続を確認",
"other": "その他", "other": "その他",
"host": "ホスト", "host": "ホスト",
"port": "ポート", "port": "ポート",
"hostPlaceHolder": "ホストを入力", "hostPlaceHolder": "ホストを入力",
"portPlaceHolder": "ポート番号を入力", "portPlaceHolder": "ポート番号を入力",
"noDamageDetail": "ダメージ詳細がありません", "noDamageDetail": "ダメージ詳細がありません",
"noCharactersInLineup": "編成にキャラクターがいません", "noCharactersInLineup": "編成にキャラクターがいません",
"noTurns": "まだターンがありません", "noTurns": "まだターンがありません",
"type": "タイプ", "type": "タイプ",
"warrior": "壊滅", "warrior": "壊滅",
"knight": "存護", "knight": "存護",
"mage": "知恵", "mage": "知恵",
"priest": "豊穣", "priest": "豊穣",
"rogue": "巡狩", "rogue": "巡狩",
"shaman": "調和", "shaman": "調和",
"warlock": "虚無", "warlock": "虚無",
"memory": "記憶", "memory": "記憶",
"elation": "愉悦", "elation": "愉悦",
"fire": "炎", "fire": "炎",
"ice": "氷", "ice": "氷",
"imaginary": "虚数", "imaginary": "虚数",
"physical": "物理", "physical": "物理",
"quantum": "量子", "quantum": "量子",
"thunder": "雷", "thunder": "雷",
"wind": "風", "wind": "風",
"cycle": "サイクル", "cycle": "サイクル",
"wave": "ウェーブ", "wave": "ウェーブ",
"hp": "HP", "hp": "HP",
"atk": "攻撃力", "atk": "攻撃力",
"speed": "速度", "speed": "速度",
"critRate": "会心率", "critRate": "会心率",
"critDmg": "会心ダメージ", "critDmg": "会心ダメージ",
"breakEffect": "撃破特効", "breakEffect": "撃破特効",
"effectRes": "効果抵抗", "effectRes": "効果抵抗",
"energyRegenerationRate": "エネルギー回復効率", "energyRegenerationRate": "エネルギー回復効率",
"effectHitRate": "効果命中率", "effectHitRate": "効果命中率",
"outgoingHealingBoost": "治癒量上昇", "outgoingHealingBoost": "治癒量上昇",
"fireDmgBoost": "炎属性ダメージ上昇", "fireDmgBoost": "炎属性ダメージ上昇",
"iceDmgBoost": "氷属性ダメージ上昇", "iceDmgBoost": "氷属性ダメージ上昇",
"imaginaryDmgBoost": "虚数属性ダメージ上昇", "imaginaryDmgBoost": "虚数属性ダメージ上昇",
"physicalDmgBoost": "物理属性ダメージ上昇", "physicalDmgBoost": "物理属性ダメージ上昇",
"quantumDmgBoost": "量子属性ダメージ上昇", "quantumDmgBoost": "量子属性ダメージ上昇",
"thunderDmgBoost": "雷属性ダメージ上昇", "thunderDmgBoost": "雷属性ダメージ上昇",
"windDmgBoost": "風属性ダメージ上昇", "windDmgBoost": "風属性ダメージ上昇",
"pursued": "追加ダメージ", "pursued": "追加ダメージ",
"true damage": "貫通ダメージ", "true damage": "貫通ダメージ",
"elation damage": "愉悦ダメージ", "elation damage": "愉悦ダメージ",
"follow-up": "追撃ダメージ", "follow-up": "追撃ダメージ",
"elemental damage": "撃破・超撃破ダメージ", "elemental damage": "撃破・超撃破ダメージ",
"dot": "継続ダメージ", "dot": "継続ダメージ",
"damagePerCycle": "サイクル毎のダメージ", "damagePerCycle": "サイクル毎のダメージ",
"damagePerCycleAndWave": "サイクル・ウェーブ毎のダメージ", "damagePerCycleAndWave": "サイクル・ウェーブ毎のダメージ",
"damagePerWave": "ウェーブ毎のダメージ", "damagePerWave": "ウェーブ毎のダメージ",
"lastTurn": "最後のターン", "lastTurn": "最後のターン",
"qte": "QTEスキル", "qte": "QTEスキル",
"mazenormal": "通常迷宮", "mazenormal": "通常迷宮",
"level": "レベル", "level": "レベル",
"relics": "遺物", "relics": "遺物",
"eidolons": "星魂", "eidolons": "星魂",
"lightcones": "光円錐", "lightcones": "光円錐",
"trailblazer": "開拓者" "trailblazer": "開拓者"
} }
} }
+111 -111
View File
@@ -1,111 +1,111 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly 분석", "title": "Firefly 분석",
"description": "Veritas의 분석 도구" "description": "Veritas의 분석 도구"
}, },
"DataAnalysisPage": { "DataAnalysisPage": {
"useSkill": "스킬 사용", "useSkill": "스킬 사용",
"totalDamage": "총 피해량", "totalDamage": "총 피해량",
"damagePerAV": "피해량/행동값", "damagePerAV": "피해량/행동값",
"totalAV": "총 행동값", "totalAV": "총 행동값",
"skillType": "스킬 유형", "skillType": "스킬 유형",
"skillName": "스킬 이름", "skillName": "스킬 이름",
"actionValue": "행동값", "actionValue": "행동값",
"character": "캐릭터", "character": "캐릭터",
"id": "ID", "id": "ID",
"path": "운명", "path": "운명",
"rarity": "희귀도", "rarity": "희귀도",
"element": "속성", "element": "속성",
"totalTurn": "총 턴 수", "totalTurn": "총 턴 수",
"technique": "비전", "technique": "비전",
"talent": "재능", "talent": "재능",
"basic": "기본 공격", "basic": "기본 공격",
"skill": "스킬", "skill": "스킬",
"ultimate": "필살기", "ultimate": "필살기",
"servant": "소환", "servant": "소환",
"skillDamageBreakdown": "스킬 피해 세부사항", "skillDamageBreakdown": "스킬 피해 세부사항",
"skillUsageDistribution": "스킬 사용 분포", "skillUsageDistribution": "스킬 사용 분포",
"damageOverTime": "시간당 피해", "damageOverTime": "시간당 피해",
"damage": "피해", "damage": "피해",
"cumulativeDamage": "누적 피해", "cumulativeDamage": "누적 피해",
"damageByActionValue": "행동값별 피해", "damageByActionValue": "행동값별 피해",
"characterInformation": "캐릭터 정보", "characterInformation": "캐릭터 정보",
"turnDetail": "턴 상세 정보", "turnDetail": "턴 상세 정보",
"damageDetails": "피해 세부사항", "damageDetails": "피해 세부사항",
"cycleCount": "사이클 수", "cycleCount": "사이클 수",
"chartInfo": "차트 정보", "chartInfo": "차트 정보",
"actionBar": "행동 타임라인", "actionBar": "행동 타임라인",
"lineupInfo": "편성 정보", "lineupInfo": "편성 정보",
"loadData": "전투 데이터 불러오기", "loadData": "전투 데이터 불러오기",
"exportData": "전투 데이터 내보내기", "exportData": "전투 데이터 내보내기",
"connectSetting": "연결 설정", "connectSetting": "연결 설정",
"connected": "연결됨", "connected": "연결됨",
"unconnected": "연결되지 않음", "unconnected": "연결되지 않음",
"socketConnection": "소켓 연결", "socketConnection": "소켓 연결",
"connectionType": "연결 유형", "connectionType": "연결 유형",
"status": "상태", "status": "상태",
"connect": "연결", "connect": "연결",
"checkGameConnect": "게임 연결 확인", "checkGameConnect": "게임 연결 확인",
"other": "기타", "other": "기타",
"host": "호스트", "host": "호스트",
"port": "포트", "port": "포트",
"hostPlaceHolder": "호스트 입력", "hostPlaceHolder": "호스트 입력",
"portPlaceHolder": "포트 번호 입력", "portPlaceHolder": "포트 번호 입력",
"noDamageDetail": "피해 세부사항 없음", "noDamageDetail": "피해 세부사항 없음",
"noCharactersInLineup": "편성에 캐릭터 없음", "noCharactersInLineup": "편성에 캐릭터 없음",
"noTurns": "아직 턴이 없습니다", "noTurns": "아직 턴이 없습니다",
"type": "유형", "type": "유형",
"warrior": "파괴", "warrior": "파괴",
"knight": "보존", "knight": "보존",
"mage": "지혜", "mage": "지혜",
"priest": "풍요", "priest": "풍요",
"rogue": "사냥", "rogue": "사냥",
"shaman": "조화", "shaman": "조화",
"warlock": "허무", "warlock": "허무",
"memory": "기억", "memory": "기억",
"elation": "환락", "elation": "환락",
"fire": "불", "fire": "불",
"ice": "얼음", "ice": "얼음",
"imaginary": "허수", "imaginary": "허수",
"physical": "물리", "physical": "물리",
"quantum": "양자", "quantum": "양자",
"thunder": "번개", "thunder": "번개",
"wind": "바람", "wind": "바람",
"cycle": "사이클", "cycle": "사이클",
"wave": "웨이브", "wave": "웨이브",
"hp": "HP", "hp": "HP",
"atk": "공격력", "atk": "공격력",
"speed": "속도", "speed": "속도",
"critRate": "치명타 확률", "critRate": "치명타 확률",
"critDmg": "치명타 피해", "critDmg": "치명타 피해",
"breakEffect": "파괴 효과", "breakEffect": "파괴 효과",
"effectRes": "효과 저항", "effectRes": "효과 저항",
"energyRegenerationRate": "에너지 회복 속도", "energyRegenerationRate": "에너지 회복 속도",
"effectHitRate": "효과 적중률", "effectHitRate": "효과 적중률",
"outgoingHealingBoost": "치유량 증가", "outgoingHealingBoost": "치유량 증가",
"fireDmgBoost": "불 속성 피해 증가", "fireDmgBoost": "불 속성 피해 증가",
"iceDmgBoost": "얼음 속성 피해 증가", "iceDmgBoost": "얼음 속성 피해 증가",
"imaginaryDmgBoost": "허수 속성 피해 증가", "imaginaryDmgBoost": "허수 속성 피해 증가",
"physicalDmgBoost": "물리 속성 피해 증가", "physicalDmgBoost": "물리 속성 피해 증가",
"quantumDmgBoost": "양자 속성 피해 증가", "quantumDmgBoost": "양자 속성 피해 증가",
"thunderDmgBoost": "번개 속성 피해 증가", "thunderDmgBoost": "번개 속성 피해 증가",
"windDmgBoost": "바람 속성 피해 증가", "windDmgBoost": "바람 속성 피해 증가",
"pursued": "추가 피해", "pursued": "추가 피해",
"true damage": "관통 피해", "true damage": "관통 피해",
"elation damage": "환락 피해", "elation damage": "환락 피해",
"follow-up": "추가 공격 피해", "follow-up": "추가 공격 피해",
"elemental damage": "파괴 및 초파괴 피해", "elemental damage": "파괴 및 초파괴 피해",
"dot": "지속 피해", "dot": "지속 피해",
"damagePerCycle": "사이클당 피해", "damagePerCycle": "사이클당 피해",
"damagePerCycleAndWave": "사이클 및 웨이브당 피해", "damagePerCycleAndWave": "사이클 및 웨이브당 피해",
"damagePerWave": "웨이브당 피해", "damagePerWave": "웨이브당 피해",
"lastTurn": "마지막 턴", "lastTurn": "마지막 턴",
"qte": "QTE 스킬", "qte": "QTE 스킬",
"mazenormal": "일반 미궁", "mazenormal": "일반 미궁",
"level": "레벨", "level": "레벨",
"relics": "유물", "relics": "유물",
"eidolons": "에이돌론", "eidolons": "에이돌론",
"lightcones": "광추", "lightcones": "광추",
"trailblazer": "개척자" "trailblazer": "개척자"
} }
} }
+111 -111
View File
@@ -1,111 +1,111 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly Analytics", "title": "Firefly Analytics",
"description": "Công cụ phân tích cho Veritas" "description": "Công cụ phân tích cho Veritas"
}, },
"DataAnalysisPage": { "DataAnalysisPage": {
"useSkill": "Sử dụng kỹ năng", "useSkill": "Sử dụng kỹ năng",
"totalDamage": "Tổng sát thương", "totalDamage": "Tổng sát thương",
"damagePerAV": "Sát thương/giá trị hành động", "damagePerAV": "Sát thương/giá trị hành động",
"totalAV": "Tổng giá trị hành động", "totalAV": "Tổng giá trị hành động",
"skillType": "Loại kỹ năng", "skillType": "Loại kỹ năng",
"skillName": "Tên kỹ năng", "skillName": "Tên kỹ năng",
"actionValue": "Giá trị hành động", "actionValue": "Giá trị hành động",
"character": "Nhân vật", "character": "Nhân vật",
"id": "ID", "id": "ID",
"path": "Vận mệnh", "path": "Vận mệnh",
"rarity": "Số sao", "rarity": "Số sao",
"element": "Nguyên tố", "element": "Nguyên tố",
"totalTurn": "Tổng lượt", "totalTurn": "Tổng lượt",
"damageByActionValue": "Sát thương theo giá trị hành động", "damageByActionValue": "Sát thương theo giá trị hành động",
"technique": "Bĩ kỹ", "technique": "Bĩ kỹ",
"talent": "Thiên phú", "talent": "Thiên phú",
"basic": "Tấn công thường", "basic": "Tấn công thường",
"skill": "Chiến kỹ", "skill": "Chiến kỹ",
"ultimate": "Tuyệt Kĩ", "ultimate": "Tuyệt Kĩ",
"servant": "Vật triệu hồi", "servant": "Vật triệu hồi",
"skillDamageBreakdown": "Phân tích sát thương kỹ năng", "skillDamageBreakdown": "Phân tích sát thương kỹ năng",
"skillUsageDistribution": "Phân phối sử dụng kỹ năng", "skillUsageDistribution": "Phân phối sử dụng kỹ năng",
"damageOverTime": "Sát thương theo thời gian", "damageOverTime": "Sát thương theo thời gian",
"damage": "Sát thương", "damage": "Sát thương",
"cumulativeDamage": "Sát thương tích lũy", "cumulativeDamage": "Sát thương tích lũy",
"characterInformation": "Thông tin nhân vật", "characterInformation": "Thông tin nhân vật",
"turnDetail": "Chi tiết lượt", "turnDetail": "Chi tiết lượt",
"damageDetails": "Chi tiết sát thương", "damageDetails": "Chi tiết sát thương",
"cycleCount": "Số vòng", "cycleCount": "Số vòng",
"chartInfo": "Thông tin biểu đồ", "chartInfo": "Thông tin biểu đồ",
"actionBar": "Thanh hành động", "actionBar": "Thanh hành động",
"lineupInfo": "Thông tin đội hình", "lineupInfo": "Thông tin đội hình",
"loadData": "Tải dữ liệu trận đấu", "loadData": "Tải dữ liệu trận đấu",
"exportData": "Xuất dữ liệu trận đấu", "exportData": "Xuất dữ liệu trận đấu",
"connectSetting": "Cài đặt kết nối", "connectSetting": "Cài đặt kết nối",
"connected": "Đã kết nối", "connected": "Đã kết nối",
"unconnected": "Chưa kết nối", "unconnected": "Chưa kết nối",
"socketConnection": "Kết nối socket", "socketConnection": "Kết nối socket",
"connectionType": "Loại kết nối", "connectionType": "Loại kết nối",
"status": "Trạng thái", "status": "Trạng thái",
"connect": "Kết nối", "connect": "Kết nối",
"checkGameConnect": "Kiểm tra kết nối game", "checkGameConnect": "Kiểm tra kết nối game",
"other": "Khác", "other": "Khác",
"host": "Địa chỉ kết nối", "host": "Địa chỉ kết nối",
"port": "Cổng kết nối", "port": "Cổng kết nối",
"hostPlaceHolder": "Nhập địa chỉ kết nối", "hostPlaceHolder": "Nhập địa chỉ kết nối",
"portPlaceHolder": "Nhâp cổng kết nối", "portPlaceHolder": "Nhâp cổng kết nối",
"noDamageDetail": "Không có chi tiết sát thương", "noDamageDetail": "Không có chi tiết sát thương",
"noCharactersInLineup": "Không có nhân vật trong đội hình", "noCharactersInLineup": "Không có nhân vật trong đội hình",
"noTurns": "Chưa có lượt hành động", "noTurns": "Chưa có lượt hành động",
"type": "Loại", "type": "Loại",
"warrior": "Hủy Diệt", "warrior": "Hủy Diệt",
"knight": "Bảo Hộ", "knight": "Bảo Hộ",
"mage": "Tri Thức", "mage": "Tri Thức",
"priest": "Phong Phú", "priest": "Phong Phú",
"rogue": "Săn Bắn", "rogue": "Săn Bắn",
"shaman": "Hài Hòa", "shaman": "Hài Hòa",
"warlock": "Hư Vô", "warlock": "Hư Vô",
"memory": "Ký Ức", "memory": "Ký Ức",
"elation": "Vui vẻ", "elation": "Vui vẻ",
"fire": "Lửa", "fire": "Lửa",
"ice": "Băng", "ice": "Băng",
"imaginary": "Số ảo", "imaginary": "Số ảo",
"physical": "Vật Lý", "physical": "Vật Lý",
"quantum": "Lượng Tử", "quantum": "Lượng Tử",
"thunder": "Lôi", "thunder": "Lôi",
"wind": "Phong", "wind": "Phong",
"cycle": "Vòng", "cycle": "Vòng",
"wave": "Đợt quái", "wave": "Đợt quái",
"hp": "Máu", "hp": "Máu",
"atk": "Tấn công", "atk": "Tấn công",
"speed": "Tốc độ", "speed": "Tốc độ",
"critRate": "Tỉ lệ chí mạng", "critRate": "Tỉ lệ chí mạng",
"critDmg": "Sát thương chí mạng", "critDmg": "Sát thương chí mạng",
"breakEffect": "Tấn công kích phá", "breakEffect": "Tấn công kích phá",
"effectRes": "Kháng hiệu ứng", "effectRes": "Kháng hiệu ứng",
"energyRegenerationRate": "Hiệu quả nạp", "energyRegenerationRate": "Hiệu quả nạp",
"effectHitRate": "Chinh xác hiệu ứng", "effectHitRate": "Chinh xác hiệu ứng",
"outgoingHealingBoost": "Hiệu quả trị liệu", "outgoingHealingBoost": "Hiệu quả trị liệu",
"fireDmgBoost": "Tăng sát thương hỏa", "fireDmgBoost": "Tăng sát thương hỏa",
"iceDmgBoost": "Tăng sát thương băng", "iceDmgBoost": "Tăng sát thương băng",
"imaginaryDmgBoost": "Tăng sát thương số ảo", "imaginaryDmgBoost": "Tăng sát thương số ảo",
"physicalDmgBoost": "Tăng sát thương vật lý", "physicalDmgBoost": "Tăng sát thương vật lý",
"quantumDmgBoost": "Tăng sát thương lượng tử", "quantumDmgBoost": "Tăng sát thương lượng tử",
"thunderDmgBoost": "Tăng sát thương lôi", "thunderDmgBoost": "Tăng sát thương lôi",
"windDmgBoost": "Tăng sát thương phong", "windDmgBoost": "Tăng sát thương phong",
"pursued": "Sát thương kèm theo", "pursued": "Sát thương kèm theo",
"true damage": "Sát thương chuẩn", "true damage": "Sát thương chuẩn",
"elation damage": "Sát thương vui vẻ", "elation damage": "Sát thương vui vẻ",
"follow-up": "Sát thương theo sau", "follow-up": "Sát thương theo sau",
"elemental damage": "Sát thương phá vỡ và siêu phá vỡ", "elemental damage": "Sát thương phá vỡ và siêu phá vỡ",
"dot": "Sát Thương Duy Trì", "dot": "Sát Thương Duy Trì",
"damagePerCycle": "Sát thương mỗi vòng", "damagePerCycle": "Sát thương mỗi vòng",
"damagePerCycleAndWave": "Sát thương mỗi vòng và đợt", "damagePerCycleAndWave": "Sát thương mỗi vòng và đợt",
"damagePerWave": "Sát thương mỗi đợt", "damagePerWave": "Sát thương mỗi đợt",
"lastTurn": "Lượt cuối", "lastTurn": "Lượt cuối",
"qte": "Kỹ năng QTE", "qte": "Kỹ năng QTE",
"mazenormal": "Mê cung (Thường)", "mazenormal": "Mê cung (Thường)",
"level": "Cấp độ", "level": "Cấp độ",
"relics": "Di vật", "relics": "Di vật",
"eidolons": "Tinh hồn", "eidolons": "Tinh hồn",
"lightcones": "Nón Ánh sáng", "lightcones": "Nón Ánh sáng",
"trailblazer": "Nhà khai phá" "trailblazer": "Nhà khai phá"
} }
} }
+111 -111
View File
@@ -1,111 +1,111 @@
{ {
"TabTitle": { "TabTitle": {
"title": "Firefly 分析器", "title": "Firefly 分析器",
"description": "Veritas 分析工具" "description": "Veritas 分析工具"
}, },
"DataAnalysisPage": { "DataAnalysisPage": {
"useSkill": "使用技能", "useSkill": "使用技能",
"totalDamage": "总伤害", "totalDamage": "总伤害",
"damagePerAV": "每行动值伤害", "damagePerAV": "每行动值伤害",
"totalAV": "总行动值", "totalAV": "总行动值",
"skillType": "技能类型", "skillType": "技能类型",
"skillName": "技能名称", "skillName": "技能名称",
"actionValue": "行动值", "actionValue": "行动值",
"character": "角色", "character": "角色",
"id": "ID", "id": "ID",
"path": "命途", "path": "命途",
"rarity": "稀有度", "rarity": "稀有度",
"damageByActionValue": "此行动值的伤害", "damageByActionValue": "此行动值的伤害",
"element": "属性", "element": "属性",
"totalTurn": "总回合", "totalTurn": "总回合",
"technique": "密技", "technique": "密技",
"talent": "天赋", "talent": "天赋",
"basic": "普攻", "basic": "普攻",
"skill": "战技", "skill": "战技",
"ultimate": "终结技", "ultimate": "终结技",
"servant": "忆灵", "servant": "忆灵",
"skillDamageBreakdown": "技能类别", "skillDamageBreakdown": "技能类别",
"skillUsageDistribution": "技能使用分布", "skillUsageDistribution": "技能使用分布",
"damageOverTime": "累计伤害", "damageOverTime": "累计伤害",
"damage": "伤害", "damage": "伤害",
"cumulativeDamage": "累计伤害量", "cumulativeDamage": "累计伤害量",
"characterInformation": "角色信息", "characterInformation": "角色信息",
"turnDetail": "轮次详情", "turnDetail": "轮次详情",
"damageDetails": "伤害详情", "damageDetails": "伤害详情",
"cycleCount": "轮次", "cycleCount": "轮次",
"chartInfo": "图表信息", "chartInfo": "图表信息",
"actionBar": "行动轴", "actionBar": "行动轴",
"lineupInfo": "阵容信息", "lineupInfo": "阵容信息",
"loadData": "加载战斗数据", "loadData": "加载战斗数据",
"exportData": "导出战斗数据", "exportData": "导出战斗数据",
"connectSetting": "连接设置", "connectSetting": "连接设置",
"connected": "已连接", "connected": "已连接",
"unconnected": "未连接", "unconnected": "未连接",
"socketConnection": "socket连接", "socketConnection": "socket连接",
"connectionType": "连接类型", "connectionType": "连接类型",
"status": "状态", "status": "状态",
"connect": "连接", "connect": "连接",
"checkGameConnect": "检查游戏连接", "checkGameConnect": "检查游戏连接",
"other": "其他", "other": "其他",
"host": "主机", "host": "主机",
"port": "端口", "port": "端口",
"hostPlaceHolder": "输入主机地址", "hostPlaceHolder": "输入主机地址",
"portPlaceHolder": "输入端口号", "portPlaceHolder": "输入端口号",
"noDamageDetail": "没有可用的伤害详情", "noDamageDetail": "没有可用的伤害详情",
"noCharactersInLineup": "队伍中没有角色", "noCharactersInLineup": "队伍中没有角色",
"noTurns": "尚未有回合", "noTurns": "尚未有回合",
"type": "类型 ", "type": "类型 ",
"warrior": "毁灭", "warrior": "毁灭",
"knight": "存护", "knight": "存护",
"mage": "智识", "mage": "智识",
"priest": "丰饶", "priest": "丰饶",
"rogue": "巡猎", "rogue": "巡猎",
"shaman": "同协", "shaman": "同协",
"warlock": "虚无", "warlock": "虚无",
"memory": "记忆", "memory": "记忆",
"elation": "欢愉", "elation": "欢愉",
"fire": "火", "fire": "火",
"ice": "冰", "ice": "冰",
"imaginary": "虚数", "imaginary": "虚数",
"physical": "物理", "physical": "物理",
"quantum": "量子", "quantum": "量子",
"thunder": "雷", "thunder": "雷",
"wind": "风", "wind": "风",
"cycle": "回合", "cycle": "回合",
"wave": "波次", "wave": "波次",
"hp": "生命值", "hp": "生命值",
"atk": "攻击力", "atk": "攻击力",
"speed": "速度", "speed": "速度",
"critRate": "暴击率", "critRate": "暴击率",
"critDmg": "暴击伤害", "critDmg": "暴击伤害",
"breakEffect": "击破特攻", "breakEffect": "击破特攻",
"effectRes": "效果抵抗", "effectRes": "效果抵抗",
"energyRegenerationRate": "能量回复效率", "energyRegenerationRate": "能量回复效率",
"effectHitRate": "效果命中", "effectHitRate": "效果命中",
"outgoingHealingBoost": "治疗量加成", "outgoingHealingBoost": "治疗量加成",
"fireDmgBoost": "火属性伤害提高", "fireDmgBoost": "火属性伤害提高",
"iceDmgBoost": "冰属性伤害提高", "iceDmgBoost": "冰属性伤害提高",
"imaginaryDmgBoost": "虚数属性伤害提高", "imaginaryDmgBoost": "虚数属性伤害提高",
"physicalDmgBoost": "物理属性伤害提高", "physicalDmgBoost": "物理属性伤害提高",
"quantumDmgBoost": "量子属性伤害提高", "quantumDmgBoost": "量子属性伤害提高",
"thunderDmgBoost": "雷属性伤害提高", "thunderDmgBoost": "雷属性伤害提高",
"windDmgBoost": "风属性伤害提高", "windDmgBoost": "风属性伤害提高",
"pursued": "附加伤害", "pursued": "附加伤害",
"true damage": "真实伤害", "true damage": "真实伤害",
"elation damage": "欢愉伤害", "elation damage": "欢愉伤害",
"follow-up": "追加攻击", "follow-up": "追加攻击",
"elemental damage": "击破与超击破伤害", "elemental damage": "击破与超击破伤害",
"dot": "持续伤害 ", "dot": "持续伤害 ",
"damagePerCycle": "每轮伤害", "damagePerCycle": "每轮伤害",
"damagePerCycleAndWave": "每轮/波伤害", "damagePerCycleAndWave": "每轮/波伤害",
"damagePerWave": "每波伤害", "damagePerWave": "每波伤害",
"lastTurn": "最后一回合", "lastTurn": "最后一回合",
"qte": "QTE技能", "qte": "QTE技能",
"mazenormal": "普通迷宫", "mazenormal": "普通迷宫",
"level": "等级", "level": "等级",
"relics": "遗器", "relics": "遗器",
"eidolons": "星魂", "eidolons": "星魂",
"lightcones": "光锥", "lightcones": "光锥",
"trailblazer": "开拓者" "trailblazer": "开拓者"
} }
} }
+24 -24
View File
@@ -1,25 +1,25 @@
import { getDataCache } from "@/lib/cache/cache" import { getDataCache } from "@/lib/cache/cache"
export async function GET( export async function GET(
req: Request, req: Request,
{ params }: { params: Promise<{ name: string }> } { params }: { params: Promise<{ name: string }> }
) { ) {
const { name } = await params const { name } = await params
const item = getDataCache(name) const item = getDataCache(name)
if (!item) { if (!item) {
return new Response("Not found", { status: 404 }) return new Response("Not found", { status: 404 })
} }
const headers: Record<string, string> = { const headers: Record<string, string> = {
"Content-Type": "application/json", "Content-Type": "application/json",
"Cache-Control": "public, max-age=3600" "Cache-Control": "public, max-age=3600"
} }
if (item.type === "br") { if (item.type === "br") {
headers["Content-Encoding"] = "br" headers["Content-Encoding"] = "br"
} }
return new Response(new Uint8Array(item.buf), { headers }) return new Response(new Uint8Array(item.buf), { headers })
} }
+282 -282
View File
@@ -1,283 +1,283 @@
"use client"; "use client";
import useAvatarDataStore from "@/stores/avatarDataStore"; import useAvatarDataStore from "@/stores/avatarDataStore";
import useBattleDataStore from "@/stores/battleDataStore"; import useBattleDataStore from "@/stores/battleDataStore";
import useLocaleStore from "@/stores/localeStore"; import useLocaleStore from "@/stores/localeStore";
import { attackTypeToString, CharacterBasic } from "@/types"; import { attackTypeToString, CharacterBasic } from "@/types";
import { SkillBattleInfo } from "@/types/mics"; import { SkillBattleInfo } from "@/types/mics";
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { getNameChar } from "@/helper"; import { getNameChar } from "@/helper";
import Image from "next/image"; import Image from "next/image";
import NameAvatar from "../nameAvatar"; import NameAvatar from "../nameAvatar";
export default function ActionBar() { export default function ActionBar() {
const [selectTurn, setSelectTurn] = useState<SkillBattleInfo | null>(null); const [selectTurn, setSelectTurn] = useState<SkillBattleInfo | null>(null);
const [selectAvatar, setSelectAvatar] = useState<CharacterBasic | null>(null); const [selectAvatar, setSelectAvatar] = useState<CharacterBasic | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const { skillHistory, turnHistory, cycleIndex, waveIndex, maxWave } = useBattleDataStore(); const { skillHistory, turnHistory, cycleIndex, waveIndex, maxWave } = useBattleDataStore();
const { listAvatar } = useAvatarDataStore(); const { listAvatar } = useAvatarDataStore();
const { locale } = useLocaleStore(); const { locale } = useLocaleStore();
const transI18n = useTranslations("DataAnalysisPage"); const transI18n = useTranslations("DataAnalysisPage");
const turnListRef = useRef<HTMLDivElement>(null); const turnListRef = useRef<HTMLDivElement>(null);
const contentStyle: React.CSSProperties = { const contentStyle: React.CSSProperties = {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
}; };
const handleShow = (modalId: string, avatar: CharacterBasic, turn: SkillBattleInfo) => { const handleShow = (modalId: string, avatar: CharacterBasic, turn: SkillBattleInfo) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null; const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) { if (modal) {
setSelectAvatar(avatar); setSelectAvatar(avatar);
setSelectTurn(turn); setSelectTurn(turn);
setIsModalOpen(true); setIsModalOpen(true);
modal.showModal(); modal.showModal();
} }
}; };
// Close modal handler // Close modal handler
const handleCloseModal = (modalId: string) => { const handleCloseModal = (modalId: string) => {
setIsModalOpen(false); setIsModalOpen(false);
setSelectAvatar(null); setSelectAvatar(null);
const modal = document.getElementById(modalId) as HTMLDialogElement | null; const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) { if (modal) {
modal.close(); modal.close();
} }
}; };
// Handle ESC key to close modal // Handle ESC key to close modal
useEffect(() => { useEffect(() => {
const handleEscKey = (event: KeyboardEvent) => { const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isModalOpen) { if (event.key === 'Escape' && isModalOpen) {
handleCloseModal("action_detail_modal"); handleCloseModal("action_detail_modal");
} }
}; };
window.addEventListener('keydown', handleEscKey); window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey); return () => window.removeEventListener('keydown', handleEscKey);
}, [isModalOpen]); }, [isModalOpen]);
// Scroll to the bottom when new turns are added // Scroll to the bottom when new turns are added
useEffect(() => { useEffect(() => {
if (turnListRef.current && skillHistory.length > 0) { if (turnListRef.current && skillHistory.length > 0) {
turnListRef.current.scrollTop = turnListRef.current.scrollHeight; turnListRef.current.scrollTop = turnListRef.current.scrollHeight;
} }
}, [skillHistory.length]); }, [skillHistory.length]);
return ( return (
<div className="p-4 md:p-1 rounded-lg shadow-lg w-full h-full"> <div className="p-4 md:p-1 rounded-lg shadow-lg w-full h-full">
<motion.h2 <motion.h2
className="text-center text-xl lg:text-2xl mb-2 font-bold text-transparent bg-clip-text bg-linear-to-r from-pink-500 via-purple-500 to-cyan-500" className="text-center text-xl lg:text-2xl mb-2 font-bold text-transparent bg-clip-text bg-linear-to-r from-pink-500 via-purple-500 to-cyan-500"
initial={{ opacity: 0, y: -20 }} initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
> >
{transI18n("actionBar")} {transI18n("actionBar")}
</motion.h2> </motion.h2>
<div className="grid grid-cols-2 md:grid-cols-1 xl:grid-cols-2 items-center justify-items-center w-full gap-2 mb-2 mx-2"> <div className="grid grid-cols-2 md:grid-cols-1 xl:grid-cols-2 items-center justify-items-center w-full gap-2 mb-2 mx-2">
<div className="badge badge-soft badge-info gap-2 text-lg py-3"> <div className="badge badge-soft badge-info gap-2 text-lg py-3">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-6"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-6">
<path fillRule="evenodd" d="M3 2.25a.75.75 0 0 1 .75.75v.54l1.838-.46a9.75 9.75 0 0 1 6.725.738l.108.054A8.25 8.25 0 0 0 18 4.524l3.11-.732a.75.75 0 0 1 .917.81 47.784 47.784 0 0 0 .005 10.337.75.75 0 0 1-.574.812l-3.114.733a9.75 9.75 0 0 1-6.594-.77l-.108-.054a8.25 8.25 0 0 0-5.69-.625l-2.202.55V21a.75.75 0 0 1-1.5 0V3A.75.75 0 0 1 3 2.25Z" clipRule="evenodd" /> <path fillRule="evenodd" d="M3 2.25a.75.75 0 0 1 .75.75v.54l1.838-.46a9.75 9.75 0 0 1 6.725.738l.108.054A8.25 8.25 0 0 0 18 4.524l3.11-.732a.75.75 0 0 1 .917.81 47.784 47.784 0 0 0 .005 10.337.75.75 0 0 1-.574.812l-3.114.733a9.75 9.75 0 0 1-6.594-.77l-.108-.054a8.25 8.25 0 0 0-5.69-.625l-2.202.55V21a.75.75 0 0 1-1.5 0V3A.75.75 0 0 1 3 2.25Z" clipRule="evenodd" />
</svg> </svg>
<span>{waveIndex}/{maxWave}</span> <span>{waveIndex}/{maxWave}</span>
</div> </div>
<div className="badge badge-soft badge-error gap-2 text-lg py-3 items-center"> <div className="badge badge-soft badge-error gap-2 text-lg py-3 items-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" fill="currentColor" className="size-6"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" fill="currentColor" className="size-6">
<path d="M0 32C0 14.3 14.3 0 32 0L64 0 320 0l32 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l0 11c0 42.4-16.9 83.1-46.9 113.1L237.3 256l67.9 67.9c30 30 46.9 70.7 46.9 113.1l0 11c17.7 0 32 14.3 32 32s-14.3 32-32 32l-32 0L64 512l-32 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l0-11c0-42.4 16.9-83.1 46.9-113.1L146.7 256 78.9 188.1C48.9 158.1 32 117.4 32 75l0-11C14.3 64 0 49.7 0 32zM96 64l0 11c0 25.5 10.1 49.9 28.1 67.9L192 210.7l67.9-67.9c18-18 28.1-42.4 28.1-67.9l0-11L96 64zm0 384l192 0 0-11c0-25.5-10.1-49.9-28.1-67.9L192 301.3l-67.9 67.9c-18 18-28.1 42.4-28.1 67.9l0 11z" /> <path d="M0 32C0 14.3 14.3 0 32 0L64 0 320 0l32 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l0 11c0 42.4-16.9 83.1-46.9 113.1L237.3 256l67.9 67.9c30 30 46.9 70.7 46.9 113.1l0 11c17.7 0 32 14.3 32 32s-14.3 32-32 32l-32 0L64 512l-32 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l0-11c0-42.4 16.9-83.1 46.9-113.1L146.7 256 78.9 188.1C48.9 158.1 32 117.4 32 75l0-11C14.3 64 0 49.7 0 32zM96 64l0 11c0 25.5 10.1 49.9 28.1 67.9L192 210.7l67.9-67.9c18-18 28.1-42.4 28.1-67.9l0-11L96 64zm0 384l192 0 0-11c0-25.5-10.1-49.9-28.1-67.9L192 301.3l-67.9 67.9c-18 18-28.1 42.4-28.1 67.9l0 11z" />
</svg> </svg>
<span>{cycleIndex}</span> <span>{cycleIndex}</span>
</div> </div>
</div> </div>
<div <div
ref={turnListRef} ref={turnListRef}
className="flex md:block px-2 md:px-0 w-full pt-2 border-t-2 border-accent overflow-x-auto md:overflow-x-hidden md:overflow-y-auto max-h-[90vh] lg:max-h-[80vh]" className="flex md:block px-2 md:px-0 w-full pt-2 border-t-2 border-accent overflow-x-auto md:overflow-x-hidden md:overflow-y-auto max-h-[90vh] lg:max-h-[80vh]"
> >
<div className="flex flex-nowrap md:grid md:grid-cols-1 gap-2 w-fit md:w-full"> <div className="flex flex-nowrap md:grid md:grid-cols-1 gap-2 w-fit md:w-full">
{skillHistory.length === 0 ? ( {skillHistory.length === 0 ? (
<div className="flex items-center justify-center h-full w-full"> <div className="flex items-center justify-center h-full w-full">
<p className="text-base-content opacity-50">{transI18n("noTurns")}</p> <p className="text-base-content opacity-50">{transI18n("noTurns")}</p>
</div> </div>
) : ( ) : (
skillHistory.map((turn, index) => { skillHistory.map((turn, index) => {
const data = listAvatar.find(it => it.id === turn.avatarId.toString()); const data = listAvatar.find(it => it.id === turn.avatarId.toString());
if (!data) return null; if (!data) return null;
const text = getNameChar(locale, transI18n, data); const text = getNameChar(locale, transI18n, data);
return ( return (
<div key={index} className="h-full md:w-full"> <div key={index} className="h-full md:w-full">
<div <div
onClick={() => handleShow("action_detail_modal", data, turn)} onClick={() => handleShow("action_detail_modal", data, turn)}
className="h-full grid grid-cols-2 gap-2 border bg-base-100 w-full hover:bg-base-200 transition-colors duration-200 border-cyan-400 border-l-4 cursor-pointer min-w-50 sm:min-w-62.5 md:min-w-0" className="h-full grid grid-cols-2 gap-2 border bg-base-100 w-full hover:bg-base-200 transition-colors duration-200 border-cyan-400 border-l-4 cursor-pointer min-w-50 sm:min-w-62.5 md:min-w-0"
> >
<div <div
style={contentStyle} style={contentStyle}
className="lg:col-span-1 grid grid-cols-1 items-center justify-center py-2" className="lg:col-span-1 grid grid-cols-1 items-center justify-center py-2"
> >
<div className="avatar"> <div className="avatar">
<div className="w-12 h-12 rounded-full border-2 flex items-center justify-center bg-base-300 border-cyan-400 border-l-4"> <div className="w-12 h-12 rounded-full border-2 flex items-center justify-center bg-base-300 border-cyan-400 border-l-4">
<Image <Image
src={`${process.env.CDN_URL}/${data.icon}`} src={`${process.env.CDN_URL}/${data.icon}`}
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
alt={text} alt={text}
width={125} width={125}
height={125} height={125}
className="w-8 h-8 object-contain" className="w-8 h-8 object-contain"
/> />
</div> </div>
</div> </div>
<NameAvatar <NameAvatar
locale={locale} locale={locale}
text={getNameChar(locale, transI18n, data)} text={getNameChar(locale, transI18n, data)}
className="text-base-content text-center text-sm mt-1 font-medium" className="text-base-content text-center text-sm mt-1 font-medium"
/> />
</div> </div>
<div className="grid grid-cols-1 justify-center gap-2 py-2 w-full"> <div className="grid grid-cols-1 justify-center gap-2 py-2 w-full">
<div className="bg-local text-primary text-xs max-w-full"> <div className="bg-local text-primary text-xs max-w-full">
{`${transI18n("useSkill")}: ${transI18n(attackTypeToString(turn.skillType).toLowerCase())}`} {`${transI18n("useSkill")}: ${transI18n(attackTypeToString(turn.skillType).toLowerCase())}`}
</div> </div>
<div className="text-primary text-xs max-w-full"> <div className="text-primary text-xs max-w-full">
{`${transI18n("totalDamage")}: ${Number(turn.totalDamage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}`} {`${transI18n("totalDamage")}: ${Number(turn.totalDamage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}`}
</div> </div>
<div className="text-primary text-xs max-w-full"> <div className="text-primary text-xs max-w-full">
{`${transI18n("actionValue")}: ${Number(turnHistory[turn.turnBattleId]?.actionValue || 0).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}`} {`${transI18n("actionValue")}: ${Number(turnHistory[turn.turnBattleId]?.actionValue || 0).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}`}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
); );
}) })
)} )}
</div> </div>
</div> </div>
{/* Character Detail Modal */} {/* Character Detail Modal */}
<dialog id="action_detail_modal" className="modal sm:modal-middle backdrop-blur-sm"> <dialog id="action_detail_modal" className="modal sm:modal-middle backdrop-blur-sm">
<div className="modal-box w-11/12 max-w-7xl bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20"> <div className="modal-box w-11/12 max-w-7xl bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10"> <div className="sticky top-0 z-10">
<motion.button <motion.button
whileHover={{ scale: 1.1, rotate: 90 }} whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }} 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" 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")} onClick={() => handleCloseModal("action_detail_modal")}
> >
✕ ✕
</motion.button> </motion.button>
</div> </div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4"> <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"> <h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{transI18n("turnDetail").toUpperCase()} {transI18n("turnDetail").toUpperCase()}
</h3> </h3>
</div> </div>
{selectAvatar && selectTurn && ( {selectAvatar && selectTurn && (
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="mt-4 w-full" className="mt-4 w-full"
> >
<div className="bg-base-200 rounded-lg p-4 shadow-md mb-4"> <div className="bg-base-200 rounded-lg p-4 shadow-md mb-4">
<h4 className="text-lg font-semibold mb-2 text-pink-500">{transI18n("characterInformation")}</h4> <h4 className="text-lg font-semibold mb-2 text-pink-500">{transI18n("characterInformation")}</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 items-center"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 items-center">
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-medium text-base-content/70">{transI18n("id")}:</span> <span className="font-medium text-base-content/70">{transI18n("id")}:</span>
<span className="font-bold">{selectAvatar.id}</span> <span className="font-bold">{selectAvatar.id}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-medium text-base-content/70">{transI18n("character")}:</span> <span className="font-medium text-base-content/70">{transI18n("character")}:</span>
<NameAvatar <NameAvatar
locale={locale} locale={locale}
text={getNameChar(locale, transI18n, selectAvatar)} text={getNameChar(locale, transI18n, selectAvatar)}
className="font-bold" className="font-bold"
/> />
</div> </div>
</div> </div>
<div className="flex justify-center items-center"> <div className="flex justify-center items-center">
<Image <Image
src={`${process.env.CDN_URL}/${selectAvatar?.icon}`} src={`${process.env.CDN_URL}/${selectAvatar?.icon}`}
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
alt={getNameChar(locale, transI18n, selectAvatar)} alt={getNameChar(locale, transI18n, selectAvatar)}
width={80} width={80}
height={80} height={80}
className="h-20 w-20 object-cover rounded-full border-2 border-purple-500 shadow-lg shadow-purple-500/20" className="h-20 w-20 object-cover rounded-full border-2 border-purple-500 shadow-lg shadow-purple-500/20"
/> />
</div> </div>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div className="bg-base-200 rounded-lg p-4 shadow-md"> <div className="bg-base-200 rounded-lg p-4 shadow-md">
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("skillType")}</h4> <h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("skillType")}</h4>
<p className="mt-2">{transI18n(attackTypeToString(selectTurn?.skillType).toLowerCase())}</p> <p className="mt-2">{transI18n(attackTypeToString(selectTurn?.skillType).toLowerCase())}</p>
</div> </div>
<div className="bg-base-200 rounded-lg p-4 shadow-md"> <div className="bg-base-200 rounded-lg p-4 shadow-md">
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("skillName")}</h4> <h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("skillName")}</h4>
<p className="mt-2">{selectTurn?.skillName}</p> <p className="mt-2">{selectTurn?.skillName}</p>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div className="bg-base-200 rounded-lg p-4 shadow-md"> <div className="bg-base-200 rounded-lg p-4 shadow-md">
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("cycle")}</h4> <h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("cycle")}</h4>
<p className="mt-2">{turnHistory[selectTurn?.turnBattleId].cycleIndex}</p> <p className="mt-2">{turnHistory[selectTurn?.turnBattleId].cycleIndex}</p>
</div> </div>
<div className="bg-base-200 rounded-lg p-4 shadow-md"> <div className="bg-base-200 rounded-lg p-4 shadow-md">
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("wave")}</h4> <h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("wave")}</h4>
<p className="mt-2">{turnHistory[selectTurn?.turnBattleId].waveIndex}</p> <p className="mt-2">{turnHistory[selectTurn?.turnBattleId].waveIndex}</p>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div className="bg-base-200 rounded-lg p-4 shadow-md"> <div className="bg-base-200 rounded-lg p-4 shadow-md">
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("actionValue")}</h4> <h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("actionValue")}</h4>
<p className="mt-2">{Number(turnHistory[selectTurn?.turnBattleId].actionValue).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</p> <p className="mt-2">{Number(turnHistory[selectTurn?.turnBattleId].actionValue).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</p>
</div> </div>
<div className="bg-base-200 rounded-lg p-4 shadow-md"> <div className="bg-base-200 rounded-lg p-4 shadow-md">
<h4 className="text-lg font-semibold mb-2 text-purple-500 border-b border-purple-300/30 pb-1">{transI18n("totalDamage")}</h4> <h4 className="text-lg font-semibold mb-2 text-purple-500 border-b border-purple-300/30 pb-1">{transI18n("totalDamage")}</h4>
<p className="mt-2 font-bold text-lg">{Number(selectTurn?.totalDamage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</p> <p className="mt-2 font-bold text-lg">{Number(selectTurn?.totalDamage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</p>
</div> </div>
</div> </div>
<div className="bg-base-200 rounded-lg p-4 shadow-md mb-4"> <div className="bg-base-200 rounded-lg p-4 shadow-md mb-4">
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("damageDetails")}</h4> <h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("damageDetails")}</h4>
{selectTurn?.damageDetail?.length > 0 ? ( {selectTurn?.damageDetail?.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 mt-3"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 mt-3">
{selectTurn.damageDetail.map((detail, idx) => ( {selectTurn.damageDetail.map((detail, idx) => (
<div <div
key={idx} key={idx}
className="flex flex-col items-start gap-1 p-3 rounded-lg shadow bg-base-200" className="flex flex-col items-start gap-1 p-3 rounded-lg shadow bg-base-200"
> >
<span className="text-lg font-semibold text-primary"> <span className="text-lg font-semibold text-primary">
{Number(detail.damage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })} {Number(detail.damage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}
</span> </span>
<span className="text-xs uppercase text-gray-500 tracking-wide"> <span className="text-xs uppercase text-gray-500 tracking-wide">
{transI18n(attackTypeToString(detail?.damage_type).toLowerCase())} {transI18n(attackTypeToString(detail?.damage_type).toLowerCase())}
</span> </span>
</div> </div>
))} ))}
</div> </div>
) : ( ) : (
<p className="mt-2 italic opacity-70">{transI18n("noDamageDetail")}</p> <p className="mt-2 italic opacity-70">{transI18n("noDamageDetail")}</p>
)} )}
</div> </div>
</motion.div> </motion.div>
)} )}
</div> </div>
</dialog> </dialog>
</div> </div>
); );
} }
+99 -99
View File
@@ -1,99 +1,99 @@
"use client"; "use client";
import { getNameChar } from '@/helper'; import { getNameChar } from '@/helper';
import useLocaleStore from '@/stores/localeStore'; import useLocaleStore from '@/stores/localeStore';
import { CharacterBasic } from '@/types'; import { CharacterBasic } from '@/types';
import NameAvatar from '../nameAvatar'; import NameAvatar from '../nameAvatar';
import useBattleDataStore from '@/stores/battleDataStore'; import useBattleDataStore from '@/stores/battleDataStore';
import Image from 'next/image'; import Image from 'next/image';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
interface CharacterCardProps { interface CharacterCardProps {
data: CharacterBasic data: CharacterBasic
} }
export default function CharacterCard({ data }: CharacterCardProps) { export default function CharacterCard({ data }: CharacterCardProps) {
const { locale } = useLocaleStore(); const { locale } = useLocaleStore();
const { avatarDetail } = useBattleDataStore() const { avatarDetail } = useBattleDataStore()
const transI18n = useTranslations("DataAnalysisPage"); const transI18n = useTranslations("DataAnalysisPage");
const text = getNameChar(locale, transI18n, data) const text = getNameChar(locale, transI18n, data)
return ( return (
<li className="z-10 flex flex-col w-28 items-center p-1 rounded-md shadow-lg bg-linear-to-b from-customStart to-customEnd transform transition-transform duration-300 hover:scale-105 m-1"> <li className="z-10 flex flex-col w-28 items-center p-1 rounded-md shadow-lg bg-linear-to-b from-customStart to-customEnd transform transition-transform duration-300 hover:scale-105 m-1">
<div <div
className={`w-20 rounded-md p-0.5 bg-linear-to-br ${data.rank === "CombatPowerAvatarRarityType5" className={`w-20 rounded-md p-0.5 bg-linear-to-br ${data.rank === "CombatPowerAvatarRarityType5"
? "from-yellow-400 via-yellow-300 to-yellow-500" ? "from-yellow-400 via-yellow-300 to-yellow-500"
: "from-purple-300 via-purple-200 to-purple-400" : "from-purple-300 via-purple-200 to-purple-400"
}`} }`}
> >
<div className="relative w-full h-full"> <div className="relative w-full h-full">
<Image <Image
width={376} width={376}
height={512} height={512}
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${data.icon}`} src={`${process.env.CDN_URL}/${data.icon}`}
className="w-full h-full rounded-md object-cover" className="w-full h-full rounded-md object-cover"
alt="ALT" alt="ALT"
/> />
<Image <Image
width={48} width={48}
height={48} height={48}
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
src={`/icon/${data?.damageType?.toLowerCase()}.webp`} src={`/icon/${data?.damageType?.toLowerCase()}.webp`}
className="absolute top-0 left-0 w-6 h-6" className="absolute top-0 left-0 w-6 h-6"
alt={data?.damageType?.toLowerCase()} alt={data?.damageType?.toLowerCase()}
/> />
<Image <Image
width={48} width={48}
height={48} height={48}
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
src={`/icon/${data.baseType.toLowerCase()}.webp`} src={`/icon/${data.baseType.toLowerCase()}.webp`}
className="absolute top-0 right-0 w-6 h-6" className="absolute top-0 right-0 w-6 h-6"
alt={data.baseType.toLowerCase()} alt={data.baseType.toLowerCase()}
/> />
</div> </div>
</div> </div>
<NameAvatar <NameAvatar
locale={locale} locale={locale}
text={text} text={text}
className="mt-2 text-center text-base font-normal leading-tight" className="mt-2 text-center text-base font-normal leading-tight"
/> />
{avatarDetail && ( {avatarDetail && (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-xs text-base-content/70 mx-1">HP:</span> <span className="text-xs text-base-content/70 mx-1">HP:</span>
<span className="text-xs font-medium"> <span className="text-xs font-medium">
<span className="text-error"> <span className="text-error">
{Number(avatarDetail?.[Number(data.id)]?.stats?.HP ?? 0).toLocaleString(undefined, { maximumFractionDigits: 0 })} {Number(avatarDetail?.[Number(data.id)]?.stats?.HP ?? 0).toLocaleString(undefined, { maximumFractionDigits: 0 })}
</span> </span>
<span className="text-base-content/50">/</span> <span className="text-base-content/50">/</span>
<span className="text-base-content/70"> <span className="text-base-content/70">
{Number(avatarDetail?.[Number(data.id)]?.stats?.MaxHP ?? 100).toLocaleString(undefined, { maximumFractionDigits: 0 })} {Number(avatarDetail?.[Number(data.id)]?.stats?.MaxHP ?? 100).toLocaleString(undefined, { maximumFractionDigits: 0 })}
</span> </span>
</span> </span>
</div> </div>
<div className="relative w-full bg-base-300 rounded-full h-2.5"> <div className="relative w-full bg-base-300 rounded-full h-2.5">
<div <div
className="bg-error h-2.5 rounded-full transition-all duration-300" className="bg-error h-2.5 rounded-full transition-all duration-300"
style={{ style={{
width: `${Math.max(0, Math.min(100, ((avatarDetail?.[Number(data.id)]?.stats?.HP || 0) / (avatarDetail?.[Number(data.id)]?.stats?.MaxHP || 100)) * 100))}%` width: `${Math.max(0, Math.min(100, ((avatarDetail?.[Number(data.id)]?.stats?.HP || 0) / (avatarDetail?.[Number(data.id)]?.stats?.MaxHP || 100)) * 100))}%`
}} }}
/> />
<span className="absolute inset-0 flex items-center justify-center text-xs text-white font-medium"> <span className="absolute inset-0 flex items-center justify-center text-xs text-white font-medium">
{Math.round(((avatarDetail?.[Number(data.id)]?.stats?.HP || 0) / (avatarDetail?.[Number(data.id)]?.stats?.MaxHP || 100)) * 100)}% {Math.round(((avatarDetail?.[Number(data.id)]?.stats?.HP || 0) / (avatarDetail?.[Number(data.id)]?.stats?.MaxHP || 100)) * 100)}%
</span> </span>
</div> </div>
</div> </div>
)} )}
</li> </li>
); );
} }
+84 -84
View File
@@ -1,84 +1,84 @@
'use client'; 'use client';
import { import {
Chart as ChartJS, Chart as ChartJS,
LineElement, LineElement,
CategoryScale, CategoryScale,
LinearScale, LinearScale,
PointElement, PointElement,
Tooltip, Tooltip,
Legend, Legend,
} from 'chart.js'; } from 'chart.js';
import { Line } from 'react-chartjs-2'; import { Line } from 'react-chartjs-2';
import { useDamageLinesForAll } from '@/hooks/useDamageLinesForAll'; import { useDamageLinesForAll } from '@/hooks/useDamageLinesForAll';
import useAvatarDataStore from '@/stores/avatarDataStore'; import useAvatarDataStore from '@/stores/avatarDataStore';
import useLocaleStore from '@/stores/localeStore'; import useLocaleStore from '@/stores/localeStore';
import { getNameChar } from '@/helper'; import { getNameChar } from '@/helper';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
ChartJS.register(LineElement, CategoryScale, LinearScale, PointElement, Tooltip, Legend); ChartJS.register(LineElement, CategoryScale, LinearScale, PointElement, Tooltip, Legend);
const colors = ['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c']; const colors = ['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c'];
export default function MultiCharLineChart() { export default function MultiCharLineChart() {
const [mode, setMode] = useState<1 | 2>(2); const [mode, setMode] = useState<1 | 2>(2);
const dataByAvatar = useDamageLinesForAll(mode); const dataByAvatar = useDamageLinesForAll(mode);
const avatarIds = Object.keys(dataByAvatar).map(Number); const avatarIds = Object.keys(dataByAvatar).map(Number);
const { mapAvatar } = useAvatarDataStore() const { mapAvatar } = useAvatarDataStore()
const { locale } = useLocaleStore(); const { locale } = useLocaleStore();
const transI18n = useTranslations("DataAnalysisPage") const transI18n = useTranslations("DataAnalysisPage")
const data = { const data = {
datasets: avatarIds.map((id, idx) => ({ datasets: avatarIds.map((id, idx) => ({
label: getNameChar(locale, transI18n, mapAvatar?.[id.toString()]), label: getNameChar(locale, transI18n, mapAvatar?.[id.toString()]),
data: dataByAvatar[id].map(({ x, y }: { x: number; y: number }) => { data: dataByAvatar[id].map(({ x, y }: { x: number; y: number }) => {
return { return {
x: x.toFixed(2), x: x.toFixed(2),
y: y.toFixed(2) y: y.toFixed(2)
} }
}), }),
borderColor: colors[idx % colors.length], borderColor: colors[idx % colors.length],
backgroundColor: colors[idx % colors.length], backgroundColor: colors[idx % colors.length],
fill: false, fill: false,
tension: 0.3, tension: 0.3,
})), })),
}; };
const options = { const options = {
responsive: true, responsive: true,
scales: { scales: {
x: { x: {
type: 'linear' as const, type: 'linear' as const,
position: 'bottom' as "bottom" | "center" | "left" | "top" | "right", position: 'bottom' as "bottom" | "center" | "left" | "top" | "right",
ticks: { ticks: {
stepSize: 1, stepSize: 1,
}, },
}, },
y: { y: {
beginAtZero: true, beginAtZero: true,
}, },
}, },
}; };
return ( return (
<div className="w-full"> <div className="w-full">
<div className="mb-4 flex items-start gap-2 justify-end"> <div className="mb-4 flex items-start gap-2 justify-end">
{[ {[
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" }, { mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" }, { mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
].map(({ mode: m, label, className }) => ( ].map(({ mode: m, label, className }) => (
<button <button
key={m} key={m}
onClick={() => setMode(m as 1 | 2)} onClick={() => setMode(m as 1 | 2)}
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`} className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
> >
{label} {label}
</button> </button>
))} ))}
</div> </div>
<Line data={data} options={options} /> <Line data={data} options={options} />
</div> </div>
) )
} }
+80 -80
View File
@@ -1,80 +1,80 @@
"use client"; "use client";
import { Line } from "react-chartjs-2"; import { Line } from "react-chartjs-2";
import { ChartOptions } from "chart.js"; import { ChartOptions } from "chart.js";
import { useDamageLineForOne } from "@/hooks/useDamageLineForOne"; import { useDamageLineForOne } from "@/hooks/useDamageLineForOne";
import { import {
Chart as ChartJS, Chart as ChartJS,
LineElement, LineElement,
PointElement, PointElement,
CategoryScale, CategoryScale,
LinearScale, LinearScale,
Title, Title,
Tooltip, Tooltip,
Legend, Legend,
Filler, Filler,
} from "chart.js"; } from "chart.js";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useState } from "react"; import { useState } from "react";
ChartJS.register( ChartJS.register(
LineElement, LineElement,
PointElement, PointElement,
CategoryScale, CategoryScale,
LinearScale, LinearScale,
Title, Title,
Tooltip, Tooltip,
Legend, Legend,
Filler Filler
); );
export function DamageLineForOne({ avatarId }: { avatarId: number; }) { export function DamageLineForOne({ avatarId }: { avatarId: number; }) {
const [mode, setMode] = useState<0 | 1>(1); const [mode, setMode] = useState<0 | 1>(1);
const dataRaw = useDamageLineForOne(avatarId, mode); const dataRaw = useDamageLineForOne(avatarId, mode);
const transI18n = useTranslations("DataAnalysisPage"); const transI18n = useTranslations("DataAnalysisPage");
const data = { const data = {
labels: dataRaw.map(d => Number(d.x.toFixed(2))), labels: dataRaw.map(d => Number(d.x.toFixed(2))),
datasets: [ datasets: [
{ {
label: mode === 1 ? transI18n("cumulativeDamage") : transI18n("damageByActionValue"), label: mode === 1 ? transI18n("cumulativeDamage") : transI18n("damageByActionValue"),
data: dataRaw.map(d => Number(d.y.toFixed(2))), data: dataRaw.map(d => Number(d.y.toFixed(2))),
borderColor: "rgba(75,192,192,1)", borderColor: "rgba(75,192,192,1)",
backgroundColor: "rgba(75,192,192,0.2)", backgroundColor: "rgba(75,192,192,0.2)",
tension: 0.3, tension: 0.3,
fill: true, fill: true,
}, },
], ],
}; };
const options: ChartOptions<"line"> = { const options: ChartOptions<"line"> = {
responsive: true, responsive: true,
plugins: { plugins: {
legend: { display: true }, legend: { display: true },
}, },
scales: { scales: {
x: { title: { display: true, text: transI18n("actionValue") } }, x: { title: { display: true, text: transI18n("actionValue") } },
y: { title: { display: true, text: transI18n("damage") }, beginAtZero: true }, y: { title: { display: true, text: transI18n("damage") }, beginAtZero: true },
}, },
}; };
return ( return (
<div className="w-full"> <div className="w-full">
<div className="mb-4 flex items-start gap-2 justify-end"> <div className="mb-4 flex items-start gap-2 justify-end">
{[ {[
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" }, { mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" }, { mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
].map(({ mode: m, label, className }) => ( ].map(({ mode: m, label, className }) => (
<button <button
key={m} key={m}
onClick={() => setMode(m as 0 | 1)} onClick={() => setMode(m as 0 | 1)}
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`} className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
> >
{label} {label}
</button> </button>
))} ))}
</div> </div>
<Line data={data} options={options} /> <Line data={data} options={options} />
</div> </div>
) )
} }
+177 -177
View File
@@ -1,177 +1,177 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { getNameChar } from '@/helper'; import { getNameChar } from '@/helper';
import useAvatarDataStore from '@/stores/avatarDataStore'; import useAvatarDataStore from '@/stores/avatarDataStore';
import useBattleDataStore from '@/stores/battleDataStore'; import useBattleDataStore from '@/stores/battleDataStore';
import useLocaleStore from '@/stores/localeStore'; import useLocaleStore from '@/stores/localeStore';
import { import {
Chart as ChartJS, Chart as ChartJS,
BarElement, BarElement,
CategoryScale, CategoryScale,
LinearScale, LinearScale,
Tooltip, Tooltip,
Legend Legend
} from 'chart.js'; } from 'chart.js';
import { Bar } from 'react-chartjs-2'; import { Bar } from 'react-chartjs-2';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import {attackTypeToString} from "@/types"; import {attackTypeToString} from "@/types";
ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Legend); ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Legend);
const colorPalette = [ const colorPalette = [
'rgba(255, 99, 132, 0.6)', 'rgba(255, 99, 132, 0.6)',
'rgba(54, 162, 235, 0.6)', 'rgba(54, 162, 235, 0.6)',
'rgba(255, 206, 86, 0.6)', 'rgba(255, 206, 86, 0.6)',
'rgba(75, 192, 192, 0.6)', 'rgba(75, 192, 192, 0.6)',
'rgba(153, 102, 255, 0.6)', 'rgba(153, 102, 255, 0.6)',
'rgba(255, 159, 64, 0.6)', 'rgba(255, 159, 64, 0.6)',
'rgba(199, 199, 199, 0.6)', 'rgba(199, 199, 199, 0.6)',
'rgba(255, 99, 255, 0.6)', 'rgba(255, 99, 255, 0.6)',
'rgba(0, 200, 83, 0.6)', 'rgba(0, 200, 83, 0.6)',
'rgba(255, 112, 67, 0.6)', 'rgba(255, 112, 67, 0.6)',
'rgba(63, 81, 181, 0.6)', 'rgba(63, 81, 181, 0.6)',
'rgba(0, 188, 212, 0.6)', 'rgba(0, 188, 212, 0.6)',
'rgba(233, 30, 99, 0.6)', 'rgba(233, 30, 99, 0.6)',
'rgba(124, 179, 66, 0.6)', 'rgba(124, 179, 66, 0.6)',
'rgba(255, 235, 59, 0.6)', 'rgba(255, 235, 59, 0.6)',
'rgba(158, 158, 158, 0.6)', 'rgba(158, 158, 158, 0.6)',
'rgba(121, 85, 72, 0.6)', 'rgba(121, 85, 72, 0.6)',
'rgba(96, 125, 139, 0.6)', 'rgba(96, 125, 139, 0.6)',
'rgba(100, 181, 246, 0.6)', 'rgba(100, 181, 246, 0.6)',
'rgba(255, 171, 145, 0.6)', 'rgba(255, 171, 145, 0.6)',
'rgba(174, 213, 129, 0.6)', 'rgba(174, 213, 129, 0.6)',
'rgba(255, 245, 157, 0.6)' 'rgba(255, 245, 157, 0.6)'
]; ];
const borderPalette = colorPalette.map((c) => c.replace('0.6', '1')); const borderPalette = colorPalette.map((c) => c.replace('0.6', '1'));
export default function DamagePerAvatarForAll() { export default function DamagePerAvatarForAll() {
const transI18n = useTranslations("DataAnalysisPage"); const transI18n = useTranslations("DataAnalysisPage");
const { lineup, skillHistory } = useBattleDataStore(); const { lineup, skillHistory } = useBattleDataStore();
const { mapAvatar } = useAvatarDataStore(); const { mapAvatar } = useAvatarDataStore();
const { locale } = useLocaleStore(); const { locale } = useLocaleStore();
const [mode, setMode] = useState<number>(2); const [mode, setMode] = useState<number>(2);
const avatarMap = lineup.map((avatar) => { const avatarMap = lineup.map((avatar) => {
if (!mapAvatar?.[avatar.avatarId.toString()]) return undefined; if (!mapAvatar?.[avatar.avatarId.toString()]) return undefined;
return { return {
avatarId: avatar.avatarId, avatarId: avatar.avatarId,
avatarName: getNameChar(locale, transI18n, mapAvatar?.[avatar.avatarId.toString()]) avatarName: getNameChar(locale, transI18n, mapAvatar?.[avatar.avatarId.toString()])
}; };
}).filter(Boolean) as { avatarId: number, avatarName: string }[]; }).filter(Boolean) as { avatarId: number, avatarName: string }[];
const labels = avatarMap.map(a => a.avatarName); const labels = avatarMap.map(a => a.avatarName);
let datasets: Array<{ let datasets: Array<{
label: string; label: string;
data: number[]; data: number[];
backgroundColor: string | string[]; backgroundColor: string | string[];
borderColor: string | string[]; borderColor: string | string[];
borderWidth: number; borderWidth: number;
stack?: string; stack?: string;
}> = []; }> = [];
if (mode === 1) { if (mode === 1) {
datasets = [ datasets = [
{ {
label: transI18n("totalDamage"), label: transI18n("totalDamage"),
data: avatarMap.map(({ avatarId }) => { data: avatarMap.map(({ avatarId }) => {
const total = skillHistory const total = skillHistory
.filter(skill => skill.avatarId === avatarId) .filter(skill => skill.avatarId === avatarId)
.reduce((sum, s) => sum + s.totalDamage, 0); .reduce((sum, s) => sum + s.totalDamage, 0);
return total; return total;
}), }),
backgroundColor: avatarMap.map((_, i) => colorPalette[i % colorPalette.length]), backgroundColor: avatarMap.map((_, i) => colorPalette[i % colorPalette.length]),
borderColor: avatarMap.map((_, i) => borderPalette[i % borderPalette.length]), borderColor: avatarMap.map((_, i) => borderPalette[i % borderPalette.length]),
borderWidth: 1 borderWidth: 1
} }
]; ];
} }
if (mode === 2) { if (mode === 2) {
const damageTypesSet = new Set<string>(); const damageTypesSet = new Set<string>();
avatarMap.forEach(({ avatarId }) => { avatarMap.forEach(({ avatarId }) => {
skillHistory.filter(s => s.avatarId === avatarId).forEach(s => skillHistory.filter(s => s.avatarId === avatarId).forEach(s =>
s.damageDetail?.forEach(d => d.damage_type && damageTypesSet.add(transI18n(attackTypeToString(d?.damage_type).toLowerCase()))) s.damageDetail?.forEach(d => d.damage_type && damageTypesSet.add(transI18n(attackTypeToString(d?.damage_type).toLowerCase())))
); );
}); });
const damageTypes = Array.from(damageTypesSet); const damageTypes = Array.from(damageTypesSet);
datasets = damageTypes.map((dt, i) => ({ datasets = damageTypes.map((dt, i) => ({
label: dt, label: dt,
data: avatarMap.map(({ avatarId }) => { data: avatarMap.map(({ avatarId }) => {
const total = skillHistory const total = skillHistory
.filter(skill => skill.avatarId === avatarId) .filter(skill => skill.avatarId === avatarId)
.flatMap(s => s.damageDetail || []) .flatMap(s => s.damageDetail || [])
.filter(d => transI18n(attackTypeToString(d?.damage_type).toLowerCase()) === dt) .filter(d => transI18n(attackTypeToString(d?.damage_type).toLowerCase()) === dt)
.reduce((sum, d) => sum + d.damage, 0); .reduce((sum, d) => sum + d.damage, 0);
return total; return total;
}), }),
backgroundColor: colorPalette[i % colorPalette.length], backgroundColor: colorPalette[i % colorPalette.length],
borderColor: borderPalette[i % borderPalette.length], borderColor: borderPalette[i % borderPalette.length],
borderWidth: 1, borderWidth: 1,
stack: transI18n('damage') stack: transI18n('damage')
})); }));
} }
if (mode === 3) { if (mode === 3) {
const skillTypesSet = new Set<string>(); const skillTypesSet = new Set<string>();
skillHistory.forEach(s => skillTypesSet.add(transI18n(attackTypeToString(s?.skillType)?.toLowerCase()))); skillHistory.forEach(s => skillTypesSet.add(transI18n(attackTypeToString(s?.skillType)?.toLowerCase())));
const skillTypes = Array.from(skillTypesSet); const skillTypes = Array.from(skillTypesSet);
datasets = skillTypes.map((st, i) => ({ datasets = skillTypes.map((st, i) => ({
label: st, label: st,
data: avatarMap.map(({ avatarId }) => { data: avatarMap.map(({ avatarId }) => {
const total = skillHistory const total = skillHistory
.filter(skill => skill.avatarId === avatarId && transI18n(attackTypeToString(skill?.skillType).toLowerCase()) === st) .filter(skill => skill.avatarId === avatarId && transI18n(attackTypeToString(skill?.skillType).toLowerCase()) === st)
.reduce((sum, s) => sum + s.totalDamage, 0); .reduce((sum, s) => sum + s.totalDamage, 0);
return total; return total;
}), }),
backgroundColor: colorPalette[i % colorPalette.length], backgroundColor: colorPalette[i % colorPalette.length],
borderColor: borderPalette[i % borderPalette.length], borderColor: borderPalette[i % borderPalette.length],
borderWidth: 1, borderWidth: 1,
stack: transI18n('skill') stack: transI18n('skill')
})); }));
} }
const options = { const options = {
responsive: true, responsive: true,
scales: { scales: {
y: { y: {
beginAtZero: true, beginAtZero: true,
stacked: mode !== 1, stacked: mode !== 1,
ticks: { ticks: {
precision: 0, precision: 0,
}, },
}, },
x: { x: {
stacked: mode !== 1, stacked: mode !== 1,
}, },
}, },
}; };
return ( return (
<div className="w-full"> <div className="w-full">
<div className="mb-4 flex items-start gap-2 justify-end"> <div className="mb-4 flex items-start gap-2 justify-end">
{[ {[
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" }, { mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" }, { mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
{ mode: 3, label: `${transI18n("type")} 3`, className: "btn-accent" }, { mode: 3, label: `${transI18n("type")} 3`, className: "btn-accent" },
].map(({ mode: m, label, className }) => ( ].map(({ mode: m, label, className }) => (
<button <button
key={m} key={m}
onClick={() => setMode(m)} onClick={() => setMode(m)}
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`} className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
> >
{label} {label}
</button> </button>
))} ))}
</div> </div>
<Bar data={{ labels, datasets }} options={options}/> <Bar data={{ labels, datasets }} options={options}/>
</div> </div>
); );
} }
+60 -60
View File
@@ -1,60 +1,60 @@
"use client"; "use client";
import { Bar } from "react-chartjs-2"; import { Bar } from "react-chartjs-2";
import { ChartOptions } from "chart.js"; import { ChartOptions } from "chart.js";
import { useDamagePerCycleForAll } from "@/hooks/useDamagePerCycle"; import { useDamagePerCycleForAll } from "@/hooks/useDamagePerCycle";
import { Chart as ChartJS, BarElement, ArcElement, RadialLinearScale } from "chart.js"; import { Chart as ChartJS, BarElement, ArcElement, RadialLinearScale } from "chart.js";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useState } from "react"; import { useState } from "react";
ChartJS.register(BarElement, ArcElement, RadialLinearScale); ChartJS.register(BarElement, ArcElement, RadialLinearScale);
export default function DamagePerCycleForAll() { export default function DamagePerCycleForAll() {
const [mode, setMode] = useState<0 | 1 | 2>(0); const [mode, setMode] = useState<0 | 1 | 2>(0);
const dataRaw = useDamagePerCycleForAll(mode); const dataRaw = useDamagePerCycleForAll(mode);
const transI18n = useTranslations("DataAnalysisPage"); const transI18n = useTranslations("DataAnalysisPage");
const data = { const data = {
labels: dataRaw.map(d => d.x), labels: dataRaw.map(d => d.x),
datasets: [ datasets: [
{ {
label: mode === 0 ? `${transI18n("damagePerCycleAndWave")}` : mode === 1 ? `${transI18n("damagePerCycle")}` : `${transI18n("damagePerWave")}`, label: mode === 0 ? `${transI18n("damagePerCycleAndWave")}` : mode === 1 ? `${transI18n("damagePerCycle")}` : `${transI18n("damagePerWave")}`,
data: dataRaw.map(d => d.y), data: dataRaw.map(d => d.y),
backgroundColor: dataRaw.map((_, i) => backgroundColor: dataRaw.map((_, i) =>
['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c', '#f472b6'][i % 7] ['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c', '#f472b6'][i % 7]
), ),
}, },
], ],
}; };
const options: ChartOptions<"bar"> = { const options: ChartOptions<"bar"> = {
responsive: true, responsive: true,
plugins: { plugins: {
legend: { display: true }, legend: { display: true },
}, },
scales: { scales: {
x: { title: { display: true, text: transI18n("cycleCount") } }, x: { title: { display: true, text: transI18n("cycleCount") } },
y: { title: { display: true, text: transI18n("totalDamage") }, beginAtZero: true }, y: { title: { display: true, text: transI18n("totalDamage") }, beginAtZero: true },
}, },
}; };
return ( return (
<div className="w-full"> <div className="w-full">
<div className="mb-4 flex items-start gap-2 justify-end"> <div className="mb-4 flex items-start gap-2 justify-end">
{[ {[
{ mode: 0, label: `${transI18n("type")} 1`, className: "btn-primary" }, { mode: 0, label: `${transI18n("type")} 1`, className: "btn-primary" },
{ mode: 1, label: `${transI18n("type")} 2`, className: "btn-warning" }, { mode: 1, label: `${transI18n("type")} 2`, className: "btn-warning" },
{ mode: 2, label: `${transI18n("type")} 3`, className: "btn-accent" }, { mode: 2, label: `${transI18n("type")} 3`, className: "btn-accent" },
].map(({ mode: m, label, className }) => ( ].map(({ mode: m, label, className }) => (
<button <button
key={m} key={m}
onClick={() => setMode(m as 0 | 1 | 2)} onClick={() => setMode(m as 0 | 1 | 2)}
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`} className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
> >
{label} {label}
</button> </button>
))} ))}
</div> </div>
<Bar data={data} options={options} /> <Bar data={data} options={options} />
</div> </div>
) )
} }
+64 -64
View File
@@ -1,64 +1,64 @@
"use client"; "use client";
import { Bar } from "react-chartjs-2"; import { Bar } from "react-chartjs-2";
import { ChartOptions } from "chart.js"; import { ChartOptions } from "chart.js";
import { useDamagePerCycleForOne } from "@/hooks/useDamagePerCycle"; import { useDamagePerCycleForOne } from "@/hooks/useDamagePerCycle";
import { Chart as ChartJS, BarElement, ArcElement, RadialLinearScale } from "chart.js"; import { Chart as ChartJS, BarElement, ArcElement, RadialLinearScale } from "chart.js";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useState } from "react"; import { useState } from "react";
ChartJS.register(BarElement, ArcElement, RadialLinearScale); ChartJS.register(BarElement, ArcElement, RadialLinearScale);
export function DamagePerCycleForOne({ export function DamagePerCycleForOne({
avatarId, avatarId,
}: { }: {
avatarId: number; avatarId: number;
}) { }) {
const [mode, setMode] = useState<0 | 1 | 2>(0); const [mode, setMode] = useState<0 | 1 | 2>(0);
const dataRaw = useDamagePerCycleForOne(avatarId, mode); const dataRaw = useDamagePerCycleForOne(avatarId, mode);
const transI18n = useTranslations("DataAnalysisPage"); const transI18n = useTranslations("DataAnalysisPage");
const data = { const data = {
labels: dataRaw.map(d => d.x), labels: dataRaw.map(d => d.x),
datasets: [ datasets: [
{ {
label: mode === 0 ? `${transI18n("damagePerCycleAndWave")}` : mode === 1 ? `${transI18n("damagePerCycle")}` : `${transI18n("damagePerWave")}`, label: mode === 0 ? `${transI18n("damagePerCycleAndWave")}` : mode === 1 ? `${transI18n("damagePerCycle")}` : `${transI18n("damagePerWave")}`,
data: dataRaw.map(d => d.y), data: dataRaw.map(d => d.y),
backgroundColor: dataRaw.map((_, i) => backgroundColor: dataRaw.map((_, i) =>
['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c', '#f472b6'][i % 7] ['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c', '#f472b6'][i % 7]
), ),
}, },
], ],
}; };
const options: ChartOptions<"bar"> = { const options: ChartOptions<"bar"> = {
responsive: true, responsive: true,
plugins: { plugins: {
legend: { display: true }, legend: { display: true },
}, },
scales: { scales: {
x: { title: { display: true, text: transI18n("cycleCount") } }, x: { title: { display: true, text: transI18n("cycleCount") } },
y: { title: { display: true, text: transI18n("totalDamage") }, beginAtZero: true }, y: { title: { display: true, text: transI18n("totalDamage") }, beginAtZero: true },
}, },
}; };
return ( return (
<div className="w-full"> <div className="w-full">
<div className="mb-4 flex items-start gap-2 justify-end"> <div className="mb-4 flex items-start gap-2 justify-end">
{[ {[
{ mode: 0, label: `${transI18n("type")} 1`, className: "btn-primary" }, { mode: 0, label: `${transI18n("type")} 1`, className: "btn-primary" },
{ mode: 1, label: `${transI18n("type")} 2`, className: "btn-warning" }, { mode: 1, label: `${transI18n("type")} 2`, className: "btn-warning" },
{ mode: 2, label: `${transI18n("type")} 3`, className: "btn-warning" }, { mode: 2, label: `${transI18n("type")} 3`, className: "btn-warning" },
].map(({ mode: m, label, className }) => ( ].map(({ mode: m, label, className }) => (
<button <button
key={m} key={m}
onClick={() => setMode(m as 0 | 1 | 2)} onClick={() => setMode(m as 0 | 1 | 2)}
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`} className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
> >
{label} {label}
</button> </button>
))} ))}
</div> </div>
<Bar data={data} options={options} /> <Bar data={data} options={options} />
</div> </div>
) )
} }
+91 -91
View File
@@ -1,91 +1,91 @@
'use client'; 'use client';
import { import {
Chart as ChartJS, Chart as ChartJS,
ArcElement, ArcElement,
Tooltip, Tooltip,
Legend Legend
} from 'chart.js'; } from 'chart.js';
import { Pie } from 'react-chartjs-2'; import { Pie } from 'react-chartjs-2';
import { useDamagePercentPerAvatar } from '@/hooks/useDamagePercentPerAvatar'; import { useDamagePercentPerAvatar } from '@/hooks/useDamagePercentPerAvatar';
import useAvatarDataStore from '@/stores/avatarDataStore'; import useAvatarDataStore from '@/stores/avatarDataStore';
import useLocaleStore from '@/stores/localeStore'; import useLocaleStore from '@/stores/localeStore';
import { getNameChar } from '@/helper'; import { getNameChar } from '@/helper';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useState } from 'react'; import { useState } from 'react';
import { useDamagePercentByType } from '@/hooks/useDamagePercentForDamgeType'; import { useDamagePercentByType } from '@/hooks/useDamagePercentForDamgeType';
ChartJS.register(ArcElement, Tooltip, Legend); ChartJS.register(ArcElement, Tooltip, Legend);
const colors = [ const colors = [
'#f87171', '#34d399', '#60a5fa', '#facc15', '#f87171', '#34d399', '#60a5fa', '#facc15',
'#a78bfa', '#fb923c', '#f472b6', '#10b981', '#a78bfa', '#fb923c', '#f472b6', '#10b981',
'#fcd34d', '#c084fc', '#f97316', '#ec4899', '#fcd34d', '#c084fc', '#f97316', '#ec4899',
'#4ade80', '#fbbf24', '#8b5cf6', '#f43f5e' '#4ade80', '#fbbf24', '#8b5cf6', '#f43f5e'
]; ];
export default function DamagePercentChartForAll() { export default function DamagePercentChartForAll() {
const [mode, setMode] = useState<1 | 2>(1); const [mode, setMode] = useState<1 | 2>(1);
const damageByAvatar = useDamagePercentPerAvatar(); const damageByAvatar = useDamagePercentPerAvatar();
const damageByType = useDamagePercentByType(); const damageByType = useDamagePercentByType();
const { mapAvatar } = useAvatarDataStore(); const { mapAvatar } = useAvatarDataStore();
const { locale } = useLocaleStore(); const { locale } = useLocaleStore();
const transI18n = useTranslations("DataAnalysisPage"); const transI18n = useTranslations("DataAnalysisPage");
const chartData = { const chartData = {
labels: (mode === 1 labels: (mode === 1
? damageByAvatar.map(d => ? damageByAvatar.map(d =>
getNameChar(locale, transI18n, mapAvatar?.[d.avatarId.toString()]) getNameChar(locale, transI18n, mapAvatar?.[d.avatarId.toString()])
) )
: damageByType.map(d => transI18n(d.type.toLowerCase())) : damageByType.map(d => transI18n(d.type.toLowerCase()))
), ),
datasets: [ datasets: [
{ {
label: '% ' + transI18n("damage"), label: '% ' + transI18n("damage"),
data: (mode === 1 data: (mode === 1
? damageByAvatar.map(d => d.percent.toFixed(2)) ? damageByAvatar.map(d => d.percent.toFixed(2))
: damageByType.map(d => d.percent.toFixed(2)) : damageByType.map(d => d.percent.toFixed(2))
), ),
backgroundColor: colors, backgroundColor: colors,
borderWidth: 1, borderWidth: 1,
}, },
], ],
}; };
const options = { const options = {
responsive: true, responsive: true,
plugins: { plugins: {
tooltip: { tooltip: {
callbacks: { callbacks: {
label: (ctx: import('chart.js').TooltipItem<'pie'>) => `${ctx.label}: ${ctx.raw}%`, label: (ctx: import('chart.js').TooltipItem<'pie'>) => `${ctx.label}: ${ctx.raw}%`,
}, },
}, },
datalabels: { datalabels: {
display: false, display: false,
}, },
}, },
}; };
return ( return (
<div className='w-full'> <div className='w-full'>
<div className="mb-4 flex items-start gap-2 justify-end"> <div className="mb-4 flex items-start gap-2 justify-end">
{[ {[
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" }, { mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" }, { mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
].map(({ mode: m, label, className }) => ( ].map(({ mode: m, label, className }) => (
<button <button
key={m} key={m}
onClick={() => setMode(m as 1 | 2)} onClick={() => setMode(m as 1 | 2)}
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`} className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
> >
{label} {label}
</button> </button>
))} ))}
</div> </div>
<div className="flex justify-center"> <div className="flex justify-center">
<Pie data={chartData} options={options} /> <Pie data={chartData} options={options} />
</div> </div>
</div> </div>
); );
} }
+70 -70
View File
@@ -1,70 +1,70 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { Bar } from 'react-chartjs-2'; import { Bar } from 'react-chartjs-2';
import { Chart as ChartJS, BarElement, CategoryScale, LinearScale, Tooltip, Legend } from 'chart.js'; import { Chart as ChartJS, BarElement, CategoryScale, LinearScale, Tooltip, Legend } from 'chart.js';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useSkillDamageForAvatar } from '@/hooks/useSkillDamageForAvatar'; import { useSkillDamageForAvatar } from '@/hooks/useSkillDamageForAvatar';
import { useDamageByTypeForAvatar } from '@/hooks/useDamageTypeForAvatar'; import { useDamageByTypeForAvatar } from '@/hooks/useDamageTypeForAvatar';
ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Legend); ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Legend);
export default function SkillBarChart({ avatarId }: { avatarId: number }) { export default function SkillBarChart({ avatarId }: { avatarId: number }) {
const [mode, setMode] = useState<1 | 2>(2); const [mode, setMode] = useState<1 | 2>(2);
const transI18n = useTranslations('DataAnalysisPage'); const transI18n = useTranslations('DataAnalysisPage');
const { labels: skillLabels, damageValues: skillDamageValues } = useSkillDamageForAvatar(avatarId); const { labels: skillLabels, damageValues: skillDamageValues } = useSkillDamageForAvatar(avatarId);
const { labels: typeLabels, damageValues: typeDamageValues } = useDamageByTypeForAvatar(avatarId); const { labels: typeLabels, damageValues: typeDamageValues } = useDamageByTypeForAvatar(avatarId);
const colors = ['#f87171', '#facc15', '#34d399', '#60a5fa', '#a78bfa', '#fb923c']; const colors = ['#f87171', '#facc15', '#34d399', '#60a5fa', '#a78bfa', '#fb923c'];
const data = { const data = {
labels: mode === 1 ? skillLabels.map(it => transI18n(it)) : typeLabels.map(it => transI18n(it)), labels: mode === 1 ? skillLabels.map(it => transI18n(it)) : typeLabels.map(it => transI18n(it)),
datasets: [ datasets: [
{ {
label: mode === 1 label: mode === 1
? transI18n('skill') ? transI18n('skill')
: transI18n('damage'), : transI18n('damage'),
data: mode === 1 ? skillDamageValues : typeDamageValues, data: mode === 1 ? skillDamageValues : typeDamageValues,
backgroundColor: colors.slice(0, (mode === 1 ? skillLabels : typeLabels).length), backgroundColor: colors.slice(0, (mode === 1 ? skillLabels : typeLabels).length),
borderColor: '#111', borderColor: '#111',
borderWidth: 1, borderWidth: 1,
}, },
], ],
}; };
const options = { const options = {
responsive: true, responsive: true,
scales: { scales: {
y: { y: {
beginAtZero: true, beginAtZero: true,
ticks: { precision: 0 }, ticks: { precision: 0 },
}, },
}, },
plugins: { plugins: {
legend: { legend: {
display: false, display: false,
}, },
}, },
}; };
return ( return (
<div className="w-full"> <div className="w-full">
<div className="mb-4 flex items-start gap-2 justify-end"> <div className="mb-4 flex items-start gap-2 justify-end">
{[ {[
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" }, { mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" }, { mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
].map(({ mode: m, label, className }) => ( ].map(({ mode: m, label, className }) => (
<button <button
key={m} key={m}
onClick={() => setMode(m as 1 | 2)} onClick={() => setMode(m as 1 | 2)}
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`} className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
> >
{label} {label}
</button> </button>
))} ))}
</div> </div>
<Bar data={data} options={options} /> <Bar data={data} options={options} />
</div> </div>
); );
} }
+75 -75
View File
@@ -1,75 +1,75 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import { Pie } from 'react-chartjs-2'; import { Pie } from 'react-chartjs-2';
import { import {
Chart as ChartJS, Chart as ChartJS,
ArcElement, ArcElement,
Tooltip, Tooltip,
Legend, Legend,
} from 'chart.js'; } from 'chart.js';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useSkillDamageForAvatar } from '@/hooks/useSkillDamageForAvatar'; import { useSkillDamageForAvatar } from '@/hooks/useSkillDamageForAvatar';
import { useDamageByTypeForAvatar } from '@/hooks/useDamageTypeForAvatar'; import { useDamageByTypeForAvatar } from '@/hooks/useDamageTypeForAvatar';
ChartJS.register(ArcElement, Tooltip, Legend); ChartJS.register(ArcElement, Tooltip, Legend);
const COLORS = [ const COLORS = [
'#f87171', '#facc15', '#34d399', '#60a5fa', '#a78bfa', '#fb923c', '#f87171', '#facc15', '#34d399', '#60a5fa', '#a78bfa', '#fb923c',
'#f472b6', '#38bdf8', '#a3e635', '#fdba74', '#c084fc', '#4ade80', '#f472b6', '#38bdf8', '#a3e635', '#fdba74', '#c084fc', '#4ade80',
'#fcd34d', '#818cf8', '#f43f5e', '#14b8a6', '#e879f9', '#86efac', '#fcd34d', '#818cf8', '#f43f5e', '#14b8a6', '#e879f9', '#86efac',
]; ];
export default function SkillPieChart({ avatarId }: { avatarId: number }) { export default function SkillPieChart({ avatarId }: { avatarId: number }) {
const [mode, setMode] = useState<1 | 2>(2); const [mode, setMode] = useState<1 | 2>(2);
const transI18n = useTranslations("DataAnalysisPage"); const transI18n = useTranslations("DataAnalysisPage");
const skillData = useSkillDamageForAvatar(avatarId); const skillData = useSkillDamageForAvatar(avatarId);
const typeData = useDamageByTypeForAvatar(avatarId); const typeData = useDamageByTypeForAvatar(avatarId);
const { labels, damageValues } = mode === 1 ? skillData : typeData; const { labels, damageValues } = mode === 1 ? skillData : typeData;
const total = damageValues.reduce((sum, val) => sum + val, 0); const total = damageValues.reduce((sum, val) => sum + val, 0);
const labelsWithPercent = labels.map((label, index) => { const labelsWithPercent = labels.map((label, index) => {
const value = damageValues[index]; const value = damageValues[index];
const percent = total ? ((value / total) * 100).toFixed(1) : '0.0'; const percent = total ? ((value / total) * 100).toFixed(1) : '0.0';
return `${transI18n(label.toLowerCase())} (${percent}%)`; return `${transI18n(label.toLowerCase())} (${percent}%)`;
}); });
const data = { const data = {
labels: labelsWithPercent, labels: labelsWithPercent,
datasets: [ datasets: [
{ {
data: damageValues, data: damageValues,
backgroundColor: COLORS.slice(0, labels.length), backgroundColor: COLORS.slice(0, labels.length),
borderColor: '#fff', borderColor: '#fff',
borderWidth: 1, borderWidth: 1,
}, },
], ],
}; };
return ( return (
<div className="w-full "> <div className="w-full ">
<div className="mb-4 flex items-start gap-2 justify-end"> <div className="mb-4 flex items-start gap-2 justify-end">
{[ {[
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" }, { mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" }, { mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
].map(({ mode: m, label, className }) => ( ].map(({ mode: m, label, className }) => (
<button <button
key={m} key={m}
onClick={() => setMode(m as 1 | 2)} onClick={() => setMode(m as 1 | 2)}
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`} className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
> >
{label} {label}
</button> </button>
))} ))}
</div> </div>
<Pie <Pie
data={data} data={data}
options={{ options={{
responsive: true, responsive: true,
cutout: '30%', cutout: '30%',
}} }}
/> />
</div> </div>
); );
} }
+78 -78
View File
@@ -1,79 +1,79 @@
"use client" "use client"
import useBattleDataStore from "@/stores/battleDataStore"; import useBattleDataStore from "@/stores/battleDataStore";
import Image from "next/image"; import Image from "next/image";
import useAvatarDataStore from "@/stores/avatarDataStore"; import useAvatarDataStore from "@/stores/avatarDataStore";
import { getNameEnemy } from "@/helper/getNameChar"; import { getNameEnemy } from "@/helper/getNameChar";
import useLocaleStore from "@/stores/localeStore"; import useLocaleStore from "@/stores/localeStore";
import NameAvatar from "../nameAvatar"; import NameAvatar from "../nameAvatar";
export default function EnemyBar() { export default function EnemyBar() {
const { enemyDetail } = useBattleDataStore() const { enemyDetail } = useBattleDataStore()
const { mapEnemy } = useAvatarDataStore() const { mapEnemy } = useAvatarDataStore()
const { locale } = useLocaleStore() const { locale } = useLocaleStore()
return ( return (
<div className="p-3 w-full"> <div className="p-3 w-full">
<div className="flex gap-3 overflow-x-auto pb-2"> <div className="flex gap-3 overflow-x-auto pb-2">
{enemyDetail && Object.values(enemyDetail).filter((enemy) => ( {enemyDetail && Object.values(enemyDetail).filter((enemy) => (
enemy.stats.HP <= enemy.maxHP)).map((enemy, uid) => ( enemy.stats.HP <= enemy.maxHP)).map((enemy, uid) => (
<div key={uid} className="bg-base-200 rounded-lg p-3 border border-gray-700 w-52 shrink-0"> <div key={uid} className="bg-base-200 rounded-lg p-3 border border-gray-700 w-52 shrink-0">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{mapEnemy?.[enemy.id.toString()]?.icon && ( {mapEnemy?.[enemy.id.toString()]?.icon && (
<Image <Image
src={`${process.env.CDN_URL}/${mapEnemy?.[enemy.id.toString()]?.icon}`} src={`${process.env.CDN_URL}/${mapEnemy?.[enemy.id.toString()]?.icon}`}
alt={enemy.name} alt={enemy.name}
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
width={40} width={40}
height={40} height={40}
className="object-cover w-10 h-10 rounded-lg" className="object-cover w-10 h-10 rounded-lg"
/> />
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<NameAvatar <NameAvatar
text={getNameEnemy(locale, mapEnemy?.[enemy.id.toString()])} text={getNameEnemy(locale, mapEnemy?.[enemy.id.toString()])}
locale={locale} locale={locale}
className="text-base font-semibold leading-tight truncate overflow-hidden" className="text-base font-semibold leading-tight truncate overflow-hidden"
/> />
<p className="text-base-content/70 text-xs">Level {enemy.level || 1}</p> <p className="text-base-content/70 text-xs">Level {enemy.level || 1}</p>
</div> </div>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div className="text-xs text-base-content/70">HP:</div> <div className="text-xs text-base-content/70">HP:</div>
<div className="flex text-xs font-medium"> <div className="flex text-xs font-medium">
<div className="text-error"> <div className="text-error">
{Number(enemy?.stats?.HP ?? 0).toLocaleString(undefined, { maximumFractionDigits: 0 })} {Number(enemy?.stats?.HP ?? 0).toLocaleString(undefined, { maximumFractionDigits: 0 })}
</div> </div>
<div className="text-base-content/50 mx-1">/</div> <div className="text-base-content/50 mx-1">/</div>
<div className="text-base-content/70"> <div className="text-base-content/70">
{Number(enemy?.maxHP ?? 100).toLocaleString(undefined, { maximumFractionDigits: 0 })} {Number(enemy?.maxHP ?? 100).toLocaleString(undefined, { maximumFractionDigits: 0 })}
</div> </div>
</div> </div>
</div> </div>
<div className="relative w-full bg-base-300 rounded-full h-2.5"> <div className="relative w-full bg-base-300 rounded-full h-2.5">
<div <div
className="bg-error h-2.5 rounded-full transition-all duration-300" className="bg-error h-2.5 rounded-full transition-all duration-300"
style={{ style={{
width: `${Math.max(0, Math.min(100, ((enemy.stats?.HP || 0) / (enemy.maxHP || 100)) * 100))}%` width: `${Math.max(0, Math.min(100, ((enemy.stats?.HP || 0) / (enemy.maxHP || 100)) * 100))}%`
}} }}
/> />
<div className="absolute inset-0 flex items-center justify-center text-xs text-white font-medium"> <div className="absolute inset-0 flex items-center justify-center text-xs text-white font-medium">
{Math.round(((enemy.stats?.HP || 0) / (enemy.maxHP || 100)) * 100)}% {Math.round(((enemy.stats?.HP || 0) / (enemy.maxHP || 100)) * 100)}%
</div> </div>
</div> </div>
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div> </div>
) )
} }
+11 -11
View File
@@ -1,11 +1,11 @@
export default function Footer() { export default function Footer() {
return( return(
<footer className="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10"> <footer className="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10">
<aside> <aside>
<p>Copyright © {new Date().getFullYear()} - Kain (Powered by Nextjs & DaisyUi)</p> <p>Copyright © {new Date().getFullYear()} - Kain (Powered by Nextjs & DaisyUi)</p>
</aside> </aside>
</footer> </footer>
) )
} }
+498 -498
View File
@@ -1,499 +1,499 @@
"use client" "use client"
import { exportBattleData, importBattleData } from "@/helper"; import { exportBattleData, importBattleData } from "@/helper";
import { useChangeTheme } from "@/hooks/useChangeTheme"; import { useChangeTheme } from "@/hooks/useChangeTheme";
import { checkConnectTcpApi } from "@/lib/api"; import { checkConnectTcpApi } from "@/lib/api";
import { listCurrentLanguage } from "@/lib/constant"; import { listCurrentLanguage } from "@/lib/constant";
import { connectSocket, disconnectSocket, getSocket, isSocketConnected } from "@/lib/socket"; import { connectSocket, disconnectSocket, getSocket, isSocketConnected } from "@/lib/socket";
import useBattleDataStore from "@/stores/battleDataStore"; import useBattleDataStore from "@/stores/battleDataStore";
import useLocaleStore from "@/stores/localeStore"; import useLocaleStore from "@/stores/localeStore";
import useSocketStore from "@/stores/socketSettingStore"; import useSocketStore from "@/stores/socketSettingStore";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Image from "next/image"; import Image from "next/image";
const themes = [ const themes = [
{ label: "Winter" }, { label: "Winter" },
{ label: "Night" }, { label: "Night" },
{ label: "Cupcake" }, { label: "Cupcake" },
{ label: "Coffee" }, { label: "Coffee" },
]; ];
export default function Header() { export default function Header() {
const { changeTheme } = useChangeTheme() const { changeTheme } = useChangeTheme()
const { locale, setLocale } = useLocaleStore() const { locale, setLocale } = useLocaleStore()
const { loadBattleDataFromJSON, version } = useBattleDataStore() const { loadBattleDataFromJSON, version } = useBattleDataStore()
const router = useRouter() const router = useRouter()
const transI18n = useTranslations("DataAnalysisPage") const transI18n = useTranslations("DataAnalysisPage")
const { host, port, status, connectionType, setHost, setPort, setStatus, setConnectionType } = useSocketStore(); const { host, port, status, connectionType, setHost, setPort, setStatus, setConnectionType } = useSocketStore();
const [message, setMessage] = useState({ text: '', type: '' }); const [message, setMessage] = useState({ text: '', type: '' });
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
useEffect(() => { useEffect(() => {
const cookieLocale = document.cookie.split("; ") const cookieLocale = document.cookie.split("; ")
.find((row) => row.startsWith("MYNEXTAPP_LOCALE")) .find((row) => row.startsWith("MYNEXTAPP_LOCALE"))
?.split("=")[1]; ?.split("=")[1];
if (cookieLocale) { if (cookieLocale) {
if (!listCurrentLanguage.hasOwnProperty(cookieLocale)) { if (!listCurrentLanguage.hasOwnProperty(cookieLocale)) {
setLocale("en") setLocale("en")
} else { } else {
setLocale(cookieLocale) setLocale(cookieLocale)
} }
} else { } else {
let browserLocale = navigator.language.slice(0, 2); let browserLocale = navigator.language.slice(0, 2);
if (!listCurrentLanguage.hasOwnProperty(browserLocale)) { if (!listCurrentLanguage.hasOwnProperty(browserLocale)) {
browserLocale = "en" browserLocale = "en"
} }
setLocale(browserLocale); setLocale(browserLocale);
document.cookie = `MYNEXTAPP_LOCALE=${browserLocale};` document.cookie = `MYNEXTAPP_LOCALE=${browserLocale};`
router.refresh() router.refresh()
} }
}, [router, setLocale]) }, [router, setLocale])
const changeLocale = (newLocale: string) => { const changeLocale = (newLocale: string) => {
setLocale(newLocale) setLocale(newLocale)
document.cookie = `MYNEXTAPP_LOCALE=${newLocale};` document.cookie = `MYNEXTAPP_LOCALE=${newLocale};`
router.refresh() router.refresh()
} }
useEffect(() => { useEffect(() => {
const checkStatus = () => { const checkStatus = () => {
const connected = isSocketConnected(); const connected = isSocketConnected();
if (connected !== status) { if (connected !== status) {
setStatus(connected); setStatus(connected);
} }
}; };
checkStatus(); checkStatus();
const intervalId = setInterval(checkStatus, 5000); const intervalId = setInterval(checkStatus, 5000);
return () => clearInterval(intervalId); return () => clearInterval(intervalId);
}, [status, setStatus]); }, [status, setStatus]);
useEffect(() => { useEffect(() => {
const socket = getSocket(); const socket = getSocket();
if (!socket) return; if (!socket) return;
return () => { return () => {
disconnectSocket() disconnectSocket()
}; };
}, []); }, []);
const handleConnect = () => { const handleConnect = () => {
if (!host || !port) { if (!host || !port) {
setMessage({ text: 'IP and Port are required', type: 'error' }); setMessage({ text: 'IP and Port are required', type: 'error' });
return; return;
} }
connectSocket(); connectSocket();
setTimeout(() => { setTimeout(() => {
const connected = isSocketConnected(); const connected = isSocketConnected();
setStatus(connected); setStatus(connected);
if (connected) { if (connected) {
setMessage({ text: 'Socket connected successfully!', type: 'success' }); setMessage({ text: 'Socket connected successfully!', type: 'success' });
} else { } else {
setMessage({ text: 'Failed to connect. Please check IP and port.', type: 'error' }); setMessage({ text: 'Failed to connect. Please check IP and port.', type: 'error' });
} }
}, 2000); }, 2000);
}; };
const checkConnection = async () => { const checkConnection = async () => {
setMessage({ text: 'Checking connection...', type: 'info' }); setMessage({ text: 'Checking connection...', type: 'info' });
let isConnect = false let isConnect = false
try { try {
isConnect = await checkConnectTcpApi() isConnect = await checkConnectTcpApi()
} catch { } catch {
} }
if (!isConnect) { if (!isConnect) {
setMessage({ text: 'Connection bridge to TCP Server failed', type: 'error' }) setMessage({ text: 'Connection bridge to TCP Server failed', type: 'error' })
} else { } else {
setMessage({ text: 'Connection OK!', type: 'success' }); setMessage({ text: 'Connection OK!', type: 'success' });
} }
} }
const handleShow = (modalId: string) => { const handleShow = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null; const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) { if (modal) {
setIsModalOpen(true); setIsModalOpen(true);
modal.showModal(); modal.showModal();
} }
}; };
// Close modal handler // Close modal handler
const handleCloseModal = (modalId: string) => { const handleCloseModal = (modalId: string) => {
setIsModalOpen(false); setIsModalOpen(false);
const modal = document.getElementById(modalId) as HTMLDialogElement | null; const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) { if (modal) {
modal.close() modal.close()
} }
}; };
// Handle ESC key to close modal // Handle ESC key to close modal
useEffect(() => { useEffect(() => {
const handleEscKey = (event: KeyboardEvent) => { const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isModalOpen) { if (event.key === 'Escape' && isModalOpen) {
handleCloseModal("character_detail_modal"); handleCloseModal("character_detail_modal");
} }
}; };
window.addEventListener('keydown', handleEscKey); window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey); return () => window.removeEventListener('keydown', handleEscKey);
}, [isModalOpen]); }, [isModalOpen]);
return ( return (
<div className="navbar bg-base-100 shadow-md sticky top-0 z-50 px-3 py-1"> <div className="navbar bg-base-100 shadow-md sticky top-0 z-50 px-3 py-1">
<div className="navbar-start"> <div className="navbar-start">
{/* Mobile menu dropdown */} {/* Mobile menu dropdown */}
<div className="dropdown"> <div className="dropdown">
<div tabIndex={0} role="button" className="btn btn-ghost btn-sm lg:hidden hover:bg-base-200 transition-all duration-300"> <div tabIndex={0} role="button" className="btn btn-ghost btn-sm lg:hidden hover:bg-base-200 transition-all duration-300">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h8m-8 6h16" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h8m-8 6h16" />
</svg> </svg>
</div> </div>
<ul <ul
tabIndex={0} tabIndex={0}
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-10 mt-3 w-52 p-2 shadow-md border border-base-200" className="menu menu-sm dropdown-content bg-base-100 rounded-box z-10 mt-3 w-52 p-2 shadow-md border border-base-200"
> >
<li> <li>
<> <>
<input <input
type="file" type="file"
accept="application/json" accept="application/json"
id="battle-data-upload" id="battle-data-upload"
className="hidden" className="hidden"
onChange={(e) => { onChange={(e) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) { if (file) {
importBattleData(file, loadBattleDataFromJSON) importBattleData(file, loadBattleDataFromJSON)
.then(() => console.log('Data loaded')) .then(() => console.log('Data loaded'))
.catch(err => alert('Failed to load data: ' + err.message)); .catch(err => alert('Failed to load data: ' + err.message));
} }
}} }}
/> />
<button <button
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium" className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => document.getElementById('battle-data-upload')?.click()} onClick={() => document.getElementById('battle-data-upload')?.click()}
> >
{transI18n("loadData")} {transI18n("loadData")}
</button> </button>
</> </>
</li> </li>
<li> <li>
<button <button
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium" className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => exportBattleData()} onClick={() => exportBattleData()}
> >
{transI18n("exportData")} {transI18n("exportData")}
</button> </button>
</li> </li>
<li> <li>
<button <button
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium" className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => handleShow("socket_connection_modal")} onClick={() => handleShow("socket_connection_modal")}
> >
{transI18n("connectSetting")} {transI18n("connectSetting")}
</button> </button>
</li> </li>
</ul> </ul>
</div> </div>
{/* Logo */} {/* Logo */}
<a className="hidden sm:grid sm:grid-cols-1 items-start text-left gap-0 hover:scale-105 px-2"> <a className="hidden sm:grid sm:grid-cols-1 items-start text-left gap-0 hover:scale-105 px-2">
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<Image src="/ff-sranalysis.png" alt="Logo" width={50} height={50} /> <Image src="/ff-sranalysis.png" alt="Logo" width={50} height={50} />
<div className="flex flex-col justify-center items-start"> <div className="flex flex-col justify-center items-start">
<h1 className="text-xl font-bold"> <h1 className="text-xl font-bold">
<span className="text-emerald-500">Firefly Analy</span> <span className="text-emerald-500">Firefly Analy</span>
<span className="bg-clip-text text-transparent bg-linear-to-r from-emerald-400 via-orange-500 to-red-500"> <span className="bg-clip-text text-transparent bg-linear-to-r from-emerald-400 via-orange-500 to-red-500">
sis sis
</span> </span>
</h1> </h1>
<p className="text-sm text-gray-500">For Veritas</p> <p className="text-sm text-gray-500">For Veritas</p>
</div> </div>
</div> </div>
</a> </a>
{version && ( {version && (
<div className="px-2"> <div className="px-2">
<div className="inline-flex items-center space-x-2 px-3 py-1.5 bg-linear-to-r from-blue-500 to-purple-600 rounded-full shadow-md hover:shadow-lg transition-all duration-200"> <div className="inline-flex items-center space-x-2 px-3 py-1.5 bg-linear-to-r from-blue-500 to-purple-600 rounded-full shadow-md hover:shadow-lg transition-all duration-200">
<div className="w-1.5 h-1.5 bg-white rounded-full animate-pulse"></div> <div className="w-1.5 h-1.5 bg-white rounded-full animate-pulse"></div>
<div className="text-xs font-semibold text-white"> <div className="text-xs font-semibold text-white">
{version} {version}
</div> </div>
</div> </div>
</div> </div>
)} )}
</div> </div>
{/* Desktop navigation */} {/* Desktop navigation */}
<div className="navbar-center hidden lg:flex"> <div className="navbar-center hidden lg:flex">
<ul className="menu menu-horizontal gap-1"> <ul className="menu menu-horizontal gap-1">
<li> <li>
<> <>
<input <input
type="file" type="file"
accept="application/json" accept="application/json"
id="battle-data-upload" id="battle-data-upload"
className="hidden" className="hidden"
onChange={(e) => { onChange={(e) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) { if (file) {
importBattleData(file, loadBattleDataFromJSON) importBattleData(file, loadBattleDataFromJSON)
.then(() => console.log('Data loaded')) .then(() => console.log('Data loaded'))
.catch(err => alert('Failed to load data: ' + err.message)); .catch(err => alert('Failed to load data: ' + err.message));
} }
}} }}
/> />
<button <button
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium" className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => document.getElementById('battle-data-upload')?.click()} onClick={() => document.getElementById('battle-data-upload')?.click()}
> >
{transI18n("loadData")} {transI18n("loadData")}
</button> </button>
</> </>
</li> </li>
<li> <li>
<button <button
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium" className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => exportBattleData()} onClick={() => exportBattleData()}
> >
{transI18n("exportData")} {transI18n("exportData")}
</button> </button>
</li> </li>
<li> <li>
<button <button
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium" className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => handleShow("socket_connection_modal")} onClick={() => handleShow("socket_connection_modal")}
> >
{transI18n("connectSetting")} {transI18n("connectSetting")}
</button> </button>
</li> </li>
</ul> </ul>
</div> </div>
{/* Right side items */} {/* Right side items */}
<div className="navbar-end gap-2"> <div className="navbar-end gap-2">
<div className="px-2"> <div className="px-2">
<div className="flex items-center space-x-2 p-1.5 rounded-full shadow-md"> <div className="flex items-center space-x-2 p-1.5 rounded-full shadow-md">
<div className={`hidden lg:block text-sm italic ${status ? 'text-green-500' : 'text-red-500'}`}> <div className={`hidden lg:block text-sm italic ${status ? 'text-green-500' : 'text-red-500'}`}>
{status ? transI18n("connected") : transI18n("unconnected")} {status ? transI18n("connected") : transI18n("unconnected")}
</div> </div>
<div className={`w-3 h-3 rounded-full ${status ? 'bg-green-500' : 'bg-red-500'}`}></div> <div className={`w-3 h-3 rounded-full ${status ? 'bg-green-500' : 'bg-red-500'}`}></div>
</div> </div>
</div> </div>
{/* Language selector - REFINED */} {/* Language selector - REFINED */}
<div className="dropdown dropdown-end"> <div className="dropdown dropdown-end">
<div className="flex items-center gap-1 border border-base-300 rounded text-sm px-1.5 py-0.5 hover:bg-base-200 cursor-pointer transition-all duration-200"> <div className="flex items-center gap-1 border border-base-300 rounded text-sm px-1.5 py-0.5 hover:bg-base-200 cursor-pointer transition-all duration-200">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-4"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-4">
<path strokeLinecap="round" strokeLinejoin="round" d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802" /> <path strokeLinecap="round" strokeLinejoin="round" d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802" />
</svg> </svg>
<select <select
className="outline-none bg-base-200 cursor-pointer text-sm pr-0" className="outline-none bg-base-200 cursor-pointer text-sm pr-0"
value={locale} value={locale}
onChange={(e) => changeLocale(e.target.value)} onChange={(e) => changeLocale(e.target.value)}
> >
{Object.entries(listCurrentLanguage).map(([key, value]) => ( {Object.entries(listCurrentLanguage).map(([key, value]) => (
<option key={key} value={key}>{value}</option> <option key={key} value={key}>{value}</option>
))} ))}
</select> </select>
</div> </div>
</div> </div>
<div title="Change Theme" className="dropdown dropdown-end"> <div title="Change Theme" className="dropdown dropdown-end">
<div tabIndex={0} role="button" className="btn btn-ghost btn-sm hover:bg-base-200 transition-all duration-200 px-2"> <div tabIndex={0} role="button" className="btn btn-ghost btn-sm hover:bg-base-200 transition-all duration-200 px-2">
<svg <svg
width={16} width={16}
height={16} height={16}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
className="h-4 w-4 stroke-current" className="h-4 w-4 stroke-current"
> >
<path <path
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth={2} strokeWidth={2}
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
/> />
</svg> </svg>
<svg <svg
width="10px" width="10px"
height="10px" height="10px"
className="ml-1 h-2 w-2 fill-current opacity-60" className="ml-1 h-2 w-2 fill-current opacity-60"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 2048 2048" viewBox="0 0 2048 2048"
> >
<path d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z" /> <path d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z" />
</svg> </svg>
</div> </div>
<div <div
tabIndex={0} tabIndex={0}
className="dropdown-content bg-base-100 text-base-content rounded-lg top-px overflow-y-auto border border-base-200 shadow-md mt-12" className="dropdown-content bg-base-100 text-base-content rounded-lg top-px overflow-y-auto border border-base-200 shadow-md mt-12"
> >
<ul className="menu w-40 p-1"> <ul className="menu w-40 p-1">
{themes.map((theme) => ( {themes.map((theme) => (
<li <li
key={theme.label} key={theme.label}
onClick={() => { onClick={() => {
if (changeTheme) changeTheme(theme.label.toLowerCase()); if (changeTheme) changeTheme(theme.label.toLowerCase());
}} }}
> >
<button <button
className="gap-2 px-2 py-1.5 hover:bg-base-200 rounded text-sm transition-all duration-200" className="gap-2 px-2 py-1.5 hover:bg-base-200 rounded text-sm transition-all duration-200"
data-set-theme={theme.label.toLowerCase()} data-set-theme={theme.label.toLowerCase()}
data-act-class="[&_svg]:visible" data-act-class="[&_svg]:visible"
> >
<div <div
data-theme={theme.label.toLowerCase()} data-theme={theme.label.toLowerCase()}
className="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded p-1 shadow-sm" className="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded p-1 shadow-sm"
> >
<div className="bg-base-content size-1 rounded-full" /> <div className="bg-base-content size-1 rounded-full" />
<div className="bg-primary size-1 rounded-full" /> <div className="bg-primary size-1 rounded-full" />
<div className="bg-secondary size-1 rounded-full" /> <div className="bg-secondary size-1 rounded-full" />
<div className="bg-accent size-1 rounded-full" /> <div className="bg-accent size-1 rounded-full" />
</div> </div>
<div className="text-sm">{theme.label}</div> <div className="text-sm">{theme.label}</div>
</button> </button>
</li> </li>
))} ))}
</ul> </ul>
</div> </div>
</div> </div>
{/* GitHub Link */} {/* GitHub Link */}
<Link <Link
className='hidden sm:flex btn btn-ghost btn-sm btn-circle bg-white/20 hover:bg-white transition-all duration-200 items-center justify-center' className='hidden sm:flex btn btn-ghost btn-sm btn-circle bg-white/20 hover:bg-white transition-all duration-200 items-center justify-center'
href={"https://github.com/AzenKain/SR-Analysis"} href={"https://github.com/AzenKain/SR-Analysis"}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"> <svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512">
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" /> <path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
</svg> </svg>
</Link> </Link>
</div> </div>
<dialog id="socket_connection_modal" className="modal sm:modal-middle backdrop-blur-sm"> <dialog id="socket_connection_modal" className="modal sm:modal-middle backdrop-blur-sm">
<div className="modal-box w-11/12 max-w-7xl bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20"> <div className="modal-box w-11/12 max-w-7xl bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10"> <div className="sticky top-0 z-10">
<motion.button <motion.button
whileHover={{ scale: 1.1, rotate: 90 }} whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }} 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" className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={() => handleCloseModal("socket_connection_modal")} onClick={() => handleCloseModal("socket_connection_modal")}
> >
✕ ✕
</motion.button> </motion.button>
</div> </div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4"> <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"> <h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{transI18n("socketConnection").toUpperCase()} {transI18n("socketConnection").toUpperCase()}
</h3> </h3>
</div> </div>
<div className="px-6 py-4"> <div className="px-6 py-4">
{/* Select connection type */} {/* Select connection type */}
<div className="form-control grid grid-cols-1 w-full mb-6"> <div className="form-control grid grid-cols-1 w-full mb-6">
<label className="label"> <label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("connectionType")}</span> <span className="label-text font-semibold text-purple-300">{transI18n("connectionType")}</span>
</label> </label>
<select <select
className="select select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200" className="select select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200"
value={connectionType} value={connectionType}
onChange={(e) => setConnectionType(e.target.value)} onChange={(e) => setConnectionType(e.target.value)}
> >
<option value="Native">Native</option> <option value="Native">Native</option>
<option value="PS">PS</option> <option value="PS">PS</option>
<option value="Other">{transI18n("other")}</option> <option value="Other">{transI18n("other")}</option>
</select> </select>
</div> </div>
{/* Show host/port if Other */} {/* Show host/port if Other */}
{connectionType === "Other" && ( {connectionType === "Other" && (
<div className="flex flex-col md:flex-row md:space-x-4 mb-6"> <div className="flex flex-col md:flex-row md:space-x-4 mb-6">
<div className="form-control w-full mb-4 md:mb-0"> <div className="form-control w-full mb-4 md:mb-0">
<label className="label"> <label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("host")}</span> <span className="label-text font-semibold text-purple-300">{transI18n("host")}</span>
</label> </label>
<input <input
type="text" type="text"
placeholder={transI18n("hostPlaceHolder")} placeholder={transI18n("hostPlaceHolder")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200" className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200"
value={host} value={host}
onChange={(e) => setHost(e.target.value)} onChange={(e) => setHost(e.target.value)}
/> />
</div> </div>
<div className="form-control w-full"> <div className="form-control w-full">
<label className="label"> <label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("port")}</span> <span className="label-text font-semibold text-purple-300">{transI18n("port")}</span>
</label> </label>
<input <input
type="number" type="number"
placeholder={transI18n("portPlaceHolder")} placeholder={transI18n("portPlaceHolder")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200" className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200"
value={port || ''} value={port || ''}
onChange={(e) => setPort(parseInt(e.target.value) || 0)} onChange={(e) => setPort(parseInt(e.target.value) || 0)}
/> />
</div> </div>
</div> </div>
)} )}
{message.text && ( {message.text && (
<div className={`alert ${message.type === 'success' ? 'alert-success' : <div className={`alert ${message.type === 'success' ? 'alert-success' :
message.type === 'error' ? 'alert-error' : 'alert-info' message.type === 'error' ? 'alert-error' : 'alert-info'
} mb-6`}> } mb-6`}>
<span>{message.text}</span> <span>{message.text}</span>
</div> </div>
)} )}
<div className="flex flex-col sm:flex-row justify-between items-center mt-6 mb-2"> <div className="flex flex-col sm:flex-row justify-between items-center mt-6 mb-2">
<div className="mb-4 sm:mb-0"> <div className="mb-4 sm:mb-0">
<span className="text-md mr-2">{transI18n("status")}:</span> <span className="text-md mr-2">{transI18n("status")}:</span>
<span className={`badge ${status ? 'badge-success' : 'badge-error'} badge-lg`}> <span className={`badge ${status ? 'badge-success' : 'badge-error'} badge-lg`}>
{status ? transI18n("connected") : transI18n("unconnected")} {status ? transI18n("connected") : transI18n("unconnected")}
</span> </span>
</div> </div>
<div className="flex space-x-2"> <div className="flex space-x-2">
<button <button
className={`btn btn-primary`} className={`btn btn-primary`}
onClick={handleConnect} onClick={handleConnect}
> >
{transI18n("connect")} {transI18n("connect")}
</button> </button>
{connectionType !== "Native" && ( {connectionType !== "Native" && (
<button <button
className={`btn btn-secondary`} className={`btn btn-secondary`}
onClick={checkConnection} onClick={checkConnection}
> >
{transI18n("checkGameConnect")} {transI18n("checkGameConnect")}
</button> </button>
)} )}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</dialog> </dialog>
</div> </div>
) )
} }
+306 -306
View File
@@ -1,307 +1,307 @@
"use client"; "use client";
import useAvatarDataStore from "@/stores/avatarDataStore"; import useAvatarDataStore from "@/stores/avatarDataStore";
import useBattleDataStore from "@/stores/battleDataStore"; import useBattleDataStore from "@/stores/battleDataStore";
import CharacterCard from "../card/characterCard"; import CharacterCard from "../card/characterCard";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { CharacterBasic } from "@/types"; import { CharacterBasic } from "@/types";
import useLocaleStore from "@/stores/localeStore"; import useLocaleStore from "@/stores/localeStore";
import { getNameChar } from '@/helper/getNameChar'; import { getNameChar } from '@/helper/getNameChar';
import SkillBarChart from "../chart/skillBarChart"; import SkillBarChart from "../chart/skillBarChart";
import SkillPieChart from "../chart/skillPieChart"; import SkillPieChart from "../chart/skillPieChart";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { DamageLineForOne } from "../chart/damageLineForOne"; import { DamageLineForOne } from "../chart/damageLineForOne";
import { DamagePerCycleForOne } from "../chart/damagePerCycleForOne"; import { DamagePerCycleForOne } from "../chart/damagePerCycleForOne";
import { useCalcTotalDmgAvatar, useCalcTotalTurnAvatar } from "@/hooks/useCalcAvatarData"; import { useCalcTotalDmgAvatar, useCalcTotalTurnAvatar } from "@/hooks/useCalcAvatarData";
import Image from "next/image"; import Image from "next/image";
import NameAvatar from "../nameAvatar"; import NameAvatar from "../nameAvatar";
// import ShowCaseInfo from "../card/showCaseCard"; // import ShowCaseInfo from "../card/showCaseCard";
export default function LineupBar() { export default function LineupBar() {
const [selectedCharacter, setSelectedCharacter] = useState<CharacterBasic | undefined>(undefined); const [selectedCharacter, setSelectedCharacter] = useState<CharacterBasic | undefined>(undefined);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const transI18n = useTranslations("DataAnalysisPage"); const transI18n = useTranslations("DataAnalysisPage");
const { lineup, turnHistory, dataAvatar } = useBattleDataStore(); const { lineup, turnHistory, dataAvatar } = useBattleDataStore();
const { listAvatar, mapAvatar } = useAvatarDataStore(); const { listAvatar, mapAvatar } = useAvatarDataStore();
const { locale } = useLocaleStore(); const { locale } = useLocaleStore();
const totalDamage = useCalcTotalDmgAvatar(selectedCharacter ? Number(selectedCharacter.id) : 0); const totalDamage = useCalcTotalDmgAvatar(selectedCharacter ? Number(selectedCharacter.id) : 0);
const totalTurn = useCalcTotalTurnAvatar(selectedCharacter ? Number(selectedCharacter.id) : 0) const totalTurn = useCalcTotalTurnAvatar(selectedCharacter ? Number(selectedCharacter.id) : 0)
const lineupAvatars = listAvatar?.filter(item => const lineupAvatars = listAvatar?.filter(item =>
lineup?.some(av => av?.avatarId?.toString() === item.id) lineup?.some(av => av?.avatarId?.toString() === item.id)
); );
const handleShow = (modalId: string, item: CharacterBasic) => { const handleShow = (modalId: string, item: CharacterBasic) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null; const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) { if (modal) {
setSelectedCharacter(item); setSelectedCharacter(item);
setIsModalOpen(true); setIsModalOpen(true);
modal.showModal(); modal.showModal();
} }
}; };
// Close modal handler // Close modal handler
const handleCloseModal = (modalId: string) => { const handleCloseModal = (modalId: string) => {
setIsModalOpen(false); setIsModalOpen(false);
setSelectedCharacter(undefined); setSelectedCharacter(undefined);
const modal = document.getElementById(modalId) as HTMLDialogElement | null; const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) { if (modal) {
modal.close() modal.close()
} }
}; };
// Handle ESC key to close modal // Handle ESC key to close modal
useEffect(() => { useEffect(() => {
const handleEscKey = (event: KeyboardEvent) => { const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isModalOpen) { if (event.key === 'Escape' && isModalOpen) {
handleCloseModal("character_detail_modal"); handleCloseModal("character_detail_modal");
} }
}; };
window.addEventListener('keydown', handleEscKey); window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey); return () => window.removeEventListener('keydown', handleEscKey);
}, [isModalOpen]); }, [isModalOpen]);
return ( return (
<div className="p-4 md:p-1 rounded-lg shadow-lg w-full h-full"> <div className="p-4 md:p-1 rounded-lg shadow-lg w-full h-full">
<motion.h2 <motion.h2
className="text-center text-xl lg:text-2xl pb-2 font-bold text-transparent bg-clip-text bg-linear-to-r from-pink-500 via-purple-500 to-cyan-500" className="text-center text-xl lg:text-2xl pb-2 font-bold text-transparent bg-clip-text bg-linear-to-r from-pink-500 via-purple-500 to-cyan-500"
initial={{ opacity: 0, y: -20 }} initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
> >
{transI18n("lineupInfo")} {transI18n("lineupInfo")}
</motion.h2> </motion.h2>
<div className="flex justify-center mb-2"> <div className="flex justify-center mb-2">
<div className="badge badge-accent gap-2 text-lg items-center px-4 py-2 whitespace-nowrap max-w-full"> <div className="badge badge-accent gap-2 text-lg items-center px-4 py-2 whitespace-nowrap max-w-full">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 shrink-0">
<path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg> </svg>
<span className="text-sm truncate flex flex-row"> <span className="text-sm truncate flex flex-row">
<div> <div>
{transI18n("lastTurn")}: {transI18n("lastTurn")}:
</div> </div>
<NameAvatar <NameAvatar
locale={locale} locale={locale}
text={getNameChar(locale, transI18n, mapAvatar?.[turnHistory.findLast(i => i?.avatarId)?.avatarId?.toString() || ""])} text={getNameChar(locale, transI18n, mapAvatar?.[turnHistory.findLast(i => i?.avatarId)?.avatarId?.toString() || ""])}
/> />
</span> </span>
</div> </div>
</div> </div>
<div className="relative h-full pt-2 max-h-[90vh] lg:max-h-[80vh] border-t-2 border-accent"> <div className="relative h-full pt-2 max-h-[90vh] lg:max-h-[80vh] border-t-2 border-accent">
{lineupAvatars.length === 0 ? ( {lineupAvatars.length === 0 ? (
<div className="h-full flex justify-center items-start"> <div className="h-full flex justify-center items-start">
<p className="text-base-content opacity-50">{transI18n("noCharactersInLineup")}</p> <p className="text-base-content opacity-50">{transI18n("noCharactersInLineup")}</p>
</div> </div>
) : ( ) : (
<div className="h-full w-full overflow-x-auto md:overflow-x-hidden md:overflow-y-auto rounded-lg"> <div className="h-full w-full overflow-x-auto md:overflow-x-hidden md:overflow-y-auto rounded-lg">
<div className="flex flex-nowrap md:grid md:grid-cols-1 w-fit md:w-full justify-items-center items-start gap-2"> <div className="flex flex-nowrap md:grid md:grid-cols-1 w-fit md:w-full justify-items-center items-start gap-2">
{lineupAvatars?.map((item, index) => { {lineupAvatars?.map((item, index) => {
const lastTurnAvatarId = turnHistory.findLast(i => i?.avatarId)?.avatarId || -1; const lastTurnAvatarId = turnHistory.findLast(i => i?.avatarId)?.avatarId || -1;
const isLastTurn = item.id === lastTurnAvatarId.toString(); const isLastTurn = item.id === lastTurnAvatarId.toString();
return ( return (
<motion.div <motion.div
key={item.id} key={item.id}
initial={{ opacity: 0, scale: 0.9 }} initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: index * 0.1 }} transition={{ duration: 0.3, delay: index * 0.1 }}
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
onClick={() => handleShow("character_detail_modal", item)} onClick={() => handleShow("character_detail_modal", item)}
className={`cursor-pointer shrink-0 justify-items-center ${isLastTurn ? "shadow-[inset_0_0_10px_2px_rgba(59,130,246,0.7),0_0_20px_5px_rgba(59,130,246,0.3)]" : "" className={`cursor-pointer shrink-0 justify-items-center ${isLastTurn ? "shadow-[inset_0_0_10px_2px_rgba(59,130,246,0.7),0_0_20px_5px_rgba(59,130,246,0.3)]" : ""
}`} }`}
> >
<CharacterCard data={item} /> <CharacterCard data={item} />
</motion.div> </motion.div>
); );
})} })}
</div> </div>
</div> </div>
)} )}
{/* Character Detail Modal */} {/* Character Detail Modal */}
<dialog id="character_detail_modal" className="modal backdrop-blur-sm"> <dialog id="character_detail_modal" className="modal backdrop-blur-sm">
<div className="modal-box w-11/12 max-w-7xl bg-linear-to-b bg-base-100 text-base-content border-purple-500 shadow-lg shadow-purple-500/20"> <div className="modal-box w-11/12 max-w-7xl bg-linear-to-b bg-base-100 text-base-content border-purple-500 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10"> <div className="sticky top-0 z-10">
<motion.button <motion.button
whileHover={{ scale: 1.1, rotate: 90 }} whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }} 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" className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={() => handleCloseModal("character_detail_modal")} onClick={() => handleCloseModal("character_detail_modal")}
> >
✕ ✕
</motion.button> </motion.button>
</div> </div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4"> <div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<NameAvatar <NameAvatar
locale={locale} locale={locale}
text={getNameChar(locale, transI18n, selectedCharacter).toUpperCase()} text={getNameChar(locale, transI18n, selectedCharacter).toUpperCase()}
className={"font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400"} className={"font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400"}
/> />
</div> </div>
{selectedCharacter && ( {selectedCharacter && (
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="grid grid-cols-1 md:grid-cols-2 gap-6 p-2" className="grid grid-cols-1 md:grid-cols-2 gap-6 p-2"
> >
<div className="md:col-span-2 bg-base-200 rounded-lg p-4 shadow-md"> <div className="md:col-span-2 bg-base-200 rounded-lg p-4 shadow-md">
<h4 className="text-lg font-semibold mb-2 text-pink-500">{transI18n("characterInformation")}</h4> <h4 className="text-lg font-semibold mb-2 text-pink-500">{transI18n("characterInformation")}</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2"> <div className="grid grid-cols-1 sm:grid-cols-2">
<div className="grid grid-cols-1 lg:grid-cols-2 justify-items-start items-center gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 justify-items-start items-center gap-6">
<p> <p>
{transI18n("id")}: <span className="font-bold">{selectedCharacter.id}</span> {transI18n("id")}: <span className="font-bold">{selectedCharacter.id}</span>
</p> </p>
<p className="flex items-center space-x-2"> <p className="flex items-center space-x-2">
<span>{transI18n("path")}:</span> <span>{transI18n("path")}:</span>
<span className="font-bold">{transI18n(selectedCharacter.baseType.toLowerCase())}</span> <span className="font-bold">{transI18n(selectedCharacter.baseType.toLowerCase())}</span>
{selectedCharacter.baseType && ( {selectedCharacter.baseType && (
<Image <Image
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
src={`/icon/${selectedCharacter.baseType.toLowerCase()}.webp`} src={`/icon/${selectedCharacter.baseType.toLowerCase()}.webp`}
className="w-6 h-6" className="w-6 h-6"
alt={selectedCharacter.baseType.toLowerCase()} alt={selectedCharacter.baseType.toLowerCase()}
width={24} width={24}
height={24} height={24}
/> />
)} )}
</p> </p>
<p> <p>
{transI18n("rarity")}: <span className="font-bold">{selectedCharacter.rank === "CombatPowerAvatarRarityType5" ? "5*" : "4*"}</span> {transI18n("rarity")}: <span className="font-bold">{selectedCharacter.rank === "CombatPowerAvatarRarityType5" ? "5*" : "4*"}</span>
</p> </p>
<p className="flex items-center space-x-2"> <p className="flex items-center space-x-2">
<span>{transI18n("element")}:</span> <span>{transI18n("element")}:</span>
<Image <Image
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
src={`/icon/${selectedCharacter.damageType.toLowerCase()}.webp`} src={`/icon/${selectedCharacter.damageType.toLowerCase()}.webp`}
className="w-6 h-6" className="w-6 h-6"
alt={selectedCharacter.damageType.toLowerCase()} alt={selectedCharacter.damageType.toLowerCase()}
width={24} width={24}
height={24} height={24}
/> />
</p> </p>
{(() => { {(() => {
const avatar = dataAvatar.find(it => it.avatar_id.toString() === selectedCharacter.id); const avatar = dataAvatar.find(it => it.avatar_id.toString() === selectedCharacter.id);
if (!avatar) return null; if (!avatar) return null;
const relicIds = avatar.relics.map(item => { const relicIds = avatar.relics.map(item => {
const relicIdStr = item.relic_id.toString().slice(1); const relicIdStr = item.relic_id.toString().slice(1);
const slot = relicIdStr.slice(-1); const slot = relicIdStr.slice(-1);
return `${item.relic_set_id}_${slot}`; return `${item.relic_set_id}_${slot}`;
}); });
return ( return (
<> <>
<p> <p>
{transI18n("level")}: <span className="font-bold">{avatar.level}</span> {transI18n("level")}: <span className="font-bold">{avatar.level}</span>
</p> </p>
<p> <p>
{transI18n("eidolons")}: <span className="font-bold">{avatar?.data?.rank}</span> {transI18n("eidolons")}: <span className="font-bold">{avatar?.data?.rank}</span>
</p> </p>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<span>{transI18n("lightcones")}:</span> <span>{transI18n("lightcones")}:</span>
<Image <Image
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
src={`${process.env.CDN_URL}/spriteoutput/lightconemediumicon/${avatar?.Lightcone?.item_id}.png`} src={`${process.env.CDN_URL}/spriteoutput/lightconemediumicon/${avatar?.Lightcone?.item_id}.png`}
className="w-12 h-12" className="w-12 h-12"
alt={avatar?.Lightcone?.item_id?.toString() || ""} alt={avatar?.Lightcone?.item_id?.toString() || ""}
width={200} width={200}
height={200} height={200}
/> />
</div> </div>
<div className="flex items-center space-x-2 w-full"> <div className="flex items-center space-x-2 w-full">
<span>{transI18n("relics")}:</span> <span>{transI18n("relics")}:</span>
<div className="grid grid-cols-3 md:flex md:flex-row w-full"> <div className="grid grid-cols-3 md:flex md:flex-row w-full">
{relicIds.map(it => ( {relicIds.map(it => (
<Image <Image
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
key={it} key={it}
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${it}.png`} src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${it}.png`}
className="w-12 h-12" className="w-12 h-12"
alt={avatar?.Lightcone?.item_id?.toString() || ""} alt={avatar?.Lightcone?.item_id?.toString() || ""}
width={200} width={200}
height={200} height={200}
/> />
))} ))}
</div> </div>
</div> </div>
</> </>
); );
})()} })()}
</div> </div>
</div> </div>
<Image <Image
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${selectedCharacter.icon}`} src={`${process.env.CDN_URL}/${selectedCharacter.icon}`}
alt={getNameChar(locale, transI18n, selectedCharacter)} alt={getNameChar(locale, transI18n, selectedCharacter)}
className="h-32 w-32 object-cover rounded-full border-2 border-purple-500" className="h-32 w-32 object-cover rounded-full border-2 border-purple-500"
width={128} width={128}
height={128} height={128}
/> />
</div> </div>
</div> </div>
{/* <div className="md:col-span-2 bg-base-200 rounded-lg p-4 shadow-md"> {/* <div className="md:col-span-2 bg-base-200 rounded-lg p-4 shadow-md">
<h4 className="text-lg font-semibold mb-2 text-pink-500">{transI18n("characterInformation")}</h4> <h4 className="text-lg font-semibold mb-2 text-pink-500">{transI18n("characterInformation")}</h4>
<ShowCaseInfo></ShowCaseInfo> <ShowCaseInfo></ShowCaseInfo>
</div> */} </div> */}
<div className="bg-base-200 rounded-lg p-4 shadow-md"> <div className="bg-base-200 rounded-lg p-4 shadow-md">
<p className="mt-2 font-bold text-lg text-cyan-500">{transI18n("totalTurn")}: <span className="text-base-content">{Number(totalTurn).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</span></p> <p className="mt-2 font-bold text-lg text-cyan-500">{transI18n("totalTurn")}: <span className="text-base-content">{Number(totalTurn).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</span></p>
</div> </div>
<div className="bg-base-200 rounded-lg p-4 shadow-md"> <div className="bg-base-200 rounded-lg p-4 shadow-md">
<h4 className="text-lg font-semibold mb-2 text-purple-500">{transI18n("totalDamage")}: <span className="text-base-content">{Number(totalDamage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</span></h4> <h4 className="text-lg font-semibold mb-2 text-purple-500">{transI18n("totalDamage")}: <span className="text-base-content">{Number(totalDamage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</span></h4>
</div> </div>
<div className="bg-base-200 rounded-lg p-4 shadow-md"> <div className="bg-base-200 rounded-lg p-4 shadow-md">
<h4 className="text-lg font-semibold mb-4 text-purple-500">{transI18n("skillDamageBreakdown")}</h4> <h4 className="text-lg font-semibold mb-4 text-purple-500">{transI18n("skillDamageBreakdown")}</h4>
<SkillBarChart avatarId={Number(selectedCharacter.id) ?? 0} /> <SkillBarChart avatarId={Number(selectedCharacter.id) ?? 0} />
</div> </div>
<div className="bg-base-200 rounded-lg p-4 shadow-md"> <div className="bg-base-200 rounded-lg p-4 shadow-md">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h4 className="text-lg font-semibold mb-4 text-cyan-500">{transI18n("damageOverTime")}</h4> <h4 className="text-lg font-semibold mb-4 text-cyan-500">{transI18n("damageOverTime")}</h4>
</div> </div>
<DamageLineForOne avatarId={Number(selectedCharacter.id) ?? 0} /> <DamageLineForOne avatarId={Number(selectedCharacter.id) ?? 0} />
</div> </div>
<div className="bg-base-200 rounded-lg p-4 shadow-md"> <div className="bg-base-200 rounded-lg p-4 shadow-md">
<h4 className="text-lg font-semibold mb-4 text-cyan-500">{transI18n("skillUsageDistribution")}</h4> <h4 className="text-lg font-semibold mb-4 text-cyan-500">{transI18n("skillUsageDistribution")}</h4>
<SkillPieChart avatarId={Number(selectedCharacter.id) ?? 0} /> <SkillPieChart avatarId={Number(selectedCharacter.id) ?? 0} />
</div> </div>
<div className="bg-base-200 rounded-lg p-4 shadow-md"> <div className="bg-base-200 rounded-lg p-4 shadow-md">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h4 className="text-lg font-semibold text-purple-500">{transI18n("damagePerCycle")}</h4> <h4 className="text-lg font-semibold text-purple-500">{transI18n("damagePerCycle")}</h4>
</div> </div>
<DamagePerCycleForOne avatarId={Number(selectedCharacter.id) ?? 0} /> <DamagePerCycleForOne avatarId={Number(selectedCharacter.id) ?? 0} />
</div> </div>
</motion.div> </motion.div>
)} )}
</div> </div>
</dialog> </dialog>
</div> </div>
</div> </div>
); );
} }
+15 -15
View File
@@ -1,16 +1,16 @@
import { parseRuby } from "@/helper"; import { parseRuby } from "@/helper";
interface TextProps { interface TextProps {
text: string; text: string;
locale: string; locale: string;
className?: string; className?: string;
} }
export default function NameAvatar({ text, locale, className }: TextProps) { export default function NameAvatar({ text, locale, className }: TextProps) {
if (locale === "ja") { if (locale === "ja") {
return <div className={className} dangerouslySetInnerHTML={{ __html: parseRuby(text) }} />; return <div className={className} dangerouslySetInnerHTML={{ __html: parseRuby(text) }} />;
} }
return <div className={className}>{text}</div>; return <div className={className}>{text}</div>;
} }
@@ -1,8 +1,8 @@
"use client"; "use client";
import { PropsWithChildren, useContext } from "react"; import { PropsWithChildren, useContext } from "react";
import { ThemeContext } from "./themeContext"; import { ThemeContext } from "./themeContext";
export function ClientThemeWrapper({ children }: PropsWithChildren) { export function ClientThemeWrapper({ children }: PropsWithChildren) {
const { theme } = useContext(ThemeContext); const { theme } = useContext(ThemeContext);
return <div data-theme={theme} className="h-full">{children}</div>; return <div data-theme={theme} className="h-full">{children}</div>;
} }
+2 -2
View File
@@ -1,2 +1,2 @@
export * from "./clientThemeWrapper" export * from "./clientThemeWrapper"
export * from "./themeContext" export * from "./themeContext"
+26 -26
View File
@@ -1,27 +1,27 @@
"use client"; "use client";
import useLocaleStore from "@/stores/localeStore"; import useLocaleStore from "@/stores/localeStore";
import { createContext, PropsWithChildren } from "react"; import { createContext, PropsWithChildren } from "react";
interface ThemeContextType { interface ThemeContextType {
theme?: string; theme?: string;
changeTheme?: (nextTheme: string | null) => void; changeTheme?: (nextTheme: string | null) => void;
} }
export const ThemeContext = createContext<ThemeContextType>({}); export const ThemeContext = createContext<ThemeContextType>({});
export const ThemeProvider = ({ children }: PropsWithChildren) => { export const ThemeProvider = ({ children }: PropsWithChildren) => {
const { theme, setTheme } = useLocaleStore() const { theme, setTheme } = useLocaleStore()
const changeTheme = (nextTheme: string | null) => { const changeTheme = (nextTheme: string | null) => {
if (nextTheme) { if (nextTheme) {
setTheme(nextTheme); setTheme(nextTheme);
} else { } else {
setTheme(theme === "winter" ? "night" : "winter"); setTheme(theme === "winter" ? "night" : "winter");
} }
}; };
return ( return (
<ThemeContext.Provider value={{ theme, changeTheme }}> <ThemeContext.Provider value={{ theme, changeTheme }}>
{children} {children}
</ThemeContext.Provider> </ThemeContext.Provider>
); );
}; };
+4 -4
View File
@@ -1,4 +1,4 @@
declare module '*.css' { declare module '*.css' {
const content: { [className: string]: string }; const content: { [className: string]: string };
export default content; export default content;
} }
+47 -47
View File
@@ -1,48 +1,48 @@
import useBattleDataStore from "@/stores/battleDataStore"; import useBattleDataStore from "@/stores/battleDataStore";
import { BattleDataStateJson } from "@/types/mics"; import { BattleDataStateJson } from "@/types/mics";
export const exportBattleData = ( export const exportBattleData = (
filename = 'battle_data.json' filename = 'battle_data.json'
) => { ) => {
const { lineup, const { lineup,
turnHistory, turnHistory,
skillHistory, skillHistory,
totalAV, totalAV,
totalDamage, totalDamage,
damagePerAV, damagePerAV,
cycleIndex, cycleIndex,
waveIndex, waveIndex,
dataAvatar, dataAvatar,
maxWave, maxWave,
maxCycle, maxCycle,
version, version,
avatarDetail, avatarDetail,
enemyDetail enemyDetail
} = useBattleDataStore.getState(); } = useBattleDataStore.getState();
const data: BattleDataStateJson = { const data: BattleDataStateJson = {
lineup, lineup,
turnHistory, turnHistory,
skillHistory, skillHistory,
dataAvatar, dataAvatar,
totalAV, totalAV,
totalDamage, totalDamage,
damagePerAV, damagePerAV,
cycleIndex, cycleIndex,
waveIndex, waveIndex,
maxWave, maxWave,
maxCycle, maxCycle,
version, version,
avatarDetail, avatarDetail,
enemyDetail enemyDetail
} }
const dataStr = JSON.stringify(data, null, 2); const dataStr = JSON.stringify(data, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' }); const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = filename; a.download = filename;
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}; };
+62 -62
View File
@@ -1,63 +1,63 @@
import { listCurrentLanguage } from "@/lib/constant"; import { listCurrentLanguage } from "@/lib/constant";
import { CharacterBasic, MonsterBasic } from "@/types"; import { CharacterBasic, MonsterBasic } from "@/types";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
type TFunc = ReturnType<typeof useTranslations> type TFunc = ReturnType<typeof useTranslations>
function cleanText(text: string): string { function cleanText(text: string): string {
if (!text) return "" if (!text) return ""
return text.replace(/<unbreak>(.*?)<\/unbreak>/g, "$1") return text.replace(/<unbreak>(.*?)<\/unbreak>/g, "$1")
} }
export function getNameChar( export function getNameChar(
locale: string, locale: string,
t: TFunc, t: TFunc,
data: CharacterBasic | undefined data: CharacterBasic | undefined
): string { ): string {
if (!data) return ""; if (!data) return "";
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguage, locale)) { if (!Object.prototype.hasOwnProperty.call(listCurrentLanguage, locale)) {
return ""; return "";
} }
const langKey = listCurrentLanguage[locale as keyof typeof listCurrentLanguage].toLowerCase(); const langKey = listCurrentLanguage[locale as keyof typeof listCurrentLanguage].toLowerCase();
let text = data.lang[langKey] ?? ""; let text = data.lang[langKey] ?? "";
if (!text) { if (!text) {
text = data.lang["en"] ?? ""; text = data.lang["en"] ?? "";
} }
if (Number(data.id) > 8000) { if (Number(data.id) > 8000) {
text = `${t("trailblazer")} • ${t(data?.baseType?.toLowerCase() ?? "")}`; text = `${t("trailblazer")} • ${t(data?.baseType?.toLowerCase() ?? "")}`;
} }
return cleanText(text); return cleanText(text);
} }
export function getNameEnemy(locale: string, data: MonsterBasic | undefined): string { export function getNameEnemy(locale: string, data: MonsterBasic | undefined): string {
if (!data) { if (!data) {
return "" return ""
} }
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguage, locale)) { if (!Object.prototype.hasOwnProperty.call(listCurrentLanguage, locale)) {
return "" return ""
} }
const langKey = listCurrentLanguage[locale as keyof typeof listCurrentLanguage].toLowerCase(); const langKey = listCurrentLanguage[locale as keyof typeof listCurrentLanguage].toLowerCase();
let text = data.lang[langKey] ?? ""; let text = data.lang[langKey] ?? "";
if (!text) { if (!text) {
text = data.lang["en"] ?? ""; text = data.lang["en"] ?? "";
} }
return text return text
} }
export function parseRuby(text: string): string { export function parseRuby(text: string): string {
const rubyRegex = /\{RUBY_B#(.*?)\}(.*?)\{RUBY_E#\}/gs; const rubyRegex = /\{RUBY_B#(.*?)\}(.*?)\{RUBY_E#\}/gs;
return text.replace(rubyRegex, (_match, furigana, kanji) => { return text.replace(rubyRegex, (_match, furigana, kanji) => {
return `<ruby>${kanji}<rt>${furigana}</rt></ruby>`; return `<ruby>${kanji}<rt>${furigana}</rt></ruby>`;
}); });
} }
+20 -20
View File
@@ -1,21 +1,21 @@
import { BattleDataStateJson } from "@/types/mics"; import { BattleDataStateJson } from "@/types/mics";
export const importBattleData = (file: File, loadBattleDataFromJSON: (data: BattleDataStateJson) => void) => { export const importBattleData = (file: File, loadBattleDataFromJSON: (data: BattleDataStateJson) => void) => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
try { try {
const result = reader.result as string; const result = reader.result as string;
const parsed = JSON.parse(result) as BattleDataStateJson; const parsed = JSON.parse(result) as BattleDataStateJson;
loadBattleDataFromJSON(parsed); loadBattleDataFromJSON(parsed);
resolve(); resolve();
} catch (err) { } catch (err) {
reject(err); reject(err);
} }
}; };
reader.onerror = () => reject(reader.error); reader.onerror = () => reject(reader.error);
reader.readAsText(file); reader.readAsText(file);
}); });
}; };
+2 -2
View File
@@ -1,3 +1,3 @@
export * from "./getNameChar" export * from "./getNameChar"
export * from "./exportDataBattle" export * from "./exportDataBattle"
export * from "./importDataBattle" export * from "./importDataBattle"
+27 -27
View File
@@ -1,28 +1,28 @@
import useBattleDataStore from "@/stores/battleDataStore"; import useBattleDataStore from "@/stores/battleDataStore";
import { useMemo } from "react"; import { useMemo } from "react";
export function useCalcTotalDmgAvatar(avatarId: number) { export function useCalcTotalDmgAvatar(avatarId: number) {
const { skillHistory } = useBattleDataStore.getState(); const { skillHistory } = useBattleDataStore.getState();
return useMemo(() => { return useMemo(() => {
return skillHistory return skillHistory
.filter(t => t.avatarId === avatarId) .filter(t => t.avatarId === avatarId)
.reduce((sum, turn) => sum + turn.totalDamage, 0); .reduce((sum, turn) => sum + turn.totalDamage, 0);
}, [avatarId, skillHistory]); }, [avatarId, skillHistory]);
} }
export function useCalcTotalTurnAvatar(avatarId: number) { export function useCalcTotalTurnAvatar(avatarId: number) {
const { turnHistory } = useBattleDataStore.getState(); const { turnHistory } = useBattleDataStore.getState();
return useMemo(() => { return useMemo(() => {
const uniqueActionValues = new Set<number>(); const uniqueActionValues = new Set<number>();
turnHistory.forEach(turn => { turnHistory.forEach(turn => {
if (turn.avatarId === avatarId) { if (turn.avatarId === avatarId) {
uniqueActionValues.add(turn.actionValue); uniqueActionValues.add(turn.actionValue);
} }
}); });
return uniqueActionValues.size; return uniqueActionValues.size;
}, [avatarId, turnHistory]); }, [avatarId, turnHistory]);
} }
+3 -3
View File
@@ -1,4 +1,4 @@
import { useContext } from "react"; import { useContext } from "react";
import { ThemeContext } from "@/components/themeController"; import { ThemeContext } from "@/components/themeController";
export const useChangeTheme = () => useContext(ThemeContext); export const useChangeTheme = () => useContext(ThemeContext);
+31 -31
View File
@@ -1,31 +1,31 @@
import useBattleDataStore from "@/stores/battleDataStore"; import useBattleDataStore from "@/stores/battleDataStore";
import { useMemo } from "react"; import { useMemo } from "react";
export function useDamageLineForOne(avatarId: number, mode: 0 | 1 = 0) { export function useDamageLineForOne(avatarId: number, mode: 0 | 1 = 0) {
const { skillHistory, turnHistory } = useBattleDataStore.getState(); const { skillHistory, turnHistory } = useBattleDataStore.getState();
return useMemo(() => { return useMemo(() => {
const map = new Map<number, number>(); const map = new Map<number, number>();
for (const skill of skillHistory) { for (const skill of skillHistory) {
if (skill.avatarId !== avatarId) continue; if (skill.avatarId !== avatarId) continue;
const actionValue = turnHistory[skill.turnBattleId].actionValue const actionValue = turnHistory[skill.turnBattleId].actionValue
const prev = map.get(actionValue) || 0; const prev = map.get(actionValue) || 0;
map.set(actionValue, prev + skill.totalDamage); map.set(actionValue, prev + skill.totalDamage);
} }
const points = Array.from(map.entries()) const points = Array.from(map.entries())
.map(([x, y]) => ({ x, y })) .map(([x, y]) => ({ x, y }))
.sort((a, b) => a.x - b.x); .sort((a, b) => a.x - b.x);
if (mode === 1) { if (mode === 1) {
let cumulative = 0; let cumulative = 0;
return points.map(p => { return points.map(p => {
cumulative += p.y; cumulative += p.y;
return { x: p.x, y: Number(cumulative.toFixed(2)) }; return { x: p.x, y: Number(cumulative.toFixed(2)) };
}); });
} }
return points.map(p => ({ x: p.x, y: Number(p.y.toFixed(2)) })); return points.map(p => ({ x: p.x, y: Number(p.y.toFixed(2)) }));
}, [avatarId, skillHistory, turnHistory, mode]); }, [avatarId, skillHistory, turnHistory, mode]);
} }
+44 -44
View File
@@ -1,44 +1,44 @@
import useBattleDataStore from "@/stores/battleDataStore"; import useBattleDataStore from "@/stores/battleDataStore";
import { useMemo } from "react"; import { useMemo } from "react";
export function useDamageLinesForAll(mode: 1 | 2 = 1) { export function useDamageLinesForAll(mode: 1 | 2 = 1) {
const { turnHistory, skillHistory } = useBattleDataStore.getState(); const { turnHistory, skillHistory } = useBattleDataStore.getState();
return useMemo(() => { return useMemo(() => {
const avatarMap = new Map<number, Map<number, number>>(); const avatarMap = new Map<number, Map<number, number>>();
for (const skill of skillHistory) { for (const skill of skillHistory) {
if (!avatarMap.has(skill.avatarId)) { if (!avatarMap.has(skill.avatarId)) {
avatarMap.set(skill.avatarId, new Map()); avatarMap.set(skill.avatarId, new Map());
} }
const charMap = avatarMap.get(skill.avatarId)!; const charMap = avatarMap.get(skill.avatarId)!;
const actionValue = turnHistory[skill.turnBattleId].actionValue const actionValue = turnHistory[skill.turnBattleId].actionValue
const prev = charMap.get(actionValue) || 0; const prev = charMap.get(actionValue) || 0;
charMap.set(actionValue, prev + skill.totalDamage); charMap.set(actionValue, prev + skill.totalDamage);
} }
const result: Record<number, { x: number; y: number }[]> = {}; const result: Record<number, { x: number; y: number }[]> = {};
for (const [avatarId, damageMap] of avatarMap.entries()) { for (const [avatarId, damageMap] of avatarMap.entries()) {
const points = Array.from(damageMap.entries()) const points = Array.from(damageMap.entries())
.map(([x, y]) => ({ x, y })) .map(([x, y]) => ({ x, y }))
.sort((a, b) => a.x - b.x); .sort((a, b) => a.x - b.x);
if (mode === 1) { if (mode === 1) {
let cumulative = 0; let cumulative = 0;
result[avatarId] = points.map(p => { result[avatarId] = points.map(p => {
cumulative += Number(p.y); cumulative += Number(p.y);
return { x: p.x, y: Number(cumulative.toFixed(2)) }; return { x: p.x, y: Number(cumulative.toFixed(2)) };
}); });
} else { } else {
result[avatarId] = points.map(p => ({ result[avatarId] = points.map(p => ({
x: p.x, x: p.x,
y: Number(p.y.toFixed(2)), y: Number(p.y.toFixed(2)),
})); }));
} }
} }
return result; return result;
}, [turnHistory, skillHistory, mode]); }, [turnHistory, skillHistory, mode]);
} }
+68 -68
View File
@@ -1,69 +1,69 @@
import useBattleDataStore from "@/stores/battleDataStore"; import useBattleDataStore from "@/stores/battleDataStore";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useMemo } from "react"; import { useMemo } from "react";
type Mode = 0 | 1 | 2; type Mode = 0 | 1 | 2;
export function useDamagePerCycleForOne(avatarId: number, mode: Mode) { export function useDamagePerCycleForOne(avatarId: number, mode: Mode) {
const { skillHistory, turnHistory } = useBattleDataStore.getState(); const { skillHistory, turnHistory } = useBattleDataStore.getState();
const transI18n = useTranslations("DataAnalysisPage"); const transI18n = useTranslations("DataAnalysisPage");
return useMemo(() => { return useMemo(() => {
const damageMap = new Map<string, number>(); const damageMap = new Map<string, number>();
skillHistory skillHistory
.filter(s => s.avatarId === avatarId) .filter(s => s.avatarId === avatarId)
.forEach(s => { .forEach(s => {
const turn = turnHistory[s.turnBattleId]; const turn = turnHistory[s.turnBattleId];
if (!turn) return; if (!turn) return;
let key = ''; let key = '';
if (mode === 0) { if (mode === 0) {
key = `${transI18n('cycle')} ${turn.cycleIndex} - ${transI18n('wave')} ${turn.waveIndex}`; key = `${transI18n('cycle')} ${turn.cycleIndex} - ${transI18n('wave')} ${turn.waveIndex}`;
} else if (mode === 1) { } else if (mode === 1) {
key = `${transI18n('cycle')} ${turn.cycleIndex}`; key = `${transI18n('cycle')} ${turn.cycleIndex}`;
} else if (mode === 2) { } else if (mode === 2) {
key = `${transI18n('wave')} ${turn.waveIndex}`; key = `${transI18n('wave')} ${turn.waveIndex}`;
} }
damageMap.set(key, (damageMap.get(key) || 0) + s.totalDamage); damageMap.set(key, (damageMap.get(key) || 0) + s.totalDamage);
}); });
const result = Array.from(damageMap.entries()) const result = Array.from(damageMap.entries())
.map(([x, y]) => ({ x, y })) .map(([x, y]) => ({ x, y }))
.sort((a, b) => a.x.localeCompare(b.x, undefined, { numeric: true })); .sort((a, b) => a.x.localeCompare(b.x, undefined, { numeric: true }));
return result; return result;
}, [avatarId, mode, skillHistory, turnHistory, transI18n]); }, [avatarId, mode, skillHistory, turnHistory, transI18n]);
} }
export function useDamagePerCycleForAll(mode: Mode) { export function useDamagePerCycleForAll(mode: Mode) {
const { skillHistory, turnHistory } = useBattleDataStore.getState(); const { skillHistory, turnHistory } = useBattleDataStore.getState();
const transI18n = useTranslations("DataAnalysisPage"); const transI18n = useTranslations("DataAnalysisPage");
return useMemo(() => { return useMemo(() => {
const damageMap = new Map<string, number>(); const damageMap = new Map<string, number>();
skillHistory.forEach(s => { skillHistory.forEach(s => {
const turn = turnHistory[s.turnBattleId]; const turn = turnHistory[s.turnBattleId];
if (!turn) return; if (!turn) return;
let key = ''; let key = '';
if (mode === 0) { if (mode === 0) {
key = `${transI18n('cycle')} ${turn.cycleIndex} - ${transI18n('wave')} ${turn.waveIndex}`; key = `${transI18n('cycle')} ${turn.cycleIndex} - ${transI18n('wave')} ${turn.waveIndex}`;
} else if (mode === 1) { } else if (mode === 1) {
key = `${transI18n('cycle')} ${turn.cycleIndex}`; key = `${transI18n('cycle')} ${turn.cycleIndex}`;
} else if (mode === 2) { } else if (mode === 2) {
key = `${transI18n('wave')} ${turn.waveIndex}`; key = `${transI18n('wave')} ${turn.waveIndex}`;
} }
damageMap.set(key, (damageMap.get(key) || 0) + s.totalDamage); damageMap.set(key, (damageMap.get(key) || 0) + s.totalDamage);
}); });
const result = Array.from(damageMap.entries()) const result = Array.from(damageMap.entries())
.map(([x, y]) => ({ x, y })) .map(([x, y]) => ({ x, y }))
.sort((a, b) => a.x.localeCompare(b.x, undefined, { numeric: true })); .sort((a, b) => a.x.localeCompare(b.x, undefined, { numeric: true }));
return result; return result;
}, [mode, skillHistory, turnHistory, transI18n]); }, [mode, skillHistory, turnHistory, transI18n]);
} }
+26 -26
View File
@@ -1,26 +1,26 @@
import { useMemo } from "react"; import { useMemo } from "react";
import useBattleDataStore from "@/stores/battleDataStore"; import useBattleDataStore from "@/stores/battleDataStore";
import {attackTypeToString} from "@/types"; import {attackTypeToString} from "@/types";
export function useDamagePercentByType() { export function useDamagePercentByType() {
const { skillHistory } = useBattleDataStore.getState(); const { skillHistory } = useBattleDataStore.getState();
return useMemo(() => { return useMemo(() => {
const dmgByType = new Map<string, number>(); const dmgByType = new Map<string, number>();
skillHistory.forEach(skill => { skillHistory.forEach(skill => {
skill.damageDetail.forEach(detail => { skill.damageDetail.forEach(detail => {
const type = detail.damage_type; const type = detail.damage_type;
if (!type) return; if (!type) return;
dmgByType.set(attackTypeToString(type), (dmgByType.get(attackTypeToString(type)) || 0) + detail.damage); dmgByType.set(attackTypeToString(type), (dmgByType.get(attackTypeToString(type)) || 0) + detail.damage);
}); });
}); });
const totalDmg = Array.from(dmgByType.values()).reduce((sum, val) => sum + val, 0); const totalDmg = Array.from(dmgByType.values()).reduce((sum, val) => sum + val, 0);
return Array.from(dmgByType.entries()).map(([type, dmg]) => ({ return Array.from(dmgByType.entries()).map(([type, dmg]) => ({
type, type,
percent: totalDmg > 0 ? (dmg / totalDmg) * 100 : 0, percent: totalDmg > 0 ? (dmg / totalDmg) * 100 : 0,
})); }));
}, [skillHistory]); }, [skillHistory]);
} }
+21 -21
View File
@@ -1,21 +1,21 @@
import useBattleDataStore from "@/stores/battleDataStore"; import useBattleDataStore from "@/stores/battleDataStore";
import { useMemo } from "react"; import { useMemo } from "react";
export function useDamagePercentPerAvatar() { export function useDamagePercentPerAvatar() {
const { skillHistory } = useBattleDataStore.getState(); const { skillHistory } = useBattleDataStore.getState();
return useMemo(() => { return useMemo(() => {
const dmgByAvatar = new Map<number, number>(); const dmgByAvatar = new Map<number, number>();
skillHistory.forEach(t => { skillHistory.forEach(t => {
dmgByAvatar.set(t.avatarId, (dmgByAvatar.get(t.avatarId) || 0) + t.totalDamage); dmgByAvatar.set(t.avatarId, (dmgByAvatar.get(t.avatarId) || 0) + t.totalDamage);
}); });
const totalDmg = Array.from(dmgByAvatar.values()).reduce((sum, v) => sum + v, 0); const totalDmg = Array.from(dmgByAvatar.values()).reduce((sum, v) => sum + v, 0);
return Array.from(dmgByAvatar.entries()).map(([avatarId, dmg]) => ({ return Array.from(dmgByAvatar.entries()).map(([avatarId, dmg]) => ({
avatarId, avatarId,
percent: totalDmg > 0 ? (dmg / totalDmg) * 100 : 0, percent: totalDmg > 0 ? (dmg / totalDmg) * 100 : 0,
})); }));
}, [skillHistory]); }, [skillHistory]);
} }
+26 -26
View File
@@ -1,26 +1,26 @@
"use client"; "use client";
import { useMemo } from "react"; import { useMemo } from "react";
import useBattleDataStore from "@/stores/battleDataStore"; import useBattleDataStore from "@/stores/battleDataStore";
import {attackTypeToString} from "@/types"; import {attackTypeToString} from "@/types";
export function useDamageByTypeForAvatar(avatarId: number) { export function useDamageByTypeForAvatar(avatarId: number) {
const { skillHistory } = useBattleDataStore.getState(); const { skillHistory } = useBattleDataStore.getState();
return useMemo(() => { return useMemo(() => {
const dmgMap = new Map<string, number>(); const dmgMap = new Map<string, number>();
for (const skill of skillHistory) { for (const skill of skillHistory) {
if (skill.avatarId !== avatarId) continue; if (skill.avatarId !== avatarId) continue;
for (const damage of skill.damageDetail) { for (const damage of skill.damageDetail) {
const type = attackTypeToString(damage?.damage_type)?.toLowerCase() || ""; const type = attackTypeToString(damage?.damage_type)?.toLowerCase() || "";
dmgMap.set(type, (dmgMap.get(type) || 0) + damage.damage); dmgMap.set(type, (dmgMap.get(type) || 0) + damage.damage);
} }
} }
const labels = Array.from(dmgMap.keys()); const labels = Array.from(dmgMap.keys());
const damageValues = Array.from(dmgMap.values()); const damageValues = Array.from(dmgMap.values());
return { labels, damageValues }; return { labels, damageValues };
}, [avatarId, skillHistory]); }, [avatarId, skillHistory]);
} }
+24 -24
View File
@@ -1,24 +1,24 @@
"use client"; "use client";
import { useMemo } from "react"; import { useMemo } from "react";
import useBattleDataStore from "@/stores/battleDataStore"; import useBattleDataStore from "@/stores/battleDataStore";
import {attackTypeToString} from "@/types"; import {attackTypeToString} from "@/types";
export function useSkillDamageForAvatar(avatarId: number) { export function useSkillDamageForAvatar(avatarId: number) {
const { skillHistory } = useBattleDataStore.getState(); const { skillHistory } = useBattleDataStore.getState();
return useMemo(() => { return useMemo(() => {
const dmgMap = new Map<string, number>(); const dmgMap = new Map<string, number>();
for (const skill of skillHistory) { for (const skill of skillHistory) {
if (skill.avatarId !== avatarId) continue; if (skill.avatarId !== avatarId) continue;
const type = attackTypeToString(skill.skillType).toLowerCase(); const type = attackTypeToString(skill.skillType).toLowerCase();
dmgMap.set(type, (dmgMap.get(type) || 0) + skill.totalDamage); dmgMap.set(type, (dmgMap.get(type) || 0) + skill.totalDamage);
} }
const labels = Array.from(dmgMap.keys()); const labels = Array.from(dmgMap.keys());
const damageValues = Array.from(dmgMap.values()); const damageValues = Array.from(dmgMap.values());
return { labels, damageValues }; return { labels, damageValues };
}, [avatarId, skillHistory]); }, [avatarId, skillHistory]);
} }
+51 -51
View File
@@ -1,51 +1,51 @@
import useSocketStore from "@/stores/socketSettingStore"; import useSocketStore from "@/stores/socketSettingStore";
import { CharacterBasic, MonsterBasic } from "@/types"; import { CharacterBasic, MonsterBasic } from "@/types";
import axios from 'axios'; import axios from 'axios';
export async function checkConnectTcpApi(): Promise<boolean> { export async function checkConnectTcpApi(): Promise<boolean> {
const { host, port, connectionType } = useSocketStore.getState() const { host, port, connectionType } = useSocketStore.getState()
let url = `${host}:${port}/check-tcp` let url = `${host}:${port}/check-tcp`
if (connectionType === "PS") { if (connectionType === "PS") {
url = "http://localhost:21000/check-tcp" url = "http://localhost:21000/check-tcp"
} }
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}); });
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
return data.status return data.status
} }
return false return false
} }
export async function getCharacterListApi(): Promise<CharacterBasic[]> { export async function getCharacterListApi(): Promise<CharacterBasic[]> {
try { try {
const res = await axios.get<CharacterBasic[]>("/api/data/avatar_basic"); const res = await axios.get<CharacterBasic[]>("/api/data/avatar_basic");
return res.data return res.data
} catch (error: unknown) { } catch (error: unknown) {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`); console.log(`Error: ${error.response?.status} - ${error.message}`);
} else { } else {
console.log(`Unexpected error: ${String(error)}`); console.log(`Unexpected error: ${String(error)}`);
} }
return []; return [];
} }
} }
export async function getEnemyListApi(): Promise<MonsterBasic[]> { export async function getEnemyListApi(): Promise<MonsterBasic[]> {
try { try {
const res = await axios.get<MonsterBasic[]>("/api/data/monster_basic"); const res = await axios.get<MonsterBasic[]>("/api/data/monster_basic");
return res.data return res.data
} catch (error: unknown) { } catch (error: unknown) {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`); console.log(`Error: ${error.response?.status} - ${error.message}`);
} else { } else {
console.log(`Unexpected error: ${String(error)}`); console.log(`Unexpected error: ${String(error)}`);
} }
return []; return [];
} }
} }
+37 -37
View File
@@ -1,38 +1,38 @@
import { readFileSync, readdirSync } from "fs" import { readFileSync, readdirSync } from "fs"
import path from "path" import path from "path"
type CacheItem = { type CacheItem = {
buf: Uint8Array buf: Uint8Array
type: "json" | "br" type: "json" | "br"
} }
const cache = new Map<string, CacheItem>() const cache = new Map<string, CacheItem>()
const dir = path.join(process.cwd(), "data") const dir = path.join(process.cwd(), "data")
for (const f of readdirSync(dir)) { for (const f of readdirSync(dir)) {
const file = path.join(dir, f) const file = path.join(dir, f)
if (f.endsWith(".json.br")) { if (f.endsWith(".json.br")) {
const name = f.replace(".json.br", "") const name = f.replace(".json.br", "")
const buf = new Uint8Array(readFileSync(file)) const buf = new Uint8Array(readFileSync(file))
cache.set(name, { cache.set(name, {
buf, buf,
type: "br" type: "br"
}) })
} }
if (f.endsWith(".json")) { if (f.endsWith(".json")) {
const name = f.replace(".json", "") const name = f.replace(".json", "")
const buf = new Uint8Array(readFileSync(file)) const buf = new Uint8Array(readFileSync(file))
cache.set(name, { cache.set(name, {
buf, buf,
type: "json" type: "json"
}) })
} }
} }
export function getDataCache(name: string) { export function getDataCache(name: string) {
return cache.get(name) return cache.get(name)
} }
+6 -6
View File
@@ -1,7 +1,7 @@
export const listCurrentLanguage = { export const listCurrentLanguage = {
ja: "JP", ja: "JP",
ko: "KR", ko: "KR",
en: "US", en: "US",
vi: "VN", vi: "VN",
zh: "CN" zh: "CN"
}; };
+255 -255
View File
@@ -1,255 +1,255 @@
import { io, Socket } from "socket.io-client"; import { io, Socket } from "socket.io-client";
import useSocketStore from "@/stores/socketSettingStore"; import useSocketStore from "@/stores/socketSettingStore";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import useBattleDataStore from "@/stores/battleDataStore"; import useBattleDataStore from "@/stores/battleDataStore";
import type { import type {
AvatarAnalysisJson, AvatarAnalysisJson,
BattleBeginType, BattleBeginType,
BattleEndType, BattleEndType,
DamageType, DamageType,
EntityDefeatedType, EntityDefeatedType,
InitializeEnemyType, InitializeEnemyType,
SetBattleLineupType, SetBattleLineupType,
StatChangeType, StatChangeType,
TurnBeginType, TurnBeginType,
TurnEndType, TurnEndType,
UpdateCycleType, UpdateCycleType,
UpdateTeamFormationType, UpdateTeamFormationType,
UpdateWaveType, UpdateWaveType,
UseSkillType, UseSkillType,
VersionType, VersionType,
} from "@/types"; } from "@/types";
let socket: Socket | null = null; let socket: Socket | null = null;
const notify = (msg: string, type: "info" | "success" | "error" = "info") => { const notify = (msg: string, type: "info" | "success" | "error" = "info") => {
if (type === "success") toast.success(msg); if (type === "success") toast.success(msg);
else if (type === "error") toast.error(msg); else if (type === "error") toast.error(msg);
else toast.info(msg); else toast.info(msg);
}; };
interface SocketEvents { interface SocketEvents {
Connected: VersionType; Connected: VersionType;
OnBattleBegin: BattleBeginType; OnBattleBegin: BattleBeginType;
OnSetBattleLineup: SetBattleLineupType; OnSetBattleLineup: SetBattleLineupType;
OnDamage: DamageType; OnDamage: DamageType;
OnTurnBegin: TurnBeginType; OnTurnBegin: TurnBeginType;
OnTurnEnd: TurnEndType; OnTurnEnd: TurnEndType;
OnEntityDefeated: EntityDefeatedType; OnEntityDefeated: EntityDefeatedType;
OnUseSkill: UseSkillType; OnUseSkill: UseSkillType;
OnUpdateWave: UpdateWaveType; OnUpdateWave: UpdateWaveType;
OnUpdateCycle: UpdateCycleType; OnUpdateCycle: UpdateCycleType;
OnStatChange: StatChangeType; OnStatChange: StatChangeType;
OnUpdateTeamFormation: UpdateTeamFormationType; OnUpdateTeamFormation: UpdateTeamFormationType;
OnInitializeEnemy: InitializeEnemyType; OnInitializeEnemy: InitializeEnemyType;
OnBattleEnd: BattleEndType; OnBattleEnd: BattleEndType;
OnCreateBattle: AvatarAnalysisJson[]; OnCreateBattle: AvatarAnalysisJson[];
Error: string; Error: string;
} }
type ListenerMap = { type ListenerMap = {
[K in keyof SocketEvents]: (payload: SocketEvents[K]) => void; [K in keyof SocketEvents]: (payload: SocketEvents[K]) => void;
}; };
let listeners: ListenerMap | null = null; let listeners: ListenerMap | null = null;
let workerPool: Worker[] | null = null; let workerPool: Worker[] | null = null;
let nextWorker = 0; let nextWorker = 0;
let reqId = 1; let reqId = 1;
const pendingMap = new Map<number, (res: { parsed: unknown | null; err?: string }) => void>(); const pendingMap = new Map<number, (res: { parsed: unknown | null; err?: string }) => void>();
let workerBlobUrl: string | null = null; let workerBlobUrl: string | null = null;
const createWorkerPool = (size = Math.max(1, (navigator.hardwareConcurrency || 4) - 1)) => { const createWorkerPool = (size = Math.max(1, (navigator.hardwareConcurrency || 4) - 1)) => {
if (workerPool) return workerPool; if (workerPool) return workerPool;
const code = `self.onmessage = function(e) { try { const { id, raw } = e.data; const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw; self.postMessage({ id, parsed }); } catch (err) { self.postMessage({ id, err: err && err.message ? err.message : String(err) }); } }`; const code = `self.onmessage = function(e) { try { const { id, raw } = e.data; const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw; self.postMessage({ id, parsed }); } catch (err) { self.postMessage({ id, err: err && err.message ? err.message : String(err) }); } }`;
if (!workerBlobUrl) { if (!workerBlobUrl) {
const blob = new Blob([code], { type: 'application/javascript' }); const blob = new Blob([code], { type: 'application/javascript' });
workerBlobUrl = URL.createObjectURL(blob); workerBlobUrl = URL.createObjectURL(blob);
} }
workerPool = Array.from({ length: size }).map(() => new Worker(workerBlobUrl!)); workerPool = Array.from({ length: size }).map(() => new Worker(workerBlobUrl!));
workerPool.forEach((w) => { workerPool.forEach((w) => {
w.onmessage = (ev) => { w.onmessage = (ev) => {
const { id, parsed, err } = ev.data as { id: number; parsed: unknown; err?: string }; const { id, parsed, err } = ev.data as { id: number; parsed: unknown; err?: string };
const cb = pendingMap.get(id); const cb = pendingMap.get(id);
if (cb) { if (cb) {
cb({ parsed: parsed ?? null, err }); cb({ parsed: parsed ?? null, err });
pendingMap.delete(id); pendingMap.delete(id);
} }
}; };
w.onerror = (ev) => { w.onerror = (ev) => {
console.error('worker error', ev.message); console.error('worker error', ev.message);
}; };
}); });
return workerPool; return workerPool;
}; };
const parseWithWorker = (raw: unknown): Promise<{ parsed: unknown | null; err?: string }> => { const parseWithWorker = (raw: unknown): Promise<{ parsed: unknown | null; err?: string }> => {
if (!workerPool) createWorkerPool(); if (!workerPool) createWorkerPool();
if (!workerPool) return Promise.resolve({ parsed: null, err: 'no worker' }); if (!workerPool) return Promise.resolve({ parsed: null, err: 'no worker' });
const id = reqId++; const id = reqId++;
return new Promise((resolve) => { return new Promise((resolve) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
pendingMap.delete(id); pendingMap.delete(id);
resolve({ parsed: null, err: "worker timeout" }); resolve({ parsed: null, err: "worker timeout" });
}, 3000); }, 3000);
pendingMap.set(id, (res) => { pendingMap.set(id, (res) => {
clearTimeout(timeout); clearTimeout(timeout);
resolve(res); resolve(res);
}); });
const w = workerPool![nextWorker]; const w = workerPool![nextWorker];
nextWorker = (nextWorker + 1) % workerPool!.length; nextWorker = (nextWorker + 1) % workerPool!.length;
try { try {
w.postMessage({ id, raw }); w.postMessage({ id, raw });
} catch (err) { } catch (err) {
pendingMap.delete(id); pendingMap.delete(id);
resolve({ parsed: null, err: err && (err as Error).message ? (err as Error).message : String(err) }); resolve({ parsed: null, err: err && (err as Error).message ? (err as Error).message : String(err) });
} }
}); });
}; };
let eventQueue: { name: keyof SocketEvents; data: unknown }[] = []; let eventQueue: { name: keyof SocketEvents; data: unknown }[] = [];
let rafScheduled = false; let rafScheduled = false;
const MAX_QUEUE = 1000; const MAX_QUEUE = 1000;
const flushQueue = () => { const flushQueue = () => {
const items = eventQueue.splice(0, eventQueue.length); const items = eventQueue.splice(0, eventQueue.length);
if (items.length === 0) return; if (items.length === 0) return;
const l = listeners; const l = listeners;
if (!l) return; if (!l) return;
for (const it of items) { for (const it of items) {
try { try {
l[it.name](it.data as never); l[it.name](it.data as never);
} catch (err) { } catch (err) {
console.error('listener handler error', err); console.error('listener handler error', err);
} }
} }
}; };
const scheduleFlush = () => { const scheduleFlush = () => {
if (rafScheduled) return; if (rafScheduled) return;
rafScheduled = true; rafScheduled = true;
requestAnimationFrame(() => { requestAnimationFrame(() => {
rafScheduled = false; rafScheduled = false;
flushQueue(); flushQueue();
}); });
}; };
const safeEnqueue = (name: keyof SocketEvents, data: unknown) => { const safeEnqueue = (name: keyof SocketEvents, data: unknown) => {
if (eventQueue.length > MAX_QUEUE) { if (eventQueue.length > MAX_QUEUE) {
eventQueue.shift(); eventQueue.shift();
} }
eventQueue.push({ name, data }); eventQueue.push({ name, data });
scheduleFlush(); scheduleFlush();
}; };
export const connectSocket = async (): Promise<Socket> => { export const connectSocket = async (): Promise<Socket> => {
const { host, port, connectionType, setStatus } = useSocketStore.getState(); const { host, port, connectionType, setStatus } = useSocketStore.getState();
let url = `${host}:${port}`; let url = `${host}:${port}`;
if (connectionType === "Native") url = "http://localhost:1305"; if (connectionType === "Native") url = "http://localhost:1305";
else if (connectionType === "PS") url = "http://localhost:21000"; else if (connectionType === "PS") url = "http://localhost:21000";
if (socket) socket.disconnect(); if (socket) socket.disconnect();
socket = io(url, { socket = io(url, {
reconnectionAttempts: 5, reconnectionAttempts: 5,
timeout: 10000, timeout: 10000,
autoConnect: true, autoConnect: true,
}); });
socket.on("connect", () => { socket.on("connect", () => {
setStatus(true); setStatus(true);
notify(`Connected: ${socket?.id}`, "success"); notify(`Connected: ${socket?.id}`, "success");
}); });
socket.on("disconnect", () => setStatus(false)); socket.on("disconnect", () => setStatus(false));
socket.on("connect_error", () => setStatus(false)); socket.on("connect_error", () => setStatus(false));
socket.on("connect_timeout", () => setStatus(false)); socket.on("connect_timeout", () => setStatus(false));
socket.on("reconnect_failed", () => setStatus(false)); socket.on("reconnect_failed", () => setStatus(false));
listeners = { listeners = {
Connected: (payload) => useBattleDataStore.getState().onConnectedService(payload), Connected: (payload) => useBattleDataStore.getState().onConnectedService(payload),
OnBattleBegin: (payload) => { OnBattleBegin: (payload) => {
notify("Battle Started!", "info"); notify("Battle Started!", "info");
useBattleDataStore.getState().onBattleBeginService(payload); useBattleDataStore.getState().onBattleBeginService(payload);
}, },
OnSetBattleLineup: (payload) => useBattleDataStore.getState().onSetBattleLineupService(payload), OnSetBattleLineup: (payload) => useBattleDataStore.getState().onSetBattleLineupService(payload),
OnDamage: (payload) => useBattleDataStore.getState().onDamageService(payload), OnDamage: (payload) => useBattleDataStore.getState().onDamageService(payload),
OnTurnBegin: (payload) => useBattleDataStore.getState().onTurnBeginService(payload), OnTurnBegin: (payload) => useBattleDataStore.getState().onTurnBeginService(payload),
OnTurnEnd: (payload) => useBattleDataStore.getState().onTurnEndService(payload), OnTurnEnd: (payload) => useBattleDataStore.getState().onTurnEndService(payload),
OnEntityDefeated: (payload) => useBattleDataStore.getState().onEntityDefeatedService(payload), OnEntityDefeated: (payload) => useBattleDataStore.getState().onEntityDefeatedService(payload),
OnUseSkill: (payload) => useBattleDataStore.getState().onUseSkillService(payload), OnUseSkill: (payload) => useBattleDataStore.getState().onUseSkillService(payload),
OnUpdateWave: (payload) => useBattleDataStore.getState().onUpdateWaveService(payload), OnUpdateWave: (payload) => useBattleDataStore.getState().onUpdateWaveService(payload),
OnUpdateCycle: (payload) => useBattleDataStore.getState().onUpdateCycleService(payload), OnUpdateCycle: (payload) => useBattleDataStore.getState().onUpdateCycleService(payload),
OnStatChange: (payload) => useBattleDataStore.getState().onStatChange(payload), OnStatChange: (payload) => useBattleDataStore.getState().onStatChange(payload),
OnUpdateTeamFormation: (payload) => useBattleDataStore.getState().onUpdateTeamFormation(payload), OnUpdateTeamFormation: (payload) => useBattleDataStore.getState().onUpdateTeamFormation(payload),
OnInitializeEnemy: (payload) => useBattleDataStore.getState().onInitializeEnemyService(payload), OnInitializeEnemy: (payload) => useBattleDataStore.getState().onInitializeEnemyService(payload),
OnBattleEnd: (payload) => useBattleDataStore.getState().onBattleEndService(payload), OnBattleEnd: (payload) => useBattleDataStore.getState().onBattleEndService(payload),
OnCreateBattle: (payload) => useBattleDataStore.getState().onCreateBattleService(payload), OnCreateBattle: (payload) => useBattleDataStore.getState().onCreateBattleService(payload),
Error: (msg) => console.error("Server Error:", msg), Error: (msg) => console.error("Server Error:", msg),
}; };
const register = (eventName: string) => { const register = (eventName: string) => {
socket?.on(eventName, async (raw: unknown) => { socket?.on(eventName, async (raw: unknown) => {
if (!listeners) return; if (!listeners) return;
if (workerPool) { if (workerPool) {
const res = await parseWithWorker(raw); const res = await parseWithWorker(raw);
if (res.err) { if (res.err) {
safeEnqueue("Error", `Worker parse error for ${eventName}: ${res.err}`); safeEnqueue("Error", `Worker parse error for ${eventName}: ${res.err}`);
} else if (res.parsed !== null) { } else if (res.parsed !== null) {
safeEnqueue(eventName as keyof SocketEvents, res.parsed); safeEnqueue(eventName as keyof SocketEvents, res.parsed);
} }
} else { } else {
try { try {
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw; const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
safeEnqueue(eventName as keyof SocketEvents, parsed); safeEnqueue(eventName as keyof SocketEvents, parsed);
} catch (err) { } catch (err) {
safeEnqueue("Error", `Parse error for ${eventName}: ${err && (err as Error).message ? (err as Error).message : String(err)}`); safeEnqueue("Error", `Parse error for ${eventName}: ${err && (err as Error).message ? (err as Error).message : String(err)}`);
} }
} }
}); });
}; };
Object.keys(listeners).forEach((k) => register(k)); Object.keys(listeners).forEach((k) => register(k));
createWorkerPool(); createWorkerPool();
return socket; return socket;
}; };
export const disconnectSocket = (): void => { export const disconnectSocket = (): void => {
if (!socket) return; if (!socket) return;
if (listeners) { if (listeners) {
Object.keys(listeners).forEach((eventName) => { Object.keys(listeners).forEach((eventName) => {
socket?.off(eventName); socket?.off(eventName);
}); });
} }
if (workerBlobUrl) { if (workerBlobUrl) {
URL.revokeObjectURL(workerBlobUrl); URL.revokeObjectURL(workerBlobUrl);
workerBlobUrl = null; workerBlobUrl = null;
} }
socket.disconnect(); socket.disconnect();
useSocketStore.getState().setStatus(false); useSocketStore.getState().setStatus(false);
listeners = null; listeners = null;
if (workerPool) { if (workerPool) {
workerPool.forEach((w) => w.terminate()); workerPool.forEach((w) => w.terminate());
workerPool = null; workerPool = null;
} }
pendingMap.clear(); pendingMap.clear();
eventQueue = []; eventQueue = [];
}; };
export const isSocketConnected = (): boolean => socket?.connected || false; export const isSocketConnected = (): boolean => socket?.connected || false;
export const getSocket = (): Socket | null => socket; export const getSocket = (): Socket | null => socket;
export const setWorkerPoolSize = (size: number) => { export const setWorkerPoolSize = (size: number) => {
if (workerPool) { if (workerPool) {
workerPool.forEach((w) => w.terminate()); workerPool.forEach((w) => w.terminate());
workerPool = null; workerPool = null;
} }
createWorkerPool(Math.max(1, size)); createWorkerPool(Math.max(1, size));
}; };
+29 -29
View File
@@ -1,30 +1,30 @@
import { CharacterBasic, MonsterBasic } from '@/types'; import { CharacterBasic, MonsterBasic } from '@/types';
import { create } from 'zustand' import { create } from 'zustand'
interface AvatarDataState { interface AvatarDataState {
listAvatar: CharacterBasic[]; listAvatar: CharacterBasic[];
mapAvatar: Record<string, CharacterBasic>; mapAvatar: Record<string, CharacterBasic>;
listEnemy: MonsterBasic[]; listEnemy: MonsterBasic[];
mapEnemy: Record<string, MonsterBasic>; mapEnemy: Record<string, MonsterBasic>;
setListAvatar: (list: CharacterBasic[]) => void; setListAvatar: (list: CharacterBasic[]) => void;
setMapAvatar: (data: Record<string, CharacterBasic>) => void; setMapAvatar: (data: Record<string, CharacterBasic>) => void;
setListEnemy: (list: MonsterBasic[]) => void; setListEnemy: (list: MonsterBasic[]) => void;
setMapEnemy: (data: Record<string, MonsterBasic>) => void; setMapEnemy: (data: Record<string, MonsterBasic>) => void;
} }
const useAvatarDataStore = create<AvatarDataState>((set) => ({ const useAvatarDataStore = create<AvatarDataState>((set) => ({
listAvatar: [], listAvatar: [],
listEnemy: [], listEnemy: [],
mapAvatar: {}, mapAvatar: {},
mapEnemy: {}, mapEnemy: {},
setListAvatar: (list: CharacterBasic[]) => set({ listAvatar: list }), setListAvatar: (list: CharacterBasic[]) => set({ listAvatar: list }),
setMapAvatar: (data: Record<string, CharacterBasic>) => set({ mapAvatar: data }), setMapAvatar: (data: Record<string, CharacterBasic>) => set({ mapAvatar: data }),
setListEnemy: (list: MonsterBasic[]) => set({ listEnemy: list }), setListEnemy: (list: MonsterBasic[]) => set({ listEnemy: list }),
setMapEnemy: (data: Record<string, MonsterBasic>) => set({ mapEnemy: data }) setMapEnemy: (data: Record<string, MonsterBasic>) => set({ mapEnemy: data })
})); }));
export default useAvatarDataStore; export default useAvatarDataStore;
+368 -368
View File
@@ -1,369 +1,369 @@
import { DamageType, AvatarAnalysisJson, UseSkillType, BattleBeginType, BattleEndType, DamageDetailType, EntityDefeatedType, SetBattleLineupType, TurnBeginType, TurnEndType, UpdateCycleType, UpdateWaveType, VersionType, StatChangeType, UpdateTeamFormationType, Team, ParseAttackType } from '@/types'; import { DamageType, AvatarAnalysisJson, UseSkillType, BattleBeginType, BattleEndType, DamageDetailType, EntityDefeatedType, SetBattleLineupType, TurnBeginType, TurnEndType, UpdateCycleType, UpdateWaveType, VersionType, StatChangeType, UpdateTeamFormationType, Team, ParseAttackType } from '@/types';
import { InitializeEnemyType } from '@/types/enemy'; import { InitializeEnemyType } from '@/types/enemy';
import { AvatarBattleInfo, AvatarInfo, BattleDataStateJson, EnemyInfo, SkillBattleInfo, TurnBattleInfo } from '@/types/mics'; import { AvatarBattleInfo, AvatarInfo, BattleDataStateJson, EnemyInfo, SkillBattleInfo, TurnBattleInfo } from '@/types/mics';
import { create } from 'zustand' import { create } from 'zustand'
interface BattleDataState { interface BattleDataState {
lineup: AvatarBattleInfo[]; lineup: AvatarBattleInfo[];
turnHistory: TurnBattleInfo[] turnHistory: TurnBattleInfo[]
skillHistory: SkillBattleInfo[] skillHistory: SkillBattleInfo[]
dataAvatar: AvatarAnalysisJson[] dataAvatar: AvatarAnalysisJson[]
totalAV: number; totalAV: number;
totalDamage: number; totalDamage: number;
damagePerAV: number; damagePerAV: number;
cycleIndex: number; cycleIndex: number;
waveIndex: number; waveIndex: number;
maxWave: number; maxWave: number;
maxCycle: number; maxCycle: number;
version?: string; version?: string;
avatarDetail?: Record<number, AvatarInfo>; avatarDetail?: Record<number, AvatarInfo>;
enemyDetail?: Record<number, EnemyInfo>; enemyDetail?: Record<number, EnemyInfo>;
onConnectedService: (data: VersionType) => void onConnectedService: (data: VersionType) => void
onBattleBeginService: (data: BattleBeginType) => void; onBattleBeginService: (data: BattleBeginType) => void;
onSetBattleLineupService: (data: SetBattleLineupType) => void; onSetBattleLineupService: (data: SetBattleLineupType) => void;
onDamageService: (data: DamageType) => void; onDamageService: (data: DamageType) => void;
onTurnBeginService: (data: TurnBeginType) => void; onTurnBeginService: (data: TurnBeginType) => void;
onTurnEndService: (data: TurnEndType) => void; onTurnEndService: (data: TurnEndType) => void;
onEntityDefeatedService: (data: EntityDefeatedType) => void; onEntityDefeatedService: (data: EntityDefeatedType) => void;
onUseSkillService: (data: UseSkillType) => void; onUseSkillService: (data: UseSkillType) => void;
onUpdateWaveService: (data: UpdateWaveType) => void; onUpdateWaveService: (data: UpdateWaveType) => void;
onUpdateCycleService: (data: UpdateCycleType) => void; onUpdateCycleService: (data: UpdateCycleType) => void;
onStatChange: (data: StatChangeType) => void; onStatChange: (data: StatChangeType) => void;
onUpdateTeamFormation: (data: UpdateTeamFormationType) => void; onUpdateTeamFormation: (data: UpdateTeamFormationType) => void;
onInitializeEnemyService: (data: InitializeEnemyType) => void; onInitializeEnemyService: (data: InitializeEnemyType) => void;
onBattleEndService: (data: BattleEndType) => void; onBattleEndService: (data: BattleEndType) => void;
onCreateBattleService: (data: AvatarAnalysisJson[]) => void; onCreateBattleService: (data: AvatarAnalysisJson[]) => void;
loadBattleDataFromJSON: (data: BattleDataStateJson) => void; loadBattleDataFromJSON: (data: BattleDataStateJson) => void;
} }
const useBattleDataStore = create<BattleDataState>((set, get) => ({ const useBattleDataStore = create<BattleDataState>((set, get) => ({
lineup: [], lineup: [],
turnHistory: [], turnHistory: [],
skillHistory: [], skillHistory: [],
dataAvatar: [], dataAvatar: [],
totalAV: 0, totalAV: 0,
totalDamage: 0, totalDamage: 0,
damagePerAV: 0, damagePerAV: 0,
cycleIndex: 0, cycleIndex: 0,
waveIndex: 1, waveIndex: 1,
maxWave: Infinity, maxWave: Infinity,
maxCycle: Infinity, maxCycle: Infinity,
version: undefined, version: undefined,
avatarDetail: undefined, avatarDetail: undefined,
enemyDetail: undefined, enemyDetail: undefined,
loadBattleDataFromJSON: (data: BattleDataStateJson) => { loadBattleDataFromJSON: (data: BattleDataStateJson) => {
const skillHistory = data.skillHistory.map(it => { const skillHistory = data.skillHistory.map(it => {
it.damageDetail = it.damageDetail.map(it => { it.damageDetail = it.damageDetail.map(it => {
return { return {
...it, ...it,
damage_type: ParseAttackType(it.damage_type) damage_type: ParseAttackType(it.damage_type)
} }
}) })
return it return it
}) })
set({ set({
lineup: data.lineup, lineup: data.lineup,
turnHistory: data.turnHistory, turnHistory: data.turnHistory,
skillHistory: skillHistory, skillHistory: skillHistory,
dataAvatar: data.dataAvatar, dataAvatar: data.dataAvatar,
totalAV: data.totalAV, totalAV: data.totalAV,
totalDamage: data.totalDamage, totalDamage: data.totalDamage,
damagePerAV: data.damagePerAV, damagePerAV: data.damagePerAV,
cycleIndex: data.cycleIndex, cycleIndex: data.cycleIndex,
waveIndex: data.waveIndex, waveIndex: data.waveIndex,
maxWave: data.maxWave, maxWave: data.maxWave,
maxCycle: data.maxCycle, maxCycle: data.maxCycle,
version: data.version, version: data.version,
avatarDetail: data.avatarDetail, avatarDetail: data.avatarDetail,
enemyDetail: data.enemyDetail enemyDetail: data.enemyDetail
}) })
}, },
onConnectedService: (data: VersionType) => { onConnectedService: (data: VersionType) => {
set({ set({
version: data.version version: data.version
}) })
}, },
onCreateBattleService: (data: AvatarAnalysisJson[]) => { onCreateBattleService: (data: AvatarAnalysisJson[]) => {
set({ set({
dataAvatar: data dataAvatar: data
}) })
}, },
onBattleBeginService: (data: BattleBeginType) => { onBattleBeginService: (data: BattleBeginType) => {
set({ set({
maxWave: data.max_waves, maxWave: data.max_waves,
maxCycle: data.max_cycles maxCycle: data.max_cycles
}) })
}, },
onSetBattleLineupService: (data: SetBattleLineupType) => { onSetBattleLineupService: (data: SetBattleLineupType) => {
const lineups: AvatarBattleInfo[] = [] const lineups: AvatarBattleInfo[] = []
for (const avatar of data.avatars) { for (const avatar of data.avatars) {
lineups.push({ avatarId: avatar.id, isDie: false } as AvatarBattleInfo) lineups.push({ avatarId: avatar.id, isDie: false } as AvatarBattleInfo)
} }
set(() => ({ set(() => ({
lineup: lineups, lineup: lineups,
turnHistory: [{ turnHistory: [{
avatarId: -1, avatarId: -1,
actionValue: 0, actionValue: 0,
waveIndex: 1, waveIndex: 1,
cycleIndex: 0, cycleIndex: 0,
} as TurnBattleInfo], } as TurnBattleInfo],
skillHistory: [], skillHistory: [],
totalAV: 0, totalAV: 0,
totalDamage: 0, totalDamage: 0,
damagePerAV: 0, damagePerAV: 0,
cycleIndex: 0, cycleIndex: 0,
maxWave: Infinity, maxWave: Infinity,
maxCycle: Infinity, maxCycle: Infinity,
waveIndex: 1, waveIndex: 1,
})); }));
}, },
onDamageService: (data: DamageType) => { onDamageService: (data: DamageType) => {
const skillHistory = get().skillHistory const skillHistory = get().skillHistory
const skillIdx = skillHistory.findLastIndex(it => it.avatarId === data.attacker.uid) const skillIdx = skillHistory.findLastIndex(it => it.avatarId === data.attacker.uid)
if (skillIdx === -1) { if (skillIdx === -1) {
return return
} }
const newTh = [...skillHistory] const newTh = [...skillHistory]
newTh[skillIdx].damageDetail.push({ newTh[skillIdx].damageDetail.push({
damage: data.damage, damage: data.damage,
overkill_damage: data.overkill_damage, overkill_damage: data.overkill_damage,
damage_type: ParseAttackType(data.damage_type ? data.damage_type : data.type), damage_type: ParseAttackType(data.damage_type ? data.damage_type : data.type),
} as DamageDetailType) } as DamageDetailType)
newTh[skillIdx].totalDamage += data.damage newTh[skillIdx].totalDamage += data.damage
set({ set({
skillHistory: newTh, skillHistory: newTh,
totalDamage: get().totalDamage + data.damage, totalDamage: get().totalDamage + data.damage,
damagePerAV: (get().totalDamage + data.damage) / (get().totalAV === 0 ? 1 : get().totalAV) damagePerAV: (get().totalDamage + data.damage) / (get().totalAV === 0 ? 1 : get().totalAV)
}) })
}, },
onTurnBeginService: (data: TurnBeginType) => { onTurnBeginService: (data: TurnBeginType) => {
set((state) => ({ set((state) => ({
totalAV: data.action_value, totalAV: data.action_value,
damagePerAV: state.totalDamage / (data.action_value === 0 ? 1 : data.action_value), damagePerAV: state.totalDamage / (data.action_value === 0 ? 1 : data.action_value),
turnHistory: [...state.turnHistory, { turnHistory: [...state.turnHistory, {
avatarId: data?.turn_owner?.uid, avatarId: data?.turn_owner?.uid,
actionValue: data.action_value, actionValue: data.action_value,
waveIndex: state.waveIndex, waveIndex: state.waveIndex,
cycleIndex: state.cycleIndex cycleIndex: state.cycleIndex
} as TurnBattleInfo] } as TurnBattleInfo]
})) }))
}, },
onTurnEndService: (data: TurnEndType) => { onTurnEndService: (data: TurnEndType) => {
set((state) => ({ set((state) => ({
totalDamage: state.totalDamage === data.turn_info.total_damage ? data.turn_info.total_damage : state.totalDamage, totalDamage: state.totalDamage === data.turn_info.total_damage ? data.turn_info.total_damage : state.totalDamage,
currentAV: data.turn_info.action_value, currentAV: data.turn_info.action_value,
damagePerAV: (state.totalDamage === data.turn_info.total_damage ? data.turn_info.total_damage : state.totalDamage) damagePerAV: (state.totalDamage === data.turn_info.total_damage ? data.turn_info.total_damage : state.totalDamage)
/ (data.turn_info.action_value === 0 ? 1 : data.turn_info.action_value) / (data.turn_info.action_value === 0 ? 1 : data.turn_info.action_value)
})); }));
}, },
onEntityDefeatedService: (data: EntityDefeatedType) => { onEntityDefeatedService: (data: EntityDefeatedType) => {
let avatarDetail = get().avatarDetail let avatarDetail = get().avatarDetail
let enemyDetail = get().enemyDetail let enemyDetail = get().enemyDetail
if (!enemyDetail) { if (!enemyDetail) {
enemyDetail = {} as Record<number, EnemyInfo> enemyDetail = {} as Record<number, EnemyInfo>
} }
if (!avatarDetail) { if (!avatarDetail) {
avatarDetail = {} as Record<number, AvatarInfo> avatarDetail = {} as Record<number, AvatarInfo>
} }
if (data.killer.team === "Player" && enemyDetail[data.entity_defeated.uid]) { if (data.killer.team === "Player" && enemyDetail[data.entity_defeated.uid]) {
enemyDetail[data.entity_defeated.uid].isDie = true enemyDetail[data.entity_defeated.uid].isDie = true
enemyDetail[data.entity_defeated.uid].killer_uid = data.killer.uid enemyDetail[data.entity_defeated.uid].killer_uid = data.killer.uid
} else if (data.killer.team === "Enemy" && avatarDetail[data.entity_defeated.uid]) { } else if (data.killer.team === "Enemy" && avatarDetail[data.entity_defeated.uid]) {
avatarDetail[data.entity_defeated.uid].isDie = true avatarDetail[data.entity_defeated.uid].isDie = true
avatarDetail[data.entity_defeated.uid].killer_uid = data.killer.uid avatarDetail[data.entity_defeated.uid].killer_uid = data.killer.uid
} }
set({ set({
avatarDetail: avatarDetail, avatarDetail: avatarDetail,
enemyDetail: enemyDetail enemyDetail: enemyDetail
}) })
}, },
onUseSkillService: (data: UseSkillType) => { onUseSkillService: (data: UseSkillType) => {
set((state) => ({ set((state) => ({
skillHistory: [...state.skillHistory, { skillHistory: [...state.skillHistory, {
avatarId: data.avatar.uid, avatarId: data.avatar.uid,
damageDetail: [], damageDetail: [],
totalDamage: 0, totalDamage: 0,
skillType: ParseAttackType(data.skill.type), skillType: ParseAttackType(data.skill.type),
skillName: data.skill.name, skillName: data.skill.name,
turnBattleId: state.turnHistory.length - 1 turnBattleId: state.turnHistory.length - 1
} as SkillBattleInfo] } as SkillBattleInfo]
})) }))
}, },
onUpdateWaveService: (data: UpdateWaveType) => { onUpdateWaveService: (data: UpdateWaveType) => {
set({ set({
waveIndex: data.wave waveIndex: data.wave
}) })
}, },
onUpdateCycleService: (data: UpdateCycleType) => { onUpdateCycleService: (data: UpdateCycleType) => {
set({ set({
cycleIndex: data.cycle cycleIndex: data.cycle
}) })
}, },
onStatChange: (data: StatChangeType) => { onStatChange: (data: StatChangeType) => {
let avatarDetail = get().avatarDetail let avatarDetail = get().avatarDetail
let enemyDetail = get().enemyDetail let enemyDetail = get().enemyDetail
if (!enemyDetail) { if (!enemyDetail) {
enemyDetail = {} as Record<number, EnemyInfo> enemyDetail = {} as Record<number, EnemyInfo>
} }
if (!avatarDetail) { if (!avatarDetail) {
avatarDetail = {} as Record<number, AvatarInfo> avatarDetail = {} as Record<number, AvatarInfo>
} }
let key: string; let key: string;
let value: number; let value: number;
if (data.property) { if (data.property) {
key = data.property.type; key = data.property.type;
value = data.property.value; value = data.property.value;
} else if (data.stat) { } else if (data.stat) {
if ( if (
data.stat && data.stat &&
typeof data.stat === 'object' && typeof data.stat === 'object' &&
'type' in data.stat && 'type' in data.stat &&
'value' in data.stat && 'value' in data.stat &&
typeof data.stat.type === 'string' typeof data.stat.type === 'string'
) { ) {
key = data.stat.type; key = data.stat.type;
value = Number(data.stat.value); value = Number(data.stat.value);
} else { } else {
const entries = Object.entries(data.stat); const entries = Object.entries(data.stat);
if (entries.length === 0) return; if (entries.length === 0) return;
[key, value] = entries[0] as [string, number]; [key, value] = entries[0] as [string, number];
} }
} else { } else {
return; return;
} }
if (key === "CurrentHP") key = "HP"; if (key === "CurrentHP") key = "HP";
if (data.entity.team === "Player") { if (data.entity.team === "Player") {
const uid = data.entity.uid; const uid = data.entity.uid;
if (!avatarDetail[uid]) { if (!avatarDetail[uid]) {
avatarDetail[uid] = { avatarDetail[uid] = {
id: uid, id: uid,
isDie: false, isDie: false,
killer_uid: -1, killer_uid: -1,
stats: {}, stats: {},
statsHistory: [] statsHistory: []
}; };
} }
avatarDetail[uid].stats[key] = value avatarDetail[uid].stats[key] = value
avatarDetail[uid].statsHistory.push({ avatarDetail[uid].statsHistory.push({
stats: { [key]: value }, stats: { [key]: value },
turnBattleId: get().turnHistory.length - 1 turnBattleId: get().turnHistory.length - 1
}) })
} else { } else {
const uid = data.entity.uid; const uid = data.entity.uid;
if (!enemyDetail[uid]) { if (!enemyDetail[uid]) {
enemyDetail[uid] = { enemyDetail[uid] = {
id: uid, id: uid,
isDie: false, isDie: false,
killer_uid: -1, killer_uid: -1,
name: "", name: "",
positionIndex: Object.keys(get().enemyDetail || {}).length, positionIndex: Object.keys(get().enemyDetail || {}).length,
maxHP: 0, maxHP: 0,
waveIndex: 0, waveIndex: 0,
level: 0, level: 0,
stats: {}, stats: {},
statsHistory: [] statsHistory: []
}; };
} }
enemyDetail[uid].stats[key] = value enemyDetail[uid].stats[key] = value
enemyDetail[uid].statsHistory.push({ enemyDetail[uid].statsHistory.push({
stats: { [key]: value }, stats: { [key]: value },
turnBattleId: get().turnHistory.length - 1 turnBattleId: get().turnHistory.length - 1
}) })
} }
set({ set({
avatarDetail: avatarDetail, avatarDetail: avatarDetail,
enemyDetail: enemyDetail enemyDetail: enemyDetail
}) })
}, },
onUpdateTeamFormation: (data: UpdateTeamFormationType) => { onUpdateTeamFormation: (data: UpdateTeamFormationType) => {
let avatarDetail = get().avatarDetail let avatarDetail = get().avatarDetail
let enemyDetail = get().enemyDetail let enemyDetail = get().enemyDetail
if (!enemyDetail) { if (!enemyDetail) {
enemyDetail = {} as Record<number, EnemyInfo> enemyDetail = {} as Record<number, EnemyInfo>
} }
if (!avatarDetail) { if (!avatarDetail) {
avatarDetail = {} as Record<number, AvatarInfo> avatarDetail = {} as Record<number, AvatarInfo>
} }
if (data.team === Team.Enemy) { if (data.team === Team.Enemy) {
for (let i = 0; i < data.entities.length; i++) { for (let i = 0; i < data.entities.length; i++) {
const entity = data.entities[i]; const entity = data.entities[i];
if (entity.team === Team.Enemy && enemyDetail?.[entity.uid]) { if (entity.team === Team.Enemy && enemyDetail?.[entity.uid]) {
enemyDetail[entity.uid].positionIndex = i enemyDetail[entity.uid].positionIndex = i
enemyDetail[entity.uid].waveIndex = get().waveIndex enemyDetail[entity.uid].waveIndex = get().waveIndex
} }
} }
} }
set({ set({
avatarDetail: avatarDetail, avatarDetail: avatarDetail,
enemyDetail: enemyDetail enemyDetail: enemyDetail
}) })
}, },
onInitializeEnemyService: (data: InitializeEnemyType) => { onInitializeEnemyService: (data: InitializeEnemyType) => {
const enemyDetail = get().enemyDetail const enemyDetail = get().enemyDetail
if (!enemyDetail) { if (!enemyDetail) {
return return
} }
let maxHP = 0; let maxHP = 0;
let level = 0; let level = 0;
if ('properties' in data.enemy.base_stats) { if ('properties' in data.enemy.base_stats) {
maxHP = data.enemy.base_stats.properties["MaxHP"] || 0; maxHP = data.enemy.base_stats.properties["MaxHP"] || 0;
level = data.enemy.base_stats.properties["Level"] || 0; level = data.enemy.base_stats.properties["Level"] || 0;
} else { } else {
maxHP = data.enemy.base_stats.hp || data.enemy.base_stats.CurrentHP || 0; maxHP = data.enemy.base_stats.hp || data.enemy.base_stats.CurrentHP || 0;
level = data.enemy.base_stats.level; level = data.enemy.base_stats.level;
} }
enemyDetail[data.enemy.uid] = { enemyDetail[data.enemy.uid] = {
id: data.enemy.id, id: data.enemy.id,
isDie: false, isDie: false,
killer_uid: -1, killer_uid: -1,
positionIndex: enemyDetail?.[data.enemy.uid]?.positionIndex || 0, positionIndex: enemyDetail?.[data.enemy.uid]?.positionIndex || 0,
waveIndex: get().waveIndex, waveIndex: get().waveIndex,
name: data.enemy.name, name: data.enemy.name,
maxHP: maxHP, maxHP: maxHP,
level: level, level: level,
stats: {}, stats: {},
statsHistory: [] statsHistory: []
} }
set({ set({
enemyDetail: enemyDetail enemyDetail: enemyDetail
}) })
}, },
onBattleEndService: (data: BattleEndType) => { onBattleEndService: (data: BattleEndType) => {
const lineups: AvatarBattleInfo[] = [] const lineups: AvatarBattleInfo[] = []
for (const avatar of data.avatars) { for (const avatar of data.avatars) {
lineups.push({ avatarId: avatar.id, isDie: false } as AvatarBattleInfo) lineups.push({ avatarId: avatar.id, isDie: false } as AvatarBattleInfo)
} }
set({ set({
lineup: lineups, lineup: lineups,
totalDamage: data.total_damage, totalDamage: data.total_damage,
totalAV: data.action_value, totalAV: data.action_value,
damagePerAV: data.total_damage / (data.action_value === 0 ? 1 : data.action_value) damagePerAV: data.total_damage / (data.action_value === 0 ? 1 : data.action_value)
}) })
}, },
})); }));
export default useBattleDataStore; export default useBattleDataStore;
+24 -24
View File
@@ -1,25 +1,25 @@
import { create } from 'zustand' import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware'; import { createJSONStorage, persist } from 'zustand/middleware';
interface LocaleState { interface LocaleState {
locale: string; locale: string;
theme: string theme: string
setTheme: (newTheme: string) => void; setTheme: (newTheme: string) => void;
setLocale: (newLocale: string) => void; setLocale: (newLocale: string) => void;
} }
const useLocaleStore = create<LocaleState>()(persist( const useLocaleStore = create<LocaleState>()(persist(
(set) => ({ (set) => ({
locale: "en", locale: "en",
theme: "night", theme: "night",
setTheme: (newTheme: string) => set({ theme: newTheme }), setTheme: (newTheme: string) => set({ theme: newTheme }),
setLocale: (newLocale: string) => set({ locale: newLocale }), setLocale: (newLocale: string) => set({ locale: newLocale }),
}), }),
{ {
name: 'locale-storage', name: 'locale-storage',
storage: createJSONStorage(() => localStorage), storage: createJSONStorage(() => localStorage),
} }
)); ));
export default useLocaleStore; export default useLocaleStore;
+33 -33
View File
@@ -1,33 +1,33 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
interface SocketState { interface SocketState {
host: string; host: string;
port: number; port: number;
status: boolean; status: boolean;
connectionType: string; connectionType: string;
setHost: (host: string) => void; setHost: (host: string) => void;
setConnectionType: (host: string) => void; setConnectionType: (host: string) => void;
setPort: (port: number) => void; setPort: (port: number) => void;
setStatus: (status: boolean) => void; setStatus: (status: boolean) => void;
} }
const useSocketStore = create<SocketState>()( const useSocketStore = create<SocketState>()(
persist( persist(
(set) => ({ (set) => ({
host: "http://localhost", host: "http://localhost",
port: 3443, port: 3443,
status: false, status: false,
connectionType: "Native", connectionType: "Native",
setHost: (host: string) => set({ host }), setHost: (host: string) => set({ host }),
setConnectionType: (connectionType: string) => set({ connectionType }), setConnectionType: (connectionType: string) => set({ connectionType }),
setPort: (port: number) => set({ port }), setPort: (port: number) => set({ port }),
setStatus: (status: boolean) => set({ status }), setStatus: (status: boolean) => set({ status }),
}), }),
{ {
name: 'socket-storage', name: 'socket-storage',
} }
) )
); );
export default useSocketStore; export default useSocketStore;
+101 -101
View File
@@ -1,101 +1,101 @@
import { EntityType } from "./entity"; import { EntityType } from "./entity";
export interface DamageType { export interface DamageType {
attacker: EntityType; attacker: EntityType;
damage: number; damage: number;
overkill_damage?: number; overkill_damage?: number;
damage_type?: AttackType | string; damage_type?: AttackType | string;
type?: AttackType | string; type?: AttackType | string;
} }
export interface DamageDetailType { export interface DamageDetailType {
damage: number; damage: number;
overkill_damage?: number; overkill_damage?: number;
damage_type?: AttackType; damage_type?: AttackType;
} }
export enum AttackType { export enum AttackType {
Unknown = 0, Unknown = 0,
Normal = 1, Normal = 1,
BPSkill = 2, BPSkill = 2,
Ultra = 3, Ultra = 3,
QTE = 4, QTE = 4,
DOT = 5, DOT = 5,
Pursued = 6, Pursued = 6,
Maze = 7, Maze = 7,
MazeNormal = 8, MazeNormal = 8,
Insert = 9, Insert = 9,
ElementDamage = 10, ElementDamage = 10,
Level = 11, Level = 11,
Servant = 12, Servant = 12,
TrueDamage = 13, TrueDamage = 13,
ElationDamage = 14 ElationDamage = 14
} }
const attackTypeMap: Record<string, AttackType> = { const attackTypeMap: Record<string, AttackType> = {
Talent: AttackType.Unknown, Talent: AttackType.Unknown,
Basic: AttackType.Normal, Basic: AttackType.Normal,
Skill: AttackType.BPSkill, Skill: AttackType.BPSkill,
Ultimate: AttackType.Ultra, Ultimate: AttackType.Ultra,
QTE: AttackType.QTE, QTE: AttackType.QTE,
DOT: AttackType.DOT, DOT: AttackType.DOT,
DoT: AttackType.DOT, DoT: AttackType.DOT,
Pursued: AttackType.Pursued, Pursued: AttackType.Pursued,
Additional: AttackType.Pursued, Additional: AttackType.Pursued,
Technique: AttackType.Maze, Technique: AttackType.Maze,
MazeNormal: AttackType.MazeNormal, MazeNormal: AttackType.MazeNormal,
"Follow-up": AttackType.Insert, "Follow-up": AttackType.Insert,
"Follow-Up": AttackType.Insert, "Follow-Up": AttackType.Insert,
"Elemental Damage": AttackType.ElementDamage, "Elemental Damage": AttackType.ElementDamage,
Break: AttackType.ElementDamage, Break: AttackType.ElementDamage,
Level: AttackType.Level, Level: AttackType.Level,
Servant: AttackType.Servant, Servant: AttackType.Servant,
"True Damage": AttackType.TrueDamage, "True Damage": AttackType.TrueDamage,
True: AttackType.TrueDamage, True: AttackType.TrueDamage,
"Elation Damage": AttackType.ElationDamage, "Elation Damage": AttackType.ElationDamage,
Elation: AttackType.ElationDamage, Elation: AttackType.ElationDamage,
}; };
export function ParseAttackType(type: AttackType | string | undefined): AttackType { export function ParseAttackType(type: AttackType | string | undefined): AttackType {
if (type === undefined || type === null) { if (type === undefined || type === null) {
return AttackType.Unknown; return AttackType.Unknown;
} }
if (typeof type === "number") { if (typeof type === "number") {
return type in AttackType ? type : AttackType.Unknown; return type in AttackType ? type : AttackType.Unknown;
} }
const num = Number(type); const num = Number(type);
if (!isNaN(num)) { if (!isNaN(num)) {
return num in AttackType ? num as AttackType : AttackType.Unknown; return num in AttackType ? num as AttackType : AttackType.Unknown;
} }
return attackTypeMap[type] ?? AttackType.Unknown; return attackTypeMap[type] ?? AttackType.Unknown;
} }
export function attackTypeToString(type: AttackType | undefined): string { export function attackTypeToString(type: AttackType | undefined): string {
if (type === undefined) { if (type === undefined) {
return "" return ""
} }
switch (type) { switch (type) {
case AttackType.Unknown: return "Talent"; case AttackType.Unknown: return "Talent";
case AttackType.Normal: return "Basic"; case AttackType.Normal: return "Basic";
case AttackType.BPSkill: return "Skill"; case AttackType.BPSkill: return "Skill";
case AttackType.Ultra: return "Ultimate"; case AttackType.Ultra: return "Ultimate";
case AttackType.QTE: return "QTE"; case AttackType.QTE: return "QTE";
case AttackType.DOT: return "DOT"; case AttackType.DOT: return "DOT";
case AttackType.Pursued: return "Pursued"; case AttackType.Pursued: return "Pursued";
case AttackType.Maze: return "Technique"; case AttackType.Maze: return "Technique";
case AttackType.MazeNormal: return "MazeNormal"; case AttackType.MazeNormal: return "MazeNormal";
case AttackType.Insert: return "Follow-up"; case AttackType.Insert: return "Follow-up";
case AttackType.ElementDamage: return "Elemental Damage"; case AttackType.ElementDamage: return "Elemental Damage";
case AttackType.Level: return "Level"; case AttackType.Level: return "Level";
case AttackType.Servant: return "Servant"; case AttackType.Servant: return "Servant";
case AttackType.TrueDamage: return "True Damage"; case AttackType.TrueDamage: return "True Damage";
case AttackType.ElationDamage: return "Elation Damage"; case AttackType.ElationDamage: return "Elation Damage";
default: return "Unknown"; default: return "Unknown";
} }
} }
+8 -8
View File
@@ -1,8 +1,8 @@
export interface CharacterBasic { export interface CharacterBasic {
id: string; id: string;
icon: string; icon: string;
rank: string; rank: string;
baseType: string; baseType: string;
damageType: string; damageType: string;
lang: Record<string, string>; lang: Record<string, string>;
} }
+17 -17
View File
@@ -1,18 +1,18 @@
import { AvatarType } from "./lineup"; import { AvatarType } from "./lineup";
import { TurnInfoType } from "./turn"; import { TurnInfoType } from "./turn";
export interface BattleEndType { export interface BattleEndType {
avatars: AvatarType[]; avatars: AvatarType[];
turn_history: TurnInfoType[]; turn_history: TurnInfoType[];
av_history: TurnInfoType[]; av_history: TurnInfoType[];
turn_count: number; turn_count: number;
total_damage: number; total_damage: number;
action_value: number; action_value: number;
stage_id: number; stage_id: number;
} }
export interface BattleBeginType { export interface BattleBeginType {
max_waves: number max_waves: number
max_cycles: number max_cycles: number
stage_id: number stage_id: number
} }
+26 -26
View File
@@ -1,26 +1,26 @@
import { StatsType } from "./stat"; import { StatsType } from "./stat";
export interface BattleStatsType { export interface BattleStatsType {
properties: Record<string, number>; properties: Record<string, number>;
} }
export interface EnemyType { export interface EnemyType {
id: number; id: number;
uid: number; uid: number;
name: string; name: string;
base_stats: BattleStatsType | StatsType; base_stats: BattleStatsType | StatsType;
} }
export interface InitializeEnemyType { export interface InitializeEnemyType {
enemy: EnemyType enemy: EnemyType
} }
export interface MonsterBasic { export interface MonsterBasic {
id: string; id: string;
rank: string; rank: string;
icon: string; icon: string;
image: string; image: string;
weak: string[]; weak: string[];
desc: Record<string, string>; desc: Record<string, string>;
lang: Record<string, string>; lang: Record<string, string>;
} }
+10 -10
View File
@@ -1,11 +1,11 @@
import { Team } from "./team"; import { Team } from "./team";
export interface EntityType { export interface EntityType {
uid: number; uid: number;
team: Team; team: Team;
} }
export interface EntityDefeatedType { export interface EntityDefeatedType {
killer: EntityType, killer: EntityType,
entity_defeated: EntityType entity_defeated: EntityType
} }
+2 -2
View File
@@ -1,3 +1,3 @@
export interface ErrorType { export interface ErrorType {
msg: string msg: string
} }
+13 -13
View File
@@ -1,13 +1,13 @@
export * from "./attack" export * from "./attack"
export * from "./avatar" export * from "./avatar"
export * from "./battle" export * from "./battle"
export * from "./lineup" export * from "./lineup"
export * from "./skill" export * from "./skill"
export * from "./turn" export * from "./turn"
export * from "./waveAndCycle" export * from "./waveAndCycle"
export * from "./srtools" export * from "./srtools"
export * from "./version" export * from "./version"
export * from "./entity" export * from "./entity"
export * from "./stat" export * from "./stat"
export * from "./enemy" export * from "./enemy"
export * from "./team" export * from "./team"
+16 -16
View File
@@ -1,17 +1,17 @@
import { EntityType } from "./entity"; import { EntityType } from "./entity";
import { Team } from "./team"; import { Team } from "./team";
export interface AvatarType{ export interface AvatarType{
id: number; id: number;
name: string; name: string;
} }
export interface SetBattleLineupType { export interface SetBattleLineupType {
avatars: AvatarType[]; avatars: AvatarType[];
} }
export interface UpdateTeamFormationType { export interface UpdateTeamFormationType {
entities: EntityType[], entities: EntityType[],
team: Team team: Team
} }
+68 -68
View File
@@ -1,68 +1,68 @@
import {AttackType, DamageDetailType} from "./attack"; import {AttackType, DamageDetailType} from "./attack";
import { AvatarAnalysisJson } from "./srtools"; import { AvatarAnalysisJson } from "./srtools";
import { StatType } from "./stat"; import { StatType } from "./stat";
export interface AvatarBattleInfo { export interface AvatarBattleInfo {
avatarId: number; avatarId: number;
isDie?: boolean; isDie?: boolean;
} }
export interface SkillBattleInfo { export interface SkillBattleInfo {
avatarId: number; avatarId: number;
damageDetail: DamageDetailType[]; damageDetail: DamageDetailType[];
totalDamage: number; totalDamage: number;
skillType: AttackType; skillType: AttackType;
skillName: string; skillName: string;
turnBattleId: number; turnBattleId: number;
} }
export interface TurnBattleInfo { export interface TurnBattleInfo {
avatarId: number; avatarId: number;
actionValue: number; actionValue: number;
waveIndex: number; waveIndex: number;
cycleIndex: number; cycleIndex: number;
} }
export interface BattleDataStateJson { export interface BattleDataStateJson {
lineup: AvatarBattleInfo[]; lineup: AvatarBattleInfo[];
turnHistory: TurnBattleInfo[] turnHistory: TurnBattleInfo[]
skillHistory: SkillBattleInfo[] skillHistory: SkillBattleInfo[]
dataAvatar: AvatarAnalysisJson[] dataAvatar: AvatarAnalysisJson[]
totalAV: number; totalAV: number;
totalDamage: number; totalDamage: number;
damagePerAV: number; damagePerAV: number;
maxWave: number; maxWave: number;
cycleIndex: number, cycleIndex: number,
waveIndex: number, waveIndex: number,
maxCycle: number, maxCycle: number,
version?: string, version?: string,
avatarDetail?: Record<number, AvatarInfo>; avatarDetail?: Record<number, AvatarInfo>;
enemyDetail?: Record<number, EnemyInfo>; enemyDetail?: Record<number, EnemyInfo>;
} }
export interface StatsHistoryType { export interface StatsHistoryType {
stats: StatType stats: StatType
turnBattleId: number; turnBattleId: number;
} }
export interface EnemyInfo { export interface EnemyInfo {
id: number; id: number;
name: string; name: string;
maxHP: number; maxHP: number;
level: number; level: number;
isDie: boolean; isDie: boolean;
positionIndex: number; positionIndex: number;
waveIndex: number; waveIndex: number;
killer_uid: number; killer_uid: number;
stats: Record<string, number>; stats: Record<string, number>;
statsHistory: StatsHistoryType[]; statsHistory: StatsHistoryType[];
} }
export interface AvatarInfo { export interface AvatarInfo {
id: number; id: number;
isDie: boolean; isDie: boolean;
killer_uid: number; killer_uid: number;
stats: Record<string, number>; stats: Record<string, number>;
statsHistory: StatsHistoryType[]; statsHistory: StatsHistoryType[];
} }
+13 -13
View File
@@ -1,14 +1,14 @@
import { AttackType } from "@/types/attack"; import { AttackType } from "@/types/attack";
import { EntityType } from "./entity"; import { EntityType } from "./entity";
export interface SkillInfo { export interface SkillInfo {
name: string; name: string;
type: AttackType | string; type: AttackType | string;
skill_config_id: number; skill_config_id: number;
} }
export interface UseSkillType { export interface UseSkillType {
avatar: EntityType; avatar: EntityType;
skill: SkillInfo; skill: SkillInfo;
} }
+42 -42
View File
@@ -1,42 +1,42 @@
export interface AvatarAnalysisJson { export interface AvatarAnalysisJson {
owner_uid: number; owner_uid: number;
avatar_id: number; avatar_id: number;
data: AvatarData | null; data: AvatarData | null;
level: number; level: number;
promotion: number; promotion: number;
techniques: number[]; techniques: number[];
relics: RelicJson[]; relics: RelicJson[];
Lightcone: LightconeJson | null; Lightcone: LightconeJson | null;
sp_value: number; sp_value: number;
sp_max: number; sp_max: number;
} }
export interface AvatarData { export interface AvatarData {
rank: number; rank: number;
skills: Record<string, number>; skills: Record<string, number>;
} }
export interface SubAffix { export interface SubAffix {
sub_affix_id: number; sub_affix_id: number;
count: number; count: number;
step: number; step: number;
} }
export interface RelicJson { export interface RelicJson {
level: number; level: number;
relic_id: number; relic_id: number;
relic_set_id: number; relic_set_id: number;
main_affix_id: number; main_affix_id: number;
sub_affixes: SubAffix[]; sub_affixes: SubAffix[];
internal_uid: number; internal_uid: number;
equip_avatar: number; equip_avatar: number;
} }
export interface LightconeJson { export interface LightconeJson {
level: number; level: number;
item_id: number; item_id: number;
equip_avatar: number; equip_avatar: number;
rank: number; rank: number;
promotion: number; promotion: number;
internal_uid: number; internal_uid: number;
} }
+3 -3
View File
@@ -1,4 +1,4 @@
export enum Team { export enum Team {
Player = "Player", Player = "Player",
Enemy = "Enemy" Enemy = "Enemy"
} }
+17 -17
View File
@@ -1,18 +1,18 @@
import { EntityType } from "./entity"; import { EntityType } from "./entity";
export interface TurnInfoType { export interface TurnInfoType {
avatars_turn_damage: number[]; avatars_turn_damage: number[];
total_damage: number; total_damage: number;
action_value: number, action_value: number,
cycle: number, cycle: number,
wave: number, wave: number,
} }
export interface TurnBeginType { export interface TurnBeginType {
action_value: number; action_value: number;
turn_owner?: EntityType | null turn_owner?: EntityType | null
} }
export interface TurnEndType { export interface TurnEndType {
turn_info: TurnInfoType turn_info: TurnInfoType
} }
+2 -2
View File
@@ -1,3 +1,3 @@
export interface VersionType { export interface VersionType {
version: string version: string
} }
+6 -6
View File
@@ -1,7 +1,7 @@
export interface UpdateWaveType { export interface UpdateWaveType {
wave: number wave: number
} }
export interface UpdateCycleType { export interface UpdateCycleType {
cycle: number cycle: number
} }
+1 -1
View File
@@ -38,4 +38,4 @@
"exclude": [ "exclude": [
"node_modules" "node_modules"
] ]
} }