StockMarket

  • community
  • economy
  • by limon
  • Takaro v0.0.29
  • 7 Days to Die
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 (Resource Extraction, Manufacturing, Defense, 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 "Horde Night Approaching" or "Societal Collapse Setback" 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.

Market Alerts

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.

Configuration
Sectors

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

Who says the apocalypse can't have a thriving economy? Now players can stress about both zombie hordes AND their Duke's Casino Tokens dropping 40% after a market event! As one trader put it: "I used to check my 401K daily, and now I check my AMMO stock before every Blood Moon. Some habits die harder than zombies."

Configuration 9

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

SettingTypeDefaultDescription
sectors Wasteland Economic Sectors array [{"id":"VERY_LOW_RISK","name":"Very Low Risk"},{"id":"LOW_RISK","name":"Low Risk"},{"id":"MEDIUM_RISK","name":"Medium Risk"},{"id":"HIGH_RISK","name":"High Risk"},{"id":"VERY_HIGH_RISK","name":"Very High Risk"}] Define economic sectors relevant to survival in Navezgane
stocks Wasteland Stocks array [{"id":"NMC","name":"Navezgane Mining Co.","sector":"HIGH_RISK","initialPrice":50,"volatility":15},{"id":"NIOM","name":"Navezgane Iron Ore Mining Co.","sector":"HIGH_RISK","initialPrice":55,"volatility":16},{"id":"NLMC","name":"Navezgane Lead Mining Co.","sector":"MEDIUM_RISK","initialPrice":45,"volatility":14},{"id":"WOOD","name":"Wasteland Lumberjacks","sector":"LOW_RISK","initialPrice":20,"volatility":8},{"id":"SHALE","name":"Oil Shale Extractors","sector":"VERY_HIGH_RISK","initialPrice":40,"volatility":45},{"id":"FORGE","name":"Forge Ahead Metals","sector":"MEDIUM_RISK","initialPrice":75,"volatility":12},{"id":"TOOLS","name":"Working Stiff Tools","sector":"LOW_RISK","initialPrice":60,"volatility":10},{"id":"AIA","name":"A1 Auto","sector":"MEDIUM_RISK","initialPrice":40,"volatility":14},{"id":"AMC","name":"Atlas Metal Company","sector":"MEDIUM_RISK","initialPrice":65,"volatility":13},{"id":"COC","name":"COC Chemicals","sector":"VERY_HIGH_RISK","initialPrice":55,"volatility":50},{"id":"MECH","name":"JunkTech Robotics","sector":"HIGH_RISK","initialPrice":110,"volatility":25},{"id":"BUILD","name":"Barricade Builders","sector":"LOW_RISK","initialPrice":45,"volatility":9},{"id":"ZBI","name":"Zombie Bashers Inc.","sector":"HIGH_RISK","initialPrice":85,"volatility":17},{"id":"TTC","name":"Turret Tracker Co.","sector":"VERY_HIGH_RISK","initialPrice":125,"volatility":60},{"id":"AMMO","name":"AmmoNation Surplus","sector":"VERY_HIGH_RISK","initialPrice":90,"volatility":35},{"id":"SHM","name":"Shotgun Messiah","sector":"VERY_HIGH_RISK","initialPrice":120,"volatility":30},{"id":"SHAM","name":"Shamway Foods","sector":"VERY_LOW_RISK","initialPrice":30,"volatility":5},{"id":"SSHM","name":"Super Shamway","sector":"VERY_LOW_RISK","initialPrice":32,"volatility":4},{"id":"MEDS","name":"Pop-N-Pills Medical","sector":"VERY_HIGH_RISK","initialPrice":100,"volatility":48},{"id":"SAVG","name":"Savage Country","sector":"LOW_RISK","initialPrice":35,"volatility":6},{"id":"CRKB","name":"Crack-A-Book","sector":"HIGH_RISK","initialPrice":80,"volatility":16},{"id":"TRADE","name":"Trader Guild Network","sector":"VERY_HIGH_RISK","initialPrice":150,"volatility":40},{"id":"PNG","name":"Pass-N-Gas","sector":"MEDIUM_RISK","initialPrice":50,"volatility":11},{"id":"BOB","name":"Trader Bob's","sector":"LOW_RISK","initialPrice":160,"volatility":9},{"id":"HUGH","name":"Trader Hugh's","sector":"MEDIUM_RISK","initialPrice":170,"volatility":12},{"id":"JEN","name":"Trader Jen's","sector":"LOW_RISK","initialPrice":180,"volatility":10},{"id":"JOEL","name":"Trader Joel's","sector":"LOW_RISK","initialPrice":175,"volatility":11},{"id":"REKT","name":"Trader Rekt's","sector":"LOW_RISK","initialPrice":165,"volatility":6},{"id":"DUKES","name":"Duke's Casino Tokens","sector":"VERY_LOW_RISK","initialPrice":1,"volatility":3}] List of stocks representing Navezgane entities and resources
marketEvents Wasteland Events array [{"id":"HORDE_INCOMING","name":"Horde Night Approaching","description":"Increased Zed activity signals the Blood Moon. Very High and High Risk defense stocks soar, while Very Low Risk stocks see minimal impact.","sectorImpacts":[{"sectorId":"VERY_HIGH_RISK","impact":50},{"sectorId":"HIGH_RISK","impact":30},{"sectorId":"MEDIUM_RISK","impact":15},{"sectorId":"LOW_RISK","impact":5},{"sectorId":"VERY_LOW_RISK","impact":1}]},{"id":"STABLE_ECONOMY","name":"Period of Stability","description":"A rare period of peace has settled over the wasteland. Very Low and Low Risk assets flourish, with higher risk stocks seeing less significant gains.","sectorImpacts":[{"sectorId":"VERY_LOW_RISK","impact":15},{"sectorId":"LOW_RISK","impact":10},{"sectorId":"MEDIUM_RISK","impact":5},{"sectorId":"HIGH_RISK","impact":2},{"sectorId":"VERY_HIGH_RISK","impact":1}]},{"id":"GENERAL_PANIC","name":"General Market Panic","description":"A major setback reminds everyone how fragile survival is. Fear grips the entire market, causing higher volatility sectors to plummet.","sectorImpacts":[{"sectorId":"VERY_HIGH_RISK","impact":-90},{"sectorId":"HIGH_RISK","impact":-70},{"sectorId":"MEDIUM_RISK","impact":-40},{"sectorId":"LOW_RISK","impact":-15},{"sectorId":"VERY_LOW_RISK","impact":-5}]},{"id":"ADVANCED_WEAPONRY_COLLAPSE","name":"Turret AI Virus","description":"A malicious code is spreading through advanced turret systems, causing them to malfunction violently. Very High Risk weaponry stocks are obliterated.","sectorImpacts":[{"sectorId":"VERY_HIGH_RISK","impact":-99},{"sectorId":"HIGH_RISK","impact":-20}]},{"id":"VOLATILE_CHEMICALS_COLLAPSE","name":"Irreversible Contamination","description":"A major chemical disaster has occurred. Very High Risk chemical stocks are worthless, impacting related sectors.","sectorImpacts":[{"sectorId":"VERY_HIGH_RISK","impact":-99},{"sectorId":"HIGH_RISK","impact":-25},{"sectorId":"MEDIUM_RISK","impact":-10}]},{"id":"FOSSIL_FUELS_COLLAPSE","name":"Clean Energy Breakthrough","description":"A new energy source has been discovered. Very High Risk fossil fuel stocks become obsolete, while other sectors see minor adjustments.","sectorImpacts":[{"sectorId":"VERY_HIGH_RISK","impact":-99},{"sectorId":"MEDIUM_RISK","impact":10},{"sectorId":"LOW_RISK","impact":5}]},{"id":"PHARMACEUTICALS_COLLAPSE","name":"The Cure is Real","description":"A cure for the zombie infection has been found! Very High Risk pharmaceutical stocks are wiped out, and even low-risk survival goods see a hit.","sectorImpacts":[{"sectorId":"VERY_HIGH_RISK","impact":-99},{"sectorId":"HIGH_RISK","impact":-30},{"sectorId":"MEDIUM_RISK","impact":-15},{"sectorId":"LOW_RISK","impact":-10},{"sectorId":"VERY_LOW_RISK","impact":-5}]},{"id":"GUILD_OPERATIONS_COLLAPSE","name":"Trader Guild Dissolved","description":"The Trader's Guild has been formally dissolved. Very High Risk trade operation stocks become worthless, affecting overall trade.","sectorImpacts":[{"sectorId":"VERY_HIGH_RISK","impact":-99},{"sectorId":"HIGH_RISK","impact":-40},{"sectorId":"MEDIUM_RISK","impact":-20},{"sectorId":"LOW_RISK","impact":-10}]},{"id":"AMMUNITION_DEPLETION","name":"Ammunition Shortage","description":"A major supply chain disruption has caused ammunition stockpiles to dwindle. Very High Risk ammunition stocks plummet.","sectorImpacts":[{"sectorId":"VERY_HIGH_RISK","impact":-99},{"sectorId":"HIGH_RISK","impact":-30},{"sectorId":"MEDIUM_RISK","impact":-15}]},{"id":"RESOURCE_BOOM","name":"Resource Discovery","description":"A massive new ore deposit has been found! All resource-related stocks are surging, with higher risk ones seeing the biggest gains.","sectorImpacts":[{"sectorId":"VERY_HIGH_RISK","impact":60},{"sectorId":"HIGH_RISK","impact":40},{"sectorId":"MEDIUM_RISK","impact":20},{"sectorId":"LOW_RISK","impact":10}]},{"id":"MANUFACTURING_SURGE","name":"Innovation Breakthrough","description":"New manufacturing techniques have greatly increased production efficiency. Manufacturing stocks are up across the board.","sectorImpacts":[{"sectorId":"VERY_HIGH_RISK","impact":40},{"sectorId":"HIGH_RISK","impact":30},{"sectorId":"MEDIUM_RISK","impact":20},{"sectorId":"LOW_RISK","impact":10},{"sectorId":"VERY_LOW_RISK","impact":5}]},{"id":"TRADE_ROUTE_OPENED","name":"New Trade Route Established","description":"A secure trade route has been opened, boosting commerce. Trade-related stocks are seeing significant gains.","sectorImpacts":[{"sectorId":"VERY_HIGH_RISK","impact":50},{"sectorId":"HIGH_RISK","impact":30},{"sectorId":"MEDIUM_RISK","impact":20},{"sectorId":"LOW_RISK","impact":15},{"sectorId":"VERY_LOW_RISK","impact":10}]}] Events impacting the Navezgane economy
eventFrequency Event Frequency number 8 Average number of update cycles between market events (0 to disable). Depends on mod's update frequency (e.g., per in-game hour?).
defaultEventDuration Default Event Duration number 3 Default number of update cycles an event lasts.
transactionFee Transaction Fee (Dukes Tax) number 3 Percentage fee charged on all buy/sell transactions (5 = 5%)
vipDiscount Trader's Favor Discount number 50 Percentage discount on transaction fees for players with 'STOCK_MARKET_BROKER' permission (50 = 50%)
priceAlertThreshold Price Alert Threshold number 15 Percentage change that triggers a market alert (15 = 15%)
marketNewsChannelId Discord Channel ID (for Market News) string "" The ID of the Discord channel where market news messages should be posted. Leave empty to disable.
Raw config schema
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "sectors": {
      "type": "array",
      "title": "Wasteland Economic Sectors",
      "description": "Define economic sectors relevant to survival in Navezgane",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Sector ID (e.g., VERY_LOW_RISK)"
          },
          "name": {
            "type": "string",
            "description": "Sector name (e.g., Very Low Risk)"
          }
        }
      },
      "default": [
        {
          "id": "VERY_LOW_RISK",
          "name": "Very Low Risk"
        },
        {
          "id": "LOW_RISK",
          "name": "Low Risk"
        },
        {
          "id": "MEDIUM_RISK",
          "name": "Medium Risk"
        },
        {
          "id": "HIGH_RISK",
          "name": "High Risk"
        },
        {
          "id": "VERY_HIGH_RISK",
          "name": "Very High Risk"
        }
      ]
    },
    "stocks": {
      "type": "array",
      "title": "Wasteland Stocks",
      "description": "List of stocks representing Navezgane entities and resources",
      "items": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stock ticker symbol (e.g., NMC)"
          },
          "name": {
            "type": "string",
            "description": "Entity/Resource name"
          },
          "sector": {
            "type": "string",
            "description": "Sector ID this stock belongs to"
          },
          "initialPrice": {
            "type": "number",
            "description": "Starting price in Dukes",
            "minimum": 1
          },
          "volatility": {
            "type": "number",
            "description": "Base volatility percentage (5 = 5%)",
            "minimum": 1,
            "maximum": 60
          }
        }
      },
      "default": [
        {
          "id": "NMC",
          "name": "Navezgane Mining Co.",
          "sector": "HIGH_RISK",
          "initialPrice": 50,
          "volatility": 15
        },
        {
          "id": "NIOM",
          "name": "Navezgane Iron Ore Mining Co.",
          "sector": "HIGH_RISK",
          "initialPrice": 55,
          "volatility": 16
        },
        {
          "id": "NLMC",
          "name": "Navezgane Lead Mining Co.",
          "sector": "MEDIUM_RISK",
          "initialPrice": 45,
          "volatility": 14
        },
        {
          "id": "WOOD",
          "name": "Wasteland Lumberjacks",
          "sector": "LOW_RISK",
          "initialPrice": 20,
          "volatility": 8
        },
        {
          "id": "SHALE",
          "name": "Oil Shale Extractors",
          "sector": "VERY_HIGH_RISK",
          "initialPrice": 40,
          "volatility": 45
        },
        {
          "id": "FORGE",
          "name": "Forge Ahead Metals",
          "sector": "MEDIUM_RISK",
          "initialPrice": 75,
          "volatility": 12
        },
        {
          "id": "TOOLS",
          "name": "Working Stiff Tools",
          "sector": "LOW_RISK",
          "initialPrice": 60,
          "volatility": 10
        },
        {
          "id": "AIA",
          "name": "A1 Auto",
          "sector": "MEDIUM_RISK",
          "initialPrice": 40,
          "volatility": 14
        },
        {
          "id": "AMC",
          "name": "Atlas Metal Company",
          "sector": "MEDIUM_RISK",
          "initialPrice": 65,
          "volatility": 13
        },
        {
          "id": "COC",
          "name": "COC Chemicals",
          "sector": "VERY_HIGH_RISK",
          "initialPrice": 55,
          "volatility": 50
        },
        {
          "id": "MECH",
          "name": "JunkTech Robotics",
          "sector": "HIGH_RISK",
          "initialPrice": 110,
          "volatility": 25
        },
        {
          "id": "BUILD",
          "name": "Barricade Builders",
          "sector": "LOW_RISK",
          "initialPrice": 45,
          "volatility": 9
        },
        {
          "id": "ZBI",
          "name": "Zombie Bashers Inc.",
          "sector": "HIGH_RISK",
          "initialPrice": 85,
          "volatility": 17
        },
        {
          "id": "TTC",
          "name": "Turret Tracker Co.",
          "sector": "VERY_HIGH_RISK",
          "initialPrice": 125,
          "volatility": 60
        },
        {
          "id": "AMMO",
          "name": "AmmoNation Surplus",
          "sector": "VERY_HIGH_RISK",
          "initialPrice": 90,
          "volatility": 35
        },
        {
          "id": "SHM",
          "name": "Shotgun Messiah",
          "sector": "VERY_HIGH_RISK",
          "initialPrice": 120,
          "volatility": 30
        },
        {
          "id": "SHAM",
          "name": "Shamway Foods",
          "sector": "VERY_LOW_RISK",
          "initialPrice": 30,
          "volatility": 5
        },
        {
          "id": "SSHM",
          "name": "Super Shamway",
          "sector": "VERY_LOW_RISK",
          "initialPrice": 32,
          "volatility": 4
        },
        {
          "id": "MEDS",
          "name": "Pop-N-Pills Medical",
          "sector": "VERY_HIGH_RISK",
          "initialPrice": 100,
          "volatility": 48
        },
        {
          "id": "SAVG",
          "name": "Savage Country",
          "sector": "LOW_RISK",
          "initialPrice": 35,
          "volatility": 6
        },
        {
          "id": "CRKB",
          "name": "Crack-A-Book",
          "sector": "HIGH_RISK",
          "initialPrice": 80,
          "volatility": 16
        },
        {
          "id": "TRADE",
          "name": "Trader Guild Network",
          "sector": "VERY_HIGH_RISK",
          "initialPrice": 150,
          "volatility": 40
        },
        {
          "id": "PNG",
          "name": "Pass-N-Gas",
          "sector": "MEDIUM_RISK",
          "initialPrice": 50,
          "volatility": 11
        },
        {
          "id": "BOB",
          "name": "Trader Bob's",
          "sector": "LOW_RISK",
          "initialPrice": 160,
          "volatility": 9
        },
        {
          "id": "HUGH",
          "name": "Trader Hugh's",
          "sector": "MEDIUM_RISK",
          "initialPrice": 170,
          "volatility": 12
        },
        {
          "id": "JEN",
          "name": "Trader Jen's",
          "sector": "LOW_RISK",
          "initialPrice": 180,
          "volatility": 10
        },
        {
          "id": "JOEL",
          "name": "Trader Joel's",
          "sector": "LOW_RISK",
          "initialPrice": 175,
          "volatility": 11
        },
        {
          "id": "REKT",
          "name": "Trader Rekt's",
          "sector": "LOW_RISK",
          "initialPrice": 165,
          "volatility": 6
        },
        {
          "id": "DUKES",
          "name": "Duke's Casino Tokens",
          "sector": "VERY_LOW_RISK",
          "initialPrice": 1,
          "volatility": 3
        }
      ]
    },
    "marketEvents": {
      "type": "array",
      "title": "Wasteland Events",
      "description": "Events impacting the Navezgane economy",
      "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 the event's market effect"
          },
          "sectorImpacts": {
            "type": "array",
            "description": "How each sector is affected",
            "items": {
              "type": "object",
              "properties": {
                "sectorId": {
                  "type": "string",
                  "description": "Sector ID"
                },
                "impact": {
                  "type": "number",
                  "description": "Percentage impact (-99 = -99%)",
                  "minimum": -99,
                  "maximum": 75
                }
              }
            }
          }
        }
      },
      "default": [
        {
          "id": "HORDE_INCOMING",
          "name": "Horde Night Approaching",
          "description": "Increased Zed activity signals the Blood Moon. Very High and High Risk defense stocks soar, while Very Low Risk stocks see minimal impact.",
          "sectorImpacts": [
            {
              "sectorId": "VERY_HIGH_RISK",
              "impact": 50
            },
            {
              "sectorId": "HIGH_RISK",
              "impact": 30
            },
            {
              "sectorId": "MEDIUM_RISK",
              "impact": 15
            },
            {
              "sectorId": "LOW_RISK",
              "impact": 5
            },
            {
              "sectorId": "VERY_LOW_RISK",
              "impact": 1
            }
          ]
        },
        {
          "id": "STABLE_ECONOMY",
          "name": "Period of Stability",
          "description": "A rare period of peace has settled over the wasteland. Very Low and Low Risk assets flourish, with higher risk stocks seeing less significant gains.",
          "sectorImpacts": [
            {
              "sectorId": "VERY_LOW_RISK",
              "impact": 15
            },
            {
              "sectorId": "LOW_RISK",
              "impact": 10
            },
            {
              "sectorId": "MEDIUM_RISK",
              "impact": 5
            },
            {
              "sectorId": "HIGH_RISK",
              "impact": 2
            },
            {
              "sectorId": "VERY_HIGH_RISK",
              "impact": 1
            }
          ]
        },
        {
          "id": "GENERAL_PANIC",
          "name": "General Market Panic",
          "description": "A major setback reminds everyone how fragile survival is. Fear grips the entire market, causing higher volatility sectors to plummet.",
          "sectorImpacts": [
            {
              "sectorId": "VERY_HIGH_RISK",
              "impact": -90
            },
            {
              "sectorId": "HIGH_RISK",
              "impact": -70
            },
            {
              "sectorId": "MEDIUM_RISK",
              "impact": -40
            },
            {
              "sectorId": "LOW_RISK",
              "impact": -15
            },
            {
              "sectorId": "VERY_LOW_RISK",
              "impact": -5
            }
          ]
        },
        {
          "id": "ADVANCED_WEAPONRY_COLLAPSE",
          "name": "Turret AI Virus",
          "description": "A malicious code is spreading through advanced turret systems, causing them to malfunction violently. Very High Risk weaponry stocks are obliterated.",
          "sectorImpacts": [
            {
              "sectorId": "VERY_HIGH_RISK",
              "impact": -99
            },
            {
              "sectorId": "HIGH_RISK",
              "impact": -20
            }
          ]
        },
        {
          "id": "VOLATILE_CHEMICALS_COLLAPSE",
          "name": "Irreversible Contamination",
          "description": "A major chemical disaster has occurred. Very High Risk chemical stocks are worthless, impacting related sectors.",
          "sectorImpacts": [
            {
              "sectorId": "VERY_HIGH_RISK",
              "impact": -99
            },
            {
              "sectorId": "HIGH_RISK",
              "impact": -25
            },
            {
              "sectorId": "MEDIUM_RISK",
              "impact": -10
            }
          ]
        },
        {
          "id": "FOSSIL_FUELS_COLLAPSE",
          "name": "Clean Energy Breakthrough",
          "description": "A new energy source has been discovered. Very High Risk fossil fuel stocks become obsolete, while other sectors see minor adjustments.",
          "sectorImpacts": [
            {
              "sectorId": "VERY_HIGH_RISK",
              "impact": -99
            },
            {
              "sectorId": "MEDIUM_RISK",
              "impact": 10
            },
            {
              "sectorId": "LOW_RISK",
              "impact": 5
            }
          ]
        },
        {
          "id": "PHARMACEUTICALS_COLLAPSE",
          "name": "The Cure is Real",
          "description": "A cure for the zombie infection has been found! Very High Risk pharmaceutical stocks are wiped out, and even low-risk survival goods see a hit.",
          "sectorImpacts": [
            {
              "sectorId": "VERY_HIGH_RISK",
              "impact": -99
            },
            {
              "sectorId": "HIGH_RISK",
              "impact": -30
            },
            {
              "sectorId": "MEDIUM_RISK",
              "impact": -15
            },
            {
              "sectorId": "LOW_RISK",
              "impact": -10
            },
            {
              "sectorId": "VERY_LOW_RISK",
              "impact": -5
            }
          ]
        },
        {
          "id": "GUILD_OPERATIONS_COLLAPSE",
          "name": "Trader Guild Dissolved",
          "description": "The Trader's Guild has been formally dissolved. Very High Risk trade operation stocks become worthless, affecting overall trade.",
          "sectorImpacts": [
            {
              "sectorId": "VERY_HIGH_RISK",
              "impact": -99
            },
            {
              "sectorId": "HIGH_RISK",
              "impact": -40
            },
            {
              "sectorId": "MEDIUM_RISK",
              "impact": -20
            },
            {
              "sectorId": "LOW_RISK",
              "impact": -10
            }
          ]
        },
        {
          "id": "AMMUNITION_DEPLETION",
          "name": "Ammunition Shortage",
          "description": "A major supply chain disruption has caused ammunition stockpiles to dwindle. Very High Risk ammunition stocks plummet.",
          "sectorImpacts": [
            {
              "sectorId": "VERY_HIGH_RISK",
              "impact": -99
            },
            {
              "sectorId": "HIGH_RISK",
              "impact": -30
            },
            {
              "sectorId": "MEDIUM_RISK",
              "impact": -15
            }
          ]
        },
        {
          "id": "RESOURCE_BOOM",
          "name": "Resource Discovery",
          "description": "A massive new ore deposit has been found! All resource-related stocks are surging, with higher risk ones seeing the biggest gains.",
          "sectorImpacts": [
            {
              "sectorId": "VERY_HIGH_RISK",
              "impact": 60
            },
            {
              "sectorId": "HIGH_RISK",
              "impact": 40
            },
            {
              "sectorId": "MEDIUM_RISK",
              "impact": 20
            },
            {
              "sectorId": "LOW_RISK",
              "impact": 10
            }
          ]
        },
        {
          "id": "MANUFACTURING_SURGE",
          "name": "Innovation Breakthrough",
          "description": "New manufacturing techniques have greatly increased production efficiency. Manufacturing stocks are up across the board.",
          "sectorImpacts": [
            {
              "sectorId": "VERY_HIGH_RISK",
              "impact": 40
            },
            {
              "sectorId": "HIGH_RISK",
              "impact": 30
            },
            {
              "sectorId": "MEDIUM_RISK",
              "impact": 20
            },
            {
              "sectorId": "LOW_RISK",
              "impact": 10
            },
            {
              "sectorId": "VERY_LOW_RISK",
              "impact": 5
            }
          ]
        },
        {
          "id": "TRADE_ROUTE_OPENED",
          "name": "New Trade Route Established",
          "description": "A secure trade route has been opened, boosting commerce. Trade-related stocks are seeing significant gains.",
          "sectorImpacts": [
            {
              "sectorId": "VERY_HIGH_RISK",
              "impact": 50
            },
            {
              "sectorId": "HIGH_RISK",
              "impact": 30
            },
            {
              "sectorId": "MEDIUM_RISK",
              "impact": 20
            },
            {
              "sectorId": "LOW_RISK",
              "impact": 15
            },
            {
              "sectorId": "VERY_LOW_RISK",
              "impact": 10
            }
          ]
        }
      ]
    },
    "eventFrequency": {
      "title": "Event Frequency",
      "type": "number",
      "description": "Average number of update cycles between market events (0 to disable). Depends on mod's update frequency (e.g., per in-game hour?).",
      "default": 8,
      "minimum": 1
    },
    "defaultEventDuration": {
      "title": "Default Event Duration",
      "type": "number",
      "description": "Default number of update cycles an event lasts.",
      "default": 3,
      "minimum": 1,
      "maximum": 96
    },
    "transactionFee": {
      "title": "Transaction Fee (Dukes Tax)",
      "type": "number",
      "description": "Percentage fee charged on all buy/sell transactions (5 = 5%)",
      "default": 3,
      "minimum": 0,
      "maximum": 25
    },
    "vipDiscount": {
      "title": "Trader's Favor Discount",
      "type": "number",
      "description": "Percentage discount on transaction fees for players with '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 (15 = 15%)",
      "default": 15,
      "minimum": 5,
      "maximum": 50
    },
    "marketNewsChannelId": {
      "title": "Discord Channel ID (for Market News)",
      "type": "string",
      "description": "The ID of the Discord channel where market news messages should be posted. Leave empty to disable.",
      "default": ""
    }
  },
  "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.

  • 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);
            });
    
            // MODIFICATION START
            // Display stocks by sector, chunking them into messages of 3
            const chunkSize = 3;
            for (const [sector, sectorStocks] of Object.entries(stocksBySector)) {
                for (let i = 0; i < sectorStocks.length; i += chunkSize) {
                    const chunk = sectorStocks.slice(i, i + chunkSize);
    
                    // Add the sector header only to the first message for that sector.
                    let sectorMessage = (i === 0) ? `\n--- ${sector} SECTOR ---\n` : '';
    
                    chunk.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);
                }
            }
            // MODIFICATION END
    
            // 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();
  • 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();
  • sellstock

    Sell shares of a stock

    ArgumentTypeDefaultHelp
    amount number Number of shares to sell
    stock string The stock ticker symbol
    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;
        }
    
        // Helper function to format numbers with commas and a dollar sign
        const formatNumber = (num) => {
            return `$${Math.round(num).toLocaleString()}`;
        };
    
        // 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: ${formatNumber(data.averagePrice)}\n`;
            stockMessage += `Current price: ${formatNumber(stock.price)}\n`;
            stockMessage += `Total value: ${formatNumber(currentValue)}\n`;
    
            if (profit >= 0) {
                stockMessage += `Profit: +${formatNumber(profit)} (+${profitPercent}%)\n`;
            } else {
                stockMessage += `Loss: -${formatNumber(Math.abs(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: ${formatNumber(totalInvestment)}\n`;
            summaryMessage += `Current value: ${formatNumber(totalValue)}\n`;
    
            if (totalProfit >= 0) {
                summaryMessage += `Overall profit: +${formatNumber(totalProfit)} (+${totalProfitPercent}%)\n`;
            } else {
                summaryMessage += `Overall loss: -${formatNumber(Math.abs(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();
  • 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();

Cron jobs 2

Work the module runs on a schedule.

  • marketnews

    Cron job source
    import { takaro, data } from '@takaro/helpers';
    
    async function main() {
        const { gameServerId, module: mod } = data;
    
        const discordChannelId = mod.userConfig.marketNewsChannelId;
    
        if (!discordChannelId) {
            takaro.log.info('Market news Discord channel ID (marketNewsChannelId) not configured in module settings. Skipping Discord messages.');
        }
    
        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;
        }
    
        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;
        }
    
        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;
        }
    
        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);
                if (typeof activeEvent !== 'object' || activeEvent === null || Object.keys(activeEvent).length === 0) {
                    activeEvent = null;
                }
            } catch (error) {
                takaro.log.warn(`Failed to parse active_market_event: ${error}`);
                activeEvent = null;
            }
        }
    
        let gameMessage1 = "==== DAILY MARKET REPORT ====\n";
    
        if (activeEvent && activeEvent.name && activeEvent.description) {
            const eventText = `\n🌍 ACTIVE EVENT: ${activeEvent.name} 🌍\n${activeEvent.description}\n`;
            gameMessage1 += eventText;
        } else {
            const noEventText = '\nNo active market events today.\n';
            gameMessage1 += noEventText;
        }
    
        await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
            message: gameMessage1
        }).catch(err => takaro.log.error(`Failed to send game message 1: ${err}`));
    
        const stocksWithPerformance = stocks.map(stock => {
            let change = 0;
            let percentChange = 0;
            let changeSymbol = '→';
    
            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) {
                    changeSymbol = '↑';
                } else if (percentChange < -0.05) {
                    changeSymbol = '↓';
                }
            } else if (typeof stock.price === 'number' && stock.lastPrice === undefined) {
                changeSymbol = '🆕';
                percentChange = 0;
            }
            return {
                ...stock,
                change,
                percentChange,
                changeSymbol
            };
        });
    
        const sectorPerformance = {};
        stocksWithPerformance.forEach(stock => {
            const sectorId = stock.sector || 'Uncategorized';
            if (!sectorPerformance[sectorId]) {
                sectorPerformance[sectorId] = {
                    stocks: [],
                    totalPercentChange: 0,
                    count: 0
                };
            }
            sectorPerformance[sectorId].stocks.push(stock);
            if (typeof stock.percentChange === 'number' && isFinite(stock.percentChange)) {
                sectorPerformance[sectorId].totalPercentChange += stock.percentChange;
                sectorPerformance[sectorId].count++;
            }
        });
    
        for (const sectorId in sectorPerformance) {
            const sectorData = sectorPerformance[sectorId];
            if (sectorData.count > 0) {
                sectorData.avgPerformance = sectorData.totalPercentChange / sectorData.count;
            } else {
                sectorData.avgPerformance = 0;
            }
        }
    
        const sortedSectors = Object.entries(sectorPerformance)
            .sort(([, a], [, b]) => b.avgPerformance - a.avgPerformance);
    
        let gameMessage2 = "\n=== SECTOR PERFORMANCE ===\n";
    
        if (sortedSectors.length > 0) {
            sortedSectors.forEach(([sectorId, sectorData]) => {
                const avgSymbol = sectorData.avgPerformance > 0.05 ? '↑' : sectorData.avgPerformance < -0.05 ? '↓' : '→';
                let line = `${sectorId}: ${avgSymbol} ${Math.abs(sectorData.avgPerformance).toFixed(1)}%\n`;
    
                if (sectorData.avgPerformance > 0.05) {
                    line = `[70FE02]${sectorId}: ${avgSymbol} ${Math.abs(sectorData.avgPerformance).toFixed(1)}%[-]\n`;
                } else if (sectorData.avgPerformance < -0.05) {
                    line = `[FF0001]${sectorId}: ${avgSymbol} ${Math.abs(sectorData.avgPerformance).toFixed(1)}%[-]\n`;
                }
                gameMessage2 += line;
            });
        } else {
            const noDataText = "No sector performance data available.\n";
            gameMessage2 += noDataText;
        }
    
        await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
            message: gameMessage2
        }).catch(err => takaro.log.error(`Failed to send game message 2: ${err}`));
    
        const validPerformers = stocksWithPerformance.filter(s => typeof s.percentChange === 'number' && isFinite(s.percentChange));
        validPerformers.sort((a, b) => b.percentChange - a.percentChange);
    
        let gameMessage3 = "";
    
        const topPerformersTitleGame = "\n🔥 TOP PERFORMERS 🔥\n";
        gameMessage3 += topPerformersTitleGame;
    
        if (validPerformers.length > 0) {
            for (let i = 0; i < Math.min(3, validPerformers.length); i++) {
                const stock = validPerformers[i];
                gameMessage3 += `[70FE02]${stock.id}: $${Math.round(stock.price)} ${stock.changeSymbol} ${Math.abs(stock.percentChange).toFixed(1)}%[-]\n`;
            }
        } else {
            gameMessage3 += "No top performers today.\n";
        }
    
        const worstPerformersTitleGame = "\n📉 WORST PERFORMERS 📉\n";
        gameMessage3 += worstPerformersTitleGame;
    
        if (validPerformers.length > 0) {
            const worstStartIndex = Math.max(0, validPerformers.length - 3);
            for (let i = validPerformers.length - 1; i >= worstStartIndex; i--) {
                const stock = validPerformers[i];
                gameMessage3 += `[FF0001]${stock.id}: $${Math.round(stock.price)} ${stock.changeSymbol} ${Math.abs(stock.percentChange).toFixed(1)}%[-]\n`;
            }
        } else {
            gameMessage3 += "No worst performers today.\n";
        }
    
        await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
            message: gameMessage3
        }).catch(err => takaro.log.error(`Failed to send game message 3: ${err}`));
    
        function hslToRgb(h, s, l) { let a = s * Math.min(l, 1 - l); let f = n => { let k = (n + h / 30) % 12; return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); }; return [Math.round(255 * f(0)), Math.round(255 * f(8)), Math.round(255 * f(4))]; }
        function rgbToHex(r, g, b) { return ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase(); }
        function stringToHexColor(str) { let hash = 0; for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } hash = Math.abs(hash); const hue = (hash * 137.508) % 360; const [r, g, b] = hslToRgb(hue, 0.9, 0.7); return rgbToHex(r, g, b); }
    
        const stockColors = {};
        stocksWithPerformance.forEach(stock => {
            stockColors[stock.id] = stringToHexColor(stock.name);
        });
    
        const stocksBySectorGame = {};
        stocksWithPerformance.forEach(stock => {
            const sector = stock.sector || 'Uncategorized';
            if (!stocksBySectorGame[sector]) {
                stocksBySectorGame[sector] = [];
            }
            stocksBySectorGame[sector].push(stock);
        });
    
        const chunkSize = 3;
        for (const [sector, sectorStocks] of Object.entries(stocksBySectorGame)) {
            for (let i = 0; i < sectorStocks.length; i += chunkSize) {
                const chunk = sectorStocks.slice(i, i + chunkSize);
                let sectorMessage = (i === 0) ? `\n--- ${sector} SECTOR ---\n` : '';
    
                chunk.forEach(stock => {
                    let changeText = '';
                    let priceColorPrefix = '', priceColorSuffix = '';
    
                    if (stock.lastPrice) {
                        const percentChange = ((stock.price - stock.lastPrice) / stock.lastPrice) * 100;
                        changeText = ` ${stock.changeSymbol} ${Math.abs(percentChange).toFixed(1)}%`;
                        if (percentChange > 0) {
                            priceColorPrefix = '[70FE02]';
                            priceColorSuffix = '[-]';
                        } else if (percentChange < 0) {
                            priceColorPrefix = '[FF0001]';
                            priceColorSuffix = '[-]';
                        }
                    }
    
                    const stockNameColorPrefix = stockColors[stock.id] ? `[${stockColors[stock.id]}]` : '';
                    const stockNameColorSuffix = stockColors[stock.id] ? '[-]' : '';
    
                    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.25) riskIndicator = "H";
                    else riskIndicator = "VH";
    
                    sectorMessage += `${stockNameColorPrefix}${stock.name} (${stock.id})${stockNameColorSuffix} (${riskIndicator}): ${priceColorPrefix}$${Math.round(stock.price)}${changeText}${priceColorSuffix}\n`;
                });
    
                await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                    message: sectorMessage
                }).catch(err => takaro.log.error(`Failed to send game message for sector ${sector}: ${err}`));
            }
        }
    
        const legendMessage = "\n=== LEGEND ===\n" +
            "Risk Levels: VL=Very Low, L=Low, M=Moderate, H=High, VH=Very High\n";
        await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
            message: legendMessage
        }).catch(err => takaro.log.error(`Failed to send game legend message: ${err}`));
    
        if (discordChannelId) {
            // --- MESSAGE 1: The Summary ---
            let discordMessageSummary = "**==== DAILY MARKET REPORT ====**\n\n";
    
            if (activeEvent && activeEvent.name && activeEvent.description) {
                discordMessageSummary += `🌍 **ACTIVE EVENT: ${activeEvent.name}** 🌍\n${activeEvent.description}\n\n`;
            } else {
                discordMessageSummary += 'No active market events today.\n\n';
            }
    
            discordMessageSummary += "**=== SECTOR PERFORMANCE ===**\n";
            if (sortedSectors.length > 0) {
                sortedSectors.forEach(([sectorId, sectorData]) => {
                    const avgSymbol = sectorData.avgPerformance > 0.05 ? '↑' : sectorData.avgPerformance < -0.05 ? '↓' : '→';
                    discordMessageSummary += `${sectorId}: ${avgSymbol} ${Math.abs(sectorData.avgPerformance).toFixed(1)}%\n`;
                });
            } else {
                discordMessageSummary += "No sector performance data available.\n";
            }
            discordMessageSummary += "\n";
    
            discordMessageSummary += "**🔥 TOP PERFORMERS 🔥**\n";
            if (validPerformers.length > 0) {
                for (let i = 0; i < Math.min(3, validPerformers.length); i++) {
                    const stock = validPerformers[i];
                    discordMessageSummary += `${stock.id}: $${Math.round(stock.price)} ${stock.changeSymbol} ${Math.abs(stock.percentChange).toFixed(1)}%\n`;
                }
            } else {
                discordMessageSummary += "No top performers today.\n";
            }
            discordMessageSummary += "\n";
    
            discordMessageSummary += "**📉 WORST PERFORMERS 📉**\n";
            if (validPerformers.length > 0) {
                const worstStartIndex = Math.max(0, validPerformers.length - 3);
                for (let i = validPerformers.length - 1; i >= worstStartIndex; i--) {
                    const stock = validPerformers[i];
                    discordMessageSummary += `${stock.id}: $${Math.round(stock.price)} ${stock.changeSymbol} ${Math.abs(stock.percentChange).toFixed(1)}%\n`;
                }
            } else {
                discordMessageSummary += "No worst performers today.\n";
            }
    
            await takaro.discord.discordControllerSendMessage(discordChannelId, {
                message: discordMessageSummary
            }).catch(err => takaro.log.error(`Failed to send summary Discord message: ${err.message || err}`));
    
            // --- MESSAGE 2: All Stock Listings ---
            let discordMessageDetails = "**=== ALL STOCK LISTINGS ===**\n";
            const stocksBySector = {};
            stocksWithPerformance.forEach(stock => {
                const sector = stock.sector || 'Uncategorized';
                if (!stocksBySector[sector]) {
                    stocksBySector[sector] = [];
                }
                stocksBySector[sector].push(stock);
            });
    
            for (const [sector, sectorStocks] of Object.entries(stocksBySector)) {
                discordMessageDetails += `\n--- **${sector} SECTOR** ---\n`;
                sectorStocks.forEach(stock => {
                    let changeText = '';
                    if (stock.lastPrice) {
                        const percentChange = ((stock.price - stock.lastPrice) / stock.lastPrice) * 100;
                        changeText = ` ${stock.changeSymbol} ${Math.abs(percentChange).toFixed(1)}%`;
                    }
    
                    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.25) riskIndicator = "H";
                    else riskIndicator = "VH";
    
                    discordMessageDetails += `${stock.id} (${riskIndicator}): ${stock.name} - $${Math.round(stock.price)}${changeText}\n`;
                });
            }
    
            discordMessageDetails += "\n**=== LEGEND ===**\n" +
                "Risk Levels: VL=Very Low, L=Low, M=Moderate, H=High, VH=Very High\n";
    
            await takaro.discord.discordControllerSendMessage(discordChannelId, {
                message: discordMessageDetails
            }).catch(err => takaro.log.error(`Failed to send details Discord message: ${err.message || err}`));
        }
    
        takaro.log.info('Daily market report sent successfully.');
    }
    
    await main();
  • 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) {
                console.log("Market data not found, initializing...");
                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);
                console.log(`Event counter: ${eventCounter}`);
            }
    
            let cooldownCounter = 0;
            if (cooldownCounterVar.data.data.length > 0) {
                cooldownCounter = parseInt(cooldownCounterVar.data.data[0].value, 10);
                console.log(`Cooldown counter: ${cooldownCounter}`);
            }
    
            // 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);
                console.log(`Event duration: ${eventDuration}`);
            }
    
            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;
                    }
                    console.log(`Active event: ${activeEvent ? activeEvent.name : 'None'}`);
                } catch (e) {
                    // In case of parsing error, consider no active event
                    activeEvent = null;
                    console.log(`Error parsing active event: ${e.message}`);
                }
            }
    
            // Check if the current event should end and increment counter
            if (activeEvent) {
                eventCounter++;
                console.log(`Incremented event counter to: ${eventCounter}`);
    
                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
                    });
                }
    
                if (eventCounter >= eventDuration) {
                    console.log("Event duration reached, ending event.");
                    if (activeEventVar.data.data.length > 0) {
                        await takaro.variable.variableControllerUpdate(activeEventVar.data.data[0].id, {
                            value: ''
                        });
                    }
    
                    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
                        });
                    }
    
                    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
                        });
                    }
    
                    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
                        });
                    }
    
                    activeEvent = null;
                }
            } else {
                cooldownCounter++;
                console.log(`Incremented cooldown counter to: ${cooldownCounter}`);
    
                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
                    });
                }
            }
    
            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;
    
            if (!activeEvent && cooldownCounter >= eventFrequency) {
                shouldTriggerEvent = Math.random() < 0.5;
                if (cooldownCounter >= eventFrequency * 2) {
                    shouldTriggerEvent = true;
                }
                console.log(`Should trigger new event: ${shouldTriggerEvent}`);
            }
    
            if (shouldTriggerEvent) {
                const events = (mod.userConfig && mod.userConfig.marketEvents) || [];
                if (events.length > 0) {
                    const randomEvent = events[Math.floor(Math.random() * events.length)];
                    activeEvent = randomEvent;
                    console.log(`New event triggered: ${activeEvent.name}`);
    
                    const maxDuration = (mod.userConfig && mod.userConfig.defaultEventDuration) || 5;
                    const randomDuration = Math.floor(Math.random() * maxDuration) + 1;
    
                    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
                        });
                    }
    
                    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
                        });
                    }
    
                    if (cooldownCounterVar.data.data.length > 0) {
                        await takaro.variable.variableControllerUpdate(cooldownCounterVar.data.data[0].id, {
                            value: '0'
                        });
                    }
    
                    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
                        });
                    }
    
                    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
                        });
                    }
    
                    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
                        });
                    }
    
                    const onlinePlayers = await takaro.playerOnGameserver.playerOnGameServerControllerSearch({
                        filters: {
                            gameServerId: [gameServerId],
                            online: [true]
                        }
                    });
    
                    if (onlinePlayers.data.meta.total > 0) {
                        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!`;
    
                        const maxLength = 400;
                        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) {
                console.log("Market data is empty or invalid after initial check, re-initializing.");
                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");
                }
                // CRITICAL FIX: Ensure each stock has valid numerical price properties
                stocks = stocks.map(s => {
                    const sanitizedPrice = typeof s.price === 'number' && s.price > 0 ? s.price : 1;
                    const sanitizedLastPrice = typeof s.lastPrice === 'number' && s.lastPrice > 0 ? s.lastPrice : sanitizedPrice;
                    return {
                        ...s,
                        price: sanitizedPrice,
                        lastPrice: sanitizedLastPrice
                    };
                });
                console.log("Stocks loaded and sanitized successfully.");
                console.log("Initial stocks for this cycle:");
                stocks.forEach(s => console.log(`  ${s.id}: Price ${s.price}, LastPrice ${s.lastPrice}`));
    
            } catch (e) {
                console.log(`Error parsing or sanitizing stock data: ${e.message}`);
                await initializeMarketIfNeeded(gameServerId, mod);
                return;
            }
    
            const significantChanges = [];
    
            // Update each stock price
            stocks.forEach(stock => {
                // Always capture oldPrice at the very beginning of the iteration
                const oldPrice = stock.price;
                // Set lastPrice to the value before this current update cycle for accurate change tracking
                stock.lastPrice = oldPrice;
    
                console.log(`Processing stock: ${stock.id}, Old Price: ${oldPrice}`);
    
                // Define the threshold for "penny stock" and the chance/magnitude of a breakout
                const pennyStockThreshold = 1.9;
                const breakoutChance = 0.6;
    
                const targetMinPrice = 2.0;
                const targetMaxPrice = 10.0;
    
                let calculatedNewPrice;
    
                // Check if it's a penny stock and qualifies for a breakout
                if (oldPrice <= pennyStockThreshold && Math.random() < breakoutChance) {
                    console.log(`${stock.id} is a penny stock (${oldPrice}) and hit breakout chance!`);
                    calculatedNewPrice = targetMinPrice + Math.random() * (targetMaxPrice - targetMinPrice);
                    calculatedNewPrice = Math.max(calculatedNewPrice, oldPrice + 0.01); // Ensure at least a penny increase
                    console.log(`${stock.id} calculated breakout price: ${calculatedNewPrice}`);
                } else {
                    const randomFactor = Math.random() * 2 - 1;
                    const totalChangePercent = randomFactor * stock.volatility;
                    calculatedNewPrice = oldPrice * (1 + totalChangePercent);
                    console.log(`${stock.id} calculated normal price: ${calculatedNewPrice} (Change: ${totalChangePercent * 100}%)`);
                }
    
                let finalPrice = calculatedNewPrice;
                if (activeEvent) {
                    const sectorImpact = activeEvent.sectorImpacts.find(impact => impact.sectorId === stock.sector);
                    if (sectorImpact) {
                        const eventImpactPercentage = sectorImpact.impact;
                        const eventImpactDecimal = eventImpactPercentage / 100;
                        const randomImpactFactor = 0.5 + Math.random();
                        const eventImpact = eventImpactDecimal * randomImpactFactor;
    
                        finalPrice = calculatedNewPrice * (1 + eventImpact);
                        console.log(`${stock.id} final price after event impact: ${finalPrice} (Event impact: ${eventImpact * 100}%)`);
                    }
                }
    
                stock.price = finalPrice;
    
                if (stock.price < 1) {
                    console.log(`${stock.id} price fell below 1, setting to 1.`);
                    stock.price = 1;
                }
    
                const actualPercentChange = ((stock.price - oldPrice) / oldPrice) * 100;
                console.log(`${stock.id} - Final Price: ${stock.price.toFixed(2)}, Actual Change: ${actualPercentChange.toFixed(2)}%`);
    
                const changeThreshold = ((mod.userConfig && mod.userConfig.priceAlertThreshold) || 10) / 100;
                if (Math.abs(actualPercentChange) > changeThreshold * 100) {
                    significantChanges.push({
                        ...stock,
                        changePercent: actualPercentChange
                    });
                }
            });
    
            console.log("All stocks processed, saving updated prices...");
            await takaro.variable.variableControllerUpdate(marketDataVar.data.data[0].id, {
                value: JSON.stringify(stocks)
            });
            console.log("Updated prices saved.");
    
            if (significantChanges.length > 0) {
                console.log("Significant changes detected, sending alert.");
                const onlinePlayers = await takaro.playerOnGameserver.playerOnGameServerControllerSearch({
                    filters: {
                        gameServerId: [gameServerId],
                        online: [true]
                    }
                });
    
                if (onlinePlayers.data.meta.total > 0) {
                    significantChanges.sort((a, b) => Math.abs(b.changePercent) - Math.abs(a.changePercent));
                    const topChanges = significantChanges.slice(0, Math.min(3, significantChanges.length));
    
                    let message = "📊 STOCK MARKET ALERT 📊\n";
                    topChanges.forEach(stock => {
                        const changeDir = stock.changePercent > 0 ? "up" : "down";
                        const displayPrice = stock.price.toFixed(2);
                        const changePercent = Math.abs(Math.round(stock.changePercent));
    
                        message += `${stock.id} (${stock.sector}): ${changeDir} ${changePercent}% to $${displayPrice}!\n`;
                    });
    
                    if (activeEvent) {
                        message += `\nCurrent market event: ${activeEvent.name}`;
                    }
    
                    await takaro.gameserver.gameServerControllerSendMessage(gameServerId, {
                        message
                    });
                } else {
                    console.log("No online players to send alert to.");
                }
            } else {
                console.log("No significant changes to alert players about.");
            }
        } catch (error) {
            console.log(`FATAL Error in updateStockPrices: ${error.message}`);
            // Optionally, send an admin message if a fatal error occurs
            // await takaro.gameserver.gameServerControllerSendMessage(data.gameServerId, {
            //     message: `🚨 Market Error: An error occurred during stock update: ${error.message}`
            // });
        }
    }
    
    async function initializeMarketIfNeeded(gameServerId, mod) {
        try {
            console.log("Initializing market if needed...");
            const configStocks = (mod.userConfig && mod.userConfig.stocks) || [];
    
            if (!configStocks || configStocks.length === 0) {
                console.log("Error: No stocks defined in module configuration for initialization.");
                return;
            }
    
            const [marketDataVar, lastEventTimeVar, eventStartTimeVar, activeEventVar, marketInitVar, eventCounterVar, cooldownCounterVar, eventDurationVar] =
                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] } }),
                    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] } })
                ]);
    
            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
            }));
            console.log("Mapped initial stocks from config.");
    
            const createPromises = [];
    
            if (marketDataVar.data.data.length === 0) {
                createPromises.push(takaro.variable.variableControllerCreate({ key: 'stock_market_data', value: JSON.stringify(stocks), gameServerId, moduleId: mod.moduleId }));
            }
            if (lastEventTimeVar.data.data.length === 0) {
                createPromises.push(takaro.variable.variableControllerCreate({ key: 'last_market_event_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }));
            }
            if (eventStartTimeVar.data.data.length === 0) {
                createPromises.push(takaro.variable.variableControllerCreate({ key: 'event_start_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }));
            }
            if (activeEventVar.data.data.length === 0) {
                createPromises.push(takaro.variable.variableControllerCreate({ key: 'active_market_event', value: '', gameServerId, moduleId: mod.moduleId }));
            }
            if (eventCounterVar.data.data.length === 0) {
                createPromises.push(takaro.variable.variableControllerCreate({ key: 'event_execution_counter', value: '0', gameServerId, moduleId: mod.moduleId }));
            }
            if (cooldownCounterVar.data.data.length === 0) {
                createPromises.push(takaro.variable.variableControllerCreate({ key: 'event_cooldown_counter', value: '0', gameServerId, moduleId: mod.moduleId }));
            }
            if (eventDurationVar.data.data.length === 0) {
                const defaultDuration = (mod.userConfig && mod.userConfig.defaultEventDuration) || 5;
                createPromises.push(takaro.variable.variableControllerCreate({ key: 'current_event_duration', value: defaultDuration.toString(), gameServerId, moduleId: mod.moduleId }));
            }
            if (marketInitVar.data.data.length === 0) {
                createPromises.push(takaro.variable.variableControllerCreate({ key: 'stock_market_initialized', value: 'true', gameServerId, moduleId: mod.moduleId }));
            }
    
            if (createPromises.length > 0) {
                console.log(`Creating ${createPromises.length} initial market variables.`);
                await Promise.allSettled(createPromises);
            } else {
                console.log("All market variables already exist, no new initialization needed.");
            }
    
            if (marketDataVar.data.data.length === 0) { // Only announce if we actually created the stock_market_data variable
                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 });
                }
            }
            console.log("Market initialization process complete.");
        } catch (error) {
            console.log(`Error in initializeMarketIfNeeded: ${error.message}`);
        }
    }
    
    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