feat: 로그 항목 및 에너지 상태 추가
This commit is contained in:
@@ -58,6 +58,24 @@ const zMatch = z.object({
|
|||||||
defenderId: z.number().int().positive(),
|
defenderId: z.number().int().positive(),
|
||||||
winnerId: z.number().int().positive().optional(),
|
winnerId: z.number().int().positive().optional(),
|
||||||
log: z.array(z.string()).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({
|
const zBetEntry = z.object({
|
||||||
|
|||||||
@@ -35,6 +35,18 @@ export interface TournamentMatchEntry {
|
|||||||
defenderId: number;
|
defenderId: number;
|
||||||
winnerId?: number;
|
winnerId?: number;
|
||||||
log?: string[];
|
log?: string[];
|
||||||
|
logEntries?: Array<{
|
||||||
|
phase: number;
|
||||||
|
attackerEnergy: number;
|
||||||
|
defenderEnergy: number;
|
||||||
|
attackerDamage: number;
|
||||||
|
defenderDamage: number;
|
||||||
|
text: string;
|
||||||
|
}>;
|
||||||
|
lastEnergy?: {
|
||||||
|
attacker: number;
|
||||||
|
defender: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TournamentBetEntry {
|
export interface TournamentBetEntry {
|
||||||
|
|||||||
@@ -227,10 +227,16 @@ const applyBattle = async (
|
|||||||
baseSeed,
|
baseSeed,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const lastLogEntry = result.logEntries[result.logEntries.length - 1] ?? null;
|
||||||
|
|
||||||
const updatedMatch: TournamentMatchEntry = {
|
const updatedMatch: TournamentMatchEntry = {
|
||||||
...target,
|
...target,
|
||||||
winnerId: result.winnerId ?? undefined,
|
winnerId: result.winnerId ?? undefined,
|
||||||
log: result.log,
|
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));
|
const nextMatches = matches.map((entry) => (entry.id === target.id ? updatedMatch : entry));
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useMediaQuery } from '@vueuse/core';
|
|||||||
import PanelCard from '../components/ui/PanelCard.vue';
|
import PanelCard from '../components/ui/PanelCard.vue';
|
||||||
import SkeletonLines from '../components/ui/SkeletonLines.vue';
|
import SkeletonLines from '../components/ui/SkeletonLines.vue';
|
||||||
import { trpc } from '../utils/trpc';
|
import { trpc } from '../utils/trpc';
|
||||||
import { formatLog } from '../utils/formatLog';
|
|
||||||
|
|
||||||
type TournamentSnapshot = Awaited<ReturnType<typeof trpc.tournament.getSnapshot.query>>;
|
type TournamentSnapshot = Awaited<ReturnType<typeof trpc.tournament.getSnapshot.query>>;
|
||||||
type TournamentBettingSummary = Awaited<ReturnType<typeof trpc.tournament.getBettingSummary.query>>;
|
type TournamentBettingSummary = Awaited<ReturnType<typeof trpc.tournament.getBettingSummary.query>>;
|
||||||
@@ -138,30 +137,66 @@ const currentMatch = computed(() => {
|
|||||||
return matches[fallbackIndex] ?? matches.find((match) => !match.winnerId) ?? matches[0];
|
return matches[fallbackIndex] ?? matches.find((match) => !match.winnerId) ?? matches[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
const extractEnergy = (logs?: string[]) => {
|
const currentEnergy = computed(() => {
|
||||||
if (!logs || logs.length === 0) {
|
const match = currentMatch.value;
|
||||||
|
if (!match) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const line = [...logs].reverse().find((entry) => entry.includes('合')) ?? null;
|
if (match.lastEnergy) {
|
||||||
if (!line) {
|
return match.lastEnergy;
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
const values: number[] = [];
|
const lastEntry = match.logEntries?.[match.logEntries.length - 1];
|
||||||
const regex = /<C>(\d+)<\/>/g;
|
if (lastEntry) {
|
||||||
let match: RegExpExecArray | null = null;
|
return { attacker: lastEntry.attackerEnergy, defender: lastEntry.defenderEnergy };
|
||||||
while ((match = regex.exec(line)) !== null) {
|
|
||||||
values.push(Number(match[1]));
|
|
||||||
}
|
}
|
||||||
if (values.length < 2) {
|
return null;
|
||||||
return null;
|
});
|
||||||
}
|
|
||||||
return { attacker: values[0], defender: values[1] };
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentEnergy = computed(() => extractEnergy(currentMatch.value?.log));
|
const currentEnergyStats = computed(() => {
|
||||||
const formattedLogs = computed(() =>
|
const match = currentMatch.value;
|
||||||
(currentMatch.value?.log ?? []).map((entry) => formatLog(entry))
|
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 finalists = computed(() => {
|
||||||
const matches = stageMatches(7);
|
const matches = stageMatches(7);
|
||||||
@@ -228,8 +263,8 @@ const toggleJoin = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const bettingTotals = computed(() => bettingSummary.value?.totals ?? {});
|
const bettingTotals = computed<Record<number, number>>(() => bettingSummary.value?.totals ?? {});
|
||||||
const bettingMine = computed(() => bettingSummary.value?.myTotals ?? {});
|
const bettingMine = computed<Record<number, number>>(() => bettingSummary.value?.myTotals ?? {});
|
||||||
const totalBetAmount = computed(() => bettingSummary.value?.totalAmount ?? 0);
|
const totalBetAmount = computed(() => bettingSummary.value?.totalAmount ?? 0);
|
||||||
const myBetAmount = computed(() => bettingSummary.value?.myAmount ?? 0);
|
const myBetAmount = computed(() => bettingSummary.value?.myAmount ?? 0);
|
||||||
</script>
|
</script>
|
||||||
@@ -293,12 +328,28 @@ const myBetAmount = computed(() => bettingSummary.value?.myAmount ?? 0);
|
|||||||
<div class="match-meta">
|
<div class="match-meta">
|
||||||
남은 체력: {{ currentEnergy?.attacker ?? '-' }} / {{ currentEnergy?.defender ?? '-' }}
|
남은 체력: {{ currentEnergy?.attacker ?? '-' }} / {{ currentEnergy?.defender ?? '-' }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="currentEnergyStats" class="energy-bars">
|
||||||
|
<div class="energy-row">
|
||||||
|
<span class="energy-label">공격</span>
|
||||||
|
<div class="energy-track">
|
||||||
|
<div class="energy-fill attacker" :style="{ width: `${attackerEnergyPercent}%` }" />
|
||||||
|
</div>
|
||||||
|
<span class="energy-value">{{ currentEnergyStats.attacker }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="energy-row">
|
||||||
|
<span class="energy-label">수비</span>
|
||||||
|
<div class="energy-track">
|
||||||
|
<div class="energy-fill defender" :style="{ width: `${defenderEnergyPercent}%` }" />
|
||||||
|
</div>
|
||||||
|
<span class="energy-value">{{ currentEnergyStats.defender }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="log-box">
|
<div class="log-box">
|
||||||
<div
|
<div
|
||||||
v-for="(entry, idx) in formattedLogs"
|
v-for="(entry, idx) in formattedLogs"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
class="log-line"
|
class="log-line"
|
||||||
v-html="entry"
|
v-text="entry"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -433,12 +484,28 @@ const myBetAmount = computed(() => bettingSummary.value?.myAmount ?? 0);
|
|||||||
<div class="match-meta">
|
<div class="match-meta">
|
||||||
남은 체력: {{ currentEnergy?.attacker ?? '-' }} / {{ currentEnergy?.defender ?? '-' }}
|
남은 체력: {{ currentEnergy?.attacker ?? '-' }} / {{ currentEnergy?.defender ?? '-' }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="currentEnergyStats" class="energy-bars">
|
||||||
|
<div class="energy-row">
|
||||||
|
<span class="energy-label">공격</span>
|
||||||
|
<div class="energy-track">
|
||||||
|
<div class="energy-fill attacker" :style="{ width: `${attackerEnergyPercent}%` }" />
|
||||||
|
</div>
|
||||||
|
<span class="energy-value">{{ currentEnergyStats.attacker }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="energy-row">
|
||||||
|
<span class="energy-label">수비</span>
|
||||||
|
<div class="energy-track">
|
||||||
|
<div class="energy-fill defender" :style="{ width: `${defenderEnergyPercent}%` }" />
|
||||||
|
</div>
|
||||||
|
<span class="energy-value">{{ currentEnergyStats.defender }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="log-box">
|
<div class="log-box">
|
||||||
<div
|
<div
|
||||||
v-for="(entry, idx) in formattedLogs"
|
v-for="(entry, idx) in formattedLogs"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
class="log-line"
|
class="log-line"
|
||||||
v-html="entry"
|
v-text="entry"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -684,6 +751,52 @@ const myBetAmount = computed(() => bettingSummary.value?.myAmount ?? 0);
|
|||||||
color: rgba(232, 221, 196, 0.7);
|
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 {
|
.log-box {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 1px solid rgba(201, 164, 90, 0.2);
|
border: 1px solid rgba(201, 164, 90, 0.2);
|
||||||
|
|||||||
Reference in New Issue
Block a user