/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var AIPlayer = Container.expand(function (playerIndex) {
var self = Container.call(this);
var playerGraphics = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
// Different tints for each AI player
var playerColors = [0xFFFF00, 0xFF00FF, 0x00FFFF]; // Yellow, Magenta, Cyan
playerGraphics.tint = playerColors[playerIndex % 3];
// Add number display for each AI player with different numbers (not 456)
var playerNumbers = ['123', '789', '321']; // Different numbers for each AI player
var numberText = new Text2(playerNumbers[playerIndex], {
size: 40,
fill: 0x000000 // Black text
});
numberText.anchor.set(0.5, 0.5);
numberText.x = 0;
numberText.y = 0;
self.addChild(numberText);
self.targetX = 0;
self.targetY = 0;
self.isMoving = false;
self.health = 3;
self.moveTimer = 0;
self.nextMoveTime = Math.random() * 120 + 60; // Random 1-3 seconds
self.playerIndex = playerIndex;
// AI Personality traits
var personalityTypes = ['cowardly', 'fast', 'cautious'];
self.personality = personalityTypes[playerIndex % 3];
self.riskLevel = 0; // Track current risk assessment
self.safetyTimer = 0; // Time since last risky action
self.panicMode = false; // Emergency state for cowardly AI
self.moveTo = function (x, y) {
// Calculate distance from current position
var dx = x - self.x;
var dy = y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Personality-based movement adjustments
var maxDistance = 180;
var moveDuration = 350;
var animationDuration = 150;
if (self.personality === 'cowardly') {
// Cowardly AI: smaller, more cautious movements
maxDistance = self.panicMode ? 80 : 120;
moveDuration = 450; // Slower movement
} else if (self.personality === 'fast') {
// Fast AI: larger, quicker movements
maxDistance = 220;
moveDuration = 250; // Faster movement
animationDuration = 100; // Quicker animation
} else if (self.personality === 'cautious') {
// Cautious AI: moderate but calculated movements
maxDistance = self.riskLevel > 5 ? 100 : 160;
moveDuration = 380;
}
if (distance > maxDistance) {
// Scale down the movement to fit within max distance
var scale = maxDistance / distance;
x = self.x + dx * scale;
y = self.y + dy * scale;
}
self.targetX = x;
self.targetY = y;
self.isMoving = true;
// Personality-based animation variations
var scaleVariation = 0.9;
var rotationVariation = 0.1;
if (self.personality === 'fast') {
scaleVariation = 0.8; // More dramatic scaling
rotationVariation = 0.15;
} else if (self.personality === 'cowardly') {
scaleVariation = 0.95; // Less dramatic scaling
rotationVariation = 0.05;
}
// Add walking animation
tween(playerGraphics, {
scaleX: scaleVariation,
scaleY: 1.1,
rotation: rotationVariation
}, {
duration: animationDuration,
easing: tween.easeInOut
});
tween(self, {
x: x,
y: y
}, {
duration: moveDuration,
easing: tween.easeOut,
onFinish: function onFinish() {
self.isMoving = false;
// Reset player graphics to normal state after movement
tween(playerGraphics, {
scaleX: 1,
scaleY: 1,
rotation: 0
}, {
duration: animationDuration,
easing: tween.easeOut
});
}
});
};
self.takeDamage = function () {
self.health--;
LK.effects.flashObject(self, 0xFF0000, 500);
LK.getSound('caught').play();
if (self.health <= 0) {
// AI player is eliminated but game continues
self.visible = false;
self.isMoving = false;
}
};
self.update = function () {
// Update personality-specific timers and states
self.safetyTimer++;
if (self.personality === 'cowardly' && self.health < 3) {
self.panicMode = true;
}
// Only move if alive and singer is facing away
if (self.health > 0 && !self.isMoving && singer.facingAway) {
self.moveTimer++;
// Personality-based timing adjustments
var moveThreshold = self.nextMoveTime;
if (self.personality === 'cowardly') {
// Cowardly AI waits longer and is more hesitant
moveThreshold = self.panicMode ? self.nextMoveTime * 0.7 : self.nextMoveTime * 1.5;
} else if (self.personality === 'fast') {
// Fast AI moves more frequently
moveThreshold = self.nextMoveTime * 0.6;
} else if (self.personality === 'cautious') {
// Cautious AI timing depends on risk level
moveThreshold = self.nextMoveTime * (1 + self.riskLevel * 0.1);
}
if (self.moveTimer >= moveThreshold) {
var targetX, targetY;
var validMove = false;
var attempts = 0;
var maxAttempts = 10;
while (!validMove && attempts < maxAttempts) {
if (self.personality === 'cowardly') {
// Cowardly AI: small, safe movements, prefers staying back
if (self.panicMode) {
// In panic mode, try to move away from singer
targetX = self.x + (Math.random() - 0.5) * 150;
targetY = self.y + Math.random() * 100 + 20; // Move away from win line
} else {
// Normal cowardly behavior: very small forward movements
targetX = self.x + (Math.random() - 0.5) * 200;
targetY = self.y - Math.random() * 80 - 20;
}
} else if (self.personality === 'fast') {
// Fast AI: aggressive movements towards win line
targetX = self.x + (Math.random() - 0.5) * 400;
targetY = self.y - Math.random() * 200 - 80;
} else if (self.personality === 'cautious') {
// Cautious AI: calculated movements, avoids risky areas
var forwardness = Math.max(50, 120 - self.riskLevel * 15);
targetX = self.x + (Math.random() - 0.5) * 250;
targetY = self.y - Math.random() * forwardness - 30;
}
// Keep within bounds
targetX = Math.max(200, Math.min(1848, targetX));
targetY = Math.max(500, targetY);
// Enhanced obstacle avoidance with personality considerations
validMove = true;
var obstacleRiskCount = 0;
for (var i = 0; i < obstacles.length; i++) {
var obstacle = obstacles[i];
var dx = targetX - obstacle.x;
var dy = targetY - obstacle.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Personality-based safety margins
var safetyMargin = 100;
if (self.personality === 'cowardly') {
safetyMargin = 150; // Larger safety margin
} else if (self.personality === 'fast') {
safetyMargin = 80; // Smaller safety margin, more risk-taking
} else if (self.personality === 'cautious') {
safetyMargin = 120; // Moderate safety margin
}
if (distance < safetyMargin) {
if (self.personality === 'cautious') {
obstacleRiskCount++;
}
validMove = false;
break;
}
}
// Update risk level for cautious AI
if (self.personality === 'cautious') {
self.riskLevel = Math.max(0, self.riskLevel + obstacleRiskCount - 1);
if (self.safetyTimer > 300) {
// Reset risk after 5 seconds of safety
self.riskLevel = Math.max(0, self.riskLevel - 1);
self.safetyTimer = 0;
}
}
attempts++;
}
if (validMove) {
self.moveTo(targetX, targetY);
if (self.personality === 'cautious') {
self.safetyTimer = 0; // Reset safety timer on movement
}
}
self.moveTimer = 0;
// Personality-based next move timing
if (self.personality === 'cowardly') {
self.nextMoveTime = Math.random() * 180 + 120; // 2-5 seconds, more cautious
} else if (self.personality === 'fast') {
self.nextMoveTime = Math.random() * 60 + 30; // 0.5-1.5 seconds, very aggressive
} else if (self.personality === 'cautious') {
self.nextMoveTime = Math.random() * 120 + 80; // 1.3-3.3 seconds, measured
}
}
}
};
return self;
});
var Obstacle = Container.expand(function (assetId) {
var self = Container.call(this);
var obstacleGraphics = self.attachAsset(assetId || 'obstacle1', {
anchorX: 0.5,
anchorY: 0.5
});
// Add trigger point visualization
var triggerPoint = self.attachAsset('triggerPoint', {
anchorX: 0.5,
anchorY: 0.5
});
triggerPoint.alpha = 0; // Make it completely invisible
triggerPoint.x = 0; // Center it horizontally on the obstacle
triggerPoint.y = obstacleGraphics.height / 2 + 5; // Position 5 pixels below bottom center
// 30% chance this obstacle can melt
self.canMelt = Math.random() < 0.3;
// Random chance (50%) this obstacle can move the character
self.canMoveCharacter = Math.random() < 0.5;
// Set trigger point color to green for meltable obstacles
if (self.canMelt) {
triggerPoint.tint = 0x00ff00; // Green color for meltable obstacles
}
self.isPlayerOnTrigger = false;
self.lastPlayerOnTrigger = false;
self.triggerTimer = 0;
self.melted = false;
self.lastPlayerColliding = false;
// Store reference to trigger point for collision detection
self.triggerPoint = triggerPoint;
return self;
});
var Player = Container.expand(function () {
var self = Container.call(this);
var playerGraphics = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
// Apply blue tint to main player to differentiate from AI players
playerGraphics.tint = 0x0000FF; // Blue color for main player
// Add number display for main player showing 456
var numberText = new Text2('456', {
size: 40,
fill: 0x000000 // Black text
});
numberText.anchor.set(0.5, 0.5);
numberText.x = 0;
numberText.y = 0;
self.addChild(numberText);
self.targetX = 0;
self.targetY = 0;
self.isMoving = false;
self.health = 3;
self.moveTo = function (x, y) {
// Calculate distance from current position
var dx = x - self.x;
var dy = y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Limit maximum movement distance to 200 pixels
var maxDistance = 200;
if (distance > maxDistance) {
// Scale down the movement to fit within max distance
var scale = maxDistance / distance;
x = self.x + dx * scale;
y = self.y + dy * scale;
}
self.targetX = x;
self.targetY = y;
self.isMoving = true;
// Add walking animation - slight scaling and rotation
tween(playerGraphics, {
scaleX: 0.9,
scaleY: 1.1,
rotation: 0.1
}, {
duration: 150,
easing: tween.easeInOut
});
tween(self, {
x: x,
y: y
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
self.isMoving = false;
// Reset player graphics to normal state after movement
tween(playerGraphics, {
scaleX: 1,
scaleY: 1,
rotation: 0
}, {
duration: 150,
easing: tween.easeOut
});
}
});
};
self.takeDamage = function () {
self.health--;
LK.effects.flashObject(self, 0xFF0000, 500);
LK.getSound('caught').play();
if (self.health <= 0) {
LK.showGameOver();
}
};
return self;
});
var Singer = Container.expand(function () {
var self = Container.call(this);
var singerAwayGraphics = self.attachAsset('singerAway', {
anchorX: 0.5,
anchorY: 0.5
});
var singerLookingGraphics = self.attachAsset('singerLooking', {
anchorX: 0.5,
anchorY: 0.5
});
singerLookingGraphics.visible = false; // Start with looking image hidden
self.facingAway = true;
self.turnTimer = 0;
self.nextTurnTime = 180; // 3 seconds at 60fps
self.lookingTime = 0;
self.maxLookingTime = 120; // 2 seconds looking
self.update = function () {
self.turnTimer++;
if (self.facingAway) {
// Singer is facing away, count down to turn around
if (self.turnTimer >= self.nextTurnTime) {
self.turnAround();
}
} else {
// Singer is looking, count down to face away again
self.lookingTime++;
if (self.lookingTime >= self.maxLookingTime) {
self.faceAway();
}
}
};
self.turnAround = function () {
self.facingAway = false;
self.lookingTime = 0;
// Switch to looking image
singerAwayGraphics.visible = false;
singerLookingGraphics.visible = true;
LK.getSound('turn').play();
// Start playing looping music when looking up
LK.playMusic('singerLookingMusic');
// Check if player is moving and apply damage
if (player.isMoving) {
player.takeDamage();
}
};
self.faceAway = function () {
self.facingAway = true;
self.turnTimer = 0;
self.nextTurnTime = Math.random() * 180 + 120; // Random 2-5 seconds
// Switch to away image
singerAwayGraphics.visible = true;
singerLookingGraphics.visible = false;
// Stop music when facing away
LK.stopMusic();
// Play 'song' audio file on loop when facing away
LK.getSound('song').play();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x87CEEB
});
/****
* Game Code
****/
// Create win line at top
var winLine = game.addChild(LK.getAsset('winLine', {
anchorX: 0.5,
anchorY: 0.5
}));
winLine.x = 2048 / 2;
winLine.y = 450;
// Create singer at top
var singer = game.addChild(new Singer());
singer.x = 2048 / 2;
singer.y = 300;
// Start playing 'song' audio since singer starts facing away
LK.getSound('song').play();
// Create player at bottom
var player = game.addChild(new Player());
player.x = 2048 / 2;
player.y = 2400;
player.targetX = player.x;
player.targetY = player.y;
// Create 3 AI players for competition
var aiPlayers = [];
var startPositions = [{
x: 2048 / 2 - 300,
y: 2400
},
// Left of main player
{
x: 2048 / 2 + 300,
y: 2400
},
// Right of main player
{
x: 2048 / 2,
y: 2350
} // Behind main player
];
for (var ai = 0; ai < 3; ai++) {
var aiPlayer = game.addChild(new AIPlayer(ai));
aiPlayer.x = startPositions[ai].x;
aiPlayer.y = startPositions[ai].y;
aiPlayer.targetX = aiPlayer.x;
aiPlayer.targetY = aiPlayer.y;
aiPlayers.push(aiPlayer);
}
// Create obstacles
var obstacles = [];
// Generate 20 obstacles using different assets
var obstacleAssets = ['obstacle1', 'obstacle2', 'obstacle3', 'obstacle4', 'obstacle5', 'obstacle6', 'obstacle7', 'obstacle8', 'obstacle9', 'obstacle10', 'obstacle11', 'obstacle12', 'obstacle13', 'obstacle14', 'obstacle15', 'obstacle16', 'obstacle17', 'obstacle18', 'obstacle19', 'obstacle20'];
// Function to check if a position is too close to existing obstacles
function isPositionValid(x, y, minDistance) {
for (var j = 0; j < obstacles.length; j++) {
var existingObstacle = obstacles[j];
var dx = x - existingObstacle.x;
var dy = y - existingObstacle.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < minDistance) {
return false;
}
}
return true;
}
for (var i = 0; i < 20; i++) {
var obstacle = game.addChild(new Obstacle(obstacleAssets[i]));
var validPosition = false;
var attempts = 0;
var maxAttempts = 100; // Prevent infinite loops
var minSpacing = 250; // Minimum distance between obstacles
// Try to find a valid position
while (!validPosition && attempts < maxAttempts) {
// Random x position between 200 and 1800 (avoiding edges)
var newX = Math.random() * 1600 + 200;
// Random y position between 600 and 2200 (between singer area and player start)
var newY = Math.random() * 1600 + 600;
if (isPositionValid(newX, newY, minSpacing)) {
obstacle.x = newX;
obstacle.y = newY;
validPosition = true;
}
attempts++;
}
// If we couldn't find a valid position after max attempts, place it anyway
// but try to space it out from the center
if (!validPosition) {
obstacle.x = Math.random() * 1600 + 200;
obstacle.y = Math.random() * 1600 + 600;
}
obstacles.push(obstacle);
}
// Create health display
var healthTxt = new Text2('Health: 3', {
size: 60,
fill: 0xFFFFFF
});
healthTxt.anchor.set(0, 0);
LK.gui.topRight.addChild(healthTxt);
// Create countdown timer display
var countdownTxt = new Text2('180', {
size: 80,
fill: 0xFFFFFF
});
countdownTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(countdownTxt);
// Game state
var pendingMove = null;
var moveDelay = 15; // 0.25 seconds at 60fps
var delayTimer = 0;
// Countdown timer state
var countdownSeconds = 180; // 3 minutes = 180 seconds
var countdownTimer = 0;
var countdownFramesPerSecond = 60; // 60 frames per second
// Touch input with delay
game.down = function (x, y, obj) {
// Only allow movement if not already moving and singer is facing away
if (!player.isMoving && singer.facingAway) {
pendingMove = {
x: x,
y: y
};
delayTimer = moveDelay;
}
};
// Track game over state
var gameEnded = false;
var lastGameEndedState = false;
game.update = function () {
// Handle delayed movement
if (pendingMove && delayTimer > 0) {
delayTimer--;
if (delayTimer <= 0) {
// Check if target position is valid (not inside obstacles)
var validMove = true;
for (var i = 0; i < obstacles.length; i++) {
var obstacle = obstacles[i];
var dx = pendingMove.x - obstacle.x;
var dy = pendingMove.y - obstacle.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 100) {
// Collision detection
validMove = false;
break;
}
}
if (validMove) {
player.moveTo(pendingMove.x, pendingMove.y);
}
pendingMove = null;
}
}
// Update health display
healthTxt.setText('Health: ' + player.health);
// Update countdown timer
countdownTimer++;
if (countdownTimer >= countdownFramesPerSecond) {
countdownTimer = 0;
countdownSeconds--;
countdownTxt.setText(countdownSeconds.toString());
// Change color when time is running low
if (countdownSeconds <= 10) {
countdownTxt.fill = 0xFF0000; // Red color for urgency
} else if (countdownSeconds <= 20) {
countdownTxt.fill = 0xFFFF00; // Yellow color for warning
}
// Game over when countdown reaches 0
if (countdownSeconds <= 0) {
LK.showGameOver();
}
}
// Check for game end state transitions
var currentGameEnded = player.health <= 0 || player.y <= winLine.y + 50;
if (!lastGameEndedState && currentGameEnded) {
// Game just ended, spawn random obstacles
for (var j = 0; j < 8; j++) {
var randomAssetIndex = Math.floor(Math.random() * obstacleAssets.length);
var newObstacle = game.addChild(new Obstacle(obstacleAssets[randomAssetIndex]));
newObstacle.x = Math.random() * 1800 + 200; // Random x between 200 and 2000
newObstacle.y = Math.random() * 1800 + 600; // Random y between 600 and 2400
obstacles.push(newObstacle);
}
}
lastGameEndedState = currentGameEnded;
// Check win condition
if (player.y <= winLine.y + 50) {
LK.showYouWin();
}
// Check obstacle collisions
for (var k = 0; k < obstacles.length; k++) {
var obstacle = obstacles[k];
// Check if player is colliding with obstacle
var obstacleDx = player.x - obstacle.x;
var obstacleDy = player.y - obstacle.y;
var obstacleDistance = Math.sqrt(obstacleDx * obstacleDx + obstacleDy * obstacleDy);
var isPlayerCollidingWithObstacle = obstacleDistance < 100;
// Check for obstacles that can move the character
if (obstacle.canMoveCharacter && !obstacle.melted) {
// Check for collision transition (just started colliding)
if (!obstacle.lastPlayerColliding && isPlayerCollidingWithObstacle) {
// Move player to a random nearby position
var randomAngle = Math.random() * Math.PI * 2;
var moveDistance = 150 + Math.random() * 100; // Random distance between 150-250
var newX = player.x + Math.cos(randomAngle) * moveDistance;
var newY = player.y + Math.sin(randomAngle) * moveDistance;
// Keep player within game bounds
newX = Math.max(100, Math.min(1948, newX));
newY = Math.max(500, Math.min(2600, newY));
// Use tween to move player smoothly
tween(player, {
x: newX,
y: newY
}, {
duration: 400,
easing: tween.easeOut
});
}
obstacle.lastPlayerColliding = isPlayerCollidingWithObstacle;
}
// Check trigger point collisions for melting obstacles
if (obstacle.canMelt && !obstacle.melted) {
// Check if player is on trigger point
var triggerX = obstacle.x + obstacle.triggerPoint.x;
var triggerY = obstacle.y + obstacle.triggerPoint.y;
var dx = player.x - triggerX;
var dy = player.y - triggerY;
var distance = Math.sqrt(dx * dx + dy * dy);
obstacle.isPlayerOnTrigger = distance < 75; // Half of trigger point size
// Check for transition from not on trigger to on trigger
if (!obstacle.lastPlayerOnTrigger && obstacle.isPlayerOnTrigger) {
obstacle.triggerTimer = 0; // Reset timer when player enters trigger
}
// If player is on trigger, increment timer
if (obstacle.isPlayerOnTrigger) {
obstacle.triggerTimer++;
// If player has been on trigger for 2 seconds (120 frames at 60fps)
if (obstacle.triggerTimer >= 120) {
// Start melting effect
obstacle.melted = true;
// Melting animation - scale down and fade out
tween(obstacle, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Remove from obstacles array and destroy
var index = obstacles.indexOf(obstacle);
if (index > -1) {
obstacles.splice(index, 1);
}
obstacle.destroy();
}
});
}
} else {
obstacle.triggerTimer = 0; // Reset timer if player leaves trigger
}
obstacle.lastPlayerOnTrigger = obstacle.isPlayerOnTrigger;
}
}
// Update AI players
for (var ai = 0; ai < aiPlayers.length; ai++) {
var aiPlayer = aiPlayers[ai];
if (aiPlayer.health > 0) {
aiPlayer.update();
// Check AI player obstacle collisions
for (var m = 0; m < obstacles.length; m++) {
var obstacle = obstacles[m];
var aiObstacleDx = aiPlayer.x - obstacle.x;
var aiObstacleDy = aiPlayer.y - obstacle.y;
var aiObstacleDistance = Math.sqrt(aiObstacleDx * aiObstacleDx + aiObstacleDy * aiObstacleDy);
var isAICollidingWithObstacle = aiObstacleDistance < 100;
// Check for obstacles that can move the AI character
if (obstacle.canMoveCharacter && !obstacle.melted) {
if (!obstacle['lastAIColliding' + ai] && isAICollidingWithObstacle) {
// Move AI player to a random nearby position
var randomAngle = Math.random() * Math.PI * 2;
var moveDistance = 150 + Math.random() * 100;
var newX = aiPlayer.x + Math.cos(randomAngle) * moveDistance;
var newY = aiPlayer.y + Math.sin(randomAngle) * moveDistance;
// Keep AI player within game bounds
newX = Math.max(100, Math.min(1948, newX));
newY = Math.max(500, Math.min(2600, newY));
tween(aiPlayer, {
x: newX,
y: newY
}, {
duration: 400,
easing: tween.easeOut
});
}
obstacle['lastAIColliding' + ai] = isAICollidingWithObstacle;
}
}
// Check if AI player wins
if (aiPlayer.y <= winLine.y + 50) {
LK.showGameOver(); // AI player wins, human player loses
}
}
}
// Check if player is caught moving when singer turns
if (!singer.facingAway && player.isMoving) {
// This is handled in the singer's turnAround method
// but we also check continuously in case of timing issues
if (LK.ticks % 30 === 0) {
// Check every half second
player.takeDamage();
}
}
// Check if AI players are caught moving when singer turns
if (!singer.facingAway) {
for (var aiIndex = 0; aiIndex < aiPlayers.length; aiIndex++) {
var aiPlayer = aiPlayers[aiIndex];
if (aiPlayer.isMoving && aiPlayer.health > 0) {
if (LK.ticks % 30 === 0) {
// Check every half second
aiPlayer.takeDamage();
}
}
}
}
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
var AIPlayer = Container.expand(function (playerIndex) {
var self = Container.call(this);
var playerGraphics = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
// Different tints for each AI player
var playerColors = [0xFFFF00, 0xFF00FF, 0x00FFFF]; // Yellow, Magenta, Cyan
playerGraphics.tint = playerColors[playerIndex % 3];
// Add number display for each AI player with different numbers (not 456)
var playerNumbers = ['123', '789', '321']; // Different numbers for each AI player
var numberText = new Text2(playerNumbers[playerIndex], {
size: 40,
fill: 0x000000 // Black text
});
numberText.anchor.set(0.5, 0.5);
numberText.x = 0;
numberText.y = 0;
self.addChild(numberText);
self.targetX = 0;
self.targetY = 0;
self.isMoving = false;
self.health = 3;
self.moveTimer = 0;
self.nextMoveTime = Math.random() * 120 + 60; // Random 1-3 seconds
self.playerIndex = playerIndex;
// AI Personality traits
var personalityTypes = ['cowardly', 'fast', 'cautious'];
self.personality = personalityTypes[playerIndex % 3];
self.riskLevel = 0; // Track current risk assessment
self.safetyTimer = 0; // Time since last risky action
self.panicMode = false; // Emergency state for cowardly AI
self.moveTo = function (x, y) {
// Calculate distance from current position
var dx = x - self.x;
var dy = y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Personality-based movement adjustments
var maxDistance = 180;
var moveDuration = 350;
var animationDuration = 150;
if (self.personality === 'cowardly') {
// Cowardly AI: smaller, more cautious movements
maxDistance = self.panicMode ? 80 : 120;
moveDuration = 450; // Slower movement
} else if (self.personality === 'fast') {
// Fast AI: larger, quicker movements
maxDistance = 220;
moveDuration = 250; // Faster movement
animationDuration = 100; // Quicker animation
} else if (self.personality === 'cautious') {
// Cautious AI: moderate but calculated movements
maxDistance = self.riskLevel > 5 ? 100 : 160;
moveDuration = 380;
}
if (distance > maxDistance) {
// Scale down the movement to fit within max distance
var scale = maxDistance / distance;
x = self.x + dx * scale;
y = self.y + dy * scale;
}
self.targetX = x;
self.targetY = y;
self.isMoving = true;
// Personality-based animation variations
var scaleVariation = 0.9;
var rotationVariation = 0.1;
if (self.personality === 'fast') {
scaleVariation = 0.8; // More dramatic scaling
rotationVariation = 0.15;
} else if (self.personality === 'cowardly') {
scaleVariation = 0.95; // Less dramatic scaling
rotationVariation = 0.05;
}
// Add walking animation
tween(playerGraphics, {
scaleX: scaleVariation,
scaleY: 1.1,
rotation: rotationVariation
}, {
duration: animationDuration,
easing: tween.easeInOut
});
tween(self, {
x: x,
y: y
}, {
duration: moveDuration,
easing: tween.easeOut,
onFinish: function onFinish() {
self.isMoving = false;
// Reset player graphics to normal state after movement
tween(playerGraphics, {
scaleX: 1,
scaleY: 1,
rotation: 0
}, {
duration: animationDuration,
easing: tween.easeOut
});
}
});
};
self.takeDamage = function () {
self.health--;
LK.effects.flashObject(self, 0xFF0000, 500);
LK.getSound('caught').play();
if (self.health <= 0) {
// AI player is eliminated but game continues
self.visible = false;
self.isMoving = false;
}
};
self.update = function () {
// Update personality-specific timers and states
self.safetyTimer++;
if (self.personality === 'cowardly' && self.health < 3) {
self.panicMode = true;
}
// Only move if alive and singer is facing away
if (self.health > 0 && !self.isMoving && singer.facingAway) {
self.moveTimer++;
// Personality-based timing adjustments
var moveThreshold = self.nextMoveTime;
if (self.personality === 'cowardly') {
// Cowardly AI waits longer and is more hesitant
moveThreshold = self.panicMode ? self.nextMoveTime * 0.7 : self.nextMoveTime * 1.5;
} else if (self.personality === 'fast') {
// Fast AI moves more frequently
moveThreshold = self.nextMoveTime * 0.6;
} else if (self.personality === 'cautious') {
// Cautious AI timing depends on risk level
moveThreshold = self.nextMoveTime * (1 + self.riskLevel * 0.1);
}
if (self.moveTimer >= moveThreshold) {
var targetX, targetY;
var validMove = false;
var attempts = 0;
var maxAttempts = 10;
while (!validMove && attempts < maxAttempts) {
if (self.personality === 'cowardly') {
// Cowardly AI: small, safe movements, prefers staying back
if (self.panicMode) {
// In panic mode, try to move away from singer
targetX = self.x + (Math.random() - 0.5) * 150;
targetY = self.y + Math.random() * 100 + 20; // Move away from win line
} else {
// Normal cowardly behavior: very small forward movements
targetX = self.x + (Math.random() - 0.5) * 200;
targetY = self.y - Math.random() * 80 - 20;
}
} else if (self.personality === 'fast') {
// Fast AI: aggressive movements towards win line
targetX = self.x + (Math.random() - 0.5) * 400;
targetY = self.y - Math.random() * 200 - 80;
} else if (self.personality === 'cautious') {
// Cautious AI: calculated movements, avoids risky areas
var forwardness = Math.max(50, 120 - self.riskLevel * 15);
targetX = self.x + (Math.random() - 0.5) * 250;
targetY = self.y - Math.random() * forwardness - 30;
}
// Keep within bounds
targetX = Math.max(200, Math.min(1848, targetX));
targetY = Math.max(500, targetY);
// Enhanced obstacle avoidance with personality considerations
validMove = true;
var obstacleRiskCount = 0;
for (var i = 0; i < obstacles.length; i++) {
var obstacle = obstacles[i];
var dx = targetX - obstacle.x;
var dy = targetY - obstacle.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Personality-based safety margins
var safetyMargin = 100;
if (self.personality === 'cowardly') {
safetyMargin = 150; // Larger safety margin
} else if (self.personality === 'fast') {
safetyMargin = 80; // Smaller safety margin, more risk-taking
} else if (self.personality === 'cautious') {
safetyMargin = 120; // Moderate safety margin
}
if (distance < safetyMargin) {
if (self.personality === 'cautious') {
obstacleRiskCount++;
}
validMove = false;
break;
}
}
// Update risk level for cautious AI
if (self.personality === 'cautious') {
self.riskLevel = Math.max(0, self.riskLevel + obstacleRiskCount - 1);
if (self.safetyTimer > 300) {
// Reset risk after 5 seconds of safety
self.riskLevel = Math.max(0, self.riskLevel - 1);
self.safetyTimer = 0;
}
}
attempts++;
}
if (validMove) {
self.moveTo(targetX, targetY);
if (self.personality === 'cautious') {
self.safetyTimer = 0; // Reset safety timer on movement
}
}
self.moveTimer = 0;
// Personality-based next move timing
if (self.personality === 'cowardly') {
self.nextMoveTime = Math.random() * 180 + 120; // 2-5 seconds, more cautious
} else if (self.personality === 'fast') {
self.nextMoveTime = Math.random() * 60 + 30; // 0.5-1.5 seconds, very aggressive
} else if (self.personality === 'cautious') {
self.nextMoveTime = Math.random() * 120 + 80; // 1.3-3.3 seconds, measured
}
}
}
};
return self;
});
var Obstacle = Container.expand(function (assetId) {
var self = Container.call(this);
var obstacleGraphics = self.attachAsset(assetId || 'obstacle1', {
anchorX: 0.5,
anchorY: 0.5
});
// Add trigger point visualization
var triggerPoint = self.attachAsset('triggerPoint', {
anchorX: 0.5,
anchorY: 0.5
});
triggerPoint.alpha = 0; // Make it completely invisible
triggerPoint.x = 0; // Center it horizontally on the obstacle
triggerPoint.y = obstacleGraphics.height / 2 + 5; // Position 5 pixels below bottom center
// 30% chance this obstacle can melt
self.canMelt = Math.random() < 0.3;
// Random chance (50%) this obstacle can move the character
self.canMoveCharacter = Math.random() < 0.5;
// Set trigger point color to green for meltable obstacles
if (self.canMelt) {
triggerPoint.tint = 0x00ff00; // Green color for meltable obstacles
}
self.isPlayerOnTrigger = false;
self.lastPlayerOnTrigger = false;
self.triggerTimer = 0;
self.melted = false;
self.lastPlayerColliding = false;
// Store reference to trigger point for collision detection
self.triggerPoint = triggerPoint;
return self;
});
var Player = Container.expand(function () {
var self = Container.call(this);
var playerGraphics = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
// Apply blue tint to main player to differentiate from AI players
playerGraphics.tint = 0x0000FF; // Blue color for main player
// Add number display for main player showing 456
var numberText = new Text2('456', {
size: 40,
fill: 0x000000 // Black text
});
numberText.anchor.set(0.5, 0.5);
numberText.x = 0;
numberText.y = 0;
self.addChild(numberText);
self.targetX = 0;
self.targetY = 0;
self.isMoving = false;
self.health = 3;
self.moveTo = function (x, y) {
// Calculate distance from current position
var dx = x - self.x;
var dy = y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// Limit maximum movement distance to 200 pixels
var maxDistance = 200;
if (distance > maxDistance) {
// Scale down the movement to fit within max distance
var scale = maxDistance / distance;
x = self.x + dx * scale;
y = self.y + dy * scale;
}
self.targetX = x;
self.targetY = y;
self.isMoving = true;
// Add walking animation - slight scaling and rotation
tween(playerGraphics, {
scaleX: 0.9,
scaleY: 1.1,
rotation: 0.1
}, {
duration: 150,
easing: tween.easeInOut
});
tween(self, {
x: x,
y: y
}, {
duration: 300,
easing: tween.easeOut,
onFinish: function onFinish() {
self.isMoving = false;
// Reset player graphics to normal state after movement
tween(playerGraphics, {
scaleX: 1,
scaleY: 1,
rotation: 0
}, {
duration: 150,
easing: tween.easeOut
});
}
});
};
self.takeDamage = function () {
self.health--;
LK.effects.flashObject(self, 0xFF0000, 500);
LK.getSound('caught').play();
if (self.health <= 0) {
LK.showGameOver();
}
};
return self;
});
var Singer = Container.expand(function () {
var self = Container.call(this);
var singerAwayGraphics = self.attachAsset('singerAway', {
anchorX: 0.5,
anchorY: 0.5
});
var singerLookingGraphics = self.attachAsset('singerLooking', {
anchorX: 0.5,
anchorY: 0.5
});
singerLookingGraphics.visible = false; // Start with looking image hidden
self.facingAway = true;
self.turnTimer = 0;
self.nextTurnTime = 180; // 3 seconds at 60fps
self.lookingTime = 0;
self.maxLookingTime = 120; // 2 seconds looking
self.update = function () {
self.turnTimer++;
if (self.facingAway) {
// Singer is facing away, count down to turn around
if (self.turnTimer >= self.nextTurnTime) {
self.turnAround();
}
} else {
// Singer is looking, count down to face away again
self.lookingTime++;
if (self.lookingTime >= self.maxLookingTime) {
self.faceAway();
}
}
};
self.turnAround = function () {
self.facingAway = false;
self.lookingTime = 0;
// Switch to looking image
singerAwayGraphics.visible = false;
singerLookingGraphics.visible = true;
LK.getSound('turn').play();
// Start playing looping music when looking up
LK.playMusic('singerLookingMusic');
// Check if player is moving and apply damage
if (player.isMoving) {
player.takeDamage();
}
};
self.faceAway = function () {
self.facingAway = true;
self.turnTimer = 0;
self.nextTurnTime = Math.random() * 180 + 120; // Random 2-5 seconds
// Switch to away image
singerAwayGraphics.visible = true;
singerLookingGraphics.visible = false;
// Stop music when facing away
LK.stopMusic();
// Play 'song' audio file on loop when facing away
LK.getSound('song').play();
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x87CEEB
});
/****
* Game Code
****/
// Create win line at top
var winLine = game.addChild(LK.getAsset('winLine', {
anchorX: 0.5,
anchorY: 0.5
}));
winLine.x = 2048 / 2;
winLine.y = 450;
// Create singer at top
var singer = game.addChild(new Singer());
singer.x = 2048 / 2;
singer.y = 300;
// Start playing 'song' audio since singer starts facing away
LK.getSound('song').play();
// Create player at bottom
var player = game.addChild(new Player());
player.x = 2048 / 2;
player.y = 2400;
player.targetX = player.x;
player.targetY = player.y;
// Create 3 AI players for competition
var aiPlayers = [];
var startPositions = [{
x: 2048 / 2 - 300,
y: 2400
},
// Left of main player
{
x: 2048 / 2 + 300,
y: 2400
},
// Right of main player
{
x: 2048 / 2,
y: 2350
} // Behind main player
];
for (var ai = 0; ai < 3; ai++) {
var aiPlayer = game.addChild(new AIPlayer(ai));
aiPlayer.x = startPositions[ai].x;
aiPlayer.y = startPositions[ai].y;
aiPlayer.targetX = aiPlayer.x;
aiPlayer.targetY = aiPlayer.y;
aiPlayers.push(aiPlayer);
}
// Create obstacles
var obstacles = [];
// Generate 20 obstacles using different assets
var obstacleAssets = ['obstacle1', 'obstacle2', 'obstacle3', 'obstacle4', 'obstacle5', 'obstacle6', 'obstacle7', 'obstacle8', 'obstacle9', 'obstacle10', 'obstacle11', 'obstacle12', 'obstacle13', 'obstacle14', 'obstacle15', 'obstacle16', 'obstacle17', 'obstacle18', 'obstacle19', 'obstacle20'];
// Function to check if a position is too close to existing obstacles
function isPositionValid(x, y, minDistance) {
for (var j = 0; j < obstacles.length; j++) {
var existingObstacle = obstacles[j];
var dx = x - existingObstacle.x;
var dy = y - existingObstacle.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < minDistance) {
return false;
}
}
return true;
}
for (var i = 0; i < 20; i++) {
var obstacle = game.addChild(new Obstacle(obstacleAssets[i]));
var validPosition = false;
var attempts = 0;
var maxAttempts = 100; // Prevent infinite loops
var minSpacing = 250; // Minimum distance between obstacles
// Try to find a valid position
while (!validPosition && attempts < maxAttempts) {
// Random x position between 200 and 1800 (avoiding edges)
var newX = Math.random() * 1600 + 200;
// Random y position between 600 and 2200 (between singer area and player start)
var newY = Math.random() * 1600 + 600;
if (isPositionValid(newX, newY, minSpacing)) {
obstacle.x = newX;
obstacle.y = newY;
validPosition = true;
}
attempts++;
}
// If we couldn't find a valid position after max attempts, place it anyway
// but try to space it out from the center
if (!validPosition) {
obstacle.x = Math.random() * 1600 + 200;
obstacle.y = Math.random() * 1600 + 600;
}
obstacles.push(obstacle);
}
// Create health display
var healthTxt = new Text2('Health: 3', {
size: 60,
fill: 0xFFFFFF
});
healthTxt.anchor.set(0, 0);
LK.gui.topRight.addChild(healthTxt);
// Create countdown timer display
var countdownTxt = new Text2('180', {
size: 80,
fill: 0xFFFFFF
});
countdownTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(countdownTxt);
// Game state
var pendingMove = null;
var moveDelay = 15; // 0.25 seconds at 60fps
var delayTimer = 0;
// Countdown timer state
var countdownSeconds = 180; // 3 minutes = 180 seconds
var countdownTimer = 0;
var countdownFramesPerSecond = 60; // 60 frames per second
// Touch input with delay
game.down = function (x, y, obj) {
// Only allow movement if not already moving and singer is facing away
if (!player.isMoving && singer.facingAway) {
pendingMove = {
x: x,
y: y
};
delayTimer = moveDelay;
}
};
// Track game over state
var gameEnded = false;
var lastGameEndedState = false;
game.update = function () {
// Handle delayed movement
if (pendingMove && delayTimer > 0) {
delayTimer--;
if (delayTimer <= 0) {
// Check if target position is valid (not inside obstacles)
var validMove = true;
for (var i = 0; i < obstacles.length; i++) {
var obstacle = obstacles[i];
var dx = pendingMove.x - obstacle.x;
var dy = pendingMove.y - obstacle.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance < 100) {
// Collision detection
validMove = false;
break;
}
}
if (validMove) {
player.moveTo(pendingMove.x, pendingMove.y);
}
pendingMove = null;
}
}
// Update health display
healthTxt.setText('Health: ' + player.health);
// Update countdown timer
countdownTimer++;
if (countdownTimer >= countdownFramesPerSecond) {
countdownTimer = 0;
countdownSeconds--;
countdownTxt.setText(countdownSeconds.toString());
// Change color when time is running low
if (countdownSeconds <= 10) {
countdownTxt.fill = 0xFF0000; // Red color for urgency
} else if (countdownSeconds <= 20) {
countdownTxt.fill = 0xFFFF00; // Yellow color for warning
}
// Game over when countdown reaches 0
if (countdownSeconds <= 0) {
LK.showGameOver();
}
}
// Check for game end state transitions
var currentGameEnded = player.health <= 0 || player.y <= winLine.y + 50;
if (!lastGameEndedState && currentGameEnded) {
// Game just ended, spawn random obstacles
for (var j = 0; j < 8; j++) {
var randomAssetIndex = Math.floor(Math.random() * obstacleAssets.length);
var newObstacle = game.addChild(new Obstacle(obstacleAssets[randomAssetIndex]));
newObstacle.x = Math.random() * 1800 + 200; // Random x between 200 and 2000
newObstacle.y = Math.random() * 1800 + 600; // Random y between 600 and 2400
obstacles.push(newObstacle);
}
}
lastGameEndedState = currentGameEnded;
// Check win condition
if (player.y <= winLine.y + 50) {
LK.showYouWin();
}
// Check obstacle collisions
for (var k = 0; k < obstacles.length; k++) {
var obstacle = obstacles[k];
// Check if player is colliding with obstacle
var obstacleDx = player.x - obstacle.x;
var obstacleDy = player.y - obstacle.y;
var obstacleDistance = Math.sqrt(obstacleDx * obstacleDx + obstacleDy * obstacleDy);
var isPlayerCollidingWithObstacle = obstacleDistance < 100;
// Check for obstacles that can move the character
if (obstacle.canMoveCharacter && !obstacle.melted) {
// Check for collision transition (just started colliding)
if (!obstacle.lastPlayerColliding && isPlayerCollidingWithObstacle) {
// Move player to a random nearby position
var randomAngle = Math.random() * Math.PI * 2;
var moveDistance = 150 + Math.random() * 100; // Random distance between 150-250
var newX = player.x + Math.cos(randomAngle) * moveDistance;
var newY = player.y + Math.sin(randomAngle) * moveDistance;
// Keep player within game bounds
newX = Math.max(100, Math.min(1948, newX));
newY = Math.max(500, Math.min(2600, newY));
// Use tween to move player smoothly
tween(player, {
x: newX,
y: newY
}, {
duration: 400,
easing: tween.easeOut
});
}
obstacle.lastPlayerColliding = isPlayerCollidingWithObstacle;
}
// Check trigger point collisions for melting obstacles
if (obstacle.canMelt && !obstacle.melted) {
// Check if player is on trigger point
var triggerX = obstacle.x + obstacle.triggerPoint.x;
var triggerY = obstacle.y + obstacle.triggerPoint.y;
var dx = player.x - triggerX;
var dy = player.y - triggerY;
var distance = Math.sqrt(dx * dx + dy * dy);
obstacle.isPlayerOnTrigger = distance < 75; // Half of trigger point size
// Check for transition from not on trigger to on trigger
if (!obstacle.lastPlayerOnTrigger && obstacle.isPlayerOnTrigger) {
obstacle.triggerTimer = 0; // Reset timer when player enters trigger
}
// If player is on trigger, increment timer
if (obstacle.isPlayerOnTrigger) {
obstacle.triggerTimer++;
// If player has been on trigger for 2 seconds (120 frames at 60fps)
if (obstacle.triggerTimer >= 120) {
// Start melting effect
obstacle.melted = true;
// Melting animation - scale down and fade out
tween(obstacle, {
scaleX: 0,
scaleY: 0,
alpha: 0
}, {
duration: 1000,
easing: tween.easeInOut,
onFinish: function onFinish() {
// Remove from obstacles array and destroy
var index = obstacles.indexOf(obstacle);
if (index > -1) {
obstacles.splice(index, 1);
}
obstacle.destroy();
}
});
}
} else {
obstacle.triggerTimer = 0; // Reset timer if player leaves trigger
}
obstacle.lastPlayerOnTrigger = obstacle.isPlayerOnTrigger;
}
}
// Update AI players
for (var ai = 0; ai < aiPlayers.length; ai++) {
var aiPlayer = aiPlayers[ai];
if (aiPlayer.health > 0) {
aiPlayer.update();
// Check AI player obstacle collisions
for (var m = 0; m < obstacles.length; m++) {
var obstacle = obstacles[m];
var aiObstacleDx = aiPlayer.x - obstacle.x;
var aiObstacleDy = aiPlayer.y - obstacle.y;
var aiObstacleDistance = Math.sqrt(aiObstacleDx * aiObstacleDx + aiObstacleDy * aiObstacleDy);
var isAICollidingWithObstacle = aiObstacleDistance < 100;
// Check for obstacles that can move the AI character
if (obstacle.canMoveCharacter && !obstacle.melted) {
if (!obstacle['lastAIColliding' + ai] && isAICollidingWithObstacle) {
// Move AI player to a random nearby position
var randomAngle = Math.random() * Math.PI * 2;
var moveDistance = 150 + Math.random() * 100;
var newX = aiPlayer.x + Math.cos(randomAngle) * moveDistance;
var newY = aiPlayer.y + Math.sin(randomAngle) * moveDistance;
// Keep AI player within game bounds
newX = Math.max(100, Math.min(1948, newX));
newY = Math.max(500, Math.min(2600, newY));
tween(aiPlayer, {
x: newX,
y: newY
}, {
duration: 400,
easing: tween.easeOut
});
}
obstacle['lastAIColliding' + ai] = isAICollidingWithObstacle;
}
}
// Check if AI player wins
if (aiPlayer.y <= winLine.y + 50) {
LK.showGameOver(); // AI player wins, human player loses
}
}
}
// Check if player is caught moving when singer turns
if (!singer.facingAway && player.isMoving) {
// This is handled in the singer's turnAround method
// but we also check continuously in case of timing issues
if (LK.ticks % 30 === 0) {
// Check every half second
player.takeDamage();
}
}
// Check if AI players are caught moving when singer turns
if (!singer.facingAway) {
for (var aiIndex = 0; aiIndex < aiPlayers.length; aiIndex++) {
var aiPlayer = aiPlayers[aiIndex];
if (aiPlayer.isMoving && aiPlayer.health > 0) {
if (LK.ticks % 30 === 0) {
// Check every half second
aiPlayer.takeDamage();
}
}
}
}
};
evil little girl looking down. In-Game asset. 2d. High contrast. No shadows
downward looking devil little girl turned back
Cat with 456 written on its back. In-Game asset. 2d. High contrast. No shadows
fence. In-Game asset
hedgerow. In-Game asset
tree. In-Game asset
mud and stone. In-Game asset
mud and stone front view. In-Game asset
mold fungus. In-Game asset
robot monkey. In-Game asset
outgoing gas infrastructure system. In-Game asset
outgoing gasoline infrastructure system. In-Game asset