/****
* 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