# Save the room

> Save and restore a scene with its objects and scripts. The HUD shows the controls and document state.

Example `save-the-room`, running at https://nacatamalon.com/en/examples/save-the-room. The whole catalogue: https://nacatamalon.com/llms/examples.md.

Save and restore a scene with its objects and scripts. The HUD shows the controls and document state.

## Code

`index.ts`:

```ts
import { createGame } from 'nacatamalon';
import { WIDTH, HEIGHT } from './settings';
import { registerKeeper } from './scripts/keeper';
import { registerWalk } from './scripts/walk';
import { createRoom } from './scenes/room';
import { createHud } from './scenes/hud';

const keeper = registerKeeper();

registerWalk();

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

// Declare Scenes
game({
    Room: createRoom(keeper),
    Hud: createHud(keeper)
}, 'Room');
```

`components/objects.ts`:

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

import { HERO, COIN } from '../settings';

export const Hero = () => {
    createSprite({
        tint: HERO,
        width: 18,
        height: 18,
        transform: {
            x: 40,
            y: 96
        }
    });
    useScript('walk', {
        speed: 90
    });
};

export const Coin = (x: number, y: number) => {
    createSprite({
        tint: COIN,
        width: 10,
        height: 10,
        transform: {
            x: x,
            y: y
        }
    });
};
```

`scenes/hud.ts`:

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

import { FONT, PAPER, DIM } from '../settings';
import type { TKeeper } from '../scripts/keeper';

export const createHud = (keeper: TKeeper) => () => {
    const font = useLoadBitmapFont(FONT);

    createText({
        text: 'ARROWS MOVE   S SAVE   L LOAD',
        font,
        style: {
            fontSize: 8
        },
        tint: DIM,
        transform: {
            x: 20,
            y: 156
        },
    });

    const state = createText({
        text: '',
        font,
        style: {
            fontSize: 8
        },
        tint: PAPER,
        transform: {
            x: 20,
            y: 176
        }
    });

    let shown: ReturnType<TKeeper['savedRoom']> | undefined;

    useUpdate(() => {
        const doc = keeper.savedRoom();

        if (doc === shown) {
            return;
        }

        shown = doc;

        // Read out of the document itself, not out of the game: the numbers on screen are the ones
        // in the file.
        const hero = doc === null ? undefined : doc.root.children.find((box) => box.name === 'Hero');
        const art = hero === undefined ? undefined : hero.components.find((one) => one.type === 'sprite');

        const left = doc === null ? 0 : doc.root.children.filter((box) => box.name === 'Coin'
            && !box.components.some((one) => 'visible' in one && one.visible === false)).length;

        state.text = doc === null
            ? 'NOTHING SAVED YET'
            : 'SAVED ' + keeper.savedBytes() + ' B   HERO X ' + Math.round(art === undefined ? 0 : (art.transform?.x ?? 0)) + '   COINS ' + left;
    });

    return createScene();
};
```

`scenes/room.ts`:

```ts
import { createScene, createSprite, useScene, useScript, useSpawn } from 'nacatamalon';
import { sceneFromDoc } from 'nacatamalon/files';
import { FLOOR } from '../settings';
import { Hero, Coin } from '../components/objects';
import type { TKeeper } from '../scripts/keeper';

const freshRoom = () => {
    const scene = useScene();

    createSprite({
        tint: FLOOR,
        width: 320,
        height: 120,
        anchor: {
            x: 0,
            y: 0
        },
        transform: {
            x: 20,
            y: 20
        }
    });
    useSpawn(Hero, 'Hero')();
    useSpawn(Coin, 'Coin')(120, 40);
    useSpawn(Coin, 'Coin')(210, 96);
    useSpawn(Coin, 'Coin')(300, 60);

    // The behaviour that saves and loads lives on the room itself, so it comes back with it.
    useScript('keeper');

    scene.launch('Hud');

    return createScene();
};

// With a save, the room IS the save: same name, same scene, nothing in the engine knowing the
// difference. That is what makes it a load and not a second way of building a level.
export const createRoom = (keeper: TKeeper) => () => {
    const doc = keeper.savedRoom();

    return doc === null ? freshRoom() : sceneFromDoc(doc, 'save.json')();
};
```

`scripts/keeper.ts`:

```ts
import { useKeyboard, useScene, useUpdate, registerScript } from 'nacatamalon';
import { parseSceneDoc } from 'nacatamalon/files';
import { serializeScene } from 'nacatamalon/authoring';

// The save file, as it would be on disk: one piece of text. Nothing else in the game can reach it
// except by asking, which is what a save file is.
export const registerKeeper = () => {
    let saved: ReturnType<typeof parseSceneDoc> | null = null;
    let bytes = 0;

    const savedRoom = () => saved;
    const savedBytes = () => bytes;

    registerScript('keeper', (self) => {
        const scene = useScene();
        const keys = useKeyboard();

        useUpdate(() => {
            if (keys.justPressed('s')) {
                // serializeScene walks the object it is given and hands back a document: plain data,
                // no engine in it. Through text and back on purpose, because a save file that only
                // works while the game is still running is not a save file.
                const text = JSON.stringify(serializeScene(self));

                bytes = text.length;
                saved = parseSceneDoc(JSON.parse(text), 'save.json');
            }

            // Restarting the scene is the whole load: the scene function below builds from the save
            // when there is one, so there is no second way to make a room.
            if (keys.justPressed('l') && saved !== null) {
                scene.change('Room');
            }
        });
    });

    return {
        savedRoom,
        savedBytes
    };
};
export type TKeeper = ReturnType<typeof registerKeeper>;
```

`scripts/walk.ts`:

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

export const registerWalk = () => {
    registerScript<{ speed: number; }>('walk', (self, props) => {
        const [hero] = self.drawables;

        if (hero === undefined) {
            return;
        }

        const keys = useKeyboard();
        // The boxes alongside this one, so the behaviour finds the coins by name rather than by
        // importing anything. The LIST and not a filter of it: this runs while the room is still being
        // built, and the coins are added after the hero, so anything picked out now would be picked out
        // of a room with no coins in it yet. Same rule as useScriptApi, and it fails just as quietly.
        const siblings = self.parent === null ? [] : self.parent.children;

        useUpdate((delta) => {
            const speed = Number(props.speed) * delta;

            if (keys.isDown('ArrowLeft')) {
                hero.transform.x -= speed;
            }

            if (keys.isDown('ArrowRight')) {
                hero.transform.x += speed;
            }

            if (keys.isDown('ArrowUp')) {
                hero.transform.y -= speed;
            }

            if (keys.isDown('ArrowDown')) {
                hero.transform.y += speed;
            }

            for (const box of siblings) {
                if (box.name !== 'Coin') {
                    continue;
                }

                const [coin] = box.drawables;

                // `=== false` and not `!visible`: what nobody set is absent, which means as it would
                // have been anyway, not off. The same rule the document keeps about a missing field.
                if (coin === undefined || coin.visible === false) {
                    continue;
                }

                if (Math.abs(coin.transform.x - hero.transform.x) < 16 && Math.abs(coin.transform.y - hero.transform.y) < 16) {
                    coin.visible = false;
                }
            }
        });
    }, [
        {
            key: 'speed',
            label: 'Speed (px/s)',
            type: 'number',
            default: 80,
            min: 0,
            step: 10
        }
    ]);
};
```

`settings.ts`:

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

export const WIDTH = 360;
export const HEIGHT = 200;
export const FONT = {
    json: '/example-assets/fonts/bitmaps/nacatamal-arcade/nacatamal-arcade.json',
    atlas: '/example-assets/fonts/bitmaps/nacatamal-arcade/nacatamal-arcade.png'
};
export const FLOOR = getColor('#1d2b4d');
export const HERO = getColor('#5bd75b');
export const COIN = getColor('#e5c93b');
export const PAPER = getColor('#f2f6fa');
export const DIM = getColor('#8fa4c4');
```
