/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Anchor = Container.expand(function (size) {
var self = Container.call(this);
// Determine anchor asset and properties based on size
self.size = size || 'medium';
var assetName = 'anchor_' + self.size;
var anchorGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Add range indicator graphics
var rangeAssetName = 'range_' + self.size;
var rangeGraphics = self.attachAsset(rangeAssetName, {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.alpha = 0.3; // Make it semi-transparent
// Range and score vary by size - increased ranges for better grappling
if (self.size === 'small') {
self.range = 250;
self.scoreValue = 15;
} else if (self.size === 'large') {
self.range = 350;
self.scoreValue = 5;
} else {
self.range = 300;
self.scoreValue = 10;
}
self.used = false;
self.update = function () {
// Pulse animation
var scale = 1 + Math.sin(LK.ticks * 0.1) * 0.1;
anchorGraphics.scaleX = scale;
anchorGraphics.scaleY = scale;
// Scale range indicator proportionally to anchor size
var rangeScale = scale;
if (self.size === 'small') {
rangeScale *= 0.8; // Smaller range visual for small anchors
} else if (self.size === 'large') {
rangeScale *= 1.2; // Larger range visual for large anchors
}
rangeGraphics.scaleX = rangeScale;
rangeGraphics.scaleY = rangeScale;
};
self.isInRange = function (playerX, playerY) {
var distance = Math.sqrt(Math.pow(self.x - playerX, 2) + Math.pow(self.y - playerY, 2));
return distance <= self.range;
};
return self;
});
var GrappleLine = Container.expand(function () {
var self = Container.call(this);
self.segments = [];
self.createLine = function (startX, startY, endX, endY) {
// Clear existing segments
for (var i = 0; i < self.segments.length; i++) {
self.segments[i].destroy();
}
self.segments = [];
// Calculate line segments
var distance = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2));
var segmentCount = Math.floor(distance / 20); // One segment every 20 pixels
for (var i = 0; i < segmentCount; i++) {
var t = i / segmentCount;
var segmentX = startX + (endX - startX) * t;
var segmentY = startY + (endY - startY) * t;
var segment = self.attachAsset('grapple_line', {
anchorX: 0.5,
anchorY: 0.5
});
segment.x = segmentX;
segment.y = segmentY;
self.segments.push(segment);
}
};
self.clear = function () {
for (var i = 0; i < self.segments.length; i++) {
self.segments[i].destroy();
}
self.segments = [];
};
return self;
});
var MainMenu = Container.expand(function () {
var self = Container.call(this);
// Menu background
var menuBg = self.attachAsset('sky_background', {
anchorX: 0,
anchorY: 0
});
menuBg.alpha = 0.8;
// Title text
var titleText = new Text2('SKY GRAPPLER', {
size: 120,
fill: 0x000000,
font: "'Arial Black', 'Helvetica-Bold', 'Impact', sans-serif"
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 1024;
titleText.y = 600;
self.addChild(titleText);
// High score display
var highScore = storage.highScore || 0;
var highScoreText = new Text2('HIGH SCORE: ' + highScore, {
size: 60,
fill: 0xFFCC00
});
highScoreText.anchor.set(0.5, 0.5);
highScoreText.x = 1024;
highScoreText.y = 800;
self.addChild(highScoreText);
// Distance display
var maxDistance = storage.maxDistance || 0;
var distanceText = new Text2('MAX DISTANCE: ' + Math.floor(maxDistance) + 'm', {
size: 60,
fill: 0x88FF88
});
distanceText.anchor.set(0.5, 0.5);
distanceText.x = 1024;
distanceText.y = 900;
self.addChild(distanceText);
// Play button
var playButton = new Text2('PLAY', {
size: 80,
fill: 0xFF0000
});
playButton.anchor.set(0.5, 0.5);
playButton.x = 1024;
playButton.y = 1200;
self.addChild(playButton);
// Music toggle button
var musicEnabled = storage.musicEnabled !== false; // Default to true
var musicButton = new Text2('MUSIC: ' + (musicEnabled ? 'ON' : 'OFF'), {
size: 60,
fill: 0xFFFFFF
});
musicButton.anchor.set(0.5, 0.5);
musicButton.x = 1024;
musicButton.y = 1400;
self.addChild(musicButton);
// Sound effects toggle button
var soundEnabled = storage.soundEnabled !== false; // Default to true
var soundButton = new Text2('SOUND: ' + (soundEnabled ? 'ON' : 'OFF'), {
size: 60,
fill: 0xFFFFFF
});
soundButton.anchor.set(0.5, 0.5);
soundButton.x = 1024;
soundButton.y = 1500;
self.addChild(soundButton);
// Button interactions
playButton.down = function (x, y, obj) {
self.startGame();
};
musicButton.down = function (x, y, obj) {
musicEnabled = !musicEnabled;
storage.musicEnabled = musicEnabled;
musicButton.setText('MUSIC: ' + (musicEnabled ? 'ON' : 'OFF'));
if (musicEnabled) {
LK.playMusic('background_music');
} else {
LK.stopMusic();
}
};
soundButton.down = function (x, y, obj) {
soundEnabled = !soundEnabled;
storage.soundEnabled = soundEnabled;
soundButton.setText('SOUND: ' + (soundEnabled ? 'ON' : 'OFF'));
};
self.startGame = function () {
self.destroy();
startGameplay();
};
self.updateHighScore = function (newScore, newDistance) {
if (newScore > highScore) {
highScore = newScore;
storage.highScore = highScore;
highScoreText.setText('HIGH SCORE: ' + highScore);
}
if (newDistance > maxDistance) {
maxDistance = newDistance;
storage.maxDistance = maxDistance;
distanceText.setText('MAX DISTANCE: ' + Math.floor(maxDistance) + 'm');
}
};
return self;
});
var Player = Container.expand(function () {
var self = Container.call(this);
var playerGraphics = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
self.velocityX = 0;
self.velocityY = 0;
self.speed = 12;
self.isGrappling = false;
self.grappleAnchor = null;
self.grappleDistance = 0;
self.grappleAngle = 0;
self.swingDirection = 1;
self.trail = [];
self.grappleLine = null;
self.trailParticles = [];
self.lastTrailTime = 0;
self.update = function () {
if (self.isGrappling && self.grappleAnchor) {
// Swing around anchor point using calculated swing direction
self.grappleAngle += 0.08 * self.swingDirection;
self.x = self.grappleAnchor.x + Math.cos(self.grappleAngle) * self.grappleDistance;
self.y = self.grappleAnchor.y + Math.sin(self.grappleAngle) * self.grappleDistance;
// Update velocity based on swing direction
var nextAngle = self.grappleAngle + 0.08 * self.swingDirection;
var nextX = self.grappleAnchor.x + Math.cos(nextAngle) * self.grappleDistance;
var nextY = self.grappleAnchor.y + Math.sin(nextAngle) * self.grappleDistance;
self.velocityX = (nextX - self.x) * 1.5;
self.velocityY = (nextY - self.y) * 1.5;
// Update grapple line
if (self.grappleLine) {
self.grappleLine.createLine(self.x, self.y, self.grappleAnchor.x, self.grappleAnchor.y);
}
} else {
// Move in straight line
self.x += self.velocityX;
self.y += self.velocityY;
}
// Add trail effect
self.trail.push({
x: self.x,
y: self.y
});
if (self.trail.length > 10) {
self.trail.shift();
}
// Generate flying effect particles when not grappling
if (!self.isGrappling) {
var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY);
if (speed > 3 && LK.ticks - self.lastTrailTime > 3) {
// Create trail particles behind the player
var particle = new TrailParticle(self.x - self.velocityX * 0.3 + (Math.random() - 0.5) * 20, self.y - self.velocityY * 0.3 + (Math.random() - 0.5) * 20);
self.trailParticles.push(particle);
game.addChild(particle);
self.lastTrailTime = LK.ticks;
}
}
// Update and remove old trail particles
for (var i = self.trailParticles.length - 1; i >= 0; i--) {
var particle = self.trailParticles[i];
if (particle.shouldRemove) {
particle.destroy();
self.trailParticles.splice(i, 1);
}
}
};
self.startGrapple = function (anchor) {
if (!self.isGrappling) {
self.isGrappling = true;
self.grappleAnchor = anchor;
self.grappleDistance = Math.sqrt(Math.pow(self.x - anchor.x, 2) + Math.pow(self.y - anchor.y, 2));
self.grappleAngle = Math.atan2(self.y - anchor.y, self.x - anchor.x);
// Calculate swing direction based on player's velocity and position relative to anchor
var velocityAngle = Math.atan2(self.velocityY, self.velocityX);
var toAnchorAngle = Math.atan2(anchor.y - self.y, anchor.x - self.x);
var angleDiff = velocityAngle - toAnchorAngle;
// Normalize angle difference to [-π, π]
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
// Determine swing direction: positive for clockwise, negative for counter-clockwise
self.swingDirection = angleDiff > 0 ? 1 : -1;
// Create grapple line
if (!self.grappleLine) {
self.grappleLine = game.addChild(new GrappleLine());
}
self.grappleLine.createLine(self.x, self.y, anchor.x, anchor.y);
if (storage.soundEnabled !== false) {
LK.getSound('grapple').play();
}
// Visual feedback
tween(anchor, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 200,
easing: tween.easeOut
});
tween(anchor, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeIn
});
// Add grapple impact effect
LK.effects.flashObject(self, 0x44aaff, 300);
}
};
self.releaseGrapple = function () {
if (self.isGrappling) {
self.isGrappling = false;
self.grappleAnchor = null;
// Clear grapple line
if (self.grappleLine) {
self.grappleLine.clear();
}
if (storage.soundEnabled !== false) {
LK.getSound('release').play();
}
// Normalize velocity
var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY);
if (speed > 0) {
self.velocityX = self.velocityX / speed * self.speed;
self.velocityY = self.velocityY / speed * self.speed;
}
}
};
return self;
});
var TrailParticle = Container.expand(function (startX, startY) {
var self = Container.call(this);
var particleGraphics = self.attachAsset('trail_particle', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = startX;
self.y = startY;
self.life = 1.0;
self.velocityX = (Math.random() - 0.5) * 4;
self.velocityY = (Math.random() - 0.5) * 4;
self.initialScale = 0.5 + Math.random() * 0.5;
particleGraphics.scaleX = self.initialScale;
particleGraphics.scaleY = self.initialScale;
self.update = function () {
self.life -= 0.08;
self.x += self.velocityX;
self.y += self.velocityY;
// Fade out and shrink over time
particleGraphics.alpha = self.life;
var scale = self.initialScale * self.life;
particleGraphics.scaleX = scale;
particleGraphics.scaleY = scale;
// Mark for removal when life is depleted
if (self.life <= 0) {
self.shouldRemove = true;
}
};
return self;
});
var Wall = Container.expand(function () {
var self = Container.call(this);
var wallGraphics = self.attachAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
});
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a1a
});
/****
* Game Code
****/
// Game state
var gameState = 'menu'; // 'menu' or 'playing'
var mainMenu = null;
// Game variables
var player;
var anchors = [];
var walls = [];
var gameSpeed = 1;
var distanceTraveled = 0;
var lastAnchorY = 0;
var lastWallY = 0;
// UI elements (will be created in startGameplay)
var scoreTxt;
var distanceTxt;
// Initialize main menu
function showMainMenu() {
gameState = 'menu';
mainMenu = game.addChild(new MainMenu());
}
// Start gameplay function
function startGameplay() {
gameState = 'playing';
// Clear any existing game objects
for (var i = 0; i < anchors.length; i++) {
anchors[i].destroy();
}
for (var i = 0; i < walls.length; i++) {
walls[i].destroy();
}
anchors = [];
walls = [];
// Reset game variables
gameSpeed = 1;
distanceTraveled = 0;
lastAnchorY = 0;
lastWallY = 0;
LK.setScore(0);
// Create UI elements
scoreTxt = new Text2('0', {
size: 80,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
distanceTxt = new Text2('0m', {
size: 60,
fill: 0x888888
});
distanceTxt.anchor.set(0.5, 0);
distanceTxt.y = 100;
LK.gui.top.addChild(distanceTxt);
// Add background
var skyBackground = game.attachAsset('sky_background', {
anchorX: 0,
anchorY: 0
});
skyBackground.x = 0;
skyBackground.y = 0;
// Initialize player
player = game.addChild(new Player());
player.x = 1024;
player.y = 2200;
player.velocityX = 0;
player.velocityY = -player.speed;
// Generate initial anchors
for (var i = 0; i < 5; i++) {
var anchorX = 400 + Math.random() * 1248;
var anchorY = 2000 - i * 500; // Increased spacing between initial anchors
generateAnchor(anchorX, anchorY);
lastAnchorY = anchorY;
}
// Start background music if enabled
if (storage.musicEnabled !== false) {
LK.playMusic('background_music');
}
}
function generateAnchor(x, y) {
// Check for overlap with existing anchors
var minDistance = 120; // Minimum distance between anchors
for (var i = 0; i < anchors.length; i++) {
var existingAnchor = anchors[i];
var distance = Math.sqrt(Math.pow(x - existingAnchor.x, 2) + Math.pow(y - existingAnchor.y, 2));
if (distance < minDistance) {
// Too close to existing anchor, don't create new one
return null;
}
}
var sizes = ['small', 'medium', 'large'];
var randomSize = sizes[Math.floor(Math.random() * sizes.length)];
var anchor = new Anchor(randomSize);
anchor.x = x;
anchor.y = y;
anchors.push(anchor);
game.addChild(anchor);
return anchor;
}
function generateWall(x, y, width, height) {
var wall = new Wall();
wall.x = x;
wall.y = y;
if (width) wall.width = width;
if (height) wall.height = height;
walls.push(wall);
game.addChild(wall);
return wall;
}
// Show main menu initially
showMainMenu();
// Walls removed - no obstacles to crash into
// Mouse hold grappling mechanism
game.down = function (x, y, obj) {
if (gameState !== 'playing') return;
// Start grappling when mouse is pressed down
if (!player.isGrappling) {
// Find nearest anchor in range
var nearestAnchor = null;
var nearestDistance = Infinity;
for (var i = 0; i < anchors.length; i++) {
var anchor = anchors[i];
if (anchor.isInRange(player.x, player.y)) {
var distance = Math.sqrt(Math.pow(player.x - anchor.x, 2) + Math.pow(player.y - anchor.y, 2));
if (distance < nearestDistance) {
nearestDistance = distance;
nearestAnchor = anchor;
}
}
}
if (nearestAnchor) {
player.startGrapple(nearestAnchor);
if (!nearestAnchor.used) {
nearestAnchor.used = true;
LK.setScore(LK.getScore() + nearestAnchor.scoreValue);
scoreTxt.setText(LK.getScore());
}
}
}
};
// Mouse release handler - release grapple when mouse is released
game.up = function (x, y, obj) {
if (gameState !== 'playing') return;
// Release grappling when mouse is released
if (player.isGrappling) {
player.releaseGrapple();
}
};
// Game update loop
game.update = function () {
if (gameState !== 'playing') return;
// Update distance - increase when moving upward, decrease when moving downward
if (player.velocityY < 0) {
// Negative velocity means moving up
distanceTraveled += Math.abs(player.velocityY) * 0.1;
} else if (player.velocityY > 0) {
// Positive velocity means moving down
distanceTraveled -= Math.abs(player.velocityY) * 0.1;
// Prevent distance from going below 0
if (distanceTraveled < 0) {
distanceTraveled = 0;
}
}
distanceTxt.setText(Math.floor(distanceTraveled) + 'm');
// Increase game speed gradually
gameSpeed = 1 + distanceTraveled * 0.001;
// Generate new anchors as player moves up - reduced frequency for increased difficulty
if (player.y < lastAnchorY + 500) {
// Generate only 1 anchor instead of 3 to increase spacing
var anchorX = 200 + Math.random() * 1648;
var anchorY = lastAnchorY - 400 - Math.random() * 200; // Increased spacing
generateAnchor(anchorX, anchorY);
lastAnchorY -= 500; // Increased distance between anchor generations
}
// Generate strategic obstacles occasionally for difficulty - reduced frequency
if (player.y < lastWallY + 1200 && Math.random() < 0.15) {
// Create narrow passages or single walls at strategic positions
if (Math.random() < 0.5) {
// Single wall obstacle
var wallX = 300 + Math.random() * 1448;
var wallY = lastWallY - 600 - Math.random() * 200;
generateWall(wallX, wallY, 150, 30);
// Add one anchor in front of the wall for grappling opportunity
var attempts = 0;
var anchorPlaced = false;
while (!anchorPlaced && attempts < 5) {
var anchorX = wallX + (Math.random() - 0.5) * 200;
var anchorY = wallY - 150 - Math.random() * 100;
if (generateAnchor(anchorX, anchorY)) {
anchorPlaced = true;
}
attempts++;
}
} else {
// Narrow passage between two walls
var gapCenter = 400 + Math.random() * 1248;
var gapSize = 300 + Math.random() * 200;
var wallY = lastWallY - 600 - Math.random() * 200;
// Left wall
generateWall(gapCenter - gapSize / 2 - 100, wallY, 200, 30);
// Right wall
generateWall(gapCenter + gapSize / 2 + 100, wallY, 200, 30);
// Add one anchor in front of the passage for strategic grappling
var attempts = 0;
var anchorPlaced = false;
while (!anchorPlaced && attempts < 5) {
var anchorX = gapCenter + (Math.random() - 0.5) * gapSize * 0.5;
var anchorY = wallY - 150 - Math.random() * 100;
if (generateAnchor(anchorX, anchorY)) {
anchorPlaced = true;
}
attempts++;
}
}
lastWallY -= 1200;
}
for (var i = anchors.length - 1; i >= 0; i--) {
var anchor = anchors[i];
if (anchor.y > player.y + 1000) {
anchor.destroy();
anchors.splice(i, 1);
}
}
// Clean up old walls
for (var i = walls.length - 1; i >= 0; i--) {
var wall = walls[i];
if (wall.y > player.y + 1000) {
wall.destroy();
walls.splice(i, 1);
}
}
// Wall collision detection
for (var i = 0; i < walls.length; i++) {
var wall = walls[i];
if (player.intersects(wall)) {
LK.effects.flashScreen(0xff0000, 1000);
handleGameOver();
return;
}
}
if (player.x < 0 || player.x > 2048 || player.y > 2732 + 200) {
LK.effects.flashScreen(0xff0000, 1000);
handleGameOver();
return;
}
// Camera follow (move everything down as player moves up)
if (player.y < 1366) {
var offset = 1366 - player.y;
player.y = 1366;
// Move all anchors down
for (var i = 0; i < anchors.length; i++) {
anchors[i].y += offset;
}
// Move all walls down
for (var i = 0; i < walls.length; i++) {
walls[i].y += offset;
}
lastAnchorY += offset;
lastWallY += offset;
}
};
// Handle game over with high score saving
function handleGameOver() {
// Update high score and max distance if needed
var currentScore = LK.getScore();
var currentDistance = distanceTraveled;
var highScore = storage.highScore || 0;
var maxDistance = storage.maxDistance || 0;
if (currentScore > highScore) {
storage.highScore = currentScore;
}
if (currentDistance > maxDistance) {
storage.maxDistance = currentDistance;
}
// Clear UI elements
if (scoreTxt) {
scoreTxt.destroy();
scoreTxt = null;
}
if (distanceTxt) {
distanceTxt.destroy();
distanceTxt = null;
}
// Show game over and then return to menu
LK.showGameOver();
LK.setTimeout(function () {
showMainMenu();
}, 2000);
} /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
var Anchor = Container.expand(function (size) {
var self = Container.call(this);
// Determine anchor asset and properties based on size
self.size = size || 'medium';
var assetName = 'anchor_' + self.size;
var anchorGraphics = self.attachAsset(assetName, {
anchorX: 0.5,
anchorY: 0.5
});
// Add range indicator graphics
var rangeAssetName = 'range_' + self.size;
var rangeGraphics = self.attachAsset(rangeAssetName, {
anchorX: 0.5,
anchorY: 0.5
});
rangeGraphics.alpha = 0.3; // Make it semi-transparent
// Range and score vary by size - increased ranges for better grappling
if (self.size === 'small') {
self.range = 250;
self.scoreValue = 15;
} else if (self.size === 'large') {
self.range = 350;
self.scoreValue = 5;
} else {
self.range = 300;
self.scoreValue = 10;
}
self.used = false;
self.update = function () {
// Pulse animation
var scale = 1 + Math.sin(LK.ticks * 0.1) * 0.1;
anchorGraphics.scaleX = scale;
anchorGraphics.scaleY = scale;
// Scale range indicator proportionally to anchor size
var rangeScale = scale;
if (self.size === 'small') {
rangeScale *= 0.8; // Smaller range visual for small anchors
} else if (self.size === 'large') {
rangeScale *= 1.2; // Larger range visual for large anchors
}
rangeGraphics.scaleX = rangeScale;
rangeGraphics.scaleY = rangeScale;
};
self.isInRange = function (playerX, playerY) {
var distance = Math.sqrt(Math.pow(self.x - playerX, 2) + Math.pow(self.y - playerY, 2));
return distance <= self.range;
};
return self;
});
var GrappleLine = Container.expand(function () {
var self = Container.call(this);
self.segments = [];
self.createLine = function (startX, startY, endX, endY) {
// Clear existing segments
for (var i = 0; i < self.segments.length; i++) {
self.segments[i].destroy();
}
self.segments = [];
// Calculate line segments
var distance = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2));
var segmentCount = Math.floor(distance / 20); // One segment every 20 pixels
for (var i = 0; i < segmentCount; i++) {
var t = i / segmentCount;
var segmentX = startX + (endX - startX) * t;
var segmentY = startY + (endY - startY) * t;
var segment = self.attachAsset('grapple_line', {
anchorX: 0.5,
anchorY: 0.5
});
segment.x = segmentX;
segment.y = segmentY;
self.segments.push(segment);
}
};
self.clear = function () {
for (var i = 0; i < self.segments.length; i++) {
self.segments[i].destroy();
}
self.segments = [];
};
return self;
});
var MainMenu = Container.expand(function () {
var self = Container.call(this);
// Menu background
var menuBg = self.attachAsset('sky_background', {
anchorX: 0,
anchorY: 0
});
menuBg.alpha = 0.8;
// Title text
var titleText = new Text2('SKY GRAPPLER', {
size: 120,
fill: 0x000000,
font: "'Arial Black', 'Helvetica-Bold', 'Impact', sans-serif"
});
titleText.anchor.set(0.5, 0.5);
titleText.x = 1024;
titleText.y = 600;
self.addChild(titleText);
// High score display
var highScore = storage.highScore || 0;
var highScoreText = new Text2('HIGH SCORE: ' + highScore, {
size: 60,
fill: 0xFFCC00
});
highScoreText.anchor.set(0.5, 0.5);
highScoreText.x = 1024;
highScoreText.y = 800;
self.addChild(highScoreText);
// Distance display
var maxDistance = storage.maxDistance || 0;
var distanceText = new Text2('MAX DISTANCE: ' + Math.floor(maxDistance) + 'm', {
size: 60,
fill: 0x88FF88
});
distanceText.anchor.set(0.5, 0.5);
distanceText.x = 1024;
distanceText.y = 900;
self.addChild(distanceText);
// Play button
var playButton = new Text2('PLAY', {
size: 80,
fill: 0xFF0000
});
playButton.anchor.set(0.5, 0.5);
playButton.x = 1024;
playButton.y = 1200;
self.addChild(playButton);
// Music toggle button
var musicEnabled = storage.musicEnabled !== false; // Default to true
var musicButton = new Text2('MUSIC: ' + (musicEnabled ? 'ON' : 'OFF'), {
size: 60,
fill: 0xFFFFFF
});
musicButton.anchor.set(0.5, 0.5);
musicButton.x = 1024;
musicButton.y = 1400;
self.addChild(musicButton);
// Sound effects toggle button
var soundEnabled = storage.soundEnabled !== false; // Default to true
var soundButton = new Text2('SOUND: ' + (soundEnabled ? 'ON' : 'OFF'), {
size: 60,
fill: 0xFFFFFF
});
soundButton.anchor.set(0.5, 0.5);
soundButton.x = 1024;
soundButton.y = 1500;
self.addChild(soundButton);
// Button interactions
playButton.down = function (x, y, obj) {
self.startGame();
};
musicButton.down = function (x, y, obj) {
musicEnabled = !musicEnabled;
storage.musicEnabled = musicEnabled;
musicButton.setText('MUSIC: ' + (musicEnabled ? 'ON' : 'OFF'));
if (musicEnabled) {
LK.playMusic('background_music');
} else {
LK.stopMusic();
}
};
soundButton.down = function (x, y, obj) {
soundEnabled = !soundEnabled;
storage.soundEnabled = soundEnabled;
soundButton.setText('SOUND: ' + (soundEnabled ? 'ON' : 'OFF'));
};
self.startGame = function () {
self.destroy();
startGameplay();
};
self.updateHighScore = function (newScore, newDistance) {
if (newScore > highScore) {
highScore = newScore;
storage.highScore = highScore;
highScoreText.setText('HIGH SCORE: ' + highScore);
}
if (newDistance > maxDistance) {
maxDistance = newDistance;
storage.maxDistance = maxDistance;
distanceText.setText('MAX DISTANCE: ' + Math.floor(maxDistance) + 'm');
}
};
return self;
});
var Player = Container.expand(function () {
var self = Container.call(this);
var playerGraphics = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
self.velocityX = 0;
self.velocityY = 0;
self.speed = 12;
self.isGrappling = false;
self.grappleAnchor = null;
self.grappleDistance = 0;
self.grappleAngle = 0;
self.swingDirection = 1;
self.trail = [];
self.grappleLine = null;
self.trailParticles = [];
self.lastTrailTime = 0;
self.update = function () {
if (self.isGrappling && self.grappleAnchor) {
// Swing around anchor point using calculated swing direction
self.grappleAngle += 0.08 * self.swingDirection;
self.x = self.grappleAnchor.x + Math.cos(self.grappleAngle) * self.grappleDistance;
self.y = self.grappleAnchor.y + Math.sin(self.grappleAngle) * self.grappleDistance;
// Update velocity based on swing direction
var nextAngle = self.grappleAngle + 0.08 * self.swingDirection;
var nextX = self.grappleAnchor.x + Math.cos(nextAngle) * self.grappleDistance;
var nextY = self.grappleAnchor.y + Math.sin(nextAngle) * self.grappleDistance;
self.velocityX = (nextX - self.x) * 1.5;
self.velocityY = (nextY - self.y) * 1.5;
// Update grapple line
if (self.grappleLine) {
self.grappleLine.createLine(self.x, self.y, self.grappleAnchor.x, self.grappleAnchor.y);
}
} else {
// Move in straight line
self.x += self.velocityX;
self.y += self.velocityY;
}
// Add trail effect
self.trail.push({
x: self.x,
y: self.y
});
if (self.trail.length > 10) {
self.trail.shift();
}
// Generate flying effect particles when not grappling
if (!self.isGrappling) {
var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY);
if (speed > 3 && LK.ticks - self.lastTrailTime > 3) {
// Create trail particles behind the player
var particle = new TrailParticle(self.x - self.velocityX * 0.3 + (Math.random() - 0.5) * 20, self.y - self.velocityY * 0.3 + (Math.random() - 0.5) * 20);
self.trailParticles.push(particle);
game.addChild(particle);
self.lastTrailTime = LK.ticks;
}
}
// Update and remove old trail particles
for (var i = self.trailParticles.length - 1; i >= 0; i--) {
var particle = self.trailParticles[i];
if (particle.shouldRemove) {
particle.destroy();
self.trailParticles.splice(i, 1);
}
}
};
self.startGrapple = function (anchor) {
if (!self.isGrappling) {
self.isGrappling = true;
self.grappleAnchor = anchor;
self.grappleDistance = Math.sqrt(Math.pow(self.x - anchor.x, 2) + Math.pow(self.y - anchor.y, 2));
self.grappleAngle = Math.atan2(self.y - anchor.y, self.x - anchor.x);
// Calculate swing direction based on player's velocity and position relative to anchor
var velocityAngle = Math.atan2(self.velocityY, self.velocityX);
var toAnchorAngle = Math.atan2(anchor.y - self.y, anchor.x - self.x);
var angleDiff = velocityAngle - toAnchorAngle;
// Normalize angle difference to [-π, π]
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
// Determine swing direction: positive for clockwise, negative for counter-clockwise
self.swingDirection = angleDiff > 0 ? 1 : -1;
// Create grapple line
if (!self.grappleLine) {
self.grappleLine = game.addChild(new GrappleLine());
}
self.grappleLine.createLine(self.x, self.y, anchor.x, anchor.y);
if (storage.soundEnabled !== false) {
LK.getSound('grapple').play();
}
// Visual feedback
tween(anchor, {
scaleX: 1.3,
scaleY: 1.3
}, {
duration: 200,
easing: tween.easeOut
});
tween(anchor, {
scaleX: 1,
scaleY: 1
}, {
duration: 300,
easing: tween.easeIn
});
// Add grapple impact effect
LK.effects.flashObject(self, 0x44aaff, 300);
}
};
self.releaseGrapple = function () {
if (self.isGrappling) {
self.isGrappling = false;
self.grappleAnchor = null;
// Clear grapple line
if (self.grappleLine) {
self.grappleLine.clear();
}
if (storage.soundEnabled !== false) {
LK.getSound('release').play();
}
// Normalize velocity
var speed = Math.sqrt(self.velocityX * self.velocityX + self.velocityY * self.velocityY);
if (speed > 0) {
self.velocityX = self.velocityX / speed * self.speed;
self.velocityY = self.velocityY / speed * self.speed;
}
}
};
return self;
});
var TrailParticle = Container.expand(function (startX, startY) {
var self = Container.call(this);
var particleGraphics = self.attachAsset('trail_particle', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = startX;
self.y = startY;
self.life = 1.0;
self.velocityX = (Math.random() - 0.5) * 4;
self.velocityY = (Math.random() - 0.5) * 4;
self.initialScale = 0.5 + Math.random() * 0.5;
particleGraphics.scaleX = self.initialScale;
particleGraphics.scaleY = self.initialScale;
self.update = function () {
self.life -= 0.08;
self.x += self.velocityX;
self.y += self.velocityY;
// Fade out and shrink over time
particleGraphics.alpha = self.life;
var scale = self.initialScale * self.life;
particleGraphics.scaleX = scale;
particleGraphics.scaleY = scale;
// Mark for removal when life is depleted
if (self.life <= 0) {
self.shouldRemove = true;
}
};
return self;
});
var Wall = Container.expand(function () {
var self = Container.call(this);
var wallGraphics = self.attachAsset('wall', {
anchorX: 0.5,
anchorY: 0.5
});
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x1a1a1a
});
/****
* Game Code
****/
// Game state
var gameState = 'menu'; // 'menu' or 'playing'
var mainMenu = null;
// Game variables
var player;
var anchors = [];
var walls = [];
var gameSpeed = 1;
var distanceTraveled = 0;
var lastAnchorY = 0;
var lastWallY = 0;
// UI elements (will be created in startGameplay)
var scoreTxt;
var distanceTxt;
// Initialize main menu
function showMainMenu() {
gameState = 'menu';
mainMenu = game.addChild(new MainMenu());
}
// Start gameplay function
function startGameplay() {
gameState = 'playing';
// Clear any existing game objects
for (var i = 0; i < anchors.length; i++) {
anchors[i].destroy();
}
for (var i = 0; i < walls.length; i++) {
walls[i].destroy();
}
anchors = [];
walls = [];
// Reset game variables
gameSpeed = 1;
distanceTraveled = 0;
lastAnchorY = 0;
lastWallY = 0;
LK.setScore(0);
// Create UI elements
scoreTxt = new Text2('0', {
size: 80,
fill: 0xFFFFFF
});
scoreTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(scoreTxt);
distanceTxt = new Text2('0m', {
size: 60,
fill: 0x888888
});
distanceTxt.anchor.set(0.5, 0);
distanceTxt.y = 100;
LK.gui.top.addChild(distanceTxt);
// Add background
var skyBackground = game.attachAsset('sky_background', {
anchorX: 0,
anchorY: 0
});
skyBackground.x = 0;
skyBackground.y = 0;
// Initialize player
player = game.addChild(new Player());
player.x = 1024;
player.y = 2200;
player.velocityX = 0;
player.velocityY = -player.speed;
// Generate initial anchors
for (var i = 0; i < 5; i++) {
var anchorX = 400 + Math.random() * 1248;
var anchorY = 2000 - i * 500; // Increased spacing between initial anchors
generateAnchor(anchorX, anchorY);
lastAnchorY = anchorY;
}
// Start background music if enabled
if (storage.musicEnabled !== false) {
LK.playMusic('background_music');
}
}
function generateAnchor(x, y) {
// Check for overlap with existing anchors
var minDistance = 120; // Minimum distance between anchors
for (var i = 0; i < anchors.length; i++) {
var existingAnchor = anchors[i];
var distance = Math.sqrt(Math.pow(x - existingAnchor.x, 2) + Math.pow(y - existingAnchor.y, 2));
if (distance < minDistance) {
// Too close to existing anchor, don't create new one
return null;
}
}
var sizes = ['small', 'medium', 'large'];
var randomSize = sizes[Math.floor(Math.random() * sizes.length)];
var anchor = new Anchor(randomSize);
anchor.x = x;
anchor.y = y;
anchors.push(anchor);
game.addChild(anchor);
return anchor;
}
function generateWall(x, y, width, height) {
var wall = new Wall();
wall.x = x;
wall.y = y;
if (width) wall.width = width;
if (height) wall.height = height;
walls.push(wall);
game.addChild(wall);
return wall;
}
// Show main menu initially
showMainMenu();
// Walls removed - no obstacles to crash into
// Mouse hold grappling mechanism
game.down = function (x, y, obj) {
if (gameState !== 'playing') return;
// Start grappling when mouse is pressed down
if (!player.isGrappling) {
// Find nearest anchor in range
var nearestAnchor = null;
var nearestDistance = Infinity;
for (var i = 0; i < anchors.length; i++) {
var anchor = anchors[i];
if (anchor.isInRange(player.x, player.y)) {
var distance = Math.sqrt(Math.pow(player.x - anchor.x, 2) + Math.pow(player.y - anchor.y, 2));
if (distance < nearestDistance) {
nearestDistance = distance;
nearestAnchor = anchor;
}
}
}
if (nearestAnchor) {
player.startGrapple(nearestAnchor);
if (!nearestAnchor.used) {
nearestAnchor.used = true;
LK.setScore(LK.getScore() + nearestAnchor.scoreValue);
scoreTxt.setText(LK.getScore());
}
}
}
};
// Mouse release handler - release grapple when mouse is released
game.up = function (x, y, obj) {
if (gameState !== 'playing') return;
// Release grappling when mouse is released
if (player.isGrappling) {
player.releaseGrapple();
}
};
// Game update loop
game.update = function () {
if (gameState !== 'playing') return;
// Update distance - increase when moving upward, decrease when moving downward
if (player.velocityY < 0) {
// Negative velocity means moving up
distanceTraveled += Math.abs(player.velocityY) * 0.1;
} else if (player.velocityY > 0) {
// Positive velocity means moving down
distanceTraveled -= Math.abs(player.velocityY) * 0.1;
// Prevent distance from going below 0
if (distanceTraveled < 0) {
distanceTraveled = 0;
}
}
distanceTxt.setText(Math.floor(distanceTraveled) + 'm');
// Increase game speed gradually
gameSpeed = 1 + distanceTraveled * 0.001;
// Generate new anchors as player moves up - reduced frequency for increased difficulty
if (player.y < lastAnchorY + 500) {
// Generate only 1 anchor instead of 3 to increase spacing
var anchorX = 200 + Math.random() * 1648;
var anchorY = lastAnchorY - 400 - Math.random() * 200; // Increased spacing
generateAnchor(anchorX, anchorY);
lastAnchorY -= 500; // Increased distance between anchor generations
}
// Generate strategic obstacles occasionally for difficulty - reduced frequency
if (player.y < lastWallY + 1200 && Math.random() < 0.15) {
// Create narrow passages or single walls at strategic positions
if (Math.random() < 0.5) {
// Single wall obstacle
var wallX = 300 + Math.random() * 1448;
var wallY = lastWallY - 600 - Math.random() * 200;
generateWall(wallX, wallY, 150, 30);
// Add one anchor in front of the wall for grappling opportunity
var attempts = 0;
var anchorPlaced = false;
while (!anchorPlaced && attempts < 5) {
var anchorX = wallX + (Math.random() - 0.5) * 200;
var anchorY = wallY - 150 - Math.random() * 100;
if (generateAnchor(anchorX, anchorY)) {
anchorPlaced = true;
}
attempts++;
}
} else {
// Narrow passage between two walls
var gapCenter = 400 + Math.random() * 1248;
var gapSize = 300 + Math.random() * 200;
var wallY = lastWallY - 600 - Math.random() * 200;
// Left wall
generateWall(gapCenter - gapSize / 2 - 100, wallY, 200, 30);
// Right wall
generateWall(gapCenter + gapSize / 2 + 100, wallY, 200, 30);
// Add one anchor in front of the passage for strategic grappling
var attempts = 0;
var anchorPlaced = false;
while (!anchorPlaced && attempts < 5) {
var anchorX = gapCenter + (Math.random() - 0.5) * gapSize * 0.5;
var anchorY = wallY - 150 - Math.random() * 100;
if (generateAnchor(anchorX, anchorY)) {
anchorPlaced = true;
}
attempts++;
}
}
lastWallY -= 1200;
}
for (var i = anchors.length - 1; i >= 0; i--) {
var anchor = anchors[i];
if (anchor.y > player.y + 1000) {
anchor.destroy();
anchors.splice(i, 1);
}
}
// Clean up old walls
for (var i = walls.length - 1; i >= 0; i--) {
var wall = walls[i];
if (wall.y > player.y + 1000) {
wall.destroy();
walls.splice(i, 1);
}
}
// Wall collision detection
for (var i = 0; i < walls.length; i++) {
var wall = walls[i];
if (player.intersects(wall)) {
LK.effects.flashScreen(0xff0000, 1000);
handleGameOver();
return;
}
}
if (player.x < 0 || player.x > 2048 || player.y > 2732 + 200) {
LK.effects.flashScreen(0xff0000, 1000);
handleGameOver();
return;
}
// Camera follow (move everything down as player moves up)
if (player.y < 1366) {
var offset = 1366 - player.y;
player.y = 1366;
// Move all anchors down
for (var i = 0; i < anchors.length; i++) {
anchors[i].y += offset;
}
// Move all walls down
for (var i = 0; i < walls.length; i++) {
walls[i].y += offset;
}
lastAnchorY += offset;
lastWallY += offset;
}
};
// Handle game over with high score saving
function handleGameOver() {
// Update high score and max distance if needed
var currentScore = LK.getScore();
var currentDistance = distanceTraveled;
var highScore = storage.highScore || 0;
var maxDistance = storage.maxDistance || 0;
if (currentScore > highScore) {
storage.highScore = currentScore;
}
if (currentDistance > maxDistance) {
storage.maxDistance = currentDistance;
}
// Clear UI elements
if (scoreTxt) {
scoreTxt.destroy();
scoreTxt = null;
}
if (distanceTxt) {
distanceTxt.destroy();
distanceTxt = null;
}
// Show game over and then return to menu
LK.showGameOver();
LK.setTimeout(function () {
showMainMenu();
}, 2000);
}