# Collisions with physics

> Crates that are physics bodies say what they land on with the same line a hitbox uses, and physics layers with names: a crate turned ghost falls through the floor.

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

A crate is a physics body, and it hears itself land with `useCollisionWith(Floor, …, { when: 'start' })`: the same line a piece with a hitbox would write, told here by the simulation, so nothing has a hitbox. A game can move from hitboxes to physics without rewriting a collision.

The layers have names, given once to the game: `physicsLayers: ['world', 'crates', 'ghost']`. The floor says `layer: 'world', collidesWith: ['crates']`, a crate `layer: 'crates', collidesWith: ['world', 'crates']`, and G turns every crate with `setLayers('ghost', [])`: on a layer nothing meets, it falls through. Two bodies meet only if each one's `collidesWith` has the other's layer. The crates carry the tag `'crate'` and the HUD counts them by it. Z drops a crate.

## Code

`index.ts`:

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

installPhysics2d();

const game = createGame('#app', {
    width: WIDTH,
    height: HEIGHT,
    step: 60,
    background: '#0d1020',
    physicsLayers: PHYSICS_LAYERS,
    actions: ACTIONS
});

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

`components/crate.ts`:

```ts
import { createSprite, destroy, getColor, useSelf, useTransform, usePhysicsBody2d, useTimer, useCollisionWith, useUpdate } from 'nacatamalon';
import { getPhysicsWorld2d } from 'nacatamalon/physics2d';
import { HEIGHT, CRATE } from '../settings';
import { Floor } from './floor';

export const Crate = ({ x }: { x: number }) => {
    const self = useSelf();

    const place = useTransform({
        x,
        y: -10
    });

    const body = createSprite({
        width: CRATE.size,
        height: CRATE.size,
        tint: '#8c6a3a',
        transform: {
            x: 0,
            y: 0
        }
    });

    usePhysicsBody2d({
        body: 'dynamic',
        collider: {
            shape: 'rect',
            width: CRATE.size,
            height: CRATE.size
        },
        bounce: CRATE.bounce,
        layer: 'crates',
        collidesWith: ['world', 'crates']
    });

    const timer = useTimer();

    useCollisionWith(Floor, () => {
        body.tint = getColor('#ffe680');
        timer.after(0.15, () => {
            body.tint = getColor('#8c6a3a');
        });
    }, {
        when: 'start'
    });
    useUpdate(() => {
        if (place.y > HEIGHT + 40) {
            destroy(self);
        }
    });

    const handle = getPhysicsWorld2d()?.bodyOf(self);

    return {
        ghost: () => {
            handle?.setLayers('ghost', []);
            body.tint = getColor('#4a5378');
        },
    };
};
```

`components/drop_controls.ts`:

```ts
import { createText, useSpawn, useActions, useUpdate, useTag } from 'nacatamalon';
import { WIDTH, CRATE } from '../settings';
import { Crate } from './crate';

export const DropControls = () => {
    const crates = useTag('crate');

    const drop = useSpawn(Crate, {
        name: 'Crate',
        tags: ['crate'],
        parent: 'scene'
    });

    // The ones made, kept typed: their api is what turns them to ghosts.
    let made: ReturnType<typeof drop>[] = [];
    let next = CRATE.startX;

    const dropOne = () => {
        made.push(drop({
            x: next
        }));
        next = next > WIDTH - 60 ? CRATE.startX : next + CRATE.gap;
    };

    dropOne();
    dropOne();

    const input = useActions();

    const hud = createText({
        text: '',
        transform: {
            x: 8,
            y: 8
        },
        tint: '#9aa4c0'
    });

    createText({
        text: 'Z DROP  G GHOSTS',
        transform: {
            x: 8,
            y: 20
        },
        tint: '#6b7390'
    });

    useUpdate(() => {
        if (input.justPressed('drop')) {
            dropOne();
        }

        if (input.justPressed('ghosts')) {
            made = made.filter((crate) => !crate.destroyed);

            for (const crate of made) {
                crate.api.ghost();
            }
        }

        hud.text = `CRATES ${crates.all().length}`;
    });
};
```

`components/floor.ts`:

```ts
import { createSprite, useTransform, usePhysicsBody2d } from 'nacatamalon';
import { WIDTH, HEIGHT } from '../settings';

export const Floor = () => {
    useTransform({
        x: WIDTH / 2,
        y: HEIGHT - 10
    });
    createSprite({
        width: WIDTH,
        height: 20,
        tint: '#262d48',
        transform: {
            x: 0,
            y: 0
        }
    });
    usePhysicsBody2d({
        body: 'static',
        collider: {
            shape: 'rect',
            width: WIDTH,
            height: 20
        },
        layer: 'world',
        collidesWith: ['crates']
    });
};
```

`scenes/demo.ts`:

```ts
import { createScene, usePhysicsWorld2d, useSpawn } from 'nacatamalon';
import { Floor } from '../components/floor';
import { DropControls } from '../components/drop_controls';

export const Level = () => {
    usePhysicsWorld2d();
    useSpawn(Floor, 'Floor')();
    useSpawn(DropControls)();

    return createScene();
};
```

`settings.ts`:

```ts
import type { TActionDef } from 'nacatamalon';

export const WIDTH = 320;
export const HEIGHT = 180;
export const PHYSICS_LAYERS = ['world', 'crates', 'ghost'];
export const CRATE = {
    size: 16,
    bounce: 0.2,
    startX: 40,
    gap: 37
};
export const ACTIONS: TActionDef[] = [
    {
        name: 'drop',
        bindings: [
            {
                type: 'key',
                key: 'z'
            }
        ]
    },
    {
        name: 'ghosts',
        bindings: [
            {
                type: 'key',
                key: 'g'
            }
        ]
    }
];
```
