User prompt
make the enemies npc that are independed from the other objects
User prompt
add a war background
User prompt
also add the future to win the first level after beating 10 enemies. and after going to the next level, make the goal is to beat 20 enemies
User prompt
make it so that the game takes long and it never ends unless you beat all the enemies that are trying to attack you. give our character a gun that can shoot when we click ↪💡 Consider importing and using the following plugins: @upit/tween.v1
Code edit (1 edits merged)
Please save this source code
User prompt
Dreams of the Forgotten Town
Initial prompt
Make me a game where the player wakes up in a quiet, abandoned town with no memory of who they are. The world should feel eerie but beautiful, like a dream that's slowly turning into a nightmare. The main character doesn't speak, but the people they meet seem to know them — and they’re hiding something. Let the gameplay focus on exploration, emotional storytelling, and solving symbolic puzzles tied to the protagonist's past. Every choice the player makes, even small ones, should quietly shape the story and how it ends. Use a hand-drawn, slightly surreal art style that feels like flipping through someone’s old sketchbook. There should be no combat — tension and conflict come from atmosphere, music, and the weight of decisions. Make the soundtrack dynamic, so it responds to where the player is and what they’re feeling. Themes like memory, identity, and regret should be at the core of everything. Think of it as a haunting mix of Omori, Inside, and a forgotten fairy tale.
/****
* Plugins
****/
var tween = LK.import("@upit/tween.v1");
var storage = LK.import("@upit/storage.v1", {
ending: 0,
foundFragments: 0
});
/****
* Classes
****/
// --- Bullet Class ---
var Bullet = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('highlight', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.7
});
self.speed = 38;
self.dirX = 0;
self.dirY = 0;
self.lastX = self.x;
self.lastY = self.y;
self.update = function () {
self.lastX = self.x;
self.lastY = self.y;
self.x += self.dirX * self.speed;
self.y += self.dirY * self.speed;
};
return self;
});
// --- Global arrays for enemies and bullets ---
// Door class (scene transition)
var Door = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('door', {
anchorX: 0.5,
anchorY: 0.5
});
self.targetScene = "";
self.highlight = null;
self.showHighlight = function () {
if (!self.highlight) {
self.highlight = self.attachAsset('highlight', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.10
});
}
self.highlight.visible = true;
};
self.hideHighlight = function () {
if (self.highlight) self.highlight.visible = false;
};
return self;
});
// --- Enemy NPC Class (independent from other objects) ---
var Enemy = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('npc', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x8b1a1a
});
self.speed = 6 + Math.random() * 4;
self.lastX = self.x;
self.lastY = self.y;
// Each enemy is an independent NPC, not derived from other objects
self.update = function () {
self.lastX = self.x;
self.lastY = self.y;
// Move toward the player independently
var dx = player.x - self.x;
var dy = player.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
self.x += self.speed * dx / dist;
self.y += self.speed * dy / dist;
}
};
return self;
});
// Memory Fragment class
var MemoryFragment = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('memoryFragment', {
anchorX: 0.5,
anchorY: 0.5
});
self.collected = false;
self.highlight = null;
self.showHighlight = function () {
if (!self.highlight) {
self.highlight = self.attachAsset('highlight', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.13
});
}
self.highlight.visible = true;
};
self.hideHighlight = function () {
if (self.highlight) self.highlight.visible = false;
};
return self;
});
// NPC class
var NPC = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('npc', {
anchorX: 0.5,
anchorY: 0.5
});
self.dialogue = [];
self.dialogueIndex = 0;
self.name = "";
self.interacted = false;
self.highlight = null;
// Show highlight when player is near
self.showHighlight = function () {
if (!self.highlight) {
self.highlight = self.attachAsset('highlight', {
anchorX: 0.5,
anchorY: 0.5,
alpha: 0.18
});
}
self.highlight.visible = true;
};
self.hideHighlight = function () {
if (self.highlight) self.highlight.visible = false;
};
return self;
});
// Player class
var Player = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('player', {
anchorX: 0.5,
anchorY: 0.5
});
self.speed = 18;
self.targetX = self.x;
self.targetY = self.y;
self.moving = false;
// Move towards target
self.update = function () {
if (!self.moving) return;
var dx = self.targetX - self.x;
var dy = self.targetY - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < self.speed) {
self.x = self.targetX;
self.y = self.targetY;
self.moving = false;
} else {
self.x += self.speed * dx / dist;
self.y += self.speed * dy / dist;
}
};
return self;
});
/****
* Initialize Game
****/
var game = new LK.Game({
backgroundColor: 0xfaf7f2
});
/****
* Game Code
****/
// --- Global arrays for enemies and bullets ---
// --- State ---
/*
We use simple shapes for the sketchbook-like look.
- 'player': the protagonist, a hand-drawn ellipse.
- 'npc': townsfolk, a box with a different color.
- 'memoryFragment': symbolic puzzle piece, a star-shaped box.
- 'door': a rectangle for scene transitions.
- 'bgTown': a large, pale rectangle as the background.
- 'highlight': a transparent overlay for interactable objects.
*/
var enemies = [];
var bullets = [];
var player;
var npcs = [];
var memoryFragments = [];
var doors = [];
var currentScene = "town";
var dialogueBox = null;
var dialogueText = null;
var choiceButtons = [];
var infoText = null;
var canMove = true;
var lastNearObject = null;
var foundFragments = storage.foundFragments || 0;
var totalFragments = 3;
var ending = storage.ending || 0;
// --- Level Progression ---
var currentLevel = 1;
var enemiesDefeated = 0;
var enemiesToDefeat = 10; // Level 1 goal
function nextLevel() {
currentLevel++;
if (currentLevel === 2) {
enemiesToDefeat = 20;
enemiesDefeated = 0;
showInfo("Level 2: Defeat 20 enemies!", 3000);
// Optionally, respawn some memory fragments or change scene visuals here
} else {
// If more levels, add here
// For now, after level 2, allow ending as before
}
}
// --- GUI ---
function showInfo(msg, duration) {
if (!infoText) {
infoText = new Text2(msg, {
size: 80,
fill: 0x444444
});
infoText.anchor.set(0.5, 0);
LK.gui.top.addChild(infoText);
}
infoText.setText(msg);
infoText.visible = true;
if (duration) {
LK.setTimeout(function () {
if (infoText) infoText.visible = false;
}, duration);
}
}
// --- Scene Setup ---
function setupTownScene() {
// Clear previous
game.removeChildren();
npcs = [];
memoryFragments = [];
doors = [];
lastNearObject = null;
// --- Reset level state ---
enemies = [];
bullets = [];
if (currentLevel === 1) {
enemiesToDefeat = 10;
enemiesDefeated = 0;
} else if (currentLevel === 2) {
enemiesToDefeat = 20;
enemiesDefeated = 0;
}
// War-torn background
var warBg = LK.getAsset('background', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0
});
game.addChild(warBg);
// Overlay town sketchbook background for style
var bg = LK.getAsset('bgTown', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
alpha: 0.55 // Let the war background show through
});
game.addChild(bg);
// Player
player = new Player();
player.x = 1024;
player.y = 2000;
game.addChild(player);
// NPCs
var npc1 = new NPC();
npc1.x = 700;
npc1.y = 1500;
npc1.name = "Caretaker";
npc1.dialogue = ["You look lost. The town remembers you, even if you don't remember it.", "Find the fragments of your memory. They are hidden in plain sight.", "Come back when you remember more."];
npcs.push(npc1);
game.addChild(npc1);
var npc2 = new NPC();
npc2.x = 1400;
npc2.y = 1100;
npc2.name = "Child";
npc2.dialogue = ["Will you play with me? Or are you too busy remembering?", "Sometimes, forgetting is easier than facing the truth."];
npcs.push(npc2);
game.addChild(npc2);
// Memory Fragments
var frag1 = new MemoryFragment();
frag1.x = 400;
frag1.y = 800;
memoryFragments.push(frag1);
game.addChild(frag1);
var frag2 = new MemoryFragment();
frag2.x = 1700;
frag2.y = 1800;
memoryFragments.push(frag2);
game.addChild(frag2);
var frag3 = new MemoryFragment();
frag3.x = 1200;
frag3.y = 2500;
memoryFragments.push(frag3);
game.addChild(frag3);
// Door to ending
var door = new Door();
door.x = 1024;
door.y = 400;
door.targetScene = "ending";
doors.push(door);
game.addChild(door);
// Show info
showInfo("Explore the town. Tap to move. Find your memories.", 3000);
}
// --- Dialogue System ---
function showDialogue(npc) {
canMove = false;
if (!dialogueBox) {
dialogueBox = new Container();
var box = LK.getAsset('bgTown', {
width: 1600,
height: 400,
color: 0xffffff,
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 2200
});
box.alpha = 0.92;
dialogueBox.addChild(box);
dialogueText = new Text2("", {
size: 70,
fill: 0x222222
});
dialogueText.anchor.set(0.5, 0.5);
dialogueText.x = 1024;
dialogueText.y = 2200;
dialogueBox.addChild(dialogueText);
LK.gui.bottom.addChild(dialogueBox);
}
dialogueBox.visible = true;
dialogueText.setText(npc.name + ": " + npc.dialogue[npc.dialogueIndex]);
// Next dialogue on tap
dialogueBox.down = function (x, y, obj) {
npc.dialogueIndex++;
if (npc.dialogueIndex >= npc.dialogue.length) {
npc.dialogueIndex = 0;
dialogueBox.visible = false;
canMove = true;
} else {
dialogueText.setText(npc.name + ": " + npc.dialogue[npc.dialogueIndex]);
}
LK.getSound('choice').play();
};
}
// --- Choice System ---
function showChoice(msg, choices, onSelect) {
canMove = false;
// Remove old
for (var i = 0; i < choiceButtons.length; ++i) {
if (choiceButtons[i].parent) choiceButtons[i].parent.removeChild(choiceButtons[i]);
}
choiceButtons = [];
// Message
if (!dialogueBox) {
dialogueBox = new Container();
var box = LK.getAsset('bgTown', {
width: 1600,
height: 400,
color: 0xffffff,
anchorX: 0.5,
anchorY: 0.5,
x: 1024,
y: 2200
});
box.alpha = 0.92;
dialogueBox.addChild(box);
dialogueText = new Text2("", {
size: 70,
fill: 0x222222
});
dialogueText.anchor.set(0.5, 0.5);
dialogueText.x = 1024;
dialogueText.y = 2200;
dialogueBox.addChild(dialogueText);
LK.gui.bottom.addChild(dialogueBox);
}
dialogueBox.visible = true;
dialogueText.setText(msg);
// Buttons
for (var i = 0; i < choices.length; ++i) {
var btn = new Text2(choices[i], {
size: 70,
fill: 0x3A3A3A
});
btn.anchor.set(0.5, 0.5);
btn.x = 1024;
btn.y = 2400 + i * 120;
btn.interactive = true;
(function (idx) {
btn.down = function (x, y, obj) {
dialogueBox.visible = false;
for (var j = 0; j < choiceButtons.length; ++j) {
if (choiceButtons[j].parent) choiceButtons[j].parent.removeChild(choiceButtons[j]);
}
choiceButtons = [];
canMove = true;
LK.getSound('choice').play();
onSelect(idx);
};
})(i);
LK.gui.bottom.addChild(btn);
choiceButtons.push(btn);
}
}
// --- Memory Fragment Collection ---
function collectFragment(fragment) {
if (fragment.collected) return;
fragment.collected = true;
foundFragments++;
storage.foundFragments = foundFragments;
LK.getSound('memoryFound').play();
tween(fragment, {
alpha: 0,
scaleX: 2,
scaleY: 2
}, {
duration: 600,
easing: tween.easeOut,
onFinish: function onFinish() {
if (fragment.parent) fragment.parent.removeChild(fragment);
}
});
showInfo("You found a memory fragment! (" + foundFragments + "/" + totalFragments + ")", 2000);
if (foundFragments === totalFragments) {
showInfo("All fragments found. The door is open.", 3000);
}
}
// --- Ending Scene ---
function setupEndingScene() {
game.removeChildren();
if (dialogueBox) dialogueBox.visible = false;
for (var i = 0; i < choiceButtons.length; ++i) {
if (choiceButtons[i].parent) choiceButtons[i].parent.removeChild(choiceButtons[i]);
}
choiceButtons = [];
// Background
var bg = LK.getAsset('bgTown', {
anchorX: 0,
anchorY: 0,
x: 0,
y: 0,
color: 0xeaeaea
});
game.addChild(bg);
// Ending text
var endingText = new Text2("", {
size: 110,
fill: 0x222222
});
endingText.anchor.set(0.5, 0.5);
endingText.x = 1024;
endingText.y = 1200;
if (foundFragments === totalFragments) {
endingText.setText("You remember everything.\nThe town is at peace.\n\nThank you for playing.");
storage.ending = 2;
} else if (foundFragments > 0) {
endingText.setText("Some memories return, but many are lost.\nThe town remains a mystery.\n\nThank you for playing.");
storage.ending = 1;
} else {
endingText.setText("You leave, still lost.\nThe town fades behind you.\n\nThank you for playing.");
storage.ending = 0;
}
game.addChild(endingText);
// Show "You Win" after a short delay
LK.setTimeout(function () {
LK.showYouWin();
}, 2500);
}
// --- Music ---
LK.playMusic('dream', {
fade: {
start: 0,
end: 0.7,
duration: 1200
}
});
// --- Start Scene ---
setupTownScene();
// --- Game Move Handler ---
game.move = function (x, y, obj) {
if (!canMove) return;
// Prevent moving into top left menu area
if (x < 100 && y < 100) return;
// Move player
player.targetX = x;
player.targetY = y;
player.moving = true;
};
// --- Game Down Handler (for interaction) ---
game.down = function (x, y, obj) {
if (!canMove) return;
// --- Gun shooting ---
// Don't shoot if clicking in top left menu area
if (!(x < 100 && y < 100)) {
// Shoot a bullet towards (x, y)
var bullet = new Bullet();
bullet.x = player.x;
bullet.y = player.y;
var dx = x - player.x;
var dy = y - player.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) {
bullet.dirX = 0;
bullet.dirY = -1;
} else {
bullet.dirX = dx / dist;
bullet.dirY = dy / dist;
}
bullets.push(bullet);
game.addChild(bullet);
}
// Check for nearby interactables
var nearObj = null;
// NPCs
for (var i = 0; i < npcs.length; ++i) {
var npc = npcs[i];
var dx = npc.x - player.x;
var dy = npc.y - player.y;
if (dx * dx + dy * dy < 300 * 300) {
nearObj = npc;
break;
}
}
// Memory Fragments
if (!nearObj) {
for (var i = 0; i < memoryFragments.length; ++i) {
var frag = memoryFragments[i];
if (frag.collected) continue;
var dx = frag.x - player.x;
var dy = frag.y - player.y;
if (dx * dx + dy * dy < 180 * 180) {
nearObj = frag;
break;
}
}
}
// Door
if (!nearObj && foundFragments === totalFragments) {
for (var i = 0; i < doors.length; ++i) {
var door = doors[i];
var dx = door.x - player.x;
var dy = door.y - player.y;
if (dx * dx + dy * dy < 220 * 220) {
nearObj = door;
break;
}
}
}
// Interact
if (nearObj) {
if (nearObj instanceof NPC) {
showDialogue(nearObj);
} else if (nearObj instanceof MemoryFragment) {
collectFragment(nearObj);
} else if (nearObj instanceof Door) {
// Ending
setupEndingScene();
}
}
};
// --- Enemy Spawning ---
if (!game.enemySpawnTimer) {
game.enemySpawnTimer = LK.setInterval(function () {
if (currentScene !== "town") return;
// Limit number of enemies
if (enemies.length < 8 + Math.floor(foundFragments * 2)) {
var enemy = new Enemy();
// Spawn at random edge
var edge = Math.floor(Math.random() * 4);
if (edge === 0) {
// top
enemy.x = Math.random() * 2048;
enemy.y = -100;
} else if (edge === 1) {
// bottom
enemy.x = Math.random() * 2048;
enemy.y = 2732 + 100;
} else if (edge === 2) {
// left
enemy.x = -100;
enemy.y = Math.random() * 2732;
} else {
// right
enemy.x = 2048 + 100;
enemy.y = Math.random() * 2732;
}
enemies.push(enemy);
game.addChild(enemy);
}
}, 1200);
}
// --- Game Update ---
game.update = function () {
// Player update
if (player && player.update) player.update();
// --- Bullets update and cleanup ---
for (var i = bullets.length - 1; i >= 0; --i) {
var bullet = bullets[i];
if (bullet.update) bullet.update();
// Remove if out of bounds
if (bullet.x < -200 || bullet.x > 2048 + 200 || bullet.y < -200 || bullet.y > 2732 + 200) {
if (bullet.parent) bullet.parent.removeChild(bullet);
bullets.splice(i, 1);
continue;
}
}
// --- Enemies update, collision with player, and bullet collision ---
for (var i = enemies.length - 1; i >= 0; --i) {
var enemy = enemies[i];
if (enemy.update) enemy.update();
// Check collision with player (game over)
var dx = enemy.x - player.x;
var dy = enemy.y - player.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 90) {
// Game over
LK.effects.flashScreen(0xff0000, 1200);
LK.showGameOver();
return;
}
// Check collision with bullets
for (var j = bullets.length - 1; j >= 0; --j) {
var bullet = bullets[j];
var bdx = enemy.x - bullet.x;
var bdy = enemy.y - bullet.y;
var bdist = Math.sqrt(bdx * bdx + bdy * bdy);
if (bdist < 80) {
// Remove enemy and bullet
if (enemy.parent) enemy.parent.removeChild(enemy);
if (bullet.parent) bullet.parent.removeChild(bullet);
enemies.splice(i, 1);
bullets.splice(j, 1);
// --- Level Progression: Count defeated enemies ---
enemiesDefeated++;
showInfo("Enemies defeated: " + enemiesDefeated + "/" + enemiesToDefeat, 1200);
// Check for level win
if (enemiesDefeated >= enemiesToDefeat) {
if (currentLevel === 1) {
showInfo("Level 1 complete! Advancing...", 2500);
LK.setTimeout(function () {
nextLevel();
}, 2000);
} else if (currentLevel === 2) {
showInfo("Level 2 complete! All enemies defeated!", 2500);
// Optionally, allow ending or further progression
// For now, open the door if all fragments are found
}
}
break;
}
}
}
// --- Endless: Only win if all enemies are gone and all fragments found ---
if (foundFragments === totalFragments && enemiesDefeated >= enemiesToDefeat && enemies.length === 0) {
// Door is open, allow ending
for (var i = 0; i < doors.length; ++i) {
doors[i].showHighlight();
}
}
// Highlight nearby interactables
var nearest = null;
var minDist = 99999;
// NPCs
for (var i = 0; i < npcs.length; ++i) {
var npc = npcs[i];
var dx = npc.x - player.x;
var dy = npc.y - player.y;
var dist = dx * dx + dy * dy;
if (dist < 300 * 300 && dist < minDist) {
nearest = npc;
minDist = dist;
}
npc.hideHighlight();
}
// Memory Fragments
for (var i = 0; i < memoryFragments.length; ++i) {
var frag = memoryFragments[i];
if (frag.collected) continue;
var dx = frag.x - player.x;
var dy = frag.y - player.y;
var dist = dx * dx + dy * dy;
if (dist < 180 * 180 && dist < minDist) {
nearest = frag;
minDist = dist;
}
frag.hideHighlight();
}
// Door
if (foundFragments === totalFragments && enemies.length === 0) {
for (var i = 0; i < doors.length; ++i) {
var door = doors[i];
var dx = door.x - player.x;
var dy = door.y - player.y;
var dist = dx * dx + dy * dy;
if (dist < 220 * 220 && dist < minDist) {
nearest = door;
minDist = dist;
}
door.hideHighlight();
}
}
// Show highlight
if (nearest && nearest.showHighlight) nearest.showHighlight();
// Store last near object
lastNearObject = nearest;
}; ===================================================================
--- original.js
+++ change.js
@@ -55,24 +55,24 @@
if (self.highlight) self.highlight.visible = false;
};
return self;
});
-// --- Enemy Class ---
+// --- Enemy NPC Class (independent from other objects) ---
var Enemy = Container.expand(function () {
var self = Container.call(this);
var sprite = self.attachAsset('npc', {
anchorX: 0.5,
anchorY: 0.5,
tint: 0x8b1a1a
});
self.speed = 6 + Math.random() * 4;
- self.targetX = 1024;
- self.targetY = 2000;
self.lastX = self.x;
self.lastY = self.y;
+ // Each enemy is an independent NPC, not derived from other objects
self.update = function () {
self.lastX = self.x;
self.lastY = self.y;
+ // Move toward the player independently
var dx = player.x - self.x;
var dy = player.y - self.y;
var dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) {
a bullet. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
an angry character that has a soldier hat on his head. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat
a pixel art coin. In-Game asset. 2d. High contrast. No shadows
a pixel war background. In-Game asset. 2d. High contrast. No shadows
an enemy that smiles evilly. No background. Transparent background. Blank background. No shadows. 2d. In-Game asset. flat