# 08 — Avatar (Navi) & Spectrum Customization

Reverse-engineering recon of the player avatar ("Navi") and its cosmetic
customization via **Spectrum Data**. Source: `extracted/client-5-8/` (client)
and `extracted/server-5-4/` (Master). All file:line citations are to those
trees. Findings are evidence-based; guesses are flagged **[GUESS]**.

> **Bottom line on depth:** Customization is **shallow but layered**. There is
> exactly **one body silhouette** (the base `NaviStandD…` sprite family). All
> "spectrum" customization is **palette swapping of 5 named body regions**, each
> a separate pre-rendered sprite overlay. No head/body part swaps, no model
> variety, no name-color customization. The only genuinely distinct avatars are
> mod-only states (joker shell) and transient effect sprites (TeleIn/HexportIn).

---

## 1. Spectrum Data model & cosmetic dimensions

### 1.1 The 5 cosmetic "sets" (regions)

A Navi's appearance is decomposed into **5 part-sets**, each independently
colorable. Canonical list (from script header comments and the spectrum
pickup message):

| Set | Region name | "A …part" label shown on pickup |
|-----|-------------|----------------------------------|
| 1 | Body / helmet | `body/helmet` |
| 2 | Gloves / boots | `glove/boots` |
| 3 | Trim | `trim` |
| 4 | Stripes | `stripe` |
| 5 | Core circle | `core circle` |

Evidence:
- `scripts/0313-set_navi_colors.gml:2-9` (header listing sets 1-5).
- `objects/0418-d_spectrum/events/KeyPress-35.gml:15-19` (`setstr` mapping).

### 1.2 Color palettes per set (master option table)

The master list of available colors per set is hard-coded at title init:
`objects/0034-title/events/Other-2.gml:48-89`.

| Set | Count `ncol_options[0,s]` | Color codes |
|-----|------|-------------|
| 1 Body/helmet | 7 | DB, DR, BR, K, W, AU, PH |
| 2 Gloves/boots | 8 | B, R, K, W, AU, AG, PH, CR |
| 3 Trim | 5 | Y, AG, R, LB, PH |
| 4 Stripes | 5 | LB, O, W, LY, R |
| 5 Core circle | 7 | R, C, G, X, DY, W, PH |

Total distinct palette options across all sets = **32**. Combinatorial
appearance space = 7·8·5·5·7 = **9,800** color combinations — but every one is
the same silhouette, just recolored.

Color codes are short letter mnemonics (`DB`=dark blue, `LB`=light blue,
`AU`=gold/aurum, `AG`=silver, `PH`=?[GUESS: a special/"phantom" palette],
`CR`=?[GUESS: chrome], etc.). The code is **not** an RGB value — it is a suffix
that selects a **pre-baked recolored sprite** (see §2.2).

### 1.3 How a spectrum cosmetic is STORED

Owned spectrums live in **inventory category 5** as string items of the form
`"<set>|<color>"` where `color` is the **index** into that set's option array
(NOT the letter code).

- Stored form: `iv_additem(5, string(set)+"|"+string(color))`
  — `objects/0418-d_spectrum/events/KeyPress-35.gml:21`.
- Ownership test: `iv_itemsearch(5, string(set)+"|"+string(color)) > 0`
  — `objects/0418-d_spectrum/events/Create.gml:19`,
  `objects/0419-swappanel_draw/events/Create.gml:14`.
- The inventory db `global.iv_itemdb[5,…]` is **server-sent at login**
  (`scripts/0094-begin_client_receive.gml:69-77`, case 8) and on the server
  side persisted per-UID via `uinv_get` (`server-5-4/.../0359-server_receive.gml:215`).
  So *ownership* of spectrums is server-authoritative; *which you equip* is not
  (see §4).

The default body color set (granted to everyone, even without the Mask Plugin)
is `DB / B / Y / LB / R` — `objects/0034-title/events/Other-2.gml:41-45` and
re-asserted in `scripts/0094-begin_client_receive.gml:87-94`.

### 1.4 How a spectrum is ACQUIRED — `d_spectrum` (object 0418)

`d_spectrum` is a physical collectible placed in rooms. Each placed instance
carries room-instance creation variables `set`, `color`, and optional
`passkey` (these are per-instance, not object defaults — object meta has no
defaults: `objects/0418-d_spectrum/meta.json`).

Pickup flow (`objects/0418-d_spectrum/events/KeyPress-35.gml`):
1. Requires you to own at least one cat-5 item already
   (`global.iv_itemdb[5,0] > 0`, line 8) — i.e. you must have the system unlocked.
2. Requires standing adjacent and **facing** the data with the correct
   directional collision rectangle (lines 8-11).
3. Optional `passkey` gate: if the instance has a `passkey`, your typed
   `keyboard_string` must equal it (line 13), else
   `"This data is locked, and requires a passkey."` (line 29).
4. On success: prints
   `"Spectrum data found! <name> (<set>) ~ A <region> part."` in `c_getdata`
   green (line 20), adds `iv_additem(5,"set|color")` (line 21), marks `got=1`,
   swaps its own sprite to `DataBW` (greyed-out "already collected") and
   disables collision (lines 23-25).
5. Already-owned data renders pre-greyed via the same `DataBW` swap in
   `Create.gml:19-24`.

`<name>` in the message is `global.ncol_options[set,color]` — i.e. the message
prints the **color letter code**, not a human name (e.g. "AU").

The `d_spectrum` object also doubles as a **pushable/moving block** in some
rooms (`mover`, `pushnavi`, mtile platform logic in `Step.gml:35-74` and
`Collision-0.gml`) — unrelated to cosmetics, it's reused as a physics prop.

### 1.5 The Spectral Gate — `swaptile` (object 0407)

`swaptile` is the **interactive customization terminal**. Gated by inventory
**category 1, key id `"3"`** = `MskPlugn` (Mask Plugin, "enables compatibility
with spectrum data" — `objects/0034-title/events/Other-2.gml:109-112`).

- Step event (`objects/0407-swaptile/events/Step.gml:5`): only shows the action
  notification if `iv_itemsearch(1,"3") > 0`.
- KeyRelease (Enter key 35, `events/KeyRelease-35.gml`): when you walk up and
  press Enter within 14px:
  - If you own `MskPlugn` (`iv_itemsearch(1,"3") > 0`, line 6): spawns the
    `swappanel_draw` customization panel (line 9), sets `global.mbout=1`, and
    freezes the avatar to a Stand sprite (lines 11-16).
  - Else: `"A navi expansion is necessary to interface with this protocol."`
    in red (line 21).

> Note: `ac_spectralgate` (object 0545) is **not** the customizer — it is just
> a zone-warp trigger: `change_area("Spectral Gate")`
> (`objects/0545-ac_spectralgate/events/Collision-0.gml`). The Mask-Plugin gate
> lives on `swaptile`.

### 1.6 The equip UI — `swappanel_draw` (object 0419)

A horizontal color-carousel. On open (`events/Create.gml`):
- Builds `col_options[set][…]` from only the colors you actually **own** in
  cat-5 (lines 9-20) — you can only equip spectrums you've collected.
- Pre-selects each set's carousel to your **currently equipped** color by
  matching `get_navi_cletter(s)` (lines 22-30).

Controls:
- ←/→ (`KeyPress-37`/`-39`) scroll color within the active set.
- ↑/↓ (`KeyPress-38`/`-40`) cycle the active set (`setsel`), in a fixed
  non-linear order 1→4→3→2→5 (`KeyPress-38.gml:5-22`).
- Draw event renders the live Navi preview by stacking `get_navi_front(set,
  color)` sprites and a UI background (`events/Draw.gml:9-48`).
- **Enter (`KeyPress-35`) = APPLY** — see §4.1. Insert (`KeyPress-45`) cancels.

---

## 2. Navi rendering

### 2.1 Base sprite family, directions, animations

The avatar has **8 facing directions × 2 motion states (Stand/Run)** = 16 base
sprites, named `Navi{Stand|Run}{D,DR,R,UR,U,UL,L,DL}`:

- `D`=down, `DR`=down-right, `R`=right, `UR`=up-right, `U`=up, `UL`=up-left,
  `L`=left, `DL`=down-left.
- The mapping sprite→(direction,motion) index 0-15 is the canonical table in
  `scripts/0314-spr2ncol.gml:5-20` (`spr2ncol`), inverse of the layout in
  `scripts/0313-set_navi_colors.gml:12-34`.

Movement & sprite selection happen in the **local player object `0000-server`**
(this object IS the local avatar):
- `Create.gml:22` `image_speed = 1`, `:23` default `global.curspeed = 3`.
- Per-direction keyboard events set `sprite_index` to the matching
  Run/Stand sprite and `fspeed` (e.g. `events/Keyboard-39.gml` for right;
  holding **Alt** forces a Stand sprite + `fspeed=0`, lines 15-24).

### 2.2 The 5-layer paint model (recolor, not swap)

The avatar is **not** a single colored sprite. It is rendered as a base
silhouette plus **5 transparent overlay sprites** (one per set), each chosen by
`(set, current-color)` and indexed by the same 0-15 direction/motion frame.

Local player draw (`objects/0000-server/events/Draw.gml`):
```
draw_sprite(sprite_index,-1,x,y);              // base silhouette (line 9)
ncval = spr2ncol(sprite_index);                // 0-15 dir/anim index (line 14)
draw_sprite(global.ncol[4,ncval],-1,x,y);      // set 4 (stripes) first (line 15)
for nc in 1..5 where nc!=4:                     // then 1,2,3,5 (lines 16-19)
   draw_sprite(global.ncol[nc,ncval],-1,x,y);
```
`global.ncol[set, 0..15]` holds the 16 directional sprite indices for that set's
currently-equipped color, populated by `set_navi_colors(set, colorletter)`
(`scripts/0313-set_navi_colors.gml`), which does
`global.ncol[set,frame] = NaviStandD_S<set>_<color>` (etc., via `execute_string`).

So a color is applied by **selecting a different pre-baked sprite asset** whose
name encodes the region+color, e.g. `NaviStandD_S1_DB`, `NaviStandD_S5_PH`.
There is **no runtime palette/shader blending** — each (region × color × frame)
is its own sprite in `sprites/` (confirmed: `NaviStandD_S1_*` = 7 sprites
matching the 7 body colors; `NaviStandD_S5_*` = 7; etc.).

**What gets swapped vs recolored:** The base silhouette (`NaviStandD…`) is
**always the same** — it is the only Navi body that exists. Only the 5 overlay
layers change, and they only change **color**, never shape. There is **one**
avatar mesh/silhouette in the entire game.

### 2.3 Speed tiers (walk / fast / joker)

`global.curspeed` drives run speed and animation speed:

| Tier | curspeed | image_speed | Trigger |
|------|----------|-------------|---------|
| Walk / normal | **3** | 0.3 | default (`0000-server/Create.gml:23`) |
| Fast | **5** | 0.5 | Ctrl+R toggle (`KeyPress-82.gml:5-13`) / `-` key (`KeyPress-45.gml`) |
| Joker shell | **7** | — | Ctrl+J, **gated by mod item `"909"` Joker Shell** (`KeyPress-74` dnd) |

- Fast toggle: `objects/0000-server/events/KeyPress-82.gml:5-13`.
- Joker: `objects/0000-server/events/KeyPress-74` (dnd json) requires
  `iv_itemsearch(1,"909") > 0` and `vk_control`; sets `curspeed=7`, spawns the
  `jokershell` object (0481) as a separate avatar, hides the normal Navi. On
  exit, `jokershell` Alarm resets `curspeed=5` (`objects/0481-jokershell/events/Alarm.gml:15`).
- Transient effect sprites cap speed: arriving via TeleIn/JoinIn clamps
  `curspeed` to ≤5 (`0000-server/events/Other-7.gml:4`).

Speed tier is a movement attribute, **not** a cosmetic and **not** part of the
spectrum system.

---

## 3. Name / nameplate & adornments

- **Nameplate**: drawn as plain text centered above the avatar at `y-16`.
  - Local: `objects/0000-server/events/Draw.gml:12`
    `draw_text(x+(sprite_width/2)-(string_width(global.playername)/2), y-16, global.playername)`,
    font `fixedsys`, `font_color = c_aqua`.
  - Remote: `objects/0042-player/events/Draw.gml:17` — same geometry, uses
    `global.p_name[pid]`, `font_color = c_white`.
- **Name color is NOT customizable.** It is a hard-coded constant per object
  (aqua for self, white for others). No per-player name-color field exists in
  the wire protocol or inventory.
- Nameplate is **suppressed** during transient effect sprites
  (TeleIn/Out, JoinIn/Out, HexportIn/Out/Hexport) and while in joker shell
  (`Draw.gml:10-12`).
- **No titles / status badges / rank adornments** are drawn on or near the
  avatar. Moderator/mod state manifests only as inventory `<M:…>` items and the
  joker shell, not as a visible nameplate adornment. The `cusername` object
  (0040) just draws `"> User Name: <name>"` as a HUD label, unrelated to the
  in-world nameplate.

---

## 4. Wire protocol & authority

### 4.1 Which msgid carries appearance — **msgid 2**

Appearance is broadcast as **UDP message id 2** ("Player's sprite index").
Sent on apply (`objects/0419-swappanel_draw/events/KeyPress-35.gml:9-14`):
```
writebyte(2);
writeint(NaviSplash);                  // sprite_index field
for nc in 1..5: writestring(get_navi_cletter(nc));   // 5 color letter codes
sendmessage(global.s_udpsocket, ...);
```
The protocol header documents msgid 2 as `[UDP] Player's sprite index`
(`scripts/0094-begin_client_receive.gml:11`).

**Payload layout of msgid 2:** `byte msgid=2`, `int sprite_index`,
`string×5 colorLetters[set1..set5]`. (The receiving server prepends the sender
`pid` itself; UDP source identifies the player.)

`NaviSplash` (sprite 0899) is a **transient sparkle/flash animation** sent as
the sprite_index whenever colors change, so peers play a `pcolchange` effect and
then snap back to `NaviStandD`:
- Receiver special-case: `objects/0042-player/events/Step.gml:102-112` — on
  `NaviSplash`, spawn `pcolchange`, then reset `p_spr[pid,0]=NaviStandD`.
- So the int field of msgid 2 is normally the player's **current base motion
  sprite**; on a wardrobe change it's specifically `NaviSplash` to cue the
  effect. The 5 strings are the actual persistent appearance.

### 4.2 Receiving a peer's appearance

Client receive (`scripts/0097-client_receive.gml`):
- **case 2** (lines 90-102): reads `pid`, `p_spr[pid,0]=readint()` (sprite),
  then 5 strings; for each changed letter calls
  `set_user_colors(pid, letter, set)` to rebuild that peer's `us<set>col[pid,…]`
  16-frame sprite array (`scripts/0322-set_user_colors.gml`).
- **case 11** (lines 146-167): room-population batch — same `int sprite + 5
  strings` per player, applied identically. This is how appearance is synced on
  room entry.

Remote draw stacks the peer's layers exactly like the local one
(`objects/0042-player/events/Draw.gml:10-15`), set 4 first then 1,2,3,5, guarded
by `sprite_exists` so missing colors silently skip.

### 4.3 Authority — appearance is **fully client-authoritative (NOT validated)**

The Master stores and rebroadcasts whatever the client sends, with **zero
validation**:

`server-5-4/scripts/0359-server_receive.gml:122-134` (case 2):
```
global.p_spr[upid,0] = readint();          // raw int, stored as-is
for tempint in 1..5:
   global.p_spr[upid,tempint] = readstring();   // raw strings, stored as-is
allupdate[2] = 1;                          // flag for rebroadcast
```
No check that the color is a legal `ncol_options` value, no check that the
sending player **owns** that spectrum in inventory, no check that the int is a
real Navi sprite. The server then fans the stored `p_spr` out to other clients
(`server-5-4/.../0359-server_receive.gml:97-100` and `:577-580` in the
room/join broadcast paths).

**Implication for REBNO:** appearance is a presentation/cosmetic field the
client owns end-to-end. The only server-side gating is on **ownership of
spectrum items** (cat-5 inventory is server-persisted and sent at login,
§1.3), but that gating is **never enforced at appearance-broadcast time** — a
modified client could equip and broadcast any color (or any sprite int) it
likes. Matches the project's expectation: appearance = client-authoritative,
not server-validated.

> Anti-cheat note: because the int field is an arbitrary `readint()` echoed to
> peers, a malicious client could broadcast a non-Navi or out-of-room sprite
> index. Legacy clients guard at draw time with `sprite_exists()` and the
> TeleIn/Splash special-cases, but there is no server-side allowlist. Flag for
> REBNO hardening if appearance is ever made server-authoritative.

---

## 5. Summary table — customization surface

| Dimension | Customizable? | Mechanism | Server-validated? |
|-----------|---------------|-----------|-------------------|
| Body silhouette / model | **No** — one mesh only | n/a | n/a |
| Body/helmet color (set 1) | Yes, 7 options | spectrum recolor overlay | ownership only, not on broadcast |
| Gloves/boots color (set 2) | Yes, 8 | recolor overlay | ownership only |
| Trim color (set 3) | Yes, 5 | recolor overlay | ownership only |
| Stripes color (set 4) | Yes, 5 | recolor overlay | ownership only |
| Core-circle color (set 5) | Yes, 7 | recolor overlay | ownership only |
| Name text | Yes (login) | `global.playername` | server stores name |
| Name color | **No** | hard-coded aqua/white | n/a |
| Titles / badges | **No** | none exist | n/a |
| Speed tier (walk/fast/joker) | Yes (joker mod-gated) | `global.curspeed` 3/5/7 | not a cosmetic; movement only |

---

## Key file:line evidence index

- Sets 1-5 definition: `scripts/0313-set_navi_colors.gml:2-9`
- Master color tables: `objects/0034-title/events/Other-2.gml:48-89`
- Default colors DB/B/Y/LB/R: `objects/0034-title/events/Other-2.gml:41-45`
- Spectrum stored as `set|color` in cat-5: `objects/0418-d_spectrum/events/KeyPress-35.gml:21`
- Pickup gating + passkey: `objects/0418-d_spectrum/events/KeyPress-35.gml:8-30`
- Spectral Gate terminal `swaptile`, MskPlugn (cat1 "3") gate:
  `objects/0407-swaptile/events/KeyRelease-35.gml:6-21`
- Equip panel build (own-only): `objects/0419-swappanel_draw/events/Create.gml:9-30`
- Apply + send msgid 2: `objects/0419-swappanel_draw/events/KeyPress-35.gml:5-18`
- 5-layer local draw: `objects/0000-server/events/Draw.gml:9-19`
- 5-layer remote draw: `objects/0042-player/events/Draw.gml:10-17`
- Direction/anim index table: `scripts/0314-spr2ncol.gml`
- Speed tiers: `objects/0000-server/events/{Create.gml:23, KeyPress-82.gml, KeyPress-74}`
- msgid 2 receive (client): `scripts/0097-client_receive.gml:90-102`, batch `:146-167`
- msgid 2 receive (server, no validation): `server-5-4/scripts/0359-server_receive.gml:122-134`
- NaviSplash transient flash: `objects/0042-player/events/Step.gml:102-112`
