GeneralstockMarket

  • community
  • economy
  • by limon
  • Takaro v0.0.24
  • all
Version
View export JSON

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:

Market Summary

  • 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.

Market Events

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.

Buy Stock Example

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

Sell Stock Example

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.

Portfolio View

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.

Stock Information
Industry View

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.

SettingTypeDefaultDescription
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

    ArgumentTypeDefaultHelp
    stock string The stock ticker symbol
    amount number 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

    ArgumentTypeDefaultHelp
    stock string The stock ticker symbol
    amount number 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.

    ArgumentTypeDefaultHelp
    Industry string 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.

    ArgumentTypeDefaultHelp
    ticker string 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.

    ArgumentTypeDefaultHelp
    EventName string 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