# Collisions and tags

> A shot that says itself what it hits: bats by their component, with their api typed, and crates by a tag. A HUD that counts the enemies left by their tag, and a rule of the scene for the ship.

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

Each piece says what it collides with, from its own body, so the shot and the bats are written apart and nothing in the scene has to know both. `useCollisionWith(Bat, (bat) => bat.api.hurt(1))` meets only the pieces made from `Bat`, and TypeScript knows what they return: no check, no cast. `useCollisionWith('breakable', …)` meets anything carrying the tag, here crates that are plain sprites with no component of their own, tagged with `useTag('breakable').add(sprite)`.

A tag is also how the game finds things of a kind: the bats are made with `useSpawn(Bat, { tags: ['enemy'] })`, and the HUD counts `enemies.all()`, which leaves out the ones already destroyed. `useCollisionBetween(Ship, Bat, …)` is the other way round, a rule of the scene that knows both: the ship flashes while a bat touches it. The arrow keys move, X shoots.

## Code

`index.ts`:

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

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

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

`components/bat.ts`:

```ts
import { createSprite, destroy, getColor, useSelf, useTransform, useTimer, useHitbox, useUpdate } from 'nacatamalon';
import { WIDTH } from '../settings';

export const Bat = ({ x, y, speed }: { x: number; y: number; speed: number }) => {
    const self = useSelf();

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

    const body = createSprite({
        width: 14,
        height: 8,
        tint: '#b06ad8',
        transform: {
            x: 0,
            y: 0
        }
    });

    const timer = useTimer();

    useHitbox();

    let lives = 2;
    let dx = speed;

    useUpdate((delta) => {
        place.x += dx * delta;

        if (place.x < 12 || place.x > WIDTH - 12) {
            dx = -dx;
        }
    });

    return {
        hurt: (damage: number) => {
            lives -= damage;
            body.tint = getColor('#ffffff');
            timer.after(0.08, () => {
                body.tint = getColor('#b06ad8');
            });

            if (lives <= 0) {
                destroy(self);
            }
        },
    };
};
```

`components/crate_row.ts`:

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

export const CrateRow = () => {
    const breakable = useTag('breakable');

    // Individual crates remain plain sprites, tagged independently of their owner.
    for (let i = 0; i < 6; i++) {
        breakable.add(createSprite({
            width: 16,
            height: 10,
            tint: '#8c6a3a',
            transform: {
                x: 60 + i * 40,
                y: 104
            }
        }));
    }
};
```

`components/hud.ts`:

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

export const Hud = () => {
    const enemies = useTag('enemy');
    const breakable = useTag('breakable');

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

    createText({
        text: '◀ ▶ MOVE  X SHOOT',
        transform: {
            x: 8,
            y: 20
        },
        tint: '#6b7390'
    });
    useUpdate(() => {
        const left = enemies.all().length;

        hud.text = left > 0 ? `BATS ${left}  CRATES ${breakable.all().length}` : 'CLEAR!';
    });
};
```

`components/ship.ts`:

```ts
import { createSprite, useActions, useTransform, useHitbox, useSpawn, useUpdate } from 'nacatamalon';
import { clamp } from 'nacatamalon/math';
import { WIDTH, HEIGHT, SHIP_SPEED, SHIP_TINT, HIT_TINT } from '../settings';
import { Shot } from './shot';

export const Ship = () => {
    const input = useActions();

    const place = useTransform({
        x: WIDTH / 2,
        y: HEIGHT - 16
    });

    const body = createSprite({
        width: 14,
        height: 10,
        tint: SHIP_TINT,
        transform: {
            x: 0,
            y: 0
        }
    });

    useHitbox({
        width: 10
    });

    const shoot = useSpawn(Shot, {
        name: 'Shot',
        parent: 'scene'
    });

    useUpdate((delta) => {
        const move = (input.isDown('right') ? 1 : 0) - (input.isDown('left') ? 1 : 0);

        place.x = clamp(place.x + move * SHIP_SPEED * delta, 10, WIDTH - 10);

        if (input.justPressed('fire')) {
            shoot({
                x: place.x,
                y: place.y - 8
            });
        }
    });

    return {
        flash: () => {
            body.tint = HIT_TINT;
        },
        calm: () => {
            body.tint = SHIP_TINT;
        }
    };
};
```

`components/shot.ts`:

```ts
import { createSprite, destroy, useSelf, useTransform, useHitbox, useUpdate, useCollisionWith } from 'nacatamalon';
import { SHOT_SPEED } from '../settings';
import { Bat } from './bat';

export const Shot = ({ x, y }: { x: number; y: number }) => {
    const self = useSelf();

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

    createSprite({
        width: 2,
        height: 6,
        tint: '#ffe680',
        transform: {
            x: 0,
            y: 0
        }
    });
    useHitbox();
    useUpdate((delta) => {
        place.y -= SHOT_SPEED * delta;

        if (place.y < -10) {
            destroy(self);
        }
    });
    useCollisionWith(Bat, (bat) => {
        destroy(self);
        bat.api.hurt(1);
    });
    useCollisionWith('breakable', (crate) => {
        destroy(self);
        destroy(crate);
    });
};
```

`scenes/demo.ts`:

```ts
import { createScene, useSpawn, useCollisionBetween, useUpdate } from 'nacatamalon';
import { BATS } from '../settings';
import { Bat } from '../components/bat';
import { Ship } from '../components/ship';
import { CrateRow } from '../components/crate_row';
import { Hud } from '../components/hud';

export const Level = () => {
    useSpawn(CrateRow)();

    const makeBat = useSpawn(Bat, {
        name: 'Bat',
        tags: ['enemy']
    });

    BATS.forEach((bat) => {
        makeBat(bat);
    });

    const ship = useSpawn(Ship, 'Ship')();
    // This scene rule knows both components; shots declare their own collision rules.
    let touched = false;

    useCollisionBetween(Ship, Bat, () => {
        touched = true;
    });
    useUpdate(() => {
        if (touched) {
            ship.api.flash();
        } else {
            ship.api.calm();
        }

        touched = false;
    });
    useSpawn(Hud)();

    return createScene();
};
```

`settings.ts`:

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

export const WIDTH = 320;
export const HEIGHT = 180;
export const SHIP_SPEED = 120;
export const SHOT_SPEED = 220;
export const BATS = [
    {
        x: 40,
        y: 30,
        speed: 50
    },
    {
        x: 160,
        y: 50,
        speed: -70
    },
    {
        x: 260,
        y: 70,
        speed: 40
    }
];
export const ACTIONS: TActionDef[] = [
    {
        name: 'left',
        bindings: [
            {
                type: 'key',
                key: 'ArrowLeft'
            }
        ]
    },
    {
        name: 'right',
        bindings: [
            {
                type: 'key',
                key: 'ArrowRight'
            }
        ]
    },
    {
        name: 'fire',
        bindings: [
            {
                type: 'key',
                key: 'x'
            }
        ]
    }
];

export const SHIP_TINT = getColor('#7df9ff');
export const HIT_TINT = getColor('#ff5a6e');
```
