ProAim — Linux
==============

Files in this folder:

  client        the game client — also hosts a game when you want one
  data/         maps, weapons, sounds, scripts. Keep this next to the
                exe.

Open a terminal in this folder before running anything. In most file
managers: right-click inside the folder -> "Open Terminal Here".


Play with friends
-----------------

Two official servers run around the clock, and the in-game server
browser lists them the first time you open it — one competitive, one
casual. Pick a row, press Enter. Nothing to set up.

  proaim.alex-wilkinson.ca:27015    competitive
  proaim.alex-wilkinson.ca:27016    casual

To play on your own machine instead, the client hosts a real server
in-process — there is no separate server program to run:

  ./client --host-map data/maps/example_map/map_baked.glb

Everyone else joins that machine:

  ./client <host-ip>:27015 TheirName

The host's IP is their LAN address (e.g. 192.168.1.42) if you're on the
same network, or their public IP if you're playing over the internet.
For internet play, the host has to forward UDP port 27015 on their
router to their machine.

Quick sanity check: have the host first connect locally
(`./client 127.0.0.1:27015 HostName`). If that works and a friend can't
join, it's a network / firewall / port-forward problem, not a game
problem.

Hosting flags (all optional):

  --host-port 27016                  listen on a different port
  --host-bots 9                      fill the server with bots
  --host-map data/maps/foo/map_baked.glb   which map to host

A baked .glb hosts instantly and needs no Blender; a .blend is exported
first (see below).


Try your own Blender map
------------------------

If you've got Blender installed, the client can take a `.blend` file
directly: it exports it through Blender, runs an embedded server, and
auto-connects. One command, no separate server process.

Requirements: Blender 4.x or 5.x on your PATH. Test with
`blender --version` in a terminal — if it prints a version, you're set.

Map folder layout
~~~~~~~~~~~~~~~~~

Each map is ONE folder under `data/maps/`. The folder name is the
map's identity; files inside are generic:

  data/maps/<name>/
    map.blend           your source file (Save As here)
    map.glb             unbaked export (regenerated)
    map_baked.glb       baked export (regenerated, ship-quality)
    map_baked.png       lightmap atlas (regenerated)
    hdris/              probe cubemap JPEGs (regenerated)

You only ever maintain `map.blend`. Everything else is regenerated on
launch by the exporter + runtime IBL capture. The folder name is the
ONLY place the map's name appears on disk — no `<map>_baked.glb`
filenames, no `<map>_hdri_*.hdr` prefixes.

To start a new map: `mkdir data/maps/de_house/`, Save As your blend
into it as `map.blend`, launch.

The default command
~~~~~~~~~~~~~~~~~~~

  ./client --host-map data/maps/<name>/map.blend

Does everything end-to-end:
  1. Cycles lightmap bake → `map_baked.glb`
  2. HDRI probe auto-capture if any face JPEG is missing
  3. Embedded server boots on port 27015
  4. Auto-connect

First launch on a new map: full bake (~minutes) + HDRI capture
(~seconds). Subsequent launches with no .blend edit: instant cache hit
+ instant probe load.

Fast iteration variants (add ONE to the same command):

  --skip-bake       no lightmap bake (seconds, sun-only lighting)
  --fast-bake       1-sample Cycles preview (seconds, noisy)
  --no-bake-gpu     force lightmap bake on CPU (when GPU is busy)
  --bake-workers N  override auto worker count
  --force-export    ignore bake cache for one run
  --debug-ibl       spawn a chrome ball 1 m forward of the camera that
                    reflects the active IBL probe — sanity check for
                    probe orientation
  --capture-ibl     force a fresh probe capture pass (the auto-trigger
                    only fires when probe JPEGs are missing on disk)
  --capture-ibl-equirect
                    also stitch a 2048×1024 equirect preview JPEG per
                    probe (Blender world-env reference)

If you already exported a .glb, skip the Blender step entirely:

  ./client --host-map data/maps/<name>/map_baked.glb

Playtest against bots
~~~~~~~~~~~~~~~~~~~~~

Add `--host-bots N` to spawn N bot opponents alongside the embedded
server. Team size is split 50/50, so you end up in a real match
rather than a solo walkthrough:

  ./client --host-map data/maps/<name>/map.blend --skip-bake --host-bots 5

Common counts:

  --host-bots 1     1v1 duel
  --host-bots 3     2v2
  --host-bots 5     3v3
  --host-bots 9     5v5

Each bot is a separate background window with its audio muted, so the
only sound you hear is from your own client. Close your window and
the bots shut down with you.

Custom port for parallel test:

  --host-port 27016     override the embedded server's UDP port

Example map to learn from
~~~~~~~~~~~~~~~~~~~~~~~~~

  data/maps/example_map/map.blend

Ships with this distro — open in Blender to see every convention in
action: named spawn markers, bomb zones, bomb markers, kill zones,
the `sun_main` light, a sky dome, the no_col / invisible_col naming,
named HDRI probe markers (`hdri_default` plus `hdri_<easing>` zones),
ambience markers, and a Principled-BSDF material setup that exports
cleanly. The easiest way to start your own map is `Save As` off this
one — into your OWN map folder under `data/maps/your_name/`.

Bake modes:
  --skip-bake        skip the Cycles bake. Fast to launch. No baked
                     lighting: the map runs on sun + ambient only. Use
                     this while iterating on geometry, spawn points,
                     zones.
  --fast-bake        Cycles bake at 1 sample. Seconds-to-load, but the
                     lightmap is noisy. Use this for a cheap preview of
                     the baked look without paying the full render
                     time.
  (no flag)          Ship-quality bake (128 samples) using every CPU
                     core + GPU in parallel. Slow (minutes), but the map
                     ends up looking like the final thing. Use this
                     once geometry is settled or before sharing.

Bake cache: every successful export is recorded in
~/.cache/proaim/map_bakes.json (.blend absolute path → mtime + mode).
The cache lives outside the distro so it doesn't ship with the game and
isn't shared between machines. Re-running on an unchanged .blend at the
same mode is instant — the whole pipeline short-circuits. Edit-and-save
the .blend to invalidate that map; editing the export script invalidates
all maps. Pass --force-export to ignore the cache for one run.

HDRI probe captures (separate cache):
  The runtime captures each `hdri_*` probe marker's 6 cube faces from
  the live game framebuffer once the map is loaded. JPEGs land at
  `data/maps/<name>/hdris/<easing>_<face>.jpg` (one set per probe).
  These ARE separate from the bake cache:

   * On launch, if any expected probe face JPEG is missing, the
     client auto-runs a quick `--capture-ibl` pass before letting
     you play (you'll see HUD hidden and a few seconds of capture
     log lines, then it returns to gameplay).
   * To force a re-capture (e.g. after moving a probe marker or
     changing world lighting), `rm data/maps/<name>/hdris/*.jpg`
     then re-launch. The auto-trigger fires.
   * `--capture-ibl` flag forces a capture pass even when the JPEGs
     are present, and then EXITS the process (use this in batch /
     CI flows; the auto-trigger is the path for interactive play).
   * `--debug-ibl` spawns a chrome ball 1 m forward of the camera
     that reflects the current probe — useful for verifying probe
     placement and orientation without studying every shiny mesh.


Where to keep your .blend files
-------------------------------

Inside `data/maps/<your_name>/map.blend`. The exporter REQUIRES this
layout — it derives the map's identity from the .blend's parent
folder name, and refuses to run on a .blend outside `data/maps/<name>/`.

A workable distro layout:

  proaim/
    client, README.txt                   <- shipped binary
    data/
      maps/
        example_map/                     <- shipped example
          map.blend
          map_baked.glb (regen)
          hdris/        (regen)
        de_house/                        <- your map
          map.blend
          map_baked.glb (regen)
          hdris/        (regen)

Only the `.blend` is your source-of-truth. Everything else regenerates.

If you link external image textures in Blender, either pack them
(File -> External Data -> Pack Resources) or keep them on disk next to
the .blend so their paths resolve when the exporter runs.


Map authoring — naming conventions
----------------------------------

Spawns, zones, and markers are meshes with specific names that the
game picks up at load time:

  spawn_red_0, spawn_red_1, ...     red team spawn points
  spawn_blue_0, spawn_blue_1, ...   blue team spawn points
  bomb_zone_red / bomb_zone_blue    per-team bomb plant zone meshes.
                                    `bomb_zone_<color>` is the zone
                                    that `<color>` team DEFENDS — the
                                    attacker plants on the defender's
                                    zone, same as Ts planting at CT
                                    sites in CS. So bomb_zone_red
                                    belongs next to the red spawn
                                    (red defends it, blue attacks)
                                    and bomb_zone_blue belongs next
                                    to the blue spawn.
  bomb_marker_red / bomb_marker_blue
                                    author-time visual markers for the
                                    zones (hidden at runtime)
  kill_zone                         instant-death volume (any mesh
                                    named this)
  no_col*                           drawn but has no collision
                                    (decorative grass, rubble, etc.)
  invisible_col*                    collides but not drawn (e.g. a
                                    ceiling clip-box or knee wall)
  sky_dome*                         skybox, never collides or bakes
  sun_main                          the Sun light whose direction /
                                    color / energy the exporter bakes
                                    into the GLB
  ambience_<db>_<easing>_<stem>     looping ambient sound marker; AABB
                                    defines the zone. See "Ambient
                                    sounds" below.
  hdri_default                      REQUIRED — map-wide IBL fallback
                                    cubemap probe. Exactly one per map.
                                    Tiny cube placed anywhere reasonable
                                    (typical: 1 m cube at eye-height in
                                    open part of the map). Captured by
                                    the runtime; see "HDRI probes" below.
  hdri_<easing>                     optional per-zone IBL probe. AABB
                                    defines where its capture takes
                                    precedence over `hdri_default`.
                                    `<easing>` ∈ soft | medium | hard
                                    | instant — controls the spatial
                                    crossfade curve. Blender's `.001`
                                    duplicate suffix is preserved
                                    (so `hdri_soft` + `hdri_soft.001`
                                    are two separate probes).
  movement_grass_*                  grass preset: fast flutter (~2.4 Hz),
                                    whole-blade sway. See "Wind / foliage
                                    sway" below.
  movement_foliage_*                medium preset: ~1.3 Hz sway, sharper
                                    tip emphasis. Bushes, ivy, hedges.
  movement_tree_*                   tree preset: slow (~0.55 Hz), only the
                                    top portion moves. Trees, big canopy.
  material_<tag>_*                  surface-material tag — drives the
                                    per-material bullet-impact AND
                                    footstep sample banks. 13 supported
                                    tags; see "Surface materials" below.

Spawn position = the marker mesh's origin = player's feet in game. Set
the marker's Z to match the floor exactly — no floating, no falling-in.

Blender is Z-up, the game is Y-up. The exporter handles that: Blender Z
becomes game Y. `.blend` locations translate directly; you don't have
to think about it beyond "set Z to the floor height."

Marker meshes (spawns, bomb zones, bomb markers, kill zones, ambient
zones) should not contribute to lighting. Per marker, in Object
Properties -> Visibility -> Ray Visibility, uncheck everything except
Camera.


Ambient sounds
--------------

Drop looping environmental audio into a map by placing mesh cubes
named `ambience_<db>_<easing>_<stem>` in Blender. The cube's world
AABB defines the zone; the mesh itself is removed at load (no visible
cube in-game, no collision), and the ogg at
`data/sounds/ambience/<stem>.ogg` loops at the listener's current
distance-to-center weighting.

  <db>     signed int target RMS in dBFS, range [-60, 0]. The system
           normalizes the source ogg to hit this target regardless of
           how loud/quiet it was cut.
  <easing> soft    smoothstep; gradual, most natural
           medium  linear
           hard    quadratic (strong core, subtle edges)
           instant binary inside/outside (time-smoothed by 200 ms
                   crossfade, so no click)
           default this marker plays EVERYWHERE regardless of position
                   — exactly one per map, used as the fallback that
                   zones fade against

  <stem>   anything after the third underscore; may contain underscores
           ("indoor_kitchen_hum" is fine). Resolves to
           data/sounds/ambience/<stem>.ogg — missing file = fatal error.

Example: `ambience_-24_soft_indoor_room`  — a soft-falloff zone that
plays `indoor_room.ogg` at -24 dBFS when the listener is inside the
AABB. The cube's size in Blender is the zone size.

Crossfade between the default and any zone is a 200 ms half-life;
walking through a doorway fades one source down and the other up
smoothly over a few hundred ms.


HDRI probes — chrome / glass / metal reflections
------------------------------------------------

Every shiny material in your map (chrome, glass, polished metal, glossy
plastic, anything with metallic > 0 or low roughness) reflects an HDRI
cubemap. The cubemap content comes from a "probe" you place in Blender
as a named cube mesh.

Required: exactly ONE `hdri_default` marker per map. This is the
map-wide fallback probe — its captured cubemap is what reflects on every
shiny material when the player isn't inside a zone probe. A tiny 1 m
cube placed at roughly eye height in the most "average" open spot of
your map is fine.

Optional: any number of `hdri_<easing>` zone probes. The AABB of the
cube is the zone in which the probe's capture takes priority over the
default. Use these for rooms / regions with distinctly different
lighting (a dim warehouse interior vs the sunlit outside, a neon
basement vs a daylight roof). `<easing>` controls how the zone fades
out at its edge into the default fallback:

  soft     smoothstep — natural, the default choice
  medium   linear
  hard     quadratic — strong centre, subtle edges (sharp room
           boundaries)
  instant  binary inside/outside (smoothed 200 ms in time only)

Two markers can both be named `hdri_soft` — Blender auto-suffixes
`.001`, `.002`, etc., and the engine preserves those to keep their
captures distinct.

Capture
~~~~~~~

You DON'T render the cubemaps in Blender. The game captures them at
runtime from the live framebuffer:

  * First launch of a new map: HUD hides for a few seconds while the
    client teleports through each probe position screenshotting all
    six face directions. JPEGs land at
    `data/maps/<name>/hdris/<easing_key>_<face>.jpg`. Then it returns
    to gameplay.
  * Subsequent launches: JPEGs are read from disk, no capture pass,
    instant connect.
  * To re-capture after moving / renaming a probe or changing the
    world lighting: `rm data/maps/<name>/hdris/*.jpg`, then launch.

Verify a probe is working: `--debug-ibl` spawns a chrome ball 1 m in
front of the camera. The ball should reflect a clear panoramic view of
what's around the probe (sky on top, ground on bottom, geometry on
the sides). If the ball is grey or has black faces, one of:

  * No `hdri_*` markers in the map (drop in `hdri_default` and re-launch).
  * Probe AABB doesn't contain the player AND no `hdri_default` exists.
  * JPEG capture failed mid-pass (delete `hdris/*.jpg` and retry).

Don't bake HDRI from Blender's World shader / Sky Texture. The runtime
captures the LIVE game scene including your `sky_dome` mesh's
authored material — that's what reflects on chrome. A Sky Texture in
the world shader would add a phantom second sun to the lightmap bake;
the exporter automatically strips it.


Wind / foliage sway
-------------------

Grass, bushes, and trees sway in wind via a vertex-shader pass. To
opt a mesh in, name it with one of three preset prefixes:

  movement_grass_*     ~2.4 Hz flutter, whole-blade displacement, wave
                       height 0.35 m. Grass tufts, wheat, reeds.
  movement_foliage_*   ~1.3 Hz sway, medium tip emphasis, wave height
                       1.0 m. Bushes, ivy, hedges.
  movement_tree_*      ~0.55 Hz sway, steep mask (only the canopy
                       really moves, trunk stays rigid), wave height
                       5 m. Trees and larger canopy plants.

Frequency scales inversely with plant size because real biomechanics
do: a 0.5 m grass blade has a much higher natural frequency than a
5 m tree trunk. In the same wind, small = fast, big = slow.

Modeling requirements

  * Put the mesh origin at the BASE of the plant. The shader reads
    local-space Y as the sway mask: Y = 0 is planted, Y = waveHeight
    sways at full amplitude. Off-origin meshes will sway around a
    wrong pivot.

  * Two-sided is on by default. The foliage shader disables backface
    culling, so single-plane grass cards and flat leaf cards work
    without having to duplicate + flip. (Plants with real thickness
    are still fine.)

  * Materials follow your Blender assignments. The exporter only
    prefixes each material's name with `movement_<kind>_` so the
    loader can route it to the foliage shader — it does NOT
    replace, duplicate, or force-share your material slots. Sharing
    is whatever you set in Blender: assign ONE material to 10k
    grass tufts via the material dropdown and you get one GLB
    material, which Fyrox collapses into a single instanced draw
    call. Assign 100 different materials and you get 100 draws.
    For a dense grass field, share one material. For trees where
    you want colour variety, assign each its own material.

  * Objects with no material slot get a per-kind fallback
    (`movement_grass_fallback`, etc.) — all fallback-less objects
    of the same kind share that one material automatically.

  * These meshes are excluded from the lightmap bake automatically
    (static bakes can't represent moving geometry). They render with
    the scene sun + ambient only.

Tuning

  Preset numbers (amplitude, frequency, flutter, mask gamma, wave
  height) live in code, under `SwayPreset::{GRASS,FOLIAGE,TREE}` in
  `fyrox-impl-patch/src/resource/gltf/material.rs`. Edit and rebuild
  to retune. Per-map wind direction/strength from Blender is not yet
  wired — wind defaults to a diagonal direction, same strength on
  every map.


Surface materials — footsteps and bullet impacts
------------------------------------------------

Every shootable / walkable mesh can opt into a surface-material tag
that drives two separate sound banks at runtime:

  * Bullet impacts  — a rifle burst into the mesh plays samples from
                      the tag's impact bank (concrete thwack, glass
                      shatter, metal ping, etc.).
  * Footsteps       — walking / running on the mesh plays samples
                      from the tag's footstep bank (wood creak,
                      sand shuffle, mud squelch, etc.).

Opt in by prefixing the mesh name in Blender with `material_<tag>_`.
Suffix after the tag can be anything — it's just for your own naming
sanity (`material_wood_floor_02` is fine). Untagged meshes default to
`concrete` and log a one-shot warning at load so you can spot them.
The shipping `example_map.blend` tags every kit piece, so when you
build a map from the kit you start from "all surfaces have specific
sounds" rather than "everything sounds like concrete".

Supported tags (pick the closest match for your surface):

  * concrete   — default. Hard, sharp thwack; crisp concrete
                 footsteps. Use for poured floors, rough walls,
                 sidewalks, stone rubble.
  * dirt       — soft thud; low-frequency; earthy footsteps.
                 Ground, mud-dry paths, flower beds.
  * wood       — sharp splinter crack; hollow plank footsteps.
                 Planks, pallets, wooden doors / tables, crates.
  * glass      — crystalline shatter; brittle crunchy footsteps
                 (broken-glass-underfoot feel). Windows, display
                 cases, bottles.
  * metal      — solid metal. Hard ringing ping with metallic
                 sustain; heavy metal-deck footsteps. Armour
                 plating, I-beams, thick lockers.
  * steel      — thin sheet metal — brighter, ringier than
                 `metal`. Catwalks, grates, air ducts, thin panels.
  * tile       — ceramic. Sharp crack, brighter than concrete;
                 smooth tile-floor footsteps. Bathroom floors,
                 kitchen splashback, indoor corridors.
  * carpet     — muffled, near-silent. Rugs, carpeted rooms, soft
                 furniture, foam mats.
  * sand       — gritty shuffle. Beaches, deserts, sandbag pile
                 tops, bunker floors.
  * mud        — wet squelch. Swamps, rain-soaked trenches, river
                 banks.
  * grass      — soft rustle. Lawns, meadows, groundcover under
                 open sky.
  * foliage    — brush / leaves. Bushes, ivy, hedges. Footsteps
                 fall back to `grass` because the player walks on
                 grass UNDER the bush, not on the foliage itself.
  * cardboard  — hollow crumple. Boxes, packaging, fast-food
                 cartons, thin partitions.

Name match is EXACT on the tag token: `material_metal_*` is the metal
tag, but `material_metallic_*` is UNTAGGED (the parser splits on `_`
and matches the whole first token, so `metallic`, `metalloid`,
`sandstone`, `sandbag`, `gravel`, etc. all fall through and become
the default). Case is ignored (`Material_Wood_*` is fine). Blender's
`.001` / `.002` duplicate suffixes are stripped before matching.

Live demo on `example_map.blend`:

  * A row of 13 colour-coded 1 m cubes sits between red spawn and
    the kit row, one per tag, named `material_<tag>_demo_cube`.
  * Walk onto each cube to hear its footstep bank.
  * Shoot any face to hear its impact bank.
  * That row is the ground-truth reference for which tags exist —
    `client/src/surface_material.rs` is the code version of the
    same list.

Materials — what imports from Blender, what doesn't
---------------------------------------------------

The export route is glTF (GLB), and glTF only understands a narrow
slice of Blender's material system: a Principled BSDF plugged into the
Material Output, with image textures feeding its inputs. That's the
whole supported surface. Build materials around Principled BSDF and
they come through; do anything clever and it silently flattens.

What imports cleanly:

  * Principled BSDF with constant values on its inputs (Base Color,
    Metallic, Roughness, Normal).
  * Image Texture nodes wired directly into those inputs:
      Base Color         -> albedo map (sRGB image)
      Roughness          -> roughness map (Non-Color image)
      Metallic           -> metallic map (Non-Color image)
      Normal             -> Normal Map node -> Image Texture (Non-Color)
  * One UV map per mesh (Blender exports the active UV).
  * Alpha Blend / Alpha Clip via Base Color alpha channel.

What does NOT import — silently dropped or baked flat:

  * Procedural nodes (Noise, Voronoi, Musgrave, Checker, Brick, Magic,
    Wave, Gradient Texture, Math / ColorRamp chains, Mix Shader, Bump,
    Displacement, etc.). glTF has no equivalent. For sky-dome gradients
    or procedural patterns, either bake them to an image texture
    (Shader -> Bake) or pre-build the texture in an image editor.
  * Multiple BSDFs mixed together. One Principled BSDF per material.
  * Node groups wrapping procedural setups.
  * Custom Geometry / Tangent / Texture Coordinate tricks, light-path
    nodes, AO nodes — anything that isn't a plain Principled BSDF
    with image textures.

Don't use Emission on map materials:

  Leave the Principled BSDF's Emission Color at black and Emission
  Strength at 0 on walls, floors, props — anything in your map.
  In-game it looks wrong: the surface stops shading and ends up a flat,
  uniform colour regardless of lighting. If something's coming out too
  dark, brighten its Base Color instead.

Texture tips:

  * Pack your textures once they're final (File -> External Data ->
    Pack Resources). Packed textures embed into the .blend and survive
    moving the file around.
  * Otherwise the exporter reloads images from their original on-disk
    paths — keep textures alongside the .blend.
  * Prefer .png or .jpg.

Rule of thumb: if your material preview in Blender's Material Preview
viewport looks the same after you reduce it to "Principled BSDF only,
image textures on each input", you're golden. Anything that changes
is what glTF will throw away.

Want to see how Metallic / Roughness actually look in this renderer?
The built-in material_showcase scenario spawns a grid of PBR spheres
varying both controls — for a 10-second automated orbit:

  ./client --test-scenario material_showcase --exit-after 10

Top row is metal (gold), bottom row is plastic (grey); roughness goes
from mirror-smooth on the left to fully matte on the right. Match
those spheres when you're setting values on your own materials.


Custom shaders — testing your own .ron shaders
----------------------------------------------

Mappers can swap the material on any mesh to a custom shader by
naming the mesh in Blender with the substring `shader_<stem>`, where
`<stem>` resolves to `data/shaders/<stem>.ron`. So a mesh named
`shader_ocean1` picks up `data/shaders/ocean1.ron` at map load. The
`no_col_` collision-less prefix composes:
`no_col_shader_ocean1` is both invisible to bullets/footsteps and
gets the ocean shader.

Shaders that ship in this distro:

  data/shaders/glass.ron       — translucent glass
  data/shaders/hologram.ron    — animated scanline hologram
  data/shaders/map_default.ron — default for every map mesh
  data/shaders/ocean1.ron      — animated water
  data/shaders/rim.ron         — rim-light highlight
  data/shaders/sky1.ron        — animated sky-dome gradient
  data/shaders/toon.ron        — flat-shaded cel look

Drop a new `<stem>.ron` next to those, name a mesh `shader_<stem>` in
Blender, host-map the .blend, done.

Hot reload — edit and see the result without restarting

When you launch the client with `--host-map`, two reload paths are
live for shader iteration:

  * F10 — recompile every shader the running scene uses, in place.
          Sub-millisecond. Use this if your editor's save fires odd
          filesystem events the watcher misses.
  * File watcher — auto-recompiles on every save to `data/shaders/*.ron`.
                   You'll see a `SHADER_WATCH: file change → reloaded …`
                   line in the terminal each time it fires.

Both paths panic loudly on a parse / compile error, with the file
path and the error message — no silent "previous shader stayed in
place." Fix the typo, save again, the next reload picks it up.

For pure shader work without authoring your own Blender file, host-map
the bundled example map (skipping the bake keeps the loop fast):

  ./client --host-map data/maps/example_map/map.blend --skip-bake

That gives you free movement on a small stage with `map_default`
applied to the geometry — a clean canvas for iterating on a shader
edit loop.


Useful flags
------------

Client:

  ./client <ip:port> <name>    connect to a server, set display name
  --host-map <path>            host a .blend or .glb locally + connect.
                                 .blend MUST live at
                                 data/maps/<name>/map.blend
  --skip-bake                    no Cycles bake (fast iteration,
                                 sun-only lighting)
  --fast-bake                    1-sample Cycles bake (noisy preview,
                                 seconds)
  --no-bake-gpu                  force lightmap bake on CPU
  --bake-workers <n>             override auto worker count for bake
  --force-export                 ignore the bake cache for one run
  --capture-ibl                  force a fresh HDRI probe capture pass
                                 (then exits the process)
  --capture-ibl-equirect         with --capture-ibl: also write a
                                 2048×1024 equirect preview per probe
  --debug-ibl                    spawn a chrome ball in front of the
                                 camera that reflects the active probe
  --host-bots <n>              with --host-map: spawn N bot opponents
                                 (default 0 — solo walk-the-map mode)
  --host-port <n>              override the embedded server port
                                 (default 27015)
  --name <name>                set display name
  --list-scenarios             list built-in test scenarios and exit

