UPDATE: FIx bug
All checks were successful
Gitea Auto Deploy / Deploy-Container (push) Successful in 43s

This commit is contained in:
2026-05-05 20:05:33 +07:00
parent 078ab168ad
commit 8640b62134
12 changed files with 190 additions and 90 deletions

View File

@@ -8,17 +8,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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: | run: |
docker compose down || true docker compose up -d --build --remove-orphans
- name: Remove unused Docker resources
run: |
docker system prune -a --volumes -f
- name: Build and restart containers
run: |
docker compose up -d

Binary file not shown.

Binary file not shown.

View File

@@ -51,7 +51,7 @@ export default function Home() {
setMapEnemy(monsterMap) setMapEnemy(monsterMap)
}; };
fetchData(); fetchData();
}, [setListAvatar, setListEnemy]); }, [setListAvatar, setListEnemy, setMapAvatar, setMapEnemy]);
useEffect(() => { useEffect(() => {
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));

View File

@@ -42,9 +42,9 @@ export default function CharacterCard({ data }: CharacterCardProps) {
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}

View File

@@ -29,8 +29,8 @@ export default function LineupBar() {
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) => {
@@ -103,7 +103,7 @@ export default function LineupBar() {
) : ( ) : (
<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();

View File

@@ -6,7 +6,7 @@ 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, maxCycle } = 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>();
@@ -19,9 +19,9 @@ export function useDamagePerCycleForOne(avatarId: number, mode: Mode) {
let key = ''; let key = '';
if (mode === 0) { 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) { } else if (mode === 1) {
key = `${transI18n('cycle')} ${maxCycle-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}`;
} }
@@ -39,31 +39,31 @@ export function useDamagePerCycleForOne(avatarId: number, mode: Mode) {
export function useDamagePerCycleForAll(mode: Mode) { export function useDamagePerCycleForAll(mode: Mode) {
const { skillHistory, turnHistory, maxCycle } = 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')} ${maxCycle-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')} ${maxCycle-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]);
} }

View File

@@ -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 { 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'
@@ -54,10 +54,19 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
avatarDetail: undefined, avatarDetail: undefined,
enemyDetail: undefined, enemyDetail: undefined,
loadBattleDataFromJSON: (data: BattleDataStateJson) => { 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({ set({
lineup: data.lineup, lineup: data.lineup,
turnHistory: data.turnHistory, turnHistory: data.turnHistory,
skillHistory: data.skillHistory, skillHistory: skillHistory,
dataAvatar: data.dataAvatar, dataAvatar: data.dataAvatar,
totalAV: data.totalAV, totalAV: data.totalAV,
totalDamage: data.totalDamage, totalDamage: data.totalDamage,
@@ -82,38 +91,36 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
}) })
}, },
onBattleBeginService: (data: BattleBeginType) => { onBattleBeginService: (data: BattleBeginType) => {
const current = get()
const updatedHistory = current.turnHistory.map(it => ({
...it,
cycleIndex: data.max_cycles
}))
set({ set({
maxWave: data.max_waves, maxWave: data.max_waves,
maxCycle: data.max_cycles, maxCycle: data.max_cycles
turnHistory: updatedHistory
}) })
}, },
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((state) => ({ set(() => ({
lineup: lineups, lineup: lineups,
turnHistory: [{ turnHistory: [{
avatarId: -1, avatarId: -1,
actionValue: 0, actionValue: 0,
waveIndex: 1, waveIndex: 1,
cycleIndex: state.maxCycle, cycleIndex: 0,
} as TurnBattleInfo], } as TurnBattleInfo],
skillHistory: [], skillHistory: [],
totalAV: 0, totalAV: 0,
totalDamage: 0, totalDamage: 0,
damagePerAV: 0, damagePerAV: 0,
cycleIndex: state.maxCycle, cycleIndex: 0,
maxWave: Infinity,
maxCycle: Infinity,
waveIndex: 1, waveIndex: 1,
})); }));
}, },
onDamageService: (data: DamageType) => { onDamageService: (data: DamageType) => {
const skillHistory = get().skillHistory const skillHistory = get().skillHistory
@@ -122,7 +129,11 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
return return
} }
const newTh = [...skillHistory] 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 newTh[skillIdx].totalDamage += data.damage
set({ set({
skillHistory: newTh, skillHistory: newTh,
@@ -130,6 +141,7 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
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,
@@ -142,14 +154,16 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
} 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
@@ -165,38 +179,38 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
} 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
} else {
console.error("onEntityDefeatedService", data)
console.error("onEntityDefeatedService", enemyDetail)
console.error("onEntityDefeatedService", avatarDetail)
} }
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: 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
@@ -206,8 +220,34 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
if (!avatarDetail) { if (!avatarDetail) {
avatarDetail = {} as Record<number, AvatarInfo> avatarDetail = {} as Record<number, AvatarInfo>
} }
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") { if (data.entity.team === "Player") {
const [key, value] = Object.entries(data.stat)[0]
const uid = data.entity.uid; const uid = data.entity.uid;
if (!avatarDetail[uid]) { if (!avatarDetail[uid]) {
@@ -221,11 +261,10 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
} }
avatarDetail[uid].stats[key] = value avatarDetail[uid].stats[key] = value
avatarDetail[uid].statsHistory.push({ avatarDetail[uid].statsHistory.push({
stats: data.stat, stats: { [key]: value },
turnBattleId: get().turnHistory.length-1 turnBattleId: get().turnHistory.length - 1
}) })
} else { } else {
const [key, value] = Object.entries(data.stat)[0]
const uid = data.entity.uid; const uid = data.entity.uid;
if (!enemyDetail[uid]) { if (!enemyDetail[uid]) {
@@ -244,8 +283,8 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
} }
enemyDetail[uid].stats[key] = value enemyDetail[uid].stats[key] = value
enemyDetail[uid].statsHistory.push({ enemyDetail[uid].statsHistory.push({
stats: data.stat, stats: { [key]: value },
turnBattleId: get().turnHistory.length-1 turnBattleId: get().turnHistory.length - 1
}) })
} }
set({ set({
@@ -267,7 +306,7 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
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
} }
@@ -280,20 +319,31 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
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 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] = { 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, positionIndex: enemyDetail?.[data.enemy.uid]?.positionIndex || 0,
waveIndex: enemyDetail[data.enemy.uid].waveIndex, waveIndex: get().waveIndex,
name: data.enemy.name, name: data.enemy.name,
maxHP: data.enemy.base_stats.hp, maxHP: maxHP,
level: data.enemy.base_stats.level, level: level,
stats: {}, stats: {},
statsHistory: [] statsHistory: []
} }
@@ -301,6 +351,7 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
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) {

View File

@@ -3,12 +3,15 @@ import { EntityType } from "./entity";
export interface DamageType { export interface DamageType {
attacker: EntityType; attacker: EntityType;
damage: number; damage: number;
damage_type?: AttackType overkill_damage?: number;
damage_type?: AttackType | string;
type?: AttackType | string;
} }
export interface DamageDetailType { export interface DamageDetailType {
damage: number; damage: number;
damage_type?: AttackType overkill_damage?: number;
damage_type?: AttackType;
} }
@@ -30,10 +33,53 @@ export enum AttackType {
ElationDamage = 14 ElationDamage = 14
} }
const attackTypeMap: Record<string, AttackType> = {
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 { 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";

View File

@@ -1,10 +1,14 @@
import { StatsType } from "./stat"; import { StatsType } from "./stat";
export interface BattleStatsType {
properties: Record<string, number>;
}
export interface EnemyType { export interface EnemyType {
id: number; id: number;
uid: number; uid: number;
name: string; name: string;
base_stats: StatsType base_stats: BattleStatsType | StatsType;
} }
export interface InitializeEnemyType { export interface InitializeEnemyType {

View File

@@ -4,7 +4,7 @@ import { EntityType } from "./entity";
export interface SkillInfo { export interface SkillInfo {
name: string; name: string;
type: AttackType; type: AttackType | string;
skill_config_id: number; skill_config_id: number;
} }

View File

@@ -1,13 +1,21 @@
import { EntityType } from "./entity"; import { EntityType } from "./entity";
export type StatType = Record<string, number> export type StatType = Record<string, number> | { value: number; type: string };
export interface PropertyType {
value: number;
type: string;
}
export interface StatsType { export interface StatsType {
level: number; level: number;
hp: number; hp: number;
CurrentHP?: number;
MaxHP?: number;
} }
export interface StatChangeType { export interface StatChangeType {
entity: EntityType, entity: EntityType;
stat: StatType, stat?: StatType;
property?: PropertyType;
} }