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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
2f7c10e4b8
commit
945a776a5a
@@ -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<string, unknown> | 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<string, unknown> | 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<void> {
|
||||
function addRoundToLog(round: FightRound, stagger: boolean): Promise<void> {
|
||||
if (!stagger) {
|
||||
const challenge = JSON.parse(round.challengeData)
|
||||
logItems.value.push(
|
||||
|
||||
@@ -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<IDBDatabase> {
|
||||
}
|
||||
|
||||
/** Cache a fight replay for offline viewing */
|
||||
export async function cacheFight(fightData: { id: string; [key: string]: any }): Promise<void> {
|
||||
export async function cacheFight(fightData: FightData): Promise<void> {
|
||||
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<any | null> {
|
||||
export async function getCachedFight(id: string): Promise<FightData | null> {
|
||||
try {
|
||||
const db = await openDB()
|
||||
return new Promise((resolve) => {
|
||||
|
||||
@@ -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<string>) {
|
||||
const isLoading = ref(true)
|
||||
const liveRounds = ref(0)
|
||||
const fightError = ref('')
|
||||
const fight = ref<any>(null)
|
||||
const fight = ref<FightData | null>(null)
|
||||
const liveFightData = ref<LiveFightData | null>(null)
|
||||
const spectatorCount = ref(0)
|
||||
const currentChallengeInfo = ref<{ type: string; label: string } | null>(null)
|
||||
|
||||
@@ -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<SpriteComp | PosComp | ScaleComp | AnchorComp | OpacityComp | ColorComp | RotateComp | ZComp>
|
||||
|
||||
export type KaplayInstance = ReturnType<typeof kaplay>
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
<div class="h-full bg-neon-pink transition-all duration-500 ml-auto" :style="{ width: `${liveHpB}%` }" />
|
||||
</div>
|
||||
<span class="font-mono font-bold text-[10px] w-5 text-left tabular-nums" :class="liveHpB > 50 ? 'text-neon-pink' : liveHpB > 20 ? 'text-neon-yellow' : 'text-ko'">{{ liveHpB }}</span>
|
||||
<p class="font-marker text-[10px] tracking-wider truncate text-right text-neon-pink flex-1 min-w-0">{{ liveFightData.botB.name }}</p>
|
||||
<p class="font-marker text-[10px] tracking-wider truncate text-right text-neon-pink flex-1 min-w-0">{{ liveFightData.botB?.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Desktop: single-row layout -->
|
||||
@@ -762,7 +762,7 @@ function stopAutoBattle() {
|
||||
<div class="flex-1 h-5 bg-black/50 rounded-sm overflow-hidden border border-neon-pink/30">
|
||||
<div class="h-full bg-gradient-to-l from-neon-pink to-neon-purple transition-all duration-500 ml-auto" :style="{ width: `${liveHpB}%` }" />
|
||||
</div>
|
||||
<p class="font-marker text-sm tracking-wider truncate text-right text-neon-pink flex-shrink-0 max-w-[20%]">{{ liveFightData.botB.name }}</p>
|
||||
<p class="font-marker text-sm tracking-wider truncate text-right text-neon-pink flex-shrink-0 max-w-[20%]">{{ liveFightData.botB?.name }}</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mt-0.5">
|
||||
<span class="font-pixel text-[9px]" :class="tierClass(liveFightData.botA.tier || 0)">{{ Math.round(liveFightData.botA.eloRating || 0) }}</span>
|
||||
@@ -771,7 +771,7 @@ function stopAutoBattle() {
|
||||
<span v-if="liveFightData.mode === 'ranked'" class="text-neon-cyan"> | ⚡{{ liveFightData.potSats || 42 }} SATS</span>
|
||||
<span v-if="spectatorCount > 0" class="text-neon-cyan"> | {{ spectatorCount }} watching</span>
|
||||
</span>
|
||||
<span class="font-pixel text-[9px]" :class="tierClass(liveFightData.botB.tier || 0)">{{ Math.round(liveFightData.botB.eloRating || 0) }}</span>
|
||||
<span class="font-pixel text-[9px]" :class="tierClass(liveFightData.botB?.tier || 0)">{{ Math.round(liveFightData.botB?.eloRating || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -900,7 +900,7 @@ function stopAutoBattle() {
|
||||
<div class="h-full bg-neon-pink transition-all duration-500 ml-auto" :style="{ width: `${liveHpB}%` }" />
|
||||
</div>
|
||||
<span class="font-mono font-bold text-[10px] w-5 text-left tabular-nums" :class="liveHpB > 50 ? 'text-neon-pink' : liveHpB > 20 ? 'text-neon-yellow' : 'text-ko'">{{ liveHpB }}</span>
|
||||
<p class="font-marker text-[10px] tracking-wider truncate text-right text-neon-pink flex-1 min-w-0">{{ liveFightData.botB.name }}</p>
|
||||
<p class="font-marker text-[10px] tracking-wider truncate text-right text-neon-pink flex-1 min-w-0">{{ liveFightData.botB?.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Desktop: single-row layout -->
|
||||
@@ -916,7 +916,7 @@ function stopAutoBattle() {
|
||||
<div class="flex-1 h-5 bg-black/50 rounded-sm overflow-hidden border border-neon-pink/30">
|
||||
<div class="h-full bg-gradient-to-l from-neon-pink to-neon-purple transition-all duration-500 ml-auto" :style="{ width: `${liveHpB}%` }" />
|
||||
</div>
|
||||
<p class="font-marker text-sm tracking-wider truncate text-right text-neon-pink flex-shrink-0 max-w-[20%]">{{ liveFightData.botB.name }}</p>
|
||||
<p class="font-marker text-sm tracking-wider truncate text-right text-neon-pink flex-shrink-0 max-w-[20%]">{{ liveFightData.botB?.name }}</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mt-0.5">
|
||||
<span class="font-pixel text-[9px]" :class="tierClass(liveFightData.botA.tier || 0)">{{ Math.round(liveFightData.botA.eloRating || 0) }}</span>
|
||||
@@ -924,7 +924,7 @@ function stopAutoBattle() {
|
||||
{{ liveFightData.arenaInfo?.name }} | R{{ liveCurrentRound }}
|
||||
<span v-if="spectatorCount > 0" class="text-neon-cyan ml-1">| {{ spectatorCount }} watching</span>
|
||||
</span>
|
||||
<span class="font-pixel text-[9px]" :class="tierClass(liveFightData.botB.tier || 0)">{{ Math.round(liveFightData.botB.eloRating || 0) }}</span>
|
||||
<span class="font-pixel text-[9px]" :class="tierClass(liveFightData.botB?.tier || 0)">{{ Math.round(liveFightData.botB?.eloRating || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<any>(null)
|
||||
const fight = ref<FightData | null>(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
|
||||
|
||||
Reference in New Issue
Block a user