feat: THE CREATOR god-tier character, bitcoin choreographies, omni-morph, cameos

- Guy Fawkes mask archetype with golden outline, bitcoin chest symbol, laptop, tier-gated effects
- 12 custom bitcoin-themed choreographies (8 regular + 4 exclusive ultimates)
- Omni-morph system: creator morphs into any of 80+ archetypes instead of cycling 3
- 6% per-round cameo in non-creator fights — drops golden ₿ gifts to fighters
- Custom golden code rain entrance with persistent orbiting particles
- pickChoreography: 50% ultimate chance (100% on crits), all tier ultimates + 4 exclusive
- Server auto-assigns the_creator archetype on login/register for creator pubkey
- FightViewer, payments, queue, orchestrator improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 12:08:18 +00:00
co-authored by Claude Opus 4.6
parent 8a3480e5ab
commit 6d390f69b2
14 changed files with 1513 additions and 104 deletions
+774 -51
View File
@@ -107,7 +107,29 @@ const TIER_ULTIMATES: Record<number, string[]> = {
5: ['ultimateArmageddon', 'ultimateOmegaBeam', 'ultimateBeyondTheScreen', 'ultimateMatrixDodge', 'ultimateWorldEnder', 'ultimateAbsoluteZero', 'ultimateSolarCannon', 'ultimatePhantomStrike', 'ultimateGodPunch', 'ultimateVoidCollapse'],
}
function pickChoreography(challengeType: string, isCritical: boolean, _round: number, attackerTier: number = 0): string {
// The Creator — exclusive moves (50% ultimate chance, 100% on crits, custom pool)
const CREATOR_MOVES = ['bitcoinRain', 'lightningInvoice', 'satoshiStrike', 'hashRateOverload', 'blockchainSlam', 'creatorMode', 'morphStrike', 'divineIntervention']
const CREATOR_ULTIMATES = ['ultimateGenesisBlock', 'ultimateTheHalving', 'ultimateFullNodeCrush', 'ultimateSatoshiVision']
function pickChoreography(challengeType: string, isCritical: boolean, _round: number, attackerTier: number = 0, attackerArchetype?: string): string {
// THE CREATOR: special move selection — 50% ultimate, 60% creator moves, rest normal
if (attackerArchetype === 'the_creator') {
const ultimateChance = isCritical ? 1.0 : 0.5
if (Math.random() < ultimateChance) {
// Creator gets ALL ultimates + their 4 exclusive ones
const all: string[] = [...CREATOR_ULTIMATES]
for (let t = 2; t <= 5; t++) {
if (TIER_ULTIMATES[t]) all.push(...TIER_ULTIMATES[t])
}
return all[Math.floor(Math.random() * all.length)]
}
// 60% creator-themed, 40% normal selection
if (Math.random() < 0.6) {
return CREATOR_MOVES[Math.floor(Math.random() * CREATOR_MOVES.length)]
}
// Fall through to normal selection
}
// Tier-gated ultimates: higher tier = higher chance of spectacular ultimate moves
// Tier 2: 15% chance, Tier 3: 20%, Tier 4: 30%, Tier 5: 40%
if (attackerTier >= 2) {
@@ -636,44 +658,9 @@ export async function createFightScene(config: FightSceneConfig) {
}))
}
// Grotesque close-up: spawn exaggerated impact details during zoom-ins
let grotesqueObjs: any[] = []
function spawnGrotesqueDetails(fighter: any, scaleFactor: number) {
destroyGrotesqueDetails()
if (!fighter?.exists()) return
const fx = fighter.pos.x, fy = fighter.pos.y
const s = Math.max(1, scaleFactor)
// Impact lines radiating from fighter
for (let i = 0; i < 6; i++) {
const angle = (i / 6) * Math.PI * 2 + Math.random() * 0.3
const len = 30 + Math.random() * 40
const line = k.add([
k.rect(3 * s, len * s),
k.pos(fx + Math.cos(angle) * 20, fy - 30 + Math.sin(angle) * 20),
k.color(safeColor(k, i % 2 === 0 ? '#ffffff' : '#ff2d2d')),
k.opacity(0.7),
k.z(25),
k.rotate(angle * (180 / Math.PI) + 90),
k.anchor('center'),
])
grotesqueObjs.push(line)
}
// Speed/impact dots
for (let i = 0; i < 4; i++) {
const dot = k.add([
k.circle(4 + Math.random() * 6),
k.pos(fx + (Math.random() - 0.5) * 60, fy - 30 + (Math.random() - 0.5) * 50),
k.color(safeColor(k, '#ffe14d')),
k.opacity(0.8),
k.z(25),
])
grotesqueObjs.push(dot)
}
}
function destroyGrotesqueDetails() {
for (const o of grotesqueObjs) { if (o.exists()) o.destroy() }
grotesqueObjs = []
}
// Grotesque close-up overlays removed — noops retained for call-site compatibility
function spawnGrotesqueDetails(_fighter: any, _scaleFactor: number) {}
function destroyGrotesqueDetails() {}
// Arena-specific background decoration
function drawArenaDecor() {
const a = arena
@@ -5718,6 +5705,453 @@ export async function createFightScene(config: FightSceneConfig) {
// Tier-gated pools (mapping is at module level for pickChoreography access)
// ═══════════════════════════════════════════════════════
// THE CREATOR — Exclusive choreographies (bitcoin-themed)
// ═══════════════════════════════════════════════════════
async function bitcoinRain(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special')
sfxSpecial()
await k.wait(0.2)
// Rain golden ₿ coins from above
const coinCount = isCritical ? 30 : 15
for (let i = 0; i < coinCount; i++) {
const cx = def.pos.x + (Math.random() - 0.5) * 120
const startY = def.pos.y - 200 - Math.random() * 100
const coin = k.add([
k.circle(isCritical ? 5 : 3),
k.pos(cx, startY),
k.color(safeColor(k, Math.random() > 0.3 ? '#ffd700' : '#ffee88')),
k.opacity(0.9),
k.z(18),
])
coin.onUpdate(() => {
coin.pos.y += 600 * k.dt()
if (coin.pos.y > def.pos.y + 10) {
coin.destroy()
}
})
if (i % 4 === 0) sfxCoin()
await k.wait(0.03)
}
await k.wait(0.15)
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 18 : 8)
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 20 : 10, '#ffd700')
if (isCritical) { screenFlash('#ffd700', 0.12); sfxExplosion() }
const push = dir * (isCritical ? 80 : 35)
k.tween(def.pos.x, origDX + push, 0.25, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.4)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function lightningInvoice(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special')
sfxZap()
await k.wait(0.15)
// Jagged lightning bolt from top of screen to defender
const segments = 8
const topY = -50
const botY = def.pos.y - 20
for (let i = 0; i < segments; i++) {
const t = i / segments
const bx = def.pos.x + (Math.random() - 0.5) * 40
const by2 = topY + t * (botY - topY)
const bolt = k.add([
k.rect(3, Math.abs(botY - topY) / segments + 5),
k.pos(bx, by2),
k.color(safeColor(k, i % 2 === 0 ? '#ffd700' : '#ffffff')),
k.opacity(0.95),
k.z(20),
])
setTimeout(() => { if (bolt.exists()) bolt.destroy() }, 300)
}
// ₿ symbol flash at impact
const btcFlash = k.add([
k.circle(isCritical ? 20 : 12),
k.pos(def.pos.x, def.pos.y - 30),
k.color(safeColor(k, '#ffd700')),
k.opacity(0.8),
k.z(22),
])
k.tween(btcFlash.opacity, 0, 0.4, (v: number) => { btcFlash.opacity = v })
.then(() => { if (btcFlash.exists()) btcFlash.destroy() })
await k.wait(0.1)
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 20 : 10)
sfxExplosion()
spawnSparks(def.pos.x, def.pos.y - 30, 15, '#ffd700')
if (isCritical) { screenFlash('#ffd700'); glitchRGB() }
const push = dir * (isCritical ? 90 : 40)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.4)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function satoshiStrike(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special')
sfxSpecial()
await k.wait(0.1)
// Rapid-fire gold particle stream
const shotCount = isCritical ? 25 : 15
for (let i = 0; i < shotCount; i++) {
const p = k.add([
k.circle(2),
k.pos(atk.pos.x + dir * 30, atk.pos.y - 40 + (Math.random() - 0.5) * 15),
k.color(safeColor(k, i % 3 === 0 ? '#ffd700' : i % 3 === 1 ? '#ffee88' : '#ff8c00')),
k.opacity(0.9),
k.z(16),
])
const speed = 800 + Math.random() * 400
p.onUpdate(() => {
p.pos.x += dir * speed * k.dt()
p.opacity -= 1.5 * k.dt()
if (p.opacity <= 0 || Math.abs(p.pos.x - atk.pos.x) > 500) p.destroy()
})
if (i % 3 === 0) sfxCoin()
if (i % 5 === 0) {
def.play('hit')
k.shake(2)
spawnSparks(def.pos.x, def.pos.y - 25 - Math.random() * 20, 3, '#ffd700')
}
await k.wait(0.025)
}
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 12 : 5)
if (isCritical) { screenFlash('#ffd700', 0.08); sfxCritical() }
const push = dir * (isCritical ? 70 : 30)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function hashRateOverload(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special')
sfxSpecial()
// Screen fills with scrolling hash text (simulated with falling green/gold rects)
const hashCount = isCritical ? 40 : 20
for (let i = 0; i < hashCount; i++) {
const hx = Math.random() * k.width()
const hy = Math.random() * k.height()
const h = k.add([
k.rect(8 + Math.random() * 12, 2),
k.pos(hx, hy),
k.color(safeColor(k, Math.random() > 0.5 ? '#00ff41' : '#ffd700')),
k.opacity(0.6),
k.z(25),
])
h.onUpdate(() => {
h.pos.y += 200 * k.dt()
h.opacity -= 1.5 * k.dt()
if (h.opacity <= 0) h.destroy()
})
}
await k.wait(0.5)
// Explosion
screenFlash(isCritical ? '#ffd700' : '#00ff41', 0.15)
sfxExplosion()
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 25 : 12)
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 25 : 12, '#ffd700')
if (isCritical) glitchRGB()
const push = dir * (isCritical ? 100 : 50)
k.tween(def.pos.x, origDX + push, 0.25, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.4)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function blockchainSlam(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) {
const contactX = origDX - dir * 60
sfxZoomWhoosh()
await k.tween(atk.pos.x, contactX, 0.12, (v: number) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('attack')
// Chain of blocks swinging as weapon
const blocks: any[] = []
const blockCount = isCritical ? 6 : 4
for (let i = 0; i < blockCount; i++) {
const bk = k.add([
k.rect(10, 10),
k.pos(atk.pos.x + dir * (i * 14), atk.pos.y - 40),
k.color(safeColor(k, i % 2 === 0 ? '#ffd700' : '#c8a000')),
k.opacity(0.9),
k.z(17),
])
blocks.push(bk)
}
// Swing chain toward defender
for (let i = 0; i < blockCount; i++) {
await k.tween(blocks[i].pos.x, def.pos.x + (i - blockCount / 2) * 5, 0.08, (v: number) => { blocks[i].pos.x = v }, k.easings.easeOutQuad)
await k.tween(blocks[i].pos.y, def.pos.y - 30, 0.04, (v: number) => { blocks[i].pos.y = v })
}
sfxPunch()
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 15 : 7)
spawnSparks(def.pos.x, def.pos.y - 30, 10, '#ffd700')
if (isCritical) { screenFlash('#ffd700', 0.1); sfxCritical() }
blocks.forEach(b => b.destroy())
const push = dir * (isCritical ? 80 : 35)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.35)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v: number) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
atk.play('idle')
}
async function creatorMode(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) {
atk.play('special')
sfxSpecial()
// Typing effect — rapid small flashes from attacker
for (let i = 0; i < 8; i++) {
const sp = k.add([
k.circle(2),
k.pos(atk.pos.x + dir * 20, atk.pos.y - 45 + (Math.random() - 0.5) * 10),
k.color(safeColor(k, i % 2 === 0 ? '#00ff41' : '#ffd700')),
k.opacity(0.8),
k.z(16),
])
setTimeout(() => { if (sp.exists()) sp.destroy() }, 200)
await k.wait(0.05)
}
// Reality warp — defender's sprite distorts
if (def.exists()) {
const origScaleX = def.scale?.x ?? 1
const origScaleY = def.scale?.y ?? 1
for (let w = 0; w < 4; w++) {
def.scaleTo(origScaleX * (1 + (Math.random() - 0.5) * 0.5), origScaleY * (1 + (Math.random() - 0.5) * 0.5))
await k.wait(0.06)
}
def.scaleTo(origScaleX, origScaleY)
}
sfxExplosion()
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 18 : 8)
spawnSparks(def.pos.x, def.pos.y - 30, 12, '#00ff41')
if (isCritical) { screenFlash('#00ff41'); glitchRGB() }
const push = dir * (isCritical ? 70 : 30)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.4)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function morphStrike(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Flash golden — the morph is handled by the morph system
// This move itself is a quick dash-hit with gold sparks
screenFlash('#ffd700', 0.08)
sfxZoomWhoosh()
const contactX = origDX - dir * 45
await k.tween(atk.pos.x, contactX, 0.1, (v: number) => { atk.pos.x = v }, k.easings.easeOutQuad)
atk.play('attack')
sfxPunch()
await k.wait(0.08)
def.play(isCritical ? 'knockback' : 'hit')
k.shake(isCritical ? 14 : 6)
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 15 : 8, '#ffd700')
if (isCritical) { sfxCritical(); screenFlash('#ffd700') }
const push = dir * (isCritical ? 80 : 35)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.3)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.2, (v: number) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(def.pos.x, origDX, 0.25, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
atk.play('idle')
}
async function divineIntervention(atk: any, def: any, dir: number, origAX: number, origDX: number, isCritical: boolean) {
// Ascend off screen
sfxZoomWhoosh()
atk.play('win')
await k.tween(atk.pos.y, atk.pos.y - 300, 0.25, (v: number) => { atk.pos.y = v }, k.easings.easeInQuad)
atk.opacity = 0
// Golden glow expanding where they were
const glowX = origAX
const glow = k.add([
k.circle(5),
k.pos(glowX, GROUND_Y - 40),
k.color(safeColor(k, '#ffd700')),
k.opacity(0.6),
k.z(19),
])
await k.tween(5, 60, 0.3, (v: number) => { glow.radius = v; glow.opacity = 0.6 - v / 150 })
glow.destroy()
// Slam down onto defender from above
atk.opacity = 1
atk.pos.x = def.pos.x
atk.pos.y = -100
atk.play('kick')
sfxZoomWhoosh()
await k.tween(atk.pos.y, GROUND_Y, 0.15, (v: number) => { atk.pos.y = v }, k.easings.easeInQuad)
// Massive impact
sfxExplosion()
k.shake(isCritical ? 30 : 18)
screenFlash('#ffd700', 0.15)
spawnSparks(def.pos.x, def.pos.y - 30, isCritical ? 25 : 15, '#ffd700')
spawnShockwave(def.pos.x, GROUND_Y, '#ffd700')
if (isCritical) glitchRGB()
def.play('knockback')
const push = dir * (isCritical ? 120 : 60)
k.tween(def.pos.x, origDX + push, 0.2, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.5)
await Promise.all([
k.tween(atk.pos.x, origAX, 0.25, (v: number) => { atk.pos.x = v }, k.easings.easeInQuad),
k.tween(atk.pos.y, GROUND_Y, 0.01, (v: number) => { atk.pos.y = v }),
k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad),
])
atk.play('idle')
}
// ── Creator Ultimates ──
async function ultimateGenesisBlock(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Screen goes dark, massive genesis block materializes and detonates
atk.play('special')
sfxSpecial()
const darkness = k.add([k.rect(k.width(), k.height()), k.pos(0, 0), k.color(safeColor(k, '#000000')), k.opacity(0), k.z(30)])
await k.tween(darkness.opacity, 0.7, 0.3, (v: number) => { darkness.opacity = v })
// Genesis block appears at center
const block = k.add([
k.rect(60, 60),
k.pos(k.width() / 2 - 30, k.height() / 2 - 30),
k.color(safeColor(k, '#ffd700')),
k.opacity(0),
k.z(35),
])
await k.tween(block.opacity, 1, 0.3, (v: number) => { block.opacity = v })
await k.wait(0.3)
// Detonate!
sfxExplosion()
block.destroy()
darkness.destroy()
screenFlash('#ffd700', 0.2)
k.shake(35)
glitchRGB()
spawnShockwave(k.width() / 2, k.height() / 2, '#ffd700')
spawnSparks(def.pos.x, def.pos.y - 30, 30, '#ffd700')
def.play('knockback')
const push = dir * 120
k.tween(def.pos.x, origDX + push, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.6)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function ultimateTheHalving(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Screen splits horizontally with a golden line
atk.play('special')
sfxZap()
const line = k.add([
k.rect(k.width(), 3),
k.pos(0, k.height() / 2),
k.color(safeColor(k, '#ffd700')),
k.opacity(0),
k.z(32),
])
await k.tween(line.opacity, 1, 0.15, (v: number) => { line.opacity = v })
screenFlash('#ffd700', 0.1)
glitchRGB()
sfxExplosion()
k.shake(25)
// Briefly scale defender to half
if (def.exists()) {
const origSY = def.scale?.y ?? 1
def.scaleTo(def.scale?.x ?? 1, origSY * 0.5)
await k.wait(0.2)
def.scaleTo(def.scale?.x ?? 1, origSY)
}
line.destroy()
def.play('knockback')
spawnSparks(def.pos.x, def.pos.y - 30, 25, '#ffd700')
const push = dir * 100
k.tween(def.pos.x, origDX + push, 0.25, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.5)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function ultimateFullNodeCrush(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Giant server rack drops from above
atk.play('special')
sfxSpecial()
await k.wait(0.3)
const rack = k.add([
k.rect(40, 80),
k.pos(def.pos.x - 20, -100),
k.color(safeColor(k, '#333333')),
k.opacity(0.9),
k.z(28),
])
// LEDs on the rack
for (let led = 0; led < 6; led++) {
k.add([
k.circle(1),
k.pos(rack.pos.x + 5 + led * 5, rack.pos.y + 10),
k.color(safeColor(k, led % 2 === 0 ? '#00ff41' : '#ff0000')),
k.opacity(0.8),
k.z(29),
])
}
sfxZoomWhoosh()
await k.tween(rack.pos.y, def.pos.y - 80, 0.2, (v: number) => { rack.pos.y = v }, k.easings.easeInQuad)
// Impact
sfxExplosion()
k.shake(30)
screenFlash('#ffffff', 0.15)
spawnSparks(def.pos.x, def.pos.y - 30, 20, '#ffd700')
spawnShockwave(def.pos.x, GROUND_Y, '#333333')
rack.destroy()
def.play('knockback')
const push = dir * 100
k.tween(def.pos.x, origDX + push, 0.25, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.6)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
async function ultimateSatoshiVision(atk: any, def: any, dir: number, origAX: number, origDX: number, _isCrit: boolean) {
// Everything turns gold — defender trapped in bitcoin block
atk.play('special')
sfxSpecial()
// Gold overlay
const overlay = k.add([k.rect(k.width(), k.height()), k.pos(0, 0), k.color(safeColor(k, '#ffd700')), k.opacity(0), k.z(30)])
await k.tween(overlay.opacity, 0.4, 0.3, (v: number) => { overlay.opacity = v })
// Wireframe box around defender
const bx = def.pos.x - 30
const by2 = def.pos.y - 70
const bw = 60
const bh2 = 80
const top = k.add([k.rect(bw, 2), k.pos(bx, by2), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(33)])
const bot = k.add([k.rect(bw, 2), k.pos(bx, by2 + bh2), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(33)])
const left = k.add([k.rect(2, bh2), k.pos(bx, by2), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(33)])
const right = k.add([k.rect(2, bh2), k.pos(bx + bw, by2), k.color(safeColor(k, '#ffd700')), k.opacity(0.9), k.z(33)])
await k.wait(0.3)
// Crush — shrink box
sfxZoomWhoosh()
await Promise.all([
k.tween(bx, def.pos.x - 5, 0.3, (v: number) => { left.pos.x = v; top.pos.x = v; bot.pos.x = v }),
k.tween(bx + bw, def.pos.x + 5, 0.3, (v: number) => { right.pos.x = v }),
])
sfxExplosion()
k.shake(30)
screenFlash('#ffffff', 0.15)
glitchRGB();
[top, bot, left, right].forEach(b => b.destroy())
overlay.destroy()
spawnSparks(def.pos.x, def.pos.y - 30, 30, '#ffd700')
def.play('knockback')
const push = dir * 110
k.tween(def.pos.x, origDX + push, 0.25, (v: number) => { def.pos.x = v }, k.easings.easeOutQuad)
await k.wait(0.6)
atk.play('idle')
await k.tween(def.pos.x, origDX, 0.3, (v: number) => { def.pos.x = v }, k.easings.easeInOutQuad)
}
const choreographyMap: Record<string, typeof dashPunch> = {
dashPunch, aerialSlam, flyingKick, dashThrough, uppercut,
multiHit, projectile, jetpackDive, gunBurst, groundPound,
@@ -5821,6 +6255,11 @@ export async function createFightScene(config: FightSceneConfig) {
// Tier 5+
ultimateArmageddon, ultimateOmegaBeam, ultimateBeyondTheScreen, ultimateMatrixDodge, ultimateWorldEnder,
ultimateAbsoluteZero, ultimateSolarCannon, ultimatePhantomStrike, ultimateGodPunch, ultimateVoidCollapse,
// --- THE CREATOR: Bitcoin-themed moves ---
bitcoinRain, lightningInvoice, satoshiStrike, hashRateOverload, blockchainSlam, creatorMode,
morphStrike, divineIntervention,
// --- THE CREATOR: Exclusive ultimates ---
ultimateGenesisBlock, ultimateTheHalving, ultimateFullNodeCrush, ultimateSatoshiVision,
}
// === MORPH / COSTUME SYSTEM ===
@@ -6042,6 +6481,86 @@ export async function createFightScene(config: FightSceneConfig) {
const morphAppliers = [applyElementalMorph, applyMechMorph, applyBeastMorph]
const morphNames = ['ELEMENTAL FORM', 'MECH ARMOR', 'BEAST MODE']
// === THE CREATOR: OMNI-MORPH ===
// Instead of 3 morph types, the creator morphs into a random archetype each time
const OMNI_MORPH_ARCHETYPES = [
'lobster', 'sheep', 'cyborg', 'blob', 'tank', 'dog', 'cat', 'cactus', 'pizza',
'mushroom', 'shark', 'penguin', 'octopus', 'skeleton', 'ghost', 'alien', 'dinosaur',
'pirate', 'ninja', 'cowboy', 'wizard', 'bee', 'frog', 'snail',
'robot', 'android', 'toaster', 'mech', 'minotaur', 'unicorn', 'phoenix', 'dragon',
'mermaid', 'griffin', 'cyclops', 'gargoyle', 'golem', 'vampire', 'werewolf', 'zombie',
'witch', 'demon', 'chef', 'firefighter', 'astronaut', 'clown', 'detective',
'lumberjack', 'scientist', 'wrestler', 'boxer', 'gladiator', 'samurai', 'viking', 'knight',
'elephant', 'giraffe', 'hippo', 'lion', 'monkey', 'parrot', 'raccoon', 'snake',
'turtle', 'whale', 'crocodile', 'flamingo', 'hedgehog', 'panda', 'hamster',
'sock_puppet', 'traffic_cone', 'toilet_man', 'potato', 'cloud_man', 'rock_man',
'balloon_man', 'trash_can', 'rubber_duck', 'snowman', 'scarecrow', 'jack_o_lantern',
'garden_gnome', 'lamp_post', 'broom_man',
]
function applyCreatorOmniMorph(fighter: any, _side: 'a' | 'b'): string {
const pick = OMNI_MORPH_ARCHETYPES[Math.floor(Math.random() * OMNI_MORPH_ARCHETYPES.length)]
// Golden aura unique to creator morph
const aura = k.add([
k.circle(55),
k.pos(fighter.pos.x, fighter.pos.y - 20),
k.color(safeColor(k, '#c8a000')),
k.opacity(0.2),
k.z(fighter.z - 1),
k.anchor('center'),
k.scale(1),
])
aura.onUpdate(() => {
aura.pos.x = fighter.pos.x
aura.pos.y = fighter.pos.y - 20
aura.opacity = 0.12 + Math.sin(k.time() * 6) * 0.1
const s = 1 + Math.sin(k.time() * 4) * 0.2
aura.scale.x = s; aura.scale.y = s
})
activeMorphs.push({ obj: aura, type: 'omni' })
// Orbiting golden ₿ symbols (4 orbiting)
for (let i = 0; i < 4; i++) {
const btc = k.add([
k.text('₿', { size: 10 }),
k.pos(fighter.pos.x, fighter.pos.y - 20),
k.color(safeColor(k, '#ffd700')),
k.opacity(0.7),
k.z(fighter.z + 2),
k.anchor('center'),
])
const baseAngle = (i / 4) * Math.PI * 2
const radius = 40
btc.onUpdate(() => {
const a = baseAngle + k.time() * 3
btc.pos.x = fighter.pos.x + Math.cos(a) * radius
btc.pos.y = fighter.pos.y - 20 + Math.sin(a) * radius * 0.5
btc.opacity = 0.7 + Math.sin(k.time() * 8 + i) * 0.3
})
activeMorphs.push({ obj: btc, type: 'omni' })
}
// Apply archetype-specific color tint based on pick category
const tints: Record<string, string> = {
fire: '#ff4400', ice: '#44ccff', nature: '#44cc44', dark: '#8844cc',
metal: '#aabbcc', electric: '#ffff00', beast: '#cc6622', cosmic: '#cc44ff',
}
const category =
['phoenix', 'dragon', 'demon'].includes(pick) ? 'fire' :
['penguin', 'snowman', 'yeti'].includes(pick) ? 'ice' :
['cactus', 'mushroom', 'frog', 'snail', 'turtle'].includes(pick) ? 'nature' :
['skeleton', 'ghost', 'vampire', 'zombie', 'witch'].includes(pick) ? 'dark' :
['robot', 'android', 'mech', 'toaster', 'knight'].includes(pick) ? 'metal' :
['alien', 'unicorn', 'mermaid'].includes(pick) ? 'cosmic' :
['werewolf', 'minotaur', 'lion', 'shark', 'crocodile'].includes(pick) ? 'beast' :
'electric'
fighter.color = safeColor(k, tints[category] || '#ffd700')
fighter.opacity = 0.9
return pick
}
// Get morph order for a bot (deterministic by seed)
function getMorphOrder(seed: string): number[] {
let h = 0
@@ -6332,6 +6851,49 @@ export async function createFightScene(config: FightSceneConfig) {
return lines
}
// Talking mouth animation — a small rectangle that opens/closes near the fighter's face
const talkingAnims: Record<string, { objs: any[]; timer: ReturnType<typeof setInterval> }> = {}
function startTalking(side: 'a' | 'b') {
stopTalking(side) // cleanup any existing
const fighter = k.get(side === 'a' ? 'fighterA' : 'fighterB')[0]
if (!fighter?.exists()) return
const mouthColor = side === 'a' ? '#00f0ff' : '#ff2d7b'
// Mouth position: slightly below center of the sprite
const mouthOffsetY = -28
const mouthW = 8
let open = false
const mouth = k.add([
k.rect(mouthW, 2),
k.pos(fighter.pos.x, fighter.pos.y + mouthOffsetY),
k.color(safeColor(k, mouthColor)),
k.opacity(0.9),
k.z(12),
k.anchor('center'),
])
// Toggle open/close every 120ms for a fast chatter effect
const timer = setInterval(() => {
if (!mouth.exists() || !fighter.exists()) { stopTalking(side); return }
open = !open
mouth.pos.x = fighter.pos.x
mouth.pos.y = fighter.pos.y + mouthOffsetY
mouth.height = open ? 5 : 2
}, 120)
talkingAnims[side] = { objs: [mouth], timer }
}
function stopTalking(side: 'a' | 'b') {
const anim = talkingAnims[side]
if (!anim) return
clearInterval(anim.timer)
anim.objs.forEach(o => { if (o.exists()) o.destroy() })
delete talkingAnims[side]
}
// Spawn floating text above a position
function spawnEmoteText(x: number, y: number, text: string, color: string, duration: number = 1.2) {
const label = k.add([
@@ -6706,6 +7268,9 @@ export async function createFightScene(config: FightSceneConfig) {
showSpeechBubble(side, text, duration)
},
startTalking(side: 'a' | 'b') { startTalking(side) },
stopTalking(side: 'a' | 'b') { stopTalking(side) },
async playEntrance() {
const fA = k.get('fighterA')[0]
const fB = k.get('fighterB')[0]
@@ -7119,16 +7684,77 @@ export async function createFightScene(config: FightSceneConfig) {
},
]
// THE CREATOR: Golden code rain portal entrance
const creatorEntrance = async (fighter: any, homeX: number, _fromLeft: boolean) => {
fighter.pos.x = homeX
fighter.pos.y = -100
fighter.opacity = 0
// Golden code rain columns
const codeChars = '₿01∞⚡⛏§∂∆≈≠'.split('')
const rainDrops: any[] = []
for (let col = 0; col < 12; col++) {
const cx = homeX - 60 + col * 10
for (let row = 0; row < 4; row++) {
const ch = codeChars[Math.floor(Math.random() * codeChars.length)]
const rd = k.add([
k.text(ch, { size: 8 }), k.pos(cx, -20 - row * 25),
k.color(safeColor(k, row === 0 ? '#ffd700' : '#c8a000')),
k.opacity(0.6 + Math.random() * 0.4), k.z(48), k.anchor('center'),
])
rainDrops.push(rd)
k.tween(rd.pos.y, GROUND_Y + 20, 0.6 + Math.random() * 0.3, (v) => {
rd.pos.y = v
rd.opacity = Math.max(0, 1 - (v - GROUND_Y + 30) / 50)
}).then(() => { if (rd.exists()) rd.destroy() })
}
}
await k.wait(0.3)
// Portal flash
screenFlash('#ffd700', 0.15)
spawnShockwave(homeX, GROUND_Y - 30, '#c8a000')
sfxSpecial()
// Fighter descends through golden portal
fighter.opacity = 1
await k.tween(-100, GROUND_Y - 6, 0.5, (v) => {
fighter.pos.y = v
}, k.easings.easeOutBack)
k.shake(12)
sfxExplosion()
// Persistent golden particles around creator
for (let i = 0; i < 6; i++) {
const p = k.add([
k.circle(2), k.pos(homeX, GROUND_Y - 30),
k.color(safeColor(k, '#ffd700')), k.opacity(0.5), k.z(11), k.anchor('center'),
])
p.onUpdate(() => {
const a = k.time() * (2 + i * 0.4) + i
p.pos.x = fighter.pos.x + Math.cos(a) * (20 + i * 5)
p.pos.y = fighter.pos.y - 25 + Math.sin(a) * (15 + i * 3)
p.opacity = 0.3 + Math.sin(k.time() * 6 + i) * 0.3
})
}
announceHype('THE CREATOR HAS ENTERED THE ARENA!')
rainDrops.forEach(r => { if (r.exists()) r.destroy() })
}
// Pick random entrance for each bot (different ones)
const idxA = Math.floor(Math.random() * entrances.length)
let idxB = Math.floor(Math.random() * entrances.length)
while (idxB === idxA && entrances.length > 1) idxB = Math.floor(Math.random() * entrances.length)
// Play entrances with slight stagger
// Play entrances with slight stagger — creator always gets special entrance
announceDeepIntro()
await entrances[idxA](fA, HOME_A, true)
if (botA.archetype === 'the_creator') {
await creatorEntrance(fA, HOME_A, true)
} else {
await entrances[idxA](fA, HOME_A, true)
}
await k.wait(0.3)
await entrances[idxB](fB, HOME_B, false)
if (botB.archetype === 'the_creator') {
await creatorEntrance(fB, HOME_B, false)
} else {
await entrances[idxB](fB, HOME_B, false)
}
await k.wait(0.2)
// Safety: ensure both fighters are visible and at home positions
@@ -7280,22 +7906,56 @@ export async function createFightScene(config: FightSceneConfig) {
}
const attackerTier = attackerSide === 'a' ? botA.tier : botB.tier
const choreo = pickChoreography(event.challengeType, exchangeCritical, event.round, attackerTier)
const attackerArch = attackerSide === 'a' ? botA.archetype : botB.archetype
const choreo = pickChoreography(event.challengeType, exchangeCritical, event.round, attackerTier, attackerArch)
// Morph system: transform during ultimates or devastating crits
const isUltimate = choreo.startsWith('ultimate')
const shouldMorph = isUltimate || (exchangeCritical && intensity > 0.7 && Math.random() < 0.4)
const isCreatorFighter = attackerArch === 'the_creator'
// Creator: always morph on ultimates, 70% on devastating crits
const shouldMorph = isCreatorFighter
? (isUltimate || (exchangeCritical && intensity > 0.6 && Math.random() < 0.7))
: (isUltimate || (exchangeCritical && intensity > 0.7 && Math.random() < 0.4))
let morphRevert: (() => Promise<void>) | null = null
if (shouldMorph) {
const attacker = k.get(attackerSide === 'a' ? 'fighterA' : 'fighterB')[0]
if (attacker) {
const seed = attackerSide === 'a' ? botA.seed : botB.seed
const morphOrder = getMorphOrder(seed)
// Cycle through morphs based on round number
const morphIdx = morphOrder[event.round % 3]
const result = await playMorph(attacker, attackerSide, morphIdx)
morphRevert = result.revert
await k.wait(0.2)
if (isCreatorFighter) {
// Omni-morph: creator transforms into a random archetype
const morphedTo = applyCreatorOmniMorph(attacker, attackerSide)
screenFlash('#ffd700', 0.15)
sfxZoomWhoosh()
sfxSpecial()
announceHype(`THE CREATOR — ${morphedTo.toUpperCase()} FORM!`)
const savedSX = attacker.scale.x
const savedSY = attacker.scale.y
await k.tween(0, 1, 0.3, (t) => {
attacker.scale.x = savedSX * (1 + t * 0.3)
attacker.scale.y = savedSY * (1 + t * 0.3)
}, k.easings.easeOutBack)
spawnShockwave(attacker.pos.x, attacker.pos.y - 20, '#ffd700')
k.shake(10)
glitchRGB(0.25)
morphRevert = async () => {
destroyMorphOverlays()
screenFlash('#c8a000', 0.1)
attacker.color = safeColor(k, '#ffffff')
attacker.opacity = 1
await k.tween(0, 1, 0.2, (t) => {
attacker.scale.x = savedSX * 1.3 + t * (savedSX - savedSX * 1.3)
attacker.scale.y = savedSY * 1.3 + t * (savedSY - savedSY * 1.3)
}, k.easings.easeInQuad)
}
await k.wait(0.2)
} else {
const seed = attackerSide === 'a' ? botA.seed : botB.seed
const morphOrder = getMorphOrder(seed)
// Cycle through morphs based on round number
const morphIdx = morphOrder[event.round % 3]
const result = await playMorph(attacker, attackerSide, morphIdx)
morphRevert = result.revert
await k.wait(0.2)
}
}
}
// Camera pull-back for ultimates: zoom out slightly to show the full move
@@ -7421,6 +8081,69 @@ export async function createFightScene(config: FightSceneConfig) {
k.wait(0.8).then(() => { if (judge.exists()) judge.play('idle') })
}
// === THE CREATOR CAMEO ===
// 6% chance per round (only if neither fighter IS the creator)
const neitherIsCreator = botA.archetype !== 'the_creator' && botB.archetype !== 'the_creator'
if (neitherIsCreator && Math.random() < 0.06) {
const cameoX = k.width() / 2
const cameoY = GROUND_Y - 80
// Golden portal flash
screenFlash('#ffd700', 0.1)
const portal = k.add([
k.circle(30), k.pos(cameoX, cameoY), k.color(safeColor(k, '#c8a000')),
k.opacity(0), k.z(50), k.anchor('center'), k.scale(0.1),
])
await k.tween(0, 1, 0.3, (t) => {
portal.opacity = t * 0.4
portal.scale.x = t * 1.5; portal.scale.y = t * 1.5
}, k.easings.easeOutBack)
// ₿ symbol descends from portal
const gift = k.add([
k.text('₿', { size: 18 }), k.pos(cameoX, cameoY - 30),
k.color(safeColor(k, '#ffd700')), k.opacity(0), k.z(51), k.anchor('center'),
])
const creatorLabel = k.add([
k.text('THE CREATOR', { size: 8 }), k.pos(cameoX, cameoY + 25),
k.color(safeColor(k, '#c8a000')), k.opacity(0), k.z(51), k.anchor('center'),
])
await k.tween(0, 1, 0.4, (t) => {
gift.opacity = t
gift.pos.y = cameoY - 30 + t * 20
creatorLabel.opacity = t * 0.8
}, k.easings.easeOutQuad)
// Gift flies to the round winner (or random fighter if draw)
const targetFighter = aWon ? fA : bWon ? fB : (Math.random() < 0.5 ? fA : fB)
if (targetFighter) {
const giftTexts = ['POWER UP!', 'BLESSED!', 'SATOSHI\'S GIFT!', 'HODL STRENGTH!', '21M ENERGY!']
const tx = targetFighter.pos.x, ty = targetFighter.pos.y - 30
await k.tween(0, 1, 0.35, (t) => {
gift.pos.x = cameoX + (tx - cameoX) * t
gift.pos.y = (cameoY - 10) + (ty - (cameoY - 10)) * t
}, k.easings.easeInQuad)
// Impact flash on fighter
screenFlash('#ffd700', 0.08)
spawnShockwave(tx, ty, '#ffd700')
const blessText = giftTexts[Math.floor(Math.random() * giftTexts.length)]
const bless = k.add([
k.text(blessText, { size: 10 }), k.pos(tx, ty - 20),
k.color(safeColor(k, '#ffd700')), k.opacity(1), k.z(52), k.anchor('center'),
])
k.tween(0, 1, 0.8, (t) => {
bless.pos.y = ty - 20 - t * 30
bless.opacity = 1 - t
}).then(() => { if (bless.exists()) bless.destroy() })
announceCool('THE CREATOR HAS BLESSED THIS FIGHT!')
}
gift.destroy()
// Fade out portal and label
await k.tween(1, 0, 0.3, (t) => {
portal.opacity = t * 0.4
creatorLabel.opacity = t * 0.8
})
portal.destroy()
creatorLabel.destroy()
}
// Update combos
const hasCombo = (aWon && comboA + 1 >= 3) || (bWon && comboB + 1 >= 3)
if (aWon) {