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:
co-authored by
Claude Opus 4.6
parent
0acfa4417f
commit
e12fb94ae7
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user