update for 3.3.1

This commit is contained in:
2025-05-24 15:55:48 +07:00
parent 7782691604
commit dafd0af23d
28 changed files with 513 additions and 161 deletions

View File

@@ -59,7 +59,7 @@
"knight": "存护", "knight": "存护",
"mage": "智识", "mage": "智识",
"priest": "丰饶", "priest": "丰饶",
"rouge": "巡猎", "rogue": "巡猎",
"shaman": "同协", "shaman": "同协",
"warlock": "虚无", "warlock": "虚无",
"memory": "记忆", "memory": "记忆",

View File

@@ -59,7 +59,7 @@
"knight": "Preservation", "knight": "Preservation",
"mage": "Erudition", "mage": "Erudition",
"priest": "Abundance", "priest": "Abundance",
"rouge": "The Hunt", "rogue": "The Hunt",
"shaman": "Harmony", "shaman": "Harmony",
"warlock": "Nihility", "warlock": "Nihility",
"memory": "Remembrance", "memory": "Remembrance",

View File

@@ -59,7 +59,7 @@
"knight": "存護", "knight": "存護",
"mage": "知恵", "mage": "知恵",
"priest": "豊穣", "priest": "豊穣",
"rouge": "巡狩", "rogue": "巡狩",
"shaman": "調和", "shaman": "調和",
"warlock": "虚無", "warlock": "虚無",
"memory": "記憶", "memory": "記憶",

View File

@@ -59,7 +59,7 @@
"knight": "보존", "knight": "보존",
"mage": "지혜", "mage": "지혜",
"priest": "풍요", "priest": "풍요",
"rouge": "사냥", "rogue": "사냥",
"shaman": "조화", "shaman": "조화",
"warlock": "허무", "warlock": "허무",
"memory": "기억", "memory": "기억",

View File

@@ -13,7 +13,7 @@
"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": "Đường dẫn", "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",
@@ -59,7 +59,7 @@
"knight": "Bảo Hộ", "knight": "Bảo Hộ",
"mage": "Tri Thức", "mage": "Tri Thức",
"priest": "Phong Phú", "priest": "Phong Phú",
"rouge": "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",

View File

@@ -59,7 +59,7 @@
"knight": "存护", "knight": "存护",
"mage": "智识", "mage": "智识",
"priest": "丰饶", "priest": "丰饶",
"rouge": "巡猎", "rogue": "巡猎",
"shaman": "同协", "shaman": "同协",
"warlock": "虚无", "warlock": "虚无",
"memory": "记忆", "memory": "记忆",

View File

@@ -10,6 +10,7 @@ import DamagePerAvatarForAll from "@/components/chart/damagePerAvatarForAll";
import MultiCharLineChart from "@/components/chart/damageLineForAll"; import MultiCharLineChart from "@/components/chart/damageLineForAll";
import DamagePerCycleForAll from "@/components/chart/damagePerCycleForAll"; import DamagePerCycleForAll from "@/components/chart/damagePerCycleForAll";
import DamagePercentChartForAll from "@/components/chart/damagePercentForAll"; import DamagePercentChartForAll from "@/components/chart/damagePercentForAll";
import EnemyBar from "@/components/enemybar";
export default function Home() { export default function Home() {
const transI18n = useTranslations("DataAnalysisPage"); const transI18n = useTranslations("DataAnalysisPage");
@@ -18,7 +19,8 @@ export default function Home() {
totalAV, totalAV,
totalDamage, totalDamage,
damagePerAV, damagePerAV,
turnHistory turnHistory,
enemyDetail
} = useBattleDataStore(); } = useBattleDataStore();
const [expandedCharts, setExpandedCharts] = useState<string[]>([]); const [expandedCharts, setExpandedCharts] = useState<string[]>([]);
@@ -55,22 +57,22 @@ export default function Home() {
<div className="grid grid-cols-2 gap-2 mb-3"> <div className="grid grid-cols-2 gap-2 mb-3">
<div className="p-2 text-base lg:text-lg xl:text-xl rounded bg-primary text-primary-content text-center shadow-md"> <div className="p-2 text-base lg:text-lg xl:text-xl rounded bg-primary text-primary-content text-center shadow-md">
{transI18n("totalDamage")} {transI18n("totalDamage")}
<div>{Number(totalDamage).toFixed(2)}</div> <div>{Number(totalDamage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</div>
</div> </div>
<div className="p-2 text-base lg:text-lg xl:text-xl rounded bg-secondary text-secondary-content text-center shadow-md"> <div className="p-2 text-base lg:text-lg xl:text-xl rounded bg-secondary text-secondary-content text-center shadow-md">
{transI18n("totalAV")} {transI18n("totalAV")}
<div>{Number(totalAV).toFixed(2)}</div> <div>{Number(totalAV).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</div>
</div> </div>
<div className="p-2 text-base lg:text-lg xl:text-xl rounded bg-accent text-accent-content text-center shadow-md"> <div className="p-2 text-base lg:text-lg xl:text-xl rounded bg-accent text-accent-content text-center shadow-md">
{transI18n("damagePerAV")} {transI18n("damagePerAV")}
<div>{Number(damagePerAV).toFixed(2)}</div> <div>{Number(damagePerAV).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</div>
</div> </div>
<div className="p-2 text-base lg:text-lg xl:text-xl rounded bg-warning text-warning-content text-center shadow-md"> <div className="p-2 text-base lg:text-lg xl:text-xl rounded bg-warning text-warning-content text-center shadow-md">
{transI18n("totalTurn")} {transI18n("totalTurn")}
<div>{turnHistory.filter(it => it.avatarId && it.avatarId != -1).length}</div> <div>{turnHistory.filter(it => it.avatarId && it.avatarId != -1).length}</div>
</div> </div>
</div> </div>
{enemyDetail && <EnemyBar />}
<div className="rounded-lg p-2 shadow-md flex-grow"> <div className="rounded-lg p-2 shadow-md flex-grow">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">

View File

@@ -140,7 +140,7 @@ export default function ActionBar() {
{`${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")}: ${turn.totalDamage.toFixed(2)}`} {`${transI18n("totalDamage")}: ${Number(turn.totalDamage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}`}
</div> </div>
</div> </div>
</div> </div>
@@ -234,11 +234,11 @@ export default function ActionBar() {
<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">{turnHistory[selectTurn?.turnBattleId].actionValue.toFixed(2)}</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">{selectTurn?.totalDamage.toFixed(2)}</p> <p className="mt-2 font-bold text-lg">{Number(selectTurn?.totalDamage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</p>
</div> </div>
</div> </div>
@@ -252,7 +252,7 @@ export default function ActionBar() {
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">
{detail.damage.toFixed(2)} {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())}

View File

@@ -4,6 +4,9 @@ import { getNameChar } from '@/helper';
import useLocaleStore from '@/stores/localeStore'; import useLocaleStore from '@/stores/localeStore';
import { AvatarHakushiType } from '@/types'; import { AvatarHakushiType } from '@/types';
import NameAvatar from '../nameAvatar'; import NameAvatar from '../nameAvatar';
import useBattleDataStore from '@/stores/battleDataStore';
import { useEffect, useMemo, useState } from 'react';
import { AvatarInfo } from '@/types/mics';
interface CharacterCardProps { interface CharacterCardProps {
data: AvatarHakushiType data: AvatarHakushiType
@@ -14,6 +17,8 @@ interface CharacterCardProps {
export default function CharacterCard({ data }: CharacterCardProps) { export default function CharacterCard({ data }: CharacterCardProps) {
const { locale } = useLocaleStore(); const { locale } = useLocaleStore();
const text = getNameChar(locale, data) const text = getNameChar(locale, data)
const { avatarDetail } = useBattleDataStore()
return ( return (
<li className="z-10 flex flex-col w-28 items-center p-1 rounded-md shadow-lg bg-gradient-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-gradient-to-b from-customStart to-customEnd transform transition-transform duration-300 hover:scale-105 m-1">
<div <div
@@ -50,6 +55,36 @@ export default function CharacterCard({ data }: CharacterCardProps) {
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 && (
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-xs text-base-content/70 mx-1">HP:</span>
<span className="text-xs font-medium">
<span className="text-error">
{Number(avatarDetail?.[Number(data.id)]?.stats?.HP ?? 0).toLocaleString(undefined, { maximumFractionDigits: 0 })}
</span>
<span className="text-base-content/50">/</span>
<span className="text-base-content/70">
{Number(avatarDetail?.[Number(data.id)]?.stats?.MaxHP ?? 100).toLocaleString(undefined, { maximumFractionDigits: 0 })}
</span>
</span>
</div>
<div className="relative w-full bg-base-300 rounded-full h-2.5">
<div
className="bg-error h-2.5 rounded-full transition-all duration-300"
style={{
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">
{Math.round(((avatarDetail?.[Number(data.id)]?.stats?.HP || 0) / (avatarDetail?.[Number(data.id)]?.stats?.MaxHP || 100)) * 100)}%
</span>
</div>
</div>
)}
</li> </li>
); );

View File

@@ -0,0 +1,71 @@
"use client"
import useBattleDataStore from "@/stores/battleDataStore";
import Image from "next/image";
function formatEnemyIdForURL(id?: number): string {
const n = id ?? 0;
const adjusted = n.toString().length === 9 ? n / 100 : n;
return adjusted.toFixed(0);
}
export default function EnemyBar() {
const { enemyDetail } = useBattleDataStore()
return (
<div className="p-3 w-full">
<div className="flex gap-3 overflow-x-auto pb-2">
{enemyDetail && Object.values(enemyDetail).filter((enemy) => (enemy.stats?.AV > 0
&& 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 flex-shrink-0">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/Monster_${formatEnemyIdForURL(enemy.id)}.webp`}
alt={enemy.name}
width={40}
height={40}
className="object-cover w-10 h-10 rounded-lg"
/>
<div className="flex-1 min-w-0">
<h3 className="text-base font-semibold leading-tight truncate overflow-hidden" title={enemy.name}>
{enemy.name}
</h3>
<p className="text-base-content/70 text-xs">Level {enemy.level || 1}</p>
</div>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<div className="text-xs text-base-content/70">HP:</div>
<div className="text-xs font-medium">
<div className="text-error">
{Number(enemy?.stats?.HP ?? 0).toLocaleString(undefined, { maximumFractionDigits: 0 })}
</div>
<div className="text-base-content/50 mx-1">/</div>
<div className="text-base-content/70">
{Number(enemy?.maxHP ?? 100).toLocaleString(undefined, { maximumFractionDigits: 0 })}
</div>
</div>
</div>
<div className="relative w-full bg-base-300 rounded-full h-2.5">
<div
className="bg-error h-2.5 rounded-full transition-all duration-300"
style={{
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">
{Math.round(((enemy.stats?.HP || 0) / (enemy.maxHP || 100)) * 100)}%
</div>
</div>
</div>
</div>
))}
</div>
</div>
)
}

View File

@@ -23,7 +23,7 @@ const themes = [
export default function Header() { export default function Header() {
const { changeTheme } = useChangeTheme() const { changeTheme } = useChangeTheme()
const { locale, setLocale } = useLocaleStore() const { locale, setLocale } = useLocaleStore()
const { loadBattleDataFromJSON } = 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();
@@ -219,6 +219,16 @@ export default function Header() {
</h1> </h1>
<p className="text-sm text-gray-500">For Veritas</p> <p className="text-sm text-gray-500">For Veritas</p>
</a> </a>
{version && (
<div className="px-2">
<div className="inline-flex items-center space-x-2 px-3 py-1.5 bg-gradient-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="text-xs font-semibold text-white">
{version}
</div>
</div>
</div>
)}
</div> </div>

View File

@@ -209,7 +209,7 @@ export default function LineupBar() {
<p> <p>
{transI18n("eidolons")}: <span className="font-bold">{avatar?.data?.rank}</span> {transI18n("eidolons")}: <span className="font-bold">{avatar?.data?.rank}</span>
</p> </p>
<p 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
src={`https://api.hakush.in/hsr/UI/lightconemediumicon/${avatar?.Lightcone?.item_id}.webp`} src={`https://api.hakush.in/hsr/UI/lightconemediumicon/${avatar?.Lightcone?.item_id}.webp`}
@@ -218,8 +218,8 @@ export default function LineupBar() {
width={200} width={200}
height={200} height={200}
/> />
</p> </div>
<p 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 => (
@@ -233,7 +233,7 @@ export default function LineupBar() {
/> />
))} ))}
</div> </div>
</p> </div>
</> </>
); );
})()} })()}
@@ -256,10 +256,10 @@ export default function LineupBar() {
<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">{totalTurn.toFixed(2)}</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">{totalDamage.toFixed(2)}</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">

View File

@@ -14,7 +14,10 @@ export const exportBattleData = (
waveIndex, waveIndex,
dataAvatar, dataAvatar,
maxWave, maxWave,
maxCycle maxCycle,
version,
avatarDetail,
enemyDetail
} = useBattleDataStore.getState(); } = useBattleDataStore.getState();
const data: BattleDataStateJson = { const data: BattleDataStateJson = {
@@ -28,7 +31,10 @@ export const exportBattleData = (
cycleIndex, cycleIndex,
waveIndex, waveIndex,
maxWave, maxWave,
maxCycle maxCycle,
version,
avatarDetail,
enemyDetail
} }
const dataStr = JSON.stringify(data, null, 2); const dataStr = JSON.stringify(data, null, 2);

View File

@@ -5,7 +5,7 @@ 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 === "FireflyPSLocal") { 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, {

View File

@@ -26,17 +26,21 @@ function safeParse(json: unknown | string) {
export const connectSocket = (): Socket => { export const connectSocket = (): Socket => {
const { host, port, connectionType, setStatus } = useSocketStore.getState(); const { host, port, connectionType, setStatus } = useSocketStore.getState();
const { const {
onSetBattleLineupService, onConnectedService,
onTurnEndService,
onUseSkillService,
onKillService,
onDamageService,
onBattleEndService,
onTurnBeginService,
onBattleBeginService, onBattleBeginService,
onCreateBattleService, onSetBattleLineupService,
onDamageService,
onTurnBeginService,
onTurnEndService,
onEntityDefeatedService,
onUseSkillService,
onUpdateWaveService,
onUpdateCycleService, onUpdateCycleService,
OnUpdateWaveService onStatChange,
onUpdateTeamFormation,
onInitializeEnemyService,
onBattleEndService,
onCreateBattleService,
} = useBattleDataStore.getState(); } = useBattleDataStore.getState();
let url = `${host}:${port}`; let url = `${host}:${port}`;
@@ -86,68 +90,73 @@ export const connectSocket = (): Socket => {
setStatus(true); setStatus(true);
notify(`Kết nối thành công với Socket ID: ${socket?.id}`, 'success'); notify(`Kết nối thành công với Socket ID: ${socket?.id}`, 'success');
}; };
const onBattleBegin = (data: BattleBeginType) => { const onBattleBegin = (data: BattleBeginType) => {
notify("Battle Started!", "info") notify("Battle Started!", "info")
onBattleBeginService(data) onBattleBeginService(data)
} }
if (isSocketConnected()) onConnect(); if (isSocketConnected()) onConnect();
socket.on("Connected", (json) => {
socket.on("OnSetBattleLineup", (json) => {
const data = safeParse(json); const data = safeParse(json);
if (data) onSetBattleLineupService(data); if (data) onConnectedService(data);
}); });
socket.on("OnTurnEnd", (json) => {
const data = safeParse(json);
if (data) onTurnEndService(data);
});
socket.on("OnUseSkill", (json) => {
const data = safeParse(json);
if (data) onUseSkillService(data);
});
socket.on("OnKill", (json) => {
const data = safeParse(json);
if (data) onKillService(data);
});
socket.on("OnDamage", (json) => {
const data = safeParse(json);
if (data) onDamageService(data);
});
socket.on("OnBattleBegin", (json) => { socket.on("OnBattleBegin", (json) => {
const data = safeParse(json); const data = safeParse(json);
if (data) onBattleBegin(data); if (data) onBattleBegin(data);
}); });
socket.on("OnSetBattleLineup", (json) => {
const data = safeParse(json);
if (data) onSetBattleLineupService(data);
});
socket.on("OnDamage", (json) => {
const data = safeParse(json);
if (data) onDamageService(data);
});
socket.on("OnTurnBegin", (json) => { socket.on("OnTurnBegin", (json) => {
const data = safeParse(json); const data = safeParse(json);
if (data) onTurnBeginService(data); if (data) onTurnBeginService(data);
}); });
socket.on("OnTurnEnd", (json) => {
socket.on("OnBattleEnd", (json) => {
const data = safeParse(json); const data = safeParse(json);
if (data) onBattleEndService(data); if (data) onTurnEndService(data);
});
socket.on("OnEntityDefeated", (json) => {
const data = safeParse(json);
if (data) onEntityDefeatedService(data);
});
socket.on("OnUseSkill", (json) => {
const data = safeParse(json);
if (data) onUseSkillService(data);
});
socket.on("OnUpdateWave", (json) => {
const data = safeParse(json);
if (data) onUpdateWaveService(data);
}); });
socket.on("OnUpdateCycle", (json) => { socket.on("OnUpdateCycle", (json) => {
const data = safeParse(json); const data = safeParse(json);
if (data) onUpdateCycleService(data); if (data) onUpdateCycleService(data);
}); });
socket.on("OnStatChange", (json) => {
socket.on("OnUpdateWave", (json) => {
const data = safeParse(json); const data = safeParse(json);
if (data) OnUpdateWaveService(data); if (data) onStatChange(data);
});
socket.on("OnUpdateTeamFormation", (json) => {
const data = safeParse(json);
if (data) onUpdateTeamFormation(data);
});
socket.on("OnInitializeEnemy", (json) => {
const data = safeParse(json);
if (data) onInitializeEnemyService(data);
});
socket.on("OnBattleEnd", (json) => {
const data = safeParse(json);
if (data) onBattleEndService(data);
}); });
socket.on("OnCreateBattle", (json) => { socket.on("OnCreateBattle", (json) => {
const data = safeParse(json); const data = safeParse(json);
if (data) onCreateBattleService(data); if (data) onCreateBattleService(data);
}); });
socket.on("Error", (msg: string) => { socket.on("Error", (msg: string) => {
console.error("Server Error:", msg); console.error("Server Error:", msg);
}); });
@@ -157,34 +166,42 @@ export const connectSocket = (): Socket => {
export const disconnectSocket = (): void => { export const disconnectSocket = (): void => {
const { const {
onSetBattleLineupService, onConnectedService,
onTurnEndService,
onUseSkillService,
onKillService,
onDamageService,
onBattleEndService,
onTurnBeginService,
onBattleBeginService, onBattleBeginService,
onCreateBattleService, onSetBattleLineupService,
onDamageService,
onTurnBeginService,
onTurnEndService,
onEntityDefeatedService,
onUseSkillService,
onUpdateWaveService,
onUpdateCycleService, onUpdateCycleService,
OnUpdateWaveService onStatChange,
onUpdateTeamFormation,
onInitializeEnemyService,
onBattleEndService,
onCreateBattleService,
} = useBattleDataStore.getState(); } = useBattleDataStore.getState();
const onBattleBegin = (data: BattleBeginType) => { const onBattleBegin = (data: BattleBeginType) => {
notify("Battle Started!", "info") notify("Battle Started!", "info")
onBattleBeginService(data) onBattleBeginService(data)
} }
if (socket) { if (socket) {
socket.off("Connected", (json) => onConnectedService(JSON.parse(json)));
socket.off("OnBattleBegin", (json) => onBattleBegin(JSON.parse(json)));
socket.off("OnSetBattleLineup", (json) => onSetBattleLineupService(JSON.parse(json))); socket.off("OnSetBattleLineup", (json) => onSetBattleLineupService(JSON.parse(json)));
socket.off("OnTurnEnd", (json) => onTurnEndService(JSON.parse(json))); socket.off("OnTurnEnd", (json) => onTurnEndService(JSON.parse(json)));
socket.off("OnUseSkill", (json) => onUseSkillService(JSON.parse(json))); socket.off("OnUseSkill", (json) => onUseSkillService(JSON.parse(json)));
socket.off("OnKill", (json) => onKillService(JSON.parse(json))); socket.off("OnEntityDefeated", (json) => onEntityDefeatedService(JSON.parse(json)));
socket.off("OnDamage", (json) => onDamageService(JSON.parse(json))); socket.off("OnDamage", (json) => onDamageService(JSON.parse(json)));
socket.off('OnBattleBegin', (json) => onBattleBegin(JSON.parse(json)));
socket.off('OnTurnBegin', (json) => onTurnBeginService(JSON.parse(json))); socket.off('OnTurnBegin', (json) => onTurnBeginService(JSON.parse(json)));
socket.off('OnBattleEnd', (json) => onBattleEndService(JSON.parse(json))); socket.off('OnBattleEnd', (json) => onBattleEndService(JSON.parse(json)));
socket.off('OnUpdateCycle', (json) => onUpdateCycleService(JSON.parse(json))); socket.off('OnUpdateCycle', (json) => onUpdateCycleService(JSON.parse(json)));
socket.off('OnUpdateWave', (json) => OnUpdateWaveService(JSON.parse(json))); socket.off('OnUpdateWave', (json) => onUpdateWaveService(JSON.parse(json)));
socket.off('OnCreateBattle', (json) => onCreateBattleService(JSON.parse(json))); socket.off('OnCreateBattle', (json) => onCreateBattleService(JSON.parse(json)));
socket.off('OnStatChange', (json) => onStatChange(JSON.parse(json)));
socket.off('OnUpdateTeamFormation', (json) => onUpdateTeamFormation(JSON.parse(json)));
socket.off('OnInitializeEnemy', (json) => onInitializeEnemyService(JSON.parse(json)));
socket.offAny(); socket.offAny();
socket.disconnect(); socket.disconnect();
useSocketStore.getState().setStatus(false); useSocketStore.getState().setStatus(false);

View File

@@ -1,5 +1,6 @@
import { AttackResultType, AvatarAnalysisJson, AvatarSkillType, BattleBeginType, BattleEndType, DamageDetailType, KillType, LineUpType, TurnBeginType, TurnEndType, UpdateCycleType, UpdateWaveType } from '@/types'; import { DamageType, AvatarAnalysisJson, UseSkillType, BattleBeginType, BattleEndType, DamageDetailType, EntityDefeatedType, SetBattleLineupType, TurnBeginType, TurnEndType, UpdateCycleType, UpdateWaveType, VersionType, StatType, StatChangeType, UpdateTeamFormationType } from '@/types';
import { AvatarBattleInfo, BattleDataStateJson, SkillBattleInfo, TurnBattleInfo } from '@/types/mics'; import { InitializeEnemyType } from '@/types/enemy';
import { AvatarBattleInfo, AvatarInfo, BattleDataStateJson, EnemyInfo, SkillBattleInfo, TurnBattleInfo } from '@/types/mics';
import { create } from 'zustand' import { create } from 'zustand'
@@ -14,19 +15,26 @@ interface BattleDataState {
cycleIndex: number; cycleIndex: number;
waveIndex: number; waveIndex: number;
maxWave: number; maxWave: number;
maxCycle: number maxCycle: number;
version?: string;
avatarDetail?: Record<number, AvatarInfo>;
enemyDetail?: Record<number, EnemyInfo>;
onSetBattleLineupService: (data: LineUpType) => void; onConnectedService: (data: VersionType) => void
onTurnEndService: (data: TurnEndType) => void;
onBattleEndService: (data: BattleEndType) => void;
onUseSkillService: (data: AvatarSkillType) => void;
onKillService: (data: KillType) => void
onDamageService: (data: AttackResultType) => void;
onBattleBeginService: (data: BattleBeginType) => void; onBattleBeginService: (data: BattleBeginType) => void;
onSetBattleLineupService: (data: SetBattleLineupType) => void;
onDamageService: (data: DamageType) => void;
onTurnBeginService: (data: TurnBeginType) => void; onTurnBeginService: (data: TurnBeginType) => void;
onCreateBattleService: (data: AvatarAnalysisJson[]) => void; onTurnEndService: (data: TurnEndType) => void;
OnUpdateWaveService: (data: UpdateWaveType) => void; onEntityDefeatedService: (data: EntityDefeatedType) => void;
onUseSkillService: (data: UseSkillType) => void;
onUpdateWaveService: (data: UpdateWaveType) => void;
onUpdateCycleService: (data: UpdateCycleType) => void; onUpdateCycleService: (data: UpdateCycleType) => void;
onStatChange: (data: StatChangeType) => void;
onUpdateTeamFormation: (data: UpdateTeamFormationType) => void;
onInitializeEnemyService: (data: InitializeEnemyType) => void;
onBattleEndService: (data: BattleEndType) => void;
onCreateBattleService: (data: AvatarAnalysisJson[]) => void;
loadBattleDataFromJSON: (data: BattleDataStateJson) => void; loadBattleDataFromJSON: (data: BattleDataStateJson) => void;
} }
@@ -42,7 +50,9 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
waveIndex: 1, waveIndex: 1,
maxWave: Infinity, maxWave: Infinity,
maxCycle: Infinity, maxCycle: Infinity,
version: undefined,
avatarDetail: undefined,
enemyDetail: undefined,
loadBattleDataFromJSON: (data: BattleDataStateJson) => { loadBattleDataFromJSON: (data: BattleDataStateJson) => {
set({ set({
lineup: data.lineup, lineup: data.lineup,
@@ -55,7 +65,15 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
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,
avatarDetail: data.avatarDetail,
enemyDetail: data.enemyDetail
})
},
onConnectedService: (data: VersionType) => {
set({
version: data.version
}) })
}, },
onCreateBattleService: (data: AvatarAnalysisJson[]) => { onCreateBattleService: (data: AvatarAnalysisJson[]) => {
@@ -75,36 +93,7 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
turnHistory: updatedHistory turnHistory: updatedHistory
}) })
}, },
onDamageService: (data: AttackResultType) => { onSetBattleLineupService: (data: SetBattleLineupType) => {
const skillHistory = get().skillHistory
const skillIdx = skillHistory.findLastIndex(it => it.avatarId === data.attacker.id)
if (skillIdx === -1) {
return
}
const newTh = [...skillHistory]
newTh[skillIdx].damageDetail.push({damage: data.damage, damage_type: data?.damage_type} as DamageDetailType)
newTh[skillIdx].totalDamage += data.damage
set({
skillHistory: newTh,
totalDamage: get().totalDamage + data.damage,
damagePerAV: (get().totalDamage + data.damage) / (get().totalAV === 0 ? 1 : get().totalAV)
})
},
onKillService: (data: KillType) => {
const lineups = get().lineup
const avatarIdx = lineups.findIndex(it => it.avatarId === data.attacker.id)
if (avatarIdx === -1) {
return
}
const newLn = [...lineups]
newLn[avatarIdx].isDie = true
set({
lineup: newLn
})
},
onSetBattleLineupService: (data: LineUpType) => {
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)
@@ -125,13 +114,28 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
waveIndex: 1, waveIndex: 1,
})); }));
}, },
onTurnBeginService: (data: TurnBeginType) => { 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].totalDamage += data.damage
set({
skillHistory: newTh,
totalDamage: get().totalDamage + data.damage,
damagePerAV: (get().totalDamage + data.damage) / (get().totalAV === 0 ? 1 : get().totalAV)
})
},
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?.id, 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
@@ -146,11 +150,35 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
/ (data.turn_info.action_value === 0 ? 1 : data.turn_info.action_value) / (data.turn_info.action_value === 0 ? 1 : data.turn_info.action_value)
})); }));
}, },
onUseSkillService: (data: AvatarSkillType) => { onEntityDefeatedService: (data: EntityDefeatedType) => {
let avatarDetail = get().avatarDetail
let enemyDetail = get().enemyDetail
if (!enemyDetail) {
enemyDetail = {} as Record<number, EnemyInfo>
}
if (!avatarDetail) {
avatarDetail = {} as Record<number, AvatarInfo>
}
if (data.killer.team === "Player" && enemyDetail[data.entity_defeated.uid]) {
enemyDetail[data.entity_defeated.uid].isDie = true
enemyDetail[data.entity_defeated.uid].killer_uid = data.killer.uid
} 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) => ({ set((state) => ({
skillHistory: [...state.skillHistory, { skillHistory: [...state.skillHistory, {
avatarId: data.avatar.id, avatarId: data.avatar.uid,
damageDetail: [], damageDetail: [],
totalDamage: 0, totalDamage: 0,
skillType: data.skill.type, skillType: data.skill.type,
@@ -159,6 +187,120 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
} as SkillBattleInfo] } 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
if (!enemyDetail) {
enemyDetail = {} as Record<number, EnemyInfo>
}
if (!avatarDetail) {
avatarDetail = {} as Record<number, AvatarInfo>
}
if (data.entity.team === "Player") {
const [key, value] = Object.entries(data.stat)[0]
const uid = data.entity.uid;
if (!avatarDetail[uid]) {
avatarDetail[uid] = {
id: uid,
isDie: false,
killer_uid: -1,
stats: {},
statsHistory: []
};
}
avatarDetail[uid].stats[key] = value
avatarDetail[uid].statsHistory.push({
stats: data.stat,
turnBattleId: get().turnHistory.length-1
})
} else {
const [key, value] = Object.entries(data.stat)[0]
const uid = data.entity.uid;
if (!enemyDetail[uid]) {
enemyDetail[uid] = {
id: uid,
isDie: false,
killer_uid: -1,
name: "",
positionIndex: Object.keys(get().enemyDetail || {}).length,
maxHP: 0,
waveIndex: 0,
level: 0,
stats: {},
statsHistory: []
};
}
enemyDetail[uid].stats[key] = value
enemyDetail[uid].statsHistory.push({
stats: data.stat,
turnBattleId: get().turnHistory.length-1
})
}
set({
avatarDetail: avatarDetail,
enemyDetail: enemyDetail
})
},
onUpdateTeamFormation: (data: UpdateTeamFormationType) => {
let avatarDetail = get().avatarDetail
let enemyDetail = get().enemyDetail
if (!avatarDetail) {
avatarDetail = {} as Record<number, AvatarInfo>
for (const entity of data.entities) {
if (entity.team === "Player" && avatarDetail[entity.uid]) {
}
}
}
if (!enemyDetail) {
enemyDetail = {} as Record<number, EnemyInfo>
for (let i = 0; i < data.entities.length; i++) {
const entity = data.entities[i];
if (entity.team === "Enemy" && enemyDetail[entity.uid]) {
enemyDetail[entity.uid].positionIndex = i
}
}
}
set({
avatarDetail: avatarDetail,
enemyDetail: enemyDetail
})
},
onInitializeEnemyService: (data: InitializeEnemyType) => {
const enemyDetail = get().enemyDetail
if (!enemyDetail) {
return
}
enemyDetail[data.enemy.uid] = {
id: data.enemy.id,
isDie: false,
killer_uid: -1,
positionIndex: enemyDetail[data.enemy.uid].positionIndex,
waveIndex: get().waveIndex,
name: data.enemy.name,
maxHP: data.enemy.base_stats.hp,
level: data.enemy.base_stats.level,
stats: {},
statsHistory: []
}
set({
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) {
@@ -171,16 +313,6 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
damagePerAV: data.total_damage / (data.action_value === 0 ? 1 : data.action_value) damagePerAV: data.total_damage / (data.action_value === 0 ? 1 : data.action_value)
}) })
}, },
OnUpdateWaveService: (data: UpdateWaveType) => {
set({
waveIndex: data.wave
})
},
onUpdateCycleService: (data: UpdateCycleType) => {
set({
cycleIndex: data.cycle
})
}
})); }));
export default useBattleDataStore; export default useBattleDataStore;

View File

@@ -1,7 +1,7 @@
import { AvatarType } from "./lineup"; import { EntityType } from "./entity";
export interface AttackResultType { export interface DamageType {
attacker: AvatarType; attacker: EntityType;
damage: number; damage: number;
damage_type?: AttackType damage_type?: AttackType
} }

View File

@@ -10,9 +10,6 @@ export interface BattleEndType {
action_value: number; action_value: number;
stage_id: number; stage_id: number;
} }
export interface KillType {
attacker: AvatarType;
}
export interface BattleBeginType { export interface BattleBeginType {
max_waves: number max_waves: number

12
src/types/enemy.ts Normal file
View File

@@ -0,0 +1,12 @@
import { StatsType } from "./stat";
export interface EnemyType {
id: number;
uid: number;
name: string;
base_stats: StatsType
}
export interface InitializeEnemyType {
enemy: EnemyType
}

9
src/types/entity.ts Normal file
View File

@@ -0,0 +1,9 @@
export interface EntityType {
uid: number;
team: "Player" | "Enemy";
}
export interface EntityDefeatedType {
killer: EntityType,
entity_defeated: EntityType
}

3
src/types/error.ts Normal file
View File

@@ -0,0 +1,3 @@
export interface ErrorType {
msg: string
}

View File

@@ -6,3 +6,6 @@ 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 "./entity"
export * from "./stat"

View File

@@ -1,8 +1,16 @@
import { EntityType } from "./entity";
export interface AvatarType{ export interface AvatarType{
id: number; id: number;
name: string; name: string;
} }
export interface LineUpType { export interface SetBattleLineupType {
avatars: AvatarType[]; avatars: AvatarType[];
} }
export interface UpdateTeamFormationType {
entities: EntityType[],
team: "Player" | "Enemy"
}

View File

@@ -1,9 +1,11 @@
import {AttackType, DamageDetailType} from "./attack"; import {AttackType, DamageDetailType} from "./attack";
import { AvatarAnalysisJson } from "./srtools"; import { AvatarAnalysisJson } from "./srtools";
import { StatType } from "./stat";
export interface AvatarBattleInfo { export interface AvatarBattleInfo {
avatarId: number; avatarId: number;
isDie: boolean; isDie?: boolean;
} }
export interface SkillBattleInfo { export interface SkillBattleInfo {
@@ -33,5 +35,34 @@ export interface BattleDataStateJson {
maxWave: number; maxWave: number;
cycleIndex: number, cycleIndex: number,
waveIndex: number, waveIndex: number,
maxCycle: number maxCycle: number,
version?: string,
avatarDetail?: Record<number, AvatarInfo>;
enemyDetail?: Record<number, EnemyInfo>;
} }
export interface StatsHistoryType {
stats: StatType
turnBattleId: number;
}
export interface EnemyInfo {
id: number;
name: string;
maxHP: number;
level: number;
isDie: boolean;
positionIndex: number;
waveIndex: number;
killer_uid: number;
stats: Record<string, number>;
statsHistory: StatsHistoryType[];
}
export interface AvatarInfo {
id: number;
isDie: boolean;
killer_uid: number;
stats: Record<string, number>;
statsHistory: StatsHistoryType[];
}

View File

@@ -1,12 +1,14 @@
import { AvatarType } from "./lineup";
import { AttackType } from "@/types/attack"; import { AttackType } from "@/types/attack";
import { EntityType } from "./entity";
export interface SkillInfo { export interface SkillInfo {
name: string; name: string;
type: AttackType; type: AttackType;
skill_config_id: number;
} }
export interface AvatarSkillType { export interface UseSkillType {
avatar: AvatarType; avatar: EntityType;
skill: SkillInfo; skill: SkillInfo;
} }

13
src/types/stat.ts Normal file
View File

@@ -0,0 +1,13 @@
import { EntityType } from "./entity";
export type StatType = Record<string, number>
export interface StatsType {
level: number;
hp: number;
}
export interface StatChangeType {
entity: EntityType,
stat: StatType,
}

View File

@@ -1,5 +1,4 @@
import { AvatarType } from "./lineup"; import { EntityType } from "./entity";
export interface TurnInfoType { export interface TurnInfoType {
avatars_turn_damage: number[]; avatars_turn_damage: number[];
@@ -11,10 +10,9 @@ export interface TurnInfoType {
export interface TurnBeginType { export interface TurnBeginType {
action_value: number; action_value: number;
turn_owner?: AvatarType turn_owner?: EntityType | null
} }
export interface TurnEndType { export interface TurnEndType {
avatars: AvatarType[];
turn_info: TurnInfoType turn_info: TurnInfoType
} }

3
src/types/version.ts Normal file
View File

@@ -0,0 +1,3 @@
export interface VersionType {
version: string
}