/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1"); /**** * Classes ****/ // Enemy: Bat var Bat = Container.expand(function (x, y) { var self = Container.call(this); var bat = self.attachAsset('bat', { anchorX: 0.5, anchorY: 0.5 }); self.x = x; self.y = y; self.vx = (Math.random() > 0.5 ? 1 : -1) * (2.5 + Math.random() * 2); // Slower bats self.vy = (Math.random() > 0.5 ? 1 : -1) * 0.5; // Slower vertical movement self.frozen = false; self.frozenTimer = 0; self.trapped = false; self.trapTimer = 0; self.update = function () { // Pause logic for speed powerup if (self.pausedBySpeed) { return; } if (self.trapped) { self.trapTimer--; if (self.trapTimer <= 0) { self.trapped = false; self.alpha = 1; } return; } if (self.frozen) { self.frozenTimer--; if (self.frozenTimer <= 0) { self.frozen = false; bat.tint = 0x2a2a2a; // Remove freeze overlay if present if (self.iceOverlay) { self.removeChild(self.iceOverlay); self.iceOverlay = null; } } return; } self.x += self.vx; self.y += self.vy; // Snow Bros style: wrap horizontally if (self.x < 0) self.x = 2048; if (self.x > 2048) self.x = 0; if (self.y < 200 || self.y > 2732 - 200) self.vy *= -1; // Prevent bats from touching platforms: bounce off if intersecting for (var i = 0; i < platforms.length; i++) { var p = platforms[i]; if (self.intersects(p)) { // Bounce bat away from platform // Determine if bat is coming from above/below or left/right var dx = self.x - p.x; var dy = self.y - p.y; if (Math.abs(dx) > Math.abs(dy)) { // Bounce horizontally self.vx *= -1; // Move bat out of platform horizontally if (dx > 0) { self.x = p.x + p.width / 2 + 40; } else { self.x = p.x - p.width / 2 - 40; } } else { // Bounce vertically self.vy *= -1; // Move bat out of platform vertically if (dy > 0) { self.y = p.y + p.height / 2 + 40; } else { self.y = p.y - p.height / 2 - 40; } } } } }; // Freeze effect self.freeze = function () { self.frozen = true; self.frozenTimer = 180; bat.tint = 0x7fdfff; }; // Trap effect self.trap = function () { self.trapped = true; self.trapTimer = 90; self.alpha = 0.4; }; // Shatter (destroy) self.shatter = function () { LK.getSound('shatter').play(); self.destroy(); var idx = enemies.indexOf(self); if (idx >= 0) enemies.splice(idx, 1); LK.setScore(LK.getScore() + 1); scoreTxt.setText(LK.getScore()); }; return self; }); // Boss: Moves randomly var Boss = Container.expand(function (x, y) { var self = Container.call(this); var boss = self.attachAsset('Boss', { anchorX: 0.5, anchorY: 0.5 }); self.x = x; self.y = y; self.vx = 0; self.vy = 0; self.randomMoveTimer = 60 + Math.floor(Math.random() * 60); self.lastX = self.x; self.lastY = self.y; self.update = function () { // Random movement logic self.randomMoveTimer--; if (self.randomMoveTimer <= 0) { var speed = 6 + Math.random() * 4; var angle = Math.random() * Math.PI * 2; self.vx = Math.cos(angle) * speed; self.vy = Math.sin(angle) * speed * 0.7; self.randomMoveTimer = 60 + Math.floor(Math.random() * 60); } self.lastX = self.x; self.lastY = self.y; self.x += self.vx; self.y += self.vy; // Bounce vertically at top/bottom bounds if (self.lastY <= 200 && self.y > 200 || self.lastY >= 2732 - 400 && self.y < 2732 - 400) { self.vy *= -1; } if (self.y < 200) self.y = 200; if (self.y > 2732 - 400) self.y = 2732 - 400; // Snow Bros style: wrap horizontally if (self.x < 0) self.x = 2048; if (self.x > 2048) self.x = 0; }; return self; }); // Power-up: Broom var BroomPower = Container.expand(function (x, y) { var self = Container.call(this); var broom = self.attachAsset('Broom', { anchorX: 0.5, anchorY: 0.5 }); self.x = x; self.y = y; self.type = 'broom'; self.vy = 7 + Math.random() * 3; self.lastY = self.y; self.update = function () { self.lastY = self.y; self.y += self.vy; // Snow Bros style: wrap horizontally if (self.x < 0) self.x = 2048; if (self.x > 2048) self.x = 0; // Bounce at vertical edges if (self.lastY <= 200 && self.y > 200 || self.lastY >= 2732 - 200 && self.y < 2732 - 200) { self.vy *= -1; } if (self.y < 200) self.y = 200; if (self.y > 2732 - 200) self.y = 2732 - 200; }; return self; }); // Evanasense (player) class var Evanasense = Container.expand(function () { var self = Container.call(this); // Body var body = self.attachAsset('evana', { anchorX: 0.5, anchorY: 0.5 }); // Hat var hat = self.attachAsset('evana_hat', { anchorX: 0.5, anchorY: 1.1, y: -60 }); // Physics self.vx = 0; self.vy = 0; self.lastY = 0; self.isOnGround = false; self.facing = 1; // 1: right, -1: left self.canShoot = true; self.shootCooldown = 0; self.shielded = false; self.speedBoost = 0; self.fullMoon = false; self.fullMoonTimer = 0; self.canFly = false; self.flyTimer = 0; // For power-up visuals self.shieldSprite = null; // Freeze spell self.castFreeze = function () { if (!self.canShoot) return; self.canShoot = false; self.shootCooldown = 30; // 0.5s cooldown var orb = new FreezeOrb(self.x, self.y - 60, self.facing); game.addChild(orb); freezeOrbs.push(orb); LK.getSound('freeze').play(); }; // Trap spell (ice block, only in full moon mode) self.castTrap = function () { if (!self.fullMoon) return; var trap = new IceBlock(self.x + 100 * self.facing, self.y - 40); game.addChild(trap); iceBlocks.push(trap); LK.getSound('trap').play(); }; // Power-up: Shield self.gainShield = function () { self.shielded = true; if (!self.shieldSprite) { self.shieldSprite = self.attachAsset('shield', { anchorX: 0.5, anchorY: 0.5, alpha: 0.5 }); } self.shieldSprite.visible = true; }; self.loseShield = function () { self.shielded = false; if (self.shieldSprite) self.shieldSprite.visible = false; }; // Power-up: Speed self.gainSpeed = function () { self.speedBoost = 180; LK.effects.flashObject(self, 0xffe066, 400); }; // Power-up: Full Moon self.activateFullMoon = function () { self.fullMoon = true; self.fullMoonTimer = 360; // 6 seconds LK.effects.flashObject(self, 0xf6f1c7, 600); }; // Update self.update = function () { // Flying logic if (self.canFly) { // While flying, allow free vertical movement with up/down, and reduce gravity if (upPressed) { self.vy = -18; } else if (downPressed) { self.vy = 18; } else { self.vy *= 0.7; if (Math.abs(self.vy) < 1) self.vy = 0; } // Optional: add a little gravity so she floats down if not pressing up self.vy += 0.5; if (self.vy > 24) self.vy = 24; if (self.vy < -24) self.vy = -24; // Flying timer if (typeof self.flyTimer === "undefined") self.flyTimer = 600; self.flyTimer--; if (self.flyTimer <= 0) { self.canFly = false; self.flyTimer = 0; } } else { // Gravity self.vy += 2.2; if (self.vy > 40) self.vy = 40; } // Set moveDir for left/right, and moveVert for up/down (support diagonals) self.moveDir = 0; self.moveVert = 0; if (leftPressed && !rightPressed) { self.moveDir = -1; } else if (rightPressed && !leftPressed) { self.moveDir = 1; } if (upPressed && !downPressed) { self.moveVert = -1; } else if (downPressed && !upPressed) { self.moveVert = 1; } // If both a horizontal and vertical direction are pressed, normalize diagonal speed if (self.moveDir !== 0 && self.moveVert !== 0) { // Reduce speed for diagonal movement to keep consistent velocity self.moveDir *= Math.SQRT1_2; self.moveVert *= Math.SQRT1_2; } // --- Up/Down button logic (for future use, e.g. drop through platforms) --- // (Handled above for movement) // --- Fire button logic --- if (firePressed && self.canShoot) { self.castFreeze(); firePressed = false; // Only fire once per press } // Movement var moveSpeed = 18 + (self.speedBoost > 0 ? 10 : 0); if (self.moveDir || self.moveVert) { self.vx = moveSpeed * self.moveDir; self.vy = self.canFly ? moveSpeed * self.moveVert : self.vy; if (self.moveDir) self.facing = self.moveDir > 0 ? 1 : -1; } else { self.vx *= 0.7; if (Math.abs(self.vx) < 1) self.vx = 0; if (!self.canFly) { // Only apply gravity if not flying // (already handled above) } } // Apply position self.lastY = self.y; // --- Mario-style jump: allow jump if jumpPressed and isOnGround --- // Double jump support: allow one extra jump if yuDoubleJump is true if (typeof self.yuDoubleJump === "undefined") self.yuDoubleJump = false; if (typeof self.hasDoubleJumped === "undefined") self.hasDoubleJumped = false; if (jumpPressed) { var jumpPower = -38; if (self.moonJumpTimer && self.moonJumpTimer > 0) { jumpPower = -54; // Higher jump during moon effect } if (self.isOnGround) { self.vy = jumpPower; self.isOnGround = false; self.hasDoubleJumped = false; } else if (self.yuDoubleJump && !self.hasDoubleJumped) { self.vy = jumpPower; self.hasDoubleJumped = true; // Optional: flash effect for double jump LK.effects.flashObject(self, 0xb8e6ff, 200); } } if (self.moonJumpTimer && self.moonJumpTimer > 0) { self.moonJumpTimer--; if (self.moonJumpTimer === 0) { // Optionally, add a visual effect to show moon jump ended LK.effects.flashObject(self, 0xf6f1c7, 200); } } self.x += self.vx; self.y += self.vy; // Snow Bros style: wrap horizontally if (self.x < 0) self.x = 2048; if (self.x > 2048) self.x = 0; // Clamp vertically and collide with all platforms self.isOnGround = false; var prevBottom = self.lastY + body.height / 2; var currBottom = self.y + body.height / 2; for (var i = 0; i < platforms.length; i++) { var plat = platforms[i]; var platTop = plat.y - plat.height / 2; // Allow drop through platforms if downPressed, except for the floor (y >= 2732-60) var isFloor = plat.y >= 2732 - 60; if (self.vy > 0 && prevBottom <= platTop && currBottom >= platTop && self.x + body.width / 2 > plat.x - plat.width / 2 && self.x - body.width / 2 < plat.x + plat.width / 2 && (!downPressed || isFloor)) { self.y = platTop - body.height / 2; self.vy = 0; self.isOnGround = true; // Play platform sound when landing if (typeof self.lastWasOnGround === "undefined") self.lastWasOnGround = false; // (platform sound removed as requested) self.lastWasOnGround = true; } else { if (typeof self.lastWasOnGround === "undefined") self.lastWasOnGround = false; self.lastWasOnGround = false; } } // Clamp to bottom of screen if (self.y > 2732 - 60) { self.y = 2732 - 60; self.vy = 0; self.isOnGround = true; } // Shooting cooldown if (!self.canShoot) { self.shootCooldown--; if (self.shootCooldown <= 0) { self.canShoot = true; } } // Speed boost timer if (self.speedBoost > 0) { self.speedBoost--; } // Full moon timer if (self.fullMoon) { self.fullMoonTimer--; if (self.fullMoonTimer <= 0) { self.fullMoon = false; } } }; return self; }); // Freeze orb spell var FreezeOrb = Container.expand(function (x, y, dir) { var self = Container.call(this); var orb = self.attachAsset('freeze_orb', { anchorX: 0.5, anchorY: 0.5 }); self.x = x; self.y = y; self.vx = 32 * dir; self.lifetime = 60; self.update = function () { self.x += self.vx; // Snow Bros style: wrap horizontally if (self.x < 0) self.x = 2048; if (self.x > 2048) self.x = 0; self.lifetime--; if (self.lifetime <= 0) { self.destroy(); var idx = freezeOrbs.indexOf(self); if (idx >= 0) freezeOrbs.splice(idx, 1); } }; return self; }); // Enemy: Ghost var Ghost = Container.expand(function (x, y) { var self = Container.call(this); var ghost = self.attachAsset('ghost', { anchorX: 0.5, anchorY: 0.5, alpha: 0.85 }); self.x = x; self.y = y; self.vx = (Math.random() > 0.5 ? 1 : -1) * (3 + Math.random() * 2); // Slower ghosts self.vy = (Math.random() - 0.5) * 2; // Add a little vertical movement self.frozen = false; self.frozenTimer = 0; self.trapped = false; self.trapTimer = 0; self.randomMoveTimer = 30 + Math.floor(Math.random() * 60); // How many frames until next direction change self.lastY = self.y; self.update = function () { // Pause logic for speed powerup if (self.pausedBySpeed) { return; } if (self.trapped) { self.trapTimer--; if (self.trapTimer <= 0) { self.trapped = false; self.alpha = 0.85; } return; } if (self.frozen) { self.frozenTimer--; if (self.frozenTimer <= 0) { self.frozen = false; ghost.tint = 0xcfd6e6; // Remove freeze overlay if present if (self.iceOverlay) { self.removeChild(self.iceOverlay); self.iceOverlay = null; } } return; } // Random movement logic self.randomMoveTimer--; if (self.randomMoveTimer <= 0) { // Change direction randomly var speed = 3 + Math.random() * 2; var angle = Math.random() * Math.PI * 2; self.vx = Math.cos(angle) * speed; self.vy = Math.sin(angle) * speed * 0.5; // Less vertical movement self.randomMoveTimer = 30 + Math.floor(Math.random() * 60); } self.lastY = self.y; self.x += self.vx; self.y += self.vy; // Bounce vertically at top/bottom bounds if (self.lastY <= 200 && self.y > 200 || self.lastY >= 2732 - 200 && self.y < 2732 - 200) { self.vy *= -1; } if (self.y < 200) self.y = 200; if (self.y > 2732 - 200) self.y = 2732 - 200; // Snow Bros style: wrap horizontally if (self.x < 0) self.x = 2048; if (self.x > 2048) self.x = 0; }; // Freeze effect self.freeze = function () { self.frozen = true; self.frozenTimer = 180; ghost.tint = 0x7fdfff; }; // Trap effect self.trap = function () { self.trapped = true; self.trapTimer = 90; self.alpha = 0.4; }; // Shatter (destroy) self.shatter = function () { LK.getSound('shatter').play(); self.destroy(); var idx = enemies.indexOf(self); if (idx >= 0) enemies.splice(idx, 1); LK.setScore(LK.getScore() + 1); scoreTxt.setText(LK.getScore()); }; return self; }); // Ice block trap (full moon mode) var IceBlock = Container.expand(function (x, y) { var self = Container.call(this); var block = self.attachAsset('ice_block', { anchorX: 0.5, anchorY: 0.5, alpha: 0.85 }); self.x = x; self.y = y; self.lifetime = 120; self.update = function () { // Snow Bros style: wrap horizontally if (self.x < 0) self.x = 2048; if (self.x > 2048) self.x = 0; self.lifetime--; if (self.lifetime <= 0) { self.destroy(); var idx = iceBlocks.indexOf(self); if (idx >= 0) iceBlocks.splice(idx, 1); } }; return self; }); // Power-up: Lifepotion (adds a life) var LifepotionPower = Container.expand(function (x, y) { var self = Container.call(this); var potion = self.attachAsset('Lifepotion', { anchorX: 0.5, anchorY: 0.5 }); self.x = x; self.y = y; self.type = 'lifepotion'; self.vy = 7 + Math.random() * 3; self.lastY = self.y; self.update = function () { self.lastY = self.y; self.y += self.vy; // Snow Bros style: wrap horizontally if (self.x < 0) self.x = 2048; if (self.x > 2048) self.x = 0; // Bounce at vertical edges if (self.lastY <= 200 && self.y > 200 || self.lastY >= 2732 - 200 && self.y < 2732 - 200) { self.vy *= -1; } if (self.y < 200) self.y = 200; if (self.y > 2732 - 200) self.y = 2732 - 200; }; return self; }); // Power-up: Moon (full moon mode) var MoonPower = Container.expand(function (x, y) { var self = Container.call(this); var moon = self.attachAsset('moon', { anchorX: 0.5, anchorY: 0.5 }); self.x = x; self.y = y; self.type = 'moon'; self.vx = 3 + Math.random() * 2; // Slower moon movement self.lastX = self.x; self.update = function () { self.lastX = self.x; self.x += self.vx; // Snow Bros style: wrap horizontally if (self.x < 0) self.x = 2048; if (self.x > 2048) self.x = 0; }; return self; }); // Platform var Platform = Container.expand(function (x, y, w) { var self = Container.call(this); var plat = self.attachAsset('platform', { anchorX: 0.5, anchorY: 0.5, width: w || 320 }); self.x = x; self.y = y; self.width = w || 320; self.height = 40; return self; }); // Power-up: Shield var ShieldPower = Container.expand(function (x, y) { var self = Container.call(this); var shield = self.attachAsset('shield', { anchorX: 0.5, anchorY: 0.5 }); self.x = x; self.y = y; self.type = 'shield'; self.update = function () { // Snow Bros style: wrap horizontally if (self.x < 0) self.x = 2048; if (self.x > 2048) self.x = 0; }; return self; }); // Power-up: Speed var SpeedPower = Container.expand(function (x, y) { var self = Container.call(this); var speed = self.attachAsset('speed', { anchorX: 0.5, anchorY: 0.5 }); self.x = x; self.y = y; self.type = 'speed'; self.vy = 7 + Math.random() * 3; self.lastY = self.y; self.update = function () { self.lastY = self.y; self.y += self.vy; // Snow Bros style: wrap horizontally if (self.x < 0) self.x = 2048; if (self.x > 2048) self.x = 0; // Bounce at vertical edges if (self.lastY <= 200 && self.y > 200 || self.lastY >= 2732 - 200 && self.y < 2732 - 200) { self.vy *= -1; } if (self.y < 200) self.y = 200; if (self.y > 2732 - 200) self.y = 2732 - 200; }; return self; }); // Power-up: Yu (moves randomly) var YuPower = Container.expand(function (x, y) { var self = Container.call(this); var yu = self.attachAsset('Yu', { anchorX: 0.5, anchorY: 0.5 }); self.x = x; self.y = y; self.type = 'yu'; // Random movement variables self.vx = (Math.random() > 0.5 ? 1 : -1) * (3 + Math.random() * 2); self.vy = (Math.random() - 0.5) * 2; self.randomMoveTimer = 30 + Math.floor(Math.random() * 60); self.lastY = self.y; self.update = function () { // Random movement logic self.randomMoveTimer--; if (self.randomMoveTimer <= 0) { var speed = 3 + Math.random() * 2; var angle = Math.random() * Math.PI * 2; self.vx = Math.cos(angle) * speed; self.vy = Math.sin(angle) * speed * 0.5; self.randomMoveTimer = 30 + Math.floor(Math.random() * 60); } self.lastY = self.y; self.x += self.vx; self.y += self.vy; // Bounce vertically at top/bottom bounds if (self.lastY <= 200 && self.y > 200 || self.lastY >= 2732 - 200 && self.y < 2732 - 200) { self.vy *= -1; } if (self.y < 200) self.y = 200; if (self.y > 2732 - 200) self.y = 2732 - 200; // Snow Bros style: wrap horizontally if (self.x < 0) self.x = 2048; if (self.x > 2048) self.x = 0; }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x18141e }); /**** * Game Code ****/ // Add background image // Add background image var bg = LK.getAsset('bg', { anchorX: 0, anchorY: 0, width: 2048, height: 2732, x: 0, y: 0 }); game.addChild(bg); // (rulesPanel removed as per request) // --- Game Start Overlay --- var startOverlay = new Container(); var rulesText = new Text2("š§āāļø Witch's Night: Game Rules\n\n" + "- Move the witch with the blue buttons.\n" + "- Tap the yellow button to cast freeze spells.\n" + "- Collect powerups: Moon (full moon mode), Shield, Speed, Life Potion, Broom (fly!), Yu (cancel speed).\n" + "- Avoid ghosts and bats! Touching them loses a life unless shielded.\n" + "- Freeze or trap enemies, then touch them again to shatter.\n" + "- Clear all enemies to advance to the next stage.\n" + "- Survive as long as you can!\n\n" + "Good luck! Tap Start to play.", { size: 70, fill: 0xF6F1C7, align: "center", wordWrap: true, wordWrapWidth: 1600 }); rulesText.anchor.set(0.5, 0.5); rulesText.x = 1024; rulesText.y = 900; startOverlay.addChild(rulesText); var startBtn = new Text2("START", { size: 180, fill: 0xB8E6FF, fontWeight: "bold" }); startBtn.anchor.set(0.5, 0.5); startBtn.x = 1024; startBtn.y = 1800; startOverlay.addChild(startBtn); // Block gameplay until start is pressed game.paused = true; // Add start overlay to the very front so it appears above everything LK.gui.addChild(startOverlay); // Hide overlay and start game on button press startBtn.down = function () { if (startOverlay.parent) startOverlay.parent.removeChild(startOverlay); game.paused = false; }; // Main character: Evanasense (witch) // Witch body // Witch hat // Enemy: Ghost // Enemy: Bat // Platform // Spell: Freeze orb // Spell: Trap (ice block) // Power-up: Moon // Power-up: Shield // Power-up: Speed // Sound effects // Music // --- Classic D-pad controls (eight buttons, with diagonals) --- // D-pad button size and spacing (increased for better touch usability) var dpadBtnSize = 260; var dpadSpacing = 50; // Move D-pad higher by decreasing dpadY (more negative = higher on screen) var dpadY = -420; var dpadX = 360; // Diagonal offset (for diagonal buttons) var diagOffset = Math.round((dpadBtnSize + dpadSpacing) * 0.7); // Helper: reset all direction states (for compatibility) function resetButtonDirections() { leftPressed = false; rightPressed = false; upPressed = false; downPressed = false; // Also clear jumpPressed for diagonals jumpPressed = false; } // Left button var leftBtn = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.7, scaleY: 1.7, x: dpadX - dpadBtnSize - dpadSpacing, y: dpadY, alpha: 0.45 }); LK.gui.bottomLeft.addChild(leftBtn); var leftArrow = new Text2("ā", { size: 180, fill: 0x222244 }); leftArrow.anchor.set(0.5, 0.5); leftArrow.x = leftBtn.x; leftArrow.y = leftBtn.y; LK.gui.bottomLeft.addChild(leftArrow); // Right button var rightBtn = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.7, scaleY: 1.7, x: dpadX + dpadBtnSize + dpadSpacing, y: dpadY, alpha: 0.45 }); LK.gui.bottomLeft.addChild(rightBtn); var rightArrow = new Text2("ā¶", { size: 180, fill: 0x222244 }); rightArrow.anchor.set(0.5, 0.5); rightArrow.x = rightBtn.x; rightArrow.y = rightBtn.y; LK.gui.bottomLeft.addChild(rightArrow); // Up button var upBtn = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.7, scaleY: 1.7, x: dpadX, y: dpadY - dpadBtnSize - dpadSpacing, alpha: 0.45 }); LK.gui.bottomLeft.addChild(upBtn); var upArrow = new Text2("ā²", { size: 180, fill: 0x222244 }); upArrow.anchor.set(0.5, 0.5); upArrow.x = upBtn.x; upArrow.y = upBtn.y; LK.gui.bottomLeft.addChild(upArrow); // Down button var downBtn = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.7, scaleY: 1.7, x: dpadX, y: dpadY + dpadBtnSize + dpadSpacing, alpha: 0.45 }); LK.gui.bottomLeft.addChild(downBtn); var downArrow = new Text2("ā¼", { size: 180, fill: 0x222244 }); downArrow.anchor.set(0.5, 0.5); downArrow.x = downBtn.x; downArrow.y = downBtn.y; LK.gui.bottomLeft.addChild(downArrow); // Up-Left button var upLeftBtn = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.7, scaleY: 1.7, x: dpadX - diagOffset, y: dpadY - diagOffset, alpha: 0.45 }); LK.gui.bottomLeft.addChild(upLeftBtn); var upLeftArrow = new Text2("ā¤", { size: 180, fill: 0x222244 }); upLeftArrow.anchor.set(0.5, 0.5); upLeftArrow.x = upLeftBtn.x; upLeftArrow.y = upLeftBtn.y; LK.gui.bottomLeft.addChild(upLeftArrow); // Up-Right button var upRightBtn = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.7, scaleY: 1.7, x: dpadX + diagOffset, y: dpadY - diagOffset, alpha: 0.45 }); LK.gui.bottomLeft.addChild(upRightBtn); var upRightArrow = new Text2("ā„", { size: 180, fill: 0x222244 }); upRightArrow.anchor.set(0.5, 0.5); upRightArrow.x = upRightBtn.x; upRightArrow.y = upRightBtn.y; LK.gui.bottomLeft.addChild(upRightArrow); // Down-Left button var downLeftBtn = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.7, scaleY: 1.7, x: dpadX - diagOffset, y: dpadY + diagOffset, alpha: 0.45 }); LK.gui.bottomLeft.addChild(downLeftBtn); var downLeftArrow = new Text2("ā£", { size: 180, fill: 0x222244 }); downLeftArrow.anchor.set(0.5, 0.5); downLeftArrow.x = downLeftBtn.x; downLeftArrow.y = downLeftBtn.y; LK.gui.bottomLeft.addChild(downLeftArrow); // Down-Right button var downRightBtn = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.7, scaleY: 1.7, x: dpadX + diagOffset, y: dpadY + diagOffset, alpha: 0.45 }); LK.gui.bottomLeft.addChild(downRightBtn); var downRightArrow = new Text2("ā¢", { size: 180, fill: 0x222244 }); downRightArrow.anchor.set(0.5, 0.5); downRightArrow.x = downRightBtn.x; downRightArrow.y = downRightBtn.y; LK.gui.bottomLeft.addChild(downRightArrow); // (Diagonal D-pad buttons added) // D-pad button handlers leftBtn.down = function () { if (game.paused) return; leftPressed = true; }; leftBtn.up = function () { leftPressed = false; }; rightBtn.down = function () { if (game.paused) return; rightPressed = true; }; rightBtn.up = function () { rightPressed = false; }; upBtn.down = function () { if (game.paused) return; upPressed = true; jumpPressed = true; }; upBtn.up = function () { upPressed = false; jumpPressed = false; }; downBtn.down = function () { if (game.paused) return; downPressed = true; }; downBtn.up = function () { downPressed = false; }; // Up-Left button handlers upLeftBtn.down = function () { if (game.paused) return; upPressed = true; leftPressed = true; jumpPressed = true; }; upLeftBtn.up = function () { upPressed = false; leftPressed = false; jumpPressed = false; }; // Up-Right button handlers upRightBtn.down = function () { if (game.paused) return; upPressed = true; rightPressed = true; jumpPressed = true; }; upRightBtn.up = function () { upPressed = false; rightPressed = false; jumpPressed = false; }; // Down-Left button handlers downLeftBtn.down = function () { if (game.paused) return; downPressed = true; leftPressed = true; }; downLeftBtn.up = function () { downPressed = false; leftPressed = false; }; // Down-Right button handlers downRightBtn.down = function () { if (game.paused) return; downPressed = true; rightPressed = true; }; downRightBtn.up = function () { downPressed = false; rightPressed = false; }; // (Diagonal D-pad button handlers added) var fireBtn = new Text2('š„', { size: 170, fill: 0xF6F1C7 }); fireBtn.anchor.set(0.5, 0.5); LK.gui.bottomRight.addChild(fireBtn); fireBtn.x = -500; fireBtn.y = -220; // Control state var leftPressed = false; var rightPressed = false; var upPressed = false; var downPressed = false; var jumpPressed = false; var firePressed = false; // (Diagonal (ara yƶn) support removed) // Touch handlers for fire button fireBtn.down = function (x, y, obj) { if (game.paused) return; firePressed = true; LK.getSound('Fire').play(); }; fireBtn.up = function (x, y, obj) { if (game.paused) return; firePressed = false; }; var player; var platforms = []; var enemies = []; var freezeOrbs = []; var iceBlocks = []; var powerups = []; var stage = 1; var stageCleared = false; var stageTimer = 0; var dragNode = null; var moveStartX = 0; var moveDir = 0; var scoreTxt; // Add lives var lives = 1; var livesTxt = new Text2('Lives: ' + lives, { size: 70, fill: 0xF6F1C7 }); livesTxt.anchor.set(0, 0); LK.gui.top.addChild(livesTxt); // Place lives at top left, but not in the 100x100 reserved area livesTxt.x = 120; livesTxt.y = 20; // Score display scoreTxt = new Text2('0', { size: 120, fill: 0xF6F1C7 }); scoreTxt.anchor.set(0.5, 0); LK.gui.top.addChild(scoreTxt); // Per-stage score tracking var stageScores = []; var currentStageScore = 0; // Always keep score display updated if (typeof scoreUpdateTimerId === "undefined") { var scoreUpdateTimerId = LK.setInterval(function () { if (scoreTxt && typeof LK.getScore === "function") { var currentScore = LK.getScore(); if (scoreTxt.text !== String(currentScore)) { scoreTxt.setText(currentScore); } } }, 100); } // Stage display var stageTxt = new Text2('Stage 1', { size: 70, fill: 0xB8E6FF }); stageTxt.anchor.set(0.5, 0); LK.gui.top.addChild(stageTxt); stageTxt.y = 120; // Helper: spawn platforms function spawnPlatforms() { // Clear old for (var i = 0; i < platforms.length; i++) platforms[i].destroy(); platforms = []; // Floor var floor = new Platform(1024, 2732 - 40, 2048); game.addChild(floor); platforms.push(floor); // Snow Bros style: 5-6 fixed rows, wide platforms, even spacing, classic arcade var rows = 6; var yStart = 2732 - 300; var yStep = 320; for (var i = 0; i < rows; i++) { var y = yStart - i * yStep; // Classic: alternate left/right gaps for each row if (i % 2 === 0) { // Full width platform var plat = new Platform(1024, y, 1600); game.addChild(plat); platforms.push(plat); } else { // Two half platforms with a gap in the middle var leftPlat = new Platform(512, y, 700); var rightPlat = new Platform(1536, y, 700); game.addChild(leftPlat); game.addChild(rightPlat); platforms.push(leftPlat); platforms.push(rightPlat); } } } // Helper: spawn enemies function spawnEnemies() { for (var i = 0; i < enemies.length; i++) enemies[i].destroy(); enemies = []; var ghostCount = 2 + Math.floor(stage / 2); var batCount = 3 + Math.floor(stage / 2); // Increased number of bats for more slow bats for (var i = 0; i < ghostCount; i++) { var px = 200 + Math.random() * (2048 - 400); var py = 400 + Math.random() * 1200; var g = new Ghost(px, py); // Make ghosts faster in stage 2+ if (stage >= 2) { var speed = (Math.random() > 0.5 ? 1 : -1) * (5 + Math.random() * 2.5); var angle = Math.random() * Math.PI * 2; g.vx = Math.cos(angle) * speed; g.vy = Math.sin(angle) * speed * 0.7; } game.addChild(g); enemies.push(g); } for (var i = 0; i < batCount; i++) { var px = 200 + Math.random() * (2048 - 400); var py = 300 + Math.random() * 1000; var b = new Bat(px, py); // Make bats faster in stage 2+ if (stage >= 2) { b.vx = (Math.random() > 0.5 ? 1 : -1) * (5 + Math.random() * 2.5); b.vy = (Math.random() > 0.5 ? 1 : -1) * 1.2; } game.addChild(b); enemies.push(b); } } // Helper: spawn powerups function spawnPowerups() { for (var i = 0; i < powerups.length; i++) powerups[i].destroy(); powerups = []; // Always one moon per stage var moon = new MoonPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200); game.addChild(moon); powerups.push(moon); // Limit shields to max 3 per stage var shieldsThisStage = 0; // 50% chance for shield if (Math.random() < 0.5 && shieldsThisStage < 3) { var shield = new ShieldPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200); game.addChild(shield); powerups.push(shield); shieldsThisStage++; } // 50% chance for speed if (Math.random() < 0.5) { var speed = new SpeedPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200); game.addChild(speed); powerups.push(speed); } // Always spawn a lifepotion at the beginning of every stage var lifepotion = new LifepotionPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200); game.addChild(lifepotion); powerups.push(lifepotion); // 40% chance for an extra lifepotion if (Math.random() < 0.4) { var extraLifepotion = new LifepotionPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200); game.addChild(extraLifepotion); powerups.push(extraLifepotion); } // Store shield count for this stage globally for use in other shield spawns game.shieldsThisStage = shieldsThisStage; } // Start new stage function startStage() { stageCleared = false; stageTimer = 0; stageTxt.setText('Stage ' + stage); // Store previous stage score if (typeof currentStageScore !== "undefined" && stage > 1) { stageScores[stage - 2] = currentStageScore; } // Reset score for new stage currentStageScore = 0; LK.setScore(0); scoreTxt.setText('0'); lives = 7; livesTxt.setText('Lives: ' + lives); spawnPlatforms(); spawnEnemies(); // Reset shield count for this stage before spawning powerups game.shieldsThisStage = 0; spawnPowerups(); // Send Yu in stages 2, 3, 4, 5 if (stage >= 2 && stage <= 5) { var yu = new YuPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200); game.addChild(yu); powerups.push(yu); } // Place player if (player) player.destroy(); player = new Evanasense(); player.x = 1024; player.y = 2732 - 200; game.addChild(player); } // Begin first stage startStage(); // Timer to spawn a random shield every 30 seconds if (typeof shieldTimerId === "undefined") { var shieldTimerId = LK.setInterval(function () { // Count current shields on the field var shieldCount = 0; for (var i = 0; i < powerups.length; i++) { if (powerups[i].type === 'shield') shieldCount++; } // Also count shields spawned this stage (if tracked) if (typeof game.shieldsThisStage === "undefined") game.shieldsThisStage = shieldCount; // Only spawn if less than 3 shields on the field and not more than 3 spawned this stage if (shieldCount < 3 && game.shieldsThisStage < 3) { var shield = new ShieldPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200); game.addChild(shield); powerups.push(shield); game.shieldsThisStage++; } }, 30000); // 30,000 ms = 30 seconds } // Timer to spawn a speed powerup every 40 seconds, only one at a time if (typeof speedPowerTimerId === "undefined") { var speedPowerTimerId = LK.setInterval(function () { // Only spawn if there is no speed powerup currently on the field var hasSpeed = false; for (var i = 0; i < powerups.length; i++) { if (powerups[i].type === 'speed') { hasSpeed = true; break; } } if (!hasSpeed) { var speed = new SpeedPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200); game.addChild(speed); powerups.push(speed); } }, 40000); // 40,000 ms = 40 seconds } // Timer to spawn a broom every 30 seconds if (typeof broomTimerId === "undefined") { var broomTimerId = LK.setInterval(function () { // Spawn a broom powerup at a random position var broom = new BroomPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200); game.addChild(broom); powerups.push(broom); }, 30000); // 30,000 ms = 30 seconds } // Timer to send 10 Yu powerups, one every 3 seconds, only in stage 2+ if (typeof yuSendCount === "undefined") { var yuSendCount = 0; var yuSendTimerId = LK.setInterval(function () { // Only send Yu if stage >= 2 if (stage >= 2 && yuSendCount < 10) { var yu = new YuPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200); game.addChild(yu); powerups.push(yu); yuSendCount++; } // Reset yuSendCount if stage changes (so Yu can be sent again in new stage) if (typeof lastYuStage === "undefined") { var lastYuStage = stage; } if (stage !== lastYuStage) { yuSendCount = 0; lastYuStage = stage; } if (yuSendCount >= 10) { // Don't clear interval, just stop sending until next stage } }, 3000); // 3,000 ms = 3 seconds } // Bat flying effect: swap bat/bat2 asset every 1 second if (typeof batFlyTimerId === "undefined") { var batFlyTimerId = LK.setInterval(function () { for (var i = 0; i < enemies.length; i++) { var e = enemies[i]; if (e instanceof Bat && e.children && e.children.length > 0) { // Find the bat asset (assume it's the first child) var batSprite = e.children[0]; // Swap asset: if bat, change to bat2; if bat2, change to bat var currentAssetId = batSprite.assetId || 'bat'; e.removeChild(batSprite); var newAssetId = currentAssetId === 'bat' ? 'bat2' : 'bat'; var newBat = e.attachAsset(newAssetId, { anchorX: 0.5, anchorY: 0.5 }); // Store which asset is currently used newBat.assetId = newAssetId; // Move new bat to the front e.setChildIndex(newBat, 0); } } }, 1000); // 1000 ms = 1 second } // Ghost flying effect: swap ghost/ghost2 asset every 1 second if (typeof ghostFlyTimerId === "undefined") { var ghostFlyTimerId = LK.setInterval(function () { for (var i = 0; i < enemies.length; i++) { var e = enemies[i]; if (e instanceof Ghost && e.children && e.children.length > 0) { // Find the ghost asset (assume it's the first child) var ghostSprite = e.children[0]; // Swap asset: if ghost, change to ghost2; if ghost2, change to ghost var currentAssetId = ghostSprite.assetId || 'ghost'; e.removeChild(ghostSprite); var newAssetId = currentAssetId === 'ghost' ? 'Ghost2' : 'ghost'; var newGhost = e.attachAsset(newAssetId, { anchorX: 0.5, anchorY: 0.5, alpha: 0.85 }); // Store which asset is currently used newGhost.assetId = newAssetId; // Move new ghost to the front e.setChildIndex(newGhost, 0); } } }, 1000); // 1000 ms = 1 second } // Add portals to the game var portal1 = new Container(); var portal1Sprite = portal1.attachAsset('Portal1', { anchorX: 0.5, anchorY: 0.5 }); portal1.x = 400; portal1.y = 400; game.addChild(portal1); var portal2 = new Container(); var portal2Sprite = portal2.attachAsset('Portal2', { anchorX: 0.5, anchorY: 0.5 }); portal2.x = 1600; portal2.y = 2000; game.addChild(portal2); // Track last intersection state for portal1 var lastPortal1Intersecting = false; // Main update loop game.update = function () { // Pause all gameplay and input until start is pressed if (game.paused) return; // (Removed shield spawn every 25 points logic) // --- Shield spawn every 25 points, only once per threshold --- if (typeof lastShieldScore === "undefined") { var lastShieldScore = 0; } var currentScore = LK.getScore(); if (currentScore > 0 && currentScore % 25 === 0 && lastShieldScore !== currentScore) { // Only spawn one shield per threshold, and only if less than 3 shields this stage var shieldCount = 0; for (var i = 0; i < powerups.length; i++) { if (powerups[i].type === 'shield') shieldCount++; } if (typeof game.shieldsThisStage === "undefined") game.shieldsThisStage = shieldCount; if (shieldCount < 3 && game.shieldsThisStage < 3) { var shield = new ShieldPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200); game.addChild(shield); powerups.push(shield); game.shieldsThisStage++; } lastShieldScore = currentScore; } if (currentScore % 25 !== 0) { // Reset so next threshold can trigger lastShieldScore = 0; } // Update player if (player) player.update(); // Portal teleport logic if (player && portal1 && portal2) { // Portal1 -> Portal2 var isIntersecting1 = player.intersects(portal1); if (!lastPortal1Intersecting && isIntersecting1) { // Teleport player to portal2's position player.x = portal2.x; player.y = portal2.y; // Optional: flash effect to show teleport LK.effects.flashObject(player, 0xb8e6ff, 300); } lastPortal1Intersecting = isIntersecting1; // Portal2 -> Portal1 if (typeof lastPortal2Intersecting === "undefined") { var lastPortal2Intersecting = false; } var isIntersecting2 = player.intersects(portal2); if (!lastPortal2Intersecting && isIntersecting2) { // Teleport player to portal1's position player.x = portal1.x; player.y = portal1.y; // Optional: flash effect to show teleport LK.effects.flashObject(player, 0xb8e6ff, 300); } lastPortal2Intersecting = isIntersecting2; } // Track ice orb hits for bat freeze mechanic if (typeof batFreezeHitCount === "undefined") { var batFreezeHitCount = 0; var batFreezeActive = false; var batFreezeTimer = 0; } // --- FreezeOrb grow effect: 3x size for 5 seconds after moon --- if (typeof freezeOrbGrowTimer === "undefined") { var freezeOrbGrowTimer = 0; } if (freezeOrbGrowTimer > 0) { freezeOrbGrowTimer--; for (var i = 0; i < freezeOrbs.length; i++) { var orb = freezeOrbs[i]; if (!orb._grew) { if (orb.children && orb.children.length > 0) { var sprite = orb.children[0]; sprite.scaleX = 3; sprite.scaleY = 3; orb._grew = true; } } } } else { for (var i = 0; i < freezeOrbs.length; i++) { var orb = freezeOrbs[i]; if (orb._grew && orb.children && orb.children.length > 0) { var sprite = orb.children[0]; sprite.scaleX = 1; sprite.scaleY = 1; orb._grew = false; } } } // Update freeze orbs for (var i = freezeOrbs.length - 1; i >= 0; i--) { var orb = freezeOrbs[i]; orb.update(); // Collide with enemies for (var j = 0; j < enemies.length; j++) { var e = enemies[j]; if (!e.frozen && !e.trapped && orb.intersects(e)) { // --- Freeze effect for all enemies hit by freeze_orb --- if (typeof e.freeze === "function") { e.freeze(); LK.getSound('hit').play(); } // Add a visual freeze effect: overlay an ice_block image on top of the enemy for the freeze duration if (!e.iceOverlay) { e.iceOverlay = e.attachAsset('ice_block', { anchorX: 0.5, anchorY: 0.5, alpha: 0.6 }); // Make sure overlay is above the enemy sprite e.setChildIndex(e.iceOverlay, e.children.length - 1); } // Remove overlay when unfrozen (handled in enemy update) // If enemy is a Bat, count the hit if (e instanceof Bat) { batFreezeHitCount++; // When 3 hits, freeze all bats for 60 seconds (3600 frames) if (batFreezeHitCount >= 3 && !batFreezeActive) { batFreezeActive = true; batFreezeTimer = 3600; for (var k = 0; k < enemies.length; k++) { if (enemies[k] instanceof Bat) { enemies[k].frozen = true; enemies[k].frozenTimer = 3600; if (enemies[k].children && enemies[k].children.length > 0) { enemies[k].children[0].tint = 0x7fdfff; } // Add overlay to all bats if (!enemies[k].iceOverlay) { enemies[k].iceOverlay = enemies[k].attachAsset('ice_block', { anchorX: 0.5, anchorY: 0.5, alpha: 0.6 }); enemies[k].setChildIndex(enemies[k].iceOverlay, enemies[k].children.length - 1); } } } } } orb.destroy(); freezeOrbs.splice(i, 1); break; } } } // Bat freeze global timer if (batFreezeActive) { batFreezeTimer--; if (batFreezeTimer <= 0) { batFreezeActive = false; batFreezeHitCount = 0; // Unfreeze all bats for (var k = 0; k < enemies.length; k++) { if (enemies[k] instanceof Bat) { enemies[k].frozen = false; enemies[k].frozenTimer = 0; if (enemies[k].children && enemies[k].children.length > 0) { enemies[k].children[0].tint = 0x2a2a2a; } } } } } // Update ice blocks for (var i = iceBlocks.length - 1; i >= 0; i--) { var block = iceBlocks[i]; block.update(); // Collide with enemies for (var j = 0; j < enemies.length; j++) { var e = enemies[j]; if (!e.trapped && block.intersects(e)) { if (typeof e.trap === "function") { e.trap(); } block.destroy(); iceBlocks.splice(i, 1); break; } } } // Update enemies if (typeof globalEnemyPauseTimer === "undefined") { var globalEnemyPauseTimer = 0; } if (globalEnemyPauseTimer > 0) { globalEnemyPauseTimer--; for (var i = enemies.length - 1; i >= 0; i--) { var e = enemies[i]; // Only update if enemy is frozen/trapped (so shatter still works), but skip normal update if (e.frozen || e.trapped) { e.update(); } // Keep pausedBySpeed flag set e.pausedBySpeed = true; // If frozen or trapped, can be shattered by touching again if ((e.frozen || e.trapped) && player && player.intersects(e)) { e.shatter(); } // If not frozen/trapped, collision with player if (!e.frozen && !e.trapped && player && player.intersects(e)) { if (player.shielded) { player.loseShield(); LK.getSound('hit').play(); LK.effects.flashObject(player, 0x8fffd6, 400); } else { lives--; livesTxt.setText('Lives: ' + lives); LK.effects.flashScreen(0x7e4a9c, 900); if (lives <= 0) { // Save and show best score var bestScore = 0; if (typeof storage !== "undefined" && typeof storage.get === "function") { bestScore = storage.get("bestScore") || 0; if (LK.getScore() > bestScore) { bestScore = LK.getScore(); storage.set("bestScore", bestScore); } } // Show best score overlay var bestScoreOverlay = new Container(); var bestScoreText = new Text2("Best Score: " + bestScore, { size: 120, fill: 0xF6F1C7, align: "center" }); bestScoreText.anchor.set(0.5, 0.5); bestScoreText.x = 1024; bestScoreText.y = 1366; bestScoreOverlay.addChild(bestScoreText); LK.gui.addChild(bestScoreOverlay); // Remove overlay after 2.5 seconds LK.setTimeout(function () { if (bestScoreOverlay.parent) bestScoreOverlay.parent.removeChild(bestScoreOverlay); }, 2500); LK.showGameOver(); return; } else { // Respawn player at start position player.x = 1024; player.y = 2732 - 200; player.vx = 0; player.vy = 0; player.loseShield(); } } } } // When timer ends, unpause all enemies if (globalEnemyPauseTimer === 0) { for (var i = 0; i < enemies.length; i++) { enemies[i].pausedBySpeed = false; } } } else { for (var i = enemies.length - 1; i >= 0; i--) { var e = enemies[i]; e.update(); // If frozen or trapped, can be shattered by touching again if ((e.frozen || e.trapped) && player && player.intersects(e)) { e.shatter(); } // If not frozen/trapped, collision with player if (!e.frozen && !e.trapped && player && player.intersects(e)) { if (player.shielded) { player.loseShield(); LK.getSound('hit').play(); LK.effects.flashObject(player, 0x8fffd6, 400); } else { lives--; livesTxt.setText('Lives: ' + lives); LK.effects.flashScreen(0x7e4a9c, 900); if (lives <= 0) { // Save and show best score var bestScore = 0; if (typeof storage !== "undefined" && typeof storage.get === "function") { bestScore = storage.get("bestScore") || 0; if (LK.getScore() > bestScore) { bestScore = LK.getScore(); storage.set("bestScore", bestScore); } } // Show best score overlay var bestScoreOverlay = new Container(); var bestScoreText = new Text2("Best Score: " + bestScore, { size: 120, fill: 0xF6F1C7, align: "center" }); bestScoreText.anchor.set(0.5, 0.5); bestScoreText.x = 1024; bestScoreText.y = 1366; bestScoreOverlay.addChild(bestScoreText); LK.gui.addChild(bestScoreOverlay); // Remove overlay after 2.5 seconds LK.setTimeout(function () { if (bestScoreOverlay.parent) bestScoreOverlay.parent.removeChild(bestScoreOverlay); }, 2500); LK.showGameOver(); return; } else { // Respawn player at start position player.x = 1024; player.y = 2732 - 200; player.vx = 0; player.vy = 0; player.loseShield(); } } } } } // Update powerups for (var i = powerups.length - 1; i >= 0; i--) { var p = powerups[i]; if (player && player.intersects(p)) { if (typeof moonCollectCount === "undefined") { var moonCollectCount = 0; } if (p.type === 'moon') { moonCollectCount++; player.activateFullMoon(); // Start moon jump timer for 10 seconds (600 frames) player.moonJumpTimer = 600; LK.setScore(LK.getScore() + 5); scoreTxt.setText(LK.getScore()); currentStageScore = LK.getScore(); currentStageScore = LK.getScore(); currentStageScore = LK.getScore(); // (Lightning effect removed when collecting moon) // --- FreezeOrb grow effect: 3x size for 5 seconds --- if (typeof freezeOrbGrowTimer === "undefined") { var freezeOrbGrowTimer = 0; } freezeOrbGrowTimer = 300; // 5 seconds at 60fps // Spawn a new moon at a random position var moon = new MoonPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200); game.addChild(moon); powerups.push(moon); // If 5 moons collected, spawn a random shield and reset counter if (typeof lastMoonShieldGiven === "undefined") { var lastMoonShieldGiven = 0; } if (moonCollectCount >= 5) { moonCollectCount = 0; // Only give one shield per 5 moons, and only if less than 3 shields this stage var shieldCount = 0; for (var i = 0; i < powerups.length; i++) { if (powerups[i].type === 'shield') shieldCount++; } if (typeof game.shieldsThisStage === "undefined") game.shieldsThisStage = shieldCount; if (shieldCount < 3 && game.shieldsThisStage < 3 && lastMoonShieldGiven !== LK.getScore()) { var shield = new ShieldPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200); game.addChild(shield); powerups.push(shield); game.shieldsThisStage++; lastMoonShieldGiven = LK.getScore(); } } // If 100 moons collected, spawn a shield (only once per 100 moons) if (typeof lastHundredMoonShieldGiven === "undefined") { var lastHundredMoonShieldGiven = 0; } if (moonCollectCountTotal === undefined) { var moonCollectCountTotal = 0; } moonCollectCountTotal++; if (moonCollectCountTotal > 0 && moonCollectCountTotal % 100 === 0 && lastHundredMoonShieldGiven !== moonCollectCountTotal) { // Only spawn if less than 3 shields this stage var shieldCount = 0; for (var i = 0; i < powerups.length; i++) { if (powerups[i].type === 'shield') shieldCount++; } if (typeof game.shieldsThisStage === "undefined") game.shieldsThisStage = shieldCount; if (shieldCount < 3 && game.shieldsThisStage < 3) { var shield = new ShieldPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200); game.addChild(shield); powerups.push(shield); game.shieldsThisStage++; lastHundredMoonShieldGiven = moonCollectCountTotal; } } } else if (p.type === 'shield') { player.gainShield(); // Send ice orbs in all directions var orbCount = 12; for (var d = 0; d < orbCount; d++) { var angle = 2 * Math.PI * d / orbCount; var dirX = Math.cos(angle); var orb = new FreezeOrb(player.x, player.y - 60, dirX); // Give each orb a custom vx/vy for radial spread orb.vx = 24 * Math.cos(angle); orb.vy = 24 * Math.sin(angle); // Override update to move in both x and y (function (orb) { var baseUpdate = orb.update; orb.update = function () { orb.x += orb.vx; orb.y += orb.vy; orb.lifetime--; // Wrap horizontally if (orb.x < 0) orb.x = 2048; if (orb.x > 2048) orb.x = 0; // Remove if out of bounds vertically if (orb.y < 0 || orb.y > 2732 || orb.lifetime <= 0) { orb.destroy(); var idx = freezeOrbs.indexOf(orb); if (idx >= 0) freezeOrbs.splice(idx, 1); return; } }; })(orb); game.addChild(orb); freezeOrbs.push(orb); } } else if (p.type === 'speed') { player.gainSpeed(); player.speedBoost = 240; // Speed lasts 4 seconds (60fps*4) // Only freeze all enemies for 10 seconds if not already frozen by speed if (typeof globalEnemyPauseTimer === "undefined") { var globalEnemyPauseTimer = 0; } if (globalEnemyPauseTimer <= 0) { globalEnemyPauseTimer = 600; // 10 seconds at 60fps for (var ep = 0; ep < enemies.length; ep++) { enemies[ep].pausedBySpeed = true; } } } else if (p.type === 'lifepotion') { lives++; livesTxt.setText('Lives: ' + lives); LK.effects.flashObject(player, 0x8fffd6, 400); } else if (p.type === 'broom') { // Witch can now fly for 20 seconds (1200 frames) player.canFly = true; player.flyTimer = 1200; LK.effects.flashObject(player, 0xb8e6ff, 600); } else if (p.type === 'yu') { // Cancel speed power when witch takes the yu player.speedBoost = 0; // Enable double jump for the player player.yuDoubleJump = true; player.hasDoubleJumped = false; // Remove speed pause from all enemies if (typeof globalEnemyPauseTimer !== "undefined") { globalEnemyPauseTimer = 0; for (var ep = 0; ep < enemies.length; ep++) { enemies[ep].pausedBySpeed = false; } } // Prevent shooting for 2 seconds (120 frames) player.canShoot = false; player.shootCooldown = 120; } LK.getSound('powerup').play(); p.destroy(); powerups.splice(i, 1); } } // Full moon: allow trap spell by tap with two fingers (simulate by double tap) if (player && player.fullMoon && LK.ticks % 60 === 0) { // For MVP, allow trap spell every second in full moon player.castTrap(); } // Stage clear if (!stageCleared && enemies.length === 0) { stageCleared = true; stageTimer = 90; LK.effects.flashScreen(0xb8e6ff, 600); } if (stageCleared) { stageTimer--; if (stageTimer <= 0) { stage++; if (stage % 5 === 0) { // Boss stage: show Boss asset and spawn extra enemies stageTxt.setText('Boss Stage!'); // Spawn Boss at center as a moving enemy var boss = new Boss(1024, 900); game.addChild(boss); enemies.push(boss); for (var i = 0; i < 3; i++) { var px = 200 + Math.random() * (2048 - 400); var py = 400 + Math.random() * 1200; var g = new Ghost(px, py); game.addChild(g); enemies.push(g); } for (var i = 0; i < 2; i++) { var px = 200 + Math.random() * (2048 - 400); var py = 300 + Math.random() * 1000; var b = new Bat(px, py); game.addChild(b); enemies.push(b); } stageCleared = false; } else { startStage(); } } } }; LK.playMusic('gothic_theme', { fade: { start: 0, end: 1, duration: 1200 } }); // Play music
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Enemy: Bat
var Bat = Container.expand(function (x, y) {
var self = Container.call(this);
var bat = self.attachAsset('bat', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = x;
self.y = y;
self.vx = (Math.random() > 0.5 ? 1 : -1) * (2.5 + Math.random() * 2); // Slower bats
self.vy = (Math.random() > 0.5 ? 1 : -1) * 0.5; // Slower vertical movement
self.frozen = false;
self.frozenTimer = 0;
self.trapped = false;
self.trapTimer = 0;
self.update = function () {
// Pause logic for speed powerup
if (self.pausedBySpeed) {
return;
}
if (self.trapped) {
self.trapTimer--;
if (self.trapTimer <= 0) {
self.trapped = false;
self.alpha = 1;
}
return;
}
if (self.frozen) {
self.frozenTimer--;
if (self.frozenTimer <= 0) {
self.frozen = false;
bat.tint = 0x2a2a2a;
// Remove freeze overlay if present
if (self.iceOverlay) {
self.removeChild(self.iceOverlay);
self.iceOverlay = null;
}
}
return;
}
self.x += self.vx;
self.y += self.vy;
// Snow Bros style: wrap horizontally
if (self.x < 0) self.x = 2048;
if (self.x > 2048) self.x = 0;
if (self.y < 200 || self.y > 2732 - 200) self.vy *= -1;
// Prevent bats from touching platforms: bounce off if intersecting
for (var i = 0; i < platforms.length; i++) {
var p = platforms[i];
if (self.intersects(p)) {
// Bounce bat away from platform
// Determine if bat is coming from above/below or left/right
var dx = self.x - p.x;
var dy = self.y - p.y;
if (Math.abs(dx) > Math.abs(dy)) {
// Bounce horizontally
self.vx *= -1;
// Move bat out of platform horizontally
if (dx > 0) {
self.x = p.x + p.width / 2 + 40;
} else {
self.x = p.x - p.width / 2 - 40;
}
} else {
// Bounce vertically
self.vy *= -1;
// Move bat out of platform vertically
if (dy > 0) {
self.y = p.y + p.height / 2 + 40;
} else {
self.y = p.y - p.height / 2 - 40;
}
}
}
}
};
// Freeze effect
self.freeze = function () {
self.frozen = true;
self.frozenTimer = 180;
bat.tint = 0x7fdfff;
};
// Trap effect
self.trap = function () {
self.trapped = true;
self.trapTimer = 90;
self.alpha = 0.4;
};
// Shatter (destroy)
self.shatter = function () {
LK.getSound('shatter').play();
self.destroy();
var idx = enemies.indexOf(self);
if (idx >= 0) enemies.splice(idx, 1);
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore());
};
return self;
});
// Boss: Moves randomly
var Boss = Container.expand(function (x, y) {
var self = Container.call(this);
var boss = self.attachAsset('Boss', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = x;
self.y = y;
self.vx = 0;
self.vy = 0;
self.randomMoveTimer = 60 + Math.floor(Math.random() * 60);
self.lastX = self.x;
self.lastY = self.y;
self.update = function () {
// Random movement logic
self.randomMoveTimer--;
if (self.randomMoveTimer <= 0) {
var speed = 6 + Math.random() * 4;
var angle = Math.random() * Math.PI * 2;
self.vx = Math.cos(angle) * speed;
self.vy = Math.sin(angle) * speed * 0.7;
self.randomMoveTimer = 60 + Math.floor(Math.random() * 60);
}
self.lastX = self.x;
self.lastY = self.y;
self.x += self.vx;
self.y += self.vy;
// Bounce vertically at top/bottom bounds
if (self.lastY <= 200 && self.y > 200 || self.lastY >= 2732 - 400 && self.y < 2732 - 400) {
self.vy *= -1;
}
if (self.y < 200) self.y = 200;
if (self.y > 2732 - 400) self.y = 2732 - 400;
// Snow Bros style: wrap horizontally
if (self.x < 0) self.x = 2048;
if (self.x > 2048) self.x = 0;
};
return self;
});
// Power-up: Broom
var BroomPower = Container.expand(function (x, y) {
var self = Container.call(this);
var broom = self.attachAsset('Broom', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = x;
self.y = y;
self.type = 'broom';
self.vy = 7 + Math.random() * 3;
self.lastY = self.y;
self.update = function () {
self.lastY = self.y;
self.y += self.vy;
// Snow Bros style: wrap horizontally
if (self.x < 0) self.x = 2048;
if (self.x > 2048) self.x = 0;
// Bounce at vertical edges
if (self.lastY <= 200 && self.y > 200 || self.lastY >= 2732 - 200 && self.y < 2732 - 200) {
self.vy *= -1;
}
if (self.y < 200) self.y = 200;
if (self.y > 2732 - 200) self.y = 2732 - 200;
};
return self;
});
// Evanasense (player) class
var Evanasense = Container.expand(function () {
var self = Container.call(this);
// Body
var body = self.attachAsset('evana', {
anchorX: 0.5,
anchorY: 0.5
});
// Hat
var hat = self.attachAsset('evana_hat', {
anchorX: 0.5,
anchorY: 1.1,
y: -60
});
// Physics
self.vx = 0;
self.vy = 0;
self.lastY = 0;
self.isOnGround = false;
self.facing = 1; // 1: right, -1: left
self.canShoot = true;
self.shootCooldown = 0;
self.shielded = false;
self.speedBoost = 0;
self.fullMoon = false;
self.fullMoonTimer = 0;
self.canFly = false;
self.flyTimer = 0;
// For power-up visuals
self.shieldSprite = null;
// Freeze spell
self.castFreeze = function () {
if (!self.canShoot) return;
self.canShoot = false;
self.shootCooldown = 30; // 0.5s cooldown
var orb = new FreezeOrb(self.x, self.y - 60, self.facing);
game.addChild(orb);
freezeOrbs.push(orb);
LK.getSound('freeze').play();
};
// Trap spell (ice block, only in full moon mode)
self.castTrap = function () {
if (!self.fullMoon) return;
var trap = new IceBlock(self.x + 100 * self.facing, self.y - 40);
game.addChild(trap);
iceBlocks.push(trap);
LK.getSound('trap').play();
};
// Power-up: Shield
self.gainShield = function () {
self.shielded = true;
if (!self.shieldSprite) {
self.shieldSprite = self.attachAsset('shield', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.5
});
}
self.shieldSprite.visible = true;
};
self.loseShield = function () {
self.shielded = false;
if (self.shieldSprite) self.shieldSprite.visible = false;
};
// Power-up: Speed
self.gainSpeed = function () {
self.speedBoost = 180;
LK.effects.flashObject(self, 0xffe066, 400);
};
// Power-up: Full Moon
self.activateFullMoon = function () {
self.fullMoon = true;
self.fullMoonTimer = 360; // 6 seconds
LK.effects.flashObject(self, 0xf6f1c7, 600);
};
// Update
self.update = function () {
// Flying logic
if (self.canFly) {
// While flying, allow free vertical movement with up/down, and reduce gravity
if (upPressed) {
self.vy = -18;
} else if (downPressed) {
self.vy = 18;
} else {
self.vy *= 0.7;
if (Math.abs(self.vy) < 1) self.vy = 0;
}
// Optional: add a little gravity so she floats down if not pressing up
self.vy += 0.5;
if (self.vy > 24) self.vy = 24;
if (self.vy < -24) self.vy = -24;
// Flying timer
if (typeof self.flyTimer === "undefined") self.flyTimer = 600;
self.flyTimer--;
if (self.flyTimer <= 0) {
self.canFly = false;
self.flyTimer = 0;
}
} else {
// Gravity
self.vy += 2.2;
if (self.vy > 40) self.vy = 40;
}
// Set moveDir for left/right, and moveVert for up/down (support diagonals)
self.moveDir = 0;
self.moveVert = 0;
if (leftPressed && !rightPressed) {
self.moveDir = -1;
} else if (rightPressed && !leftPressed) {
self.moveDir = 1;
}
if (upPressed && !downPressed) {
self.moveVert = -1;
} else if (downPressed && !upPressed) {
self.moveVert = 1;
}
// If both a horizontal and vertical direction are pressed, normalize diagonal speed
if (self.moveDir !== 0 && self.moveVert !== 0) {
// Reduce speed for diagonal movement to keep consistent velocity
self.moveDir *= Math.SQRT1_2;
self.moveVert *= Math.SQRT1_2;
}
// --- Up/Down button logic (for future use, e.g. drop through platforms) ---
// (Handled above for movement)
// --- Fire button logic ---
if (firePressed && self.canShoot) {
self.castFreeze();
firePressed = false; // Only fire once per press
}
// Movement
var moveSpeed = 18 + (self.speedBoost > 0 ? 10 : 0);
if (self.moveDir || self.moveVert) {
self.vx = moveSpeed * self.moveDir;
self.vy = self.canFly ? moveSpeed * self.moveVert : self.vy;
if (self.moveDir) self.facing = self.moveDir > 0 ? 1 : -1;
} else {
self.vx *= 0.7;
if (Math.abs(self.vx) < 1) self.vx = 0;
if (!self.canFly) {
// Only apply gravity if not flying
// (already handled above)
}
}
// Apply position
self.lastY = self.y;
// --- Mario-style jump: allow jump if jumpPressed and isOnGround ---
// Double jump support: allow one extra jump if yuDoubleJump is true
if (typeof self.yuDoubleJump === "undefined") self.yuDoubleJump = false;
if (typeof self.hasDoubleJumped === "undefined") self.hasDoubleJumped = false;
if (jumpPressed) {
var jumpPower = -38;
if (self.moonJumpTimer && self.moonJumpTimer > 0) {
jumpPower = -54; // Higher jump during moon effect
}
if (self.isOnGround) {
self.vy = jumpPower;
self.isOnGround = false;
self.hasDoubleJumped = false;
} else if (self.yuDoubleJump && !self.hasDoubleJumped) {
self.vy = jumpPower;
self.hasDoubleJumped = true;
// Optional: flash effect for double jump
LK.effects.flashObject(self, 0xb8e6ff, 200);
}
}
if (self.moonJumpTimer && self.moonJumpTimer > 0) {
self.moonJumpTimer--;
if (self.moonJumpTimer === 0) {
// Optionally, add a visual effect to show moon jump ended
LK.effects.flashObject(self, 0xf6f1c7, 200);
}
}
self.x += self.vx;
self.y += self.vy;
// Snow Bros style: wrap horizontally
if (self.x < 0) self.x = 2048;
if (self.x > 2048) self.x = 0;
// Clamp vertically and collide with all platforms
self.isOnGround = false;
var prevBottom = self.lastY + body.height / 2;
var currBottom = self.y + body.height / 2;
for (var i = 0; i < platforms.length; i++) {
var plat = platforms[i];
var platTop = plat.y - plat.height / 2;
// Allow drop through platforms if downPressed, except for the floor (y >= 2732-60)
var isFloor = plat.y >= 2732 - 60;
if (self.vy > 0 && prevBottom <= platTop && currBottom >= platTop && self.x + body.width / 2 > plat.x - plat.width / 2 && self.x - body.width / 2 < plat.x + plat.width / 2 && (!downPressed || isFloor)) {
self.y = platTop - body.height / 2;
self.vy = 0;
self.isOnGround = true;
// Play platform sound when landing
if (typeof self.lastWasOnGround === "undefined") self.lastWasOnGround = false;
// (platform sound removed as requested)
self.lastWasOnGround = true;
} else {
if (typeof self.lastWasOnGround === "undefined") self.lastWasOnGround = false;
self.lastWasOnGround = false;
}
}
// Clamp to bottom of screen
if (self.y > 2732 - 60) {
self.y = 2732 - 60;
self.vy = 0;
self.isOnGround = true;
}
// Shooting cooldown
if (!self.canShoot) {
self.shootCooldown--;
if (self.shootCooldown <= 0) {
self.canShoot = true;
}
}
// Speed boost timer
if (self.speedBoost > 0) {
self.speedBoost--;
}
// Full moon timer
if (self.fullMoon) {
self.fullMoonTimer--;
if (self.fullMoonTimer <= 0) {
self.fullMoon = false;
}
}
};
return self;
});
// Freeze orb spell
var FreezeOrb = Container.expand(function (x, y, dir) {
var self = Container.call(this);
var orb = self.attachAsset('freeze_orb', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = x;
self.y = y;
self.vx = 32 * dir;
self.lifetime = 60;
self.update = function () {
self.x += self.vx;
// Snow Bros style: wrap horizontally
if (self.x < 0) self.x = 2048;
if (self.x > 2048) self.x = 0;
self.lifetime--;
if (self.lifetime <= 0) {
self.destroy();
var idx = freezeOrbs.indexOf(self);
if (idx >= 0) freezeOrbs.splice(idx, 1);
}
};
return self;
});
// Enemy: Ghost
var Ghost = Container.expand(function (x, y) {
var self = Container.call(this);
var ghost = self.attachAsset('ghost', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.85
});
self.x = x;
self.y = y;
self.vx = (Math.random() > 0.5 ? 1 : -1) * (3 + Math.random() * 2); // Slower ghosts
self.vy = (Math.random() - 0.5) * 2; // Add a little vertical movement
self.frozen = false;
self.frozenTimer = 0;
self.trapped = false;
self.trapTimer = 0;
self.randomMoveTimer = 30 + Math.floor(Math.random() * 60); // How many frames until next direction change
self.lastY = self.y;
self.update = function () {
// Pause logic for speed powerup
if (self.pausedBySpeed) {
return;
}
if (self.trapped) {
self.trapTimer--;
if (self.trapTimer <= 0) {
self.trapped = false;
self.alpha = 0.85;
}
return;
}
if (self.frozen) {
self.frozenTimer--;
if (self.frozenTimer <= 0) {
self.frozen = false;
ghost.tint = 0xcfd6e6;
// Remove freeze overlay if present
if (self.iceOverlay) {
self.removeChild(self.iceOverlay);
self.iceOverlay = null;
}
}
return;
}
// Random movement logic
self.randomMoveTimer--;
if (self.randomMoveTimer <= 0) {
// Change direction randomly
var speed = 3 + Math.random() * 2;
var angle = Math.random() * Math.PI * 2;
self.vx = Math.cos(angle) * speed;
self.vy = Math.sin(angle) * speed * 0.5; // Less vertical movement
self.randomMoveTimer = 30 + Math.floor(Math.random() * 60);
}
self.lastY = self.y;
self.x += self.vx;
self.y += self.vy;
// Bounce vertically at top/bottom bounds
if (self.lastY <= 200 && self.y > 200 || self.lastY >= 2732 - 200 && self.y < 2732 - 200) {
self.vy *= -1;
}
if (self.y < 200) self.y = 200;
if (self.y > 2732 - 200) self.y = 2732 - 200;
// Snow Bros style: wrap horizontally
if (self.x < 0) self.x = 2048;
if (self.x > 2048) self.x = 0;
};
// Freeze effect
self.freeze = function () {
self.frozen = true;
self.frozenTimer = 180;
ghost.tint = 0x7fdfff;
};
// Trap effect
self.trap = function () {
self.trapped = true;
self.trapTimer = 90;
self.alpha = 0.4;
};
// Shatter (destroy)
self.shatter = function () {
LK.getSound('shatter').play();
self.destroy();
var idx = enemies.indexOf(self);
if (idx >= 0) enemies.splice(idx, 1);
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore());
};
return self;
});
// Ice block trap (full moon mode)
var IceBlock = Container.expand(function (x, y) {
var self = Container.call(this);
var block = self.attachAsset('ice_block', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.85
});
self.x = x;
self.y = y;
self.lifetime = 120;
self.update = function () {
// Snow Bros style: wrap horizontally
if (self.x < 0) self.x = 2048;
if (self.x > 2048) self.x = 0;
self.lifetime--;
if (self.lifetime <= 0) {
self.destroy();
var idx = iceBlocks.indexOf(self);
if (idx >= 0) iceBlocks.splice(idx, 1);
}
};
return self;
});
// Power-up: Lifepotion (adds a life)
var LifepotionPower = Container.expand(function (x, y) {
var self = Container.call(this);
var potion = self.attachAsset('Lifepotion', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = x;
self.y = y;
self.type = 'lifepotion';
self.vy = 7 + Math.random() * 3;
self.lastY = self.y;
self.update = function () {
self.lastY = self.y;
self.y += self.vy;
// Snow Bros style: wrap horizontally
if (self.x < 0) self.x = 2048;
if (self.x > 2048) self.x = 0;
// Bounce at vertical edges
if (self.lastY <= 200 && self.y > 200 || self.lastY >= 2732 - 200 && self.y < 2732 - 200) {
self.vy *= -1;
}
if (self.y < 200) self.y = 200;
if (self.y > 2732 - 200) self.y = 2732 - 200;
};
return self;
});
// Power-up: Moon (full moon mode)
var MoonPower = Container.expand(function (x, y) {
var self = Container.call(this);
var moon = self.attachAsset('moon', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = x;
self.y = y;
self.type = 'moon';
self.vx = 3 + Math.random() * 2; // Slower moon movement
self.lastX = self.x;
self.update = function () {
self.lastX = self.x;
self.x += self.vx;
// Snow Bros style: wrap horizontally
if (self.x < 0) self.x = 2048;
if (self.x > 2048) self.x = 0;
};
return self;
});
// Platform
var Platform = Container.expand(function (x, y, w) {
var self = Container.call(this);
var plat = self.attachAsset('platform', {
anchorX: 0.5,
anchorY: 0.5,
width: w || 320
});
self.x = x;
self.y = y;
self.width = w || 320;
self.height = 40;
return self;
});
// Power-up: Shield
var ShieldPower = Container.expand(function (x, y) {
var self = Container.call(this);
var shield = self.attachAsset('shield', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = x;
self.y = y;
self.type = 'shield';
self.update = function () {
// Snow Bros style: wrap horizontally
if (self.x < 0) self.x = 2048;
if (self.x > 2048) self.x = 0;
};
return self;
});
// Power-up: Speed
var SpeedPower = Container.expand(function (x, y) {
var self = Container.call(this);
var speed = self.attachAsset('speed', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = x;
self.y = y;
self.type = 'speed';
self.vy = 7 + Math.random() * 3;
self.lastY = self.y;
self.update = function () {
self.lastY = self.y;
self.y += self.vy;
// Snow Bros style: wrap horizontally
if (self.x < 0) self.x = 2048;
if (self.x > 2048) self.x = 0;
// Bounce at vertical edges
if (self.lastY <= 200 && self.y > 200 || self.lastY >= 2732 - 200 && self.y < 2732 - 200) {
self.vy *= -1;
}
if (self.y < 200) self.y = 200;
if (self.y > 2732 - 200) self.y = 2732 - 200;
};
return self;
});
// Power-up: Yu (moves randomly)
var YuPower = Container.expand(function (x, y) {
var self = Container.call(this);
var yu = self.attachAsset('Yu', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = x;
self.y = y;
self.type = 'yu';
// Random movement variables
self.vx = (Math.random() > 0.5 ? 1 : -1) * (3 + Math.random() * 2);
self.vy = (Math.random() - 0.5) * 2;
self.randomMoveTimer = 30 + Math.floor(Math.random() * 60);
self.lastY = self.y;
self.update = function () {
// Random movement logic
self.randomMoveTimer--;
if (self.randomMoveTimer <= 0) {
var speed = 3 + Math.random() * 2;
var angle = Math.random() * Math.PI * 2;
self.vx = Math.cos(angle) * speed;
self.vy = Math.sin(angle) * speed * 0.5;
self.randomMoveTimer = 30 + Math.floor(Math.random() * 60);
}
self.lastY = self.y;
self.x += self.vx;
self.y += self.vy;
// Bounce vertically at top/bottom bounds
if (self.lastY <= 200 && self.y > 200 || self.lastY >= 2732 - 200 && self.y < 2732 - 200) {
self.vy *= -1;
}
if (self.y < 200) self.y = 200;
if (self.y > 2732 - 200) self.y = 2732 - 200;
// Snow Bros style: wrap horizontally
if (self.x < 0) self.x = 2048;
if (self.x > 2048) self.x = 0;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x18141e
});
/****
* Game Code
****/
// Add background image
// Add background image
var bg = LK.getAsset('bg', {
anchorX: 0,
anchorY: 0,
width: 2048,
height: 2732,
x: 0,
y: 0
});
game.addChild(bg);
// (rulesPanel removed as per request)
// --- Game Start Overlay ---
var startOverlay = new Container();
var rulesText = new Text2("š§āāļø Witch's Night: Game Rules\n\n" + "- Move the witch with the blue buttons.\n" + "- Tap the yellow button to cast freeze spells.\n" + "- Collect powerups: Moon (full moon mode), Shield, Speed, Life Potion, Broom (fly!), Yu (cancel speed).\n" + "- Avoid ghosts and bats! Touching them loses a life unless shielded.\n" + "- Freeze or trap enemies, then touch them again to shatter.\n" + "- Clear all enemies to advance to the next stage.\n" + "- Survive as long as you can!\n\n" + "Good luck! Tap Start to play.", {
size: 70,
fill: 0xF6F1C7,
align: "center",
wordWrap: true,
wordWrapWidth: 1600
});
rulesText.anchor.set(0.5, 0.5);
rulesText.x = 1024;
rulesText.y = 900;
startOverlay.addChild(rulesText);
var startBtn = new Text2("START", {
size: 180,
fill: 0xB8E6FF,
fontWeight: "bold"
});
startBtn.anchor.set(0.5, 0.5);
startBtn.x = 1024;
startBtn.y = 1800;
startOverlay.addChild(startBtn);
// Block gameplay until start is pressed
game.paused = true;
// Add start overlay to the very front so it appears above everything
LK.gui.addChild(startOverlay);
// Hide overlay and start game on button press
startBtn.down = function () {
if (startOverlay.parent) startOverlay.parent.removeChild(startOverlay);
game.paused = false;
};
// Main character: Evanasense (witch)
// Witch body
// Witch hat
// Enemy: Ghost
// Enemy: Bat
// Platform
// Spell: Freeze orb
// Spell: Trap (ice block)
// Power-up: Moon
// Power-up: Shield
// Power-up: Speed
// Sound effects
// Music
// --- Classic D-pad controls (eight buttons, with diagonals) ---
// D-pad button size and spacing (increased for better touch usability)
var dpadBtnSize = 260;
var dpadSpacing = 50;
// Move D-pad higher by decreasing dpadY (more negative = higher on screen)
var dpadY = -420;
var dpadX = 360;
// Diagonal offset (for diagonal buttons)
var diagOffset = Math.round((dpadBtnSize + dpadSpacing) * 0.7);
// Helper: reset all direction states (for compatibility)
function resetButtonDirections() {
leftPressed = false;
rightPressed = false;
upPressed = false;
downPressed = false;
// Also clear jumpPressed for diagonals
jumpPressed = false;
}
// Left button
var leftBtn = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.7,
scaleY: 1.7,
x: dpadX - dpadBtnSize - dpadSpacing,
y: dpadY,
alpha: 0.45
});
LK.gui.bottomLeft.addChild(leftBtn);
var leftArrow = new Text2("ā", {
size: 180,
fill: 0x222244
});
leftArrow.anchor.set(0.5, 0.5);
leftArrow.x = leftBtn.x;
leftArrow.y = leftBtn.y;
LK.gui.bottomLeft.addChild(leftArrow);
// Right button
var rightBtn = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.7,
scaleY: 1.7,
x: dpadX + dpadBtnSize + dpadSpacing,
y: dpadY,
alpha: 0.45
});
LK.gui.bottomLeft.addChild(rightBtn);
var rightArrow = new Text2("ā¶", {
size: 180,
fill: 0x222244
});
rightArrow.anchor.set(0.5, 0.5);
rightArrow.x = rightBtn.x;
rightArrow.y = rightBtn.y;
LK.gui.bottomLeft.addChild(rightArrow);
// Up button
var upBtn = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.7,
scaleY: 1.7,
x: dpadX,
y: dpadY - dpadBtnSize - dpadSpacing,
alpha: 0.45
});
LK.gui.bottomLeft.addChild(upBtn);
var upArrow = new Text2("ā²", {
size: 180,
fill: 0x222244
});
upArrow.anchor.set(0.5, 0.5);
upArrow.x = upBtn.x;
upArrow.y = upBtn.y;
LK.gui.bottomLeft.addChild(upArrow);
// Down button
var downBtn = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.7,
scaleY: 1.7,
x: dpadX,
y: dpadY + dpadBtnSize + dpadSpacing,
alpha: 0.45
});
LK.gui.bottomLeft.addChild(downBtn);
var downArrow = new Text2("ā¼", {
size: 180,
fill: 0x222244
});
downArrow.anchor.set(0.5, 0.5);
downArrow.x = downBtn.x;
downArrow.y = downBtn.y;
LK.gui.bottomLeft.addChild(downArrow);
// Up-Left button
var upLeftBtn = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.7,
scaleY: 1.7,
x: dpadX - diagOffset,
y: dpadY - diagOffset,
alpha: 0.45
});
LK.gui.bottomLeft.addChild(upLeftBtn);
var upLeftArrow = new Text2("ā¤", {
size: 180,
fill: 0x222244
});
upLeftArrow.anchor.set(0.5, 0.5);
upLeftArrow.x = upLeftBtn.x;
upLeftArrow.y = upLeftBtn.y;
LK.gui.bottomLeft.addChild(upLeftArrow);
// Up-Right button
var upRightBtn = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.7,
scaleY: 1.7,
x: dpadX + diagOffset,
y: dpadY - diagOffset,
alpha: 0.45
});
LK.gui.bottomLeft.addChild(upRightBtn);
var upRightArrow = new Text2("ā„", {
size: 180,
fill: 0x222244
});
upRightArrow.anchor.set(0.5, 0.5);
upRightArrow.x = upRightBtn.x;
upRightArrow.y = upRightBtn.y;
LK.gui.bottomLeft.addChild(upRightArrow);
// Down-Left button
var downLeftBtn = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.7,
scaleY: 1.7,
x: dpadX - diagOffset,
y: dpadY + diagOffset,
alpha: 0.45
});
LK.gui.bottomLeft.addChild(downLeftBtn);
var downLeftArrow = new Text2("ā£", {
size: 180,
fill: 0x222244
});
downLeftArrow.anchor.set(0.5, 0.5);
downLeftArrow.x = downLeftBtn.x;
downLeftArrow.y = downLeftBtn.y;
LK.gui.bottomLeft.addChild(downLeftArrow);
// Down-Right button
var downRightBtn = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.7,
scaleY: 1.7,
x: dpadX + diagOffset,
y: dpadY + diagOffset,
alpha: 0.45
});
LK.gui.bottomLeft.addChild(downRightBtn);
var downRightArrow = new Text2("ā¢", {
size: 180,
fill: 0x222244
});
downRightArrow.anchor.set(0.5, 0.5);
downRightArrow.x = downRightBtn.x;
downRightArrow.y = downRightBtn.y;
LK.gui.bottomLeft.addChild(downRightArrow);
// (Diagonal D-pad buttons added)
// D-pad button handlers
leftBtn.down = function () {
if (game.paused) return;
leftPressed = true;
};
leftBtn.up = function () {
leftPressed = false;
};
rightBtn.down = function () {
if (game.paused) return;
rightPressed = true;
};
rightBtn.up = function () {
rightPressed = false;
};
upBtn.down = function () {
if (game.paused) return;
upPressed = true;
jumpPressed = true;
};
upBtn.up = function () {
upPressed = false;
jumpPressed = false;
};
downBtn.down = function () {
if (game.paused) return;
downPressed = true;
};
downBtn.up = function () {
downPressed = false;
};
// Up-Left button handlers
upLeftBtn.down = function () {
if (game.paused) return;
upPressed = true;
leftPressed = true;
jumpPressed = true;
};
upLeftBtn.up = function () {
upPressed = false;
leftPressed = false;
jumpPressed = false;
};
// Up-Right button handlers
upRightBtn.down = function () {
if (game.paused) return;
upPressed = true;
rightPressed = true;
jumpPressed = true;
};
upRightBtn.up = function () {
upPressed = false;
rightPressed = false;
jumpPressed = false;
};
// Down-Left button handlers
downLeftBtn.down = function () {
if (game.paused) return;
downPressed = true;
leftPressed = true;
};
downLeftBtn.up = function () {
downPressed = false;
leftPressed = false;
};
// Down-Right button handlers
downRightBtn.down = function () {
if (game.paused) return;
downPressed = true;
rightPressed = true;
};
downRightBtn.up = function () {
downPressed = false;
rightPressed = false;
};
// (Diagonal D-pad button handlers added)
var fireBtn = new Text2('š„', {
size: 170,
fill: 0xF6F1C7
});
fireBtn.anchor.set(0.5, 0.5);
LK.gui.bottomRight.addChild(fireBtn);
fireBtn.x = -500;
fireBtn.y = -220;
// Control state
var leftPressed = false;
var rightPressed = false;
var upPressed = false;
var downPressed = false;
var jumpPressed = false;
var firePressed = false;
// (Diagonal (ara yƶn) support removed)
// Touch handlers for fire button
fireBtn.down = function (x, y, obj) {
if (game.paused) return;
firePressed = true;
LK.getSound('Fire').play();
};
fireBtn.up = function (x, y, obj) {
if (game.paused) return;
firePressed = false;
};
var player;
var platforms = [];
var enemies = [];
var freezeOrbs = [];
var iceBlocks = [];
var powerups = [];
var stage = 1;
var stageCleared = false;
var stageTimer = 0;
var dragNode = null;
var moveStartX = 0;
var moveDir = 0;
var scoreTxt;
// Add lives
var lives = 1;
var livesTxt = new Text2('Lives: ' + lives, {
size: 70,
fill: 0xF6F1C7
});
livesTxt.anchor.set(0, 0);
LK.gui.top.addChild(livesTxt);
// Place lives at top left, but not in the 100x100 reserved area
livesTxt.x = 120;
livesTxt.y = 20;
// Score display
scoreTxt = new Text2('0', {
size: 120,
fill: 0xF6F1C7
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
// Per-stage score tracking
var stageScores = [];
var currentStageScore = 0;
// Always keep score display updated
if (typeof scoreUpdateTimerId === "undefined") {
var scoreUpdateTimerId = LK.setInterval(function () {
if (scoreTxt && typeof LK.getScore === "function") {
var currentScore = LK.getScore();
if (scoreTxt.text !== String(currentScore)) {
scoreTxt.setText(currentScore);
}
}
}, 100);
}
// Stage display
var stageTxt = new Text2('Stage 1', {
size: 70,
fill: 0xB8E6FF
});
stageTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(stageTxt);
stageTxt.y = 120;
// Helper: spawn platforms
function spawnPlatforms() {
// Clear old
for (var i = 0; i < platforms.length; i++) platforms[i].destroy();
platforms = [];
// Floor
var floor = new Platform(1024, 2732 - 40, 2048);
game.addChild(floor);
platforms.push(floor);
// Snow Bros style: 5-6 fixed rows, wide platforms, even spacing, classic arcade
var rows = 6;
var yStart = 2732 - 300;
var yStep = 320;
for (var i = 0; i < rows; i++) {
var y = yStart - i * yStep;
// Classic: alternate left/right gaps for each row
if (i % 2 === 0) {
// Full width platform
var plat = new Platform(1024, y, 1600);
game.addChild(plat);
platforms.push(plat);
} else {
// Two half platforms with a gap in the middle
var leftPlat = new Platform(512, y, 700);
var rightPlat = new Platform(1536, y, 700);
game.addChild(leftPlat);
game.addChild(rightPlat);
platforms.push(leftPlat);
platforms.push(rightPlat);
}
}
}
// Helper: spawn enemies
function spawnEnemies() {
for (var i = 0; i < enemies.length; i++) enemies[i].destroy();
enemies = [];
var ghostCount = 2 + Math.floor(stage / 2);
var batCount = 3 + Math.floor(stage / 2); // Increased number of bats for more slow bats
for (var i = 0; i < ghostCount; i++) {
var px = 200 + Math.random() * (2048 - 400);
var py = 400 + Math.random() * 1200;
var g = new Ghost(px, py);
// Make ghosts faster in stage 2+
if (stage >= 2) {
var speed = (Math.random() > 0.5 ? 1 : -1) * (5 + Math.random() * 2.5);
var angle = Math.random() * Math.PI * 2;
g.vx = Math.cos(angle) * speed;
g.vy = Math.sin(angle) * speed * 0.7;
}
game.addChild(g);
enemies.push(g);
}
for (var i = 0; i < batCount; i++) {
var px = 200 + Math.random() * (2048 - 400);
var py = 300 + Math.random() * 1000;
var b = new Bat(px, py);
// Make bats faster in stage 2+
if (stage >= 2) {
b.vx = (Math.random() > 0.5 ? 1 : -1) * (5 + Math.random() * 2.5);
b.vy = (Math.random() > 0.5 ? 1 : -1) * 1.2;
}
game.addChild(b);
enemies.push(b);
}
}
// Helper: spawn powerups
function spawnPowerups() {
for (var i = 0; i < powerups.length; i++) powerups[i].destroy();
powerups = [];
// Always one moon per stage
var moon = new MoonPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200);
game.addChild(moon);
powerups.push(moon);
// Limit shields to max 3 per stage
var shieldsThisStage = 0;
// 50% chance for shield
if (Math.random() < 0.5 && shieldsThisStage < 3) {
var shield = new ShieldPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200);
game.addChild(shield);
powerups.push(shield);
shieldsThisStage++;
}
// 50% chance for speed
if (Math.random() < 0.5) {
var speed = new SpeedPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200);
game.addChild(speed);
powerups.push(speed);
}
// Always spawn a lifepotion at the beginning of every stage
var lifepotion = new LifepotionPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200);
game.addChild(lifepotion);
powerups.push(lifepotion);
// 40% chance for an extra lifepotion
if (Math.random() < 0.4) {
var extraLifepotion = new LifepotionPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200);
game.addChild(extraLifepotion);
powerups.push(extraLifepotion);
}
// Store shield count for this stage globally for use in other shield spawns
game.shieldsThisStage = shieldsThisStage;
}
// Start new stage
function startStage() {
stageCleared = false;
stageTimer = 0;
stageTxt.setText('Stage ' + stage);
// Store previous stage score
if (typeof currentStageScore !== "undefined" && stage > 1) {
stageScores[stage - 2] = currentStageScore;
}
// Reset score for new stage
currentStageScore = 0;
LK.setScore(0);
scoreTxt.setText('0');
lives = 7;
livesTxt.setText('Lives: ' + lives);
spawnPlatforms();
spawnEnemies();
// Reset shield count for this stage before spawning powerups
game.shieldsThisStage = 0;
spawnPowerups();
// Send Yu in stages 2, 3, 4, 5
if (stage >= 2 && stage <= 5) {
var yu = new YuPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200);
game.addChild(yu);
powerups.push(yu);
}
// Place player
if (player) player.destroy();
player = new Evanasense();
player.x = 1024;
player.y = 2732 - 200;
game.addChild(player);
}
// Begin first stage
startStage();
// Timer to spawn a random shield every 30 seconds
if (typeof shieldTimerId === "undefined") {
var shieldTimerId = LK.setInterval(function () {
// Count current shields on the field
var shieldCount = 0;
for (var i = 0; i < powerups.length; i++) {
if (powerups[i].type === 'shield') shieldCount++;
}
// Also count shields spawned this stage (if tracked)
if (typeof game.shieldsThisStage === "undefined") game.shieldsThisStage = shieldCount;
// Only spawn if less than 3 shields on the field and not more than 3 spawned this stage
if (shieldCount < 3 && game.shieldsThisStage < 3) {
var shield = new ShieldPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200);
game.addChild(shield);
powerups.push(shield);
game.shieldsThisStage++;
}
}, 30000); // 30,000 ms = 30 seconds
}
// Timer to spawn a speed powerup every 40 seconds, only one at a time
if (typeof speedPowerTimerId === "undefined") {
var speedPowerTimerId = LK.setInterval(function () {
// Only spawn if there is no speed powerup currently on the field
var hasSpeed = false;
for (var i = 0; i < powerups.length; i++) {
if (powerups[i].type === 'speed') {
hasSpeed = true;
break;
}
}
if (!hasSpeed) {
var speed = new SpeedPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200);
game.addChild(speed);
powerups.push(speed);
}
}, 40000); // 40,000 ms = 40 seconds
}
// Timer to spawn a broom every 30 seconds
if (typeof broomTimerId === "undefined") {
var broomTimerId = LK.setInterval(function () {
// Spawn a broom powerup at a random position
var broom = new BroomPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200);
game.addChild(broom);
powerups.push(broom);
}, 30000); // 30,000 ms = 30 seconds
}
// Timer to send 10 Yu powerups, one every 3 seconds, only in stage 2+
if (typeof yuSendCount === "undefined") {
var yuSendCount = 0;
var yuSendTimerId = LK.setInterval(function () {
// Only send Yu if stage >= 2
if (stage >= 2 && yuSendCount < 10) {
var yu = new YuPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200);
game.addChild(yu);
powerups.push(yu);
yuSendCount++;
}
// Reset yuSendCount if stage changes (so Yu can be sent again in new stage)
if (typeof lastYuStage === "undefined") {
var lastYuStage = stage;
}
if (stage !== lastYuStage) {
yuSendCount = 0;
lastYuStage = stage;
}
if (yuSendCount >= 10) {
// Don't clear interval, just stop sending until next stage
}
}, 3000); // 3,000 ms = 3 seconds
}
// Bat flying effect: swap bat/bat2 asset every 1 second
if (typeof batFlyTimerId === "undefined") {
var batFlyTimerId = LK.setInterval(function () {
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
if (e instanceof Bat && e.children && e.children.length > 0) {
// Find the bat asset (assume it's the first child)
var batSprite = e.children[0];
// Swap asset: if bat, change to bat2; if bat2, change to bat
var currentAssetId = batSprite.assetId || 'bat';
e.removeChild(batSprite);
var newAssetId = currentAssetId === 'bat' ? 'bat2' : 'bat';
var newBat = e.attachAsset(newAssetId, {
anchorX: 0.5,
anchorY: 0.5
});
// Store which asset is currently used
newBat.assetId = newAssetId;
// Move new bat to the front
e.setChildIndex(newBat, 0);
}
}
}, 1000); // 1000 ms = 1 second
}
// Ghost flying effect: swap ghost/ghost2 asset every 1 second
if (typeof ghostFlyTimerId === "undefined") {
var ghostFlyTimerId = LK.setInterval(function () {
for (var i = 0; i < enemies.length; i++) {
var e = enemies[i];
if (e instanceof Ghost && e.children && e.children.length > 0) {
// Find the ghost asset (assume it's the first child)
var ghostSprite = e.children[0];
// Swap asset: if ghost, change to ghost2; if ghost2, change to ghost
var currentAssetId = ghostSprite.assetId || 'ghost';
e.removeChild(ghostSprite);
var newAssetId = currentAssetId === 'ghost' ? 'Ghost2' : 'ghost';
var newGhost = e.attachAsset(newAssetId, {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.85
});
// Store which asset is currently used
newGhost.assetId = newAssetId;
// Move new ghost to the front
e.setChildIndex(newGhost, 0);
}
}
}, 1000); // 1000 ms = 1 second
}
// Add portals to the game
var portal1 = new Container();
var portal1Sprite = portal1.attachAsset('Portal1', {
anchorX: 0.5,
anchorY: 0.5
});
portal1.x = 400;
portal1.y = 400;
game.addChild(portal1);
var portal2 = new Container();
var portal2Sprite = portal2.attachAsset('Portal2', {
anchorX: 0.5,
anchorY: 0.5
});
portal2.x = 1600;
portal2.y = 2000;
game.addChild(portal2);
// Track last intersection state for portal1
var lastPortal1Intersecting = false;
// Main update loop
game.update = function () {
// Pause all gameplay and input until start is pressed
if (game.paused) return;
// (Removed shield spawn every 25 points logic)
// --- Shield spawn every 25 points, only once per threshold ---
if (typeof lastShieldScore === "undefined") {
var lastShieldScore = 0;
}
var currentScore = LK.getScore();
if (currentScore > 0 && currentScore % 25 === 0 && lastShieldScore !== currentScore) {
// Only spawn one shield per threshold, and only if less than 3 shields this stage
var shieldCount = 0;
for (var i = 0; i < powerups.length; i++) {
if (powerups[i].type === 'shield') shieldCount++;
}
if (typeof game.shieldsThisStage === "undefined") game.shieldsThisStage = shieldCount;
if (shieldCount < 3 && game.shieldsThisStage < 3) {
var shield = new ShieldPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200);
game.addChild(shield);
powerups.push(shield);
game.shieldsThisStage++;
}
lastShieldScore = currentScore;
}
if (currentScore % 25 !== 0) {
// Reset so next threshold can trigger
lastShieldScore = 0;
}
// Update player
if (player) player.update();
// Portal teleport logic
if (player && portal1 && portal2) {
// Portal1 -> Portal2
var isIntersecting1 = player.intersects(portal1);
if (!lastPortal1Intersecting && isIntersecting1) {
// Teleport player to portal2's position
player.x = portal2.x;
player.y = portal2.y;
// Optional: flash effect to show teleport
LK.effects.flashObject(player, 0xb8e6ff, 300);
}
lastPortal1Intersecting = isIntersecting1;
// Portal2 -> Portal1
if (typeof lastPortal2Intersecting === "undefined") {
var lastPortal2Intersecting = false;
}
var isIntersecting2 = player.intersects(portal2);
if (!lastPortal2Intersecting && isIntersecting2) {
// Teleport player to portal1's position
player.x = portal1.x;
player.y = portal1.y;
// Optional: flash effect to show teleport
LK.effects.flashObject(player, 0xb8e6ff, 300);
}
lastPortal2Intersecting = isIntersecting2;
}
// Track ice orb hits for bat freeze mechanic
if (typeof batFreezeHitCount === "undefined") {
var batFreezeHitCount = 0;
var batFreezeActive = false;
var batFreezeTimer = 0;
}
// --- FreezeOrb grow effect: 3x size for 5 seconds after moon ---
if (typeof freezeOrbGrowTimer === "undefined") {
var freezeOrbGrowTimer = 0;
}
if (freezeOrbGrowTimer > 0) {
freezeOrbGrowTimer--;
for (var i = 0; i < freezeOrbs.length; i++) {
var orb = freezeOrbs[i];
if (!orb._grew) {
if (orb.children && orb.children.length > 0) {
var sprite = orb.children[0];
sprite.scaleX = 3;
sprite.scaleY = 3;
orb._grew = true;
}
}
}
} else {
for (var i = 0; i < freezeOrbs.length; i++) {
var orb = freezeOrbs[i];
if (orb._grew && orb.children && orb.children.length > 0) {
var sprite = orb.children[0];
sprite.scaleX = 1;
sprite.scaleY = 1;
orb._grew = false;
}
}
}
// Update freeze orbs
for (var i = freezeOrbs.length - 1; i >= 0; i--) {
var orb = freezeOrbs[i];
orb.update();
// Collide with enemies
for (var j = 0; j < enemies.length; j++) {
var e = enemies[j];
if (!e.frozen && !e.trapped && orb.intersects(e)) {
// --- Freeze effect for all enemies hit by freeze_orb ---
if (typeof e.freeze === "function") {
e.freeze();
LK.getSound('hit').play();
}
// Add a visual freeze effect: overlay an ice_block image on top of the enemy for the freeze duration
if (!e.iceOverlay) {
e.iceOverlay = e.attachAsset('ice_block', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.6
});
// Make sure overlay is above the enemy sprite
e.setChildIndex(e.iceOverlay, e.children.length - 1);
}
// Remove overlay when unfrozen (handled in enemy update)
// If enemy is a Bat, count the hit
if (e instanceof Bat) {
batFreezeHitCount++;
// When 3 hits, freeze all bats for 60 seconds (3600 frames)
if (batFreezeHitCount >= 3 && !batFreezeActive) {
batFreezeActive = true;
batFreezeTimer = 3600;
for (var k = 0; k < enemies.length; k++) {
if (enemies[k] instanceof Bat) {
enemies[k].frozen = true;
enemies[k].frozenTimer = 3600;
if (enemies[k].children && enemies[k].children.length > 0) {
enemies[k].children[0].tint = 0x7fdfff;
}
// Add overlay to all bats
if (!enemies[k].iceOverlay) {
enemies[k].iceOverlay = enemies[k].attachAsset('ice_block', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.6
});
enemies[k].setChildIndex(enemies[k].iceOverlay, enemies[k].children.length - 1);
}
}
}
}
}
orb.destroy();
freezeOrbs.splice(i, 1);
break;
}
}
}
// Bat freeze global timer
if (batFreezeActive) {
batFreezeTimer--;
if (batFreezeTimer <= 0) {
batFreezeActive = false;
batFreezeHitCount = 0;
// Unfreeze all bats
for (var k = 0; k < enemies.length; k++) {
if (enemies[k] instanceof Bat) {
enemies[k].frozen = false;
enemies[k].frozenTimer = 0;
if (enemies[k].children && enemies[k].children.length > 0) {
enemies[k].children[0].tint = 0x2a2a2a;
}
}
}
}
}
// Update ice blocks
for (var i = iceBlocks.length - 1; i >= 0; i--) {
var block = iceBlocks[i];
block.update();
// Collide with enemies
for (var j = 0; j < enemies.length; j++) {
var e = enemies[j];
if (!e.trapped && block.intersects(e)) {
if (typeof e.trap === "function") {
e.trap();
}
block.destroy();
iceBlocks.splice(i, 1);
break;
}
}
}
// Update enemies
if (typeof globalEnemyPauseTimer === "undefined") {
var globalEnemyPauseTimer = 0;
}
if (globalEnemyPauseTimer > 0) {
globalEnemyPauseTimer--;
for (var i = enemies.length - 1; i >= 0; i--) {
var e = enemies[i];
// Only update if enemy is frozen/trapped (so shatter still works), but skip normal update
if (e.frozen || e.trapped) {
e.update();
}
// Keep pausedBySpeed flag set
e.pausedBySpeed = true;
// If frozen or trapped, can be shattered by touching again
if ((e.frozen || e.trapped) && player && player.intersects(e)) {
e.shatter();
}
// If not frozen/trapped, collision with player
if (!e.frozen && !e.trapped && player && player.intersects(e)) {
if (player.shielded) {
player.loseShield();
LK.getSound('hit').play();
LK.effects.flashObject(player, 0x8fffd6, 400);
} else {
lives--;
livesTxt.setText('Lives: ' + lives);
LK.effects.flashScreen(0x7e4a9c, 900);
if (lives <= 0) {
// Save and show best score
var bestScore = 0;
if (typeof storage !== "undefined" && typeof storage.get === "function") {
bestScore = storage.get("bestScore") || 0;
if (LK.getScore() > bestScore) {
bestScore = LK.getScore();
storage.set("bestScore", bestScore);
}
}
// Show best score overlay
var bestScoreOverlay = new Container();
var bestScoreText = new Text2("Best Score: " + bestScore, {
size: 120,
fill: 0xF6F1C7,
align: "center"
});
bestScoreText.anchor.set(0.5, 0.5);
bestScoreText.x = 1024;
bestScoreText.y = 1366;
bestScoreOverlay.addChild(bestScoreText);
LK.gui.addChild(bestScoreOverlay);
// Remove overlay after 2.5 seconds
LK.setTimeout(function () {
if (bestScoreOverlay.parent) bestScoreOverlay.parent.removeChild(bestScoreOverlay);
}, 2500);
LK.showGameOver();
return;
} else {
// Respawn player at start position
player.x = 1024;
player.y = 2732 - 200;
player.vx = 0;
player.vy = 0;
player.loseShield();
}
}
}
}
// When timer ends, unpause all enemies
if (globalEnemyPauseTimer === 0) {
for (var i = 0; i < enemies.length; i++) {
enemies[i].pausedBySpeed = false;
}
}
} else {
for (var i = enemies.length - 1; i >= 0; i--) {
var e = enemies[i];
e.update();
// If frozen or trapped, can be shattered by touching again
if ((e.frozen || e.trapped) && player && player.intersects(e)) {
e.shatter();
}
// If not frozen/trapped, collision with player
if (!e.frozen && !e.trapped && player && player.intersects(e)) {
if (player.shielded) {
player.loseShield();
LK.getSound('hit').play();
LK.effects.flashObject(player, 0x8fffd6, 400);
} else {
lives--;
livesTxt.setText('Lives: ' + lives);
LK.effects.flashScreen(0x7e4a9c, 900);
if (lives <= 0) {
// Save and show best score
var bestScore = 0;
if (typeof storage !== "undefined" && typeof storage.get === "function") {
bestScore = storage.get("bestScore") || 0;
if (LK.getScore() > bestScore) {
bestScore = LK.getScore();
storage.set("bestScore", bestScore);
}
}
// Show best score overlay
var bestScoreOverlay = new Container();
var bestScoreText = new Text2("Best Score: " + bestScore, {
size: 120,
fill: 0xF6F1C7,
align: "center"
});
bestScoreText.anchor.set(0.5, 0.5);
bestScoreText.x = 1024;
bestScoreText.y = 1366;
bestScoreOverlay.addChild(bestScoreText);
LK.gui.addChild(bestScoreOverlay);
// Remove overlay after 2.5 seconds
LK.setTimeout(function () {
if (bestScoreOverlay.parent) bestScoreOverlay.parent.removeChild(bestScoreOverlay);
}, 2500);
LK.showGameOver();
return;
} else {
// Respawn player at start position
player.x = 1024;
player.y = 2732 - 200;
player.vx = 0;
player.vy = 0;
player.loseShield();
}
}
}
}
}
// Update powerups
for (var i = powerups.length - 1; i >= 0; i--) {
var p = powerups[i];
if (player && player.intersects(p)) {
if (typeof moonCollectCount === "undefined") {
var moonCollectCount = 0;
}
if (p.type === 'moon') {
moonCollectCount++;
player.activateFullMoon();
// Start moon jump timer for 10 seconds (600 frames)
player.moonJumpTimer = 600;
LK.setScore(LK.getScore() + 5);
scoreTxt.setText(LK.getScore());
currentStageScore = LK.getScore();
currentStageScore = LK.getScore();
currentStageScore = LK.getScore();
// (Lightning effect removed when collecting moon)
// --- FreezeOrb grow effect: 3x size for 5 seconds ---
if (typeof freezeOrbGrowTimer === "undefined") {
var freezeOrbGrowTimer = 0;
}
freezeOrbGrowTimer = 300; // 5 seconds at 60fps
// Spawn a new moon at a random position
var moon = new MoonPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200);
game.addChild(moon);
powerups.push(moon);
// If 5 moons collected, spawn a random shield and reset counter
if (typeof lastMoonShieldGiven === "undefined") {
var lastMoonShieldGiven = 0;
}
if (moonCollectCount >= 5) {
moonCollectCount = 0;
// Only give one shield per 5 moons, and only if less than 3 shields this stage
var shieldCount = 0;
for (var i = 0; i < powerups.length; i++) {
if (powerups[i].type === 'shield') shieldCount++;
}
if (typeof game.shieldsThisStage === "undefined") game.shieldsThisStage = shieldCount;
if (shieldCount < 3 && game.shieldsThisStage < 3 && lastMoonShieldGiven !== LK.getScore()) {
var shield = new ShieldPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200);
game.addChild(shield);
powerups.push(shield);
game.shieldsThisStage++;
lastMoonShieldGiven = LK.getScore();
}
}
// If 100 moons collected, spawn a shield (only once per 100 moons)
if (typeof lastHundredMoonShieldGiven === "undefined") {
var lastHundredMoonShieldGiven = 0;
}
if (moonCollectCountTotal === undefined) {
var moonCollectCountTotal = 0;
}
moonCollectCountTotal++;
if (moonCollectCountTotal > 0 && moonCollectCountTotal % 100 === 0 && lastHundredMoonShieldGiven !== moonCollectCountTotal) {
// Only spawn if less than 3 shields this stage
var shieldCount = 0;
for (var i = 0; i < powerups.length; i++) {
if (powerups[i].type === 'shield') shieldCount++;
}
if (typeof game.shieldsThisStage === "undefined") game.shieldsThisStage = shieldCount;
if (shieldCount < 3 && game.shieldsThisStage < 3) {
var shield = new ShieldPower(200 + Math.random() * (2048 - 400), 400 + Math.random() * 1200);
game.addChild(shield);
powerups.push(shield);
game.shieldsThisStage++;
lastHundredMoonShieldGiven = moonCollectCountTotal;
}
}
} else if (p.type === 'shield') {
player.gainShield();
// Send ice orbs in all directions
var orbCount = 12;
for (var d = 0; d < orbCount; d++) {
var angle = 2 * Math.PI * d / orbCount;
var dirX = Math.cos(angle);
var orb = new FreezeOrb(player.x, player.y - 60, dirX);
// Give each orb a custom vx/vy for radial spread
orb.vx = 24 * Math.cos(angle);
orb.vy = 24 * Math.sin(angle);
// Override update to move in both x and y
(function (orb) {
var baseUpdate = orb.update;
orb.update = function () {
orb.x += orb.vx;
orb.y += orb.vy;
orb.lifetime--;
// Wrap horizontally
if (orb.x < 0) orb.x = 2048;
if (orb.x > 2048) orb.x = 0;
// Remove if out of bounds vertically
if (orb.y < 0 || orb.y > 2732 || orb.lifetime <= 0) {
orb.destroy();
var idx = freezeOrbs.indexOf(orb);
if (idx >= 0) freezeOrbs.splice(idx, 1);
return;
}
};
})(orb);
game.addChild(orb);
freezeOrbs.push(orb);
}
} else if (p.type === 'speed') {
player.gainSpeed();
player.speedBoost = 240; // Speed lasts 4 seconds (60fps*4)
// Only freeze all enemies for 10 seconds if not already frozen by speed
if (typeof globalEnemyPauseTimer === "undefined") {
var globalEnemyPauseTimer = 0;
}
if (globalEnemyPauseTimer <= 0) {
globalEnemyPauseTimer = 600; // 10 seconds at 60fps
for (var ep = 0; ep < enemies.length; ep++) {
enemies[ep].pausedBySpeed = true;
}
}
} else if (p.type === 'lifepotion') {
lives++;
livesTxt.setText('Lives: ' + lives);
LK.effects.flashObject(player, 0x8fffd6, 400);
} else if (p.type === 'broom') {
// Witch can now fly for 20 seconds (1200 frames)
player.canFly = true;
player.flyTimer = 1200;
LK.effects.flashObject(player, 0xb8e6ff, 600);
} else if (p.type === 'yu') {
// Cancel speed power when witch takes the yu
player.speedBoost = 0;
// Enable double jump for the player
player.yuDoubleJump = true;
player.hasDoubleJumped = false;
// Remove speed pause from all enemies
if (typeof globalEnemyPauseTimer !== "undefined") {
globalEnemyPauseTimer = 0;
for (var ep = 0; ep < enemies.length; ep++) {
enemies[ep].pausedBySpeed = false;
}
}
// Prevent shooting for 2 seconds (120 frames)
player.canShoot = false;
player.shootCooldown = 120;
}
LK.getSound('powerup').play();
p.destroy();
powerups.splice(i, 1);
}
}
// Full moon: allow trap spell by tap with two fingers (simulate by double tap)
if (player && player.fullMoon && LK.ticks % 60 === 0) {
// For MVP, allow trap spell every second in full moon
player.castTrap();
}
// Stage clear
if (!stageCleared && enemies.length === 0) {
stageCleared = true;
stageTimer = 90;
LK.effects.flashScreen(0xb8e6ff, 600);
}
if (stageCleared) {
stageTimer--;
if (stageTimer <= 0) {
stage++;
if (stage % 5 === 0) {
// Boss stage: show Boss asset and spawn extra enemies
stageTxt.setText('Boss Stage!');
// Spawn Boss at center as a moving enemy
var boss = new Boss(1024, 900);
game.addChild(boss);
enemies.push(boss);
for (var i = 0; i < 3; i++) {
var px = 200 + Math.random() * (2048 - 400);
var py = 400 + Math.random() * 1200;
var g = new Ghost(px, py);
game.addChild(g);
enemies.push(g);
}
for (var i = 0; i < 2; i++) {
var px = 200 + Math.random() * (2048 - 400);
var py = 300 + Math.random() * 1000;
var b = new Bat(px, py);
game.addChild(b);
enemies.push(b);
}
stageCleared = false;
} else {
startStage();
}
}
}
};
LK.playMusic('gothic_theme', {
fade: {
start: 0,
end: 1,
duration: 1200
}
});
// Play music
Ghost. In-Game asset. 2d. High contrast. No shadows
One life potion. In-Game asset. 2d. High contrast. No shadows
Change
Witch boiler. In-Game asset. 2d. High contrast. No shadows
Diffrent colour
Broom. In-Game asset. 2d. High contrast. No shadows
Snake. In-Game asset. 2d. High contrast. No shadows
Add legs
Snowball. In-Game asset. 2d. High contrast. No shadows
Bat closed wings
Behind
Flying boss. In-Game asset. 2d. High contrast. No shadows
Fireball. In-Game asset. 2d. High contrast. No shadows
Moon. In-Game asset. 2d. High contrast. No shadows
Dark forrest. In-Game asset. 2d. High contrast. No shadows
Ice block. In-Game asset. 2d. High contrast. No shadows
Ice wall. In-Game asset. 2d. High contrast. No shadows
Portal. In-Game asset. 2d. High contrast. No shadows