feat: omni-morph sprite swap, bullet time showboats, music tuning, server hardening

- Creator omni-morph now generates actual sprite sheets for morphed archetypes
- 3 new Creator showboats: bullet time attack, ₿ throne summon, disco dance
- Music: subtle tempo shift (+10 BPM max), longer phrases (8/16/24 bars),
  smoother crossfades, less chaotic hi-hat at high intensity
- Server: security headers, body size limit, production error masking,
  CORS origin warning, graceful shutdown with drain
- Payments: atomic consume (eliminates SELECT/UPDATE race), release reverts DB
- Fight loop: round events for live TUI, retro displayPrompt
- Frontend: pass pubkey in payment/queue requests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 16:23:47 +00:00
co-authored by Claude Opus 4.6
parent 0acfa4417f
commit e12fb94ae7
13 changed files with 565 additions and 514 deletions
+99 -3
View File
@@ -6734,9 +6734,24 @@ export async function createFightScene(config: FightSceneConfig) {
'garden_gnome', 'lamp_post', 'broom_man',
]
function applyCreatorOmniMorph(fighter: any, _side: 'a' | 'b'): string {
let omniMorphCounter = 0
async function applyCreatorOmniMorph(fighter: any, side: 'a' | 'b'): Promise<string> {
const pick = OMNI_MORPH_ARCHETYPES[Math.floor(Math.random() * OMNI_MORPH_ARCHETYPES.length)]
// Generate and load a sprite sheet for the picked archetype
const bot = side === 'a' ? botA : botB
const colors = side === 'a' ? colorsA : colorsB
const morphKey = `omniMorph_${side}_${omniMorphCounter++}`
try {
const morphSheet = generateSpriteSheet(bot.seed, bot.tier, colors.primary, colors.secondary, pick)
await loadSpriteWithTimeout(morphKey, morphSheet, { sliceX: MAX_FRAMES, sliceY: TOTAL_ROWS, anims: spriteAnims })
// Swap the fighter's sprite to the morphed archetype
fighter.use(k.sprite(morphKey, { anim: fighter.curAnim() || 'idle' }))
} catch (err) {
console.warn('[FightScene] omni-morph sprite failed:', err)
}
// Golden aura unique to creator morph
const aura = k.add([
k.circle(55),
@@ -8259,6 +8274,84 @@ export async function createFightScene(config: FightSceneConfig) {
spawnSparks(f.pos.x, hy - 20, 6, '#ffd700')
await k.tween(f.pos.x, hx, 0.2, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
},
// 12: Bullet time attack — slow-mo dash with afterimages + golden bullet traces
async (f, hx, hy, _sx, _sy, dir) => {
f.play('attack')
// Darken screen for bullet time
const overlay = k.add([k.rect(W, H), k.pos(0, 0), k.color(safeColor(k, '#000000')), k.opacity(0), k.z(50)])
await k.tween(0, 0.35, 0.08, (v) => { overlay.opacity = v })
// Slow-mo lean back
await k.tween(0, -30, 0.2, (v) => { f.angle = v }, k.easings.easeOutQuad)
// Golden bullet traces fly past
for (let i = 0; i < 6; i++) {
const by = hy - 15 - Math.random() * 50
const bullet = k.add([k.rect(15, 2), k.pos(dir > 0 ? W + 10 : -10, by), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(51), k.anchor('center')])
const bdir = dir > 0 ? -1 : 1
k.tween(bullet.pos.x, bullet.pos.x + bdir * (W + 30), 0.35, (v) => { bullet.pos.x = v }).then(() => { if (bullet.exists()) bullet.destroy() })
sfxZoomWhoosh()
await k.wait(0.04)
}
await k.wait(0.1)
// Snap upright and dash through
await k.tween(-30, 0, 0.06, (v) => { f.angle = v })
f.play('special'); sfxZoomWhoosh()
// Dash with afterimages
const endX = hx + dir * 160
for (let i = 0; i < 6; i++) {
const ghostX = hx + (endX - hx) * (i / 6)
const ghost = k.add([k.rect(16, 30), k.pos(ghostX, hy - 15), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0.3 - i * 0.04), k.z(49)])
k.tween(0.3, 0, 0.4, (v) => { ghost.opacity = v }).then(() => { if (ghost.exists()) ghost.destroy() })
}
await k.tween(f.pos.x, endX, 0.12, (v) => { f.pos.x = v }, k.easings.easeInQuad)
screenFlash('#ffd700', 0.06); sfxCritical(); k.shake(8)
spawnSparks(f.pos.x, hy - 20, 10, '#ffd700')
await k.wait(0.15)
// Zip back
await k.tween(overlay.opacity, 0, 0.1, (v) => { overlay.opacity = v })
overlay.destroy()
await k.tween(f.pos.x, hx, 0.1, (v) => { f.pos.x = v }, k.easings.easeOutQuad)
},
// 13: ₿ throne summon — conjure golden throne, sit imperiously, dismiss
async (f, hx, hy) => {
sfxSpecial()
const cs = SF
// Throne pieces
const seat = k.add([k.rect(40 * cs, 8 * cs), k.pos(hx, hy - 5), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0), k.z(9)])
const back = k.add([k.rect(35 * cs, 30 * cs), k.pos(hx, hy - 25 * cs), k.anchor('center'), k.color(safeColor(k, '#c8a000')), k.opacity(0), k.z(8)])
const armL = k.add([k.rect(6 * cs, 15 * cs), k.pos(hx - 20 * cs, hy - 12 * cs), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0), k.z(9)])
const armR = k.add([k.rect(6 * cs, 15 * cs), k.pos(hx + 20 * cs, hy - 12 * cs), k.anchor('center'), k.color(safeColor(k, '#ffd700')), k.opacity(0), k.z(9)])
const parts = [seat, back, armL, armR]
// Fade in
for (const p of parts) k.tween(0, 0.8, 0.2, (v) => { p.opacity = v })
spawnSparks(hx, hy - 15, 6, '#ffd700')
await k.wait(0.25)
// Sit
f.play('win')
await k.tween(f.pos.y, hy - 12, 0.12, (v) => { f.pos.y = v }, k.easings.easeInQuad)
spawnEmoteText(hx, f.pos.y - 50, 'kneel', '#ffd700')
await k.wait(0.6)
// Stand and dismiss
await k.tween(f.pos.y, hy, 0.1, (v) => { f.pos.y = v })
for (const p of parts) k.tween(p.opacity, 0, 0.2, (v) => { p.opacity = v }).then(() => { if (p.exists()) p.destroy() })
await k.wait(0.2)
},
// 14: Disco dance — Creator gets down with golden disco moves
async (f, hx, hy, _sx, _sy, dir) => {
f.play('win')
const colors = ['#ffd700', '#ff8c00', '#ffee88', '#c8a000', '#ffffff', '#ffd700']
spawnEmoteText(f.pos.x, f.pos.y - 55, '~♪ SATOSHI SHUFFLE ♪~', '#ffd700')
for (let i = 0; i < 10; i++) {
const dx = (i % 2 === 0 ? 25 : -25) + (Math.random() - 0.5) * 15
f.play(i % 3 === 0 ? 'attack' : i % 3 === 1 ? 'kick' : 'win')
await Promise.all([
k.tween(f.pos.x, hx + dx, 0.06, (v) => { f.pos.x = v }),
k.tween(f.pos.y, hy - 15, 0.03, (v) => { f.pos.y = v }).then(() =>
k.tween(f.pos.y, hy, 0.03, (v) => { f.pos.y = v })),
])
spawnSparks(f.pos.x, hy - 10, 2, colors[i % colors.length])
}
f.pos.x = hx
},
]
// Showboat loop: picks random showboats in sequence while active
@@ -9074,8 +9167,9 @@ export async function createFightScene(config: FightSceneConfig) {
const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0]
if (attacker) {
if (isCreatorFighter) {
// Omni-morph: creator transforms into a random archetype
const morphedTo = applyCreatorOmniMorph(attacker, attackerSide)
// Omni-morph: creator transforms into a random archetype (sprite swap)
const originalSpriteKey = attackerSide === 'a' ? 'botA' : 'botB'
const morphedTo = await applyCreatorOmniMorph(attacker, attackerSide)
screenFlash('#ffd700', 0.15)
sfxZoomWhoosh()
sfxSpecial()
@@ -9092,6 +9186,8 @@ export async function createFightScene(config: FightSceneConfig) {
morphRevert = async () => {
destroyMorphOverlays()
screenFlash('#c8a000', 0.1)
// Swap sprite back to original Creator
attacker.use(k.sprite(originalSpriteKey, { anim: 'idle' }))
attacker.color = safeColor(k, '#ffffff')
attacker.opacity = 1
await k.tween(0, 1, 0.2, (t) => {