# Platformer on tiles

> A character walking the map with no physics engine, the classic way: ledges to jump up through, a hill, ladders, belts that carry you, spikes that hurt and ice.

Example `tile-platformer`, running at https://nacatamalon.com/en/examples/tile-platformer. The whole catalogue: https://nacatamalon.com/llms/examples.md.

`useTileBody2d` moves a character against the map's tiles: across and against walls first, then up or down against floors. The tiles say what they are in the `.tilemap`: `oneWay` for ledges you jump up through and drop down from, `slope` for the hill, `ladder`, `push` for the belts, `damage` for the spikes, and `props` for anything else, here the ice the game makes slippery itself.

Nothing is simulated: with `step: 60` it is the same game on every machine.

← → walk, Z jump, ↑ ↓ climb, ↓ on a ledge to drop through.

## Code

`index.ts`:

```ts
import { createGame } from 'nacatamalon';
import { WIDTH, HEIGHT } from './settings';
import { Level } from './scenes/demo';

const game = createGame('#app', {
    width: WIDTH,
    height: HEIGHT,
    background: '#262a3c',
    step: 60
});

// Declare Scenes
game({
    Level
});
```

`components/hero.ts`:

```ts
import { createSprite, useKeyboard, useLoadTexture, useTileBody2d, useUpdate } from 'nacatamalon';
import type { TCharacterBody2d } from 'nacatamalon';
import * as Settings from '../settings';

export const Hero = (status: { text: string }) => {
    const sprite = createSprite({
        texture: useLoadTexture({
            src: '/example-assets/assets/platformer/hero.png'
        }),
        transform: {
            x: Settings.START.x,
            y: Settings.START.y
        }
    });

    // The box it collides with is narrower than its picture, so it fits up a ladder one tile wide.
    const body: TCharacterBody2d = useTileBody2d({
        width: 12,
        height: 22,
        offset: [0, 1]
    });

    const keys = useKeyboard();
    let hurt = 0;

    useUpdate((delta) => {
        const walk = (keys.isDown('ArrowRight') ? 1 : 0) - (keys.isDown('ArrowLeft') ? 1 : 0);
        const up = keys.isDown('ArrowUp');
        const down = keys.isDown('ArrowDown');
        const jump = keys.justPressed('z') || keys.justPressed('ArrowUp');

        // Ice is a prop of the tile, not something the engine knows: the game makes it slippery by
        // reaching its speed slowly.
        const target = walk * 100;
        const ice = body.ground?.props?.ice === true;

        body.velocity.x = ice ? body.velocity.x + (target - body.velocity.x) * Math.min(1, delta * 1.5) : target;

        if (walk !== 0) {
            sprite.flipX = walk < 0;
        }

        // On a ladder: up in front of it, or down standing on its top, starts climbing.
        // (Up from its top would only climb out of it again.)
        if (body.isOnLadder && !body.climbing && ((up && !body.isOnOneWay) || (down && body.isOnOneWay))) {
            body.climbing = true;
        }

        if (body.climbing) {
            body.velocity.x = walk * 40;
            body.velocity.y = up ? -70 : down ? 70 : 0;

            if (keys.justPressed('z')) {
                body.climbing = false;
            }
        } else {
            if (jump && body.isOnGround) {
                body.velocity.y = -330;
            }

            if (keys.justPressed('ArrowDown') && body.isOnOneWay && !body.isOnLadder) {
                body.dropThrough();
            }
        }

        // Spikes: the body says how much harm, the game decides what that does. Here, a hop back.
        hurt = Math.max(0, hurt - delta);

        if (body.damage > 0 && hurt === 0) {
            hurt = 1;
            body.velocity.y = -220;
            body.velocity.x = sprite.flipX ? 120 : -120;
        }

        sprite.visible = hurt === 0 || Math.floor(hurt * 12) % 2 === 0;

        status.text = [body.isOnGround ? 'GROUND' : 'AIR', body.isOnOneWay ? 'LEDGE' : '', body.climbing ? 'CLIMBING' : body.isOnLadder ? 'LADDER' : '', body.wallSide !== null ? `WALL ${body.wallSide.toUpperCase()}` : '', body.ground?.push !== undefined ? 'BELT' : '', ice ? 'ICE' : '', body.damage > 0 ? `DAMAGE ${body.damage}` : '',].filter((word) => word !== '').join(' · ');
    });
};
```

`components/hud.ts`:

```ts
import { createText, useScreenSpace, useUpdate } from 'nacatamalon';

import * as Settings from '../settings';

export const Hud = (status: { text: string }) => {
    useScreenSpace();

    const line = createText({
        text: '',
        transform: {
            x: 8,
            y: 8
        }
    });

    createText({
        text: '← → WALK  Z JUMP  ↓ DROP  ↑↓ LADDER',
        transform: {
            x: 8,
            y: Settings.HEIGHT - 14
        },
        tint: '#9aa4c0'
    });
    useUpdate(() => {
        line.text = status.text;
    });
};
```

`scenes/demo.ts`:

```ts
import { createScene, createTilemap, useSpawn } from 'nacatamalon';

import * as Settings from '../settings';

import { Hero } from '../components/hero';
import { Hud } from '../components/hud';

export const Level = () => {
    createTilemap({
        src: Settings.MAP
    });

    const status = {
        text: ''
    };

    useSpawn(Hero)(status);
    useSpawn(Hud)(status);

    return createScene();
};
```

`settings.ts`:

```ts
export const WIDTH = 480;
export const HEIGHT = 320;
export const START = {
    x: 40,
    y: 230
};
export const MAP = '/example-assets/assets/platformer/walk.tilemap';
```
