diff --git a/src/bot/events.js b/src/bot/events.js index 9c2dacb..45c4fe9 100644 --- a/src/bot/events.js +++ b/src/bot/events.js @@ -12,7 +12,7 @@ export function initializeEvents(client, io) { // --- on 'ready' --- // This event fires once the bot has successfully logged in and is ready to operate. // It's a good place for setup tasks that require the bot to be online. - client.once('ready', async () => { + client.once('clientReady', async () => { console.log(`Bot is ready and logged in as ${client.user.tag}!`); console.log('[Startup] Bot is ready, performing initial data sync...'); await getAkhys(client); diff --git a/src/game/blackjack.js b/src/game/blackjack.js index 7d339c3..a37adf0 100644 --- a/src/game/blackjack.js +++ b/src/game/blackjack.js @@ -2,6 +2,9 @@ // Core blackjack helpers for a single continuous room. // Inspired by your poker helpers API style. +import {emitToast} from "../server/socket.js"; +import {getUser, insertLog, updateUserCoins} from "../database/index.js"; + export const RANKS = ["A","2","3","4","5","6","7","8","9","T","J","Q","K"]; export const SUITS = ["d","s","c","h"]; @@ -88,7 +91,7 @@ export function settleHand({ bet, playerCards, dealerCards, doubled = false, sur if (pBJ && dBJ) return { delta: 0, result: "push" }; const outcome = compareHands(playerCards, dealerCards); - let unit = bet * (doubled ? 2 : 1); + let unit = bet; if (outcome === "win") return { delta: unit, result: "win" }; if (outcome === "lose") return { delta: -unit, result: "lose" }; return { delta: 0, result: "push" }; @@ -131,7 +134,16 @@ export function createBlackjackRoom({ hitSoft17 = false, blackjackPayout = 1.5, cutCardRatio = 0.25, // reshuffle when 25% of shoe remains - phaseDurations = { bettingMs: 15000, dealMs: 1000, playMsPerPlayer: 15000, revealMs: 1000, payoutMs: 2000 } + phaseDurations = { + bettingMs: 15000, + dealMs: 1000, + playMsPerPlayer: 15000, + revealMs: 1000, + payoutMs: 10000, + }, + animation = { + dealerDrawMs: 500, + } } = {}) { return { id: "blackjack-room", @@ -140,7 +152,7 @@ export function createBlackjackRoom({ status: "betting", // betting | dealing | playing | dealer | payout | shuffle phase_ends_at: Date.now() + phaseDurations.bettingMs, minBet, maxBet, fakeMoney, - settings: { decks, hitSoft17, blackjackPayout, cutCardRatio, phaseDurations }, + settings: { decks, hitSoft17, blackjackPayout, cutCardRatio, phaseDurations, animation }, shoe: buildShoe(decks), discard: [], dealer: { cards: [], holeHidden: true }, @@ -226,6 +238,7 @@ export function dealerPlay(room) { export function settleAll(room) { room.status = "payout"; + const allRes = {} for (const p of Object.values(room.players)) { if (!p.inRound) continue; const hand = p.hands[p.activeHand]; @@ -237,6 +250,24 @@ export function settleAll(room) { surrendered: hand.surrendered, blackjackPayout: room.settings.blackjackPayout, }); + allRes[p.id] = res; + if (res.result === 'win' || res.result === 'push') { + const userDB = getUser.get(p.id); + if (userDB) { + try { + updateUserCoins.run({ id: p.id, coins: userDB.coins + p.currentBet + res.delta }); + insertLog.run({ + id: `${p.id}-blackjack-${Date.now()}`, + user_id: p.id, target_user_id: null, + action: 'BLACKJACK_PAYOUT', + coins_amount: res.delta + p.currentBet, user_new_amount: userDB.coins + p.currentBet + res.delta, + }); + } catch (e) { + console.log(e) + } + } + } + emitToast({ type: `payout-res`, allRes }); hand.result = res.result; hand.delta = res.delta; } @@ -264,6 +295,7 @@ export function applyAction(room, playerId, action) { case "double": { if (!canDouble(hand)) throw new Error("Cannot double now"); hand.doubled = true; + p.currentBet*=2 hand.hasActed = true; // The caller (routes) must also handle additional balance lock on the bet if using real coins hand.cards.push(draw(room.shoe)); diff --git a/src/server/app.js b/src/server/app.js index afceae5..897b277 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -22,7 +22,7 @@ const FLAPI_URL = process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : // CORS Middleware app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', FLAPI_URL); - res.header('Access-Control-Allow-Headers', 'Content-Type, X-API-Key, ngrok-skip-browser-warning'); + res.header('Access-Control-Allow-Headers', 'Content-Type, X-API-Key, ngrok-skip-browser-warning, Cache-Control, Pragma, Expires'); next(); }); diff --git a/src/server/routes/blackjack.js b/src/server/routes/blackjack.js index 7f3d295..6571950 100644 --- a/src/server/routes/blackjack.js +++ b/src/server/routes/blackjack.js @@ -1,6 +1,18 @@ // /routes/blackjack.js import express from "express"; -import { createBlackjackRoom, startBetting, dealInitial, autoActions, everyoneDone, dealerPlay, settleAll, applyAction, publicPlayerView, handValue } from "../../game/blackjack.js"; +import { + createBlackjackRoom, + startBetting, + dealInitial, + autoActions, + everyoneDone, + dealerPlay, + settleAll, + applyAction, + publicPlayerView, + handValue, + dealerShouldHit, draw +} from "../../game/blackjack.js"; // Optional: hook into your DB & Discord systems if available import { getUser, updateUserCoins, insertLog } from "../../database/index.js"; @@ -19,9 +31,65 @@ export function blackjackRoutes(io) { hitSoft17: false, // S17 (dealer stands on soft 17) if false blackjackPayout: 1.5, // 3:2 cutCardRatio: 0.25, - phaseDurations: { bettingMs: 15000, dealMs: 1000, playMsPerPlayer: 15000, revealMs: 1000, payoutMs: 2000 }, + phaseDurations: { bettingMs: 10000, dealMs: 2000, playMsPerPlayer: 15000, revealMs: 1000, payoutMs: 7000 }, + animation: { dealerDrawMs: 1000 } }); + const sleep = (ms) => new Promise(res => setTimeout(res, ms)); + let animatingDealer = false; + + async function runDealerAnimation() { + if (animatingDealer) return; + animatingDealer = true; + + room.status = "dealer"; + room.dealer.holeHidden = false; + await sleep(room.settings.phaseDurations.revealMs ?? 1000); + room.phase_ends_at = Date.now() + (room.settings.phaseDurations.revealMs ?? 1000); + emitUpdate("dealer-reveal", snapshot(room)); + await sleep(room.settings.phaseDurations.revealMs ?? 1000); + + while (dealerShouldHit(room.dealer.cards, room.settings.hitSoft17)) { + room.dealer.cards.push(draw(room.shoe)); + room.phase_ends_at = Date.now() + (room.settings.animation?.dealerDrawMs ?? 500); + emitUpdate("dealer-hit", snapshot(room)); + await sleep(room.settings.animation?.dealerDrawMs ?? 500); + } + + settleAll(room); + room.status = "payout"; + room.phase_ends_at = Date.now() + (room.settings.phaseDurations.payoutMs ?? 10000); + emitUpdate("payout", snapshot(room)) + + animatingDealer = false; + } + + function autoTimeoutAFK(now) { + if (room.status !== "playing") return false; + if (!room.phase_ends_at || now < room.phase_ends_at) return false; + + let changed = false; + for (const p of Object.values(room.players)) { + if (!p.inRound) continue; + const h = p.hands[p.activeHand]; + if (!h.hasActed && !h.busted && !h.stood && !h.surrendered) { + h.surrendered = true; + h.stood = true; + h.hasActed = true; + room.leavingAfterRound[p.id] = true; // kick at end of round + emitToast({ type: "player-timeout", userId: p.id }); + changed = true; + } else if (h.hasActed && !h.stood) { + h.stood = true; + room.leavingAfterRound[p.id] = true; // kick at end of round + emitToast({ type: "player-auto-stand", userId: p.id }); + changed = true; + } + } + if (changed) emitUpdate("auto-surrender", snapshot(room)); + return changed; + } + function snapshot(r) { return { id: r.id, @@ -144,7 +212,7 @@ export function blackjackRoutes(io) { // --- Game loop --- // Simple phase machine that runs regardless of player count. - setInterval(() => { + setInterval(async () => { const now = Date.now(); if (room.status === "betting" && now >= room.phase_ends_at) { @@ -158,41 +226,28 @@ export function blackjackRoutes(io) { dealInitial(room); autoActions(room); emitUpdate("initial-deal", snapshot(room)); + + room.phase_ends_at = Date.now() + room.settings.phaseDurations.playMsPerPlayer; + emitUpdate("playing-start", snapshot(room)); + return; } if (room.status === "playing") { - // When all active players are done, proceed to dealer play - if (everyoneDone(room)) { - dealerPlay(room); - emitUpdate("dealer-start", snapshot(room)); - } - } - - if (room.status === "dealer") { - settleAll(room); - - // Apply coin deltas - for (const p of Object.values(room.players)) { - if (!p.inRound) continue; - const h = p.hands[p.activeHand]; - if (room.settings.fakeMoney) continue; - if (typeof h.delta === "number" && h.delta !== 0) { - const userDB = getUser.get(p.id); - if (userDB) { - updateUserCoins.run({ id: p.id, coins: userDB.coins + h.delta }); - insertLog.run({ - id: `${p.id}-blackjack-${Date.now()}`, - user_id: p.id, target_user_id: null, - action: `BLACKJACK_${h.delta > 0 ? "WIN" : "LOSE"}`, - coins_amount: h.delta, user_new_amount: userDB.coins + h.delta, - }); - } - } + // If the per-round playing timer expired, auto-surrender AFKs (you already added this) + if (room.phase_ends_at && now >= room.phase_ends_at) { + autoTimeoutAFK(now); } - room.phase_ends_at = now + room.settings.phaseDurations.payoutMs; - emitUpdate("payout", snapshot(room)); - room.status = "payout"; + // Everyone acted before the timer? Cut short and go straight to dealer. + if (everyoneDone(room) && !animatingDealer) { + // Set a new server-driven deadline for the reveal pause, + // so the client's countdown immediately reflects the phase change. + room.phase_ends_at = Date.now(); + emitUpdate("playing-cut-short", snapshot(room)); + + // Now run the animated dealer with per-step updates + runDealerAnimation(); + } } if (room.status === "payout" && now >= room.phase_ends_at) { @@ -204,7 +259,7 @@ export function blackjackRoutes(io) { startBetting(room, now); emitUpdate("new-round", snapshot(room)); } - }, 400); + }, 100); return router; } \ No newline at end of file