/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var ArrowIndicator = Container.expand(function (targetX, targetY, color, arrowType) {
var self = Container.call(this);
// Create arrow shape using dedicated arrow assets
var assetName = arrowType === 'pickup' ? 'pickupArrow' : 'deliveryArrow';
var arrowGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Make arrows larger
arrowGraphics.scaleX = 1.5;
arrowGraphics.scaleY = 1.8;
self.targetX = targetX;
self.targetY = targetY;
self.baseScaleX = 1.5;
self.baseScaleY = 1.8;
self.update = function () {
// Calculate direction to target
var dx = self.targetX - courier.x;
var dy = self.targetY - courier.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 100) {
// Position arrow closer to player
var angle = Math.atan2(dy, dx);
var edgeDistance = 180; // Reduced from 300 to make arrow closer
// Position relative to screen center (where courier appears)
self.x = 1024 + Math.cos(angle) * edgeDistance;
self.y = 1366 + Math.sin(angle) * edgeDistance;
// Rotate arrow to point toward target
arrowGraphics.rotation = angle;
self.visible = true;
// Heart beat animation - faster when closer to target
var maxDistance = 500; // Maximum distance for animation calculation
var normalizedDistance = Math.min(distance, maxDistance) / maxDistance;
// Animation speed: closer = faster heartbeat (lower period)
var animationSpeed = 0.02 + (1 - normalizedDistance) * 0.08; // 0.02 to 0.1
var heartbeat = 1 + 0.3 * Math.sin(LK.ticks * animationSpeed);
// Apply heartbeat animation
arrowGraphics.scaleX = self.baseScaleX * heartbeat;
arrowGraphics.scaleY = self.baseScaleY * heartbeat;
} else {
self.visible = false;
}
};
return self;
});
var Courier = Container.expand(function () {
var self = Container.call(this);
var courierGraphics = self.attachAsset('courierAsset', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 8;
self.hasOrder = false;
self.targetRestaurant = null;
self.targetCustomer = null;
self.moveToward = function (targetX, targetY) {
var dx = targetX - self.x;
var dy = targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 5) {
// Smooth movement with momentum for agar.io feel
var moveSpeed = Math.min(self.speed, distance * 0.15);
var newX = self.x + dx / distance * moveSpeed;
var newY = self.y + dy / distance * moveSpeed;
// Check collision with buildings
var canMove = true;
for (var i = 0; i < buildings.length; i++) {
var tempCourier = {
x: newX,
y: newY,
width: 9,
height: 9
};
var building = buildings[i];
if (tempCourier.x < building.x + building.width / 2 && tempCourier.x + tempCourier.width / 2 > building.x - building.width / 2 && tempCourier.y < building.y + building.height / 2 && tempCourier.y + tempCourier.height / 2 > building.y - building.height / 2) {
canMove = false;
break;
}
}
// Check collision with restaurants/shops
for (var i = 0; i < restaurants.length; i++) {
var tempCourier = {
x: newX,
y: newY,
width: 9,
height: 9
};
var restaurant = restaurants[i];
if (tempCourier.x < restaurant.x + restaurant.width / 2 && tempCourier.x + tempCourier.width / 2 > restaurant.x - restaurant.width / 2 && tempCourier.y < restaurant.y + restaurant.height / 2 && tempCourier.y + tempCourier.height / 2 > restaurant.y - restaurant.height / 2) {
canMove = false;
break;
}
}
// Check collision with customers
for (var i = 0; i < customers.length; i++) {
var tempCourier = {
x: newX,
y: newY,
width: 9,
height: 9
};
var customer = customers[i];
if (tempCourier.x < customer.x + 1 / 2 && tempCourier.x + tempCourier.width / 2 > customer.x - 1 / 2 && tempCourier.y < customer.y + 1 / 2 && tempCourier.y + tempCourier.height / 2 > customer.y - 1 / 2) {
canMove = false;
break;
}
}
if (canMove) {
self.x = newX;
self.y = newY;
} else {
// Slide along building edges when collision occurs
var canMoveX = true;
var canMoveY = true;
// Test horizontal movement only
for (var i = 0; i < buildings.length; i++) {
var tempCourier = {
x: newX,
y: self.y,
width: 9,
height: 9
};
var building = buildings[i];
if (tempCourier.x < building.x + building.width / 2 && tempCourier.x + tempCourier.width / 2 > building.x - building.width / 2 && tempCourier.y < building.y + building.height / 2 && tempCourier.y + tempCourier.height / 2 > building.y - building.height / 2) {
canMoveX = false;
break;
}
}
// Test vertical movement only
for (var i = 0; i < buildings.length; i++) {
var tempCourier = {
x: self.x,
y: newY,
width: 9,
height: 9
};
var building = buildings[i];
if (tempCourier.x < building.x + building.width / 2 && tempCourier.x + tempCourier.width / 2 > building.x - building.width / 2 && tempCourier.y < building.y + building.height / 2 && tempCourier.y + tempCourier.height / 2 > building.y - building.height / 2) {
canMoveY = false;
break;
}
}
// Apply sliding movement - move in whichever direction is not blocked
if (canMoveX) {
self.x = newX;
}
if (canMoveY) {
self.y = newY;
}
// If both directions are blocked, try to slide with reduced movement using tween for smooth effect
if (!canMoveX && !canMoveY) {
var slideDistance = 15;
var slideX = self.x + (dx > 0 ? slideDistance : -slideDistance);
var slideY = self.y + (dy > 0 ? slideDistance : -slideDistance);
// Animate sliding motion
tween(self, {
x: slideX,
y: slideY
}, {
duration: 200,
easing: tween.easeOut
});
}
}
}
};
return self;
});
var Customer = Container.expand(function () {
var self = Container.call(this);
var customerGraphics = self.attachAsset('customer', {
anchorX: 0.5,
anchorY: 0.5
});
self.waitingForOrder = false;
self.orderTime = 0;
self.maxWaitTime = 10000; // 10 seconds
self.deliveryTarget = null;
self.orderColor = 0x2196f3; // Default blue
self.setOrderColor = function (color) {
self.orderColor = color;
customerGraphics.tint = color;
};
self.update = function () {
if (self.waitingForOrder) {
self.orderTime += 16.67; // Approximate ms per frame at 60fps
}
};
return self;
});
var NPCCourier = Container.expand(function () {
var self = Container.call(this);
var courierGraphics = self.attachAsset('courierAsset', {
anchorX: 0.5,
anchorY: 0.5
});
courierGraphics.tint = 0xff5722;
self.speed = 2;
self.detectionRadius = 20;
self.isMoving = false;
self.hasInsurance = false;
self.lastX = self.x;
self.lastY = self.y;
self.lastSeenPolice = false;
self.lastSeenVandal = false;
self.startRoaming = function () {
if (self.isMoving) return;
self.isMoving = true;
var newX = 200 + Math.random() * 1600;
var newY = 200 + Math.random() * 2300;
var dx = newX - self.x;
var dy = newY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var duration = distance * 50; // Adjust speed by changing multiplier
tween(self, {
x: newX,
y: newY
}, {
duration: duration,
easing: tween.linear,
onFinish: function onFinish() {
self.isMoving = false;
// Start next movement after a short pause
LK.setTimeout(function () {
self.startRoaming();
}, 500 + Math.random() * 1000);
}
});
};
self.update = function () {
// Start roaming if not already moving
if (!self.isMoving) {
self.startRoaming();
}
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
var NPCPolice = Container.expand(function () {
var self = Container.call(this);
var policeGraphics = self.attachAsset('policeAsset', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 3;
self.detectionRadius = 20;
self.target = null;
self.isMoving = false;
self.lastX = self.x;
self.lastY = self.y;
self.thinkTimer = 0;
self.startRoaming = function () {
if (self.isMoving) return;
self.isMoving = true;
var newX = 200 + Math.random() * 1600;
var newY = 200 + Math.random() * 2300;
var dx = newX - self.x;
var dy = newY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var duration = distance * 40; // Faster than couriers
tween(self, {
x: newX,
y: newY
}, {
duration: duration,
easing: tween.linear,
onFinish: function onFinish() {
self.isMoving = false;
// Start next movement after a short pause
LK.setTimeout(function () {
self.startRoaming();
}, 300 + Math.random() * 800);
}
});
};
self.update = function () {
self.thinkTimer += 16.67;
if (self.thinkTimer >= 100) {
self.thinkTimer = 0;
self.target = null;
// Look for vandals
for (var i = 0; i < npcs.length; i++) {
var npc = npcs[i];
if (npc.npcType === 'vandal') {
var dx = npc.x - self.x;
var dy = npc.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.detectionRadius) {
self.target = npc;
// Stop current movement and chase target
tween.stop(self, {
x: true,
y: true
});
self.isMoving = false;
break;
}
}
}
// Look for uninsured couriers
if (!self.target) {
for (var i = 0; i < npcs.length; i++) {
var npc = npcs[i];
if (npc.npcType === 'courier' && !npc.hasInsurance) {
var dx = npc.x - self.x;
var dy = npc.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.detectionRadius) {
self.target = npc;
// Stop current movement and chase target
tween.stop(self, {
x: true,
y: true
});
self.isMoving = false;
break;
}
}
}
}
// Check player courier if no insurance
if (!self.target && playerRole === 'courier' && courier && !hasInsurance) {
var dx = courier.x - self.x;
var dy = courier.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.detectionRadius) {
self.target = courier;
// Stop current movement and chase target
tween.stop(self, {
x: true,
y: true
});
self.isMoving = false;
}
}
}
// Move behavior
if (self.target) {
// Chase target with tween
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 5) {
tween.stop(self, {
x: true,
y: true
});
tween(self, {
x: self.target.x,
y: self.target.y
}, {
duration: distance * 30,
easing: tween.linear
});
}
} else {
// Start roaming if not already moving
if (!self.isMoving) {
self.startRoaming();
}
}
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
var NPCVandal = Container.expand(function () {
var self = Container.call(this);
var vandalGraphics = self.attachAsset('vandalAsset', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 2;
self.detectionRadius = 20;
self.fleeing = false;
self.isMoving = false;
self.lastX = self.x;
self.lastY = self.y;
self.thinkTimer = 0;
self.startRoaming = function () {
if (self.isMoving || self.fleeing) return;
self.isMoving = true;
var newX = 200 + Math.random() * 1600;
var newY = 200 + Math.random() * 2300;
var dx = newX - self.x;
var dy = newY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var duration = distance * 60; // Slower than police
tween(self, {
x: newX,
y: newY
}, {
duration: duration,
easing: tween.linear,
onFinish: function onFinish() {
self.isMoving = false;
// Start next movement after a short pause
LK.setTimeout(function () {
self.startRoaming();
}, 800 + Math.random() * 1200);
}
});
};
self.flee = function (fromX, fromY) {
self.fleeing = true;
tween.stop(self, {
x: true,
y: true
});
self.isMoving = false;
var dx = self.x - fromX;
var dy = self.y - fromY;
var fleeX = self.x + dx * 5;
var fleeY = self.y + dy * 5;
// Keep within bounds
fleeX = Math.max(100, Math.min(1900, fleeX));
fleeY = Math.max(200, Math.min(2500, fleeY));
tween(self, {
x: fleeX,
y: fleeY
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
self.fleeing = false;
LK.setTimeout(function () {
self.startRoaming();
}, 1000);
}
});
};
self.update = function () {
self.thinkTimer += 16.67;
if (self.thinkTimer >= 100) {
self.thinkTimer = 0;
// Check if close to player courier with package to steal it
if (playerRole === 'courier' && courier && courier.hasOrder) {
var dx = courier.x - self.x;
var dy = courier.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.detectionRadius) {
// Steal the package
courier.hasOrder = false;
if (currentOrder) {
currentOrder.destroy();
currentOrder = null;
gameState = 'pickup';
// Clean up arrows
if (deliveryArrow) {
deliveryArrow.destroy();
deliveryArrow = null;
}
}
// Play theft sound effects
if (soundEnabled) {
var soundChoice = Math.random();
if (soundChoice < 0.5) {
LK.getSound('vandal_laugh').play();
} else {
LK.getSound('crash').play();
}
}
// Flash courier red to show package stolen
LK.effects.flashObject(courier, 0xff0000, 1000);
// Create new order after delay
LK.setTimeout(function () {
createNewOrder();
}, 2000);
}
}
var shouldFlee = false;
// Look for police
for (var i = 0; i < npcs.length; i++) {
var npc = npcs[i];
if (npc.npcType === 'police') {
var dx = npc.x - self.x;
var dy = npc.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.detectionRadius) {
self.flee(npc.x, npc.y);
shouldFlee = true;
break;
}
}
}
// Check player police
if (!shouldFlee && playerRole === 'police' && courier) {
var dx = courier.x - self.x;
var dy = courier.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.detectionRadius) {
self.flee(courier.x, courier.y);
shouldFlee = true;
}
}
}
// Start roaming if not fleeing and not already moving
if (!self.fleeing && !self.isMoving) {
self.startRoaming();
}
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
var Order = Container.expand(function () {
var self = Container.call(this);
var orderGraphics = self.attachAsset('order', {
anchorX: 0.5,
anchorY: 0.5
});
self.restaurant = null;
self.customer = null;
self.baseTimeLimit = 25000; // 25 seconds minimum
self.timeLimit = self.baseTimeLimit;
self.timeRemaining = self.timeLimit;
self.isPickedUp = false;
self.courierSpeed = 8; // Default courier speed
self.setColor = function (color) {
orderGraphics.tint = color;
};
self.startDeliveryTimer = function (courierSpeed) {
self.isPickedUp = true;
self.courierSpeed = courierSpeed;
self.timeLimit = self.baseTimeLimit;
self.timeRemaining = self.timeLimit;
};
self.updateTimeLimit = function (courierX, courierY) {
if (!self.isPickedUp || !self.customer) {
return;
}
// Calculate distance to customer
var dx = self.customer.x - courierX;
var dy = self.customer.y - courierY;
var distance = Math.sqrt(dx * dx + dy * dy);
// Calculate time needed to reach customer at max speed + 2 seconds buffer
var timeToReach = distance / self.courierSpeed * 16.67 + 2000; // Convert to ms and add 2s buffer
// Use the greater of base time limit or calculated time
var newTimeLimit = Math.max(self.baseTimeLimit, timeToReach);
// Only extend time, never reduce it
if (newTimeLimit > self.timeLimit) {
var extension = newTimeLimit - self.timeLimit;
self.timeLimit = newTimeLimit;
self.timeRemaining += extension;
}
};
self.update = function () {
if (self.isPickedUp && self.timeRemaining > 0) {
self.timeRemaining -= 16.67;
}
};
return self;
});
var PowerUp = Container.expand(function (type) {
var self = Container.call(this);
var powerupGraphics = self.attachAsset(type === 'health' ? 'healthPowerup' : 'timePowerup', {
anchorX: 0.5,
anchorY: 0.5
});
self.type = type;
self.lifeTime = 0;
self.maxLifeTime = 8000; // 8 seconds
self.update = function () {
self.lifeTime += 16.67;
// Pulse animation
var pulse = 1 + 0.2 * Math.sin(self.lifeTime * 0.01);
powerupGraphics.scaleX = pulse;
powerupGraphics.scaleY = pulse;
};
return self;
});
var Restaurant = Container.expand(function () {
var self = Container.call(this);
var restaurantGraphics = self.attachAsset('restaurant', {
anchorX: 0.5,
anchorY: 0.5
});
self.hasOrder = false;
self.orderReady = false;
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x8BC34A
});
/****
* Game Code
****/
// Game variables
var courier;
var npcs = [];
var restaurants = [];
var customers = [];
var orders = [];
var aiOrders = [];
var powerUps = [];
var buildings = [];
var health = 100;
var money = 20;
var currentOrder = null;
var gameState = 'pickup'; // 'pickup', 'delivery'
var timeFreezeActive = false;
var timeFreezeRemaining = 0;
// Role system
var playerRole = storage.playerRole || null; // 'courier', 'police', 'vandal'
var hasInsurance = false;
var insuranceEndTime = 0;
var gameStarted = false;
// Camera system
var cameraX = 0;
var cameraY = 0;
var worldContainer;
var pickupArrow = null;
var deliveryArrow = null;
// God mode system
var godMode = false;
var godModeScale = 0.8; // Zoom out scale for full map view
var normalScale = 3; // Normal game scale
// Sound system
var isBeingChased = false;
var sirenSound = null;
var lastChasedState = false;
// Sound control system
var soundEnabled = storage.soundEnabled !== undefined ? storage.soundEnabled : true;
var musicEnabled = storage.musicEnabled !== undefined ? storage.musicEnabled : true;
// Order colors for variety
var orderColors = [0xffff00, 0xff4444, 0x44ff44, 0x4444ff, 0xff44ff, 0x44ffff, 0xffa500, 0x800080];
// UI Elements
var healthBar = new Text2('Health: 100', {
size: 48
});
healthBar.anchor.set(0, 0);
LK.gui.topLeft.addChild(healthBar);
healthBar.x = 120;
healthBar.y = 20;
var moneyText = new Text2('$0', {
size: 48
});
moneyText.anchor.set(1, 0);
LK.gui.topRight.addChild(moneyText);
moneyText.x = -20;
moneyText.y = 20;
var orderTimer = new Text2('', {
size: 42
});
orderTimer.anchor.set(0.5, 0);
LK.gui.top.addChild(orderTimer);
orderTimer.y = 100;
// Role selection UI
var roleSelectionContainer = new Container();
LK.gui.center.addChild(roleSelectionContainer);
var roleTitle = new Text2('Choose Your Role', {
size: 72,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 8
});
roleTitle.anchor.set(0.5, 0.5);
roleTitle.y = -200;
roleSelectionContainer.addChild(roleTitle);
// Create green background for courier button
var courierButtonBg = LK.getAsset('road', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 1
});
courierButtonBg.tint = 0x4caf50;
courierButtonBg.y = -50;
roleSelectionContainer.addChild(courierButtonBg);
var courierButton = new Text2('COURIER', {
size: 48,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 8
});
courierButton.anchor.set(0.5, 0.5);
courierButton.y = -50;
roleSelectionContainer.addChild(courierButton);
// Create blue background for police button
var policeButtonBg = LK.getAsset('road', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 1
});
policeButtonBg.tint = 0x2196f3;
policeButtonBg.y = 50;
roleSelectionContainer.addChild(policeButtonBg);
var policeButton = new Text2('POLICE', {
size: 48,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 8
});
policeButton.anchor.set(0.5, 0.5);
policeButton.y = 50;
roleSelectionContainer.addChild(policeButton);
// Create white background for vandal button
var vandalButtonBg = LK.getAsset('road', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 1
});
vandalButtonBg.tint = 0xFFFFFF;
vandalButtonBg.y = 150;
roleSelectionContainer.addChild(vandalButtonBg);
var vandalButton = new Text2('VANDAL', {
size: 48,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 8
});
vandalButton.anchor.set(0.5, 0.5);
vandalButton.y = 150;
roleSelectionContainer.addChild(vandalButton);
// Insurance UI (for couriers)
var insuranceButton = new Text2('Buy Insurance ($5)', {
size: 36
});
insuranceButton.anchor.set(0.5, 0);
LK.gui.top.addChild(insuranceButton);
insuranceButton.y = 160;
insuranceButton.visible = false;
var insuranceStatus = new Text2('', {
size: 32
});
insuranceStatus.anchor.set(0.5, 0);
LK.gui.top.addChild(insuranceStatus);
insuranceStatus.y = 210;
// God mode toggle button
var godModeButton = new Text2('God Mode: OFF', {
size: 36,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 4
});
godModeButton.anchor.set(1, 0);
LK.gui.topRight.addChild(godModeButton);
godModeButton.x = -20;
godModeButton.y = 80;
// Sound control buttons
var soundButton = new Text2('Sound: ON', {
size: 36,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 4
});
soundButton.anchor.set(1, 0);
LK.gui.topRight.addChild(soundButton);
soundButton.x = -20;
soundButton.y = 130;
var musicButton = new Text2('Music: ON', {
size: 36,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 4
});
musicButton.anchor.set(1, 0);
LK.gui.topRight.addChild(musicButton);
musicButton.x = -20;
musicButton.y = 180;
// Create neighborhood layout
function createNeighborhood() {
// Fill entire screen with buildings and restaurants
var buildingRows = 20; // More rows to fill the map
var buildingCols = 15; // More columns to fill the map
var gridSpacingX = 135; // Tighter spacing
var gridSpacingY = 135; // Tighter spacing
var startX = 70; // Start position
var startY = 150; // Start from top, leaving space for UI
// Helper function to check if two objects overlap
function checkOverlap(obj1, obj2) {
var obj1Left = obj1.x - obj1.width / 2;
var obj1Right = obj1.x + obj1.width / 2;
var obj1Top = obj1.y - obj1.height / 2;
var obj1Bottom = obj1.y + obj1.height / 2;
var obj2Left = obj2.x - obj2.width / 2;
var obj2Right = obj2.x + obj2.width / 2;
var obj2Top = obj2.y - obj2.height / 2;
var obj2Bottom = obj2.y + obj2.height / 2;
return !(obj1Right < obj2Left || obj1Left > obj2Right || obj1Bottom < obj2Top || obj1Top > obj2Bottom);
}
// Create array of all positions
var allPositions = [];
for (var row = 0; row < buildingRows; row++) {
for (var col = 0; col < buildingCols; col++) {
var buildingX = startX + col * gridSpacingX;
var buildingY = startY + row * gridSpacingY;
// Ensure buildings stay within reasonable bounds
if (buildingX < 2000 && buildingY < 2700) {
allPositions.push({
x: buildingX,
y: buildingY
});
}
}
}
// Calculate shop count (1 shop per 15 buildings)
var totalPositions = allPositions.length;
var shopCount = Math.floor(totalPositions / 16); // 1 shop for every 15 buildings (16 total positions)
// Shuffle positions to randomly distribute shops
for (var i = allPositions.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = allPositions[i];
allPositions[i] = allPositions[j];
allPositions[j] = temp;
}
// Track all placed objects to prevent overlaps
var placedObjects = [];
// Place shops in first N positions after shuffle
for (var i = 0; i < shopCount; i++) {
var pos = allPositions[i];
var restaurant = worldContainer.addChild(new Restaurant());
restaurant.x = pos.x;
restaurant.y = pos.y;
restaurant.width = 60; // Restaurant asset width
restaurant.height = 60; // Restaurant asset height
// Check for overlaps with existing objects
var overlaps = false;
for (var j = 0; j < placedObjects.length; j++) {
if (checkOverlap(restaurant, placedObjects[j])) {
overlaps = true;
break;
}
}
if (!overlaps) {
restaurants.push(restaurant);
placedObjects.push(restaurant);
} else {
// Remove restaurant if it overlaps
restaurant.destroy();
}
}
// Place buildings in remaining positions
for (var i = shopCount; i < totalPositions; i++) {
var pos = allPositions[i];
var building = worldContainer.addChild(LK.getAsset('building', {
anchorX: 0.5,
anchorY: 0.5,
x: pos.x,
y: pos.y,
scaleX: 2.5,
scaleY: 2.5
}));
building.width = 40; // Building collision width (unchanged)
building.height = 40; // Building collision height (unchanged)
// Check for overlaps with existing objects
var overlaps = false;
for (var j = 0; j < placedObjects.length; j++) {
if (checkOverlap(building, placedObjects[j])) {
overlaps = true;
break;
}
}
if (!overlaps) {
buildings.push(building);
placedObjects.push(building);
} else {
// Remove building if it overlaps
building.destroy();
}
}
// Create customers - place them outside buildings (not restaurants)
var customerCount = Math.min(25, buildings.length);
for (var i = 0; i < customerCount; i++) {
var customer = worldContainer.addChild(new Customer());
// Place customer further outside building to account for 2.5x visual scale
// Buildings are visually 2.5x larger, so move customers proportionally further out
customer.x = buildings[i].x + buildings[i].width / 2 * 2.5 + 15;
customer.y = buildings[i].y + buildings[i].height / 2 * 2.5 + 15;
// Hide all customers initially - only show when they have an order
customer.visible = false;
customers.push(customer);
}
// Create boundary fences around the neighborhood
// Top fence
var topFence = worldContainer.addChild(LK.getAsset('fence', {
anchorX: 0.5,
anchorY: 0.5,
x: 1000,
y: 10,
scaleX: 20,
scaleY: 1
}));
topFence.width = 2000;
topFence.height = 20;
buildings.push(topFence);
// Bottom fence
var bottomFence = worldContainer.addChild(LK.getAsset('fence', {
anchorX: 0.5,
anchorY: 0.5,
x: 1000,
y: 2722,
scaleX: 20,
scaleY: 1
}));
bottomFence.width = 2000;
bottomFence.height = 20;
buildings.push(bottomFence);
// Left fence
var leftFence = worldContainer.addChild(LK.getAsset('fence', {
anchorX: 0.5,
anchorY: 0.5,
x: 10,
y: 1400,
scaleX: 1,
scaleY: 135
}));
leftFence.width = 20;
leftFence.height = 2700;
buildings.push(leftFence);
// Right fence
var rightFence = worldContainer.addChild(LK.getAsset('fence', {
anchorX: 0.5,
anchorY: 0.5,
x: 2038,
y: 1400,
scaleX: 1,
scaleY: 135
}));
rightFence.width = 20;
rightFence.height = 2700;
buildings.push(rightFence);
}
function createNewOrder() {
if (currentOrder) {
return;
}
var availableRestaurants = restaurants.filter(function (r) {
return !r.hasOrder;
});
var availableCustomers = customers.filter(function (c) {
return !c.waitingForOrder;
});
if (availableRestaurants.length === 0 || availableCustomers.length === 0) {
return;
}
var restaurant = availableRestaurants[Math.floor(Math.random() * availableRestaurants.length)];
var customer = availableCustomers[Math.floor(Math.random() * availableCustomers.length)];
var order = worldContainer.addChild(new Order());
order.x = restaurant.x;
order.y = restaurant.y - 60;
order.restaurant = restaurant;
order.customer = customer;
order.claimed = false; // Allow AI couriers to claim this order
// Set random order color and match customer color
var orderColor = orderColors[Math.floor(Math.random() * orderColors.length)];
order.setColor(orderColor);
customer.setOrderColor(orderColor);
restaurant.hasOrder = true;
restaurant.orderReady = true;
customer.waitingForOrder = true;
customer.orderTime = 0;
// Show only this customer for player orders
customer.visible = true;
// Hide all other customers that might be visible from AI orders
for (var c = 0; c < customers.length; c++) {
if (customers[c] !== customer) {
customers[c].visible = false;
}
}
// Add "I'm here" text to the building that has the customer
var building = null;
// Find the building this customer belongs to (customers are positioned relative to buildings)
for (var b = 0; b < buildings.length; b++) {
var buildingCenterX = buildings[b].x;
var buildingCenterY = buildings[b].y;
var expectedCustomerX = buildingCenterX + buildings[b].width / 2 * 2.5 + 15;
var expectedCustomerY = buildingCenterY + buildings[b].height / 2 * 2.5 + 15;
if (Math.abs(customer.x - expectedCustomerX) < 5 && Math.abs(customer.y - expectedCustomerY) < 5) {
building = buildings[b];
break;
}
}
if (building && !building.customerText) {
var customerText = new Text2("I'm here", {
size: 10,
fill: 0xffffff,
stroke: 0x000000,
strokeThickness: 3
});
customerText.anchor.set(0.5, 0.5);
customerText.x = 0;
customerText.y = -30;
building.addChild(customerText);
building.customerText = customerText;
}
customer.deliveryTarget = null;
currentOrder = order;
orders.push(order);
gameState = 'pickup';
// Create pickup arrow indicator
if (pickupArrow) {
pickupArrow.destroy();
}
pickupArrow = game.addChild(new ArrowIndicator(restaurant.x, restaurant.y, 0x00ff00, 'pickup'));
}
function createAIOrder() {
var availableRestaurants = restaurants.filter(function (r) {
return !r.hasOrder;
});
var availableCustomers = customers.filter(function (c) {
return !c.waitingForOrder;
});
if (availableRestaurants.length === 0 || availableCustomers.length === 0) {
return;
}
var restaurant = availableRestaurants[Math.floor(Math.random() * availableRestaurants.length)];
var customer = availableCustomers[Math.floor(Math.random() * availableCustomers.length)];
var order = worldContainer.addChild(new Order());
order.x = restaurant.x;
order.y = restaurant.y - 60;
order.restaurant = restaurant;
order.customer = customer;
order.claimed = false; // For AI to claim
// Set random order color and match customer color
var orderColor = orderColors[Math.floor(Math.random() * orderColors.length)];
order.setColor(orderColor);
customer.setOrderColor(orderColor);
restaurant.hasOrder = true;
restaurant.orderReady = true;
customer.waitingForOrder = true;
customer.orderTime = 0;
// Don't show AI order customers - only player's customer should be visible
customer.visible = false;
// Add "I'm here" text to the building that has the customer
var building = null;
// Find the building this customer belongs to (customers are positioned relative to buildings)
for (var b = 0; b < buildings.length; b++) {
var buildingCenterX = buildings[b].x;
var buildingCenterY = buildings[b].y;
var expectedCustomerX = buildingCenterX + buildings[b].width / 2 * 2.5 + 15;
var expectedCustomerY = buildingCenterY + buildings[b].height / 2 * 2.5 + 15;
if (Math.abs(customer.x - expectedCustomerX) < 5 && Math.abs(customer.y - expectedCustomerY) < 5) {
building = buildings[b];
break;
}
}
if (building && !building.customerText) {
var customerText = new Text2("I'm here", {
size: 10,
fill: 0xffffff,
stroke: 0x000000,
strokeThickness: 3
});
customerText.anchor.set(0.5, 0.5);
customerText.x = 0;
customerText.y = -30;
building.addChild(customerText);
building.customerText = customerText;
}
customer.deliveryTarget = null;
aiOrders.push(order);
}
function spawnPowerUp() {
if (powerUps.length >= 3) {
return;
}
var type = Math.random() < 0.6 ? 'health' : 'time';
var powerUp = worldContainer.addChild(new PowerUp(type));
// Spawn power-ups on road positions between buildings
var roadPositions = [{
x: 200,
y: 220
}, {
x: 335,
y: 220
}, {
x: 470,
y: 220
}, {
x: 605,
y: 220
}, {
x: 740,
y: 220
}, {
x: 200,
y: 355
}, {
x: 335,
y: 355
}, {
x: 470,
y: 355
}, {
x: 605,
y: 355
}, {
x: 740,
y: 355
}, {
x: 200,
y: 490
}, {
x: 335,
y: 490
}, {
x: 470,
y: 490
}, {
x: 605,
y: 490
}, {
x: 740,
y: 490
}, {
x: 200,
y: 625
}, {
x: 335,
y: 625
}, {
x: 470,
y: 625
}, {
x: 605,
y: 625
}, {
x: 740,
y: 625
}];
var roadPos = roadPositions[Math.floor(Math.random() * roadPositions.length)];
powerUp.x = roadPos.x;
powerUp.y = roadPos.y;
powerUps.push(powerUp);
}
function updateUI() {
if (playerRole === 'courier') {
healthBar.setText('Health: ' + Math.floor(health));
moneyText.setText('$' + money);
if (hasInsurance) {
var timeLeft = Math.max(0, (insuranceEndTime - Date.now()) / 1000);
insuranceStatus.setText('Insurance: ' + timeLeft.toFixed(0) + 's');
insuranceStatus.visible = true;
} else {
insuranceStatus.visible = false;
}
if (currentOrder && gameState === 'delivery') {
var timeLeft = Math.max(0, currentOrder.timeRemaining / 1000);
orderTimer.setText('Deliver in: ' + timeLeft.toFixed(1) + 's');
} else {
orderTimer.setText('');
}
} else if (playerRole === 'police') {
healthBar.setText('Police Money: $' + courier.money);
moneyText.setText('Player: $' + money);
orderTimer.setText('');
insuranceStatus.visible = false;
} else if (playerRole === 'vandal') {
healthBar.setText('Vandal Money: $' + courier.money);
moneyText.setText('Player: $' + money);
orderTimer.setText('');
insuranceStatus.visible = false;
}
}
function checkCollisions() {
// Check courier-restaurant collision for pickup
if (gameState === 'pickup' && currentOrder) {
if (courier.intersects(currentOrder.restaurant)) {
// Pick up order
if (soundEnabled) {
LK.getSound('pickup').play();
// Play "thanks, drive safe" voice line
LK.setTimeout(function () {
LK.getSound('thanks_drive_safe').play();
}, 500);
}
courier.hasOrder = true;
gameState = 'delivery';
currentOrder.claimed = true; // Mark as claimed by player
currentOrder.x = courier.x;
currentOrder.y = courier.y - 40;
// Start delivery timer now that order is picked up
currentOrder.startDeliveryTimer(courier.speed);
// Remove pickup arrow and create delivery arrow
if (pickupArrow) {
pickupArrow.destroy();
pickupArrow = null;
}
if (deliveryArrow) {
deliveryArrow.destroy();
}
// Create delivery arrow pointing to customer location
deliveryArrow = game.addChild(new ArrowIndicator(currentOrder.customer.x, currentOrder.customer.y, 0xff0000, 'delivery'));
}
}
// Check courier-customer collision for delivery
if (gameState === 'delivery' && currentOrder && courier.hasOrder) {
// Check if courier intersects directly with the customer
if (courier.intersects(currentOrder.customer)) {
// Deliver order
if (soundEnabled) {
LK.getSound('delivery').play();
// Play "thank you, here's a tip" voice line
LK.setTimeout(function () {
LK.getSound('thank_you_tip').play();
}, 300);
// Play coin drop sound for tip
LK.setTimeout(function () {
LK.getSound('coin_drop').play();
}, 800);
}
completeDelivery();
}
}
// Check courier-powerup collisions
for (var i = powerUps.length - 1; i >= 0; i--) {
var powerUp = powerUps[i];
if (courier.intersects(powerUp)) {
if (soundEnabled) {
LK.getSound('powerup').play();
}
if (powerUp.type === 'health') {
health = Math.min(100, health + 25);
} else if (powerUp.type === 'time') {
timeFreezeActive = true;
timeFreezeRemaining = 3000; // 3 seconds
}
powerUp.destroy();
powerUps.splice(i, 1);
}
}
}
function completeDelivery() {
if (!currentOrder) {
return;
}
var deliveryTime = currentOrder.timeLimit - currentOrder.timeRemaining;
var bonus = 0;
if (currentOrder.timeRemaining > 0) {
// Successful delivery - calculate price based on distance
var dx = currentOrder.restaurant.x - currentOrder.customer.x;
var dy = currentOrder.restaurant.y - currentOrder.customer.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var basePrice = Math.max(2, Math.min(10, Math.floor(distance / 100) + 2));
var timeBonus = Math.floor(currentOrder.timeRemaining / 1000);
bonus = basePrice + Math.min(timeBonus, 3); // Cap time bonus at $3
money += bonus;
// Flash green for success
LK.effects.flashObject(courier, 0x00ff00, 500);
} else {
// Late delivery - health penalty
health -= 20;
LK.effects.flashObject(courier, 0xff0000, 1000);
if (health <= 0) {
LK.showGameOver();
return;
}
}
// Clean up order
currentOrder.restaurant.hasOrder = false;
currentOrder.customer.waitingForOrder = false;
// Reset customer color to default
currentOrder.customer.setOrderColor(0x2196f3);
// Hide customer after delivery
currentOrder.customer.visible = false;
// Remove "I'm here" text from building
var customer = currentOrder.customer;
var building = null;
// Find the building this customer belongs to using the same logic as creation
for (var b = 0; b < buildings.length; b++) {
var buildingCenterX = buildings[b].x;
var buildingCenterY = buildings[b].y;
var expectedCustomerX = buildingCenterX + buildings[b].width / 2 * 2.5 + 15;
var expectedCustomerY = buildingCenterY + buildings[b].height / 2 * 2.5 + 15;
if (Math.abs(customer.x - expectedCustomerX) < 5 && Math.abs(customer.y - expectedCustomerY) < 5) {
building = buildings[b];
break;
}
}
if (building && building.customerText) {
building.customerText.destroy();
building.customerText = null;
}
currentOrder.destroy();
for (var i = orders.length - 1; i >= 0; i--) {
if (orders[i] === currentOrder) {
orders.splice(i, 1);
break;
}
}
currentOrder = null;
courier.hasOrder = false;
gameState = 'pickup';
// Clean up arrow indicators
if (deliveryArrow) {
deliveryArrow.destroy();
deliveryArrow = null;
}
// Create new order after short delay
LK.setTimeout(function () {
createNewOrder();
}, 1000);
}
// Create world container for scrolling
worldContainer = game.addChild(new Container());
// Scale up the world to zoom in more - this makes roads wider and couriers appear smaller
worldContainer.scaleX = 3;
worldContainer.scaleY = 3;
// Initialize game elements
createNeighborhood();
// Courier will be created after role selection
// Role selection handlers
courierButtonBg.down = function () {
selectRole('courier');
};
courierButton.down = function () {
selectRole('courier');
};
policeButtonBg.down = function () {
selectRole('police');
};
policeButton.down = function () {
selectRole('police');
};
vandalButtonBg.down = function () {
selectRole('vandal');
};
vandalButton.down = function () {
selectRole('vandal');
};
insuranceButton.down = function () {
buyInsurance();
};
godModeButton.down = function () {
toggleGodMode();
};
soundButton.down = function () {
toggleSound();
};
musicButton.down = function () {
toggleMusic();
};
function selectRole(role) {
playerRole = role;
storage.playerRole = role;
roleSelectionContainer.visible = false;
if (role === 'courier') {
insuranceButton.visible = true;
courier = worldContainer.addChild(new Courier());
courier.x = 1000;
courier.y = 1400;
} else if (role === 'police') {
courier = worldContainer.addChild(new Police());
courier.x = 1000;
courier.y = 1400;
} else if (role === 'vandal') {
courier = worldContainer.addChild(new Vandal());
courier.x = 1000;
courier.y = 1400;
}
gameStarted = true;
// Update button text to reflect current settings
soundButton.setText('Sound: ' + (soundEnabled ? 'ON' : 'OFF'));
musicButton.setText('Music: ' + (musicEnabled ? 'ON' : 'OFF'));
// Start background music only if enabled
if (musicEnabled) {
LK.playMusic('background_music');
}
if (role === 'courier') {
createNewOrder();
}
}
function buyInsurance() {
if (money >= 5 && !hasInsurance) {
money -= 5;
hasInsurance = true;
insuranceEndTime = Date.now() + 300000; // 5 minutes
insuranceButton.visible = false;
}
}
function toggleGodMode() {
godMode = !godMode;
godModeButton.setText('God Mode: ' + (godMode ? 'ON' : 'OFF'));
if (godMode) {
// Switch to god mode - zoom out to see full map
worldContainer.scaleX = godModeScale;
worldContainer.scaleY = godModeScale;
// Center camera on map center
cameraX = 1024 - 1024 / godModeScale;
cameraY = 1366 - 1366 / godModeScale;
} else {
// Switch back to normal mode
worldContainer.scaleX = normalScale;
worldContainer.scaleY = normalScale;
// Center camera back on courier if available
if (courier) {
cameraX = courier.x - 1024 / worldContainer.scaleX;
cameraY = courier.y - 1366 / worldContainer.scaleY;
}
}
}
function toggleSound() {
soundEnabled = !soundEnabled;
storage.soundEnabled = soundEnabled;
soundButton.setText('Sound: ' + (soundEnabled ? 'ON' : 'OFF'));
// Stop current siren if sound is disabled
if (!soundEnabled && sirenSound) {
sirenSound.stop();
sirenSound = null;
}
}
function toggleMusic() {
musicEnabled = !musicEnabled;
storage.musicEnabled = musicEnabled;
musicButton.setText('Music: ' + (musicEnabled ? 'ON' : 'OFF'));
if (musicEnabled) {
// Resume music if game has started
if (gameStarted) {
LK.playMusic('background_music');
}
} else {
// Stop music
LK.stopMusic();
}
}
// Touch controls
var targetX = 1000;
var targetY = 1400;
game.down = function (x, y, obj) {
if (godMode) {
// In god mode, don't set movement target, just allow camera control
return;
}
// Convert screen coordinates to world coordinates accounting for scale
targetX = x / worldContainer.scaleX + cameraX;
targetY = y / worldContainer.scaleY + cameraY;
};
game.move = function (x, y, obj) {
if (godMode) {
// In god mode, don't set movement target, just allow camera control
return;
}
// Convert screen coordinates to world coordinates accounting for scale
targetX = x / worldContainer.scaleX + cameraX;
targetY = y / worldContainer.scaleY + cameraY;
};
// Create NPCs with unified system
// Create 8 police - spawn from center of map
for (var i = 0; i < 8; i++) {
var policeNPC = worldContainer.addChild(new NPCPolice());
// Spawn police from center area of the map
var centerX = 1024;
var centerY = 1366;
var radius = 150;
var angle = i / 8 * Math.PI * 2;
policeNPC.x = centerX + Math.cos(angle) * radius;
policeNPC.y = centerY + Math.sin(angle) * radius;
policeNPC.npcType = 'police';
npcs.push(policeNPC);
}
// Create 6 vandals - spawn from 4 corners of map
var cornerPositions = [{
x: 100,
y: 200
},
// Top-left
{
x: 1900,
y: 200
},
// Top-right
{
x: 100,
y: 2500
},
// Bottom-left
{
x: 1900,
y: 2500
},
// Bottom-right
{
x: 100,
y: 1350
},
// Left middle
{
x: 1900,
y: 1350
} // Right middle
];
for (var i = 0; i < 6; i++) {
var vandalNPC = worldContainer.addChild(new NPCVandal());
var pos = cornerPositions[i];
vandalNPC.x = pos.x;
vandalNPC.y = pos.y;
vandalNPC.npcType = 'vandal';
npcs.push(vandalNPC);
}
// Create NPC couriers - spawn around buildings
for (var i = 0; i < 10; i++) {
var courierNPC = worldContainer.addChild(new NPCCourier());
// Spawn around buildings randomly
if (buildings.length > 0) {
var building = buildings[Math.floor(Math.random() * buildings.length)];
courierNPC.x = building.x + (Math.random() - 0.5) * 100;
courierNPC.y = building.y + (Math.random() - 0.5) * 100;
} else {
courierNPC.x = 200 + Math.random() * 1600;
courierNPC.y = 200 + Math.random() * 2300;
}
courierNPC.npcType = 'courier';
npcs.push(courierNPC);
}
// Create initial AI orders immediately and more frequently
LK.setTimeout(function () {
createAIOrder();
}, 100);
LK.setTimeout(function () {
createAIOrder();
}, 300);
LK.setTimeout(function () {
createAIOrder();
}, 600);
LK.setTimeout(function () {
createAIOrder();
}, 900);
// Spawn initial power-up
LK.setTimeout(function () {
spawnPowerUp();
}, 3000);
game.update = function () {
// Only run game logic if role is selected
if (!gameStarted || !courier) {
return;
}
// Update insurance status
if (hasInsurance && Date.now() > insuranceEndTime) {
hasInsurance = false;
if (playerRole === 'courier') {
insuranceButton.visible = true;
}
}
// Move courier toward target
courier.moveToward(targetX, targetY);
// Update camera system
if (godMode) {
// In god mode, allow free camera movement or keep centered on map
// Keep camera centered on the map center for full view
cameraX = 1024 - 1024 / worldContainer.scaleX;
cameraY = 1366 - 1366 / worldContainer.scaleY;
} else {
// Normal mode - follow courier
cameraX = courier.x - 1024 / worldContainer.scaleX; // Center courier horizontally accounting for scale
cameraY = courier.y - 1366 / worldContainer.scaleY; // Center courier vertically accounting for scale
}
// Apply camera offset to world container
worldContainer.x = -cameraX * worldContainer.scaleX;
worldContainer.y = -cameraY * worldContainer.scaleY;
// Update time freeze
if (timeFreezeActive) {
timeFreezeRemaining -= 16.67;
if (timeFreezeRemaining <= 0) {
timeFreezeActive = false;
}
}
// Update current order timer (if not frozen)
if (currentOrder && !timeFreezeActive) {
// Update time limit based on courier distance if picked up
if (courier.hasOrder && gameState === 'delivery') {
currentOrder.updateTimeLimit(courier.x, courier.y);
}
currentOrder.update();
// Move order with courier if picked up
if (courier.hasOrder && gameState === 'delivery') {
currentOrder.x = courier.x;
currentOrder.y = courier.y - 40;
}
// No delivery target flashing needed anymore - buildings show "I'm here" text instead
// Check for failed delivery
if (currentOrder.timeRemaining <= 0 && gameState === 'delivery') {
completeDelivery();
}
}
// Update customers
for (var i = 0; i < customers.length; i++) {
customers[i].update();
}
// Update all NPCs
for (var i = 0; i < npcs.length; i++) {
npcs[i].update();
}
// Check if player is being chased by police
if (playerRole === 'courier') {
isBeingChased = false;
// Check distance to all police NPCs
for (var i = 0; i < npcs.length; i++) {
var npc = npcs[i];
if (npc.npcType === 'police' && npc.target === courier) {
var dx = npc.x - courier.x;
var dy = npc.y - courier.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= 50) {
// Within chase range
isBeingChased = true;
break;
}
}
}
// Handle siren sound based on chase state
if (isBeingChased && !lastChasedState) {
// Just started being chased - start siren
if (sirenSound) {
sirenSound.stop();
}
if (soundEnabled) {
sirenSound = LK.getSound('police_siren');
sirenSound.play();
}
} else if (!isBeingChased && lastChasedState) {
// No longer being chased - stop siren
if (sirenSound) {
sirenSound.stop();
sirenSound = null;
}
}
lastChasedState = isBeingChased;
}
// Create new AI orders periodically - more frequently and higher limit
if (LK.ticks % 90 === 0 && aiOrders.length < 12) {
// Every 1.5 seconds, maintain up to 12 AI orders
createAIOrder();
}
// Create additional AI orders periodically
if (LK.ticks % 90 === 0 && aiOrders.length < 8) {
createAIOrder();
}
// Update power-ups
for (var i = powerUps.length - 1; i >= 0; i--) {
var powerUp = powerUps[i];
powerUp.update();
if (powerUp.lifeTime >= powerUp.maxLifeTime) {
powerUp.destroy();
powerUps.splice(i, 1);
}
}
// Spawn new power-ups occasionally
if (LK.ticks % 600 === 0) {
// Every 10 seconds
spawnPowerUp();
}
// Check collisions
checkCollisions();
// Update arrow indicators
if (pickupArrow) {
pickupArrow.update();
}
if (deliveryArrow) {
deliveryArrow.update();
}
// Update UI
updateUI();
// Game is now endless - no win condition
}; /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var ArrowIndicator = Container.expand(function (targetX, targetY, color, arrowType) {
var self = Container.call(this);
// Create arrow shape using dedicated arrow assets
var assetName = arrowType === 'pickup' ? 'pickupArrow' : 'deliveryArrow';
var arrowGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Make arrows larger
arrowGraphics.scaleX = 1.5;
arrowGraphics.scaleY = 1.8;
self.targetX = targetX;
self.targetY = targetY;
self.baseScaleX = 1.5;
self.baseScaleY = 1.8;
self.update = function () {
// Calculate direction to target
var dx = self.targetX - courier.x;
var dy = self.targetY - courier.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 100) {
// Position arrow closer to player
var angle = Math.atan2(dy, dx);
var edgeDistance = 180; // Reduced from 300 to make arrow closer
// Position relative to screen center (where courier appears)
self.x = 1024 + Math.cos(angle) * edgeDistance;
self.y = 1366 + Math.sin(angle) * edgeDistance;
// Rotate arrow to point toward target
arrowGraphics.rotation = angle;
self.visible = true;
// Heart beat animation - faster when closer to target
var maxDistance = 500; // Maximum distance for animation calculation
var normalizedDistance = Math.min(distance, maxDistance) / maxDistance;
// Animation speed: closer = faster heartbeat (lower period)
var animationSpeed = 0.02 + (1 - normalizedDistance) * 0.08; // 0.02 to 0.1
var heartbeat = 1 + 0.3 * Math.sin(LK.ticks * animationSpeed);
// Apply heartbeat animation
arrowGraphics.scaleX = self.baseScaleX * heartbeat;
arrowGraphics.scaleY = self.baseScaleY * heartbeat;
} else {
self.visible = false;
}
};
return self;
});
var Courier = Container.expand(function () {
var self = Container.call(this);
var courierGraphics = self.attachAsset('courierAsset', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 8;
self.hasOrder = false;
self.targetRestaurant = null;
self.targetCustomer = null;
self.moveToward = function (targetX, targetY) {
var dx = targetX - self.x;
var dy = targetY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 5) {
// Smooth movement with momentum for agar.io feel
var moveSpeed = Math.min(self.speed, distance * 0.15);
var newX = self.x + dx / distance * moveSpeed;
var newY = self.y + dy / distance * moveSpeed;
// Check collision with buildings
var canMove = true;
for (var i = 0; i < buildings.length; i++) {
var tempCourier = {
x: newX,
y: newY,
width: 9,
height: 9
};
var building = buildings[i];
if (tempCourier.x < building.x + building.width / 2 && tempCourier.x + tempCourier.width / 2 > building.x - building.width / 2 && tempCourier.y < building.y + building.height / 2 && tempCourier.y + tempCourier.height / 2 > building.y - building.height / 2) {
canMove = false;
break;
}
}
// Check collision with restaurants/shops
for (var i = 0; i < restaurants.length; i++) {
var tempCourier = {
x: newX,
y: newY,
width: 9,
height: 9
};
var restaurant = restaurants[i];
if (tempCourier.x < restaurant.x + restaurant.width / 2 && tempCourier.x + tempCourier.width / 2 > restaurant.x - restaurant.width / 2 && tempCourier.y < restaurant.y + restaurant.height / 2 && tempCourier.y + tempCourier.height / 2 > restaurant.y - restaurant.height / 2) {
canMove = false;
break;
}
}
// Check collision with customers
for (var i = 0; i < customers.length; i++) {
var tempCourier = {
x: newX,
y: newY,
width: 9,
height: 9
};
var customer = customers[i];
if (tempCourier.x < customer.x + 1 / 2 && tempCourier.x + tempCourier.width / 2 > customer.x - 1 / 2 && tempCourier.y < customer.y + 1 / 2 && tempCourier.y + tempCourier.height / 2 > customer.y - 1 / 2) {
canMove = false;
break;
}
}
if (canMove) {
self.x = newX;
self.y = newY;
} else {
// Slide along building edges when collision occurs
var canMoveX = true;
var canMoveY = true;
// Test horizontal movement only
for (var i = 0; i < buildings.length; i++) {
var tempCourier = {
x: newX,
y: self.y,
width: 9,
height: 9
};
var building = buildings[i];
if (tempCourier.x < building.x + building.width / 2 && tempCourier.x + tempCourier.width / 2 > building.x - building.width / 2 && tempCourier.y < building.y + building.height / 2 && tempCourier.y + tempCourier.height / 2 > building.y - building.height / 2) {
canMoveX = false;
break;
}
}
// Test vertical movement only
for (var i = 0; i < buildings.length; i++) {
var tempCourier = {
x: self.x,
y: newY,
width: 9,
height: 9
};
var building = buildings[i];
if (tempCourier.x < building.x + building.width / 2 && tempCourier.x + tempCourier.width / 2 > building.x - building.width / 2 && tempCourier.y < building.y + building.height / 2 && tempCourier.y + tempCourier.height / 2 > building.y - building.height / 2) {
canMoveY = false;
break;
}
}
// Apply sliding movement - move in whichever direction is not blocked
if (canMoveX) {
self.x = newX;
}
if (canMoveY) {
self.y = newY;
}
// If both directions are blocked, try to slide with reduced movement using tween for smooth effect
if (!canMoveX && !canMoveY) {
var slideDistance = 15;
var slideX = self.x + (dx > 0 ? slideDistance : -slideDistance);
var slideY = self.y + (dy > 0 ? slideDistance : -slideDistance);
// Animate sliding motion
tween(self, {
x: slideX,
y: slideY
}, {
duration: 200,
easing: tween.easeOut
});
}
}
}
};
return self;
});
var Customer = Container.expand(function () {
var self = Container.call(this);
var customerGraphics = self.attachAsset('customer', {
anchorX: 0.5,
anchorY: 0.5
});
self.waitingForOrder = false;
self.orderTime = 0;
self.maxWaitTime = 10000; // 10 seconds
self.deliveryTarget = null;
self.orderColor = 0x2196f3; // Default blue
self.setOrderColor = function (color) {
self.orderColor = color;
customerGraphics.tint = color;
};
self.update = function () {
if (self.waitingForOrder) {
self.orderTime += 16.67; // Approximate ms per frame at 60fps
}
};
return self;
});
var NPCCourier = Container.expand(function () {
var self = Container.call(this);
var courierGraphics = self.attachAsset('courierAsset', {
anchorX: 0.5,
anchorY: 0.5
});
courierGraphics.tint = 0xff5722;
self.speed = 2;
self.detectionRadius = 20;
self.isMoving = false;
self.hasInsurance = false;
self.lastX = self.x;
self.lastY = self.y;
self.lastSeenPolice = false;
self.lastSeenVandal = false;
self.startRoaming = function () {
if (self.isMoving) return;
self.isMoving = true;
var newX = 200 + Math.random() * 1600;
var newY = 200 + Math.random() * 2300;
var dx = newX - self.x;
var dy = newY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var duration = distance * 50; // Adjust speed by changing multiplier
tween(self, {
x: newX,
y: newY
}, {
duration: duration,
easing: tween.linear,
onFinish: function onFinish() {
self.isMoving = false;
// Start next movement after a short pause
LK.setTimeout(function () {
self.startRoaming();
}, 500 + Math.random() * 1000);
}
});
};
self.update = function () {
// Start roaming if not already moving
if (!self.isMoving) {
self.startRoaming();
}
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
var NPCPolice = Container.expand(function () {
var self = Container.call(this);
var policeGraphics = self.attachAsset('policeAsset', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 3;
self.detectionRadius = 20;
self.target = null;
self.isMoving = false;
self.lastX = self.x;
self.lastY = self.y;
self.thinkTimer = 0;
self.startRoaming = function () {
if (self.isMoving) return;
self.isMoving = true;
var newX = 200 + Math.random() * 1600;
var newY = 200 + Math.random() * 2300;
var dx = newX - self.x;
var dy = newY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var duration = distance * 40; // Faster than couriers
tween(self, {
x: newX,
y: newY
}, {
duration: duration,
easing: tween.linear,
onFinish: function onFinish() {
self.isMoving = false;
// Start next movement after a short pause
LK.setTimeout(function () {
self.startRoaming();
}, 300 + Math.random() * 800);
}
});
};
self.update = function () {
self.thinkTimer += 16.67;
if (self.thinkTimer >= 100) {
self.thinkTimer = 0;
self.target = null;
// Look for vandals
for (var i = 0; i < npcs.length; i++) {
var npc = npcs[i];
if (npc.npcType === 'vandal') {
var dx = npc.x - self.x;
var dy = npc.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.detectionRadius) {
self.target = npc;
// Stop current movement and chase target
tween.stop(self, {
x: true,
y: true
});
self.isMoving = false;
break;
}
}
}
// Look for uninsured couriers
if (!self.target) {
for (var i = 0; i < npcs.length; i++) {
var npc = npcs[i];
if (npc.npcType === 'courier' && !npc.hasInsurance) {
var dx = npc.x - self.x;
var dy = npc.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.detectionRadius) {
self.target = npc;
// Stop current movement and chase target
tween.stop(self, {
x: true,
y: true
});
self.isMoving = false;
break;
}
}
}
}
// Check player courier if no insurance
if (!self.target && playerRole === 'courier' && courier && !hasInsurance) {
var dx = courier.x - self.x;
var dy = courier.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.detectionRadius) {
self.target = courier;
// Stop current movement and chase target
tween.stop(self, {
x: true,
y: true
});
self.isMoving = false;
}
}
}
// Move behavior
if (self.target) {
// Chase target with tween
var dx = self.target.x - self.x;
var dy = self.target.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 5) {
tween.stop(self, {
x: true,
y: true
});
tween(self, {
x: self.target.x,
y: self.target.y
}, {
duration: distance * 30,
easing: tween.linear
});
}
} else {
// Start roaming if not already moving
if (!self.isMoving) {
self.startRoaming();
}
}
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
var NPCVandal = Container.expand(function () {
var self = Container.call(this);
var vandalGraphics = self.attachAsset('vandalAsset', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 2;
self.detectionRadius = 20;
self.fleeing = false;
self.isMoving = false;
self.lastX = self.x;
self.lastY = self.y;
self.thinkTimer = 0;
self.startRoaming = function () {
if (self.isMoving || self.fleeing) return;
self.isMoving = true;
var newX = 200 + Math.random() * 1600;
var newY = 200 + Math.random() * 2300;
var dx = newX - self.x;
var dy = newY - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var duration = distance * 60; // Slower than police
tween(self, {
x: newX,
y: newY
}, {
duration: duration,
easing: tween.linear,
onFinish: function onFinish() {
self.isMoving = false;
// Start next movement after a short pause
LK.setTimeout(function () {
self.startRoaming();
}, 800 + Math.random() * 1200);
}
});
};
self.flee = function (fromX, fromY) {
self.fleeing = true;
tween.stop(self, {
x: true,
y: true
});
self.isMoving = false;
var dx = self.x - fromX;
var dy = self.y - fromY;
var fleeX = self.x + dx * 5;
var fleeY = self.y + dy * 5;
// Keep within bounds
fleeX = Math.max(100, Math.min(1900, fleeX));
fleeY = Math.max(200, Math.min(2500, fleeY));
tween(self, {
x: fleeX,
y: fleeY
}, {
duration: 2000,
easing: tween.easeOut,
onFinish: function onFinish() {
self.fleeing = false;
LK.setTimeout(function () {
self.startRoaming();
}, 1000);
}
});
};
self.update = function () {
self.thinkTimer += 16.67;
if (self.thinkTimer >= 100) {
self.thinkTimer = 0;
// Check if close to player courier with package to steal it
if (playerRole === 'courier' && courier && courier.hasOrder) {
var dx = courier.x - self.x;
var dy = courier.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.detectionRadius) {
// Steal the package
courier.hasOrder = false;
if (currentOrder) {
currentOrder.destroy();
currentOrder = null;
gameState = 'pickup';
// Clean up arrows
if (deliveryArrow) {
deliveryArrow.destroy();
deliveryArrow = null;
}
}
// Play theft sound effects
if (soundEnabled) {
var soundChoice = Math.random();
if (soundChoice < 0.5) {
LK.getSound('vandal_laugh').play();
} else {
LK.getSound('crash').play();
}
}
// Flash courier red to show package stolen
LK.effects.flashObject(courier, 0xff0000, 1000);
// Create new order after delay
LK.setTimeout(function () {
createNewOrder();
}, 2000);
}
}
var shouldFlee = false;
// Look for police
for (var i = 0; i < npcs.length; i++) {
var npc = npcs[i];
if (npc.npcType === 'police') {
var dx = npc.x - self.x;
var dy = npc.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.detectionRadius) {
self.flee(npc.x, npc.y);
shouldFlee = true;
break;
}
}
}
// Check player police
if (!shouldFlee && playerRole === 'police' && courier) {
var dx = courier.x - self.x;
var dy = courier.y - self.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= self.detectionRadius) {
self.flee(courier.x, courier.y);
shouldFlee = true;
}
}
}
// Start roaming if not fleeing and not already moving
if (!self.fleeing && !self.isMoving) {
self.startRoaming();
}
self.lastX = self.x;
self.lastY = self.y;
};
return self;
});
var Order = Container.expand(function () {
var self = Container.call(this);
var orderGraphics = self.attachAsset('order', {
anchorX: 0.5,
anchorY: 0.5
});
self.restaurant = null;
self.customer = null;
self.baseTimeLimit = 25000; // 25 seconds minimum
self.timeLimit = self.baseTimeLimit;
self.timeRemaining = self.timeLimit;
self.isPickedUp = false;
self.courierSpeed = 8; // Default courier speed
self.setColor = function (color) {
orderGraphics.tint = color;
};
self.startDeliveryTimer = function (courierSpeed) {
self.isPickedUp = true;
self.courierSpeed = courierSpeed;
self.timeLimit = self.baseTimeLimit;
self.timeRemaining = self.timeLimit;
};
self.updateTimeLimit = function (courierX, courierY) {
if (!self.isPickedUp || !self.customer) {
return;
}
// Calculate distance to customer
var dx = self.customer.x - courierX;
var dy = self.customer.y - courierY;
var distance = Math.sqrt(dx * dx + dy * dy);
// Calculate time needed to reach customer at max speed + 2 seconds buffer
var timeToReach = distance / self.courierSpeed * 16.67 + 2000; // Convert to ms and add 2s buffer
// Use the greater of base time limit or calculated time
var newTimeLimit = Math.max(self.baseTimeLimit, timeToReach);
// Only extend time, never reduce it
if (newTimeLimit > self.timeLimit) {
var extension = newTimeLimit - self.timeLimit;
self.timeLimit = newTimeLimit;
self.timeRemaining += extension;
}
};
self.update = function () {
if (self.isPickedUp && self.timeRemaining > 0) {
self.timeRemaining -= 16.67;
}
};
return self;
});
var PowerUp = Container.expand(function (type) {
var self = Container.call(this);
var powerupGraphics = self.attachAsset(type === 'health' ? 'healthPowerup' : 'timePowerup', {
anchorX: 0.5,
anchorY: 0.5
});
self.type = type;
self.lifeTime = 0;
self.maxLifeTime = 8000; // 8 seconds
self.update = function () {
self.lifeTime += 16.67;
// Pulse animation
var pulse = 1 + 0.2 * Math.sin(self.lifeTime * 0.01);
powerupGraphics.scaleX = pulse;
powerupGraphics.scaleY = pulse;
};
return self;
});
var Restaurant = Container.expand(function () {
var self = Container.call(this);
var restaurantGraphics = self.attachAsset('restaurant', {
anchorX: 0.5,
anchorY: 0.5
});
self.hasOrder = false;
self.orderReady = false;
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x8BC34A
});
/****
* Game Code
****/
// Game variables
var courier;
var npcs = [];
var restaurants = [];
var customers = [];
var orders = [];
var aiOrders = [];
var powerUps = [];
var buildings = [];
var health = 100;
var money = 20;
var currentOrder = null;
var gameState = 'pickup'; // 'pickup', 'delivery'
var timeFreezeActive = false;
var timeFreezeRemaining = 0;
// Role system
var playerRole = storage.playerRole || null; // 'courier', 'police', 'vandal'
var hasInsurance = false;
var insuranceEndTime = 0;
var gameStarted = false;
// Camera system
var cameraX = 0;
var cameraY = 0;
var worldContainer;
var pickupArrow = null;
var deliveryArrow = null;
// God mode system
var godMode = false;
var godModeScale = 0.8; // Zoom out scale for full map view
var normalScale = 3; // Normal game scale
// Sound system
var isBeingChased = false;
var sirenSound = null;
var lastChasedState = false;
// Sound control system
var soundEnabled = storage.soundEnabled !== undefined ? storage.soundEnabled : true;
var musicEnabled = storage.musicEnabled !== undefined ? storage.musicEnabled : true;
// Order colors for variety
var orderColors = [0xffff00, 0xff4444, 0x44ff44, 0x4444ff, 0xff44ff, 0x44ffff, 0xffa500, 0x800080];
// UI Elements
var healthBar = new Text2('Health: 100', {
size: 48
});
healthBar.anchor.set(0, 0);
LK.gui.topLeft.addChild(healthBar);
healthBar.x = 120;
healthBar.y = 20;
var moneyText = new Text2('$0', {
size: 48
});
moneyText.anchor.set(1, 0);
LK.gui.topRight.addChild(moneyText);
moneyText.x = -20;
moneyText.y = 20;
var orderTimer = new Text2('', {
size: 42
});
orderTimer.anchor.set(0.5, 0);
LK.gui.top.addChild(orderTimer);
orderTimer.y = 100;
// Role selection UI
var roleSelectionContainer = new Container();
LK.gui.center.addChild(roleSelectionContainer);
var roleTitle = new Text2('Choose Your Role', {
size: 72,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 8
});
roleTitle.anchor.set(0.5, 0.5);
roleTitle.y = -200;
roleSelectionContainer.addChild(roleTitle);
// Create green background for courier button
var courierButtonBg = LK.getAsset('road', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 1
});
courierButtonBg.tint = 0x4caf50;
courierButtonBg.y = -50;
roleSelectionContainer.addChild(courierButtonBg);
var courierButton = new Text2('COURIER', {
size: 48,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 8
});
courierButton.anchor.set(0.5, 0.5);
courierButton.y = -50;
roleSelectionContainer.addChild(courierButton);
// Create blue background for police button
var policeButtonBg = LK.getAsset('road', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 1
});
policeButtonBg.tint = 0x2196f3;
policeButtonBg.y = 50;
roleSelectionContainer.addChild(policeButtonBg);
var policeButton = new Text2('POLICE', {
size: 48,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 8
});
policeButton.anchor.set(0.5, 0.5);
policeButton.y = 50;
roleSelectionContainer.addChild(policeButton);
// Create white background for vandal button
var vandalButtonBg = LK.getAsset('road', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 3,
scaleY: 1
});
vandalButtonBg.tint = 0xFFFFFF;
vandalButtonBg.y = 150;
roleSelectionContainer.addChild(vandalButtonBg);
var vandalButton = new Text2('VANDAL', {
size: 48,
fill: 0xFFFFFF,
stroke: 0xFFFFFF,
strokeThickness: 8
});
vandalButton.anchor.set(0.5, 0.5);
vandalButton.y = 150;
roleSelectionContainer.addChild(vandalButton);
// Insurance UI (for couriers)
var insuranceButton = new Text2('Buy Insurance ($5)', {
size: 36
});
insuranceButton.anchor.set(0.5, 0);
LK.gui.top.addChild(insuranceButton);
insuranceButton.y = 160;
insuranceButton.visible = false;
var insuranceStatus = new Text2('', {
size: 32
});
insuranceStatus.anchor.set(0.5, 0);
LK.gui.top.addChild(insuranceStatus);
insuranceStatus.y = 210;
// God mode toggle button
var godModeButton = new Text2('God Mode: OFF', {
size: 36,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 4
});
godModeButton.anchor.set(1, 0);
LK.gui.topRight.addChild(godModeButton);
godModeButton.x = -20;
godModeButton.y = 80;
// Sound control buttons
var soundButton = new Text2('Sound: ON', {
size: 36,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 4
});
soundButton.anchor.set(1, 0);
LK.gui.topRight.addChild(soundButton);
soundButton.x = -20;
soundButton.y = 130;
var musicButton = new Text2('Music: ON', {
size: 36,
fill: 0xFFFFFF,
stroke: 0x000000,
strokeThickness: 4
});
musicButton.anchor.set(1, 0);
LK.gui.topRight.addChild(musicButton);
musicButton.x = -20;
musicButton.y = 180;
// Create neighborhood layout
function createNeighborhood() {
// Fill entire screen with buildings and restaurants
var buildingRows = 20; // More rows to fill the map
var buildingCols = 15; // More columns to fill the map
var gridSpacingX = 135; // Tighter spacing
var gridSpacingY = 135; // Tighter spacing
var startX = 70; // Start position
var startY = 150; // Start from top, leaving space for UI
// Helper function to check if two objects overlap
function checkOverlap(obj1, obj2) {
var obj1Left = obj1.x - obj1.width / 2;
var obj1Right = obj1.x + obj1.width / 2;
var obj1Top = obj1.y - obj1.height / 2;
var obj1Bottom = obj1.y + obj1.height / 2;
var obj2Left = obj2.x - obj2.width / 2;
var obj2Right = obj2.x + obj2.width / 2;
var obj2Top = obj2.y - obj2.height / 2;
var obj2Bottom = obj2.y + obj2.height / 2;
return !(obj1Right < obj2Left || obj1Left > obj2Right || obj1Bottom < obj2Top || obj1Top > obj2Bottom);
}
// Create array of all positions
var allPositions = [];
for (var row = 0; row < buildingRows; row++) {
for (var col = 0; col < buildingCols; col++) {
var buildingX = startX + col * gridSpacingX;
var buildingY = startY + row * gridSpacingY;
// Ensure buildings stay within reasonable bounds
if (buildingX < 2000 && buildingY < 2700) {
allPositions.push({
x: buildingX,
y: buildingY
});
}
}
}
// Calculate shop count (1 shop per 15 buildings)
var totalPositions = allPositions.length;
var shopCount = Math.floor(totalPositions / 16); // 1 shop for every 15 buildings (16 total positions)
// Shuffle positions to randomly distribute shops
for (var i = allPositions.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = allPositions[i];
allPositions[i] = allPositions[j];
allPositions[j] = temp;
}
// Track all placed objects to prevent overlaps
var placedObjects = [];
// Place shops in first N positions after shuffle
for (var i = 0; i < shopCount; i++) {
var pos = allPositions[i];
var restaurant = worldContainer.addChild(new Restaurant());
restaurant.x = pos.x;
restaurant.y = pos.y;
restaurant.width = 60; // Restaurant asset width
restaurant.height = 60; // Restaurant asset height
// Check for overlaps with existing objects
var overlaps = false;
for (var j = 0; j < placedObjects.length; j++) {
if (checkOverlap(restaurant, placedObjects[j])) {
overlaps = true;
break;
}
}
if (!overlaps) {
restaurants.push(restaurant);
placedObjects.push(restaurant);
} else {
// Remove restaurant if it overlaps
restaurant.destroy();
}
}
// Place buildings in remaining positions
for (var i = shopCount; i < totalPositions; i++) {
var pos = allPositions[i];
var building = worldContainer.addChild(LK.getAsset('building', {
anchorX: 0.5,
anchorY: 0.5,
x: pos.x,
y: pos.y,
scaleX: 2.5,
scaleY: 2.5
}));
building.width = 40; // Building collision width (unchanged)
building.height = 40; // Building collision height (unchanged)
// Check for overlaps with existing objects
var overlaps = false;
for (var j = 0; j < placedObjects.length; j++) {
if (checkOverlap(building, placedObjects[j])) {
overlaps = true;
break;
}
}
if (!overlaps) {
buildings.push(building);
placedObjects.push(building);
} else {
// Remove building if it overlaps
building.destroy();
}
}
// Create customers - place them outside buildings (not restaurants)
var customerCount = Math.min(25, buildings.length);
for (var i = 0; i < customerCount; i++) {
var customer = worldContainer.addChild(new Customer());
// Place customer further outside building to account for 2.5x visual scale
// Buildings are visually 2.5x larger, so move customers proportionally further out
customer.x = buildings[i].x + buildings[i].width / 2 * 2.5 + 15;
customer.y = buildings[i].y + buildings[i].height / 2 * 2.5 + 15;
// Hide all customers initially - only show when they have an order
customer.visible = false;
customers.push(customer);
}
// Create boundary fences around the neighborhood
// Top fence
var topFence = worldContainer.addChild(LK.getAsset('fence', {
anchorX: 0.5,
anchorY: 0.5,
x: 1000,
y: 10,
scaleX: 20,
scaleY: 1
}));
topFence.width = 2000;
topFence.height = 20;
buildings.push(topFence);
// Bottom fence
var bottomFence = worldContainer.addChild(LK.getAsset('fence', {
anchorX: 0.5,
anchorY: 0.5,
x: 1000,
y: 2722,
scaleX: 20,
scaleY: 1
}));
bottomFence.width = 2000;
bottomFence.height = 20;
buildings.push(bottomFence);
// Left fence
var leftFence = worldContainer.addChild(LK.getAsset('fence', {
anchorX: 0.5,
anchorY: 0.5,
x: 10,
y: 1400,
scaleX: 1,
scaleY: 135
}));
leftFence.width = 20;
leftFence.height = 2700;
buildings.push(leftFence);
// Right fence
var rightFence = worldContainer.addChild(LK.getAsset('fence', {
anchorX: 0.5,
anchorY: 0.5,
x: 2038,
y: 1400,
scaleX: 1,
scaleY: 135
}));
rightFence.width = 20;
rightFence.height = 2700;
buildings.push(rightFence);
}
function createNewOrder() {
if (currentOrder) {
return;
}
var availableRestaurants = restaurants.filter(function (r) {
return !r.hasOrder;
});
var availableCustomers = customers.filter(function (c) {
return !c.waitingForOrder;
});
if (availableRestaurants.length === 0 || availableCustomers.length === 0) {
return;
}
var restaurant = availableRestaurants[Math.floor(Math.random() * availableRestaurants.length)];
var customer = availableCustomers[Math.floor(Math.random() * availableCustomers.length)];
var order = worldContainer.addChild(new Order());
order.x = restaurant.x;
order.y = restaurant.y - 60;
order.restaurant = restaurant;
order.customer = customer;
order.claimed = false; // Allow AI couriers to claim this order
// Set random order color and match customer color
var orderColor = orderColors[Math.floor(Math.random() * orderColors.length)];
order.setColor(orderColor);
customer.setOrderColor(orderColor);
restaurant.hasOrder = true;
restaurant.orderReady = true;
customer.waitingForOrder = true;
customer.orderTime = 0;
// Show only this customer for player orders
customer.visible = true;
// Hide all other customers that might be visible from AI orders
for (var c = 0; c < customers.length; c++) {
if (customers[c] !== customer) {
customers[c].visible = false;
}
}
// Add "I'm here" text to the building that has the customer
var building = null;
// Find the building this customer belongs to (customers are positioned relative to buildings)
for (var b = 0; b < buildings.length; b++) {
var buildingCenterX = buildings[b].x;
var buildingCenterY = buildings[b].y;
var expectedCustomerX = buildingCenterX + buildings[b].width / 2 * 2.5 + 15;
var expectedCustomerY = buildingCenterY + buildings[b].height / 2 * 2.5 + 15;
if (Math.abs(customer.x - expectedCustomerX) < 5 && Math.abs(customer.y - expectedCustomerY) < 5) {
building = buildings[b];
break;
}
}
if (building && !building.customerText) {
var customerText = new Text2("I'm here", {
size: 10,
fill: 0xffffff,
stroke: 0x000000,
strokeThickness: 3
});
customerText.anchor.set(0.5, 0.5);
customerText.x = 0;
customerText.y = -30;
building.addChild(customerText);
building.customerText = customerText;
}
customer.deliveryTarget = null;
currentOrder = order;
orders.push(order);
gameState = 'pickup';
// Create pickup arrow indicator
if (pickupArrow) {
pickupArrow.destroy();
}
pickupArrow = game.addChild(new ArrowIndicator(restaurant.x, restaurant.y, 0x00ff00, 'pickup'));
}
function createAIOrder() {
var availableRestaurants = restaurants.filter(function (r) {
return !r.hasOrder;
});
var availableCustomers = customers.filter(function (c) {
return !c.waitingForOrder;
});
if (availableRestaurants.length === 0 || availableCustomers.length === 0) {
return;
}
var restaurant = availableRestaurants[Math.floor(Math.random() * availableRestaurants.length)];
var customer = availableCustomers[Math.floor(Math.random() * availableCustomers.length)];
var order = worldContainer.addChild(new Order());
order.x = restaurant.x;
order.y = restaurant.y - 60;
order.restaurant = restaurant;
order.customer = customer;
order.claimed = false; // For AI to claim
// Set random order color and match customer color
var orderColor = orderColors[Math.floor(Math.random() * orderColors.length)];
order.setColor(orderColor);
customer.setOrderColor(orderColor);
restaurant.hasOrder = true;
restaurant.orderReady = true;
customer.waitingForOrder = true;
customer.orderTime = 0;
// Don't show AI order customers - only player's customer should be visible
customer.visible = false;
// Add "I'm here" text to the building that has the customer
var building = null;
// Find the building this customer belongs to (customers are positioned relative to buildings)
for (var b = 0; b < buildings.length; b++) {
var buildingCenterX = buildings[b].x;
var buildingCenterY = buildings[b].y;
var expectedCustomerX = buildingCenterX + buildings[b].width / 2 * 2.5 + 15;
var expectedCustomerY = buildingCenterY + buildings[b].height / 2 * 2.5 + 15;
if (Math.abs(customer.x - expectedCustomerX) < 5 && Math.abs(customer.y - expectedCustomerY) < 5) {
building = buildings[b];
break;
}
}
if (building && !building.customerText) {
var customerText = new Text2("I'm here", {
size: 10,
fill: 0xffffff,
stroke: 0x000000,
strokeThickness: 3
});
customerText.anchor.set(0.5, 0.5);
customerText.x = 0;
customerText.y = -30;
building.addChild(customerText);
building.customerText = customerText;
}
customer.deliveryTarget = null;
aiOrders.push(order);
}
function spawnPowerUp() {
if (powerUps.length >= 3) {
return;
}
var type = Math.random() < 0.6 ? 'health' : 'time';
var powerUp = worldContainer.addChild(new PowerUp(type));
// Spawn power-ups on road positions between buildings
var roadPositions = [{
x: 200,
y: 220
}, {
x: 335,
y: 220
}, {
x: 470,
y: 220
}, {
x: 605,
y: 220
}, {
x: 740,
y: 220
}, {
x: 200,
y: 355
}, {
x: 335,
y: 355
}, {
x: 470,
y: 355
}, {
x: 605,
y: 355
}, {
x: 740,
y: 355
}, {
x: 200,
y: 490
}, {
x: 335,
y: 490
}, {
x: 470,
y: 490
}, {
x: 605,
y: 490
}, {
x: 740,
y: 490
}, {
x: 200,
y: 625
}, {
x: 335,
y: 625
}, {
x: 470,
y: 625
}, {
x: 605,
y: 625
}, {
x: 740,
y: 625
}];
var roadPos = roadPositions[Math.floor(Math.random() * roadPositions.length)];
powerUp.x = roadPos.x;
powerUp.y = roadPos.y;
powerUps.push(powerUp);
}
function updateUI() {
if (playerRole === 'courier') {
healthBar.setText('Health: ' + Math.floor(health));
moneyText.setText('$' + money);
if (hasInsurance) {
var timeLeft = Math.max(0, (insuranceEndTime - Date.now()) / 1000);
insuranceStatus.setText('Insurance: ' + timeLeft.toFixed(0) + 's');
insuranceStatus.visible = true;
} else {
insuranceStatus.visible = false;
}
if (currentOrder && gameState === 'delivery') {
var timeLeft = Math.max(0, currentOrder.timeRemaining / 1000);
orderTimer.setText('Deliver in: ' + timeLeft.toFixed(1) + 's');
} else {
orderTimer.setText('');
}
} else if (playerRole === 'police') {
healthBar.setText('Police Money: $' + courier.money);
moneyText.setText('Player: $' + money);
orderTimer.setText('');
insuranceStatus.visible = false;
} else if (playerRole === 'vandal') {
healthBar.setText('Vandal Money: $' + courier.money);
moneyText.setText('Player: $' + money);
orderTimer.setText('');
insuranceStatus.visible = false;
}
}
function checkCollisions() {
// Check courier-restaurant collision for pickup
if (gameState === 'pickup' && currentOrder) {
if (courier.intersects(currentOrder.restaurant)) {
// Pick up order
if (soundEnabled) {
LK.getSound('pickup').play();
// Play "thanks, drive safe" voice line
LK.setTimeout(function () {
LK.getSound('thanks_drive_safe').play();
}, 500);
}
courier.hasOrder = true;
gameState = 'delivery';
currentOrder.claimed = true; // Mark as claimed by player
currentOrder.x = courier.x;
currentOrder.y = courier.y - 40;
// Start delivery timer now that order is picked up
currentOrder.startDeliveryTimer(courier.speed);
// Remove pickup arrow and create delivery arrow
if (pickupArrow) {
pickupArrow.destroy();
pickupArrow = null;
}
if (deliveryArrow) {
deliveryArrow.destroy();
}
// Create delivery arrow pointing to customer location
deliveryArrow = game.addChild(new ArrowIndicator(currentOrder.customer.x, currentOrder.customer.y, 0xff0000, 'delivery'));
}
}
// Check courier-customer collision for delivery
if (gameState === 'delivery' && currentOrder && courier.hasOrder) {
// Check if courier intersects directly with the customer
if (courier.intersects(currentOrder.customer)) {
// Deliver order
if (soundEnabled) {
LK.getSound('delivery').play();
// Play "thank you, here's a tip" voice line
LK.setTimeout(function () {
LK.getSound('thank_you_tip').play();
}, 300);
// Play coin drop sound for tip
LK.setTimeout(function () {
LK.getSound('coin_drop').play();
}, 800);
}
completeDelivery();
}
}
// Check courier-powerup collisions
for (var i = powerUps.length - 1; i >= 0; i--) {
var powerUp = powerUps[i];
if (courier.intersects(powerUp)) {
if (soundEnabled) {
LK.getSound('powerup').play();
}
if (powerUp.type === 'health') {
health = Math.min(100, health + 25);
} else if (powerUp.type === 'time') {
timeFreezeActive = true;
timeFreezeRemaining = 3000; // 3 seconds
}
powerUp.destroy();
powerUps.splice(i, 1);
}
}
}
function completeDelivery() {
if (!currentOrder) {
return;
}
var deliveryTime = currentOrder.timeLimit - currentOrder.timeRemaining;
var bonus = 0;
if (currentOrder.timeRemaining > 0) {
// Successful delivery - calculate price based on distance
var dx = currentOrder.restaurant.x - currentOrder.customer.x;
var dy = currentOrder.restaurant.y - currentOrder.customer.y;
var distance = Math.sqrt(dx * dx + dy * dy);
var basePrice = Math.max(2, Math.min(10, Math.floor(distance / 100) + 2));
var timeBonus = Math.floor(currentOrder.timeRemaining / 1000);
bonus = basePrice + Math.min(timeBonus, 3); // Cap time bonus at $3
money += bonus;
// Flash green for success
LK.effects.flashObject(courier, 0x00ff00, 500);
} else {
// Late delivery - health penalty
health -= 20;
LK.effects.flashObject(courier, 0xff0000, 1000);
if (health <= 0) {
LK.showGameOver();
return;
}
}
// Clean up order
currentOrder.restaurant.hasOrder = false;
currentOrder.customer.waitingForOrder = false;
// Reset customer color to default
currentOrder.customer.setOrderColor(0x2196f3);
// Hide customer after delivery
currentOrder.customer.visible = false;
// Remove "I'm here" text from building
var customer = currentOrder.customer;
var building = null;
// Find the building this customer belongs to using the same logic as creation
for (var b = 0; b < buildings.length; b++) {
var buildingCenterX = buildings[b].x;
var buildingCenterY = buildings[b].y;
var expectedCustomerX = buildingCenterX + buildings[b].width / 2 * 2.5 + 15;
var expectedCustomerY = buildingCenterY + buildings[b].height / 2 * 2.5 + 15;
if (Math.abs(customer.x - expectedCustomerX) < 5 && Math.abs(customer.y - expectedCustomerY) < 5) {
building = buildings[b];
break;
}
}
if (building && building.customerText) {
building.customerText.destroy();
building.customerText = null;
}
currentOrder.destroy();
for (var i = orders.length - 1; i >= 0; i--) {
if (orders[i] === currentOrder) {
orders.splice(i, 1);
break;
}
}
currentOrder = null;
courier.hasOrder = false;
gameState = 'pickup';
// Clean up arrow indicators
if (deliveryArrow) {
deliveryArrow.destroy();
deliveryArrow = null;
}
// Create new order after short delay
LK.setTimeout(function () {
createNewOrder();
}, 1000);
}
// Create world container for scrolling
worldContainer = game.addChild(new Container());
// Scale up the world to zoom in more - this makes roads wider and couriers appear smaller
worldContainer.scaleX = 3;
worldContainer.scaleY = 3;
// Initialize game elements
createNeighborhood();
// Courier will be created after role selection
// Role selection handlers
courierButtonBg.down = function () {
selectRole('courier');
};
courierButton.down = function () {
selectRole('courier');
};
policeButtonBg.down = function () {
selectRole('police');
};
policeButton.down = function () {
selectRole('police');
};
vandalButtonBg.down = function () {
selectRole('vandal');
};
vandalButton.down = function () {
selectRole('vandal');
};
insuranceButton.down = function () {
buyInsurance();
};
godModeButton.down = function () {
toggleGodMode();
};
soundButton.down = function () {
toggleSound();
};
musicButton.down = function () {
toggleMusic();
};
function selectRole(role) {
playerRole = role;
storage.playerRole = role;
roleSelectionContainer.visible = false;
if (role === 'courier') {
insuranceButton.visible = true;
courier = worldContainer.addChild(new Courier());
courier.x = 1000;
courier.y = 1400;
} else if (role === 'police') {
courier = worldContainer.addChild(new Police());
courier.x = 1000;
courier.y = 1400;
} else if (role === 'vandal') {
courier = worldContainer.addChild(new Vandal());
courier.x = 1000;
courier.y = 1400;
}
gameStarted = true;
// Update button text to reflect current settings
soundButton.setText('Sound: ' + (soundEnabled ? 'ON' : 'OFF'));
musicButton.setText('Music: ' + (musicEnabled ? 'ON' : 'OFF'));
// Start background music only if enabled
if (musicEnabled) {
LK.playMusic('background_music');
}
if (role === 'courier') {
createNewOrder();
}
}
function buyInsurance() {
if (money >= 5 && !hasInsurance) {
money -= 5;
hasInsurance = true;
insuranceEndTime = Date.now() + 300000; // 5 minutes
insuranceButton.visible = false;
}
}
function toggleGodMode() {
godMode = !godMode;
godModeButton.setText('God Mode: ' + (godMode ? 'ON' : 'OFF'));
if (godMode) {
// Switch to god mode - zoom out to see full map
worldContainer.scaleX = godModeScale;
worldContainer.scaleY = godModeScale;
// Center camera on map center
cameraX = 1024 - 1024 / godModeScale;
cameraY = 1366 - 1366 / godModeScale;
} else {
// Switch back to normal mode
worldContainer.scaleX = normalScale;
worldContainer.scaleY = normalScale;
// Center camera back on courier if available
if (courier) {
cameraX = courier.x - 1024 / worldContainer.scaleX;
cameraY = courier.y - 1366 / worldContainer.scaleY;
}
}
}
function toggleSound() {
soundEnabled = !soundEnabled;
storage.soundEnabled = soundEnabled;
soundButton.setText('Sound: ' + (soundEnabled ? 'ON' : 'OFF'));
// Stop current siren if sound is disabled
if (!soundEnabled && sirenSound) {
sirenSound.stop();
sirenSound = null;
}
}
function toggleMusic() {
musicEnabled = !musicEnabled;
storage.musicEnabled = musicEnabled;
musicButton.setText('Music: ' + (musicEnabled ? 'ON' : 'OFF'));
if (musicEnabled) {
// Resume music if game has started
if (gameStarted) {
LK.playMusic('background_music');
}
} else {
// Stop music
LK.stopMusic();
}
}
// Touch controls
var targetX = 1000;
var targetY = 1400;
game.down = function (x, y, obj) {
if (godMode) {
// In god mode, don't set movement target, just allow camera control
return;
}
// Convert screen coordinates to world coordinates accounting for scale
targetX = x / worldContainer.scaleX + cameraX;
targetY = y / worldContainer.scaleY + cameraY;
};
game.move = function (x, y, obj) {
if (godMode) {
// In god mode, don't set movement target, just allow camera control
return;
}
// Convert screen coordinates to world coordinates accounting for scale
targetX = x / worldContainer.scaleX + cameraX;
targetY = y / worldContainer.scaleY + cameraY;
};
// Create NPCs with unified system
// Create 8 police - spawn from center of map
for (var i = 0; i < 8; i++) {
var policeNPC = worldContainer.addChild(new NPCPolice());
// Spawn police from center area of the map
var centerX = 1024;
var centerY = 1366;
var radius = 150;
var angle = i / 8 * Math.PI * 2;
policeNPC.x = centerX + Math.cos(angle) * radius;
policeNPC.y = centerY + Math.sin(angle) * radius;
policeNPC.npcType = 'police';
npcs.push(policeNPC);
}
// Create 6 vandals - spawn from 4 corners of map
var cornerPositions = [{
x: 100,
y: 200
},
// Top-left
{
x: 1900,
y: 200
},
// Top-right
{
x: 100,
y: 2500
},
// Bottom-left
{
x: 1900,
y: 2500
},
// Bottom-right
{
x: 100,
y: 1350
},
// Left middle
{
x: 1900,
y: 1350
} // Right middle
];
for (var i = 0; i < 6; i++) {
var vandalNPC = worldContainer.addChild(new NPCVandal());
var pos = cornerPositions[i];
vandalNPC.x = pos.x;
vandalNPC.y = pos.y;
vandalNPC.npcType = 'vandal';
npcs.push(vandalNPC);
}
// Create NPC couriers - spawn around buildings
for (var i = 0; i < 10; i++) {
var courierNPC = worldContainer.addChild(new NPCCourier());
// Spawn around buildings randomly
if (buildings.length > 0) {
var building = buildings[Math.floor(Math.random() * buildings.length)];
courierNPC.x = building.x + (Math.random() - 0.5) * 100;
courierNPC.y = building.y + (Math.random() - 0.5) * 100;
} else {
courierNPC.x = 200 + Math.random() * 1600;
courierNPC.y = 200 + Math.random() * 2300;
}
courierNPC.npcType = 'courier';
npcs.push(courierNPC);
}
// Create initial AI orders immediately and more frequently
LK.setTimeout(function () {
createAIOrder();
}, 100);
LK.setTimeout(function () {
createAIOrder();
}, 300);
LK.setTimeout(function () {
createAIOrder();
}, 600);
LK.setTimeout(function () {
createAIOrder();
}, 900);
// Spawn initial power-up
LK.setTimeout(function () {
spawnPowerUp();
}, 3000);
game.update = function () {
// Only run game logic if role is selected
if (!gameStarted || !courier) {
return;
}
// Update insurance status
if (hasInsurance && Date.now() > insuranceEndTime) {
hasInsurance = false;
if (playerRole === 'courier') {
insuranceButton.visible = true;
}
}
// Move courier toward target
courier.moveToward(targetX, targetY);
// Update camera system
if (godMode) {
// In god mode, allow free camera movement or keep centered on map
// Keep camera centered on the map center for full view
cameraX = 1024 - 1024 / worldContainer.scaleX;
cameraY = 1366 - 1366 / worldContainer.scaleY;
} else {
// Normal mode - follow courier
cameraX = courier.x - 1024 / worldContainer.scaleX; // Center courier horizontally accounting for scale
cameraY = courier.y - 1366 / worldContainer.scaleY; // Center courier vertically accounting for scale
}
// Apply camera offset to world container
worldContainer.x = -cameraX * worldContainer.scaleX;
worldContainer.y = -cameraY * worldContainer.scaleY;
// Update time freeze
if (timeFreezeActive) {
timeFreezeRemaining -= 16.67;
if (timeFreezeRemaining <= 0) {
timeFreezeActive = false;
}
}
// Update current order timer (if not frozen)
if (currentOrder && !timeFreezeActive) {
// Update time limit based on courier distance if picked up
if (courier.hasOrder && gameState === 'delivery') {
currentOrder.updateTimeLimit(courier.x, courier.y);
}
currentOrder.update();
// Move order with courier if picked up
if (courier.hasOrder && gameState === 'delivery') {
currentOrder.x = courier.x;
currentOrder.y = courier.y - 40;
}
// No delivery target flashing needed anymore - buildings show "I'm here" text instead
// Check for failed delivery
if (currentOrder.timeRemaining <= 0 && gameState === 'delivery') {
completeDelivery();
}
}
// Update customers
for (var i = 0; i < customers.length; i++) {
customers[i].update();
}
// Update all NPCs
for (var i = 0; i < npcs.length; i++) {
npcs[i].update();
}
// Check if player is being chased by police
if (playerRole === 'courier') {
isBeingChased = false;
// Check distance to all police NPCs
for (var i = 0; i < npcs.length; i++) {
var npc = npcs[i];
if (npc.npcType === 'police' && npc.target === courier) {
var dx = npc.x - courier.x;
var dy = npc.y - courier.y;
var distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= 50) {
// Within chase range
isBeingChased = true;
break;
}
}
}
// Handle siren sound based on chase state
if (isBeingChased && !lastChasedState) {
// Just started being chased - start siren
if (sirenSound) {
sirenSound.stop();
}
if (soundEnabled) {
sirenSound = LK.getSound('police_siren');
sirenSound.play();
}
} else if (!isBeingChased && lastChasedState) {
// No longer being chased - stop siren
if (sirenSound) {
sirenSound.stop();
sirenSound = null;
}
}
lastChasedState = isBeingChased;
}
// Create new AI orders periodically - more frequently and higher limit
if (LK.ticks % 90 === 0 && aiOrders.length < 12) {
// Every 1.5 seconds, maintain up to 12 AI orders
createAIOrder();
}
// Create additional AI orders periodically
if (LK.ticks % 90 === 0 && aiOrders.length < 8) {
createAIOrder();
}
// Update power-ups
for (var i = powerUps.length - 1; i >= 0; i--) {
var powerUp = powerUps[i];
powerUp.update();
if (powerUp.lifeTime >= powerUp.maxLifeTime) {
powerUp.destroy();
powerUps.splice(i, 1);
}
}
// Spawn new power-ups occasionally
if (LK.ticks % 600 === 0) {
// Every 10 seconds
spawnPowerUp();
}
// Check collisions
checkCollisions();
// Update arrow indicators
if (pickupArrow) {
pickupArrow.update();
}
if (deliveryArrow) {
deliveryArrow.update();
}
// Update UI
updateUI();
// Game is now endless - no win condition
};
ıts a courier scooter its looking behind . No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
police car with flashing lights. In-Game asset. 2d. High contrast. No shadows
vandal riding dirt bike. In-Game asset. High contrast. No shadows
health powerup. In-Game asset. 2d. High contrast. No shadows
pizza boxes. In-Game asset. 2d. High contrast. No shadows
extra time powerup. In-Game asset. 2d. High contrast. No shadows
restaurant shop. In-Game asset. High contrast. No shadows
town house. In-Game asset. 2d. High contrast. No shadows
a man waiting outside. In-Game asset. 2d. High contrast. No shadows
arrow. In-Game asset. 2d. High contrast. No shadows
fence. In-Game asset. 2d. High contrast. No shadows