GeneralstockMarket
- community
- economy
- by limon
- Takaro v0.0.24
- all
Stock Market: A Dynamic Virtual Trading System
Create an immersive economic experience with this comprehensive stock market module that lets players invest, trade, and react to market events.
Market Features:

- Multiple Economic Sectors: Configure various industry sectors (Technology, Healthcare, Energy, etc.) each with their own market dynamics.
- Realistic Stock Behavior: Stocks have configurable volatility, sector-based pricing, and react differently to market events.
- Dynamic Market Events: Random or admin-triggered events like "Global Pandemic" or "Tech Boom" impact different sectors in realistic ways.

Trading System:
- Buy & Sell Shares: Players can purchase stocks and sell them later for profit or loss.
- Transaction Fees: Configurable fee percentage on all trades creates a realistic market economy.
- VIP Benefits: Special permissions for designated "Stock Brokers" who receive fee discounts.

Players can track price changes and sell at the right moment to maximize profits or minimize losses.

Player Portfolio:
- Portfolio Tracking: Players can view their holdings, average purchase prices, and current profit/loss.
- Transaction History: System tracks all buy/sell transactions for each player.
- Summary Statistics: See overall portfolio performance and value across all holdings.

Market Information:
- Detailed Stock Info: Players can research specific stocks, including risk levels and sector trends.
- Industry Filters: View stocks by specific sectors to better understand market segments.
- Market Alerts: Server-wide notifications for significant price changes keep players engaged.


Configuration Options:
- Sectors & Stocks: Define your own market sectors and individual stocks with customizable starting prices and volatility.
- Market Events: Create unique events with specific sector impacts to drive market dynamics.
- Transaction Settings: Configure fee percentages, VIP discounts, and alert thresholds.
- Event Frequency: Control how often random market events occur and how long they last.
Commands:
/markets [industry]- View all stocks or filter by industry/stockinfo [ticker]- Get detailed information about a specific stock/buystock <ticker> <amount>- Purchase shares of a stock/sellstock <ticker> <amount>- Sell shares from your portfolio/stockportfolio- View your current holdings and performance/triggerevent [event]- Admin command to trigger specific market events
Perfect for servers wanting to add economic depth, encourage player engagement, and create opportunities for strategic gameplay!
Configuration 8
Settings you fill in when installing the module on a server.
| Setting | Type | Default | Description |
|---|---|---|---|
sectors Market Sectors | array | [{"id":"TECH","name":"Technology"},{"id":"HEALTH","name":"Healthcare"},{"id":"ENERGY","name":"Energy"},{"id":"FINANCE","name":"Financial"},{"id":"CONSUMER","name":"Consumer Goods"}] | Define economic sectors for categorizing stocks |
stocks Stocks | array | [{"id":"AAPL","name":"Apple Inc.","sector":"TECH","initialPrice":150,"volatility":8},{"id":"MSFT","name":"MicroSoft Corp","sector":"TECH","initialPrice":200,"volatility":7},{"id":"DRUG","name":"MediPharma","sector":"HEALTH","initialPrice":120,"volatility":12},{"id":"HOSP","name":"Global Healthcare","sector":"HEALTH","initialPrice":80,"volatility":9},{"id":"OIL","name":"Petrol Giants","sector":"ENERGY","initialPrice":95,"volatility":15},{"id":"SOLAR","name":"Sun Energy","sector":"ENERGY","initialPrice":45,"volatility":18},{"id":"BANK","name":"United Banking","sector":"FINANCE","initialPrice":175,"volatility":10},{"id":"FOOD","name":"Quality Foods","sector":"CONSUMER","initialPrice":65,"volatility":5}] | List of stocks available for trading |
marketEvents Market Events | array | [{"id":"PANDEMIC","name":"Global Pandemic","description":"A worldwide health crisis affects markets","sectorImpacts":[{"sectorId":"TECH","impact":15},{"sectorId":"HEALTH","impact":30},{"sectorId":"ENERGY","impact":-20},{"sectorId":"FINANCE","impact":-10},{"sectorId":"CONSUMER","impact":-5}]},{"id":"OIL_CRISIS","name":"Oil Supply Crisis","description":"Major disruption in oil supply chains","sectorImpacts":[{"sectorId":"TECH","impact":-5},{"sectorId":"HEALTH","impact":0},{"sectorId":"ENERGY","impact":35},{"sectorId":"FINANCE","impact":-15},{"sectorId":"CONSUMER","impact":-20}]},{"id":"TECH_BOOM","name":"Technology Innovation Boom","description":"Revolutionary new technologies emerge","sectorImpacts":[{"sectorId":"TECH","impact":40},{"sectorId":"HEALTH","impact":10},{"sectorId":"ENERGY","impact":5},{"sectorId":"FINANCE","impact":15},{"sectorId":"CONSUMER","impact":0}]},{"id":"RECESSION","name":"Economic Recession","description":"General economic downturn affects all markets","sectorImpacts":[{"sectorId":"TECH","impact":-25},{"sectorId":"HEALTH","impact":-10},{"sectorId":"ENERGY","impact":-30},{"sectorId":"FINANCE","impact":-35},{"sectorId":"CONSUMER","impact":-15}]},{"id":"RECOVERY","name":"Economic Recovery","description":"Markets rebound from previous downturn","sectorImpacts":[{"sectorId":"TECH","impact":20},{"sectorId":"HEALTH","impact":15},{"sectorId":"ENERGY","impact":25},{"sectorId":"FINANCE","impact":30},{"sectorId":"CONSUMER","impact":10}]}] | Special events that can impact the market |
eventFrequency Event Frequency | number | 10 | Average number of cronjob runs between market events (0 to disable events) |
defaultEventDuration Default Event Duration | number | 5 | Default number of cronjob runs an event lasts if not specified in the event definition itself |
transactionFee Transaction Fee | number | 5 | Percentage fee charged on all buy/sell transactions (5 = 5%) |
vipDiscount VIP Discount | number | 50 | Percentage discount on transaction fees for players with the STOCK_MARKET_BROKER permission (50 = 50%) |
priceAlertThreshold Price Alert Threshold | number | 10 | Percentage change that triggers a market alert (10 = 10%) |
Raw config schema
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"sectors": {
"type": "array",
"title": "Market Sectors",
"description": "Define economic sectors for categorizing stocks",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Sector ID (e.g., TECH)"
},
"name": {
"type": "string",
"description": "Sector name (e.g., Technology)"
}
}
},
"default": [
{
"id": "TECH",
"name": "Technology"
},
{
"id": "HEALTH",
"name": "Healthcare"
},
{
"id": "ENERGY",
"name": "Energy"
},
{
"id": "FINANCE",
"name": "Financial"
},
{
"id": "CONSUMER",
"name": "Consumer Goods"
}
]
},
"stocks": {
"type": "array",
"title": "Stocks",
"description": "List of stocks available for trading",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Stock ticker symbol (e.g., IRON)"
},
"name": {
"type": "string",
"description": "Full company name"
},
"sector": {
"type": "string",
"description": "Sector this stock belongs to (must match a sector ID)"
},
"initialPrice": {
"type": "number",
"description": "Starting price in whole currency units",
"minimum": 1
},
"volatility": {
"type": "number",
"description": "Base volatility percentage (5 = 5%)",
"minimum": 1,
"maximum": 25
}
}
},
"default": [
{
"id": "AAPL",
"name": "Apple Inc.",
"sector": "TECH",
"initialPrice": 150,
"volatility": 8
},
{
"id": "MSFT",
"name": "MicroSoft Corp",
"sector": "TECH",
"initialPrice": 200,
"volatility": 7
},
{
"id": "DRUG",
"name": "MediPharma",
"sector": "HEALTH",
"initialPrice": 120,
"volatility": 12
},
{
"id": "HOSP",
"name": "Global Healthcare",
"sector": "HEALTH",
"initialPrice": 80,
"volatility": 9
},
{
"id": "OIL",
"name": "Petrol Giants",
"sector": "ENERGY",
"initialPrice": 95,
"volatility": 15
},
{
"id": "SOLAR",
"name": "Sun Energy",
"sector": "ENERGY",
"initialPrice": 45,
"volatility": 18
},
{
"id": "BANK",
"name": "United Banking",
"sector": "FINANCE",
"initialPrice": 175,
"volatility": 10
},
{
"id": "FOOD",
"name": "Quality Foods",
"sector": "CONSUMER",
"initialPrice": 65,
"volatility": 5
}
]
},
"marketEvents": {
"type": "array",
"title": "Market Events",
"description": "Special events that can impact the market",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique event ID"
},
"name": {
"type": "string",
"description": "Name of the event"
},
"description": {
"type": "string",
"description": "Description of how this event affects the market"
},
"sectorImpacts": {
"type": "array",
"description": "How each sector is affected",
"items": {
"type": "object",
"properties": {
"sectorId": {
"type": "string",
"description": "Sector ID this impact applies to"
},
"impact": {
"type": "number",
"description": "Percentage impact (-30 = -30%, +20 = +20%)",
"minimum": -50,
"maximum": 50
}
}
}
}
}
},
"default": [
{
"id": "PANDEMIC",
"name": "Global Pandemic",
"description": "A worldwide health crisis affects markets",
"sectorImpacts": [
{
"sectorId": "TECH",
"impact": 15
},
{
"sectorId": "HEALTH",
"impact": 30
},
{
"sectorId": "ENERGY",
"impact": -20
},
{
"sectorId": "FINANCE",
"impact": -10
},
{
"sectorId": "CONSUMER",
"impact": -5
}
]
},
{
"id": "OIL_CRISIS",
"name": "Oil Supply Crisis",
"description": "Major disruption in oil supply chains",
"sectorImpacts": [
{
"sectorId": "TECH",
"impact": -5
},
{
"sectorId": "HEALTH",
"impact": 0
},
{
"sectorId": "ENERGY",
"impact": 35
},
{
"sectorId": "FINANCE",
"impact": -15
},
{
"sectorId": "CONSUMER",
"impact": -20
}
]
},
{
"id": "TECH_BOOM",
"name": "Technology Innovation Boom",
"description": "Revolutionary new technologies emerge",
"sectorImpacts": [
{
"sectorId": "TECH",
"impact": 40
},
{
"sectorId": "HEALTH",
"impact": 10
},
{
"sectorId": "ENERGY",
"impact": 5
},
{
"sectorId": "FINANCE",
"impact": 15
},
{
"sectorId": "CONSUMER",
"impact": 0
}
]
},
{
"id": "RECESSION",
"name": "Economic Recession",
"description": "General economic downturn affects all markets",
"sectorImpacts": [
{
"sectorId": "TECH",
"impact": -25
},
{
"sectorId": "HEALTH",
"impact": -10
},
{
"sectorId": "ENERGY",
"impact": -30
},
{
"sectorId": "FINANCE",
"impact": -35
},
{
"sectorId": "CONSUMER",
"impact": -15
}
]
},
{
"id": "RECOVERY",
"name": "Economic Recovery",
"description": "Markets rebound from previous downturn",
"sectorImpacts": [
{
"sectorId": "TECH",
"impact": 20
},
{
"sectorId": "HEALTH",
"impact": 15
},
{
"sectorId": "ENERGY",
"impact": 25
},
{
"sectorId": "FINANCE",
"impact": 30
},
{
"sectorId": "CONSUMER",
"impact": 10
}
]
}
]
},
"eventFrequency": {
"title": "Event Frequency",
"type": "number",
"description": "Average number of cronjob runs between market events (0 to disable events)",
"default": 10,
"minimum": 0
},
"defaultEventDuration": {
"title": "Default Event Duration",
"type": "number",
"description": "Default number of cronjob runs an event lasts if not specified in the event definition itself",
"default": 5,
"minimum": 1,
"maximum": 20
},
"transactionFee": {
"title": "Transaction Fee",
"type": "number",
"description": "Percentage fee charged on all buy/sell transactions (5 = 5%)",
"default": 5,
"minimum": 0,
"maximum": 25
},
"vipDiscount": {
"title": "VIP Discount",
"type": "number",
"description": "Percentage discount on transaction fees for players with the STOCK_MARKET_BROKER permission (50 = 50%)",
"default": 50,
"minimum": 0,
"maximum": 100
},
"priceAlertThreshold": {
"title": "Price Alert Threshold",
"type": "number",
"description": "Percentage change that triggers a market alert (10 = 10%)",
"default": 10,
"minimum": 5,
"maximum": 50
}
},
"additionalProperties": false
} Raw UI schema
{
"sectors": {
"items": {
"ui:order": [
"id",
"name"
]
}
},
"stocks": {
"items": {
"ui:order": [
"id",
"name",
"sector",
"initialPrice",
"volatility"
]
}
},
"marketEvents": {
"items": {
"ui:order": [
"id",
"name",
"description",
"sectorImpacts"
],
"sectorImpacts": {
"items": {
"ui:order": [
"sectorId",
"impact"
]
}
}
}
},
"transactionFee": {
"ui:widget": "range"
},
"vipDiscount": {
"ui:widget": "range"
},
"eventFrequency": {
"ui:help": "Set to 0 to disable random events"
},
"priceAlertThreshold": {
"ui:widget": "range"
}
} Commands 6
Chat commands players trigger in game.
-
buystock
Buy shares of a stock
Argument Type Default Help stockstring The stock ticker symbol amountnumber Number of shares to buy Command source
import { takaro, data, TakaroUserError, checkPermission } from '@takaro/helpers'; async function main() { const { player, gameServerId, module: mod, arguments: args } = data; // Check permission if (!checkPermission(data.pog, 'STOCK_MARKET_TRADE')) { throw new TakaroUserError("You don't have permission to trade stocks."); } // Validate input parameters if (!args.stock) { throw new TakaroUserError("Please specify a stock ticker. Usage: /buystock STOCK AMOUNT"); } // Convert amount to integer and validate const amount = parseInt(args.amount); if (isNaN(amount) || amount <= 0) { throw new TakaroUserError("Amount must be a positive whole number. Usage: /buystock STOCK AMOUNT"); } try { // Get current stock data const marketDataVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); if (marketDataVar.data.data.length === 0) { throw new TakaroUserError("The stock market isn't available right now. Please try again later."); } const stocks = JSON.parse(marketDataVar.data.data[0].value); const stock = stocks.find(s => s.id.toUpperCase() === args.stock.toUpperCase()); if (!stock) { throw new TakaroUserError(`Stock ${args.stock} not found. Use /market to see available stocks.`); } // Calculate transaction fee - using integers to avoid floating point issues let feePercentage = mod.userConfig.transactionFee || 5; // Check for VIP discount if (checkPermission(data.pog, 'STOCK_MARKET_BROKER')) { const discount = (mod.userConfig.vipDiscount || 50) / 100; // Convert to decimal feePercentage = feePercentage * (1 - discount); } // Calculate costs using Math.round to ensure we work with integers const subtotal = Math.round(stock.price) * amount; const fee = Math.round((subtotal * feePercentage) / 100); const totalCost = subtotal + fee; // Check if player has enough currency const playerData = await takaro.playerOnGameserver.playerOnGameServerControllerGetOne(gameServerId, player.id); const currentBalance = playerData.data.data.currency; if (currentBalance < totalCost) { throw new TakaroUserError(`You don't have enough currency. Cost: $${subtotal} + $${fee} fee = $${totalCost}. Your balance: $${currentBalance}`); } // Deduct the currency - using integer value to avoid precision errors await takaro.playerOnGameserver.playerOnGameServerControllerDeductCurrency( gameServerId, player.id, { currency: totalCost } ); // Get player's portfolio or create new one const portfolioVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_portfolio'], gameServerId: [gameServerId], moduleId: [mod.moduleId], playerId: [player.id] } }); let portfolio; if (portfolioVar.data.data.length === 0) { portfolio = {}; } else { portfolio = JSON.parse(portfolioVar.data.data[0].value); } // Update portfolio using rounded values for consistency const stockPrice = Math.round(stock.price); if (!portfolio[stock.id]) { portfolio[stock.id] = { shares: amount, averagePrice: stockPrice }; } else { const totalShares = portfolio[stock.id].shares + amount; const totalValue = (portfolio[stock.id].shares * portfolio[stock.id].averagePrice) + (amount * stockPrice); portfolio[stock.id].shares = totalShares; portfolio[stock.id].averagePrice = Math.round(totalValue / totalShares); } // Save updated portfolio if (portfolioVar.data.data.length === 0) { await takaro.variable.variableControllerCreate({ key: 'stock_portfolio', value: JSON.stringify(portfolio), gameServerId, moduleId: mod.moduleId, playerId: player.id }); } else { await takaro.variable.variableControllerUpdate(portfolioVar.data.data[0].id, { value: JSON.stringify(portfolio) }); } // Track this transaction in transaction history await recordTransaction(gameServerId, mod.moduleId, player.id, { type: 'BUY', stockId: stock.id, shares: amount, pricePerShare: stockPrice, subtotal: subtotal, fee: fee, total: totalCost, timestamp: new Date().toISOString() }); let message = `Successfully bought ${amount} shares of ${stock.id} at $${stockPrice} each.\n`; message += `Subtotal: $${subtotal}\n`; message += `Transaction fee: $${fee}\n`; message += `Total cost: $${totalCost}\n`; message += `Current portfolio: ${portfolio[stock.id].shares} shares of ${stock.id}`; await player.pm(message); } catch (error) { // If it's our custom error, just pass it through if (error instanceof TakaroUserError) { throw error; } // For unexpected errors, log them and provide a friendlier message console.error("Error in buystock command:", error); throw new TakaroUserError("An error occurred while processing your purchase. Please try again later."); } } // Record transaction history for reporting and analytics async function recordTransaction(gameServerId, moduleId, playerId, transaction) { try { const historyVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_transaction_history'], gameServerId: [gameServerId], moduleId: [moduleId], playerId: [playerId] } }); let history = []; if (historyVar.data.data.length > 0) { history = JSON.parse(historyVar.data.data[0].value); } // Add new transaction to history history.push(transaction); // Keep only the last 50 transactions to avoid variable size limits if (history.length > 50) { history = history.slice(history.length - 50); } if (historyVar.data.data.length === 0) { await takaro.variable.variableControllerCreate({ key: 'stock_transaction_history', value: JSON.stringify(history), gameServerId, moduleId, playerId }); } else { await takaro.variable.variableControllerUpdate(historyVar.data.data[0].id, { value: JSON.stringify(history) }); } } catch (error) { // Don't let transaction history errors prevent the main operation console.error("Error recording transaction history:", error); } } await main(); -
sellstock
Sell shares of a stock
Argument Type Default Help stockstring The stock ticker symbol amountnumber Number of shares to sell Command source
import { takaro, data, TakaroUserError, checkPermission } from '@takaro/helpers'; async function main() { const { player, gameServerId, module: mod, arguments: args } = data; // Check permission if (!checkPermission(data.pog, 'STOCK_MARKET_TRADE')) { throw new TakaroUserError("You don't have permission to trade stocks."); } // Validate input parameters if (!args.stock) { throw new TakaroUserError("Please specify a stock ticker. Usage: /sellstock STOCK AMOUNT"); } // Convert amount to integer and validate const amount = parseInt(args.amount); if (isNaN(amount) || amount <= 0) { throw new TakaroUserError("Amount must be a positive whole number. Usage: /sellstock STOCK AMOUNT"); } try { // Get player's portfolio const portfolioVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_portfolio'], gameServerId: [gameServerId], moduleId: [mod.moduleId], playerId: [player.id] } }); if (portfolioVar.data.data.length === 0) { throw new TakaroUserError("You don't own any stocks to sell."); } const portfolio = JSON.parse(portfolioVar.data.data[0].value); const stockId = args.stock.toUpperCase(); if (!portfolio[stockId] || portfolio[stockId].shares < amount) { throw new TakaroUserError(`You don't own ${amount} shares of ${stockId}.`); } // Get current stock data const marketDataVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); if (marketDataVar.data.data.length === 0) { throw new TakaroUserError("The stock market isn't available right now. Please try again later."); } const stocks = JSON.parse(marketDataVar.data.data[0].value); const stock = stocks.find(s => s.id.toUpperCase() === stockId); if (!stock) { throw new TakaroUserError(`Stock ${stockId} not found in current market data. Please contact an admin.`); } // Calculate transaction fee - using integers to avoid floating point issues let feePercentage = mod.userConfig.transactionFee || 5; // Check for VIP discount if (checkPermission(data.pog, 'STOCK_MARKET_BROKER')) { const discount = (mod.userConfig.vipDiscount || 50) / 100; // Convert to decimal feePercentage = feePercentage * (1 - discount); } // Calculate sale proceeds using Math.round to ensure we work with integers const stockPrice = Math.round(stock.price); const subtotal = stockPrice * amount; const fee = Math.round((subtotal * feePercentage) / 100); const netProceeds = subtotal - fee; // Add money to player - using integer value to avoid precision errors await takaro.playerOnGameserver.playerOnGameServerControllerAddCurrency( gameServerId, player.id, { currency: netProceeds } ); // Calculate profit/loss const profitPerShare = stockPrice - portfolio[stockId].averagePrice; const totalProfit = Math.round(profitPerShare * amount); const profitText = totalProfit >= 0 ? `profit of $${totalProfit}` : `loss of $${Math.abs(totalProfit)}`; // Update portfolio portfolio[stockId].shares -= amount; if (portfolio[stockId].shares === 0) { delete portfolio[stockId]; } // Save updated portfolio await takaro.variable.variableControllerUpdate(portfolioVar.data.data[0].id, { value: JSON.stringify(portfolio) }); // Track this transaction in transaction history await recordTransaction(gameServerId, mod.moduleId, player.id, { type: 'SELL', stockId: stock.id, shares: amount, pricePerShare: stockPrice, subtotal: subtotal, fee: fee, total: netProceeds, profit: totalProfit, timestamp: new Date().toISOString() }); let message = `Successfully sold ${amount} shares of ${stockId} at $${stockPrice} each.\n`; message += `Subtotal: $${subtotal}\n`; message += `Transaction fee: $${fee}\n`; message += `Net proceeds: $${netProceeds} (${profitText})`; // Add remaining shares info if player still has some if (portfolio[stockId]) { message += `\nRemaining shares: ${portfolio[stockId].shares}`; } else { message += `\nYou've sold all your ${stockId} shares.`; } await player.pm(message); } catch (error) { // If it's our custom error, just pass it through if (error instanceof TakaroUserError) { throw error; } // For unexpected errors, log them and provide a friendlier message console.error("Error in sellstock command:", error); throw new TakaroUserError("An error occurred while processing your sale. Please try again later."); } } // Record transaction history for reporting and analytics async function recordTransaction(gameServerId, moduleId, playerId, transaction) { try { const historyVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_transaction_history'], gameServerId: [gameServerId], moduleId: [moduleId], playerId: [playerId] } }); let history = []; if (historyVar.data.data.length > 0) { history = JSON.parse(historyVar.data.data[0].value); } // Add new transaction to history history.push(transaction); // Keep only the last 50 transactions to avoid variable size limits if (history.length > 50) { history = history.slice(history.length - 50); } if (historyVar.data.data.length === 0) { await takaro.variable.variableControllerCreate({ key: 'stock_transaction_history', value: JSON.stringify(history), gameServerId, moduleId, playerId }); } else { await takaro.variable.variableControllerUpdate(historyVar.data.data[0].id, { value: JSON.stringify(history) }); } } catch (error) { // Don't let transaction history errors prevent the main operation console.error("Error recording transaction history:", error); } } await main(); -
stockportfolio
No help text available
Command source
import { takaro, data, TakaroUserError, checkPermission } from '@takaro/helpers'; async function main() { const { player, gameServerId, module: mod } = data; // Check permission if (!checkPermission(data.pog, 'STOCK_MARKET_USE')) { throw new TakaroUserError("You don't have permission to use the stock market."); } // Get current stock data const marketDataVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); if (marketDataVar.data.data.length === 0) { throw new TakaroUserError("The stock market isn't available right now. Please try again later."); } const stocks = JSON.parse(marketDataVar.data.data[0].value); // Get player's portfolio const portfolioVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_portfolio'], gameServerId: [gameServerId], moduleId: [mod.moduleId], playerId: [player.id] } }); let portfolio = {}; if (portfolioVar.data.data.length > 0) { portfolio = JSON.parse(portfolioVar.data.data[0].value); } // Send header await player.pm("=== YOUR STOCK PORTFOLIO ==="); let hasStocks = false; let totalValue = 0; let totalInvestment = 0; // If no stocks, send a simple message if (Object.keys(portfolio).length === 0) { await player.pm("You don't own any stocks yet.\n" + "Use /markets to see available stocks and prices.\n" + "Use /buy <stock> <amount> to purchase stocks."); return; } // Send each stock as a separate message to avoid length issues for (const [stockId, data] of Object.entries(portfolio)) { hasStocks = true; const stock = stocks.find(s => s.id === stockId); if (!stock) continue; // Stock might have been removed from config const currentValue = stock.price * data.shares; const investmentValue = data.averagePrice * data.shares; totalValue += currentValue; totalInvestment += investmentValue; const profit = currentValue - investmentValue; const profitPercent = ((profit / investmentValue) * 100).toFixed(1); let stockMessage = `--- ${stockId} (${stock.sector}) ---\n`; stockMessage += `Shares: ${data.shares}\n`; stockMessage += `Avg buy: $${Math.round(data.averagePrice)}\n`; stockMessage += `Current price: $${Math.round(stock.price)}\n`; stockMessage += `Total value: $${Math.round(currentValue)}\n`; if (profit >= 0) { stockMessage += `Profit: +$${Math.round(profit)} (+${profitPercent}%)\n`; } else { stockMessage += `Loss: -$${Math.abs(Math.round(profit))} (${profitPercent}%)\n`; } await player.pm(stockMessage); } if (hasStocks) { const totalProfit = totalValue - totalInvestment; const totalProfitPercent = ((totalProfit / totalInvestment) * 100).toFixed(1); let summaryMessage = "=== PORTFOLIO SUMMARY ===\n"; summaryMessage += `Total investment: $${Math.round(totalInvestment)}\n`; summaryMessage += `Current value: $${Math.round(totalValue)}\n`; if (totalProfit >= 0) { summaryMessage += `Overall profit: +$${Math.round(totalProfit)} (+${totalProfitPercent}%)\n`; } else { summaryMessage += `Overall loss: -$${Math.abs(Math.round(totalProfit))} (${totalProfitPercent}%)\n`; } // Get transaction history count try { const historyVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_transaction_history'], gameServerId: [gameServerId], moduleId: [mod.moduleId], playerId: [player.id] } }); if (historyVar.data.data.length > 0) { const history = JSON.parse(historyVar.data.data[0].value); const buyCount = history.filter(t => t.type === 'BUY').length; const sellCount = history.filter(t => t.type === 'SELL').length; summaryMessage += `\nTransactions: ${history.length} (${buyCount} buys, ${sellCount} sells)\n`; } } catch (error) { // Just ignore history errors } await player.pm(summaryMessage); } } await main(); -
markets
View stock market prices and activity. Use '/markets ALL' to see all industries, or specify an industry name (e.g., '/markets TECH') to see stocks in that industry only.
Argument Type Default Help Industrystring all View stock market prices and activity. Use '/markets ALL' to see all industries, or specify an industry name (e.g., '/markets TECH') to see stocks in that industry only. Command source
import { takaro, data, TakaroUserError, checkPermission } from '@takaro/helpers'; async function main() { const { player, gameServerId, module: mod, arguments: args } = data; // Check permission if (!checkPermission(data.pog, 'STOCK_MARKET_USE')) { throw new TakaroUserError("You don't have permission to use the stock market."); } // Handle the industry argument - use "ALL" as a special value to show all industries const industryFilter = args.Industry ? args.Industry.toUpperCase() : "ALL"; // Get current stock data const marketDataVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); // If market data doesn't exist, try to initialize it if (marketDataVar.data.data.length === 0) { const initialized = await initializeMarketIfNeeded(gameServerId, mod); if (!initialized) { throw new TakaroUserError("The stock market isn't available right now. Please try again later."); } // Get the freshly initialized market data const refreshedMarketData = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); if (refreshedMarketData.data.data.length === 0) { throw new TakaroUserError("There was an issue initializing the stock market. Please try again later."); } // Continue with the refreshed data await displayMarketSummary(player, gameServerId, mod, refreshedMarketData.data.data[0], industryFilter); } else { // Market data exists, display it await displayMarketSummary(player, gameServerId, mod, marketDataVar.data.data[0], industryFilter); } } // Display the market summary to the player async function displayMarketSummary(player, gameServerId, mod, marketDataVariable, industryFilter) { // Get active market event const activeEventVar = await takaro.variable.variableControllerSearch({ filters: { key: ['active_market_event'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); let activeEvent = null; if (activeEventVar.data.data.length > 0 && activeEventVar.data.data[0].value) { try { activeEvent = JSON.parse(activeEventVar.data.data[0].value); // Handle empty string or empty object if (!activeEvent || Object.keys(activeEvent).length === 0) { activeEvent = null; } } catch (e) { // In case of parsing error activeEvent = null; } } const stocks = JSON.parse(marketDataVariable.value); // Get all available industries for reference const availableIndustries = [...new Set(stocks.map(stock => stock.sector))]; // If industry filter is provided and not "ALL", check if it's valid if (industryFilter !== "ALL") { // Check if the industry exists const industryExists = availableIndustries.includes(industryFilter); if (!industryExists) { throw new TakaroUserError(`Industry "${industryFilter}" not found. Available industries: ${availableIndustries.join(', ')}\nUse "ALL" to view all industries.`); } } // Send header message let headerMessage = "=== STOCK MARKET SUMMARY ===\n"; // If filtering by industry, mention it in the header if (industryFilter !== "ALL") { headerMessage = `=== ${industryFilter} INDUSTRY ===\n`; } // If there's an active event, include it in the header if (activeEvent) { headerMessage += `\n🌍 ACTIVE EVENT: ${activeEvent.name} 🌍\n`; headerMessage += `${activeEvent.description}\n\n`; // If filtering by industry, only show relevant impacts if (industryFilter !== "ALL") { const relevantImpact = activeEvent.sectorImpacts.find( impact => impact.sectorId === industryFilter ); if (relevantImpact) { const direction = relevantImpact.impact >= 0 ? "↑" : "↓"; headerMessage += `Industry Impact: ${direction} ${Math.abs(relevantImpact.impact)}%\n`; } else { headerMessage += "This industry is not directly affected by the current event.\n"; } } else { // Show all industry impacts headerMessage += "Industry Impacts:\n"; for (const impact of activeEvent.sectorImpacts) { const direction = impact.impact >= 0 ? "↑" : "↓"; headerMessage += `${impact.sectorId}: ${direction} ${Math.abs(impact.impact)}%\n`; } } } // Send header message first await player.pm(headerMessage); // Group stocks by industry const stocksByIndustry = {}; stocks.forEach(stock => { if (!stocksByIndustry[stock.sector]) { stocksByIndustry[stock.sector] = []; } stocksByIndustry[stock.sector].push(stock); }); // If industry filter is not "ALL", only show that industry if (industryFilter !== "ALL") { const filteredStocks = stocksByIndustry[industryFilter] || []; if (filteredStocks.length > 0) { let stockMessage = ""; filteredStocks.forEach(stock => { let changeIcon = ''; if (stock.lastPrice) { const percentChange = ((stock.price - stock.lastPrice) / stock.lastPrice) * 100; changeIcon = percentChange > 0 ? `↑ ${percentChange.toFixed(1)}%` : percentChange < 0 ? `↓ ${Math.abs(percentChange).toFixed(1)}%` : '→'; } stockMessage += `${stock.id}: $${Math.round(stock.price)} ${changeIcon}\n`; }); await player.pm(stockMessage); } else { await player.pm(`No stocks found in the ${industryFilter} industry.`); } } else { // Send each industry as a separate message for (const industryId in stocksByIndustry) { let industryMessage = `=== ${industryId} INDUSTRY ===\n`; stocksByIndustry[industryId].forEach(stock => { let changeIcon = ''; if (stock.lastPrice) { const percentChange = ((stock.price - stock.lastPrice) / stock.lastPrice) * 100; changeIcon = percentChange > 0 ? `↑ ${percentChange.toFixed(1)}%` : percentChange < 0 ? `↓ ${Math.abs(percentChange).toFixed(1)}%` : '→'; } industryMessage += `${stock.id}: $${Math.round(stock.price)} ${changeIcon}\n`; }); await player.pm(industryMessage); } } // Add a help message for industry filtering if showing all industries if (industryFilter === "ALL") { const allIndustries = Object.keys(stocksByIndustry).join(', '); await player.pm(`\nTip: Use '/markets [industry]' to view only stocks in a specific industry. Available industries: ${allIndustries}`); } } // Initialize the market with stocks from the configuration async function initializeMarketIfNeeded(gameServerId, mod) { try { // Get stocks from config const configStocks = (mod.userConfig && mod.userConfig.stocks) || []; if (!configStocks || configStocks.length === 0) { console.log("Error: No stocks defined in configuration"); return false; } // Check for each required variable first const [marketDataVar, lastEventTimeVar, eventStartTimeVar, activeEventVar, marketInitVar] = await Promise.all([ takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['last_market_event_time'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['event_start_time'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['active_market_event'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_initialized'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }) ]); // Map config stocks to our internal format const stocks = configStocks.map(stock => ({ id: stock.id, name: stock.name, sector: stock.sector, price: stock.initialPrice, volatility: stock.volatility / 100, // Convert percentage to decimal lastPrice: stock.initialPrice })); // Create each variable only if it doesn't exist already const createPromises = []; // Store the stocks data if it doesn't exist if (marketDataVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'stock_market_data', value: JSON.stringify(stocks), gameServerId, moduleId: mod.moduleId }) ); } // Initialize last event time if it doesn't exist if (lastEventTimeVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'last_market_event_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }) ); } // Initialize event start time variable if it doesn't exist if (eventStartTimeVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'event_start_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }) ); } // Initialize active event if it doesn't exist if (activeEventVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'active_market_event', value: '', gameServerId, moduleId: mod.moduleId }) ); } // Mark market as initialized if not already marked if (marketInitVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'stock_market_initialized', value: 'true', gameServerId, moduleId: mod.moduleId }) ); } // Wait for all creation promises to complete if (createPromises.length > 0) { const results = await Promise.allSettled(createPromises); // Check if the critical stock_market_data was created successfully const stockDataPromise = results[0]; if (stockDataPromise && stockDataPromise.status === 'rejected') { console.log(`Failed to create stock_market_data: ${stockDataPromise.reason}`); return false; } } return true; } catch (error) { console.log(`Error in initializeMarketIfNeeded: ${error.message}`); return false; } } await main(); -
stockinfo
Shows all available stocks in the market. Use with a ticker symbol (e.g., /stockinfo ticker) to see detailed information about a specific stock.
Argument Type Default Help tickerstring all stock ticker or ALL to get detailed information about a specific stock Command source
import { takaro, data, TakaroUserError, checkPermission } from '@takaro/helpers'; async function main() { const { player, gameServerId, module: mod, arguments: args } = data; // Check permission if (!checkPermission(data.pog, 'STOCK_MARKET_USE')) { throw new TakaroUserError("You don't have permission to use the stock market."); } // Get current stock data const marketDataVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); if (marketDataVar.data.data.length === 0) { throw new TakaroUserError("The stock market isn't available right now. Please try again later."); } const stocks = JSON.parse(marketDataVar.data.data[0].value); // If a specific stock ticker is provided, show detailed info for that stock const specificTicker = args.ticker ? args.ticker.toUpperCase() : null; if (specificTicker && specificTicker !== "ALL") { const stock = stocks.find(s => s.id.toUpperCase() === specificTicker); if (!stock) { throw new TakaroUserError(`Stock ${specificTicker} not found. Use /stockinfo without parameters to see all available stocks.`); } // Get active event to see if this stock's sector is affected const activeEventVar = await takaro.variable.variableControllerSearch({ filters: { key: ['active_market_event'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); let activeEvent = null; let sectorImpact = null; if (activeEventVar.data.data.length > 0 && activeEventVar.data.data[0].value) { try { activeEvent = JSON.parse(activeEventVar.data.data[0].value); if (activeEvent && activeEvent.sectorImpacts) { sectorImpact = activeEvent.sectorImpacts.find(impact => impact.sectorId === stock.sector ); } } catch (e) { // Ignore parsing errors } } // Calculate price change indicators let changeText = ''; if (stock.lastPrice) { const percentChange = ((stock.price - stock.lastPrice) / stock.lastPrice) * 100; const changeIcon = percentChange > 0 ? '↑' : percentChange < 0 ? '↓' : '→'; changeText = ` ${changeIcon} ${Math.abs(percentChange).toFixed(1)}%`; } // Add risk assessment based on volatility without showing the raw value let riskLevel; if (stock.volatility <= 0.05) riskLevel = "Very Low"; else if (stock.volatility <= 0.10) riskLevel = "Low"; else if (stock.volatility <= 0.15) riskLevel = "Moderate"; else if (stock.volatility <= 0.20) riskLevel = "High"; else riskLevel = "Very High"; // Build detailed stock info message let message = `=== ${stock.id}: ${stock.name} ===\n\n`; message += `Sector: ${stock.sector}\n`; message += `Current Price: $${Math.round(stock.price)}${changeText}\n`; message += `Risk Level: ${riskLevel}\n`; // Add sector trend info if available from event if (activeEvent && sectorImpact) { const direction = sectorImpact.impact >= 0 ? "Positive" : "Negative"; const strength = Math.abs(sectorImpact.impact); let trend; if (strength < 10) trend = "Slight"; else if (strength < 25) trend = "Moderate"; else trend = "Strong"; message += `\nCurrent Trend: ${trend} ${direction} (${activeEvent.name})\n`; message += `Event: ${activeEvent.description}\n`; } // Add trading guidance based on sector and risk, without mentioning volatility message += `\nTrading Notes:\n`; if (activeEvent && sectorImpact) { if (sectorImpact.impact > 0) { message += `- Currently bullish due to the ${activeEvent.name} event\n`; } else { message += `- Currently bearish due to the ${activeEvent.name} event\n`; } } if (riskLevel === "High" || riskLevel === "Very High") { message += `- Expect significant price fluctuations with this stock\n`; } else if (riskLevel === "Low" || riskLevel === "Very Low") { message += `- Typically has stable price movement\n`; } message += `\nUse /buystock ${stock.id} [amount] to purchase shares`; await player.pm(message); } else { // No specific ticker provided or ALL specified, show summary of all stocks await player.pm("=== STOCK MARKET LISTINGS ===\n"); // Group stocks by sector const stocksBySector = {}; stocks.forEach(stock => { if (!stocksBySector[stock.sector]) { stocksBySector[stock.sector] = []; } stocksBySector[stock.sector].push(stock); }); // Display stocks by sector for (const [sector, sectorStocks] of Object.entries(stocksBySector)) { let sectorMessage = `\n--- ${sector} SECTOR ---\n`; sectorStocks.forEach(stock => { // Add price change indicators let changeText = ''; if (stock.lastPrice) { const percentChange = ((stock.price - stock.lastPrice) / stock.lastPrice) * 100; const changeIcon = percentChange > 0 ? '↑' : percentChange < 0 ? '↓' : '→'; changeText = ` ${changeIcon} ${Math.abs(percentChange).toFixed(1)}%`; } // Add risk level based on volatility let riskIndicator; if (stock.volatility <= 0.05) riskIndicator = "VL"; else if (stock.volatility <= 0.10) riskIndicator = "L"; else if (stock.volatility <= 0.15) riskIndicator = "M"; else if (stock.volatility <= 0.20) riskIndicator = "H"; else riskIndicator = "VH"; sectorMessage += `${stock.id} (${riskIndicator}): ${stock.name} - $${Math.round(stock.price)}${changeText}\n`; }); await player.pm(sectorMessage); } // Add legend for risk indicators const legend = "\n=== LEGEND ===\n" + "Risk Levels: VL=Very Low, L=Low, M=Moderate, H=High, VH=Very High\n" + "Use /stockinfo [ticker] for detailed information about a specific stock"; await player.pm(legend); } } await main(); -
triggerevent
Shows all available market events when run without parameters. Use with an event name (e.g., /triggerevent TECH_BOOM) to trigger a specific market event.
Argument Type Default Help EventNamestring all Event name to trigger a specific market event Command source
import { takaro, data, TakaroUserError, checkPermission } from '@takaro/helpers'; async function main() { const { player, gameServerId, module: mod, arguments: args } = data; // Check permission if (!checkPermission(data.pog, 'STOCK_MARKET_TRIGGER_EVENT')) { throw new TakaroUserError("You don't have permission to trigger market events."); } try { // Get current stock data to check market initialization const marketDataVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); // Initialize market if needed if (marketDataVar.data.data.length === 0) { await initializeMarketIfNeeded(gameServerId, mod); await player.pm("Market was not initialized. Initializing now..."); return; } // Get events from module config const marketEvents = mod.userConfig.marketEvents || []; if (marketEvents.length === 0) { throw new TakaroUserError("No market events configured in this module."); } // Handle case when no event is specified or "ALL" is provided if (!args.EventName || args.EventName.toUpperCase() === "ALL") { await player.pm("=== AVAILABLE MARKET EVENTS ===\n"); // Group events by category or type if possible const eventCategories = {}; // Create a simple categorization based on positive/negative impact marketEvents.forEach(event => { let category = "Mixed"; // Calculate net impact across all sectors const netImpact = event.sectorImpacts.reduce((sum, impact) => sum + impact.impact, 0); if (netImpact > 0) category = "Positive"; else if (netImpact < 0) category = "Negative"; if (!eventCategories[category]) { eventCategories[category] = []; } eventCategories[category].push(event); }); // Display events by category for (const [category, events] of Object.entries(eventCategories)) { if (events.length > 0) { await player.pm(`\n--- ${category.toUpperCase()} EVENTS ---`); let message = ""; events.forEach(event => { // Format primary sectors affected const primarySectors = event.sectorImpacts .filter(impact => Math.abs(impact.impact) >= 15) .map(impact => { const direction = impact.impact >= 0 ? "↑" : "↓"; return `${impact.sectorId} ${direction}${Math.abs(impact.impact)}%`; }) .join(", "); message += `${event.id}: ${event.name}\n`; message += ` ${event.description}\n`; if (primarySectors) { message += ` Major impacts: ${primarySectors}\n`; } message += "\n"; }); await player.pm(message); } } await player.pm("=== HOW TO USE ===\nUse `/triggerevent <EventName>` to trigger a specific event (e.g., `/triggerevent TECH_BOOM`)"); return; } // Find the requested event const eventId = args.EventName.toUpperCase(); const event = marketEvents.find(e => e.id === eventId); if (!event) { throw new TakaroUserError(`Event "${eventId}" not found. Use /triggerevent without parameters to see all available events.`); } // Get necessary variables for managing the event const [activeEventVar, eventStartTimeVar, eventCounterVar, cooldownCounterVar, eventDurationVar] = await Promise.all([ takaro.variable.variableControllerSearch({ filters: { key: ['active_market_event'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['event_start_time'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['event_execution_counter'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['event_cooldown_counter'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['current_event_duration'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }) ]); // Check if there's an active event we need to cancel let activeEventObj = null; if (activeEventVar.data.data.length > 0 && activeEventVar.data.data[0].value) { try { activeEventObj = JSON.parse(activeEventVar.data.data[0].value); if (activeEventObj && Object.keys(activeEventObj).length > 0) { await player.pm(`Cancelling active event "${activeEventObj.name}" to trigger new event.`); } } catch (e) { // Invalid event data, will be overwritten } } // Generate a random duration for the event const maxDuration = (mod.userConfig && mod.userConfig.defaultEventDuration) || 5; const randomDuration = Math.floor(Math.random() * maxDuration) + 1; // 1 to maxDuration // Update or create all event-related variables const updatePromises = []; // Save or update the random duration if (eventDurationVar.data.data.length > 0) { updatePromises.push( takaro.variable.variableControllerUpdate(eventDurationVar.data.data[0].id, { value: randomDuration.toString() }) ); } else { updatePromises.push( takaro.variable.variableControllerCreate({ key: 'current_event_duration', value: randomDuration.toString(), gameServerId, moduleId: mod.moduleId }) ); } // Reset the event counter to 0 if (eventCounterVar.data.data.length > 0) { updatePromises.push( takaro.variable.variableControllerUpdate(eventCounterVar.data.data[0].id, { value: '0' }) ); } else { updatePromises.push( takaro.variable.variableControllerCreate({ key: 'event_execution_counter', value: '0', gameServerId, moduleId: mod.moduleId }) ); } // Reset the cooldown counter if (cooldownCounterVar.data.data.length > 0) { updatePromises.push( takaro.variable.variableControllerUpdate(cooldownCounterVar.data.data[0].id, { value: '0' }) ); } else { updatePromises.push( takaro.variable.variableControllerCreate({ key: 'event_cooldown_counter', value: '0', gameServerId, moduleId: mod.moduleId }) ); } // Update active event if (activeEventVar.data.data.length > 0) { updatePromises.push( takaro.variable.variableControllerUpdate(activeEventVar.data.data[0].id, { value: JSON.stringify(event) }) ); } else { updatePromises.push( takaro.variable.variableControllerCreate({ key: 'active_market_event', value: JSON.stringify(event), gameServerId, moduleId: mod.moduleId }) ); } // Update event start time if (eventStartTimeVar.data.data.length > 0) { updatePromises.push( takaro.variable.variableControllerUpdate(eventStartTimeVar.data.data[0].id, { value: new Date().toISOString() }) ); } else { updatePromises.push( takaro.variable.variableControllerCreate({ key: 'event_start_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }) ); } // Update last event time const lastEventTimeVar = await takaro.variable.variableControllerSearch({ filters: { key: ['last_market_event_time'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); if (lastEventTimeVar.data.data.length > 0) { updatePromises.push( takaro.variable.variableControllerUpdate(lastEventTimeVar.data.data[0].id, { value: new Date().toISOString() }) ); } else { updatePromises.push( takaro.variable.variableControllerCreate({ key: 'last_market_event_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }) ); } // Wait for all updates to complete await Promise.all(updatePromises); // Check if there are online players to announce the event const onlinePlayers = await takaro.playerOnGameserver.playerOnGameServerControllerSearch({ filters: { gameServerId: [gameServerId], online: [true] } }); if (onlinePlayers.data.meta.total > 0) { // Format sector impacts for announcement let impactText = ""; for (const impact of event.sectorImpacts) { const direction = impact.impact >= 0 ? "↑" : "↓"; impactText += `\n${impact.sectorId}: ${direction} ${Math.abs(impact.impact)}%`; } const message = `🌍 BREAKING MARKET NEWS 🌍\n\n${event.name}\n${event.description}\n\nSector Impacts:${impactText}\n\nThis event will influence stock prices! Use /markets to see opportunities!`; // Split the message if it's too long const maxLength = 400; // Safe limit for most games for (let i = 0; i < message.length; i += maxLength) { await takaro.gameserver.gameServerControllerSendMessage(gameServerId, { message: message.substring(i, i + maxLength) }); } } await player.pm(`Successfully triggered the "${event.name}" market event for ${randomDuration} cycles!`); } catch (error) { // If something goes wrong, log it and let the player know console.log(`Error in triggerEvent: ${error.message}`); throw new TakaroUserError(`Error triggering event: ${error.message}`); } } // Initialize the market with stocks from the configuration async function initializeMarketIfNeeded(gameServerId, mod) { try { // Get stocks from config const configStocks = (mod.userConfig && mod.userConfig.stocks) || []; if (!configStocks || configStocks.length === 0) { console.log("Error: No stocks defined in configuration"); return; } // Map config stocks to our internal format const stocks = configStocks.map(stock => ({ id: stock.id, name: stock.name, sector: stock.sector, price: stock.initialPrice, volatility: stock.volatility / 100, // Convert percentage to decimal lastPrice: stock.initialPrice })); // Store the stocks data await takaro.variable.variableControllerCreate({ key: 'stock_market_data', value: JSON.stringify(stocks), gameServerId, moduleId: mod.moduleId }); // Initialize last event time to now await takaro.variable.variableControllerCreate({ key: 'last_market_event_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }); // Initialize event start time variable await takaro.variable.variableControllerCreate({ key: 'event_start_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }); // Initialize active event (empty string means no active event) await takaro.variable.variableControllerCreate({ key: 'active_market_event', value: '', gameServerId, moduleId: mod.moduleId }); // Mark market as initialized await takaro.variable.variableControllerCreate({ key: 'stock_market_initialized', value: 'true', gameServerId, moduleId: mod.moduleId }); // Announce market initialization to online players const onlinePlayers = await takaro.playerOnGameserver.playerOnGameServerControllerSearch({ filters: { gameServerId: [gameServerId], online: [true] } }); if (onlinePlayers.data.meta.total > 0) { const message = "📈 STOCK MARKET INITIALIZED 📈\n\nThe stock market is now open for trading! Use /markets to see available stocks and /buystock to start investing."; await takaro.gameserver.gameServerControllerSendMessage(gameServerId, { message }); } } catch (error) { console.log(`Error in initializeMarketIfNeeded: ${error.message}`); } } await main();
Cron jobs 2
Work the module runs on a schedule.
-
updatestockprices
Cron job source
import { takaro, data } from '@takaro/helpers'; async function main() { try { const { gameServerId, module: mod } = data; // Get current stock data const marketDataVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); // Check if market is initialized if (marketDataVar.data.data.length === 0) { await initializeMarketIfNeeded(gameServerId, mod); return; // Exit after initialization } // Get active market event const activeEventVar = await takaro.variable.variableControllerSearch({ filters: { key: ['active_market_event'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); // Get event start time const eventStartTimeVar = await takaro.variable.variableControllerSearch({ filters: { key: ['event_start_time'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); // Get event execution counter const eventCounterVar = await takaro.variable.variableControllerSearch({ filters: { key: ['event_execution_counter'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); // Get event cooldown counter const cooldownCounterVar = await takaro.variable.variableControllerSearch({ filters: { key: ['event_cooldown_counter'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); let eventCounter = 0; if (eventCounterVar.data.data.length > 0) { eventCounter = parseInt(eventCounterVar.data.data[0].value, 10); } let cooldownCounter = 0; if (cooldownCounterVar.data.data.length > 0) { cooldownCounter = parseInt(cooldownCounterVar.data.data[0].value, 10); } // Get event duration const eventDurationVar = await takaro.variable.variableControllerSearch({ filters: { key: ['current_event_duration'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); // Use stored random duration if available, otherwise use defaultEventDuration let eventDuration = (mod.userConfig && mod.userConfig.defaultEventDuration) || 5; if (eventDurationVar.data.data.length > 0) { eventDuration = parseInt(eventDurationVar.data.data[0].value, 10); } let activeEvent = null; if (activeEventVar.data.data.length > 0 && activeEventVar.data.data[0].value) { try { activeEvent = JSON.parse(activeEventVar.data.data[0].value); // If it's an empty string, treat as no active event if (!activeEvent || Object.keys(activeEvent).length === 0) { activeEvent = null; } } catch (e) { // In case of parsing error, consider no active event activeEvent = null; } } // Check if the current event should end and increment counter if (activeEvent) { // Increment the event execution counter eventCounter++; // Update or create the counter if (eventCounterVar.data.data.length > 0) { await takaro.variable.variableControllerUpdate(eventCounterVar.data.data[0].id, { value: eventCounter.toString() }); } else { await takaro.variable.variableControllerCreate({ key: 'event_execution_counter', value: eventCounter.toString(), gameServerId, moduleId: mod.moduleId }); } // End the event if counter reached duration if (eventCounter >= eventDuration) { // Clear the active event if (activeEventVar.data.data.length > 0) { await takaro.variable.variableControllerUpdate(activeEventVar.data.data[0].id, { value: '' }); } // Reset counter to 0 if (eventCounterVar.data.data.length > 0) { await takaro.variable.variableControllerUpdate(eventCounterVar.data.data[0].id, { value: '0' }); } // Reset cooldown counter to 0 to begin cooldown period if (cooldownCounterVar.data.data.length > 0) { await takaro.variable.variableControllerUpdate(cooldownCounterVar.data.data[0].id, { value: '0' }); } else { await takaro.variable.variableControllerCreate({ key: 'event_cooldown_counter', value: '0', gameServerId, moduleId: mod.moduleId }); } // Announce the end of the event to all online players const onlinePlayers = await takaro.playerOnGameserver.playerOnGameServerControllerSearch({ filters: { gameServerId: [gameServerId], online: [true] } }); if (onlinePlayers.data.meta.total > 0) { const message = `📈 MARKET UPDATE 📉\n\nThe "${activeEvent.name}" event has ended. Markets are returning to normal conditions.`; await takaro.gameserver.gameServerControllerSendMessage(gameServerId, { message: message }); } // Reset active event activeEvent = null; } } else { // No active event, increment cooldown counter cooldownCounter++; // Update or create the cooldown counter if (cooldownCounterVar.data.data.length > 0) { await takaro.variable.variableControllerUpdate(cooldownCounterVar.data.data[0].id, { value: cooldownCounter.toString() }); } else { await takaro.variable.variableControllerCreate({ key: 'event_cooldown_counter', value: cooldownCounter.toString(), gameServerId, moduleId: mod.moduleId }); } } // Get last event time for event frequency calculation const lastEventTimeVar = await takaro.variable.variableControllerSearch({ filters: { key: ['last_market_event_time'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); const eventFrequency = (mod.userConfig && mod.userConfig.eventFrequency) || 10; let shouldTriggerEvent = false; // Only trigger a new event if: // 1. There's no active event // 2. We have passed the cooldown period (cooldownCounter >= eventFrequency) if (!activeEvent && cooldownCounter >= eventFrequency) { // Base chance to trigger an event shouldTriggerEvent = Math.random() < 0.5; // 50% chance // Force an event if we're well past the cooldown period (2x frequency) if (cooldownCounter >= eventFrequency * 2) { shouldTriggerEvent = true; } } // If we should trigger a new event, select a random one if (shouldTriggerEvent) { const events = (mod.userConfig && mod.userConfig.marketEvents) || []; if (events.length > 0) { const randomEvent = events[Math.floor(Math.random() * events.length)]; activeEvent = randomEvent; // Generate a random duration between 1 and defaultEventDuration const maxDuration = (mod.userConfig && mod.userConfig.defaultEventDuration) || 5; const randomDuration = Math.floor(Math.random() * maxDuration) + 1; // 1 to maxDuration // Save the random duration if (eventDurationVar.data.data.length > 0) { await takaro.variable.variableControllerUpdate(eventDurationVar.data.data[0].id, { value: randomDuration.toString() }); } else { await takaro.variable.variableControllerCreate({ key: 'current_event_duration', value: randomDuration.toString(), gameServerId, moduleId: mod.moduleId }); } // Reset the event counter to 0 if (eventCounterVar.data.data.length > 0) { await takaro.variable.variableControllerUpdate(eventCounterVar.data.data[0].id, { value: '0' }); } else { await takaro.variable.variableControllerCreate({ key: 'event_execution_counter', value: '0', gameServerId, moduleId: mod.moduleId }); } // Reset the cooldown counter if (cooldownCounterVar.data.data.length > 0) { await takaro.variable.variableControllerUpdate(cooldownCounterVar.data.data[0].id, { value: '0' }); } // Update active event if (activeEventVar.data.data.length > 0) { await takaro.variable.variableControllerUpdate(activeEventVar.data.data[0].id, { value: JSON.stringify(activeEvent) }); } else { await takaro.variable.variableControllerCreate({ key: 'active_market_event', value: JSON.stringify(activeEvent), gameServerId, moduleId: mod.moduleId }); } // Update event start time if (eventStartTimeVar.data.data.length > 0) { await takaro.variable.variableControllerUpdate(eventStartTimeVar.data.data[0].id, { value: new Date().toISOString() }); } else { await takaro.variable.variableControllerCreate({ key: 'event_start_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }); } // Update last event time if (lastEventTimeVar.data.data.length > 0) { await takaro.variable.variableControllerUpdate(lastEventTimeVar.data.data[0].id, { value: new Date().toISOString() }); } else { await takaro.variable.variableControllerCreate({ key: 'last_market_event_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }); } // Announce the event to all online players const onlinePlayers = await takaro.playerOnGameserver.playerOnGameServerControllerSearch({ filters: { gameServerId: [gameServerId], online: [true] } }); if (onlinePlayers.data.meta.total > 0) { // Format sector impacts for announcement let impactText = ""; for (const impact of activeEvent.sectorImpacts) { const direction = impact.impact >= 0 ? "↑" : "↓"; impactText += `\n${impact.sectorId}: ${direction} ${Math.abs(impact.impact)}%`; } const message = `🌍 BREAKING MARKET NEWS 🌍\n\n${activeEvent.name}\n${activeEvent.description}\n\nSector Impacts:${impactText}\n\nThis event will influence stock prices! Use /market to see opportunities!`; // Split the message if it's too long const maxLength = 400; // Safe limit for most games for (let i = 0; i < message.length; i += maxLength) { await takaro.gameserver.gameServerControllerSendMessage(gameServerId, { message: message.substring(i, i + maxLength) }); } } } } // Get stocks if (!marketDataVar.data.data[0] || !marketDataVar.data.data[0].value) { // Re-initialize market if value is missing await initializeMarketIfNeeded(gameServerId, mod); return; } let stocks; try { stocks = JSON.parse(marketDataVar.data.data[0].value); if (!Array.isArray(stocks)) { throw new Error("Parsed stocks data is not an array"); } } catch (e) { // Re-initialize market if data is corrupt await initializeMarketIfNeeded(gameServerId, mod); return; } const significantChanges = []; // Update each stock price stocks.forEach(stock => { // Save the current price as lastPrice for change tracking if (!stock.lastPrice) { stock.lastPrice = stock.price; } else { stock.lastPrice = stock.price; } // Calculate base price change based on volatility // Using a more normalized random approach // Math.random() * 2 - 1 gives a value between -1 and 1 const randomFactor = Math.random() * 2 - 1; const baseChangePercent = randomFactor * stock.volatility; let totalChangePercent = baseChangePercent; // Apply active event effects if any if (activeEvent) { const sectorImpact = activeEvent.sectorImpacts.find(impact => impact.sectorId === stock.sector); if (sectorImpact) { // Convert impact percentage to decimal and apply a random factor // to create varied effects within each sector const eventImpactPercentage = sectorImpact.impact; // This is already a percentage const eventImpactDecimal = eventImpactPercentage / 100; // Convert to decimal const randomImpactFactor = 0.5 + Math.random(); // Between 0.5 and 1.5 const eventImpact = eventImpactDecimal * randomImpactFactor; // Add the event impact to the total change percent totalChangePercent += eventImpact; } } // Apply the price change const oldPrice = stock.price; const priceChange = stock.price * totalChangePercent; stock.price = Math.max(1, stock.price + priceChange); // Check if this is a significant change const changeThreshold = ((mod.userConfig && mod.userConfig.priceAlertThreshold) || 10) / 100; const percentChange = (stock.price - stock.lastPrice) / stock.lastPrice; if (Math.abs(percentChange) > changeThreshold) { significantChanges.push({ ...stock, changePercent: percentChange * 100 }); } }); // Save updated prices await takaro.variable.variableControllerUpdate(marketDataVar.data.data[0].id, { value: JSON.stringify(stocks) }); // Broadcast major changes to all players if (significantChanges.length > 0) { const onlinePlayers = await takaro.playerOnGameserver.playerOnGameServerControllerSearch({ filters: { gameServerId: [gameServerId], online: [true] } }); if (onlinePlayers.data.meta.total > 0) { // Sort by absolute change percentage significantChanges.sort((a, b) => Math.abs(b.changePercent) - Math.abs(a.changePercent)); // Take top 3 most significant changes const topChanges = significantChanges.slice(0, Math.min(3, significantChanges.length)); // Formulate message about big market changes let message = "📊 STOCK MARKET ALERT 📊\n"; topChanges.forEach(stock => { const changeDir = stock.changePercent > 0 ? "up" : "down"; const changePercent = Math.abs(Math.round(stock.changePercent)); message += `${stock.id} (${stock.sector}): ${changeDir} ${changePercent}% to $${Math.round(stock.price)}!\n`; }); if (activeEvent) { message += `\nCurrent market event: ${activeEvent.name}`; } // Send the message await takaro.gameserver.gameServerControllerSendMessage(gameServerId, { message }); } } } catch (error) { // If something goes wrong, log it console.log(`Error in updateStockPrices: ${error.message}`); } } // Initialize the market with stocks from the configuration async function initializeMarketIfNeeded(gameServerId, mod) { try { // Get stocks from config const configStocks = (mod.userConfig && mod.userConfig.stocks) || []; if (!configStocks || configStocks.length === 0) { console.log("Error: No stocks defined in configuration"); return; } // Check for each required variable first const [marketDataVar, lastEventTimeVar, eventStartTimeVar, activeEventVar, marketInitVar] = await Promise.all([ takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['last_market_event_time'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['event_start_time'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['active_market_event'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_initialized'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }) ]); // Map config stocks to our internal format const stocks = configStocks.map(stock => ({ id: stock.id, name: stock.name, sector: stock.sector, price: stock.initialPrice, volatility: stock.volatility / 100, // Convert percentage to decimal lastPrice: stock.initialPrice })); // Create each variable only if it doesn't exist already const createPromises = []; // Store the stocks data if it doesn't exist if (marketDataVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'stock_market_data', value: JSON.stringify(stocks), gameServerId, moduleId: mod.moduleId }) ); } // Initialize last event time if it doesn't exist if (lastEventTimeVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'last_market_event_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }) ); } // Initialize event start time variable if it doesn't exist if (eventStartTimeVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'event_start_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }) ); } // Initialize active event if it doesn't exist if (activeEventVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'active_market_event', value: '', gameServerId, moduleId: mod.moduleId }) ); } // Mark market as initialized if not already marked if (marketInitVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'stock_market_initialized', value: 'true', gameServerId, moduleId: mod.moduleId }) ); } // Wait for all creation promises to complete if (createPromises.length > 0) { await Promise.allSettled(createPromises); } // Only announce if we had to create at least the stock data (indicating a new market) if (marketDataVar.data.data.length === 0) { // Announce market initialization to online players const onlinePlayers = await takaro.playerOnGameserver.playerOnGameServerControllerSearch({ filters: { gameServerId: [gameServerId], online: [true] } }); if (onlinePlayers.data.meta.total > 0) { const message = "📈 STOCK MARKET INITIALIZED 📈\n\nThe stock market is now open for trading! Use /market to see available stocks and /buy to start investing."; await takaro.gameserver.gameServerControllerSendMessage(gameServerId, { message }); } } } catch (error) { // Log the error but don't throw, so the cronjob can continue console.log(`Error in initializeMarketIfNeeded: ${error.message}`); } } await main(); -
marketnews
gives you market news
Cron job source
import { takaro, data } from '@takaro/helpers'; async function main() { const { gameServerId, module: mod } = data; // Check for online players const currentPlayers = (await takaro.playerOnGameserver.playerOnGameServerControllerSearch({ filters: { gameServerId: [gameServerId], online: [true] } })).data.meta; if (currentPlayers.total === 0) { takaro.log.info('Skipping daily market report: No players online.'); return; // No players online, skip the market news } // Get current stock data (which should include lastPrice) const marketDataVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); if (marketDataVar.data.data.length === 0) { takaro.log.warn('Skipping daily market report: Market data variable not found.'); return; // Market not initialized yet } let stocks; try { stocks = JSON.parse(marketDataVar.data.data[0].value); if (!Array.isArray(stocks)) { throw new Error('Parsed market data is not an array.'); } } catch (error) { takaro.log.error(`Failed to parse stock_market_data: ${error}`); return; // Invalid market data } // Get active market event const activeEventVar = await takaro.variable.variableControllerSearch({ filters: { key: ['active_market_event'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); let activeEvent = null; if (activeEventVar.data.data.length > 0 && activeEventVar.data.data[0].value) { try { activeEvent = JSON.parse(activeEventVar.data.data[0].value); } catch (error) { takaro.log.warn(`Failed to parse active_market_event: ${error}`); // Continue without event info if parsing fails } } // --- Message 1: Header and active event --- let message1 = "==== DAILY MARKET REPORT ====\n"; if (activeEvent && activeEvent.name && activeEvent.description) { message1 += `\n🌍 ACTIVE EVENT: ${activeEvent.name} 🌍\n${activeEvent.description}\n`; } else { message1 += '\nNo active market events today.\n'; } // Send first part of the report await takaro.gameserver.gameServerControllerSendMessage(gameServerId, { message: message1 }).catch(err => takaro.log.error(`Failed to send message 1: ${err}`)); // --- Calculate stock performance using stock.price and stock.lastPrice --- const stocksWithPerformance = stocks.map(stock => { let change = 0; let percentChange = 0; let changeSymbol = '→'; // Default: No change or insufficient data // Ensure required fields exist and lastPrice is a valid number > 0 for percentage calculation if (typeof stock.price === 'number' && typeof stock.lastPrice === 'number' && stock.lastPrice !== 0) { change = stock.price - stock.lastPrice; percentChange = (change / stock.lastPrice) * 100; if (percentChange > 0.05) { // Use a small threshold to avoid 'noise' changeSymbol = '↑'; } else if (percentChange < -0.05) { changeSymbol = '↓'; } } else if (typeof stock.price === 'number' && stock.lastPrice === undefined) { // Handle case where lastPrice might not exist (e.g., new stock) changeSymbol = '🆕'; // Indicate 'New' or similar percentChange = 0; // Or handle as needed } // If lastPrice is 0, or types are wrong, change/percentChange remain 0, symbol remains '→' return { ...stock, change, // Absolute change percentChange, // Percentage change changeSymbol // Visual indicator }; }); // --- Group by sector --- const sectorPerformance = {}; stocksWithPerformance.forEach(stock => { const sectorId = stock.sector || 'Uncategorized'; // Default sector if missing if (!sectorPerformance[sectorId]) { sectorPerformance[sectorId] = { stocks: [], totalPercentChange: 0, // Sum percentages for averaging count: 0 }; } sectorPerformance[sectorId].stocks.push(stock); // Only include stocks with valid percentage change in the average if (typeof stock.percentChange === 'number' && isFinite(stock.percentChange)) { sectorPerformance[sectorId].totalPercentChange += stock.percentChange; sectorPerformance[sectorId].count++; } }); // --- Calculate average sector performance --- for (const sectorId in sectorPerformance) { const sectorData = sectorPerformance[sectorId]; if (sectorData.count > 0) { sectorData.avgPerformance = sectorData.totalPercentChange / sectorData.count; } else { sectorData.avgPerformance = 0; // Avoid division by zero if no stocks had valid changes } } // Sort sectors by average performance const sortedSectors = Object.entries(sectorPerformance) .sort(([, a], [, b]) => b.avgPerformance - a.avgPerformance); // --- Message 2: Sector performance --- let message2 = "\n=== SECTOR PERFORMANCE ===\n"; if (sortedSectors.length > 0) { sortedSectors.forEach(([sectorId, data]) => { const avgSymbol = data.avgPerformance > 0.05 ? '↑' : data.avgPerformance < -0.05 ? '↓' : '→'; message2 += `${sectorId}: ${avgSymbol} ${Math.abs(data.avgPerformance).toFixed(1)}%\n`; // Use toFixed(1) like command }); } else { message2 += "No sector performance data available.\n"; } // Send sector performance report await takaro.gameserver.gameServerControllerSendMessage(gameServerId, { message: message2 }).catch(err => takaro.log.error(`Failed to send message 2: ${err}`)); // --- Message 3: Top and Worst performers --- // Filter out stocks where percentChange couldn't be calculated properly before sorting const validPerformers = stocksWithPerformance.filter(s => typeof s.percentChange === 'number' && isFinite(s.percentChange)); // Sort valid performers validPerformers.sort((a, b) => b.percentChange - a.percentChange); let message3 = "\n🔥 TOP PERFORMERS 🔥\n"; if (validPerformers.length > 0) { for (let i = 0; i < Math.min(3, validPerformers.length); i++) { const stock = validPerformers[i]; // Use the pre-calculated changeSymbol based on percentChange message3 += `${stock.id}: $${Math.round(stock.price)} ${stock.changeSymbol} ${Math.abs(stock.percentChange).toFixed(1)}%\n`; } } else { message3 += "No top performers today.\n"; } message3 += "\n📉 WORST PERFORMERS 📉\n"; if (validPerformers.length > 0) { // Sort for worst (ascending order) - no need to create a new sorted array if we just reverse iteration const worstStartIndex = Math.max(0, validPerformers.length - 3); for (let i = validPerformers.length - 1; i >= worstStartIndex; i--) { const stock = validPerformers[i]; // Use the pre-calculated changeSymbol based on percentChange message3 += `${stock.id}: $${Math.round(stock.price)} ${stock.changeSymbol} ${Math.abs(stock.percentChange).toFixed(1)}%\n`; } } else { message3 += "No worst performers today.\n"; } // Send top/worst performers report await takaro.gameserver.gameServerControllerSendMessage(gameServerId, { message: message3 }).catch(err => takaro.log.error(`Failed to send message 3: ${err}`)); // --- IMPORTANT: REMOVED the update of 'stock_market_yesterday' --- // This script now assumes 'stock_market_data' contains 'lastPrice'. // The responsibility of updating 'lastPrice' and the new 'price' // must lie in another script/process that runs *before* this report. takaro.log.info('Daily market report sent successfully.'); } await main();
Permissions 4
Roles you can grant to decide who may use what.
-
Use Stock Market
Allows the player to view market prices and their portfolio
-
Trade Stocks
Allows the player to buy and sell stocks
-
Stock Broker
VIP status that reduces transaction fees
-
Trigger Market Events
Allows admins to manually trigger market events