StockMarket
- community
- economy
- by limon
- Takaro v0.0.29
- 7 Days to Die
Stock Market: A Dynamic Virtual Trading System
๐ฅ SETUP TUTORIAL
โบ Watch the Complete Guide
Learn how to set up and use the Stock Market module
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 High Risk","name":"Very High Risk"},{"id":"High Risk","name":"High Risk"},{"id":"Medium Risk","name":"Medium Risk"},{"id":"Low Risk","name":"Low Risk"},{"id":"Very Low Risk","name":"Very Low 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 High Risk",
"name": "Very High Risk"
},
{
"id": "High Risk",
"name": "High Risk"
},
{
"id": "Medium Risk",
"name": "Medium Risk"
},
{
"id": "Low Risk",
"name": "Low Risk"
},
{
"id": "Very Low Risk",
"name": "Very Low 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.
-
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(); -
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(); -
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(); -
markets
View stock market prices and activity. Use '/markets ALL' to see all industries, or specify an industry name (e.g., '/markets TECH') to see stocks in that industry only.
Argument Type Default Help Industrystring all View stock market prices and activity. Use '/markets ALL' to see all industries, or specify an industry name (e.g., '/markets TECH') to see stocks in that industry only. Command source
import { takaro, data, TakaroUserError, checkPermission } from '@takaro/helpers'; async function main() { const { player, gameServerId, module: mod, arguments: args } = data; // Check permission if (!checkPermission(data.pog, 'STOCK_MARKET_USE')) { throw new TakaroUserError("You don't have permission to use the stock market."); } // Handle the industry argument - use "ALL" as a special value to show all industries const industryFilter = args.Industry ? args.Industry.toUpperCase() : "ALL"; // Get current stock data const marketDataVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); // If market data doesn't exist, try to initialize it if (marketDataVar.data.data.length === 0) { const initialized = await initializeMarketIfNeeded(gameServerId, mod); if (!initialized) { throw new TakaroUserError("The stock market isn't available right now. Please try again later."); } // Get the freshly initialized market data const refreshedMarketData = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); if (refreshedMarketData.data.data.length === 0) { throw new TakaroUserError("There was an issue initializing the stock market. Please try again later."); } // Continue with the refreshed data await displayMarketSummary(player, gameServerId, mod, refreshedMarketData.data.data[0], industryFilter); } else { // Market data exists, display it await displayMarketSummary(player, gameServerId, mod, marketDataVar.data.data[0], industryFilter); } } // Display the market summary to the player async function displayMarketSummary(player, gameServerId, mod, marketDataVariable, industryFilter) { // Get active market event const activeEventVar = await takaro.variable.variableControllerSearch({ filters: { key: ['active_market_event'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); let activeEvent = null; if (activeEventVar.data.data.length > 0 && activeEventVar.data.data[0].value) { try { activeEvent = JSON.parse(activeEventVar.data.data[0].value); // Handle empty string or empty object if (!activeEvent || Object.keys(activeEvent).length === 0) { activeEvent = null; } } catch (e) { // In case of parsing error activeEvent = null; } } const stocks = JSON.parse(marketDataVariable.value); // Get all available industries for reference const availableIndustries = [...new Set(stocks.map(stock => stock.sector))]; // If industry filter is provided and not "ALL", check if it's valid if (industryFilter !== "ALL") { // Check if the industry exists const industryExists = availableIndustries.includes(industryFilter); if (!industryExists) { throw new TakaroUserError(`Industry "${industryFilter}" not found. Available industries: ${availableIndustries.join(', ')}\nUse "ALL" to view all industries.`); } } // Send header message let headerMessage = "=== STOCK MARKET SUMMARY ===\n"; // If filtering by industry, mention it in the header if (industryFilter !== "ALL") { headerMessage = `=== ${industryFilter} INDUSTRY ===\n`; } // If there's an active event, include it in the header if (activeEvent) { headerMessage += `\n๐ ACTIVE EVENT: ${activeEvent.name} ๐\n`; headerMessage += `${activeEvent.description}\n\n`; // If filtering by industry, only show relevant impacts if (industryFilter !== "ALL") { const relevantImpact = activeEvent.sectorImpacts.find( impact => impact.sectorId === industryFilter ); if (relevantImpact) { const direction = relevantImpact.impact >= 0 ? "โ" : "โ"; headerMessage += `Industry Impact: ${direction} ${Math.abs(relevantImpact.impact)}%\n`; } else { headerMessage += "This industry is not directly affected by the current event.\n"; } } else { // Show all industry impacts headerMessage += "Industry Impacts:\n"; for (const impact of activeEvent.sectorImpacts) { const direction = impact.impact >= 0 ? "โ" : "โ"; headerMessage += `${impact.sectorId}: ${direction} ${Math.abs(impact.impact)}%\n`; } } } // Send header message first await player.pm(headerMessage); // Group stocks by industry const stocksByIndustry = {}; stocks.forEach(stock => { if (!stocksByIndustry[stock.sector]) { stocksByIndustry[stock.sector] = []; } stocksByIndustry[stock.sector].push(stock); }); // If industry filter is not "ALL", only show that industry if (industryFilter !== "ALL") { const filteredStocks = stocksByIndustry[industryFilter] || []; if (filteredStocks.length > 0) { let stockMessage = ""; filteredStocks.forEach(stock => { let changeIcon = ''; if (stock.lastPrice) { const percentChange = ((stock.price - stock.lastPrice) / stock.lastPrice) * 100; changeIcon = percentChange > 0 ? `โ ${percentChange.toFixed(1)}%` : percentChange < 0 ? `โ ${Math.abs(percentChange).toFixed(1)}%` : 'โ'; } stockMessage += `${stock.id}: $${Math.round(stock.price)} ${changeIcon}\n`; }); await player.pm(stockMessage); } else { await player.pm(`No stocks found in the ${industryFilter} industry.`); } } else { // Send each industry as a separate message for (const industryId in stocksByIndustry) { let industryMessage = `=== ${industryId} INDUSTRY ===\n`; stocksByIndustry[industryId].forEach(stock => { let changeIcon = ''; if (stock.lastPrice) { const percentChange = ((stock.price - stock.lastPrice) / stock.lastPrice) * 100; changeIcon = percentChange > 0 ? `โ ${percentChange.toFixed(1)}%` : percentChange < 0 ? `โ ${Math.abs(percentChange).toFixed(1)}%` : 'โ'; } industryMessage += `${stock.id}: $${Math.round(stock.price)} ${changeIcon}\n`; }); await player.pm(industryMessage); } } // Add a help message for industry filtering if showing all industries if (industryFilter === "ALL") { const allIndustries = Object.keys(stocksByIndustry).join(', '); await player.pm(`\nTip: Use '/markets [industry]' to view only stocks in a specific industry. Available industries: ${allIndustries}`); } } // Initialize the market with stocks from the configuration async function initializeMarketIfNeeded(gameServerId, mod) { try { // Get stocks from config const configStocks = (mod.userConfig && mod.userConfig.stocks) || []; if (!configStocks || configStocks.length === 0) { console.log("Error: No stocks defined in configuration"); return false; } // Check for each required variable first const [marketDataVar, lastEventTimeVar, eventStartTimeVar, activeEventVar, marketInitVar] = await Promise.all([ takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['last_market_event_time'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['event_start_time'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['active_market_event'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }), takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_initialized'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }) ]); // Map config stocks to our internal format const stocks = configStocks.map(stock => ({ id: stock.id, name: stock.name, sector: stock.sector, price: stock.initialPrice, volatility: stock.volatility / 100, // Convert percentage to decimal lastPrice: stock.initialPrice })); // Create each variable only if it doesn't exist already const createPromises = []; // Store the stocks data if it doesn't exist if (marketDataVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'stock_market_data', value: JSON.stringify(stocks), gameServerId, moduleId: mod.moduleId }) ); } // Initialize last event time if it doesn't exist if (lastEventTimeVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'last_market_event_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }) ); } // Initialize event start time variable if it doesn't exist if (eventStartTimeVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'event_start_time', value: new Date().toISOString(), gameServerId, moduleId: mod.moduleId }) ); } // Initialize active event if it doesn't exist if (activeEventVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'active_market_event', value: '', gameServerId, moduleId: mod.moduleId }) ); } // Mark market as initialized if not already marked if (marketInitVar.data.data.length === 0) { createPromises.push( takaro.variable.variableControllerCreate({ key: 'stock_market_initialized', value: 'true', gameServerId, moduleId: mod.moduleId }) ); } // Wait for all creation promises to complete if (createPromises.length > 0) { const results = await Promise.allSettled(createPromises); // Check if the critical stock_market_data was created successfully const stockDataPromise = results[0]; if (stockDataPromise && stockDataPromise.status === 'rejected') { console.log(`Failed to create stock_market_data: ${stockDataPromise.reason}`); return false; } } return true; } catch (error) { console.log(`Error in initializeMarketIfNeeded: ${error.message}`); return false; } } await main(); -
stockinfo
Shows all available stocks in the market. Use with a ticker symbol (e.g., /stockinfo ticker) to see detailed information about a specific stock.
Argument Type Default Help tickerstring all stock ticker or ALL to get detailed information about a specific stock Command source
import { takaro, data, TakaroUserError, checkPermission } from '@takaro/helpers'; async function main() { const { player, gameServerId, module: mod, arguments: args } = data; // Check permission if (!checkPermission(data.pog, 'STOCK_MARKET_USE')) { throw new TakaroUserError("You don't have permission to use the stock market."); } // Get current stock data const marketDataVar = await takaro.variable.variableControllerSearch({ filters: { key: ['stock_market_data'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); if (marketDataVar.data.data.length === 0) { throw new TakaroUserError("The stock market isn't available right now. Please try again later."); } const stocks = JSON.parse(marketDataVar.data.data[0].value); // If a specific stock ticker is provided, show detailed info for that stock const specificTicker = args.ticker ? args.ticker.toUpperCase() : null; if (specificTicker && specificTicker !== "ALL") { const stock = stocks.find(s => s.id.toUpperCase() === specificTicker); if (!stock) { throw new TakaroUserError(`Stock ${specificTicker} not found. Use /stockinfo without parameters to see all available stocks.`); } // Get active event to see if this stock's sector is affected const activeEventVar = await takaro.variable.variableControllerSearch({ filters: { key: ['active_market_event'], gameServerId: [gameServerId], moduleId: [mod.moduleId] } }); let activeEvent = null; let sectorImpact = null; if (activeEventVar.data.data.length > 0 && activeEventVar.data.data[0].value) { try { activeEvent = JSON.parse(activeEventVar.data.data[0].value); if (activeEvent && activeEvent.sectorImpacts) { sectorImpact = activeEvent.sectorImpacts.find(impact => impact.sectorId === stock.sector ); } } catch (e) { // Ignore parsing errors } } // Calculate price change indicators let changeText = ''; if (stock.lastPrice) { const percentChange = ((stock.price - stock.lastPrice) / stock.lastPrice) * 100; const changeIcon = percentChange > 0 ? 'โ' : percentChange < 0 ? 'โ' : 'โ'; changeText = ` ${changeIcon} ${Math.abs(percentChange).toFixed(1)}%`; } // Add risk assessment based on volatility without showing the raw value let riskLevel; if (stock.volatility <= 0.05) riskLevel = "Very Low"; else if (stock.volatility <= 0.10) riskLevel = "Low"; else if (stock.volatility <= 0.15) riskLevel = "Moderate"; else if (stock.volatility <= 0.20) riskLevel = "High"; else riskLevel = "Very High"; // Build detailed stock info message let message = `=== ${stock.id}: ${stock.name} ===\n\n`; message += `Sector: ${stock.sector}\n`; message += `Current Price: $${Math.round(stock.price)}${changeText}\n`; message += `Risk Level: ${riskLevel}\n`; // Add sector trend info if available from event if (activeEvent && sectorImpact) { const direction = sectorImpact.impact >= 0 ? "Positive" : "Negative"; const strength = Math.abs(sectorImpact.impact); let trend; if (strength < 10) trend = "Slight"; else if (strength < 25) trend = "Moderate"; else trend = "Strong"; message += `\nCurrent Trend: ${trend} ${direction} (${activeEvent.name})\n`; message += `Event: ${activeEvent.description}\n`; } // Add trading guidance based on sector and risk, without mentioning volatility message += `\nTrading Notes:\n`; if (activeEvent && sectorImpact) { if (sectorImpact.impact > 0) { message += `- Currently bullish due to the ${activeEvent.name} event\n`; } else { message += `- Currently bearish due to the ${activeEvent.name} event\n`; } } if (riskLevel === "High" || riskLevel === "Very High") { message += `- Expect significant price fluctuations with this stock\n`; } else if (riskLevel === "Low" || riskLevel === "Very Low") { message += `- Typically has stable price movement\n`; } message += `\nUse /buystock ${stock.id} [amount] to purchase shares`; await player.pm(message); } else { // No specific ticker provided or ALL specified, show summary of all stocks await player.pm("=== STOCK MARKET LISTINGS ===\n"); // Group stocks by sector const stocksBySector = {}; stocks.forEach(stock => { if (!stocksBySector[stock.sector]) { stocksBySector[stock.sector] = []; } stocksBySector[stock.sector].push(stock); }); // 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(); -
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();
Cron jobs 2
Work the module runs on a schedule.
-
updatestockprices
When he cronjob runs
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(); -
marketnews
gives you market news
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 = {}; // Define the desired display order and mapping for sorting const riskOrder = { "Very High Risk": 5, "High Risk": 4, "Medium Risk": 3, "Low Risk": 2, "Very Low Risk": 1, "Uncategorized": 0 // Fallback for any stock without a defined sector }; stocksWithPerformance.forEach(stock => { const sectorName = stock.sector || 'Uncategorized'; // Use the sector name directly if (!sectorPerformance[sectorName]) { sectorPerformance[sectorName] = { stocks: [], totalPercentChange: 0, count: 0 }; } sectorPerformance[sectorName].stocks.push(stock); if (typeof stock.percentChange === 'number' && isFinite(stock.percentChange)) { sectorPerformance[sectorName].totalPercentChange += stock.percentChange; sectorPerformance[sectorName].count++; } }); for (const sectorName in sectorPerformance) { const sectorData = sectorPerformance[sectorName]; if (sectorData.count > 0) { sectorData.avgPerformance = sectorData.totalPercentChange / sectorData.count; } else { sectorData.avgPerformance = 0; } } // Sort sector performance by risk order (VH to VL) const sortedSectors = Object.entries(sectorPerformance) .sort(([sectorNameA], [sectorNameB]) => riskOrder[sectorNameB] - riskOrder[sectorNameA]); let gameMessage2 = "\n=== SECTOR PERFORMANCE ===\n"; if (sortedSectors.length > 0) { sortedSectors.forEach(([sectorName, sectorData]) => { // Use sectorName directly const avgSymbol = sectorData.avgPerformance > 0.05 ? 'โ' : sectorData.avgPerformance < -0.05 ? 'โ' : 'โ'; let line = `${sectorName}: ${avgSymbol} ${Math.abs(sectorData.avgPerformance).toFixed(1)}%\n`; if (sectorData.avgPerformance > 0.05) { line = `[70FE02]${sectorName}: ${avgSymbol} ${Math.abs(sectorData.avgPerformance).toFixed(1)}%[-]\n`; } else if (sectorData.avgPerformance < -0.05) { line = `[FF0001]${sectorName}: ${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'; // Use the sector name directly if (!stocksBySectorGame[sector]) { stocksBySectorGame[sector] = []; } stocksBySectorGame[sector].push(stock); }); // Sort the sector names for display in the desired order (VH, H, M, L, VL) const sortedSectorNamesGame = Object.keys(stocksBySectorGame).sort((sectorNameA, sectorNameB) => riskOrder[sectorNameB] - riskOrder[sectorNameA]); for (const sectorName of sortedSectorNamesGame) { // Iterate through sorted sector names const sectorStocks = stocksBySectorGame[sectorName]; const chunkSize = 3; for (let i = 0; i < sectorStocks.length; i += chunkSize) { const chunk = sectorStocks.slice(i, i + chunkSize); // Use sectorName directly for the header let sectorMessage = (i === 0) ? `\n--- ${sectorName} 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; // This is derived from volatility, not directly from sector ID 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 ${sectorName}: ${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(([sectorName, sectorData]) => { // Use sectorName directly const avgSymbol = sectorData.avgPerformance > 0.05 ? 'โ' : sectorData.avgPerformance < -0.05 ? 'โ' : 'โ'; discordMessageSummary += `${sectorName}: ${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'; // Use the sector name directly if (!stocksBySector[sector]) { stocksBySector[sector] = []; } stocksBySector[sector].push(stock); }); // Sort the sector names for display in the desired order (VH, H, M, L, VL) const sortedSectorNamesDiscord = Object.keys(stocksBySector).sort((sectorNameA, sectorNameB) => riskOrder[sectorNameB] - riskOrder[sectorNameA]); for (const sectorName of sortedSectorNamesDiscord) { // Iterate through sorted sector names const sectorStocks = stocksBySector[sectorName]; discordMessageDetails += `\n--- **${sectorName} SECTOR** ---\n`; // Use sectorName directly 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();
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