feat: retro mode — arcade combo round with gamepad overlays
Every fight now includes one Retro Mode round where bots submit gamepad combo inputs (↑↓←→ A B). 24 moves across 4 tiers: basic (always shown), standard (partially revealed), super (must discover), and ultra (KONAMI CODE for 50 dmg). Discovery bonus gives 1.5x damage. Includes pixel-art gamepad overlays (P1/P2) with animated button presses, retro-specific narrations, and mock bot combo responses scaled by ELO. Also adds loops/plan.md with 11-phase production hardening roadmap. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
0d6ba7d5a7
commit
1f8e8569ef
@@ -32,6 +32,8 @@ export interface RoundEvent {
|
||||
isCritical: boolean
|
||||
botAScore: number
|
||||
botBScore: number
|
||||
botAResponse?: string
|
||||
botBResponse?: string
|
||||
}
|
||||
|
||||
const ARENA_THEMES: Record<string, { bg: string; ground: string; accent: string }> = {
|
||||
@@ -97,6 +99,7 @@ const CHALLENGE_THEMED: Record<string, { themed: string[]; generic: string[] }>
|
||||
demolition: { themed: ['dynamiteBlast', 'c4Detonation', 'nukeStrike', 'grenadeBlast', 'rocketLauncher', 'volcanoErupt'], generic: ['fireworksBurst', 'partyPopper', 'pinataSmash', 'cherryBomb', 'confettiCannon', 'anvilDrop', 'pianoDrop'] },
|
||||
vehicle_mayhem: { themed: ['motorbikeCharge', 'carSmash', 'tankRoll', 'helicopterStrike', 'zamboniCrush', 'tractorPlow'], generic: ['shoppingCart', 'forkliftCharge', 'airplaneSwoop', 'rocketRide', 'unicycleRun', 'golfCartDrive', 'boatCannon'] },
|
||||
medieval_combat: { themed: ['swordSlash', 'katanaCombo', 'battleAxe', 'maceSwing', 'crossbowBolt', 'shieldBash'], generic: ['flailSwing', 'holyWater', 'dragonBreath', 'enchantedArrow', 'laserSword', 'scrollBlast'] },
|
||||
retro_mode: { themed: ['dashPunch', 'uppercut', 'rapidFlurry', 'flyingKick', 'multiHit', 'fullScreenDash', 'backflipKick', 'cycloneKick'], generic: ['bodySlam', 'grappleFlurry', 'suplex', 'katanaCombo', 'breakdanceSweep', 'corkscrewDive', 'pinballCombo'] },
|
||||
}
|
||||
|
||||
// Tier-gated ultimate move pools — cumulative (tier 3 bot can use tier 2+3 moves)
|
||||
@@ -7833,6 +7836,7 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
hallucination_check: () => announceHype('HALLUCINATION CHECK! REALITY IS ABOUT TO HIT DIFFERENT!'),
|
||||
trap_card: () => announceDramatic('TRAP CARD ACTIVATED! SOMEBODY FELL FOR IT!'),
|
||||
token_economy: () => announceSilly('TOKEN ECONOMY! SOMEBODY\'S GOING BANKRUPT!'),
|
||||
retro_mode: () => announceHype('RETRO MODE! INSERT COIN! FIGHT!'),
|
||||
}
|
||||
const voiceFn = challengeVoice[event.challengeType]
|
||||
if (voiceFn) { voiceFn(); didChallengeVoice = true }
|
||||
@@ -7881,6 +7885,98 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
// === RETRO MODE GAMEPAD OVERLAY ===
|
||||
const retroObjs: any[] = []
|
||||
if (event.challengeType === 'retro_mode') {
|
||||
const padW = 90
|
||||
const padH = 70
|
||||
const padY = 18
|
||||
const padAX = 12
|
||||
const padBX = W - padW - 12
|
||||
const btnSize = 14
|
||||
const dpadColor = '#333333'
|
||||
const btnOff = '#444444'
|
||||
const btnA = '#22cc44'
|
||||
const btnB = '#cc3333'
|
||||
const labelColor = '#00f0ff'
|
||||
|
||||
// Draw two gamepads
|
||||
for (const side of ['a', 'b'] as const) {
|
||||
const px = side === 'a' ? padAX : padBX
|
||||
// Pad background
|
||||
retroObjs.push(k.add([k.rect(padW, padH, { radius: 6 }), k.pos(px, padY), k.color(safeColor(k, '#111111')), k.opacity(0.85), k.z(48)]))
|
||||
retroObjs.push(k.add([k.rect(padW, padH, { radius: 6 }), k.pos(px, padY), k.color(safeColor(k, '#00f0ff')), k.opacity(0.15), k.z(48), k.outline(1)]))
|
||||
// D-pad
|
||||
const dx = px + 20
|
||||
const dy = padY + 28
|
||||
// Up
|
||||
retroObjs.push(k.add([k.rect(btnSize, btnSize, { radius: 2 }), k.pos(dx - btnSize / 2, dy - btnSize * 1.2), k.color(safeColor(k, dpadColor)), k.opacity(0.9), k.z(49), 'retro_' + side + '_up']))
|
||||
// Down
|
||||
retroObjs.push(k.add([k.rect(btnSize, btnSize, { radius: 2 }), k.pos(dx - btnSize / 2, dy + btnSize * 0.2), k.color(safeColor(k, dpadColor)), k.opacity(0.9), k.z(49), 'retro_' + side + '_down']))
|
||||
// Left
|
||||
retroObjs.push(k.add([k.rect(btnSize, btnSize, { radius: 2 }), k.pos(dx - btnSize * 1.6, dy - btnSize / 2), k.color(safeColor(k, dpadColor)), k.opacity(0.9), k.z(49), 'retro_' + side + '_left']))
|
||||
// Right
|
||||
retroObjs.push(k.add([k.rect(btnSize, btnSize, { radius: 2 }), k.pos(dx + btnSize * 0.6, dy - btnSize / 2), k.color(safeColor(k, dpadColor)), k.opacity(0.9), k.z(49), 'retro_' + side + '_right']))
|
||||
// A button
|
||||
retroObjs.push(k.add([k.circle(8), k.pos(px + padW - 30, dy - 6), k.color(safeColor(k, btnOff)), k.opacity(0.9), k.z(49), 'retro_' + side + '_A']))
|
||||
retroObjs.push(k.add([k.text('A', { size: 8 }), k.pos(px + padW - 33, dy - 10), k.color(safeColor(k, '#888')), k.z(50)]))
|
||||
// B button
|
||||
retroObjs.push(k.add([k.circle(8), k.pos(px + padW - 16, dy + 6), k.color(safeColor(k, btnOff)), k.opacity(0.9), k.z(49), 'retro_' + side + '_B']))
|
||||
retroObjs.push(k.add([k.text('B', { size: 8 }), k.pos(px + padW - 19, dy + 2), k.color(safeColor(k, '#888')), k.z(50)]))
|
||||
// Label
|
||||
const label = side === 'a' ? 'P1' : 'P2'
|
||||
retroObjs.push(k.add([k.text(label, { size: 10 }), k.pos(px + padW / 2 - 6, padY + 3), k.color(safeColor(k, labelColor)), k.z(50)]))
|
||||
}
|
||||
|
||||
// Animate gamepad button presses based on bot responses
|
||||
const flashBtn = async (side: 'a' | 'b', inputStr: string) => {
|
||||
if (!inputStr) return
|
||||
const arrows: Record<string, string> = { '↑': 'up', '↓': 'down', '←': 'left', '→': 'right' }
|
||||
for (const ch of inputStr) {
|
||||
const dir = arrows[ch]
|
||||
if (dir) {
|
||||
const tag = 'retro_' + side + '_' + dir
|
||||
const objs = k.get(tag)
|
||||
for (const o of objs) { o.color = safeColor(k, '#00f0ff'); }
|
||||
await k.wait(0.08)
|
||||
for (const o of objs) { o.color = safeColor(k, dpadColor); }
|
||||
}
|
||||
if (ch === 'A' || ch === 'a') {
|
||||
const objs = k.get('retro_' + side + '_A')
|
||||
for (const o of objs) { if (o.color) o.color = safeColor(k, btnA); }
|
||||
await k.wait(0.08)
|
||||
for (const o of objs) { if (o.color) o.color = safeColor(k, btnOff); }
|
||||
}
|
||||
if (ch === 'B' || ch === 'b') {
|
||||
const objs = k.get('retro_' + side + '_B')
|
||||
for (const o of objs) { if (o.color) o.color = safeColor(k, btnB); }
|
||||
await k.wait(0.08)
|
||||
for (const o of objs) { if (o.color) o.color = safeColor(k, btnOff); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse and animate each bot's combo moves
|
||||
const movesA = (event.botAResponse || '').split('|').map(s => s.trim()).filter(Boolean).slice(0, 3)
|
||||
const movesB = (event.botBResponse || '').split('|').map(s => s.trim()).filter(Boolean).slice(0, 3)
|
||||
|
||||
// Flash moves in parallel for both pads
|
||||
const animateCombo = async (side: 'a' | 'b', moves: string[]) => {
|
||||
for (const combo of moves) {
|
||||
await flashBtn(side, combo)
|
||||
// Show combo text above pad
|
||||
const px = side === 'a' ? padAX : padBX
|
||||
const comboLabel = k.add([k.text(combo, { size: 8 }), k.pos(px + 4, padY + padH + 4), k.color(safeColor(k, '#ffff00')), k.opacity(1), k.z(50)])
|
||||
retroObjs.push(comboLabel)
|
||||
await k.wait(0.25)
|
||||
comboLabel.opacity = 0
|
||||
}
|
||||
}
|
||||
// Fire-and-forget — the animations run during the exchange loop below
|
||||
animateCombo('a', movesA)
|
||||
animateCombo('b', movesB)
|
||||
}
|
||||
|
||||
for (let ex = 0; ex < exchangeCount; ex++) {
|
||||
const isLastExchange = ex === exchangeCount - 1
|
||||
// In earlier exchanges, sometimes the loser attacks back
|
||||
@@ -8051,6 +8147,9 @@ export async function createFightScene(config: FightSceneConfig) {
|
||||
speedLines.forEach(l => { if (l.exists()) l.destroy() })
|
||||
speedLines = []
|
||||
|
||||
// Clean up retro gamepad overlays
|
||||
retroObjs.forEach(o => { if (o.exists()) o.destroy() })
|
||||
|
||||
// Clean up grotesque overlays and any leftover morphs before zooming out
|
||||
destroyGrotesqueDetails()
|
||||
destroyMorphOverlays()
|
||||
|
||||
Reference in New Issue
Block a user