> For the complete documentation index, see [llms.txt](https://sbscripts.gitbook.io/sb-scripts/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://sbscripts.gitbook.io/sb-scripts/sb-minigames/custom-games.md).

# Custom Games

### Adding a game to the UI

The UI lives in `ui/app.js`. Each game is an entry in the `GAMES` table:

```js
GAMES.example = {
    title: 'My game', width: 24,
    build(g, o) {
        // g is the game context, o is the options sent from Lua
        const btn = h('button', { text: 'Click me', on: { click: () => g.finish(true) } });
        g.body.append(btn);
        g.hint('Press ', kbd('E'), ' or click');
        g.key(e => { if (e.key.toLowerCase() === 'e') g.finish(true); });
        g.timer(o.time);
    },
};
```

Use the context helpers instead of raw timers and listeners. They clean up when the game ends.

| Helper                                  | Use                                                                                            |
| --------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `g.body`                                | The element you build the game in.                                                             |
| `g.key(fn)`                             | Keydown handler. Key repeat is ignored. Pass `true` as the second argument to receive repeats. |
| `g.on(target, type, fn)`                | Event listener, removed at the end.                                                            |
| `g.after(ms, fn)` and `g.every(ms, fn)` | Timers that stop when the game ends.                                                           |
| `g.loop((dt, ts) => {})`                | Animation loop. `dt` is in seconds.                                                            |
| `g.timer(seconds, onExpire)`            | Shows the time bar. Fails the game on expiry, unless you pass your own handler.                |
| `g.stat(text)` and `g.hint(...nodes)`   | Header status text and footer hint.                                                            |
| `g.finish(success, delay)`              | Ends the game and shows the result.                                                            |

### Adding the Lua export

Copy this into `client/main.lua`, and add a matching table in `shared/config.lua`:

```lua
exports('Example', function(a1, a2)
    if gameOpen then return false end
    local opts, cb
    if IsCallable(a1) then opts, cb = {}, a1
    elseif type(a1) == 'table' then opts, cb = a1, a2
    else opts, cb = { time = a1 }, a2 end
    local o = {}
    o.time = Num(opts.time, Default('ExampleDefaults', 'time', 15), 0, 60, true)
    return Launch('example', o, cb)
end)
```

The action name passed to `Launch` is the lowercase key in `GAMES`.

### Recipes

**Lock a door behind a pick**

```lua
local function TryLockpick(door)
    if exports['sbminigames']:IsOpen() then return end
    exports['sbminigames']:Lockpick({ pins = 4, picks = 3, time = 40 }, function(success)
        if success then
            TriggerServerEvent('myresource:unlockDoor', door)
        else
            print('The pick snapped.')
        end
    end)
end
```

**Run games one after another**

```lua
CreateThread(function()
    if not exports['sbminigames']:Keypad({ length = 5 }) then return end
    if not exports['sbminigames']:Wires({ pairs = 4 }) then return end
    if not exports['sbminigames']:Scrambler({ length = 6 }) then return end
    print('Hacked')
end)
```

**Harder with every attempt**

```lua
local attempt = 0

local function Hack()
    attempt += 1
    exports['sbminigames']:Circle({
        difficulty = attempt > 2 and 'expert' or 'medium',
        circles    = math.min(attempt + 1, 6),
    }, function(success)
        if success then attempt = 0 end
    end)
end
```

### Keep rewards on the server

A minigame result is decided on the player's machine, so a cheater can fake `true`. Never give a reward just because a client event says the player won. Check on the server that the player is near the target, has the required item, and hasn't triggered the reward recently. Keep the minigame as the player-facing part only.
