ProAim — Windows
================

Files in this folder:

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


First, open a terminal in THIS folder
-------------------------------------

Every command in this README is typed into a terminal sitting in this
folder. Two easy ways to get one open here:

  * In File Explorer, click into this folder. Then in the address bar
    at the top, type `powershell` and press Enter. A blue PowerShell
    window opens already pointing at this folder.

  * Or: Shift + Right-click on an empty spot inside the folder and pick
    "Open PowerShell window here" (Windows 10) or "Open in Terminal"
    (Windows 11).

When you type a command, the terminal needs to be in THIS folder.
You'll know it is if the prompt line ends with the folder name, e.g.
`PS C:\Users\you\Desktop\proaim>`.


First launch — the SmartScreen warning
--------------------------------------

client.exe is unsigned. The first time you run it, Windows shows a
blue box:

  "Windows protected your PC"

Click "More info" -> "Run anyway". That's it — you only see it once. It's not malware; Windows just doesn't recognize the
publisher because I haven't paid for a code-signing cert.


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 PC instead, the client hosts a real server
in-process — there is no separate server program to run:

  client.exe --host-map data\maps\example_map\map_baked.glb

Everyone else joins that PC (from their own copy of this folder on
their own PC):

  client.exe <host-ip>:27015 TheirName

The host's IP is their LAN address (e.g. 192.168.1.42) if you're on
the same Wi-Fi / 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 PC, and probably allow client.exe
through Windows Defender Firewall the first time they run.

Quick sanity check: have the host first connect to themselves with
`client.exe 127.0.0.1:27015 HostName`. If that works and a friend
still can't join, it's a firewall / router / 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, client.exe 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, installed the normal way from blender.org.
The default install location (C:\Program Files\Blender Foundation\)
is detected automatically — you don't have to do anything. If you
installed it somewhere else, put Blender on your PATH.

Quick iteration — no lightmap bake, loads in seconds:

  client.exe --host-map path\to\your_map.blend --skip-bake

Noisy preview — 1-sample Cycles bake, loads in ~seconds:

  client.exe --host-map path\to\your_map.blend --fast-bake

Final look — Cycles lightmap bake (uses every CPU core + GPU), minutes:

  client.exe --host-map path\to\your_map.blend

Bakes are cached: re-running on the same .blend (no edits since the last
bake) is instant. Edit-and-save the .blend to invalidate; pass
--force-export to ignore the cache for one run.

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

  client.exe --host-map data\maps\your_map.glb

Playtest against bots
---------------------

Add `--host-bots N` to have the client auto-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.exe --host-map path\to\your_map.blend --skip-bake --host-bots 5

Good for checking spawn placement, bomb-zone flow, sightlines — anything
you can only tell from "real players" actually moving through the space.
Common counts:

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

Leave the flag off to just walk the map solo (no bot processes start
up, nothing shoots at you while you're inspecting geometry). 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.

The embedded server runs on port 27015, so friends join your map with
`client.exe <your-ip>:27015`. If you want two of them hosting on the
same box (e.g. one on 27015, the other on 27016 for a parallel test),
override the port:

  client.exe --host-map path\to\map.blend --skip-bake --host-port 27016

Re-running --host-map re-exports every time, so edits you make in
Blender show up on the next launch. The exported .glb lands in
data\maps\ next to the exe automatically — you don't pick that path.

Example map to learn from:

  data\maps\example_map\map.blend

Ships with this distro — the actual level the game's built-in map was
made from. Run it with:

  client.exe --host-map data\maps\example_map\map.blend --skip-bake

Open it 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, and a Principled-BSDF
material setup that actually exports cleanly. Easiest way to start
your own map is Save As off this one.

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
%USERPROFILE%\.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.


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

Anywhere. The .blend doesn't have to live under data\ — you pass a
path to --host-map and that's it. A workable layout:

  proaim\
    client.exe, data\, README.txt        <- what's in this distro
    levels\                                      <- your folder, any name
      de_house.blend
      textures\                                  (if you link external images)

Only data\ and the exes are load-bearing. Your .blend files, reference
art, and scratch exports can live wherever you like.

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.
  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.


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.exe --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.exe --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.exe <ip:port> <name>    connect to a server, set display name
  --host-map <path>              host a .blend or .glb locally + connect
  --skip-bake                      no bake (fast iteration, sun-only lighting)
  --fast-bake                      1-sample Cycles bake (noisy preview, seconds)
  --host-bots <n>            with --host-map: spawn N bot opponents
                                   (default 0 — solo walk-the-map mode)
  --host-port <n>                override the host port (default 27015)
  --server <ip:port>             connect to this server on startup
  --name <name>                  set display name
  --list-scenarios               list built-in test scenarios and exit

