feat: mobile human vs AI — multiple choice, two-row VS bar, responsive arena
- Server-side choice generation for all 22 challenge types (factual + creative) - Mobile: 3 multiple-choice buttons replace keyboard input (4s timer) - Two-row VS bar on mobile (names row + HP bars row) in FightPage and FightViewer - Battle log hidden on mobile to maximize canvas visibility - Arena page responsive: stacking header, tighter fight cards, no name overflow - Profile page two-column desktop layout with contained sprite display - Human fighter sprites with baby growth system Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1906821452
commit
4b46aaf643
@@ -1,5 +1,5 @@
|
||||
import kaplay from 'kaplay'
|
||||
import { generateSpriteSheet, generateJudgeSpriteSheet, generateHumanSpriteSheet, getBotColors, FRAME_SIZE, MAX_FRAMES, TOTAL_ROWS, ANIMATIONS, JUDGE_ANIMATIONS, JUDGE_MAX_FRAMES, JUDGE_ROWS, HUMAN_ANIMATIONS, HUMAN_MAX_FRAMES, HUMAN_ROWS, type SpriteCustomization } from './sprites'
|
||||
import { generateSpriteSheet, generateJudgeSpriteSheet, generateHumanSpriteSheet, generateHumanFighterSpriteSheet, getBotColors, FRAME_SIZE, MAX_FRAMES, TOTAL_ROWS, ANIMATIONS, JUDGE_ANIMATIONS, JUDGE_MAX_FRAMES, JUDGE_ROWS, HUMAN_ANIMATIONS, HUMAN_MAX_FRAMES, HUMAN_ROWS, type SpriteCustomization } from './sprites'
|
||||
import {
|
||||
sfxPunch, sfxKick, sfxSpecial, sfxCritical, sfxGunshot, sfxBulletHit,
|
||||
sfxJetpack, sfxExplosion, sfxKO, sfxWin, sfxWinAnnounce, sfxPerfect,
|
||||
@@ -231,25 +231,47 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
|
||||
let sheetA: string
|
||||
let sheetB: string
|
||||
|
||||
// Human fighters use their profile human character, not a bot sprite
|
||||
const winRateA = (botA.wins || 0) / Math.max(1, (botA.wins || 0) + (botA.losses || 0))
|
||||
const winRateB = (botB.wins || 0) / Math.max(1, (botB.wins || 0) + (botB.losses || 0))
|
||||
|
||||
try {
|
||||
sheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary, botA.archetype, botA.customization)
|
||||
sheetA = botA.archetype === 'human'
|
||||
? generateHumanFighterSpriteSheet(botA.seed, botA.archetype, colorsA.primary, colorsA.secondary, winRateA)
|
||||
: generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary, botA.archetype, botA.customization)
|
||||
} catch (err) {
|
||||
console.error('[FightScene] Failed to generate sprite for botA:', err)
|
||||
sheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary)
|
||||
}
|
||||
try {
|
||||
sheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary, botB.archetype, botB.customization)
|
||||
sheetB = botB.archetype === 'human'
|
||||
? generateHumanFighterSpriteSheet(botB.seed, botB.archetype, colorsB.primary, colorsB.secondary, winRateB)
|
||||
: generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary, botB.archetype, botB.customization)
|
||||
} catch (err) {
|
||||
console.error('[FightScene] Failed to generate sprite for botB:', err)
|
||||
sheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary)
|
||||
}
|
||||
|
||||
const isHumanA = botA.archetype === 'human'
|
||||
const isHumanB = botB.archetype === 'human'
|
||||
|
||||
// Await sprite loading to prevent race condition where scene starts before sprites decode
|
||||
await Promise.all([
|
||||
k.loadSprite('botA', sheetA, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims }),
|
||||
k.loadSprite('botB', sheetB, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims }),
|
||||
])
|
||||
|
||||
// Preload bot sprites for human fighters (used when humans morph into bots)
|
||||
if (isHumanA) {
|
||||
const botSheetA = generateSpriteSheet(botA.seed, botA.tier, colorsA.primary, colorsA.secondary)
|
||||
k.loadSprite('botA_morph', botSheetA, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims })
|
||||
}
|
||||
if (isHumanB) {
|
||||
const botSheetB = generateSpriteSheet(botB.seed, botB.tier, colorsB.primary, colorsB.secondary)
|
||||
k.loadSprite('botB_morph', botSheetB, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims })
|
||||
}
|
||||
|
||||
// Judge sprite (red lobster referee)
|
||||
const judgeSheet = generateJudgeSpriteSheet()
|
||||
const judgeAnims = {
|
||||
@@ -260,9 +282,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
}
|
||||
await k.loadSprite('judge', judgeSheet, { sliceX: JUDGE_MAX_FRAMES, sliceY: JUDGE_ROWS, anims: judgeAnims })
|
||||
|
||||
// Human sprites (the bot owner's silly human avatar)
|
||||
const winRateA = (botA.wins || 0) / Math.max(1, (botA.wins || 0) + (botA.losses || 0))
|
||||
const winRateB = (botB.wins || 0) / Math.max(1, (botB.wins || 0) + (botB.losses || 0))
|
||||
// Human sprites (the bot owner's silly human avatar for crowd)
|
||||
const humanSheetA = generateHumanSpriteSheet(botA.seed, botA.archetype || 'standard', colorsA.primary, colorsA.secondary, winRateA)
|
||||
const humanSheetB = generateHumanSpriteSheet(botB.seed, botB.archetype || 'standard', colorsB.primary, colorsB.secondary, winRateB)
|
||||
const humanAnims: Record<string, { from: number; to: number; loop: boolean; speed: number }> = {}
|
||||
@@ -5941,6 +5961,9 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
async function playMorph(fighter: any, side: 'a' | 'b', morphIndex: number) {
|
||||
const savedScaleX = fighter.scale.x
|
||||
const savedScaleY = fighter.scale.y
|
||||
const isHumanFighter = side === 'a' ? isHumanA : isHumanB
|
||||
const morphSpriteKey = side === 'a' ? 'botA_morph' : 'botB_morph'
|
||||
const normalSpriteKey = side === 'a' ? 'botA' : 'botB'
|
||||
|
||||
// Flash + announcement
|
||||
screenFlash('#ffffff', 0.15)
|
||||
@@ -5956,6 +5979,11 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
fighter.scale.y = savedScaleY * (1 + t * (growFactor - 1))
|
||||
}, k.easings.easeOutBack)
|
||||
|
||||
// Human fighters morph into bots (opposite direction!)
|
||||
if (isHumanFighter) {
|
||||
fighter.use(k.sprite(morphSpriteKey, { anim: 'idle' }))
|
||||
}
|
||||
|
||||
// Apply the morph visuals
|
||||
morphAppliers[morphIndex](fighter, side)
|
||||
|
||||
@@ -5970,6 +5998,10 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
screenFlash(theme.accent, 0.1)
|
||||
fighter.color = safeColor(k,'#ffffff')
|
||||
fighter.opacity = 1
|
||||
// Revert human fighters back to their human sprite
|
||||
if (isHumanFighter) {
|
||||
fighter.use(k.sprite(normalSpriteKey, { anim: 'idle' }))
|
||||
}
|
||||
await k.tween(0, 1, 0.2, (t) => {
|
||||
fighter.scale.x = savedScaleX * growFactor + t * (savedScaleX - savedScaleX * growFactor)
|
||||
fighter.scale.y = savedScaleY * growFactor + t * (savedScaleY - savedScaleY * growFactor)
|
||||
|
||||
Reference in New Issue
Block a user