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