horseRacing

  • community
  • minigames
  • by Limon
  • Takaro main
  • all
Version
View export JSON

A virtual horse racing system where players can bet and win currency.

Configuration 3

Settings you fill in when installing the module on a server.

SettingTypeDefaultDescription
minBet minBet number 50 Minimum amount required to place a bet
maxBet maxBet number 1000 Maximum amount allowed for a bet
Horses Horses array ["Thunder; 2","Lightning; 3","Shadow; 3","Blaze; 4","Seacove; 5","Arrow; 5","Dark Knight; 6"] List of horses that can race. Format: HorseName; Odds (whole number) - one horse per line
Raw config schema
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": [],
  "additionalProperties": false,
  "properties": {
    "minBet": {
      "title": "minBet",
      "description": "Minimum amount required to place a bet",
      "default": 50,
      "type": "number",
      "minimum": 1
    },
    "maxBet": {
      "title": "maxBet",
      "description": "Maximum amount allowed for a bet",
      "default": 1000,
      "type": "number",
      "minimum": 1
    },
    "Horses": {
      "title": "Horses",
      "description": "List of horses that can race. Format: HorseName; Odds (whole number) - one horse per line",
      "default": [
        "Thunder; 2",
        "Lightning; 3",
        "Shadow; 3",
        "Blaze; 4",
        "Seacove; 5",
        "Arrow; 5",
        "Dark Knight; 6"
      ],
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  }
}
Raw UI schema
{}

Commands 8

Chat commands players trigger in game.

  • horses

    No help text available

    Command source
    import { data, TakaroUserError } from '@takaro/helpers';
    import { parseHorses, getRaceData, getTimeUntilRace } from './utils.js';
    
    async function main() {
        try {
            const { player, module: mod, gameServerId } = data;
    
            // Parse horses using the utility function
            const horses = parseHorses(mod.userConfig);
    
            if (horses.length === 0) {
                throw new TakaroUserError('No horses are configured for racing!');
            }
    
            // Get current race info
            const raceData = await getRaceData(gameServerId, mod.moduleId);
            const timeUntil = getTimeUntilRace(raceData.nextRaceTime);
    
            await player.pm('🏇 HORSE RACING INFORMATION 🏇');
            await player.pm('═══════════════════════════════════');
            await player.pm(`🏁 Next Race: #${raceData.raceNumber} in ${timeUntil}`);
            await player.pm(`🎫 Current Bets: ${raceData.bets.length}`);
            await player.pm('🐎 AVAILABLE HORSES:');
    
            // Show horses with enhanced info
            for (let i = 0; i < horses.length; i++) {
                const horse = horses[i];
                const betsOnHorse = raceData.bets.filter(bet => 
                    bet.horse.toLowerCase() === horse.name.toLowerCase()).length;
                
                let horseLine = `  ${i + 1}. ${horse.name} - ${horse.odds}:1 odds`;
                if (betsOnHorse > 0) {
                    horseLine += ` (${betsOnHorse} bet${betsOnHorse !== 1 ? 's' : ''})`;
                }
                
                // Add performance indicator
                if (horse.odds <= 2) {
                    horseLine += ' ⭐ Favorite';
                } else if (horse.odds >= 6) {
                    horseLine += ' 🎯 Longshot';
                }
                
                await player.pm(horseLine);
            }
    
            const minBet = mod.userConfig?.minBet || 50;
            const maxBet = mod.userConfig?.maxBet || 1000;
    
            await player.pm('📋 BETTING INFORMATION:');
            await player.pm(`💰 Bet Range: ${minBet} - ${maxBet} currency`);
            await player.pm('🎯 Command: /horsebet <horse> <amount>');
            await player.pm('📊 Example: /horsebet Thunder 100');
    
            // Show current player's bets if any
            const playerBets = raceData.bets.filter(bet => bet.playerId === player.id);
            if (playerBets.length > 0) {
                await player.pm('🎫 YOUR CURRENT BETS:');
                for (const bet of playerBets) {
                    const potentialWin = Math.floor(bet.amount * bet.odds);
                    await player.pm(`  ${bet.horse}: ${bet.amount} → potential win: ${potentialWin}`);
                }
            }
    
            // Show helpful tips
            await player.pm('💡 TIP: Lower odds = higher chance to win, but lower payout');
            await player.pm('🎰 Special jackpot races trigger randomly for extra prizes!');
    
        } catch (error) {
            if (error instanceof TakaroUserError) {
                throw error;
            }
            console.log('Error in horses command:', error);
            throw new TakaroUserError('Unable to load horse information. Please try again.');
        }
    }
    
    await main();
  • horseBet

    Name of the horse you want to bet on

    ArgumentTypeDefaultHelp
    horse string Name of the horse you want to bet on
    amount string Amount of currency to bet
    Command source
    import { takaro, data, checkPermission, TakaroUserError } from '@takaro/helpers';
    import { parseHorses, getRaceData } from './utils.js';
    
    async function main() {
        const { player, gameServerId, module: mod, pog, arguments: args } = data;
    
        // Check permission
        if (!checkPermission(pog, 'HORSE_RACING_BET')) {
            throw new TakaroUserError('You do not have permission to place bets.');
        }
    
        // Validate arguments
        if (!args.horse) {
            throw new TakaroUserError('Please specify the horse name you want to bet on.');
        }
    
        const betAmount = parseInt(args.amount);
        const minBet = mod.userConfig?.minBet || 50;
        const maxBet = mod.userConfig?.maxBet || 1000;
    
        if (!betAmount || betAmount < minBet || betAmount > maxBet) {
            throw new TakaroUserError(`Please specify a valid amount to bet (min: ${minBet}, max: ${maxBet}).`);
        }
    
        try {
            // Get horse list using utility function
            const horses = parseHorses(mod.userConfig);
    
            // Find the horse
            const horseName = args.horse.toLowerCase().trim();
            const horseMatch = horses.find(h => h.name.toLowerCase() === horseName);
    
            if (!horseMatch) {
                const availableHorses = horses.map(h => h.name).join(', ');
                throw new TakaroUserError(`Horse "${args.horse}" not found. Available horses: ${availableHorses}`);
            }
    
            // Check if player has enough currency
            const playerData = await takaro.playerOnGameserver.playerOnGameServerControllerGetOne(gameServerId, player.id);
            const balance = playerData.data.data.currency;
    
            if (balance < betAmount) {
                throw new TakaroUserError(`You don't have enough currency. Your balance: ${balance}`);
            }
    
            // Get race data using utility function
            const raceData = await getRaceData(gameServerId, mod.moduleId);
    
            // Check if player already has a bet
            const existingBetIndex = raceData.bets.findIndex(bet => bet.playerId === player.id);
    
            if (existingBetIndex >= 0) {
                // Check if it's on the same horse
                if (raceData.bets[existingBetIndex].horse.toLowerCase() === horseMatch.name.toLowerCase()) {
                    throw new TakaroUserError(`You already have a bet on ${horseMatch.name}. Use /myhorsebets to see your bets.`);
                }
    
                // Refund previous bet
                const oldBet = raceData.bets[existingBetIndex];
                await takaro.playerOnGameserver.playerOnGameServerControllerAddCurrency(
                    gameServerId,
                    player.id,
                    { currency: oldBet.amount }
                );
    
                // Remove from array
                raceData.bets.splice(existingBetIndex, 1);
                await player.pm(`Refunded your previous bet of ${oldBet.amount} on ${oldBet.horse}`);
            }
    
            // Deduct currency from player
            await takaro.playerOnGameserver.playerOnGameServerControllerDeductCurrency(
                gameServerId,
                player.id,
                { currency: betAmount }
            );
    
            // Create new bet
            raceData.bets.push({
                playerId: player.id,
                playerName: player.name,
                horse: horseMatch.name,
                amount: betAmount,
                odds: horseMatch.odds,
                placedAt: Date.now()
            });
    
            // Update race data
            const currentRace = await takaro.variable.variableControllerSearch({
                filters: {
                    key: ['current_race'],
                    gameServerId: [gameServerId],
                    moduleId: [mod.moduleId],
                },
            });
    
            await takaro.variable.variableControllerUpdate(currentRace.data.data[0].id, {
                value: JSON.stringify(raceData),
            });
    
            // Calculate potential winnings
            const potentialWin = Math.floor(betAmount * horseMatch.odds);
            
            await player.pm(`✅ Bet placed: ${betAmount} on ${horseMatch.name} (${horseMatch.odds}:1 odds)`);
            await player.pm(`💰 Potential winnings: ${potentialWin}`);
    
            // Show time until race
            const timeUntilRace = Math.max(0, raceData.nextRaceTime - Date.now());
            if (timeUntilRace > 0) {
                const minutesUntilRace = Math.floor(timeUntilRace / 60000);
                if (minutesUntilRace > 0) {
                    await player.pm(`⏰ Next race (#${raceData.raceNumber}) in approximately ${minutesUntilRace} minutes`);
                } else {
                    await player.pm(`⏰ Next race (#${raceData.raceNumber}) starting soon!`);
                }
            }
    
            // Show total bets for this race
            await player.pm(`📊 Total bets for this race: ${raceData.bets.length}`);
    
        } catch (error) {
            if (error instanceof TakaroUserError) {
                throw error;
            }
            console.log('Error in horsebet command:', error);
            throw new TakaroUserError('Something went wrong placing your bet. Please try again.');
        }
    }
    
    await main();
  • mybets

    No help text available

    Command source
    import { data, TakaroUserError } from '@takaro/helpers';
    import { getRaceData } from './utils.js';
    
    async function main() {
        try {
            const { player, module: mod, gameServerId } = data;
    
            const raceData = await getRaceData(gameServerId, mod.moduleId);
    
            // Check if player has bets
            const playerBets = raceData.bets.filter(bet => bet.playerId === player.id);
    
            if (playerBets.length === 0) {
                await player.pm('You have not placed any bets for the upcoming race.');
                await player.pm('Use /horsebet <horse> <amount> to place a bet!');
                return;
            }
    
            await player.pm(`🏇 Your bets for Race #${raceData.raceNumber}:`);
    
            let totalPotential = 0;
            for (const bet of playerBets) {
                const potential = Math.floor(bet.amount * bet.odds);
                totalPotential += potential;
                await player.pm(`${bet.horse} - ${bet.amount} (potential win: ${potential})`);
            }
    
            await player.pm(`Total potential winnings: ${totalPotential}`);
        } catch (error) {
            console.log('Error in myBets command:', error);
            throw new TakaroUserError('Something went wrong. Please try again.');
        }
    }
    
    await main();
  • startrace

    Immediately starts a horse race with current bets

    Command source
    import { takaro, data, checkPermission, TakaroUserError } from '@takaro/helpers';
    import { parseHorses, simulateRace } from './utils.js';
    
    async function main() {
        const { gameServerId, module: mod, player, pog } = data;
    
        // Check admin permission
        if (!checkPermission(pog, 'HORSE_RACING_ADMIN')) {
            throw new TakaroUserError('You need admin permissions to manually start races.');
        }
    
        try {
            // Get race data
            const currentRace = await takaro.variable.variableControllerSearch({
                filters: {
                    key: ['current_race'],
                    gameServerId: [gameServerId],
                    moduleId: [mod.moduleId],
                },
            });
    
            let raceData;
            if (currentRace.data.data.length === 0) {
                // Initialize race data if it doesn't exist
                raceData = {
                    nextRaceTime: Date.now() + 3600000,
                    bets: [],
                    lastRaceResults: null,
                    raceNumber: 1,
                    state: 'waiting'
                };
    
                await takaro.variable.variableControllerCreate({
                    key: 'current_race',
                    value: JSON.stringify(raceData),
                    gameServerId,
                    moduleId: mod.moduleId,
                });
            } else {
                raceData = JSON.parse(currentRace.data.data[0].value);
            }
    
            // Check if no bets
            if (!raceData.bets || raceData.bets.length === 0) {
                throw new TakaroUserError('Cannot start race - no bets have been placed!');
            }
    
            // Parse horses and simulate race
            const horses = parseHorses(mod.userConfig);
            const raceResults = simulateRace(horses);
    
            // Store race results
            raceData.results = raceResults;
            raceData.state = 'complete';
    
            // Get winner
            const winningHorse = raceResults[0].name;
            const winningBets = raceData.bets.filter(bet => 
                bet.horse.toLowerCase() === winningHorse.toLowerCase());
    
            // Announce race and results immediately
            await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                message: `🏇 MANUAL RACE #${raceData.raceNumber} - The horses are off!`
            });
    
            // Small delay for drama
            await new Promise(resolve => setTimeout(resolve, 3000));
    
            await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                message: `🏆 ${winningHorse} WINS Race #${raceData.raceNumber}!`
            });
    
            // Process winnings
            let totalPayout = 0;
            if (winningBets.length > 0) {
                for (const bet of winningBets) {
                    const winnings = Math.floor(bet.amount * bet.odds);
                    totalPayout += winnings;
    
                    try {
                        await takaro.playerOnGameserver.playerOnGameServerControllerAddCurrency(
                            gameServerId,
                            bet.playerId,
                            { currency: winnings }
                        );
                    } catch (error) {
                        console.log(`Error processing win for player ${bet.playerName}:`, error);
                    }
                }
    
                await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                    message: `💰 ${winningBets.length} winner${winningBets.length !== 1 ? 's' : ''} received ${totalPayout} total currency!`
                });
            } else {
                await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                    message: "💸 No one bet on the winning horse. House wins!"
                });
            }
    
            // Save race results for history
            raceData.lastRaceResults = {
                raceNumber: raceData.raceNumber,
                results: raceResults,
                winners: winningBets,
                totalPayout: totalPayout,
                timestamp: Date.now()
            };
    
            // Setup next race
            raceData.raceNumber++;
            raceData.nextRaceTime = Date.now() + (2 * 60 * 60 * 1000);
            raceData.bets = [];
            raceData.state = 'waiting';
    
            // Update race data
            if (currentRace.data.data.length === 0) {
                await takaro.variable.variableControllerCreate({
                    key: 'current_race',
                    value: JSON.stringify(raceData),
                    gameServerId,
                    moduleId: mod.moduleId,
                });
            } else {
                await takaro.variable.variableControllerUpdate(currentRace.data.data[0].id, {
                    value: JSON.stringify(raceData),
                });
            }
    
            await player.pm(`Race #${raceData.raceNumber - 1} completed successfully. Next race scheduled for 2 hours from now.`);
    
        } catch (error) {
            if (error instanceof TakaroUserError) {
                throw error;
            }
            console.log('Error in manual startrace:', error);
            throw new TakaroUserError('Failed to start race. Check logs for details.');
        }
    }
    
    await main();
  • horseLeaderboard

    View top bettors by winnings and win rate

    Command source
    import { takaro, data, TakaroUserError } from '@takaro/helpers';
    
    async function main() {
        const { player, gameServerId, module: mod } = data;
    
        try {
            // Get all player statistics
            const statsSearch = await takaro.variable.variableControllerSearch({
                filters: {
                    key: ['horse_racing_stats'],
                    gameServerId: [gameServerId],
                    moduleId: [mod.moduleId],
                },
            });
    
            if (statsSearch.data.data.length === 0) {
                throw new TakaroUserError('No betting statistics available yet. Place some bets and race to build up the leaderboard!');
            }
    
            // Parse all player stats
            const allStats = [];
            for (const statVar of statsSearch.data.data) {
                try {
                    const stats = JSON.parse(statVar.value);
                    const playerInfo = await takaro.playerOnGameserver.playerOnGameServerControllerGetOne(gameServerId, statVar.playerId);
                    
                    allStats.push({
                        playerId: statVar.playerId,
                        playerName: playerInfo.data.data.player.name,
                        totalWinnings: stats.totalWinnings || 0,
                        totalBets: stats.totalBets || 0,
                        totalWagered: stats.totalWagered || 0,
                        wins: stats.wins || 0,
                        losses: stats.losses || 0,
                        biggestWin: stats.biggestWin || 0,
                        favoriteHorse: stats.favoriteHorse || 'None'
                    });
                } catch (error) {
                    console.log('Error parsing player stats:', error);
                    continue;
                }
            }
    
            if (allStats.length === 0) {
                throw new TakaroUserError('No valid betting statistics found.');
            }
    
            // Sort by total winnings (descending)
            allStats.sort((a, b) => b.totalWinnings - a.totalWinnings);
    
            await player.pm('🏆 HORSE RACING LEADERBOARD 🏆');
            await player.pm('═══════════════════════════════');
    
            // Show top 10
            const topPlayers = allStats.slice(0, 10);
            for (let i = 0; i < topPlayers.length; i++) {
                const p = topPlayers[i];
                const winRate = p.totalBets > 0 ? Math.round((p.wins / p.totalBets) * 100) : 0;
                const roi = p.totalWagered > 0 ? Math.round(((p.totalWinnings - p.totalWagered) / p.totalWagered) * 100) : 0;
                
                await player.pm(`${i + 1}. ${p.playerName}`);
                await player.pm(`   💰 Net: ${p.totalWinnings - p.totalWagered} | Wins: ${p.wins}/${p.totalBets} (${winRate}%)`);
                if (i < 3) { // Show extra details for top 3
                    await player.pm(`   🎯 ROI: ${roi}% | Biggest Win: ${p.biggestWin} | Fav: ${p.favoriteHorse}`);
                }
            }
    
            // Show current player's position if not in top 10
            const playerStats = allStats.find(s => s.playerId === player.id);
            if (playerStats) {
                const playerRank = allStats.findIndex(s => s.playerId === player.id) + 1;
                if (playerRank > 10) {
                    const winRate = playerStats.totalBets > 0 ? Math.round((playerStats.wins / playerStats.totalBets) * 100) : 0;
                    await player.pm(`Your rank: #${playerRank}`);
                    await player.pm(`Your stats: ${playerStats.wins}/${playerStats.totalBets} wins (${winRate}%) | Net: ${playerStats.totalWinnings - playerStats.totalWagered}`);
                }
            } else {
                await player.pm('You haven\'t placed any bets yet! Use /horsebet to get started.');
            }
    
        } catch (error) {
            if (error instanceof TakaroUserError) {
                throw error;
            }
            console.log('Error in horse leaderboard:', error);
            throw new TakaroUserError('Failed to load leaderboard. Please try again.');
        }
    }
    
    await main();
  • nextRace

    No help text available

    Command source
    import { data, TakaroUserError } from '@takaro/helpers';
    import { getRaceData, getTimeUntilRace } from './utils.js';
    
    async function main() {
        try {
            const { player, module: mod, gameServerId } = data;
    
            const raceData = await getRaceData(gameServerId, mod.moduleId);
            const timeUntil = getTimeUntilRace(raceData.nextRaceTime);
    
            await player.pm(`🏇 Race #${raceData.raceNumber} will begin in ${timeUntil}.`);
            await player.pm(`${raceData.bets.length} bet${raceData.bets.length !== 1 ? 's' : ''} have been placed so far.`);
    
            // Check if player has bets
            const playerBets = raceData.bets.filter(bet => bet.playerId === player.id);
            if (playerBets.length > 0) {
                await player.pm('Your bets:');
                for (const bet of playerBets) {
                    await player.pm(`${bet.horse} - ${bet.amount} (potential win: ${Math.floor(bet.amount * bet.odds)})`);
                }
            } else {
                await player.pm('You have not placed any bets yet. Use /horsebet <horse> <amount> to place a bet!');
            }
        } catch (error) {
            console.log('Error in nextRace command:', error);
            throw new TakaroUserError('Something went wrong. Please try again.');
        }
    }
    
    await main();
  • lastRace

    No help text available

    Command source
    import { data, TakaroUserError } from '@takaro/helpers';
    import { getRaceData } from './utils.js';
    
    async function main() {
        try {
            const { player, module: mod, gameServerId } = data;
    
            const raceData = await getRaceData(gameServerId, mod.moduleId);
    
            if (!raceData.lastRaceResults) {
                throw new TakaroUserError('No previous race results found. Wait for a race to finish!');
            }
    
            const results = raceData.lastRaceResults;
            const raceDate = new Date(results.timestamp).toLocaleString();
    
            await player.pm(`🏇 RACE #${results.raceNumber} RESULTS`);
            await player.pm('═══════════════════════════════');
            await player.pm(`📅 Completed: ${raceDate}`);
    
            // Show top 5 finishing positions
            const topHorses = results.results.slice(0, Math.min(5, results.results.length));
            await player.pm('🏁 FINAL STANDINGS:');
            for (let i = 0; i < topHorses.length; i++) {
                const position = i + 1;
                const medal = position === 1 ? '🥇' : position === 2 ? '🥈' : position === 3 ? '🥉' : `${position}.`;
                await player.pm(`  ${medal} ${topHorses[i].name}`);
            }
    
            // Show payout information
            if (results.winners.length > 0) {
                await player.pm(`💰 PAYOUTS: ${results.winners.length} winner${results.winners.length !== 1 ? 's' : ''} - ${results.totalPayout} total currency paid out`);
                
                // Show jackpot if there was one
                if (results.jackpot && results.jackpot > 0) {
                    await player.pm(`🎰 Jackpot bonus: ${results.jackpot} currency!`);
                }
    
                // Check if current player won
                const playerWin = results.winners.find(bet => bet.playerId === player.id);
                if (playerWin) {
                    let winnings = Math.floor(playerWin.amount * playerWin.odds);
                    if (results.jackpot) {
                        winnings += Math.floor(results.jackpot / results.winners.length);
                    }
                    await player.pm(`🎉 YOU WON ${winnings} with your ${playerWin.amount} bet on ${playerWin.horse}!`);
                } else {
                    // Check if player had any losing bets
                    const playerBets = results.winners.filter(bet => bet.playerId === player.id);
                    if (playerBets.length === 0) {
                        await player.pm('🤷‍♂️ You didn\'t place any bets in this race.');
                    } else {
                        await player.pm('😔 Your horse didn\'t win this time. Better luck next race!');
                    }
                }
            } else {
                await player.pm('💸 No one bet on the winning horse - house wins!');
            }
    
            // Show race statistics
            const totalBets = results.winners.length;
            await player.pm(`📊 Race Stats: ${totalBets} total bet${totalBets !== 1 ? 's' : ''} placed`);
    
        } catch (error) {
            if (error instanceof TakaroUserError) {
                throw error;
            }
            console.log('Error in lastRace command:', error);
            throw new TakaroUserError('Unable to retrieve race results. Please try again.');
        }
    }
    
    await main();
  • horseStats

    Shows your betting history, win rate, and favorite horses

    Command source
    import { takaro, data, TakaroUserError } from '@takaro/helpers';
    
    async function main() {
        const { player, gameServerId, module: mod } = data;
    
        try {
            // Get player statistics
            const statsSearch = await takaro.variable.variableControllerSearch({
                filters: {
                    key: ['horse_racing_stats'],
                    gameServerId: [gameServerId],
                    moduleId: [mod.moduleId],
                    playerId: [player.id],
                },
            });
    
            if (statsSearch.data.data.length === 0) {
                throw new TakaroUserError('You haven\'t placed any bets yet! Use /horsebet <horse> <amount> to get started.');
            }
    
            const stats = JSON.parse(statsSearch.data.data[0].value);
            
            // Calculate derived stats
            const winRate = stats.totalBets > 0 ? Math.round((stats.wins / stats.totalBets) * 100) : 0;
            const netProfit = stats.totalWinnings - stats.totalWagered;
            const roi = stats.totalWagered > 0 ? Math.round((netProfit / stats.totalWagered) * 100) : 0;
    
            await player.pm('📊 YOUR HORSE RACING STATISTICS 📊');
            await player.pm('═══════════════════════════════════');
            await player.pm(`🎯 Win Rate: ${stats.wins}/${stats.totalBets} (${winRate}%)`);
            await player.pm(`💰 Net Profit: ${netProfit}`);
            await player.pm(`📈 ROI: ${roi}%`);
            await player.pm(`🏆 Biggest Win: ${stats.biggestWin}`);
            await player.pm(`💎 Total Wagered: ${stats.totalWagered}`);
            await player.pm(`🐎 Favorite Horse: ${stats.favoriteHorse}`);
    
            // Show per-horse breakdown if available
            if (stats.horseStats && Object.keys(stats.horseStats).length > 0) {
                await player.pm('🐎 PER-HORSE BREAKDOWN:');
                
                // Sort horses by most bet on
                const horseEntries = Object.entries(stats.horseStats)
                    .sort(([,a], [,b]) => b.bets - a.bets)
                    .slice(0, 5); // Show top 5
    
                for (const [horseName, horseStats] of horseEntries) {
                    const horseWinRate = horseStats.bets > 0 ? Math.round((horseStats.wins / horseStats.bets) * 100) : 0;
                    await player.pm(`  ${horseName}: ${horseStats.wins}/${horseStats.bets} wins (${horseWinRate}%) - ${horseStats.totalWagered} wagered`);
                }
            }
    
            // Performance category
            let category = '🔰 Novice Bettor';
            if (stats.totalBets >= 50 && winRate >= 30) {
                category = '🥇 Professional Gambler';
            } else if (stats.totalBets >= 20 && winRate >= 25) {
                category = '🥈 Experienced Bettor';
            } else if (stats.totalBets >= 10) {
                category = '🥉 Regular Bettor';
            }
            
            await player.pm(`🏷️  Status: ${category}`);
    
            // Give tips based on performance
            if (winRate < 20 && stats.totalBets > 5) {
                await player.pm('💡 Tip: Try betting on horses with lower odds for better chances!');
            } else if (roi > 50) {
                await player.pm('🎉 Outstanding performance! You\'re a horse racing champion!');
            }
    
        } catch (error) {
            if (error instanceof TakaroUserError) {
                throw error;
            }
            console.log('Error in horseStats command:', error);
            throw new TakaroUserError('Failed to load your statistics. Please try again.');
        }
    }
    
    await main();

Hooks 1

Code that runs in reaction to a game or Takaro event.

  • test

    Hook for log events

    Hook source
    import { data, takaro } from '@takaro/helpers';
    async function main() {
        const {} = data;
    }
    await main();

Cron jobs 2

Work the module runs on a schedule.

  • announceRace

    Cron job source
    import { takaro, data } from '@takaro/helpers';
    
    async function main() {
        try {
            const { gameServerId, module: mod } = data;
    
            // Get online players
            const onlinePlayers = await takaro.playerOnGameserver.playerOnGameServerControllerSearch({
                filters: {
                    gameServerId: [gameServerId],
                    online: [true]
                }
            });
    
            // Skip if no one is online
            if (onlinePlayers.data.meta.total === 0) {
                return true;
            }
    
            // Get race data
            const currentRace = await takaro.variable.variableControllerSearch({
                filters: {
                    key: ['current_race'],
                    gameServerId: [gameServerId],
                    moduleId: [mod.moduleId],
                },
            });
    
            let raceData;
            if (currentRace.data.data.length === 0) {
                // Initialize race data if it doesn't exist
                raceData = {
                    nextRaceTime: Date.now() + 3600000, // 1 hour from now
                    bets: [],
                    lastRaceResults: null,
                    raceNumber: 1,
                    state: 'waiting'
                };
    
                await takaro.variable.variableControllerCreate({
                    key: 'current_race',
                    value: JSON.stringify(raceData),
                    gameServerId,
                    moduleId: mod.moduleId,
                });
            } else {
                raceData = JSON.parse(currentRace.data.data[0].value);
            }
    
            // Get horse list
            let horses = [];
            if (mod.userConfig?.Horses && Array.isArray(mod.userConfig.Horses)) {
                horses = mod.userConfig.Horses.map(horseStr => {
                    const [name, oddsStr] = horseStr.split(';').map(s => s.trim());
                    return `${name} (${oddsStr}x)`;
                });
            } else {
                horses = [
                    "Thunder (2x)",
                    "Lightning (3x)",
                    "Shadow (3x)",
                    "Blaze (4x)",
                    "Seacove (5x)",
                    "Arrow (5x)",
                    "Dark Knight (6x)"
                ];
            }
    
            // Announce upcoming race
            await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                message: `🏇 Attention! Horse Race #${raceData.raceNumber} will begin in 5 minutes!`
            });
    
            await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                message: `Available horses: ${horses.join(', ')}`
            });
    
            await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                message: `Place your bets with /horsebet <horse> <amount> (min: 50, max: 1000)`
            });
    
            await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                message: `${raceData.bets.length} bet${raceData.bets.length !== 1 ? 's' : ''} placed so far!`
            });
    
            return true;
        } catch (error) {
            console.log('Error in announceRace cronjob:', error);
            return true;
        }
    }
    
    await main();
  • runRace

    Cron job source
    import { takaro, data } from '@takaro/helpers';
    import { parseHorses, simulateRace, getRaceCommentary, updatePlayerStats, checkAndCreateJackpot } from './utils.js';
    
    async function main() {
        try {
            const { gameServerId, module: mod } = data;
    
            // Get online players
            const onlinePlayers = await takaro.playerOnGameserver.playerOnGameServerControllerSearch({
                filters: {
                    gameServerId: [gameServerId],
                    online: [true]
                }
            });
    
            // Skip race if no one is online
            if (onlinePlayers.data.meta.total === 0) {
                return true;
            }
    
            // Get race data
            const currentRace = await takaro.variable.variableControllerSearch({
                filters: {
                    key: ['current_race'],
                    gameServerId: [gameServerId],
                    moduleId: [mod.moduleId],
                },
            });
    
            let raceData;
            if (currentRace.data.data.length === 0) {
                // Initialize race data if it doesn't exist
                raceData = {
                    nextRaceTime: Date.now() + 3600000, // 1 hour from now
                    bets: [],
                    lastRaceResults: null,
                    raceNumber: 1,
                    state: 'waiting'
                };
    
                await takaro.variable.variableControllerCreate({
                    key: 'current_race',
                    value: JSON.stringify(raceData),
                    gameServerId,
                    moduleId: mod.moduleId,
                });
            } else {
                raceData = JSON.parse(currentRace.data.data[0].value);
            }
    
            // If no bets, just announce and set next race
            if (!raceData.bets || raceData.bets.length === 0) {
                await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                    message: "🏇 The race track is quiet... No bets have been placed! Next race in 2 hours."
                });
    
                raceData.nextRaceTime = Date.now() + (2 * 60 * 60 * 1000);
                
                await takaro.variable.variableControllerUpdate(currentRace.data.data[0].id, {
                    value: JSON.stringify(raceData),
                });
    
                return true;
            }
    
            // Check for jackpot
            const totalBetsAmount = raceData.bets.reduce((sum, bet) => sum + bet.amount, 0);
            const jackpotInfo = await checkAndCreateJackpot(gameServerId, mod.moduleId, totalBetsAmount);
    
            // Parse horses and simulate race
            const horses = parseHorses(mod.userConfig);
            const raceResults = simulateRace(horses);
    
            // Get winner
            const winningHorse = raceResults[0].name;
            const winningBets = raceData.bets.filter(bet => 
                bet.horse.toLowerCase() === winningHorse.toLowerCase());
    
            // RACE PROGRESSION SEQUENCE - No setTimeout, just progressive messages
            await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                message: `🏁 RACE #${raceData.raceNumber} IS STARTING! 🏁`
            });
    
            await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                message: getRaceCommentary('start', horses, raceResults)
            });
    
            if (jackpotInfo.isJackpot) {
                await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                    message: `🎰 JACKPOT RACE! Winner gets an extra ${jackpotInfo.amount} currency!`
                });
            }
    
            // Show race positions as it progresses
            await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                message: `🐎 Early positions: ${raceResults[0].name} leads, followed by ${raceResults[1].name} and ${raceResults[2].name}!`
            });
    
            await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                message: getRaceCommentary('midRace', horses, raceResults)
            });
    
            await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                message: `⚡ Final stretch! ${raceResults[0].name} is pulling ahead, ${raceResults[1].name} charging hard behind!`
            });
    
            await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                message: getRaceCommentary('finish', horses, raceResults)
            });
    
            // Show final standings
            await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                message: `🏆 FINAL RESULTS: 1st: ${raceResults[0].name} 🥇 | 2nd: ${raceResults[1].name} 🥈 | 3rd: ${raceResults[2].name} 🥉`
            });
    
            // Process winnings and update stats
            let totalPayout = 0;
            if (winningBets.length > 0) {
                for (const bet of winningBets) {
                    let winnings = Math.floor(bet.amount * bet.odds);
                    
                    // Add jackpot bonus
                    if (jackpotInfo.isJackpot) {
                        winnings += Math.floor(jackpotInfo.amount / winningBets.length);
                    }
                    
                    totalPayout += winnings;
    
                    try {
                        await takaro.playerOnGameserver.playerOnGameServerControllerAddCurrency(
                            gameServerId,
                            bet.playerId,
                            { currency: winnings }
                        );
    
                        // Update player statistics
                        await updatePlayerStats(gameServerId, mod.moduleId, bet.playerId, bet, true, winnings);
    
                        // Notify winner if online
                        const isOnline = onlinePlayers.data.data.some(p => p.playerId === bet.playerId);
                        if (isOnline) {
                            let winMessage = `🎉 Congratulations! ${winningHorse} won! You received ${winnings} currency!`;
                            if (jackpotInfo.isJackpot) {
                                winMessage += ` (Including jackpot bonus!)`;
                            }
                            
                            await takaro.player.playerControllerSendMessage(bet.playerId, {
                                message: winMessage
                            });
                        }
                    } catch (error) {
                        console.log(`Error processing win for player ${bet.playerName}:`, error);
                    }
                }
    
                let payoutMessage = `💰 WINNERS: ${winningBets.length} lucky bettor${winningBets.length !== 1 ? 's' : ''} won ${totalPayout} total currency!`;
                if (jackpotInfo.isJackpot) {
                    payoutMessage += ` 🎰`;
                }
                
                await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                    message: payoutMessage
                });
    
                // Show who won what
                const winnerNames = winningBets.map(bet => bet.playerName).slice(0, 3);
                if (winnerNames.length > 0) {
                    await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                        message: `🎊 Winners: ${winnerNames.join(', ')} bet on the winning horse ${winningHorse}!`
                    });
                }
            } else {
                await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                    message: `💸 Nobody bet on ${winningHorse}! The house wins this round!`
                });
            }
    
            // Update stats for losing bets
            for (const bet of raceData.bets) {
                const isWinner = winningBets.some(wb => wb.playerId === bet.playerId);
                if (!isWinner) {
                    await updatePlayerStats(gameServerId, mod.moduleId, bet.playerId, bet, false, 0);
                }
            }
    
            // Clear jackpot if used
            if (jackpotInfo.isJackpot) {
                const jackpotSearch = await takaro.variable.variableControllerSearch({
                    filters: {
                        key: ['current_jackpot'],
                        gameServerId: [gameServerId],
                        moduleId: [mod.moduleId],
                    },
                });
    
                if (jackpotSearch.data.data.length > 0) {
                    await takaro.variable.variableControllerDelete(jackpotSearch.data.data[0].id);
                }
            }
    
            // Save race results for history
            raceData.lastRaceResults = {
                raceNumber: raceData.raceNumber,
                results: raceResults,
                winners: winningBets,
                totalPayout: totalPayout,
                jackpot: jackpotInfo.isJackpot ? jackpotInfo.amount : 0,
                timestamp: Date.now()
            };
    
            // Setup next race
            raceData.raceNumber++;
            raceData.nextRaceTime = Date.now() + (2 * 60 * 60 * 1000);
            raceData.bets = [];
            raceData.state = 'waiting';
    
            // Update race data
            await takaro.variable.variableControllerUpdate(currentRace.data.data[0].id, {
                value: JSON.stringify(raceData),
            });
    
            await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                message: "🗓️ Next race in 2 hours! Use /horsebet <horse> <amount> to place your bets!"
            });
    
            return true;
        } catch (error) {
            console.log('Error in runRace cronjob:', error);
            return true;
        }
    }
    
    await main();

Functions 1

Shared helpers the module's commands, hooks and cron jobs import.

  • utils

    Function source
    // utils.js
    import { takaro } from '@takaro/helpers';
    
    // Helper to find a horse by name
    export function findHorse(horses, horseName) {
        return horses.find(h => h.name.toLowerCase() === horseName.toLowerCase());
    }
    
    // Parse horses from config - improved with better error handling
    export function parseHorses(userConfig) {
        try {
            let horses = [];
    
            if (userConfig?.Horses && Array.isArray(userConfig.Horses)) {
                horses = userConfig.Horses.map(horseStr => {
                    if (typeof horseStr !== 'string') {
                        return {
                            name: String(horseStr),
                            odds: 2
                        };
                    }
    
                    const parts = horseStr.split(';');
                    if (parts.length >= 2) {
                        const name = parts[0].trim();
                        const oddsStr = parts[1].trim();
                        const odds = parseInt(oddsStr, 10);
                        return {
                            name: name,
                            odds: isNaN(odds) ? 2 : odds
                        };
                    } else {
                        return {
                            name: horseStr.trim(),
                            odds: 2
                        };
                    }
                });
            } else {
                // Fall back to default horses
                horses = [
                    { name: "Thunder", odds: 2 },
                    { name: "Lightning", odds: 3 },
                    { name: "Shadow", odds: 3 },
                    { name: "Blaze", odds: 4 }
                ];
            }
    
            // Filter out any invalid horses
            horses = horses.filter(h => h && h.name);
    
            return horses;
        } catch (error) {
            // Return default horses on error
            return [
                { name: "Thunder", odds: 2 },
                { name: "Lightning", odds: 3 },
                { name: "Shadow", odds: 3 },
                { name: "Blaze", odds: 4 }
            ];
        }
    }
    
    // Helper function to manage race data
    export async function getRaceData(gameServerId, moduleId) {
        const currentRace = await takaro.variable.variableControllerSearch({
            filters: {
                key: ['current_race'],
                gameServerId: [gameServerId],
                moduleId: [moduleId],
            },
        });
    
        if (currentRace.data.data.length === 0) {
            // Initialize race data if it doesn't exist
            const initialRace = {
                nextRaceTime: Date.now() + 3600000, // 1 hour from now
                bets: [],
                lastRaceResults: null,
                raceNumber: 1
            };
    
            await takaro.variable.variableControllerCreate({
                key: 'current_race',
                value: JSON.stringify(initialRace),
                gameServerId,
                moduleId,
            });
    
            return initialRace;
        }
    
        // Safely parse JSON with fallback
        try {
            return JSON.parse(currentRace.data.data[0].value);
        } catch (e) {
            console.log('Error parsing race data:', e);
            // Return a new race if parsing fails
            return {
                nextRaceTime: Date.now() + 3600000,
                bets: [],
                lastRaceResults: null,
                raceNumber: 1
            };
        }
    }
    
    // Update race data
    export async function updateRaceData(gameServerId, moduleId, raceData) {
        try {
            const currentRace = await takaro.variable.variableControllerSearch({
                filters: {
                    key: ['current_race'],
                    gameServerId: [gameServerId],
                    moduleId: [moduleId],
                },
            });
    
            if (currentRace.data.data.length === 0) {
                // Create if it doesn't exist
                await takaro.variable.variableControllerCreate({
                    key: 'current_race',
                    value: JSON.stringify(raceData),
                    gameServerId,
                    moduleId,
                });
            } else {
                // Update existing
                await takaro.variable.variableControllerUpdate(currentRace.data.data[0].id, {
                    value: JSON.stringify(raceData),
                });
            }
        } catch (error) {
            console.log('Error updating race data:', error);
        }
    }
    
    // Helper to get the next race time in a human-readable format
    export function getTimeUntilRace(nextRaceTime) {
        const timeRemaining = nextRaceTime - Date.now();
    
        if (timeRemaining <= 0) {
            return "any moment now";
        }
    
        const minutes = Math.floor(timeRemaining / 60000);
        if (minutes < 60) {
            return `${minutes} minute${minutes !== 1 ? 's' : ''}`;
        }
    
        const hours = Math.floor(minutes / 60);
        const remainingMinutes = minutes % 60;
        return `${hours} hour${hours !== 1 ? 's' : ''} and ${remainingMinutes} minute${remainingMinutes !== 1 ? 's' : ''}`;
    }
    
    // Simulate race with weighted probabilities based on odds
    export function simulateRace(horses) {
        if (!horses || !Array.isArray(horses) || horses.length === 0) {
            console.log('Invalid horses data for simulation, using defaults');
            horses = [
                { name: "Thunder", odds: 2 },
                { name: "Lightning", odds: 3 },
                { name: "Shadow", odds: 3 },
                { name: "Blaze", odds: 4 }
            ];
        }
    
        // Convert odds to weights (lower odds = higher chance to win)
        const totalWeight = horses.reduce((sum, horse) => sum + (1 / horse.odds), 0);
    
        // Create weighted array
        const weightedHorses = [];
        for (const horse of horses) {
            // Calculate weight - inverse of odds, normalized
            const weight = (1 / horse.odds) / totalWeight;
            weightedHorses.push({
                name: horse.name,
                weight,
                position: 0,
                speed: 0.5 + (Math.random() * 0.5) // Randomize speed a bit
            });
        }
    
        // Run the race simulation - 10 steps
        for (let step = 0; step < 10; step++) {
            for (const horse of weightedHorses) {
                // Movement based on weight (higher weight = more likely to advance) and speed
                horse.position += horse.weight * horse.speed * (0.8 + Math.random() * 0.4);
            }
        }
    
        // Sort by final position
        return weightedHorses.sort((a, b) => b.position - a.position);
    }
    
    // Generate a visual representation of the race
    export function generateRaceProgress(results) {
        if (!results || !Array.isArray(results) || results.length === 0) {
            return "The race is too close to call!";
        }
    
        const top3 = results.slice(0, Math.min(3, results.length));
        return top3.map((horse, index) => `${horse.name}${index === 0 ? ' in the lead' : ''}`).join(', ') + "!";
    }
    
    // Track player statistics
    export async function updatePlayerStats(gameServerId, moduleId, playerId, bet, isWin, winnings = 0) {
        try {
            // Get existing player stats
            const statsSearch = await takaro.variable.variableControllerSearch({
                filters: {
                    key: ['horse_racing_stats'],
                    gameServerId: [gameServerId],
                    moduleId: [moduleId],
                    playerId: [playerId],
                },
            });
    
            let stats;
            if (statsSearch.data.data.length === 0) {
                // Initialize new player stats
                stats = {
                    totalWinnings: 0,
                    totalBets: 0,
                    totalWagered: 0,
                    wins: 0,
                    losses: 0,
                    biggestWin: 0,
                    favoriteHorse: bet.horse,
                    horseStats: {}
                };
            } else {
                stats = JSON.parse(statsSearch.data.data[0].value);
            }
    
            // Update stats
            stats.totalBets++;
            stats.totalWagered += bet.amount;
            
            if (isWin) {
                stats.wins++;
                stats.totalWinnings += winnings;
                if (winnings > stats.biggestWin) {
                    stats.biggestWin = winnings;
                }
            } else {
                stats.losses++;
            }
    
            // Track horse betting patterns
            if (!stats.horseStats) {
                stats.horseStats = {};
            }
            if (!stats.horseStats[bet.horse]) {
                stats.horseStats[bet.horse] = { bets: 0, wins: 0, totalWagered: 0 };
            }
            
            stats.horseStats[bet.horse].bets++;
            stats.horseStats[bet.horse].totalWagered += bet.amount;
            if (isWin) {
                stats.horseStats[bet.horse].wins++;
            }
    
            // Update favorite horse (most bet on)
            const mostBetHorse = Object.entries(stats.horseStats).reduce((a, b) => 
                stats.horseStats[a[0]].bets > stats.horseStats[b[0]].bets ? a : b
            );
            stats.favoriteHorse = mostBetHorse[0];
    
            // Save or update stats
            if (statsSearch.data.data.length === 0) {
                await takaro.variable.variableControllerCreate({
                    key: 'horse_racing_stats',
                    value: JSON.stringify(stats),
                    gameServerId,
                    moduleId,
                    playerId,
                });
            } else {
                await takaro.variable.variableControllerUpdate(statsSearch.data.data[0].id, {
                    value: JSON.stringify(stats),
                });
            }
    
        } catch (error) {
            console.log('Error updating player stats:', error);
        }
    }
    
    // Get exciting race commentary
    export function getRaceCommentary(stage, horses, results) {
        const commentaries = {
            start: [
                "🏇 The horses are lined up at the starting gate!",
                "🚩 And they're off! The race has begun!",
                "⚡ Lightning start as the horses burst from the gate!",
                "🎯 The field is packed and ready to run!"
            ],
            midRace: [
                `🔥 ${results[0].name} takes the early lead!`,
                `💨 It's a tight race between ${results[0].name} and ${results[1].name}!`,
                `🏃‍♂️ The pack is bunched together - anyone could win!`,
                `⚡ ${results[0].name} pulls ahead with a burst of speed!`
            ],
            finish: [
                `🏆 Victory! ${results[0].name} crosses the finish line first!`,
                `🥇 What a race! ${results[0].name} wins by a nose!`,
                `🎊 ${results[0].name} takes the checkered flag!`,
                `🏅 Champion! ${results[0].name} dominates the field!`
            ]
        };
    
        const stageComments = commentaries[stage] || commentaries.start;
        return stageComments[Math.floor(Math.random() * stageComments.length)];
    }
    
    // Generate jackpot system
    export async function checkAndCreateJackpot(gameServerId, moduleId, totalBets) {
        try {
            // Create jackpot every 10 races or when there are 20+ total bets
            if (totalBets >= 20) {
                const jackpotSearch = await takaro.variable.variableControllerSearch({
                    filters: {
                        key: ['current_jackpot'],
                        gameServerId: [gameServerId],
                        moduleId: [moduleId],
                    },
                });
    
                let jackpotAmount = Math.floor(totalBets * 50); // Base jackpot
                
                if (jackpotSearch.data.data.length === 0) {
                    // Create new jackpot
                    await takaro.variable.variableControllerCreate({
                        key: 'current_jackpot',
                        value: JSON.stringify({
                            amount: jackpotAmount,
                            active: true,
                            createdAt: Date.now()
                        }),
                        gameServerId,
                        moduleId,
                    });
                    return { isJackpot: true, amount: jackpotAmount };
                }
            }
            
            return { isJackpot: false, amount: 0 };
        } catch (error) {
            console.log('Error checking jackpot:', error);
            return { isJackpot: false, amount: 0 };
        }
    }

Permissions 2

Roles you can grant to decide who may use what.

  • Place Horse Racing Bets

    Allows the player to bet on horse races

  • Horse Racing Admin

    Allows the player to manage horse races