/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Artifact class: collectible for score
var Artifact = Container.expand(function () {
var self = Container.call(this);
var artifactSprite = self.attachAsset('artifact', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = artifactSprite.width / 2;
self.collected = false;
self.getCenter = function () {
return {
x: self.x,
y: self.y
};
};
return self;
});
// Door class: exit to next area
var Door = Container.expand(function () {
var self = Container.call(this);
var doorSprite = self.attachAsset('door', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = doorSprite.width;
self.height = doorSprite.height;
self.getRect = function () {
return {
x: self.x - self.width / 2,
y: self.y - self.height / 2,
width: self.width,
height: self.height
};
};
return self;
});
// Hero class: player character
var Hero = Container.expand(function () {
var self = Container.call(this);
var heroSprite = self.attachAsset('hero', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = heroSprite.width / 2;
self.lightRadius = 300; // Initial light radius
self.hasLight = false; // If currently holding a light orb
// For dragging
self.isDragging = false;
// For upgrades
self.abilities = {
lightRadius: 300
};
// For collision detection
self.getCenter = function () {
return {
x: self.x,
y: self.y
};
};
// For light effect
self.setLightRadius = function (r) {
self.lightRadius = r;
};
return self;
});
// LightOrb class: collectible light source
var LightOrb = Container.expand(function () {
var self = Container.call(this);
var orbSprite = self.attachAsset('lightOrb', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = orbSprite.width / 2;
self.collected = false;
self.getCenter = function () {
return {
x: self.x,
y: self.y
};
};
return self;
});
// Shadow class: obstacles/enemies
var Shadow = Container.expand(function () {
var self = Container.call(this);
var shadowSprite = self.attachAsset('shadow', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = shadowSprite.width / 2;
self.speed = 2 + Math.random() * 2; // Random speed for movement
self.direction = Math.random() * Math.PI * 2; // Random direction
// For simple movement (patrol)
self.update = function () {
// If reactive (Survival Camp), chase or avoid hero based on light
if (self.isReactive && typeof hero !== "undefined") {
var dx = hero.x - self.x;
var dy = hero.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (hero.hasLight && dist < hero.lightRadius + 100) {
// Avoid hero if hero has light
var angle = Math.atan2(-dy, -dx);
self.direction = angle + (Math.random() - 0.5) * 0.5;
self.speed = 3.5 + Math.random() * 1.5;
} else if (dist < 600) {
// Chase hero if close and not lit
var angle = Math.atan2(dy, dx);
self.direction = angle + (Math.random() - 0.5) * 0.2;
self.speed = 3 + Math.random();
}
}
self.x += Math.cos(self.direction) * self.speed;
self.y += Math.sin(self.direction) * self.speed;
// Bounce off walls
if (self.x < self.radius || self.x > 2048 - self.radius) {
self.direction = Math.PI - self.direction;
}
if (self.y < self.radius + 100 || self.y > 2732 - self.radius) {
self.direction = -self.direction;
}
};
// For collision detection
self.getCenter = function () {
return {
x: self.x,
y: self.y
};
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x111122
});
/****
* Game Code
****/
// --- Global variables ---
/*
We use simple shapes for the MVP:
- 'hero': the player character (circle, bright color)
- 'shadow': shadow obstacles (ellipse, dark color)
- 'lightOrb': collectible light source (circle, yellowish)
- 'door': exit to next area (rectangle, light blue)
- 'fog': semi-transparent overlay for darkness (rectangle, black, alpha)
- 'artifact': collectible (star-shaped, but use ellipse for MVP, gold)
*/
// Music asset (id: 'bgmusic', volume: 0.7)
var hero;
var shadows = [];
var lightOrbs = [];
var artifacts = [];
var door;
var fogOverlay;
var dragging = false;
var dragOffset = {
x: 0,
y: 0
};
var lastTouch = {
x: 0,
y: 0
};
var level = 1;
var maxLevel = 20;
var artifactCount = 0;
var artifactTotal = 0;
var lightFadeTween = null;
// --- GUI ---
var artifactTxt = new Text2('Artifacts: 0/0', {
size: 80,
fill: 0xFFE066
});
artifactTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(artifactTxt);
var levelTxt = new Text2('Level 1', {
size: 80,
fill: 0x66CCFF
});
levelTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(levelTxt);
// Health bar: show as "Health: [|||]" (3 max)
var heroMaxHealth = 3;
var heroHealth = heroMaxHealth;
var healthBarTxt = new Text2('Health: |||', {
size: 80,
fill: 0xFF6666
});
healthBarTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(healthBarTxt);
// Position GUI elements
artifactTxt.y = 100;
levelTxt.y = 10;
healthBarTxt.y = 190;
// Helper to update health bar display
function updateHealthBar() {
var bars = '';
for (var i = 0; i < heroHealth; i++) bars += '|';
for (var i = heroHealth; i < heroMaxHealth; i++) bars += ' ';
healthBarTxt.setText('Health: ' + bars);
}
updateHealthBar();
// --- Helper functions ---
function distance(a, b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
function circleRectIntersect(cx, cy, cr, rx, ry, rw, rh) {
// Find the closest point to the circle within the rectangle
var closestX = Math.max(rx, Math.min(cx, rx + rw));
var closestY = Math.max(ry, Math.min(cy, ry + rh));
// Calculate the distance between the circle's center and this closest point
var dx = cx - closestX;
var dy = cy - closestY;
// If the distance is less than the circle's radius, an intersection occurs
return dx * dx + dy * dy < cr * cr;
}
// --- Level setup ---
function setupLevel(lvl) {
// Clear previous
for (var i = 0; i < shadows.length; i++) {
shadows[i].destroy();
}
for (var i = 0; i < lightOrbs.length; i++) {
lightOrbs[i].destroy();
}
for (var i = 0; i < artifacts.length; i++) {
artifacts[i].destroy();
}
if (door) door.destroy();
if (fogOverlay) fogOverlay.destroy();
shadows = [];
lightOrbs = [];
artifacts = [];
door = null;
fogOverlay = null;
artifactCount = 0;
// Hero position
if (!hero) {
hero = new Hero();
game.addChild(hero);
}
hero.x = 400;
hero.y = 2732 / 2;
hero.setLightRadius(300 + (lvl - 1) * 40);
hero.hasLight = false;
// Reset health for new level
heroHealth = heroMaxHealth;
updateHealthBar();
// --- Location logic ---
// Ruined Laboratory (level 1): default
// Ruined City (level 2-4): more shadows, more artifacts, shadows move faster
// Survival Camp (level 5+): shadows react to hero, more light orbs, artifacts clustered
if (lvl === 1) {
// Ruined Laboratory: default
// Shadows: none for level 1
// Light orb: always one, placed randomly
var orb = new LightOrb();
orb.x = 400 + Math.random() * (2048 - 800);
orb.y = 300 + Math.random() * (2732 - 600);
lightOrbs.push(orb);
game.addChild(orb);
// Artifacts: 1-2
artifactTotal = 1 + Math.floor(Math.random() * 2);
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
var tries = 0;
do {
a.x = 400 + Math.random() * (2048 - 800);
a.y = 300 + Math.random() * (2732 - 600);
var ok = true;
if (distance({
x: a.x,
y: a.y
}, {
x: hero.x,
y: hero.y
}) < 300) ok = false;
for (var j = 0; j < artifacts.length; j++) {
if (distance({
x: a.x,
y: a.y
}, {
x: artifacts[j].x,
y: artifacts[j].y
}) < 200) ok = false;
}
tries++;
if (tries > 10) break;
} while (!ok);
artifacts.push(a);
game.addChild(a);
}
} else if (lvl >= 2 && lvl <= 4) {
// Ruined City: more/faster shadows, more artifacts
// Shadows: 2-4, move faster
var shadowCount = 2 + Math.floor(Math.random() * 3);
for (var i = 0; i < shadowCount; i++) {
var s = new Shadow();
s.x = 800 + Math.random() * (2048 - 1200);
s.y = 400 + Math.random() * (2732 - 800);
s.speed = 3 + Math.random() * 2;
shadows.push(s);
game.addChild(s);
}
// Light orb: always one, placed randomly
var orb = new LightOrb();
orb.x = 400 + Math.random() * (2048 - 800);
orb.y = 300 + Math.random() * (2732 - 600);
lightOrbs.push(orb);
game.addChild(orb);
// Artifacts: 2-4
artifactTotal = 2 + Math.floor(Math.random() * 3);
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
var tries = 0;
do {
a.x = 400 + Math.random() * (2048 - 800);
a.y = 300 + Math.random() * (2732 - 600);
var ok = true;
if (distance({
x: a.x,
y: a.y
}, {
x: hero.x,
y: hero.y
}) < 300) ok = false;
for (var j = 0; j < artifacts.length; j++) {
if (distance({
x: a.x,
y: a.y
}, {
x: artifacts[j].x,
y: artifacts[j].y
}) < 200) ok = false;
}
tries++;
if (tries > 10) break;
} while (!ok);
artifacts.push(a);
game.addChild(a);
}
} else if (lvl >= 5 && lvl <= 7) {
// Survival Camp (lvl 5-7): shadows react to hero, more light orbs, artifacts clustered
// Shadows: 3-5, reactive to hero
var shadowCount = 3 + Math.floor(Math.random() * 3);
for (var i = 0; i < shadowCount; i++) {
var s = new Shadow();
s.x = 1000 + Math.random() * 800;
s.y = 800 + Math.random() * 1200;
s.isReactive = true;
shadows.push(s);
game.addChild(s);
}
// Two light orbs, placed near each other
for (var i = 0; i < 2; i++) {
var orb = new LightOrb();
orb.x = 1200 + Math.random() * 400;
orb.y = 1000 + Math.random() * 400;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 3-5, clustered
artifactTotal = 3 + Math.floor(Math.random() * 3);
var clusterX = 1500 + Math.random() * 300;
var clusterY = 1500 + Math.random() * 300;
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = clusterX + Math.random() * 120 - 60;
a.y = clusterY + Math.random() * 120 - 60;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 8) {
// Level 8: "Collapsed Tunnels" - narrow corridor, many shadows, few orbs
// Shadows: 6, placed along the tunnel
for (var i = 0; i < 6; i++) {
var s = new Shadow();
s.x = 800 + i * 120;
s.y = 600 + Math.random() * 1500;
shadows.push(s);
game.addChild(s);
}
// Only one light orb, placed at far end
var orb = new LightOrb();
orb.x = 1500;
orb.y = 2732 / 2;
lightOrbs.push(orb);
game.addChild(orb);
// Artifacts: 2, placed at dangerous spots
artifactTotal = 2;
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = 1200 + i * 300;
a.y = 800 + Math.random() * 1200;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 9) {
// Level 9: "Watcher’s Lair" - fewer, but very fast shadows, more orbs, artifacts near center
// Shadows: 3, very fast
for (var i = 0; i < 3; i++) {
var s = new Shadow();
s.x = 900 + i * 200;
s.y = 1000 + Math.random() * 800;
s.speed = 5 + Math.random() * 2;
shadows.push(s);
game.addChild(s);
}
// Three light orbs, spread out
for (var i = 0; i < 3; i++) {
var orb = new LightOrb();
orb.x = 700 + i * 400;
orb.y = 700 + Math.random() * 1500;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 4, near center
artifactTotal = 4;
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = 1000 + Math.random() * 400;
a.y = 1200 + Math.random() * 400;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 10) {
// Level 10: "Shadow Nexus" - final, many reactive shadows, clustered orbs/artifacts
// Shadows: 6, all reactive, but start in more mixed spots
var shadowPositions = [{
x: 1550 + Math.random() * 200,
y: 1200 + Math.random() * 200
}, {
x: 1700 + Math.random() * 200,
y: 1500 + Math.random() * 200
}, {
x: 1200 + Math.random() * 300,
y: 1100 + Math.random() * 600
}, {
x: 1400 + Math.random() * 400,
y: 1700 + Math.random() * 300
}, {
x: 1800 + Math.random() * 100,
y: 1300 + Math.random() * 400
}, {
x: 1300 + Math.random() * 400,
y: 1200 + Math.random() * 400
}];
for (var i = 0; i < 6; i++) {
var s = new Shadow();
s.x = shadowPositions[i].x;
s.y = shadowPositions[i].y;
s.isReactive = true;
shadows.push(s);
game.addChild(s);
}
// Four light orbs, clustered
for (var i = 0; i < 4; i++) {
var orb = new LightOrb();
orb.x = 1600 + Math.random() * 200;
orb.y = 1200 + Math.random() * 400;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 5, clustered
artifactTotal = 5;
var clusterX = 1700;
var clusterY = 1400;
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = clusterX + Math.random() * 100 - 50;
a.y = clusterY + Math.random() * 100 - 50;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 11) {
// Level 11: "The Forgotten Vault" - maze-like, slow but smart shadows, orbs at dead ends
// Shadows: 4, slow, placed at maze entrances
for (var i = 0; i < 4; i++) {
var s = new Shadow();
s.x = 600 + i * 350;
s.y = 400 + Math.random() * (2732 - 800);
s.speed = 1.5 + Math.random();
s.isReactive = true;
shadows.push(s);
game.addChild(s);
}
// Two light orbs, at far left and right
for (var i = 0; i < 2; i++) {
var orb = new LightOrb();
orb.x = i === 0 ? 400 : 2048 - 400;
orb.y = 400 + Math.random() * (2732 - 800);
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 3, spaced out
artifactTotal = 3;
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = 700 + i * 400;
a.y = 800 + Math.random() * 1200;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 12) {
// Level 12: "Flooded Passage" - orbs clustered bottom right, artifacts top left and top right
// Shadows: 3, slow, near bottom
for (var i = 0; i < 3; i++) {
var s = new Shadow();
s.x = 1200 + i * 200;
s.y = 2200 + Math.random() * 200;
s.speed = 1.5 + Math.random();
shadows.push(s);
game.addChild(s);
}
// Three light orbs, clustered near bottom right
for (var i = 0; i < 3; i++) {
var orb = new LightOrb();
orb.x = 1500 + Math.random() * 300;
orb.y = 2200 + Math.random() * 300;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 2, one at top left, one at top right
artifactTotal = 2;
var artifactPositions = [{
x: 350,
y: 350 + Math.random() * 150
}, {
x: 2048 - 350,
y: 350 + Math.random() * 150
}];
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = artifactPositions[i].x;
a.y = artifactPositions[i].y;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 13) {
// Level 13: "The Mirror Hall" - shadows mirror hero's movement, orbs/artifacts in corners
// Shadows: 2, mirror hero's movement (simulate by following hero with offset)
for (var i = 0; i < 2; i++) {
var s = new Shadow();
s.x = i === 0 ? 200 : 1848;
s.y = i === 0 ? 2532 : 200;
s.isReactive = true;
shadows.push(s);
game.addChild(s);
}
// Four light orbs, one in each corner
var corners = [{
x: 200,
y: 200
}, {
x: 1848,
y: 200
}, {
x: 200,
y: 2532
}, {
x: 1848,
y: 2532
}];
for (var i = 0; i < 4; i++) {
var orb = new LightOrb();
orb.x = corners[i].x;
orb.y = corners[i].y;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 4, one in each corner (offset from orbs)
artifactTotal = 4;
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = corners[i].x + 60;
a.y = corners[i].y + 60;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 14) {
// Level 14: "The Gauntlet" - many fast, non-reactive shadows, orbs in a vertical line, artifacts in zigzag spanning the play area
// Shadows: 7, fast, non-reactive
for (var i = 0; i < 7; i++) {
var s = new Shadow();
s.x = 400 + i * 220;
s.y = 400 + i % 2 * 1800;
s.speed = 4 + Math.random() * 2;
shadows.push(s);
game.addChild(s);
}
// Five light orbs, in a vertical line
for (var i = 0; i < 5; i++) {
var orb = new LightOrb();
orb.x = 1024;
orb.y = 400 + i * 500;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 5, zigzag pattern spanning the play area
artifactTotal = 5;
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = 500 + i * 300;
a.y = 600 + (i % 2 === 0 ? 600 : 1800);
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 15) {
// Level 15: "The Final Eclipse" - all shadows reactive, orbs/artifacts at center, max difficulty
// Shadows: 8, all reactive, clustered at center
for (var i = 0; i < 8; i++) {
var s = new Shadow();
s.x = 1024 + Math.cos(i / 8 * Math.PI * 2) * 200;
s.y = 1366 + Math.sin(i / 8 * Math.PI * 2) * 200;
s.isReactive = true;
shadows.push(s);
game.addChild(s);
}
// Six light orbs, clustered at center
for (var i = 0; i < 6; i++) {
var orb = new LightOrb();
orb.x = 1024 + Math.random() * 120 - 60;
orb.y = 1366 + Math.random() * 120 - 60;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 6, clustered at center
artifactTotal = 6;
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = 1024 + Math.random() * 100 - 50;
a.y = 1366 + Math.random() * 100 - 50;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 16) {
// Level 16: "Crystal Crossing" - orbs in a cross, artifacts at each arm tip
// Five light orbs, one at center, four at cross arms
var cross = [{
x: 1024,
y: 1366
},
// center
{
x: 1024,
y: 566
},
// top
{
x: 1024,
y: 2166
},
// bottom
{
x: 324,
y: 1366
},
// left
{
x: 1724,
y: 1366
} // right
];
// Shadows: 5, at cross arms
for (var i = 0; i < 5; i++) {
var s = new Shadow();
s.x = cross[i].x;
s.y = cross[i].y;
shadows.push(s);
game.addChild(s);
}
for (var i = 0; i < cross.length; i++) {
var orb = new LightOrb();
orb.x = cross[i].x;
orb.y = cross[i].y;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 4, at the tips of the cross (not center)
artifactTotal = 4;
var tips = [{
x: 1024,
y: 566
},
// top
{
x: 1024,
y: 2166
},
// bottom
{
x: 324,
y: 1366
},
// left
{
x: 1724,
y: 1366
} // right
];
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = tips[i].x;
a.y = tips[i].y;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 17) {
// Level 17: "Spiral of Light" - orbs in a spiral, artifacts at spiral endpoints
// Shadows: 8, distributed along the spiral (denser, more challenging)
var spiralCenterX = 1024;
var spiralCenterY = 1366;
var spiralRadiusStart = 200;
var spiralRadiusEnd = 800;
var spiralTurns = 2.25; // more turns for a clear spiral
var spiralOrbs = 8;
var shadowCount = 8;
for (var i = 0; i < shadowCount; i++) {
var t = i / (shadowCount - 1);
var angle = t * spiralTurns * Math.PI * 2;
var radius = spiralRadiusStart + (spiralRadiusEnd - spiralRadiusStart) * t;
var s = new Shadow();
s.x = spiralCenterX + Math.cos(angle) * radius;
s.y = spiralCenterY + Math.sin(angle) * radius;
shadows.push(s);
game.addChild(s);
}
// Add an extra shadow at the spiral endpoint (for extra challenge)
var endpointAngle = spiralTurns * Math.PI * 2;
var endpointX = spiralCenterX + Math.cos(endpointAngle) * spiralRadiusEnd;
var endpointY = spiralCenterY + Math.sin(endpointAngle) * spiralRadiusEnd;
var sEndpoint = new Shadow();
sEndpoint.x = endpointX;
sEndpoint.y = endpointY;
shadows.push(sEndpoint);
game.addChild(sEndpoint);
// Place 8 light orbs in a spiral pattern (tighter spiral, more visually clear)
for (var i = 0; i < spiralOrbs; i++) {
var t = i / (spiralOrbs - 1);
var angle = t * spiralTurns * Math.PI * 2;
var radius = spiralRadiusStart + (spiralRadiusEnd - spiralRadiusStart) * t;
var orb = new LightOrb();
orb.x = spiralCenterX + Math.cos(angle) * radius;
orb.y = spiralCenterY + Math.sin(angle) * radius;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 2, at the start and end of the spiral
artifactTotal = 2;
var artifactPositions = [{
x: spiralCenterX + Math.cos(0) * spiralRadiusStart,
y: spiralCenterY + Math.sin(0) * spiralRadiusStart
}, {
x: spiralCenterX + Math.cos(spiralTurns * Math.PI * 2) * spiralRadiusEnd,
y: spiralCenterY + Math.sin(spiralTurns * Math.PI * 2) * spiralRadiusEnd
}];
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = artifactPositions[i].x;
a.y = artifactPositions[i].y;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 18) {
// Level 18: "Circle of Light" - orbs in a ring, artifacts at cardinal points
var ringCenterX = 1024;
var ringCenterY = 1366;
var ringRadius = 900; // Increased from 700 to 900 to move orbs further from the door
// Shadows: 4, at cardinal points of the ring
for (var i = 0; i < 4; i++) {
var angle = i / 4 * Math.PI * 2;
var s = new Shadow();
s.x = ringCenterX + Math.cos(angle) * (ringRadius - 120);
s.y = ringCenterY + Math.sin(angle) * (ringRadius - 120);
shadows.push(s);
game.addChild(s);
}
// Add a shadow at the center of the ring
var sCenter = new Shadow();
sCenter.x = ringCenterX;
sCenter.y = ringCenterY;
shadows.push(sCenter);
game.addChild(sCenter);
// Place 8 light orbs in a circle
var ringOrbs = 8;
for (var i = 0; i < ringOrbs; i++) {
var angle = i / ringOrbs * Math.PI * 2;
var orb = new LightOrb();
orb.x = ringCenterX + Math.cos(angle) * ringRadius;
orb.y = ringCenterY + Math.sin(angle) * ringRadius;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 4, at N/E/S/W points of the ring
artifactTotal = 4;
var artifactPositions = [{
x: ringCenterX,
y: ringCenterY - ringRadius
}, {
x: ringCenterX + ringRadius,
y: ringCenterY
}, {
x: ringCenterX,
y: ringCenterY + ringRadius
}, {
x: ringCenterX - ringRadius,
y: ringCenterY
} // West
];
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = artifactPositions[i].x;
a.y = artifactPositions[i].y;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 19) {
// Level 19: "Zigzag Triangle" - orbs in a zigzag, artifacts in a triangle
// Artifacts and Shadows: 3, at triangle points
var triangle = [{
x: 1024,
y: 500
}, {
x: 400,
y: 2200
}, {
x: 1648,
y: 2200
}];
// Shadows: 3, at triangle points
for (var i = 0; i < 3; i++) {
var s = new Shadow();
s.x = triangle[i].x;
s.y = triangle[i].y;
shadows.push(s);
game.addChild(s);
}
// Add a shadow at the centroid of the triangle
var centroidX = (triangle[0].x + triangle[1].x + triangle[2].x) / 3;
var centroidY = (triangle[0].y + triangle[1].y + triangle[2].y) / 3;
var sCentroid = new Shadow();
sCentroid.x = centroidX;
sCentroid.y = centroidY;
shadows.push(sCentroid);
game.addChild(sCentroid);
// Place 6 light orbs in a zigzag pattern
var zigzagOrbs = 6;
for (var i = 0; i < zigzagOrbs; i++) {
var orb = new LightOrb();
orb.x = 400 + i * 280;
orb.y = i % 2 === 0 ? 700 : 2000;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 3, at triangle points
artifactTotal = 3;
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = triangle[i].x;
a.y = triangle[i].y;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 20) {
// Level 20: "Starfall Sanctuary" - orbs in a star, artifacts at star points
// Shadows: way too many, dense starburst pattern
var starCenterX = 1024;
var starCenterY = 1366;
var starRadius = 700;
var starPoints = 5;
// Add a dense burst of shadows radiating from the center
var shadowRings = 6; // number of concentric rings
var shadowsPerRing = 18; // number of shadows per ring
for (var ring = 1; ring <= shadowRings; ring++) {
var r = (starRadius - 200) * (ring / shadowRings) + 120;
for (var j = 0; j < shadowsPerRing; j++) {
var angle = j / shadowsPerRing * Math.PI * 2 + ring % 2 * (Math.PI / shadowsPerRing);
var s = new Shadow();
s.x = starCenterX + Math.cos(angle) * r;
s.y = starCenterY + Math.sin(angle) * r;
// Make some move faster for extra chaos
if (ring % 2 === 0) s.speed += 1.5;
shadows.push(s);
game.addChild(s);
}
}
// Add extra shadows at the star points (for visual emphasis)
for (var i = 0; i < starPoints; i++) {
var angle = -Math.PI / 2 + i * (2 * Math.PI / starPoints);
var s = new Shadow();
s.x = starCenterX + Math.cos(angle) * (starRadius - 100);
s.y = starCenterY + Math.sin(angle) * (starRadius - 100);
s.speed += 2;
shadows.push(s);
game.addChild(s);
}
// Place 5 light orbs in a star pattern (center + 4 points)
for (var i = 0; i < starPoints; i++) {
var angle = -Math.PI / 2 + i * (2 * Math.PI / starPoints);
var orb = new LightOrb();
orb.x = starCenterX + Math.cos(angle) * starRadius;
orb.y = starCenterY + Math.sin(angle) * starRadius;
lightOrbs.push(orb);
game.addChild(orb);
}
// Center orb
var centerOrb = new LightOrb();
centerOrb.x = starCenterX;
centerOrb.y = starCenterY;
lightOrbs.push(centerOrb);
game.addChild(centerOrb);
// Artifacts: 5, at the star points
artifactTotal = 5;
for (var i = 0; i < starPoints; i++) {
var angle = -Math.PI / 2 + i * (2 * Math.PI / starPoints);
var a = new Artifact();
a.x = starCenterX + Math.cos(angle) * (starRadius + 120);
a.y = starCenterY + Math.sin(angle) * (starRadius + 120);
artifacts.push(a);
game.addChild(a);
}
}
// Door: always at far right
door = new Door();
door.x = 2048 - 200;
door.y = 2732 / 2;
game.addChild(door);
// Fog overlay: covers the whole screen, alpha depends on light
fogOverlay = LK.getAsset('fog', {
anchorX: 1,
anchorY: 0,
x: 2048,
y: 0
});
fogOverlay.alpha = 0.7;
game.addChild(fogOverlay);
// Update GUI
artifactTxt.setText('Artifacts: 0/' + artifactTotal);
levelTxt.setText('Level ' + lvl + ' / ' + maxLevel);
// --- Save progress for persistence ---
if (typeof storage !== "undefined") {
storage.level = lvl;
storage.artifacts = artifactCount;
}
}
// --- Light effect ---
function updateFogAlpha() {
// Fog alpha is lower if hero has light
if (!fogOverlay) return;
var targetAlpha = hero.hasLight ? 0.2 : 0.7;
if (lightFadeTween) tween.stop(fogOverlay, {
alpha: true
});
lightFadeTween = tween(fogOverlay, {
alpha: targetAlpha
}, {
duration: 400,
easing: tween.easeInOut
});
}
// --- Touch controls ---
game.down = function (x, y, obj) {
// Only start drag if touch is inside hero
var local = {
x: x,
y: y
};
if (distance(local, {
x: hero.x,
y: hero.y
}) < hero.radius + 30) {
dragging = true;
dragOffset.x = hero.x - x;
dragOffset.y = hero.y - y;
lastTouch.x = x;
lastTouch.y = y;
}
};
game.move = function (x, y, obj) {
if (dragging) {
// Clamp hero position to screen
var nx = x + dragOffset.x;
var ny = y + dragOffset.y;
nx = Math.max(hero.radius, Math.min(2048 - hero.radius, nx));
ny = Math.max(hero.radius + 100, Math.min(2732 - hero.radius, ny));
hero.x = nx;
hero.y = ny;
lastTouch.x = x;
lastTouch.y = y;
}
};
game.up = function (x, y, obj) {
dragging = false;
};
// --- Main update loop ---
game.update = function () {
// Shadows update and collision
for (var i = 0; i < shadows.length; i++) {
var s = shadows[i];
if (typeof s.lastX === "undefined") s.lastX = s.x;
if (typeof s.lastY === "undefined") s.lastY = s.y;
if (typeof s.lastWasIntersecting === "undefined") s.lastWasIntersecting = false;
if (typeof s.update === "function") s.update();
// Collision with hero
var isIntersecting = distance(hero.getCenter(), s.getCenter()) < hero.radius + s.radius - 10;
if (!s.lastWasIntersecting && isIntersecting) {
// Decrease health, update bar, flash hero, game over if 0
heroHealth--;
updateHealthBar();
LK.effects.flashObject(hero, 0xFF2222, 600);
if (heroHealth <= 0) {
LK.effects.flashScreen(0x000000, 1000);
LK.showGameOver();
return;
}
}
s.lastX = s.x;
s.lastY = s.y;
s.lastWasIntersecting = isIntersecting;
}
// Light orb collection
for (var i = 0; i < lightOrbs.length; i++) {
var orb = lightOrbs[i];
if (!orb.collected && distance(hero.getCenter(), orb.getCenter()) < hero.radius + orb.radius + 10) {
orb.collected = true;
hero.hasLight = true;
hero.setLightRadius(500 + (level - 1) * 50);
updateFogAlpha();
// Earn 1 life (up to max)
if (heroHealth < heroMaxHealth) {
heroHealth++;
updateHealthBar();
}
// Animate orb fade out
tween(orb, {
alpha: 0
}, {
duration: 400,
onFinish: function onFinish() {
orb.destroy();
}
});
}
}
// Artifact collection
for (var i = 0; i < artifacts.length; i++) {
var a = artifacts[i];
if (!a.collected && distance(hero.getCenter(), a.getCenter()) < hero.radius + a.radius + 10) {
a.collected = true;
artifactCount++;
artifactTxt.setText('Artifacts: ' + artifactCount + '/' + artifactTotal);
tween(a, {
alpha: 0
}, {
duration: 400,
onFinish: function onFinish() {
a.destroy();
}
});
// --- Story-driven choice: prompt on artifact collection ---
if (level >= 2) {
// Only show for city/camp, not tutorial
LK.setTimeout(function () {
// Show a simple choice popup (simulate with LK.effects.flashScreen and Text2 for MVP)
var choiceTxt = new Text2("You found a mysterious artifact!\nDo you use it for power or save it for later?\n(Tap left: Power, Tap right: Save)", {
size: 70,
fill: 0xFFD700
});
choiceTxt.anchor.set(0.5, 0.5);
choiceTxt.x = 2048 / 2;
choiceTxt.y = 2732 / 2;
LK.gui.center.addChild(choiceTxt);
// Listen for tap (simulate with game.down)
var choiceHandler = function choiceHandler(x, y, obj) {
if (x < 2048 / 2) {
// Power: increase light radius
hero.setLightRadius(hero.lightRadius + 80);
LK.effects.flashObject(hero, 0xFFE066, 800);
} else {
// Save: bonus score (simulate with artifactCount++)
if (artifactCount < artifactTotal) {
artifactCount++;
artifactTxt.setText('Artifacts: ' + artifactCount + '/' + artifactTotal);
}
LK.effects.flashObject(artifactTxt, 0xFFD700, 800);
}
LK.gui.center.removeChild(choiceTxt);
game.down = origDown;
};
var origDown = game.down;
game.down = choiceHandler;
}, 500);
}
}
}
// Door: check if hero is at door and has light
var canExit = false;
if (level === 8 || level === 10 || level === 16 || level === 18) {
// On these levels, require all artifacts and all light orbs to be collected
var allArtifactsCollected = true;
for (var i = 0; i < artifacts.length; i++) {
if (!artifacts[i].collected) {
allArtifactsCollected = false;
break;
}
}
var allOrbsCollected = true;
for (var i = 0; i < lightOrbs.length; i++) {
if (!lightOrbs[i].collected) {
allOrbsCollected = false;
break;
}
}
if (hero.hasLight && allArtifactsCollected && allOrbsCollected && circleRectIntersect(hero.x, hero.y, hero.radius, door.x - door.width / 2, door.y - door.height / 2, door.width, door.height)) {
canExit = true;
}
} else {
if (hero.hasLight && circleRectIntersect(hero.x, hero.y, hero.radius, door.x - door.width / 2, door.y - door.height / 2, door.width, door.height)) {
canExit = true;
}
}
if (canExit) {
// Next level or win
if (level < maxLevel) {
level++;
setupLevel(level);
} else {
LK.showYouWin();
}
return;
}
// No shadows to collide with hero
// Fog overlay: create a "light hole" around hero if has light
if (fogOverlay) {
// For MVP, just set fog alpha lower if hero has light
// (Advanced: could use a mask, but not supported in MVP)
// Optionally, animate a "pulse" when light is collected
}
};
// --- Start game ---
// Start background music (looping by default)
LK.playMusic('bgmusic');
setupLevel(level); /****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1");
/****
* Classes
****/
// Artifact class: collectible for score
var Artifact = Container.expand(function () {
var self = Container.call(this);
var artifactSprite = self.attachAsset('artifact', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = artifactSprite.width / 2;
self.collected = false;
self.getCenter = function () {
return {
x: self.x,
y: self.y
};
};
return self;
});
// Door class: exit to next area
var Door = Container.expand(function () {
var self = Container.call(this);
var doorSprite = self.attachAsset('door', {
anchorX: 0.5,
anchorY: 0.5
});
self.width = doorSprite.width;
self.height = doorSprite.height;
self.getRect = function () {
return {
x: self.x - self.width / 2,
y: self.y - self.height / 2,
width: self.width,
height: self.height
};
};
return self;
});
// Hero class: player character
var Hero = Container.expand(function () {
var self = Container.call(this);
var heroSprite = self.attachAsset('hero', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = heroSprite.width / 2;
self.lightRadius = 300; // Initial light radius
self.hasLight = false; // If currently holding a light orb
// For dragging
self.isDragging = false;
// For upgrades
self.abilities = {
lightRadius: 300
};
// For collision detection
self.getCenter = function () {
return {
x: self.x,
y: self.y
};
};
// For light effect
self.setLightRadius = function (r) {
self.lightRadius = r;
};
return self;
});
// LightOrb class: collectible light source
var LightOrb = Container.expand(function () {
var self = Container.call(this);
var orbSprite = self.attachAsset('lightOrb', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = orbSprite.width / 2;
self.collected = false;
self.getCenter = function () {
return {
x: self.x,
y: self.y
};
};
return self;
});
// Shadow class: obstacles/enemies
var Shadow = Container.expand(function () {
var self = Container.call(this);
var shadowSprite = self.attachAsset('shadow', {
anchorX: 0.5,
anchorY: 0.5
});
self.radius = shadowSprite.width / 2;
self.speed = 2 + Math.random() * 2; // Random speed for movement
self.direction = Math.random() * Math.PI * 2; // Random direction
// For simple movement (patrol)
self.update = function () {
// If reactive (Survival Camp), chase or avoid hero based on light
if (self.isReactive && typeof hero !== "undefined") {
var dx = hero.x - self.x;
var dy = hero.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (hero.hasLight && dist < hero.lightRadius + 100) {
// Avoid hero if hero has light
var angle = Math.atan2(-dy, -dx);
self.direction = angle + (Math.random() - 0.5) * 0.5;
self.speed = 3.5 + Math.random() * 1.5;
} else if (dist < 600) {
// Chase hero if close and not lit
var angle = Math.atan2(dy, dx);
self.direction = angle + (Math.random() - 0.5) * 0.2;
self.speed = 3 + Math.random();
}
}
self.x += Math.cos(self.direction) * self.speed;
self.y += Math.sin(self.direction) * self.speed;
// Bounce off walls
if (self.x < self.radius || self.x > 2048 - self.radius) {
self.direction = Math.PI - self.direction;
}
if (self.y < self.radius + 100 || self.y > 2732 - self.radius) {
self.direction = -self.direction;
}
};
// For collision detection
self.getCenter = function () {
return {
x: self.x,
y: self.y
};
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0x111122
});
/****
* Game Code
****/
// --- Global variables ---
/*
We use simple shapes for the MVP:
- 'hero': the player character (circle, bright color)
- 'shadow': shadow obstacles (ellipse, dark color)
- 'lightOrb': collectible light source (circle, yellowish)
- 'door': exit to next area (rectangle, light blue)
- 'fog': semi-transparent overlay for darkness (rectangle, black, alpha)
- 'artifact': collectible (star-shaped, but use ellipse for MVP, gold)
*/
// Music asset (id: 'bgmusic', volume: 0.7)
var hero;
var shadows = [];
var lightOrbs = [];
var artifacts = [];
var door;
var fogOverlay;
var dragging = false;
var dragOffset = {
x: 0,
y: 0
};
var lastTouch = {
x: 0,
y: 0
};
var level = 1;
var maxLevel = 20;
var artifactCount = 0;
var artifactTotal = 0;
var lightFadeTween = null;
// --- GUI ---
var artifactTxt = new Text2('Artifacts: 0/0', {
size: 80,
fill: 0xFFE066
});
artifactTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(artifactTxt);
var levelTxt = new Text2('Level 1', {
size: 80,
fill: 0x66CCFF
});
levelTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(levelTxt);
// Health bar: show as "Health: [|||]" (3 max)
var heroMaxHealth = 3;
var heroHealth = heroMaxHealth;
var healthBarTxt = new Text2('Health: |||', {
size: 80,
fill: 0xFF6666
});
healthBarTxt.anchor.set(0.5, 0);
LK.gui.top.addChild(healthBarTxt);
// Position GUI elements
artifactTxt.y = 100;
levelTxt.y = 10;
healthBarTxt.y = 190;
// Helper to update health bar display
function updateHealthBar() {
var bars = '';
for (var i = 0; i < heroHealth; i++) bars += '|';
for (var i = heroHealth; i < heroMaxHealth; i++) bars += ' ';
healthBarTxt.setText('Health: ' + bars);
}
updateHealthBar();
// --- Helper functions ---
function distance(a, b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
function circleRectIntersect(cx, cy, cr, rx, ry, rw, rh) {
// Find the closest point to the circle within the rectangle
var closestX = Math.max(rx, Math.min(cx, rx + rw));
var closestY = Math.max(ry, Math.min(cy, ry + rh));
// Calculate the distance between the circle's center and this closest point
var dx = cx - closestX;
var dy = cy - closestY;
// If the distance is less than the circle's radius, an intersection occurs
return dx * dx + dy * dy < cr * cr;
}
// --- Level setup ---
function setupLevel(lvl) {
// Clear previous
for (var i = 0; i < shadows.length; i++) {
shadows[i].destroy();
}
for (var i = 0; i < lightOrbs.length; i++) {
lightOrbs[i].destroy();
}
for (var i = 0; i < artifacts.length; i++) {
artifacts[i].destroy();
}
if (door) door.destroy();
if (fogOverlay) fogOverlay.destroy();
shadows = [];
lightOrbs = [];
artifacts = [];
door = null;
fogOverlay = null;
artifactCount = 0;
// Hero position
if (!hero) {
hero = new Hero();
game.addChild(hero);
}
hero.x = 400;
hero.y = 2732 / 2;
hero.setLightRadius(300 + (lvl - 1) * 40);
hero.hasLight = false;
// Reset health for new level
heroHealth = heroMaxHealth;
updateHealthBar();
// --- Location logic ---
// Ruined Laboratory (level 1): default
// Ruined City (level 2-4): more shadows, more artifacts, shadows move faster
// Survival Camp (level 5+): shadows react to hero, more light orbs, artifacts clustered
if (lvl === 1) {
// Ruined Laboratory: default
// Shadows: none for level 1
// Light orb: always one, placed randomly
var orb = new LightOrb();
orb.x = 400 + Math.random() * (2048 - 800);
orb.y = 300 + Math.random() * (2732 - 600);
lightOrbs.push(orb);
game.addChild(orb);
// Artifacts: 1-2
artifactTotal = 1 + Math.floor(Math.random() * 2);
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
var tries = 0;
do {
a.x = 400 + Math.random() * (2048 - 800);
a.y = 300 + Math.random() * (2732 - 600);
var ok = true;
if (distance({
x: a.x,
y: a.y
}, {
x: hero.x,
y: hero.y
}) < 300) ok = false;
for (var j = 0; j < artifacts.length; j++) {
if (distance({
x: a.x,
y: a.y
}, {
x: artifacts[j].x,
y: artifacts[j].y
}) < 200) ok = false;
}
tries++;
if (tries > 10) break;
} while (!ok);
artifacts.push(a);
game.addChild(a);
}
} else if (lvl >= 2 && lvl <= 4) {
// Ruined City: more/faster shadows, more artifacts
// Shadows: 2-4, move faster
var shadowCount = 2 + Math.floor(Math.random() * 3);
for (var i = 0; i < shadowCount; i++) {
var s = new Shadow();
s.x = 800 + Math.random() * (2048 - 1200);
s.y = 400 + Math.random() * (2732 - 800);
s.speed = 3 + Math.random() * 2;
shadows.push(s);
game.addChild(s);
}
// Light orb: always one, placed randomly
var orb = new LightOrb();
orb.x = 400 + Math.random() * (2048 - 800);
orb.y = 300 + Math.random() * (2732 - 600);
lightOrbs.push(orb);
game.addChild(orb);
// Artifacts: 2-4
artifactTotal = 2 + Math.floor(Math.random() * 3);
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
var tries = 0;
do {
a.x = 400 + Math.random() * (2048 - 800);
a.y = 300 + Math.random() * (2732 - 600);
var ok = true;
if (distance({
x: a.x,
y: a.y
}, {
x: hero.x,
y: hero.y
}) < 300) ok = false;
for (var j = 0; j < artifacts.length; j++) {
if (distance({
x: a.x,
y: a.y
}, {
x: artifacts[j].x,
y: artifacts[j].y
}) < 200) ok = false;
}
tries++;
if (tries > 10) break;
} while (!ok);
artifacts.push(a);
game.addChild(a);
}
} else if (lvl >= 5 && lvl <= 7) {
// Survival Camp (lvl 5-7): shadows react to hero, more light orbs, artifacts clustered
// Shadows: 3-5, reactive to hero
var shadowCount = 3 + Math.floor(Math.random() * 3);
for (var i = 0; i < shadowCount; i++) {
var s = new Shadow();
s.x = 1000 + Math.random() * 800;
s.y = 800 + Math.random() * 1200;
s.isReactive = true;
shadows.push(s);
game.addChild(s);
}
// Two light orbs, placed near each other
for (var i = 0; i < 2; i++) {
var orb = new LightOrb();
orb.x = 1200 + Math.random() * 400;
orb.y = 1000 + Math.random() * 400;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 3-5, clustered
artifactTotal = 3 + Math.floor(Math.random() * 3);
var clusterX = 1500 + Math.random() * 300;
var clusterY = 1500 + Math.random() * 300;
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = clusterX + Math.random() * 120 - 60;
a.y = clusterY + Math.random() * 120 - 60;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 8) {
// Level 8: "Collapsed Tunnels" - narrow corridor, many shadows, few orbs
// Shadows: 6, placed along the tunnel
for (var i = 0; i < 6; i++) {
var s = new Shadow();
s.x = 800 + i * 120;
s.y = 600 + Math.random() * 1500;
shadows.push(s);
game.addChild(s);
}
// Only one light orb, placed at far end
var orb = new LightOrb();
orb.x = 1500;
orb.y = 2732 / 2;
lightOrbs.push(orb);
game.addChild(orb);
// Artifacts: 2, placed at dangerous spots
artifactTotal = 2;
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = 1200 + i * 300;
a.y = 800 + Math.random() * 1200;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 9) {
// Level 9: "Watcher’s Lair" - fewer, but very fast shadows, more orbs, artifacts near center
// Shadows: 3, very fast
for (var i = 0; i < 3; i++) {
var s = new Shadow();
s.x = 900 + i * 200;
s.y = 1000 + Math.random() * 800;
s.speed = 5 + Math.random() * 2;
shadows.push(s);
game.addChild(s);
}
// Three light orbs, spread out
for (var i = 0; i < 3; i++) {
var orb = new LightOrb();
orb.x = 700 + i * 400;
orb.y = 700 + Math.random() * 1500;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 4, near center
artifactTotal = 4;
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = 1000 + Math.random() * 400;
a.y = 1200 + Math.random() * 400;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 10) {
// Level 10: "Shadow Nexus" - final, many reactive shadows, clustered orbs/artifacts
// Shadows: 6, all reactive, but start in more mixed spots
var shadowPositions = [{
x: 1550 + Math.random() * 200,
y: 1200 + Math.random() * 200
}, {
x: 1700 + Math.random() * 200,
y: 1500 + Math.random() * 200
}, {
x: 1200 + Math.random() * 300,
y: 1100 + Math.random() * 600
}, {
x: 1400 + Math.random() * 400,
y: 1700 + Math.random() * 300
}, {
x: 1800 + Math.random() * 100,
y: 1300 + Math.random() * 400
}, {
x: 1300 + Math.random() * 400,
y: 1200 + Math.random() * 400
}];
for (var i = 0; i < 6; i++) {
var s = new Shadow();
s.x = shadowPositions[i].x;
s.y = shadowPositions[i].y;
s.isReactive = true;
shadows.push(s);
game.addChild(s);
}
// Four light orbs, clustered
for (var i = 0; i < 4; i++) {
var orb = new LightOrb();
orb.x = 1600 + Math.random() * 200;
orb.y = 1200 + Math.random() * 400;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 5, clustered
artifactTotal = 5;
var clusterX = 1700;
var clusterY = 1400;
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = clusterX + Math.random() * 100 - 50;
a.y = clusterY + Math.random() * 100 - 50;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 11) {
// Level 11: "The Forgotten Vault" - maze-like, slow but smart shadows, orbs at dead ends
// Shadows: 4, slow, placed at maze entrances
for (var i = 0; i < 4; i++) {
var s = new Shadow();
s.x = 600 + i * 350;
s.y = 400 + Math.random() * (2732 - 800);
s.speed = 1.5 + Math.random();
s.isReactive = true;
shadows.push(s);
game.addChild(s);
}
// Two light orbs, at far left and right
for (var i = 0; i < 2; i++) {
var orb = new LightOrb();
orb.x = i === 0 ? 400 : 2048 - 400;
orb.y = 400 + Math.random() * (2732 - 800);
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 3, spaced out
artifactTotal = 3;
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = 700 + i * 400;
a.y = 800 + Math.random() * 1200;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 12) {
// Level 12: "Flooded Passage" - orbs clustered bottom right, artifacts top left and top right
// Shadows: 3, slow, near bottom
for (var i = 0; i < 3; i++) {
var s = new Shadow();
s.x = 1200 + i * 200;
s.y = 2200 + Math.random() * 200;
s.speed = 1.5 + Math.random();
shadows.push(s);
game.addChild(s);
}
// Three light orbs, clustered near bottom right
for (var i = 0; i < 3; i++) {
var orb = new LightOrb();
orb.x = 1500 + Math.random() * 300;
orb.y = 2200 + Math.random() * 300;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 2, one at top left, one at top right
artifactTotal = 2;
var artifactPositions = [{
x: 350,
y: 350 + Math.random() * 150
}, {
x: 2048 - 350,
y: 350 + Math.random() * 150
}];
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = artifactPositions[i].x;
a.y = artifactPositions[i].y;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 13) {
// Level 13: "The Mirror Hall" - shadows mirror hero's movement, orbs/artifacts in corners
// Shadows: 2, mirror hero's movement (simulate by following hero with offset)
for (var i = 0; i < 2; i++) {
var s = new Shadow();
s.x = i === 0 ? 200 : 1848;
s.y = i === 0 ? 2532 : 200;
s.isReactive = true;
shadows.push(s);
game.addChild(s);
}
// Four light orbs, one in each corner
var corners = [{
x: 200,
y: 200
}, {
x: 1848,
y: 200
}, {
x: 200,
y: 2532
}, {
x: 1848,
y: 2532
}];
for (var i = 0; i < 4; i++) {
var orb = new LightOrb();
orb.x = corners[i].x;
orb.y = corners[i].y;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 4, one in each corner (offset from orbs)
artifactTotal = 4;
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = corners[i].x + 60;
a.y = corners[i].y + 60;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 14) {
// Level 14: "The Gauntlet" - many fast, non-reactive shadows, orbs in a vertical line, artifacts in zigzag spanning the play area
// Shadows: 7, fast, non-reactive
for (var i = 0; i < 7; i++) {
var s = new Shadow();
s.x = 400 + i * 220;
s.y = 400 + i % 2 * 1800;
s.speed = 4 + Math.random() * 2;
shadows.push(s);
game.addChild(s);
}
// Five light orbs, in a vertical line
for (var i = 0; i < 5; i++) {
var orb = new LightOrb();
orb.x = 1024;
orb.y = 400 + i * 500;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 5, zigzag pattern spanning the play area
artifactTotal = 5;
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = 500 + i * 300;
a.y = 600 + (i % 2 === 0 ? 600 : 1800);
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 15) {
// Level 15: "The Final Eclipse" - all shadows reactive, orbs/artifacts at center, max difficulty
// Shadows: 8, all reactive, clustered at center
for (var i = 0; i < 8; i++) {
var s = new Shadow();
s.x = 1024 + Math.cos(i / 8 * Math.PI * 2) * 200;
s.y = 1366 + Math.sin(i / 8 * Math.PI * 2) * 200;
s.isReactive = true;
shadows.push(s);
game.addChild(s);
}
// Six light orbs, clustered at center
for (var i = 0; i < 6; i++) {
var orb = new LightOrb();
orb.x = 1024 + Math.random() * 120 - 60;
orb.y = 1366 + Math.random() * 120 - 60;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 6, clustered at center
artifactTotal = 6;
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = 1024 + Math.random() * 100 - 50;
a.y = 1366 + Math.random() * 100 - 50;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 16) {
// Level 16: "Crystal Crossing" - orbs in a cross, artifacts at each arm tip
// Five light orbs, one at center, four at cross arms
var cross = [{
x: 1024,
y: 1366
},
// center
{
x: 1024,
y: 566
},
// top
{
x: 1024,
y: 2166
},
// bottom
{
x: 324,
y: 1366
},
// left
{
x: 1724,
y: 1366
} // right
];
// Shadows: 5, at cross arms
for (var i = 0; i < 5; i++) {
var s = new Shadow();
s.x = cross[i].x;
s.y = cross[i].y;
shadows.push(s);
game.addChild(s);
}
for (var i = 0; i < cross.length; i++) {
var orb = new LightOrb();
orb.x = cross[i].x;
orb.y = cross[i].y;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 4, at the tips of the cross (not center)
artifactTotal = 4;
var tips = [{
x: 1024,
y: 566
},
// top
{
x: 1024,
y: 2166
},
// bottom
{
x: 324,
y: 1366
},
// left
{
x: 1724,
y: 1366
} // right
];
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = tips[i].x;
a.y = tips[i].y;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 17) {
// Level 17: "Spiral of Light" - orbs in a spiral, artifacts at spiral endpoints
// Shadows: 8, distributed along the spiral (denser, more challenging)
var spiralCenterX = 1024;
var spiralCenterY = 1366;
var spiralRadiusStart = 200;
var spiralRadiusEnd = 800;
var spiralTurns = 2.25; // more turns for a clear spiral
var spiralOrbs = 8;
var shadowCount = 8;
for (var i = 0; i < shadowCount; i++) {
var t = i / (shadowCount - 1);
var angle = t * spiralTurns * Math.PI * 2;
var radius = spiralRadiusStart + (spiralRadiusEnd - spiralRadiusStart) * t;
var s = new Shadow();
s.x = spiralCenterX + Math.cos(angle) * radius;
s.y = spiralCenterY + Math.sin(angle) * radius;
shadows.push(s);
game.addChild(s);
}
// Add an extra shadow at the spiral endpoint (for extra challenge)
var endpointAngle = spiralTurns * Math.PI * 2;
var endpointX = spiralCenterX + Math.cos(endpointAngle) * spiralRadiusEnd;
var endpointY = spiralCenterY + Math.sin(endpointAngle) * spiralRadiusEnd;
var sEndpoint = new Shadow();
sEndpoint.x = endpointX;
sEndpoint.y = endpointY;
shadows.push(sEndpoint);
game.addChild(sEndpoint);
// Place 8 light orbs in a spiral pattern (tighter spiral, more visually clear)
for (var i = 0; i < spiralOrbs; i++) {
var t = i / (spiralOrbs - 1);
var angle = t * spiralTurns * Math.PI * 2;
var radius = spiralRadiusStart + (spiralRadiusEnd - spiralRadiusStart) * t;
var orb = new LightOrb();
orb.x = spiralCenterX + Math.cos(angle) * radius;
orb.y = spiralCenterY + Math.sin(angle) * radius;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 2, at the start and end of the spiral
artifactTotal = 2;
var artifactPositions = [{
x: spiralCenterX + Math.cos(0) * spiralRadiusStart,
y: spiralCenterY + Math.sin(0) * spiralRadiusStart
}, {
x: spiralCenterX + Math.cos(spiralTurns * Math.PI * 2) * spiralRadiusEnd,
y: spiralCenterY + Math.sin(spiralTurns * Math.PI * 2) * spiralRadiusEnd
}];
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = artifactPositions[i].x;
a.y = artifactPositions[i].y;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 18) {
// Level 18: "Circle of Light" - orbs in a ring, artifacts at cardinal points
var ringCenterX = 1024;
var ringCenterY = 1366;
var ringRadius = 900; // Increased from 700 to 900 to move orbs further from the door
// Shadows: 4, at cardinal points of the ring
for (var i = 0; i < 4; i++) {
var angle = i / 4 * Math.PI * 2;
var s = new Shadow();
s.x = ringCenterX + Math.cos(angle) * (ringRadius - 120);
s.y = ringCenterY + Math.sin(angle) * (ringRadius - 120);
shadows.push(s);
game.addChild(s);
}
// Add a shadow at the center of the ring
var sCenter = new Shadow();
sCenter.x = ringCenterX;
sCenter.y = ringCenterY;
shadows.push(sCenter);
game.addChild(sCenter);
// Place 8 light orbs in a circle
var ringOrbs = 8;
for (var i = 0; i < ringOrbs; i++) {
var angle = i / ringOrbs * Math.PI * 2;
var orb = new LightOrb();
orb.x = ringCenterX + Math.cos(angle) * ringRadius;
orb.y = ringCenterY + Math.sin(angle) * ringRadius;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 4, at N/E/S/W points of the ring
artifactTotal = 4;
var artifactPositions = [{
x: ringCenterX,
y: ringCenterY - ringRadius
}, {
x: ringCenterX + ringRadius,
y: ringCenterY
}, {
x: ringCenterX,
y: ringCenterY + ringRadius
}, {
x: ringCenterX - ringRadius,
y: ringCenterY
} // West
];
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = artifactPositions[i].x;
a.y = artifactPositions[i].y;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 19) {
// Level 19: "Zigzag Triangle" - orbs in a zigzag, artifacts in a triangle
// Artifacts and Shadows: 3, at triangle points
var triangle = [{
x: 1024,
y: 500
}, {
x: 400,
y: 2200
}, {
x: 1648,
y: 2200
}];
// Shadows: 3, at triangle points
for (var i = 0; i < 3; i++) {
var s = new Shadow();
s.x = triangle[i].x;
s.y = triangle[i].y;
shadows.push(s);
game.addChild(s);
}
// Add a shadow at the centroid of the triangle
var centroidX = (triangle[0].x + triangle[1].x + triangle[2].x) / 3;
var centroidY = (triangle[0].y + triangle[1].y + triangle[2].y) / 3;
var sCentroid = new Shadow();
sCentroid.x = centroidX;
sCentroid.y = centroidY;
shadows.push(sCentroid);
game.addChild(sCentroid);
// Place 6 light orbs in a zigzag pattern
var zigzagOrbs = 6;
for (var i = 0; i < zigzagOrbs; i++) {
var orb = new LightOrb();
orb.x = 400 + i * 280;
orb.y = i % 2 === 0 ? 700 : 2000;
lightOrbs.push(orb);
game.addChild(orb);
}
// Artifacts: 3, at triangle points
artifactTotal = 3;
for (var i = 0; i < artifactTotal; i++) {
var a = new Artifact();
a.x = triangle[i].x;
a.y = triangle[i].y;
artifacts.push(a);
game.addChild(a);
}
} else if (lvl === 20) {
// Level 20: "Starfall Sanctuary" - orbs in a star, artifacts at star points
// Shadows: way too many, dense starburst pattern
var starCenterX = 1024;
var starCenterY = 1366;
var starRadius = 700;
var starPoints = 5;
// Add a dense burst of shadows radiating from the center
var shadowRings = 6; // number of concentric rings
var shadowsPerRing = 18; // number of shadows per ring
for (var ring = 1; ring <= shadowRings; ring++) {
var r = (starRadius - 200) * (ring / shadowRings) + 120;
for (var j = 0; j < shadowsPerRing; j++) {
var angle = j / shadowsPerRing * Math.PI * 2 + ring % 2 * (Math.PI / shadowsPerRing);
var s = new Shadow();
s.x = starCenterX + Math.cos(angle) * r;
s.y = starCenterY + Math.sin(angle) * r;
// Make some move faster for extra chaos
if (ring % 2 === 0) s.speed += 1.5;
shadows.push(s);
game.addChild(s);
}
}
// Add extra shadows at the star points (for visual emphasis)
for (var i = 0; i < starPoints; i++) {
var angle = -Math.PI / 2 + i * (2 * Math.PI / starPoints);
var s = new Shadow();
s.x = starCenterX + Math.cos(angle) * (starRadius - 100);
s.y = starCenterY + Math.sin(angle) * (starRadius - 100);
s.speed += 2;
shadows.push(s);
game.addChild(s);
}
// Place 5 light orbs in a star pattern (center + 4 points)
for (var i = 0; i < starPoints; i++) {
var angle = -Math.PI / 2 + i * (2 * Math.PI / starPoints);
var orb = new LightOrb();
orb.x = starCenterX + Math.cos(angle) * starRadius;
orb.y = starCenterY + Math.sin(angle) * starRadius;
lightOrbs.push(orb);
game.addChild(orb);
}
// Center orb
var centerOrb = new LightOrb();
centerOrb.x = starCenterX;
centerOrb.y = starCenterY;
lightOrbs.push(centerOrb);
game.addChild(centerOrb);
// Artifacts: 5, at the star points
artifactTotal = 5;
for (var i = 0; i < starPoints; i++) {
var angle = -Math.PI / 2 + i * (2 * Math.PI / starPoints);
var a = new Artifact();
a.x = starCenterX + Math.cos(angle) * (starRadius + 120);
a.y = starCenterY + Math.sin(angle) * (starRadius + 120);
artifacts.push(a);
game.addChild(a);
}
}
// Door: always at far right
door = new Door();
door.x = 2048 - 200;
door.y = 2732 / 2;
game.addChild(door);
// Fog overlay: covers the whole screen, alpha depends on light
fogOverlay = LK.getAsset('fog', {
anchorX: 1,
anchorY: 0,
x: 2048,
y: 0
});
fogOverlay.alpha = 0.7;
game.addChild(fogOverlay);
// Update GUI
artifactTxt.setText('Artifacts: 0/' + artifactTotal);
levelTxt.setText('Level ' + lvl + ' / ' + maxLevel);
// --- Save progress for persistence ---
if (typeof storage !== "undefined") {
storage.level = lvl;
storage.artifacts = artifactCount;
}
}
// --- Light effect ---
function updateFogAlpha() {
// Fog alpha is lower if hero has light
if (!fogOverlay) return;
var targetAlpha = hero.hasLight ? 0.2 : 0.7;
if (lightFadeTween) tween.stop(fogOverlay, {
alpha: true
});
lightFadeTween = tween(fogOverlay, {
alpha: targetAlpha
}, {
duration: 400,
easing: tween.easeInOut
});
}
// --- Touch controls ---
game.down = function (x, y, obj) {
// Only start drag if touch is inside hero
var local = {
x: x,
y: y
};
if (distance(local, {
x: hero.x,
y: hero.y
}) < hero.radius + 30) {
dragging = true;
dragOffset.x = hero.x - x;
dragOffset.y = hero.y - y;
lastTouch.x = x;
lastTouch.y = y;
}
};
game.move = function (x, y, obj) {
if (dragging) {
// Clamp hero position to screen
var nx = x + dragOffset.x;
var ny = y + dragOffset.y;
nx = Math.max(hero.radius, Math.min(2048 - hero.radius, nx));
ny = Math.max(hero.radius + 100, Math.min(2732 - hero.radius, ny));
hero.x = nx;
hero.y = ny;
lastTouch.x = x;
lastTouch.y = y;
}
};
game.up = function (x, y, obj) {
dragging = false;
};
// --- Main update loop ---
game.update = function () {
// Shadows update and collision
for (var i = 0; i < shadows.length; i++) {
var s = shadows[i];
if (typeof s.lastX === "undefined") s.lastX = s.x;
if (typeof s.lastY === "undefined") s.lastY = s.y;
if (typeof s.lastWasIntersecting === "undefined") s.lastWasIntersecting = false;
if (typeof s.update === "function") s.update();
// Collision with hero
var isIntersecting = distance(hero.getCenter(), s.getCenter()) < hero.radius + s.radius - 10;
if (!s.lastWasIntersecting && isIntersecting) {
// Decrease health, update bar, flash hero, game over if 0
heroHealth--;
updateHealthBar();
LK.effects.flashObject(hero, 0xFF2222, 600);
if (heroHealth <= 0) {
LK.effects.flashScreen(0x000000, 1000);
LK.showGameOver();
return;
}
}
s.lastX = s.x;
s.lastY = s.y;
s.lastWasIntersecting = isIntersecting;
}
// Light orb collection
for (var i = 0; i < lightOrbs.length; i++) {
var orb = lightOrbs[i];
if (!orb.collected && distance(hero.getCenter(), orb.getCenter()) < hero.radius + orb.radius + 10) {
orb.collected = true;
hero.hasLight = true;
hero.setLightRadius(500 + (level - 1) * 50);
updateFogAlpha();
// Earn 1 life (up to max)
if (heroHealth < heroMaxHealth) {
heroHealth++;
updateHealthBar();
}
// Animate orb fade out
tween(orb, {
alpha: 0
}, {
duration: 400,
onFinish: function onFinish() {
orb.destroy();
}
});
}
}
// Artifact collection
for (var i = 0; i < artifacts.length; i++) {
var a = artifacts[i];
if (!a.collected && distance(hero.getCenter(), a.getCenter()) < hero.radius + a.radius + 10) {
a.collected = true;
artifactCount++;
artifactTxt.setText('Artifacts: ' + artifactCount + '/' + artifactTotal);
tween(a, {
alpha: 0
}, {
duration: 400,
onFinish: function onFinish() {
a.destroy();
}
});
// --- Story-driven choice: prompt on artifact collection ---
if (level >= 2) {
// Only show for city/camp, not tutorial
LK.setTimeout(function () {
// Show a simple choice popup (simulate with LK.effects.flashScreen and Text2 for MVP)
var choiceTxt = new Text2("You found a mysterious artifact!\nDo you use it for power or save it for later?\n(Tap left: Power, Tap right: Save)", {
size: 70,
fill: 0xFFD700
});
choiceTxt.anchor.set(0.5, 0.5);
choiceTxt.x = 2048 / 2;
choiceTxt.y = 2732 / 2;
LK.gui.center.addChild(choiceTxt);
// Listen for tap (simulate with game.down)
var choiceHandler = function choiceHandler(x, y, obj) {
if (x < 2048 / 2) {
// Power: increase light radius
hero.setLightRadius(hero.lightRadius + 80);
LK.effects.flashObject(hero, 0xFFE066, 800);
} else {
// Save: bonus score (simulate with artifactCount++)
if (artifactCount < artifactTotal) {
artifactCount++;
artifactTxt.setText('Artifacts: ' + artifactCount + '/' + artifactTotal);
}
LK.effects.flashObject(artifactTxt, 0xFFD700, 800);
}
LK.gui.center.removeChild(choiceTxt);
game.down = origDown;
};
var origDown = game.down;
game.down = choiceHandler;
}, 500);
}
}
}
// Door: check if hero is at door and has light
var canExit = false;
if (level === 8 || level === 10 || level === 16 || level === 18) {
// On these levels, require all artifacts and all light orbs to be collected
var allArtifactsCollected = true;
for (var i = 0; i < artifacts.length; i++) {
if (!artifacts[i].collected) {
allArtifactsCollected = false;
break;
}
}
var allOrbsCollected = true;
for (var i = 0; i < lightOrbs.length; i++) {
if (!lightOrbs[i].collected) {
allOrbsCollected = false;
break;
}
}
if (hero.hasLight && allArtifactsCollected && allOrbsCollected && circleRectIntersect(hero.x, hero.y, hero.radius, door.x - door.width / 2, door.y - door.height / 2, door.width, door.height)) {
canExit = true;
}
} else {
if (hero.hasLight && circleRectIntersect(hero.x, hero.y, hero.radius, door.x - door.width / 2, door.y - door.height / 2, door.width, door.height)) {
canExit = true;
}
}
if (canExit) {
// Next level or win
if (level < maxLevel) {
level++;
setupLevel(level);
} else {
LK.showYouWin();
}
return;
}
// No shadows to collide with hero
// Fog overlay: create a "light hole" around hero if has light
if (fogOverlay) {
// For MVP, just set fog alpha lower if hero has light
// (Advanced: could use a mask, but not supported in MVP)
// Optionally, animate a "pulse" when light is collected
}
};
// --- Start game ---
// Start background music (looping by default)
LK.playMusic('bgmusic');
setupLevel(level);
food. In-Game asset. 2d. High contrast. No shadows
Shadow creature. In-Game asset. 2d. High contrast. No shadows
first aid kit. In-Game asset. 2d. High contrast. No shadows
✅ Hair: Medium-length, slightly unkempt dark hair—indicating survival hardships. ✅ Height: Around 1.75m (5'9"), agile but not overly strong. ✅ Build: Lean, slightly worn-out clothing, showing signs of past struggles. ✅ Eyes: Piercing green or amber eyes, reflecting a determined personality. ✅ Accessories: A damaged jacket with scratches, adding realism to the post-apocalyptic theme. ✅ Hand Item: A broken flashlight – essential for revealing enemies but risky in the dark! Arin’s design should reflect their survival skills—not looking overly combat-ready but practical for the harsh environment. The flashlight mechanic will play a big role in interacting with the game’s shadow creatures.. In-Game asset. 2d. High contrast. No shadows
The door. In-Game asset. 2d. High contrast. No shadows