# Mine lights

> Explore a mine with local lights that colour its walls and objects.

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

Explore a mine with local lights that colour its walls and objects.

## Code

`index.ts`:

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

// Both backends run this same scene. Change to 'WEBGL2' to compare.
const game = createGame('#app', {
    width: WIDTH,
    height: HEIGHT,
    renderer: 'AUTO',
    background: '#000000',
    pixelSnap: 'screen',
});

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

`level.ts`:

```ts
import {
    createScene, createSprite, createText, useAmbientLight2d, useCamera2d, useKeyboard, useLight2d,
    useScreenSpace, useSpawn, useTransform, useUpdate,
} from 'nacatamalon';
import { clamp } from 'nacatamalon/math';
import { Torch, Tunnel } from './mine';
import { FLOOR_Y, HEIGHT, SPACING, TORCHES, TORCH_RADIUS, WIDTH, WORLD_WIDTH } from './settings';

const Miner = () => {
    const place = useTransform({ x: 60, y: FLOOR_Y - 9 });
    createSprite({ width: 10, height: 16, tint: '#c9b48f' });
    createSprite({ width: 12, height: 5, tint: '#e3c33a', transform: { y: -8 } });
    // The lamp on the helmet goes where the miner goes: it belongs to the same box.
    const lamp = useLight2d({ transform: { x: 5, y: -8 }, color: '#fff1c8', radius: 70, intensity: 0.9 });
    return { place, lamp };
};

const Hud = () => {
    useScreenSpace();
    createSprite({ width: WIDTH, height: 40, anchor: { x: 0, y: 0 }, tint: '#120e0c', zIndex: 100 });
    createText({ text: '2D LIGHTS / THE LONG MINE', tint: '#f2e3cf', transform: { x: 10, y: 8 }, zIndex: 101 });
    const status = createText({ text: '', tint: '#ffbf75', style: { fontSize: 8 }, transform: { x: 10, y: 25 }, zIndex: 101 });
    createSprite({ width: WIDTH, height: 18, anchor: { x: 0, y: 0 }, tint: '#120e0c', transform: { y: HEIGHT - 18 }, zIndex: 100 });
    createText({ text: 'AD WALK  T HALF THE TORCHES  L HELMET  R RESET',
        tint: '#cbbba8', style: { fontSize: 8 }, transform: { x: 10, y: HEIGHT - 13 }, zIndex: 101 });
    return status;
};

export const Level = () => {
    useAmbientLight2d({ color: '#7d8bb0', intensity: 0.08 });
    const camera = useCamera2d();
    useSpawn(Tunnel)();
    const torches = Array.from({ length: TORCHES }, (_, i) => useSpawn(() => Torch(SPACING * (i + 1) - 26, i * 1.7))().api);
    const { place, lamp } = useSpawn(Miner)().api;
    const status = useSpawn(Hud)().api;
    const keys = useKeyboard();
    let half = false;
    let helmet = true;

    // The torches whose light reaches what the camera shows: the view picks its eight from these.
    const reaching = () => torches.filter((torch, i) => (!half || i % 2 === 0)
        && torch.x + TORCH_RADIUS > camera.transform.x && torch.x - TORCH_RADIUS < camera.transform.x + WIDTH).length;

    useUpdate((delta) => {
        const walk = Number(keys.isDown('ArrowRight') || keys.isDown('d')) - Number(keys.isDown('ArrowLeft') || keys.isDown('a'));
        place.x = clamp(place.x + walk * 110 * delta, 20, WORLD_WIDTH - 20);
        if (keys.justPressed('t')) {
            half = !half;
            torches.forEach((torch, i) => torch.setLit(!half || i % 2 === 0));
        }
        if (keys.justPressed('l')) helmet = !helmet;
        if (keys.justPressed('r')) {
            place.x = 60; half = false; helmet = true;
            torches.forEach((torch) => torch.setLit(true));
        }
        lamp.intensity = helmet ? 0.9 : 0;
        camera.transform.x = clamp(place.x - WIDTH / 2, 0, WORLD_WIDTH - WIDTH);
        const lit = half ? Math.ceil(TORCHES / 2) : TORCHES;
        status.text = `${lit} TORCHES LIT, ${reaching()} REACH THIS VIEW` + (helmet ? ' + HELMET' : '');
    });
    return createScene();
};
```

`mine.ts`:

```ts
import { createSprite, useLight2d, useSpawn, useTransform, useUnlit2d, useUpdate } from 'nacatamalon';
import { createPixels, createPixelTexture, fillNoise } from 'nacatamalon/pixels';
import { FLOOR_Y, HEIGHT, TORCH_RADIUS, WORLD_WIDTH } from './settings';

// The rock, the floor and the rails, all the way along. Painted once, in code.
export const Tunnel = () => {
    const rock = createPixels(WORLD_WIDTH, HEIGHT);
    fillNoise(rock, { seed: 7, scale: 28, octaves: 4, from: '#2a221d', to: '#75624f' });
    createSprite({ texture: createPixelTexture(rock), width: WORLD_WIDTH, height: HEIGHT, anchor: { x: 0, y: 0 } });

    const ground = createPixels(WORLD_WIDTH, HEIGHT - FLOOR_Y);
    fillNoise(ground, { seed: 3, scale: 10, octaves: 3, from: '#3b3029', to: '#5c4a3c' });
    createSprite({ texture: createPixelTexture(ground), width: WORLD_WIDTH, height: HEIGHT - FLOOR_Y, anchor: { x: 0, y: 0 }, transform: { y: FLOOR_Y } });

    for (let x = 4; x < WORLD_WIDTH; x += 16) {
        createSprite({ width: 10, height: 4, tint: '#5a4030', anchor: { x: 0, y: 0 }, transform: { x, y: FLOOR_Y + 9 } });
    }
    createSprite({ width: WORLD_WIDTH, height: 2, tint: '#a3a7b0', anchor: { x: 0, y: 0 }, transform: { y: FLOOR_Y + 7 } });
    createSprite({ width: WORLD_WIDTH, height: 2, tint: '#a3a7b0', anchor: { x: 0, y: 0 }, transform: { y: FLOOR_Y + 13 } });
};

// A wooden frame holding up the roof, with a torch on it. Each has a lamp of its own.
export const Torch = (x: number, phase: number) => {
    useTransform({ x, y: 0 });
    createSprite({ width: 8, height: FLOOR_Y - 40, tint: '#6e4e34', anchor: { x: 0.5, y: 0 }, transform: { x: -26, y: 40 } });
    createSprite({ width: 8, height: FLOOR_Y - 40, tint: '#6e4e34', anchor: { x: 0.5, y: 0 }, transform: { x: 26, y: 40 } });
    createSprite({ width: 70, height: 10, tint: '#7d5a3c', transform: { y: 40 } });
    createSprite({ width: 4, height: 10, tint: '#3d3f46', transform: { x: 26, y: 139 } });

    const fire = useSpawn(() => {
        useTransform({ x: 26, y: 130 });
        // The flame gives its own light: left out of the lamps, it keeps its colour.
        useUnlit2d();
        return [
            createSprite({ width: 6, height: 8, tint: '#ffb347' }),
            createSprite({ width: 2, height: 4, tint: '#fff2b0', transform: { y: 1 } }),
        ];
    })().api;

    const lamp = useLight2d({ transform: { x: 26, y: 130 }, color: '#ffae5e', radius: TORCH_RADIUS, falloff: 1.3 });
    let time = phase;
    let lit = true;
    useUpdate((delta) => {
        time += delta;
        const flicker = Math.sin(time * 9) * 0.06 + Math.sin(time * 23) * 0.04;
        lamp.intensity = lit ? 1.05 + flicker : 0;
        for (const flame of fire) flame.visible = lit;
    });
    return {
        x: x + 26,
        lamp,
        setLit: (on: boolean) => { lit = on; },
    };
};
```

`settings.ts`:

```ts
export const WIDTH = 480;
export const HEIGHT = 270;
// Twenty-four torches, one every hundred pixels: far more than the eight a view is lit by.
export const TORCHES = 24;
export const SPACING = 100;
export const WORLD_WIDTH = (TORCHES + 1) * SPACING;
export const FLOOR_Y = 206;
export const TORCH_RADIUS = 100;
```
