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