/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Ball (player) class
var Ball = Container.expand(function () {
var self = Container.call(this);
var ballGfx = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.7,
scaleY: 1.7
});
self.radius = ballGfx.width / 2;
self.vx = 0;
self.vy = 0;
self.launched = false;
self.resting = true; // true if waiting for launch
self.bounceCooldown = 0; // prevent double bounces
self.health = 5; // Lives per launch
self.update = function () {
if (self.launched) {
// Gravity
self.vy += 0.1344;
// Move
self.x += self.vx;
self.y += self.vy;
// Friction (air)
self.vx *= 0.995;
// Clamp to game area
if (self.x < self.radius) self.x = self.radius;
if (self.x > 2048 - self.radius) self.x = 2048 - self.radius;
// If falls below screen, game over
if (self.y > 2732 + 200) {
LK.effects.flashScreen(0xff0000, 800);
LK.showGameOver();
}
}
};
// Reset ball to start
self.reset = function () {
self.x = 2048 / 2;
self.y = typeof ballStartY !== "undefined" ? ballStartY : 2732 - 180;
self.vx = 0;
self.vy = 0;
self.launched = false;
self.resting = true;
self.bounceCooldown = 0;
self.health = 5; // Reset health for new launch
};
// Bounce up (from bouncy car)
self.bounce = function (strength) {
self.vy = -Math.abs(strength || 38 + Math.random() * 8);
self.launched = true;
self.resting = false;
self.bounceCooldown = 10;
};
return self;
});
// BouncyCar (bouncy vehicle) class
var BouncyCar = Container.expand(function () {
var self = Container.call(this);
var bouncyGfx = self.attachAsset('bouncycar', {
anchorX: 0.5,
anchorY: 0.5
});
self.scale.set(0.72, 0.72);
self.width = bouncyGfx.width * 0.72;
self.height = bouncyGfx.height * 0.72;
self.speed = 0;
self.lane = 0;
self.type = 'bouncycar';
self.bouncePhase = Math.random() * Math.PI * 2;
self.update = function () {
self.x += self.speed;
// Bouncy up-down
self.y0 = self.y0 || self.y;
self.y = self.y0 + Math.sin(LK.ticks / 12 + self.bouncePhase) * 38;
if (self.speed > 0 && self.x > 2048 + 200) self.destroyed = true;
if (self.speed < 0 && self.x < -200) self.destroyed = true;
};
return self;
});
// Car (normal) class
var Car = Container.expand(function () {
var self = Container.call(this);
var carGfx = self.attachAsset('car', {
anchorX: 0.5,
anchorY: 0.5
});
self.scale.set(0.72, 0.72);
self.width = carGfx.width * 0.72;
self.height = carGfx.height * 0.72;
self.speed = 0;
self.lane = 0;
self.type = 'car';
self.hitCooldown = 0; // Cooldown after being hit by ball
self.update = function () {
self.x += self.speed;
// Update hit cooldown
if (self.hitCooldown > 0) self.hitCooldown--;
// Remove if out of screen
if (self.speed > 0 && self.x > 2048 + 200) self.destroyed = true;
if (self.speed < 0 && self.x < -200) self.destroyed = true;
};
return self;
});
// Motorcycle class
var Motorcycle = Container.expand(function () {
var self = Container.call(this);
var motorcycleGfx = self.attachAsset('moto', {
anchorX: 0.5,
anchorY: 0.5
});
self.scale.set(0.58, 0.58); // Smaller than cars (0.72 * 0.8)
self.width = motorcycleGfx.width * 0.58;
self.height = motorcycleGfx.height * 0.58;
self.speed = 0;
self.lane = 0;
self.type = 'motorcycle';
self.hitCooldown = 0; // Cooldown after being hit by ball
self.hasJumped = false; // Only jump once
self.lastJumpCheck = 0;
self.isJumping = false;
self.originalY = 0;
self.originalScale = 0.58;
self.update = function () {
self.x += self.speed;
// Update hit cooldown
if (self.hitCooldown > 0) self.hitCooldown--;
// Jump logic - check every second (60 ticks) with 20% chance
if (!self.hasJumped && !self.isJumping && LK.ticks - self.lastJumpCheck >= 60) {
self.lastJumpCheck = LK.ticks;
if (Math.random() < 0.2) {
self.performJump();
}
}
// Remove if out of screen
if (self.speed > 0 && self.x > 2048 + 200) self.destroyed = true;
if (self.speed < 0 && self.x < -200) self.destroyed = true;
};
self.performJump = function () {
if (self.hasJumped || self.isJumping) return;
self.isJumping = true;
self.hasJumped = true;
self.originalY = self.y;
// Reduce speed by 20% during jump
self.originalSpeed = self.speed;
self.speed = self.speed * 0.8;
// Determine target lane based on current lane
var targetLane;
if (self.lane === 2) {
// Lane 3 (index 2)
targetLane = Math.random() < 0.5 ? 0 : 4; // Jump to lane 1 or 5 (index 0 or 4)
} else if (self.lane === 3) {
// Lane 4 (index 3)
targetLane = Math.random() < 0.5 ? 1 : 5; // Jump to lane 2 or 6 (index 1 or 5)
}
var targetY = laneYs[targetLane];
// Calculate rotation angle based on jump direction
var rotationAngle = 0;
if (targetLane < self.lane) {
rotationAngle = self.speed > 0 ? -0.3 : 0.3; // Upward jump
} else {
rotationAngle = self.speed > 0 ? 0.3 : -0.3; // Downward jump
}
// Enhanced jump animation with parabolic trajectory and realistic scaling
var jumpDuration = 1500;
var peakScale = self.originalScale * 2.2; // Increased scale for more dramatic effect
var halfDuration = jumpDuration / 2;
// First half: jump up with scale increase (ascending arc)
tween(self, {
y: self.originalY + (targetY - self.originalY) * 0.5,
// Halfway point
scaleX: peakScale,
scaleY: peakScale,
rotation: rotationAngle * 0.7 // Partial rotation during ascent
}, {
duration: halfDuration,
easing: tween.easeOut,
// Slow down as it reaches peak
onFinish: function onFinish() {
// Second half: descend with scale decrease (descending arc)
tween(self, {
y: targetY,
scaleX: self.originalScale,
scaleY: self.originalScale,
rotation: rotationAngle
}, {
duration: halfDuration,
easing: tween.easeIn,
// Speed up as it falls
onFinish: function onFinish() {
// Final landing animation - quick rotation reset
tween(self, {
rotation: 0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
self.isJumping = false;
// Restore original speed
self.speed = self.originalSpeed;
}
});
}
});
}
});
// Update lane
self.lane = targetLane;
};
return self;
});
// Truck (long vehicle) class
var Truck = Container.expand(function () {
var self = Container.call(this);
var truckGfx = self.attachAsset('truck', {
anchorX: 0.5,
anchorY: 0.5
});
self.scale.set(0.72, 0.72);
self.width = truckGfx.width * 0.72;
self.height = truckGfx.height * 0.72;
self.speed = 0;
self.lane = 0;
self.type = 'truck';
self.hitCooldown = 0; // Cooldown after being hit by ball
self.update = function () {
self.x += self.speed;
// Update hit cooldown
if (self.hitCooldown > 0) self.hitCooldown--;
if (self.speed > 0 && self.x > 2048 + 300) self.destroyed = true;
if (self.speed < 0 && self.x < -300) self.destroyed = true;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222222
});
/****
* Game Code
****/
// Slingshot band
// Bouncy car
// Truck
// Car
// Lane divider
// Road background
// Stop (target)
// Ball (player)
// --- Game variables ---
var lanes = [0, 1, 2, 3, 4, 5];
var laneYs = [];
var laneCount = 6;
var verticalOffset = -200; // Move everything up by 200px
var roadTop = 600 + verticalOffset;
var roadHeight = 1536;
var laneGap = roadHeight / laneCount;
for (var i = 0; i < laneCount; ++i) {
laneYs[i] = roadTop + laneGap / 2 + i * laneGap;
}
// Place stop (target) at the top of the road, centered horizontally
var stopY = roadTop - 100;
var stopX = 2048 / 2;
// Update stop trigger area to match new stop (target) width
var stopRadius = {
x: 158,
// half of stop image width (316/2), matches texture
y: 90
}; // x: horizontal radius, y: vertical radius
var level = 1;
var vehicles = [];
var ball = null;
var stop = null;
var slingshotBand = null;
var isDragging = false;
// Place the ball higher up (not at the very bottom)
var ballStartY = roadTop + roadHeight + 120;
var dragStart = {
x: 0,
y: 0
};
var dragCurrent = {
x: 0,
y: 0
};
var launchPower = 0;
var launchAngle = 0;
var canLaunch = true;
var scoreTxt = null;
var levelTxt = null;
var winTimeout = null;
// --- Draw road and lanes ---
var road = LK.getAsset('road', {
x: 0,
y: roadTop,
width: 2048,
height: roadHeight
});
game.addChild(road);
// Lane dividers
for (var i = 1; i < laneCount; ++i) {
var divider = LK.getAsset('divider', {
x: 0,
y: roadTop + i * laneGap - 12,
width: 2048,
height: 24
});
game.addChild(divider);
}
// --- Stop (target) ---
stop = LK.getAsset('stop', {
anchorX: 0.5,
anchorY: 0.5,
x: stopX,
y: stopY,
scaleX: 1.2,
scaleY: 1.2
});
game.addChild(stop);
// --- Ball (player) ---
ball = new Ball();
ball.scale.set(0.9, 0.9);
game.addChild(ball);
ball.reset();
// --- Slingshot band (visual) ---
slingshotBand = LK.getAsset('band', {
anchorX: 0.5,
anchorY: 1,
x: ball.x,
y: ball.y,
scaleX: 0.7,
scaleY: 0.7
});
slingshotBand.visible = false;
game.addChild(slingshotBand);
// --- GUI: Score and Level ---
scoreTxt = new Text2('0', {
size: 110,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
levelTxt = new Text2('1', {
size: 70,
fill: "#fff"
});
levelTxt.anchor.set(0.5, 0);
LK.gui.topRight.addChild(levelTxt);
// Health display
var healthTxt = new Text2('♥ 5', {
size: 80,
fill: 0xFF4444
});
healthTxt.anchor.set(1, 0);
LK.gui.topRight.addChild(healthTxt);
healthTxt.x = -120; // Position to the left of level text
// --- Game state ---
function resetGameState() {
// Remove vehicles
for (var i = 0; i < vehicles.length; ++i) {
vehicles[i].destroy();
}
vehicles = [];
// Reset ball
ball.reset();
// Update health display
healthTxt.setText('♥ ' + ball.health);
// Reset slingshot
slingshotBand.visible = false;
isDragging = false;
canLaunch = true;
// Reset score/level
LK.setScore(0);
scoreTxt.setText('0');
level = 1;
levelTxt.setText('1');
}
resetGameState();
// --- Vehicle spawner ---
function spawnVehicle() {
// Check if we should spawn a motorcycle (only in lanes 2 and 3, which are indices 2 and 3)
var isMotorcycleSpawn = Math.random() < 0.15; // 15% chance for motorcycle
var lane;
var y;
var v, dir, speed;
if (isMotorcycleSpawn) {
// Motorcycles only spawn in lanes 3 and 4 (indices 2 and 3)
lane = Math.random() < 0.5 ? 2 : 3;
y = laneYs[lane];
v = new Motorcycle();
speed = (10 + level * 1.0 + Math.random() * 2) * 0.5 * 0.9; // Slightly slower than cars
} else {
// Randomly pick lane for other vehicles
lane = Math.floor(Math.random() * laneCount);
y = laneYs[lane];
// Adjusted spawn probabilities: All vehicle types reduced by 5%
// Old: Car 60%, Truck 25%, BouncyCar 15%
// New: Car 55%, Truck 20%, BouncyCar 10%
var t = Math.random();
if (t < 0.55) {
v = new Car();
speed = (12 + level * 1.2 + Math.random() * 2) * 0.5 * 0.95;
} else if (t < 0.75) {
v = new Truck();
speed = (8 + level * 1.1 + Math.random() * 2) * 0.5 * 0.9 * 0.95; // Truck speed reduced by 10% then by 5%
} else {
v = new BouncyCar();
speed = (10 + level * 1.3 + Math.random() * 2) * 0.5 * 1.1 * 0.95; // BouncyCar speed increased by 10% then reduced by 5%
}
}
// Direction: even lanes left->right, odd right->left
dir = lane % 2 === 0 ? 1 : -1;
v.lane = lane;
v.y = y;
v.y0 = y;
v.speed = speed * dir;
if (dir > 0) {
v.x = -200;
} else {
v.x = 2048 + 200;
// Flip the vehicle horizontally if moving right-to-left
if (v.children && v.children.length > 0 && v.children[0]) {
v.children[0].scale.x = -1;
}
}
vehicles.push(v);
game.addChild(v);
}
// --- Level progression ---
function nextLevel() {
level += 1;
levelTxt.setText(level + '');
// Remove all vehicles
for (var i = 0; i < vehicles.length; ++i) vehicles[i].destroy();
vehicles = [];
// Reset ball
ball.reset();
// Update health display
healthTxt.setText('♥ ' + ball.health);
canLaunch = true;
slingshotBand.visible = false;
}
// --- Game update ---
game.update = function () {
// Ball update
ball.update();
// Vehicles update
// --- Make vehicles follow the speed of the vehicle in front if they are too close in the same lane ---
for (var i = vehicles.length - 1; i >= 0; --i) {
var v = vehicles[i];
// Find the closest vehicle ahead in the same lane and direction
var minDist = Infinity;
var frontVehicle = null;
for (var j = 0; j < vehicles.length; ++j) {
if (i === j) continue;
var v2 = vehicles[j];
if (v2.lane !== v.lane) continue;
// Only consider vehicles in the same direction
if (v.speed > 0 && v2.x > v.x || v.speed < 0 && v2.x < v.x) {
var dist = Math.abs(v2.x - v.x);
if (dist < minDist) {
minDist = dist;
frontVehicle = v2;
}
}
}
// If too close, match speed to the front vehicle
if (frontVehicle) {
// Use the larger of the two widths for minimum gap
var minGap = Math.max(v.width, frontVehicle.width) * 0.85;
if (minDist < minGap) {
v.speed = frontVehicle.speed;
// Snap position to avoid overlap
if (v.speed > 0) {
v.x = Math.min(v.x, frontVehicle.x - minGap + 1);
} else {
v.x = Math.max(v.x, frontVehicle.x + minGap - 1);
}
}
}
v.update();
// Remove destroyed
if (v.destroyed) {
v.destroy();
vehicles.splice(i, 1);
continue;
}
}
// Ball-vehicle collision
if (ball.launched && ball.bounceCooldown <= 0) {
for (var i = 0; i < vehicles.length; ++i) {
var v = vehicles[i];
// Skip if vehicle is in hit cooldown
if (v.hitCooldown > 0) continue;
// AABB collision
var dx = Math.abs(ball.x - v.x);
var dy = Math.abs(ball.y - v.y);
var overlapX = dx < ball.radius + v.width / 2 - 10;
var overlapY = dy < ball.radius + v.height / 2 - 10;
if (overlapX && overlapY) {
if (v.type === 'bouncycar') {
// Calculate the incoming angle and speed
var incomingAngle = Math.atan2(ball.vy, ball.vx);
var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
// Reverse the angle (go in the exact opposite direction)
var baseBounceAngle = incomingAngle + Math.PI;
// Add up to ±10% randomness to the angle
var randomAngle = (Math.random() - 0.5) * (Math.PI * 0.2); // ±0.1π ≈ ±18°
var bounceAngle = baseBounceAngle + randomAngle;
// Bounce speed is 120% of the incoming speed
var bounceSpeed = speed * 1.2;
// Set new velocity
ball.vx = Math.cos(bounceAngle) * bounceSpeed;
ball.vy = Math.sin(bounceAngle) * bounceSpeed;
ball.launched = true;
ball.resting = false;
ball.bounceCooldown = 12;
LK.effects.flashObject(v, 0xffff00, 400);
// Do NOT randomize X anymore, keep physics consistent
// After bounce, check if ball is immediately on stop (target)
var dxStop = ball.x - stop.x;
var dyStop = ball.y - stop.y;
// Use ellipse collision: (dx/a)^2 + (dy/b)^2 < 1
if (dxStop * dxStop / ((stopRadius.x + ball.radius - 10) * (stopRadius.x + ball.radius - 10)) + dyStop * dyStop / ((stopRadius.y + ball.radius - 10) * (stopRadius.y + ball.radius - 10)) < 1 && Math.abs(ball.vy) < 18) {
// Win!
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore() + '');
LK.effects.flashObject(stop, 0x00ff00, 600);
canLaunch = false;
ball.launched = false;
ball.resting = true;
// Randomize stop X position (avoid edges)
var stopMargin = 180 + stopRadius.x;
stop.x = stopMargin + Math.random() * (2048 - 2 * stopMargin);
// Next level after short delay
if (winTimeout) LK.clearTimeout(winTimeout);
winTimeout = LK.setTimeout(function () {
nextLevel();
}, 1200);
return;
}
} else {
// Vehicle collision - reduce health and deflect ball
ball.health -= 1;
healthTxt.setText('♥ ' + ball.health);
// Set vehicle hit cooldown (2 seconds = 120 ticks at 60fps)
v.hitCooldown = 120;
// Flash the vehicle red to indicate hit
LK.effects.flashObject(v, 0xff0000, 500);
// New collision logic: Average of ball's vector and vehicle's vector.
// Vehicle's velocity vector is (v.speed, 0) as it only moves horizontally.
// Ball's current velocity vector before collision is (ball.vx, ball.vy).
// Calculate the average vector components:
// new_vx = (ball.vx + vehicle.vx) / 2
// new_vy = (ball.vy + vehicle.vy) / 2
// Since vehicle.vy is 0:
// New formula: (ball_speed + 3 * vehicle_speed) / 1.20
var avgVx = (ball.vx + 3 * v.speed) / 1.20;
var avgVy = (ball.vy + 3 * 0) / 1.20; // vehicle.vy is 0
// Apply the new averaged velocity to the ball
ball.vx = avgVx;
ball.vy = avgVy;
// Set bounce cooldown to prevent multiple hits
ball.bounceCooldown = 8;
// Check if health is depleted
if (ball.health <= 0) {
// Game over
LK.effects.flashScreen(0xff0000, 800);
LK.showGameOver();
return;
}
}
}
}
}
if (ball.bounceCooldown > 0) ball.bounceCooldown--;
// Ball-stop (target) collision
if (ball.launched && !ball.resting) {
var dx = ball.x - stop.x;
var dy = ball.y - stop.y;
// Use ellipse collision: (dx/a)^2 + (dy/b)^2 < 1
if (dx * dx / ((stopRadius.x + ball.radius - 10) * (stopRadius.x + ball.radius - 10)) + dy * dy / ((stopRadius.y + ball.radius - 10) * (stopRadius.y + ball.radius - 10)) < 1 && Math.abs(ball.vy) < 18) {
// Win!
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore() + '');
LK.effects.flashObject(stop, 0x00ff00, 600);
canLaunch = false;
ball.launched = false;
ball.resting = true;
// Randomize stop X position (avoid edges)
var margin = 180 + stopRadius.x;
stop.x = margin + Math.random() * (2048 - 2 * margin);
// Next level after short delay
if (winTimeout) LK.clearTimeout(winTimeout);
winTimeout = LK.setTimeout(function () {
nextLevel();
}, 1200);
}
}
// Vehicle spawn logic - reduced by 30% total (20% + 10%)
if (LK.ticks % Math.max(52 - Math.min(level * 2, 30), 13) === 0) {
spawnVehicle();
}
};
// --- Slingshot controls ---
function getDragVector() {
var dx = dragCurrent.x - dragStart.x;
var dy = dragCurrent.y - dragStart.y;
return {
dx: dx,
dy: dy
};
}
function updateSlingshotBand() {
if (!isDragging) {
slingshotBand.visible = false;
return;
}
var vec = getDragVector();
var len = Math.sqrt(vec.dx * vec.dx + vec.dy * vec.dy);
var maxLen = 420;
if (len > maxLen) {
vec.dx *= maxLen / len;
vec.dy *= maxLen / len;
len = maxLen;
}
// Band position: from ball center to drag point
slingshotBand.visible = true;
slingshotBand.x = ball.x;
slingshotBand.y = ball.y;
slingshotBand.height = len;
slingshotBand.rotation = Math.atan2(vec.dy, vec.dx) + Math.PI / 2;
}
// --- Input handlers ---
game.down = function (x, y, obj) {
// Only allow drag if ball is at rest and not in win state
if (!canLaunch || !ball.resting) return;
// Only allow drag if touch is near ball
var dx = x - ball.x;
var dy = y - ball.y;
if (dx * dx + dy * dy < ball.radius * ball.radius * 2.2) {
isDragging = true;
dragStart.x = ball.x;
dragStart.y = ball.y;
dragCurrent.x = x;
dragCurrent.y = y;
updateSlingshotBand();
}
};
game.move = function (x, y, obj) {
if (!isDragging) return;
dragCurrent.x = x;
dragCurrent.y = y;
updateSlingshotBand();
};
game.up = function (x, y, obj) {
if (!isDragging) return;
isDragging = false;
slingshotBand.visible = false;
// Launch!
var vec = getDragVector();
var len = Math.sqrt(vec.dx * vec.dx + vec.dy * vec.dy);
if (len < 60) return; // too short
var maxLen = 420;
if (len > maxLen) {
vec.dx *= maxLen / len;
vec.dy *= maxLen / len;
len = maxLen;
}
// Set velocity (scaled)
ball.vx = -vec.dx * 0.055;
ball.vy = -vec.dy * 0.055;
ball.launched = true;
ball.resting = false;
canLaunch = false;
};
// --- Game over/win handling ---
LK.on('gameover', function () {
resetGameState();
});
LK.on('youwin', function () {
resetGameState();
}); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
/****
* Classes
****/
// Ball (player) class
var Ball = Container.expand(function () {
var self = Container.call(this);
var ballGfx = self.attachAsset('ball', {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 1.7,
scaleY: 1.7
});
self.radius = ballGfx.width / 2;
self.vx = 0;
self.vy = 0;
self.launched = false;
self.resting = true; // true if waiting for launch
self.bounceCooldown = 0; // prevent double bounces
self.health = 5; // Lives per launch
self.update = function () {
if (self.launched) {
// Gravity
self.vy += 0.1344;
// Move
self.x += self.vx;
self.y += self.vy;
// Friction (air)
self.vx *= 0.995;
// Clamp to game area
if (self.x < self.radius) self.x = self.radius;
if (self.x > 2048 - self.radius) self.x = 2048 - self.radius;
// If falls below screen, game over
if (self.y > 2732 + 200) {
LK.effects.flashScreen(0xff0000, 800);
LK.showGameOver();
}
}
};
// Reset ball to start
self.reset = function () {
self.x = 2048 / 2;
self.y = typeof ballStartY !== "undefined" ? ballStartY : 2732 - 180;
self.vx = 0;
self.vy = 0;
self.launched = false;
self.resting = true;
self.bounceCooldown = 0;
self.health = 5; // Reset health for new launch
};
// Bounce up (from bouncy car)
self.bounce = function (strength) {
self.vy = -Math.abs(strength || 38 + Math.random() * 8);
self.launched = true;
self.resting = false;
self.bounceCooldown = 10;
};
return self;
});
// BouncyCar (bouncy vehicle) class
var BouncyCar = Container.expand(function () {
var self = Container.call(this);
var bouncyGfx = self.attachAsset('bouncycar', {
anchorX: 0.5,
anchorY: 0.5
});
self.scale.set(0.72, 0.72);
self.width = bouncyGfx.width * 0.72;
self.height = bouncyGfx.height * 0.72;
self.speed = 0;
self.lane = 0;
self.type = 'bouncycar';
self.bouncePhase = Math.random() * Math.PI * 2;
self.update = function () {
self.x += self.speed;
// Bouncy up-down
self.y0 = self.y0 || self.y;
self.y = self.y0 + Math.sin(LK.ticks / 12 + self.bouncePhase) * 38;
if (self.speed > 0 && self.x > 2048 + 200) self.destroyed = true;
if (self.speed < 0 && self.x < -200) self.destroyed = true;
};
return self;
});
// Car (normal) class
var Car = Container.expand(function () {
var self = Container.call(this);
var carGfx = self.attachAsset('car', {
anchorX: 0.5,
anchorY: 0.5
});
self.scale.set(0.72, 0.72);
self.width = carGfx.width * 0.72;
self.height = carGfx.height * 0.72;
self.speed = 0;
self.lane = 0;
self.type = 'car';
self.hitCooldown = 0; // Cooldown after being hit by ball
self.update = function () {
self.x += self.speed;
// Update hit cooldown
if (self.hitCooldown > 0) self.hitCooldown--;
// Remove if out of screen
if (self.speed > 0 && self.x > 2048 + 200) self.destroyed = true;
if (self.speed < 0 && self.x < -200) self.destroyed = true;
};
return self;
});
// Motorcycle class
var Motorcycle = Container.expand(function () {
var self = Container.call(this);
var motorcycleGfx = self.attachAsset('moto', {
anchorX: 0.5,
anchorY: 0.5
});
self.scale.set(0.58, 0.58); // Smaller than cars (0.72 * 0.8)
self.width = motorcycleGfx.width * 0.58;
self.height = motorcycleGfx.height * 0.58;
self.speed = 0;
self.lane = 0;
self.type = 'motorcycle';
self.hitCooldown = 0; // Cooldown after being hit by ball
self.hasJumped = false; // Only jump once
self.lastJumpCheck = 0;
self.isJumping = false;
self.originalY = 0;
self.originalScale = 0.58;
self.update = function () {
self.x += self.speed;
// Update hit cooldown
if (self.hitCooldown > 0) self.hitCooldown--;
// Jump logic - check every second (60 ticks) with 20% chance
if (!self.hasJumped && !self.isJumping && LK.ticks - self.lastJumpCheck >= 60) {
self.lastJumpCheck = LK.ticks;
if (Math.random() < 0.2) {
self.performJump();
}
}
// Remove if out of screen
if (self.speed > 0 && self.x > 2048 + 200) self.destroyed = true;
if (self.speed < 0 && self.x < -200) self.destroyed = true;
};
self.performJump = function () {
if (self.hasJumped || self.isJumping) return;
self.isJumping = true;
self.hasJumped = true;
self.originalY = self.y;
// Reduce speed by 20% during jump
self.originalSpeed = self.speed;
self.speed = self.speed * 0.8;
// Determine target lane based on current lane
var targetLane;
if (self.lane === 2) {
// Lane 3 (index 2)
targetLane = Math.random() < 0.5 ? 0 : 4; // Jump to lane 1 or 5 (index 0 or 4)
} else if (self.lane === 3) {
// Lane 4 (index 3)
targetLane = Math.random() < 0.5 ? 1 : 5; // Jump to lane 2 or 6 (index 1 or 5)
}
var targetY = laneYs[targetLane];
// Calculate rotation angle based on jump direction
var rotationAngle = 0;
if (targetLane < self.lane) {
rotationAngle = self.speed > 0 ? -0.3 : 0.3; // Upward jump
} else {
rotationAngle = self.speed > 0 ? 0.3 : -0.3; // Downward jump
}
// Enhanced jump animation with parabolic trajectory and realistic scaling
var jumpDuration = 1500;
var peakScale = self.originalScale * 2.2; // Increased scale for more dramatic effect
var halfDuration = jumpDuration / 2;
// First half: jump up with scale increase (ascending arc)
tween(self, {
y: self.originalY + (targetY - self.originalY) * 0.5,
// Halfway point
scaleX: peakScale,
scaleY: peakScale,
rotation: rotationAngle * 0.7 // Partial rotation during ascent
}, {
duration: halfDuration,
easing: tween.easeOut,
// Slow down as it reaches peak
onFinish: function onFinish() {
// Second half: descend with scale decrease (descending arc)
tween(self, {
y: targetY,
scaleX: self.originalScale,
scaleY: self.originalScale,
rotation: rotationAngle
}, {
duration: halfDuration,
easing: tween.easeIn,
// Speed up as it falls
onFinish: function onFinish() {
// Final landing animation - quick rotation reset
tween(self, {
rotation: 0
}, {
duration: 200,
easing: tween.easeOut,
onFinish: function onFinish() {
self.isJumping = false;
// Restore original speed
self.speed = self.originalSpeed;
}
});
}
});
}
});
// Update lane
self.lane = targetLane;
};
return self;
});
// Truck (long vehicle) class
var Truck = Container.expand(function () {
var self = Container.call(this);
var truckGfx = self.attachAsset('truck', {
anchorX: 0.5,
anchorY: 0.5
});
self.scale.set(0.72, 0.72);
self.width = truckGfx.width * 0.72;
self.height = truckGfx.height * 0.72;
self.speed = 0;
self.lane = 0;
self.type = 'truck';
self.hitCooldown = 0; // Cooldown after being hit by ball
self.update = function () {
self.x += self.speed;
// Update hit cooldown
if (self.hitCooldown > 0) self.hitCooldown--;
if (self.speed > 0 && self.x > 2048 + 300) self.destroyed = true;
if (self.speed < 0 && self.x < -300) self.destroyed = true;
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222222
});
/****
* Game Code
****/
// Slingshot band
// Bouncy car
// Truck
// Car
// Lane divider
// Road background
// Stop (target)
// Ball (player)
// --- Game variables ---
var lanes = [0, 1, 2, 3, 4, 5];
var laneYs = [];
var laneCount = 6;
var verticalOffset = -200; // Move everything up by 200px
var roadTop = 600 + verticalOffset;
var roadHeight = 1536;
var laneGap = roadHeight / laneCount;
for (var i = 0; i < laneCount; ++i) {
laneYs[i] = roadTop + laneGap / 2 + i * laneGap;
}
// Place stop (target) at the top of the road, centered horizontally
var stopY = roadTop - 100;
var stopX = 2048 / 2;
// Update stop trigger area to match new stop (target) width
var stopRadius = {
x: 158,
// half of stop image width (316/2), matches texture
y: 90
}; // x: horizontal radius, y: vertical radius
var level = 1;
var vehicles = [];
var ball = null;
var stop = null;
var slingshotBand = null;
var isDragging = false;
// Place the ball higher up (not at the very bottom)
var ballStartY = roadTop + roadHeight + 120;
var dragStart = {
x: 0,
y: 0
};
var dragCurrent = {
x: 0,
y: 0
};
var launchPower = 0;
var launchAngle = 0;
var canLaunch = true;
var scoreTxt = null;
var levelTxt = null;
var winTimeout = null;
// --- Draw road and lanes ---
var road = LK.getAsset('road', {
x: 0,
y: roadTop,
width: 2048,
height: roadHeight
});
game.addChild(road);
// Lane dividers
for (var i = 1; i < laneCount; ++i) {
var divider = LK.getAsset('divider', {
x: 0,
y: roadTop + i * laneGap - 12,
width: 2048,
height: 24
});
game.addChild(divider);
}
// --- Stop (target) ---
stop = LK.getAsset('stop', {
anchorX: 0.5,
anchorY: 0.5,
x: stopX,
y: stopY,
scaleX: 1.2,
scaleY: 1.2
});
game.addChild(stop);
// --- Ball (player) ---
ball = new Ball();
ball.scale.set(0.9, 0.9);
game.addChild(ball);
ball.reset();
// --- Slingshot band (visual) ---
slingshotBand = LK.getAsset('band', {
anchorX: 0.5,
anchorY: 1,
x: ball.x,
y: ball.y,
scaleX: 0.7,
scaleY: 0.7
});
slingshotBand.visible = false;
game.addChild(slingshotBand);
// --- GUI: Score and Level ---
scoreTxt = new Text2('0', {
size: 110,
fill: "#fff"
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
levelTxt = new Text2('1', {
size: 70,
fill: "#fff"
});
levelTxt.anchor.set(0.5, 0);
LK.gui.topRight.addChild(levelTxt);
// Health display
var healthTxt = new Text2('♥ 5', {
size: 80,
fill: 0xFF4444
});
healthTxt.anchor.set(1, 0);
LK.gui.topRight.addChild(healthTxt);
healthTxt.x = -120; // Position to the left of level text
// --- Game state ---
function resetGameState() {
// Remove vehicles
for (var i = 0; i < vehicles.length; ++i) {
vehicles[i].destroy();
}
vehicles = [];
// Reset ball
ball.reset();
// Update health display
healthTxt.setText('♥ ' + ball.health);
// Reset slingshot
slingshotBand.visible = false;
isDragging = false;
canLaunch = true;
// Reset score/level
LK.setScore(0);
scoreTxt.setText('0');
level = 1;
levelTxt.setText('1');
}
resetGameState();
// --- Vehicle spawner ---
function spawnVehicle() {
// Check if we should spawn a motorcycle (only in lanes 2 and 3, which are indices 2 and 3)
var isMotorcycleSpawn = Math.random() < 0.15; // 15% chance for motorcycle
var lane;
var y;
var v, dir, speed;
if (isMotorcycleSpawn) {
// Motorcycles only spawn in lanes 3 and 4 (indices 2 and 3)
lane = Math.random() < 0.5 ? 2 : 3;
y = laneYs[lane];
v = new Motorcycle();
speed = (10 + level * 1.0 + Math.random() * 2) * 0.5 * 0.9; // Slightly slower than cars
} else {
// Randomly pick lane for other vehicles
lane = Math.floor(Math.random() * laneCount);
y = laneYs[lane];
// Adjusted spawn probabilities: All vehicle types reduced by 5%
// Old: Car 60%, Truck 25%, BouncyCar 15%
// New: Car 55%, Truck 20%, BouncyCar 10%
var t = Math.random();
if (t < 0.55) {
v = new Car();
speed = (12 + level * 1.2 + Math.random() * 2) * 0.5 * 0.95;
} else if (t < 0.75) {
v = new Truck();
speed = (8 + level * 1.1 + Math.random() * 2) * 0.5 * 0.9 * 0.95; // Truck speed reduced by 10% then by 5%
} else {
v = new BouncyCar();
speed = (10 + level * 1.3 + Math.random() * 2) * 0.5 * 1.1 * 0.95; // BouncyCar speed increased by 10% then reduced by 5%
}
}
// Direction: even lanes left->right, odd right->left
dir = lane % 2 === 0 ? 1 : -1;
v.lane = lane;
v.y = y;
v.y0 = y;
v.speed = speed * dir;
if (dir > 0) {
v.x = -200;
} else {
v.x = 2048 + 200;
// Flip the vehicle horizontally if moving right-to-left
if (v.children && v.children.length > 0 && v.children[0]) {
v.children[0].scale.x = -1;
}
}
vehicles.push(v);
game.addChild(v);
}
// --- Level progression ---
function nextLevel() {
level += 1;
levelTxt.setText(level + '');
// Remove all vehicles
for (var i = 0; i < vehicles.length; ++i) vehicles[i].destroy();
vehicles = [];
// Reset ball
ball.reset();
// Update health display
healthTxt.setText('♥ ' + ball.health);
canLaunch = true;
slingshotBand.visible = false;
}
// --- Game update ---
game.update = function () {
// Ball update
ball.update();
// Vehicles update
// --- Make vehicles follow the speed of the vehicle in front if they are too close in the same lane ---
for (var i = vehicles.length - 1; i >= 0; --i) {
var v = vehicles[i];
// Find the closest vehicle ahead in the same lane and direction
var minDist = Infinity;
var frontVehicle = null;
for (var j = 0; j < vehicles.length; ++j) {
if (i === j) continue;
var v2 = vehicles[j];
if (v2.lane !== v.lane) continue;
// Only consider vehicles in the same direction
if (v.speed > 0 && v2.x > v.x || v.speed < 0 && v2.x < v.x) {
var dist = Math.abs(v2.x - v.x);
if (dist < minDist) {
minDist = dist;
frontVehicle = v2;
}
}
}
// If too close, match speed to the front vehicle
if (frontVehicle) {
// Use the larger of the two widths for minimum gap
var minGap = Math.max(v.width, frontVehicle.width) * 0.85;
if (minDist < minGap) {
v.speed = frontVehicle.speed;
// Snap position to avoid overlap
if (v.speed > 0) {
v.x = Math.min(v.x, frontVehicle.x - minGap + 1);
} else {
v.x = Math.max(v.x, frontVehicle.x + minGap - 1);
}
}
}
v.update();
// Remove destroyed
if (v.destroyed) {
v.destroy();
vehicles.splice(i, 1);
continue;
}
}
// Ball-vehicle collision
if (ball.launched && ball.bounceCooldown <= 0) {
for (var i = 0; i < vehicles.length; ++i) {
var v = vehicles[i];
// Skip if vehicle is in hit cooldown
if (v.hitCooldown > 0) continue;
// AABB collision
var dx = Math.abs(ball.x - v.x);
var dy = Math.abs(ball.y - v.y);
var overlapX = dx < ball.radius + v.width / 2 - 10;
var overlapY = dy < ball.radius + v.height / 2 - 10;
if (overlapX && overlapY) {
if (v.type === 'bouncycar') {
// Calculate the incoming angle and speed
var incomingAngle = Math.atan2(ball.vy, ball.vx);
var speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy);
// Reverse the angle (go in the exact opposite direction)
var baseBounceAngle = incomingAngle + Math.PI;
// Add up to ±10% randomness to the angle
var randomAngle = (Math.random() - 0.5) * (Math.PI * 0.2); // ±0.1π ≈ ±18°
var bounceAngle = baseBounceAngle + randomAngle;
// Bounce speed is 120% of the incoming speed
var bounceSpeed = speed * 1.2;
// Set new velocity
ball.vx = Math.cos(bounceAngle) * bounceSpeed;
ball.vy = Math.sin(bounceAngle) * bounceSpeed;
ball.launched = true;
ball.resting = false;
ball.bounceCooldown = 12;
LK.effects.flashObject(v, 0xffff00, 400);
// Do NOT randomize X anymore, keep physics consistent
// After bounce, check if ball is immediately on stop (target)
var dxStop = ball.x - stop.x;
var dyStop = ball.y - stop.y;
// Use ellipse collision: (dx/a)^2 + (dy/b)^2 < 1
if (dxStop * dxStop / ((stopRadius.x + ball.radius - 10) * (stopRadius.x + ball.radius - 10)) + dyStop * dyStop / ((stopRadius.y + ball.radius - 10) * (stopRadius.y + ball.radius - 10)) < 1 && Math.abs(ball.vy) < 18) {
// Win!
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore() + '');
LK.effects.flashObject(stop, 0x00ff00, 600);
canLaunch = false;
ball.launched = false;
ball.resting = true;
// Randomize stop X position (avoid edges)
var stopMargin = 180 + stopRadius.x;
stop.x = stopMargin + Math.random() * (2048 - 2 * stopMargin);
// Next level after short delay
if (winTimeout) LK.clearTimeout(winTimeout);
winTimeout = LK.setTimeout(function () {
nextLevel();
}, 1200);
return;
}
} else {
// Vehicle collision - reduce health and deflect ball
ball.health -= 1;
healthTxt.setText('♥ ' + ball.health);
// Set vehicle hit cooldown (2 seconds = 120 ticks at 60fps)
v.hitCooldown = 120;
// Flash the vehicle red to indicate hit
LK.effects.flashObject(v, 0xff0000, 500);
// New collision logic: Average of ball's vector and vehicle's vector.
// Vehicle's velocity vector is (v.speed, 0) as it only moves horizontally.
// Ball's current velocity vector before collision is (ball.vx, ball.vy).
// Calculate the average vector components:
// new_vx = (ball.vx + vehicle.vx) / 2
// new_vy = (ball.vy + vehicle.vy) / 2
// Since vehicle.vy is 0:
// New formula: (ball_speed + 3 * vehicle_speed) / 1.20
var avgVx = (ball.vx + 3 * v.speed) / 1.20;
var avgVy = (ball.vy + 3 * 0) / 1.20; // vehicle.vy is 0
// Apply the new averaged velocity to the ball
ball.vx = avgVx;
ball.vy = avgVy;
// Set bounce cooldown to prevent multiple hits
ball.bounceCooldown = 8;
// Check if health is depleted
if (ball.health <= 0) {
// Game over
LK.effects.flashScreen(0xff0000, 800);
LK.showGameOver();
return;
}
}
}
}
}
if (ball.bounceCooldown > 0) ball.bounceCooldown--;
// Ball-stop (target) collision
if (ball.launched && !ball.resting) {
var dx = ball.x - stop.x;
var dy = ball.y - stop.y;
// Use ellipse collision: (dx/a)^2 + (dy/b)^2 < 1
if (dx * dx / ((stopRadius.x + ball.radius - 10) * (stopRadius.x + ball.radius - 10)) + dy * dy / ((stopRadius.y + ball.radius - 10) * (stopRadius.y + ball.radius - 10)) < 1 && Math.abs(ball.vy) < 18) {
// Win!
LK.setScore(LK.getScore() + 1);
scoreTxt.setText(LK.getScore() + '');
LK.effects.flashObject(stop, 0x00ff00, 600);
canLaunch = false;
ball.launched = false;
ball.resting = true;
// Randomize stop X position (avoid edges)
var margin = 180 + stopRadius.x;
stop.x = margin + Math.random() * (2048 - 2 * margin);
// Next level after short delay
if (winTimeout) LK.clearTimeout(winTimeout);
winTimeout = LK.setTimeout(function () {
nextLevel();
}, 1200);
}
}
// Vehicle spawn logic - reduced by 30% total (20% + 10%)
if (LK.ticks % Math.max(52 - Math.min(level * 2, 30), 13) === 0) {
spawnVehicle();
}
};
// --- Slingshot controls ---
function getDragVector() {
var dx = dragCurrent.x - dragStart.x;
var dy = dragCurrent.y - dragStart.y;
return {
dx: dx,
dy: dy
};
}
function updateSlingshotBand() {
if (!isDragging) {
slingshotBand.visible = false;
return;
}
var vec = getDragVector();
var len = Math.sqrt(vec.dx * vec.dx + vec.dy * vec.dy);
var maxLen = 420;
if (len > maxLen) {
vec.dx *= maxLen / len;
vec.dy *= maxLen / len;
len = maxLen;
}
// Band position: from ball center to drag point
slingshotBand.visible = true;
slingshotBand.x = ball.x;
slingshotBand.y = ball.y;
slingshotBand.height = len;
slingshotBand.rotation = Math.atan2(vec.dy, vec.dx) + Math.PI / 2;
}
// --- Input handlers ---
game.down = function (x, y, obj) {
// Only allow drag if ball is at rest and not in win state
if (!canLaunch || !ball.resting) return;
// Only allow drag if touch is near ball
var dx = x - ball.x;
var dy = y - ball.y;
if (dx * dx + dy * dy < ball.radius * ball.radius * 2.2) {
isDragging = true;
dragStart.x = ball.x;
dragStart.y = ball.y;
dragCurrent.x = x;
dragCurrent.y = y;
updateSlingshotBand();
}
};
game.move = function (x, y, obj) {
if (!isDragging) return;
dragCurrent.x = x;
dragCurrent.y = y;
updateSlingshotBand();
};
game.up = function (x, y, obj) {
if (!isDragging) return;
isDragging = false;
slingshotBand.visible = false;
// Launch!
var vec = getDragVector();
var len = Math.sqrt(vec.dx * vec.dx + vec.dy * vec.dy);
if (len < 60) return; // too short
var maxLen = 420;
if (len > maxLen) {
vec.dx *= maxLen / len;
vec.dy *= maxLen / len;
len = maxLen;
}
// Set velocity (scaled)
ball.vx = -vec.dx * 0.055;
ball.vy = -vec.dy * 0.055;
ball.launched = true;
ball.resting = false;
canLaunch = false;
};
// --- Game over/win handling ---
LK.on('gameover', function () {
resetGameState();
});
LK.on('youwin', function () {
resetGameState();
});
bir palyanço kafasının arka üsten görünümü çizgi film stili. In-Game asset. 2d. High contrast. No shadows
ileriyi gösteren . No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
zıpzıp taşıyan kamyonet üsten görünüm. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
lüks sarı araba üsten görünüş. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
birebir aynı tıs tekerlerin düzeltilmesi gerek sadece
otobuş durağı. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat