/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Light class: represents a single light (red or green)
var StartLight = Container.expand(function () {
var self = Container.call(this);
// By default, start as "off" (invisible)
self.state = 'off'; // 'off', 'red', 'green'
self.lightAsset = null;
// Set state: 'off', 'red', 'green'
self.setState = function (state) {
if (self.lightAsset) {
self.removeChild(self.lightAsset);
self.lightAsset = null;
}
self.state = state;
if (state === 'red') {
self.lightAsset = self.attachAsset('redLight', {
anchorX: 0.5,
anchorY: 0.5
});
self.lightAsset.alpha = 1;
} else if (state === 'green') {
self.lightAsset = self.attachAsset('greenLight', {
anchorX: 0.5,
anchorY: 0.5
});
self.lightAsset.alpha = 1;
}
// else: off, do not show anything
};
// Flash effect for green
self.flashGreen = function () {
if (self.state === 'green' && self.lightAsset) {
self.lightAsset.alpha = 0.5;
tween(self.lightAsset, {
alpha: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
// No title, no description
// Always backgroundColor is black
backgroundColor: 0x000000
});
/****
* Game Code
****/
// --- Layout constants ---
// 5 red lights (circles), 1 green light (circle), warning text, result text
// We'll use shapes for the lights
// Red light asset
// Green light asset
// We'll use tween for light animations
// shape asset is only used as a background in background containers, not as a top shape
var NUM_LIGHTS = 5;
var LIGHT_SPACING = 260; // px between lights
var LIGHT_Y = 900; // vertical position of lights
// Center the row of lights, accounting for the width of a light
var LIGHT_WIDTH = 180; // matches asset width
var LIGHT_TABLE_WIDTH = (NUM_LIGHTS - 1) * LIGHT_SPACING + LIGHT_WIDTH;
var LIGHT_START_X = (2048 - LIGHT_TABLE_WIDTH) / 2 + LIGHT_WIDTH / 2;
// --- State variables ---
var lights = [];
var currentPhase = 'idle'; // 'idle', 'lights', 'waitGreen', 'green', 'result', 'falseStart'
var lightIndex = 0;
var timers = [];
var greenTimestamp = 0;
var tapTimestamp = 0;
var resultText = null;
var warningText = null;
var instructionText = null;
// --- GUI elements ---
var guiResultText = null;
// --- Best time tracking ---
var bestTime = null;
var BEST_TIME_KEY = "f1_best_time";
// Load best time from storage if available
var storedBest = storage[BEST_TIME_KEY];
if (storedBest !== null && storedBest !== undefined) {
bestTime = storedBest;
}
// --- Best time GUI text ---
var bestTimeText = new Text2("", {
size: 80,
fill: 0xFFD700
});
bestTimeText.anchor.set(0.5, 0);
LK.gui.top.addChild(bestTimeText);
bestTimeText.x = 2048 / 2;
bestTimeText.y = 80;
function updateBestTimeText() {
if (bestTime !== null) {
bestTimeText.setText("Best: " + formatReaction(bestTime) + "s");
bestTimeText.visible = true;
} else {
bestTimeText.setText("Best: --");
bestTimeText.visible = true;
}
}
updateBestTimeText();
// --- Background containers ---
// Pre-game and post-game backgrounds
var backgroundPre = LK.getAsset('background', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 1366 // center vertically, 2732/2
});
var backgroundPost = LK.getAsset('background', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 1366
});
backgroundPost.visible = false;
game.addChild(backgroundPre);
game.addChild(backgroundPost);
// --- Helper functions ---
function clearTimers() {
for (var i = 0; i < timers.length; ++i) {
LK.clearTimeout(timers[i]);
}
timers = [];
}
function resetLights() {
for (var i = 0; i < lights.length; ++i) {
lights[i].setState('off');
}
}
function showInstruction(msg) {
if (!instructionText) {
instructionText = new Text2(msg, {
size: 90,
fill: 0xFFFFFF
});
instructionText.anchor.set(0.5, 0);
LK.gui.top.addChild(instructionText);
instructionText.y = 180;
}
instructionText.setText(msg);
instructionText.visible = true;
}
function hideInstruction() {
if (instructionText) instructionText.visible = false;
}
function showWarning(msg) {
if (!warningText) {
warningText = new Text2(msg, {
size: 120,
fill: 0xFF4444
});
warningText.anchor.set(0.5, 0.5);
LK.gui.center.addChild(warningText);
}
warningText.setText(msg);
warningText.visible = true;
}
function hideWarning() {
if (warningText) warningText.visible = false;
}
function showResult(msg) {
if (!resultText) {
resultText = new Text2(msg, {
size: 120,
fill: 0x22FF22
});
resultText.anchor.set(0.5, 0.5);
LK.gui.center.addChild(resultText);
}
resultText.setText(msg);
resultText.visible = true;
}
function hideResult() {
if (resultText) resultText.visible = false;
}
// --- Game logic ---
function startSequence() {
clearTimers();
resetLights();
hideWarning();
hideResult();
backgroundPre.visible = true;
backgroundPost.visible = false;
showInstruction("Wait for green, then tap as fast as you can!");
// Play F1 lights sound slightly earlier before the first red light
LK.setTimeout(function () {
LK.getSound('f1lights').play();
}, 500);
currentPhase = 'lights';
lightIndex = 0;
// Start lighting up the reds, one by one
timers.push(LK.setTimeout(lightNext, 700));
}
function lightNext() {
if (lightIndex < NUM_LIGHTS) {
lights[lightIndex].setState('red');
// Play F1 lights sound for each red light as it turns on (5 times, one per light)
LK.getSound('f1lights').play();
lightIndex += 1;
timers.push(LK.setTimeout(lightNext, 700));
} else {
// All reds are on, now wait random time before green
currentPhase = 'waitGreen';
// Random delay: 0.7s to 2.2s
var delay = 700 + Math.floor(Math.random() * 1500);
timers.push(LK.setTimeout(showGreen, delay));
}
}
function showGreen() {
// Play green light sound at the same time as lights turn green
LK.getSound('f1greenlightsound').play();
// All lights turn green
for (var i = 0; i < lights.length; ++i) {
lights[i].setState('green');
lights[i].flashGreen();
}
currentPhase = 'green';
greenTimestamp = Date.now();
showInstruction("GO!");
}
function handleTap() {
if (currentPhase === 'idle' || currentPhase === 'result' || currentPhase === 'falseStart') {
// Start new round
startSequence();
return;
}
if (currentPhase === 'lights' || currentPhase === 'waitGreen') {
// False start!
clearTimers();
// Play false start sound
LK.getSound('f1falsestart').play();
currentPhase = 'falseStart';
hideInstruction();
showWarning("False Start!\nTap to try again");
resetLights();
backgroundPre.visible = false;
backgroundPost.visible = true;
return;
}
if (currentPhase === 'green') {
tapTimestamp = Date.now();
var reaction = tapTimestamp - greenTimestamp;
currentPhase = 'result';
hideInstruction();
// Check and update best time
var isBest = false;
if (bestTime === null || reaction < bestTime) {
bestTime = reaction;
storage[BEST_TIME_KEY] = bestTime;
isBest = true;
updateBestTimeText();
// Play new best score sound
LK.getSound('f1newbest').play();
}
var resultMsg = "Reaction Time:\n" + formatReaction(reaction);
if (bestTime !== null) {
resultMsg += "\nBest: " + formatReaction(bestTime);
}
if (isBest) {
resultMsg += "\n(New Record!)";
}
resultMsg += "\n\nTap to try again";
showResult(resultMsg);
resetLights();
backgroundPre.visible = false;
backgroundPost.visible = true;
return;
}
}
function formatReaction(ms) {
// Always show as seconds with 3 decimals, e.g. 0.287
var seconds = ms / 1000;
var str = seconds.toFixed(3);
return str;
}
// --- Add background behind the lights ---
// (Handled by backgroundPre and backgroundPost above)
// --- Setup lights ---
for (var i = 0; i < NUM_LIGHTS; ++i) {
var light = new StartLight();
light.x = LIGHT_START_X + i * LIGHT_SPACING;
light.y = LIGHT_Y;
light.setState('off');
game.addChild(light);
lights.push(light);
}
// --- Setup instruction text ---
showInstruction("Tap to start!");
// --- Input handling ---
game.down = function (x, y, obj) {
handleTap();
};
// --- Clean up on game over (not needed, but for completeness) ---
game.destroy = function () {
clearTimers();
};
// --- Start in idle state ---
currentPhase = 'idle';
// --- No update loop needed ---; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Light class: represents a single light (red or green)
var StartLight = Container.expand(function () {
var self = Container.call(this);
// By default, start as "off" (invisible)
self.state = 'off'; // 'off', 'red', 'green'
self.lightAsset = null;
// Set state: 'off', 'red', 'green'
self.setState = function (state) {
if (self.lightAsset) {
self.removeChild(self.lightAsset);
self.lightAsset = null;
}
self.state = state;
if (state === 'red') {
self.lightAsset = self.attachAsset('redLight', {
anchorX: 0.5,
anchorY: 0.5
});
self.lightAsset.alpha = 1;
} else if (state === 'green') {
self.lightAsset = self.attachAsset('greenLight', {
anchorX: 0.5,
anchorY: 0.5
});
self.lightAsset.alpha = 1;
}
// else: off, do not show anything
};
// Flash effect for green
self.flashGreen = function () {
if (self.state === 'green' && self.lightAsset) {
self.lightAsset.alpha = 0.5;
tween(self.lightAsset, {
alpha: 1
}, {
duration: 200,
easing: tween.easeOut
});
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
// No title, no description
// Always backgroundColor is black
backgroundColor: 0x000000
});
/****
* Game Code
****/
// --- Layout constants ---
// 5 red lights (circles), 1 green light (circle), warning text, result text
// We'll use shapes for the lights
// Red light asset
// Green light asset
// We'll use tween for light animations
// shape asset is only used as a background in background containers, not as a top shape
var NUM_LIGHTS = 5;
var LIGHT_SPACING = 260; // px between lights
var LIGHT_Y = 900; // vertical position of lights
// Center the row of lights, accounting for the width of a light
var LIGHT_WIDTH = 180; // matches asset width
var LIGHT_TABLE_WIDTH = (NUM_LIGHTS - 1) * LIGHT_SPACING + LIGHT_WIDTH;
var LIGHT_START_X = (2048 - LIGHT_TABLE_WIDTH) / 2 + LIGHT_WIDTH / 2;
// --- State variables ---
var lights = [];
var currentPhase = 'idle'; // 'idle', 'lights', 'waitGreen', 'green', 'result', 'falseStart'
var lightIndex = 0;
var timers = [];
var greenTimestamp = 0;
var tapTimestamp = 0;
var resultText = null;
var warningText = null;
var instructionText = null;
// --- GUI elements ---
var guiResultText = null;
// --- Best time tracking ---
var bestTime = null;
var BEST_TIME_KEY = "f1_best_time";
// Load best time from storage if available
var storedBest = storage[BEST_TIME_KEY];
if (storedBest !== null && storedBest !== undefined) {
bestTime = storedBest;
}
// --- Best time GUI text ---
var bestTimeText = new Text2("", {
size: 80,
fill: 0xFFD700
});
bestTimeText.anchor.set(0.5, 0);
LK.gui.top.addChild(bestTimeText);
bestTimeText.x = 2048 / 2;
bestTimeText.y = 80;
function updateBestTimeText() {
if (bestTime !== null) {
bestTimeText.setText("Best: " + formatReaction(bestTime) + "s");
bestTimeText.visible = true;
} else {
bestTimeText.setText("Best: --");
bestTimeText.visible = true;
}
}
updateBestTimeText();
// --- Background containers ---
// Pre-game and post-game backgrounds
var backgroundPre = LK.getAsset('background', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 1366 // center vertically, 2732/2
});
var backgroundPost = LK.getAsset('background', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 1366
});
backgroundPost.visible = false;
game.addChild(backgroundPre);
game.addChild(backgroundPost);
// --- Helper functions ---
function clearTimers() {
for (var i = 0; i < timers.length; ++i) {
LK.clearTimeout(timers[i]);
}
timers = [];
}
function resetLights() {
for (var i = 0; i < lights.length; ++i) {
lights[i].setState('off');
}
}
function showInstruction(msg) {
if (!instructionText) {
instructionText = new Text2(msg, {
size: 90,
fill: 0xFFFFFF
});
instructionText.anchor.set(0.5, 0);
LK.gui.top.addChild(instructionText);
instructionText.y = 180;
}
instructionText.setText(msg);
instructionText.visible = true;
}
function hideInstruction() {
if (instructionText) instructionText.visible = false;
}
function showWarning(msg) {
if (!warningText) {
warningText = new Text2(msg, {
size: 120,
fill: 0xFF4444
});
warningText.anchor.set(0.5, 0.5);
LK.gui.center.addChild(warningText);
}
warningText.setText(msg);
warningText.visible = true;
}
function hideWarning() {
if (warningText) warningText.visible = false;
}
function showResult(msg) {
if (!resultText) {
resultText = new Text2(msg, {
size: 120,
fill: 0x22FF22
});
resultText.anchor.set(0.5, 0.5);
LK.gui.center.addChild(resultText);
}
resultText.setText(msg);
resultText.visible = true;
}
function hideResult() {
if (resultText) resultText.visible = false;
}
// --- Game logic ---
function startSequence() {
clearTimers();
resetLights();
hideWarning();
hideResult();
backgroundPre.visible = true;
backgroundPost.visible = false;
showInstruction("Wait for green, then tap as fast as you can!");
// Play F1 lights sound slightly earlier before the first red light
LK.setTimeout(function () {
LK.getSound('f1lights').play();
}, 500);
currentPhase = 'lights';
lightIndex = 0;
// Start lighting up the reds, one by one
timers.push(LK.setTimeout(lightNext, 700));
}
function lightNext() {
if (lightIndex < NUM_LIGHTS) {
lights[lightIndex].setState('red');
// Play F1 lights sound for each red light as it turns on (5 times, one per light)
LK.getSound('f1lights').play();
lightIndex += 1;
timers.push(LK.setTimeout(lightNext, 700));
} else {
// All reds are on, now wait random time before green
currentPhase = 'waitGreen';
// Random delay: 0.7s to 2.2s
var delay = 700 + Math.floor(Math.random() * 1500);
timers.push(LK.setTimeout(showGreen, delay));
}
}
function showGreen() {
// Play green light sound at the same time as lights turn green
LK.getSound('f1greenlightsound').play();
// All lights turn green
for (var i = 0; i < lights.length; ++i) {
lights[i].setState('green');
lights[i].flashGreen();
}
currentPhase = 'green';
greenTimestamp = Date.now();
showInstruction("GO!");
}
function handleTap() {
if (currentPhase === 'idle' || currentPhase === 'result' || currentPhase === 'falseStart') {
// Start new round
startSequence();
return;
}
if (currentPhase === 'lights' || currentPhase === 'waitGreen') {
// False start!
clearTimers();
// Play false start sound
LK.getSound('f1falsestart').play();
currentPhase = 'falseStart';
hideInstruction();
showWarning("False Start!\nTap to try again");
resetLights();
backgroundPre.visible = false;
backgroundPost.visible = true;
return;
}
if (currentPhase === 'green') {
tapTimestamp = Date.now();
var reaction = tapTimestamp - greenTimestamp;
currentPhase = 'result';
hideInstruction();
// Check and update best time
var isBest = false;
if (bestTime === null || reaction < bestTime) {
bestTime = reaction;
storage[BEST_TIME_KEY] = bestTime;
isBest = true;
updateBestTimeText();
// Play new best score sound
LK.getSound('f1newbest').play();
}
var resultMsg = "Reaction Time:\n" + formatReaction(reaction);
if (bestTime !== null) {
resultMsg += "\nBest: " + formatReaction(bestTime);
}
if (isBest) {
resultMsg += "\n(New Record!)";
}
resultMsg += "\n\nTap to try again";
showResult(resultMsg);
resetLights();
backgroundPre.visible = false;
backgroundPost.visible = true;
return;
}
}
function formatReaction(ms) {
// Always show as seconds with 3 decimals, e.g. 0.287
var seconds = ms / 1000;
var str = seconds.toFixed(3);
return str;
}
// --- Add background behind the lights ---
// (Handled by backgroundPre and backgroundPost above)
// --- Setup lights ---
for (var i = 0; i < NUM_LIGHTS; ++i) {
var light = new StartLight();
light.x = LIGHT_START_X + i * LIGHT_SPACING;
light.y = LIGHT_Y;
light.setState('off');
game.addChild(light);
lights.push(light);
}
// --- Setup instruction text ---
showInstruction("Tap to start!");
// --- Input handling ---
game.down = function (x, y, obj) {
handleTap();
};
// --- Clean up on game over (not needed, but for completeness) ---
game.destroy = function () {
clearTimers();
};
// --- Start in idle state ---
currentPhase = 'idle';
// --- No update loop needed ---;