User prompt
it doesn't seem to affect the next buy or sell action
User prompt
Add four buttons labeled 1, 5, 10, and 25 above the first pair of Buy and Sell buttons in the stock list. Function: - When the player clicks one of these 4 buttons at the top, it should update the value shown in each of the textboxes located between the Buy and Sell buttons. - This value determines how many shares will be bought or sold when the Buy or Sell button is clicked. - These buttons should NOT affect any previous purchases or existing holdings — they only change the number of shares that will be involved in the next Buy or Sell action. Visual Layout: - Place the four buttons (1, 5, 10, 25) in a horizontal row above the Buy and Sell section. - Highlight the currently selected amount if possible, so the player knows what quantity is active.
User prompt
Add four buttons labeled 1, 5, 10, and 25 above the first pair of Buy and Sell buttons in the stock list. Function: - When the player clicks one of these buttons, it should update the value shown in the textbox located between the Buy and Sell buttons. - This value determines how many shares will be bought or sold when the Buy or Sell button is clicked. - These buttons should NOT affect any previous purchases or existing holdings — they only change the number of shares that will be involved in the next Buy or Sell action. Visual Layout: - Place the four buttons (1, 5, 10, 25) in a horizontal row above the Buy and Sell section. - Fix the spacings after placing these buttons, if needed. - Highlight the currently selected amount if possible, so the player knows what quantity is active.
User prompt
Please fix the bug: 'Cannot read properties of undefined (reading 'text')' in or related to this line: 'if (parseInt(amountTxt.text, 10) === quickAmounts[i]) {' Line Number: 123
User prompt
Add four buttons labeled 1, 5, 10, and 25 above the first pair of Buy and Sell buttons in the stock list. Function: - When the player clicks one of these buttons, it should update the value shown in the textbox located between the Buy and Sell buttons. - This value determines how many shares will be bought or sold when the Buy or Sell button is clicked. - These buttons should NOT affect any previous purchases or existing holdings — they only change the number of shares that will be involved in the next Buy or Sell action. Visual Layout: - Place the four buttons (1, 5, 10, 25) in a horizontal row above the Buy and Sell section for each stock. - Highlight the currently selected amount if possible, so the player knows what quantity is active.
User prompt
display total cash top right corner
User prompt
add 1, 5, 10 and 25 as buttons on top of the first buy and sell buttons, they will affect how many shares will be bought so update the text boxes between all buy and sell buttons when 1, 5, 10 or 25 is selected
User prompt
Please fix the bug: 'Maximum call stack size exceeded' in or related to this line: 'optBg._setVisible(v);' Line Number: 178
User prompt
okay add dropdown list with selections 1, 5, 10 and 50 between the buy and sell buttons and buy correct number of shares according to the selection
User prompt
total money still isn't displayed at the top right corner, total profit needs a label to the left of it, text boxes between buy and sell buttons don't work
User prompt
display total cash (stock values not included) at the top right corner, display total profit & loss at the end of profit column, also add a textbox between buy and sell buttons so that investors can put how many shares they want to buy
User prompt
Please fix the bug: 'Cannot set properties of undefined (setting 'fill')' in or related to this line: 'profitTxt.style.fill = profitColor;' Line Number: 126
Code edit (1 edits merged)
Please save this source code
User prompt
SimuTrade: Stock Market Simulator
Initial prompt
Create a stock market simulation game called “SimuTrade”. Core Rules: - The player starts with 10,000 currency. - There are 10 fictional companies across different sectors. - Each company has a live stock price that fluctuates over time. Game Mechanics: - Display a table showing each company’s stock price, updated every 10 seconds. - Allow the player to buy or sell shares at current price. - Store the player’s portfolio, showing: - Company name - Number of shares owned - Average purchase price - Current value - Profit or loss per company - Update the player's total cash and portfolio value after every transaction. - End condition: optionally, after 5 or 10 minutes, show final wealth or profit ranking. Stock Price Logic: - Each company’s stock price changes every 10 seconds. - Use random fluctuations: ±1% to ±10% - Occasionally, simulate news events that cause dramatic changes (±25%) Interface: - Show player’s current balance at the top. - Show stock list with real-time updates. - Buy/Sell buttons for each stock. - Show player’s holdings and profit/loss.
/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // StockRow: A row in the stock table, with price, holdings, buy/sell buttons var StockRow = Container.expand(function () { var self = Container.call(this); // Properties self.companyIndex = 0; // 0-9 self.companyName = ''; self.colorId = ''; self.price = 0; self.holdings = 0; self.lastPrice = 0; // UI elements var yPad = 0; // Stock icon var icon = self.attachAsset(self.colorId, { anchorX: 0.5, anchorY: 0.5, x: 80, y: 60 + yPad, scaleX: 0.7, scaleY: 0.7 }); // Company name button background var nameBtnBg = self.attachAsset('buyBtn', { anchorX: 0, anchorY: 0, x: 160, y: 20 + yPad, width: 320, height: 60, color: 0x34495e }); nameBtnBg.alpha = 0.25; // Company name var nameTxt = new Text2(self.companyName, { size: 48, fill: 0xFFFFFF }); nameTxt.x = 170; nameTxt.y = 30 + yPad; nameTxt.anchor.set(0, 0); self.addChild(nameTxt); // Make the button background clickable for company details nameBtnBg.interactive = true; nameBtnBg.buttonMode = true; nameBtnBg.down = function (x, y, obj) { if (typeof showCompanyDetail === "function") { showCompanyDetail(self.companyIndex); } }; // Price text var priceTxt = new Text2('', { size: 48, fill: 0xF1C40F }); priceTxt.x = 500; priceTxt.y = 30 + yPad; priceTxt.anchor.set(0, 0); self.addChild(priceTxt); // Last price change percent text (new column) var changeTxt = new Text2('', { size: 44, fill: 0xFFFFFF }); changeTxt.x = 670; changeTxt.y = 34 + yPad; changeTxt.anchor.set(0, 0); self.addChild(changeTxt); // Holdings text var holdingsTxt = new Text2('', { size: 48, fill: 0x2ECC71 }); holdingsTxt.x = 880; holdingsTxt.y = 30 + yPad; holdingsTxt.anchor.set(0, 0); self.addChild(holdingsTxt); // Profit text var profitTxt = new Text2('', { size: 48, fill: 0xFFFFFF }); profitTxt.x = 1130; profitTxt.y = 30 + yPad; profitTxt.anchor.set(0, 0); self.addChild(profitTxt); // Buy button var buyBtn = self.attachAsset('buyBtn', { anchorX: 0.5, anchorY: 0.5, x: 1450, y: 60 + yPad }); var buyTxt = new Text2('Buy', { size: 36, fill: 0xFFFFFF }); buyTxt.anchor.set(0.5, 0.5); buyTxt.x = buyBtn.x; buyTxt.y = buyBtn.y; self.addChild(buyTxt); // Amount textbox (between buy and sell) var amountBoxBg = self.attachAsset('buyBtn', { anchorX: 0.5, anchorY: 0.5, x: 1575, y: 60 + yPad, scaleX: 0.7, scaleY: 0.7 }); var amountTxt = new Text2(typeof selectedShareAmount !== "undefined" ? '' + selectedShareAmount : '1', { size: 36, fill: 0x222a36 }); amountTxt.anchor.set(0.5, 0.5); amountTxt.x = amountBoxBg.x; amountTxt.y = amountBoxBg.y; self.addChild(amountTxt); // Touch to edit amount (prompt for number) amountBoxBg.down = function (x, y, obj) { // On mobile, prompt is supported in LK sandbox for numeric input var current = parseInt(amountTxt.text, 10); if (isNaN(current) || current < 1) current = 1; var entered = prompt("Enter number of shares:", current); var val = parseInt(entered, 10); if (isNaN(val) || val < 1) val = 1; amountTxt.setText('' + val); // Also update global selectedShareAmount and highlight if matches a selector if (typeof selectedShareAmount !== "undefined") { selectedShareAmount = val; if (typeof shareAmountBtnBg !== "undefined" && typeof shareAmounts !== "undefined") { for (var j = 0; j < shareAmountBtnBg.length; j++) { shareAmountBtnBg[j].tint = shareAmounts[j] === selectedShareAmount ? 0xf1c40f : 0x2980b9; } } // Update all amount boxes to match if (typeof stockRows !== "undefined") { for (var k = 0; k < stockRows.length; k++) { if (stockRows[k].setAmountBoxValue) { stockRows[k].setAmountBoxValue(selectedShareAmount); } } } } }; // Method to allow global selector to update this textbox self.setAmountBoxValue = function (val) { if (typeof val === "number" && val > 0) { amountTxt.setText('' + val); } }; // Sell button var sellBtn = self.attachAsset('sellBtn', { anchorX: 0.5, anchorY: 0.5, x: 1700, y: 60 + yPad }); var sellTxt = new Text2('Sell', { size: 36, fill: 0xFFFFFF }); sellTxt.anchor.set(0.5, 0.5); sellTxt.x = sellBtn.x; sellTxt.y = sellBtn.y; self.addChild(sellTxt); // Update UI self.updateUI = function () { nameTxt.setText(self.companyName); priceTxt.setText('₲' + self.price); // Show last price change percent and color var pctChange = 0; if (self.lastPrice && self.lastPrice !== 0) { pctChange = (self.price - self.lastPrice) / self.lastPrice * 100; } var pctStr = ''; var pctColor = "#BDC3C7"; if (pctChange > 0.01) { pctStr = '+' + pctChange.toFixed(2) + '%'; pctColor = "#2ecc71"; } else if (pctChange < -0.01) { pctStr = pctChange.toFixed(2) + '%'; pctColor = "#e74c3c"; } else { pctStr = '+0.00%'; pctColor = "#BDC3C7"; } changeTxt.setText(pctStr); changeTxt.setStyle({ fill: pctColor }); holdingsTxt.setText(self.holdings + ' shares'); // Calculate average cost for this stock var lots = playerLots[self.companyIndex]; var totalShares = 0; var totalCost = 0; for (var i = 0; i < lots.length; i++) { totalShares += lots[i].shares; totalCost += lots[i].shares * lots[i].price; } var avgCost = totalShares > 0 ? totalCost / totalShares : 0; // Profit is unrealized gain/loss: (current price - avgCost) * shares held var profit = totalShares > 0 ? Math.round((self.price - avgCost) * totalShares) : 0; var profitColor = "#ffffff"; if (profit > 0) profitColor = "#2ecc71"; if (profit < 0) profitColor = "#e74c3c"; profitTxt.setText((profit >= 0 ? '+' : '') + profit); // Use setStyle to update fill color safely profitTxt.setStyle({ fill: profitColor }); }; // Buy handler buyBtn.down = function (x, y, obj) { // Use global selectedShareAmount for buy var amount = typeof selectedShareAmount !== "undefined" && selectedShareAmount > 0 ? selectedShareAmount : 1; var maxBuy = Math.floor(playerCash / self.price); var buyCount = Math.min(amount, maxBuy); if (buyCount > 0) { playerCash -= self.price * buyCount; self.holdings += buyCount; // Add a new lot for these shares at current price var lots = playerLots[self.companyIndex]; lots.push({ shares: buyCount, price: self.price }); // Clean up: merge lots with same price (optional, for tidiness) var merged = []; for (var i = 0; i < lots.length; i++) { var found = false; for (var j = 0; j < merged.length; j++) { if (merged[j].price === lots[i].price) { merged[j].shares += lots[i].shares; found = true; break; } } if (!found) merged.push({ shares: lots[i].shares, price: lots[i].price }); } playerLots[self.companyIndex] = merged; self.updateUI(); updateCashUI(); } }; // Sell handler sellBtn.down = function (x, y, obj) { // Use global selectedShareAmount for sell var amount = typeof selectedShareAmount !== "undefined" && selectedShareAmount > 0 ? selectedShareAmount : 1; var sellCount = Math.min(amount, self.holdings); if (sellCount > 0) { playerCash += self.price * sellCount; self.holdings -= sellCount; // Remove shares from lots (FIFO) var lots = playerLots[self.companyIndex]; var toSell = sellCount; var idx = 0; while (toSell > 0 && idx < lots.length) { if (lots[idx].shares <= toSell) { toSell -= lots[idx].shares; lots[idx].shares = 0; idx++; } else { lots[idx].shares -= toSell; toSell = 0; } } // Remove empty lots var newLots = []; for (var i = 0; i < lots.length; i++) { if (lots[i].shares > 0) newLots.push(lots[i]); } playerLots[self.companyIndex] = newLots; self.updateUI(); updateCashUI(); } }; // For updating price and profit self.setPrice = function (newPrice) { self.lastPrice = self.price; self.price = newPrice; // Update holdings from lots (in case lots changed externally) var lots = playerLots[self.companyIndex]; var totalShares = 0; for (var i = 0; i < lots.length; i++) { totalShares += lots[i].shares; } self.holdings = totalShares; self.updateUI(); }; // For updating holdings externally self.setHoldings = function (newHoldings) { self.holdings = newHoldings; // Also reset lots to match (used for reset/game start) var lots = playerLots[self.companyIndex]; lots.length = 0; if (newHoldings > 0) { lots.push({ shares: newHoldings, price: self.price }); } self.updateUI(); }; // For updating profit display externally self.updateProfit = function () { self.updateUI(); }; // For setting up after construction self.init = function (companyIndex, companyName, colorId, price) { self.companyIndex = companyIndex; self.companyName = companyName; self.colorId = colorId; self.price = price; self.lastPrice = price; icon.setAsset(colorId); self.updateUI(); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222a36 }); /**** * Game Code ****/ // Music: Lightly tense, smooth, jazzy/lo-fi, gentle bass, soft percussion, subtle synths // --- Game Data --- // 10 company colors for stocks (distinct, visually clear) // Button shapes var companyNames = ["AlphaTech", "BetaBank", "GammaFoods", "DeltaPharma", "EpsilonEnergy", "ZetaAuto", "EtaRetail", "ThetaMedia", "IotaLogix", "KappaSpace"]; var companyColors = ['stockA', 'stockB', 'stockC', 'stockD', 'stockE', 'stockF', 'stockG', 'stockH', 'stockI', 'stockJ']; // Initial prices (randomized a bit for variety) var initialPrices = []; for (var i = 0; i < 10; i++) { initialPrices[i] = 80 + Math.floor(Math.random() * 40) * 5; // 80-280 } // Player state var playerCash = 10000; // playerHoldings: number of shares per stock (for UI/wealth only) var playerHoldings = []; for (var i = 0; i < 10; i++) playerHoldings[i] = 0; // playerLots: for each stock, an array of {shares, price} objects representing purchase lots var playerLots = []; for (var i = 0; i < 10; i++) playerLots[i] = []; // Stock state var stockPrices = []; var stockLastPrices = []; for (var i = 0; i < 10; i++) { stockPrices[i] = initialPrices[i]; stockLastPrices[i] = initialPrices[i]; } // Timer var gameDurationSec = 900; // 15 minutes var timeLeft = gameDurationSec; var timerInterval = null; // UI elements var cashTxt = null; var timerTxt = null; var wealthTxt = null; var stockRows = []; var tableContainer = null; // --- UI Setup --- // --- Mute Button (top left, avoid 100x100 area) --- var isMusicMuted = false; var muteBtnSize = 80; var muteBtn = LK.getAsset('buyBtn', { anchorX: 0, anchorY: 0, x: 110, // leave 10px gap from left edge and out of 100x100 menu area y: 10, width: muteBtnSize, height: muteBtnSize, color: 0x34495e }); muteBtn.alpha = 0.7; muteBtn.interactive = true; muteBtn.buttonMode = true; // Mute icon text (simple, as we can't use images) var muteIconTxt = new Text2('🔊', { size: 54, fill: 0xF1C40F }); muteIconTxt.anchor.set(0.5, 0.5); muteIconTxt.x = muteBtn.x + muteBtnSize / 2; muteIconTxt.y = muteBtn.y + muteBtnSize / 2; // Mute/unmute logic muteBtn.down = function (x, y, obj) { isMusicMuted = !isMusicMuted; if (isMusicMuted) { LK.stopMusic(); muteIconTxt.setText('🔇'); } else { LK.playMusic('bg_stockfloor', { loop: true, fade: { start: 0, end: 0.7, duration: 800 } }); muteIconTxt.setText('🔊'); } }; // Add to gui.topLeft (but offset to avoid menu) LK.gui.topLeft.addChild(muteBtn); LK.gui.topLeft.addChild(muteIconTxt); // Cash display (top right, avoid top left 100x100) cashTxt = new Text2('Cash: ₲' + playerCash, { size: 72, fill: 0xF1C40F }); cashTxt.anchor.set(1, 0); cashTxt.x = 0; // x/y are ignored for gui.topRight, but set to 0 for clarity cashTxt.y = 0; LK.gui.topRight.addChild(cashTxt); // Timer display (top center) timerTxt = new Text2('15:00', { size: 72, fill: 0xFFFFFF }); timerTxt.anchor.set(0.5, 0); // Do not set x/y directly, let LK.gui.top handle centering LK.gui.top.addChild(timerTxt); // Wealth display (below timer) wealthTxt = new Text2('Wealth: ₲' + playerCash, { size: 56, fill: 0x2ECC71 }); wealthTxt.anchor.set(0.5, 0); wealthTxt.x = 2048 / 2; wealthTxt.y = 130; LK.gui.top.addChild(wealthTxt); // Table container (centered, scroll not needed for 10 rows) tableContainer = new Container(); tableContainer.x = 0; tableContainer.y = 300; game.addChild(tableContainer); // Table header var headerY = 0; // Table header columns for better alignment var headerX = [160, 500, 670, 880, 1130]; // Company, Price, Change, Holdings, Profit (Profit moved from 1100 to 1130) var headerTitles = ["Company", "Price", "Change", "Holdings", "Profit"]; for (var i = 0; i < headerTitles.length; i++) { var colHeader = new Text2(headerTitles[i], { size: 48, fill: 0xBDC3C7 }); colHeader.anchor.set(0, 0); colHeader.x = headerX[i]; colHeader.y = headerY; tableContainer.addChild(colHeader); } // Total Profit/Loss label and display (at end of profit column) var totalProfitLabel = new Text2('Total Profit:', { size: 48, fill: 0xBDC3C7 }); totalProfitLabel.anchor.set(1, 0); totalProfitLabel.x = 1130; totalProfitLabel.y = 80 + 10 * 120 + 10; tableContainer.addChild(totalProfitLabel); var totalProfitTxt = new Text2('', { size: 48, fill: 0xFFFFFF }); totalProfitTxt.anchor.set(0, 0); totalProfitTxt.x = 1150; totalProfitTxt.y = 80 + 10 * 120 + 10; tableContainer.addChild(totalProfitTxt); // --- Global Share Amount Selector Buttons --- var shareAmounts = [1, 5, 10, 25]; var selectedShareAmount = 1; var shareAmountBtns = []; var shareAmountBtnTxts = []; var shareAmountBtnBg = []; var shareAmountBtnY = 10; // y offset above first row var shareAmountBtnX0 = 1575; // align with amount box column var shareAmountBtnSpacing = 110; var shareAmountBtnW = 90; var shareAmountBtnH = 60; var shareAmountBtnColor = 0x2980b9; var shareAmountBtnColorSelected = 0xf1c40f; // Container for selector buttons var selectorBtnContainer = new Container(); selectorBtnContainer.x = shareAmountBtnX0 - shareAmountBtnSpacing * 1.5; selectorBtnContainer.y = shareAmountBtnY; tableContainer.addChild(selectorBtnContainer); for (var i = 0; i < shareAmounts.length; i++) { // Use buyBtn shape for button background var btnBg = LK.getAsset('buyBtn', { anchorX: 0.5, anchorY: 0.5, x: i * shareAmountBtnSpacing, y: 0, width: shareAmountBtnW, height: shareAmountBtnH }); btnBg.tint = shareAmounts[i] === selectedShareAmount ? shareAmountBtnColorSelected : shareAmountBtnColor; selectorBtnContainer.addChild(btnBg); shareAmountBtnBg.push(btnBg); // Button label var btnTxt = new Text2('' + shareAmounts[i], { size: 36, fill: 0xFFFFFF }); btnTxt.anchor.set(0.5, 0.5); btnTxt.x = btnBg.x; btnTxt.y = btnBg.y; selectorBtnContainer.addChild(btnTxt); shareAmountBtnTxts.push(btnTxt); // Button logic (function (idx, amount) { btnBg.down = function (x, y, obj) { selectedShareAmount = amount; // Update highlight for (var j = 0; j < shareAmountBtnBg.length; j++) { shareAmountBtnBg[j].tint = shareAmounts[j] === selectedShareAmount ? shareAmountBtnColorSelected : shareAmountBtnColor; } // Update all amount textboxes in all rows for (var k = 0; k < stockRows.length; k++) { if (stockRows[k].setAmountBoxValue) { stockRows[k].setAmountBoxValue(selectedShareAmount); } } }; })(i, shareAmounts[i]); shareAmountBtns.push(btnBg); } // --- Stock Rows --- for (var i = 0; i < 10; i++) { var row = new StockRow(); row.init(i, companyNames[i], companyColors[i], stockPrices[i]); row.y = 80 + i * 120; tableContainer.addChild(row); stockRows.push(row); } // --- Company Detail Section (Chart + News) --- // Price history for each company (last 20 prices) var companyPriceHistory = []; for (var i = 0; i < 10; i++) { companyPriceHistory[i] = []; for (var j = 0; j < 20; j++) companyPriceHistory[i].push(stockPrices[i]); } // Fictional news headlines for each company, classified as positive/negative/neutral var companyNews = [{ positive: ["AlphaTech launches new AI chip", "AlphaTech CEO: 'Innovation is our DNA'", "AlphaTech partners with BetaBank", "AlphaTech stock soars on strong earnings", "AlphaTech unveils breakthrough technology"], negative: ["AlphaTech faces supply chain delays", "AlphaTech stock dips after market uncertainty", "AlphaTech recalls product line", "AlphaTech under investigation for patent dispute"], neutral: ["AlphaTech holds annual shareholder meeting", "AlphaTech: No major news today"] }, { positive: ["BetaBank expands to new markets", "BetaBank reports record profits", "BetaBank launches mobile app", "BetaBank receives top customer service award"], negative: ["BetaBank fined for compliance issues", "BetaBank stock falls after earnings miss", "BetaBank faces cyberattack"], neutral: ["BetaBank: No significant changes reported"] }, { positive: ["GammaFoods unveils plant-based burger", "GammaFoods opens 100th store", "GammaFoods: 'Healthy eating for all'", "GammaFoods sales hit all-time high"], negative: ["GammaFoods faces supply shortage", "GammaFoods stock drops after recall", "GammaFoods reports lower quarterly profits"], neutral: ["GammaFoods: No major news today"] }, { positive: ["DeltaPharma vaccine approved", "DeltaPharma acquires Medix", "DeltaPharma Q2 profits soar", "DeltaPharma receives innovation award"], negative: ["DeltaPharma faces regulatory setback", "DeltaPharma stock falls on trial results", "DeltaPharma issues product warning"], neutral: ["DeltaPharma: No significant news"] }, { positive: ["EpsilonEnergy invests in solar", "EpsilonEnergy wins green award", "EpsilonEnergy: Oil prices stable", "EpsilonEnergy expands wind farm"], negative: ["EpsilonEnergy stock drops on oil price fall", "EpsilonEnergy faces environmental protest", "EpsilonEnergy reports lower revenue"], neutral: ["EpsilonEnergy: No major news today"] }, { positive: ["ZetaAuto reveals electric SUV", "ZetaAuto sales up 20%", "ZetaAuto opens new factory", "ZetaAuto receives safety award"], negative: ["ZetaAuto recalls vehicles", "ZetaAuto stock dips after earnings", "ZetaAuto faces supply chain issues"], neutral: ["ZetaAuto: No significant news"] }, { positive: ["EtaRetail launches online store", "EtaRetail Black Friday success", "EtaRetail expands to Europe", "EtaRetail reports record sales"], negative: ["EtaRetail faces data breach", "EtaRetail stock falls on weak quarter", "EtaRetail closes underperforming stores"], neutral: ["EtaRetail: No major news today"] }, { positive: ["ThetaMedia signs streaming deal", "ThetaMedia launches new channel", "ThetaMedia ad revenue climbs", "ThetaMedia wins industry award"], negative: ["ThetaMedia stock drops after ratings slip", "ThetaMedia faces copyright lawsuit", "ThetaMedia cuts staff"], neutral: ["ThetaMedia: No significant news"] }, { positive: ["IotaLogix automates warehouses", "IotaLogix wins logistics award", "IotaLogix: 'Efficiency first'", "IotaLogix expands robotics division"], negative: ["IotaLogix stock falls on earnings miss", "IotaLogix faces labor strike", "IotaLogix recalls faulty robots"], neutral: ["IotaLogix: No major news today"] }, { positive: ["KappaSpace launches satellite", "KappaSpace partners with NASA", "KappaSpace: 'Space for everyone'", "KappaSpace secures new contracts"], negative: ["KappaSpace rocket launch fails", "KappaSpace stock dips after delay", "KappaSpace faces funding shortfall"], neutral: ["KappaSpace: No significant news"] }]; // Container for detail section var companyDetailSection = new Container(); // Move detail section further down for more space companyDetailSection.x = 0; companyDetailSection.y = tableContainer.y + 80 + 10 * 120 + 120; // more padding below last row game.addChild(companyDetailSection); companyDetailSection.visible = false; // hidden by default // Chart and news containers, both larger and spaced out var chartContainer = new Container(); chartContainer.x = 120; chartContainer.y = 0; companyDetailSection.addChild(chartContainer); var newsContainer = new Container(); newsContainer.x = 900; // move news further right for larger chart newsContainer.y = 80; // move news section a bit lower companyDetailSection.addChild(newsContainer); // Helper to clear chart/news function clearCompanyDetailSection() { while (chartContainer.children.length > 0) chartContainer.removeChild(chartContainer.children[0]); while (newsContainer.children.length > 0) newsContainer.removeChild(newsContainer.children[0]); } // Draw chart for companyIndex function drawCompanyChart(companyIndex) { clearCompanyDetailSection(); // Chart area: larger for better visibility var chartW = 700, chartH = 320; var margin = 60; var history = companyPriceHistory[companyIndex]; // Find min/max for scaling var minP = history[0], maxP = history[0]; for (var i = 1; i < history.length; i++) { if (history[i] < minP) minP = history[i]; if (history[i] > maxP) maxP = history[i]; } if (maxP === minP) maxP = minP + 1; // avoid div0 // Draw axes (Y and X) var axisColor = 0xBDC3C7; // Y axis var yAxis = LK.getAsset('buyBtn', { anchorX: 0, anchorY: 0, x: margin - 4, y: margin, width: 8, height: chartH - 2 * margin, color: axisColor }); yAxis.alpha = 0.5; chartContainer.addChild(yAxis); // X axis var xAxis = LK.getAsset('buyBtn', { anchorX: 0, anchorY: 0, x: margin, y: chartH - margin - 4, width: chartW - 2 * margin, height: 8, color: axisColor }); xAxis.alpha = 0.5; chartContainer.addChild(xAxis); // Draw Y-axis labels (min, mid, max) var yLabels = [{ val: maxP, y: margin - 20 }, { val: Math.round((maxP + minP) / 2), y: (chartH - 2 * margin) / 2 + margin - 20 }, { val: minP, y: chartH - margin - 20 }]; for (var i = 0; i < yLabels.length; i++) { var yLabel = new Text2('₲' + yLabels[i].val, { size: 28, fill: 0xBDC3C7 }); yLabel.anchor.set(1, 0.5); yLabel.x = margin - 10; yLabel.y = yLabels[i].y + 16; chartContainer.addChild(yLabel); } // Draw X-axis labels (first, mid, last tick) var xLabels = [{ val: 1, x: margin }, { val: Math.floor(history.length / 2) + 1, x: margin + (chartW - 2 * margin) / 2 }, { val: history.length, x: chartW - margin }]; for (var i = 0; i < xLabels.length; i++) { var xLabel = new Text2('' + xLabels[i].val, { size: 28, fill: 0xBDC3C7 }); xLabel.anchor.set(0.5, 0); xLabel.x = xLabels[i].x; xLabel.y = chartH - margin + 12; chartContainer.addChild(xLabel); } // Draw connected line (simulate with small rectangles between points) for (var i = 1; i < history.length; i++) { var px0 = margin + (chartW - 2 * margin) * ((i - 1) / (history.length - 1)); var py0 = margin + (chartH - 2 * margin) * (1 - (history[i - 1] - minP) / (maxP - minP)); var px1 = margin + (chartW - 2 * margin) * (i / (history.length - 1)); var py1 = margin + (chartH - 2 * margin) * (1 - (history[i] - minP) / (maxP - minP)); // Draw a thin rectangle between (px0,py0) and (px1,py1) var dx = px1 - px0; var dy = py1 - py0; var len = Math.sqrt(dx * dx + dy * dy); var angle = Math.atan2(dy, dx); var line = LK.getAsset('buyBtn', { anchorX: 0.5, anchorY: 0.5, x: (px0 + px1) / 2, y: (py0 + py1) / 2, width: len, height: 8, color: 0xF1C40F }); line.rotation = angle; line.alpha = 0.8; chartContainer.addChild(line); } // Draw points (dots) on top of the line for (var i = 0; i < history.length; i++) { var px = margin + (chartW - 2 * margin) * (i / (history.length - 1)); var py = margin + (chartH - 2 * margin) * (1 - (history[i] - minP) / (maxP - minP)); var dot = LK.getAsset(companyColors[companyIndex], { anchorX: 0.5, anchorY: 0.5, x: px, y: py, width: 18, height: 18 }); chartContainer.addChild(dot); } // Chart border var border = LK.getAsset('buyBtn', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: chartW, height: chartH, color: 0x222a36 }); border.alpha = 0.3; chartContainer.addChild(border); // Chart title var chartTitle = new Text2(companyNames[companyIndex] + " Price History", { size: 44, fill: 0xFFFFFF }); chartTitle.anchor.set(0, 0); chartTitle.x = 0; chartTitle.y = chartH + 18; chartContainer.addChild(chartTitle); } // Draw news for companyIndex function drawCompanyNews(companyIndex) { // Determine price trend: up, down, or neutral var trend = "neutral"; if (stockPrices[companyIndex] > stockLastPrices[companyIndex]) trend = "positive";else if (stockPrices[companyIndex] < stockLastPrices[companyIndex]) trend = "negative"; // Pick news pool var newsPool = companyNews[companyIndex][trend]; // If no news in pool, fallback to neutral if (!newsPool || newsPool.length === 0) newsPool = companyNews[companyIndex].neutral; // Pick up to 3 random headlines from the pool (no repeats) var shown = []; var used = {}; for (var i = 0; i < Math.min(3, newsPool.length); i++) { var idx; do { idx = Math.floor(Math.random() * newsPool.length); } while (used[idx] && Object.keys(used).length < newsPool.length); used[idx] = true; shown.push(newsPool[idx]); } // Show news for (var i = 0; i < shown.length; i++) { var fillColor = trend === "positive" ? 0x2ecc71 : trend === "negative" ? 0xe74c3c : 0xBDC3C7; var newsTxt = new Text2("- " + shown[i], { size: 44, fill: fillColor }); newsTxt.anchor.set(0, 0); newsTxt.x = 0; newsTxt.y = i * 64; newsContainer.addChild(newsTxt); } // News title var newsTitle = new Text2(trend === "positive" ? "Positive News" : trend === "negative" ? "Negative News" : "Recent News", { size: 44, fill: 0xFFFFFF }); newsTitle.anchor.set(0, 0); newsTitle.x = 0; newsTitle.y = -60; newsContainer.addChild(newsTitle); } // Show detail section for company var currentDetailCompany = -1; function showCompanyDetail(companyIndex) { if (currentDetailCompany === companyIndex) return; currentDetailCompany = companyIndex; companyDetailSection.visible = true; // Move detail section to top of display list so it's not hidden by table if (companyDetailSection.parent && companyDetailSection.parent.children) { var parent = companyDetailSection.parent; var idx = parent.children.indexOf(companyDetailSection); if (idx !== -1 && idx !== parent.children.length - 1) { parent.removeChild(companyDetailSection); parent.addChild(companyDetailSection); } } drawCompanyChart(companyIndex); drawCompanyNews(companyIndex); } // Hide detail section function hideCompanyDetail() { companyDetailSection.visible = false; currentDetailCompany = -1; clearCompanyDetailSection(); } // Company name click handled by button background in StockRow // Optionally, tap outside detail section to hide it (not required, but nice UX) companyDetailSection.down = function (x, y, obj) { hideCompanyDetail(); }; // Update total profit/loss function updateTotalProfit() { var totalProfit = 0; for (var i = 0; i < 10; i++) { var lots = playerLots[i]; var curPrice = stockRows[i].price; for (var j = 0; j < lots.length; j++) { totalProfit += Math.round((curPrice - lots[j].price) * lots[j].shares); } } var profitColor = "#ffffff"; if (totalProfit > 0) profitColor = "#2ecc71"; if (totalProfit < 0) profitColor = "#e74c3c"; totalProfitTxt.setText((totalProfit >= 0 ? '+' : '') + totalProfit); totalProfitTxt.setStyle({ fill: profitColor }); } // --- UI Update Functions --- function updateCashUI() { cashTxt.setText('Cash: ₲' + playerCash); updateWealthUI(); updateTotalProfit(); } function updateWealthUI() { var wealth = playerCash; for (var i = 0; i < 10; i++) { wealth += playerHoldings[i] * stockPrices[i]; } wealthTxt.setText('Wealth: ₲' + wealth); } function updateTimerUI() { var min = Math.floor(timeLeft / 60); var sec = timeLeft % 60; var t = (min < 10 ? '0' : '') + min + ':' + (sec < 10 ? '0' : '') + sec; timerTxt.setText(t); } // --- Stock Price Update Logic --- // Simulate price changes every 10 seconds function updateStockPrices() { for (var i = 0; i < 10; i++) { stockLastPrices[i] = stockPrices[i]; // Random walk: -8% to +8% var pct = Math.random() * 0.16 - 0.08; // Occasionally, a "news event" (1 in 8 chance): -25% to +25% if (Math.random() < 0.125) { pct = Math.random() * 0.5 - 0.25; } var newPrice = Math.max(10, Math.round(stockPrices[i] * (1 + pct))); stockPrices[i] = newPrice; // Update price history for chart if (typeof companyPriceHistory !== "undefined" && companyPriceHistory[i]) { companyPriceHistory[i].push(newPrice); if (companyPriceHistory[i].length > 20) companyPriceHistory[i].shift(); // If this company is currently shown in detail, redraw chart and news if (typeof currentDetailCompany !== "undefined" && currentDetailCompany === i && companyDetailSection.visible) { drawCompanyChart(i); drawCompanyNews(i); } } // Update row stockRows[i].setPrice(newPrice); playerHoldings[i] = stockRows[i].holdings; } updateWealthUI(); updateTotalProfit(); } // --- Timer Logic --- function tickTimer() { timeLeft -= 1; if (timeLeft < 0) timeLeft = 0; updateTimerUI(); // Only end game if timeLeft < 0 (so timer shows 00:00 for a full second) if (timeLeft < 0) { endGame(); } } // --- End Game --- function endGame() { // Calculate final wealth var finalWealth = playerCash; for (var i = 0; i < 10; i++) { finalWealth += playerHoldings[i] * stockPrices[i]; } // Show game over (handled by LK) LK.showGameOver(); } // --- Game Update Loop --- game.update = function () { // No per-frame logic needed for this MVP }; // --- Start Game Logic --- function startGame() { // Reset state playerCash = 10000; for (var i = 0; i < 10; i++) { stockPrices[i] = initialPrices[i]; stockLastPrices[i] = initialPrices[i]; playerHoldings[i] = 0; playerLots[i] = []; stockRows[i].setPrice(stockPrices[i]); stockRows[i].setHoldings(0); } timeLeft = gameDurationSec; updateCashUI(); updateTimerUI(); updateWealthUI(); hideCompanyDetail(); // Start price update interval (every 10s) if (timerInterval) LK.clearInterval(timerInterval); timerInterval = LK.setInterval(function () { updateStockPrices(); }, 10000); // Start timer (every 1s) if (game._timerTick) LK.clearInterval(game._timerTick); game._timerTick = LK.setInterval(function () { tickTimer(); }, 1000); } // Play background music (looping, fade in for smoothness) LK.playMusic('bg_stockfloor', { loop: true, fade: { start: 0, end: 0.7, duration: 1200 } }); // --- Start the game --- startGame(); ;
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// StockRow: A row in the stock table, with price, holdings, buy/sell buttons
var StockRow = Container.expand(function () {
var self = Container.call(this);
// Properties
self.companyIndex = 0; // 0-9
self.companyName = '';
self.colorId = '';
self.price = 0;
self.holdings = 0;
self.lastPrice = 0;
// UI elements
var yPad = 0;
// Stock icon
var icon = self.attachAsset(self.colorId, {
anchorX: 0.5,
anchorY: 0.5,
x: 80,
y: 60 + yPad,
scaleX: 0.7,
scaleY: 0.7
});
// Company name button background
var nameBtnBg = self.attachAsset('buyBtn', {
anchorX: 0,
anchorY: 0,
x: 160,
y: 20 + yPad,
width: 320,
height: 60,
color: 0x34495e
});
nameBtnBg.alpha = 0.25;
// Company name
var nameTxt = new Text2(self.companyName, {
size: 48,
fill: 0xFFFFFF
});
nameTxt.x = 170;
nameTxt.y = 30 + yPad;
nameTxt.anchor.set(0, 0);
self.addChild(nameTxt);
// Make the button background clickable for company details
nameBtnBg.interactive = true;
nameBtnBg.buttonMode = true;
nameBtnBg.down = function (x, y, obj) {
if (typeof showCompanyDetail === "function") {
showCompanyDetail(self.companyIndex);
}
};
// Price text
var priceTxt = new Text2('', {
size: 48,
fill: 0xF1C40F
});
priceTxt.x = 500;
priceTxt.y = 30 + yPad;
priceTxt.anchor.set(0, 0);
self.addChild(priceTxt);
// Last price change percent text (new column)
var changeTxt = new Text2('', {
size: 44,
fill: 0xFFFFFF
});
changeTxt.x = 670;
changeTxt.y = 34 + yPad;
changeTxt.anchor.set(0, 0);
self.addChild(changeTxt);
// Holdings text
var holdingsTxt = new Text2('', {
size: 48,
fill: 0x2ECC71
});
holdingsTxt.x = 880;
holdingsTxt.y = 30 + yPad;
holdingsTxt.anchor.set(0, 0);
self.addChild(holdingsTxt);
// Profit text
var profitTxt = new Text2('', {
size: 48,
fill: 0xFFFFFF
});
profitTxt.x = 1130;
profitTxt.y = 30 + yPad;
profitTxt.anchor.set(0, 0);
self.addChild(profitTxt);
// Buy button
var buyBtn = self.attachAsset('buyBtn', {
anchorX: 0.5,
anchorY: 0.5,
x: 1450,
y: 60 + yPad
});
var buyTxt = new Text2('Buy', {
size: 36,
fill: 0xFFFFFF
});
buyTxt.anchor.set(0.5, 0.5);
buyTxt.x = buyBtn.x;
buyTxt.y = buyBtn.y;
self.addChild(buyTxt);
// Amount textbox (between buy and sell)
var amountBoxBg = self.attachAsset('buyBtn', {
anchorX: 0.5,
anchorY: 0.5,
x: 1575,
y: 60 + yPad,
scaleX: 0.7,
scaleY: 0.7
});
var amountTxt = new Text2(typeof selectedShareAmount !== "undefined" ? '' + selectedShareAmount : '1', {
size: 36,
fill: 0x222a36
});
amountTxt.anchor.set(0.5, 0.5);
amountTxt.x = amountBoxBg.x;
amountTxt.y = amountBoxBg.y;
self.addChild(amountTxt);
// Touch to edit amount (prompt for number)
amountBoxBg.down = function (x, y, obj) {
// On mobile, prompt is supported in LK sandbox for numeric input
var current = parseInt(amountTxt.text, 10);
if (isNaN(current) || current < 1) current = 1;
var entered = prompt("Enter number of shares:", current);
var val = parseInt(entered, 10);
if (isNaN(val) || val < 1) val = 1;
amountTxt.setText('' + val);
// Also update global selectedShareAmount and highlight if matches a selector
if (typeof selectedShareAmount !== "undefined") {
selectedShareAmount = val;
if (typeof shareAmountBtnBg !== "undefined" && typeof shareAmounts !== "undefined") {
for (var j = 0; j < shareAmountBtnBg.length; j++) {
shareAmountBtnBg[j].tint = shareAmounts[j] === selectedShareAmount ? 0xf1c40f : 0x2980b9;
}
}
// Update all amount boxes to match
if (typeof stockRows !== "undefined") {
for (var k = 0; k < stockRows.length; k++) {
if (stockRows[k].setAmountBoxValue) {
stockRows[k].setAmountBoxValue(selectedShareAmount);
}
}
}
}
};
// Method to allow global selector to update this textbox
self.setAmountBoxValue = function (val) {
if (typeof val === "number" && val > 0) {
amountTxt.setText('' + val);
}
};
// Sell button
var sellBtn = self.attachAsset('sellBtn', {
anchorX: 0.5,
anchorY: 0.5,
x: 1700,
y: 60 + yPad
});
var sellTxt = new Text2('Sell', {
size: 36,
fill: 0xFFFFFF
});
sellTxt.anchor.set(0.5, 0.5);
sellTxt.x = sellBtn.x;
sellTxt.y = sellBtn.y;
self.addChild(sellTxt);
// Update UI
self.updateUI = function () {
nameTxt.setText(self.companyName);
priceTxt.setText('₲' + self.price);
// Show last price change percent and color
var pctChange = 0;
if (self.lastPrice && self.lastPrice !== 0) {
pctChange = (self.price - self.lastPrice) / self.lastPrice * 100;
}
var pctStr = '';
var pctColor = "#BDC3C7";
if (pctChange > 0.01) {
pctStr = '+' + pctChange.toFixed(2) + '%';
pctColor = "#2ecc71";
} else if (pctChange < -0.01) {
pctStr = pctChange.toFixed(2) + '%';
pctColor = "#e74c3c";
} else {
pctStr = '+0.00%';
pctColor = "#BDC3C7";
}
changeTxt.setText(pctStr);
changeTxt.setStyle({
fill: pctColor
});
holdingsTxt.setText(self.holdings + ' shares');
// Calculate average cost for this stock
var lots = playerLots[self.companyIndex];
var totalShares = 0;
var totalCost = 0;
for (var i = 0; i < lots.length; i++) {
totalShares += lots[i].shares;
totalCost += lots[i].shares * lots[i].price;
}
var avgCost = totalShares > 0 ? totalCost / totalShares : 0;
// Profit is unrealized gain/loss: (current price - avgCost) * shares held
var profit = totalShares > 0 ? Math.round((self.price - avgCost) * totalShares) : 0;
var profitColor = "#ffffff";
if (profit > 0) profitColor = "#2ecc71";
if (profit < 0) profitColor = "#e74c3c";
profitTxt.setText((profit >= 0 ? '+' : '') + profit);
// Use setStyle to update fill color safely
profitTxt.setStyle({
fill: profitColor
});
};
// Buy handler
buyBtn.down = function (x, y, obj) {
// Use global selectedShareAmount for buy
var amount = typeof selectedShareAmount !== "undefined" && selectedShareAmount > 0 ? selectedShareAmount : 1;
var maxBuy = Math.floor(playerCash / self.price);
var buyCount = Math.min(amount, maxBuy);
if (buyCount > 0) {
playerCash -= self.price * buyCount;
self.holdings += buyCount;
// Add a new lot for these shares at current price
var lots = playerLots[self.companyIndex];
lots.push({
shares: buyCount,
price: self.price
});
// Clean up: merge lots with same price (optional, for tidiness)
var merged = [];
for (var i = 0; i < lots.length; i++) {
var found = false;
for (var j = 0; j < merged.length; j++) {
if (merged[j].price === lots[i].price) {
merged[j].shares += lots[i].shares;
found = true;
break;
}
}
if (!found) merged.push({
shares: lots[i].shares,
price: lots[i].price
});
}
playerLots[self.companyIndex] = merged;
self.updateUI();
updateCashUI();
}
};
// Sell handler
sellBtn.down = function (x, y, obj) {
// Use global selectedShareAmount for sell
var amount = typeof selectedShareAmount !== "undefined" && selectedShareAmount > 0 ? selectedShareAmount : 1;
var sellCount = Math.min(amount, self.holdings);
if (sellCount > 0) {
playerCash += self.price * sellCount;
self.holdings -= sellCount;
// Remove shares from lots (FIFO)
var lots = playerLots[self.companyIndex];
var toSell = sellCount;
var idx = 0;
while (toSell > 0 && idx < lots.length) {
if (lots[idx].shares <= toSell) {
toSell -= lots[idx].shares;
lots[idx].shares = 0;
idx++;
} else {
lots[idx].shares -= toSell;
toSell = 0;
}
}
// Remove empty lots
var newLots = [];
for (var i = 0; i < lots.length; i++) {
if (lots[i].shares > 0) newLots.push(lots[i]);
}
playerLots[self.companyIndex] = newLots;
self.updateUI();
updateCashUI();
}
};
// For updating price and profit
self.setPrice = function (newPrice) {
self.lastPrice = self.price;
self.price = newPrice;
// Update holdings from lots (in case lots changed externally)
var lots = playerLots[self.companyIndex];
var totalShares = 0;
for (var i = 0; i < lots.length; i++) {
totalShares += lots[i].shares;
}
self.holdings = totalShares;
self.updateUI();
};
// For updating holdings externally
self.setHoldings = function (newHoldings) {
self.holdings = newHoldings;
// Also reset lots to match (used for reset/game start)
var lots = playerLots[self.companyIndex];
lots.length = 0;
if (newHoldings > 0) {
lots.push({
shares: newHoldings,
price: self.price
});
}
self.updateUI();
};
// For updating profit display externally
self.updateProfit = function () {
self.updateUI();
};
// For setting up after construction
self.init = function (companyIndex, companyName, colorId, price) {
self.companyIndex = companyIndex;
self.companyName = companyName;
self.colorId = colorId;
self.price = price;
self.lastPrice = price;
icon.setAsset(colorId);
self.updateUI();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222a36
});
/****
* Game Code
****/
// Music: Lightly tense, smooth, jazzy/lo-fi, gentle bass, soft percussion, subtle synths
// --- Game Data ---
// 10 company colors for stocks (distinct, visually clear)
// Button shapes
var companyNames = ["AlphaTech", "BetaBank", "GammaFoods", "DeltaPharma", "EpsilonEnergy", "ZetaAuto", "EtaRetail", "ThetaMedia", "IotaLogix", "KappaSpace"];
var companyColors = ['stockA', 'stockB', 'stockC', 'stockD', 'stockE', 'stockF', 'stockG', 'stockH', 'stockI', 'stockJ'];
// Initial prices (randomized a bit for variety)
var initialPrices = [];
for (var i = 0; i < 10; i++) {
initialPrices[i] = 80 + Math.floor(Math.random() * 40) * 5; // 80-280
}
// Player state
var playerCash = 10000;
// playerHoldings: number of shares per stock (for UI/wealth only)
var playerHoldings = [];
for (var i = 0; i < 10; i++) playerHoldings[i] = 0;
// playerLots: for each stock, an array of {shares, price} objects representing purchase lots
var playerLots = [];
for (var i = 0; i < 10; i++) playerLots[i] = [];
// Stock state
var stockPrices = [];
var stockLastPrices = [];
for (var i = 0; i < 10; i++) {
stockPrices[i] = initialPrices[i];
stockLastPrices[i] = initialPrices[i];
}
// Timer
var gameDurationSec = 900; // 15 minutes
var timeLeft = gameDurationSec;
var timerInterval = null;
// UI elements
var cashTxt = null;
var timerTxt = null;
var wealthTxt = null;
var stockRows = [];
var tableContainer = null;
// --- UI Setup ---
// --- Mute Button (top left, avoid 100x100 area) ---
var isMusicMuted = false;
var muteBtnSize = 80;
var muteBtn = LK.getAsset('buyBtn', {
anchorX: 0,
anchorY: 0,
x: 110,
// leave 10px gap from left edge and out of 100x100 menu area
y: 10,
width: muteBtnSize,
height: muteBtnSize,
color: 0x34495e
});
muteBtn.alpha = 0.7;
muteBtn.interactive = true;
muteBtn.buttonMode = true;
// Mute icon text (simple, as we can't use images)
var muteIconTxt = new Text2('🔊', {
size: 54,
fill: 0xF1C40F
});
muteIconTxt.anchor.set(0.5, 0.5);
muteIconTxt.x = muteBtn.x + muteBtnSize / 2;
muteIconTxt.y = muteBtn.y + muteBtnSize / 2;
// Mute/unmute logic
muteBtn.down = function (x, y, obj) {
isMusicMuted = !isMusicMuted;
if (isMusicMuted) {
LK.stopMusic();
muteIconTxt.setText('🔇');
} else {
LK.playMusic('bg_stockfloor', {
loop: true,
fade: {
start: 0,
end: 0.7,
duration: 800
}
});
muteIconTxt.setText('🔊');
}
};
// Add to gui.topLeft (but offset to avoid menu)
LK.gui.topLeft.addChild(muteBtn);
LK.gui.topLeft.addChild(muteIconTxt);
// Cash display (top right, avoid top left 100x100)
cashTxt = new Text2('Cash: ₲' + playerCash, {
size: 72,
fill: 0xF1C40F
});
cashTxt.anchor.set(1, 0);
cashTxt.x = 0; // x/y are ignored for gui.topRight, but set to 0 for clarity
cashTxt.y = 0;
LK.gui.topRight.addChild(cashTxt);
// Timer display (top center)
timerTxt = new Text2('15:00', {
size: 72,
fill: 0xFFFFFF
});
timerTxt.anchor.set(0.5, 0);
// Do not set x/y directly, let LK.gui.top handle centering
LK.gui.top.addChild(timerTxt);
// Wealth display (below timer)
wealthTxt = new Text2('Wealth: ₲' + playerCash, {
size: 56,
fill: 0x2ECC71
});
wealthTxt.anchor.set(0.5, 0);
wealthTxt.x = 2048 / 2;
wealthTxt.y = 130;
LK.gui.top.addChild(wealthTxt);
// Table container (centered, scroll not needed for 10 rows)
tableContainer = new Container();
tableContainer.x = 0;
tableContainer.y = 300;
game.addChild(tableContainer);
// Table header
var headerY = 0;
// Table header columns for better alignment
var headerX = [160, 500, 670, 880, 1130]; // Company, Price, Change, Holdings, Profit (Profit moved from 1100 to 1130)
var headerTitles = ["Company", "Price", "Change", "Holdings", "Profit"];
for (var i = 0; i < headerTitles.length; i++) {
var colHeader = new Text2(headerTitles[i], {
size: 48,
fill: 0xBDC3C7
});
colHeader.anchor.set(0, 0);
colHeader.x = headerX[i];
colHeader.y = headerY;
tableContainer.addChild(colHeader);
}
// Total Profit/Loss label and display (at end of profit column)
var totalProfitLabel = new Text2('Total Profit:', {
size: 48,
fill: 0xBDC3C7
});
totalProfitLabel.anchor.set(1, 0);
totalProfitLabel.x = 1130;
totalProfitLabel.y = 80 + 10 * 120 + 10;
tableContainer.addChild(totalProfitLabel);
var totalProfitTxt = new Text2('', {
size: 48,
fill: 0xFFFFFF
});
totalProfitTxt.anchor.set(0, 0);
totalProfitTxt.x = 1150;
totalProfitTxt.y = 80 + 10 * 120 + 10;
tableContainer.addChild(totalProfitTxt);
// --- Global Share Amount Selector Buttons ---
var shareAmounts = [1, 5, 10, 25];
var selectedShareAmount = 1;
var shareAmountBtns = [];
var shareAmountBtnTxts = [];
var shareAmountBtnBg = [];
var shareAmountBtnY = 10; // y offset above first row
var shareAmountBtnX0 = 1575; // align with amount box column
var shareAmountBtnSpacing = 110;
var shareAmountBtnW = 90;
var shareAmountBtnH = 60;
var shareAmountBtnColor = 0x2980b9;
var shareAmountBtnColorSelected = 0xf1c40f;
// Container for selector buttons
var selectorBtnContainer = new Container();
selectorBtnContainer.x = shareAmountBtnX0 - shareAmountBtnSpacing * 1.5;
selectorBtnContainer.y = shareAmountBtnY;
tableContainer.addChild(selectorBtnContainer);
for (var i = 0; i < shareAmounts.length; i++) {
// Use buyBtn shape for button background
var btnBg = LK.getAsset('buyBtn', {
anchorX: 0.5,
anchorY: 0.5,
x: i * shareAmountBtnSpacing,
y: 0,
width: shareAmountBtnW,
height: shareAmountBtnH
});
btnBg.tint = shareAmounts[i] === selectedShareAmount ? shareAmountBtnColorSelected : shareAmountBtnColor;
selectorBtnContainer.addChild(btnBg);
shareAmountBtnBg.push(btnBg);
// Button label
var btnTxt = new Text2('' + shareAmounts[i], {
size: 36,
fill: 0xFFFFFF
});
btnTxt.anchor.set(0.5, 0.5);
btnTxt.x = btnBg.x;
btnTxt.y = btnBg.y;
selectorBtnContainer.addChild(btnTxt);
shareAmountBtnTxts.push(btnTxt);
// Button logic
(function (idx, amount) {
btnBg.down = function (x, y, obj) {
selectedShareAmount = amount;
// Update highlight
for (var j = 0; j < shareAmountBtnBg.length; j++) {
shareAmountBtnBg[j].tint = shareAmounts[j] === selectedShareAmount ? shareAmountBtnColorSelected : shareAmountBtnColor;
}
// Update all amount textboxes in all rows
for (var k = 0; k < stockRows.length; k++) {
if (stockRows[k].setAmountBoxValue) {
stockRows[k].setAmountBoxValue(selectedShareAmount);
}
}
};
})(i, shareAmounts[i]);
shareAmountBtns.push(btnBg);
}
// --- Stock Rows ---
for (var i = 0; i < 10; i++) {
var row = new StockRow();
row.init(i, companyNames[i], companyColors[i], stockPrices[i]);
row.y = 80 + i * 120;
tableContainer.addChild(row);
stockRows.push(row);
}
// --- Company Detail Section (Chart + News) ---
// Price history for each company (last 20 prices)
var companyPriceHistory = [];
for (var i = 0; i < 10; i++) {
companyPriceHistory[i] = [];
for (var j = 0; j < 20; j++) companyPriceHistory[i].push(stockPrices[i]);
}
// Fictional news headlines for each company, classified as positive/negative/neutral
var companyNews = [{
positive: ["AlphaTech launches new AI chip", "AlphaTech CEO: 'Innovation is our DNA'", "AlphaTech partners with BetaBank", "AlphaTech stock soars on strong earnings", "AlphaTech unveils breakthrough technology"],
negative: ["AlphaTech faces supply chain delays", "AlphaTech stock dips after market uncertainty", "AlphaTech recalls product line", "AlphaTech under investigation for patent dispute"],
neutral: ["AlphaTech holds annual shareholder meeting", "AlphaTech: No major news today"]
}, {
positive: ["BetaBank expands to new markets", "BetaBank reports record profits", "BetaBank launches mobile app", "BetaBank receives top customer service award"],
negative: ["BetaBank fined for compliance issues", "BetaBank stock falls after earnings miss", "BetaBank faces cyberattack"],
neutral: ["BetaBank: No significant changes reported"]
}, {
positive: ["GammaFoods unveils plant-based burger", "GammaFoods opens 100th store", "GammaFoods: 'Healthy eating for all'", "GammaFoods sales hit all-time high"],
negative: ["GammaFoods faces supply shortage", "GammaFoods stock drops after recall", "GammaFoods reports lower quarterly profits"],
neutral: ["GammaFoods: No major news today"]
}, {
positive: ["DeltaPharma vaccine approved", "DeltaPharma acquires Medix", "DeltaPharma Q2 profits soar", "DeltaPharma receives innovation award"],
negative: ["DeltaPharma faces regulatory setback", "DeltaPharma stock falls on trial results", "DeltaPharma issues product warning"],
neutral: ["DeltaPharma: No significant news"]
}, {
positive: ["EpsilonEnergy invests in solar", "EpsilonEnergy wins green award", "EpsilonEnergy: Oil prices stable", "EpsilonEnergy expands wind farm"],
negative: ["EpsilonEnergy stock drops on oil price fall", "EpsilonEnergy faces environmental protest", "EpsilonEnergy reports lower revenue"],
neutral: ["EpsilonEnergy: No major news today"]
}, {
positive: ["ZetaAuto reveals electric SUV", "ZetaAuto sales up 20%", "ZetaAuto opens new factory", "ZetaAuto receives safety award"],
negative: ["ZetaAuto recalls vehicles", "ZetaAuto stock dips after earnings", "ZetaAuto faces supply chain issues"],
neutral: ["ZetaAuto: No significant news"]
}, {
positive: ["EtaRetail launches online store", "EtaRetail Black Friday success", "EtaRetail expands to Europe", "EtaRetail reports record sales"],
negative: ["EtaRetail faces data breach", "EtaRetail stock falls on weak quarter", "EtaRetail closes underperforming stores"],
neutral: ["EtaRetail: No major news today"]
}, {
positive: ["ThetaMedia signs streaming deal", "ThetaMedia launches new channel", "ThetaMedia ad revenue climbs", "ThetaMedia wins industry award"],
negative: ["ThetaMedia stock drops after ratings slip", "ThetaMedia faces copyright lawsuit", "ThetaMedia cuts staff"],
neutral: ["ThetaMedia: No significant news"]
}, {
positive: ["IotaLogix automates warehouses", "IotaLogix wins logistics award", "IotaLogix: 'Efficiency first'", "IotaLogix expands robotics division"],
negative: ["IotaLogix stock falls on earnings miss", "IotaLogix faces labor strike", "IotaLogix recalls faulty robots"],
neutral: ["IotaLogix: No major news today"]
}, {
positive: ["KappaSpace launches satellite", "KappaSpace partners with NASA", "KappaSpace: 'Space for everyone'", "KappaSpace secures new contracts"],
negative: ["KappaSpace rocket launch fails", "KappaSpace stock dips after delay", "KappaSpace faces funding shortfall"],
neutral: ["KappaSpace: No significant news"]
}];
// Container for detail section
var companyDetailSection = new Container();
// Move detail section further down for more space
companyDetailSection.x = 0;
companyDetailSection.y = tableContainer.y + 80 + 10 * 120 + 120; // more padding below last row
game.addChild(companyDetailSection);
companyDetailSection.visible = false; // hidden by default
// Chart and news containers, both larger and spaced out
var chartContainer = new Container();
chartContainer.x = 120;
chartContainer.y = 0;
companyDetailSection.addChild(chartContainer);
var newsContainer = new Container();
newsContainer.x = 900; // move news further right for larger chart
newsContainer.y = 80; // move news section a bit lower
companyDetailSection.addChild(newsContainer);
// Helper to clear chart/news
function clearCompanyDetailSection() {
while (chartContainer.children.length > 0) chartContainer.removeChild(chartContainer.children[0]);
while (newsContainer.children.length > 0) newsContainer.removeChild(newsContainer.children[0]);
}
// Draw chart for companyIndex
function drawCompanyChart(companyIndex) {
clearCompanyDetailSection();
// Chart area: larger for better visibility
var chartW = 700,
chartH = 320;
var margin = 60;
var history = companyPriceHistory[companyIndex];
// Find min/max for scaling
var minP = history[0],
maxP = history[0];
for (var i = 1; i < history.length; i++) {
if (history[i] < minP) minP = history[i];
if (history[i] > maxP) maxP = history[i];
}
if (maxP === minP) maxP = minP + 1; // avoid div0
// Draw axes (Y and X)
var axisColor = 0xBDC3C7;
// Y axis
var yAxis = LK.getAsset('buyBtn', {
anchorX: 0,
anchorY: 0,
x: margin - 4,
y: margin,
width: 8,
height: chartH - 2 * margin,
color: axisColor
});
yAxis.alpha = 0.5;
chartContainer.addChild(yAxis);
// X axis
var xAxis = LK.getAsset('buyBtn', {
anchorX: 0,
anchorY: 0,
x: margin,
y: chartH - margin - 4,
width: chartW - 2 * margin,
height: 8,
color: axisColor
});
xAxis.alpha = 0.5;
chartContainer.addChild(xAxis);
// Draw Y-axis labels (min, mid, max)
var yLabels = [{
val: maxP,
y: margin - 20
}, {
val: Math.round((maxP + minP) / 2),
y: (chartH - 2 * margin) / 2 + margin - 20
}, {
val: minP,
y: chartH - margin - 20
}];
for (var i = 0; i < yLabels.length; i++) {
var yLabel = new Text2('₲' + yLabels[i].val, {
size: 28,
fill: 0xBDC3C7
});
yLabel.anchor.set(1, 0.5);
yLabel.x = margin - 10;
yLabel.y = yLabels[i].y + 16;
chartContainer.addChild(yLabel);
}
// Draw X-axis labels (first, mid, last tick)
var xLabels = [{
val: 1,
x: margin
}, {
val: Math.floor(history.length / 2) + 1,
x: margin + (chartW - 2 * margin) / 2
}, {
val: history.length,
x: chartW - margin
}];
for (var i = 0; i < xLabels.length; i++) {
var xLabel = new Text2('' + xLabels[i].val, {
size: 28,
fill: 0xBDC3C7
});
xLabel.anchor.set(0.5, 0);
xLabel.x = xLabels[i].x;
xLabel.y = chartH - margin + 12;
chartContainer.addChild(xLabel);
}
// Draw connected line (simulate with small rectangles between points)
for (var i = 1; i < history.length; i++) {
var px0 = margin + (chartW - 2 * margin) * ((i - 1) / (history.length - 1));
var py0 = margin + (chartH - 2 * margin) * (1 - (history[i - 1] - minP) / (maxP - minP));
var px1 = margin + (chartW - 2 * margin) * (i / (history.length - 1));
var py1 = margin + (chartH - 2 * margin) * (1 - (history[i] - minP) / (maxP - minP));
// Draw a thin rectangle between (px0,py0) and (px1,py1)
var dx = px1 - px0;
var dy = py1 - py0;
var len = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
var line = LK.getAsset('buyBtn', {
anchorX: 0.5,
anchorY: 0.5,
x: (px0 + px1) / 2,
y: (py0 + py1) / 2,
width: len,
height: 8,
color: 0xF1C40F
});
line.rotation = angle;
line.alpha = 0.8;
chartContainer.addChild(line);
}
// Draw points (dots) on top of the line
for (var i = 0; i < history.length; i++) {
var px = margin + (chartW - 2 * margin) * (i / (history.length - 1));
var py = margin + (chartH - 2 * margin) * (1 - (history[i] - minP) / (maxP - minP));
var dot = LK.getAsset(companyColors[companyIndex], {
anchorX: 0.5,
anchorY: 0.5,
x: px,
y: py,
width: 18,
height: 18
});
chartContainer.addChild(dot);
}
// Chart border
var border = LK.getAsset('buyBtn', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: chartW,
height: chartH,
color: 0x222a36
});
border.alpha = 0.3;
chartContainer.addChild(border);
// Chart title
var chartTitle = new Text2(companyNames[companyIndex] + " Price History", {
size: 44,
fill: 0xFFFFFF
});
chartTitle.anchor.set(0, 0);
chartTitle.x = 0;
chartTitle.y = chartH + 18;
chartContainer.addChild(chartTitle);
}
// Draw news for companyIndex
function drawCompanyNews(companyIndex) {
// Determine price trend: up, down, or neutral
var trend = "neutral";
if (stockPrices[companyIndex] > stockLastPrices[companyIndex]) trend = "positive";else if (stockPrices[companyIndex] < stockLastPrices[companyIndex]) trend = "negative";
// Pick news pool
var newsPool = companyNews[companyIndex][trend];
// If no news in pool, fallback to neutral
if (!newsPool || newsPool.length === 0) newsPool = companyNews[companyIndex].neutral;
// Pick up to 3 random headlines from the pool (no repeats)
var shown = [];
var used = {};
for (var i = 0; i < Math.min(3, newsPool.length); i++) {
var idx;
do {
idx = Math.floor(Math.random() * newsPool.length);
} while (used[idx] && Object.keys(used).length < newsPool.length);
used[idx] = true;
shown.push(newsPool[idx]);
}
// Show news
for (var i = 0; i < shown.length; i++) {
var fillColor = trend === "positive" ? 0x2ecc71 : trend === "negative" ? 0xe74c3c : 0xBDC3C7;
var newsTxt = new Text2("- " + shown[i], {
size: 44,
fill: fillColor
});
newsTxt.anchor.set(0, 0);
newsTxt.x = 0;
newsTxt.y = i * 64;
newsContainer.addChild(newsTxt);
}
// News title
var newsTitle = new Text2(trend === "positive" ? "Positive News" : trend === "negative" ? "Negative News" : "Recent News", {
size: 44,
fill: 0xFFFFFF
});
newsTitle.anchor.set(0, 0);
newsTitle.x = 0;
newsTitle.y = -60;
newsContainer.addChild(newsTitle);
}
// Show detail section for company
var currentDetailCompany = -1;
function showCompanyDetail(companyIndex) {
if (currentDetailCompany === companyIndex) return;
currentDetailCompany = companyIndex;
companyDetailSection.visible = true;
// Move detail section to top of display list so it's not hidden by table
if (companyDetailSection.parent && companyDetailSection.parent.children) {
var parent = companyDetailSection.parent;
var idx = parent.children.indexOf(companyDetailSection);
if (idx !== -1 && idx !== parent.children.length - 1) {
parent.removeChild(companyDetailSection);
parent.addChild(companyDetailSection);
}
}
drawCompanyChart(companyIndex);
drawCompanyNews(companyIndex);
}
// Hide detail section
function hideCompanyDetail() {
companyDetailSection.visible = false;
currentDetailCompany = -1;
clearCompanyDetailSection();
}
// Company name click handled by button background in StockRow
// Optionally, tap outside detail section to hide it (not required, but nice UX)
companyDetailSection.down = function (x, y, obj) {
hideCompanyDetail();
};
// Update total profit/loss
function updateTotalProfit() {
var totalProfit = 0;
for (var i = 0; i < 10; i++) {
var lots = playerLots[i];
var curPrice = stockRows[i].price;
for (var j = 0; j < lots.length; j++) {
totalProfit += Math.round((curPrice - lots[j].price) * lots[j].shares);
}
}
var profitColor = "#ffffff";
if (totalProfit > 0) profitColor = "#2ecc71";
if (totalProfit < 0) profitColor = "#e74c3c";
totalProfitTxt.setText((totalProfit >= 0 ? '+' : '') + totalProfit);
totalProfitTxt.setStyle({
fill: profitColor
});
}
// --- UI Update Functions ---
function updateCashUI() {
cashTxt.setText('Cash: ₲' + playerCash);
updateWealthUI();
updateTotalProfit();
}
function updateWealthUI() {
var wealth = playerCash;
for (var i = 0; i < 10; i++) {
wealth += playerHoldings[i] * stockPrices[i];
}
wealthTxt.setText('Wealth: ₲' + wealth);
}
function updateTimerUI() {
var min = Math.floor(timeLeft / 60);
var sec = timeLeft % 60;
var t = (min < 10 ? '0' : '') + min + ':' + (sec < 10 ? '0' : '') + sec;
timerTxt.setText(t);
}
// --- Stock Price Update Logic ---
// Simulate price changes every 10 seconds
function updateStockPrices() {
for (var i = 0; i < 10; i++) {
stockLastPrices[i] = stockPrices[i];
// Random walk: -8% to +8%
var pct = Math.random() * 0.16 - 0.08;
// Occasionally, a "news event" (1 in 8 chance): -25% to +25%
if (Math.random() < 0.125) {
pct = Math.random() * 0.5 - 0.25;
}
var newPrice = Math.max(10, Math.round(stockPrices[i] * (1 + pct)));
stockPrices[i] = newPrice;
// Update price history for chart
if (typeof companyPriceHistory !== "undefined" && companyPriceHistory[i]) {
companyPriceHistory[i].push(newPrice);
if (companyPriceHistory[i].length > 20) companyPriceHistory[i].shift();
// If this company is currently shown in detail, redraw chart and news
if (typeof currentDetailCompany !== "undefined" && currentDetailCompany === i && companyDetailSection.visible) {
drawCompanyChart(i);
drawCompanyNews(i);
}
}
// Update row
stockRows[i].setPrice(newPrice);
playerHoldings[i] = stockRows[i].holdings;
}
updateWealthUI();
updateTotalProfit();
}
// --- Timer Logic ---
function tickTimer() {
timeLeft -= 1;
if (timeLeft < 0) timeLeft = 0;
updateTimerUI();
// Only end game if timeLeft < 0 (so timer shows 00:00 for a full second)
if (timeLeft < 0) {
endGame();
}
}
// --- End Game ---
function endGame() {
// Calculate final wealth
var finalWealth = playerCash;
for (var i = 0; i < 10; i++) {
finalWealth += playerHoldings[i] * stockPrices[i];
}
// Show game over (handled by LK)
LK.showGameOver();
}
// --- Game Update Loop ---
game.update = function () {
// No per-frame logic needed for this MVP
};
// --- Start Game Logic ---
function startGame() {
// Reset state
playerCash = 10000;
for (var i = 0; i < 10; i++) {
stockPrices[i] = initialPrices[i];
stockLastPrices[i] = initialPrices[i];
playerHoldings[i] = 0;
playerLots[i] = [];
stockRows[i].setPrice(stockPrices[i]);
stockRows[i].setHoldings(0);
}
timeLeft = gameDurationSec;
updateCashUI();
updateTimerUI();
updateWealthUI();
hideCompanyDetail();
// Start price update interval (every 10s)
if (timerInterval) LK.clearInterval(timerInterval);
timerInterval = LK.setInterval(function () {
updateStockPrices();
}, 10000);
// Start timer (every 1s)
if (game._timerTick) LK.clearInterval(game._timerTick);
game._timerTick = LK.setInterval(function () {
tickTimer();
}, 1000);
}
// Play background music (looping, fade in for smoothness)
LK.playMusic('bg_stockfloor', {
loop: true,
fade: {
start: 0,
end: 0.7,
duration: 1200
}
});
// --- Start the game ---
startGame();
;