# A lit cave

> A dark cave lit with 2D lights. Settings and components live in separate files.

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

A dark cave lit with 2D lights. Settings and components live in separate files.

## 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, getColor, useAmbientLight2d, useCamera2d, useKeyboard, useLight2d,
    useScreenSpace, useSpawn, useTransform, useUpdate,
} from 'nacatamalon';
import { clamp, lerp } from 'nacatamalon/math';
import { Cave, Hillside, Mushroom, Sky } from './world';
import { GROUND_Y, HEIGHT, MOUTH, WIDTH, WORLD_WIDTH } from './settings';

const Explorer = () => {
    const place = useTransform({ x: 80, y: GROUND_Y - 8 });
    createSprite({ width: 10, height: 16, tint: '#d4dbe6' });
    createSprite({ width: 8, height: 4, tint: '#7b5a3e', transform: { y: -7 } });
    createSprite({ width: 4, height: 5, tint: '#ffd48a', transform: { x: 8, y: 2 } });
    const lantern = useLight2d({ transform: { x: 8, y: 2 }, color: '#ffd28f', radius: 95, intensity: 1 });
    return { place, lantern };
};

const Hud = () => {
    useScreenSpace();
    createSprite({ width: WIDTH, height: 40, anchor: { x: 0, y: 0 }, tint: '#10131c', zIndex: 100 });
    createText({ text: '2D LIGHTS / OUT OF THE CAVE', tint: '#e6ebf2', transform: { x: 10, y: 8 }, zIndex: 101 });
    const status = createText({ text: '', tint: '#ffcf89', style: { fontSize: 8 }, transform: { x: 10, y: 25 }, zIndex: 101 });
    createSprite({ width: WIDTH, height: 18, anchor: { x: 0, y: 0 }, tint: '#10131c', transform: { y: HEIGHT - 18 }, zIndex: 100 });
    createText({ text: 'ARROWS/AD WALK  N DAY OR NIGHT  R RESET', tint: '#c3c9d6', style: { fontSize: 8 },
        transform: { x: 10, y: HEIGHT - 13 }, zIndex: 101 });
    return status;
};

// How far out of the cave the explorer is: 0 deep inside, 1 out on the hillside.
const outside = (x: number) => {
    const t = clamp((x - (MOUTH - 160)) / 260, 0, 1);
    return t * t * (3 - 2 * t);
};

export const Level = () => {
    // One ambient light for the whole world, set by where the explorer stands: dark in the cave,
    // daylight outside, like eyes getting used to the light.
    const ambient = useAmbientLight2d({ color: '#6c7aa8', intensity: 0.1 });
    const camera = useCamera2d();
    const sky = useSpawn(Sky)().api;
    const moon = useSpawn(Hillside)().api;
    useSpawn(Cave)();
    [180, 340, 470, 610, 760].forEach((x, i) => useSpawn(() => Mushroom(x, i * 1.3))());
    const { place, lantern } = useSpawn(Explorer)().api;
    const status = useSpawn(Hud)().api;
    const keys = useKeyboard();
    const cave = getColor('#6c7aa8');
    const day = getColor('#ffffff');
    const night = getColor('#7d8cc8');
    let isNight = false;

    useUpdate((delta) => {
        const walk = Number(keys.isDown('ArrowRight') || keys.isDown('d')) - Number(keys.isDown('ArrowLeft') || keys.isDown('a'));
        place.x = clamp(place.x + walk * 120 * delta, 20, WORLD_WIDTH - 20);
        if (keys.justPressed('n')) isNight = !isNight;
        if (keys.justPressed('r')) { place.x = 80; isNight = false; }
        camera.transform.x = clamp(place.x - WIDTH / 2, 0, WORLD_WIDTH - WIDTH);

        const t = outside(place.x);
        const out = isNight ? night : day;
        ambient.color = { r: lerp(cave.r, out.r, t), g: lerp(cave.g, out.g, t), b: lerp(cave.b, out.b, t), a: 1 };
        ambient.intensity = lerp(0.1, isNight ? 0.22 : 1, t);
        // In daylight the lantern is put away; at night it stays lit.
        lantern.intensity = lerp(1, isNight ? 0.8 : 0, t);
        moon.intensity = isNight ? 0.45 : 0;
        sky.day.visible = !isNight;
        sky.night.visible = isNight;
        const where = t === 0 ? 'IN THE CAVE' : t === 1 ? 'OUTSIDE' : 'AT THE MOUTH';
        status.text = `${where}  AMBIENT ${ambient.intensity.toFixed(2)}  LANTERN ${lantern.intensity.toFixed(2)}  ${isNight ? 'NIGHT' : 'DAY'}`;
    });
    return createScene();
};
```

`settings.ts`:

```ts
export const WIDTH = 480;
export const HEIGHT = 270;
export const WORLD_WIDTH = 1700;
// Where the cave opens onto the hillside.
export const MOUTH = 1000;
export const GROUND_Y = 212;
export const CEILING = 44;
```

`world.ts`:

```ts
import { createSprite, useLight2d, useSpawn, useTransform, useUnlit2d, useUpdate } from 'nacatamalon';
import { createPixels, createPixelTexture, fillCircle, fillGradient, fillNoise, fillPolygon, mapPixels, setPixel } from 'nacatamalon/pixels';
import { CEILING, GROUND_Y, HEIGHT, MOUTH, WORLD_WIDTH } from './settings';

const OUTSIDE = WORLD_WIDTH - MOUTH + 80;

// The sky gives its own light: left out of the lamps, it is bright through the cave's mouth even
// from deep inside, where the rest of the world is dark. Day and night are two pictures.
export const Sky = () => {
    useTransform({ x: MOUTH - 80, y: 0 });
    useUnlit2d();
    const day = createPixels(OUTSIDE, GROUND_Y);
    fillGradient(day, { colors: ['#4f9be0', '#8cc8f2', '#d9eefa'] });
    const night = createPixels(OUTSIDE, GROUND_Y);
    fillGradient(night, { colors: ['#070b1e', '#16224a', '#2c3c6e'] });
    for (let i = 0; i < 90; i++) {
        setPixel(night, (i * 137) % OUTSIDE, (i * 53) % (GROUND_Y - 40), i % 5 === 0 ? '#ffffff' : '#9fb1e0');
    }
    fillCircle(night, OUTSIDE - 150, 50, 14, '#f4f1dc');
    const size = { width: OUTSIDE, height: GROUND_Y, anchor: { x: 0, y: 0 } };
    return {
        day: createSprite({ texture: createPixelTexture(day), ...size }),
        night: createSprite({ texture: createPixelTexture(night), ...size, visible: false }),
    };
};

// The hillside outside, lit like the rest of the world.
export const Hillside = () => {
    const grass = createPixels(OUTSIDE, HEIGHT - GROUND_Y);
    fillNoise(grass, { seed: 4, scale: 8, octaves: 3, from: '#3d6b2c', to: '#79a64a' });
    createSprite({ texture: createPixelTexture(grass), width: OUTSIDE, height: HEIGHT - GROUND_Y, anchor: { x: 0, y: 0 }, transform: { x: MOUTH - 80, y: GROUND_Y } });
    const bush = createPixels(40, 30);
    fillCircle(bush, 12, 18, 11, '#2f6b33');
    fillCircle(bush, 26, 16, 13, '#3b7f3c');
    const shrub = createPixelTexture(bush);
    for (const x of [MOUTH + 140, MOUTH + 330, MOUTH + 520]) {
        createSprite({ texture: shrub, width: 40, height: 30, anchor: { x: 0.5, y: 1 }, transform: { x, y: GROUND_Y + 2 } });
    }
    // The moon lights the hillside at night: one big lamp, far up, over all of it.
    return useLight2d({ transform: { x: WORLD_WIDTH - 150, y: 50 }, color: '#b9c8ff', radius: 900, intensity: 0, falloff: 0.6 });
};

// The rock of the cave: its back wall and its floor, with the ragged edge of its mouth, and the
// teeth hanging from its roof.
const CLEAR = { r: 0, g: 0, b: 0, a: 0 };
const mouthEdge = (y: number) => MOUTH - 36 + Math.sin(y * 0.045) * 22 + Math.sin(y * 0.19) * 6;

export const Cave = () => {
    const wall = createPixels(MOUTH, HEIGHT);
    fillNoise(wall, { seed: 9, scale: 26, octaves: 4, from: '#241d1a', to: '#6a5747' });
    mapPixels(wall, (color, x, y) => (x > mouthEdge(y) ? CLEAR : color));
    createSprite({ texture: createPixelTexture(wall), width: MOUTH, height: HEIGHT, anchor: { x: 0, y: 0 } });

    const roof = createPixels(MOUTH, CEILING + 26);
    const teeth = [{ x: 0, y: 0 }, { x: MOUTH, y: 0 }];
    for (let x = MOUTH; x >= 0; x -= 16) teeth.push({ x, y: CEILING + [0, 20, 6, 26, 2, 12][(x / 16) % 6]! });
    fillPolygon(roof, teeth, '#3a2f29');
    mapPixels(roof, (color, x, y) => (x > mouthEdge(y) ? CLEAR : color));
    createSprite({ texture: createPixelTexture(roof), width: MOUTH, height: CEILING + 26, anchor: { x: 0, y: 0 } });

    const floor = createPixels(MOUTH, HEIGHT - GROUND_Y);
    fillNoise(floor, { seed: 2, scale: 9, octaves: 3, from: '#2f2622', to: '#5d4c3f' });
    mapPixels(floor, (color, x, y) => (x > MOUTH - 20 + Math.sin(y * 0.4) * 6 ? CLEAR : color));
    createSprite({ texture: createPixelTexture(floor), width: MOUTH, height: HEIGHT - GROUND_Y, anchor: { x: 0, y: 0 }, transform: { y: GROUND_Y } });
};

// A mushroom that glows: its cap keeps its colour in the dark, and it lights a little round it.
export const Mushroom = (x: number, phase: number) => {
    useTransform({ x, y: GROUND_Y - 4 });
    createSprite({ width: 3, height: 8, tint: '#d8d2c0', transform: { y: 0 } });
    useSpawn(() => {
        useUnlit2d();
        createSprite({ width: 11, height: 5, tint: '#6ff2e0', transform: { y: -5 } });
    })();
    const glow = useLight2d({ transform: { y: -6 }, color: '#4fe6d4', radius: 48, intensity: 0.7 });
    let time = phase;
    useUpdate((delta) => {
        time += delta;
        glow.intensity = 0.6 + Math.sin(time * 1.5) * 0.15;
    });
};
```
