# React HUD

> nacatamalon/react: the game runs in <Game> and the HUD is React components over the canvas. Both sides read the same store.

Example `react-hud`, running at https://nacatamalon.com/en/examples/react-hud. The whole catalogue: https://nacatamalon.com/llms/examples.md.

The pet is a scene; the bars, the buttons and the flash are React components inside `<Game>`, from `nacatamalon/react`. Each side keeps its own rules. The scene's body runs **once**, so it reads the store with the engine's `useStore`, which calls a function when the value changes. A component **renders again**, so it reads it with `useGameStore`, which returns the value. **F** and **P** work from the keyboard too, and the FEED button and the F key go through the same signal.

Watch the render counters: the bars select a value that changes every frame and render every frame; the mood selects `hunger > 30` and only renders when the answer flips. Pick values, not objects: the state is changed in place, so `(s) => s.items` never looks changed and `(s) => s.items.length` does.

**Browser only, for now.** The native (desktop) runtime shows only what the engine draws, so a React HUD is not on screen there. If your game will ship natively, build the HUD with the engine: `createText`, sprites and `createNineSlice`.

## Code

`index.tsx`:

```tsx
import { createRoot } from 'react-dom/client';
import { Game } from 'nacatamalon/react';
import { createPetScene } from './scenes/pet';
import { createPetModel } from './model';
import { clearGameStores } from 'nacatamalon/authoring';
import { WIDTH, HEIGHT } from './settings';
import { Hud } from './hud/hud';

// Re-running this editable project creates a fresh store with the same key.
clearGameStores();

const model = createPetModel();
const Pet = createPetScene(model);

// The game runs in <Game>; whatever goes inside it is React, drawn over the canvas.
const App = () => (
    <Game
        options={{
            width: WIDTH,
            height: HEIGHT,
            background: '#14121c'
        }}
        scenes={{
            Pet
        }}
        onReady={(game) => console.log('[react-hud] ready on', game.getBackend())}
    >
        <Hud model={model} />
    </Game>
);

createRoot(document.querySelector('#app')!).render(<App />);
```

`hud/controls.tsx`:

```tsx
import type { TPetModel } from '../model';
import { useState } from 'react';
import { useGame } from 'nacatamalon/react';

const BUTTON = 'rounded bg-white/15 px-1.5 py-0.5 text-[8px] font-mono uppercase hover:bg-white/25 disabled:opacity-40';

export const Controls = ({ model: { petStore, feed } }: { model: TPetModel ;}) => {
    // The same handle a scene gets from useGame: null for a moment, while the renderer starts.
    const game = useGame();
    const [paused, setPaused] = useState(false);

    const togglePause = () => {
        if (game === null) {
            return;
        }

        if (game.isPaused()) {
            game.resume('menu');
        } else {
            game.pause('menu');
        }

        setPaused(game.isPaused());
    };

    return (
        <div className="flex gap-1">
            <button className={BUTTON} onClick={feed}>Feed</button>
            <button className={BUTTON} onClick={petStore.actions.play}>Play</button>
            <button className={BUTTON} onClick={petStore.reset}>Reset</button>
            <button className={BUTTON} disabled={game === null} onClick={togglePause}>{paused ? 'Resume' : 'Pause'}</button>
        </div>
    );
};
```

`hud/hud.tsx`:

```tsx
import type { TPetModel } from '../model';
import { useState } from 'react';
import { useSignal } from 'nacatamalon/react';
import { Bars, Mood } from './stats';
import { Controls } from './controls';

// Listens to a signal: the same useSignal a scene has, from the other side of the canvas.
const FedFlash = ({ model: { petFed } }: { model: TPetModel ;}) => {
    const [flash, setFlash] = useState<number | null>(null);

    useSignal(petFed, (times) => {
        setFlash(times);
        setTimeout(() => setFlash((current) => (current === times ? null : current)), 600);
    });

    if (flash === null) {
        return null;
    }

    return <div className="absolute left-1/2 top-1/4 -translate-x-1/2 text-xs font-bold text-yellow-300">+15 ({flash})</div>;
};

// Drawn over the canvas: place things with position absolute.
export const Hud = ({ model }: { model: TPetModel ;}) => (
    <>
        <div className="absolute inset-x-0 top-0 flex justify-between p-1.5 text-white">
            <Bars model={model} />
            <Mood model={model} />
        </div>
        <FedFlash model={model} />
        <div className="absolute inset-x-0 bottom-0 flex justify-center p-1.5 text-white">
            <Controls model={model} />
        </div>
    </>
);
```

`hud/stats.tsx`:

```tsx
import type { TPetModel } from '../model';
import { useRef } from 'react';
import { useGameStore } from 'nacatamalon/react';

// Counts a component's renders, to see which ones a store change reaches.
const useRenders = () => {
    const renders = useRef(0);

    renders.current++;

    return renders.current;
};

const Bar = ({ label, value }: { label: string; value: number; }) => (
    <div className="flex items-center gap-1">
        <span className="w-10 text-[8px] font-mono uppercase">{label}</span>
        <div className="h-1.5 w-20 rounded bg-white/15">
            <div className={'h-full rounded ' + (value > 30 ? 'bg-green-400' : 'bg-red-500')} style={{
                width: value + '%'
            }} />
        </div>
    </div>
);

// Selects values that change every frame, so it renders every frame.
export const Bars = ({ model: { petStore } }: { model: TPetModel ;}) => {
    const hunger = useGameStore(petStore, (s) => s.hunger);
    const happy = useGameStore(petStore, (s) => s.happy);
    const renders = useRenders();

    return (
        <div className="flex flex-col gap-0.5">
            <Bar label="Hunger" value={hunger} />
            <Bar label="Happy" value={happy} />
            <span className="text-[8px] font-mono opacity-60">renders: {renders}</span>
        </div>
    );
};

// Selects "is it content?", so it renders only when the answer flips.
export const Mood = ({ model: { petStore } }: { model: TPetModel ;}) => {
    const content = useGameStore(petStore, (s) => s.hunger > 30 && s.happy > 30);
    const timesFed = useGameStore(petStore, (s) => s.timesFed);
    const renders = useRenders();

    return (
        <div className="text-right text-[8px] font-mono">
            <div className={content ? 'text-green-400' : 'text-red-400'}>{content ? 'CONTENT' : 'UNHAPPY'}</div>
            <div>FED {timesFed}</div>
            <div className="opacity-60">renders: {renders}</div>
        </div>
    );
};
```

`model.ts`:

```ts
import { createGameSignal } from 'nacatamalon';
import { createPetStore } from './stores/pet';

export const createPetModel = () => {
    const petStore = createPetStore();
    const petFed = createGameSignal<number>();

    // Fed from a key in the game or from a button in the HUD: both go through here.
    const feed = () => {
        petStore.actions.feed();
        petFed.emit(petStore.state.timesFed);
    };

    return {
        petStore,
        petFed,
        feed
    };
};
export type TPetModel = ReturnType<typeof createPetModel>;
```

`scenes/pet.ts`:

```ts
import type { TPetModel } from '../model';
import { createScene, createSprite, getColor, useKeyboard, useStore, useUpdate } from 'nacatamalon';

const BODY = getColor('#8ab4f8');
const SAD = getColor('#e5533b');
const isContent = (s: TPetModel['petStore']['state']) => s.hunger > 30 && s.happy > 30;

export const createPetScene = ({ petStore, feed }: TPetModel) => () => {
    const keys = useKeyboard();

    const pet = createSprite({
        tint: isContent(petStore.state) ? BODY : SAD,
        width: 64,
        height: 64,
        transform: {
            x: 170,
            y: 120
        }
    });

    // The engine's useStore: a handler, because nothing here renders again.
    useStore(petStore, isContent, (content) => {
        pet.tint = content ? BODY : SAD;
    });

    let time = 0;

    useUpdate((delta) => {
        time += delta;
        pet.transform!.y = 120 + Math.sin(time * 3) * 4;
        petStore.actions.decay(delta);

        if (keys.justPressed('f')) {
            feed();
        }

        if (keys.justPressed('p')) {
            petStore.actions.play();
        }
    });

    return createScene();
};
```

`settings.ts`:

```ts
export const WIDTH = 340;
export const HEIGHT = 220;
```

`stores/pet.ts`:

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

export const createPetStore = () => createGameStore({
    key: 'nacatamalon-labs-react-pet',
    state: {
        hunger: 70,
        happy: 70,
        timesFed: 0
    },
    actions: (set) => ({
        feed: () => set((s) => {
            s.hunger = Math.min(100, s.hunger + 15);
            s.timesFed++;
        }),
        play: () => set((s) => {
            s.happy = Math.min(100, s.happy + 15);
        }),
        decay: (delta: number) => set((s) => {
            s.hunger = Math.max(0, s.hunger - delta * 3);
            s.happy = Math.max(0, s.happy - delta * 2);
        }),
    }),
});
```
