mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-28 10:38:32 +01:00
add address modal and try to fix payout
This commit is contained in:
+29
-4
@@ -22,6 +22,8 @@ class Game {
|
||||
this.players = {};
|
||||
this.monsters = {};
|
||||
this.dungeon = null;
|
||||
this.moveCount = 0;
|
||||
this.startedAt = Date.now();
|
||||
|
||||
// Get default configuration from DungeonGenerator
|
||||
const dungeonConfig = DungeonGenerator.getConfig();
|
||||
@@ -140,7 +142,8 @@ class Game {
|
||||
this.dungeon.map[newY][newX] !== undefined &&
|
||||
(this.dungeon.map[newY][newX] === primaryFloor || this.dungeon.map[newY][newX] === secondaryFloor)) {
|
||||
|
||||
this.player.moveTo(newX, newY);
|
||||
this.player.moveTo(newX, newY);
|
||||
this.moveCount += 1;
|
||||
if (CONSOLE_LOGGING) {
|
||||
console.log(`Player moved to ${newX},${newY} in game for socket ${this.socketId}`);
|
||||
}
|
||||
@@ -151,7 +154,7 @@ class Game {
|
||||
// Check for game events like finding treasure or exit
|
||||
if (this.dungeon.exit && this.player.isAt(this.dungeon.exit[0], this.dungeon.exit[1])) {
|
||||
this.gameState = 'won';
|
||||
return { status: 'moved', event: 'escaped', player: this.player.getState(), visibleTiles: this.visibleTiles };
|
||||
return { status: 'moved', event: 'escaped', player: this.player.getState(), visibleTiles: this.visibleTiles, moves: this.moveCount };
|
||||
}
|
||||
if (this.dungeon.treasure && this.player.isAt(this.dungeon.treasure[0], this.dungeon.treasure[1]) && !this.player.hasTreasure) {
|
||||
this.player.hasTreasure = true;
|
||||
@@ -163,10 +166,10 @@ class Game {
|
||||
console.log(`🗑️ TREASURE REMOVED: dungeon.treasure is now null for player ${this.socketId}`);
|
||||
console.log(`Player ${this.socketId} collected treasure! hasTreasure: ${this.player.hasTreasure}`);
|
||||
}
|
||||
return { status: 'moved', event: 'treasure_found', player: this.player.getState(), visibleTiles: this.visibleTiles };
|
||||
return { status: 'moved', event: 'treasure_found', player: this.player.getState(), visibleTiles: this.visibleTiles, moves: this.moveCount };
|
||||
}
|
||||
|
||||
return { status: 'moved', player: this.player.getState(), visibleTiles: this.visibleTiles };
|
||||
return { status: 'moved', player: this.player.getState(), visibleTiles: this.visibleTiles, moves: this.moveCount };
|
||||
}
|
||||
|
||||
if (CONSOLE_LOGGING) {
|
||||
@@ -176,7 +179,27 @@ class Game {
|
||||
}
|
||||
|
||||
moveMonster() {
|
||||
if (!this.monster || !this.player) {
|
||||
return { status: 'idle' };
|
||||
}
|
||||
|
||||
this.monster.moveTowardPlayer(this.player, this.dungeon);
|
||||
|
||||
if (this.monster.hasCaughtPlayer(this.player)) {
|
||||
this.gameState = 'lost';
|
||||
return {
|
||||
status: 'monster_caught',
|
||||
event: 'monster_caught',
|
||||
player: this.player.getState(),
|
||||
monster: this.monster.getState()
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'moved',
|
||||
player: this.player.getState(),
|
||||
monster: this.monster.getState()
|
||||
};
|
||||
}
|
||||
|
||||
getState() {
|
||||
@@ -189,6 +212,8 @@ class Game {
|
||||
entrance: this.dungeon ? this.dungeon.entrance : null,
|
||||
exit: this.dungeon ? this.dungeon.exit : null,
|
||||
treasure: this.dungeon ? this.dungeon.treasure : null,
|
||||
moves: this.moveCount,
|
||||
startedAt: this.startedAt
|
||||
};
|
||||
|
||||
if (CONSOLE_LOGGING) {
|
||||
|
||||
+39
-13
@@ -54,12 +54,17 @@ class GameManager {
|
||||
async handleGameOver(socket, game, status, reason, message, score = 0) {
|
||||
try {
|
||||
const socketId = socket.id || socket;
|
||||
|
||||
|
||||
game.gameState = status;
|
||||
const gameStats = {
|
||||
score: score,
|
||||
const moves = game.moveCount || 0;
|
||||
const durationSeconds = game.startedAt ? Math.max(0, Math.round((Date.now() - game.startedAt) / 1000)) : null;
|
||||
const finalScore = score > 0 ? score : this._calculateScore(game, status, reason);
|
||||
const gameStats = {
|
||||
score: finalScore,
|
||||
reason: reason,
|
||||
treasuresFound: game.player.hasTreasure ? 1 : 0
|
||||
treasuresFound: game.player.hasTreasure ? 1 : 0,
|
||||
moves,
|
||||
durationSeconds
|
||||
};
|
||||
game.endGame(status, gameStats);
|
||||
|
||||
@@ -71,7 +76,8 @@ class GameManager {
|
||||
socketId,
|
||||
game.id,
|
||||
status === 'won',
|
||||
game.player.hasTreasure || false
|
||||
game.player.hasTreasure || false,
|
||||
{ moves, durationSeconds, score: finalScore }
|
||||
);
|
||||
|
||||
if (this.debugManager.CONSOLE_LOGGING && payoutInfo) {
|
||||
@@ -87,13 +93,15 @@ class GameManager {
|
||||
status: status,
|
||||
reason: reason,
|
||||
message: message,
|
||||
score: score,
|
||||
score: finalScore,
|
||||
moves,
|
||||
durationSeconds,
|
||||
payout: payoutInfo,
|
||||
treasure: game.player.hasTreasure || false
|
||||
});
|
||||
|
||||
// Persist completion details if DB available
|
||||
await this._updateGameRecord(game, socketId, status, reason);
|
||||
await this._updateGameRecord(game, socketId, status, reason, moves, durationSeconds);
|
||||
|
||||
// Clean up game from active games
|
||||
this.activeGames.delete(socketId);
|
||||
@@ -199,6 +207,24 @@ class GameManager {
|
||||
|
||||
// Private helper methods
|
||||
|
||||
_calculateScore(game, status, reason) {
|
||||
if (!game) return 0;
|
||||
const moves = game.moveCount || 0;
|
||||
let score = 0;
|
||||
if (status === 'won' && reason === 'escaped') {
|
||||
score += 100; // base win
|
||||
if (game.player?.hasTreasure) {
|
||||
score += 100; // treasure bonus
|
||||
}
|
||||
const moveBonus = Math.max(0, 100 - Math.max(moves - 40, 0) * 2);
|
||||
score += moveBonus;
|
||||
} else if (game.player?.hasTreasure) {
|
||||
// Reached treasure but did not escape
|
||||
score += 50;
|
||||
}
|
||||
return Math.max(0, Math.round(score));
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a database record for the game
|
||||
* @param {Object} game - Game instance
|
||||
@@ -213,8 +239,8 @@ class GameManager {
|
||||
|
||||
db.query(`
|
||||
INSERT INTO games (user_id, socket_id, game_mode, status, start_block_height, dungeon_seed, created_at)
|
||||
VALUES ((SELECT id FROM users WHERE socket_id = $1), $1, $2, 'active', $3, $4, NOW())
|
||||
`, [socketId, gameMode, blockHeight, game.id])
|
||||
VALUES ((SELECT id FROM users WHERE socket_id = $1), $2, $3, 'active', $4, $5, NOW())
|
||||
`, [socketId, socketId, gameMode, blockHeight, game.id])
|
||||
.catch(err => console.error('Game insert failed:', err.message));
|
||||
}
|
||||
}
|
||||
@@ -226,16 +252,16 @@ class GameManager {
|
||||
* @param {string} status - Final game status
|
||||
* @param {string} reason - Reason for ending
|
||||
*/
|
||||
async _updateGameRecord(game, socketId, status, reason) {
|
||||
async _updateGameRecord(game, socketId, status, reason, moves = 0, durationSeconds = null) {
|
||||
if (this.gameModeManager && this.gameModeManager.db) {
|
||||
const db = this.gameModeManager.db;
|
||||
const outcome = reason === 'escaped' ? 'escaped' : (reason === 'monster' ? 'caught_by_monster' : reason);
|
||||
|
||||
try {
|
||||
await db.query(`
|
||||
UPDATE games SET status = $1, outcome = $2, treasure_found = $3, completed_at = NOW()
|
||||
WHERE dungeon_seed = $4 AND socket_id = $5
|
||||
`, [status, outcome, game.player.hasTreasure, game.id, socketId]);
|
||||
UPDATE games SET status = $1, outcome = $2, treasure_found = $3, moves_made = $4, duration_seconds = $5, completed_at = NOW()
|
||||
WHERE dungeon_seed = $6 AND socket_id = $7
|
||||
`, [status, outcome, game.player.hasTreasure, moves, durationSeconds, game.id, socketId]);
|
||||
|
||||
if (this.debugManager.CONSOLE_LOGGING) {
|
||||
console.log(`✅ Updated game record for ${socketId}: ${status} (${outcome})`);
|
||||
|
||||
+31
-19
@@ -425,12 +425,14 @@ class GameModeManager {
|
||||
if (payoutEligibleMode && outcome === 'escaped' && game.payout_address) {
|
||||
const { amount: payoutAmount, multiplier } = this.calculatePayout(recordedMode, { treasureFound });
|
||||
if (payoutAmount > 0) {
|
||||
await this.walletService.processPayout(
|
||||
game.user_id,
|
||||
await this.walletService.processPayout({
|
||||
userId: game.user_id,
|
||||
gameId,
|
||||
game.payout_address,
|
||||
payoutAmount
|
||||
);
|
||||
address: game.payout_address,
|
||||
amount: payoutAmount,
|
||||
multiplier,
|
||||
description: `Game ${gameId} payout`
|
||||
});
|
||||
|
||||
console.log(`💸 Created payout: ${payoutAmount} atomic units for game ${gameId} (multiplier ${multiplier}x)`);
|
||||
}
|
||||
@@ -644,10 +646,10 @@ class GameModeManager {
|
||||
* @param {boolean} treasureFound - Whether treasure was found
|
||||
* @returns {object} payout / completion info
|
||||
*/
|
||||
async completeGame(socketId, gameId, won, treasureFound) {
|
||||
async completeGame(socketId, gameId, won, treasureFound, metrics = {}) {
|
||||
// For FREE mode we just return basic info
|
||||
if (this.gameMode === 'FREE') {
|
||||
return { success: true, mode: 'FREE', payout: null };
|
||||
return { success: true, mode: 'FREE', payout: null, score: metrics.score ?? null };
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -655,9 +657,16 @@ class GameModeManager {
|
||||
// If a games table exists with at least id & user reference, mark completion.
|
||||
try {
|
||||
await this.db.query(`
|
||||
UPDATE games SET status = $1, treasure_found = $2, completed_at = NOW()
|
||||
WHERE id = $3
|
||||
`, [won ? 'won' : 'lost', treasureFound, gameId]);
|
||||
UPDATE games SET status = $1, treasure_found = $2, moves_made = COALESCE($3, moves_made), duration_seconds = COALESCE($4, duration_seconds), completed_at = NOW()
|
||||
WHERE dungeon_seed = $5 AND socket_id = $6
|
||||
`, [
|
||||
won ? 'won' : 'lost',
|
||||
treasureFound,
|
||||
metrics.moves ?? null,
|
||||
metrics.durationSeconds ?? null,
|
||||
gameId,
|
||||
socketId
|
||||
]);
|
||||
} catch (e) {
|
||||
// Non-fatal if games table differs during early dev.
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
@@ -675,12 +684,14 @@ class GameModeManager {
|
||||
const userRow = userResult.rows[0];
|
||||
if (userRow && userRow.payout_address) {
|
||||
try {
|
||||
await this.walletService.processPayout(
|
||||
userRow.id,
|
||||
await this.walletService.processPayout({
|
||||
userId: userRow.id,
|
||||
gameId,
|
||||
userRow.payout_address,
|
||||
payoutAmount
|
||||
);
|
||||
address: userRow.payout_address,
|
||||
amount: payoutAmount,
|
||||
multiplier,
|
||||
description: `Game ${gameId} payout`
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
mode: this.gameMode,
|
||||
@@ -688,19 +699,20 @@ class GameModeManager {
|
||||
amount: payoutAmount,
|
||||
multiplier,
|
||||
treasure: treasureFound
|
||||
}
|
||||
},
|
||||
score: metrics.score ?? null
|
||||
};
|
||||
} catch (payoutErr) {
|
||||
const normalizedPayout = normalizeError(payoutErr, 'Failed to send payout');
|
||||
console.error('❌ Error creating payout:', normalizedPayout.message);
|
||||
return { success: true, mode: this.gameMode, payout: null, payoutError: normalizedPayout.safeMessage };
|
||||
return { success: true, mode: this.gameMode, payout: null, payoutError: normalizedPayout.safeMessage, score: metrics.score ?? null };
|
||||
}
|
||||
}
|
||||
return { success: true, mode: this.gameMode, payout: null, reason: 'No payout address' };
|
||||
return { success: true, mode: this.gameMode, payout: null, reason: 'No payout address', score: metrics.score ?? null };
|
||||
}
|
||||
|
||||
// Credits mode: decrement nothing here (already handled start). Optionally could award stats.
|
||||
return { success: true, mode: this.gameMode, payout: null };
|
||||
return { success: true, mode: this.gameMode, payout: null, score: metrics.score ?? null };
|
||||
} catch (err) {
|
||||
const normalized = normalizeError(err, 'Failed to complete game');
|
||||
console.error('❌ completeGame error:', normalized.message);
|
||||
|
||||
@@ -64,6 +64,23 @@ class MovementManager {
|
||||
this.io.to(socketId).emit('game_event', { event: moveResult.event });
|
||||
}
|
||||
}
|
||||
|
||||
emitGameUpdate(socketId) {
|
||||
const game = this.activeGames.get(socketId);
|
||||
if (!game) {
|
||||
return;
|
||||
}
|
||||
let state;
|
||||
if (typeof game.getState === 'function') {
|
||||
state = game.getState();
|
||||
} else {
|
||||
state = { player: game.player };
|
||||
}
|
||||
if (this.debugManager && typeof this.debugManager.getCurrentBlockHeight === 'function') {
|
||||
state.blockHeight = this.debugManager.getCurrentBlockHeight();
|
||||
}
|
||||
this.io.to(socketId).emit('game_update', state);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MovementManager;
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
* Handles detection, pending confirmation, validation, and persistence of payout addresses.
|
||||
* Keeps socketHandlers slimmer by encapsulating this concern.
|
||||
*/
|
||||
const { AppError, normalizeError } = require('../utils/errors');
|
||||
|
||||
const ADDRESS_REGEX = /((?:4|8)[1-9A-HJ-NP-Za-km-z]{90,110}|(?:Wo|WO|ww|WW)[0-9A-Za-z]{88,112}|W[0-9A-Za-z]{90,112})/;
|
||||
|
||||
class AddressManager {
|
||||
constructor({ gameModeManager, broadcastManager, io, debugManager, onConfirmed = null }) {
|
||||
this.gameModeManager = gameModeManager;
|
||||
@@ -15,11 +19,18 @@ class AddressManager {
|
||||
|
||||
detectInText(text) {
|
||||
if (!text || typeof text !== 'string') return null;
|
||||
const re = /((?:4|8)[0-9A-Za-z]{90,110}|Wo[0-9A-Za-z]{88,110})/; // Monero / Wownero style
|
||||
const m = text.match(re);
|
||||
if (!m) return null;
|
||||
if (m[1].length < 80) return null;
|
||||
return m[1];
|
||||
const match = text.match(ADDRESS_REGEX);
|
||||
if (!match) return null;
|
||||
const candidate = match[1];
|
||||
return this.isValidAddress(candidate) ? candidate : null;
|
||||
}
|
||||
|
||||
isValidAddress(address) {
|
||||
const value = typeof address === 'string' ? address.trim() : '';
|
||||
if (!value) return false;
|
||||
if (!ADDRESS_REGEX.test(value)) return false;
|
||||
// Basic length guard for sanity (allowing integrated addresses)
|
||||
return value.length >= 80 && value.length <= 120;
|
||||
}
|
||||
|
||||
async handleDetection(socketId, address) {
|
||||
@@ -27,7 +38,9 @@ class AddressManager {
|
||||
if (this.gameModeManager) {
|
||||
const userRow = await this.gameModeManager.getOrCreateUser(socketId);
|
||||
if (userRow && userRow.payout_address === address) {
|
||||
this.io.to(socketId).emit('address_confirmed', { address, message: 'Payout address already set.' });
|
||||
const shortAddr = address.slice(0, 10) + '…' + address.slice(-6);
|
||||
this.io.to(socketId).emit('address_confirmed', { address, message: `Payout address already set to ${shortAddr}. You can update it via the address manager.` });
|
||||
this.broadcastManager?.sendStatusUpdate(socketId, 'info', 'Address already on file. Use the address button/command to replace it.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -40,6 +53,29 @@ class AddressManager {
|
||||
this.broadcastManager?.sendStatusUpdate(socketId, 'info', 'Address detected. Type confirm or cancel.');
|
||||
}
|
||||
|
||||
async saveAddress(socketId, address, { autoConfirm = true } = {}) {
|
||||
const trimmed = typeof address === 'string' ? address.trim() : '';
|
||||
if (!trimmed) {
|
||||
throw new AppError('No payout address supplied', {
|
||||
safeMessage: 'Please enter a payout address.'
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.isValidAddress(trimmed)) {
|
||||
throw new AppError('Invalid payout address format', {
|
||||
safeMessage: 'That does not look like a valid XMR/WOW address.'
|
||||
});
|
||||
}
|
||||
|
||||
this.pending.set(socketId, trimmed);
|
||||
|
||||
if (autoConfirm) {
|
||||
await this.confirm(socketId, true);
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
async confirm(socketId, accept) {
|
||||
const pending = this.pending.get(socketId);
|
||||
if (!pending) {
|
||||
@@ -61,7 +97,8 @@ class AddressManager {
|
||||
this.broadcastManager?.sendStatusUpdate(socketId, 'success', 'Payout address saved.');
|
||||
this.io.to(socketId).emit('address_confirmed', { address: pending, message: 'Payout address saved.' });
|
||||
} catch (e) {
|
||||
this.broadcastManager?.sendStatusUpdate(socketId, 'error', 'Failed to save address. Try again.');
|
||||
const normalized = normalizeError?.(e, 'Failed to save address') || e;
|
||||
this.broadcastManager?.sendStatusUpdate(socketId, 'error', normalized.safeMessage || 'Failed to save address. Try again.');
|
||||
return;
|
||||
} finally {
|
||||
this.pending.delete(socketId);
|
||||
|
||||
@@ -102,6 +102,10 @@ class ChatHandler {
|
||||
|
||||
// Private helper methods
|
||||
|
||||
async promptAddress(socket) {
|
||||
await this._promptAddress(socket);
|
||||
}
|
||||
|
||||
async _handleAddressDetection(socket, detected) {
|
||||
// Rate limiting for address setting
|
||||
const rateLimitResult = await this.rateLimiter.checkLimit(socket.id, 'address:set');
|
||||
@@ -150,8 +154,7 @@ class ChatHandler {
|
||||
|
||||
case 'address':
|
||||
case 'payout':
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'info',
|
||||
'Please paste your XMR/WOW address directly in chat. The system will automatically detect and confirm it.');
|
||||
await this._promptAddress(socket);
|
||||
return true;
|
||||
|
||||
case 'payment':
|
||||
@@ -217,6 +220,31 @@ class ChatHandler {
|
||||
console.log(`🧹 ChatHandler cleanup: removed ${cleanedCount} old timestamps`);
|
||||
}
|
||||
}
|
||||
|
||||
async _promptAddress(socket) {
|
||||
let existing = null;
|
||||
try {
|
||||
if (this.gameModeManager) {
|
||||
const userRow = await this.gameModeManager.getOrCreateUser(socket.id);
|
||||
existing = userRow?.payout_address || null;
|
||||
}
|
||||
} catch (e) {
|
||||
if (this.debugManager?.CONSOLE_LOGGING) {
|
||||
console.warn('Address prompt lookup failed:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
socket.emit('address_prompt', {
|
||||
existingAddress: existing,
|
||||
canUpdate: true
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'info', 'Update your payout address using the address manager.');
|
||||
} else {
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'info', 'Add your payout address to receive rewards.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ChatHandler;
|
||||
@@ -15,6 +15,7 @@ const ConnectionHandler = require('./connectionHandler');
|
||||
const ChatHandler = require('./chatHandler');
|
||||
const QueueHandler = require('./queueHandler');
|
||||
const MemoryManager = require('../utils/memoryManager');
|
||||
const { normalizeError } = require('../utils/errors');
|
||||
|
||||
class SocketHandlers {
|
||||
constructor(io, activeGames, broadcastManager, debugManager, gameModeManager = null, walletService = null) {
|
||||
@@ -149,7 +150,7 @@ class SocketHandlers {
|
||||
io: this.io,
|
||||
debugManager: this.debugManager,
|
||||
moveCooldown: this.MOVE_COOLDOWN,
|
||||
postMoveHook: ({ socketId, game }) => this.afterPlayerMove(socketId, game)
|
||||
postMoveHook: ({ socketId, game, moveResult }) => this.afterPlayerMove(socketId, game, moveResult)
|
||||
});
|
||||
|
||||
// Register memory cleanup functions
|
||||
@@ -245,6 +246,10 @@ class SocketHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof game.moveMonster === 'function') {
|
||||
game.moveMonster();
|
||||
}
|
||||
|
||||
// Build updated state
|
||||
let state;
|
||||
if (typeof game.getState === 'function') {
|
||||
@@ -269,24 +274,35 @@ class SocketHandlers {
|
||||
* Called after a successful player move (before update is emitted) via MovementManager postMoveHook.
|
||||
* Handles monster chasing logic and immediate game over if monster catches player.
|
||||
*/
|
||||
afterPlayerMove(socketId, game) {
|
||||
afterPlayerMove(socketId, game, moveResult) {
|
||||
let monsterResult = null;
|
||||
|
||||
// Move monster one step toward player each player action
|
||||
if (game && typeof game.moveMonster === 'function') {
|
||||
try {
|
||||
game.moveMonster();
|
||||
monsterResult = game.moveMonster();
|
||||
} catch (e) {
|
||||
console.error('Monster move error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if monster catches player
|
||||
try {
|
||||
if (game && game.monster && game.player && game.monster.x === game.player.x && game.monster.y === game.player.y) {
|
||||
const fakeSocket = { id: socketId };
|
||||
this.handleGameOver(fakeSocket, game, 'lost', 'monster', 'You were caught by the monster!', 0);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Monster catch check error:', e);
|
||||
// If the monster caught the player, end the game immediately
|
||||
if (monsterResult && monsterResult.event === 'monster_caught') {
|
||||
const fakeSocket = { id: socketId };
|
||||
this.handleGameOver(fakeSocket, game, 'lost', 'monster', 'You were caught by the monster!', 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// If player escaped or triggered another event, handle via game manager
|
||||
if (moveResult && moveResult.event === 'escaped') {
|
||||
const fakeSocket = { id: socketId };
|
||||
this.handleGameOver(fakeSocket, game, 'won', 'escaped', 'You escaped the dungeon!', 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Re-emit state if treasure collected without auto game_update (fallback)
|
||||
if (moveResult && moveResult.event === 'treasure_found') {
|
||||
this.movementManager.emitGameUpdate(socketId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,16 +319,18 @@ class SocketHandlers {
|
||||
handleCancelEntry: (socket) => this.queueHandler.handleCancelEntry(socket),
|
||||
handleStatsRequest: (socket) => this.handleStatsRequest(socket)
|
||||
}));
|
||||
socket.on('player_move', (moveData) => this.movementManager.handleMove(socket.id, moveData));
|
||||
socket.on('player_move', (moveData) => this.movementManager.handleMove(socket.id, moveData));
|
||||
socket.on('disconnect', () => this.handleDisconnect(socket));
|
||||
socket.on('debug_ping', (data) => this.handleDebugPing(socket, data));
|
||||
socket.on('register_client', (data) => this.connectionHandler.handleRegisterClient(socket, data));
|
||||
socket.on('auto_start', () => this.handleAutoStart(socket)); // New handler for start button
|
||||
socket.on('address:prompt', () => this.handleAddressPrompt(socket));
|
||||
|
||||
// Payment system handlers
|
||||
socket.on('request_payment', (data) => this.paymentHandlers.handlePaymentRequest(socket, data));
|
||||
socket.on('check_payment_status', (data) => this.handleCheckPaymentStatus(socket, data));
|
||||
socket.on('get_user_credits', () => this.handleGetUserCredits(socket));
|
||||
socket.on('address:update', (data) => this.handleAddressUpdate(socket, data));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -408,6 +426,62 @@ class SocketHandlers {
|
||||
}
|
||||
}
|
||||
|
||||
async handleAddressPrompt(socket) {
|
||||
try {
|
||||
if (this.chatHandler && typeof this.chatHandler.promptAddress === 'function') {
|
||||
await this.chatHandler.promptAddress(socket);
|
||||
return;
|
||||
}
|
||||
|
||||
let existingAddress = null;
|
||||
if (this.gameModeManager) {
|
||||
try {
|
||||
const userRow = await this.gameModeManager.getOrCreateUser(socket.id);
|
||||
existingAddress = userRow?.payout_address || null;
|
||||
} catch (err) {
|
||||
if (this.debugManager?.CONSOLE_LOGGING) {
|
||||
console.warn('Address prompt lookup failed:', err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.io.to(socket.id).emit('address_prompt', {
|
||||
existingAddress,
|
||||
message: existingAddress ? 'Update your payout address anytime.' : 'Add a payout address to receive rewards.'
|
||||
});
|
||||
} catch (error) {
|
||||
const normalized = normalizeError(error, 'Unable to open address manager');
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'error', normalized.safeMessage || 'Address manager unavailable.');
|
||||
}
|
||||
}
|
||||
|
||||
async handleAddressUpdate(socket, data) {
|
||||
try {
|
||||
const address = typeof data?.address === 'string' ? data.address.trim() : '';
|
||||
if (!address) {
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'warning', 'Please enter a payout address before saving.');
|
||||
this.io.to(socket.id).emit('address_update_error', { message: 'Please enter a payout address before saving.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const rateLimitResult = await this.rateLimiter.checkLimit(socket.id, 'address:set');
|
||||
if (!rateLimitResult.allowed) {
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'warning',
|
||||
`Address changes are rate limited. Try again in ${Math.ceil(rateLimitResult.retryAfter / 1000)} seconds.`);
|
||||
this.io.to(socket.id).emit('address_update_error', { message: 'Address changes are temporarily rate limited.' });
|
||||
return;
|
||||
}
|
||||
|
||||
await this.rateLimiter.recordAttempt(socket.id, 'address:set');
|
||||
|
||||
await this.addressManager.saveAddress(socket.id, address);
|
||||
} catch (err) {
|
||||
const normalized = normalizeError?.(err, 'Failed to update payout address') || err;
|
||||
this.broadcastManager.sendStatusUpdate(socket.id, 'error', normalized.safeMessage || 'Failed to update payout address.');
|
||||
this.io.to(socket.id).emit('address_update_error', { message: normalized.safeMessage || 'Failed to update payout address.' });
|
||||
}
|
||||
}
|
||||
|
||||
/** Return the active socket instance by id (io.sockets.sockets Map) */
|
||||
_getLiveSocket(socketId) {
|
||||
try {
|
||||
|
||||
@@ -282,24 +282,93 @@ class WalletRPCService {
|
||||
return { processed: 0 };
|
||||
}
|
||||
|
||||
async processPayout(recipientAddress, amount, description) {
|
||||
normalizeAtomicAmount(value) {
|
||||
if (value === undefined || value === null) {
|
||||
throw new AppError('Payout amount was not provided', {
|
||||
safeMessage: 'Invalid payout amount supplied.'
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof value === 'bigint') {
|
||||
if (value <= 0n) {
|
||||
throw new AppError('Payout amount must be greater than zero', {
|
||||
safeMessage: 'Invalid payout amount supplied.'
|
||||
});
|
||||
}
|
||||
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
||||
return value.toString();
|
||||
}
|
||||
return Number(value);
|
||||
}
|
||||
|
||||
const num = Number(value);
|
||||
if (!Number.isFinite(num) || num <= 0) {
|
||||
throw new AppError('Payout amount must be a positive number', {
|
||||
safeMessage: 'Invalid payout amount supplied.'
|
||||
});
|
||||
}
|
||||
|
||||
const truncated = Math.trunc(num);
|
||||
if (truncated <= 0) {
|
||||
throw new AppError('Payout amount rounded below minimum atomic unit', {
|
||||
safeMessage: 'Invalid payout amount supplied.'
|
||||
});
|
||||
}
|
||||
|
||||
return truncated;
|
||||
}
|
||||
|
||||
async processPayout({
|
||||
address,
|
||||
amount,
|
||||
userId = null,
|
||||
gameId = null,
|
||||
description = null,
|
||||
multiplier = null,
|
||||
subaddressIndex = null
|
||||
} = {}) {
|
||||
try {
|
||||
// Send payment to user
|
||||
const response = await this.rpcCall('transfer', {
|
||||
if (!address || typeof address !== 'string') {
|
||||
throw new AppError('Payout address was not provided or invalid', {
|
||||
safeMessage: 'Invalid payout address supplied.'
|
||||
});
|
||||
}
|
||||
|
||||
const normalizedAmount = this.normalizeAtomicAmount(amount);
|
||||
|
||||
const transferParams = {
|
||||
destinations: [{
|
||||
amount: amount,
|
||||
address: recipientAddress
|
||||
amount: normalizedAmount,
|
||||
address
|
||||
}],
|
||||
account_index: this.accountIndex,
|
||||
priority: 1, // Normal priority
|
||||
priority: 1,
|
||||
get_tx_key: true
|
||||
});
|
||||
};
|
||||
|
||||
if (Number.isInteger(subaddressIndex) && subaddressIndex >= 0) {
|
||||
transferParams.subaddr_indices = [subaddressIndex];
|
||||
}
|
||||
|
||||
if (this.debugManager?.CONSOLE_LOGGING) {
|
||||
console.log(`💸 Payout sent:`, {
|
||||
to: recipientAddress.substring(0, 10) + '...',
|
||||
amount: amount,
|
||||
txHash: response.result.tx_hash
|
||||
console.log('🚀 Initiating payout transfer', {
|
||||
to: address.substring(0, 10) + '...',
|
||||
amount: normalizedAmount,
|
||||
userId,
|
||||
gameId,
|
||||
multiplier,
|
||||
description
|
||||
});
|
||||
}
|
||||
|
||||
const response = await this.rpcCall('transfer', transferParams);
|
||||
|
||||
if (this.debugManager?.CONSOLE_LOGGING) {
|
||||
console.log('💸 Payout sent successfully', {
|
||||
to: address.substring(0, 10) + '...',
|
||||
amount: normalizedAmount,
|
||||
txHash: response.result.tx_hash,
|
||||
fee: response.result.fee
|
||||
});
|
||||
}
|
||||
|
||||
@@ -307,14 +376,19 @@ class WalletRPCService {
|
||||
success: true,
|
||||
txHash: response.result.tx_hash,
|
||||
txKey: response.result.tx_key,
|
||||
fee: response.result.fee
|
||||
fee: response.result.fee,
|
||||
userId,
|
||||
gameId,
|
||||
amount: normalizedAmount
|
||||
};
|
||||
} catch (error) {
|
||||
const wrapped = error instanceof AppError ? error : new ExternalServiceError('Failed to process payout', {
|
||||
safeMessage: 'Unable to send payout at this time.',
|
||||
cause: error
|
||||
});
|
||||
console.error('❌ Failed to process payout:', wrapped.message);
|
||||
|
||||
const meta = { userId, gameId, address: address?.substring?.(0, 10) + '...', amount };
|
||||
console.error('❌ Failed to process payout:', wrapped.message, meta);
|
||||
throw wrapped;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user