/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ // Bullet var Bullet = Container.expand(function () { var self = Container.call(this); var bullet = self.attachAsset('bullet', { anchorX: 0.5, anchorY: 0.5 }); self.width = bullet.width; self.height = bullet.height; self.speed = -36; // Upwards self.power = 1; // Default bullet power self.angle = 0; // For spread self.update = function () { if (self.angle !== 0) { // Move in angle for spread self.x += Math.sin(self.angle) * 12; self.y += Math.cos(self.angle) * self.speed; } else { self.y += self.speed; } }; return self; }); // Enemy base class var Enemy = Container.expand(function () { var self = Container.call(this); self.type = 'enemy'; self.hp = 1; self.speed = 8; self.scoreValue = 1; self.update = function () {}; return self; }); // Skeleton enemy var Skeleton = Enemy.expand(function () { var self = Enemy.call(this); var skeleton = self.attachAsset('enemy_skeleton', { anchorX: 0.5, anchorY: 0.5 }); // Make skeleton bigger skeleton.scaleX = 1.7; skeleton.scaleY = 1.7; self.width = skeleton.width * 1.7; self.height = skeleton.height * 1.7; self.hp = 1; // Make skeleton slower self.speed = 6 + Math.random() * 2.5; self.scoreValue = 2; self.update = function () { self.y += self.speed; }; return self; }); // Goblin enemy var Goblin = Enemy.expand(function () { var self = Enemy.call(this); var goblin = self.attachAsset('enemy_goblin', { anchorX: 0.5, anchorY: 0.5 }); // Make goblin bigger goblin.scaleX = 1.7; goblin.scaleY = 1.7; self.width = goblin.width * 1.7; self.height = goblin.height * 1.7; self.hp = 2; // Make goblin slower self.speed = 5 + Math.random() * 2; self.scoreValue = 3; self.update = function () { self.y += self.speed; }; return self; }); // Ginger enemy var Ginger = Enemy.expand(function () { var self = Enemy.call(this); // Use goblin as base, but with different color and stats var ginger = self.attachAsset('enemy_goblin', { anchorX: 0.5, anchorY: 0.5 }); // Tint to orange/ginger color ginger.tint = 0xffa14e; ginger.scaleX = 1.7; ginger.scaleY = 1.7; self.width = ginger.width * 1.7; self.height = ginger.height * 1.7; self.hp = 3; self.speed = 7 + Math.random() * 2; self.scoreValue = 7; self.update = function () { self.y += self.speed; }; return self; }); // Bat enemy var Bat = Enemy.expand(function () { var self = Enemy.call(this); var bat = self.attachAsset('enemy_bat', { anchorX: 0.5, anchorY: 0.5 }); // Make bat much bigger so it stands out bat.scaleX = 3.5; bat.scaleY = 3.5; self.width = bat.width * 3.5; self.height = bat.height * 3.5; // Make bat very hard to kill self.hp = 30; // Make bat slower self.speed = 7 + Math.random() * 2.5; // Bat is worth 1000 coins self.scoreValue = 1000; self.update = function () { self.y += self.speed; self.x += Math.sin(LK.ticks / 12 + self._batWobble) * 8; }; self._batWobble = Math.random() * Math.PI * 2; return self; }); // Player Minecart var Minecart = Container.expand(function () { var self = Container.call(this); // Use selected player model from storage or default based on unlocks var assetId = 'minecart'; var selectedModel = storage.selectedPlayerModel || 'default'; if (selectedModel === 'skeleton' && typeof skeletonPlayerUnlocked !== 'undefined' && skeletonPlayerUnlocked) { assetId = 'Skeletonmodel'; } else if (selectedModel === 'player' && typeof playerUnlocked !== 'undefined' && playerUnlocked) { assetId = 'Player'; } else if (typeof skeletonPlayerUnlocked !== 'undefined' && skeletonPlayerUnlocked && selectedModel === 'default') { assetId = 'Skeletonmodel'; } else if (typeof playerUnlocked !== 'undefined' && playerUnlocked && selectedModel === 'default') { assetId = 'Player'; } var cart = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); self.width = cart.width; self.height = cart.height; self.weapon = 'gun'; // 'gun' or 'knife' self.weaponLevel = 1; // Upgrades increase this self.invincible = false; self.invincibleTimer = 0; self.attackCooldown = 0; self.knifeSwinging = false; self.knife = null; // Flash minecart when hit self.flash = function () { self.invincible = true; self.invincibleTimer = 60; // 1 second at 60fps tween(self, { alpha: 0.5 }, { duration: 100, easing: tween.easeOut, onFinish: function onFinish() { tween(self, { alpha: 1 }, { duration: 100 }); } }); }; // Switch weapon self.setWeapon = function (type) { self.weapon = type; }; // Upgrade weapon self.upgradeWeapon = function () { self.weaponLevel += 1; if (self.weaponLevel > 3) { self.weaponLevel = 3; } }; // Knife attack animation self.swingKnife = function () { if (self.knifeSwinging) { return; } self.knifeSwinging = true; if (!self.knife) { self.knife = self.attachAsset('knife', { anchorX: 0.1, anchorY: 0.5, x: self.width / 2 + 20, y: 0, rotation: 0 }); } self.knife.visible = true; self.knife.rotation = -0.7; tween(self.knife, { rotation: 0.7 }, { duration: 180, easing: tween.cubicOut, onFinish: function onFinish() { self.knife.visible = false; self.knifeSwinging = false; self.knife.rotation = -0.7; } }); }; return self; }); // Obstacle (rock) var Obstacle = Container.expand(function () { var self = Container.call(this); var rock = self.attachAsset('obstacle_rock', { anchorX: 0.5, anchorY: 0.5 }); self.width = rock.width; self.height = rock.height; self.speed = 14 + Math.random() * 6; self.update = function () { self.y += self.speed; }; return self; }); // Upgrade (gun or knife) var Upgrade = Container.expand(function () { var self = Container.call(this); self.kind = Math.random() < 0.5 ? 'gun' : 'knife'; var assetId = self.kind === 'gun' ? 'upgrade_gun' : 'upgrade_knife'; var upg = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); self.width = upg.width; self.height = upg.height; self.speed = 10 + Math.random() * 4; self.update = function () { self.y += self.speed; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x181818 }); /**** * Game Code ****/ // Game constants // Minecart (player) // Player's knife // Player's bullet // Enemy: Bat // Enemy: Goblin // Enemy: Skeleton // Obstacle: Rock // Upgrade: Gun // Upgrade: Knife // Maze background image (replace with your maze image asset id) var GAME_W = 2048; var GAME_H = 2732; var TRACK_LEFT = 300; var TRACK_RIGHT = GAME_W - 300; var PLAYER_Y = GAME_H - 350; // Game state var minecart = null; var bullets = []; var enemies = []; var obstacles = []; var upgrades = []; var dragNode = null; var lastTouchX = 0; var lastTouchY = 0; var scoreTxt = null; var lastScore = 0; var gameOver = false; var spawnTimer = 0; var obstacleTimer = 0; var upgradeTimer = 0; var difficulty = 1; var knifeHitEnemies = []; var lastKnifeSwingTick = -100; // Track if player has unlocked the upgraded knife by beating super hard mode var upgradedKnifeUnlocked = false; // Track if player has chosen to use upgraded knife in normal mode (set via menu) var useUpgradedKnifeNextGame = false; // Track if player has unlocked the player (250 kills in super hard mode) var playerUnlocked = false; var playerKills = 0; // Track skeleton player unlock (900 kills in normal mode) var skeletonPlayerUnlocked = storage.skeletonPlayerUnlocked || false; var normalModeKills = storage.normalModeKills || 0; // Try to load from persistent storage if available if (typeof storage !== "undefined" && storage.upgradedKnifeUnlocked) { upgradedKnifeUnlocked = true; } if (typeof storage !== "undefined" && storage.playerUnlocked) { playerUnlocked = true; } if (typeof storage !== "undefined" && storage.playerKills) { playerKills = parseInt(storage.playerKills, 10) || 0; } // Always reset useUpgradedKnifeNextGame on load useUpgradedKnifeNextGame = false; // --- Main Menu Overlay --- var mainMenuOverlay = null; var mainMenuTitle = null; var normalBtn = null; var hardBtn = null; var menuActive = true; function showMainMenu() { menuActive = true; // Overlay container mainMenuOverlay = new Container(); // Semi-transparent dark background var bg = LK.getAsset('maze_bg', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: GAME_W, height: GAME_H }); bg.alpha = 0.82; mainMenuOverlay.addChild(bg); // Title mainMenuTitle = new Text2("Boneshaft 1: UPGRADED", { size: 180, fill: "#fff", font: "Impact, Arial Black, Tahoma" }); mainMenuTitle.anchor.set(0.5, 0.5); mainMenuTitle.x = GAME_W / 2; mainMenuTitle.y = 600; mainMenuOverlay.addChild(mainMenuTitle); // Normal Mode Button var normalLabel = "Normal Mode"; if (upgradedKnifeUnlocked) { normalLabel += " (Upgraded Knife!)"; } normalBtn = new Text2(normalLabel, { size: 120, fill: 0x44FF44, font: "Impact, Arial Black, Tahoma" }); normalBtn.anchor.set(0.5, 0.5); normalBtn.x = GAME_W / 2; normalBtn.y = 1100; mainMenuOverlay.addChild(normalBtn); // Super Hard Mode Button hardBtn = new Text2("Super Hard Mode", { size: 120, fill: 0xFF4444, font: "Impact, Arial Black, Tahoma" }); hardBtn.anchor.set(0.5, 0.5); hardBtn.x = GAME_W / 2; hardBtn.y = 1350; mainMenuOverlay.addChild(hardBtn); // --- Unlocked Section --- var unlockedTitle = new Text2("Unlocked", { size: 90, fill: "#fff", font: "Impact, Arial Black, Tahoma" }); unlockedTitle.anchor.set(0.5, 0.5); unlockedTitle.x = GAME_W / 2; unlockedTitle.y = 1550; mainMenuOverlay.addChild(unlockedTitle); var unlockedKnifeText, unlockedKnifeIcon; var unlockedPlayerText, unlockedPlayerIcon; var playerUnlocked = false; var playerKills = 0; // Try to load from persistent storage if available if (typeof storage !== "undefined" && storage.playerUnlocked) { playerUnlocked = true; } if (typeof storage !== "undefined" && storage.playerKills) { playerKills = parseInt(storage.playerKills, 10) || 0; } if (upgradedKnifeUnlocked) { var showKnifeConfirmDialog = function showKnifeConfirmDialog() { if (knifeConfirmDialog) return; knifeConfirmDialog = new Container(); // Dialog background var dialogBg = LK.getAsset('maze_bg', { anchorX: 0, anchorY: 0, x: GAME_W / 2 - 400, y: 1200, width: 800, height: 400 }); dialogBg.alpha = 0.93; knifeConfirmDialog.addChild(dialogBg); // Dialog text var dialogText = new Text2("Use Upgraded Knife in Normal Mode?", { size: 60, fill: "#fff", font: "Impact, Arial Black, Tahoma" }); dialogText.anchor.set(0.5, 0.5); dialogText.x = GAME_W / 2; dialogText.y = 1320; knifeConfirmDialog.addChild(dialogText); // Yes button var yesBtn = new Text2("Yes", { size: 90, fill: 0x44FF44, font: "Impact, Arial Black, Tahoma" }); yesBtn.anchor.set(0.5, 0.5); yesBtn.x = GAME_W / 2 - 140; yesBtn.y = 1450; knifeConfirmDialog.addChild(yesBtn); // No button var noBtn = new Text2("No", { size: 90, fill: 0xFF4444, font: "Impact, Arial Black, Tahoma" }); noBtn.anchor.set(0.5, 0.5); noBtn.x = GAME_W / 2 + 140; noBtn.y = 1450; knifeConfirmDialog.addChild(noBtn); // Button handlers yesBtn.interactive = true; noBtn.interactive = true; yesBtn.down = function () { // Set a global flag to use upgraded knife in normal mode useUpgradedKnifeNextGame = true; // Remove dialog if (knifeConfirmDialog) { knifeConfirmDialog.destroy(); knifeConfirmDialog = null; } }; noBtn.down = function () { // Just close dialog if (knifeConfirmDialog) { knifeConfirmDialog.destroy(); knifeConfirmDialog = null; } }; yesBtn.on('down', yesBtn.down); noBtn.on('down', noBtn.down); mainMenuOverlay.addChild(knifeConfirmDialog); }; // Handler for both icon and text var unlockedKnifeDownHandler = function unlockedKnifeDownHandler() { showKnifeConfirmDialog(); }; unlockedKnifeText = new Text2("Upgraded Knife", { size: 80, fill: 0xFFD700, font: "Impact, Arial Black, Tahoma" }); unlockedKnifeIcon = LK.getAsset('upgrade_knife', { anchorX: 0.5, anchorY: 0.5, x: GAME_W / 2 - 120, y: 1650 }); unlockedKnifeText.anchor.set(0, 0.5); unlockedKnifeText.x = GAME_W / 2 - 60; unlockedKnifeText.y = 1650; mainMenuOverlay.addChild(unlockedKnifeIcon); mainMenuOverlay.addChild(unlockedKnifeText); // Make the unlocked knife section pressable unlockedKnifeText.interactive = true; unlockedKnifeIcon.interactive = true; // Confirmation dialog container (created on demand) var knifeConfirmDialog = null; unlockedKnifeText.down = unlockedKnifeDownHandler; unlockedKnifeIcon.down = unlockedKnifeDownHandler; unlockedKnifeText.on('down', unlockedKnifeDownHandler); unlockedKnifeIcon.on('down', unlockedKnifeDownHandler); } else { unlockedKnifeText = new Text2("??? (Beat Super Hard Mode to unlock)", { size: 70, fill: 0x888888, font: "Impact, Arial Black, Tahoma" }); unlockedKnifeText.anchor.set(0.5, 0.5); unlockedKnifeText.x = GAME_W / 2; unlockedKnifeText.y = 1650; mainMenuOverlay.addChild(unlockedKnifeText); } // --- Player Unlock Section --- var playerSectionY = 1800; if (playerUnlocked) { unlockedPlayerText = new Text2("Player (Unlocked!)", { size: 80, fill: 0x00BFFF, font: "Impact, Arial Black, Tahoma" }); unlockedPlayerIcon = LK.getAsset('Player', { anchorX: 0.5, anchorY: 0.5, x: GAME_W / 2 - 120, y: playerSectionY }); unlockedPlayerText.anchor.set(0, 0.5); unlockedPlayerText.x = GAME_W / 2 - 60; unlockedPlayerText.y = playerSectionY; mainMenuOverlay.addChild(unlockedPlayerIcon); mainMenuOverlay.addChild(unlockedPlayerText); // Make player section interactive to switch model var playerDownHandler = function playerDownHandler() { // Set player model as selected player model storage.selectedPlayerModel = 'player'; }; unlockedPlayerText.interactive = true; unlockedPlayerIcon.interactive = true; unlockedPlayerText.down = playerDownHandler; unlockedPlayerIcon.down = playerDownHandler; unlockedPlayerText.on('down', playerDownHandler); unlockedPlayerIcon.on('down', playerDownHandler); } else { var killsNeeded = 250 - playerKills; if (killsNeeded < 0) killsNeeded = 0; unlockedPlayerText = new Text2("??? (Get 250 kills in Super Hard Mode to unlock)\nKills: " + playerKills + " / 250", { size: 60, fill: 0x888888, font: "Impact, Arial Black, Tahoma" }); unlockedPlayerText.anchor.set(0.5, 0.5); unlockedPlayerText.x = GAME_W / 2; unlockedPlayerText.y = playerSectionY; mainMenuOverlay.addChild(unlockedPlayerText); } // --- Skeleton Player Unlock Section --- var skeletonSectionY = 1950; if (skeletonPlayerUnlocked) { var unlockedSkeletonText = new Text2("Skeleton Player (Unlocked!)", { size: 80, fill: 0xFFD700, font: "Impact, Arial Black, Tahoma" }); var unlockedSkeletonIcon = LK.getAsset('Skeletonmodel', { anchorX: 0.5, anchorY: 0.5, x: GAME_W / 2 - 120, y: skeletonSectionY }); unlockedSkeletonText.anchor.set(0, 0.5); unlockedSkeletonText.x = GAME_W / 2 - 60; unlockedSkeletonText.y = skeletonSectionY; mainMenuOverlay.addChild(unlockedSkeletonIcon); mainMenuOverlay.addChild(unlockedSkeletonText); // Make skeleton player section interactive to switch model var skeletonPlayerDownHandler = function skeletonPlayerDownHandler() { // Set skeleton model as selected player model storage.selectedPlayerModel = 'skeleton'; }; unlockedSkeletonText.interactive = true; unlockedSkeletonIcon.interactive = true; unlockedSkeletonText.down = skeletonPlayerDownHandler; unlockedSkeletonIcon.down = skeletonPlayerDownHandler; unlockedSkeletonText.on('down', skeletonPlayerDownHandler); unlockedSkeletonIcon.on('down', skeletonPlayerDownHandler); } else { var killsNeeded = 900 - normalModeKills; if (killsNeeded < 0) killsNeeded = 0; var unlockedSkeletonText = new Text2("??? (Get 900 kills in Normal Mode to unlock)\nKills: " + normalModeKills + " / 900", { size: 60, fill: 0x888888, font: "Impact, Arial Black, Tahoma" }); unlockedSkeletonText.anchor.set(0.5, 0.5); unlockedSkeletonText.x = GAME_W / 2; unlockedSkeletonText.y = skeletonSectionY; mainMenuOverlay.addChild(unlockedSkeletonText); } // Add overlay to game game.addChild(mainMenuOverlay); // Button hit areas normalBtn.interactive = true; hardBtn.interactive = true; // Add direct down event handlers to buttons for pressable effect normalBtn.down = function (x, y, obj) { if (!mainMenuOverlay) { return; } selectMode('normal'); }; hardBtn.down = function (x, y, obj) { if (!mainMenuOverlay) { return; } selectMode('hard'); }; normalBtn.on('down', normalBtn.down); hardBtn.on('down', hardBtn.down); // Touch/click handlers mainMenuOverlay.down = function (x, y, obj) { // Defensive: If overlay is destroyed or null, do nothing if (!mainMenuOverlay) { return; } // Convert to overlay local coordinates var local = mainMenuOverlay.toLocal({ x: x, y: y }); // Check if normalBtn was pressed if (pointInText(normalBtn, local.x, local.y)) { selectMode('normal'); } else if (pointInText(hardBtn, local.x, local.y)) { selectMode('hard'); } }; // Attach to overlay mainMenuOverlay.on('down', mainMenuOverlay.down); // Also attach to game for mobile game.down = function (x, y, obj) { if (menuActive && mainMenuOverlay) { mainMenuOverlay.down(x, y, obj); return; } if (gameOver) { return; } dragNode = minecart; lastTouchX = x; lastTouchY = y; }; } // Helper: check if point is inside Text2 bounds function pointInText(txt, x, y) { var w = txt.width; var h = txt.height; var tx = txt.x - w * txt.anchor.x; var ty = txt.y - h * txt.anchor.y; return x >= tx && x <= tx + w && y >= ty && y <= ty + h; } // Remove menu and start game function selectMode(mode) { if (!mainMenuOverlay) { return; } menuActive = false; mainMenuOverlay.destroy(); mainMenuOverlay = null; // Set difficulty and level mode if (mode === 'normal') { // Set up original game state for normal mode difficulty = 1; game._superHardMode = false; // Remove any super hard mode UI if present if (game._batHealthBarBg) { LK.gui.top.removeChild(game._batHealthBarBg); game._batHealthBarBg = null; } if (game._batHealthBar) { LK.gui.top.removeChild(game._batHealthBar); game._batHealthBar = null; } game._batEnemy = null; game._batMaxHp = null; // Place player at original starting position if (minecart) { minecart.x = GAME_W / 2; minecart.y = PLAYER_Y; // If upgraded knife is unlocked AND player chose to use it, start with knife and max level if (upgradedKnifeUnlocked && useUpgradedKnifeNextGame) { minecart.setWeapon('knife'); minecart.weaponLevel = 3; useUpgradedKnifeNextGame = false; // Reset after use } } } else if (mode === 'hard') { difficulty = 7; game._superHardMode = true; } // Reset game state LK.setScore(0); if (scoreTxt) { scoreTxt.setText('0'); } if (minecart) { minecart.x = GAME_W / 2; minecart.y = PLAYER_Y; minecart.weapon = 'gun'; minecart.weaponLevel = 1; minecart.invincible = false; minecart.invincibleTimer = 0; minecart.attackCooldown = 0; minecart.knifeSwinging = false; minecart.setWeapon('gun'); } bullets = []; enemies = []; obstacles = []; upgrades = []; spawnTimer = 0; obstacleTimer = 60; upgradeTimer = 600; knifeHitEnemies = []; lastKnifeSwingTick = -100; gameOver = false; // If super hard mode, set up new level if (game._superHardMode) { // Remove all enemies, obstacles, upgrades, and spawn a new set for the new level for (var i = 0; i < enemies.length; ++i) { enemies[i].destroy(); } for (var i = 0; i < obstacles.length; ++i) { obstacles[i].destroy(); } for (var i = 0; i < upgrades.length; ++i) { upgrades[i].destroy(); } enemies = []; obstacles = []; upgrades = []; // Place player at the same starting position as normal mode minecart.x = GAME_W / 2; minecart.y = PLAYER_Y; // Optionally, change background or add new obstacles/enemy patterns // Example: spawn a wall of goblins at the top for (var gx = TRACK_LEFT + 120; gx < TRACK_RIGHT - 120; gx += 180) { var g = new Goblin(); g.x = gx; g.y = 0; enemies.push(g); game.addChild(g); } // Example: spawn a bat immediately var bat = new Bat(); bat.x = GAME_W / 2; bat.y = -100; enemies.push(bat); game.addChild(bat); // Set up bat health bar UI for super hard mode var barWidth = 1700; var barHeight = 90; var barBg = LK.getAsset('enemy_goblin', { width: barWidth, height: barHeight, color: 0x222222, anchorX: 0.5, anchorY: 0 }); barBg.x = GAME_W / 2; barBg.y = 10; var bar = LK.getAsset('enemy_goblin', { width: barWidth - 20, height: barHeight - 18, color: 0xff2222, anchorX: 0.5, anchorY: 0 }); bar.x = GAME_W / 2; bar.y = 19; LK.gui.top.addChild(barBg); LK.gui.top.addChild(bar); game._batHealthBarBg = barBg; game._batHealthBar = bar; game._batEnemy = bat; game._batMaxHp = bat.hp; } } // Add maze background image var mazeBg = LK.getAsset('maze_bg', { anchorX: 0, anchorY: 0, x: 0, y: 0, width: GAME_W, height: GAME_H }); game.addChild(mazeBg); // Score display scoreTxt = new Text2('0', { size: 120, fill: "#fff" }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // Create player minecart = new Minecart(); game.addChild(minecart); minecart.x = GAME_W / 2; minecart.y = PLAYER_Y; // Vertical shooter: drag minecart only in X, fixed Y game.down = function (x, y, obj) { if (gameOver) { return; } dragNode = minecart; lastTouchX = x; lastTouchY = y; }; game.up = function (x, y, obj) { dragNode = null; }; game.move = function (x, y, obj) { if (gameOver) { return; } if (dragNode === minecart) { // Only allow movement in X, clamp to track var nx = x; if (nx < TRACK_LEFT) { nx = TRACK_LEFT; } if (nx > TRACK_RIGHT) { nx = TRACK_RIGHT; } minecart.x = nx; minecart.y = PLAYER_Y; } }; // Tap to attack (shoot or knife) game.tap = function (x, y, obj) { if (gameOver) { return; } if (minecart.weapon === 'gun') { if (minecart.attackCooldown <= 0) { fireBullet(); minecart.attackCooldown = 12 - minecart.weaponLevel * 2; // Faster with upgrades if (minecart.attackCooldown < 4) { minecart.attackCooldown = 4; } } } else if (minecart.weapon === 'knife') { if (LK.ticks - lastKnifeSwingTick > 18) { minecart.swingKnife(); lastKnifeSwingTick = LK.ticks; } } }; // Fire bullet(s) function fireBullet() { var spread = minecart.weaponLevel; var angleSpread = 0.18; // radians, about 10 degrees var basePower = 1 + Math.floor((minecart.weaponLevel - 1) / 2); // Level 1-2: 1, Level 3: 2 for (var i = 0; i < spread; ++i) { var b = new Bullet(); // Spread bullets horizontally and with angle var offset = i - (spread - 1) / 2; b.x = minecart.x + offset * 40; b.y = minecart.y - minecart.height / 2 - 10; // Angle for spread: center is 0, sides are negative/positive if (spread > 1) { b.angle = offset * angleSpread; } b.power = basePower; bullets.push(b); game.addChild(b); } } // Main update loop game.update = function () { if (menuActive) { return; } if (gameOver) { return; } // Difficulty scaling if (LK.ticks % 300 === 0 && difficulty < 10) { difficulty += 1; } // Attack cooldown if (minecart && minecart.attackCooldown > 0) { minecart.attackCooldown -= 1; } // Invincibility timer if (minecart && minecart.invincible) { minecart.invincibleTimer -= 1; if (minecart.invincibleTimer <= 0) { minecart.invincible = false; } } // Bullets update for (var i = bullets.length - 1; i >= 0; --i) { var b = bullets[i]; b.update(); if (b.y < -50) { b.destroy(); bullets.splice(i, 1); continue; } // Bullet hits enemy for (var j = enemies.length - 1; j >= 0; --j) { var e = enemies[j]; if (b.intersects(e)) { // Use bullet power for damage e.hp -= b.power || 1; // If this is the Bat, update health bar if (game._batEnemy && e === game._batEnemy && game._batHealthBar) { var percent = Math.max(0, e.hp) / game._batMaxHp; // Clamp width to minimum 0 game._batHealthBar.width = 1680 * percent; // Optionally, you can tint the bar as it gets low (not required) } b.destroy(); bullets.splice(i, 1); if (e.hp <= 0) { addScore(e.scoreValue); e.destroy(); enemies.splice(j, 1); // If Bat is dead, remove health bar UI if (game._batEnemy && e === game._batEnemy) { if (game._batHealthBarBg) { LK.gui.top.removeChild(game._batHealthBarBg); game._batHealthBarBg = null; } if (game._batHealthBar) { LK.gui.top.removeChild(game._batHealthBar); game._batHealthBar = null; } game._batEnemy = null; // If in super hard mode, check for score 1000 to win and unlock upgraded knife if (game._superHardMode && !upgradedKnifeUnlocked) { if (LK.getScore() >= 1000) { upgradedKnifeUnlocked = true; if (typeof storage !== "undefined") { storage.upgradedKnifeUnlocked = true; } // Show a win popup LK.showYouWin(); gameOver = true; return; } // Otherwise, keep playing until score 1000 is reached } } // --- Player unlock logic: count kills in super hard mode --- if (game._superHardMode && !playerUnlocked) { if (typeof playerKills === "undefined") playerKills = 0; playerKills++; if (playerKills >= 250) { playerUnlocked = true; if (typeof storage !== "undefined") { storage.playerUnlocked = true; } } if (typeof storage !== "undefined") { storage.playerKills = playerKills; } } // --- Skeleton player unlock logic: count kills in normal mode --- if (!game._superHardMode && !skeletonPlayerUnlocked) { if (typeof normalModeKills === "undefined") normalModeKills = 0; normalModeKills++; storage.normalModeKills = normalModeKills; if (normalModeKills >= 900) { skeletonPlayerUnlocked = true; storage.skeletonPlayerUnlocked = true; } } } break; } } } // Enemies update (vertical shooter: always visible, move down) for (var i = enemies.length - 1; i >= 0; --i) { var e = enemies[i]; e.update(); // Remove if out of bounds if (e.y > GAME_H + 200) { e.destroy(); enemies.splice(i, 1); continue; } // Enemy hits minecart if (minecart && minecart !== null && !minecart.invincible && e.intersects(minecart)) { // Check if Bat killed the player for jump scare if (game._batEnemy && e === game._batEnemy) { // Show a big jump scare: flash white, show a big FACE in the center, and play a loud sound if available LK.effects.flashScreen(0xffffff, 1200); // Create a big FACE image in the center (using the goblin face as a placeholder for a jumpscare face) var jumpscareFace = LK.getAsset('enemy_goblin', { anchorX: 0.5, anchorY: 0.5, scaleX: 2, scaleY: 2, x: GAME_W / 2, y: GAME_H / 2 }); jumpscareFace.alpha = 0; game.addChild(jumpscareFace); // Animate the face: scale up and fade in quickly, then fade out tween(jumpscareFace, { alpha: 1, scaleX: 7, scaleY: 7 }, { duration: 120, onFinish: function onFinish() { tween(jumpscareFace, { alpha: 0 }, { duration: 700, delay: 350, onFinish: function onFinish() { jumpscareFace.destroy(); } }); } }); // Optionally, play a loud sound here if you have a jumpscare sound asset } minecart.flash(); LK.effects.flashScreen(0xff0000, 600); // End game LK.showGameOver(); gameOver = true; return; } // Enemy hit by knife if (minecart.weapon === 'knife' && minecart.knife && minecart.knife.visible && !knifeHitEnemies.includes(e)) { // Knife is in swing, check collision var knifeGlobal = minecart.toGlobal(minecart.knife.position); var knifeRect = new Rectangle(knifeGlobal.x - 30, knifeGlobal.y - 12, 60, 24); var enemyRect = new Rectangle(e.x - e.width / 2, e.y - e.height / 2, e.width, e.height); if (rectsIntersect(knifeRect, enemyRect)) { e.hp -= 1; // If this is the Bat, update health bar if (game._batEnemy && e === game._batEnemy && game._batHealthBar) { var percent = Math.max(0, e.hp) / game._batMaxHp; game._batHealthBar.width = 1680 * percent; } knifeHitEnemies.push(e); if (e.hp <= 0) { addScore(e.scoreValue); e.destroy(); enemies.splice(i, 1); // If Bat is dead, remove health bar UI if (game._batEnemy && e === game._batEnemy) { if (game._batHealthBarBg) { LK.gui.top.removeChild(game._batHealthBarBg); game._batHealthBarBg = null; } if (game._batHealthBar) { LK.gui.top.removeChild(game._batHealthBar); game._batHealthBar = null; } game._batEnemy = null; } } } } } // Reset knife hit list if knife not swinging if (!minecart || !minecart.knife || !minecart.knife.visible) { knifeHitEnemies = []; } // Obstacles update for (var i = obstacles.length - 1; i >= 0; --i) { var o = obstacles[i]; o.update(); if (o.y > GAME_H + 100) { o.destroy(); obstacles.splice(i, 1); continue; } // Obstacle hits minecart if (minecart && !minecart.invincible && o.intersects(minecart)) { minecart.flash(); LK.effects.flashScreen(0xff0000, 600); LK.showGameOver(); gameOver = true; return; } } // Upgrades update for (var i = upgrades.length - 1; i >= 0; --i) { var u = upgrades[i]; u.update(); if (u.y > GAME_H + 100) { u.destroy(); upgrades.splice(i, 1); continue; } // Pickup if (u.intersects(minecart)) { if (u.kind === 'gun') { minecart.setWeapon('gun'); minecart.upgradeWeapon(); } else { minecart.setWeapon('knife'); minecart.upgradeWeapon(); } u.destroy(); upgrades.splice(i, 1); } } // Spawning enemies spawnTimer -= 1; if (spawnTimer <= 0) { spawnEnemy(); spawnTimer = Math.max(24, 60 - difficulty * 4 - Math.floor(Math.random() * 10)); } // Spawning obstacles obstacleTimer -= 1; if (obstacleTimer <= 0) { if (Math.random() < 0.5 + difficulty * 0.04) { spawnObstacle(); } obstacleTimer = 90 + Math.floor(Math.random() * 60) - difficulty * 4; if (obstacleTimer < 30) { obstacleTimer = 30; } } // Spawning upgrades upgradeTimer -= 1; if (upgradeTimer <= 0) { if (game._superHardMode) { // Easier: upgrades spawn more often in super hard mode if (Math.random() < 0.38) { spawnUpgrade(); } upgradeTimer = 340 + Math.floor(Math.random() * 120); } else { if (Math.random() < 0.18) { spawnUpgrade(); } upgradeTimer = 600 + Math.floor(Math.random() * 300); } } // Update score display if (LK.getScore() !== lastScore) { scoreTxt.setText(LK.getScore()); lastScore = LK.getScore(); } // Win condition for super hard mode: reach score 1000 if (game._superHardMode && !upgradedKnifeUnlocked && LK.getScore() >= 1000) { upgradedKnifeUnlocked = true; if (typeof storage !== "undefined") { storage.upgradedKnifeUnlocked = true; } LK.showYouWin(); gameOver = true; return; } }; // Spawn enemy at random X at the top (vertical shooter style) function spawnEnemy() { // If in super hard mode, spawn new enemy patterns if (game._superHardMode) { // Example: spawn a slightly slower skeleton and a goblin together (easier) var e1 = new Skeleton(); e1.speed = 10 + Math.random() * 2.5; e1.x = TRACK_LEFT + 120 + Math.random() * (TRACK_RIGHT - TRACK_LEFT - 240); e1.y = -80; enemies.push(e1); game.addChild(e1); var e2 = new Goblin(); e2.speed = 8 + Math.random() * 2; e2.x = TRACK_LEFT + 120 + Math.random() * (TRACK_RIGHT - TRACK_LEFT - 240); e2.y = -180; enemies.push(e2); game.addChild(e2); // Occasionally spawn a Bat if (Math.random() < 0.12) { var bat = new Bat(); bat.x = TRACK_LEFT + 120 + Math.random() * (TRACK_RIGHT - TRACK_LEFT - 240); bat.y = -200; enemies.push(bat); game.addChild(bat); // Set up bat health bar UI var barWidth = 1700; var barHeight = 90; var barBg = LK.getAsset('enemy_goblin', { width: barWidth, height: barHeight, color: 0x222222, anchorX: 0.5, anchorY: 0 }); barBg.x = GAME_W / 2; barBg.y = 10; var bar = LK.getAsset('enemy_goblin', { width: barWidth - 20, height: barHeight - 18, color: 0xff2222, anchorX: 0.5, anchorY: 0 }); bar.x = GAME_W / 2; bar.y = 19; LK.gui.top.addChild(barBg); LK.gui.top.addChild(bar); game._batHealthBarBg = barBg; game._batHealthBar = bar; game._batEnemy = bat; game._batMaxHp = bat.hp; } // Sometimes spawn a ginger if (Math.random() < 0.25) { var g = new Ginger(); g.x = TRACK_LEFT + 120 + Math.random() * (TRACK_RIGHT - TRACK_LEFT - 240); g.y = -120; enemies.push(g); game.addChild(g); } return; } // --- Normal mode enemy spawn --- var enemyType = Math.random(); var e = null; // Bat is now extremely rare: only spawn if enemyType < 0.02 (2% chance) if (enemyType < 0.02) { e = new Bat(); // Show warning text and play creepy music when Bat spawns if (!game._batWarningText) { // Create warning text if not already present var batWarning = new Text2("That is not you.", { size: 120, fill: 0xFF2222, font: "Impact, Arial Black, Tahoma" }); batWarning.anchor.set(0.5, 0.5); batWarning.alpha = 0.0; game._batWarningText = batWarning; // Add to LK.gui.center for true center of screen LK.gui.center.addChild(batWarning); } // Fade in text var warningText = game._batWarningText; warningText.alpha = 0.0; tween(warningText, { alpha: 1 }, { duration: 400, onFinish: function onFinish() { // Fade out after 1.2s tween(warningText, { alpha: 0 }, { duration: 800, delay: 1200 }); } }); // Play creepy music (music asset id: 'creepy_bat') LK.playMusic('creepy_bat', { fade: { start: 0, end: 1, duration: 800 } }); // --- Bat Health Bar UI --- // Remove any previous bat health bar if (game._batHealthBarBg) { LK.gui.top.removeChild(game._batHealthBarBg); game._batHealthBarBg = null; } if (game._batHealthBar) { LK.gui.top.removeChild(game._batHealthBar); game._batHealthBar = null; } // Create background bar (much bigger, at the very top) var barWidth = 1700; var barHeight = 90; var barBg = LK.getAsset('enemy_goblin', { width: barWidth, height: barHeight, color: 0x222222, anchorX: 0.5, anchorY: 0 }); barBg.x = GAME_W / 2; barBg.y = 10; // Very top of the screen // Create health bar (red, much bigger) var bar = LK.getAsset('enemy_goblin', { width: barWidth - 20, height: barHeight - 18, color: 0xff2222, anchorX: 0.5, anchorY: 0 }); bar.x = GAME_W / 2; bar.y = 19; // Just below the background bar // Add to GUI LK.gui.top.addChild(barBg); LK.gui.top.addChild(bar); game._batHealthBarBg = barBg; game._batHealthBar = bar; // Store reference to the Bat instance for health bar updates game._batEnemy = e; game._batMaxHp = e.hp; } else if (enemyType < 0.10) { // Ginger is rare, 8% chance (between 2% and 10%) e = new Ginger(); } else if (enemyType < 0.65) { e = new Goblin(); } else { e = new Skeleton(); } // Spawn at random X at the top of the screen e.x = TRACK_LEFT + 80 + Math.random() * (TRACK_RIGHT - TRACK_LEFT - 160); e.y = -80; enemies.push(e); game.addChild(e); } // Spawn obstacle function spawnObstacle() { var o = new Obstacle(); o.x = TRACK_LEFT + 80 + Math.random() * (TRACK_RIGHT - TRACK_LEFT - 160); o.y = -60; obstacles.push(o); game.addChild(o); } // Spawn upgrade function spawnUpgrade() { var u = new Upgrade(); u.x = TRACK_LEFT + 80 + Math.random() * (TRACK_RIGHT - TRACK_LEFT - 160); u.y = -60; upgrades.push(u); game.addChild(u); } // Add score function addScore(val) { LK.setScore(LK.getScore() + val); } // Rectangle intersection helper function rectsIntersect(r1, r2) { return !(r2.x > r1.x + r1.width || r2.x + r2.width < r1.x || r2.y > r1.y + r1.height || r2.y + r2.height < r1.y); } // Touch to attack (simulate tap on mobile) game.down = function (origDown) { return function (x, y, obj) { if (origDown) { origDown(x, y, obj); } game.tap(x, y, obj); }; }(game.down); // Reset game state on game over LK.on('gameover', function () { gameOver = true; setTimeout(function () { showMainMenu(); }, 600); }); // Track run count for Bat spawn logic if (typeof game._runCount === "undefined") { game._runCount = 0; } LK.on('newgame', function () { game._runCount = (game._runCount || 0) + 1; gameOver = false; LK.setScore(0); if (scoreTxt) { scoreTxt.setText('0'); } // Remove all objects for (var i = 0; i < bullets.length; ++i) { bullets[i].destroy(); } for (var i = 0; i < enemies.length; ++i) { enemies[i].destroy(); } for (var i = 0; i < obstacles.length; ++i) { obstacles[i].destroy(); } for (var i = 0; i < upgrades.length; ++i) { upgrades[i].destroy(); } bullets = []; enemies = []; obstacles = []; upgrades = []; minecart.x = GAME_W / 2; minecart.y = PLAYER_Y; minecart.weapon = 'gun'; minecart.weaponLevel = 1; minecart.invincible = false; minecart.invincibleTimer = 0; minecart.attackCooldown = 0; minecart.knifeSwinging = false; minecart.setWeapon('gun'); difficulty = 1; spawnTimer = 0; obstacleTimer = 0; upgradeTimer = 0; knifeHitEnemies = []; lastKnifeSwingTick = -100; // Remove Bat health bar UI if present if (game._batHealthBarBg) { LK.gui.top.removeChild(game._batHealthBarBg); game._batHealthBarBg = null; } if (game._batHealthBar) { LK.gui.top.removeChild(game._batHealthBar); game._batHealthBar = null; } game._batEnemy = null; game._batMaxHp = null; }); // Initial state spawnTimer = 0; obstacleTimer = 60; upgradeTimer = 600; difficulty = 1; gameOver = false; LK.setScore(0); if (scoreTxt) { scoreTxt.setText('0'); } // Show main menu on load showMainMenu();
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Bullet
var Bullet = Container.expand(function () {
var self = Container.call(this);
var bullet = self.attachAsset('bullet', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = bullet.width;
self.height = bullet.height;
self.speed = -36; // Upwards
self.power = 1; // Default bullet power
self.angle = 0; // For spread
self.update = function () {
if (self.angle !== 0) {
// Move in angle for spread
self.x += Math.sin(self.angle) * 12;
self.y += Math.cos(self.angle) * self.speed;
} else {
self.y += self.speed;
}
};
return self;
});
// Enemy base class
var Enemy = Container.expand(function () {
var self = Container.call(this);
self.type = 'enemy';
self.hp = 1;
self.speed = 8;
self.scoreValue = 1;
self.update = function () {};
return self;
});
// Skeleton enemy
var Skeleton = Enemy.expand(function () {
var self = Enemy.call(this);
var skeleton = self.attachAsset('enemy_skeleton', {
anchorX: 0.5,
anchorY: 0.5
});
// Make skeleton bigger
skeleton.scaleX = 1.7;
skeleton.scaleY = 1.7;
self.width = skeleton.width * 1.7;
self.height = skeleton.height * 1.7;
self.hp = 1;
// Make skeleton slower
self.speed = 6 + Math.random() * 2.5;
self.scoreValue = 2;
self.update = function () {
self.y += self.speed;
};
return self;
});
// Goblin enemy
var Goblin = Enemy.expand(function () {
var self = Enemy.call(this);
var goblin = self.attachAsset('enemy_goblin', {
anchorX: 0.5,
anchorY: 0.5
});
// Make goblin bigger
goblin.scaleX = 1.7;
goblin.scaleY = 1.7;
self.width = goblin.width * 1.7;
self.height = goblin.height * 1.7;
self.hp = 2;
// Make goblin slower
self.speed = 5 + Math.random() * 2;
self.scoreValue = 3;
self.update = function () {
self.y += self.speed;
};
return self;
});
// Ginger enemy
var Ginger = Enemy.expand(function () {
var self = Enemy.call(this);
// Use goblin as base, but with different color and stats
var ginger = self.attachAsset('enemy_goblin', {
anchorX: 0.5,
anchorY: 0.5
});
// Tint to orange/ginger color
ginger.tint = 0xffa14e;
ginger.scaleX = 1.7;
ginger.scaleY = 1.7;
self.width = ginger.width * 1.7;
self.height = ginger.height * 1.7;
self.hp = 3;
self.speed = 7 + Math.random() * 2;
self.scoreValue = 7;
self.update = function () {
self.y += self.speed;
};
return self;
});
// Bat enemy
var Bat = Enemy.expand(function () {
var self = Enemy.call(this);
var bat = self.attachAsset('enemy_bat', {
anchorX: 0.5,
anchorY: 0.5
});
// Make bat much bigger so it stands out
bat.scaleX = 3.5;
bat.scaleY = 3.5;
self.width = bat.width * 3.5;
self.height = bat.height * 3.5;
// Make bat very hard to kill
self.hp = 30;
// Make bat slower
self.speed = 7 + Math.random() * 2.5;
// Bat is worth 1000 coins
self.scoreValue = 1000;
self.update = function () {
self.y += self.speed;
self.x += Math.sin(LK.ticks / 12 + self._batWobble) * 8;
};
self._batWobble = Math.random() * Math.PI * 2;
return self;
});
// Player Minecart
var Minecart = Container.expand(function () {
var self = Container.call(this);
// Use selected player model from storage or default based on unlocks
var assetId = 'minecart';
var selectedModel = storage.selectedPlayerModel || 'default';
if (selectedModel === 'skeleton' && typeof skeletonPlayerUnlocked !== 'undefined' && skeletonPlayerUnlocked) {
assetId = 'Skeletonmodel';
} else if (selectedModel === 'player' && typeof playerUnlocked !== 'undefined' && playerUnlocked) {
assetId = 'Player';
} else if (typeof skeletonPlayerUnlocked !== 'undefined' && skeletonPlayerUnlocked && selectedModel === 'default') {
assetId = 'Skeletonmodel';
} else if (typeof playerUnlocked !== 'undefined' && playerUnlocked && selectedModel === 'default') {
assetId = 'Player';
}
var cart = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.width = cart.width;
self.height = cart.height;
self.weapon = 'gun'; // 'gun' or 'knife'
self.weaponLevel = 1; // Upgrades increase this
self.invincible = false;
self.invincibleTimer = 0;
self.attackCooldown = 0;
self.knifeSwinging = false;
self.knife = null;
// Flash minecart when hit
self.flash = function () {
self.invincible = true;
self.invincibleTimer = 60; // 1 second at 60fps
tween(self, {
alpha: 0.5
}, {
duration: 100,
easing: tween.easeOut,
onFinish: function onFinish() {
tween(self, {
alpha: 1
}, {
duration: 100
});
}
});
};
// Switch weapon
self.setWeapon = function (type) {
self.weapon = type;
};
// Upgrade weapon
self.upgradeWeapon = function () {
self.weaponLevel += 1;
if (self.weaponLevel > 3) {
self.weaponLevel = 3;
}
};
// Knife attack animation
self.swingKnife = function () {
if (self.knifeSwinging) {
return;
}
self.knifeSwinging = true;
if (!self.knife) {
self.knife = self.attachAsset('knife', {
anchorX: 0.1,
anchorY: 0.5,
x: self.width / 2 + 20,
y: 0,
rotation: 0
});
}
self.knife.visible = true;
self.knife.rotation = -0.7;
tween(self.knife, {
rotation: 0.7
}, {
duration: 180,
easing: tween.cubicOut,
onFinish: function onFinish() {
self.knife.visible = false;
self.knifeSwinging = false;
self.knife.rotation = -0.7;
}
});
};
return self;
});
// Obstacle (rock)
var Obstacle = Container.expand(function () {
var self = Container.call(this);
var rock = self.attachAsset('obstacle_rock', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = rock.width;
self.height = rock.height;
self.speed = 14 + Math.random() * 6;
self.update = function () {
self.y += self.speed;
};
return self;
});
// Upgrade (gun or knife)
var Upgrade = Container.expand(function () {
var self = Container.call(this);
self.kind = Math.random() < 0.5 ? 'gun' : 'knife';
var assetId = self.kind === 'gun' ? 'upgrade_gun' : 'upgrade_knife';
var upg = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
self.width = upg.width;
self.height = upg.height;
self.speed = 10 + Math.random() * 4;
self.update = function () {
self.y += self.speed;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x181818
});
/****
* Game Code
****/
// Game constants
// Minecart (player)
// Player's knife
// Player's bullet
// Enemy: Bat
// Enemy: Goblin
// Enemy: Skeleton
// Obstacle: Rock
// Upgrade: Gun
// Upgrade: Knife
// Maze background image (replace with your maze image asset id)
var GAME_W = 2048;
var GAME_H = 2732;
var TRACK_LEFT = 300;
var TRACK_RIGHT = GAME_W - 300;
var PLAYER_Y = GAME_H - 350;
// Game state
var minecart = null;
var bullets = [];
var enemies = [];
var obstacles = [];
var upgrades = [];
var dragNode = null;
var lastTouchX = 0;
var lastTouchY = 0;
var scoreTxt = null;
var lastScore = 0;
var gameOver = false;
var spawnTimer = 0;
var obstacleTimer = 0;
var upgradeTimer = 0;
var difficulty = 1;
var knifeHitEnemies = [];
var lastKnifeSwingTick = -100;
// Track if player has unlocked the upgraded knife by beating super hard mode
var upgradedKnifeUnlocked = false;
// Track if player has chosen to use upgraded knife in normal mode (set via menu)
var useUpgradedKnifeNextGame = false;
// Track if player has unlocked the player (250 kills in super hard mode)
var playerUnlocked = false;
var playerKills = 0;
// Track skeleton player unlock (900 kills in normal mode)
var skeletonPlayerUnlocked = storage.skeletonPlayerUnlocked || false;
var normalModeKills = storage.normalModeKills || 0;
// Try to load from persistent storage if available
if (typeof storage !== "undefined" && storage.upgradedKnifeUnlocked) {
upgradedKnifeUnlocked = true;
}
if (typeof storage !== "undefined" && storage.playerUnlocked) {
playerUnlocked = true;
}
if (typeof storage !== "undefined" && storage.playerKills) {
playerKills = parseInt(storage.playerKills, 10) || 0;
}
// Always reset useUpgradedKnifeNextGame on load
useUpgradedKnifeNextGame = false;
// --- Main Menu Overlay ---
var mainMenuOverlay = null;
var mainMenuTitle = null;
var normalBtn = null;
var hardBtn = null;
var menuActive = true;
function showMainMenu() {
menuActive = true;
// Overlay container
mainMenuOverlay = new Container();
// Semi-transparent dark background
var bg = LK.getAsset('maze_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: GAME_W,
height: GAME_H
});
bg.alpha = 0.82;
mainMenuOverlay.addChild(bg);
// Title
mainMenuTitle = new Text2("Boneshaft 1: UPGRADED", {
size: 180,
fill: "#fff",
font: "Impact, Arial Black, Tahoma"
});
mainMenuTitle.anchor.set(0.5, 0.5);
mainMenuTitle.x = GAME_W / 2;
mainMenuTitle.y = 600;
mainMenuOverlay.addChild(mainMenuTitle);
// Normal Mode Button
var normalLabel = "Normal Mode";
if (upgradedKnifeUnlocked) {
normalLabel += " (Upgraded Knife!)";
}
normalBtn = new Text2(normalLabel, {
size: 120,
fill: 0x44FF44,
font: "Impact, Arial Black, Tahoma"
});
normalBtn.anchor.set(0.5, 0.5);
normalBtn.x = GAME_W / 2;
normalBtn.y = 1100;
mainMenuOverlay.addChild(normalBtn);
// Super Hard Mode Button
hardBtn = new Text2("Super Hard Mode", {
size: 120,
fill: 0xFF4444,
font: "Impact, Arial Black, Tahoma"
});
hardBtn.anchor.set(0.5, 0.5);
hardBtn.x = GAME_W / 2;
hardBtn.y = 1350;
mainMenuOverlay.addChild(hardBtn);
// --- Unlocked Section ---
var unlockedTitle = new Text2("Unlocked", {
size: 90,
fill: "#fff",
font: "Impact, Arial Black, Tahoma"
});
unlockedTitle.anchor.set(0.5, 0.5);
unlockedTitle.x = GAME_W / 2;
unlockedTitle.y = 1550;
mainMenuOverlay.addChild(unlockedTitle);
var unlockedKnifeText, unlockedKnifeIcon;
var unlockedPlayerText, unlockedPlayerIcon;
var playerUnlocked = false;
var playerKills = 0;
// Try to load from persistent storage if available
if (typeof storage !== "undefined" && storage.playerUnlocked) {
playerUnlocked = true;
}
if (typeof storage !== "undefined" && storage.playerKills) {
playerKills = parseInt(storage.playerKills, 10) || 0;
}
if (upgradedKnifeUnlocked) {
var showKnifeConfirmDialog = function showKnifeConfirmDialog() {
if (knifeConfirmDialog) return;
knifeConfirmDialog = new Container();
// Dialog background
var dialogBg = LK.getAsset('maze_bg', {
anchorX: 0,
anchorY: 0,
x: GAME_W / 2 - 400,
y: 1200,
width: 800,
height: 400
});
dialogBg.alpha = 0.93;
knifeConfirmDialog.addChild(dialogBg);
// Dialog text
var dialogText = new Text2("Use Upgraded Knife in Normal Mode?", {
size: 60,
fill: "#fff",
font: "Impact, Arial Black, Tahoma"
});
dialogText.anchor.set(0.5, 0.5);
dialogText.x = GAME_W / 2;
dialogText.y = 1320;
knifeConfirmDialog.addChild(dialogText);
// Yes button
var yesBtn = new Text2("Yes", {
size: 90,
fill: 0x44FF44,
font: "Impact, Arial Black, Tahoma"
});
yesBtn.anchor.set(0.5, 0.5);
yesBtn.x = GAME_W / 2 - 140;
yesBtn.y = 1450;
knifeConfirmDialog.addChild(yesBtn);
// No button
var noBtn = new Text2("No", {
size: 90,
fill: 0xFF4444,
font: "Impact, Arial Black, Tahoma"
});
noBtn.anchor.set(0.5, 0.5);
noBtn.x = GAME_W / 2 + 140;
noBtn.y = 1450;
knifeConfirmDialog.addChild(noBtn);
// Button handlers
yesBtn.interactive = true;
noBtn.interactive = true;
yesBtn.down = function () {
// Set a global flag to use upgraded knife in normal mode
useUpgradedKnifeNextGame = true;
// Remove dialog
if (knifeConfirmDialog) {
knifeConfirmDialog.destroy();
knifeConfirmDialog = null;
}
};
noBtn.down = function () {
// Just close dialog
if (knifeConfirmDialog) {
knifeConfirmDialog.destroy();
knifeConfirmDialog = null;
}
};
yesBtn.on('down', yesBtn.down);
noBtn.on('down', noBtn.down);
mainMenuOverlay.addChild(knifeConfirmDialog);
}; // Handler for both icon and text
var unlockedKnifeDownHandler = function unlockedKnifeDownHandler() {
showKnifeConfirmDialog();
};
unlockedKnifeText = new Text2("Upgraded Knife", {
size: 80,
fill: 0xFFD700,
font: "Impact, Arial Black, Tahoma"
});
unlockedKnifeIcon = LK.getAsset('upgrade_knife', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_W / 2 - 120,
y: 1650
});
unlockedKnifeText.anchor.set(0, 0.5);
unlockedKnifeText.x = GAME_W / 2 - 60;
unlockedKnifeText.y = 1650;
mainMenuOverlay.addChild(unlockedKnifeIcon);
mainMenuOverlay.addChild(unlockedKnifeText);
// Make the unlocked knife section pressable
unlockedKnifeText.interactive = true;
unlockedKnifeIcon.interactive = true;
// Confirmation dialog container (created on demand)
var knifeConfirmDialog = null;
unlockedKnifeText.down = unlockedKnifeDownHandler;
unlockedKnifeIcon.down = unlockedKnifeDownHandler;
unlockedKnifeText.on('down', unlockedKnifeDownHandler);
unlockedKnifeIcon.on('down', unlockedKnifeDownHandler);
} else {
unlockedKnifeText = new Text2("??? (Beat Super Hard Mode to unlock)", {
size: 70,
fill: 0x888888,
font: "Impact, Arial Black, Tahoma"
});
unlockedKnifeText.anchor.set(0.5, 0.5);
unlockedKnifeText.x = GAME_W / 2;
unlockedKnifeText.y = 1650;
mainMenuOverlay.addChild(unlockedKnifeText);
}
// --- Player Unlock Section ---
var playerSectionY = 1800;
if (playerUnlocked) {
unlockedPlayerText = new Text2("Player (Unlocked!)", {
size: 80,
fill: 0x00BFFF,
font: "Impact, Arial Black, Tahoma"
});
unlockedPlayerIcon = LK.getAsset('Player', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_W / 2 - 120,
y: playerSectionY
});
unlockedPlayerText.anchor.set(0, 0.5);
unlockedPlayerText.x = GAME_W / 2 - 60;
unlockedPlayerText.y = playerSectionY;
mainMenuOverlay.addChild(unlockedPlayerIcon);
mainMenuOverlay.addChild(unlockedPlayerText);
// Make player section interactive to switch model
var playerDownHandler = function playerDownHandler() {
// Set player model as selected player model
storage.selectedPlayerModel = 'player';
};
unlockedPlayerText.interactive = true;
unlockedPlayerIcon.interactive = true;
unlockedPlayerText.down = playerDownHandler;
unlockedPlayerIcon.down = playerDownHandler;
unlockedPlayerText.on('down', playerDownHandler);
unlockedPlayerIcon.on('down', playerDownHandler);
} else {
var killsNeeded = 250 - playerKills;
if (killsNeeded < 0) killsNeeded = 0;
unlockedPlayerText = new Text2("??? (Get 250 kills in Super Hard Mode to unlock)\nKills: " + playerKills + " / 250", {
size: 60,
fill: 0x888888,
font: "Impact, Arial Black, Tahoma"
});
unlockedPlayerText.anchor.set(0.5, 0.5);
unlockedPlayerText.x = GAME_W / 2;
unlockedPlayerText.y = playerSectionY;
mainMenuOverlay.addChild(unlockedPlayerText);
}
// --- Skeleton Player Unlock Section ---
var skeletonSectionY = 1950;
if (skeletonPlayerUnlocked) {
var unlockedSkeletonText = new Text2("Skeleton Player (Unlocked!)", {
size: 80,
fill: 0xFFD700,
font: "Impact, Arial Black, Tahoma"
});
var unlockedSkeletonIcon = LK.getAsset('Skeletonmodel', {
anchorX: 0.5,
anchorY: 0.5,
x: GAME_W / 2 - 120,
y: skeletonSectionY
});
unlockedSkeletonText.anchor.set(0, 0.5);
unlockedSkeletonText.x = GAME_W / 2 - 60;
unlockedSkeletonText.y = skeletonSectionY;
mainMenuOverlay.addChild(unlockedSkeletonIcon);
mainMenuOverlay.addChild(unlockedSkeletonText);
// Make skeleton player section interactive to switch model
var skeletonPlayerDownHandler = function skeletonPlayerDownHandler() {
// Set skeleton model as selected player model
storage.selectedPlayerModel = 'skeleton';
};
unlockedSkeletonText.interactive = true;
unlockedSkeletonIcon.interactive = true;
unlockedSkeletonText.down = skeletonPlayerDownHandler;
unlockedSkeletonIcon.down = skeletonPlayerDownHandler;
unlockedSkeletonText.on('down', skeletonPlayerDownHandler);
unlockedSkeletonIcon.on('down', skeletonPlayerDownHandler);
} else {
var killsNeeded = 900 - normalModeKills;
if (killsNeeded < 0) killsNeeded = 0;
var unlockedSkeletonText = new Text2("??? (Get 900 kills in Normal Mode to unlock)\nKills: " + normalModeKills + " / 900", {
size: 60,
fill: 0x888888,
font: "Impact, Arial Black, Tahoma"
});
unlockedSkeletonText.anchor.set(0.5, 0.5);
unlockedSkeletonText.x = GAME_W / 2;
unlockedSkeletonText.y = skeletonSectionY;
mainMenuOverlay.addChild(unlockedSkeletonText);
}
// Add overlay to game
game.addChild(mainMenuOverlay);
// Button hit areas
normalBtn.interactive = true;
hardBtn.interactive = true;
// Add direct down event handlers to buttons for pressable effect
normalBtn.down = function (x, y, obj) {
if (!mainMenuOverlay) {
return;
}
selectMode('normal');
};
hardBtn.down = function (x, y, obj) {
if (!mainMenuOverlay) {
return;
}
selectMode('hard');
};
normalBtn.on('down', normalBtn.down);
hardBtn.on('down', hardBtn.down);
// Touch/click handlers
mainMenuOverlay.down = function (x, y, obj) {
// Defensive: If overlay is destroyed or null, do nothing
if (!mainMenuOverlay) {
return;
}
// Convert to overlay local coordinates
var local = mainMenuOverlay.toLocal({
x: x,
y: y
});
// Check if normalBtn was pressed
if (pointInText(normalBtn, local.x, local.y)) {
selectMode('normal');
} else if (pointInText(hardBtn, local.x, local.y)) {
selectMode('hard');
}
};
// Attach to overlay
mainMenuOverlay.on('down', mainMenuOverlay.down);
// Also attach to game for mobile
game.down = function (x, y, obj) {
if (menuActive && mainMenuOverlay) {
mainMenuOverlay.down(x, y, obj);
return;
}
if (gameOver) {
return;
}
dragNode = minecart;
lastTouchX = x;
lastTouchY = y;
};
}
// Helper: check if point is inside Text2 bounds
function pointInText(txt, x, y) {
var w = txt.width;
var h = txt.height;
var tx = txt.x - w * txt.anchor.x;
var ty = txt.y - h * txt.anchor.y;
return x >= tx && x <= tx + w && y >= ty && y <= ty + h;
}
// Remove menu and start game
function selectMode(mode) {
if (!mainMenuOverlay) {
return;
}
menuActive = false;
mainMenuOverlay.destroy();
mainMenuOverlay = null;
// Set difficulty and level mode
if (mode === 'normal') {
// Set up original game state for normal mode
difficulty = 1;
game._superHardMode = false;
// Remove any super hard mode UI if present
if (game._batHealthBarBg) {
LK.gui.top.removeChild(game._batHealthBarBg);
game._batHealthBarBg = null;
}
if (game._batHealthBar) {
LK.gui.top.removeChild(game._batHealthBar);
game._batHealthBar = null;
}
game._batEnemy = null;
game._batMaxHp = null;
// Place player at original starting position
if (minecart) {
minecart.x = GAME_W / 2;
minecart.y = PLAYER_Y;
// If upgraded knife is unlocked AND player chose to use it, start with knife and max level
if (upgradedKnifeUnlocked && useUpgradedKnifeNextGame) {
minecart.setWeapon('knife');
minecart.weaponLevel = 3;
useUpgradedKnifeNextGame = false; // Reset after use
}
}
} else if (mode === 'hard') {
difficulty = 7;
game._superHardMode = true;
}
// Reset game state
LK.setScore(0);
if (scoreTxt) {
scoreTxt.setText('0');
}
if (minecart) {
minecart.x = GAME_W / 2;
minecart.y = PLAYER_Y;
minecart.weapon = 'gun';
minecart.weaponLevel = 1;
minecart.invincible = false;
minecart.invincibleTimer = 0;
minecart.attackCooldown = 0;
minecart.knifeSwinging = false;
minecart.setWeapon('gun');
}
bullets = [];
enemies = [];
obstacles = [];
upgrades = [];
spawnTimer = 0;
obstacleTimer = 60;
upgradeTimer = 600;
knifeHitEnemies = [];
lastKnifeSwingTick = -100;
gameOver = false;
// If super hard mode, set up new level
if (game._superHardMode) {
// Remove all enemies, obstacles, upgrades, and spawn a new set for the new level
for (var i = 0; i < enemies.length; ++i) {
enemies[i].destroy();
}
for (var i = 0; i < obstacles.length; ++i) {
obstacles[i].destroy();
}
for (var i = 0; i < upgrades.length; ++i) {
upgrades[i].destroy();
}
enemies = [];
obstacles = [];
upgrades = [];
// Place player at the same starting position as normal mode
minecart.x = GAME_W / 2;
minecart.y = PLAYER_Y;
// Optionally, change background or add new obstacles/enemy patterns
// Example: spawn a wall of goblins at the top
for (var gx = TRACK_LEFT + 120; gx < TRACK_RIGHT - 120; gx += 180) {
var g = new Goblin();
g.x = gx;
g.y = 0;
enemies.push(g);
game.addChild(g);
}
// Example: spawn a bat immediately
var bat = new Bat();
bat.x = GAME_W / 2;
bat.y = -100;
enemies.push(bat);
game.addChild(bat);
// Set up bat health bar UI for super hard mode
var barWidth = 1700;
var barHeight = 90;
var barBg = LK.getAsset('enemy_goblin', {
width: barWidth,
height: barHeight,
color: 0x222222,
anchorX: 0.5,
anchorY: 0
});
barBg.x = GAME_W / 2;
barBg.y = 10;
var bar = LK.getAsset('enemy_goblin', {
width: barWidth - 20,
height: barHeight - 18,
color: 0xff2222,
anchorX: 0.5,
anchorY: 0
});
bar.x = GAME_W / 2;
bar.y = 19;
LK.gui.top.addChild(barBg);
LK.gui.top.addChild(bar);
game._batHealthBarBg = barBg;
game._batHealthBar = bar;
game._batEnemy = bat;
game._batMaxHp = bat.hp;
}
}
// Add maze background image
var mazeBg = LK.getAsset('maze_bg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: GAME_W,
height: GAME_H
});
game.addChild(mazeBg);
// Score display
scoreTxt = new Text2('0', {
size: 120,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Create player
minecart = new Minecart();
game.addChild(minecart);
minecart.x = GAME_W / 2;
minecart.y = PLAYER_Y;
// Vertical shooter: drag minecart only in X, fixed Y
game.down = function (x, y, obj) {
if (gameOver) {
return;
}
dragNode = minecart;
lastTouchX = x;
lastTouchY = y;
};
game.up = function (x, y, obj) {
dragNode = null;
};
game.move = function (x, y, obj) {
if (gameOver) {
return;
}
if (dragNode === minecart) {
// Only allow movement in X, clamp to track
var nx = x;
if (nx < TRACK_LEFT) {
nx = TRACK_LEFT;
}
if (nx > TRACK_RIGHT) {
nx = TRACK_RIGHT;
}
minecart.x = nx;
minecart.y = PLAYER_Y;
}
};
// Tap to attack (shoot or knife)
game.tap = function (x, y, obj) {
if (gameOver) {
return;
}
if (minecart.weapon === 'gun') {
if (minecart.attackCooldown <= 0) {
fireBullet();
minecart.attackCooldown = 12 - minecart.weaponLevel * 2; // Faster with upgrades
if (minecart.attackCooldown < 4) {
minecart.attackCooldown = 4;
}
}
} else if (minecart.weapon === 'knife') {
if (LK.ticks - lastKnifeSwingTick > 18) {
minecart.swingKnife();
lastKnifeSwingTick = LK.ticks;
}
}
};
// Fire bullet(s)
function fireBullet() {
var spread = minecart.weaponLevel;
var angleSpread = 0.18; // radians, about 10 degrees
var basePower = 1 + Math.floor((minecart.weaponLevel - 1) / 2); // Level 1-2: 1, Level 3: 2
for (var i = 0; i < spread; ++i) {
var b = new Bullet();
// Spread bullets horizontally and with angle
var offset = i - (spread - 1) / 2;
b.x = minecart.x + offset * 40;
b.y = minecart.y - minecart.height / 2 - 10;
// Angle for spread: center is 0, sides are negative/positive
if (spread > 1) {
b.angle = offset * angleSpread;
}
b.power = basePower;
bullets.push(b);
game.addChild(b);
}
}
// Main update loop
game.update = function () {
if (menuActive) {
return;
}
if (gameOver) {
return;
}
// Difficulty scaling
if (LK.ticks % 300 === 0 && difficulty < 10) {
difficulty += 1;
}
// Attack cooldown
if (minecart && minecart.attackCooldown > 0) {
minecart.attackCooldown -= 1;
}
// Invincibility timer
if (minecart && minecart.invincible) {
minecart.invincibleTimer -= 1;
if (minecart.invincibleTimer <= 0) {
minecart.invincible = false;
}
}
// Bullets update
for (var i = bullets.length - 1; i >= 0; --i) {
var b = bullets[i];
b.update();
if (b.y < -50) {
b.destroy();
bullets.splice(i, 1);
continue;
}
// Bullet hits enemy
for (var j = enemies.length - 1; j >= 0; --j) {
var e = enemies[j];
if (b.intersects(e)) {
// Use bullet power for damage
e.hp -= b.power || 1;
// If this is the Bat, update health bar
if (game._batEnemy && e === game._batEnemy && game._batHealthBar) {
var percent = Math.max(0, e.hp) / game._batMaxHp;
// Clamp width to minimum 0
game._batHealthBar.width = 1680 * percent;
// Optionally, you can tint the bar as it gets low (not required)
}
b.destroy();
bullets.splice(i, 1);
if (e.hp <= 0) {
addScore(e.scoreValue);
e.destroy();
enemies.splice(j, 1);
// If Bat is dead, remove health bar UI
if (game._batEnemy && e === game._batEnemy) {
if (game._batHealthBarBg) {
LK.gui.top.removeChild(game._batHealthBarBg);
game._batHealthBarBg = null;
}
if (game._batHealthBar) {
LK.gui.top.removeChild(game._batHealthBar);
game._batHealthBar = null;
}
game._batEnemy = null;
// If in super hard mode, check for score 1000 to win and unlock upgraded knife
if (game._superHardMode && !upgradedKnifeUnlocked) {
if (LK.getScore() >= 1000) {
upgradedKnifeUnlocked = true;
if (typeof storage !== "undefined") {
storage.upgradedKnifeUnlocked = true;
}
// Show a win popup
LK.showYouWin();
gameOver = true;
return;
}
// Otherwise, keep playing until score 1000 is reached
}
}
// --- Player unlock logic: count kills in super hard mode ---
if (game._superHardMode && !playerUnlocked) {
if (typeof playerKills === "undefined") playerKills = 0;
playerKills++;
if (playerKills >= 250) {
playerUnlocked = true;
if (typeof storage !== "undefined") {
storage.playerUnlocked = true;
}
}
if (typeof storage !== "undefined") {
storage.playerKills = playerKills;
}
}
// --- Skeleton player unlock logic: count kills in normal mode ---
if (!game._superHardMode && !skeletonPlayerUnlocked) {
if (typeof normalModeKills === "undefined") normalModeKills = 0;
normalModeKills++;
storage.normalModeKills = normalModeKills;
if (normalModeKills >= 900) {
skeletonPlayerUnlocked = true;
storage.skeletonPlayerUnlocked = true;
}
}
}
break;
}
}
}
// Enemies update (vertical shooter: always visible, move down)
for (var i = enemies.length - 1; i >= 0; --i) {
var e = enemies[i];
e.update();
// Remove if out of bounds
if (e.y > GAME_H + 200) {
e.destroy();
enemies.splice(i, 1);
continue;
}
// Enemy hits minecart
if (minecart && minecart !== null && !minecart.invincible && e.intersects(minecart)) {
// Check if Bat killed the player for jump scare
if (game._batEnemy && e === game._batEnemy) {
// Show a big jump scare: flash white, show a big FACE in the center, and play a loud sound if available
LK.effects.flashScreen(0xffffff, 1200);
// Create a big FACE image in the center (using the goblin face as a placeholder for a jumpscare face)
var jumpscareFace = LK.getAsset('enemy_goblin', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2,
scaleY: 2,
x: GAME_W / 2,
y: GAME_H / 2
});
jumpscareFace.alpha = 0;
game.addChild(jumpscareFace);
// Animate the face: scale up and fade in quickly, then fade out
tween(jumpscareFace, {
alpha: 1,
scaleX: 7,
scaleY: 7
}, {
duration: 120,
onFinish: function onFinish() {
tween(jumpscareFace, {
alpha: 0
}, {
duration: 700,
delay: 350,
onFinish: function onFinish() {
jumpscareFace.destroy();
}
});
}
});
// Optionally, play a loud sound here if you have a jumpscare sound asset
}
minecart.flash();
LK.effects.flashScreen(0xff0000, 600);
// End game
LK.showGameOver();
gameOver = true;
return;
}
// Enemy hit by knife
if (minecart.weapon === 'knife' && minecart.knife && minecart.knife.visible && !knifeHitEnemies.includes(e)) {
// Knife is in swing, check collision
var knifeGlobal = minecart.toGlobal(minecart.knife.position);
var knifeRect = new Rectangle(knifeGlobal.x - 30, knifeGlobal.y - 12, 60, 24);
var enemyRect = new Rectangle(e.x - e.width / 2, e.y - e.height / 2, e.width, e.height);
if (rectsIntersect(knifeRect, enemyRect)) {
e.hp -= 1;
// If this is the Bat, update health bar
if (game._batEnemy && e === game._batEnemy && game._batHealthBar) {
var percent = Math.max(0, e.hp) / game._batMaxHp;
game._batHealthBar.width = 1680 * percent;
}
knifeHitEnemies.push(e);
if (e.hp <= 0) {
addScore(e.scoreValue);
e.destroy();
enemies.splice(i, 1);
// If Bat is dead, remove health bar UI
if (game._batEnemy && e === game._batEnemy) {
if (game._batHealthBarBg) {
LK.gui.top.removeChild(game._batHealthBarBg);
game._batHealthBarBg = null;
}
if (game._batHealthBar) {
LK.gui.top.removeChild(game._batHealthBar);
game._batHealthBar = null;
}
game._batEnemy = null;
}
}
}
}
}
// Reset knife hit list if knife not swinging
if (!minecart || !minecart.knife || !minecart.knife.visible) {
knifeHitEnemies = [];
}
// Obstacles update
for (var i = obstacles.length - 1; i >= 0; --i) {
var o = obstacles[i];
o.update();
if (o.y > GAME_H + 100) {
o.destroy();
obstacles.splice(i, 1);
continue;
}
// Obstacle hits minecart
if (minecart && !minecart.invincible && o.intersects(minecart)) {
minecart.flash();
LK.effects.flashScreen(0xff0000, 600);
LK.showGameOver();
gameOver = true;
return;
}
}
// Upgrades update
for (var i = upgrades.length - 1; i >= 0; --i) {
var u = upgrades[i];
u.update();
if (u.y > GAME_H + 100) {
u.destroy();
upgrades.splice(i, 1);
continue;
}
// Pickup
if (u.intersects(minecart)) {
if (u.kind === 'gun') {
minecart.setWeapon('gun');
minecart.upgradeWeapon();
} else {
minecart.setWeapon('knife');
minecart.upgradeWeapon();
}
u.destroy();
upgrades.splice(i, 1);
}
}
// Spawning enemies
spawnTimer -= 1;
if (spawnTimer <= 0) {
spawnEnemy();
spawnTimer = Math.max(24, 60 - difficulty * 4 - Math.floor(Math.random() * 10));
}
// Spawning obstacles
obstacleTimer -= 1;
if (obstacleTimer <= 0) {
if (Math.random() < 0.5 + difficulty * 0.04) {
spawnObstacle();
}
obstacleTimer = 90 + Math.floor(Math.random() * 60) - difficulty * 4;
if (obstacleTimer < 30) {
obstacleTimer = 30;
}
}
// Spawning upgrades
upgradeTimer -= 1;
if (upgradeTimer <= 0) {
if (game._superHardMode) {
// Easier: upgrades spawn more often in super hard mode
if (Math.random() < 0.38) {
spawnUpgrade();
}
upgradeTimer = 340 + Math.floor(Math.random() * 120);
} else {
if (Math.random() < 0.18) {
spawnUpgrade();
}
upgradeTimer = 600 + Math.floor(Math.random() * 300);
}
}
// Update score display
if (LK.getScore() !== lastScore) {
scoreTxt.setText(LK.getScore());
lastScore = LK.getScore();
}
// Win condition for super hard mode: reach score 1000
if (game._superHardMode && !upgradedKnifeUnlocked && LK.getScore() >= 1000) {
upgradedKnifeUnlocked = true;
if (typeof storage !== "undefined") {
storage.upgradedKnifeUnlocked = true;
}
LK.showYouWin();
gameOver = true;
return;
}
};
// Spawn enemy at random X at the top (vertical shooter style)
function spawnEnemy() {
// If in super hard mode, spawn new enemy patterns
if (game._superHardMode) {
// Example: spawn a slightly slower skeleton and a goblin together (easier)
var e1 = new Skeleton();
e1.speed = 10 + Math.random() * 2.5;
e1.x = TRACK_LEFT + 120 + Math.random() * (TRACK_RIGHT - TRACK_LEFT - 240);
e1.y = -80;
enemies.push(e1);
game.addChild(e1);
var e2 = new Goblin();
e2.speed = 8 + Math.random() * 2;
e2.x = TRACK_LEFT + 120 + Math.random() * (TRACK_RIGHT - TRACK_LEFT - 240);
e2.y = -180;
enemies.push(e2);
game.addChild(e2);
// Occasionally spawn a Bat
if (Math.random() < 0.12) {
var bat = new Bat();
bat.x = TRACK_LEFT + 120 + Math.random() * (TRACK_RIGHT - TRACK_LEFT - 240);
bat.y = -200;
enemies.push(bat);
game.addChild(bat);
// Set up bat health bar UI
var barWidth = 1700;
var barHeight = 90;
var barBg = LK.getAsset('enemy_goblin', {
width: barWidth,
height: barHeight,
color: 0x222222,
anchorX: 0.5,
anchorY: 0
});
barBg.x = GAME_W / 2;
barBg.y = 10;
var bar = LK.getAsset('enemy_goblin', {
width: barWidth - 20,
height: barHeight - 18,
color: 0xff2222,
anchorX: 0.5,
anchorY: 0
});
bar.x = GAME_W / 2;
bar.y = 19;
LK.gui.top.addChild(barBg);
LK.gui.top.addChild(bar);
game._batHealthBarBg = barBg;
game._batHealthBar = bar;
game._batEnemy = bat;
game._batMaxHp = bat.hp;
}
// Sometimes spawn a ginger
if (Math.random() < 0.25) {
var g = new Ginger();
g.x = TRACK_LEFT + 120 + Math.random() * (TRACK_RIGHT - TRACK_LEFT - 240);
g.y = -120;
enemies.push(g);
game.addChild(g);
}
return;
}
// --- Normal mode enemy spawn ---
var enemyType = Math.random();
var e = null;
// Bat is now extremely rare: only spawn if enemyType < 0.02 (2% chance)
if (enemyType < 0.02) {
e = new Bat();
// Show warning text and play creepy music when Bat spawns
if (!game._batWarningText) {
// Create warning text if not already present
var batWarning = new Text2("That is not you.", {
size: 120,
fill: 0xFF2222,
font: "Impact, Arial Black, Tahoma"
});
batWarning.anchor.set(0.5, 0.5);
batWarning.alpha = 0.0;
game._batWarningText = batWarning;
// Add to LK.gui.center for true center of screen
LK.gui.center.addChild(batWarning);
}
// Fade in text
var warningText = game._batWarningText;
warningText.alpha = 0.0;
tween(warningText, {
alpha: 1
}, {
duration: 400,
onFinish: function onFinish() {
// Fade out after 1.2s
tween(warningText, {
alpha: 0
}, {
duration: 800,
delay: 1200
});
}
});
// Play creepy music (music asset id: 'creepy_bat')
LK.playMusic('creepy_bat', {
fade: {
start: 0,
end: 1,
duration: 800
}
});
// --- Bat Health Bar UI ---
// Remove any previous bat health bar
if (game._batHealthBarBg) {
LK.gui.top.removeChild(game._batHealthBarBg);
game._batHealthBarBg = null;
}
if (game._batHealthBar) {
LK.gui.top.removeChild(game._batHealthBar);
game._batHealthBar = null;
}
// Create background bar (much bigger, at the very top)
var barWidth = 1700;
var barHeight = 90;
var barBg = LK.getAsset('enemy_goblin', {
width: barWidth,
height: barHeight,
color: 0x222222,
anchorX: 0.5,
anchorY: 0
});
barBg.x = GAME_W / 2;
barBg.y = 10; // Very top of the screen
// Create health bar (red, much bigger)
var bar = LK.getAsset('enemy_goblin', {
width: barWidth - 20,
height: barHeight - 18,
color: 0xff2222,
anchorX: 0.5,
anchorY: 0
});
bar.x = GAME_W / 2;
bar.y = 19; // Just below the background bar
// Add to GUI
LK.gui.top.addChild(barBg);
LK.gui.top.addChild(bar);
game._batHealthBarBg = barBg;
game._batHealthBar = bar;
// Store reference to the Bat instance for health bar updates
game._batEnemy = e;
game._batMaxHp = e.hp;
} else if (enemyType < 0.10) {
// Ginger is rare, 8% chance (between 2% and 10%)
e = new Ginger();
} else if (enemyType < 0.65) {
e = new Goblin();
} else {
e = new Skeleton();
}
// Spawn at random X at the top of the screen
e.x = TRACK_LEFT + 80 + Math.random() * (TRACK_RIGHT - TRACK_LEFT - 160);
e.y = -80;
enemies.push(e);
game.addChild(e);
}
// Spawn obstacle
function spawnObstacle() {
var o = new Obstacle();
o.x = TRACK_LEFT + 80 + Math.random() * (TRACK_RIGHT - TRACK_LEFT - 160);
o.y = -60;
obstacles.push(o);
game.addChild(o);
}
// Spawn upgrade
function spawnUpgrade() {
var u = new Upgrade();
u.x = TRACK_LEFT + 80 + Math.random() * (TRACK_RIGHT - TRACK_LEFT - 160);
u.y = -60;
upgrades.push(u);
game.addChild(u);
}
// Add score
function addScore(val) {
LK.setScore(LK.getScore() + val);
}
// Rectangle intersection helper
function rectsIntersect(r1, r2) {
return !(r2.x > r1.x + r1.width || r2.x + r2.width < r1.x || r2.y > r1.y + r1.height || r2.y + r2.height < r1.y);
}
// Touch to attack (simulate tap on mobile)
game.down = function (origDown) {
return function (x, y, obj) {
if (origDown) {
origDown(x, y, obj);
}
game.tap(x, y, obj);
};
}(game.down);
// Reset game state on game over
LK.on('gameover', function () {
gameOver = true;
setTimeout(function () {
showMainMenu();
}, 600);
});
// Track run count for Bat spawn logic
if (typeof game._runCount === "undefined") {
game._runCount = 0;
}
LK.on('newgame', function () {
game._runCount = (game._runCount || 0) + 1;
gameOver = false;
LK.setScore(0);
if (scoreTxt) {
scoreTxt.setText('0');
}
// Remove all objects
for (var i = 0; i < bullets.length; ++i) {
bullets[i].destroy();
}
for (var i = 0; i < enemies.length; ++i) {
enemies[i].destroy();
}
for (var i = 0; i < obstacles.length; ++i) {
obstacles[i].destroy();
}
for (var i = 0; i < upgrades.length; ++i) {
upgrades[i].destroy();
}
bullets = [];
enemies = [];
obstacles = [];
upgrades = [];
minecart.x = GAME_W / 2;
minecart.y = PLAYER_Y;
minecart.weapon = 'gun';
minecart.weaponLevel = 1;
minecart.invincible = false;
minecart.invincibleTimer = 0;
minecart.attackCooldown = 0;
minecart.knifeSwinging = false;
minecart.setWeapon('gun');
difficulty = 1;
spawnTimer = 0;
obstacleTimer = 0;
upgradeTimer = 0;
knifeHitEnemies = [];
lastKnifeSwingTick = -100;
// Remove Bat health bar UI if present
if (game._batHealthBarBg) {
LK.gui.top.removeChild(game._batHealthBarBg);
game._batHealthBarBg = null;
}
if (game._batHealthBar) {
LK.gui.top.removeChild(game._batHealthBar);
game._batHealthBar = null;
}
game._batEnemy = null;
game._batMaxHp = null;
});
// Initial state
spawnTimer = 0;
obstacleTimer = 60;
upgradeTimer = 600;
difficulty = 1;
gameOver = false;
LK.setScore(0);
if (scoreTxt) {
scoreTxt.setText('0');
}
// Show main menu on load
showMainMenu();
Fullscreen modern App Store landscape banner, 16:9, high definition, for a game titled "Minecart Mayhem: Armed Run" and with the description "Survive a wild minecart ride by battling multiple enemy types with guns and knives, dodging obstacles, and collecting upgrades for a high-score chase.". No text on banner!
A skeleton with 3 legs and a creepy face and red eyes. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
A silver bullet. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
a maze . No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Man with bag on head and red eyes. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
A man with a bag over his head and red eyes. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Knife. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Rock. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Zombie. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
A knife with purple and golden around it. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Man with bag on head with cross on it. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
Skeleton player model. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat