# Old machine

> A 3D room drawn the way an old machine did: small and unsmoothed, toon light, a few colours with a dither, glow and a tube.

Example `post-old-machine`, running at https://nacatamalon.com/en/examples/post-old-machine. The whole catalogue: https://nacatamalon.com/llms/examples.md.

A 3D room drawn the way some games draw now to look like an old machine did, with nothing written for the look: the game is 320 × 200 and shown bigger without smoothing, the light is cut into flat bands (`toonLighting()` as each material's lighting), `bloom()` makes the bright parts glow, every colour, the glow's too, is moved to the nearest of five or six with `paletteMatch()` and a dither between them, and `crt()` lays lines and glass over all of it.

A pit in the floor fills with fog by height (`useFog({ distance: false, height })`), dark below its rim however near it is. The nightcrawler and the fox wear the toon light while they walk and run.

Click the preview first so it hears the keyboard. P for another palette, D for the dither, T for the toon light, C for the tube, H for the fog in the pit and space to switch the screen effects off.

## Code

`index.ts`:

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

const game = createGame('#app', {
    width: WIDTH,
    height: HEIGHT,
    smooth: false,
    scaling: 'contain',
    background: '#05070d'
});

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

`components/characters.ts`:

```ts
import { createModel, useLoadModel, useSkeletalAnimation, useUpdate } from 'nacatamalon';

export const Characters = () => {
    // The creatures: the nightcrawler walks round the lamp and the fox runs on the spot.
    const crawler = useLoadModel({
        src: '/example-assets/assets/3d/gltf/nightcrawler/nightcrawler.glb'
    });

    const vixen = useLoadModel({
        src: '/example-assets/assets/3d/gltf/Fox/Fox.gltf'
    });

    const walker = createModel({
        model: crawler,
        tint: '#e8e0c8'
    });

    const fox = createModel({
        model: vixen,
        transform: {
            x: 1.8,
            z: 2.4,
            rotationY: -0.6,
            scaleX: 0.012,
            scaleY: 0.012,
            scaleZ: 0.012
        }
    });

    useSkeletalAnimation(walker, {
        play: 'Walk'
    });

    useSkeletalAnimation(fox, {
        play: 'Run'
    });

    const models = [walker, fox];

    useUpdate((_delta, time) => {
        // Round the lamp, facing the way it walks.
        const turn = time * 0.35;

        walker.transform.x = 0.5 + Math.cos(turn) * 2.2;
        walker.transform.z = 0.3 + Math.sin(turn) * 1.8;
        walker.transform.rotationY = -turn;
    });

    return models;
};
```

`components/look_controls.ts`:

```ts
import { useKeyboard, useUpdate } from 'nacatamalon';

import { bloom, crt, paletteMatch, useLoadPalette, usePostProcess, POST_EFFECTS } from 'nacatamalon/effects';
import type { TModel, TFog, TMesh } from 'nacatamalon';
import type { TDressedMesh } from './room';
import { useToonMaterial } from '../hooks/use_toon_material';
import { PALETTES, GLOW, DITHER } from '../settings';

type TLookProps = {
    dressed: TDressedMesh[];
    models: TModel[];
    fog: TFog;
};

export const LookControls = ({ dressed, models, fog }: TLookProps) => {
    const wear = useToonMaterial();
    const seen = new WeakSet<TMesh>();
    // The screen, in order. The glow before the palette, so what it lights is still shown in the
    // palette's colours; one palette effect per palette, only one of them on.
    const glow = usePostProcess(bloom(GLOW));

    const names = PALETTES;

    const palettes = names.map((name, i) => usePostProcess({
        ...paletteMatch({
            dither: DITHER
        }),
        palette: useLoadPalette({
            src: '/example-assets/palettes/' + name + '.palette'
        }),
        enabled: i === 0,
    }));

    const tube = usePostProcess(crt(POST_EFFECTS.CRT_PRESETS.pvm));

    let palette = 0;

    let toonOn = true;

    let dithered = true;

    let off = false;

    const show = () => {
        palettes.forEach((effect, i) => {
            effect.enabled = !off && i === palette;
            effect.uniforms.dither = dithered ? DITHER : 0;
        });
        glow.enabled = !off;
        tube.enabled = !off;
        dressed.forEach((entry) => {
            entry.mesh.material = toonOn ? entry.banded : entry.plain;
        });
        models.forEach((model) => model.parts.forEach((part) => wear(part.material, toonOn)));
    };

    const keys = useKeyboard();

    useUpdate(() => {
        if (keys.justPressed('p')) {
            palette = (palette + 1) % palettes.length;
            show();
        }

        if (keys.justPressed('d')) {
            dithered = !dithered;
            show();
        }

        if (keys.justPressed('t')) {
            toonOn = !toonOn;
            show();
        }

        if (keys.justPressed('c')) {
            tube.enabled = !tube.enabled;
        }

        if (keys.justPressed('h')) {
            fog.height.enabled = !fog.height.enabled;
        }

        if (keys.justPressed(' ')) {
            off = !off;
            show();
        }

        // The pieces arrive with the file, after the scene is built, so they are dressed when they come.
        models.forEach((model) => model.parts.forEach((part) => {
            if (!seen.has(part)) {
                seen.add(part);
                wear(part.material, toonOn);
            }
        }));
    });
};
```

`components/room.ts`:

```ts
import { createMaterial, createMesh, getColor, useCubeGeometry, useCylinderGeometry } from 'nacatamalon';

import type { TGeometry, TMesh, TMeshMaterial, TMeshOptions } from 'nacatamalon';
import { LOOK } from '../settings';

export type TDressedMesh = {
    mesh: TMesh;
    plain: TMeshMaterial;
    banded: TMeshMaterial;
};

export const Room = () => {
    // Each thing has two materials made now, toon and not, and a key swaps which it wears.
    const dressed: TDressedMesh[] = [];

    const make = (geometry: TGeometry, colour: string, at: TMeshOptions['transform'], castShadow = true) => {
        const tint = getColor(colour);

        const plain = createMaterial({
            shader: 'mesh3d',
            tint
        });

        const banded = createMaterial({
            shader: 'mesh3d',
            tint,
            lighting: LOOK
        });

        const mesh = createMesh({
            geometry,
            material: banded,
            transform: at,
            castShadow
        });

        dressed.push({
            mesh,
            plain,
            banded
        });

        return mesh;
    };

    // The floor: tiles with a gap between them, so the grid reads even in four colours, and a pit
    // three tiles across in the front corner.
    const tile = useCubeGeometry({
        width: 0.94,
        height: 0.1,
        depth: 0.94
    });

    const inPit = (x: number, z: number) => x >= 3 && x <= 5 && z >= 2 && z <= 4;

    for (let x = -5; x <= 5; x++) {
        for (let z = -4; z <= 4; z++) {
            if (inPit(x, z)) {
                // Its bottom, far down.
                make(tile, '#355064', {
                    x,
                    y: -6,
                    z
                }, false);
                continue;
            }

            const path = x === 0 || x === 1;

            make(tile, path ? '#c9c0a0' : ((x + z) % 2 === 0 ? '#3d5a70' : '#355064'), {
                x,
                y: -0.05,
                z
            }, false);
        }
    }

    // The pit's sides: a column of ground under every tile at its edge.
    const ground = useCubeGeometry({
        width: 1,
        height: 6,
        depth: 1
    });

    for (let x = 2; x <= 6; x++) {
        for (let z = 1; z <= 5; z++) {
            if (!inPit(x, z) && (inPit(x - 1, z) || inPit(x + 1, z) || inPit(x, z - 1) || inPit(x, z + 1))) {
                make(ground, '#2c3e52', {
                    x,
                    y: -3.1,
                    z
                }, false);
            }
        }
    }

    // Two walls and what stands against them.
    const wall = useCubeGeometry({
        width: 1,
        height: 3,
        depth: 1
    });

    for (let x = -5; x <= 5; x++) {
        make(wall, '#2c3e52', {
            x,
            y: 1.5,
            z: -5
        });
    }

    for (let z = -4; z <= 4; z++) {
        make(wall, '#26364a', {
            x: -6,
            y: 1.5,
            z
        });
    }

    const pillar = useCylinderGeometry({
        radius: 0.35,
        height: 3,
        segments: 16
    });

    make(pillar, '#8a8f96', {
        x: -3,
        y: 1.5,
        z: -3
    });

    make(pillar, '#8a8f96', {
        x: 3,
        y: 1.5,
        z: -3
    });

    const crate = useCubeGeometry({
        width: 0.9,
        height: 0.9,
        depth: 0.9
    });

    make(crate, '#9c7a52', {
        x: -4,
        y: 0.45,
        z: 1
    });

    make(crate, '#9c7a52', {
        x: -4,
        y: 1.35,
        z: 1.2
    });

    make(crate, '#8a6a46', {
        x: -3.1,
        y: 0.45,
        z: 2
    });

    const machine = useCubeGeometry({
        width: 1.4,
        height: 2.2,
        depth: 1
    });

    make(machine, '#a0a6ad', {
        x: 3.8,
        y: 1.1,
        z: 0.5
    });

    make(useCubeGeometry({
        width: 0.5,
        height: 0.3,
        depth: 0.1
    }), '#ff3a3a', {
        x: 3.8,
        y: 1.6,
        z: 1.05
    });

    return dressed;
};
```

`hooks/use_toon_material.ts`:

```ts
import { createMaterial } from 'nacatamalon';
import type { TMeshMaterial } from 'nacatamalon';
import { LOOK } from '../settings';

export const useToonMaterial = () => {
    // The constructor compiles the look and derives its uniform layout once.
    const template = createMaterial({
        shader: 'mesh3d',
        lighting: LOOK
    });

    return (material: TMeshMaterial, on: boolean) => {
        material.fragment = on ? template.fragment : null;
        material.fragmentGlsl = on ? template.fragmentGlsl : null;
        material.uniforms = on ? structuredClone(template.uniforms) : null;
        material.uniformSig = on ? template.uniformSig : null;
    };
};
```

`scenes/demo.ts`:

```ts
import { createScene, useCamera3d, useDirectionalLight, useFog, usePointLight, useSpawn, useUpdate } from 'nacatamalon';
import { Room } from '../components/room';
import { Characters } from '../components/characters';
import { LookControls } from '../components/look_controls';

export const Game = () => {
    useCamera3d({
        projection: 'perspective',
        fov: 50,
        transform: {
            y: 9,
            z: 9.5,
            rotationX: -0.75
        }
    });

    const sun = useDirectionalLight({
        color: '#cfe0ff',
        intensity: 0.9,
        ambient: 0.12,
        transform: {
            rotationX: -0.9,
            rotationY: 0.5
        },
        castShadow: true,
        shadowArea: 22
    });

    usePointLight({
        color: '#ffd9a0',
        intensity: 1.8,
        range: 7,
        transform: {
            x: 0.5,
            y: 2.2,
            z: 0.5
        }
    });

    const fog = useFog({
        color: '#05070d',
        distance: false,
        height: {
            top: -0.3,
            bottom: -4.5
        }
    });

    const room = useSpawn(Room)();
    const characters = useSpawn(Characters)();

    useSpawn(LookControls)({
        dressed: room.api,
        models: characters.api,
        fog
    });

    useUpdate((_delta, time) => {
        sun.transform.rotationY = 0.5 + Math.sin(time * 0.2) * 0.3;
    });

    return createScene();
};
```

`settings.ts`:

```ts
import { toonLighting } from 'nacatamalon/shader';

export const WIDTH = 320;
export const HEIGHT = 200;
export const LOOK = toonLighting({
    bands: 3,
    shadow: 0.3
});
export const PALETTES = ['night_station', 'moon_dust', 'old_print'];
export const GLOW = {
    threshold: 0.7,
    intensity: 0.5
};
export const DITHER = 0.12;
```
