/**** * Plugins ****/ var tween = LK.import("@upit/tween.v1"); var storage = LK.import("@upit/storage.v1", { unlockedSkins: [0], collectedSacks: {}, currentLevel: 1, totalSacks: 0 }); /**** * Classes ****/ // Character var Character = Container.expand(function () { var self = Container.call(this); self.skinId = 0; self.sprite = null; self.vx = 0; self.vy = 0; self.onGround = false; self.jumpQueued = false; self.width = 100; self.height = 100; self.setSkin = function (skinId) { if (self.sprite) { self.removeChild(self.sprite); } self.skinId = skinId; self.sprite = self.attachAsset('char_skin_' + skinId, { anchorX: 0.5, anchorY: 0.5 }); self.width = self.sprite.width; self.height = self.sprite.height; }; self.init = function (skinId) { self.setSkin(skinId); self.x = 200; self.y = 200; self.vx = 0; self.vy = 0; self.onGround = false; self.jumpQueued = false; }; self.update = function () { // Physics handled in main game loop }; return self; }); /**** * Level Data ****/ // For MVP, define a few sample levels. Each level: {platforms:[], sacks:[], portal:{x,y}, spawn:{x,y}} // Platform (static or moving) var Platform = Container.expand(function () { var self = Container.call(this); self.isMoving = false; self.moveDir = 1; self.moveAxis = 'x'; self.moveRange = 0; self.moveSpeed = 0; self.startPos = { x: 0, y: 0 }; self.init = function (data) { // data: {x, y, width, height, moving, axis, range, speed} var assetId = data.moving ? 'platform_moving' : 'platform'; var plat = self.attachAsset(assetId, { anchorX: 0.5, anchorY: 0.5, width: data.width, height: data.height }); self.x = data.x; self.y = data.y; self.width = data.width; self.height = data.height; self.isMoving = !!data.moving; if (self.isMoving) { self.moveAxis = data.axis || 'x'; self.moveRange = data.range || 0; self.moveSpeed = data.speed || 0; self.startPos.x = data.x; self.startPos.y = data.y; } }; self.update = function () { if (self.isMoving) { var t = Math.sin(LK.ticks * self.moveSpeed / 60) * self.moveRange; if (self.moveAxis === 'x') { self.x = self.startPos.x + t; } else { self.y = self.startPos.y + t; } } }; return self; }); // Portal (level exit) var Portal = Container.expand(function () { var self = Container.call(this); var portal = self.attachAsset('portal', { anchorX: 0.5, anchorY: 0.5 }); self.x = 0; self.y = 0; self.init = function (data) { self.x = data.x; self.y = data.y; }; return self; }); // Sack collectible var Sack = Container.expand(function () { var self = Container.call(this); var sack = self.attachAsset('sack', { anchorX: 0.5, anchorY: 0.5 }); self.collected = false; self.level = 1; self.sackId = 0; // unique per level self.init = function (data) { self.x = data.x; self.y = data.y; self.level = data.level; self.sackId = data.sackId; self.collected = false; sack.alpha = 1; }; self.collect = function () { if (!self.collected) { self.collected = true; tween(sack, { alpha: 0 }, { duration: 300, onFinish: function onFinish() { self.visible = false; } }); } }; return self; }); /**** * Initialize Game ****/ var game = new LK.Game({ backgroundColor: 0x222222 }); /**** * Game Code ****/ // For MVP, define a few sample levels. Each level: {platforms:[], sacks:[], portal:{x,y}, spawn:{x,y}} /**** * Level Data ****/ // Character skins (15 colors) // Portal (level exit) // Sack collectible // Moving platform // Platform (static) // --- Global State --- var LEVELS = [ // Level 1 { platforms: [{ x: 1024, y: 2600, width: 800, height: 60 }, // ground { x: 600, y: 2200, width: 400, height: 60 }, { x: 1400, y: 2000, width: 400, height: 60 }, { x: 1024, y: 1700, width: 400, height: 60, moving: true, axis: 'x', range: 200, speed: 1.2 }], sacks: [{ x: 600, y: 2100, sackId: 0 }, { x: 1400, y: 1900, sackId: 1 }], portal: { x: 1024, y: 1550 }, spawn: { x: 1024, y: 2500 } }, // Level 2 { platforms: [{ x: 1024, y: 2600, width: 800, height: 60 }, { x: 400, y: 2200, width: 400, height: 60, moving: true, axis: 'y', range: 150, speed: 1.5 }, { x: 1648, y: 2200, width: 400, height: 60, moving: true, axis: 'y', range: 150, speed: 1.5 }, { x: 1024, y: 1800, width: 400, height: 60 }], sacks: [{ x: 400, y: 2100, sackId: 0 }, { x: 1648, y: 2100, sackId: 1 }, { x: 1024, y: 1700, sackId: 2 }], portal: { x: 1024, y: 1650 }, spawn: { x: 1024, y: 2500 } } // ... up to 200 levels in future ]; var currentLevel = storage.currentLevel || 1; var unlockedSkins = storage.unlockedSkins || [0]; var selectedSkin = unlockedSkins[0]; var collectedSacks = storage.collectedSacks || {}; var totalSacks = storage.totalSacks || 0; var character = null; var platforms = []; var sacks = []; var portal = null; var levelData = null; var isTouching = false; var touchStartX = 0, touchStartY = 0; var moveDir = 0; // -1 left, 1 right, 0 none var jumpPressed = false; var canJump = false; var cameraY = 0; var levelText = null; var sackText = null; var skinText = null; var skinSelectNodes = []; var skinSelectActive = false; // --- UI --- function updateSackText() { var collected = 0, total = 0; if (levelData && levelData.sacks) { total = levelData.sacks.length; var key = 'L' + currentLevel; var arr = collectedSacks[key] || []; for (var i = 0; i < arr.length; ++i) if (arr[i]) collected++; } sackText.setText('Sacks: ' + collected + '/' + total); } function updateLevelText() { levelText.setText('Level ' + currentLevel); } function updateSkinText() { skinText.setText('Skins: ' + unlockedSkins.length + '/15'); } // --- UI Elements --- levelText = new Text2('Level 1', { size: 90, fill: "#fff" }); levelText.anchor.set(0.5, 0); LK.gui.top.addChild(levelText); sackText = new Text2('Sacks: 0/0', { size: 90, fill: "#fff" }); sackText.anchor.set(0.5, 0); LK.gui.top.addChild(sackText); skinText = new Text2('Skins: 1/15', { size: 70, fill: "#fff" }); skinText.anchor.set(0.5, 0); LK.gui.bottom.addChild(skinText); // --- Skin Select UI --- function showSkinSelect() { if (skinSelectActive) return; skinSelectActive = true; for (var i = 0; i < 15; ++i) { var node = LK.getAsset('char_skin_' + i, { anchorX: 0.5, anchorY: 0.5, scaleX: 0.7, scaleY: 0.7 }); node.x = 300 + i * 120; node.y = 400; node.alpha = unlockedSkins.indexOf(i) !== -1 ? 1 : 0.3; node.skinId = i; node.interactive = true; node.down = function (x, y, obj) { if (unlockedSkins.indexOf(obj.skinId) !== -1) { selectedSkin = obj.skinId; storage.selectedSkin = selectedSkin; character.setSkin(selectedSkin); hideSkinSelect(); } }; skinSelectNodes.push(node); LK.gui.center.addChild(node); } } function hideSkinSelect() { skinSelectActive = false; for (var i = 0; i < skinSelectNodes.length; ++i) { LK.gui.center.removeChild(skinSelectNodes[i]); } skinSelectNodes = []; } // --- Level Loading --- function loadLevel(n) { // Clean up previous for (var i = 0; i < platforms.length; ++i) game.removeChild(platforms[i]); for (var i = 0; i < sacks.length; ++i) game.removeChild(sacks[i]); if (portal) game.removeChild(portal); platforms = []; sacks = []; portal = null; cameraY = 0; // Clamp level if (n < 1) n = 1; if (n > LEVELS.length) n = LEVELS.length; currentLevel = n; storage.currentLevel = currentLevel; levelData = LEVELS[n - 1]; // Platforms for (var i = 0; i < levelData.platforms.length; ++i) { var p = new Platform(); p.init(levelData.platforms[i]); platforms.push(p); game.addChild(p); } // Sacks var key = 'L' + currentLevel; if (!collectedSacks[key]) collectedSacks[key] = []; for (var i = 0; i < levelData.sacks.length; ++i) { var s = new Sack(); var data = levelData.sacks[i]; s.init({ x: data.x, y: data.y, level: currentLevel, sackId: data.sackId }); if (collectedSacks[key][data.sackId]) { s.collected = true; s.visible = false; } sacks.push(s); game.addChild(s); } // Portal portal = new Portal(); portal.init(levelData.portal); game.addChild(portal); // Character if (!character) { character = new Character(); character.init(selectedSkin); game.addChild(character); } else { character.init(selectedSkin); } character.x = levelData.spawn.x; character.y = levelData.spawn.y; updateLevelText(); updateSackText(); updateSkinText(); } // --- Physics/Collision --- function rectsIntersect(a, b) { return a.x - a.width / 2 < b.x + b.width / 2 && a.x + a.width / 2 > b.x - b.width / 2 && a.y - a.height / 2 < b.y + b.height / 2 && a.y + a.height / 2 > b.y - b.height / 2; } // --- Touch Controls --- // Touch left half: move left. Right half: move right. Tap top 1/3: jump. Tap bottom: open skin select. game.down = function (x, y, obj) { if (skinSelectActive) return; isTouching = true; touchStartX = x; touchStartY = y; var w = 2048, h = 2732; if (y > h - 300) { // Bottom area: open skin select showSkinSelect(); return; } if (y < h / 3) { // Top: jump jumpPressed = true; return; } if (x < w / 2) { moveDir = -1; } else { moveDir = 1; } }; game.up = function (x, y, obj) { isTouching = false; moveDir = 0; jumpPressed = false; }; game.move = function (x, y, obj) { // Not used for movement, but could be for drag controls }; // --- Main Game Loop --- game.update = function () { // Update moving platforms for (var i = 0; i < platforms.length; ++i) { platforms[i].update(); } // Character physics var g = 2.2; var maxVy = 40; var maxVx = 18; var accel = 2.5; var friction = 0.85; var jumpV = -38; var c = character; // Horizontal movement if (moveDir !== 0) { c.vx += moveDir * accel; if (c.vx > maxVx) c.vx = maxVx; if (c.vx < -maxVx) c.vx = -maxVx; } else { c.vx *= friction; if (Math.abs(c.vx) < 0.5) c.vx = 0; } // Gravity c.vy += g; if (c.vy > maxVy) c.vy = maxVy; // Platform collision c.onGround = false; var nextX = c.x + c.vx; var nextY = c.y + c.vy; for (var i = 0; i < platforms.length; ++i) { var p = platforms[i]; // Only check collision if close if (Math.abs(nextX - p.x) < 400 && Math.abs(nextY - p.y) < 200) { // Simple AABB var px = p.x, py = p.y, pw = p.width, ph = p.height; // Check vertical collision (falling onto platform) if (c.x + c.width / 2 > px - pw / 2 && c.x - c.width / 2 < px + pw / 2) { // From above if (c.y + c.height / 2 <= py - ph / 2 && nextY + c.height / 2 >= py - ph / 2) { // Land on platform nextY = py - ph / 2 - c.height / 2; c.vy = 0; c.onGround = true; } } // Horizontal collision (run into side) if (c.y + c.height / 2 > py - ph / 2 && c.y - c.height / 2 < py + ph / 2) { // From left if (c.x - c.width / 2 <= px + pw / 2 && nextX - c.width / 2 >= px + pw / 2) { nextX = px + pw / 2 + c.width / 2; c.vx = 0; } // From right if (c.x + c.width / 2 >= px - pw / 2 && nextX + c.width / 2 <= px - pw / 2) { nextX = px - pw / 2 - c.width / 2; c.vx = 0; } } } } // Jump if (jumpPressed && c.onGround) { c.vy = jumpV; c.onGround = false; jumpPressed = false; } // Move character c.x = nextX; c.y = nextY; // Prevent falling out of bounds if (c.y > 2800) { // Respawn c.x = levelData.spawn.x; c.y = levelData.spawn.y; c.vx = 0; c.vy = 0; } // Camera follow var targetCamY = c.y - 1200; if (targetCamY < 0) targetCamY = 0; cameraY += (targetCamY - cameraY) * 0.15; game.y = -cameraY; // Sack collection var key = 'L' + currentLevel; for (var i = 0; i < sacks.length; ++i) { var s = sacks[i]; if (!s.collected && rectsIntersect(c, s)) { s.collect(); collectedSacks[key][s.sackId] = 1; storage.collectedSacks = collectedSacks; totalSacks++; storage.totalSacks = totalSacks; updateSackText(); // Unlock new skin every 10 sacks var unlockCount = unlockedSkins.length; if (totalSacks >= unlockCount * 10 && unlockCount < 15) { unlockedSkins.push(unlockCount); storage.unlockedSkins = unlockedSkins; updateSkinText(); // Flash to show unlock LK.effects.flashScreen(0x00ff00, 600); } } } // Portal check if (rectsIntersect(c, portal)) { // Next level if (currentLevel < LEVELS.length) { loadLevel(currentLevel + 1); } else { // Game complete LK.showYouWin(); } } }; // --- Start Game --- loadLevel(currentLevel); // --- Touch: Hide skin select on up if open LK.gui.center.up = function (x, y, obj) { if (skinSelectActive) hideSkinSelect(); };
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
unlockedSkins: [0],
collectedSacks: {},
currentLevel: 1,
totalSacks: 0
});
/****
* Classes
****/
// Character
var Character = Container.expand(function () {
var self = Container.call(this);
self.skinId = 0;
self.sprite = null;
self.vx = 0;
self.vy = 0;
self.onGround = false;
self.jumpQueued = false;
self.width = 100;
self.height = 100;
self.setSkin = function (skinId) {
if (self.sprite) {
self.removeChild(self.sprite);
}
self.skinId = skinId;
self.sprite = self.attachAsset('char_skin_' + skinId, {
anchorX: 0.5,
anchorY: 0.5
});
self.width = self.sprite.width;
self.height = self.sprite.height;
};
self.init = function (skinId) {
self.setSkin(skinId);
self.x = 200;
self.y = 200;
self.vx = 0;
self.vy = 0;
self.onGround = false;
self.jumpQueued = false;
};
self.update = function () {
// Physics handled in main game loop
};
return self;
});
/****
* Level Data
****/
// For MVP, define a few sample levels. Each level: {platforms:[], sacks:[], portal:{x,y}, spawn:{x,y}}
// Platform (static or moving)
var Platform = Container.expand(function () {
var self = Container.call(this);
self.isMoving = false;
self.moveDir = 1;
self.moveAxis = 'x';
self.moveRange = 0;
self.moveSpeed = 0;
self.startPos = {
x: 0,
y: 0
};
self.init = function (data) {
// data: {x, y, width, height, moving, axis, range, speed}
var assetId = data.moving ? 'platform_moving' : 'platform';
var plat = self.attachAsset(assetId, {
anchorX: 0.5,
anchorY: 0.5,
width: data.width,
height: data.height
});
self.x = data.x;
self.y = data.y;
self.width = data.width;
self.height = data.height;
self.isMoving = !!data.moving;
if (self.isMoving) {
self.moveAxis = data.axis || 'x';
self.moveRange = data.range || 0;
self.moveSpeed = data.speed || 0;
self.startPos.x = data.x;
self.startPos.y = data.y;
}
};
self.update = function () {
if (self.isMoving) {
var t = Math.sin(LK.ticks * self.moveSpeed / 60) * self.moveRange;
if (self.moveAxis === 'x') {
self.x = self.startPos.x + t;
} else {
self.y = self.startPos.y + t;
}
}
};
return self;
});
// Portal (level exit)
var Portal = Container.expand(function () {
var self = Container.call(this);
var portal = self.attachAsset('portal', {
anchorX: 0.5,
anchorY: 0.5
});
self.x = 0;
self.y = 0;
self.init = function (data) {
self.x = data.x;
self.y = data.y;
};
return self;
});
// Sack collectible
var Sack = Container.expand(function () {
var self = Container.call(this);
var sack = self.attachAsset('sack', {
anchorX: 0.5,
anchorY: 0.5
});
self.collected = false;
self.level = 1;
self.sackId = 0; // unique per level
self.init = function (data) {
self.x = data.x;
self.y = data.y;
self.level = data.level;
self.sackId = data.sackId;
self.collected = false;
sack.alpha = 1;
};
self.collect = function () {
if (!self.collected) {
self.collected = true;
tween(sack, {
alpha: 0
}, {
duration: 300,
onFinish: function onFinish() {
self.visible = false;
}
});
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x222222
});
/****
* Game Code
****/
// For MVP, define a few sample levels. Each level: {platforms:[], sacks:[], portal:{x,y}, spawn:{x,y}}
/****
* Level Data
****/
// Character skins (15 colors)
// Portal (level exit)
// Sack collectible
// Moving platform
// Platform (static)
// --- Global State ---
var LEVELS = [
// Level 1
{
platforms: [{
x: 1024,
y: 2600,
width: 800,
height: 60
},
// ground
{
x: 600,
y: 2200,
width: 400,
height: 60
}, {
x: 1400,
y: 2000,
width: 400,
height: 60
}, {
x: 1024,
y: 1700,
width: 400,
height: 60,
moving: true,
axis: 'x',
range: 200,
speed: 1.2
}],
sacks: [{
x: 600,
y: 2100,
sackId: 0
}, {
x: 1400,
y: 1900,
sackId: 1
}],
portal: {
x: 1024,
y: 1550
},
spawn: {
x: 1024,
y: 2500
}
},
// Level 2
{
platforms: [{
x: 1024,
y: 2600,
width: 800,
height: 60
}, {
x: 400,
y: 2200,
width: 400,
height: 60,
moving: true,
axis: 'y',
range: 150,
speed: 1.5
}, {
x: 1648,
y: 2200,
width: 400,
height: 60,
moving: true,
axis: 'y',
range: 150,
speed: 1.5
}, {
x: 1024,
y: 1800,
width: 400,
height: 60
}],
sacks: [{
x: 400,
y: 2100,
sackId: 0
}, {
x: 1648,
y: 2100,
sackId: 1
}, {
x: 1024,
y: 1700,
sackId: 2
}],
portal: {
x: 1024,
y: 1650
},
spawn: {
x: 1024,
y: 2500
}
}
// ... up to 200 levels in future
];
var currentLevel = storage.currentLevel || 1;
var unlockedSkins = storage.unlockedSkins || [0];
var selectedSkin = unlockedSkins[0];
var collectedSacks = storage.collectedSacks || {};
var totalSacks = storage.totalSacks || 0;
var character = null;
var platforms = [];
var sacks = [];
var portal = null;
var levelData = null;
var isTouching = false;
var touchStartX = 0,
touchStartY = 0;
var moveDir = 0; // -1 left, 1 right, 0 none
var jumpPressed = false;
var canJump = false;
var cameraY = 0;
var levelText = null;
var sackText = null;
var skinText = null;
var skinSelectNodes = [];
var skinSelectActive = false;
// --- UI ---
function updateSackText() {
var collected = 0,
total = 0;
if (levelData && levelData.sacks) {
total = levelData.sacks.length;
var key = 'L' + currentLevel;
var arr = collectedSacks[key] || [];
for (var i = 0; i < arr.length; ++i) if (arr[i]) collected++;
}
sackText.setText('Sacks: ' + collected + '/' + total);
}
function updateLevelText() {
levelText.setText('Level ' + currentLevel);
}
function updateSkinText() {
skinText.setText('Skins: ' + unlockedSkins.length + '/15');
}
// --- UI Elements ---
levelText = new Text2('Level 1', {
size: 90,
fill: "#fff"
});
levelText.anchor.set(0.5, 0);
LK.gui.top.addChild(levelText);
sackText = new Text2('Sacks: 0/0', {
size: 90,
fill: "#fff"
});
sackText.anchor.set(0.5, 0);
LK.gui.top.addChild(sackText);
skinText = new Text2('Skins: 1/15', {
size: 70,
fill: "#fff"
});
skinText.anchor.set(0.5, 0);
LK.gui.bottom.addChild(skinText);
// --- Skin Select UI ---
function showSkinSelect() {
if (skinSelectActive) return;
skinSelectActive = true;
for (var i = 0; i < 15; ++i) {
var node = LK.getAsset('char_skin_' + i, {
anchorX: 0.5,
anchorY: 0.5,
scaleX: 0.7,
scaleY: 0.7
});
node.x = 300 + i * 120;
node.y = 400;
node.alpha = unlockedSkins.indexOf(i) !== -1 ? 1 : 0.3;
node.skinId = i;
node.interactive = true;
node.down = function (x, y, obj) {
if (unlockedSkins.indexOf(obj.skinId) !== -1) {
selectedSkin = obj.skinId;
storage.selectedSkin = selectedSkin;
character.setSkin(selectedSkin);
hideSkinSelect();
}
};
skinSelectNodes.push(node);
LK.gui.center.addChild(node);
}
}
function hideSkinSelect() {
skinSelectActive = false;
for (var i = 0; i < skinSelectNodes.length; ++i) {
LK.gui.center.removeChild(skinSelectNodes[i]);
}
skinSelectNodes = [];
}
// --- Level Loading ---
function loadLevel(n) {
// Clean up previous
for (var i = 0; i < platforms.length; ++i) game.removeChild(platforms[i]);
for (var i = 0; i < sacks.length; ++i) game.removeChild(sacks[i]);
if (portal) game.removeChild(portal);
platforms = [];
sacks = [];
portal = null;
cameraY = 0;
// Clamp level
if (n < 1) n = 1;
if (n > LEVELS.length) n = LEVELS.length;
currentLevel = n;
storage.currentLevel = currentLevel;
levelData = LEVELS[n - 1];
// Platforms
for (var i = 0; i < levelData.platforms.length; ++i) {
var p = new Platform();
p.init(levelData.platforms[i]);
platforms.push(p);
game.addChild(p);
}
// Sacks
var key = 'L' + currentLevel;
if (!collectedSacks[key]) collectedSacks[key] = [];
for (var i = 0; i < levelData.sacks.length; ++i) {
var s = new Sack();
var data = levelData.sacks[i];
s.init({
x: data.x,
y: data.y,
level: currentLevel,
sackId: data.sackId
});
if (collectedSacks[key][data.sackId]) {
s.collected = true;
s.visible = false;
}
sacks.push(s);
game.addChild(s);
}
// Portal
portal = new Portal();
portal.init(levelData.portal);
game.addChild(portal);
// Character
if (!character) {
character = new Character();
character.init(selectedSkin);
game.addChild(character);
} else {
character.init(selectedSkin);
}
character.x = levelData.spawn.x;
character.y = levelData.spawn.y;
updateLevelText();
updateSackText();
updateSkinText();
}
// --- Physics/Collision ---
function rectsIntersect(a, b) {
return a.x - a.width / 2 < b.x + b.width / 2 && a.x + a.width / 2 > b.x - b.width / 2 && a.y - a.height / 2 < b.y + b.height / 2 && a.y + a.height / 2 > b.y - b.height / 2;
}
// --- Touch Controls ---
// Touch left half: move left. Right half: move right. Tap top 1/3: jump. Tap bottom: open skin select.
game.down = function (x, y, obj) {
if (skinSelectActive) return;
isTouching = true;
touchStartX = x;
touchStartY = y;
var w = 2048,
h = 2732;
if (y > h - 300) {
// Bottom area: open skin select
showSkinSelect();
return;
}
if (y < h / 3) {
// Top: jump
jumpPressed = true;
return;
}
if (x < w / 2) {
moveDir = -1;
} else {
moveDir = 1;
}
};
game.up = function (x, y, obj) {
isTouching = false;
moveDir = 0;
jumpPressed = false;
};
game.move = function (x, y, obj) {
// Not used for movement, but could be for drag controls
};
// --- Main Game Loop ---
game.update = function () {
// Update moving platforms
for (var i = 0; i < platforms.length; ++i) {
platforms[i].update();
}
// Character physics
var g = 2.2;
var maxVy = 40;
var maxVx = 18;
var accel = 2.5;
var friction = 0.85;
var jumpV = -38;
var c = character;
// Horizontal movement
if (moveDir !== 0) {
c.vx += moveDir * accel;
if (c.vx > maxVx) c.vx = maxVx;
if (c.vx < -maxVx) c.vx = -maxVx;
} else {
c.vx *= friction;
if (Math.abs(c.vx) < 0.5) c.vx = 0;
}
// Gravity
c.vy += g;
if (c.vy > maxVy) c.vy = maxVy;
// Platform collision
c.onGround = false;
var nextX = c.x + c.vx;
var nextY = c.y + c.vy;
for (var i = 0; i < platforms.length; ++i) {
var p = platforms[i];
// Only check collision if close
if (Math.abs(nextX - p.x) < 400 && Math.abs(nextY - p.y) < 200) {
// Simple AABB
var px = p.x,
py = p.y,
pw = p.width,
ph = p.height;
// Check vertical collision (falling onto platform)
if (c.x + c.width / 2 > px - pw / 2 && c.x - c.width / 2 < px + pw / 2) {
// From above
if (c.y + c.height / 2 <= py - ph / 2 && nextY + c.height / 2 >= py - ph / 2) {
// Land on platform
nextY = py - ph / 2 - c.height / 2;
c.vy = 0;
c.onGround = true;
}
}
// Horizontal collision (run into side)
if (c.y + c.height / 2 > py - ph / 2 && c.y - c.height / 2 < py + ph / 2) {
// From left
if (c.x - c.width / 2 <= px + pw / 2 && nextX - c.width / 2 >= px + pw / 2) {
nextX = px + pw / 2 + c.width / 2;
c.vx = 0;
}
// From right
if (c.x + c.width / 2 >= px - pw / 2 && nextX + c.width / 2 <= px - pw / 2) {
nextX = px - pw / 2 - c.width / 2;
c.vx = 0;
}
}
}
}
// Jump
if (jumpPressed && c.onGround) {
c.vy = jumpV;
c.onGround = false;
jumpPressed = false;
}
// Move character
c.x = nextX;
c.y = nextY;
// Prevent falling out of bounds
if (c.y > 2800) {
// Respawn
c.x = levelData.spawn.x;
c.y = levelData.spawn.y;
c.vx = 0;
c.vy = 0;
}
// Camera follow
var targetCamY = c.y - 1200;
if (targetCamY < 0) targetCamY = 0;
cameraY += (targetCamY - cameraY) * 0.15;
game.y = -cameraY;
// Sack collection
var key = 'L' + currentLevel;
for (var i = 0; i < sacks.length; ++i) {
var s = sacks[i];
if (!s.collected && rectsIntersect(c, s)) {
s.collect();
collectedSacks[key][s.sackId] = 1;
storage.collectedSacks = collectedSacks;
totalSacks++;
storage.totalSacks = totalSacks;
updateSackText();
// Unlock new skin every 10 sacks
var unlockCount = unlockedSkins.length;
if (totalSacks >= unlockCount * 10 && unlockCount < 15) {
unlockedSkins.push(unlockCount);
storage.unlockedSkins = unlockedSkins;
updateSkinText();
// Flash to show unlock
LK.effects.flashScreen(0x00ff00, 600);
}
}
}
// Portal check
if (rectsIntersect(c, portal)) {
// Next level
if (currentLevel < LEVELS.length) {
loadLevel(currentLevel + 1);
} else {
// Game complete
LK.showYouWin();
}
}
};
// --- Start Game ---
loadLevel(currentLevel);
// --- Touch: Hide skin select on up if open
LK.gui.center.up = function (x, y, obj) {
if (skinSelectActive) hideSkinSelect();
};