Merge pull request #50 from cassoule/blackjack

Blackjack
This commit is contained in:
Milo Gourvest
2025-09-12 14:55:07 +02:00
committed by GitHub
8 changed files with 639 additions and 9 deletions

View File

@@ -19,6 +19,8 @@ export const io = new Server(server, {
origin: FLAPI_URL,
methods: ['GET', 'POST', 'PUT', 'OPTIONS'],
},
pingInterval: 5000,
pingTimeout: 5000,
});
initializeSocket(io, client);

View File

@@ -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);

316
src/game/blackjack.js Normal file
View File

@@ -0,0 +1,316 @@
// /game/blackjack.js
// 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"];
// Build a single 52-card deck like "Ad","Ts", etc.
export const singleDeck = RANKS.flatMap(r => SUITS.map(s => `${r}${s}`));
export function buildShoe(decks = 6) {
const shoe = [];
for (let i = 0; i < decks; i++) shoe.push(...singleDeck);
return shuffle(shoe);
}
export function shuffle(arr) {
// FisherYates
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
// Draw one card from the shoe; if empty, caller should reshuffle at end of round.
export function draw(shoe) {
return shoe.pop();
}
// Return an object describing the best value of a hand with flexible Aces.
export function handValue(cards) {
// Count with all aces as 11, then reduce as needed
let total = 0;
let aces = 0;
for (const c of cards) {
const r = c[0];
if (r === "A") { total += 11; aces += 1; }
else if (r === "T" || r === "J" || r === "Q" || r === "K") total += 10;
else total += Number(r);
}
while (total > 21 && aces > 0) {
total -= 10; // convert an Ace from 11 to 1
aces -= 1;
}
const soft = (aces > 0); // if any Ace still counted as 11, it's a soft hand
return { total, soft };
}
export function isBlackjack(cards) {
return cards.length === 2 && handValue(cards).total === 21;
}
export function isBust(cards) {
return handValue(cards).total > 21;
}
// Dealer draw rule. By default, dealer stands on soft 17 (S17).
export function dealerShouldHit(dealerCards, hitSoft17 = false) {
const v = handValue(dealerCards);
if (v.total < 17) return true;
if (v.total === 17 && v.soft && hitSoft17) return true;
return false;
}
// Compare a player hand to dealer and return outcome.
export function compareHands(playerCards, dealerCards) {
const pv = handValue(playerCards).total;
const dv = handValue(dealerCards).total;
if (pv > 21) return "lose";
if (dv > 21) return "win";
if (pv > dv) return "win";
if (pv < dv) return "lose";
return "push";
}
// Compute payout for a single finished hand (no splits here).
// options: { blackjackPayout: 1.5, allowSurrender: false }
export function settleHand({ bet, playerCards, dealerCards, doubled = false, surrendered = false, blackjackPayout = 1.5 }) {
if (surrendered) return { delta: -bet / 2, result: "surrender" };
const pBJ = isBlackjack(playerCards);
const dBJ = isBlackjack(dealerCards);
if (pBJ && !dBJ) return { delta: bet * blackjackPayout, result: "blackjack" };
if (!pBJ && dBJ) return { delta: -bet, result: "lose" };
if (pBJ && dBJ) return { delta: 0, result: "push" };
const outcome = compareHands(playerCards, dealerCards);
let unit = bet;
if (outcome === "win") return { delta: unit, result: "win" };
if (outcome === "lose") return { delta: -unit, result: "lose" };
return { delta: 0, result: "push" };
}
// Helper to decide if doubling is still allowed (first decision, 2 cards, not hit yet).
export function canDouble(hand) {
return hand.cards.length === 2 && !hand.hasActed;
}
// Very small utility to format a public-safe snapshot of room state
export function publicPlayerView(player) {
// Hide hole cards until dealer reveal is fine for dealer only; player cards are visible.
return {
id: player.id,
globalName: player.globalName,
avatar: player.avatar,
bank: player.bank,
currentBet: player.currentBet,
inRound: player.inRound,
hands: player.hands.map(h => ({
cards: h.cards,
stood: h.stood,
busted: h.busted,
doubled: h.doubled,
surrendered: h.surrendered,
result: h.result ?? null,
total: handValue(h.cards).total,
soft: handValue(h.cards).soft,
})),
};
}
// Build initial room object
export function createBlackjackRoom({
minBet = 10,
maxBet = 1000,
fakeMoney = false,
decks = 6,
hitSoft17 = false,
blackjackPayout = 1.5,
cutCardRatio = 0.25, // reshuffle when 25% of shoe remains
phaseDurations = {
bettingMs: 15000,
dealMs: 1000,
playMsPerPlayer: 15000,
revealMs: 1000,
payoutMs: 10000,
},
animation = {
dealerDrawMs: 500,
}
} = {}) {
return {
id: "blackjack-room",
name: "Blackjack",
created_at: Date.now(),
status: "betting", // betting | dealing | playing | dealer | payout | shuffle
phase_ends_at: Date.now() + phaseDurations.bettingMs,
minBet, maxBet, fakeMoney,
settings: { decks, hitSoft17, blackjackPayout, cutCardRatio, phaseDurations, animation },
shoe: buildShoe(decks),
discard: [],
dealer: { cards: [], holeHidden: true },
players: {}, // userId -> { id, globalName, avatar, bank, currentBet, inRound, hands: [{cards, stood, busted, doubled, surrendered, hasActed}], activeHand: 0 }
leavingAfterRound: {},
};
}
// Reshuffle at start of the next round if the shoe is low
export function needsReshuffle(room) {
return room.shoe.length < singleDeck.length * room.settings.decks * room.settings.cutCardRatio;
}
// --- Round Lifecycle helpers ---
export function resetForNewRound(room) {
room.status = "betting";
room.dealer = { cards: [], holeHidden: true };
room.leavingAfterRound = {};
// Clear per-round attributes on players, but keep bank and presence
for (const p of Object.values(room.players)) {
p.inRound = false;
p.currentBet = 0;
p.hands = [ { cards: [], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false } ];
p.activeHand = 0;
}
}
export function startBetting(room, now) {
resetForNewRound(room);
if (needsReshuffle(room)) {
room.status = "shuffle";
// quick shuffle animation phase
room.shoe = buildShoe(room.settings.decks);
}
room.status = "betting";
room.phase_ends_at = now + room.settings.phaseDurations.bettingMs;
}
export function dealInitial(room) {
room.status = "dealing";
// Deal one to each player who placed a bet, then again, then dealer up + hole
const actives = Object.values(room.players).filter(p => p.currentBet >= room.minBet);
for (const p of actives) {
p.inRound = true;
p.hands = [ { cards: [draw(room.shoe)], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false } ];
}
room.dealer.cards = [draw(room.shoe), draw(room.shoe)];
room.dealer.holeHidden = true;
for (const p of actives) {
p.hands[0].cards.push(draw(room.shoe));
}
room.status = "playing";
}
export function autoActions(room) {
// Auto-stand if player already blackjack
for (const p of Object.values(room.players)) {
if (!p.inRound) continue;
const h = p.hands[p.activeHand];
if (isBlackjack(h.cards)) {
h.stood = true;
h.hasActed = true;
}
}
}
export function everyoneDone(room) {
return Object.values(room.players).every(p => {
if (!p.inRound) return true;
const h = p.hands[p.activeHand];
return h.stood || h.busted || isBlackjack(h.cards) || h.surrendered;
});
}
export function dealerPlay(room) {
room.status = "dealer";
room.dealer.holeHidden = false;
while (dealerShouldHit(room.dealer.cards, room.settings.hitSoft17)) {
room.dealer.cards.push(draw(room.shoe));
}
}
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];
const res = settleHand({
bet: p.currentBet,
playerCards: hand.cards,
dealerCards: room.dealer.cards,
doubled: hand.doubled,
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;
}
}
// Apply a player decision; returns a string event or throws on invalid.
export function applyAction(room, playerId, action) {
const p = room.players[playerId];
if (!p || !p.inRound || room.status !== "playing") throw new Error("Not allowed");
const hand = p.hands[p.activeHand];
switch (action) {
case "hit": {
if (hand.stood || hand.busted) throw new Error("Already ended");
hand.hasActed = true;
hand.cards.push(draw(room.shoe));
if (isBust(hand.cards)) hand.busted = true;
return "hit";
}
case "stand": {
hand.stood = true;
hand.hasActed = true;
return "stand";
}
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));
if (isBust(hand.cards)) hand.busted = true;
else hand.stood = true;
return "double";
}
case "surrender": {
if (hand.cards.length !== 2 || hand.hasActed) throw new Error("Cannot surrender now");
hand.surrendered = true;
hand.stood = true;
hand.hasActed = true;
return "surrender";
}
default:
throw new Error("Invalid action");
}
}

View File

@@ -53,6 +53,8 @@ export let tictactoeQueue = [];
// Stores user IDs waiting to play Connect 4.
export let connect4Queue = [];
export let queueMessagesEndpoints = [];
// --- Rate Limiting and Caching ---

View File

@@ -10,6 +10,7 @@ import { pokerRoutes } from './routes/poker.js';
import { solitaireRoutes } from './routes/solitaire.js';
import {getSocketIo} from "./socket.js";
import {erinyesRoutes} from "./routes/erinyes.js";
import {blackjackRoutes} from "./routes/blackjack.js";
// --- EXPRESS APP INITIALIZATION ---
const app = express();
@@ -21,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();
});
@@ -50,6 +51,8 @@ app.use('/api/poker', pokerRoutes(client, io));
// Solitaire-specific routes
app.use('/api/solitaire', solitaireRoutes(client, io));
app.use('/api/blackjack', blackjackRoutes(client, io));
// erinyes-specific routes
app.use('/api/erinyes', erinyesRoutes(client, io));

View File

@@ -0,0 +1,265 @@
// /routes/blackjack.js
import express from "express";
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";
import { client } from "../../bot/client.js";
import {emitToast, emitUpdate} from "../socket.js";
export function blackjackRoutes(io) {
const router = express.Router();
// --- Singleton continuous room ---
const room = createBlackjackRoom({
minBet: 10,
maxBet: 5000,
fakeMoney: false,
decks: 6,
hitSoft17: false, // S17 (dealer stands on soft 17) if false
blackjackPayout: 1.5, // 3:2
cutCardRatio: 0.25,
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,
name: r.name,
status: r.status,
phase_ends_at: r.phase_ends_at,
minBet: r.minBet,
maxBet: r.maxBet,
settings: r.settings,
dealer: { cards: r.dealer.holeHidden ? [r.dealer.cards[0], "XX"] : r.dealer.cards, total: r.dealer.holeHidden ? null : handValue(r.dealer.cards).total },
players: Object.values(r.players).map(publicPlayerView),
shoeCount: r.shoe.length,
};
}
// --- Public endpoints ---
router.get("/", (req, res) => res.status(200).json({ room: snapshot(room) }));
router.post("/join", async (req, res) => {
const { userId } = req.body;
if (!userId) return res.status(400).json({ message: "userId required" });
if (room.players[userId]) return res.status(200).json({ message: "Already here" });
const user = await client.users.fetch(userId);
const bank = getUser.get(userId)?.coins ?? 0;
room.players[userId] = {
id: userId,
globalName: user.globalName || user.username,
avatar: user.displayAvatarURL({ dynamic: true, size: 256 }),
bank,
currentBet: 0,
inRound: false,
hands: [{ cards: [], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false }],
activeHand: 0,
joined_at: Date.now(),
};
emitUpdate("player-joined", snapshot(room));
return res.status(200).json({ message: "joined" });
});
router.post("/leave", (req, res) => {
const { userId } = req.body;
if (!userId || !room.players[userId]) return res.status(404).json({ message: "not in room" });
const p = room.players[userId];
if (p.inRound) {
// leave after round to avoid abandoning an active bet
room.leavingAfterRound[userId] = true;
return res.status(200).json({ message: "will-leave-after-round" });
} else {
delete room.players[userId];
emitUpdate("player-left", snapshot(room));
return res.status(200).json({ message: "left" });
}
});
router.post("/bet", (req, res) => {
const { userId, amount } = req.body;
const p = room.players[userId];
if (!p) return res.status(404).json({ message: "not in room" });
if (room.status !== "betting") return res.status(403).json({ message: "betting-closed" });
const bet = Math.floor(Number(amount) || 0);
if (bet < room.minBet || bet > room.maxBet) return res.status(400).json({ message: "invalid-bet" });
if (!room.settings.fakeMoney) {
const userDB = getUser.get(userId);
const coins = userDB?.coins ?? 0;
if (coins < bet) return res.status(403).json({ message: "insufficient-funds" });
updateUserCoins.run({ id: userId, coins: coins - bet });
insertLog.run({
id: `${userId}-blackjack-${Date.now()}`,
user_id: userId, target_user_id: null,
action: 'BLACKJACK_BET',
coins_amount: -bet, user_new_amount: coins - bet,
});
p.bank = coins - bet;
}
p.currentBet = bet;
emitToast({ type: "player-bet", userId, amount: bet });
emitUpdate("bet-placed", snapshot(room));
return res.status(200).json({ message: "bet-accepted" });
});
router.post("/action/:action", (req, res) => {
const { userId } = req.body;
const action = req.params.action;
const p = room.players[userId];
if (!p) return res.status(404).json({ message: "not in room" });
if (!p.inRound || room.status !== "playing") return res.status(403).json({ message: "not-your-turn" });
// Handle extra coin lock for double
if (action === "double" && !room.settings.fakeMoney) {
const userDB = getUser.get(userId);
const coins = userDB?.coins ?? 0;
if (coins < p.currentBet) return res.status(403).json({ message: "insufficient-funds-for-double" });
updateUserCoins.run({ id: userId, coins: coins - p.currentBet });
insertLog.run({
id: `${userId}-blackjack-${Date.now()}`,
user_id: userId, target_user_id: null,
action: 'BLACKJACK_DOUBLE',
coins_amount: -p.currentBet, user_new_amount: coins - p.currentBet,
});
p.bank = coins - p.currentBet;
// effective bet size is handled in settlement via hand.doubled flag
}
try {
const evt = applyAction(room, userId, action);
emitToast({ type: `player-${evt}`, userId });
emitUpdate("player-action", snapshot(room));
return res.status(200).json({ message: "ok" });
} catch (e) {
return res.status(400).json({ message: e.message });
}
});
// --- Game loop ---
// Simple phase machine that runs regardless of player count.
setInterval(async () => {
const now = Date.now();
if (room.status === "betting" && now >= room.phase_ends_at) {
const hasBets = Object.values(room.players).some(p => p.currentBet >= room.minBet);
if (!hasBets) {
// Extend betting window if no one bet
room.phase_ends_at = now + room.settings.phaseDurations.bettingMs;
emitUpdate("betting-extend", snapshot(room));
return;
}
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") {
// 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);
}
// 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) {
// Remove leavers
for (const userId of Object.keys(room.leavingAfterRound)) {
delete room.players[userId];
}
// Prepare next round
startBetting(room, now);
emitUpdate("new-round", snapshot(room));
}
}, 100);
return router;
}

View File

@@ -1,5 +1,11 @@
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
import { activeTicTacToeGames, tictactoeQueue, activeConnect4Games, connect4Queue } from '../game/state.js';
import {
activeTicTacToeGames,
tictactoeQueue,
activeConnect4Games,
connect4Queue,
queueMessagesEndpoints, activePredis
} from '../game/state.js';
import { createConnect4Board, formatConnect4BoardForDiscord, checkConnect4Win, checkConnect4Draw, C4_ROWS } from '../game/various.js';
import { eloHandler } from '../game/elo.js';
import { getUser } from "../database/index.js";
@@ -21,6 +27,14 @@ export function initializeSocket(server, client) {
registerTicTacToeEvents(socket, client);
registerConnect4Events(socket, client);
socket.on('tictactoe:queue:leave', async ({ discordId }) => await refreshQueuesForUser(discordId, client));
// catch tab kills / network drops
socket.on('disconnecting', async () => {
const discordId = socket.handshake.auth?.discordId; // or your mapping
await refreshQueuesForUser(discordId, client);
});
socket.on('disconnect', () => {
//
});
@@ -211,10 +225,34 @@ async function createGame(client, gameType) {
async function refreshQueuesForUser(userId, client) {
// FIX: Mutate the array instead of reassigning it.
let index = tictactoeQueue.indexOf(userId);
if (index > -1) tictactoeQueue.splice(index, 1);
if (index > -1) {
tictactoeQueue.splice(index, 1);
try {
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
const user = await client.users.fetch(userId);
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId])
const updatedEmbed = new EmbedBuilder().setTitle('Tic Tac Toe').setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`).setColor(0xED4245).setTimestamp(new Date());
await queueMsg.edit({ embeds: [updatedEmbed], components: [] });
delete queueMessagesEndpoints[userId];
} catch (e) {
console.error('Error updating queue message : ', e);
}
}
index = connect4Queue.indexOf(userId);
if (index > -1) connect4Queue.splice(index, 1);
if (index > -1) {
connect4Queue.splice(index, 1);
try {
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
const user = await client.users.fetch(userId);
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId])
const updatedEmbed = new EmbedBuilder().setTitle('Puissance 4').setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`).setColor(0xED4245).setTimestamp(new Date());
await queueMsg.edit({ embeds: [updatedEmbed], components: [] });
delete queueMessagesEndpoints[userId];
} catch (e) {
console.error('Error updating queue message : ', e);
}
}
await emitQueueUpdate(client, 'tictactoe');
await emitQueueUpdate(client, 'connect4');
@@ -239,9 +277,10 @@ async function postQueueToDiscord(client, playerId, title, url) {
try {
const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID);
const user = await client.users.fetch(playerId);
const embed = new EmbedBuilder().setTitle(title).setDescription(`**${user.globalName || user.username}** est dans la file d'attente.`).setColor('#5865F2');
const embed = new EmbedBuilder().setTitle(title).setDescription(`**${user.globalName || user.username}** est dans la file d'attente.`).setColor('#5865F2').setTimestamp(new Date());
const row = new ActionRowBuilder().addComponents(new ButtonBuilder().setLabel(`Jouer contre ${user.username}`).setURL(`${process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}${url}`).setStyle(ButtonStyle.Link));
await generalChannel.send({ embeds: [embed], components: [row] });
const msg = await generalChannel.send({ embeds: [embed], components: [row] });
queueMessagesEndpoints[playerId] = msg.id
} catch (e) { console.error(`Failed to post queue message for ${title}:`, e); }
}
@@ -302,4 +341,7 @@ export async function emitPokerUpdate(data) {
export async function emitPokerToast(data) {
io.emit('poker-toast', data);
}
}
export const emitUpdate = (type, room) => io.emit("blackjack:update", { type, room });
export const emitToast = (payload) => io.emit("blackjack:toast", payload);

View File

@@ -209,7 +209,7 @@ export async function getOnlineUsersWithRole(guild, roleId) {
if (!guild || !roleId) return new Map();
try {
const members = await guild.members.fetch();
return members.filter(m => !m.user.bot && m.presence?.status !== 'offline' && m.roles.cache.has(roleId));
return members.filter(m => !m.user.bot && m.presence?.status !== 'offline' && m.presence?.status !== undefined && m.roles.cache.has(roleId));
} catch (err) {
console.error('Error fetching online members with role:', err);
return new Map();