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
+26 -6
View File
@@ -1,7 +1,7 @@
import { db, schema } from '../db/index.js'
import { eq, sql } from 'drizzle-orm'
import { runFightAsync, isInFight } from './orchestrator.js'
import { checkPaymentStatus, refundEntry } from './payments.js'
import { checkPaymentStatus, refundEntry, consumePaymentForQueue, linkPaymentsToFight, releasePayment } from './payments.js'
interface RankedQueueEntry {
botId: string
@@ -36,35 +36,49 @@ export function getRankedQueueStatus(): { waiting: number } {
* NEVER matches against mock bots.
*/
export async function joinRankedQueue(botId: string, paymentId: string): Promise<string> {
// Verify payment is confirmed
// Verify payment is confirmed (checks NWC if still pending)
const status = await checkPaymentStatus(paymentId)
if (status !== 'confirmed') {
throw new Error(`Payment not confirmed (status: ${status}). Cannot join ranked queue.`)
}
// Consume the payment — prevents double-spend
// Verifies payment is confirmed, unused, belongs to this bot
const consumed = await consumePaymentForQueue(paymentId, botId)
if (!consumed) {
throw new Error('Payment already used or does not belong to this bot.')
}
// Check cooldown
const cooldownUntil = rankedCooldowns.get(botId)
if (cooldownUntil && Date.now() < cooldownUntil) {
const waitSec = Math.ceil((cooldownUntil - Date.now()) / 1000)
releasePayment(paymentId)
throw new Error(`Cooldown active. Wait ${waitSec}s.`)
}
// Check if already in a fight
if (isInFight(botId)) {
releasePayment(paymentId)
throw new Error('Bot is already in a fight.')
}
// Load bot
const botRows = await db.select().from(schema.bots).where(eq(schema.bots.id, botId)).limit(1)
if (botRows.length === 0) throw new Error('Bot not found')
if (botRows.length === 0) {
releasePayment(paymentId)
throw new Error('Bot not found')
}
const bot = botRows[0]
if (!bot.isActive) {
releasePayment(paymentId)
throw new Error('Bot is deactivated due to webhook errors. Re-test your webhook to reactivate.')
}
// NEVER allow mock/classic bots in ranked
if (bot.webhookUrl.startsWith('http://mock.local') || bot.webhookUrl.startsWith('http://classic.local')) {
releasePayment(paymentId)
throw new Error('Practice bots cannot join ranked fights.')
}
@@ -75,6 +89,8 @@ export async function joinRankedQueue(botId: string, paymentId: string): Promise
if (existing !== -1) {
const old = rankedQueue.splice(existing, 1)[0]
clearTimeout(old.timeoutHandle)
// Release the OLD payment (the new one is already consumed)
releasePayment(old.paymentId)
old.reject(new Error('Rejoined ranked queue'))
}
@@ -89,8 +105,9 @@ export async function joinRankedQueue(botId: string, paymentId: string): Promise
const opponent = rankedQueue.shift()!
clearTimeout(opponent.timeoutHandle)
// Start ranked fight
// Start ranked fight and link both payments
const fightId = await runFightAsync(opponent.botId, botId, 'ranked')
await linkPaymentsToFight(fightId, [opponent.paymentId, paymentId])
opponent.resolve(fightId)
return fightId
}
@@ -103,6 +120,7 @@ export async function joinRankedQueue(botId: string, paymentId: string): Promise
const mock = mockBots[Math.floor(Math.random() * mockBots.length)]
console.log(`[ranked-queue] dev: auto-matching ${bot.name} vs mock bot ${mock.name}`)
const fightId = await runFightAsync(botId, mock.id, 'ranked')
await linkPaymentsToFight(fightId, [paymentId])
return fightId
}
}
@@ -113,7 +131,8 @@ export async function joinRankedQueue(botId: string, paymentId: string): Promise
const idx = rankedQueue.findIndex(e => e.botId === botId)
if (idx !== -1) {
rankedQueue.splice(idx, 1)
// Refund the entry fee
// Release payment back to refundable state, then refund
releasePayment(paymentId)
try {
await refundEntry(paymentId)
reject(new Error('No ranked opponent found — entry fee refunded.'))
@@ -146,8 +165,9 @@ export async function leaveRankedQueue(botId: string): Promise<boolean> {
const entry = rankedQueue.splice(idx, 1)[0]
clearTimeout(entry.timeoutHandle)
// Refund entry fee
// Release payment, then refund
try {
releasePayment(entry.paymentId)
await refundEntry(entry.paymentId)
} catch (err) {
console.error(`[ranked-queue] refund failed for ${entry.paymentId}:`, err)