/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Invader class for enemy invaders
var Invader = Container.expand(function () {
var self = Container.call(this);
// Use the 'character' asset for invader for now, but tint it green
var invaderSprite = self.attachAsset('character', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x00ff00,
scaleX: 0.7,
scaleY: 0.7
});
self.speedX = 4; // Horizontal speed
self.speedY = 0; // Vertical speed (drops down on edge)
self.lastX = self.x;
self.lastY = self.y;
self.update = function () {
// Move horizontally
self.x += self.speedX;
// Bounce off left edge
if (self.lastX >= 60 && self.x < 60) {
self.x = 60; // Clamp to edge
self.speedX = -self.speedX * 1.12; // Increase speed and reverse
// Cap speedX to a maximum of 12 (preserve sign)
if (self.speedX > 12) self.speedX = 12;
if (self.speedX < -12) self.speedX = -12;
self.y += 40; // Move down on bounce
}
// Bounce off right edge
if (self.lastX <= 2048 - 60 && self.x > 2048 - 60) {
self.x = 2048 - 60; // Clamp to edge
self.speedX = -self.speedX * 1.12; // Increase speed and reverse
// Cap speedX to a maximum of 12 (preserve sign)
if (self.speedX > 12) self.speedX = 12;
if (self.speedX < -12) self.speedX = -12;
self.y += 40; // Move down on bounce
}
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Game state variables
var p1Score = 0;
var p2Score = 0;
// High score tracking (persistent for session)
var highScore = 0;
// Score display Text2 objects
var p1ScoreDisplay, p2ScoreDisplay, highScoreDisplay;
// Helper to show score displays at top
function showScoreDisplays() {
// Remove previous if any
if (p1ScoreDisplay) p1ScoreDisplay.alpha = 0;
if (p2ScoreDisplay) p2ScoreDisplay.alpha = 0;
if (highScoreDisplay) highScoreDisplay.alpha = 0;
// High Score
highScoreDisplay = new Text2('HIGH: ' + highScore, {
size: 60,
fill: 0xFFD700 // Gold
});
highScoreDisplay.anchor.set(0.5, 0);
highScoreDisplay.x = 2048 / 2;
highScoreDisplay.y = 20;
LK.gui.top.addChild(highScoreDisplay);
// P1 Score (left)
p1ScoreDisplay = new Text2('P1: ' + p1Score, {
size: 50,
fill: 0xFF4444
});
p1ScoreDisplay.anchor.set(0, 0);
p1ScoreDisplay.x = 120;
p1ScoreDisplay.y = 20;
LK.gui.top.addChild(p1ScoreDisplay);
// P2 Score (right)
p2ScoreDisplay = new Text2('P2: ' + p2Score, {
size: 50,
fill: 0x4444FF
});
p2ScoreDisplay.anchor.set(1, 0);
p2ScoreDisplay.x = 2048 - 120;
p2ScoreDisplay.y = 20;
LK.gui.top.addChild(p2ScoreDisplay);
}
// Helper to update score displays
function updateScoreDisplays() {
if (p1ScoreDisplay) p1ScoreDisplay.setText('P1: ' + p1Score);
if (p2ScoreDisplay) p2ScoreDisplay.setText('P2: ' + p2Score);
// High score is always the highest of P1 and P2
highScore = Math.max(p1Score, p2Score, highScore);
if (highScoreDisplay) highScoreDisplay.setText('HIGH: ' + highScore);
}
// Red ship for P1
// Blue ship for P2
// Yellow target
var coins = 0;
var maxCoins = 2; // Require 2 credits for 2 players
var gameStarted = false;
var insertCoinText;
var creditsText;
var pressStartText;
var holdStartTime = 0; // Only used for hold-to-skip in ready screen
var isHolding = false; // Only used for hold-to-skip in ready screen
var ship1;
var ship2;
var countdownText;
var countdownTimer;
var gameplayShips = [];
var target;
var target2; // Second target for P2
var cannons = [];
var cannonLines = [];
// Track dragging for both targets independently
var draggingTarget1 = false;
var draggingTarget2 = false;
var isShooting = false;
var isInReadyScreen = false;
// Create main title text "Invader Blits"
var titleText = new Text2('Invader Blits', {
size: 120,
fill: 0xFFFFFF
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 2732 / 2 - 50;
game.addChild(titleText);
// Create the "2" text (initially hidden)
var numberText = new Text2('2', {
size: 120,
fill: 0xFFFFFF
});
numberText.anchor.set(0.5, 0.5);
numberText.x = 2048 / 2 + titleText.width / 2 + 20; // Position it after "Invader Blits"
numberText.y = 2732 / 2 - 50;
numberText.alpha = 0; // Start invisible
game.addChild(numberText);
// Position the "2" below the screen initially for slide-up animation
numberText.y = 2732 + 100; // Start below screen
numberText.alpha = 1; // Make it visible but positioned off-screen
// Show the "2" after 3 seconds with a slide-up animation
LK.setTimeout(function () {
tween(numberText, {
y: 2732 / 2 - 50 // Slide up to final position next to title
}, {
duration: 800,
easing: tween.easeOut
});
// After the "2" animation completes, show the INSERT COIN! text
LK.setTimeout(function () {
// Create "INSERT COIN!" text
insertCoinText = new Text2('INSERT COIN!', {
size: 60,
fill: 0xFFFF00 // Yellow color
});
insertCoinText.anchor.set(0.5, 0.5);
insertCoinText.x = 2048 / 2;
insertCoinText.y = 300; // Top of screen
game.addChild(insertCoinText);
// Create "0/1 CREDITS" text
creditsText = new Text2('0/1 CREDITS', {
size: 40,
fill: 0xFFFFFF // White color
});
creditsText.anchor.set(0.5, 0.5);
creditsText.x = 2048 / 2;
creditsText.y = 380; // Below INSERT COIN!
game.addChild(creditsText);
}, 800); // Wait for the "2" animation to complete
}, 3000);
// Function to update credits display
function updateCreditsDisplay() {
if (creditsText) {
creditsText.setText(coins + '/' + maxCoins + ' CREDITS');
}
// (Removed 'PRESS START' text creation for gameplay)
// Handle target dragging
game.move = function (x, y, obj) {
// Allow both targets to be dragged at the same time
// Use draggingTarget1 and draggingTarget2 to track each target's drag state
// Move target1 if dragging
if (draggingTarget1 && target) {
target.x = x;
target.y = y;
// Only update lines for P1 (first two lines)
for (var i = 0; i < 2 && i < cannons.length; i++) {
var cannon = cannons[i];
var line = cannonLines[i];
var dx = target.x - cannon.x;
var dy = target.y - cannon.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
line.x = cannon.x;
line.y = cannon.y;
line.rotation = angle;
line.scaleX = distance / 100;
}
}
// Move target2 if dragging
if (draggingTarget2 && typeof target2 !== "undefined" && target2) {
target2.x = x;
target2.y = y;
// Only update lines for P2 (last two lines)
for (var i = 2; i < cannons.length; i++) {
var cannon = cannons[i];
var line = cannonLines[i];
var dx = target2.x - cannon.x;
var dy = target2.y - cannon.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
line.x = cannon.x;
line.y = cannon.y;
line.rotation = angle;
line.scaleX = distance / 100;
}
}
};
// Update down handler to include target dragging
var originalDown = game.down;
game.down = function (x, y, obj) {
// Call original down handler first
originalDown.call(this, x, y, obj);
// Allow both targets to be picked up for dragging at the same time
if (gameStarted) {
// Check if clicking on target1
if (target) {
var dx = x - target.x;
var dy = y - target.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 60) {
draggingTarget1 = true;
// Show lines for P1 cannons only
for (var i = 0; i < 2 && i < cannonLines.length; i++) {
cannonLines[i].alpha = 0.8;
}
}
}
// Check if clicking on target2
if (typeof target2 !== "undefined" && target2) {
var dx2 = x - target2.x;
var dy2 = y - target2.y;
var distance2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
if (distance2 < 60) {
draggingTarget2 = true;
// Show lines for P2 cannons only
for (var i = 2; i < cannonLines.length; i++) {
cannonLines[i].alpha = 0.8;
}
}
}
}
};
// Update up handler to stop target dragging
var originalUp = game.up;
game.up = function (x, y, obj) {
// Call original up handler first
originalUp.call(this, x, y, obj);
// Stop dragging for both targets independently
if (draggingTarget1) {
// Hide lines for P1 cannons only
for (var i = 0; i < 2 && i < cannonLines.length; i++) {
cannonLines[i].alpha = 0;
}
draggingTarget1 = false;
}
if (draggingTarget2) {
// Hide lines for P2 cannons only
for (var i = 2; i < cannonLines.length; i++) {
cannonLines[i].alpha = 0;
}
draggingTarget2 = false;
}
};
// Function to update cannon lines to point at target
function updateCannonLines() {
if (!target && !target2 || !isShooting) return;
for (var i = 0; i < cannons.length; i++) {
var cannon = cannons[i];
var line = cannonLines[i];
var dx, dy;
// First two cannons/lines (P1) point to target, last two (P2) to target2
if (i < 2) {
dx = target.x - cannon.x;
dy = target.y - cannon.y;
} else {
dx = target2.x - cannon.x;
dy = target2.y - cannon.y;
}
var distance = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
line.x = cannon.x;
line.y = cannon.y;
line.rotation = angle;
line.scaleX = distance / 100; // Adjust line length
}
}
// Add shooting functionality
game.update = function () {
// Lines are now controlled by dragging, not automatic shooting
};
}
// Touch down event for inserting coins and starting game
game.down = function (x, y, obj) {
if (gameStarted) {
// (Handled by new drag logic in the other game.down handler)
return;
}
// If in ready screen (countdown), allow tap to insert coin
if (isInReadyScreen) {
// Allow inserting coin during ready screen
if (coins < maxCoins) {
coins++;
updateCreditsDisplay();
// Hide INSERT COIN text when max coins reached
if (coins >= maxCoins && insertCoinText) {
insertCoinText.alpha = 0.3; // Dim it
}
}
// Do NOT skip countdown on tap
return;
}
// Insert coin with tap
if (coins < maxCoins) {
coins++;
updateCreditsDisplay();
// Hide INSERT COIN text when max coins reached
if (coins >= maxCoins && insertCoinText) {
insertCoinText.alpha = 0.3; // Dim it
}
// Automatically go to ready screen when credit is added
if (!isInReadyScreen) {
startReadyScreen();
}
return;
}
// Start ready screen immediately when pressing start (no hold)
// Only allow starting when 2 credits are inserted
if (coins === maxCoins && !isInReadyScreen) {
startReadyScreen();
}
};
// Touch up event to stop holding
game.up = function (x, y, obj) {
if (gameStarted) {
// Only allow hold-to-skip during ready screen countdown
if (isInReadyScreen && countdownTimer && Date.now() - holdStartTime >= 1000) {
isHolding = false;
// Skip countdown
LK.clearInterval(countdownTimer);
countdownText.setText('GO!');
LK.setTimeout(function () {
if (countdownText) countdownText.alpha = 0;
isInReadyScreen = false;
// Show player ships after countdown
showPlayerShips();
// Start gameplay sequence
startGameplaySequence();
}, 500);
}
return;
}
isHolding = false;
// No hold-to-start for game; do nothing here except for hold-to-skip in ready screen
};
// Function to start ready screen
function startReadyScreen() {
gameStarted = true;
isInReadyScreen = true;
// Consume one credit when starting
coins--;
updateCreditsDisplay();
// Hide all title screen elements except creditsText (keep creditsText visible in ready screen)
if (titleText) titleText.alpha = 0;
if (numberText) numberText.alpha = 0;
if (insertCoinText) insertCoinText.alpha = 0;
if (creditsText) creditsText.alpha = 1; // Show creditsText during ready screen
if (pressStartText) pressStartText.alpha = 0;
// Make ship2 fully visible (unfade it) only if ship2 exists
if (ship2) {
tween(ship2, {
alpha: 1
}, {
duration: 500,
easing: tween.easeOut
});
}
// Show 15 second countdown
countdownText = new Text2('15', {
size: 80,
fill: 0xFF0000 // Red color
});
countdownText.anchor.set(0.5, 0.5);
countdownText.x = 2048 / 2;
countdownText.y = 400;
game.addChild(countdownText);
// Start countdown timer
var timeLeft = 15;
countdownTimer = LK.setInterval(function () {
timeLeft--;
if (timeLeft <= 0) {
LK.clearInterval(countdownTimer);
countdownText.setText('GO!');
LK.setTimeout(function () {
if (countdownText) countdownText.alpha = 0;
isInReadyScreen = false;
// Show player ships after countdown
showPlayerShips();
// Start gameplay sequence
startGameplaySequence();
}, 1000);
} else {
countdownText.setText(timeLeft.toString());
}
}, 1000);
// Game would start here - for now just clear screen
game.setBackgroundColor(0x000020); // Dark blue to show game started
}
// Function to show player ships on ready screen
function showPlayerShips() {
// Hide the hold-to-start ships
if (ship1) ship1.alpha = 0;
if (ship2) ship2.alpha = 0;
// Always show 2-player setup (force 2 players)
var shipPositions = [{
x: 100,
y: 2732 - 100
}, {
x: 100,
y: 2732 - 200
}, {
x: 2048 - 100,
y: 2732 - 100
}, {
x: 2048 - 100,
y: 2732 - 200
}];
// Create ships
for (var i = 0; i < shipPositions.length; i++) {
var ship = LK.getAsset('character', {
anchorX: 0.5,
anchorY: 0.5,
x: shipPositions[i].x,
y: shipPositions[i].y,
scaleX: 0.6,
scaleY: 0.6
});
game.addChild(ship);
}
}
// Function to start the gameplay sequence
function startGameplaySequence() {
// Show "Shoot Enemy Ships" text
var shootText = new Text2('Shoot Enemy Ships', {
size: 80,
fill: 0xFFFF00 // Yellow
});
shootText.anchor.set(0.5, 0.5);
shootText.x = 2048 / 2;
shootText.y = 2732 / 2 - 200;
game.addChild(shootText);
// After 2 seconds, show "Ready?"
LK.setTimeout(function () {
shootText.alpha = 0;
var readyText = new Text2('Ready?', {
size: 100,
fill: 0x00FF00 // Green
});
readyText.anchor.set(0.5, 0.5);
readyText.x = 2048 / 2;
readyText.y = 2732 / 2;
game.addChild(readyText);
// After 1 more second, show "Go!" and start game
LK.setTimeout(function () {
readyText.alpha = 0;
var goText = new Text2('Go!', {
size: 120,
fill: 0xFF0000 // Red
});
goText.anchor.set(0.5, 0.5);
goText.x = 2048 / 2;
goText.y = 2732 / 2;
game.addChild(goText);
// Show "Protect Cannons" with arrows at bottom
var protectText = new Text2('Protect Cannons', {
size: 60,
fill: 0xFFFFFF // White
});
protectText.anchor.set(0.5, 0.5);
protectText.x = 2048 / 2;
protectText.y = 2732 - 100;
game.addChild(protectText);
// Create left arrow
var leftArrow = new Text2('←', {
size: 80,
fill: 0xFFFFFF // White
});
leftArrow.anchor.set(0.5, 0.5);
leftArrow.x = 2048 / 2 - 300;
leftArrow.y = 2732 - 100;
game.addChild(leftArrow);
// Create right arrow
var rightArrow = new Text2('→', {
size: 80,
fill: 0xFFFFFF // White
});
rightArrow.anchor.set(0.5, 0.5);
rightArrow.x = 2048 / 2 + 300;
rightArrow.y = 2732 - 100;
game.addChild(rightArrow);
// Fade out "Go!" after a moment
LK.setTimeout(function () {
goText.alpha = 0;
// Gameplay starts here
// Always create 2-player ships (force 2 players)
// P1 ships (red) - left corners
var p1Ship1 = LK.getAsset('p1Ship', {
anchorX: 0.5,
anchorY: 0.5,
x: 100,
y: 100,
scaleX: 1,
scaleY: 1
});
game.addChild(p1Ship1);
gameplayShips.push(p1Ship1);
var p1Ship2 = LK.getAsset('p1Ship', {
anchorX: 0.5,
anchorY: 0.5,
x: 100,
y: 2732 - 100,
scaleX: 1,
scaleY: 1
});
game.addChild(p1Ship2);
gameplayShips.push(p1Ship2);
// P2 ships (blue) - right corners
var p2Ship1 = LK.getAsset('p2Ship', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 100,
y: 100,
scaleX: 1,
scaleY: 1
});
game.addChild(p2Ship1);
gameplayShips.push(p2Ship1);
var p2Ship2 = LK.getAsset('p2Ship', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 100,
y: 2732 - 100,
scaleX: 1,
scaleY: 1
});
game.addChild(p2Ship2);
gameplayShips.push(p2Ship2);
// Create target in center
target = LK.getAsset('target', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
scaleX: 1,
scaleY: 1
});
game.addChild(target);
// Create a second target for P2, positioned near the right side
target2 = LK.getAsset('target', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 300,
y: 2732 / 2,
scaleX: 1,
scaleY: 1
});
game.addChild(target2);
// Always use 2-player: 4 cannons
var cannonPositions = [{
x: 100,
y: 2732 - 100
}, {
x: 200,
y: 2732 - 100
}, {
x: 2048 - 200,
y: 2732 - 100
}, {
x: 2048 - 100,
y: 2732 - 100
}];
for (var i = 0; i < cannonPositions.length; i++) {
var cannon = LK.getAsset('character', {
anchorX: 0.5,
anchorY: 0.5,
x: cannonPositions[i].x,
y: cannonPositions[i].y,
scaleX: 0.5,
scaleY: 0.5,
tint: 0x808080 // Gray color for cannons
});
game.addChild(cannon);
cannons.push(cannon);
}
// Create lines from cannons to target (initially hidden)
for (var i = 0; i < cannons.length; i++) {
var line = LK.getAsset('character', {
anchorX: 0,
anchorY: 0.5,
x: cannons[i].x,
y: cannons[i].y,
scaleX: 1,
scaleY: 0.02,
// Very thin to simulate a line
tint: 0xFF0000,
// Red laser color
alpha: 0 // Initially hidden
});
game.addChild(line);
cannonLines.push(line);
}
// Show score displays at the start of gameplay
showScoreDisplays();
updateScoreDisplays();
// --- INVADER WAVE SETUP ---
var invaders = [];
var invaderRows = 3;
var invaderCols = 8;
var invaderSpacingX = 180;
var invaderSpacingY = 120;
var invaderStartX = 2048 / 2 - (invaderCols - 1) * invaderSpacingX / 2;
var invaderStartY = 300;
for (var row = 0; row < invaderRows; row++) {
for (var col = 0; col < invaderCols; col++) {
var invader = new Invader();
invader.x = invaderStartX + col * invaderSpacingX;
invader.y = invaderStartY + row * invaderSpacingY;
invader.lastX = invader.x;
invader.lastY = invader.y;
game.addChild(invader);
invaders.push(invader);
}
}
// Add invader update logic to game.update
function invaderUpdateHandler() {
// Keep score displays visible and updated
if (typeof showScoreDisplays === "function" && (!p1ScoreDisplay || !p2ScoreDisplay || !highScoreDisplay)) {
showScoreDisplays();
}
updateScoreDisplays();
// Keep score displays visible and updated
if (typeof showScoreDisplays === "function" && (!p1ScoreDisplay || !p2ScoreDisplay || !highScoreDisplay)) {
showScoreDisplays();
}
updateScoreDisplays();
// Move invaders and check for collisions
for (var i = invaders.length - 1; i >= 0; i--) {
var invader = invaders[i];
invader.update();
// Check collision with target1 ONLY if P1 lines are visible (shooting)
if (target && invader.intersects(target) && cannonLines.length >= 2 && (cannonLines[0].alpha > 0 || cannonLines[1].alpha > 0)) {
invader.destroy();
invaders.splice(i, 1);
// Add 50 points to P1 score only
p1Score += 50;
if (p1Score + p2Score > highScore) highScore = p1Score + p2Score;
if (p1Score + p2Score > highScore) highScore = p1Score + p2Score;
LK.setScore(p1Score + p2Score); // Update total score for compatibility
updateScoreDisplays();
updateScoreDisplays();
continue;
}
// Check collision with target2 ONLY if P2 lines are visible (shooting)
if (typeof target2 !== "undefined" && target2 && invader.intersects(target2) && cannonLines.length >= 4 && (cannonLines[2].alpha > 0 || cannonLines[3].alpha > 0)) {
invader.destroy();
invaders.splice(i, 1);
// Add 50 points to P2 score only
p2Score += 50;
if (p1Score + p2Score > highScore) highScore = p1Score + p2Score;
if (p1Score + p2Score > highScore) highScore = p1Score + p2Score;
LK.setScore(p1Score + p2Score); // Update total score for compatibility
updateScoreDisplays();
updateScoreDisplays();
continue;
}
// Game over if invader reaches bottom
if (invader.lastY <= 2732 - 60 && invader.y > 2732 - 60) {
LK.showGameOver();
return;
}
}
// Check if all invaders from the current level are killed
if (invaders.length > 0) {
var allKilled = true;
for (var i = 0; i < invaders.length; i++) {
// Only count invaders that are visible and not destroyed
if (!invaders[i].destroyed && invaders[i].alpha !== 0) {
allKilled = false;
break;
}
}
if (allKilled) {
// Remove all old invaders before starting new level
for (var i = invaders.length - 1; i >= 0; i--) {
if (invaders[i] && typeof invaders[i].destroy === "function") {
invaders[i].destroy();
}
invaders.splice(i, 1);
}
// Add new invaders for the new level
var invaderRows = 3;
var invaderCols = 8;
var invaderSpacingX = 180;
var invaderSpacingY = 120;
var invaderStartX = 2048 / 2 - (invaderCols - 1) * invaderSpacingX / 2;
var invaderStartY = 300;
for (var row = 0; row < invaderRows; row++) {
for (var col = 0; col < invaderCols; col++) {
var invader = new Invader();
invader.x = invaderStartX + col * invaderSpacingX;
invader.y = invaderStartY + row * invaderSpacingY;
invader.lastX = invader.x;
invader.lastY = invader.y;
// Ensure invader is killable and active for this level
invader.alpha = 1;
invader.visible = true;
invader.destroyed = false;
game.addChild(invader);
invaders.push(invader);
}
}
// No stage complete popup, just continue to next level
return;
}
}
}
// Always call invaderUpdateHandler every frame
game.update = invaderUpdateHandler;
}, 1000);
}, 1000);
}, 2000);
} /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Invader class for enemy invaders
var Invader = Container.expand(function () {
var self = Container.call(this);
// Use the 'character' asset for invader for now, but tint it green
var invaderSprite = self.attachAsset('character', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x00ff00,
scaleX: 0.7,
scaleY: 0.7
});
self.speedX = 4; // Horizontal speed
self.speedY = 0; // Vertical speed (drops down on edge)
self.lastX = self.x;
self.lastY = self.y;
self.update = function () {
// Move horizontally
self.x += self.speedX;
// Bounce off left edge
if (self.lastX >= 60 && self.x < 60) {
self.x = 60; // Clamp to edge
self.speedX = -self.speedX * 1.12; // Increase speed and reverse
// Cap speedX to a maximum of 12 (preserve sign)
if (self.speedX > 12) self.speedX = 12;
if (self.speedX < -12) self.speedX = -12;
self.y += 40; // Move down on bounce
}
// Bounce off right edge
if (self.lastX <= 2048 - 60 && self.x > 2048 - 60) {
self.x = 2048 - 60; // Clamp to edge
self.speedX = -self.speedX * 1.12; // Increase speed and reverse
// Cap speedX to a maximum of 12 (preserve sign)
if (self.speedX > 12) self.speedX = 12;
if (self.speedX < -12) self.speedX = -12;
self.y += 40; // Move down on bounce
}
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x000000
});
/****
* Game Code
****/
// Game state variables
var p1Score = 0;
var p2Score = 0;
// High score tracking (persistent for session)
var highScore = 0;
// Score display Text2 objects
var p1ScoreDisplay, p2ScoreDisplay, highScoreDisplay;
// Helper to show score displays at top
function showScoreDisplays() {
// Remove previous if any
if (p1ScoreDisplay) p1ScoreDisplay.alpha = 0;
if (p2ScoreDisplay) p2ScoreDisplay.alpha = 0;
if (highScoreDisplay) highScoreDisplay.alpha = 0;
// High Score
highScoreDisplay = new Text2('HIGH: ' + highScore, {
size: 60,
fill: 0xFFD700 // Gold
});
highScoreDisplay.anchor.set(0.5, 0);
highScoreDisplay.x = 2048 / 2;
highScoreDisplay.y = 20;
LK.gui.top.addChild(highScoreDisplay);
// P1 Score (left)
p1ScoreDisplay = new Text2('P1: ' + p1Score, {
size: 50,
fill: 0xFF4444
});
p1ScoreDisplay.anchor.set(0, 0);
p1ScoreDisplay.x = 120;
p1ScoreDisplay.y = 20;
LK.gui.top.addChild(p1ScoreDisplay);
// P2 Score (right)
p2ScoreDisplay = new Text2('P2: ' + p2Score, {
size: 50,
fill: 0x4444FF
});
p2ScoreDisplay.anchor.set(1, 0);
p2ScoreDisplay.x = 2048 - 120;
p2ScoreDisplay.y = 20;
LK.gui.top.addChild(p2ScoreDisplay);
}
// Helper to update score displays
function updateScoreDisplays() {
if (p1ScoreDisplay) p1ScoreDisplay.setText('P1: ' + p1Score);
if (p2ScoreDisplay) p2ScoreDisplay.setText('P2: ' + p2Score);
// High score is always the highest of P1 and P2
highScore = Math.max(p1Score, p2Score, highScore);
if (highScoreDisplay) highScoreDisplay.setText('HIGH: ' + highScore);
}
// Red ship for P1
// Blue ship for P2
// Yellow target
var coins = 0;
var maxCoins = 2; // Require 2 credits for 2 players
var gameStarted = false;
var insertCoinText;
var creditsText;
var pressStartText;
var holdStartTime = 0; // Only used for hold-to-skip in ready screen
var isHolding = false; // Only used for hold-to-skip in ready screen
var ship1;
var ship2;
var countdownText;
var countdownTimer;
var gameplayShips = [];
var target;
var target2; // Second target for P2
var cannons = [];
var cannonLines = [];
// Track dragging for both targets independently
var draggingTarget1 = false;
var draggingTarget2 = false;
var isShooting = false;
var isInReadyScreen = false;
// Create main title text "Invader Blits"
var titleText = new Text2('Invader Blits', {
size: 120,
fill: 0xFFFFFF
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 2732 / 2 - 50;
game.addChild(titleText);
// Create the "2" text (initially hidden)
var numberText = new Text2('2', {
size: 120,
fill: 0xFFFFFF
});
numberText.anchor.set(0.5, 0.5);
numberText.x = 2048 / 2 + titleText.width / 2 + 20; // Position it after "Invader Blits"
numberText.y = 2732 / 2 - 50;
numberText.alpha = 0; // Start invisible
game.addChild(numberText);
// Position the "2" below the screen initially for slide-up animation
numberText.y = 2732 + 100; // Start below screen
numberText.alpha = 1; // Make it visible but positioned off-screen
// Show the "2" after 3 seconds with a slide-up animation
LK.setTimeout(function () {
tween(numberText, {
y: 2732 / 2 - 50 // Slide up to final position next to title
}, {
duration: 800,
easing: tween.easeOut
});
// After the "2" animation completes, show the INSERT COIN! text
LK.setTimeout(function () {
// Create "INSERT COIN!" text
insertCoinText = new Text2('INSERT COIN!', {
size: 60,
fill: 0xFFFF00 // Yellow color
});
insertCoinText.anchor.set(0.5, 0.5);
insertCoinText.x = 2048 / 2;
insertCoinText.y = 300; // Top of screen
game.addChild(insertCoinText);
// Create "0/1 CREDITS" text
creditsText = new Text2('0/1 CREDITS', {
size: 40,
fill: 0xFFFFFF // White color
});
creditsText.anchor.set(0.5, 0.5);
creditsText.x = 2048 / 2;
creditsText.y = 380; // Below INSERT COIN!
game.addChild(creditsText);
}, 800); // Wait for the "2" animation to complete
}, 3000);
// Function to update credits display
function updateCreditsDisplay() {
if (creditsText) {
creditsText.setText(coins + '/' + maxCoins + ' CREDITS');
}
// (Removed 'PRESS START' text creation for gameplay)
// Handle target dragging
game.move = function (x, y, obj) {
// Allow both targets to be dragged at the same time
// Use draggingTarget1 and draggingTarget2 to track each target's drag state
// Move target1 if dragging
if (draggingTarget1 && target) {
target.x = x;
target.y = y;
// Only update lines for P1 (first two lines)
for (var i = 0; i < 2 && i < cannons.length; i++) {
var cannon = cannons[i];
var line = cannonLines[i];
var dx = target.x - cannon.x;
var dy = target.y - cannon.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
line.x = cannon.x;
line.y = cannon.y;
line.rotation = angle;
line.scaleX = distance / 100;
}
}
// Move target2 if dragging
if (draggingTarget2 && typeof target2 !== "undefined" && target2) {
target2.x = x;
target2.y = y;
// Only update lines for P2 (last two lines)
for (var i = 2; i < cannons.length; i++) {
var cannon = cannons[i];
var line = cannonLines[i];
var dx = target2.x - cannon.x;
var dy = target2.y - cannon.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
line.x = cannon.x;
line.y = cannon.y;
line.rotation = angle;
line.scaleX = distance / 100;
}
}
};
// Update down handler to include target dragging
var originalDown = game.down;
game.down = function (x, y, obj) {
// Call original down handler first
originalDown.call(this, x, y, obj);
// Allow both targets to be picked up for dragging at the same time
if (gameStarted) {
// Check if clicking on target1
if (target) {
var dx = x - target.x;
var dy = y - target.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 60) {
draggingTarget1 = true;
// Show lines for P1 cannons only
for (var i = 0; i < 2 && i < cannonLines.length; i++) {
cannonLines[i].alpha = 0.8;
}
}
}
// Check if clicking on target2
if (typeof target2 !== "undefined" && target2) {
var dx2 = x - target2.x;
var dy2 = y - target2.y;
var distance2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
if (distance2 < 60) {
draggingTarget2 = true;
// Show lines for P2 cannons only
for (var i = 2; i < cannonLines.length; i++) {
cannonLines[i].alpha = 0.8;
}
}
}
}
};
// Update up handler to stop target dragging
var originalUp = game.up;
game.up = function (x, y, obj) {
// Call original up handler first
originalUp.call(this, x, y, obj);
// Stop dragging for both targets independently
if (draggingTarget1) {
// Hide lines for P1 cannons only
for (var i = 0; i < 2 && i < cannonLines.length; i++) {
cannonLines[i].alpha = 0;
}
draggingTarget1 = false;
}
if (draggingTarget2) {
// Hide lines for P2 cannons only
for (var i = 2; i < cannonLines.length; i++) {
cannonLines[i].alpha = 0;
}
draggingTarget2 = false;
}
};
// Function to update cannon lines to point at target
function updateCannonLines() {
if (!target && !target2 || !isShooting) return;
for (var i = 0; i < cannons.length; i++) {
var cannon = cannons[i];
var line = cannonLines[i];
var dx, dy;
// First two cannons/lines (P1) point to target, last two (P2) to target2
if (i < 2) {
dx = target.x - cannon.x;
dy = target.y - cannon.y;
} else {
dx = target2.x - cannon.x;
dy = target2.y - cannon.y;
}
var distance = Math.sqrt(dx * dx + dy * dy);
var angle = Math.atan2(dy, dx);
line.x = cannon.x;
line.y = cannon.y;
line.rotation = angle;
line.scaleX = distance / 100; // Adjust line length
}
}
// Add shooting functionality
game.update = function () {
// Lines are now controlled by dragging, not automatic shooting
};
}
// Touch down event for inserting coins and starting game
game.down = function (x, y, obj) {
if (gameStarted) {
// (Handled by new drag logic in the other game.down handler)
return;
}
// If in ready screen (countdown), allow tap to insert coin
if (isInReadyScreen) {
// Allow inserting coin during ready screen
if (coins < maxCoins) {
coins++;
updateCreditsDisplay();
// Hide INSERT COIN text when max coins reached
if (coins >= maxCoins && insertCoinText) {
insertCoinText.alpha = 0.3; // Dim it
}
}
// Do NOT skip countdown on tap
return;
}
// Insert coin with tap
if (coins < maxCoins) {
coins++;
updateCreditsDisplay();
// Hide INSERT COIN text when max coins reached
if (coins >= maxCoins && insertCoinText) {
insertCoinText.alpha = 0.3; // Dim it
}
// Automatically go to ready screen when credit is added
if (!isInReadyScreen) {
startReadyScreen();
}
return;
}
// Start ready screen immediately when pressing start (no hold)
// Only allow starting when 2 credits are inserted
if (coins === maxCoins && !isInReadyScreen) {
startReadyScreen();
}
};
// Touch up event to stop holding
game.up = function (x, y, obj) {
if (gameStarted) {
// Only allow hold-to-skip during ready screen countdown
if (isInReadyScreen && countdownTimer && Date.now() - holdStartTime >= 1000) {
isHolding = false;
// Skip countdown
LK.clearInterval(countdownTimer);
countdownText.setText('GO!');
LK.setTimeout(function () {
if (countdownText) countdownText.alpha = 0;
isInReadyScreen = false;
// Show player ships after countdown
showPlayerShips();
// Start gameplay sequence
startGameplaySequence();
}, 500);
}
return;
}
isHolding = false;
// No hold-to-start for game; do nothing here except for hold-to-skip in ready screen
};
// Function to start ready screen
function startReadyScreen() {
gameStarted = true;
isInReadyScreen = true;
// Consume one credit when starting
coins--;
updateCreditsDisplay();
// Hide all title screen elements except creditsText (keep creditsText visible in ready screen)
if (titleText) titleText.alpha = 0;
if (numberText) numberText.alpha = 0;
if (insertCoinText) insertCoinText.alpha = 0;
if (creditsText) creditsText.alpha = 1; // Show creditsText during ready screen
if (pressStartText) pressStartText.alpha = 0;
// Make ship2 fully visible (unfade it) only if ship2 exists
if (ship2) {
tween(ship2, {
alpha: 1
}, {
duration: 500,
easing: tween.easeOut
});
}
// Show 15 second countdown
countdownText = new Text2('15', {
size: 80,
fill: 0xFF0000 // Red color
});
countdownText.anchor.set(0.5, 0.5);
countdownText.x = 2048 / 2;
countdownText.y = 400;
game.addChild(countdownText);
// Start countdown timer
var timeLeft = 15;
countdownTimer = LK.setInterval(function () {
timeLeft--;
if (timeLeft <= 0) {
LK.clearInterval(countdownTimer);
countdownText.setText('GO!');
LK.setTimeout(function () {
if (countdownText) countdownText.alpha = 0;
isInReadyScreen = false;
// Show player ships after countdown
showPlayerShips();
// Start gameplay sequence
startGameplaySequence();
}, 1000);
} else {
countdownText.setText(timeLeft.toString());
}
}, 1000);
// Game would start here - for now just clear screen
game.setBackgroundColor(0x000020); // Dark blue to show game started
}
// Function to show player ships on ready screen
function showPlayerShips() {
// Hide the hold-to-start ships
if (ship1) ship1.alpha = 0;
if (ship2) ship2.alpha = 0;
// Always show 2-player setup (force 2 players)
var shipPositions = [{
x: 100,
y: 2732 - 100
}, {
x: 100,
y: 2732 - 200
}, {
x: 2048 - 100,
y: 2732 - 100
}, {
x: 2048 - 100,
y: 2732 - 200
}];
// Create ships
for (var i = 0; i < shipPositions.length; i++) {
var ship = LK.getAsset('character', {
anchorX: 0.5,
anchorY: 0.5,
x: shipPositions[i].x,
y: shipPositions[i].y,
scaleX: 0.6,
scaleY: 0.6
});
game.addChild(ship);
}
}
// Function to start the gameplay sequence
function startGameplaySequence() {
// Show "Shoot Enemy Ships" text
var shootText = new Text2('Shoot Enemy Ships', {
size: 80,
fill: 0xFFFF00 // Yellow
});
shootText.anchor.set(0.5, 0.5);
shootText.x = 2048 / 2;
shootText.y = 2732 / 2 - 200;
game.addChild(shootText);
// After 2 seconds, show "Ready?"
LK.setTimeout(function () {
shootText.alpha = 0;
var readyText = new Text2('Ready?', {
size: 100,
fill: 0x00FF00 // Green
});
readyText.anchor.set(0.5, 0.5);
readyText.x = 2048 / 2;
readyText.y = 2732 / 2;
game.addChild(readyText);
// After 1 more second, show "Go!" and start game
LK.setTimeout(function () {
readyText.alpha = 0;
var goText = new Text2('Go!', {
size: 120,
fill: 0xFF0000 // Red
});
goText.anchor.set(0.5, 0.5);
goText.x = 2048 / 2;
goText.y = 2732 / 2;
game.addChild(goText);
// Show "Protect Cannons" with arrows at bottom
var protectText = new Text2('Protect Cannons', {
size: 60,
fill: 0xFFFFFF // White
});
protectText.anchor.set(0.5, 0.5);
protectText.x = 2048 / 2;
protectText.y = 2732 - 100;
game.addChild(protectText);
// Create left arrow
var leftArrow = new Text2('←', {
size: 80,
fill: 0xFFFFFF // White
});
leftArrow.anchor.set(0.5, 0.5);
leftArrow.x = 2048 / 2 - 300;
leftArrow.y = 2732 - 100;
game.addChild(leftArrow);
// Create right arrow
var rightArrow = new Text2('→', {
size: 80,
fill: 0xFFFFFF // White
});
rightArrow.anchor.set(0.5, 0.5);
rightArrow.x = 2048 / 2 + 300;
rightArrow.y = 2732 - 100;
game.addChild(rightArrow);
// Fade out "Go!" after a moment
LK.setTimeout(function () {
goText.alpha = 0;
// Gameplay starts here
// Always create 2-player ships (force 2 players)
// P1 ships (red) - left corners
var p1Ship1 = LK.getAsset('p1Ship', {
anchorX: 0.5,
anchorY: 0.5,
x: 100,
y: 100,
scaleX: 1,
scaleY: 1
});
game.addChild(p1Ship1);
gameplayShips.push(p1Ship1);
var p1Ship2 = LK.getAsset('p1Ship', {
anchorX: 0.5,
anchorY: 0.5,
x: 100,
y: 2732 - 100,
scaleX: 1,
scaleY: 1
});
game.addChild(p1Ship2);
gameplayShips.push(p1Ship2);
// P2 ships (blue) - right corners
var p2Ship1 = LK.getAsset('p2Ship', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 100,
y: 100,
scaleX: 1,
scaleY: 1
});
game.addChild(p2Ship1);
gameplayShips.push(p2Ship1);
var p2Ship2 = LK.getAsset('p2Ship', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 100,
y: 2732 - 100,
scaleX: 1,
scaleY: 1
});
game.addChild(p2Ship2);
gameplayShips.push(p2Ship2);
// Create target in center
target = LK.getAsset('target', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 / 2,
y: 2732 / 2,
scaleX: 1,
scaleY: 1
});
game.addChild(target);
// Create a second target for P2, positioned near the right side
target2 = LK.getAsset('target', {
anchorX: 0.5,
anchorY: 0.5,
x: 2048 - 300,
y: 2732 / 2,
scaleX: 1,
scaleY: 1
});
game.addChild(target2);
// Always use 2-player: 4 cannons
var cannonPositions = [{
x: 100,
y: 2732 - 100
}, {
x: 200,
y: 2732 - 100
}, {
x: 2048 - 200,
y: 2732 - 100
}, {
x: 2048 - 100,
y: 2732 - 100
}];
for (var i = 0; i < cannonPositions.length; i++) {
var cannon = LK.getAsset('character', {
anchorX: 0.5,
anchorY: 0.5,
x: cannonPositions[i].x,
y: cannonPositions[i].y,
scaleX: 0.5,
scaleY: 0.5,
tint: 0x808080 // Gray color for cannons
});
game.addChild(cannon);
cannons.push(cannon);
}
// Create lines from cannons to target (initially hidden)
for (var i = 0; i < cannons.length; i++) {
var line = LK.getAsset('character', {
anchorX: 0,
anchorY: 0.5,
x: cannons[i].x,
y: cannons[i].y,
scaleX: 1,
scaleY: 0.02,
// Very thin to simulate a line
tint: 0xFF0000,
// Red laser color
alpha: 0 // Initially hidden
});
game.addChild(line);
cannonLines.push(line);
}
// Show score displays at the start of gameplay
showScoreDisplays();
updateScoreDisplays();
// --- INVADER WAVE SETUP ---
var invaders = [];
var invaderRows = 3;
var invaderCols = 8;
var invaderSpacingX = 180;
var invaderSpacingY = 120;
var invaderStartX = 2048 / 2 - (invaderCols - 1) * invaderSpacingX / 2;
var invaderStartY = 300;
for (var row = 0; row < invaderRows; row++) {
for (var col = 0; col < invaderCols; col++) {
var invader = new Invader();
invader.x = invaderStartX + col * invaderSpacingX;
invader.y = invaderStartY + row * invaderSpacingY;
invader.lastX = invader.x;
invader.lastY = invader.y;
game.addChild(invader);
invaders.push(invader);
}
}
// Add invader update logic to game.update
function invaderUpdateHandler() {
// Keep score displays visible and updated
if (typeof showScoreDisplays === "function" && (!p1ScoreDisplay || !p2ScoreDisplay || !highScoreDisplay)) {
showScoreDisplays();
}
updateScoreDisplays();
// Keep score displays visible and updated
if (typeof showScoreDisplays === "function" && (!p1ScoreDisplay || !p2ScoreDisplay || !highScoreDisplay)) {
showScoreDisplays();
}
updateScoreDisplays();
// Move invaders and check for collisions
for (var i = invaders.length - 1; i >= 0; i--) {
var invader = invaders[i];
invader.update();
// Check collision with target1 ONLY if P1 lines are visible (shooting)
if (target && invader.intersects(target) && cannonLines.length >= 2 && (cannonLines[0].alpha > 0 || cannonLines[1].alpha > 0)) {
invader.destroy();
invaders.splice(i, 1);
// Add 50 points to P1 score only
p1Score += 50;
if (p1Score + p2Score > highScore) highScore = p1Score + p2Score;
if (p1Score + p2Score > highScore) highScore = p1Score + p2Score;
LK.setScore(p1Score + p2Score); // Update total score for compatibility
updateScoreDisplays();
updateScoreDisplays();
continue;
}
// Check collision with target2 ONLY if P2 lines are visible (shooting)
if (typeof target2 !== "undefined" && target2 && invader.intersects(target2) && cannonLines.length >= 4 && (cannonLines[2].alpha > 0 || cannonLines[3].alpha > 0)) {
invader.destroy();
invaders.splice(i, 1);
// Add 50 points to P2 score only
p2Score += 50;
if (p1Score + p2Score > highScore) highScore = p1Score + p2Score;
if (p1Score + p2Score > highScore) highScore = p1Score + p2Score;
LK.setScore(p1Score + p2Score); // Update total score for compatibility
updateScoreDisplays();
updateScoreDisplays();
continue;
}
// Game over if invader reaches bottom
if (invader.lastY <= 2732 - 60 && invader.y > 2732 - 60) {
LK.showGameOver();
return;
}
}
// Check if all invaders from the current level are killed
if (invaders.length > 0) {
var allKilled = true;
for (var i = 0; i < invaders.length; i++) {
// Only count invaders that are visible and not destroyed
if (!invaders[i].destroyed && invaders[i].alpha !== 0) {
allKilled = false;
break;
}
}
if (allKilled) {
// Remove all old invaders before starting new level
for (var i = invaders.length - 1; i >= 0; i--) {
if (invaders[i] && typeof invaders[i].destroy === "function") {
invaders[i].destroy();
}
invaders.splice(i, 1);
}
// Add new invaders for the new level
var invaderRows = 3;
var invaderCols = 8;
var invaderSpacingX = 180;
var invaderSpacingY = 120;
var invaderStartX = 2048 / 2 - (invaderCols - 1) * invaderSpacingX / 2;
var invaderStartY = 300;
for (var row = 0; row < invaderRows; row++) {
for (var col = 0; col < invaderCols; col++) {
var invader = new Invader();
invader.x = invaderStartX + col * invaderSpacingX;
invader.y = invaderStartY + row * invaderSpacingY;
invader.lastX = invader.x;
invader.lastY = invader.y;
// Ensure invader is killable and active for this level
invader.alpha = 1;
invader.visible = true;
invader.destroyed = false;
game.addChild(invader);
invaders.push(invader);
}
}
// No stage complete popup, just continue to next level
return;
}
}
}
// Always call invaderUpdateHandler every frame
game.update = invaderUpdateHandler;
}, 1000);
}, 1000);
}, 2000);
}