DEV Community

Cover image for 🎮 Building a Top‑Down Action Game with the Limn Engine
Kehinde Owolabi
Kehinde Owolabi

Posted on

🎮 Building a Top‑Down Action Game with the Limn Engine

🎮 Building a Top‑Down Action Game with the Limn Engine

A complete, line‑by‑line tutorial

Live demo: limn-engine-doc.vercel.app/test11.html


📖 Introduction

Every great game developer remembers their first "real" project – the moment when code transformed from abstract syntax into something interactive, something alive on the screen. Yet for many beginners, that milestone remains frustratingly out of reach, buried under complex frameworks, opaque build tools, and thousands of lines of boilerplate.

The Limn Engine changes that. Designed from the ground up for accessibility and clarity, it strips away unnecessary complexity and lets you focus on what matters: building your game. With just a single JavaScript file and a few lines of code, you can create a fully functional 2D game that runs in any browser.

This tutorial will guide you through every line of test11.html – a complete top‑down action game built with the Limn Engine. You'll learn not just what the code does, but why it works that way. By the end, you'll have a solid understanding of:

  • Setting up the engine and game world
  • Designing tile‑based levels
  • Implementing player movement, collisions, and AI
  • Adding visual effects, UI, and game state management
  • Using the engine's powerful features like the camera system, particles, and off‑screen rendering

Whether you're a complete beginner taking your first steps or an experienced developer looking for a refreshingly simple tool, this guide will give you the confidence to build your own games with the Limn Engine.

Let's begin.


📁 1. Project Setup & HTML Structure

<!doctype html>
<html>
  <head>
    <!-- Include the Limn Engine -->
    <script src="epic.js"></script>
  </head>
  <body>
  <script>
    // All game code goes here
Enter fullscreen mode Exit fullscreen mode

What this does:

  • <!doctype html> – Declares this as an HTML5 document.
  • <html> – The root element of the page.
  • <head> – Contains meta‑information about the page.
  • <script src="epic.js"> – Loads the entire Limn Engine into your page. This gives you access to all the classes like Display, Component, Tctxt, ParticleSystem, etc.
  • <body> – The visible part of the webpage.
  • <script> – This is where we write our game code.

🚀 2. Engine Initialization

// Create the main game display and start it
const display = new Display();
display.perform();
display.start(800, 600);
Enter fullscreen mode Exit fullscreen mode

What this does:

  • new Display() – Creates the main game window object – your game's "screen".
  • display.perform() – A special setup function that prepares the engine to use a more advanced rendering loop.
  • display.start(800, 600) – Actually creates the <canvas> element on your webpage and sets its size to 800×600 pixels. The game loop starts running immediately.
// Set up background image for the fake canvas
fake.bgComm = new Component(2000,2000, "#1a1a2e", 0 ,0);
fake.bgComm.setImage("limn-doc/img/alien.png");
Enter fullscreen mode Exit fullscreen mode

What this does:

  • fake.bgComm – The fake object is an off‑screen canvas (a hidden canvas used for rendering static things). .bgComm is a special property for a background component.
  • new Component(2000,2000, "#1a1a2e", 0, 0) – Creates a 2000×2000 pixel component at position (0,0). The color "#1a1a2e" is a dark blue‑gray.
  • setImage("limn‑doc/img/alien.png") – Changes the component from a coloured rectangle to an image. This loads and displays the alien image as the background.
// Set the game area size (for culling/performance)
TCJSgameGameArea.width = 1600;
TCJSgameGameArea.height = 1200;

// Set the camera's world boundaries
display.camera.worldWidth = 1600;
display.camera.worldHeight = 1200;
Enter fullscreen mode Exit fullscreen mode

What this does:

  • TCJSgameGameArea.width/height – Sets the size of the "game world". The engine uses this to decide what to draw (culling). If something is outside this area, it might not be rendered, which saves performance.
  • display.camera.worldWidth/Height – Tells the camera how big the entire game world is. The camera will never scroll beyond these bounds, keeping the player within the world.
// Create the particle system
const particles = new ParticleSystem(display);
Enter fullscreen mode Exit fullscreen mode

What this does:

  • new ParticleSystem(display) – Creates a new particle manager linked to our main display. We'll use this to create cool effects like explosions and sparkles.

🗺️ 3. Creating the Level (TileMap)

// Define the level layout as a 2D array (10 rows x 10 columns)
fake.map = [
    [1,1,1,1,1,1,1,1,1,1], // Row 0
    [1,0,0,0,0,0,0,0,0,1], // Row 1
    [1,0,2,0,0,0,0,2,0,1], // Row 2
    [1,0,0,1,1,0,0,0,0,1], // Row 3
    [1,0,0,0,0,0,0,0,0,1], // Row 4
    [1,0,0,0,0,1,1,0,0,1], // Row 5
    [1,0,2,0,0,0,0,2,0,1], // Row 6
    [1,0,0,0,0,0,0,0,0,1], // Row 7
    [1,0,0,0,0,0,0,0,0,1], // Row 8
    [1,1,1,1,1,1,1,1,1,1]  // Row 9
];
Enter fullscreen mode Exit fullscreen mode

What this does:

  • This is a 10×10 grid. Each number represents a tile type:
    • 0 – empty space (floor)
    • 1 – wall
    • 2 – coin
  • This array is the blueprint for your level. The walls form a border and some interior structures, and there are coins placed in the corridors.
// Define tile size and create tile components
const tileW = 40;
const tileH = 40;

fake.tile = [
    new Component(tileW, tileH, "#654321", 0, 0, "rect"), // Index 0: Wall tile
    new Component(25, 25, "gold", 0, 0, "rect")          // Index 1: Coin tile
];
Enter fullscreen mode Exit fullscreen mode

What this does:

  • tileW and tileH – Set the size of each tile to 40×40 pixels. Since our world is 1600×1200, we have exactly 40 tiles across and 30 tiles down.
  • fake.tile – This array holds the visual representation of each tile type.
    • fake.tile[0] (index 0) is a brown rectangle (#654321) – our wall.
    • fake.tile[1] (index 1) is a small gold rectangle – our coin.
  • Important: The engine automatically adds an empty placeholder at index 0, so our actual tiles start at index 1. This is why we map:
    • Tile ID 1fake.tile[1] (wall)
    • Tile ID 2fake.tile[2] (coin)
// Configure and render the tilemap
fake.mapWidth = 1600;
fake.mapHeight = 1200;
fake.canvas.width = 1600;
fake.canvas.height = 1200;
fake.tileMap();
const tilemap = fake.tileFace;
fake.tileFace.show();
Enter fullscreen mode Exit fullscreen mode

What this does:

  • fake.mapWidth/Height – Set the size of the tilemap world.
  • fake.canvas.width/Height – Resize the off‑screen canvas to match the world size.
  • fake.tileMap() – This function reads fake.map and fake.tile and creates the internal tilemap object.
  • const tilemap = fake.tileFace – We store a reference to the tilemap so we can use it later for collision detection and coin collection.
  • fake.tileFace.show() – This renders all the tiles onto the off‑screen fake canvas. It calculates the position of each tile based on its row/column and draws it. This happens only once, which is very efficient!

🎮 4. Creating Game Objects

Creating the Player

const player = new Component(40, 40, "#5b8cff", 700, 500, "rect");
display.add(player);
Enter fullscreen mode Exit fullscreen mode

What this does:

  • new Component(40, 40, "#5b8cff", 700, 500, "rect") – Creates a new component (game object).
    • 40, 40 – width and height in pixels.
    • "#5b8cff" – the colour (bright blue).
    • 700, 500 – starting X and Y position.
    • "rect" – the type is a rectangle.
  • display.add(player) – Adds the player to the main display, so it will be drawn and updated every frame.

Creating Enemies

let enemies = [];
for (let i = 0; i < 4; i++) {
    const enemy = new Component(35, 35, "#ff6b6b", 
        Math.random() * 1400 + 100, 
        Math.random() * 1000 + 100, "rect");
    enemy.speed = 60;
    display.add(enemy);
    enemies.push(enemy);
}
Enter fullscreen mode Exit fullscreen mode

What this does:

  • let enemies = [] – Creates an empty array to store all our enemy objects.
  • for (let i = 0; i < 4; i++) – Loops 4 times to create 4 enemies.
  • new Component(35, 35, "#ff6b6b", ..., ..., "rect") – Creates a 35×35 pixel red enemy.
    • Math.random() * 1400 + 100 – random X position between 100 and 1500.
    • Math.random() * 1000 + 100 – random Y position between 100 and 1100.
  • enemy.speed = 60 – Adds a custom speed property to the enemy. The engine doesn't use this automatically; we'll use it in our update function.
  • display.add(enemy) – Adds the enemy to the display.
  • enemies.push(enemy) – Stores the enemy in our array so we can update all of them easily.

Creating the UI (Health Bar & Score)

// Health bar background
const healthBarBg = new Component(220, 25, "#440000", 20, 50, "rect");
const healthBarFill = new Component(200, 20, "#00ff44", 30, 52, "rect");
display.add(healthBarBg);
display.add(healthBarFill);

// Score text with styling
const scoreText = new Tctxt("24px", "Arial", "#ffffff", 20, 100, 
    "left", false, "hanging", "rgba(0,0,0,0.5)", 10, 5);
scoreText.setText("Score: 0");
display.add(scoreText);
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Health Bar Background (healthBarBg):
    • 220, 25 – 220px wide, 25px tall.
    • "#440000" – dark red.
    • 20, 50 – position at (20, 50) from the top‑left corner of the screen.
    • This creates the "container" for the health bar.
  • Health Bar Fill (healthBarFill):
    • 200, 20 – slightly smaller to fit inside the background.
    • "#00ff44" – bright green.
    • 30, 52 – positioned slightly inside the background bar.
    • This bar will change width to represent health.
  • Score Text (scoreText):
    • new Tctxt(...)Tctxt is a special text component with more features.
    • "24px", "Arial", "#ffffff" – font size, family, colour.
    • 20, 100 – position.
    • "left" – alignment.
    • false – no stroke/outline.
    • "hanging" – text baseline.
    • "rgba(0,0,0,0.5)" – semi‑transparent black background.
    • 10, 5 – padding around the text.
    • scoreText.setText("Score: 0") – sets the initial text.
    • display.add(scoreText) – adds it to the display.

🎯 5. Game State & Helper Functions

let score = 0;
let health = 100;
let invincibleTimer = 0;
let gameActive = true;
Enter fullscreen mode Exit fullscreen mode

What this does:

  • score – tracks the player's points.
  • health – starts at 100. When it reaches 0, the game is over.
  • invincibleTimer – counts down to make the player briefly invincible after being hit.
  • gameActive – a boolean that stops the game logic when false.
function applyKnockback(target, fromX, fromY, strength = 150) {
    const angle = Math.atan2(target.y - fromY, target.x - fromX);
    target.speedX = Math.cos(angle) * strength;
    target.speedY = Math.sin(angle) * strength;
    target.physics = true;
    setTimeout(() => {
        target.speedX = 0;
        target.speedY = 0;
        target.physics = false;
    }, 200);
}
Enter fullscreen mode Exit fullscreen mode

What this does:

  • This function is called when the player is hit.
  • Math.atan2(target.y - fromY, target.x - fromX) – calculates the angle from the enemy to the player.
  • target.speedX/Y = Math.cos/Sin(angle) * strength – sets the player's speed in the direction away from the enemy (knockback).
  • target.physics = true – enables physics for the player so the speed will actually move them.
  • setTimeout(...) – after 200 milliseconds, stops the player's movement by setting speed back to 0 and disabling physics.

🔄 6. The Main Game Loop (update function)

This function runs every frame (≈60 times per second). It's the heart of your game.

📌 Section 1 – Initial Checks

function update(dt) {
    if (!gameActive) return;
Enter fullscreen mode Exit fullscreen mode

What this does:

  • function update(dt) – the engine calls this every frame and passes dt (delta time in seconds). Using dt makes movement consistent regardless of framerate.
  • if (!gameActive) return – if the game is over, it exits immediately, freezing all game logic.
    // Make camera follow the player
    display.camera.follow(player, true);
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Centers the camera on the player with smooth (interpolated) movement.
    // Keep UI elements fixed on screen
    healthBarBg.fixed();
    healthBarFill.fixed();
    scoreText.fixed();
Enter fullscreen mode Exit fullscreen mode

What this does:

  • .fixed() – a special method for UI components. Normally components move with the camera; .fixed() pins them to the screen so they stay in the corner even when the camera moves.
    // Update particle effects
    particles.update();
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Updates all particles (moves them, fades them, removes dead ones). Must be called every frame.

📌 Section 2 – Player Invincibility

    if (invincibleTimer > 0) {
        invincibleTimer -= dt;
        player.alpha = 0.5;  // Make player semi-transparent (blink)
    } else {
        player.alpha = 1;    // Fully opaque
    }
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Counts down invincibleTimer using dt.
  • player.alpha = 0.5 – makes the player 50% transparent (blink effect). When timer reaches 0, the player becomes fully opaque.

📌 Section 3 – Player Movement

    // Read keyboard input
    let mx = 0, my = 0;
    if (display.keys[37]) mx = -1; // Left arrow
    if (display.keys[39]) mx = 1;  // Right arrow
    if (display.keys[38]) my = -1; // Up arrow
    if (display.keys[40]) my = 1;  // Down arrow
Enter fullscreen mode Exit fullscreen mode

What this does:

  • display.keys – an array where the engine stores which keys are currently pressed (keycodes 37‑40 for arrow keys).
  • If a key is pressed, we set mx or my to 1 or -1 (direction).
    // Normalize diagonal movement
    if (mx !== 0 && my !== 0) {
        mx *= 0.707;
        my *= 0.707;
    }
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Prevents faster diagonal movement by multiplying both components by 0.707 (≈1/√2). This keeps the movement vector length at 1, so the player moves at the same speed in all directions.
    // Apply movement
    player.x += mx * 200 * dt;
    player.y += my * 200 * dt;
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Moves the player by mx (or my) times speed (200 px/s) times dt. This makes movement framerate‑independent.

📌 Section 4 – Wall Collision

    if (tilemap.crashWith(player, 1)) {
        if (mx > 0) player.x -= 10;
        if (mx < 0) player.x += 10;
        if (my > 0) player.y -= 10;
        if (my < 0) player.y += 10;
    }
Enter fullscreen mode Exit fullscreen mode

What this does:

  • tilemap.crashWith(player, 1) – checks if the player collides with any tile that has ID 1 (walls).
  • If collision occurs, we "push" the player back by 10 pixels in the opposite direction of movement – a simple way to prevent walking through walls.

📌 Section 5 – Coin Collection

    const coins = tilemap.tiles(2);
    for (let coin of coins) {
        if (player.crashWith(coin)) {
            tilemap.remove(coin.tx, coin.ty);
            score += 10;
            scoreText.setText("Score: " + score);
            move.particles.sparkle(particles, coin.x + coin.width/2, coin.y + coin.height/2);
            display.camera.shake(2, 2);
        }
    }
Enter fullscreen mode Exit fullscreen mode

What this does:

  • tilemap.tiles(2) – gets all tile objects with ID 2 (coins).
  • Loops through all coins, checks overlap with player.
  • tilemap.remove(coin.tx, coin.ty) – removes the coin.
  • score += 10 – adds 10 points.
  • scoreText.setText(...) – updates the display.
  • move.particles.sparkle(...) – creates a sparkle effect at the coin's center.
  • display.camera.shake(2, 2) – shakes the camera slightly.

📌 Section 6 – Enemy AI & Collisions

    for (let i = 0; i < enemies.length; i++) {
        const enemy = enemies[i];
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Uses a standard for loop (not for...of) because we may need to remove enemies from the array while iterating.
        // Enemy chases player using normalized direction
        let dx = player.x - enemy.x;
        let dy = player.y - enemy.y;
        const distance = Math.sqrt(dx * dx + dy * dy);

        if (distance > 5) {
            enemy.x += (dx / distance) * enemy.speed * dt;
            enemy.y += (dy / distance) * enemy.speed * dt;
        }
Enter fullscreen mode Exit fullscreen mode

What this does:

  • dx and dy – distance vector from enemy to player.
  • distance – straight‑line distance (Pythagoras).
  • The if (distance > 5) check prevents jitter when the enemy is already on top of the player.
  • (dx / distance) and (dy / distance)normalised direction vector (length = 1). Multiplying by enemy.speed * dt gives consistent speed in any direction – this solves the "diagonal speed boost" problem.
        // Enemy hits player (and player is not invincible)
        if (player.crashWith(enemy) && invincibleTimer <= 0) {
            health -= 15;
            const healthPercent = Math.max(0, health / 100);
            healthBarFill.width = 200 * healthPercent;

            applyKnockback(player, enemy.x, enemy.y, 200);
            display.camera.shake(6, 6);
            move.particles.blood(particles, player.x, player.y, 10);
            invincibleTimer = 1.0;

            if (health <= 0) {
                gameActive = false;
                const gameOverText = new Tctxt("48px", "Arial", "#ff0000", 
                    400 + display.camera.x, 300 + display.camera.y, "center");
                gameOverText.setText("GAME OVER - Score: " + score);
                display.add(gameOverText);
            }
        }
Enter fullscreen mode Exit fullscreen mode

What this does:

  • If player touches an enemy and is not invincible:
    • health -= 15 – reduces health.
    • healthPercent – health as a decimal (0.0‑1.0).
    • healthBarFill.width = 200 * healthPercent – shrinks the green fill.
    • applyKnockback(...) – pushes player away.
    • display.camera.shake(6, 6) – bigger shake.
    • move.particles.blood(...) – blood particle effect.
    • invincibleTimer = 1.0 – 1 second of invincibility.
    • If health ≤ 0:
    • gameActive = false – stops the loop.
    • Creates a new Tctxt for the game‑over message, centered on screen, and adds it.
        // Click to defeat enemy
        if (mouse.down && enemy.clicked()) {
            move.particles.explosion(particles, enemy.x, enemy.y, 20);
            score += 50;
            scoreText.setText("Score: " + score);
            display.camera.shake(4, 4);
            enemy.hide();
            enemies.splice(i, 1);
            i--;
        }
Enter fullscreen mode Exit fullscreen mode

What this does:

  • mouse.down – is the mouse button pressed?
  • enemy.clicked() – checks if the click occurred inside the enemy's rectangle.
  • move.particles.explosion(...) – creates an explosion effect.
  • score += 50 – bonus points.
  • scoreText.setText(...) – updates score.
  • display.camera.shake(4, 4) – moderate shake.
  • enemy.hide() – makes the enemy disappear (sets update to null).
  • enemies.splice(i, 1) – removes the enemy from the array.
  • i-- – adjusts the loop counter so the next enemy is processed correctly.

📌 Section 7 – Player Boundaries

    // Keep player inside world boundaries
    player.x = Math.max(0, Math.min(1560, player.x));
    player.y = Math.max(0, Math.min(1160, player.y));
}
Enter fullscreen mode Exit fullscreen mode

What this does:

  • Clamps the player's X position between 0 and 1560 (world width minus player width).
  • Clamps Y between 0 and 1160 (world height minus player height).
  • This ensures the player never leaves the visible world.

✅ Conclusion

You've now built a complete game using the Limn Engine! You've learned how to:

  • Set up the engine and a game world
  • Create tile‑based levels
  • Add player movement with keyboard controls
  • Implement wall collisions
  • Add coin collection with particle effects
  • Create enemies with AI that chase the player
  • Manage player health and invincibility
  • Build a UI with health bars and score text
  • Handle game‑over conditions
  • Use camera shake for game feel

The Limn Engine makes all of this possible with clean, readable code. Happy game development! 🎮
📦 Full Code: test11.html

Here's the complete, ready-to-run code for the game we just built. You can copy this directly into an HTML file and run it in your browser.


<!doctype html>
<html>
  <head>
    <script src="epic.js"></script>
  </head>
  <body>
  <script>
    const display = new Display();
display.perform();
display.start(800, 600);
//display.backgroundColor("lightblue")
fake.bgComm = new Component(2000,2000, "#1a1a2e", 0 ,0)
fake.bgComm.setImage("limn-doc/img/alien.png")
TCJSgameGameArea.width = 1600
TCJSgameGameArea.height = 1200
display.camera.worldWidth = 1600;
display.camera.worldHeight = 1200;
// display.camera.setZoom(0.75,0.75)
// display.camera.x-=100
const particles = new ParticleSystem(display);

// ===== EVENT BUS =====
const EventBus = {
    listeners: {},
    on(event, callback) {
        if (!this.listeners[event]) this.listeners[event] = [];
        this.listeners[event].push(callback);
    },
    emit(event, data) {
        if (this.listeners[event]) {
            this.listeners[event].forEach(cb => cb(data));
        }
    }
};

// ===== PLAYER =====
const player = new Component(40, 40, "#5b8cff", 700, 500, "rect");
display.add(player);

// ===== SIMPLE LEVEL (10x10 grid, no procedural generation to avoid bugs) =====
fake.map = [
    [1,1,1,1,1,1,1,1,1,1],
    [1,0,0,0,0,0,0,0,0,1],
    [1,0,2,0,0,0,0,2,0,1],
    [1,0,0,1,1,0,0,0,0,1],
    [1,0,0,0,0,0,0,0,0,1],
    [1,0,0,0,0,1,1,0,0,1],
    [1,0,2,0,0,0,0,2,0,1],
    [1,0,0,0,0,0,0,0,0,1],
    [1,0,0,0,0,0,0,0,0,1],
    [1,1,1,1,1,1,1,1,1,1]
];

const tileSize = 80; // 800/10 = 80, 600/10 = 60 but we need square
// Actually for 1600x1200 world, tiles should be 40x40
 tileW = 40;
tileH = 40;

fake.tile = [new Component(tileW, tileH, "#654321", 0, 0, "rect"),new Component(25, 25, "gold", 0, 0, "rect")
]
fake.mapWidth =1600
fake.mapHeight = 1200
fake.canvas.width = 1600
fake.canvas.height = 1200
fake.tileMap()
const tilemap = fake.tileFace
fake.tileFace.show();

// ===== ENEMIES =====
let enemies = [];
for (let i = 0; i < 4; i++) {
    const enemy = new Component(35, 35, "#ff6b6b", 
        Math.random() * 1400 + 100, 
        Math.random() * 1000 + 100, "rect");
    enemy.speed = 60;
    display.add(enemy);
    enemies.push(enemy);
}

// ===== UI =====
const healthBarBg = new Component(220, 25, "#440000", 20, 50, "rect");
const healthBarFill = new Component(200, 20, "#00ff44", 30, 52, "rect");
display.add(healthBarBg);
display.add(healthBarFill);

const scoreText = new Tctxt("24px", "Arial", "#ffffff", 20, 100, "left", false, "hanging", "rgba(0,0,0,0.5)", 10, 5);
scoreText.setText("Score: 0");
display.add(scoreText);
let hhh = [healthBarBg,healthBarFill, scoreText]
// ===== GAME STATE =====
let score = 0;
let health = 100;
let invincibleTimer = 0;
let gameActive = true;

// ===== CUSTOM KNOCKBACK =====
function applyKnockback(target, fromX, fromY, strength = 150) {
    const angle = Math.atan2(target.y - fromY, target.x - fromX);
    target.speedX = Math.cos(angle) * strength;
    target.speedY = Math.sin(angle) * strength;
    target.physics = true;
    setTimeout(() => {
        target.speedX = 0;
        target.speedY = 0;
        target.physics = false;
    }, 200);
}
// ===== UPDATE FUNCTION =====
function update(dt) {
    if (!gameActive) return;
    display.camera.follow(player,true);
    healthBarBg.fixed()
    healthBarFill.fixed()
    scoreText.fixed()
    particles.update();

    // Invincibility frames
    if (invincibleTimer > 0) {
        invincibleTimer -= dt;
        player.alpha = 0.5;
    } else {
        player.alpha = 1;
    }

    // Player movement
    let mx = 0, my = 0;
    if (display.keys[37]) mx = -1;
    if (display.keys[39]) mx = 1;
    if (display.keys[38]) my = -1;
    if (display.keys[40]) my = 1;

    if (mx !== 0 && my !== 0) {
        mx *= 0.707;
        my *= 0.707;
    }

    player.x += mx * 200 * dt;
    player.y += my * 200 * dt;

    // Wall collision
    if (tilemap.crashWith(player, 1)) {
        if (mx > 0) player.x -= 10;
        if (mx < 0) player.x += 10;
        if (my > 0) player.y -= 10;
        if (my < 0) player.y += 10;
    }

    // Coin collection
    const coins = tilemap.tiles(2);
    for (let coin of coins) {
        if (player.crashWith(coin)) {
            tilemap.remove(coin.tx, coin.ty);
            //tilemap.show()
            //fake.refresh()
            score += 10;
            scoreText.setText("Score: " + score);
            move.particles.sparkle(particles, coin.x + coin.width/2, coin.y + coin.height/2);
            display.camera.shake(2, 2);
        }
    }

    // Enemy AI
    for (let i = 0; i < enemies.length; i++) {
        const enemy = enemies[i];

        // Chase player
        if (enemy.x < player.x) enemy.x += enemy.speed * dt;
        if (enemy.x > player.x) enemy.x -= enemy.speed * dt;
        if (enemy.y < player.y) enemy.y += enemy.speed * dt;
        if (enemy.y > player.y) enemy.y -= enemy.speed * dt;

        // Enemy collision with player
        if (player.crashWith(enemy) && invincibleTimer <= 0) {
            health -= 15;
            const healthPercent = Math.max(0, health / 100);
            healthBarFill.width = 200 * healthPercent;

            applyKnockback(player, enemy.x, enemy.y, 200);
            display.camera.shake(6, 6);
            move.particles.blood(particles, player.x, player.y, 10);
            invincibleTimer = 1.0;

            if (health <= 0) {
                gameActive = false;
                const gameOverText = new Tctxt("48px", "Arial", "#ff0000", 400+display.camera.x, 300+display.camera.y, "center");
                gameOverText.setText("GAME OVER - Score: " + score);
                display.add(gameOverText);
            }
        }

        // Click to defeat enemy
        if (mouse.down && enemy.clicked()) {
          console.log("uygiik")
            move.particles.explosion(particles, enemy.x, enemy.y, 20);
            score += 50;
            scoreText.setText("Score: " + score);
            display.camera.shake(4, 4);
            enemy.hide();
            enemies.splice(i, 1);
            i--;
        }
    }

    // Boundaries

    player.x = Math.max(0, Math.min(1560, player.x));
    player.y = Math.max(0, Math.min(1160, player.y));
}
  </script>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

📝 How to Use This Code

  1. Save the code as test11.html
  2. Make sure epic.js (the Limn Engine) is in the same folder
  3. Open test11.html in your browser
  4. Play the game! Use arrow keys to move, click on enemies to defeat them


Useful Links


If anything is still unclear, just ask – I'm happy to explain any part of the code in more detail!

Top comments (0)