diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 6038e66..0f1b04d 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -8,17 +8,8 @@ jobs: runs-on: ubuntu-latest steps: - - name: Check out latest code - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Stop and remove old containers + - name: Deploy to Container run: | - docker compose down || true - - - name: Remove unused Docker resources - run: | - docker system prune -a --volumes -f - - - name: Build and restart containers - run: | - docker compose up -d \ No newline at end of file + docker compose up -d --build --remove-orphans \ No newline at end of file diff --git a/data/avatar_basic.json.br b/data/avatar_basic.json.br index 649a28b..7a4e6a1 100644 Binary files a/data/avatar_basic.json.br and b/data/avatar_basic.json.br differ diff --git a/data/monster_basic.json.br b/data/monster_basic.json.br index e594a14..230e835 100644 Binary files a/data/monster_basic.json.br and b/data/monster_basic.json.br differ diff --git a/src/app/page.tsx b/src/app/page.tsx index 0502512..53f61ff 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -51,7 +51,7 @@ export default function Home() { setMapEnemy(monsterMap) }; fetchData(); - }, [setListAvatar, setListEnemy]); + }, [setListAvatar, setListEnemy, setMapAvatar, setMapEnemy]); useEffect(() => { window.dispatchEvent(new Event('resize')); diff --git a/src/components/card/characterCard.tsx b/src/components/card/characterCard.tsx index 7b5644d..e97aa86 100644 --- a/src/components/card/characterCard.tsx +++ b/src/components/card/characterCard.tsx @@ -42,9 +42,9 @@ export default function CharacterCard({ data }: CharacterCardProps) { height={48} unoptimized crossOrigin="anonymous" - src={`/icon/${data.damageType.toLowerCase()}.webp`} + src={`/icon/${data?.damageType?.toLowerCase()}.webp`} className="absolute top-0 left-0 w-6 h-6" - alt={data.damageType.toLowerCase()} + alt={data?.damageType?.toLowerCase()} /> - lineup.some(av => av.avatarId.toString() === item.id) + const lineupAvatars = listAvatar?.filter(item => + lineup?.some(av => av?.avatarId?.toString() === item.id) ); const handleShow = (modalId: string, item: CharacterBasic) => { @@ -103,7 +103,7 @@ export default function LineupBar() { ) : (
- {lineupAvatars.map((item, index) => { + {lineupAvatars?.map((item, index) => { const lastTurnAvatarId = turnHistory.findLast(i => i?.avatarId)?.avatarId || -1; const isLastTurn = item.id === lastTurnAvatarId.toString(); diff --git a/src/hooks/useDamagePerCycle.ts b/src/hooks/useDamagePerCycle.ts index ab4c1f0..ae20b2d 100644 --- a/src/hooks/useDamagePerCycle.ts +++ b/src/hooks/useDamagePerCycle.ts @@ -6,11 +6,11 @@ import { useMemo } from "react"; type Mode = 0 | 1 | 2; export function useDamagePerCycleForOne(avatarId: number, mode: Mode) { - const { skillHistory, turnHistory, maxCycle } = useBattleDataStore.getState(); + const { skillHistory, turnHistory } = useBattleDataStore.getState(); const transI18n = useTranslations("DataAnalysisPage"); return useMemo(() => { const damageMap = new Map(); - + skillHistory .filter(s => s.avatarId === avatarId) .forEach(s => { @@ -19,9 +19,9 @@ export function useDamagePerCycleForOne(avatarId: number, mode: Mode) { let key = ''; if (mode === 0) { - key = `${transI18n('cycle')} ${maxCycle-turn.cycleIndex} - ${transI18n('wave')} ${turn.waveIndex}`; + key = `${transI18n('cycle')} ${turn.cycleIndex} - ${transI18n('wave')} ${turn.waveIndex}`; } else if (mode === 1) { - key = `${transI18n('cycle')} ${maxCycle-turn.cycleIndex}`; + key = `${transI18n('cycle')} ${turn.cycleIndex}`; } else if (mode === 2) { key = `${transI18n('wave')} ${turn.waveIndex}`; } @@ -39,31 +39,31 @@ export function useDamagePerCycleForOne(avatarId: number, mode: Mode) { export function useDamagePerCycleForAll(mode: Mode) { - const { skillHistory, turnHistory, maxCycle } = useBattleDataStore.getState(); - const transI18n = useTranslations("DataAnalysisPage"); - return useMemo(() => { - const damageMap = new Map(); - - skillHistory.forEach(s => { - const turn = turnHistory[s.turnBattleId]; - if (!turn) return; - - let key = ''; - if (mode === 0) { - key = `${transI18n('cycle')} ${maxCycle-turn.cycleIndex} - ${transI18n('wave')} ${turn.waveIndex}`; - } else if (mode === 1) { - key = `${transI18n('cycle')} ${maxCycle-turn.cycleIndex}`; - } else if (mode === 2) { - key = `${transI18n('wave')} ${turn.waveIndex}`; - } - - damageMap.set(key, (damageMap.get(key) || 0) + s.totalDamage); - }); - - const result = Array.from(damageMap.entries()) - .map(([x, y]) => ({ x, y })) - .sort((a, b) => a.x.localeCompare(b.x, undefined, { numeric: true })); - - return result; - }, [mode, skillHistory, turnHistory, transI18n]); - } \ No newline at end of file + const { skillHistory, turnHistory } = useBattleDataStore.getState(); + const transI18n = useTranslations("DataAnalysisPage"); + return useMemo(() => { + const damageMap = new Map(); + + skillHistory.forEach(s => { + const turn = turnHistory[s.turnBattleId]; + if (!turn) return; + + let key = ''; + if (mode === 0) { + key = `${transI18n('cycle')} ${turn.cycleIndex} - ${transI18n('wave')} ${turn.waveIndex}`; + } else if (mode === 1) { + key = `${transI18n('cycle')} ${turn.cycleIndex}`; + } else if (mode === 2) { + key = `${transI18n('wave')} ${turn.waveIndex}`; + } + + damageMap.set(key, (damageMap.get(key) || 0) + s.totalDamage); + }); + + const result = Array.from(damageMap.entries()) + .map(([x, y]) => ({ x, y })) + .sort((a, b) => a.x.localeCompare(b.x, undefined, { numeric: true })); + + return result; + }, [mode, skillHistory, turnHistory, transI18n]); +} \ No newline at end of file diff --git a/src/stores/battleDataStore.ts b/src/stores/battleDataStore.ts index fb3a708..709724b 100644 --- a/src/stores/battleDataStore.ts +++ b/src/stores/battleDataStore.ts @@ -1,4 +1,4 @@ -import { DamageType, AvatarAnalysisJson, UseSkillType, BattleBeginType, BattleEndType, DamageDetailType, EntityDefeatedType, SetBattleLineupType, TurnBeginType, TurnEndType, UpdateCycleType, UpdateWaveType, VersionType, StatChangeType, UpdateTeamFormationType, Team } 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 { AvatarBattleInfo, AvatarInfo, BattleDataStateJson, EnemyInfo, SkillBattleInfo, TurnBattleInfo } from '@/types/mics'; import { create } from 'zustand' @@ -54,10 +54,19 @@ const useBattleDataStore = create((set, get) => ({ avatarDetail: undefined, enemyDetail: undefined, loadBattleDataFromJSON: (data: BattleDataStateJson) => { + const skillHistory = data.skillHistory.map(it => { + it.damageDetail = it.damageDetail.map(it => { + return { + ...it, + damage_type: ParseAttackType(it.damage_type) + } + }) + return it + }) set({ lineup: data.lineup, turnHistory: data.turnHistory, - skillHistory: data.skillHistory, + skillHistory: skillHistory, dataAvatar: data.dataAvatar, totalAV: data.totalAV, totalDamage: data.totalDamage, @@ -82,47 +91,49 @@ const useBattleDataStore = create((set, get) => ({ }) }, onBattleBeginService: (data: BattleBeginType) => { - const current = get() - const updatedHistory = current.turnHistory.map(it => ({ - ...it, - cycleIndex: data.max_cycles - })) set({ maxWave: data.max_waves, - maxCycle: data.max_cycles, - turnHistory: updatedHistory + maxCycle: data.max_cycles }) }, + onSetBattleLineupService: (data: SetBattleLineupType) => { const lineups: AvatarBattleInfo[] = [] for (const avatar of data.avatars) { lineups.push({ avatarId: avatar.id, isDie: false } as AvatarBattleInfo) } - set((state) => ({ + set(() => ({ lineup: lineups, turnHistory: [{ avatarId: -1, actionValue: 0, waveIndex: 1, - cycleIndex: state.maxCycle, + cycleIndex: 0, } as TurnBattleInfo], skillHistory: [], totalAV: 0, totalDamage: 0, damagePerAV: 0, - cycleIndex: state.maxCycle, + cycleIndex: 0, + maxWave: Infinity, + maxCycle: Infinity, waveIndex: 1, })); }, + onDamageService: (data: DamageType) => { const skillHistory = get().skillHistory - + const skillIdx = skillHistory.findLastIndex(it => it.avatarId === data.attacker.uid) if (skillIdx === -1) { return } const newTh = [...skillHistory] - newTh[skillIdx].damageDetail.push({damage: data.damage, damage_type: data?.damage_type} as DamageDetailType) + newTh[skillIdx].damageDetail.push({ + damage: data.damage, + overkill_damage: data.overkill_damage, + damage_type: ParseAttackType(data.damage_type ? data.damage_type : data.type), + } as DamageDetailType) newTh[skillIdx].totalDamage += data.damage set({ skillHistory: newTh, @@ -130,6 +141,7 @@ const useBattleDataStore = create((set, get) => ({ damagePerAV: (get().totalDamage + data.damage) / (get().totalAV === 0 ? 1 : get().totalAV) }) }, + onTurnBeginService: (data: TurnBeginType) => { set((state) => ({ totalAV: data.action_value, @@ -142,14 +154,16 @@ const useBattleDataStore = create((set, get) => ({ } as TurnBattleInfo] })) }, + onTurnEndService: (data: TurnEndType) => { set((state) => ({ totalDamage: state.totalDamage === data.turn_info.total_damage ? data.turn_info.total_damage : state.totalDamage, currentAV: data.turn_info.action_value, - 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) + 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) })); }, + onEntityDefeatedService: (data: EntityDefeatedType) => { let avatarDetail = get().avatarDetail let enemyDetail = get().enemyDetail @@ -165,38 +179,38 @@ const useBattleDataStore = create((set, get) => ({ } else if (data.killer.team === "Enemy" && avatarDetail[data.entity_defeated.uid]) { avatarDetail[data.entity_defeated.uid].isDie = true avatarDetail[data.entity_defeated.uid].killer_uid = data.killer.uid - } else { - console.error("onEntityDefeatedService", data) - console.error("onEntityDefeatedService", enemyDetail) - console.error("onEntityDefeatedService", avatarDetail) } set({ avatarDetail: avatarDetail, enemyDetail: enemyDetail }) }, + onUseSkillService: (data: UseSkillType) => { set((state) => ({ skillHistory: [...state.skillHistory, { avatarId: data.avatar.uid, damageDetail: [], totalDamage: 0, - skillType: data.skill.type, + skillType: ParseAttackType(data.skill.type), skillName: data.skill.name, - turnBattleId: state.turnHistory.length-1 + turnBattleId: state.turnHistory.length - 1 } as SkillBattleInfo] })) }, + onUpdateWaveService: (data: UpdateWaveType) => { set({ waveIndex: data.wave }) }, + onUpdateCycleService: (data: UpdateCycleType) => { set({ cycleIndex: data.cycle }) }, + onStatChange: (data: StatChangeType) => { let avatarDetail = get().avatarDetail let enemyDetail = get().enemyDetail @@ -206,8 +220,34 @@ const useBattleDataStore = create((set, get) => ({ if (!avatarDetail) { avatarDetail = {} as Record } + + let key: string; + let value: number; + if (data.property) { + key = data.property.type; + value = data.property.value; + } else if (data.stat) { + if ( + data.stat && + typeof data.stat === 'object' && + 'type' in data.stat && + 'value' in data.stat && + typeof data.stat.type === 'string' + ) { + key = data.stat.type; + value = Number(data.stat.value); + } else { + const entries = Object.entries(data.stat); + if (entries.length === 0) return; + [key, value] = entries[0] as [string, number]; + } + } else { + return; + } + + if (key === "CurrentHP") key = "HP"; + if (data.entity.team === "Player") { - const [key, value] = Object.entries(data.stat)[0] const uid = data.entity.uid; if (!avatarDetail[uid]) { @@ -221,11 +261,10 @@ const useBattleDataStore = create((set, get) => ({ } avatarDetail[uid].stats[key] = value avatarDetail[uid].statsHistory.push({ - stats: data.stat, - turnBattleId: get().turnHistory.length-1 + stats: { [key]: value }, + turnBattleId: get().turnHistory.length - 1 }) } else { - const [key, value] = Object.entries(data.stat)[0] const uid = data.entity.uid; if (!enemyDetail[uid]) { @@ -244,8 +283,8 @@ const useBattleDataStore = create((set, get) => ({ } enemyDetail[uid].stats[key] = value enemyDetail[uid].statsHistory.push({ - stats: data.stat, - turnBattleId: get().turnHistory.length-1 + stats: { [key]: value }, + turnBattleId: get().turnHistory.length - 1 }) } set({ @@ -267,7 +306,7 @@ const useBattleDataStore = create((set, get) => ({ if (data.team === Team.Enemy) { for (let i = 0; i < data.entities.length; 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].waveIndex = get().waveIndex } @@ -280,20 +319,31 @@ const useBattleDataStore = create((set, get) => ({ enemyDetail: enemyDetail }) }, + onInitializeEnemyService: (data: InitializeEnemyType) => { const enemyDetail = get().enemyDetail if (!enemyDetail) { return } + let maxHP = 0; + let level = 0; + if ('properties' in data.enemy.base_stats) { + maxHP = data.enemy.base_stats.properties["MaxHP"] || 0; + level = data.enemy.base_stats.properties["Level"] || 0; + } else { + maxHP = data.enemy.base_stats.hp || data.enemy.base_stats.CurrentHP || 0; + level = data.enemy.base_stats.level; + } + enemyDetail[data.enemy.uid] = { id: data.enemy.id, isDie: false, killer_uid: -1, - positionIndex: enemyDetail[data.enemy.uid].positionIndex, - waveIndex: enemyDetail[data.enemy.uid].waveIndex, + positionIndex: enemyDetail?.[data.enemy.uid]?.positionIndex || 0, + waveIndex: get().waveIndex, name: data.enemy.name, - maxHP: data.enemy.base_stats.hp, - level: data.enemy.base_stats.level, + maxHP: maxHP, + level: level, stats: {}, statsHistory: [] } @@ -301,6 +351,7 @@ const useBattleDataStore = create((set, get) => ({ enemyDetail: enemyDetail }) }, + onBattleEndService: (data: BattleEndType) => { const lineups: AvatarBattleInfo[] = [] for (const avatar of data.avatars) { diff --git a/src/types/attack.ts b/src/types/attack.ts index 8d28ddf..a94343b 100644 --- a/src/types/attack.ts +++ b/src/types/attack.ts @@ -3,12 +3,15 @@ import { EntityType } from "./entity"; export interface DamageType { attacker: EntityType; damage: number; - damage_type?: AttackType + overkill_damage?: number; + damage_type?: AttackType | string; + type?: AttackType | string; } export interface DamageDetailType { damage: number; - damage_type?: AttackType + overkill_damage?: number; + damage_type?: AttackType; } @@ -30,10 +33,53 @@ export enum AttackType { ElationDamage = 14 } + +const attackTypeMap: Record = { + Talent: AttackType.Unknown, + Basic: AttackType.Normal, + Skill: AttackType.BPSkill, + Ultimate: AttackType.Ultra, + QTE: AttackType.QTE, + DOT: AttackType.DOT, + DoT: AttackType.DOT, + Pursued: AttackType.Pursued, + Additional: AttackType.Pursued, + Technique: AttackType.Maze, + MazeNormal: AttackType.MazeNormal, + "Follow-up": AttackType.Insert, + "Follow-Up": AttackType.Insert, + "Elemental Damage": AttackType.ElementDamage, + Break: AttackType.ElementDamage, + Level: AttackType.Level, + Servant: AttackType.Servant, + "True Damage": AttackType.TrueDamage, + True: AttackType.TrueDamage, + "Elation Damage": AttackType.ElationDamage, + Elation: AttackType.ElationDamage, +}; + +export function ParseAttackType(type: AttackType | string | undefined): AttackType { + if (type === undefined || type === null) { + return AttackType.Unknown; + } + + if (typeof type === "number") { + return type in AttackType ? type : AttackType.Unknown; + } + + const num = Number(type); + if (!isNaN(num)) { + return num in AttackType ? num as AttackType : AttackType.Unknown; + } + + return attackTypeMap[type] ?? AttackType.Unknown; +} + export function attackTypeToString(type: AttackType | undefined): string { if (type === undefined) { return "" } + switch (type) { case AttackType.Unknown: return "Talent"; case AttackType.Normal: return "Basic"; diff --git a/src/types/enemy.ts b/src/types/enemy.ts index 9acb09b..292a8ad 100644 --- a/src/types/enemy.ts +++ b/src/types/enemy.ts @@ -1,10 +1,14 @@ import { StatsType } from "./stat"; +export interface BattleStatsType { + properties: Record; +} + export interface EnemyType { id: number; uid: number; name: string; - base_stats: StatsType + base_stats: BattleStatsType | StatsType; } export interface InitializeEnemyType { diff --git a/src/types/skill.ts b/src/types/skill.ts index 3e14b9f..a579fc7 100644 --- a/src/types/skill.ts +++ b/src/types/skill.ts @@ -4,7 +4,7 @@ import { EntityType } from "./entity"; export interface SkillInfo { name: string; - type: AttackType; + type: AttackType | string; skill_config_id: number; } diff --git a/src/types/stat.ts b/src/types/stat.ts index f17df4e..94a192b 100644 --- a/src/types/stat.ts +++ b/src/types/stat.ts @@ -1,13 +1,21 @@ import { EntityType } from "./entity"; -export type StatType = Record +export type StatType = Record | { value: number; type: string }; + +export interface PropertyType { + value: number; + type: string; +} export interface StatsType { level: number; hp: number; + CurrentHP?: number; + MaxHP?: number; } export interface StatChangeType { - entity: EntityType, - stat: StatType, + entity: EntityType; + stat?: StatType; + property?: PropertyType; } \ No newline at end of file