/**** * 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();
;