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

- Multiple Economic Sectors: Configure various industry sectors (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.

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

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

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

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

Configuration Options:
- Sectors & Stocks: Define your own market sectors and individual stocks with customizable starting prices and volatility.
- Market Events: Create unique events with specific sector impacts to drive market dynamics.
- Transaction Settings: Configure fee percentages, VIP discounts, and alert thresholds.
- Event Frequency: Control how often random market events occur and how long they last.


Commands:
/markets [industry]- View all stocks or filter by industry/stockinfo [ticker]- Get detailed information about a specific stock/buystock <ticker> <amount>- Purchase shares of a stock/sellstock <ticker> <amount>- Sell shares from your portfolio/stockportfolio- View your current holdings and performance/triggerevent [event]- Admin command to trigger specific market events
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.
| Setting | Type | Default | Description |
|---|---|---|---|
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.
Argument Type Default Help tickerstring all stock ticker or ALL to get detailed information about a specific stock Command source
import { takaro, data, TakaroUserError, checkPermission } from '@takaro/helpers'; async function main() { const { player, gameServerId, module: mod, arguments: args } = data; // Check permission if (!checkPermission(data.pog, 'STOCK_MARKET_USE')) { throw new TakaroUserError("You don't have permission to use the stock market."); } // Get current stock data const marketDataVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); if (marketDataVar.data.data.length === 0) { throw new TakaroUserError("The stock market isn't available right now. Please try again later."); } const stocks = JSON.parse(marketDataVar.data.data[0].value); // If a specific stock ticker is provided, show detailed info for that stock const specificTicker = args.ticker ? args.ticker.toUpperCase() : null; if (specificTicker && specificTicker !== "ALL") { const stock = stocks.find(s => s.id.toUpperCase() === specificTicker); if (!stock) { throw new TakaroUserError(`Stock ${specificTicker} not found. Use /stockinfo without parameters to see all available stocks.`); } // Get active event to see if this stock's sector is affected const activeEventVar = await takaro.variable.variableControllerSearch({ filters: { key: ['active_market_event'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); let activeEvent = null; let sectorImpact = null; if (activeEventVar.data.data.length > 0 && activeEventVar.data.data[0].value) { try { activeEvent = JSON.parse(activeEventVar.data.data[0].value); if (activeEvent && activeEvent.sectorImpacts) { sectorImpact = activeEvent.sectorImpacts.find(impact => impact.sectorId === stock.sector ); } } catch (e) { // Ignore parsing errors } } // Calculate price change indicators let changeText = ''; if (stock.lastPrice) { const percentChange = ((stock.price - stock.lastPrice) / stock.lastPrice) * 100; const changeIcon = percentChange > 0 ? '↑' : percentChange < 0 ? '↓' : '→'; changeText = ` ${changeIcon} ${Math.abs(percentChange).toFixed(1)}%`; } // Add risk assessment based on volatility without showing the raw value let riskLevel; if (stock.volatility <= 0.05) riskLevel = "Very Low"; else if (stock.volatility <= 0.10) riskLevel = "Low"; else if (stock.volatility <= 0.15) riskLevel = "Moderate"; else if (stock.volatility <= 0.20) riskLevel = "High"; else riskLevel = "Very High"; // Build detailed stock info message let message = `=== ${stock.id}: ${stock.name} ===\n\n`; message += `Sector: ${stock.sector}\n`; message += `Current Price: $${Math.round(stock.price)}${changeText}\n`; message += `Risk Level: ${riskLevel}\n`; // Add sector trend info if available from event if (activeEvent && sectorImpact) { const direction = sectorImpact.impact >= 0 ? "Positive" : "Negative"; const strength = Math.abs(sectorImpact.impact); let trend; if (strength < 10) trend = "Slight"; else if (strength < 25) trend = "Moderate"; else trend = "Strong"; message += `\nCurrent Trend: ${trend} ${direction} (${activeEvent.name})\n`; message += `Event: ${activeEvent.description}\n`; } // Add trading guidance based on sector and risk, without mentioning volatility message += `\nTrading Notes:\n`; if (activeEvent && sectorImpact) { if (sectorImpact.impact > 0) { message += `- Currently bullish due to the ${activeEvent.name} event\n`; } else { message += `- Currently bearish due to the ${activeEvent.name} event\n`; } } if (riskLevel === "High" || riskLevel === "Very High") { message += `- Expect significant price fluctuations with this stock\n`; } else if (riskLevel === "Low" || riskLevel === "Very Low") { message += `- Typically has stable price movement\n`; } message += `\nUse /buystock ${stock.id} [amount] to purchase shares`; await player.pm(message); } else { // No specific ticker provided or ALL specified, show summary of all stocks await player.pm("=== STOCK MARKET LISTINGS ===\n"); // Group stocks by sector const stocksBySector = {}; stocks.forEach(stock => { if (!stocksBySector[stock.sector]) { stocksBySector[stock.sector] = []; } stocksBySector[stock.sector].push(stock); }); // 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.
Argument Type Default Help EventNamestring all Event name to trigger a specific market event Command source
import { takaro, data, TakaroUserError, checkPermission } from '@takaro/helpers'; async function main() { const { player, gameServerId, module: mod, arguments: args } = data; // Check permission if (!checkPermission(data.pog, 'STOCK_MARKET_TRIGGER_EVENT')) { throw new TakaroUserError("You don't have permission to trigger market events."); } try { // Get current stock data to check market initialization const marketDataVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); // Initialize market if needed if (marketDataVar.data.data.length === 0) { await initializeMarketIfNeeded(gameServerId, mod); await player.pm("Market was not initialized. Initializing now..."); return; } // Get events from module config const marketEvents = mod.userConfig.marketEvents || []; if (marketEvents.length === 0) { throw new TakaroUserError("No market events configured in this module."); } // Handle case when no event is specified or "ALL" is provided if (!args.EventName || args.EventName.toUpperCase() === "ALL") { await player.pm("=== AVAILABLE MARKET EVENTS ===\n"); // Group events by category or type if possible const eventCategories = {}; // Create a simple categorization based on positive/negative impact marketEvents.forEach(event => { let category = "Mixed"; // Calculate net impact across all sectors const netImpact = event.sectorImpacts.reduce((sum, impact) => sum + impact.impact, 0); if (netImpact > 0) category = "Positive"; else if (netImpact < 0) category = "Negative"; if (!eventCategories[category]) { eventCategories[category] = []; } eventCategories[category].push(event); }); // Display events by category for (const [category, events] of Object.entries(eventCategories)) { if (events.length > 0) { await player.pm(`\n--- ${category.toUpperCase()} EVENTS ---`); let message = ""; events.forEach(event => { // Format primary sectors affected const primarySectors = event.sectorImpacts .filter(impact => Math.abs(impact.impact) >= 15) .map(impact => { const direction = impact.impact >= 0 ? "↑" : "↓"; return `${impact.sectorId} ${direction}${Math.abs(impact.impact)}%`; }) .join(", "); message += `${event.id}: ${event.name}\n`; message += ` ${event.description}\n`; if (primarySectors) { message += ` Major impacts: ${primarySectors}\n`; } message += "\n"; }); await player.pm(message); } } await player.pm("=== HOW TO USE ===\nUse `/triggerevent <EventName>` to trigger a specific event (e.g., `/triggerevent TECH_BOOM`)"); return; } // Find the requested event const eventId = args.EventName.toUpperCase(); const event = marketEvents.find(e => e.id === eventId); if (!event) { throw new TakaroUserError(`Event "${eventId}" not found. Use /triggerevent without parameters to see all available events.`); } // Get necessary variables for managing the event const [activeEventVar, eventStartTimeVar, eventCounterVar, cooldownCounterVar, eventDurationVar] = await Promise.all([ takaro.variable.variableControllerSearch({ filters: { key: ['active_market_event'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['event_start_time'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['event_execution_counter'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['event_cooldown_counter'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['current_event_duration'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }) ]); // Check if there's an active event we need to cancel let activeEventObj = null; if (activeEventVar.data.data.length > 0 && activeEventVar.data.data[0].value) { try { activeEventObj = JSON.parse(activeEventVar.data.data[0].value); if (activeEventObj && Object.keys(activeEventObj).length > 0) { await player.pm(`Cancelling active event "${activeEventObj.name}" to trigger new event.`); } } catch (e) { // Invalid event data, will be overwritten } } // Generate a random duration for the event const maxDuration = (mod.userConfig && mod.userConfig.defaultEventDuration) || 5; const randomDuration = Math.floor(Math.random() * maxDuration) + 1; // 1 to maxDuration // Update or create all event-related variables const updatePromises = []; // Save or update the random duration if (eventDurationVar.data.data.length > 0) { updatePromises.push( takaro.variable.variableControllerUpdate(eventDurationVar.data.data[0].id, { value: randomDuration.toString() }) ); } else { updatePromises.push( takaro.variable.variableControllerCreate({ key: 'current_event_duration', value: randomDuration.toString(), gameServerId, moduleId: mod.moduleId }) ); } // Reset the event counter to 0 if (eventCounterVar.data.data.length > 0) { updatePromises.push( takaro.variable.variableControllerUpdate(eventCounterVar.data.data[0].id, { value: '0' }) ); } else { updatePromises.push( takaro.variable.variableControllerCreate({ key: 'event_execution_counter', value: '0', gameServerId, moduleId: mod.moduleId }) ); } // Reset the cooldown counter if (cooldownCounterVar.data.data.length > 0) { updatePromises.push( takaro.variable.variableControllerUpdate(cooldownCounterVar.data.data[0].id, { value: '0' }) ); } else { updatePromises.push( takaro.variable.variableControllerCreate({ key: 'event_cooldown_counter', value: '0', gameServerId, moduleId: mod.moduleId }) ); } // Update active event if (activeEventVar.data.data.length > 0) { updatePromises.push( takaro.variable.variableControllerUpdate(activeEventVar.data.data[0].id, { value: JSON.stringify(event) }) ); } else { updatePromises.push( takaro.variable.variableControllerCreate({ key: 'active_market_event', value: JSON.stringify(event), gameServerId, moduleId: mod.moduleId }) ); } // Update event start time if (eventStartTimeVar.data.data.length > 0) { updatePromises.push( takaro.variable.variableControllerUpdate(eventStartTimeVar.data.data[0].id, { value: new Date().toISOString() }) ); } else { updatePromises.push( takaro.variable.variableControllerCreate({ key: 'event_start_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }) ); } // Update last event time const lastEventTimeVar = await takaro.variable.variableControllerSearch({ filters: { key: ['last_market_event_time'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); if (lastEventTimeVar.data.data.length > 0) { updatePromises.push( takaro.variable.variableControllerUpdate(lastEventTimeVar.data.data[0].id, { value: new Date().toISOString() }) ); } else { updatePromises.push( takaro.variable.variableControllerCreate({ key: 'last_market_event_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }) ); } // Wait for all updates to complete await Promise.all(updatePromises); // Check if there are online players to announce the event const onlinePlayers = await takaro.playerOnGameserver.playerOnGameServerControllerSearch({ filters: { gameServerId: [gameServerId], online: [true] } }); if (onlinePlayers.data.meta.total > 0) { // Format sector impacts for announcement let impactText = ""; for (const impact of event.sectorImpacts) { const direction = impact.impact >= 0 ? "↑" : "↓"; impactText += `\n${impact.sectorId}: ${direction} ${Math.abs(impact.impact)}%`; } const message = `🌍 BREAKING MARKET NEWS 🌍\n\n${event.name}\n${event.description}\n\nSector Impacts:${impactText}\n\nThis event will influence stock prices! Use /markets to see opportunities!`; // Split the message if it's too long const maxLength = 400; // Safe limit for most games for (let i = 0; i < message.length; i += maxLength) { await takaro.gameserver.gameServerControllerSendMessage(gameServerId, { message: message.substring(i, i + maxLength) }); } } await player.pm(`Successfully triggered the "${event.name}" market event for ${randomDuration} cycles!`); } catch (error) { // If something goes wrong, log it and let the player know console.log(`Error in triggerEvent: ${error.message}`); throw new TakaroUserError(`Error triggering event: ${error.message}`); } } // Initialize the market with stocks from the configuration async function initializeMarketIfNeeded(gameServerId, mod) { try { // Get stocks from config const configStocks = (mod.userConfig && mod.userConfig.stocks) || []; if (!configStocks || configStocks.length === 0) { console.log("Error: No stocks defined in configuration"); return; } // Map config stocks to our internal format const stocks = configStocks.map(stock => ({ id: stock.id, name: stock.name, sector: stock.sector, price: stock.initialPrice, volatility: stock.volatility / 100, // Convert percentage to decimal lastPrice: stock.initialPrice })); // Store the stocks data await takaro.variable.variableControllerCreate({ key: 'stock_market_data', value: JSON.stringify(stocks), gameServerId, moduleId: mod.moduleId }); // Initialize last event time to now await takaro.variable.variableControllerCreate({ key: 'last_market_event_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }); // Initialize event start time variable await takaro.variable.variableControllerCreate({ key: 'event_start_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }); // Initialize active event (empty string means no active event) await takaro.variable.variableControllerCreate({ key: 'active_market_event', value: '', gameServerId, moduleId: mod.moduleId }); // Mark market as initialized await takaro.variable.variableControllerCreate({ key: 'stock_market_initialized', value: 'true', gameServerId, moduleId: mod.moduleId }); // Announce market initialization to online players const onlinePlayers = await takaro.playerOnGameserver.playerOnGameServerControllerSearch({ filters: { gameServerId: [gameServerId], online: [true] } }); if (onlinePlayers.data.meta.total > 0) { const message = "📈 STOCK MARKET INITIALIZED 📈\n\nThe stock market is now open for trading! Use /markets to see available stocks and /buystock to start investing."; await takaro.gameserver.gameServerControllerSendMessage(gameServerId, { message }); } } catch (error) { console.log(`Error in initializeMarketIfNeeded: ${error.message}`); } } await main(); -
markets
View stock market prices and activity. Use '/markets ALL' to see all industries, or specify an industry name (e.g., '/markets TECH') to see stocks in that industry only.
Argument Type Default Help Industrystring all View stock market prices and activity. Use '/markets ALL' to see all industries, or specify an industry name (e.g., '/markets TECH') to see stocks in that industry only. Command source
import { takaro, data, TakaroUserError, checkPermission } from '@takaro/helpers'; async function main() { const { player, gameServerId, module: mod, arguments: args } = data; // Check permission if (!checkPermission(data.pog, 'STOCK_MARKET_USE')) { throw new TakaroUserError("You don't have permission to use the stock market."); } // Handle the industry argument - use "ALL" as a special value to show all industries const industryFilter = args.Industry ? args.Industry.toUpperCase() : "ALL"; // Get current stock data const marketDataVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); // If market data doesn't exist, try to initialize it if (marketDataVar.data.data.length === 0) { const initialized = await initializeMarketIfNeeded(gameServerId, mod); if (!initialized) { throw new TakaroUserError("The stock market isn't available right now. Please try again later."); } // Get the freshly initialized market data const refreshedMarketData = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); if (refreshedMarketData.data.data.length === 0) { throw new TakaroUserError("There was an issue initializing the stock market. Please try again later."); } // Continue with the refreshed data await displayMarketSummary(player, gameServerId, mod, refreshedMarketData.data.data[0], industryFilter); } else { // Market data exists, display it await displayMarketSummary(player, gameServerId, mod, marketDataVar.data.data[0], industryFilter); } } // Display the market summary to the player async function displayMarketSummary(player, gameServerId, mod, marketDataVariable, industryFilter) { // Get active market event const activeEventVar = await takaro.variable.variableControllerSearch({ filters: { key: ['active_market_event'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); let activeEvent = null; if (activeEventVar.data.data.length > 0 && activeEventVar.data.data[0].value) { try { activeEvent = JSON.parse(activeEventVar.data.data[0].value); // Handle empty string or empty object if (!activeEvent || Object.keys(activeEvent).length === 0) { activeEvent = null; } } catch (e) { // In case of parsing error activeEvent = null; } } const stocks = JSON.parse(marketDataVariable.value); // Get all available industries for reference const availableIndustries = [...new Set(stocks.map(stock => stock.sector))]; // If industry filter is provided and not "ALL", check if it's valid if (industryFilter !== "ALL") { // Check if the industry exists const industryExists = availableIndustries.includes(industryFilter); if (!industryExists) { throw new TakaroUserError(`Industry "${industryFilter}" not found. Available industries: ${availableIndustries.join(', ')}\nUse "ALL" to view all industries.`); } } // Send header message let headerMessage = "=== STOCK MARKET SUMMARY ===\n"; // If filtering by industry, mention it in the header if (industryFilter !== "ALL") { headerMessage = `=== ${industryFilter} INDUSTRY ===\n`; } // If there's an active event, include it in the header if (activeEvent) { headerMessage += `\n🌍 ACTIVE EVENT: ${activeEvent.name} 🌍\n`; headerMessage += `${activeEvent.description}\n\n`; // If filtering by industry, only show relevant impacts if (industryFilter !== "ALL") { const relevantImpact = activeEvent.sectorImpacts.find( impact => impact.sectorId === industryFilter ); if (relevantImpact) { const direction = relevantImpact.impact >= 0 ? "↑" : "↓"; headerMessage += `Industry Impact: ${direction} ${Math.abs(relevantImpact.impact)}%\n`; } else { headerMessage += "This industry is not directly affected by the current event.\n"; } } else { // Show all industry impacts headerMessage += "Industry Impacts:\n"; for (const impact of activeEvent.sectorImpacts) { const direction = impact.impact >= 0 ? "↑" : "↓"; headerMessage += `${impact.sectorId}: ${direction} ${Math.abs(impact.impact)}%\n`; } } } // Send header message first await player.pm(headerMessage); // Group stocks by industry const stocksByIndustry = {}; stocks.forEach(stock => { if (!stocksByIndustry[stock.sector]) { stocksByIndustry[stock.sector] = []; } stocksByIndustry[stock.sector].push(stock); }); // If industry filter is not "ALL", only show that industry if (industryFilter !== "ALL") { const filteredStocks = stocksByIndustry[industryFilter] || []; if (filteredStocks.length > 0) { let stockMessage = ""; filteredStocks.forEach(stock => { let changeIcon = ''; if (stock.lastPrice) { const percentChange = ((stock.price - stock.lastPrice) / stock.lastPrice) * 100; changeIcon = percentChange > 0 ? `↑ ${percentChange.toFixed(1)}%` : percentChange < 0 ? `↓ ${Math.abs(percentChange).toFixed(1)}%` : '→'; } stockMessage += `${stock.id}: $${Math.round(stock.price)} ${changeIcon}\n`; }); await player.pm(stockMessage); } else { await player.pm(`No stocks found in the ${industryFilter} industry.`); } } else { // Send each industry as a separate message for (const industryId in stocksByIndustry) { let industryMessage = `=== ${industryId} INDUSTRY ===\n`; stocksByIndustry[industryId].forEach(stock => { let changeIcon = ''; if (stock.lastPrice) { const percentChange = ((stock.price - stock.lastPrice) / stock.lastPrice) * 100; changeIcon = percentChange > 0 ? `↑ ${percentChange.toFixed(1)}%` : percentChange < 0 ? `↓ ${Math.abs(percentChange).toFixed(1)}%` : '→'; } industryMessage += `${stock.id}: $${Math.round(stock.price)} ${changeIcon}\n`; }); await player.pm(industryMessage); } } // Add a help message for industry filtering if showing all industries if (industryFilter === "ALL") { const allIndustries = Object.keys(stocksByIndustry).join(', '); await player.pm(`\nTip: Use '/markets [industry]' to view only stocks in a specific industry. Available industries: ${allIndustries}`); } } // Initialize the market with stocks from the configuration async function initializeMarketIfNeeded(gameServerId, mod) { try { // Get stocks from config const configStocks = (mod.userConfig && mod.userConfig.stocks) || []; if (!configStocks || configStocks.length === 0) { console.log("Error: No stocks defined in configuration"); return false; } // Check for each required variable first const [marketDataVar, lastEventTimeVar, eventStartTimeVar, activeEventVar, marketInitVar] = await Promise.all([ takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['last_market_event_time'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['event_start_time'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['active_market_event'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_initialized'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }) ]); // Map config stocks to our internal format const stocks = configStocks.map(stock => ({ id: stock.id, name: stock.name, sector: stock.sector, price: stock.initialPrice, volatility: stock.volatility / 100, // Convert percentage to decimal lastPrice: stock.initialPrice })); // Create each variable only if it doesn't exist already const createPromises = []; // Store the stocks data if it doesn't exist if (marketDataVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'stock_market_data', value: JSON.stringify(stocks), gameServerId, moduleId: mod.moduleId }) ); } // Initialize last event time if it doesn't exist if (lastEventTimeVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'last_market_event_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }) ); } // Initialize event start time variable if it doesn't exist if (eventStartTimeVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'event_start_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }) ); } // Initialize active event if it doesn't exist if (activeEventVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'active_market_event', value: '', gameServerId, moduleId: mod.moduleId }) ); } // Mark market as initialized if not already marked if (marketInitVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'stock_market_initialized', value: 'true', gameServerId, moduleId: mod.moduleId }) ); } // Wait for all creation promises to complete if (createPromises.length > 0) { const results = await Promise.allSettled(createPromises); // Check if the critical stock_market_data was created successfully const stockDataPromise = results[0]; if (stockDataPromise && stockDataPromise.status === 'rejected') { console.log(`Failed to create stock_market_data: ${stockDataPromise.reason}`); return false; } } return true; } catch (error) { console.log(`Error in initializeMarketIfNeeded: ${error.message}`); return false; } } await main(); -
sellstock
Sell shares of a stock
Argument Type Default Help amountnumber Number of shares to sell stockstring 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
Argument Type Default Help stockstring The stock ticker symbol amountnumber Number of shares to buy Command source
import { takaro, data, TakaroUserError, checkPermission } from '@takaro/helpers'; async function main() { const { player, gameServerId, module: mod, arguments: args } = data; // Check permission if (!checkPermission(data.pog, 'STOCK_MARKET_TRADE')) { throw new TakaroUserError("You don't have permission to trade stocks."); } // Validate input parameters if (!args.stock) { throw new TakaroUserError("Please specify a stock ticker. Usage: /buystock STOCK AMOUNT"); } // Convert amount to integer and validate const amount = parseInt(args.amount); if (isNaN(amount) || amount <= 0) { throw new TakaroUserError("Amount must be a positive whole number. Usage: /buystock STOCK AMOUNT"); } try { // Get current stock data const marketDataVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); if (marketDataVar.data.data.length === 0) { throw new TakaroUserError("The stock market isn't available right now. Please try again later."); } const stocks = JSON.parse(marketDataVar.data.data[0].value); const stock = stocks.find(s => s.id.toUpperCase() === args.stock.toUpperCase()); if (!stock) { throw new TakaroUserError(`Stock ${args.stock} not found. Use /market to see available stocks.`); } // Calculate transaction fee - using integers to avoid floating point issues let feePercentage = mod.userConfig.transactionFee || 5; // Check for VIP discount if (checkPermission(data.pog, 'STOCK_MARKET_BROKER')) { const discount = (mod.userConfig.vipDiscount || 50) / 100; // Convert to decimal feePercentage = feePercentage * (1 - discount); } // Calculate costs using Math.round to ensure we work with integers const subtotal = Math.round(stock.price) * amount; const fee = Math.round((subtotal * feePercentage) / 100); const totalCost = subtotal + fee; // Check if player has enough currency const playerData = await takaro.playerOnGameserver.playerOnGameServerControllerGetOne(gameServerId, player.id); const currentBalance = playerData.data.data.currency; if (currentBalance < totalCost) { throw new TakaroUserError(`You don't have enough currency. Cost: $${subtotal} + $${fee} fee = $${totalCost}. Your balance: $${currentBalance}`); } // Deduct the currency - using integer value to avoid precision errors await takaro.playerOnGameserver.playerOnGameServerControllerDeductCurrency( gameServerId, player.id, { currency: totalCost } ); // Get player's portfolio or create new one const portfolioVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_portfolio'], gameServerId: [gameServerId], moduleId: [mod.moduleId], playerId: [player.id] } }); let portfolio; if (portfolioVar.data.data.length === 0) { portfolio = {}; } else { portfolio = JSON.parse(portfolioVar.data.data[0].value); } // Update portfolio using rounded values for consistency const stockPrice = Math.round(stock.price); if (!portfolio[stock.id]) { portfolio[stock.id] = { shares: amount, averagePrice: stockPrice }; } else { const totalShares = portfolio[stock.id].shares + amount; const totalValue = (portfolio[stock.id].shares * portfolio[stock.id].averagePrice) + (amount * stockPrice); portfolio[stock.id].shares = totalShares; portfolio[stock.id].averagePrice = Math.round(totalValue / totalShares); } // Save updated portfolio if (portfolioVar.data.data.length === 0) { await takaro.variable.variableControllerCreate({ key: 'stock_portfolio', value: JSON.stringify(portfolio), gameServerId, moduleId: mod.moduleId, playerId: player.id }); } else { await takaro.variable.variableControllerUpdate(portfolioVar.data.data[0].id, { value: JSON.stringify(portfolio) }); } // Track this transaction in transaction history await recordTransaction(gameServerId, mod.moduleId, player.id, { type: 'BUY', stockId: stock.id, shares: amount, pricePerShare: stockPrice, subtotal: subtotal, fee: fee, total: totalCost, timestamp: new Date().toISOString() }); let message = `Successfully bought ${amount} shares of ${stock.id} at $${stockPrice} each.\n`; message += `Subtotal: $${subtotal}\n`; message += `Transaction fee: $${fee}\n`; message += `Total cost: $${totalCost}\n`; message += `Current portfolio: ${portfolio[stock.id].shares} shares of ${stock.id}`; await player.pm(message); } catch (error) { // If it's our custom error, just pass it through if (error instanceof TakaroUserError) { throw error; } // For unexpected errors, log them and provide a friendlier message console.error("Error in buystock command:", error); throw new TakaroUserError("An error occurred while processing your purchase. Please try again later."); } } // Record transaction history for reporting and analytics async function recordTransaction(gameServerId, moduleId, playerId, transaction) { try { const historyVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_transaction_history'], gameServerId: [gameServerId], moduleId: [moduleId], playerId: [playerId] } }); let history = []; if (historyVar.data.data.length > 0) { history = JSON.parse(historyVar.data.data[0].value); } // Add new transaction to history history.push(transaction); // Keep only the last 50 transactions to avoid variable size limits if (history.length > 50) { history = history.slice(history.length - 50); } if (historyVar.data.data.length === 0) { await takaro.variable.variableControllerCreate({ key: 'stock_transaction_history', value: JSON.stringify(history), gameServerId, moduleId, playerId }); } else { await takaro.variable.variableControllerUpdate(historyVar.data.data[0].id, { value: JSON.stringify(history) }); } } catch (error) { // Don't let transaction history errors prevent the main operation console.error("Error recording transaction history:", error); } } await main();
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