> For the complete documentation index, see [llms.txt](https://mole-resource.gitbook.io/mole-resource-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mole-resource.gitbook.io/mole-resource-docs/scripts/tic-tac-toe/configuration.md).

# Configuration

All options are in [`config.lua`](broken://pages/7d32272a584b02b6a9671858417e50ab3af0d195), except the model names and orientation fix, which live at the top of [`client.lua`](broken://pages/59e8ff5a2605575596e03ef25fb48a3080802d30) since they're client-only streamed objects.

## Framework & item

```lua
Config.Framework = "ox"        -- "ox" | "qb" | "esx"
Config.ItemName  = "tictactoe" -- inventory item that places a board
```

Under `ox`, using the item is wired through the item's own `client.event` in `ox_inventory/data/items.lua` — see [Installation](broken://pages/f6c370fb99da79a08f9f7b0058cd60587b184743). Under `qb` / `esx` the script registers the usable item automatically.

## Interaction

```lua
Config.Target = "ox_target"    -- "none" | "ox_target" | "sleepless_interact"

Config.Controls = {
    Enter  = 38, -- E — play (PvP)
    Pickup = 47, -- G — pick up the board
    Solo   = 74, -- H — play vs AI
}
```

`Config.Target = "none"` falls back to on-foot proximity prompts using the keys above. With a target system, the same actions appear as target options and the keys are unused.

## Synchronization

```lua
Config.SyncDelay = 20000 -- ms after a player spawns before their boards are built
Config.Live      = true  -- true = nearby spectators see marks appear in real time
```

> Seated players always see moves immediately regardless of `Config.Live`; the flag only controls whether nearby **spectators** (players who never entered the game) get live updates.

`SyncDelay` exists so a player who just joined isn't hit with object-creation work while the game itself is still streaming the world in — boards are held back until it runs out, then requested from the server. Raise it on heavy servers where the world takes longer to load.

## Streaming

```lua
Config.Streaming = {
    Distance   = 150.0, -- boards within this radius (metres) are spawned
    Hysteresis = 25.0,  -- how much further you must walk before one despawns
    Interval   = 500,   -- how often the distance check runs, in ms
    Budget     = 2,     -- max boards spawned per pass, spreads the cost over several frames
}
```

Boards farther than `Distance` from the player are never spawned as game objects — only their grid state is kept in memory. Each spawned board costs 1 object plus up to 9 mark objects, so without this a server with a few hundred placed boards can exhaust the client's object pool.

* A board spawns once you're within `Distance`.
* It only despawns once you're **past `Distance + Hysteresis`** (175m by default) — the gap between the two prevents it flickering in and out while you stand near the edge.
* `Budget` caps how many boards can be built in a single pass, spreading the cost over several frames instead of stalling on a teleport into a dense area; the streamer automatically retries every 50ms until it catches up, then settles back to the normal `Interval`.
* A board you're actively playing on is never despawned by distance, however far the camera drifts.

To watch this happen, set `Config.Debug = true` and check the client console (F8) while walking away from a board — it prints `Spawned board #id (Xm away)` / `Despawned board #id (Xm away)`. For a quick test, temporarily lower `Distance`/`Hysteresis` (e.g. `15.0` / `5.0`) so the whole zone fits in a few steps, then restore them afterward.

## Timeouts

```lua
Config.EntryTimeout = 15 -- seconds the server waits for a client to confirm it entered before freeing the seat
Config.TurnTimeout   = 30 -- seconds a player has to make a move; set to 0 or nil to disable
```

`EntryTimeout` guards against a client that fails to build the board after being granted a seat (e.g. a broken model) — the seat is freed automatically so the board doesn't stay stuck forever.

`TurnTimeout` only counts down while the player on turn has their **camera locked** (Space) — i.e. while they can actually click a cell. Freely orbiting the camera with it unlocked doesn't burn their move time. Once the limit is exceeded the turn is skipped to the opponent and both players are notified.

## Gameplay & placement

```lua
Config.Distance        = 2.0                    -- interaction distance (proximity/target)
Config.MarkZOffset     = 0.01                   -- lifts marks above the cell surface
Config.Rotation        = vector3(0.0, 0.0, 0.0) -- default placement-preview rotation
Config.CellClickRadius = 0.10                   -- screen-space radius for clicking a cell
Config.MarkOffset      = vector3(0.0, 0.0, 0.0) -- fine XYZ correction of marks (board-local axes)
```

Tune `MarkZOffset` and `MarkOffset` live with `/tictacbones x` (or `o`) while `Config.Debug = true` — it prints a ready-to-paste `Config.MarkOffset` value. `/tictaccoords` prints the raw world coordinates of a nearby board's `point1..point9` bones, useful for checking the model is evenly spaced.

## Camera

Pitch is the angle above the board: `0` = level with the board, `90` = straight top-down.

```lua
Config.Camera = {
    StartPitch    = 55.0,
    MinPitch      = 5.0,
    MaxPitch      = 89.0,
    MinDistance   = 0.5,
    MaxDistance   = 2.0,
    StartDistance = 1.0,
    Height        = 0.2,
}
```

## Singleplayer (AI)

```lua
Config.AI = {
    Enabled               = true,
    Mark                  = "O",    -- AI's mark; the human gets the other one (X always moves first)
    Difficulty            = "hard", -- "easy" | "medium" | "hard" — used when AllowDifficultyChoice is false, or as a fallback
    MoveDelay             = 600,    -- ms the AI "thinks" before playing
    AllowDifficultyChoice = true,   -- false = players can't pick a difficulty, always uses Difficulty above
}
```

* `easy` — plays a random empty cell.
* `medium` — 50% random, 50% optimal.
* `hard` — full minimax, unbeatable (best case for the human is a draw).

Solo is entered with **H** (`Config.Controls.Solo`) or the **Play vs AI** target option, and requires a completely free board.

### Letting players pick the difficulty

When `AllowDifficultyChoice` is `true` (default), the player controls the AI's strength themselves — no admin action needed. Once inside a solo game, **as long as the board is still empty** (no mark placed by either side yet), the same instructional-buttons bar used for "Lock camera" / "Place mark" / "Exit" / "Hold to reset" gets an extra entry:

```
Lock camera | Place mark | Exit | Hold to reset | 1: AI difficulty: Hard
```

Pressing **1** cycles Easy → Medium → Hard → Easy and updates live; the chosen value is validated and applied server-side immediately (`mr_tictactoe:setDifficulty`), so it affects the AI's very next move. As soon as the first mark is placed (by either the player or the AI), that entry disappears and the difficulty is locked in for the rest of the match — it cannot be changed mid-game. This control is hardcoded to the `1` key (control ID 157) and isn't configurable via `Config.Controls`.

Set `AllowDifficultyChoice = false` to remove this entirely — the bar entry never appears, the server rejects any `setDifficulty` attempt, and every solo game always uses the fixed `Config.AI.Difficulty`.

## Static public boards

Boards defined here are recreated on every restart, cannot be picked up, and are not stored in the database.

```lua
Config.Boards = {
    { x = -1465.30, y = -546.51, z = 72.65, rotation = 32.0 },
}
```

## Notifications

```lua
Config.Notify = "ox_lib" -- "ESX" | "ox_lib" | "mythic_notify" | "QB-Core"
```

## Discord logging

```lua
Config.Discord = {
    Webhook = "",             -- webhook URL; empty string disables logging
    Name    = "mr_tictactoe",
    Logs = {
        place  = true, -- board placed
        pickup = true, -- board picked up
        admin  = true, -- tictacdelete used
    }
}
```

> 🔒 Never commit a real webhook. Keep it empty in the shipped config and set it only on your live server.

Log entries always include the player's license identifier (looked up across `license`, `license2`, and a full identifier scan as a last resort), plus their in-character first/last name when `Config.Framework` is `esx` or `qb` (falls back to their Rockstar/Steam name otherwise or under `ox`).

## Debug

```lua
Config.Debug = false -- enables /tictacitem, /tictacbones, /tictaccoords, hover-cell logging, and streaming spawn/despawn logs
```

Keep this `false` on production.

| Command               | Purpose                                                                                               |
| --------------------- | ----------------------------------------------------------------------------------------------------- |
| `/tictacresync`       | always available — re-requests board sync from the server                                             |
| `/tictacitem`         | gives a free `tictactoe` item                                                                         |
| `/tictacbones [x\|o]` | spawns a board, labels each `point1..point9` bone, and optionally live-calibrates `Config.MarkOffset` |
| `/tictaccoords`       | prints the world coordinates of a nearby board's bones                                                |

Server console only: `tictactoeboards` (list boards), `tictacdelete <id> [playerId]` (force-remove a board).

## Model names & orientation (client.lua)

```lua
Config.Base = "mr_tictactoe_board" -- board model, must contain bones point1..point9

Config.Marks = {
    ["X"] = "mr_tictactoe_xmark",
    ["O"] = "mr_tictactoe_omark",
}

-- Orientation fix applied on top of the board's own yaw (pitch, roll, yaw).
-- Zero if the model is authored lying flat.
Config.BoardRotationFix = vector3(0.0, 0.0, 0.0)
```

## Sound

The mark-placement sound is not a `Config` option — it's a plain audio file the NUI page plays directly. Drop an `.ogg` file at `html/place.ogg` and list it in `fxmanifest.lua`'s `files{}`. Volume is set in `html/ui.js` (`sounds.place.volume`).

## Locale

Every player-facing string is in `Config.Locale`. Edit the values to translate; keep the keys unchanged.
