/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Enemy class
var Enemy = Container.expand(function () {
var self = Container.call(this);
// Randomly choose between enemy_1 and enemy_2
var enemyAsset = Math.random() < 0.5 ? 'enemy' : 'Enemy_2';
var enemySprite = self.attachAsset(enemyAsset, {
anchorX: 0.5,
anchorY: 0.5
});
self.width = enemySprite.width;
self.height = enemySprite.height;
self.speed = 5; // Decreased speed for level 1
self.alive = true;
self.lane = 0; // Track which lane this enemy is in (0-4)
self.update = function () {
var effectiveSpeed = self.speed;
if (enemySpeedUpgrade) {
effectiveSpeed *= 0.7; // 30% slower
}
self.y += effectiveSpeed * gameSpeed;
if (self.y > GAMEPLAY_AREA_BOTTOM) {
self.alpha = 0;
} else {
self.alpha = 1;
}
};
return self;
});
// FireRateBoost drop
var FireRateBoost = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('speedBoost', {
anchorX: 0.5,
anchorY: 0.5,
color: 0x00bfff // blue
});
self.width = sprite.width;
self.height = sprite.height;
self.speed = 10;
self.type = 'firerate';
self.update = function () {
self.y += self.speed * gameSpeed;
};
return self;
});
// FireStyleBoost drop
var FireStyleBoost = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('speedBoost', {
anchorX: 0.5,
anchorY: 0.5,
color: 0xff8800 // orange
});
self.width = sprite.width;
self.height = sprite.height;
self.speed = 10;
self.type = 'firestyle';
self.update = function () {
self.y += self.speed * gameSpeed;
};
return self;
});
// Hero class
var Hero = Container.expand(function () {
var self = Container.call(this);
var heroSprite = self.attachAsset('hero', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = heroSprite.width;
self.height = heroSprite.height;
self.shootCooldown = 0; // frames until next allowed shot
// Shoot method
self.shoot = function () {
if (self.shootCooldown > 0 || isReloading || bulletCount <= 0) return;
// Calculate bullets to fire based on fire style
var bulletsToFire = fireStyle;
if (bulletCount < bulletsToFire) {
startReload();
return;
}
// Fire style: 1=single, 2=double, 3=triple
if (fireStyle === 1) {
var bullet = new HeroBullet();
bullet.x = self.x;
bullet.y = self.y - self.height / 2 - bullet.height / 2;
heroBullets.push(bullet);
game.addChild(bullet);
} else if (fireStyle === 2) {
for (var i = -1; i <= 1; i += 2) {
var bullet = new HeroBullet();
bullet.x = self.x + i * 40;
bullet.y = self.y - self.height / 2 - bullet.height / 2;
heroBullets.push(bullet);
game.addChild(bullet);
}
} else if (fireStyle === 3) {
for (var i = -1; i <= 1; i++) {
var bullet = new HeroBullet();
bullet.x = self.x + i * 40;
bullet.y = self.y - self.height / 2 - bullet.height / 2;
heroBullets.push(bullet);
game.addChild(bullet);
}
}
// Consume bullets
bulletCount -= bulletsToFire;
updateBulletDisplay();
// Remove bullet display elements based on bullets fired
for (var k = 0; k < bulletsToFire; k++) {
if (bulletTxt.children.length > 0) {
var bulletElement = bulletTxt.children[bulletTxt.children.length - 1];
bulletTxt.removeChild(bulletElement);
}
}
// Check if we need to reload
if (bulletCount <= 0) {
startReload();
}
if (fireRateActive) {
self.shootCooldown = 4; // much faster
} else {
self.shootCooldown = 20; // slower normal fire rate
}
LK.getSound('shoot').play();
};
// Called every tick
self.update = function () {
if (self.shootCooldown > 0) self.shootCooldown--;
};
return self;
});
// Hero bullet class
var HeroBullet = Container.expand(function () {
var self = Container.call(this);
var bulletSprite = self.attachAsset('heroBullet', {
anchorX: 0.5,
anchorY: 0.5
});
// Apply bullet size upgrade
if (bulletSizeUpgrade) {
bulletSprite.scaleX = 1.5;
bulletSprite.scaleY = 1.5;
}
self.width = bulletSprite.width;
self.height = bulletSprite.height;
self.speed = -36; // Upwards
self.update = function () {
self.y += self.speed;
};
return self;
});
// Market window class
var MarketWindow = Container.expand(function () {
var self = Container.call(this);
// Semi-transparent background - larger market window
var bg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: 1800,
height: 1400,
color: 0x000000
});
bg.alpha = 0.8;
self.addChild(bg);
// Market title
var title = new Text2('MARKET', {
size: 80,
fill: 0xffffff
});
title.anchor.set(0.5, 0.5);
title.x = 0;
title.y = -400;
self.addChild(title);
// Bullet size upgrade button - larger for better touch
var bulletUpgradeBtn = LK.getAsset('marketButton', {
anchorX: 0.5,
anchorY: 0.5,
color: 0x00ff00,
scaleX: 2.0,
scaleY: 1.5
});
bulletUpgradeBtn.x = 0;
bulletUpgradeBtn.y = -200;
self.addChild(bulletUpgradeBtn);
var bulletUpgradeText = new Text2('BIGGER BULLETS\n50 Coins', {
size: 40,
fill: 0xffffff
});
bulletUpgradeText.anchor.set(0.5, 0.5);
bulletUpgradeText.x = 0;
bulletUpgradeText.y = -200;
self.addChild(bulletUpgradeText);
// Enemy speed slower upgrade button - larger for better touch
var speedUpgradeBtn = LK.getAsset('marketButton', {
anchorX: 0.5,
anchorY: 0.5,
color: 0xff6600,
scaleX: 2.0,
scaleY: 1.5
});
speedUpgradeBtn.x = 0;
speedUpgradeBtn.y = 0;
self.addChild(speedUpgradeBtn);
var speedUpgradeText = new Text2('SLOWER ENEMIES\n75 Coins', {
size: 40,
fill: 0xffffff
});
speedUpgradeText.anchor.set(0.5, 0.5);
speedUpgradeText.x = 0;
speedUpgradeText.y = 0;
self.addChild(speedUpgradeText);
// Back button - larger for better touch
var backBtn = LK.getAsset('marketButton', {
anchorX: 0.5,
anchorY: 0.5,
color: 0xff0000,
scaleX: 2.0,
scaleY: 1.5
});
backBtn.x = 0;
backBtn.y = 300;
self.addChild(backBtn);
var backText = new Text2('BACK', {
size: 50,
fill: 0xffffff
});
backText.anchor.set(0.5, 0.5);
backText.x = 0;
backText.y = 300;
self.addChild(backText);
// Store button references for hit detection
self.bulletUpgradeBtn = bulletUpgradeBtn;
self.speedUpgradeBtn = speedUpgradeBtn;
self.backBtn = backBtn;
return self;
});
// ShieldBoost drop
var ShieldBoost = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('speedBoost', {
anchorX: 0.5,
anchorY: 0.5,
color: 0x00ffea // cyan
});
self.width = sprite.width;
self.height = sprite.height;
self.speed = 10;
self.type = 'shield';
self.update = function () {
self.y += self.speed * gameSpeed;
};
return self;
});
// Speed boost drop class
var SpeedBoost = Container.expand(function () {
var self = Container.call(this);
var boostSprite = self.attachAsset('speedBoost', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = boostSprite.width;
self.height = boostSprite.height;
self.speed = 10;
self.type = 'speed'; // default type
self.update = function () {
self.y += self.speed * gameSpeed;
};
return self;
});
// Warning asset that appears on idle player
var WarningAsset = Container.expand(function () {
var self = Container.call(this);
var warningSprite = self.attachAsset('laneholding', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = warningSprite.width;
self.height = warningSprite.height;
self.lifeTimer = 2000; // 2 seconds
self.update = function () {
self.lifeTimer -= 1000 / 60; // Decrease by frame time
if (self.lifeTimer <= 0) {
// Check if damage cooldown is active
if (damageCooldown <= 0) {
// Damage player
lives--;
updateLivesDisplay();
LK.getSound('hit').play();
LK.effects.flashObject(hero, 0xff0000, 400);
// Start damage cooldown
damageCooldown = damageCooldownDuration;
// Check game over
if (lives <= 0) {
finished = true;
LK.showGameOver();
return;
}
}
// Remove warning asset regardless of whether damage was dealt
if (warningAsset) {
warningAsset.destroy();
warningAsset = null;
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x111111
});
/****
* Game Code
****/
// --- Global variables ---
// Hero (player)
// Hero bullet
// Enemy
// Speed boost drop
// Road (background)
// Sound effects
// Music
// soldier: gray box
// tank: green box
// jungle green
var hero;
var heroBullets = [];
var enemies = [];
var boosts = [];
var lives = 3;
var gameTime = 0; // ms
var gameDuration = 20000; // 20 seconds
var gameSpeed = 1; // Multiplier, affected by boosts
var boostActive = false;
var boostTimer = 0;
// Power-up states
var fireRateActive = false;
var fireRateTimer = 0;
var fireStyle = 1; // 1: single, 2: double, 3: triple
var fireStyleTimer = 0;
var shieldActive = false;
var shieldTimer = 0;
var lastEnemySpawnTick = 0;
var enemySpawnInterval = 48; // frames (0.8s at 60fps)
var score = 0;
var finished = false;
var level = 1;
var killTarget = 10; // Level 1: 10, Level 2: 15, Level 3: 20, etc.
// Lane idle detection
var lastLane = 2; // Track last lane position
var laneIdleTimer = 0; // Timer for staying in same lane
var idleThreshold = 3000; // 3 seconds in milliseconds
var warningAsset = null; // Asset that appears on player
var damageCooldown = 0; // Cooldown timer after taking damage
var damageCooldownDuration = 5000; // 5 seconds cooldown after damage
// Lane holding kill tracking
var laneHoldingTimer = 0; // Timer for consecutive kills in same lane
var laneHoldingThreshold = 3000; // 3 seconds in milliseconds
var lastKillLane = -1; // Track lane of last kill (-1 means no previous kill)
// Coin system
var coins = storage.coins || 0;
// Market system
var gamePaused = false;
var marketWindowVisible = false;
var marketWindow = null;
var marketInactivityTimer = 0;
var marketInactivityTimeout = 5000; // 5 seconds in milliseconds
// Upgrade states
var bulletSizeUpgrade = false;
var enemySpeedUpgrade = false;
// Bullet system
var bulletCount = 5; // Current bullets available
var maxBullets = 5; // Maximum bullets in magazine
var isReloading = false;
var reloadTimer = 0;
var reloadDuration = 2000; // 2 seconds in milliseconds
function updateKillTarget() {
if (level === 1) {
killTarget = 10;
} else if (level === 2) {
killTarget = 15;
} else {
killTarget = 15 + (level - 2) * 5;
}
}
updateKillTarget();
// --- Define gameplay and control areas ---
var CONTROL_AREA_HEIGHT = 400; // px reserved for controller at bottom
var GAMEPLAY_AREA_TOP = 0;
var GAMEPLAY_AREA_BOTTOM = 2732 - CONTROL_AREA_HEIGHT;
// --- Define 5 lanes ---
var LANE_COUNT = 5;
var ROAD_WIDTH = 900;
var LANE_WIDTH = ROAD_WIDTH / LANE_COUNT; // 180px per lane
var ROAD_LEFT = 2048 / 2 - ROAD_WIDTH / 2;
var currentLane = 2; // Start hero in middle lane (0-4)
// Get lane center X position
function getLaneX(laneIndex) {
return ROAD_LEFT + (laneIndex + 0.5) * LANE_WIDTH;
}
// Get lane index from X position
function getLaneFromX(x) {
var relativeX = x - ROAD_LEFT;
var laneIndex = Math.floor(relativeX / LANE_WIDTH);
return Math.max(0, Math.min(LANE_COUNT - 1, laneIndex));
}
// --- Controller area background (army green) ---
var controllerBg = LK.getAsset('centerCircle', {
anchorX: 0,
anchorY: 0,
width: 2048,
height: CONTROL_AREA_HEIGHT,
color: 0x4B5320,
// army green
x: 0,
y: 2732 - CONTROL_AREA_HEIGHT
});
game.addChild(controllerBg);
// --- Road background (restricted to end at controller line) ---
var road = LK.getAsset('road', {
anchorX: 0.5,
anchorY: 0,
width: 900,
height: 2732 - CONTROL_AREA_HEIGHT // End at controller line
});
road.x = 2048 / 2;
road.y = 0;
game.addChild(road);
// --- Hero ---
hero = new Hero();
hero.x = getLaneX(currentLane);
hero.y = GAMEPLAY_AREA_BOTTOM - 150;
game.addChild(hero);
// --- GUI: Lives (Heart Icons) ---
var heartIcons = [];
for (var i = 0; i < 3; i++) {
var heart = LK.getAsset('heart', {
anchorX: 0.5,
anchorY: 0.5
});
heart.x = 150 + i * 100; // Space hearts 100px apart
heart.y = 60;
heartIcons.push(heart);
LK.gui.top.addChild(heart);
}
// --- GUI: Level/Target ---
var levelTxt = new Text2('Level 1', {
size: 90,
fill: 0xFFFFFF
});
levelTxt.anchor.set(0, 0);
LK.gui.topLeft.addChild(levelTxt);
var targetTxt = new Text2('Target: 10', {
size: 90,
fill: 0xFFFFFF
});
targetTxt.anchor.set(0.5, 0);
LK.gui.bottom.addChild(targetTxt);
function updateLevelDisplay() {
levelTxt.setText('Level ' + level);
targetTxt.setText('Target: ' + killTarget);
}
// --- GUI: Score ---
var scoreTxt = new Text2('0', {
size: 90,
fill: 0xFFE066
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- GUI: Coins ---
var coinContainer = new Container();
var coinTxt = new Text2(coins, {
size: 50,
fill: 0x000000
});
coinTxt.anchor.set(0.5, 0.5);
var coinIcon = LK.getAsset('coin', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
coinIcon.x = 0;
coinIcon.y = 0;
coinTxt.x = 0;
coinTxt.y = 0;
coinContainer.addChild(coinIcon);
coinContainer.addChild(coinTxt);
coinContainer.x = -100;
coinContainer.y = 120;
LK.gui.topRight.addChild(coinContainer);
// --- GUI: Bullet Count and Reloading ---
var bulletTxt = LK.getAsset('bulletCount', {
anchorX: 1.0,
anchorY: 0
});
LK.gui.topRight.addChild(bulletTxt);
var reloadingTxt = LK.getAsset('reloadingText', {
anchorX: 0.5,
anchorY: 0.5
});
reloadingTxt.alpha = 0;
LK.gui.center.addChild(reloadingTxt);
// --- Music ---
LK.playMusic('bgmusic');
// --- Helper: Update GUI ---
function updateLivesDisplay() {
for (var i = 0; i < heartIcons.length; i++) {
heartIcons[i].alpha = i < lives ? 1 : 0.2; // Show full hearts for remaining lives, dim for lost lives
}
}
function updateTimerDisplay() {
// Timer GUI removed, nothing to update
}
function updateScoreDisplay() {
scoreTxt.setText(score);
}
function updateBulletDisplay() {
// Clear existing bullet display elements
while (bulletTxt.children.length > 0) {
bulletTxt.removeChild(bulletTxt.children[0]);
}
// Add bullet elements based on current bullet count
for (var i = 0; i < bulletCount; i++) {
var bulletElement = LK.getAsset('heroBullet', {
anchorX: 0.5,
anchorY: 0.5
});
bulletElement.x = -30 - i * 25; // Position bullets horizontally
bulletElement.y = 0;
bulletTxt.addChild(bulletElement);
}
}
function updateCoinDisplay() {
coinTxt.setText(coins);
storage.coins = coins; // Save to persistent storage
}
function startReload() {
if (!isReloading && bulletCount < maxBullets) {
isReloading = true;
reloadTimer = reloadDuration;
reloadingTxt.alpha = 1;
tween(reloadingTxt, {
alpha: 0
}, {
duration: reloadDuration,
onFinish: function onFinish() {
bulletCount = maxBullets;
isReloading = false;
updateBulletDisplay();
}
});
}
}
// --- Touch controls: Move hero, shoot ---
var dragHero = false;
var leftArrowBtn, rightArrowBtn, fireBtn;
var leftBtnPressed = false;
var rightBtnPressed = false;
var fireBtnPressed = false;
var controllerBtnSize = 200; // Size for arrow buttons
var fireBtnSize = 250; // Size for fire button
// Move hero to specific lane
function moveHeroToLane(newLane) {
currentLane = Math.max(0, Math.min(LANE_COUNT - 1, newLane));
hero.x = getLaneX(currentLane);
}
// Controller buttons (left arrow, right arrow, fire)
leftArrowBtn = LK.getAsset('leftArrow', {
anchorX: 0.5,
anchorY: 0.5
});
rightArrowBtn = LK.getAsset('rightArrow', {
anchorX: 0.5,
anchorY: 0.5
});
fireBtn = LK.getAsset('fireButton', {
anchorX: 0.5,
anchorY: 0.5
});
leftArrowBtn.alpha = 1.0;
rightArrowBtn.alpha = 1.0;
fireBtn.alpha = 1.0;
// Place buttons side-by-side in the control area with better left-to-right spacing
leftArrowBtn.x = 300;
leftArrowBtn.y = 2732 - CONTROL_AREA_HEIGHT / 2;
rightArrowBtn.x = 600;
rightArrowBtn.y = 2732 - CONTROL_AREA_HEIGHT / 2;
fireBtn.x = 1600;
fireBtn.y = 2732 - CONTROL_AREA_HEIGHT / 2;
// Make sure controllerBg is below buttons (already added above)
// Add controller buttons to game layer to make them visible
game.addChild(leftArrowBtn);
game.addChild(rightArrowBtn);
game.addChild(fireBtn);
// Market button
var marketBtn = LK.getAsset('marketButton', {
anchorX: 0.5,
anchorY: 0.5
});
marketBtn.x = 1000;
marketBtn.y = 2732 - CONTROL_AREA_HEIGHT / 2;
marketBtn.alpha = 0.8;
game.addChild(marketBtn);
// Market text label
var marketLabel = new Text2('MARKET', {
size: 30,
fill: 0xffffff
});
marketLabel.anchor.set(0.5, 0.5);
marketLabel.x = marketBtn.x;
marketLabel.y = marketBtn.y;
game.addChild(marketLabel);
// Make buttons more visible with higher alpha
leftArrowBtn.alpha = 0.8;
rightArrowBtn.alpha = 0.8;
fireBtn.alpha = 0.8;
// Helper: check if point is inside button
function isInsideBtn(btn, x, y) {
// Account for button scaling and provide generous touch areas
var baseWidth = btn.width || 150;
var baseHeight = btn.height || 80;
var scaleX = btn.scaleX || 1;
var scaleY = btn.scaleY || 1;
var touchWidth = baseWidth * scaleX * 1.2; // 20% extra touch area
var touchHeight = baseHeight * scaleY * 1.2; // 20% extra touch area
return x >= btn.x - touchWidth / 2 && x <= btn.x + touchWidth / 2 && y >= btn.y - touchHeight / 2 && y <= btn.y + touchHeight / 2;
}
function showMarket() {
if (!marketWindowVisible) {
gamePaused = true;
marketWindowVisible = true;
marketInactivityTimer = 0; // Reset timer when market opens
// Create and show market window
marketWindow = new MarketWindow();
marketWindow.x = 2048 / 2;
marketWindow.y = 2732 / 2;
game.addChild(marketWindow);
}
}
function hideMarket() {
if (marketWindowVisible) {
gamePaused = false;
marketWindowVisible = false;
// Remove market window
if (marketWindow) {
marketWindow.destroy();
marketWindow = null;
}
}
}
game.down = function (x, y, obj) {
// If touch is on left arrow, set pressed
if (isInsideBtn(leftArrowBtn, x, y)) {
leftBtnPressed = true;
leftArrowBtn.alpha = 0.9;
return;
}
// If touch is on right arrow, set pressed
if (isInsideBtn(rightArrowBtn, x, y)) {
rightBtnPressed = true;
rightArrowBtn.alpha = 0.9;
return;
}
// If touch is on fire button, set pressed and shoot
if (isInsideBtn(fireBtn, x, y)) {
fireBtnPressed = true;
fireBtn.alpha = 0.9;
hero.shoot();
return;
}
// If touch is on market button, show market
if (isInsideBtn(marketBtn, x, y)) {
marketBtn.alpha = 0.9;
showMarket();
return;
}
// Handle market window interactions
if (marketWindowVisible && marketWindow) {
// Reset inactivity timer on any interaction with market window
marketInactivityTimer = 0;
// Convert to market window local coordinates
var localPos = marketWindow.toLocal({
x: x,
y: y
});
// Check bullet upgrade button - use global coordinates
var bulletBtnGlobalPos = marketWindow.toGlobal(marketWindow.bulletUpgradeBtn.position);
if (isInsideBtn({
x: bulletBtnGlobalPos.x,
y: bulletBtnGlobalPos.y,
width: marketWindow.bulletUpgradeBtn.width,
height: marketWindow.bulletUpgradeBtn.height
}, x, y)) {
if (coins >= 50 && !bulletSizeUpgrade) {
coins -= 50;
bulletSizeUpgrade = true;
updateCoinDisplay();
LK.effects.flashScreen(0x00ff00, 300);
}
return;
}
// Check enemy speed upgrade button - use global coordinates
var speedBtnGlobalPos = marketWindow.toGlobal(marketWindow.speedUpgradeBtn.position);
if (isInsideBtn({
x: speedBtnGlobalPos.x,
y: speedBtnGlobalPos.y,
width: marketWindow.speedUpgradeBtn.width,
height: marketWindow.speedUpgradeBtn.height
}, x, y)) {
if (coins >= 75 && !enemySpeedUpgrade) {
coins -= 75;
enemySpeedUpgrade = true;
updateCoinDisplay();
LK.effects.flashScreen(0x00ff00, 300);
}
return;
}
// Check back button - use global coordinates
var backBtnGlobalPos = marketWindow.toGlobal(marketWindow.backBtn.position);
if (isInsideBtn({
x: backBtnGlobalPos.x,
y: backBtnGlobalPos.y,
width: marketWindow.backBtn.width,
height: marketWindow.backBtn.height
}, x, y)) {
hideMarket();
return;
}
}
};
game.move = function (x, y, obj) {
// Drag control disabled
// If finger moves off controller, release
if (!isInsideBtn(leftArrowBtn, x, y)) {
leftBtnPressed = false;
leftArrowBtn.alpha = 0.8;
}
if (!isInsideBtn(rightArrowBtn, x, y)) {
rightBtnPressed = false;
rightArrowBtn.alpha = 0.8;
}
if (!isInsideBtn(fireBtn, x, y)) {
fireBtnPressed = false;
fireBtn.alpha = 0.8;
}
if (!isInsideBtn(marketBtn, x, y)) {
marketBtn.alpha = 0.8;
}
};
game.up = function (x, y, obj) {
leftBtnPressed = false;
rightBtnPressed = false;
fireBtnPressed = false;
leftArrowBtn.alpha = 0.8;
rightArrowBtn.alpha = 0.8;
fireBtn.alpha = 0.8;
marketBtn.alpha = 0.8;
};
// --- Enemy spawn logic ---
function spawnEnemy() {
// Enemies spawn randomly in one of the 5 lanes
var enemy = new Enemy();
var enemyLane = Math.floor(Math.random() * LANE_COUNT);
enemy.x = getLaneX(enemyLane);
enemy.lane = enemyLane; // Set the enemy's lane
// Spawn at the top of the gameplay area, not above the road
enemy.y = GAMEPLAY_AREA_TOP - enemy.height / 2 - 10;
// Randomize speed a bit
enemy.speed = 6 + Math.random() * 3;
enemies.push(enemy);
game.addChild(enemy);
}
// --- Boost logic ---
function spawnBoost(x, y) {
// Randomly choose power-up type
var r = Math.random();
var boost;
if (r < 0.25) {
boost = new SpeedBoost();
} else if (r < 0.5) {
boost = new FireRateBoost();
} else if (r < 0.75) {
boost = new FireStyleBoost();
} else {
boost = new ShieldBoost();
}
boost.x = x;
boost.y = y;
boosts.push(boost);
game.addChild(boost);
}
// --- Game update ---
game.update = function () {
// Handle market inactivity timer
if (marketWindowVisible) {
marketInactivityTimer += 1000 / 60; // Increase by frame time
if (marketInactivityTimer >= marketInactivityTimeout) {
// Auto-close market after 5 seconds of inactivity
tween(marketWindow, {
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
hideMarket();
}
});
marketInactivityTimer = 0; // Reset timer to prevent multiple triggers
}
return; // Don't continue with game updates when paused
}
if (finished) return;
// --- Controller movement ---
if (leftBtnPressed) {
moveHeroToLane(currentLane - 1);
leftBtnPressed = false; // Single lane move per press
leftArrowBtn.alpha = 0.8;
}
if (rightBtnPressed) {
moveHeroToLane(currentLane + 1);
rightBtnPressed = false; // Single lane move per press
rightArrowBtn.alpha = 0.8;
}
// --- Update damage cooldown ---
if (damageCooldown > 0) {
damageCooldown -= 1000 / 60; // Decrease by frame time
}
// --- Lane idle detection ---
if (currentLane === lastLane) {
// Only increase idle timer if not in damage cooldown
if (damageCooldown <= 0) {
laneIdleTimer += 1000 / 60; // Increase by frame time (milliseconds)
}
// If player has been idle for 3 seconds and no warning exists and not in cooldown
if (laneIdleTimer >= idleThreshold && !warningAsset && damageCooldown <= 0) {
warningAsset = new WarningAsset();
warningAsset.x = hero.x;
warningAsset.y = hero.y - 100; // Position above hero
game.addChild(warningAsset);
}
} else {
// Player moved to different lane, reset timer
laneIdleTimer = 0;
lastLane = currentLane;
// Remove warning if it exists
if (warningAsset) {
warningAsset.destroy();
warningAsset = null;
}
}
// --- Lane holding kill timer ---
if (lastKillLane === currentLane && lastKillLane !== -1) {
// Player is holding in same lane where they made their last kill
laneHoldingTimer += 1000 / 60; // Increase by frame time (milliseconds)
// If player has been holding for 3 seconds, reset timer and start count again
if (laneHoldingTimer >= laneHoldingThreshold) {
laneHoldingTimer = 0; // Reset timer to 0 and start count again
}
} else if (currentLane !== lastKillLane && lastKillLane !== -1) {
// Player moved away from kill lane, reset holding timer
laneHoldingTimer = 0;
}
// --- Update warning asset ---
if (warningAsset) {
warningAsset.update();
// Keep warning positioned above hero
warningAsset.x = hero.x;
warningAsset.y = hero.y - 100;
}
// --- Timer ---
// (Timer removed, no time-based win/lose condition)
updateTimerDisplay();
// --- Win condition ---
// (Handled by kill target below)
// --- Boost logic ---
if (boostActive) {
boostTimer -= 1000 / 60;
if (boostTimer <= 0) {
boostActive = false;
gameSpeed = 1;
}
}
if (fireRateActive) {
fireRateTimer -= 1000 / 60;
if (fireRateTimer <= 0) {
fireRateActive = false;
}
}
if (fireStyle > 1) {
fireStyleTimer -= 1000 / 60;
if (fireStyleTimer <= 0) {
fireStyle = 1;
}
}
if (shieldActive) {
shieldTimer -= 1000 / 60;
if (shieldTimer <= 0) {
shieldActive = false;
}
}
// --- Enemy spawn ---
if (LK.ticks - lastEnemySpawnTick >= enemySpawnInterval) {
spawnEnemy();
lastEnemySpawnTick = LK.ticks;
}
// --- Update hero ---
hero.update();
// --- Update hero bullets ---
for (var i = heroBullets.length - 1; i >= 0; i--) {
var b = heroBullets[i];
b.update();
// Remove if off screen
if (b.y < -b.height) {
b.destroy();
heroBullets.splice(i, 1);
}
}
// --- Update enemies ---
for (var i = enemies.length - 1; i >= 0; i--) {
var e = enemies[i];
e.update();
// Remove if off screen
if (e.y > 2732 + e.height) {
e.destroy();
enemies.splice(i, 1);
continue;
}
// Check collision with hero - only if enemy is in same lane
if (e.alive && e.lane === currentLane && e.intersects(hero)) {
if (shieldActive) {
// Ignore hit, just destroy enemy
LK.effects.flashObject(hero, 0x00ffea, 200);
e.alive = false;
e.destroy();
enemies.splice(i, 1);
continue;
}
// Lose a life, destroy enemy
lives--;
updateLivesDisplay();
LK.getSound('hit').play();
LK.effects.flashObject(hero, 0xff0000, 400);
e.alive = false;
e.destroy();
enemies.splice(i, 1);
// Game over?
if (lives <= 0) {
finished = true;
LK.showGameOver();
return;
}
continue;
}
// Check collision with hero bullets
for (var j = heroBullets.length - 1; j >= 0; j--) {
var b = heroBullets[j];
if (e.alive && b.intersects(e)) {
// Enemy down
e.alive = false;
e.destroy();
enemies.splice(i, 1);
b.destroy();
heroBullets.splice(j, 1);
score++;
coins++; // Award 1 coin per enemy kill
updateScoreDisplay();
updateCoinDisplay();
LK.getSound('enemyDown').play();
// Lane holding kill detection
if (lastKillLane === currentLane) {
// Player killed enemy in same lane as previous kill, reset timer
laneHoldingTimer = 0;
} else {
// Player switched lanes between kills, start new count
laneHoldingTimer = 0;
lastKillLane = currentLane;
}
// Win if enough enemies killed for this level
if (score >= killTarget) {
// Prepare for next level
level++;
updateKillTarget();
updateLevelDisplay();
// Reset score for next level
score = 0;
updateScoreDisplay();
// Restore full health when level ends
lives = 3;
updateLivesDisplay();
// Increase enemy spawn rate for higher difficulty
enemySpawnInterval = Math.max(20, enemySpawnInterval - 3);
// Flash screen to indicate level complete
LK.effects.flashScreen(0x00ff00, 500);
}
// Chance to drop boost (30%)
if (Math.random() < 0.3) {
spawnBoost(e.x, e.y);
}
break;
}
}
}
// --- Update boosts ---
for (var i = boosts.length - 1; i >= 0; i--) {
var boost = boosts[i];
boost.update();
// Remove if off screen
if (boost.y > 2732 + boost.height) {
boost.destroy();
boosts.splice(i, 1);
continue;
}
// Check collision with hero
if (boost.intersects(hero)) {
if (boost.type === 'speed') {
boostActive = true;
boostTimer = 2000; // 2 seconds
gameSpeed = 2.2;
LK.getSound('boost').play();
LK.effects.flashObject(hero, 0x44e07b, 400);
} else if (boost.type === 'firerate') {
fireRateActive = true;
fireRateTimer = 4000; // 4 seconds
LK.effects.flashObject(hero, 0x00bfff, 400);
} else if (boost.type === 'firestyle') {
fireStyle = Math.min(3, fireStyle + 1); // double, then triple
fireStyleTimer = 4000; // 4 seconds
LK.effects.flashObject(hero, 0xff8800, 400);
} else if (boost.type === 'shield') {
shieldActive = true;
shieldTimer = 4000; // 4 seconds
LK.effects.flashObject(hero, 0x00ffea, 400);
}
boost.destroy();
boosts.splice(i, 1);
}
}
};
// --- Initial GUI update ---
updateLivesDisplay();
updateScoreDisplay();
updateLevelDisplay();
updateBulletDisplay();
updateCoinDisplay(); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Enemy class
var Enemy = Container.expand(function () {
var self = Container.call(this);
// Randomly choose between enemy_1 and enemy_2
var enemyAsset = Math.random() < 0.5 ? 'enemy' : 'Enemy_2';
var enemySprite = self.attachAsset(enemyAsset, {
anchorX: 0.5,
anchorY: 0.5
});
self.width = enemySprite.width;
self.height = enemySprite.height;
self.speed = 5; // Decreased speed for level 1
self.alive = true;
self.lane = 0; // Track which lane this enemy is in (0-4)
self.update = function () {
var effectiveSpeed = self.speed;
if (enemySpeedUpgrade) {
effectiveSpeed *= 0.7; // 30% slower
}
self.y += effectiveSpeed * gameSpeed;
if (self.y > GAMEPLAY_AREA_BOTTOM) {
self.alpha = 0;
} else {
self.alpha = 1;
}
};
return self;
});
// FireRateBoost drop
var FireRateBoost = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('speedBoost', {
anchorX: 0.5,
anchorY: 0.5,
color: 0x00bfff // blue
});
self.width = sprite.width;
self.height = sprite.height;
self.speed = 10;
self.type = 'firerate';
self.update = function () {
self.y += self.speed * gameSpeed;
};
return self;
});
// FireStyleBoost drop
var FireStyleBoost = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('speedBoost', {
anchorX: 0.5,
anchorY: 0.5,
color: 0xff8800 // orange
});
self.width = sprite.width;
self.height = sprite.height;
self.speed = 10;
self.type = 'firestyle';
self.update = function () {
self.y += self.speed * gameSpeed;
};
return self;
});
// Hero class
var Hero = Container.expand(function () {
var self = Container.call(this);
var heroSprite = self.attachAsset('hero', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = heroSprite.width;
self.height = heroSprite.height;
self.shootCooldown = 0; // frames until next allowed shot
// Shoot method
self.shoot = function () {
if (self.shootCooldown > 0 || isReloading || bulletCount <= 0) return;
// Calculate bullets to fire based on fire style
var bulletsToFire = fireStyle;
if (bulletCount < bulletsToFire) {
startReload();
return;
}
// Fire style: 1=single, 2=double, 3=triple
if (fireStyle === 1) {
var bullet = new HeroBullet();
bullet.x = self.x;
bullet.y = self.y - self.height / 2 - bullet.height / 2;
heroBullets.push(bullet);
game.addChild(bullet);
} else if (fireStyle === 2) {
for (var i = -1; i <= 1; i += 2) {
var bullet = new HeroBullet();
bullet.x = self.x + i * 40;
bullet.y = self.y - self.height / 2 - bullet.height / 2;
heroBullets.push(bullet);
game.addChild(bullet);
}
} else if (fireStyle === 3) {
for (var i = -1; i <= 1; i++) {
var bullet = new HeroBullet();
bullet.x = self.x + i * 40;
bullet.y = self.y - self.height / 2 - bullet.height / 2;
heroBullets.push(bullet);
game.addChild(bullet);
}
}
// Consume bullets
bulletCount -= bulletsToFire;
updateBulletDisplay();
// Remove bullet display elements based on bullets fired
for (var k = 0; k < bulletsToFire; k++) {
if (bulletTxt.children.length > 0) {
var bulletElement = bulletTxt.children[bulletTxt.children.length - 1];
bulletTxt.removeChild(bulletElement);
}
}
// Check if we need to reload
if (bulletCount <= 0) {
startReload();
}
if (fireRateActive) {
self.shootCooldown = 4; // much faster
} else {
self.shootCooldown = 20; // slower normal fire rate
}
LK.getSound('shoot').play();
};
// Called every tick
self.update = function () {
if (self.shootCooldown > 0) self.shootCooldown--;
};
return self;
});
// Hero bullet class
var HeroBullet = Container.expand(function () {
var self = Container.call(this);
var bulletSprite = self.attachAsset('heroBullet', {
anchorX: 0.5,
anchorY: 0.5
});
// Apply bullet size upgrade
if (bulletSizeUpgrade) {
bulletSprite.scaleX = 1.5;
bulletSprite.scaleY = 1.5;
}
self.width = bulletSprite.width;
self.height = bulletSprite.height;
self.speed = -36; // Upwards
self.update = function () {
self.y += self.speed;
};
return self;
});
// Market window class
var MarketWindow = Container.expand(function () {
var self = Container.call(this);
// Semi-transparent background - larger market window
var bg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
width: 1800,
height: 1400,
color: 0x000000
});
bg.alpha = 0.8;
self.addChild(bg);
// Market title
var title = new Text2('MARKET', {
size: 80,
fill: 0xffffff
});
title.anchor.set(0.5, 0.5);
title.x = 0;
title.y = -400;
self.addChild(title);
// Bullet size upgrade button - larger for better touch
var bulletUpgradeBtn = LK.getAsset('marketButton', {
anchorX: 0.5,
anchorY: 0.5,
color: 0x00ff00,
scaleX: 2.0,
scaleY: 1.5
});
bulletUpgradeBtn.x = 0;
bulletUpgradeBtn.y = -200;
self.addChild(bulletUpgradeBtn);
var bulletUpgradeText = new Text2('BIGGER BULLETS\n50 Coins', {
size: 40,
fill: 0xffffff
});
bulletUpgradeText.anchor.set(0.5, 0.5);
bulletUpgradeText.x = 0;
bulletUpgradeText.y = -200;
self.addChild(bulletUpgradeText);
// Enemy speed slower upgrade button - larger for better touch
var speedUpgradeBtn = LK.getAsset('marketButton', {
anchorX: 0.5,
anchorY: 0.5,
color: 0xff6600,
scaleX: 2.0,
scaleY: 1.5
});
speedUpgradeBtn.x = 0;
speedUpgradeBtn.y = 0;
self.addChild(speedUpgradeBtn);
var speedUpgradeText = new Text2('SLOWER ENEMIES\n75 Coins', {
size: 40,
fill: 0xffffff
});
speedUpgradeText.anchor.set(0.5, 0.5);
speedUpgradeText.x = 0;
speedUpgradeText.y = 0;
self.addChild(speedUpgradeText);
// Back button - larger for better touch
var backBtn = LK.getAsset('marketButton', {
anchorX: 0.5,
anchorY: 0.5,
color: 0xff0000,
scaleX: 2.0,
scaleY: 1.5
});
backBtn.x = 0;
backBtn.y = 300;
self.addChild(backBtn);
var backText = new Text2('BACK', {
size: 50,
fill: 0xffffff
});
backText.anchor.set(0.5, 0.5);
backText.x = 0;
backText.y = 300;
self.addChild(backText);
// Store button references for hit detection
self.bulletUpgradeBtn = bulletUpgradeBtn;
self.speedUpgradeBtn = speedUpgradeBtn;
self.backBtn = backBtn;
return self;
});
// ShieldBoost drop
var ShieldBoost = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('speedBoost', {
anchorX: 0.5,
anchorY: 0.5,
color: 0x00ffea // cyan
});
self.width = sprite.width;
self.height = sprite.height;
self.speed = 10;
self.type = 'shield';
self.update = function () {
self.y += self.speed * gameSpeed;
};
return self;
});
// Speed boost drop class
var SpeedBoost = Container.expand(function () {
var self = Container.call(this);
var boostSprite = self.attachAsset('speedBoost', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = boostSprite.width;
self.height = boostSprite.height;
self.speed = 10;
self.type = 'speed'; // default type
self.update = function () {
self.y += self.speed * gameSpeed;
};
return self;
});
// Warning asset that appears on idle player
var WarningAsset = Container.expand(function () {
var self = Container.call(this);
var warningSprite = self.attachAsset('laneholding', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = warningSprite.width;
self.height = warningSprite.height;
self.lifeTimer = 2000; // 2 seconds
self.update = function () {
self.lifeTimer -= 1000 / 60; // Decrease by frame time
if (self.lifeTimer <= 0) {
// Check if damage cooldown is active
if (damageCooldown <= 0) {
// Damage player
lives--;
updateLivesDisplay();
LK.getSound('hit').play();
LK.effects.flashObject(hero, 0xff0000, 400);
// Start damage cooldown
damageCooldown = damageCooldownDuration;
// Check game over
if (lives <= 0) {
finished = true;
LK.showGameOver();
return;
}
}
// Remove warning asset regardless of whether damage was dealt
if (warningAsset) {
warningAsset.destroy();
warningAsset = null;
}
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x111111
});
/****
* Game Code
****/
// --- Global variables ---
// Hero (player)
// Hero bullet
// Enemy
// Speed boost drop
// Road (background)
// Sound effects
// Music
// soldier: gray box
// tank: green box
// jungle green
var hero;
var heroBullets = [];
var enemies = [];
var boosts = [];
var lives = 3;
var gameTime = 0; // ms
var gameDuration = 20000; // 20 seconds
var gameSpeed = 1; // Multiplier, affected by boosts
var boostActive = false;
var boostTimer = 0;
// Power-up states
var fireRateActive = false;
var fireRateTimer = 0;
var fireStyle = 1; // 1: single, 2: double, 3: triple
var fireStyleTimer = 0;
var shieldActive = false;
var shieldTimer = 0;
var lastEnemySpawnTick = 0;
var enemySpawnInterval = 48; // frames (0.8s at 60fps)
var score = 0;
var finished = false;
var level = 1;
var killTarget = 10; // Level 1: 10, Level 2: 15, Level 3: 20, etc.
// Lane idle detection
var lastLane = 2; // Track last lane position
var laneIdleTimer = 0; // Timer for staying in same lane
var idleThreshold = 3000; // 3 seconds in milliseconds
var warningAsset = null; // Asset that appears on player
var damageCooldown = 0; // Cooldown timer after taking damage
var damageCooldownDuration = 5000; // 5 seconds cooldown after damage
// Lane holding kill tracking
var laneHoldingTimer = 0; // Timer for consecutive kills in same lane
var laneHoldingThreshold = 3000; // 3 seconds in milliseconds
var lastKillLane = -1; // Track lane of last kill (-1 means no previous kill)
// Coin system
var coins = storage.coins || 0;
// Market system
var gamePaused = false;
var marketWindowVisible = false;
var marketWindow = null;
var marketInactivityTimer = 0;
var marketInactivityTimeout = 5000; // 5 seconds in milliseconds
// Upgrade states
var bulletSizeUpgrade = false;
var enemySpeedUpgrade = false;
// Bullet system
var bulletCount = 5; // Current bullets available
var maxBullets = 5; // Maximum bullets in magazine
var isReloading = false;
var reloadTimer = 0;
var reloadDuration = 2000; // 2 seconds in milliseconds
function updateKillTarget() {
if (level === 1) {
killTarget = 10;
} else if (level === 2) {
killTarget = 15;
} else {
killTarget = 15 + (level - 2) * 5;
}
}
updateKillTarget();
// --- Define gameplay and control areas ---
var CONTROL_AREA_HEIGHT = 400; // px reserved for controller at bottom
var GAMEPLAY_AREA_TOP = 0;
var GAMEPLAY_AREA_BOTTOM = 2732 - CONTROL_AREA_HEIGHT;
// --- Define 5 lanes ---
var LANE_COUNT = 5;
var ROAD_WIDTH = 900;
var LANE_WIDTH = ROAD_WIDTH / LANE_COUNT; // 180px per lane
var ROAD_LEFT = 2048 / 2 - ROAD_WIDTH / 2;
var currentLane = 2; // Start hero in middle lane (0-4)
// Get lane center X position
function getLaneX(laneIndex) {
return ROAD_LEFT + (laneIndex + 0.5) * LANE_WIDTH;
}
// Get lane index from X position
function getLaneFromX(x) {
var relativeX = x - ROAD_LEFT;
var laneIndex = Math.floor(relativeX / LANE_WIDTH);
return Math.max(0, Math.min(LANE_COUNT - 1, laneIndex));
}
// --- Controller area background (army green) ---
var controllerBg = LK.getAsset('centerCircle', {
anchorX: 0,
anchorY: 0,
width: 2048,
height: CONTROL_AREA_HEIGHT,
color: 0x4B5320,
// army green
x: 0,
y: 2732 - CONTROL_AREA_HEIGHT
});
game.addChild(controllerBg);
// --- Road background (restricted to end at controller line) ---
var road = LK.getAsset('road', {
anchorX: 0.5,
anchorY: 0,
width: 900,
height: 2732 - CONTROL_AREA_HEIGHT // End at controller line
});
road.x = 2048 / 2;
road.y = 0;
game.addChild(road);
// --- Hero ---
hero = new Hero();
hero.x = getLaneX(currentLane);
hero.y = GAMEPLAY_AREA_BOTTOM - 150;
game.addChild(hero);
// --- GUI: Lives (Heart Icons) ---
var heartIcons = [];
for (var i = 0; i < 3; i++) {
var heart = LK.getAsset('heart', {
anchorX: 0.5,
anchorY: 0.5
});
heart.x = 150 + i * 100; // Space hearts 100px apart
heart.y = 60;
heartIcons.push(heart);
LK.gui.top.addChild(heart);
}
// --- GUI: Level/Target ---
var levelTxt = new Text2('Level 1', {
size: 90,
fill: 0xFFFFFF
});
levelTxt.anchor.set(0, 0);
LK.gui.topLeft.addChild(levelTxt);
var targetTxt = new Text2('Target: 10', {
size: 90,
fill: 0xFFFFFF
});
targetTxt.anchor.set(0.5, 0);
LK.gui.bottom.addChild(targetTxt);
function updateLevelDisplay() {
levelTxt.setText('Level ' + level);
targetTxt.setText('Target: ' + killTarget);
}
// --- GUI: Score ---
var scoreTxt = new Text2('0', {
size: 90,
fill: 0xFFE066
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// --- GUI: Coins ---
var coinContainer = new Container();
var coinTxt = new Text2(coins, {
size: 50,
fill: 0x000000
});
coinTxt.anchor.set(0.5, 0.5);
var coinIcon = LK.getAsset('coin', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
coinIcon.x = 0;
coinIcon.y = 0;
coinTxt.x = 0;
coinTxt.y = 0;
coinContainer.addChild(coinIcon);
coinContainer.addChild(coinTxt);
coinContainer.x = -100;
coinContainer.y = 120;
LK.gui.topRight.addChild(coinContainer);
// --- GUI: Bullet Count and Reloading ---
var bulletTxt = LK.getAsset('bulletCount', {
anchorX: 1.0,
anchorY: 0
});
LK.gui.topRight.addChild(bulletTxt);
var reloadingTxt = LK.getAsset('reloadingText', {
anchorX: 0.5,
anchorY: 0.5
});
reloadingTxt.alpha = 0;
LK.gui.center.addChild(reloadingTxt);
// --- Music ---
LK.playMusic('bgmusic');
// --- Helper: Update GUI ---
function updateLivesDisplay() {
for (var i = 0; i < heartIcons.length; i++) {
heartIcons[i].alpha = i < lives ? 1 : 0.2; // Show full hearts for remaining lives, dim for lost lives
}
}
function updateTimerDisplay() {
// Timer GUI removed, nothing to update
}
function updateScoreDisplay() {
scoreTxt.setText(score);
}
function updateBulletDisplay() {
// Clear existing bullet display elements
while (bulletTxt.children.length > 0) {
bulletTxt.removeChild(bulletTxt.children[0]);
}
// Add bullet elements based on current bullet count
for (var i = 0; i < bulletCount; i++) {
var bulletElement = LK.getAsset('heroBullet', {
anchorX: 0.5,
anchorY: 0.5
});
bulletElement.x = -30 - i * 25; // Position bullets horizontally
bulletElement.y = 0;
bulletTxt.addChild(bulletElement);
}
}
function updateCoinDisplay() {
coinTxt.setText(coins);
storage.coins = coins; // Save to persistent storage
}
function startReload() {
if (!isReloading && bulletCount < maxBullets) {
isReloading = true;
reloadTimer = reloadDuration;
reloadingTxt.alpha = 1;
tween(reloadingTxt, {
alpha: 0
}, {
duration: reloadDuration,
onFinish: function onFinish() {
bulletCount = maxBullets;
isReloading = false;
updateBulletDisplay();
}
});
}
}
// --- Touch controls: Move hero, shoot ---
var dragHero = false;
var leftArrowBtn, rightArrowBtn, fireBtn;
var leftBtnPressed = false;
var rightBtnPressed = false;
var fireBtnPressed = false;
var controllerBtnSize = 200; // Size for arrow buttons
var fireBtnSize = 250; // Size for fire button
// Move hero to specific lane
function moveHeroToLane(newLane) {
currentLane = Math.max(0, Math.min(LANE_COUNT - 1, newLane));
hero.x = getLaneX(currentLane);
}
// Controller buttons (left arrow, right arrow, fire)
leftArrowBtn = LK.getAsset('leftArrow', {
anchorX: 0.5,
anchorY: 0.5
});
rightArrowBtn = LK.getAsset('rightArrow', {
anchorX: 0.5,
anchorY: 0.5
});
fireBtn = LK.getAsset('fireButton', {
anchorX: 0.5,
anchorY: 0.5
});
leftArrowBtn.alpha = 1.0;
rightArrowBtn.alpha = 1.0;
fireBtn.alpha = 1.0;
// Place buttons side-by-side in the control area with better left-to-right spacing
leftArrowBtn.x = 300;
leftArrowBtn.y = 2732 - CONTROL_AREA_HEIGHT / 2;
rightArrowBtn.x = 600;
rightArrowBtn.y = 2732 - CONTROL_AREA_HEIGHT / 2;
fireBtn.x = 1600;
fireBtn.y = 2732 - CONTROL_AREA_HEIGHT / 2;
// Make sure controllerBg is below buttons (already added above)
// Add controller buttons to game layer to make them visible
game.addChild(leftArrowBtn);
game.addChild(rightArrowBtn);
game.addChild(fireBtn);
// Market button
var marketBtn = LK.getAsset('marketButton', {
anchorX: 0.5,
anchorY: 0.5
});
marketBtn.x = 1000;
marketBtn.y = 2732 - CONTROL_AREA_HEIGHT / 2;
marketBtn.alpha = 0.8;
game.addChild(marketBtn);
// Market text label
var marketLabel = new Text2('MARKET', {
size: 30,
fill: 0xffffff
});
marketLabel.anchor.set(0.5, 0.5);
marketLabel.x = marketBtn.x;
marketLabel.y = marketBtn.y;
game.addChild(marketLabel);
// Make buttons more visible with higher alpha
leftArrowBtn.alpha = 0.8;
rightArrowBtn.alpha = 0.8;
fireBtn.alpha = 0.8;
// Helper: check if point is inside button
function isInsideBtn(btn, x, y) {
// Account for button scaling and provide generous touch areas
var baseWidth = btn.width || 150;
var baseHeight = btn.height || 80;
var scaleX = btn.scaleX || 1;
var scaleY = btn.scaleY || 1;
var touchWidth = baseWidth * scaleX * 1.2; // 20% extra touch area
var touchHeight = baseHeight * scaleY * 1.2; // 20% extra touch area
return x >= btn.x - touchWidth / 2 && x <= btn.x + touchWidth / 2 && y >= btn.y - touchHeight / 2 && y <= btn.y + touchHeight / 2;
}
function showMarket() {
if (!marketWindowVisible) {
gamePaused = true;
marketWindowVisible = true;
marketInactivityTimer = 0; // Reset timer when market opens
// Create and show market window
marketWindow = new MarketWindow();
marketWindow.x = 2048 / 2;
marketWindow.y = 2732 / 2;
game.addChild(marketWindow);
}
}
function hideMarket() {
if (marketWindowVisible) {
gamePaused = false;
marketWindowVisible = false;
// Remove market window
if (marketWindow) {
marketWindow.destroy();
marketWindow = null;
}
}
}
game.down = function (x, y, obj) {
// If touch is on left arrow, set pressed
if (isInsideBtn(leftArrowBtn, x, y)) {
leftBtnPressed = true;
leftArrowBtn.alpha = 0.9;
return;
}
// If touch is on right arrow, set pressed
if (isInsideBtn(rightArrowBtn, x, y)) {
rightBtnPressed = true;
rightArrowBtn.alpha = 0.9;
return;
}
// If touch is on fire button, set pressed and shoot
if (isInsideBtn(fireBtn, x, y)) {
fireBtnPressed = true;
fireBtn.alpha = 0.9;
hero.shoot();
return;
}
// If touch is on market button, show market
if (isInsideBtn(marketBtn, x, y)) {
marketBtn.alpha = 0.9;
showMarket();
return;
}
// Handle market window interactions
if (marketWindowVisible && marketWindow) {
// Reset inactivity timer on any interaction with market window
marketInactivityTimer = 0;
// Convert to market window local coordinates
var localPos = marketWindow.toLocal({
x: x,
y: y
});
// Check bullet upgrade button - use global coordinates
var bulletBtnGlobalPos = marketWindow.toGlobal(marketWindow.bulletUpgradeBtn.position);
if (isInsideBtn({
x: bulletBtnGlobalPos.x,
y: bulletBtnGlobalPos.y,
width: marketWindow.bulletUpgradeBtn.width,
height: marketWindow.bulletUpgradeBtn.height
}, x, y)) {
if (coins >= 50 && !bulletSizeUpgrade) {
coins -= 50;
bulletSizeUpgrade = true;
updateCoinDisplay();
LK.effects.flashScreen(0x00ff00, 300);
}
return;
}
// Check enemy speed upgrade button - use global coordinates
var speedBtnGlobalPos = marketWindow.toGlobal(marketWindow.speedUpgradeBtn.position);
if (isInsideBtn({
x: speedBtnGlobalPos.x,
y: speedBtnGlobalPos.y,
width: marketWindow.speedUpgradeBtn.width,
height: marketWindow.speedUpgradeBtn.height
}, x, y)) {
if (coins >= 75 && !enemySpeedUpgrade) {
coins -= 75;
enemySpeedUpgrade = true;
updateCoinDisplay();
LK.effects.flashScreen(0x00ff00, 300);
}
return;
}
// Check back button - use global coordinates
var backBtnGlobalPos = marketWindow.toGlobal(marketWindow.backBtn.position);
if (isInsideBtn({
x: backBtnGlobalPos.x,
y: backBtnGlobalPos.y,
width: marketWindow.backBtn.width,
height: marketWindow.backBtn.height
}, x, y)) {
hideMarket();
return;
}
}
};
game.move = function (x, y, obj) {
// Drag control disabled
// If finger moves off controller, release
if (!isInsideBtn(leftArrowBtn, x, y)) {
leftBtnPressed = false;
leftArrowBtn.alpha = 0.8;
}
if (!isInsideBtn(rightArrowBtn, x, y)) {
rightBtnPressed = false;
rightArrowBtn.alpha = 0.8;
}
if (!isInsideBtn(fireBtn, x, y)) {
fireBtnPressed = false;
fireBtn.alpha = 0.8;
}
if (!isInsideBtn(marketBtn, x, y)) {
marketBtn.alpha = 0.8;
}
};
game.up = function (x, y, obj) {
leftBtnPressed = false;
rightBtnPressed = false;
fireBtnPressed = false;
leftArrowBtn.alpha = 0.8;
rightArrowBtn.alpha = 0.8;
fireBtn.alpha = 0.8;
marketBtn.alpha = 0.8;
};
// --- Enemy spawn logic ---
function spawnEnemy() {
// Enemies spawn randomly in one of the 5 lanes
var enemy = new Enemy();
var enemyLane = Math.floor(Math.random() * LANE_COUNT);
enemy.x = getLaneX(enemyLane);
enemy.lane = enemyLane; // Set the enemy's lane
// Spawn at the top of the gameplay area, not above the road
enemy.y = GAMEPLAY_AREA_TOP - enemy.height / 2 - 10;
// Randomize speed a bit
enemy.speed = 6 + Math.random() * 3;
enemies.push(enemy);
game.addChild(enemy);
}
// --- Boost logic ---
function spawnBoost(x, y) {
// Randomly choose power-up type
var r = Math.random();
var boost;
if (r < 0.25) {
boost = new SpeedBoost();
} else if (r < 0.5) {
boost = new FireRateBoost();
} else if (r < 0.75) {
boost = new FireStyleBoost();
} else {
boost = new ShieldBoost();
}
boost.x = x;
boost.y = y;
boosts.push(boost);
game.addChild(boost);
}
// --- Game update ---
game.update = function () {
// Handle market inactivity timer
if (marketWindowVisible) {
marketInactivityTimer += 1000 / 60; // Increase by frame time
if (marketInactivityTimer >= marketInactivityTimeout) {
// Auto-close market after 5 seconds of inactivity
tween(marketWindow, {
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
hideMarket();
}
});
marketInactivityTimer = 0; // Reset timer to prevent multiple triggers
}
return; // Don't continue with game updates when paused
}
if (finished) return;
// --- Controller movement ---
if (leftBtnPressed) {
moveHeroToLane(currentLane - 1);
leftBtnPressed = false; // Single lane move per press
leftArrowBtn.alpha = 0.8;
}
if (rightBtnPressed) {
moveHeroToLane(currentLane + 1);
rightBtnPressed = false; // Single lane move per press
rightArrowBtn.alpha = 0.8;
}
// --- Update damage cooldown ---
if (damageCooldown > 0) {
damageCooldown -= 1000 / 60; // Decrease by frame time
}
// --- Lane idle detection ---
if (currentLane === lastLane) {
// Only increase idle timer if not in damage cooldown
if (damageCooldown <= 0) {
laneIdleTimer += 1000 / 60; // Increase by frame time (milliseconds)
}
// If player has been idle for 3 seconds and no warning exists and not in cooldown
if (laneIdleTimer >= idleThreshold && !warningAsset && damageCooldown <= 0) {
warningAsset = new WarningAsset();
warningAsset.x = hero.x;
warningAsset.y = hero.y - 100; // Position above hero
game.addChild(warningAsset);
}
} else {
// Player moved to different lane, reset timer
laneIdleTimer = 0;
lastLane = currentLane;
// Remove warning if it exists
if (warningAsset) {
warningAsset.destroy();
warningAsset = null;
}
}
// --- Lane holding kill timer ---
if (lastKillLane === currentLane && lastKillLane !== -1) {
// Player is holding in same lane where they made their last kill
laneHoldingTimer += 1000 / 60; // Increase by frame time (milliseconds)
// If player has been holding for 3 seconds, reset timer and start count again
if (laneHoldingTimer >= laneHoldingThreshold) {
laneHoldingTimer = 0; // Reset timer to 0 and start count again
}
} else if (currentLane !== lastKillLane && lastKillLane !== -1) {
// Player moved away from kill lane, reset holding timer
laneHoldingTimer = 0;
}
// --- Update warning asset ---
if (warningAsset) {
warningAsset.update();
// Keep warning positioned above hero
warningAsset.x = hero.x;
warningAsset.y = hero.y - 100;
}
// --- Timer ---
// (Timer removed, no time-based win/lose condition)
updateTimerDisplay();
// --- Win condition ---
// (Handled by kill target below)
// --- Boost logic ---
if (boostActive) {
boostTimer -= 1000 / 60;
if (boostTimer <= 0) {
boostActive = false;
gameSpeed = 1;
}
}
if (fireRateActive) {
fireRateTimer -= 1000 / 60;
if (fireRateTimer <= 0) {
fireRateActive = false;
}
}
if (fireStyle > 1) {
fireStyleTimer -= 1000 / 60;
if (fireStyleTimer <= 0) {
fireStyle = 1;
}
}
if (shieldActive) {
shieldTimer -= 1000 / 60;
if (shieldTimer <= 0) {
shieldActive = false;
}
}
// --- Enemy spawn ---
if (LK.ticks - lastEnemySpawnTick >= enemySpawnInterval) {
spawnEnemy();
lastEnemySpawnTick = LK.ticks;
}
// --- Update hero ---
hero.update();
// --- Update hero bullets ---
for (var i = heroBullets.length - 1; i >= 0; i--) {
var b = heroBullets[i];
b.update();
// Remove if off screen
if (b.y < -b.height) {
b.destroy();
heroBullets.splice(i, 1);
}
}
// --- Update enemies ---
for (var i = enemies.length - 1; i >= 0; i--) {
var e = enemies[i];
e.update();
// Remove if off screen
if (e.y > 2732 + e.height) {
e.destroy();
enemies.splice(i, 1);
continue;
}
// Check collision with hero - only if enemy is in same lane
if (e.alive && e.lane === currentLane && e.intersects(hero)) {
if (shieldActive) {
// Ignore hit, just destroy enemy
LK.effects.flashObject(hero, 0x00ffea, 200);
e.alive = false;
e.destroy();
enemies.splice(i, 1);
continue;
}
// Lose a life, destroy enemy
lives--;
updateLivesDisplay();
LK.getSound('hit').play();
LK.effects.flashObject(hero, 0xff0000, 400);
e.alive = false;
e.destroy();
enemies.splice(i, 1);
// Game over?
if (lives <= 0) {
finished = true;
LK.showGameOver();
return;
}
continue;
}
// Check collision with hero bullets
for (var j = heroBullets.length - 1; j >= 0; j--) {
var b = heroBullets[j];
if (e.alive && b.intersects(e)) {
// Enemy down
e.alive = false;
e.destroy();
enemies.splice(i, 1);
b.destroy();
heroBullets.splice(j, 1);
score++;
coins++; // Award 1 coin per enemy kill
updateScoreDisplay();
updateCoinDisplay();
LK.getSound('enemyDown').play();
// Lane holding kill detection
if (lastKillLane === currentLane) {
// Player killed enemy in same lane as previous kill, reset timer
laneHoldingTimer = 0;
} else {
// Player switched lanes between kills, start new count
laneHoldingTimer = 0;
lastKillLane = currentLane;
}
// Win if enough enemies killed for this level
if (score >= killTarget) {
// Prepare for next level
level++;
updateKillTarget();
updateLevelDisplay();
// Reset score for next level
score = 0;
updateScoreDisplay();
// Restore full health when level ends
lives = 3;
updateLivesDisplay();
// Increase enemy spawn rate for higher difficulty
enemySpawnInterval = Math.max(20, enemySpawnInterval - 3);
// Flash screen to indicate level complete
LK.effects.flashScreen(0x00ff00, 500);
}
// Chance to drop boost (30%)
if (Math.random() < 0.3) {
spawnBoost(e.x, e.y);
}
break;
}
}
}
// --- Update boosts ---
for (var i = boosts.length - 1; i >= 0; i--) {
var boost = boosts[i];
boost.update();
// Remove if off screen
if (boost.y > 2732 + boost.height) {
boost.destroy();
boosts.splice(i, 1);
continue;
}
// Check collision with hero
if (boost.intersects(hero)) {
if (boost.type === 'speed') {
boostActive = true;
boostTimer = 2000; // 2 seconds
gameSpeed = 2.2;
LK.getSound('boost').play();
LK.effects.flashObject(hero, 0x44e07b, 400);
} else if (boost.type === 'firerate') {
fireRateActive = true;
fireRateTimer = 4000; // 4 seconds
LK.effects.flashObject(hero, 0x00bfff, 400);
} else if (boost.type === 'firestyle') {
fireStyle = Math.min(3, fireStyle + 1); // double, then triple
fireStyleTimer = 4000; // 4 seconds
LK.effects.flashObject(hero, 0xff8800, 400);
} else if (boost.type === 'shield') {
shieldActive = true;
shieldTimer = 4000; // 4 seconds
LK.effects.flashObject(hero, 0x00ffea, 400);
}
boost.destroy();
boosts.splice(i, 1);
}
}
};
// --- Initial GUI update ---
updateLivesDisplay();
updateScoreDisplay();
updateLevelDisplay();
updateBulletDisplay();
updateCoinDisplay();
Let's remove background and resize it bigger
Make it view from sky and change color of rifle to black and brown
Change towards to right
change it mecha-style heart for hero lives. In-Game asset. 2d. High contrast. No shadows. mechaart
fire button for tank game controller. Fire button in mecha style. In-Game asset. 2d. High contrast. No shadows
green line with army style. In-Game asset. 2d. High contrast. No shadows
make shorter horizontal wing
make it vertical
remove dollar emblem from it
exclamantation. In-Game asset. 2d. High contrast. No shadows
black market which sells weapon. In-Game asset. 2d. High contrast. No shadows
blur brown