From f540808514b4a74df163db8dc2042920beb7344f Mon Sep 17 00:00:00 2001 From: Hide_D Date: Fri, 23 Jan 2026 17:42:27 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=20=ED=95=AD=EB=AA=A9?= =?UTF-8?q?=20=EB=B0=8F=20=EC=97=90=EB=84=88=EC=A7=80=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/game-api/src/router/tournament/index.ts | 18 ++ app/game-api/src/tournament/types.ts | 12 ++ app/game-api/src/tournament/worker.ts | 6 + .../src/views/TournamentView.vue | 161 +++++++++++++++--- 4 files changed, 173 insertions(+), 24 deletions(-) diff --git a/app/game-api/src/router/tournament/index.ts b/app/game-api/src/router/tournament/index.ts index 4124852..7944e58 100644 --- a/app/game-api/src/router/tournament/index.ts +++ b/app/game-api/src/router/tournament/index.ts @@ -58,6 +58,24 @@ const zMatch = z.object({ defenderId: z.number().int().positive(), winnerId: z.number().int().positive().optional(), log: z.array(z.string()).optional(), + logEntries: z + .array( + z.object({ + phase: z.number().int().min(0), + attackerEnergy: z.number().int(), + defenderEnergy: z.number().int(), + attackerDamage: z.number().int(), + defenderDamage: z.number().int(), + text: z.string(), + }) + ) + .optional(), + lastEnergy: z + .object({ + attacker: z.number().int(), + defender: z.number().int(), + }) + .optional(), }); const zBetEntry = z.object({ diff --git a/app/game-api/src/tournament/types.ts b/app/game-api/src/tournament/types.ts index 63c8361..af3fc3e 100644 --- a/app/game-api/src/tournament/types.ts +++ b/app/game-api/src/tournament/types.ts @@ -35,6 +35,18 @@ export interface TournamentMatchEntry { defenderId: number; winnerId?: number; log?: string[]; + logEntries?: Array<{ + phase: number; + attackerEnergy: number; + defenderEnergy: number; + attackerDamage: number; + defenderDamage: number; + text: string; + }>; + lastEnergy?: { + attacker: number; + defender: number; + }; } export interface TournamentBetEntry { diff --git a/app/game-api/src/tournament/worker.ts b/app/game-api/src/tournament/worker.ts index db344f0..62ecbf4 100644 --- a/app/game-api/src/tournament/worker.ts +++ b/app/game-api/src/tournament/worker.ts @@ -227,10 +227,16 @@ const applyBattle = async ( baseSeed, }); + const lastLogEntry = result.logEntries[result.logEntries.length - 1] ?? null; + const updatedMatch: TournamentMatchEntry = { ...target, winnerId: result.winnerId ?? undefined, log: result.log, + logEntries: result.logEntries, + lastEnergy: lastLogEntry + ? { attacker: lastLogEntry.attackerEnergy, defender: lastLogEntry.defenderEnergy } + : undefined, }; const nextMatches = matches.map((entry) => (entry.id === target.id ? updatedMatch : entry)); diff --git a/app/game-frontend/src/views/TournamentView.vue b/app/game-frontend/src/views/TournamentView.vue index d019e11..edb9042 100644 --- a/app/game-frontend/src/views/TournamentView.vue +++ b/app/game-frontend/src/views/TournamentView.vue @@ -4,7 +4,6 @@ import { useMediaQuery } from '@vueuse/core'; import PanelCard from '../components/ui/PanelCard.vue'; import SkeletonLines from '../components/ui/SkeletonLines.vue'; import { trpc } from '../utils/trpc'; -import { formatLog } from '../utils/formatLog'; type TournamentSnapshot = Awaited>; type TournamentBettingSummary = Awaited>; @@ -138,30 +137,66 @@ const currentMatch = computed(() => { return matches[fallbackIndex] ?? matches.find((match) => !match.winnerId) ?? matches[0]; }); -const extractEnergy = (logs?: string[]) => { - if (!logs || logs.length === 0) { +const currentEnergy = computed(() => { + const match = currentMatch.value; + if (!match) { return null; } - const line = [...logs].reverse().find((entry) => entry.includes('合')) ?? null; - if (!line) { - return null; + if (match.lastEnergy) { + return match.lastEnergy; } - const values: number[] = []; - const regex = /(\d+)<\/>/g; - let match: RegExpExecArray | null = null; - while ((match = regex.exec(line)) !== null) { - values.push(Number(match[1])); + const lastEntry = match.logEntries?.[match.logEntries.length - 1]; + if (lastEntry) { + return { attacker: lastEntry.attackerEnergy, defender: lastEntry.defenderEnergy }; } - if (values.length < 2) { - return null; - } - return { attacker: values[0], defender: values[1] }; -}; + return null; +}); -const currentEnergy = computed(() => extractEnergy(currentMatch.value?.log)); -const formattedLogs = computed(() => - (currentMatch.value?.log ?? []).map((entry) => formatLog(entry)) -); +const currentEnergyStats = computed(() => { + const match = currentMatch.value; + if (!match) { + return null; + } + const lastEnergy = currentEnergy.value; + if (!lastEnergy) { + return null; + } + let maxAttacker = lastEnergy.attacker; + let maxDefender = lastEnergy.defender; + if (match.logEntries && match.logEntries.length > 0) { + for (const entry of match.logEntries) { + if (entry.attackerEnergy > maxAttacker) { + maxAttacker = entry.attackerEnergy; + } + if (entry.defenderEnergy > maxDefender) { + maxDefender = entry.defenderEnergy; + } + } + } + return { + attacker: lastEnergy.attacker, + defender: lastEnergy.defender, + maxAttacker: Math.max(1, maxAttacker), + maxDefender: Math.max(1, maxDefender), + }; +}); + +const attackerEnergyPercent = computed(() => { + const stats = currentEnergyStats.value; + if (!stats) { + return 0; + } + return Math.max(0, Math.min(100, (stats.attacker / stats.maxAttacker) * 100)); +}); + +const defenderEnergyPercent = computed(() => { + const stats = currentEnergyStats.value; + if (!stats) { + return 0; + } + return Math.max(0, Math.min(100, (stats.defender / stats.maxDefender) * 100)); +}); +const formattedLogs = computed(() => currentMatch.value?.log ?? []); const finalists = computed(() => { const matches = stageMatches(7); @@ -228,8 +263,8 @@ const toggleJoin = async () => { } }; -const bettingTotals = computed(() => bettingSummary.value?.totals ?? {}); -const bettingMine = computed(() => bettingSummary.value?.myTotals ?? {}); +const bettingTotals = computed>(() => bettingSummary.value?.totals ?? {}); +const bettingMine = computed>(() => bettingSummary.value?.myTotals ?? {}); const totalBetAmount = computed(() => bettingSummary.value?.totalAmount ?? 0); const myBetAmount = computed(() => bettingSummary.value?.myAmount ?? 0); @@ -293,12 +328,28 @@ const myBetAmount = computed(() => bettingSummary.value?.myAmount ?? 0);
남은 체력: {{ currentEnergy?.attacker ?? '-' }} / {{ currentEnergy?.defender ?? '-' }}
+
+
+ 공격 +
+
+
+ {{ currentEnergyStats.attacker }} +
+
+ 수비 +
+
+
+ {{ currentEnergyStats.defender }} +
+
@@ -433,12 +484,28 @@ const myBetAmount = computed(() => bettingSummary.value?.myAmount ?? 0);
남은 체력: {{ currentEnergy?.attacker ?? '-' }} / {{ currentEnergy?.defender ?? '-' }}
+
+
+ 공격 +
+
+
+ {{ currentEnergyStats.attacker }} +
+
+ 수비 +
+
+
+ {{ currentEnergyStats.defender }} +
+
@@ -684,6 +751,52 @@ const myBetAmount = computed(() => bettingSummary.value?.myAmount ?? 0); color: rgba(232, 221, 196, 0.7); } +.energy-bars { + margin-top: 6px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.energy-row { + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 8px; + font-size: 0.8rem; +} + +.energy-label { + width: 36px; + color: rgba(232, 221, 196, 0.7); +} + +.energy-track { + height: 8px; + background: rgba(201, 164, 90, 0.15); + border-radius: 999px; + overflow: hidden; +} + +.energy-fill { + height: 100%; + border-radius: 999px; +} + +.energy-fill.attacker { + background: linear-gradient(90deg, rgba(245, 184, 101, 0.9), rgba(245, 184, 101, 0.4)); +} + +.energy-fill.defender { + background: linear-gradient(90deg, rgba(120, 170, 255, 0.9), rgba(120, 170, 255, 0.4)); +} + +.energy-value { + width: 40px; + text-align: right; + color: rgba(232, 221, 196, 0.8); +} + .log-box { padding: 8px; border: 1px solid rgba(201, 164, 90, 0.2);