User prompt
Make longer platforms
User prompt
Make more platforms show lifes on the up give me a background
User prompt
Give 5 lifes
User prompt
More platforms add
User prompt
Player can jump the platforms make platforms more close
User prompt
Add More platforms
User prompt
Do more slow bats
User prompt
Can you add more platforms
Code edit (1 edits merged)
Please save this source code
User prompt
Evanasense: Moonlit Spellbound
Initial prompt
Create a 2D arcade platformer game inspired by Snow Bros. The main character is a witch named Evanasense, who uses magical spells to freeze or trap enemies. The player must defeat all enemies in each level to proceed. Enemies can be immobilized with magic, then pushed or shattered to clear the area. Levels scroll vertically, with increasing difficulty and boss fights every 5 stages. Include power-ups like speed boosts, stronger magic, or temporary shields. The game should have a pixel art style with dark fantasy and gothic elements, glowing spells, and eerie backgrounds. > Add magical traps and floating platforms. Include a "full moon mode" where Evanasense becomes more powerful for a short time. Support single-player mode, with plans for local co-op in future updates.
/**** * 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