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:
co-authored by
Claude Opus 4.6
parent
8a3480e5ab
commit
6d390f69b2
+774
-51
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user