/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { currentLevel: 1, highestLevel: 1 }); /**** * Classes ****/ var GameObject = Container.expand(function (id, width, height) { var self = Container.call(this); self.id = id; self.width = width; self.height = height; self.isDestroyed = false; self.destroy = function () { self.isDestroyed = true; Container.prototype.destroy.call(self); }; return self; }); var WaterPickup = GameObject.expand(function (x, y) { var self = GameObject.call(this, 'waterPickup', 60, 60); var graphics = self.attachAsset('water', { anchorX: 0.5, anchorY: 0.5, scaleX: 3, scaleY: 3 }); self.x = x; self.y = y; self.waterAmount = 75; // Increased water amount // Make water pickup pulse to draw attention function pulseAnimation() { tween(graphics.scale, { x: 3.3, y: 3.3 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { tween(graphics.scale, { x: 3.0, y: 3.0 }, { duration: 800, easing: tween.easeInOut, onFinish: pulseAnimation }); } }); } pulseAnimation(); return self; }); var WaterParticle = GameObject.expand(function (x, y) { var self = GameObject.call(this, 'water', 20, 20); var graphics = self.attachAsset('water', { anchorX: 0.5, anchorY: 0.5 }); self.x = x; self.y = y; self.lifespan = 30; // frames self.velocity = { x: Math.random() * 6 - 3, y: -8 - Math.random() * 3 }; self.update = function () { self.x += self.velocity.x; self.y += self.velocity.y; self.lifespan--; // Apply gravity self.velocity.y += 0.3; // Fade out graphics.alpha = self.lifespan / 30; // Check collision with fires for (var i = fires.length - 1; i >= 0; i--) { var fire = fires[i]; if (self.intersects(fire)) { fire.intensity -= 5; if (fire.intensity <= 0) { LK.getSound('extinguish').play(); LK.setScore(LK.getScore() + 50); updateScoreText(); fires.splice(i, 1); fire.destroy(); } self.lifespan = 0; break; } } if (self.lifespan <= 0) { self.destroy(); for (var i = 0; i < waterParticles.length; i++) { if (waterParticles[i] === self) { waterParticles.splice(i, 1); break; } } } }; return self; }); var Wall = GameObject.expand(function (x, y, width, height) { var self = GameObject.call(this, 'wall', width, height); var graphics = self.attachAsset('wall', { anchorX: 0.5, anchorY: 0.5, width: width, height: height }); self.x = x; self.y = y; return self; }); var Smoke = GameObject.expand(function (x, y) { var self = GameObject.call(this, 'smoke', 200, 200); var graphics = self.attachAsset('smoke', { anchorX: 0.5, anchorY: 0.5, alpha: 0.3 }); self.x = x; self.y = y; self.lifespan = 120 + Math.floor(Math.random() * 60); // 2-3 seconds var scale = 0.3 + Math.random() * 0.3; graphics.scale.set(scale, scale); self.update = function () { self.y -= 0.5; self.x += Math.random() * 0.6 - 0.3; self.lifespan--; // Grow and fade var lifePercent = self.lifespan / 180; graphics.scale.set(scale * (2 - lifePercent), scale * (2 - lifePercent)); graphics.alpha = 0.3 * lifePercent; if (self.lifespan <= 0) { self.destroy(); for (var i = 0; i < smokeParticles.length; i++) { if (smokeParticles[i] === self) { smokeParticles.splice(i, 1); break; } } } }; return self; }); var Firefighter = GameObject.expand(function () { var self = GameObject.call(this, 'firefighter', 100, 150); var graphics = self.attachAsset('firefighter', { anchorX: 0.5, anchorY: 0.5 }); // Add health bar var healthBarBg = self.attachAsset('timeBarBg', { anchorX: 0.5, anchorY: 0, width: 100, height: 10, y: -90 }); var healthBarFill = self.attachAsset('timeBar', { anchorX: 0.5, anchorY: 0, width: 100, height: 10, y: -90, tint: 0x00ff00 }); self.speed = 5; self.waterLevel = 100; self.maxWaterLevel = 100; self.waterUseRate = 0.5; self.waterRefillRate = 1.5; // was 3, now slower refill self.health = 100; self.maxHealth = 100; self.damageTimer = 0; self.damageDelay = 30; // 0.5 seconds at 60fps self.isRefilling = false; self.isSprayingWater = false; self.carryingCivilian = null; self.direction = { x: 0, y: 0 }; self.update = function () { // Handle movement if (self.direction.x !== 0 || self.direction.y !== 0) { var newX = self.x + self.direction.x * self.speed; var newY = self.y + self.direction.y * self.speed; // Check for collisions with walls var wouldCollide = false; for (var i = 0; i < walls.length; i++) { var wall = walls[i]; if (willCollide(newX, newY, self.width, self.height, wall.x, wall.y, wall.width, wall.height)) { wouldCollide = true; break; } } if (!wouldCollide) { self.x = newX; self.y = newY; // Keep within game bounds self.x = Math.max(self.width / 2, Math.min(self.x, 2048 - self.width / 2)); self.y = Math.max(self.height / 2, Math.min(self.y, 2732 - self.height / 2)); // If carrying a civilian, update their position if (self.carryingCivilian) { self.carryingCivilian.x = self.x; self.carryingCivilian.y = self.y - 80; } } } // Check for water refill at fire station if (fireStation && self.intersects(fireStation)) { self.isRefilling = true; if (self.waterLevel < self.maxWaterLevel) { self.waterLevel = Math.min(self.maxWaterLevel, self.waterLevel + self.waterRefillRate); updateWaterBar(); } // Also heal when at fire station if (self.health < self.maxHealth) { self.health = Math.min(self.maxHealth, self.health + 0.2); // was 0.5, now slower recovery healthBarFill.width = self.health / self.maxHealth * 100; // Update color based on health if (self.health < 30) { healthBarFill.tint = 0xff0000; // Red } else if (self.health < 60) { healthBarFill.tint = 0xffff00; // Yellow } else { healthBarFill.tint = 0x00ff00; // Green } } } else { self.isRefilling = false; } // Check for water pickup collision for (var i = waterPickups.length - 1; i >= 0; i--) { if (self.intersects(waterPickups[i])) { // Refill water self.waterLevel = Math.min(self.maxWaterLevel, self.waterLevel + waterPickups[i].waterAmount); updateWaterBar(); // Add score LK.setScore(LK.getScore() + 25); updateScoreText(); // Show pickup effect LK.effects.flashObject(self, 0x0000ff, 500); // Create water particle burst effect for (var j = 0; j < 8; j++) { var angle = j * Math.PI / 4; var particleX = self.x + Math.cos(angle) * 50; var particleY = self.y + Math.sin(angle) * 50; var particle = new WaterParticle(particleX, particleY); particle.velocity.x = Math.cos(angle) * 4; particle.velocity.y = Math.sin(angle) * 4; game.addChild(particle); waterParticles.push(particle); } // Play sound LK.getSound('spray').play(); // Remove pickup waterPickups[i].destroy(); waterPickups.splice(i, 1); } } // Handle water spraying if (self.isSprayingWater && self.waterLevel > 0) { self.waterLevel = Math.max(0, self.waterLevel - self.waterUseRate); updateWaterBar(); // Create water particle createWaterParticle(self.x, self.y); } // Check for fire collisions and take damage if (self.damageTimer <= 0) { for (var i = 0; i < fires.length; i++) { if (self.intersects(fires[i])) { self.health -= 10; self.damageTimer = self.damageDelay; // Update health bar healthBarFill.width = self.health / self.maxHealth * 100; // Change color based on health if (self.health < 30) { healthBarFill.tint = 0xff0000; // Red } else if (self.health < 60) { healthBarFill.tint = 0xffff00; // Yellow } // Visual feedback LK.effects.flashObject(self, 0xff0000, 300); // Game over if health depleted if (self.health <= 0 && !self.isDestroyed) { self.isDestroyed = true; LK.effects.flashScreen(0xff0000, 500); LK.setTimeout(function () { LK.showGameOver(); }, 500); } break; } } } else { self.damageTimer--; } // Check exit collision when carrying civilian if (self.carryingCivilian && exit && self.intersects(exit)) { LK.getSound('rescue').play(); civiliansRescued++; self.carryingCivilian.isRescued = true; self.carryingCivilian = null; // Update the score LK.setScore(LK.getScore() + 100); updateScoreText(); // Check if all civilians are rescued checkLevelComplete(); } }; return self; }); var FireStation = GameObject.expand(function (x, y) { var self = GameObject.call(this, 'fireStation', 800, 600); var graphics = self.attachAsset('fireStation', { anchorX: 0.5, anchorY: 0.5 }); self.x = x; self.y = y; return self; }); var Fire = GameObject.expand(function (x, y) { var self = GameObject.call(this, 'fire', 60, 80); var graphics = self.attachAsset('fire', { anchorX: 0.5, anchorY: 0.5 }); self.x = x; self.y = y; self.intensity = 100; // 0-100 self.spreadTimer = 0; self.spreadRate = 3; // seconds (was 5, now faster) // Randomly vary the size var scale = 0.8 + Math.random() * 0.4; graphics.scale.set(scale, scale); self.update = function () { if (self.intensity <= 0) { self.destroy(); return; } // Animate the fire if (LK.ticks % 5 === 0) { var scaleVar = Math.random() * 0.2 - 0.1; graphics.scale.set(scale + scaleVar, scale + scaleVar); } // Attempt to spread fire self.spreadTimer++; if (self.spreadTimer > self.spreadRate * 60) { // Convert seconds to frames at 60fps self.spreadTimer = 0; if (Math.random() < 0.5 && fires.length < maxFires) { // was 0.3, now 0.5 (more likely to spread) var angle = Math.random() * Math.PI * 2; var distance = 100 + Math.random() * 100; var newX = self.x + Math.cos(angle) * distance; var newY = self.y + Math.sin(angle) * distance; // Keep within game bounds newX = Math.max(30, Math.min(newX, 2048 - 30)); newY = Math.max(30, Math.min(newY, 2732 - 30)); // Check if too close to existing fire or wall var tooClose = false; // Check walls for (var i = 0; i < walls.length; i++) { if (willCollide(newX, newY, 60, 80, walls[i].x, walls[i].y, walls[i].width, walls[i].height)) { tooClose = true; break; } } if (!tooClose) { createFire(newX, newY); LK.getSound('burn').play(); } } } }; return self; }); var Exit = GameObject.expand(function (x, y) { var self = GameObject.call(this, 'exit', 150, 100); var graphics = self.attachAsset('exit', { anchorX: 0.5, anchorY: 0.5 }); self.x = x; self.y = y; // Make exit pulse to draw attention function pulseAnimation() { tween(graphics.scale, { x: 1.3, y: 1.3 }, { duration: 800, easing: tween.easeInOut, onFinish: function onFinish() { tween(graphics.scale, { x: 1.2, y: 1.2 }, { duration: 800, easing: tween.easeInOut, onFinish: pulseAnimation }); } }); } pulseAnimation(); return self; }); var Civilian = GameObject.expand(function (x, y) { var self = GameObject.call(this, 'civilian', 80, 120); var graphics = self.attachAsset('civilian', { anchorX: 0.5, anchorY: 0.5 }); self.x = x; self.y = y; self.isRescued = false; self.healthTimer = 0; self.maxHealthTimer = 5 * 60; // 5 seconds at 60fps self.update = function () { // Check if close to fire var nearFire = false; for (var i = 0; i < fires.length; i++) { var fire = fires[i]; var dx = self.x - fire.x; var dy = self.y - fire.y; var distance = Math.sqrt(dx * dx + dy * dy); if (distance < 150) { nearFire = true; break; } } if (nearFire) { // Start losing health when near fire self.healthTimer++; if (self.healthTimer >= self.maxHealthTimer && !self.isRescued && !self.isDestroyed) { self.isDestroyed = true; LK.effects.flashScreen(0xff0000, 500); LK.setTimeout(function () { LK.showGameOver(); }, 500); } } else { // Recover when away from fire self.healthTimer = Math.max(0, self.healthTimer - 1); } // Visual feedback on health var healthPercent = 1 - self.healthTimer / self.maxHealthTimer; graphics.alpha = 0.5 + healthPercent * 0.5; // Animate when in danger if (nearFire && LK.ticks % 10 === 0) { tween(graphics, { rotation: Math.random() * 0.2 - 0.1 }, { duration: 300 }); } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x333333 }); /**** * Game Code ****/ // Game state variables var level = storage.currentLevel || 1; var firefighter; var fires = []; var civilians = []; var walls = []; var waterParticles = []; var smokeParticles = []; var waterPickups = []; var waterPickupTimer; var fireStation; var exit; var maxFires; var civiliansRescued = 0; var totalCivilians = 0; var gameTime = 90; // seconds var gameTimer; var controlPad; var actionButton; var timeBarFill; var waterBarFill; var joystickActive = false; var joystickOrigin = { x: 0, y: 0 }; var joystickPosition = { x: 0, y: 0 }; // Interface elements var scoreTxt; var levelTxt; var timeBarContainer; var timeBarBg; var waterBarContainer; var waterBarBg; var instructionsContainer; var instructionsTxt; // Initialize game function initGame() { // Reset game state LK.setScore(0); civiliansRescued = 0; // Clear previous game objects cleanupLevel(); // Set up the level setupLevel(level); // Create UI createUI(); // Play background music LK.playMusic('gameplay', { loop: true, fade: { start: 0, end: 0.4, duration: 1000 } }); // Start game timer startGameTimer(); // Show initial instructions showInstructions(); // Start water pickup spawning if (waterPickupTimer) { LK.clearInterval(waterPickupTimer); } // Create multiple initial water pickups based on level var initialPickups = 2 + Math.floor(level / 2); for (var i = 0; i < initialPickups; i++) { createWaterPickup(); } // Spawn new water pickups periodically waterPickupTimer = LK.setInterval(function () { // Limit the number of pickups based on level if (waterPickups.length < 3 + Math.floor(level)) { createWaterPickup(); } }, 25000); // New water pickup every 25 seconds (was 15s, now less frequent) } function cleanupLevel() { // Clear all game objects if (firefighter) { firefighter.destroy(); } for (var i = 0; i < fires.length; i++) { fires[i].destroy(); } fires = []; for (var i = 0; i < civilians.length; i++) { civilians[i].destroy(); } civilians = []; for (var i = 0; i < walls.length; i++) { walls[i].destroy(); } walls = []; for (var i = 0; i < waterParticles.length; i++) { waterParticles[i].destroy(); } waterParticles = []; for (var i = 0; i < smokeParticles.length; i++) { smokeParticles[i].destroy(); } smokeParticles = []; for (var i = 0; i < waterPickups.length; i++) { waterPickups[i].destroy(); } waterPickups = []; if (waterPickupTimer) { LK.clearInterval(waterPickupTimer); waterPickupTimer = null; } if (fireStation) { fireStation.destroy(); fireStation = null; } if (exit) { exit.destroy(); exit = null; } if (gameTimer) { LK.clearInterval(gameTimer); gameTimer = null; } } function setupLevel(level) { // Define distance function first so it can be used in this method function distance(x1, y1, x2, y2) { var dx = x1 - x2; var dy = y1 - y2; return Math.sqrt(dx * dx + dy * dy); } // Level configuration var levelConfig = { 1: { layout: 'basic', civilians: 2, initialFires: 3, maxFires: 6, time: 90 }, 2: { layout: 'apartment', civilians: 3, initialFires: 4, maxFires: 8, time: 120 }, 3: { layout: 'office', civilians: 4, initialFires: 5, maxFires: 10, time: 180 } }; // Use level 3 config for any level beyond 3 var config = levelConfig[level] || levelConfig[3]; config.initialFires += Math.floor((level - 3) / 2); // Increase fires for levels beyond 3 config.civilians += Math.floor((level - 3) / 2); // Increase civilians for levels beyond 3 config.time = Math.min(300, config.time + (level - 3) * 30); // Increase time (max 5 minutes) maxFires = config.maxFires; totalCivilians = config.civilians; gameTime = config.time; // Create level layout createLayout(config.layout); // Create initial fires for (var i = 0; i < config.initialFires; i++) { var fireX, fireY; var validPosition = false; // Find a valid position for the fire while (!validPosition) { fireX = 300 + Math.random() * (2048 - 600); fireY = 300 + Math.random() * (2732 - 600); // Check if too close to firefighter, fire station, or exit var dx1 = fireX - 200; var dy1 = fireY - 2500; var tooCloseToFirefighter = Math.sqrt(dx1 * dx1 + dy1 * dy1) < 300; var tooCloseToFireStation = fireStation && Math.sqrt(Math.pow(fireX - fireStation.x, 2) + Math.pow(fireY - fireStation.y, 2)) < 400; var tooCloseToExit = exit && Math.sqrt(Math.pow(fireX - exit.x, 2) + Math.pow(fireY - exit.y, 2)) < 400; // Check if inside any wall var insideWall = false; for (var j = 0; j < walls.length; j++) { if (willCollide(fireX, fireY, 60, 80, walls[j].x, walls[j].y, walls[j].width, walls[j].height)) { insideWall = true; break; } } validPosition = !tooCloseToFirefighter && !tooCloseToFireStation && !tooCloseToExit && !insideWall; } createFire(fireX, fireY); } // Create civilians for (var i = 0; i < config.civilians; i++) { var civX, civY; var validPosition = false; while (!validPosition) { civX = 300 + Math.random() * (2048 - 600); civY = 300 + Math.random() * (2732 - 600); // Not too close to fire station or exit var tooCloseToFireStation = fireStation && distance(civX, civY, fireStation.x, fireStation.y) < 300; var tooCloseToExit = exit && distance(civX, civY, exit.x, exit.y) < 300; // Check if inside any wall var insideWall = false; for (var j = 0; j < walls.length; j++) { if (willCollide(civX, civY, 80, 120, walls[j].x, walls[j].y, walls[j].width, walls[j].height)) { insideWall = true; break; } } // Must be somewhat close to at least one fire var nearFire = false; for (var j = 0; j < fires.length; j++) { if (distance(civX, civY, fires[j].x, fires[j].y) < 500) { nearFire = true; break; } } validPosition = !tooCloseToFireStation && !tooCloseToExit && !insideWall && nearFire; } createCivilian(civX, civY); } // Create firefighter firefighter = new Firefighter(); firefighter.x = 200; firefighter.y = 2500; game.addChild(firefighter); } function createLayout(layoutType) { // Create fire station fireStation = new FireStation(200, 2500); game.addChild(fireStation); // Create exit exit = new Exit(1800, 200); game.addChild(exit); // Create walls based on layout type switch (layoutType) { case 'basic': // Simple wall layout createWall(1024, 1366, 800, 50); // Center horizontal wall createWall(600, 1000, 600, 50); // Left horizontal wall createWall(1400, 1700, 600, 50); // Right horizontal wall break; case 'apartment': // Apartment-like layout createWall(300, 800, 600, 50); // Upper left horizontal createWall(1300, 800, 500, 50); // Upper right horizontal createWall(500, 1400, 600, 50); // Lower left horizontal createWall(1300, 1400, 600, 50); // Lower right horizontal createWall(800, 800, 40, 600); // Center vertical createWall(1100, 1800, 40, 800); // Lower center vertical break; case 'office': // Office building layout createWall(500, 600, 1000, 50); // Top horizontal createWall(800, 1000, 600, 50); // Middle horizontal 1 createWall(1300, 1400, 400, 50); // Middle horizontal 2 createWall(1000, 1800, 1000, 50); // Bottom horizontal createWall(500, 600, 40, 800); // Left vertical createWall(1500, 600, 40, 800); // Right vertical createWall(1000, 1400, 40, 400); // Bottom vertical break; default: // Random maze-like layout var wallCount = 6 + Math.floor(Math.random() * 6); // 6-12 walls for (var i = 0; i < wallCount; i++) { var horizontal = Math.random() > 0.5; var x, y, width, height; if (horizontal) { width = 400 + Math.random() * 800; height = 40; x = 200 + Math.random() * (2048 - 400 - width); y = 400 + Math.random() * (2732 - 800); } else { width = 40; height = 400 + Math.random() * 800; x = 400 + Math.random() * (2048 - 800); y = 200 + Math.random() * (2732 - 400 - height); } // Don't block firefighter start or exit var blocksFirefighter = willCollide(x, y, width, height, 200, 2500, 200, 200); var blocksExit = willCollide(x, y, width, height, 1800, 200, 150, 100); if (!blocksFirefighter && !blocksExit) { createWall(x, y, width, height); } } break; } } function createUI() { // Score text scoreTxt = new Text2('Score: 0', { size: 60, fill: 0xFFFFFF }); scoreTxt.anchor.set(1, 0); LK.gui.topRight.addChild(scoreTxt); // Level text levelTxt = new Text2('Level: ' + level, { size: 60, fill: 0xFFFFFF }); levelTxt.anchor.set(0, 0); LK.gui.top.addChild(levelTxt); // Time bar timeBarContainer = new Container(); timeBarBg = timeBarContainer.attachAsset('timeBarBg', { anchorX: 0, anchorY: 0.5 }); timeBarFill = timeBarContainer.attachAsset('timeBar', { anchorX: 0, anchorY: 0.5 }); timeBarFill.width = 400; // Start full timeBarContainer.addChild(timeBarBg); timeBarContainer.addChild(timeBarFill); var timeLabel = new Text2('TIME', { size: 30, fill: 0xFFFFFF }); timeLabel.anchor.set(0.5, 0.5); timeLabel.x = 200; timeBarContainer.addChild(timeLabel); timeBarContainer.x = -200; timeBarContainer.y = 100; LK.gui.top.addChild(timeBarContainer); // Water bar waterBarContainer = new Container(); waterBarBg = waterBarContainer.attachAsset('waterBarBg', { anchorX: 0, anchorY: 0.5 }); waterBarFill = waterBarContainer.attachAsset('waterBar', { anchorX: 0, anchorY: 0.5 }); waterBarFill.width = 400; // Start full waterBarContainer.addChild(waterBarBg); waterBarContainer.addChild(waterBarFill); var waterLabel = new Text2('WATER', { size: 30, fill: 0xFFFFFF }); waterLabel.anchor.set(0.5, 0.5); waterLabel.x = 200; waterBarContainer.addChild(waterLabel); waterBarContainer.x = -200; waterBarContainer.y = 160; LK.gui.top.addChild(waterBarContainer); // Instructions container instructionsContainer = new Container(); var instructionsBg = instructionsContainer.attachAsset('waterBarBg', { anchorX: 0.5, anchorY: 0.5, width: 1600, height: 400, alpha: 0.8 }); instructionsTxt = new Text2('Drag to move firefighter\nTap and hold to spray water\nRescue civilians and extinguish fires!', { size: 70, fill: 0xFFFFFF }); instructionsTxt.anchor.set(0.5, 0.5); instructionsContainer.addChild(instructionsTxt); instructionsContainer.x = 1024; instructionsContainer.y = 1366; instructionsContainer.visible = false; game.addChild(instructionsContainer); } function showInstructions() { instructionsContainer.visible = true; LK.setTimeout(function () { instructionsContainer.visible = false; }, 5000); } function startGameTimer() { var timeRemaining = gameTime; updateTimeBar(timeRemaining / gameTime); gameTimer = LK.setInterval(function () { timeRemaining--; updateTimeBar(timeRemaining / gameTime); if (timeRemaining <= 0) { LK.clearInterval(gameTimer); LK.effects.flashScreen(0xff0000, 500); LK.showGameOver(); } }, 1000); } function updateTimeBar(percentage) { timeBarFill.width = Math.max(0, 400 * percentage); // Change color as time runs out if (percentage < 0.2) { timeBarFill.tint = 0xff0000; // Red } else if (percentage < 0.5) { timeBarFill.tint = 0xffff00; // Yellow } else { timeBarFill.tint = 0xff9900; // Orange } } function updateWaterBar() { var percentage = firefighter.waterLevel / firefighter.maxWaterLevel; waterBarFill.width = Math.max(0, 400 * percentage); // Change color as water runs out if (percentage < 0.2) { waterBarFill.tint = 0xff0000; // Red } else if (percentage < 0.5) { waterBarFill.tint = 0xff9900; // Orange } else { waterBarFill.tint = 0x0000ff; // Blue } } function updateScoreText() { scoreTxt.setText('Score: ' + LK.getScore()); } function checkLevelComplete() { if (civiliansRescued >= totalCivilians) { // Level complete LK.clearInterval(gameTimer); LK.getSound('levelComplete').play(); // Award time bonus var timeRemaining = timeBarFill.width / 400 * gameTime; var timeBonus = Math.floor(timeRemaining * 10); LK.setScore(LK.getScore() + timeBonus); // Update highest level reached level++; storage.currentLevel = level; if (level > storage.highestLevel) { storage.highestLevel = level; } // Show win screen LK.showYouWin(); } } // Helper functions function createFire(x, y) { var fire = new Fire(x, y); game.addChild(fire); fires.push(fire); return fire; } function createCivilian(x, y) { var civilian = new Civilian(x, y); game.addChild(civilian); civilians.push(civilian); return civilian; } function createWall(x, y, width, height) { var wall = new Wall(x, y, width, height); game.addChild(wall); walls.push(wall); return wall; } function createWaterParticle(x, y) { // Create water spray in the direction the firefighter is facing var particleX = x + Math.random() * 40 - 20; var particleY = y - 50; // Spray from the top of the firefighter var particle = new WaterParticle(particleX, particleY); game.addChild(particle); waterParticles.push(particle); // Create smoke occasionally if (LK.ticks % 10 === 0) { createSmokeParticle(x, y); } // Play spray sound occasionally if (LK.ticks % 30 === 0) { LK.getSound('spray').play(); } return particle; } function createSmokeParticle(x, y) { var smoke = new Smoke(x + Math.random() * 60 - 30, y - 50); game.addChild(smoke); smokeParticles.push(smoke); return smoke; } function createWaterPickup() { // Define distance function locally to ensure it's available function distance(x1, y1, x2, y2) { var dx = x1 - x2; var dy = y1 - y2; return Math.sqrt(dx * dx + dy * dy); } // Find a valid position for the water pickup var pickupX, pickupY; var validPosition = false; var preferredPlacement = Math.random(); while (!validPosition) { // Try to place water pickups strategically if (fires.length > 0 && preferredPlacement < 0.5) { // Place near fires (but not too close) var randomFire = fires[Math.floor(Math.random() * fires.length)]; var angle = Math.random() * Math.PI * 2; var dist = 250 + Math.random() * 150; pickupX = randomFire.x + Math.cos(angle) * dist; pickupY = randomFire.y + Math.sin(angle) * dist; } else if (civilians.length > 0 && preferredPlacement >= 0.5 && preferredPlacement < 0.8) { // Place near civilians that need rescue var unrescuedCivilians = civilians.filter(function (civ) { return !civ.isRescued; }); if (unrescuedCivilians.length > 0) { var randomCivilian = unrescuedCivilians[Math.floor(Math.random() * unrescuedCivilians.length)]; var angle = Math.random() * Math.PI * 2; var dist = 200 + Math.random() * 100; pickupX = randomCivilian.x + Math.cos(angle) * dist; pickupY = randomCivilian.y + Math.sin(angle) * dist; } else { pickupX = 300 + Math.random() * (2048 - 600); pickupY = 300 + Math.random() * (2732 - 600); } } else { // Random placement pickupX = 300 + Math.random() * (2048 - 600); pickupY = 300 + Math.random() * (2732 - 600); } // Keep within game bounds pickupX = Math.max(100, Math.min(pickupX, 2048 - 100)); pickupY = Math.max(100, Math.min(pickupY, 2732 - 100)); // Check if too close to firefighter or other objects var tooCloseToFirefighter = distance(pickupX, pickupY, firefighter.x, firefighter.y) < 200; var tooCloseToFireStation = fireStation && distance(pickupX, pickupY, fireStation.x, fireStation.y) < 300; var tooCloseToExit = exit && distance(pickupX, pickupY, exit.x, exit.y) < 300; // Check if inside any wall var insideWall = false; for (var j = 0; j < walls.length; j++) { if (willCollide(pickupX, pickupY, 60, 60, walls[j].x, walls[j].y, walls[j].width, walls[j].height)) { insideWall = true; break; } } // Check if not too close to existing pickups var tooCloseToPickup = false; for (var j = 0; j < waterPickups.length; j++) { if (distance(pickupX, pickupY, waterPickups[j].x, waterPickups[j].y) < 300) { tooCloseToPickup = true; break; } } validPosition = !tooCloseToFirefighter && !tooCloseToFireStation && !tooCloseToExit && !insideWall && !tooCloseToPickup; } var waterPickup = new WaterPickup(pickupX, pickupY); game.addChild(waterPickup); waterPickups.push(waterPickup); return waterPickup; } function distance(x1, y1, x2, y2) { var dx = x1 - x2; var dy = y1 - y2; return Math.sqrt(dx * dx + dy * dy); } function willCollide(x1, y1, w1, h1, x2, y2, w2, h2) { return !(x1 + w1 / 2 < x2 - w2 / 2 || x1 - w1 / 2 > x2 + w2 / 2 || y1 + h1 / 2 < y2 - h2 / 2 || y1 - h1 / 2 > y2 + h2 / 2); } // Input handlers game.down = function (x, y, obj) { // Start dragging joystickActive = true; joystickOrigin.x = x; joystickOrigin.y = y; joystickPosition.x = x; joystickPosition.y = y; // If tapping close to the firefighter, start spraying water var dx = x - firefighter.x; var dy = y - firefighter.y; var distToFirefighter = Math.sqrt(dx * dx + dy * dy); if (distToFirefighter < 200) { firefighter.isSprayingWater = true; } // Check for civilian pickup if (!firefighter.carryingCivilian) { for (var i = 0; i < civilians.length; i++) { var civilian = civilians[i]; if (!civilian.isRescued && distance(firefighter.x, firefighter.y, civilian.x, civilian.y) < 100) { firefighter.carryingCivilian = civilian; LK.getSound('rescue').play(); break; } } } }; game.move = function (x, y, obj) { if (joystickActive) { joystickPosition.x = x; joystickPosition.y = y; var dx = x - joystickOrigin.x; var dy = y - joystickOrigin.y; // Calculate direction vector var length = Math.sqrt(dx * dx + dy * dy); if (length > 0) { firefighter.direction.x = dx / length; firefighter.direction.y = dy / length; } } }; game.up = function (x, y, obj) { joystickActive = false; firefighter.direction.x = 0; firefighter.direction.y = 0; firefighter.isSprayingWater = false; }; // Main game update loop game.update = function () { // Update firefighter if (firefighter) { firefighter.update(); } // Update fires for (var i = fires.length - 1; i >= 0; i--) { if (fires[i].isDestroyed) { fires.splice(i, 1); } else { fires[i].update(); } } // Update civilians for (var i = 0; i < civilians.length; i++) { if (!civilians[i].isRescued) { civilians[i].update(); } } // Update water particles for (var i = waterParticles.length - 1; i >= 0; i--) { if (waterParticles[i].isDestroyed) { waterParticles.splice(i, 1); } else { waterParticles[i].update(); } } // Update smoke particles for (var i = smokeParticles.length - 1; i >= 0; i--) { if (smokeParticles[i].isDestroyed) { smokeParticles.splice(i, 1); } else { smokeParticles[i].update(); } } // Create smoke from fires if (LK.ticks % 30 === 0) { for (var i = 0; i < fires.length; i++) { if (Math.random() < 0.3) { createSmokeParticle(fires[i].x, fires[i].y); } } } }; // Initialize the game initGame();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
currentLevel: 1,
highestLevel: 1
});
/****
* Classes
****/
var GameObject = Container.expand(function (id, width, height) {
var self = Container.call(this);
self.id = id;
self.width = width;
self.height = height;
self.isDestroyed = false;
self.destroy = function () {
self.isDestroyed = true;
Container.prototype.destroy.call(self);
};
return self;
});
var WaterPickup = GameObject.expand(function (x, y) {
var self = GameObject.call(this, 'waterPickup', 60, 60);
var graphics = self.attachAsset('water', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 3
});
self.x = x;
self.y = y;
self.waterAmount = 75; // Increased water amount
// Make water pickup pulse to draw attention
function pulseAnimation() {
tween(graphics.scale, {
x: 3.3,
y: 3.3
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(graphics.scale, {
x: 3.0,
y: 3.0
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: pulseAnimation
});
}
});
}
pulseAnimation();
return self;
});
var WaterParticle = GameObject.expand(function (x, y) {
var self = GameObject.call(this, 'water', 20, 20);
var graphics = self.attachAsset('water', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = x;
self.y = y;
self.lifespan = 30; // frames
self.velocity = {
x: Math.random() * 6 - 3,
y: -8 - Math.random() * 3
};
self.update = function () {
self.x += self.velocity.x;
self.y += self.velocity.y;
self.lifespan--;
// Apply gravity
self.velocity.y += 0.3;
// Fade out
graphics.alpha = self.lifespan / 30;
// Check collision with fires
for (var i = fires.length - 1; i >= 0; i--) {
var fire = fires[i];
if (self.intersects(fire)) {
fire.intensity -= 5;
if (fire.intensity <= 0) {
LK.getSound('extinguish').play();
LK.setScore(LK.getScore() + 50);
updateScoreText();
fires.splice(i, 1);
fire.destroy();
}
self.lifespan = 0;
break;
}
}
if (self.lifespan <= 0) {
self.destroy();
for (var i = 0; i < waterParticles.length; i++) {
if (waterParticles[i] === self) {
waterParticles.splice(i, 1);
break;
}
}
}
};
return self;
});
var Wall = GameObject.expand(function (x, y, width, height) {
var self = GameObject.call(this, 'wall', width, height);
var graphics = self.attachAsset('wall', {
anchorX: 0.5,
anchorY: 0.5,
width: width,
height: height
});
self.x = x;
self.y = y;
return self;
});
var Smoke = GameObject.expand(function (x, y) {
var self = GameObject.call(this, 'smoke', 200, 200);
var graphics = self.attachAsset('smoke', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.3
});
self.x = x;
self.y = y;
self.lifespan = 120 + Math.floor(Math.random() * 60); // 2-3 seconds
var scale = 0.3 + Math.random() * 0.3;
graphics.scale.set(scale, scale);
self.update = function () {
self.y -= 0.5;
self.x += Math.random() * 0.6 - 0.3;
self.lifespan--;
// Grow and fade
var lifePercent = self.lifespan / 180;
graphics.scale.set(scale * (2 - lifePercent), scale * (2 - lifePercent));
graphics.alpha = 0.3 * lifePercent;
if (self.lifespan <= 0) {
self.destroy();
for (var i = 0; i < smokeParticles.length; i++) {
if (smokeParticles[i] === self) {
smokeParticles.splice(i, 1);
break;
}
}
}
};
return self;
});
var Firefighter = GameObject.expand(function () {
var self = GameObject.call(this, 'firefighter', 100, 150);
var graphics = self.attachAsset('firefighter', {
anchorX: 0.5,
anchorY: 0.5
});
// Add health bar
var healthBarBg = self.attachAsset('timeBarBg', {
anchorX: 0.5,
anchorY: 0,
width: 100,
height: 10,
y: -90
});
var healthBarFill = self.attachAsset('timeBar', {
anchorX: 0.5,
anchorY: 0,
width: 100,
height: 10,
y: -90,
tint: 0x00ff00
});
self.speed = 5;
self.waterLevel = 100;
self.maxWaterLevel = 100;
self.waterUseRate = 0.5;
self.waterRefillRate = 1.5; // was 3, now slower refill
self.health = 100;
self.maxHealth = 100;
self.damageTimer = 0;
self.damageDelay = 30; // 0.5 seconds at 60fps
self.isRefilling = false;
self.isSprayingWater = false;
self.carryingCivilian = null;
self.direction = {
x: 0,
y: 0
};
self.update = function () {
// Handle movement
if (self.direction.x !== 0 || self.direction.y !== 0) {
var newX = self.x + self.direction.x * self.speed;
var newY = self.y + self.direction.y * self.speed;
// Check for collisions with walls
var wouldCollide = false;
for (var i = 0; i < walls.length; i++) {
var wall = walls[i];
if (willCollide(newX, newY, self.width, self.height, wall.x, wall.y, wall.width, wall.height)) {
wouldCollide = true;
break;
}
}
if (!wouldCollide) {
self.x = newX;
self.y = newY;
// Keep within game bounds
self.x = Math.max(self.width / 2, Math.min(self.x, 2048 - self.width / 2));
self.y = Math.max(self.height / 2, Math.min(self.y, 2732 - self.height / 2));
// If carrying a civilian, update their position
if (self.carryingCivilian) {
self.carryingCivilian.x = self.x;
self.carryingCivilian.y = self.y - 80;
}
}
}
// Check for water refill at fire station
if (fireStation && self.intersects(fireStation)) {
self.isRefilling = true;
if (self.waterLevel < self.maxWaterLevel) {
self.waterLevel = Math.min(self.maxWaterLevel, self.waterLevel + self.waterRefillRate);
updateWaterBar();
}
// Also heal when at fire station
if (self.health < self.maxHealth) {
self.health = Math.min(self.maxHealth, self.health + 0.2); // was 0.5, now slower recovery
healthBarFill.width = self.health / self.maxHealth * 100;
// Update color based on health
if (self.health < 30) {
healthBarFill.tint = 0xff0000; // Red
} else if (self.health < 60) {
healthBarFill.tint = 0xffff00; // Yellow
} else {
healthBarFill.tint = 0x00ff00; // Green
}
}
} else {
self.isRefilling = false;
}
// Check for water pickup collision
for (var i = waterPickups.length - 1; i >= 0; i--) {
if (self.intersects(waterPickups[i])) {
// Refill water
self.waterLevel = Math.min(self.maxWaterLevel, self.waterLevel + waterPickups[i].waterAmount);
updateWaterBar();
// Add score
LK.setScore(LK.getScore() + 25);
updateScoreText();
// Show pickup effect
LK.effects.flashObject(self, 0x0000ff, 500);
// Create water particle burst effect
for (var j = 0; j < 8; j++) {
var angle = j * Math.PI / 4;
var particleX = self.x + Math.cos(angle) * 50;
var particleY = self.y + Math.sin(angle) * 50;
var particle = new WaterParticle(particleX, particleY);
particle.velocity.x = Math.cos(angle) * 4;
particle.velocity.y = Math.sin(angle) * 4;
game.addChild(particle);
waterParticles.push(particle);
}
// Play sound
LK.getSound('spray').play();
// Remove pickup
waterPickups[i].destroy();
waterPickups.splice(i, 1);
}
}
// Handle water spraying
if (self.isSprayingWater && self.waterLevel > 0) {
self.waterLevel = Math.max(0, self.waterLevel - self.waterUseRate);
updateWaterBar();
// Create water particle
createWaterParticle(self.x, self.y);
}
// Check for fire collisions and take damage
if (self.damageTimer <= 0) {
for (var i = 0; i < fires.length; i++) {
if (self.intersects(fires[i])) {
self.health -= 10;
self.damageTimer = self.damageDelay;
// Update health bar
healthBarFill.width = self.health / self.maxHealth * 100;
// Change color based on health
if (self.health < 30) {
healthBarFill.tint = 0xff0000; // Red
} else if (self.health < 60) {
healthBarFill.tint = 0xffff00; // Yellow
}
// Visual feedback
LK.effects.flashObject(self, 0xff0000, 300);
// Game over if health depleted
if (self.health <= 0 && !self.isDestroyed) {
self.isDestroyed = true;
LK.effects.flashScreen(0xff0000, 500);
LK.setTimeout(function () {
LK.showGameOver();
}, 500);
}
break;
}
}
} else {
self.damageTimer--;
}
// Check exit collision when carrying civilian
if (self.carryingCivilian && exit && self.intersects(exit)) {
LK.getSound('rescue').play();
civiliansRescued++;
self.carryingCivilian.isRescued = true;
self.carryingCivilian = null;
// Update the score
LK.setScore(LK.getScore() + 100);
updateScoreText();
// Check if all civilians are rescued
checkLevelComplete();
}
};
return self;
});
var FireStation = GameObject.expand(function (x, y) {
var self = GameObject.call(this, 'fireStation', 800, 600);
var graphics = self.attachAsset('fireStation', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = x;
self.y = y;
return self;
});
var Fire = GameObject.expand(function (x, y) {
var self = GameObject.call(this, 'fire', 60, 80);
var graphics = self.attachAsset('fire', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = x;
self.y = y;
self.intensity = 100; // 0-100
self.spreadTimer = 0;
self.spreadRate = 3; // seconds (was 5, now faster)
// Randomly vary the size
var scale = 0.8 + Math.random() * 0.4;
graphics.scale.set(scale, scale);
self.update = function () {
if (self.intensity <= 0) {
self.destroy();
return;
}
// Animate the fire
if (LK.ticks % 5 === 0) {
var scaleVar = Math.random() * 0.2 - 0.1;
graphics.scale.set(scale + scaleVar, scale + scaleVar);
}
// Attempt to spread fire
self.spreadTimer++;
if (self.spreadTimer > self.spreadRate * 60) {
// Convert seconds to frames at 60fps
self.spreadTimer = 0;
if (Math.random() < 0.5 && fires.length < maxFires) {
// was 0.3, now 0.5 (more likely to spread)
var angle = Math.random() * Math.PI * 2;
var distance = 100 + Math.random() * 100;
var newX = self.x + Math.cos(angle) * distance;
var newY = self.y + Math.sin(angle) * distance;
// Keep within game bounds
newX = Math.max(30, Math.min(newX, 2048 - 30));
newY = Math.max(30, Math.min(newY, 2732 - 30));
// Check if too close to existing fire or wall
var tooClose = false;
// Check walls
for (var i = 0; i < walls.length; i++) {
if (willCollide(newX, newY, 60, 80, walls[i].x, walls[i].y, walls[i].width, walls[i].height)) {
tooClose = true;
break;
}
}
if (!tooClose) {
createFire(newX, newY);
LK.getSound('burn').play();
}
}
}
};
return self;
});
var Exit = GameObject.expand(function (x, y) {
var self = GameObject.call(this, 'exit', 150, 100);
var graphics = self.attachAsset('exit', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = x;
self.y = y;
// Make exit pulse to draw attention
function pulseAnimation() {
tween(graphics.scale, {
x: 1.3,
y: 1.3
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: function onFinish() {
tween(graphics.scale, {
x: 1.2,
y: 1.2
}, {
duration: 800,
easing: tween.easeInOut,
onFinish: pulseAnimation
});
}
});
}
pulseAnimation();
return self;
});
var Civilian = GameObject.expand(function (x, y) {
var self = GameObject.call(this, 'civilian', 80, 120);
var graphics = self.attachAsset('civilian', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = x;
self.y = y;
self.isRescued = false;
self.healthTimer = 0;
self.maxHealthTimer = 5 * 60; // 5 seconds at 60fps
self.update = function () {
// Check if close to fire
var nearFire = false;
for (var i = 0; i < fires.length; i++) {
var fire = fires[i];
var dx = self.x - fire.x;
var dy = self.y - fire.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 150) {
nearFire = true;
break;
}
}
if (nearFire) {
// Start losing health when near fire
self.healthTimer++;
if (self.healthTimer >= self.maxHealthTimer && !self.isRescued && !self.isDestroyed) {
self.isDestroyed = true;
LK.effects.flashScreen(0xff0000, 500);
LK.setTimeout(function () {
LK.showGameOver();
}, 500);
}
} else {
// Recover when away from fire
self.healthTimer = Math.max(0, self.healthTimer - 1);
}
// Visual feedback on health
var healthPercent = 1 - self.healthTimer / self.maxHealthTimer;
graphics.alpha = 0.5 + healthPercent * 0.5;
// Animate when in danger
if (nearFire && LK.ticks % 10 === 0) {
tween(graphics, {
rotation: Math.random() * 0.2 - 0.1
}, {
duration: 300
});
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x333333
});
/****
* Game Code
****/
// Game state variables
var level = storage.currentLevel || 1;
var firefighter;
var fires = [];
var civilians = [];
var walls = [];
var waterParticles = [];
var smokeParticles = [];
var waterPickups = [];
var waterPickupTimer;
var fireStation;
var exit;
var maxFires;
var civiliansRescued = 0;
var totalCivilians = 0;
var gameTime = 90; // seconds
var gameTimer;
var controlPad;
var actionButton;
var timeBarFill;
var waterBarFill;
var joystickActive = false;
var joystickOrigin = {
x: 0,
y: 0
};
var joystickPosition = {
x: 0,
y: 0
};
// Interface elements
var scoreTxt;
var levelTxt;
var timeBarContainer;
var timeBarBg;
var waterBarContainer;
var waterBarBg;
var instructionsContainer;
var instructionsTxt;
// Initialize game
function initGame() {
// Reset game state
LK.setScore(0);
civiliansRescued = 0;
// Clear previous game objects
cleanupLevel();
// Set up the level
setupLevel(level);
// Create UI
createUI();
// Play background music
LK.playMusic('gameplay', {
loop: true,
fade: {
start: 0,
end: 0.4,
duration: 1000
}
});
// Start game timer
startGameTimer();
// Show initial instructions
showInstructions();
// Start water pickup spawning
if (waterPickupTimer) {
LK.clearInterval(waterPickupTimer);
}
// Create multiple initial water pickups based on level
var initialPickups = 2 + Math.floor(level / 2);
for (var i = 0; i < initialPickups; i++) {
createWaterPickup();
}
// Spawn new water pickups periodically
waterPickupTimer = LK.setInterval(function () {
// Limit the number of pickups based on level
if (waterPickups.length < 3 + Math.floor(level)) {
createWaterPickup();
}
}, 25000); // New water pickup every 25 seconds (was 15s, now less frequent)
}
function cleanupLevel() {
// Clear all game objects
if (firefighter) {
firefighter.destroy();
}
for (var i = 0; i < fires.length; i++) {
fires[i].destroy();
}
fires = [];
for (var i = 0; i < civilians.length; i++) {
civilians[i].destroy();
}
civilians = [];
for (var i = 0; i < walls.length; i++) {
walls[i].destroy();
}
walls = [];
for (var i = 0; i < waterParticles.length; i++) {
waterParticles[i].destroy();
}
waterParticles = [];
for (var i = 0; i < smokeParticles.length; i++) {
smokeParticles[i].destroy();
}
smokeParticles = [];
for (var i = 0; i < waterPickups.length; i++) {
waterPickups[i].destroy();
}
waterPickups = [];
if (waterPickupTimer) {
LK.clearInterval(waterPickupTimer);
waterPickupTimer = null;
}
if (fireStation) {
fireStation.destroy();
fireStation = null;
}
if (exit) {
exit.destroy();
exit = null;
}
if (gameTimer) {
LK.clearInterval(gameTimer);
gameTimer = null;
}
}
function setupLevel(level) {
// Define distance function first so it can be used in this method
function distance(x1, y1, x2, y2) {
var dx = x1 - x2;
var dy = y1 - y2;
return Math.sqrt(dx * dx + dy * dy);
}
// Level configuration
var levelConfig = {
1: {
layout: 'basic',
civilians: 2,
initialFires: 3,
maxFires: 6,
time: 90
},
2: {
layout: 'apartment',
civilians: 3,
initialFires: 4,
maxFires: 8,
time: 120
},
3: {
layout: 'office',
civilians: 4,
initialFires: 5,
maxFires: 10,
time: 180
}
};
// Use level 3 config for any level beyond 3
var config = levelConfig[level] || levelConfig[3];
config.initialFires += Math.floor((level - 3) / 2); // Increase fires for levels beyond 3
config.civilians += Math.floor((level - 3) / 2); // Increase civilians for levels beyond 3
config.time = Math.min(300, config.time + (level - 3) * 30); // Increase time (max 5 minutes)
maxFires = config.maxFires;
totalCivilians = config.civilians;
gameTime = config.time;
// Create level layout
createLayout(config.layout);
// Create initial fires
for (var i = 0; i < config.initialFires; i++) {
var fireX, fireY;
var validPosition = false;
// Find a valid position for the fire
while (!validPosition) {
fireX = 300 + Math.random() * (2048 - 600);
fireY = 300 + Math.random() * (2732 - 600);
// Check if too close to firefighter, fire station, or exit
var dx1 = fireX - 200;
var dy1 = fireY - 2500;
var tooCloseToFirefighter = Math.sqrt(dx1 * dx1 + dy1 * dy1) < 300;
var tooCloseToFireStation = fireStation && Math.sqrt(Math.pow(fireX - fireStation.x, 2) + Math.pow(fireY - fireStation.y, 2)) < 400;
var tooCloseToExit = exit && Math.sqrt(Math.pow(fireX - exit.x, 2) + Math.pow(fireY - exit.y, 2)) < 400;
// Check if inside any wall
var insideWall = false;
for (var j = 0; j < walls.length; j++) {
if (willCollide(fireX, fireY, 60, 80, walls[j].x, walls[j].y, walls[j].width, walls[j].height)) {
insideWall = true;
break;
}
}
validPosition = !tooCloseToFirefighter && !tooCloseToFireStation && !tooCloseToExit && !insideWall;
}
createFire(fireX, fireY);
}
// Create civilians
for (var i = 0; i < config.civilians; i++) {
var civX, civY;
var validPosition = false;
while (!validPosition) {
civX = 300 + Math.random() * (2048 - 600);
civY = 300 + Math.random() * (2732 - 600);
// Not too close to fire station or exit
var tooCloseToFireStation = fireStation && distance(civX, civY, fireStation.x, fireStation.y) < 300;
var tooCloseToExit = exit && distance(civX, civY, exit.x, exit.y) < 300;
// Check if inside any wall
var insideWall = false;
for (var j = 0; j < walls.length; j++) {
if (willCollide(civX, civY, 80, 120, walls[j].x, walls[j].y, walls[j].width, walls[j].height)) {
insideWall = true;
break;
}
}
// Must be somewhat close to at least one fire
var nearFire = false;
for (var j = 0; j < fires.length; j++) {
if (distance(civX, civY, fires[j].x, fires[j].y) < 500) {
nearFire = true;
break;
}
}
validPosition = !tooCloseToFireStation && !tooCloseToExit && !insideWall && nearFire;
}
createCivilian(civX, civY);
}
// Create firefighter
firefighter = new Firefighter();
firefighter.x = 200;
firefighter.y = 2500;
game.addChild(firefighter);
}
function createLayout(layoutType) {
// Create fire station
fireStation = new FireStation(200, 2500);
game.addChild(fireStation);
// Create exit
exit = new Exit(1800, 200);
game.addChild(exit);
// Create walls based on layout type
switch (layoutType) {
case 'basic':
// Simple wall layout
createWall(1024, 1366, 800, 50); // Center horizontal wall
createWall(600, 1000, 600, 50); // Left horizontal wall
createWall(1400, 1700, 600, 50); // Right horizontal wall
break;
case 'apartment':
// Apartment-like layout
createWall(300, 800, 600, 50); // Upper left horizontal
createWall(1300, 800, 500, 50); // Upper right horizontal
createWall(500, 1400, 600, 50); // Lower left horizontal
createWall(1300, 1400, 600, 50); // Lower right horizontal
createWall(800, 800, 40, 600); // Center vertical
createWall(1100, 1800, 40, 800); // Lower center vertical
break;
case 'office':
// Office building layout
createWall(500, 600, 1000, 50); // Top horizontal
createWall(800, 1000, 600, 50); // Middle horizontal 1
createWall(1300, 1400, 400, 50); // Middle horizontal 2
createWall(1000, 1800, 1000, 50); // Bottom horizontal
createWall(500, 600, 40, 800); // Left vertical
createWall(1500, 600, 40, 800); // Right vertical
createWall(1000, 1400, 40, 400); // Bottom vertical
break;
default:
// Random maze-like layout
var wallCount = 6 + Math.floor(Math.random() * 6); // 6-12 walls
for (var i = 0; i < wallCount; i++) {
var horizontal = Math.random() > 0.5;
var x, y, width, height;
if (horizontal) {
width = 400 + Math.random() * 800;
height = 40;
x = 200 + Math.random() * (2048 - 400 - width);
y = 400 + Math.random() * (2732 - 800);
} else {
width = 40;
height = 400 + Math.random() * 800;
x = 400 + Math.random() * (2048 - 800);
y = 200 + Math.random() * (2732 - 400 - height);
}
// Don't block firefighter start or exit
var blocksFirefighter = willCollide(x, y, width, height, 200, 2500, 200, 200);
var blocksExit = willCollide(x, y, width, height, 1800, 200, 150, 100);
if (!blocksFirefighter && !blocksExit) {
createWall(x, y, width, height);
}
}
break;
}
}
function createUI() {
// Score text
scoreTxt = new Text2('Score: 0', {
size: 60,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(1, 0);
LK.gui.topRight.addChild(scoreTxt);
// Level text
levelTxt = new Text2('Level: ' + level, {
size: 60,
fill: 0xFFFFFF
});
levelTxt.anchor.set(0, 0);
LK.gui.top.addChild(levelTxt);
// Time bar
timeBarContainer = new Container();
timeBarBg = timeBarContainer.attachAsset('timeBarBg', {
anchorX: 0,
anchorY: 0.5
});
timeBarFill = timeBarContainer.attachAsset('timeBar', {
anchorX: 0,
anchorY: 0.5
});
timeBarFill.width = 400; // Start full
timeBarContainer.addChild(timeBarBg);
timeBarContainer.addChild(timeBarFill);
var timeLabel = new Text2('TIME', {
size: 30,
fill: 0xFFFFFF
});
timeLabel.anchor.set(0.5, 0.5);
timeLabel.x = 200;
timeBarContainer.addChild(timeLabel);
timeBarContainer.x = -200;
timeBarContainer.y = 100;
LK.gui.top.addChild(timeBarContainer);
// Water bar
waterBarContainer = new Container();
waterBarBg = waterBarContainer.attachAsset('waterBarBg', {
anchorX: 0,
anchorY: 0.5
});
waterBarFill = waterBarContainer.attachAsset('waterBar', {
anchorX: 0,
anchorY: 0.5
});
waterBarFill.width = 400; // Start full
waterBarContainer.addChild(waterBarBg);
waterBarContainer.addChild(waterBarFill);
var waterLabel = new Text2('WATER', {
size: 30,
fill: 0xFFFFFF
});
waterLabel.anchor.set(0.5, 0.5);
waterLabel.x = 200;
waterBarContainer.addChild(waterLabel);
waterBarContainer.x = -200;
waterBarContainer.y = 160;
LK.gui.top.addChild(waterBarContainer);
// Instructions container
instructionsContainer = new Container();
var instructionsBg = instructionsContainer.attachAsset('waterBarBg', {
anchorX: 0.5,
anchorY: 0.5,
width: 1600,
height: 400,
alpha: 0.8
});
instructionsTxt = new Text2('Drag to move firefighter\nTap and hold to spray water\nRescue civilians and extinguish fires!', {
size: 70,
fill: 0xFFFFFF
});
instructionsTxt.anchor.set(0.5, 0.5);
instructionsContainer.addChild(instructionsTxt);
instructionsContainer.x = 1024;
instructionsContainer.y = 1366;
instructionsContainer.visible = false;
game.addChild(instructionsContainer);
}
function showInstructions() {
instructionsContainer.visible = true;
LK.setTimeout(function () {
instructionsContainer.visible = false;
}, 5000);
}
function startGameTimer() {
var timeRemaining = gameTime;
updateTimeBar(timeRemaining / gameTime);
gameTimer = LK.setInterval(function () {
timeRemaining--;
updateTimeBar(timeRemaining / gameTime);
if (timeRemaining <= 0) {
LK.clearInterval(gameTimer);
LK.effects.flashScreen(0xff0000, 500);
LK.showGameOver();
}
}, 1000);
}
function updateTimeBar(percentage) {
timeBarFill.width = Math.max(0, 400 * percentage);
// Change color as time runs out
if (percentage < 0.2) {
timeBarFill.tint = 0xff0000; // Red
} else if (percentage < 0.5) {
timeBarFill.tint = 0xffff00; // Yellow
} else {
timeBarFill.tint = 0xff9900; // Orange
}
}
function updateWaterBar() {
var percentage = firefighter.waterLevel / firefighter.maxWaterLevel;
waterBarFill.width = Math.max(0, 400 * percentage);
// Change color as water runs out
if (percentage < 0.2) {
waterBarFill.tint = 0xff0000; // Red
} else if (percentage < 0.5) {
waterBarFill.tint = 0xff9900; // Orange
} else {
waterBarFill.tint = 0x0000ff; // Blue
}
}
function updateScoreText() {
scoreTxt.setText('Score: ' + LK.getScore());
}
function checkLevelComplete() {
if (civiliansRescued >= totalCivilians) {
// Level complete
LK.clearInterval(gameTimer);
LK.getSound('levelComplete').play();
// Award time bonus
var timeRemaining = timeBarFill.width / 400 * gameTime;
var timeBonus = Math.floor(timeRemaining * 10);
LK.setScore(LK.getScore() + timeBonus);
// Update highest level reached
level++;
storage.currentLevel = level;
if (level > storage.highestLevel) {
storage.highestLevel = level;
}
// Show win screen
LK.showYouWin();
}
}
// Helper functions
function createFire(x, y) {
var fire = new Fire(x, y);
game.addChild(fire);
fires.push(fire);
return fire;
}
function createCivilian(x, y) {
var civilian = new Civilian(x, y);
game.addChild(civilian);
civilians.push(civilian);
return civilian;
}
function createWall(x, y, width, height) {
var wall = new Wall(x, y, width, height);
game.addChild(wall);
walls.push(wall);
return wall;
}
function createWaterParticle(x, y) {
// Create water spray in the direction the firefighter is facing
var particleX = x + Math.random() * 40 - 20;
var particleY = y - 50; // Spray from the top of the firefighter
var particle = new WaterParticle(particleX, particleY);
game.addChild(particle);
waterParticles.push(particle);
// Create smoke occasionally
if (LK.ticks % 10 === 0) {
createSmokeParticle(x, y);
}
// Play spray sound occasionally
if (LK.ticks % 30 === 0) {
LK.getSound('spray').play();
}
return particle;
}
function createSmokeParticle(x, y) {
var smoke = new Smoke(x + Math.random() * 60 - 30, y - 50);
game.addChild(smoke);
smokeParticles.push(smoke);
return smoke;
}
function createWaterPickup() {
// Define distance function locally to ensure it's available
function distance(x1, y1, x2, y2) {
var dx = x1 - x2;
var dy = y1 - y2;
return Math.sqrt(dx * dx + dy * dy);
}
// Find a valid position for the water pickup
var pickupX, pickupY;
var validPosition = false;
var preferredPlacement = Math.random();
while (!validPosition) {
// Try to place water pickups strategically
if (fires.length > 0 && preferredPlacement < 0.5) {
// Place near fires (but not too close)
var randomFire = fires[Math.floor(Math.random() * fires.length)];
var angle = Math.random() * Math.PI * 2;
var dist = 250 + Math.random() * 150;
pickupX = randomFire.x + Math.cos(angle) * dist;
pickupY = randomFire.y + Math.sin(angle) * dist;
} else if (civilians.length > 0 && preferredPlacement >= 0.5 && preferredPlacement < 0.8) {
// Place near civilians that need rescue
var unrescuedCivilians = civilians.filter(function (civ) {
return !civ.isRescued;
});
if (unrescuedCivilians.length > 0) {
var randomCivilian = unrescuedCivilians[Math.floor(Math.random() * unrescuedCivilians.length)];
var angle = Math.random() * Math.PI * 2;
var dist = 200 + Math.random() * 100;
pickupX = randomCivilian.x + Math.cos(angle) * dist;
pickupY = randomCivilian.y + Math.sin(angle) * dist;
} else {
pickupX = 300 + Math.random() * (2048 - 600);
pickupY = 300 + Math.random() * (2732 - 600);
}
} else {
// Random placement
pickupX = 300 + Math.random() * (2048 - 600);
pickupY = 300 + Math.random() * (2732 - 600);
}
// Keep within game bounds
pickupX = Math.max(100, Math.min(pickupX, 2048 - 100));
pickupY = Math.max(100, Math.min(pickupY, 2732 - 100));
// Check if too close to firefighter or other objects
var tooCloseToFirefighter = distance(pickupX, pickupY, firefighter.x, firefighter.y) < 200;
var tooCloseToFireStation = fireStation && distance(pickupX, pickupY, fireStation.x, fireStation.y) < 300;
var tooCloseToExit = exit && distance(pickupX, pickupY, exit.x, exit.y) < 300;
// Check if inside any wall
var insideWall = false;
for (var j = 0; j < walls.length; j++) {
if (willCollide(pickupX, pickupY, 60, 60, walls[j].x, walls[j].y, walls[j].width, walls[j].height)) {
insideWall = true;
break;
}
}
// Check if not too close to existing pickups
var tooCloseToPickup = false;
for (var j = 0; j < waterPickups.length; j++) {
if (distance(pickupX, pickupY, waterPickups[j].x, waterPickups[j].y) < 300) {
tooCloseToPickup = true;
break;
}
}
validPosition = !tooCloseToFirefighter && !tooCloseToFireStation && !tooCloseToExit && !insideWall && !tooCloseToPickup;
}
var waterPickup = new WaterPickup(pickupX, pickupY);
game.addChild(waterPickup);
waterPickups.push(waterPickup);
return waterPickup;
}
function distance(x1, y1, x2, y2) {
var dx = x1 - x2;
var dy = y1 - y2;
return Math.sqrt(dx * dx + dy * dy);
}
function willCollide(x1, y1, w1, h1, x2, y2, w2, h2) {
return !(x1 + w1 / 2 < x2 - w2 / 2 || x1 - w1 / 2 > x2 + w2 / 2 || y1 + h1 / 2 < y2 - h2 / 2 || y1 - h1 / 2 > y2 + h2 / 2);
}
// Input handlers
game.down = function (x, y, obj) {
// Start dragging
joystickActive = true;
joystickOrigin.x = x;
joystickOrigin.y = y;
joystickPosition.x = x;
joystickPosition.y = y;
// If tapping close to the firefighter, start spraying water
var dx = x - firefighter.x;
var dy = y - firefighter.y;
var distToFirefighter = Math.sqrt(dx * dx + dy * dy);
if (distToFirefighter < 200) {
firefighter.isSprayingWater = true;
}
// Check for civilian pickup
if (!firefighter.carryingCivilian) {
for (var i = 0; i < civilians.length; i++) {
var civilian = civilians[i];
if (!civilian.isRescued && distance(firefighter.x, firefighter.y, civilian.x, civilian.y) < 100) {
firefighter.carryingCivilian = civilian;
LK.getSound('rescue').play();
break;
}
}
}
};
game.move = function (x, y, obj) {
if (joystickActive) {
joystickPosition.x = x;
joystickPosition.y = y;
var dx = x - joystickOrigin.x;
var dy = y - joystickOrigin.y;
// Calculate direction vector
var length = Math.sqrt(dx * dx + dy * dy);
if (length > 0) {
firefighter.direction.x = dx / length;
firefighter.direction.y = dy / length;
}
}
};
game.up = function (x, y, obj) {
joystickActive = false;
firefighter.direction.x = 0;
firefighter.direction.y = 0;
firefighter.isSprayingWater = false;
};
// Main game update loop
game.update = function () {
// Update firefighter
if (firefighter) {
firefighter.update();
}
// Update fires
for (var i = fires.length - 1; i >= 0; i--) {
if (fires[i].isDestroyed) {
fires.splice(i, 1);
} else {
fires[i].update();
}
}
// Update civilians
for (var i = 0; i < civilians.length; i++) {
if (!civilians[i].isRescued) {
civilians[i].update();
}
}
// Update water particles
for (var i = waterParticles.length - 1; i >= 0; i--) {
if (waterParticles[i].isDestroyed) {
waterParticles.splice(i, 1);
} else {
waterParticles[i].update();
}
}
// Update smoke particles
for (var i = smokeParticles.length - 1; i >= 0; i--) {
if (smokeParticles[i].isDestroyed) {
smokeParticles.splice(i, 1);
} else {
smokeParticles[i].update();
}
}
// Create smoke from fires
if (LK.ticks % 30 === 0) {
for (var i = 0; i < fires.length; i++) {
if (Math.random() < 0.3) {
createSmokeParticle(fires[i].x, fires[i].y);
}
}
}
};
// Initialize the game
initGame();
full size civilian Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
fire. Single Game Texture. In-Game asset. 2d. Blank background. No shadows
firestation. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
horizontal wall. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
smoke. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
water drop Single Game Texture. In-Game asset. 2d. Blank background. No shadows
firefighter using fire extinguisher Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows
Exit Gate. Single Game Texture. In-Game asset. 2d. Blank background. High contrast. No shadows