From 945a776a5a29c4eb9e44473a598c8c567fdf1327 Mon Sep 17 00:00:00 2001 From: Dorian Date: Mon, 9 Mar 2026 06:19:26 +0000 Subject: [PATCH] refactor: extract shared FightData interface to fight/types.ts Move FightData, FightRound, FightBot, FightArenaInfo types from local definitions in FightViewer.vue to shared fight/types.ts. Replace `any` typing in HumanFightPage, FightPage, useFightPolling, and useFightCache with proper typed interfaces. Fix null-safety guards exposed by typing. Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/FightViewer.vue | 31 +----------- frontend/src/composables/useFightCache.ts | 6 ++- frontend/src/composables/useFightPolling.ts | 12 +++-- frontend/src/game/fight/types.ts | 55 +++++++++++++++++++++ frontend/src/pages/FightPage.vue | 20 ++++---- frontend/src/pages/HumanFightPage.vue | 5 +- 6 files changed, 81 insertions(+), 48 deletions(-) diff --git a/frontend/src/components/FightViewer.vue b/frontend/src/components/FightViewer.vue index 4da191b..e736262 100644 --- a/frontend/src/components/FightViewer.vue +++ b/frontend/src/components/FightViewer.vue @@ -14,34 +14,7 @@ import { } from '../game/audio' import { isKokoroLoading, getKokoroProgress } from '../game/tts' import { isPerfMode, setPerfMode } from '../game/fight/config' - -interface Round { - roundNumber: number - challengeType: string - challengeData: string - botAResponse: string | null - botATimeMs: number | null - botAScore: number | null - botBResponse: string | null - botBTimeMs: number | null - botBScore: number | null - winnerId: string | null - narration: string | null -} - -interface FightData { - id: string - botA: { id: string; name: string; avatarSeed: string; archetype?: string; customization?: Record | null; profilePicUrl?: string | null; eloRating: number; wins: number; losses: number; tier: number; botType?: string } | null - botB: { id: string; name: string; avatarSeed: string; archetype?: string; customization?: Record | null; profilePicUrl?: string | null; eloRating: number; wins: number; losses: number; tier: number; botType?: string } | null - arenaInfo: { id: string; name: string; description: string; modifier: string | null } | null - arena: string - winnerId: string | null - botAHp: number - botBHp: number - totalRounds: number - status: string - rounds: Round[] -} +import type { FightData, FightRound } from '../game/fight/types' const props = defineProps<{ fight: FightData; autoplay?: boolean }>() const emit = defineEmits<{ 'replay-done': [] }>() @@ -297,7 +270,7 @@ async function showHitText(text: string, color: string, x: number) { hitTextVisible.value = false } -function addRoundToLog(round: Round, stagger: boolean): Promise { +function addRoundToLog(round: FightRound, stagger: boolean): Promise { if (!stagger) { const challenge = JSON.parse(round.challengeData) logItems.value.push( diff --git a/frontend/src/composables/useFightCache.ts b/frontend/src/composables/useFightCache.ts index 7d25f42..a36ce04 100644 --- a/frontend/src/composables/useFightCache.ts +++ b/frontend/src/composables/useFightCache.ts @@ -1,3 +1,5 @@ +import type { FightData } from '../game/fight/types' + const DB_NAME = 'botfights_cache' const STORE_NAME = 'fights' const MAX_CACHED = 5 @@ -17,7 +19,7 @@ function openDB(): Promise { } /** Cache a fight replay for offline viewing */ -export async function cacheFight(fightData: { id: string; [key: string]: any }): Promise { +export async function cacheFight(fightData: FightData): Promise { try { const db = await openDB() const tx = db.transaction(STORE_NAME, 'readwrite') @@ -50,7 +52,7 @@ export async function cacheFight(fightData: { id: string; [key: string]: any }): } /** Get a cached fight by ID */ -export async function getCachedFight(id: string): Promise { +export async function getCachedFight(id: string): Promise { try { const db = await openDB() return new Promise((resolve) => { diff --git a/frontend/src/composables/useFightPolling.ts b/frontend/src/composables/useFightPolling.ts index f4b7119..49934d0 100644 --- a/frontend/src/composables/useFightPolling.ts +++ b/frontend/src/composables/useFightPolling.ts @@ -1,4 +1,5 @@ import { ref, type Ref } from 'vue' +import type { FightData, FightBot, FightArenaInfo } from '../game/fight/types' export interface LiveLogItem { type: string @@ -8,11 +9,12 @@ export interface LiveLogItem { } export interface LiveFightData { - botA: any - botB: any + botA: FightBot | null + botB: FightBot | null arena: string - arenaInfo?: { name: string } - [key: string]: any + arenaInfo?: FightArenaInfo | null + mode?: string + potSats?: number } interface SSEListenerEntry { @@ -25,7 +27,7 @@ export function useFightPolling(fightId: Ref) { const isLoading = ref(true) const liveRounds = ref(0) const fightError = ref('') - const fight = ref(null) + const fight = ref(null) const liveFightData = ref(null) const spectatorCount = ref(0) const currentChallengeInfo = ref<{ type: string; label: string } | null>(null) diff --git a/frontend/src/game/fight/types.ts b/frontend/src/game/fight/types.ts index a6650dd..ec6f2f9 100644 --- a/frontend/src/game/fight/types.ts +++ b/frontend/src/game/fight/types.ts @@ -2,6 +2,8 @@ import type kaplay from 'kaplay' import type { GameObj, SpriteComp, PosComp, ScaleComp, AnchorComp, OpacityComp, ColorComp, RotateComp, ZComp } from 'kaplay' import type { SpriteCustomization } from '../sprites' +export type { SpriteCustomization } + export type Fighter = GameObj export type KaplayInstance = ReturnType @@ -85,3 +87,56 @@ export interface RoundEvent { botAResponse?: string botBResponse?: string } + +// --- Shared fight data types (used across pages & composables) --- + +export interface FightRound { + roundNumber: number + challengeType: string + challengeData: string + botAResponse: string | null + botATimeMs: number | null + botAScore: number | null + botBResponse: string | null + botBTimeMs: number | null + botBScore: number | null + winnerId: string | null + narration: string | null +} + +export interface FightBot { + id: string + name: string + avatarSeed: string + archetype?: string + customization?: SpriteCustomization | null + profilePicUrl?: string | null + eloRating: number + wins: number + losses: number + tier: number + botType?: string +} + +export interface FightArenaInfo { + id: string + name: string + description: string + modifier: string | null +} + +export interface FightData { + id: string + botAId: string + botBId: string + botA: FightBot | null + botB: FightBot | null + arenaInfo: FightArenaInfo | null + arena: string + winnerId: string | null + botAHp: number + botBHp: number + totalRounds: number + status: string + rounds: FightRound[] +} diff --git a/frontend/src/pages/FightPage.vue b/frontend/src/pages/FightPage.vue index 3c6e989..edb0591 100644 --- a/frontend/src/pages/FightPage.vue +++ b/frontend/src/pages/FightPage.vue @@ -139,8 +139,8 @@ async function initLiveScene() { try { liveScene = await createFightScene({ canvas: liveCanvas.value, - botA: { name: data.botA.name, seed: data.botA.avatarSeed || data.botA.name, tier: data.botA.tier, archetype: data.botA.archetype, customization: data.botA.customization, wins: data.botA.wins, losses: data.botA.losses }, - botB: { name: data.botB.name, seed: data.botB.avatarSeed || data.botB.name, tier: data.botB.tier, archetype: data.botB.archetype, customization: data.botB.customization, wins: data.botB.wins, losses: data.botB.losses }, + botA: { name: data.botA.name, seed: data.botA.avatarSeed || data.botA.name, tier: data.botA.tier, archetype: data.botA.archetype, customization: data.botA.customization ?? undefined, wins: data.botA.wins, losses: data.botA.losses }, + botB: { name: data.botB.name, seed: data.botB.avatarSeed || data.botB.name, tier: data.botB.tier, archetype: data.botB.archetype, customization: data.botB.customization ?? undefined, wins: data.botB.wins, losses: data.botB.losses }, arena: data.arena, }) } catch (err) { @@ -219,7 +219,7 @@ async function showLiveOverlay(text: string, color: string, duration: number) { async function handleRoundEnd(data: any) { const fd = liveFightData.value - if (!fd) { + if (!fd || !fd.botA || !fd.botB) { pendingSSEEvents.value.push({ type: 'round_end', data }) return } @@ -297,7 +297,7 @@ async function handleRoundEnd(data: any) { async function handleFightEnd(data: any) { const fd = liveFightData.value - if (!fd) { + if (!fd || !fd.botA || !fd.botB) { pendingSSEEvents.value.push({ type: 'fight_end', data }) return } @@ -746,7 +746,7 @@ function stopAutoBattle() {
{{ liveHpB }} -

{{ liveFightData.botB.name }}

+

{{ liveFightData.botB?.name }}

@@ -762,7 +762,7 @@ function stopAutoBattle() {
-

{{ liveFightData.botB.name }}

+

{{ liveFightData.botB?.name }}

{{ Math.round(liveFightData.botA.eloRating || 0) }} @@ -771,7 +771,7 @@ function stopAutoBattle() { | ⚡{{ liveFightData.potSats || 42 }} SATS | {{ spectatorCount }} watching - {{ Math.round(liveFightData.botB.eloRating || 0) }} + {{ Math.round(liveFightData.botB?.eloRating || 0) }}
@@ -900,7 +900,7 @@ function stopAutoBattle() {
{{ liveHpB }} -

{{ liveFightData.botB.name }}

+

{{ liveFightData.botB?.name }}

@@ -916,7 +916,7 @@ function stopAutoBattle() {
-

{{ liveFightData.botB.name }}

+

{{ liveFightData.botB?.name }}

{{ Math.round(liveFightData.botA.eloRating || 0) }} @@ -924,7 +924,7 @@ function stopAutoBattle() { {{ liveFightData.arenaInfo?.name }} | R{{ liveCurrentRound }} | {{ spectatorCount }} watching - {{ Math.round(liveFightData.botB.eloRating || 0) }} + {{ Math.round(liveFightData.botB?.eloRating || 0) }}
diff --git a/frontend/src/pages/HumanFightPage.vue b/frontend/src/pages/HumanFightPage.vue index 388b016..ae10518 100644 --- a/frontend/src/pages/HumanFightPage.vue +++ b/frontend/src/pages/HumanFightPage.vue @@ -3,13 +3,14 @@ import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue' import { useRoute, useRouter } from 'vue-router' import { useNostr } from '../composables/useNostr' import FightViewer from '../components/FightViewer.vue' +import type { FightData } from '../game/fight/types' const route = useRoute() const router = useRouter() const { bot: myBot, isLoggedIn } = useNostr() const fightId = ref(route.params.fightId as string) -const fight = ref(null) +const fight = ref(null) const phase = ref<'waiting' | 'challenge' | 'submitted' | 'between' | 'finished' | 'replay' | 'error'>('waiting') const error = ref('') @@ -198,7 +199,7 @@ async function loadFight() { opponentName.value = enemy?.name || 'Unknown' } if (data.rounds) { - roundResults.value = data.rounds.map((r: any) => { + roundResults.value = data.rounds.map((r: { roundNumber: number; winnerId: string | null }) => { return { round: r.roundNumber, won: r.winnerId === myBotId.value, hpA: 0, hpB: 0 } }) myHp.value = amSideA.value ? data.botAHp : data.botBHp