/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Emergency meeting button
var EmergencyBtn = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('emergencybtn', {
anchorX: 0.5,
anchorY: 0.5
});
self.txt = new Text2('EMERGENCY', {
size: 48,
fill: "#fff"
});
self.txt.anchor.set(0.5, 0.5);
self.txt.x = 0;
self.txt.y = 0;
self.addChild(self.txt);
self.down = function (x, y, obj) {
if (game.state === 'playing' && !game.meetingCalled) {
game.callMeeting();
}
};
return self;
});
// Player class (Crewmate or Impostor)
var Player = Container.expand(function () {
var self = Container.call(this);
// Properties
self.isImpostor = false;
self.isAlive = true;
self.playerId = null;
self.name = '';
self.avatar = null;
self.isLocal = false; // Is this the local player?
self.isVoted = false;
self.voteTarget = null;
self.showName = function () {
if (!self.nameTxt) {
// Determine color for name based on player role
var nameColor = self.isImpostor ? "#d7263d" : "#83de44";
self.nameTxt = new Text2(self.name, {
size: 48,
fill: nameColor
});
self.nameTxt.anchor.set(0.5, 0);
self.addChild(self.nameTxt);
self.nameTxt.y = self.avatar.height / 2 + 10;
}
};
// Set up avatar
self.setRole = function (isImpostor) {
self.isImpostor = isImpostor;
if (self.avatar) {
self.removeChild(self.avatar);
}
// Determine color from name
var nameColorMap = {
"Red": 0xd7263d,
"Blue": 0x3ec1d3,
"Green": 0x83de44,
"Yellow": 0xf9d423,
"Pink": 0xff69b4,
"Orange": 0xf97c1b,
"Cyan": 0x15ffff,
"Lime": 0x7fff00,
"Purple": 0x6c3483,
"Brown": 0x8b5a2b
};
var colorKey = (self.name || "").split(" ")[0];
var playerColor = nameColorMap[colorKey] !== undefined ? nameColorMap[colorKey] : isImpostor ? 0xd7263d : 0x83de44;
if (isImpostor) {
self.avatar = self.attachAsset('impostor', {
anchorX: 0.5,
anchorY: 0.5,
tint: playerColor
});
} else {
self.avatar = self.attachAsset('crewmate', {
anchorX: 0.5,
anchorY: 0.5,
tint: playerColor
});
}
};
// Show dead body
self.showDead = function () {
if (self.avatar) self.avatar.visible = false;
if (!self.deadBody) {
self.deadBody = self.attachAsset('deadbody', {
anchorX: 0.5,
anchorY: 0.5
});
}
self.isAlive = false;
};
// Hide dead body, show alive
self.revive = function () {
if (self.deadBody) self.deadBody.visible = false;
if (self.avatar) self.avatar.visible = true;
self.isAlive = true;
};
// For touch/click events
self.down = function (x, y, obj) {
// Used for voting
if (game.state === 'voting' && !game.localPlayer.isVoted && self.isAlive && self !== game.localPlayer) {
game.voteFor(self);
} else if (game.state === 'playing' && game.localPlayer && game.localPlayer.isImpostor && game.localPlayer.isAlive && self.isAlive && self !== game.localPlayer) {
// Check distance for kill range (same as tryKill)
var dx = self.x - game.localPlayer.x;
var dy = self.y - game.localPlayer.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 200) {
tryKill(self);
// Deactivate kill ability and reset cooldown
killAbilityActive = false;
lastKillTime = Date.now();
}
}
};
return self;
});
// Sabotage node class
var SabotageNode = Container.expand(function () {
var self = Container.call(this);
self.active = false;
self.asset = self.attachAsset('sabotagenode', {
anchorX: 0.5,
anchorY: 0.5
});
self.down = function (x, y, obj) {
if (game.state === 'playing' && game.localPlayer.isImpostor && !self.active) {
// Activate sabotage
self.active = true;
self.asset.alpha = 0.5;
game.sabotageTriggered(self);
}
};
return self;
});
// Task node class
var TaskNode = Container.expand(function () {
var self = Container.call(this);
self.done = false;
self.taskId = null;
self.ownerId = null; // Which player this task belongs to (for local tasks)
// Assign a color to each vaccine/task node. For example, cycle through a set of colors based on taskId.
var vaccineColors = [0x83de44, 0x3ec1d3, 0xf9d423, 0xd7263d, 0x6c3483, 0x15ffff];
var color = vaccineColors[self.taskId % vaccineColors.length];
self.asset = self.attachAsset('tasknode', {
anchorX: 0.5,
anchorY: 0.5,
color: color
});
self.down = function (x, y, obj) {
if (game.state === 'playing' && !self.done && self.ownerId === game.localPlayer.playerId) {
// Complete task
self.done = true;
self.asset.alpha = 0.3;
LK.getSound('taskdone').play();
game.completeTask(self);
}
};
return self;
});
// Voting button (for skip or confirm)
var VoteBtn = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('votebtn', {
anchorX: 0.5,
anchorY: 0.5
});
self.txt = new Text2('SKIP', {
size: 48,
fill: "#fff"
});
self.txt.anchor.set(0.5, 0.5);
self.txt.x = 0;
self.txt.y = 0;
self.addChild(self.txt);
self.down = function (x, y, obj) {
if (game.state === 'voting' && !game.localPlayer.isVoted) {
game.voteFor(null); // skip
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a2e
});
/****
* Game Code
****/
// Add background image to the game area
if (!game.bg) {
game.bg = LK.getAsset('background', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 4096,
height: 4096
});
game.addChildAt(game.bg, 0); // Add as the bottom-most layer
}
// Place Emergency button at the bottom right corner using LK.gui.bottomRight
if (emergencyBtn) {
// Remove from previous parent if needed
if (emergencyBtn.parent) emergencyBtn.parent.removeChild(emergencyBtn);
emergencyBtn.x = 0;
emergencyBtn.y = 0;
LK.gui.bottomRight.addChild(emergencyBtn);
emergencyBtn.visible = true;
}
// Place Kill button as a button just below the emergencyBtn in LK.gui.bottomRight
if (!game.killBtn) {
// Create Kill button as a Container with the Kill asset
game.killBtn = new Container();
var killAsset = game.killBtn.attachAsset('Kill', {
anchorX: 0.5,
anchorY: 0.5
});
// Optionally, you can add a label or effect here if needed
// Set up button event (no action for now, can be extended)
game.killBtn.down = function (x, y, obj) {
// Placeholder for kill action if needed
};
}
// Remove from previous parent if needed
if (game.killBtn.parent) game.killBtn.parent.removeChild(game.killBtn);
// Place killBtn just below emergencyBtn in LK.gui.bottomRight
// Calculate y offset based on emergencyBtn's height
var killBtnYOffset = 0;
if (emergencyBtn && emergencyBtn.asset && emergencyBtn.asset.height) {
killBtnYOffset = emergencyBtn.asset.height + 32; // 32px gap for touch
} else {
killBtnYOffset = 300; // fallback if asset not loaded
}
game.killBtn.x = 0;
game.killBtn.y = killBtnYOffset;
LK.gui.bottomRight.addChild(game.killBtn);
game.killBtn.visible = true;
// Place bilmem button just below emergencyBtn in LK.gui.bottomRight
if (!game.bilmemBtn) {
game.bilmemBtn = new Container();
var bilmemAsset = game.bilmemBtn.attachAsset('bilmem', {
anchorX: 0.5,
anchorY: 0.5
});
// Optionally, add a label or effect here if needed
// Set up button event (no action for now, can be extended)
game.bilmemBtn.down = function (x, y, obj) {
// Placeholder for bilmem action if needed
};
}
// Remove from previous parent if needed
if (game.bilmemBtn.parent) game.bilmemBtn.parent.removeChild(game.bilmemBtn);
// Place bilmemBtn just below emergencyBtn in LK.gui.bottomRight
var bilmemBtnYOffset = 0;
if (emergencyBtn && emergencyBtn.asset && emergencyBtn.asset.height) {
bilmemBtnYOffset = emergencyBtn.asset.height + 32; // 32px gap for touch
} else {
bilmemBtnYOffset = 300; // fallback if asset not loaded
}
game.bilmemBtn.x = 0;
game.bilmemBtn.y = bilmemBtnYOffset;
LK.gui.bottomRight.addChild(game.bilmemBtn);
game.bilmemBtn.visible = true;
// Place yeniBtn just below bilmemBtn in LK.gui.bottomRight
if (!game.yeniBtn) {
game.yeniBtn = new Container();
var yeniAsset = game.yeniBtn.attachAsset('yeni', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x00ff00 // Set to green
});
// Optionally, add a label or effect here if needed
game.yeniBtn.down = function (x, y, obj) {
// Placeholder for yeni action if needed
};
}
// Remove from previous parent if needed
if (game.yeniBtn.parent) game.yeniBtn.parent.removeChild(game.yeniBtn);
// Place yeniBtn just below bilmemBtn in LK.gui.bottomRight
var yeniBtnYOffset = 0;
if (bilmemAsset && bilmemAsset.height) {
yeniBtnYOffset = bilmemAsset.height + 32; // 32px gap for touch
} else {
yeniBtnYOffset = 300; // fallback if asset not loaded
}
game.yeniBtn.x = 0;
game.yeniBtn.y = game.bilmemBtn.y + yeniBtnYOffset;
LK.gui.bottomRight.addChild(game.yeniBtn);
game.yeniBtn.visible = true;
// Track if kill ability is active for the local player
var killAbilityActive = false;
// Track when the last kill was made (or game started)
var killCooldown = 10000; // 10 seconds in ms
var lastKillTime = Date.now(); // Initialize to game start
// When Kill button is pressed, activate kill ability for local impostor
game.killBtn.down = function (x, y, obj) {
if (game.state === 'playing' && game.localPlayer && game.localPlayer.isImpostor && game.localPlayer.isAlive) {
killAbilityActive = true;
// Optionally, you can add a visual effect to the button to show it's active
// e.g. game.killBtn.alpha = 0.7;
}
};
// Removed updateKillBtn and setupKillBtn logic as killBtn now follows the player
// Simple sound for meeting
// Simple sound for kill
// Simple sound for task complete
// Voting button (red)
// Voting button (blue)
// Button for emergency meeting
// Dead body (gray ellipse)
// Sabotage node (yellow box)
// Task node (small green box)
// Crewmate and Impostor avatars (simple colored ellipses)
// Game state: 'playing', 'meeting', 'voting', 'ended'
game.state = 'playing';
// Game variables
var playerCount = 6; // For MVP, fixed at 6 (1 impostor, 5 crewmates)
var impostorCount = 1;
var players = [];
var tasks = [];
var sabotageNodes = [];
var deadBodies = [];
var votingBtns = [];
var votes = {};
var voteResults = {};
var meetingTimer = null;
var votingTimer = null;
var taskGoal = 4; // Number of tasks per crewmate
var sabotageActive = false;
var sabotageTimer = null;
var sabotageDuration = 6000; // ms
var emergencyBtn = null;
var skipBtn = null;
var infoTxt = null;
var taskBar = null;
var taskBarBg = null;
var meetingCalled = false;
var localPlayer = null;
// Helper: Random name generator
function randomName() {
var names = ['Red', 'Blue', 'Green', 'Yellow', 'Pink', 'Orange', 'Cyan', 'Lime', 'Purple', 'Brown'];
return names[Math.floor(Math.random() * names.length)];
}
// Helper: Shuffle array
function shuffle(arr) {
for (var i = arr.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
return arr;
}
// Initialize players
function setupPlayers() {
players = [];
var roles = [];
for (var i = 0; i < impostorCount; i++) roles.push(true);
for (var i = impostorCount; i < playerCount; i++) roles.push(false);
// Always assign impostor to local player (player 0)
roles = [];
roles[0] = true; // local player is impostor
for (var i = 1; i < playerCount; i++) roles[i] = false;
shuffle(roles.slice(1)); // shuffle only the crewmates for variety
// Prepare a unique name pool so each player gets a unique color
var namePool = ['Red', 'Blue', 'Green', 'Yellow', 'Pink', 'Orange', 'Cyan', 'Lime', 'Purple', 'Brown'];
for (var i = 0; i < playerCount; i++) {
var p = new Player();
p.playerId = i;
// Assign a unique name/color to each player
var nameIdx = Math.floor(Math.random() * namePool.length);
p.name = namePool[nameIdx];
namePool.splice(nameIdx, 1); // Remove used name so no duplicates
p.setRole(i === 0); // local player is impostor, others are crewmates
p.isAlive = true;
// Spread players in larger world
p.x = 600 + i % 3 * 1200;
p.y = 800 + Math.floor(i / 3) * 1200;
p.showName();
p.isLocal = i === 0; // For MVP, player 0 is local
if (p.isLocal) {
localPlayer = p;
}
// Mark impostor visually for local player (for debug, can be removed in production)
if (p.isImpostor && p.isLocal) {
// Optionally, you could show a message or highlight
// infoTxt.setText('You are the Impostor!');
}
players.push(p);
game.addChild(p);
}
game.localPlayer = localPlayer;
lastKillTime = Date.now(); // Reset kill cooldown on new game
}
// Initialize tasks
function setupTasks() {
tasks = [];
var worldWidth = 4096;
var spacing = worldWidth / (taskGoal + 1);
for (var i = 0; i < taskGoal; i++) {
var t = new TaskNode();
t.taskId = i;
t.ownerId = localPlayer.playerId;
t.x = spacing * (i + 1);
t.y = 600;
tasks.push(t);
game.addChild(t);
}
}
// Initialize sabotage nodes
function setupSabotage() {
sabotageNodes = [];
for (var i = 0; i < 2; i++) {
var s = new SabotageNode();
s.x = 800 + i * 2400;
s.y = 3400;
sabotageNodes.push(s);
game.addChild(s);
}
}
// Emergency meeting button
function setupEmergencyBtn() {
emergencyBtn = new EmergencyBtn();
emergencyBtn.x = 2048 / 2;
emergencyBtn.y = 2732 - 200;
game.addChild(emergencyBtn);
}
// Task bar
function setupTaskBar() {
if (!taskBarBg) {
taskBarBg = LK.getAsset('tasknode', {
anchorX: 0.5,
anchorY: 0
});
taskBarBg.width = 800;
taskBarBg.height = 40;
taskBarBg.tint = 0x222222;
taskBarBg.x = 2048 / 2;
taskBarBg.y = 80;
LK.gui.top.addChild(taskBarBg);
}
if (!taskBar) {
taskBar = LK.getAsset('tasknode', {
anchorX: 0.5,
anchorY: 0
});
taskBar.width = 0;
taskBar.height = 40;
taskBar.tint = 0x83de44;
taskBar.x = 2048 / 2;
taskBar.y = 80;
LK.gui.top.addChild(taskBar);
}
}
// Info text
function setupInfoTxt() {
if (!infoTxt) {
infoTxt = new Text2('', {
size: 64,
fill: "#fff"
});
infoTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(infoTxt);
infoTxt.x = 2048 / 2;
infoTxt.y = 160;
}
}
// Voting skip button
function setupSkipBtn() {
skipBtn = new VoteBtn();
skipBtn.x = 2048 / 2;
skipBtn.y = 2732 - 300;
skipBtn.visible = false;
game.addChild(skipBtn);
}
// Update task bar
function updateTaskBar() {
var done = 0;
for (var i = 0; i < tasks.length; i++) {
if (tasks[i].done) done++;
}
var percent = done / taskGoal;
taskBar.width = 800 * percent;
}
// Complete a task
game.completeTask = function (task) {
updateTaskBar();
// Check win
var allDone = true;
for (var i = 0; i < tasks.length; i++) {
if (!tasks[i].done) allDone = false;
}
if (allDone) {
// If all tasks are done, impostor wins (crewmates lose)
gameEnd('impostors');
}
};
// Sabotage triggered
game.sabotageTriggered = function (node) {
sabotageActive = true;
sabotageTimer = LK.setTimeout(function () {
sabotageActive = false;
node.active = false;
node.asset.alpha = 1;
// If sabotage not fixed, impostors win
gameEnd('impostors');
}, sabotageDuration);
// Show info
infoTxt.setText('Sabotage! Fix it!');
tween(infoTxt, {
alpha: 1
}, {
duration: 200
});
};
// Call emergency meeting
game.callMeeting = function () {
if (meetingCalled) return;
meetingCalled = true;
LK.getSound('meeting').play();
game.state = 'meeting';
infoTxt.setText('Emergency Meeting!');
// Remove all dead bodies
for (var i = 0; i < players.length; i++) {
if (!players[i].isAlive && players[i].deadBody) {
players[i].deadBody.visible = false;
}
}
// After 2 seconds, go to voting
meetingTimer = LK.setTimeout(function () {
game.startVoting();
}, 2000);
};
// Start voting phase
game.startVoting = function () {
game.state = 'voting';
infoTxt.setText('Vote: Who is the Impostor?');
// Show voting buttons for each player
for (var i = 0; i < votingBtns.length; i++) {
votingBtns[i].destroy();
}
votingBtns = [];
var alivePlayers = [];
for (var i = 0; i < players.length; i++) {
if (players[i].isAlive) alivePlayers.push(players[i]);
}
var spacing = 2048 / (alivePlayers.length + 1);
for (var i = 0; i < alivePlayers.length; i++) {
var btn = new Container();
var asset = btn.attachAsset(alivePlayers[i].isImpostor ? 'votebtnred' : 'votebtn', {
anchorX: 0.5,
anchorY: 0.5
});
btn.x = spacing * (i + 1);
btn.y = 1200;
var txt = new Text2(alivePlayers[i].name, {
size: 48,
fill: "#fff"
});
txt.anchor.set(0.5, 0.5);
btn.addChild(txt);
btn.down = function (targetPlayer) {
return function (x, y, obj) {
if (game.state === 'voting' && !game.localPlayer.isVoted && targetPlayer.isAlive && targetPlayer !== game.localPlayer) {
game.voteFor(targetPlayer);
}
};
}(alivePlayers[i]);
votingBtns.push(btn);
game.addChild(btn);
}
// Show skip button
skipBtn.visible = true;
// Voting timer (auto skip after 10s)
votingTimer = LK.setTimeout(function () {
if (!game.localPlayer.isVoted) {
game.voteFor(null);
}
}, 10000);
};
// Vote for a player (or skip)
game.voteFor = function (targetPlayer) {
if (game.localPlayer.isVoted) return;
game.localPlayer.isVoted = true;
game.localPlayer.voteTarget = targetPlayer ? targetPlayer.playerId : null;
LK.getSound('vote').play();
// Show confirmation
infoTxt.setText('Voted!');
// Wait for all votes (MVP: AI votes randomly)
var allVoted = true;
for (var i = 0; i < players.length; i++) {
if (players[i].isAlive && !players[i].isVoted) allVoted = false;
}
if (!allVoted) {
// Simulate AI votes after 1s
LK.setTimeout(function () {
for (var i = 0; i < players.length; i++) {
if (players[i].isAlive && !players[i].isVoted) {
// AI votes randomly (not self)
var choices = [];
for (var j = 0; j < players.length; j++) {
if (players[j].isAlive && players[j] !== players[i]) choices.push(players[j]);
}
if (Math.random() < 0.2) {
// 20% skip
players[i].voteTarget = null;
} else {
players[i].voteTarget = choices[Math.floor(Math.random() * choices.length)].playerId;
}
players[i].isVoted = true;
}
}
game.tallyVotes();
}, 1000);
} else {
game.tallyVotes();
}
};
// Tally votes and resolve
game.tallyVotes = function () {
// Clear voting timer
if (votingTimer) {
LK.clearTimeout(votingTimer);
votingTimer = null;
}
// Hide voting buttons
for (var i = 0; i < votingBtns.length; i++) {
votingBtns[i].destroy();
}
votingBtns = [];
skipBtn.visible = false;
// Count votes
var counts = {};
var skipCount = 0;
for (var i = 0; i < players.length; i++) {
if (players[i].isAlive) {
var v = players[i].voteTarget;
if (v === null) {
skipCount++;
} else {
if (!counts[v]) counts[v] = 0;
counts[v]++;
}
}
}
// Find max
var maxVotes = 0;
var maxPlayerId = null;
for (var pid in counts) {
if (counts[pid] > maxVotes) {
maxVotes = counts[pid];
maxPlayerId = parseInt(pid);
}
}
// If tie or skip, no one ejected
var tie = false;
var totalVotes = 0;
for (var pid in counts) {
if (counts[pid] === maxVotes && parseInt(pid) !== maxPlayerId) tie = true;
totalVotes += counts[pid];
}
if (skipCount >= maxVotes || tie || maxPlayerId === null) {
infoTxt.setText('No one was ejected.');
} else {
// Eject player
var ejected = null;
for (var i = 0; i < players.length; i++) {
if (players[i].playerId === maxPlayerId) {
ejected = players[i];
break;
}
}
if (ejected) {
ejected.showDead();
infoTxt.setText(ejected.name + ' was ejected!');
// Check win
var impostorsLeft = 0,
crewmatesLeft = 0;
for (var i = 0; i < players.length; i++) {
if (players[i].isAlive) {
if (players[i].isImpostor) impostorsLeft++;else crewmatesLeft++;
}
}
if (impostorsLeft === 0) {
gameEnd('crewmates');
return;
}
if (impostorsLeft >= crewmatesLeft) {
gameEnd('impostors');
return;
}
}
}
// Reset votes
for (var i = 0; i < players.length; i++) {
players[i].isVoted = false;
players[i].voteTarget = null;
}
// Resume play after 2s
LK.setTimeout(function () {
infoTxt.setText('');
game.state = 'playing';
meetingCalled = false;
lastKillTime = Date.now(); // Reset kill cooldown after meeting
}, 2000);
};
// End game
function gameEnd(winner) {
game.state = 'ended';
if (winner === 'crewmates') {
LK.showYouWin();
} else {
// If local player is impostor, show win, else show game over
if (game.localPlayer && game.localPlayer.isImpostor) {
LK.showYouWin();
} else {
LK.showGameOver();
}
}
}
// Kill action (impostor only)
function tryKill(target) {
if (!game.localPlayer.isImpostor || !target.isAlive || !game.localPlayer.isAlive) return;
// Must be close
var dx = target.x - game.localPlayer.x;
var dy = target.y - game.localPlayer.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 200) {
target.showDead();
LK.getSound('kill').play();
// Show dead body
if (target.deadBody) target.deadBody.visible = true;
// Check win
var impostorsLeft = 0,
crewmatesLeft = 0;
for (var i = 0; i < players.length; i++) {
if (players[i].isAlive) {
if (players[i].isImpostor) impostorsLeft++;else crewmatesLeft++;
}
}
if (impostorsLeft === 0) {
gameEnd('crewmates');
return;
}
if (impostorsLeft >= crewmatesLeft) {
gameEnd('impostors');
return;
}
}
}
// Move handler (drag local player)
var dragNode = null;
function handleMove(x, y, obj) {
if (game.state !== 'playing') return;
// Make the local player always follow the mouse/touch position
if (game.state === 'playing' && game.localPlayer && game.localPlayer.isAlive) {
// Clamp to expanded game area
var worldWidth = 4096;
var worldHeight = 4096;
var nx = Math.max(100, Math.min(worldWidth - 100, x));
var ny = Math.max(200, Math.min(worldHeight - 200, y));
// Move local player gradually toward the pointer/touch position (reduced speed)
if (typeof game.localPlayer.targetX === "undefined") {
game.localPlayer.targetX = game.localPlayer.x;
game.localPlayer.targetY = game.localPlayer.y;
}
game.localPlayer.targetX = nx;
game.localPlayer.targetY = ny;
// Interpolate position (speed: 20 px per frame, capped to not overshoot)
var dx = game.localPlayer.targetX - game.localPlayer.x;
var dy = game.localPlayer.targetY - game.localPlayer.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var maxSpeed = 20;
if (dist > maxSpeed) {
game.localPlayer.x += dx / dist * maxSpeed;
game.localPlayer.y += dy / dist * maxSpeed;
} else {
game.localPlayer.x = game.localPlayer.targetX;
game.localPlayer.y = game.localPlayer.targetY;
}
// Camera follow: center camera on local player, clamp to world bounds
var camX = Math.max(0, Math.min(worldWidth - 2048, game.localPlayer.x - 1024));
var camY = Math.max(0, Math.min(worldHeight - 2732, game.localPlayer.y - 1366));
game.x = -camX;
game.y = -camY;
}
// If impostor, check for kill
if (game.state === 'playing' && game.localPlayer && game.localPlayer.isImpostor && game.localPlayer.isAlive && killAbilityActive) {
// Only allow kill if 10s have passed since last kill
var now = Date.now();
if (now - lastKillTime >= killCooldown) {
for (var i = 0; i < players.length; i++) {
var target = players[i];
if (target !== game.localPlayer && target.isAlive && !target.isImpostor && game.localPlayer.intersects(target)) {
tryKill(target);
// After a successful kill, deactivate kill ability until button is pressed again
killAbilityActive = false;
lastKillTime = now; // Reset cooldown
// Optionally, reset killBtn visual effect
// game.killBtn.alpha = 1;
break;
}
}
}
}
}
game.move = handleMove;
// Down handler (start drag or interact)
game.down = function (x, y, obj) {
if (game.state !== 'playing') return;
// Only drag local player if touch is on them
var lp = game.localPlayer;
var dx = x - lp.x;
var dy = y - lp.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 100 && lp.isAlive) {
dragNode = lp;
handleMove(x, y, obj);
}
};
game.up = function (x, y, obj) {
dragNode = null;
};
// Game update loop
game.update = function () {
// AI players move randomly
if (game.state === 'playing') {
for (var i = 0; i < players.length; i++) {
var p = players[i];
if (!p.isLocal && p.isAlive) {
// Random walk
if (LK.ticks % (60 + i * 7) === 0) {
var tx = 200 + Math.random() * (2048 - 400);
var ty = 500 + Math.random() * (2732 - 1000);
tween(p, {
x: tx,
y: ty
}, {
duration: 1200 + Math.random() * 800,
easing: tween.easeInOut
});
}
// AI crewmates complete tasks
if (!p.isImpostor) {
for (var j = 0; j < tasks.length; j++) {
var t = tasks[j];
if (!t.done && t.ownerId === p.playerId) {
var dx = t.x - p.x;
var dy = t.y - p.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 120) {
t.done = true;
t.asset.alpha = 0.3;
updateTaskBar();
}
}
}
}
// AI impostor kills
if (p.isImpostor) {
for (var j = 0; j < players.length; j++) {
var target = players[j];
if (target !== p && target.isAlive && !target.isImpostor) {
var dx = target.x - p.x;
var dy = target.y - p.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 180 && Math.random() < 0.01) {
target.showDead();
if (target.deadBody) target.deadBody.visible = true;
}
}
}
}
}
}
}
// Make Kill button follow the local player (impostor)
if (game.killBtn && game.localPlayer && game.localPlayer.isImpostor && game.localPlayer.isAlive) {
// Position killBtn near the local player, offset to the right and below
var offsetX = 120;
var offsetY = 120;
game.killBtn.x = game.localPlayer.x + offsetX;
game.killBtn.y = game.localPlayer.y + offsetY;
// Ensure killBtn is visible
game.killBtn.visible = true;
}
};
// Initial setup
setupPlayers();
setupTasks();
setupSabotage();
setupEmergencyBtn();
setupTaskBar();
setupInfoTxt();
setupSkipBtn();
updateTaskBar();
// If killBtn exists, it will follow the player in the update loop /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Emergency meeting button
var EmergencyBtn = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('emergencybtn', {
anchorX: 0.5,
anchorY: 0.5
});
self.txt = new Text2('EMERGENCY', {
size: 48,
fill: "#fff"
});
self.txt.anchor.set(0.5, 0.5);
self.txt.x = 0;
self.txt.y = 0;
self.addChild(self.txt);
self.down = function (x, y, obj) {
if (game.state === 'playing' && !game.meetingCalled) {
game.callMeeting();
}
};
return self;
});
// Player class (Crewmate or Impostor)
var Player = Container.expand(function () {
var self = Container.call(this);
// Properties
self.isImpostor = false;
self.isAlive = true;
self.playerId = null;
self.name = '';
self.avatar = null;
self.isLocal = false; // Is this the local player?
self.isVoted = false;
self.voteTarget = null;
self.showName = function () {
if (!self.nameTxt) {
// Determine color for name based on player role
var nameColor = self.isImpostor ? "#d7263d" : "#83de44";
self.nameTxt = new Text2(self.name, {
size: 48,
fill: nameColor
});
self.nameTxt.anchor.set(0.5, 0);
self.addChild(self.nameTxt);
self.nameTxt.y = self.avatar.height / 2 + 10;
}
};
// Set up avatar
self.setRole = function (isImpostor) {
self.isImpostor = isImpostor;
if (self.avatar) {
self.removeChild(self.avatar);
}
// Determine color from name
var nameColorMap = {
"Red": 0xd7263d,
"Blue": 0x3ec1d3,
"Green": 0x83de44,
"Yellow": 0xf9d423,
"Pink": 0xff69b4,
"Orange": 0xf97c1b,
"Cyan": 0x15ffff,
"Lime": 0x7fff00,
"Purple": 0x6c3483,
"Brown": 0x8b5a2b
};
var colorKey = (self.name || "").split(" ")[0];
var playerColor = nameColorMap[colorKey] !== undefined ? nameColorMap[colorKey] : isImpostor ? 0xd7263d : 0x83de44;
if (isImpostor) {
self.avatar = self.attachAsset('impostor', {
anchorX: 0.5,
anchorY: 0.5,
tint: playerColor
});
} else {
self.avatar = self.attachAsset('crewmate', {
anchorX: 0.5,
anchorY: 0.5,
tint: playerColor
});
}
};
// Show dead body
self.showDead = function () {
if (self.avatar) self.avatar.visible = false;
if (!self.deadBody) {
self.deadBody = self.attachAsset('deadbody', {
anchorX: 0.5,
anchorY: 0.5
});
}
self.isAlive = false;
};
// Hide dead body, show alive
self.revive = function () {
if (self.deadBody) self.deadBody.visible = false;
if (self.avatar) self.avatar.visible = true;
self.isAlive = true;
};
// For touch/click events
self.down = function (x, y, obj) {
// Used for voting
if (game.state === 'voting' && !game.localPlayer.isVoted && self.isAlive && self !== game.localPlayer) {
game.voteFor(self);
} else if (game.state === 'playing' && game.localPlayer && game.localPlayer.isImpostor && game.localPlayer.isAlive && self.isAlive && self !== game.localPlayer) {
// Check distance for kill range (same as tryKill)
var dx = self.x - game.localPlayer.x;
var dy = self.y - game.localPlayer.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 200) {
tryKill(self);
// Deactivate kill ability and reset cooldown
killAbilityActive = false;
lastKillTime = Date.now();
}
}
};
return self;
});
// Sabotage node class
var SabotageNode = Container.expand(function () {
var self = Container.call(this);
self.active = false;
self.asset = self.attachAsset('sabotagenode', {
anchorX: 0.5,
anchorY: 0.5
});
self.down = function (x, y, obj) {
if (game.state === 'playing' && game.localPlayer.isImpostor && !self.active) {
// Activate sabotage
self.active = true;
self.asset.alpha = 0.5;
game.sabotageTriggered(self);
}
};
return self;
});
// Task node class
var TaskNode = Container.expand(function () {
var self = Container.call(this);
self.done = false;
self.taskId = null;
self.ownerId = null; // Which player this task belongs to (for local tasks)
// Assign a color to each vaccine/task node. For example, cycle through a set of colors based on taskId.
var vaccineColors = [0x83de44, 0x3ec1d3, 0xf9d423, 0xd7263d, 0x6c3483, 0x15ffff];
var color = vaccineColors[self.taskId % vaccineColors.length];
self.asset = self.attachAsset('tasknode', {
anchorX: 0.5,
anchorY: 0.5,
color: color
});
self.down = function (x, y, obj) {
if (game.state === 'playing' && !self.done && self.ownerId === game.localPlayer.playerId) {
// Complete task
self.done = true;
self.asset.alpha = 0.3;
LK.getSound('taskdone').play();
game.completeTask(self);
}
};
return self;
});
// Voting button (for skip or confirm)
var VoteBtn = Container.expand(function () {
var self = Container.call(this);
self.asset = self.attachAsset('votebtn', {
anchorX: 0.5,
anchorY: 0.5
});
self.txt = new Text2('SKIP', {
size: 48,
fill: "#fff"
});
self.txt.anchor.set(0.5, 0.5);
self.txt.x = 0;
self.txt.y = 0;
self.addChild(self.txt);
self.down = function (x, y, obj) {
if (game.state === 'voting' && !game.localPlayer.isVoted) {
game.voteFor(null); // skip
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a2e
});
/****
* Game Code
****/
// Add background image to the game area
if (!game.bg) {
game.bg = LK.getAsset('background', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
width: 4096,
height: 4096
});
game.addChildAt(game.bg, 0); // Add as the bottom-most layer
}
// Place Emergency button at the bottom right corner using LK.gui.bottomRight
if (emergencyBtn) {
// Remove from previous parent if needed
if (emergencyBtn.parent) emergencyBtn.parent.removeChild(emergencyBtn);
emergencyBtn.x = 0;
emergencyBtn.y = 0;
LK.gui.bottomRight.addChild(emergencyBtn);
emergencyBtn.visible = true;
}
// Place Kill button as a button just below the emergencyBtn in LK.gui.bottomRight
if (!game.killBtn) {
// Create Kill button as a Container with the Kill asset
game.killBtn = new Container();
var killAsset = game.killBtn.attachAsset('Kill', {
anchorX: 0.5,
anchorY: 0.5
});
// Optionally, you can add a label or effect here if needed
// Set up button event (no action for now, can be extended)
game.killBtn.down = function (x, y, obj) {
// Placeholder for kill action if needed
};
}
// Remove from previous parent if needed
if (game.killBtn.parent) game.killBtn.parent.removeChild(game.killBtn);
// Place killBtn just below emergencyBtn in LK.gui.bottomRight
// Calculate y offset based on emergencyBtn's height
var killBtnYOffset = 0;
if (emergencyBtn && emergencyBtn.asset && emergencyBtn.asset.height) {
killBtnYOffset = emergencyBtn.asset.height + 32; // 32px gap for touch
} else {
killBtnYOffset = 300; // fallback if asset not loaded
}
game.killBtn.x = 0;
game.killBtn.y = killBtnYOffset;
LK.gui.bottomRight.addChild(game.killBtn);
game.killBtn.visible = true;
// Place bilmem button just below emergencyBtn in LK.gui.bottomRight
if (!game.bilmemBtn) {
game.bilmemBtn = new Container();
var bilmemAsset = game.bilmemBtn.attachAsset('bilmem', {
anchorX: 0.5,
anchorY: 0.5
});
// Optionally, add a label or effect here if needed
// Set up button event (no action for now, can be extended)
game.bilmemBtn.down = function (x, y, obj) {
// Placeholder for bilmem action if needed
};
}
// Remove from previous parent if needed
if (game.bilmemBtn.parent) game.bilmemBtn.parent.removeChild(game.bilmemBtn);
// Place bilmemBtn just below emergencyBtn in LK.gui.bottomRight
var bilmemBtnYOffset = 0;
if (emergencyBtn && emergencyBtn.asset && emergencyBtn.asset.height) {
bilmemBtnYOffset = emergencyBtn.asset.height + 32; // 32px gap for touch
} else {
bilmemBtnYOffset = 300; // fallback if asset not loaded
}
game.bilmemBtn.x = 0;
game.bilmemBtn.y = bilmemBtnYOffset;
LK.gui.bottomRight.addChild(game.bilmemBtn);
game.bilmemBtn.visible = true;
// Place yeniBtn just below bilmemBtn in LK.gui.bottomRight
if (!game.yeniBtn) {
game.yeniBtn = new Container();
var yeniAsset = game.yeniBtn.attachAsset('yeni', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x00ff00 // Set to green
});
// Optionally, add a label or effect here if needed
game.yeniBtn.down = function (x, y, obj) {
// Placeholder for yeni action if needed
};
}
// Remove from previous parent if needed
if (game.yeniBtn.parent) game.yeniBtn.parent.removeChild(game.yeniBtn);
// Place yeniBtn just below bilmemBtn in LK.gui.bottomRight
var yeniBtnYOffset = 0;
if (bilmemAsset && bilmemAsset.height) {
yeniBtnYOffset = bilmemAsset.height + 32; // 32px gap for touch
} else {
yeniBtnYOffset = 300; // fallback if asset not loaded
}
game.yeniBtn.x = 0;
game.yeniBtn.y = game.bilmemBtn.y + yeniBtnYOffset;
LK.gui.bottomRight.addChild(game.yeniBtn);
game.yeniBtn.visible = true;
// Track if kill ability is active for the local player
var killAbilityActive = false;
// Track when the last kill was made (or game started)
var killCooldown = 10000; // 10 seconds in ms
var lastKillTime = Date.now(); // Initialize to game start
// When Kill button is pressed, activate kill ability for local impostor
game.killBtn.down = function (x, y, obj) {
if (game.state === 'playing' && game.localPlayer && game.localPlayer.isImpostor && game.localPlayer.isAlive) {
killAbilityActive = true;
// Optionally, you can add a visual effect to the button to show it's active
// e.g. game.killBtn.alpha = 0.7;
}
};
// Removed updateKillBtn and setupKillBtn logic as killBtn now follows the player
// Simple sound for meeting
// Simple sound for kill
// Simple sound for task complete
// Voting button (red)
// Voting button (blue)
// Button for emergency meeting
// Dead body (gray ellipse)
// Sabotage node (yellow box)
// Task node (small green box)
// Crewmate and Impostor avatars (simple colored ellipses)
// Game state: 'playing', 'meeting', 'voting', 'ended'
game.state = 'playing';
// Game variables
var playerCount = 6; // For MVP, fixed at 6 (1 impostor, 5 crewmates)
var impostorCount = 1;
var players = [];
var tasks = [];
var sabotageNodes = [];
var deadBodies = [];
var votingBtns = [];
var votes = {};
var voteResults = {};
var meetingTimer = null;
var votingTimer = null;
var taskGoal = 4; // Number of tasks per crewmate
var sabotageActive = false;
var sabotageTimer = null;
var sabotageDuration = 6000; // ms
var emergencyBtn = null;
var skipBtn = null;
var infoTxt = null;
var taskBar = null;
var taskBarBg = null;
var meetingCalled = false;
var localPlayer = null;
// Helper: Random name generator
function randomName() {
var names = ['Red', 'Blue', 'Green', 'Yellow', 'Pink', 'Orange', 'Cyan', 'Lime', 'Purple', 'Brown'];
return names[Math.floor(Math.random() * names.length)];
}
// Helper: Shuffle array
function shuffle(arr) {
for (var i = arr.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
return arr;
}
// Initialize players
function setupPlayers() {
players = [];
var roles = [];
for (var i = 0; i < impostorCount; i++) roles.push(true);
for (var i = impostorCount; i < playerCount; i++) roles.push(false);
// Always assign impostor to local player (player 0)
roles = [];
roles[0] = true; // local player is impostor
for (var i = 1; i < playerCount; i++) roles[i] = false;
shuffle(roles.slice(1)); // shuffle only the crewmates for variety
// Prepare a unique name pool so each player gets a unique color
var namePool = ['Red', 'Blue', 'Green', 'Yellow', 'Pink', 'Orange', 'Cyan', 'Lime', 'Purple', 'Brown'];
for (var i = 0; i < playerCount; i++) {
var p = new Player();
p.playerId = i;
// Assign a unique name/color to each player
var nameIdx = Math.floor(Math.random() * namePool.length);
p.name = namePool[nameIdx];
namePool.splice(nameIdx, 1); // Remove used name so no duplicates
p.setRole(i === 0); // local player is impostor, others are crewmates
p.isAlive = true;
// Spread players in larger world
p.x = 600 + i % 3 * 1200;
p.y = 800 + Math.floor(i / 3) * 1200;
p.showName();
p.isLocal = i === 0; // For MVP, player 0 is local
if (p.isLocal) {
localPlayer = p;
}
// Mark impostor visually for local player (for debug, can be removed in production)
if (p.isImpostor && p.isLocal) {
// Optionally, you could show a message or highlight
// infoTxt.setText('You are the Impostor!');
}
players.push(p);
game.addChild(p);
}
game.localPlayer = localPlayer;
lastKillTime = Date.now(); // Reset kill cooldown on new game
}
// Initialize tasks
function setupTasks() {
tasks = [];
var worldWidth = 4096;
var spacing = worldWidth / (taskGoal + 1);
for (var i = 0; i < taskGoal; i++) {
var t = new TaskNode();
t.taskId = i;
t.ownerId = localPlayer.playerId;
t.x = spacing * (i + 1);
t.y = 600;
tasks.push(t);
game.addChild(t);
}
}
// Initialize sabotage nodes
function setupSabotage() {
sabotageNodes = [];
for (var i = 0; i < 2; i++) {
var s = new SabotageNode();
s.x = 800 + i * 2400;
s.y = 3400;
sabotageNodes.push(s);
game.addChild(s);
}
}
// Emergency meeting button
function setupEmergencyBtn() {
emergencyBtn = new EmergencyBtn();
emergencyBtn.x = 2048 / 2;
emergencyBtn.y = 2732 - 200;
game.addChild(emergencyBtn);
}
// Task bar
function setupTaskBar() {
if (!taskBarBg) {
taskBarBg = LK.getAsset('tasknode', {
anchorX: 0.5,
anchorY: 0
});
taskBarBg.width = 800;
taskBarBg.height = 40;
taskBarBg.tint = 0x222222;
taskBarBg.x = 2048 / 2;
taskBarBg.y = 80;
LK.gui.top.addChild(taskBarBg);
}
if (!taskBar) {
taskBar = LK.getAsset('tasknode', {
anchorX: 0.5,
anchorY: 0
});
taskBar.width = 0;
taskBar.height = 40;
taskBar.tint = 0x83de44;
taskBar.x = 2048 / 2;
taskBar.y = 80;
LK.gui.top.addChild(taskBar);
}
}
// Info text
function setupInfoTxt() {
if (!infoTxt) {
infoTxt = new Text2('', {
size: 64,
fill: "#fff"
});
infoTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(infoTxt);
infoTxt.x = 2048 / 2;
infoTxt.y = 160;
}
}
// Voting skip button
function setupSkipBtn() {
skipBtn = new VoteBtn();
skipBtn.x = 2048 / 2;
skipBtn.y = 2732 - 300;
skipBtn.visible = false;
game.addChild(skipBtn);
}
// Update task bar
function updateTaskBar() {
var done = 0;
for (var i = 0; i < tasks.length; i++) {
if (tasks[i].done) done++;
}
var percent = done / taskGoal;
taskBar.width = 800 * percent;
}
// Complete a task
game.completeTask = function (task) {
updateTaskBar();
// Check win
var allDone = true;
for (var i = 0; i < tasks.length; i++) {
if (!tasks[i].done) allDone = false;
}
if (allDone) {
// If all tasks are done, impostor wins (crewmates lose)
gameEnd('impostors');
}
};
// Sabotage triggered
game.sabotageTriggered = function (node) {
sabotageActive = true;
sabotageTimer = LK.setTimeout(function () {
sabotageActive = false;
node.active = false;
node.asset.alpha = 1;
// If sabotage not fixed, impostors win
gameEnd('impostors');
}, sabotageDuration);
// Show info
infoTxt.setText('Sabotage! Fix it!');
tween(infoTxt, {
alpha: 1
}, {
duration: 200
});
};
// Call emergency meeting
game.callMeeting = function () {
if (meetingCalled) return;
meetingCalled = true;
LK.getSound('meeting').play();
game.state = 'meeting';
infoTxt.setText('Emergency Meeting!');
// Remove all dead bodies
for (var i = 0; i < players.length; i++) {
if (!players[i].isAlive && players[i].deadBody) {
players[i].deadBody.visible = false;
}
}
// After 2 seconds, go to voting
meetingTimer = LK.setTimeout(function () {
game.startVoting();
}, 2000);
};
// Start voting phase
game.startVoting = function () {
game.state = 'voting';
infoTxt.setText('Vote: Who is the Impostor?');
// Show voting buttons for each player
for (var i = 0; i < votingBtns.length; i++) {
votingBtns[i].destroy();
}
votingBtns = [];
var alivePlayers = [];
for (var i = 0; i < players.length; i++) {
if (players[i].isAlive) alivePlayers.push(players[i]);
}
var spacing = 2048 / (alivePlayers.length + 1);
for (var i = 0; i < alivePlayers.length; i++) {
var btn = new Container();
var asset = btn.attachAsset(alivePlayers[i].isImpostor ? 'votebtnred' : 'votebtn', {
anchorX: 0.5,
anchorY: 0.5
});
btn.x = spacing * (i + 1);
btn.y = 1200;
var txt = new Text2(alivePlayers[i].name, {
size: 48,
fill: "#fff"
});
txt.anchor.set(0.5, 0.5);
btn.addChild(txt);
btn.down = function (targetPlayer) {
return function (x, y, obj) {
if (game.state === 'voting' && !game.localPlayer.isVoted && targetPlayer.isAlive && targetPlayer !== game.localPlayer) {
game.voteFor(targetPlayer);
}
};
}(alivePlayers[i]);
votingBtns.push(btn);
game.addChild(btn);
}
// Show skip button
skipBtn.visible = true;
// Voting timer (auto skip after 10s)
votingTimer = LK.setTimeout(function () {
if (!game.localPlayer.isVoted) {
game.voteFor(null);
}
}, 10000);
};
// Vote for a player (or skip)
game.voteFor = function (targetPlayer) {
if (game.localPlayer.isVoted) return;
game.localPlayer.isVoted = true;
game.localPlayer.voteTarget = targetPlayer ? targetPlayer.playerId : null;
LK.getSound('vote').play();
// Show confirmation
infoTxt.setText('Voted!');
// Wait for all votes (MVP: AI votes randomly)
var allVoted = true;
for (var i = 0; i < players.length; i++) {
if (players[i].isAlive && !players[i].isVoted) allVoted = false;
}
if (!allVoted) {
// Simulate AI votes after 1s
LK.setTimeout(function () {
for (var i = 0; i < players.length; i++) {
if (players[i].isAlive && !players[i].isVoted) {
// AI votes randomly (not self)
var choices = [];
for (var j = 0; j < players.length; j++) {
if (players[j].isAlive && players[j] !== players[i]) choices.push(players[j]);
}
if (Math.random() < 0.2) {
// 20% skip
players[i].voteTarget = null;
} else {
players[i].voteTarget = choices[Math.floor(Math.random() * choices.length)].playerId;
}
players[i].isVoted = true;
}
}
game.tallyVotes();
}, 1000);
} else {
game.tallyVotes();
}
};
// Tally votes and resolve
game.tallyVotes = function () {
// Clear voting timer
if (votingTimer) {
LK.clearTimeout(votingTimer);
votingTimer = null;
}
// Hide voting buttons
for (var i = 0; i < votingBtns.length; i++) {
votingBtns[i].destroy();
}
votingBtns = [];
skipBtn.visible = false;
// Count votes
var counts = {};
var skipCount = 0;
for (var i = 0; i < players.length; i++) {
if (players[i].isAlive) {
var v = players[i].voteTarget;
if (v === null) {
skipCount++;
} else {
if (!counts[v]) counts[v] = 0;
counts[v]++;
}
}
}
// Find max
var maxVotes = 0;
var maxPlayerId = null;
for (var pid in counts) {
if (counts[pid] > maxVotes) {
maxVotes = counts[pid];
maxPlayerId = parseInt(pid);
}
}
// If tie or skip, no one ejected
var tie = false;
var totalVotes = 0;
for (var pid in counts) {
if (counts[pid] === maxVotes && parseInt(pid) !== maxPlayerId) tie = true;
totalVotes += counts[pid];
}
if (skipCount >= maxVotes || tie || maxPlayerId === null) {
infoTxt.setText('No one was ejected.');
} else {
// Eject player
var ejected = null;
for (var i = 0; i < players.length; i++) {
if (players[i].playerId === maxPlayerId) {
ejected = players[i];
break;
}
}
if (ejected) {
ejected.showDead();
infoTxt.setText(ejected.name + ' was ejected!');
// Check win
var impostorsLeft = 0,
crewmatesLeft = 0;
for (var i = 0; i < players.length; i++) {
if (players[i].isAlive) {
if (players[i].isImpostor) impostorsLeft++;else crewmatesLeft++;
}
}
if (impostorsLeft === 0) {
gameEnd('crewmates');
return;
}
if (impostorsLeft >= crewmatesLeft) {
gameEnd('impostors');
return;
}
}
}
// Reset votes
for (var i = 0; i < players.length; i++) {
players[i].isVoted = false;
players[i].voteTarget = null;
}
// Resume play after 2s
LK.setTimeout(function () {
infoTxt.setText('');
game.state = 'playing';
meetingCalled = false;
lastKillTime = Date.now(); // Reset kill cooldown after meeting
}, 2000);
};
// End game
function gameEnd(winner) {
game.state = 'ended';
if (winner === 'crewmates') {
LK.showYouWin();
} else {
// If local player is impostor, show win, else show game over
if (game.localPlayer && game.localPlayer.isImpostor) {
LK.showYouWin();
} else {
LK.showGameOver();
}
}
}
// Kill action (impostor only)
function tryKill(target) {
if (!game.localPlayer.isImpostor || !target.isAlive || !game.localPlayer.isAlive) return;
// Must be close
var dx = target.x - game.localPlayer.x;
var dy = target.y - game.localPlayer.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 200) {
target.showDead();
LK.getSound('kill').play();
// Show dead body
if (target.deadBody) target.deadBody.visible = true;
// Check win
var impostorsLeft = 0,
crewmatesLeft = 0;
for (var i = 0; i < players.length; i++) {
if (players[i].isAlive) {
if (players[i].isImpostor) impostorsLeft++;else crewmatesLeft++;
}
}
if (impostorsLeft === 0) {
gameEnd('crewmates');
return;
}
if (impostorsLeft >= crewmatesLeft) {
gameEnd('impostors');
return;
}
}
}
// Move handler (drag local player)
var dragNode = null;
function handleMove(x, y, obj) {
if (game.state !== 'playing') return;
// Make the local player always follow the mouse/touch position
if (game.state === 'playing' && game.localPlayer && game.localPlayer.isAlive) {
// Clamp to expanded game area
var worldWidth = 4096;
var worldHeight = 4096;
var nx = Math.max(100, Math.min(worldWidth - 100, x));
var ny = Math.max(200, Math.min(worldHeight - 200, y));
// Move local player gradually toward the pointer/touch position (reduced speed)
if (typeof game.localPlayer.targetX === "undefined") {
game.localPlayer.targetX = game.localPlayer.x;
game.localPlayer.targetY = game.localPlayer.y;
}
game.localPlayer.targetX = nx;
game.localPlayer.targetY = ny;
// Interpolate position (speed: 20 px per frame, capped to not overshoot)
var dx = game.localPlayer.targetX - game.localPlayer.x;
var dy = game.localPlayer.targetY - game.localPlayer.y;
var dist = Math.sqrt(dx * dx + dy * dy);
var maxSpeed = 20;
if (dist > maxSpeed) {
game.localPlayer.x += dx / dist * maxSpeed;
game.localPlayer.y += dy / dist * maxSpeed;
} else {
game.localPlayer.x = game.localPlayer.targetX;
game.localPlayer.y = game.localPlayer.targetY;
}
// Camera follow: center camera on local player, clamp to world bounds
var camX = Math.max(0, Math.min(worldWidth - 2048, game.localPlayer.x - 1024));
var camY = Math.max(0, Math.min(worldHeight - 2732, game.localPlayer.y - 1366));
game.x = -camX;
game.y = -camY;
}
// If impostor, check for kill
if (game.state === 'playing' && game.localPlayer && game.localPlayer.isImpostor && game.localPlayer.isAlive && killAbilityActive) {
// Only allow kill if 10s have passed since last kill
var now = Date.now();
if (now - lastKillTime >= killCooldown) {
for (var i = 0; i < players.length; i++) {
var target = players[i];
if (target !== game.localPlayer && target.isAlive && !target.isImpostor && game.localPlayer.intersects(target)) {
tryKill(target);
// After a successful kill, deactivate kill ability until button is pressed again
killAbilityActive = false;
lastKillTime = now; // Reset cooldown
// Optionally, reset killBtn visual effect
// game.killBtn.alpha = 1;
break;
}
}
}
}
}
game.move = handleMove;
// Down handler (start drag or interact)
game.down = function (x, y, obj) {
if (game.state !== 'playing') return;
// Only drag local player if touch is on them
var lp = game.localPlayer;
var dx = x - lp.x;
var dy = y - lp.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 100 && lp.isAlive) {
dragNode = lp;
handleMove(x, y, obj);
}
};
game.up = function (x, y, obj) {
dragNode = null;
};
// Game update loop
game.update = function () {
// AI players move randomly
if (game.state === 'playing') {
for (var i = 0; i < players.length; i++) {
var p = players[i];
if (!p.isLocal && p.isAlive) {
// Random walk
if (LK.ticks % (60 + i * 7) === 0) {
var tx = 200 + Math.random() * (2048 - 400);
var ty = 500 + Math.random() * (2732 - 1000);
tween(p, {
x: tx,
y: ty
}, {
duration: 1200 + Math.random() * 800,
easing: tween.easeInOut
});
}
// AI crewmates complete tasks
if (!p.isImpostor) {
for (var j = 0; j < tasks.length; j++) {
var t = tasks[j];
if (!t.done && t.ownerId === p.playerId) {
var dx = t.x - p.x;
var dy = t.y - p.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 120) {
t.done = true;
t.asset.alpha = 0.3;
updateTaskBar();
}
}
}
}
// AI impostor kills
if (p.isImpostor) {
for (var j = 0; j < players.length; j++) {
var target = players[j];
if (target !== p && target.isAlive && !target.isImpostor) {
var dx = target.x - p.x;
var dy = target.y - p.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 180 && Math.random() < 0.01) {
target.showDead();
if (target.deadBody) target.deadBody.visible = true;
}
}
}
}
}
}
}
// Make Kill button follow the local player (impostor)
if (game.killBtn && game.localPlayer && game.localPlayer.isImpostor && game.localPlayer.isAlive) {
// Position killBtn near the local player, offset to the right and below
var offsetX = 120;
var offsetY = 120;
game.killBtn.x = game.localPlayer.x + offsetX;
game.killBtn.y = game.localPlayer.y + offsetY;
// Ensure killBtn is visible
game.killBtn.visible = true;
}
};
// Initial setup
setupPlayers();
setupTasks();
setupSabotage();
setupEmergencyBtn();
setupTaskBar();
setupInfoTxt();
setupSkipBtn();
updateTaskBar();
// If killBtn exists, it will follow the player in the update loop