# Platformer in the 2D world

> The same character inside the 2D physics world: the map is its walls, a bridge of planks on hinges sags under it, and it pushes crates.

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

`useCharacterBody2d` is the same character as `useTileBody2d`, still moved exactly as fast as the game says, but in the 2D world of `installPhysics2d`: the map's tiles become the world's walls, ledges and slopes, the bridge is nine planks joined by `useJoint2d` hinges that sag under its weight, and it pushes the crates it walks into.

← → walk, Z jump. C drops crates onto the bridge. B lets one hinge go and the bridge parts, hanging from both banks.

## Code

`index.ts`:

```ts
import { createGame } from 'nacatamalon';
import { installPhysics2d } from 'nacatamalon/physics2d';

import * as Settings from './settings';
import { Level } from './scenes/demo';

installPhysics2d();

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

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

`components/bridge.ts`:

```ts
import { useSpawn } from 'nacatamalon';
import { useJoint2d } from 'nacatamalon/physics2d';
import type { TGameObject } from 'nacatamalon';
import type { TJointHandle2d } from 'nacatamalon/physics2d';
import * as Settings from '../settings';
import { Plank } from './plank';
import { hingeAt } from '../helpers/hinge_at';

export const Bridge = () => {
    // The bridge: nine planks pinned to each other where they meet, and to the banks at both ends.
    // A little friction in each hinge, so it settles instead of swaying as long as the game runs.
    const count = (Settings.GAP.to - Settings.GAP.from) / 16;
    const spawnPlank = useSpawn(Plank);
    const planks: TGameObject[] = [];

    for (let i = 0; i < count; i++) {
        planks.push(spawnPlank(hingeAt(i, count), hingeAt(i + 1, count)));
    }

    const joints: TJointHandle2d[] = [];

    joints.push(useJoint2d(planks[0], null, {
        type: 'hinge',
        at: hingeAt(0, count),
        friction: 2
    }));

    for (let i = 1; i < count; i++) {
        joints.push(useJoint2d(planks[i - 1], planks[i], {
            type: 'hinge',
            at: hingeAt(i, count),
            friction: 2
        }));
    }

    joints.push(useJoint2d(planks[count - 1], null, {
        type: 'hinge',
        at: hingeAt(count, count),
        friction: 2
    }));

    return {
        breakMiddle: () => joints[Math.floor(joints.length / 2)]?.destroy()
    };
};
```

`components/crate.ts`:

```ts
import { createSprite, usePhysicsBody2d } from 'nacatamalon';

import type { TSpriteAtlas } from 'nacatamalon';

export const Crate = (tiles: TSpriteAtlas, x: number, y: number) => {
    createSprite({
        atlas: tiles,
        frame: 11,
        transform: {
            x,
            y
        }
    });
    usePhysicsBody2d({
        body: 'dynamic',
        collider: {
            shape: 'rect',
            width: 16,
            height: 16
        },
        density: 0.5,
        friction: 0.8
    });
};
```

`components/hero.ts`:

```ts
import { createSprite, useKeyboard, useLoadTexture, useUpdate } from 'nacatamalon';
import { useCharacterBody2d } from 'nacatamalon/physics2d';

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

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

    const body = useCharacterBody2d({
        width: 12,
        height: 22,
        offset: [0, 1]
    });

    const keys = useKeyboard();

    useUpdate(() => {
        const walk = (keys.isDown('ArrowRight') ? 1 : 0) - (keys.isDown('ArrowLeft') ? 1 : 0);

        body.velocity.x = walk * 100;

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

        if ((keys.justPressed('z') || keys.justPressed('ArrowUp')) && body.isOnGround) {
            body.velocity.y = -330;
        }

        if (keys.isDown('ArrowDown') && body.isOnOneWay) {
            body.dropThrough();
        }

        // Fallen into the river: back to the start.
        if (sprite.transform.y > Settings.HEIGHT + 40) {
            body.setPosition(Settings.START.x, Settings.START.y);
            body.velocity.y = 0;
        }
    });
};
```

`components/plank.ts`:

```ts
import { createSprite, usePhysicsBody2d } from 'nacatamalon';

export const Plank = (from: { x: number; y: number; }, to: { x: number; y: number; }) => {
    const length = Math.hypot(to.x - from.x, to.y - from.y);

    const place = {
        x: (from.x + to.x) / 2,
        y: (from.y + to.y) / 2,
        rotation: Math.atan2(to.y - from.y, to.x - from.x)
    };

    createSprite({
        width: length,
        height: 4,
        tint: '#d08a3c',
        transform: place
    });
    usePhysicsBody2d({
        body: 'dynamic',
        collider: {
            shape: 'rect',
            width: length,
            height: 4
        },
        density: 2,
        friction: 1
    });
};
```

`helpers/hinge_at.ts`:

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

export const hingeAt = (i: number, planks: number) => {
    const t = i / planks;

    return {
        x: Settings.GAP.from + Settings.OFF + t * (Settings.GAP.to - Settings.GAP.from - Settings.OFF * 2),
        y: Settings.GAP.y + 2 + Settings.SAG * 4 * t * (1 - t)
    };
};
```

`scenes/demo.ts`:

```ts
import { createScene, createSpriteAtlas, createText, createTilemap, useKeyboard, useLoadTexture, usePhysicsWorld2d, useScreenSpace, useSpawn, useUpdate } from 'nacatamalon';

import * as Settings from '../settings';
import { useRandom } from 'nacatamalon/math';
import { Bridge } from '../components/bridge';
import { Crate } from '../components/crate';
import { Hero } from '../components/hero';

export const Level = () => {
    const random = useRandom();

    usePhysicsWorld2d();
    createTilemap({
        src: '/example-assets/assets/platformer/bridge.tilemap'
    });

    const tiles = createSpriteAtlas({
        texture: useLoadTexture({
            src: '/example-assets/assets/platformer/tiles.png'
        }),
        columns: 8,
        rows: 3
    });

    const bridge = useSpawn(Bridge)().api;

    const spawnCrate = useSpawn(Crate, {
        parent: 'scene'
    });

    spawnCrate(tiles, 280, 200);
    spawnCrate(tiles, 300, 200);
    spawnCrate(tiles, 290, 180);
    useSpawn(Hero)();

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

    useUpdate(() => {
        if (keys.justPressed('c') && crates < 20) {
            crates++;
            spawnCrate(tiles, Settings.GAP.from + 20 + random.rand() * (Settings.GAP.to - Settings.GAP.from - 40), 20);
        }

        // One hinge let go of, in the middle: the bridge parts and hangs from both banks.
        if (keys.justPressed('b')) {
            bridge.breakMiddle();
        }
    });

    useSpawn(function Hud() {
        useScreenSpace();
        createText({
            text: '← → WALK  Z JUMP  C CRATE  B BREAK THE BRIDGE',
            transform: {
                x: 8,
                y: 8
            },
            tint: '#9aa4c0'
        });
    })();

    return createScene();
};
```

`settings.ts`:

```ts
export const WIDTH = 480;
export const HEIGHT = 320;
export const START = {
    x: 40,
    y: 200
};
export const GAP = {
    from: 96,
    to: 240,
    y: 224
};
export const SAG = 6;
export const OFF = 2;
```
