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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user