# Ship corridor

> Eight ceiling strips down an endless corridor, the one left behind moved to the front; A sounds the alarm.

Example `rect-light-corridor`, running at https://nacatamalon.com/en/examples/rect-light-corridor. The whole catalogue: https://nacatamalon.com/llms/examples.md.

A ship corridor walked down with no end, lit by strips across its ceiling. Eight strips are enough to light it: the one left behind is moved to the far end, out of sight in the dark.

Press `A` for the alarm, which turns every strip red and makes them pulse by changing their `color` and `intensity` each frame.

## Code

`index.ts`:

```ts
import { createGame } from 'nacatamalon';

import { WIDTH, HEIGHT } from './settings';
import { Corridor } from './scenes/demo';

const game = createGame('#app', {
    width: WIDTH,
    height: HEIGHT,
    background: '#000000'
});

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

`components/shell.ts`:

```ts
import { createMesh, getColor, usePlaneGeometry } from 'nacatamalon';

import { LONG, GAP } from '../settings';

export const Shell = () => {
    // The corridor is as long as the eight strips and moves with the camera, a stretch at a time,
    // so the walls never end either.
    const floor = usePlaneGeometry({
        width: 4,
        depth: LONG + GAP,
        widthSegments: 16,
        depthSegments: 100
    });

    const side = usePlaneGeometry({
        width: LONG + GAP,
        depth: 3,
        widthSegments: 100,
        depthSegments: 12
    });

    const metal = getColor('#7a8494');

    const shell = [
        createMesh({
            geometry: floor,
            tint: '#4a505c',
            specular: '#50586a',
            shininess: 16
        }),
        createMesh({
            geometry: floor,
            tint: '#3a404a',
            transform: {
                y: 3,
                rotationX: Math.PI
            }
        }),
        createMesh({
            geometry: side,
            tint: metal,
            transform: {
                x: -2,
                y: 1.5,
                rotationY: Math.PI / 2,
                rotationX: Math.PI / 2
            }
        }),
        createMesh({
            geometry: side,
            tint: metal,
            transform: {
                x: 2,
                y: 1.5,
                rotationY: -Math.PI / 2,
                rotationX: Math.PI / 2
            }
        }),
    ];

    return shell;
};
```

`components/strip.ts`:

```ts
import { createMesh, useCubeGeometry, useRectLight } from 'nacatamalon';
import { WHITE } from '../settings';

export const Strip = (z: number) => {
    const light = useRectLight({
        width: 2.4,
        height: 0.25,
        range: 4,
        intensity: 1.8,
        ambient: 0,
        color: WHITE,
        transform: {
            y: 2.95,
            z
        }
    });

    const tube = createMesh({
        geometry: useCubeGeometry(),
        tint: '#ffffff',
        emissive: WHITE,
        transform: {
            y: 2.99,
            z,
            scaleX: 2.4,
            scaleY: 0.02,
            scaleZ: 0.25
        }
    });

    return {
        light,
        tube
    };
};
```

`scenes/demo.ts`:

```ts
import { createScene, createText, getColor, useCamera3d, useKeyboard, useSpawn, useUpdate } from 'nacatamalon';
import * as Settings from '../settings';

import { STRIPS, GAP, LONG, WHITE, RED } from '../settings';
import { Strip } from '../components/strip';
import { Shell } from '../components/shell';

export const Corridor = () => {
    const camera = useCamera3d(Settings.CAMERA);

    const shell = useSpawn(Shell)().api;
    const spawn = useSpawn(Strip);

    const strips = Array.from({
        length: STRIPS
    }, (_, i) => spawn(-i * GAP - 1.5).api);

    const label = createText({
        text: '',
        tint: '#b8d8ff',
        transform: {
            x: 8,
            y: 8
        }
    });

    createText({
        text: 'A  ALARM',
        tint: '#6a7a90',
        transform: {
            x: 8,
            y: 20
        }
    });

    const keys = useKeyboard();
    let alarm = false;

    useUpdate((delta, time) => {
        if (keys.justPressed('a')) {
            alarm = !alarm;
        }

        camera.transform.z -= delta * 2.2;

        const z = camera.transform.z;

        // The walls follow a whole stretch at a time, so the floor's squares never seem to slide.
        const middle = Math.round(z / GAP) * GAP - LONG / 2 + GAP;

        for (const piece of shell) {
            piece.transform.z = middle;
        }

        const pulse = alarm ? 0.35 + 0.65 * Math.abs(Math.sin(time * 4)) : 1;

        for (const strip of strips) {
            // Left behind the camera: sent to the far end.
            if (strip.light.transform.z > z + GAP) {
                strip.light.transform.z -= LONG;
                strip.tube.transform.z -= LONG;
            }

            strip.light.color = alarm ? RED : WHITE;
            strip.light.intensity = 1.8 * pulse;
            strip.tube.material.emissive = alarm ? getColor({
                r: pulse,
                g: 0.1 * pulse,
                b: 0.1 * pulse,
                a: 1
            }) : WHITE;
        }

        label.text = alarm ? 'ALARM' : 'DECK 3';
    });

    return createScene();
};
```

`settings.ts`:

```ts
export const WIDTH = 480;
export const HEIGHT = 320;
import type { TCamera3dOptions } from 'nacatamalon';
export const CAMERA = {
    projection: 'perspective',
    fov: 70,
    near: 0.1,
    far: 40,
    transform: {
        y: 1.6,
        z: 0
    }
} satisfies TCamera3dOptions;
import { getColor } from 'nacatamalon';

export const STRIPS = 8;
export const GAP = 3;
export const LONG = STRIPS * GAP;
export const WHITE = getColor('#dff4ff');
export const RED = getColor('#ff3030');
```
