/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); /**** * Classes ****/ // D-Pad Button class var DPadButton = Container.expand(function () { var self = Container.call(this); self.bg = self.attachAsset('dpadBtn', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5 }); self.bg.alpha = 0.8; // Slightly more visible self.icon = null; // Will be set by game code return self; }); // Execute Button class var ExecuteButton = Container.expand(function () { var self = Container.call(this); self.bg = self.attachAsset('executeBtn', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.3, scaleY: 1.3 }); self.bg.alpha = 0.95; self.label = new Text2('EXECUTE', { size: 70, fill: "#fff" }); self.label.anchor.set(0.5, 0.5); self.addChild(self.label); self.visible = false; return self; }); // House class var House = Container.expand(function () { var self = Container.call(this); // Use a placeholder house image; you can swap the asset id for your own house image self.sprite = self.attachAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, scaleX: 2.2, scaleY: 2.2 }); // For collision and placement, use the sprite's width/height self.getBounds = function () { return { x: self.x - self.sprite.width / 2, y: self.y - self.sprite.height / 2, width: self.sprite.width, height: self.sprite.height }; }; return self; }); // Plague Doctor (player) class var PlagueDoctor = Container.expand(function () { var self = Container.call(this); var doctorSprite = self.attachAsset('plagueDoctor', { anchorX: 0.5, anchorY: 0.5 }); self.speed = 18; // Movement speed per tick // For collision box, use the asset's width/height self.getBounds = function () { return { x: self.x - doctorSprite.width / 2, y: self.y - doctorSprite.height / 2, width: doctorSprite.width, height: doctorSprite.height }; }; return self; }); // Villager class var Villager = Container.expand(function () { var self = Container.call(this); self.isSick = false; self.isAlive = true; self.init = function (isSick) { self.isSick = isSick; self.isAlive = true; self.removeChildren(); var assetId = isSick ? 'villagerSick' : 'villagerHealthy'; self.sprite = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5 }); }; self.getBounds = function () { return { x: self.x - self.sprite.width / 2, y: self.y - self.sprite.height / 2, width: self.sprite.width, height: self.sprite.height }; }; self.execute = function () { self.isAlive = false; // Animate fade out and shrink tween(self.sprite, { alpha: 0, scaleX: 0.1, scaleY: 0.1 }, { duration: 400, easing: tween.cubicIn, onFinish: function onFinish() { self.visible = false; } }); }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0xf2e5bc }); /**** * Game Code ****/ // Background image for the village map (should be at least VILLAGE_WIDTH x VILLAGE_HEIGHT) // D-pad buttons // "Execute" button // Sick villager // Healthy villager // Plague Doctor (player) // --- Game constants --- var VILLAGE_WIDTH = 6144; // Expanded map width (3x standard screen width) var VILLAGE_HEIGHT = 4096; // Expanded map height (2x standard screen height) var NUM_VILLAGERS = 40; // More villagers for the larger map var NUM_SICK = 15; // More sick villagers var VILLAGER_MIN_DIST = 220; // Minimum distance between villagers // --- Game state --- var player; var villagers = []; var sickLeft = NUM_SICK; var executeBtn; var dpadBtns = {}; var dpadState = { up: false, down: false, left: false, right: false }; var cameraX = 0; // For horizontal scrolling var cameraY = 0; // For vertical scrolling var playerWorldX = 0; // Player's true position in world coordinates var playerWorldY = 0; // Player's true position in world coordinates var draggingDPad = null; var canExecuteVillager = null; // Reference to sick villager in range // --- GUI elements --- var sickLeftTxt = new Text2('Sick left: ' + sickLeft, { size: 90, fill: 0xB16262 }); sickLeftTxt.anchor.set(0.5, 0); // Place at top center, but not in top 100px LK.gui.top.addChild(sickLeftTxt); sickLeftTxt.y = 110; // --- Game Start Screen Overlay --- var startScreenOverlay = new Container(); // Add a start screen image to the start screen overlay var overlayStartImg = LK.getAsset('startScreenImg', { anchorX: 0.5, anchorY: 0.5, scaleX: 1, scaleY: 1, x: 2048 / 2, y: 900 }); overlayStartImg.alpha = 1; startScreenOverlay.addChild(overlayStartImg); // Add a background image to the start screen overlay (behind the start image) var overlayBgImg = LK.getAsset('villageBg', { anchorX: 0.5, anchorY: 0.5, scaleX: 2048 / 30, scaleY: 2732 / 30, x: 2048 / 2, y: 2732 / 2 }); overlayBgImg.alpha = 0.98; startScreenOverlay.addChild(overlayBgImg); // Add a semi-transparent circle overlay for contrast var overlayBg = LK.getAsset('centerCircle', { anchorX: 0.5, anchorY: 0.5, scaleX: 30, scaleY: 30, x: 2048 / 2, y: 2732 / 2 }); overlayBg.alpha = 0.92; startScreenOverlay.addChild(overlayBg); // Add a large plague doctor image to the start screen var overlayDoctor = LK.getAsset('plagueDoctor', { anchorX: 0.5, anchorY: 0.5, scaleX: 3.2, scaleY: 3.2, x: 2048 / 2, y: 600 }); startScreenOverlay.addChild(overlayDoctor); var titleText = new Text2('Plague Doctor', { size: 180, fill: "#222" }); titleText.anchor.set(0.5, 0.5); titleText.x = 2048 / 2; titleText.y = 900; startScreenOverlay.addChild(titleText); var subtitleText = new Text2('Hunt down and execute sick villagers', { size: 80, fill: "#444" }); subtitleText.anchor.set(0.5, 0.5); subtitleText.x = 2048 / 2; subtitleText.y = 1100; startScreenOverlay.addChild(subtitleText); var startBtn = LK.getAsset('executeBtn', { anchorX: 0.5, anchorY: 0.5, scaleX: 1.5, scaleY: 1.5, x: 2048 / 2, y: 1600 }); startScreenOverlay.addChild(startBtn); var startBtnLabel = new Text2('START', { size: 110, fill: "#fff" }); startBtnLabel.anchor.set(0.5, 0.5); startBtnLabel.x = 2048 / 2; startBtnLabel.y = 1600; startScreenOverlay.addChild(startBtnLabel); var startScreenActive = true; game.addChild(startScreenOverlay); // Helper functions function clamp(val, min, max) { if (val < min) return min; if (val > max) return max; return val; } // Axis-aligned bounding box collision function aabbIntersect(a, b) { return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y; } // --- Add background image --- var backgroundImage = LK.getAsset('villageBg', { anchorX: 0, anchorY: 0, x: 0, y: 0, scaleX: 1, scaleY: 1 }); backgroundImage.width = VILLAGE_WIDTH; backgroundImage.height = VILLAGE_HEIGHT; game.addChild(backgroundImage); // --- Map and villagers setup --- // Array to hold house objects var houses = []; // Starting position is the center of the map (no offset needed for scrolling map) var mapCenterX = VILLAGE_WIDTH / 2; var mapCenterY = VILLAGE_HEIGHT / 2; // Check if a position is clear for placing objects (no collision with existing objects) function isPositionClear(x, y, radius, objectArrays) { // Check distance from map center (player start) if (Math.abs(x - mapCenterX) < 300 && Math.abs(y - mapCenterY) < 300) { return false; } // Check distance from existing objects for (var i = 0; i < objectArrays.length; i++) { var objects = objectArrays[i]; for (var j = 0; j < objects.length; j++) { var obj = objects[j]; var dx = x - obj.x; var dy = y - obj.y; var dist = Math.sqrt(dx * dx + dy * dy); if (dist < radius) { return false; } } } return true; } // Place objects randomly on the map function placeObjects(objectClass, count, minDistance, container) { for (var i = 0; i < count; i++) { var object = new objectClass(); // Try to find a valid position var x, y; var attempts = 0; var placed = false; while (!placed && attempts < 50) { // Random position with padding from edges x = 200 + Math.random() * (VILLAGE_WIDTH - 400); y = 200 + Math.random() * (VILLAGE_HEIGHT - 400); if (isPositionClear(x, y, minDistance, [villagers, houses])) { object.x = x; object.y = y; // Store original position for camera calculations object.x0 = x; object.y0 = y; game.addChild(object); container.push(object); placed = true; } attempts++; } } } // Place villagers randomly, but not too close to each other or the player start function placeVillagers() { villagers = []; var placed = 0; var sickPlaced = 0; while (placed < NUM_VILLAGERS) { var isSick = sickPlaced < NUM_SICK ? true : false; var villager = new Villager(); villager.init(isSick); // Random position in map bounds with padding from edges var x = 200 + Math.random() * (VILLAGE_WIDTH - 400); var y = 200 + Math.random() * (VILLAGE_HEIGHT - 400); if (isPositionClear(x, y, VILLAGER_MIN_DIST, [villagers])) { villager.x = x; villager.y = y; // Store original position for camera calculations villager.x0 = x; villager.y0 = y; game.addChild(villager); villagers.push(villager); if (isSick) sickPlaced++; placed++; } } } // --- Player setup --- player = new PlagueDoctor(); playerWorldX = mapCenterX; // Initialize player world coordinates at map center playerWorldY = mapCenterY; // Initialize player world coordinates at map center player.x = mapCenterX; // Start player at map center player.y = mapCenterY; // Start player at map center game.addChild(player); // --- Houses --- var NUM_HOUSES = 18; var HOUSE_MIN_DIST = 420; placeObjects(House, NUM_HOUSES, HOUSE_MIN_DIST, houses); // --- Villagers --- placeVillagers(); // --- Execute Button --- // Place execute button in the bottom left corner of the screen executeBtn = new ExecuteButton(); executeBtn.x = executeBtn.width * 0.7; // Position with some padding from left edge executeBtn.y = LK.gui.bottomLeft.height - executeBtn.height * 0.7; // Position with some padding from bottom LK.gui.bottomLeft.addChild(executeBtn); // --- D-Pad Controls (fit to screen, always visible in bottom middle using LK.gui.bottom) --- var dpadSize = Math.floor(LK.gui.bottom.width * 0.24); // Larger buttons (24% of screen width) if (dpadSize < 120) dpadSize = 120; // Larger minimum size if (dpadSize > 220) dpadSize = 220; // Larger maximum size // Further increase margin for more space between keys var dpadMargin = Math.floor(dpadSize * 1.6); // Adjust spacing relative to larger buttons // Place the D-pad cluster in the bottom middle of the screen, moved higher up var dpadCenterX = LK.gui.bottom.width / 2; // Center horizontally var dpadCenterY = LK.gui.bottom.height - dpadSize * 3.8; // Position higher above bottom function createDPadBtns() { // Set spacing between buttons var dpadSpacing = dpadMargin * 1.4; // Adjusted spacing for larger buttons // Up var btnUp = new DPadButton(); btnUp.x = dpadCenterX; btnUp.y = dpadCenterY - dpadSpacing; btnUp.icon = new Text2('▲', { size: Math.floor(dpadSize * 0.5), fill: "#222" }); btnUp.icon.anchor.set(0.5, 0.5); btnUp.addChild(btnUp.icon); LK.gui.bottom.addChild(btnUp); dpadBtns['up'] = btnUp; // Down var btnDown = new DPadButton(); btnDown.x = dpadCenterX; btnDown.y = dpadCenterY + dpadSpacing; btnDown.icon = new Text2('▼', { size: Math.floor(dpadSize * 0.5), fill: "#222" }); btnDown.icon.anchor.set(0.5, 0.5); btnDown.addChild(btnDown.icon); LK.gui.bottom.addChild(btnDown); dpadBtns['down'] = btnDown; // Left var btnLeft = new DPadButton(); btnLeft.x = dpadCenterX - dpadSpacing; btnLeft.y = dpadCenterY; btnLeft.icon = new Text2('◀', { size: Math.floor(dpadSize * 0.5), fill: "#222" }); btnLeft.icon.anchor.set(0.5, 0.5); btnLeft.addChild(btnLeft.icon); LK.gui.bottom.addChild(btnLeft); dpadBtns['left'] = btnLeft; // Right var btnRight = new DPadButton(); btnRight.x = dpadCenterX + dpadSpacing; btnRight.y = dpadCenterY; btnRight.icon = new Text2('▶', { size: Math.floor(dpadSize * 0.5), fill: "#222" }); btnRight.icon.anchor.set(0.5, 0.5); btnRight.addChild(btnRight.icon); LK.gui.bottom.addChild(btnRight); dpadBtns['right'] = btnRight; } createDPadBtns(); // --- Start screen overlay event handling --- startScreenOverlay.down = function (x, y, obj) { // Check if start button is pressed var btnX = 2048 / 2; var btnY = 1600; var btnW = startBtn.width * 0.5; var btnH = startBtn.height * 0.5; if (x >= btnX - btnW && x <= btnX + btnW && y >= btnY - btnH && y <= btnY + btnH) { // Hide overlay, show UI, enable game startScreenOverlay.visible = false; startScreenActive = false; } }; // --- D-Pad event handling --- function dpadBtnForPos(x, y) { var r = dpadSize / 2; for (var dir in dpadBtns) { var btn = dpadBtns[dir]; var bx = btn.x; var by = btn.y; if ((x - bx) * (x - bx) + (y - by) * (y - by) < r * r) { return dir; } } return null; } // D-Pad touch/mouse down LK.gui.bottom.down = function (x, y, obj) { var dir = dpadBtnForPos(x, y); if (dir) { dpadState[dir] = true; draggingDPad = dir; tween(dpadBtns[dir].bg, { alpha: 1 }, { duration: 80 }); } }; // D-Pad touch/mouse up LK.gui.bottom.up = function (x, y, obj) { if (draggingDPad) { dpadState[draggingDPad] = false; tween(dpadBtns[draggingDPad].bg, { alpha: 0.7 }, { duration: 120 }); draggingDPad = null; } }; // D-Pad move (drag) LK.gui.bottom.move = function (x, y, obj) { if (draggingDPad) { var dir = dpadBtnForPos(x, y); if (dir === draggingDPad) { if (!dpadState[dir]) { dpadState[dir] = true; tween(dpadBtns[dir].bg, { alpha: 1 }, { duration: 80 }); } } else { if (dpadState[draggingDPad]) { dpadState[draggingDPad] = false; tween(dpadBtns[draggingDPad].bg, { alpha: 0.7 }, { duration: 120 }); } } } }; // --- Execute Button event handling --- LK.gui.bottomLeft.down = function (x, y, obj) { // Check if the click is within the execute button bounds if (executeBtn.visible && x >= executeBtn.x - executeBtn.width / 2 && x <= executeBtn.x + executeBtn.width / 2 && y >= executeBtn.y - executeBtn.height / 2 && y <= executeBtn.y + executeBtn.height / 2) { if (canExecuteVillager && canExecuteVillager.isAlive) { canExecuteVillager.execute(); sickLeft--; sickLeftTxt.setText('Sick left: ' + sickLeft); executeBtn.visible = false; canExecuteVillager = null; // Win condition if (sickLeft === 0) { LK.showYouWin(); } } } }; // --- Main game move handler (for dragging player, not used here) --- game.move = function (x, y, obj) { // No drag-to-move, only D-pad }; // --- Main game update loop --- game.update = function () { // If start screen is active, block all game logic and hide UI if (startScreenActive) { // Hide all game objects except overlay for (var i = 0; i < game.children.length; ++i) { var obj = game.children[i]; if (obj !== startScreenOverlay) obj.visible = false;else obj.visible = true; } // Hide execute button and sick left text executeBtn.visible = false; sickLeftTxt.visible = false; // Hide D-pad for (var dir in dpadBtns) { dpadBtns[dir].visible = false; } return; } else { // Show UI when game is started sickLeftTxt.visible = true; for (var dir in dpadBtns) { dpadBtns[dir].visible = true; } } // --- Player movement --- var dx = 0, dy = 0; if (dpadState.left) dx -= 1; if (dpadState.right) dx += 1; if (dpadState.up) dy -= 1; if (dpadState.down) dy += 1; if (dx !== 0 || dy !== 0) { var len = Math.sqrt(dx * dx + dy * dy); if (len > 0) { dx /= len; dy /= len; } // Calculate new position before actually moving var newWorldX = playerWorldX + dx * player.speed; var newWorldY = playerWorldY + dy * player.speed; // Store player's world coordinates separately from rendering position playerWorldX = newWorldX; playerWorldY = newWorldY; } // Clamp player to world coordinates map bounds with consistent padding on all sides playerWorldX = clamp(playerWorldX, 60, VILLAGE_WIDTH - 60); playerWorldY = clamp(playerWorldY, 60, VILLAGE_HEIGHT - 60); // --- Camera scrolling (horizontal and vertical) --- var screenCenterX = 2048 / 2; // Use fixed resolution width var screenCenterY = 2732 / 2; // Use fixed resolution height // Calculate camera position to center player cameraX = playerWorldX - screenCenterX; cameraY = playerWorldY - screenCenterY; // Clamp camera to map bounds cameraX = clamp(cameraX, 0, VILLAGE_WIDTH - 2048); cameraY = clamp(cameraY, 0, VILLAGE_HEIGHT - 2732); // Move background image with camera (always at the back) backgroundImage.x = -cameraX; backgroundImage.y = -cameraY; // Move all game children (player, villagers) relative to camera position for (var i = 0; i < game.children.length; ++i) { var obj = game.children[i]; obj.visible = true; if (obj === player) { // Position player on screen based on world coordinates obj.x = playerWorldX - cameraX; obj.y = playerWorldY - cameraY; } else if (obj instanceof Villager || obj instanceof House) { // Position objects on screen based on their world coordinates // Make sure we're using the stored world coordinates consistently obj.x = obj.x0 - cameraX; obj.y = obj.y0 - cameraY; // Hide objects that are completely outside the visible area if (obj.x < -300 || obj.x > 2348 || obj.y < -300 || obj.y > 3032) { obj.visible = false; } } } // --- Villager proximity check --- canExecuteVillager = null; for (var i = 0; i < villagers.length; ++i) { var villager = villagers[i]; if (!villager.isAlive || !villager.isSick) continue; // Use world coordinates for collision var px = playerWorldX; var py = playerWorldY; var vx = villager.x0; // Always use stored world coordinates var vy = villager.y0; // Always use stored world coordinates var dist = Math.sqrt((px - vx) * (px - vx) + (py - vy) * (py - vy)); if (dist < 140) { canExecuteVillager = villager; break; } } // --- Execute button visibility --- if (canExecuteVillager && canExecuteVillager.isAlive) { executeBtn.visible = true; } else { executeBtn.visible = false; } }; // --- Store original world positions for villagers (for camera) --- function storeVillagerPositions() { // Store villager positions for (var i = 0; i < villagers.length; ++i) { villagers[i].x0 = villagers[i].x; villagers[i].y0 = villagers[i].y; } } // Store initial positions storeVillagerPositions(); // --- Initial camera position --- cameraX = mapCenterX - 2048 / 2; // Start camera centered on the player using fixed width cameraY = mapCenterY - 2732 / 2; // Start camera centered on the player using fixed height for (var i = 0; i < game.children.length; ++i) { var obj = game.children[i]; if (obj === player) { obj.x = player.x - cameraX; obj.y = player.y - cameraY; } else if (obj instanceof Villager) { obj.x = obj.x0 - cameraX; obj.y = obj.y0 - cameraY; } }
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// D-Pad Button class
var DPadButton = Container.expand(function () {
var self = Container.call(this);
self.bg = self.attachAsset('dpadBtn', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5
});
self.bg.alpha = 0.8; // Slightly more visible
self.icon = null; // Will be set by game code
return self;
});
// Execute Button class
var ExecuteButton = Container.expand(function () {
var self = Container.call(this);
self.bg = self.attachAsset('executeBtn', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.3,
scaleY: 1.3
});
self.bg.alpha = 0.95;
self.label = new Text2('EXECUTE', {
size: 70,
fill: "#fff"
});
self.label.anchor.set(0.5, 0.5);
self.addChild(self.label);
self.visible = false;
return self;
});
// House class
var House = Container.expand(function () {
var self = Container.call(this);
// Use a placeholder house image; you can swap the asset id for your own house image
self.sprite = self.attachAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2.2,
scaleY: 2.2
});
// For collision and placement, use the sprite's width/height
self.getBounds = function () {
return {
x: self.x - self.sprite.width / 2,
y: self.y - self.sprite.height / 2,
width: self.sprite.width,
height: self.sprite.height
};
};
return self;
});
// Plague Doctor (player) class
var PlagueDoctor = Container.expand(function () {
var self = Container.call(this);
var doctorSprite = self.attachAsset('plagueDoctor', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 18; // Movement speed per tick
// For collision box, use the asset's width/height
self.getBounds = function () {
return {
x: self.x - doctorSprite.width / 2,
y: self.y - doctorSprite.height / 2,
width: doctorSprite.width,
height: doctorSprite.height
};
};
return self;
});
// Villager class
var Villager = Container.expand(function () {
var self = Container.call(this);
self.isSick = false;
self.isAlive = true;
self.init = function (isSick) {
self.isSick = isSick;
self.isAlive = true;
self.removeChildren();
var assetId = isSick ? 'villagerSick' : 'villagerHealthy';
self.sprite = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5
});
};
self.getBounds = function () {
return {
x: self.x - self.sprite.width / 2,
y: self.y - self.sprite.height / 2,
width: self.sprite.width,
height: self.sprite.height
};
};
self.execute = function () {
self.isAlive = false;
// Animate fade out and shrink
tween(self.sprite, {
alpha: 0,
scaleX: 0.1,
scaleY: 0.1
}, {
duration: 400,
easing: tween.cubicIn,
onFinish: function onFinish() {
self.visible = false;
}
});
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xf2e5bc
});
/****
* Game Code
****/
// Background image for the village map (should be at least VILLAGE_WIDTH x VILLAGE_HEIGHT)
// D-pad buttons
// "Execute" button
// Sick villager
// Healthy villager
// Plague Doctor (player)
// --- Game constants ---
var VILLAGE_WIDTH = 6144; // Expanded map width (3x standard screen width)
var VILLAGE_HEIGHT = 4096; // Expanded map height (2x standard screen height)
var NUM_VILLAGERS = 40; // More villagers for the larger map
var NUM_SICK = 15; // More sick villagers
var VILLAGER_MIN_DIST = 220; // Minimum distance between villagers
// --- Game state ---
var player;
var villagers = [];
var sickLeft = NUM_SICK;
var executeBtn;
var dpadBtns = {};
var dpadState = {
up: false,
down: false,
left: false,
right: false
};
var cameraX = 0; // For horizontal scrolling
var cameraY = 0; // For vertical scrolling
var playerWorldX = 0; // Player's true position in world coordinates
var playerWorldY = 0; // Player's true position in world coordinates
var draggingDPad = null;
var canExecuteVillager = null; // Reference to sick villager in range
// --- GUI elements ---
var sickLeftTxt = new Text2('Sick left: ' + sickLeft, {
size: 90,
fill: 0xB16262
});
sickLeftTxt.anchor.set(0.5, 0);
// Place at top center, but not in top 100px
LK.gui.top.addChild(sickLeftTxt);
sickLeftTxt.y = 110;
// --- Game Start Screen Overlay ---
var startScreenOverlay = new Container();
// Add a start screen image to the start screen overlay
var overlayStartImg = LK.getAsset('startScreenImg', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1,
scaleY: 1,
x: 2048 / 2,
y: 900
});
overlayStartImg.alpha = 1;
startScreenOverlay.addChild(overlayStartImg);
// Add a background image to the start screen overlay (behind the start image)
var overlayBgImg = LK.getAsset('villageBg', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 2048 / 30,
scaleY: 2732 / 30,
x: 2048 / 2,
y: 2732 / 2
});
overlayBgImg.alpha = 0.98;
startScreenOverlay.addChild(overlayBgImg);
// Add a semi-transparent circle overlay for contrast
var overlayBg = LK.getAsset('centerCircle', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 30,
scaleY: 30,
x: 2048 / 2,
y: 2732 / 2
});
overlayBg.alpha = 0.92;
startScreenOverlay.addChild(overlayBg);
// Add a large plague doctor image to the start screen
var overlayDoctor = LK.getAsset('plagueDoctor', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3.2,
scaleY: 3.2,
x: 2048 / 2,
y: 600
});
startScreenOverlay.addChild(overlayDoctor);
var titleText = new Text2('Plague Doctor', {
size: 180,
fill: "#222"
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 2048 / 2;
titleText.y = 900;
startScreenOverlay.addChild(titleText);
var subtitleText = new Text2('Hunt down and execute sick villagers', {
size: 80,
fill: "#444"
});
subtitleText.anchor.set(0.5, 0.5);
subtitleText.x = 2048 / 2;
subtitleText.y = 1100;
startScreenOverlay.addChild(subtitleText);
var startBtn = LK.getAsset('executeBtn', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.5,
scaleY: 1.5,
x: 2048 / 2,
y: 1600
});
startScreenOverlay.addChild(startBtn);
var startBtnLabel = new Text2('START', {
size: 110,
fill: "#fff"
});
startBtnLabel.anchor.set(0.5, 0.5);
startBtnLabel.x = 2048 / 2;
startBtnLabel.y = 1600;
startScreenOverlay.addChild(startBtnLabel);
var startScreenActive = true;
game.addChild(startScreenOverlay);
// Helper functions
function clamp(val, min, max) {
if (val < min) return min;
if (val > max) return max;
return val;
}
// Axis-aligned bounding box collision
function aabbIntersect(a, b) {
return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
}
// --- Add background image ---
var backgroundImage = LK.getAsset('villageBg', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
scaleX: 1,
scaleY: 1
});
backgroundImage.width = VILLAGE_WIDTH;
backgroundImage.height = VILLAGE_HEIGHT;
game.addChild(backgroundImage);
// --- Map and villagers setup ---
// Array to hold house objects
var houses = [];
// Starting position is the center of the map (no offset needed for scrolling map)
var mapCenterX = VILLAGE_WIDTH / 2;
var mapCenterY = VILLAGE_HEIGHT / 2;
// Check if a position is clear for placing objects (no collision with existing objects)
function isPositionClear(x, y, radius, objectArrays) {
// Check distance from map center (player start)
if (Math.abs(x - mapCenterX) < 300 && Math.abs(y - mapCenterY) < 300) {
return false;
}
// Check distance from existing objects
for (var i = 0; i < objectArrays.length; i++) {
var objects = objectArrays[i];
for (var j = 0; j < objects.length; j++) {
var obj = objects[j];
var dx = x - obj.x;
var dy = y - obj.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < radius) {
return false;
}
}
}
return true;
}
// Place objects randomly on the map
function placeObjects(objectClass, count, minDistance, container) {
for (var i = 0; i < count; i++) {
var object = new objectClass();
// Try to find a valid position
var x, y;
var attempts = 0;
var placed = false;
while (!placed && attempts < 50) {
// Random position with padding from edges
x = 200 + Math.random() * (VILLAGE_WIDTH - 400);
y = 200 + Math.random() * (VILLAGE_HEIGHT - 400);
if (isPositionClear(x, y, minDistance, [villagers, houses])) {
object.x = x;
object.y = y;
// Store original position for camera calculations
object.x0 = x;
object.y0 = y;
game.addChild(object);
container.push(object);
placed = true;
}
attempts++;
}
}
}
// Place villagers randomly, but not too close to each other or the player start
function placeVillagers() {
villagers = [];
var placed = 0;
var sickPlaced = 0;
while (placed < NUM_VILLAGERS) {
var isSick = sickPlaced < NUM_SICK ? true : false;
var villager = new Villager();
villager.init(isSick);
// Random position in map bounds with padding from edges
var x = 200 + Math.random() * (VILLAGE_WIDTH - 400);
var y = 200 + Math.random() * (VILLAGE_HEIGHT - 400);
if (isPositionClear(x, y, VILLAGER_MIN_DIST, [villagers])) {
villager.x = x;
villager.y = y;
// Store original position for camera calculations
villager.x0 = x;
villager.y0 = y;
game.addChild(villager);
villagers.push(villager);
if (isSick) sickPlaced++;
placed++;
}
}
}
// --- Player setup ---
player = new PlagueDoctor();
playerWorldX = mapCenterX; // Initialize player world coordinates at map center
playerWorldY = mapCenterY; // Initialize player world coordinates at map center
player.x = mapCenterX; // Start player at map center
player.y = mapCenterY; // Start player at map center
game.addChild(player);
// --- Houses ---
var NUM_HOUSES = 18;
var HOUSE_MIN_DIST = 420;
placeObjects(House, NUM_HOUSES, HOUSE_MIN_DIST, houses);
// --- Villagers ---
placeVillagers();
// --- Execute Button ---
// Place execute button in the bottom left corner of the screen
executeBtn = new ExecuteButton();
executeBtn.x = executeBtn.width * 0.7; // Position with some padding from left edge
executeBtn.y = LK.gui.bottomLeft.height - executeBtn.height * 0.7; // Position with some padding from bottom
LK.gui.bottomLeft.addChild(executeBtn);
// --- D-Pad Controls (fit to screen, always visible in bottom middle using LK.gui.bottom) ---
var dpadSize = Math.floor(LK.gui.bottom.width * 0.24); // Larger buttons (24% of screen width)
if (dpadSize < 120) dpadSize = 120; // Larger minimum size
if (dpadSize > 220) dpadSize = 220; // Larger maximum size
// Further increase margin for more space between keys
var dpadMargin = Math.floor(dpadSize * 1.6); // Adjust spacing relative to larger buttons
// Place the D-pad cluster in the bottom middle of the screen, moved higher up
var dpadCenterX = LK.gui.bottom.width / 2; // Center horizontally
var dpadCenterY = LK.gui.bottom.height - dpadSize * 3.8; // Position higher above bottom
function createDPadBtns() {
// Set spacing between buttons
var dpadSpacing = dpadMargin * 1.4; // Adjusted spacing for larger buttons
// Up
var btnUp = new DPadButton();
btnUp.x = dpadCenterX;
btnUp.y = dpadCenterY - dpadSpacing;
btnUp.icon = new Text2('▲', {
size: Math.floor(dpadSize * 0.5),
fill: "#222"
});
btnUp.icon.anchor.set(0.5, 0.5);
btnUp.addChild(btnUp.icon);
LK.gui.bottom.addChild(btnUp);
dpadBtns['up'] = btnUp;
// Down
var btnDown = new DPadButton();
btnDown.x = dpadCenterX;
btnDown.y = dpadCenterY + dpadSpacing;
btnDown.icon = new Text2('▼', {
size: Math.floor(dpadSize * 0.5),
fill: "#222"
});
btnDown.icon.anchor.set(0.5, 0.5);
btnDown.addChild(btnDown.icon);
LK.gui.bottom.addChild(btnDown);
dpadBtns['down'] = btnDown;
// Left
var btnLeft = new DPadButton();
btnLeft.x = dpadCenterX - dpadSpacing;
btnLeft.y = dpadCenterY;
btnLeft.icon = new Text2('◀', {
size: Math.floor(dpadSize * 0.5),
fill: "#222"
});
btnLeft.icon.anchor.set(0.5, 0.5);
btnLeft.addChild(btnLeft.icon);
LK.gui.bottom.addChild(btnLeft);
dpadBtns['left'] = btnLeft;
// Right
var btnRight = new DPadButton();
btnRight.x = dpadCenterX + dpadSpacing;
btnRight.y = dpadCenterY;
btnRight.icon = new Text2('▶', {
size: Math.floor(dpadSize * 0.5),
fill: "#222"
});
btnRight.icon.anchor.set(0.5, 0.5);
btnRight.addChild(btnRight.icon);
LK.gui.bottom.addChild(btnRight);
dpadBtns['right'] = btnRight;
}
createDPadBtns();
// --- Start screen overlay event handling ---
startScreenOverlay.down = function (x, y, obj) {
// Check if start button is pressed
var btnX = 2048 / 2;
var btnY = 1600;
var btnW = startBtn.width * 0.5;
var btnH = startBtn.height * 0.5;
if (x >= btnX - btnW && x <= btnX + btnW && y >= btnY - btnH && y <= btnY + btnH) {
// Hide overlay, show UI, enable game
startScreenOverlay.visible = false;
startScreenActive = false;
}
};
// --- D-Pad event handling ---
function dpadBtnForPos(x, y) {
var r = dpadSize / 2;
for (var dir in dpadBtns) {
var btn = dpadBtns[dir];
var bx = btn.x;
var by = btn.y;
if ((x - bx) * (x - bx) + (y - by) * (y - by) < r * r) {
return dir;
}
}
return null;
}
// D-Pad touch/mouse down
LK.gui.bottom.down = function (x, y, obj) {
var dir = dpadBtnForPos(x, y);
if (dir) {
dpadState[dir] = true;
draggingDPad = dir;
tween(dpadBtns[dir].bg, {
alpha: 1
}, {
duration: 80
});
}
};
// D-Pad touch/mouse up
LK.gui.bottom.up = function (x, y, obj) {
if (draggingDPad) {
dpadState[draggingDPad] = false;
tween(dpadBtns[draggingDPad].bg, {
alpha: 0.7
}, {
duration: 120
});
draggingDPad = null;
}
};
// D-Pad move (drag)
LK.gui.bottom.move = function (x, y, obj) {
if (draggingDPad) {
var dir = dpadBtnForPos(x, y);
if (dir === draggingDPad) {
if (!dpadState[dir]) {
dpadState[dir] = true;
tween(dpadBtns[dir].bg, {
alpha: 1
}, {
duration: 80
});
}
} else {
if (dpadState[draggingDPad]) {
dpadState[draggingDPad] = false;
tween(dpadBtns[draggingDPad].bg, {
alpha: 0.7
}, {
duration: 120
});
}
}
}
};
// --- Execute Button event handling ---
LK.gui.bottomLeft.down = function (x, y, obj) {
// Check if the click is within the execute button bounds
if (executeBtn.visible && x >= executeBtn.x - executeBtn.width / 2 && x <= executeBtn.x + executeBtn.width / 2 && y >= executeBtn.y - executeBtn.height / 2 && y <= executeBtn.y + executeBtn.height / 2) {
if (canExecuteVillager && canExecuteVillager.isAlive) {
canExecuteVillager.execute();
sickLeft--;
sickLeftTxt.setText('Sick left: ' + sickLeft);
executeBtn.visible = false;
canExecuteVillager = null;
// Win condition
if (sickLeft === 0) {
LK.showYouWin();
}
}
}
};
// --- Main game move handler (for dragging player, not used here) ---
game.move = function (x, y, obj) {
// No drag-to-move, only D-pad
};
// --- Main game update loop ---
game.update = function () {
// If start screen is active, block all game logic and hide UI
if (startScreenActive) {
// Hide all game objects except overlay
for (var i = 0; i < game.children.length; ++i) {
var obj = game.children[i];
if (obj !== startScreenOverlay) obj.visible = false;else obj.visible = true;
}
// Hide execute button and sick left text
executeBtn.visible = false;
sickLeftTxt.visible = false;
// Hide D-pad
for (var dir in dpadBtns) {
dpadBtns[dir].visible = false;
}
return;
} else {
// Show UI when game is started
sickLeftTxt.visible = true;
for (var dir in dpadBtns) {
dpadBtns[dir].visible = true;
}
}
// --- Player movement ---
var dx = 0,
dy = 0;
if (dpadState.left) dx -= 1;
if (dpadState.right) dx += 1;
if (dpadState.up) dy -= 1;
if (dpadState.down) dy += 1;
if (dx !== 0 || dy !== 0) {
var len = Math.sqrt(dx * dx + dy * dy);
if (len > 0) {
dx /= len;
dy /= len;
}
// Calculate new position before actually moving
var newWorldX = playerWorldX + dx * player.speed;
var newWorldY = playerWorldY + dy * player.speed;
// Store player's world coordinates separately from rendering position
playerWorldX = newWorldX;
playerWorldY = newWorldY;
}
// Clamp player to world coordinates map bounds with consistent padding on all sides
playerWorldX = clamp(playerWorldX, 60, VILLAGE_WIDTH - 60);
playerWorldY = clamp(playerWorldY, 60, VILLAGE_HEIGHT - 60);
// --- Camera scrolling (horizontal and vertical) ---
var screenCenterX = 2048 / 2; // Use fixed resolution width
var screenCenterY = 2732 / 2; // Use fixed resolution height
// Calculate camera position to center player
cameraX = playerWorldX - screenCenterX;
cameraY = playerWorldY - screenCenterY;
// Clamp camera to map bounds
cameraX = clamp(cameraX, 0, VILLAGE_WIDTH - 2048);
cameraY = clamp(cameraY, 0, VILLAGE_HEIGHT - 2732);
// Move background image with camera (always at the back)
backgroundImage.x = -cameraX;
backgroundImage.y = -cameraY;
// Move all game children (player, villagers) relative to camera position
for (var i = 0; i < game.children.length; ++i) {
var obj = game.children[i];
obj.visible = true;
if (obj === player) {
// Position player on screen based on world coordinates
obj.x = playerWorldX - cameraX;
obj.y = playerWorldY - cameraY;
} else if (obj instanceof Villager || obj instanceof House) {
// Position objects on screen based on their world coordinates
// Make sure we're using the stored world coordinates consistently
obj.x = obj.x0 - cameraX;
obj.y = obj.y0 - cameraY;
// Hide objects that are completely outside the visible area
if (obj.x < -300 || obj.x > 2348 || obj.y < -300 || obj.y > 3032) {
obj.visible = false;
}
}
}
// --- Villager proximity check ---
canExecuteVillager = null;
for (var i = 0; i < villagers.length; ++i) {
var villager = villagers[i];
if (!villager.isAlive || !villager.isSick) continue;
// Use world coordinates for collision
var px = playerWorldX;
var py = playerWorldY;
var vx = villager.x0; // Always use stored world coordinates
var vy = villager.y0; // Always use stored world coordinates
var dist = Math.sqrt((px - vx) * (px - vx) + (py - vy) * (py - vy));
if (dist < 140) {
canExecuteVillager = villager;
break;
}
}
// --- Execute button visibility ---
if (canExecuteVillager && canExecuteVillager.isAlive) {
executeBtn.visible = true;
} else {
executeBtn.visible = false;
}
};
// --- Store original world positions for villagers (for camera) ---
function storeVillagerPositions() {
// Store villager positions
for (var i = 0; i < villagers.length; ++i) {
villagers[i].x0 = villagers[i].x;
villagers[i].y0 = villagers[i].y;
}
}
// Store initial positions
storeVillagerPositions();
// --- Initial camera position ---
cameraX = mapCenterX - 2048 / 2; // Start camera centered on the player using fixed width
cameraY = mapCenterY - 2732 / 2; // Start camera centered on the player using fixed height
for (var i = 0; i < game.children.length; ++i) {
var obj = game.children[i];
if (obj === player) {
obj.x = player.x - cameraX;
obj.y = player.y - cameraY;
} else if (obj instanceof Villager) {
obj.x = obj.x0 - cameraX;
obj.y = obj.y0 - cameraY;
}
}
Kill button. In-Game asset. 2d. High contrast. No shadows
Palugue doctors full body pixel art. In-Game asset. 2d. High contrast. No shadows
Sick viliger pixel art full body
Not sick viliger pixel art
Pixel art death block.
Viliger house pixel art. In-Game asset. 2d. High contrast. No shadows
Background sand image pixel art. 4k In-Game asset. 2d. High contrast. No shadows