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