Commit graph

4049 commits

Author SHA1 Message Date
Kelsi
96ec734d06 feat(editor): add --audit-project-spawns project-wide placement audit
Project-wide companion to --audit-zone-spawns. Spawns the binary
per-zone (only those with creatures.json or objects.json),
streams the per-zone reports, and exits 1 if any zone has spawns
flagged.

Optional --threshold N flag is forwarded to each sub-invocation so
projects with stricter or looser placement requirements get
consistent audits across zones.

CI-friendly pre-release placement check: drop into a pipeline,
fail the build if any spawn is more than N yards off terrain.
2026-05-07 14:27:51 -07:00
Kelsi
f870a516dd feat(editor): in-editor "Snap All Spawns to Ground" menu
Adds EditorApp::snapAllSpawnsToGround() and a menu item under
Generate, mirroring the --snap-zone-to-ground CLI for users who
want the action without context-switching to a terminal.

Walks every NPC + object, casts a downward ray from baseZ+500y,
and writes the hit Z into spawn.position.z. Patrol waypoints on
each NPC are snapped too. Marks the project dirty + auto-save
pending so the change persists without a manual Ctrl+S.

Existing per-selection "Snap Ground" button on the right panel is
unchanged; this is the bulk version for "I just edited terrain
under a populated area."
2026-05-07 14:12:39 -07:00
Kelsi
f589fa20ce feat(editor): add --audit-zone-spawns to flag misplaced spawns
Non-destructive companion to --snap-zone-to-ground. Loads the
zone's terrain, samples the height under each creature + object,
and flags any whose Z is more than <threshold> yards (default 5)
off from the terrain.

Useful for surveying placement issues before deciding whether to
run --snap-zone-to-ground (which would silently rewrite every
spawn). The flagged report shows kind / delta / spawnZ / terrainZ /
name so the user can spot whether the spawn is buried, floating,
or just out of bounds.

Threshold configurable via --threshold N. Exit 1 if any issue is
flagged so CI can gate on placement validity.

Verified: zone with FloatyMurloc at Z=5000 (vs terrain Z=99.9)
correctly flags it with +4900.1 delta; GroundedWolf at Z=100 is
within threshold and not flagged.
2026-05-07 13:57:25 -07:00
Kelsi
8f17f5ae34 feat(editor): add --snap-project-to-ground orchestrator
Project-wide companion to --snap-zone-to-ground. Spawns the binary
per-zone (only zones that actually have creatures.json or
objects.json — pure-terrain zones are skipped to avoid noise),
streams the per-zone output, then aggregates a summary.

Useful after a project-wide terrain regen, or after batch-running
--random-populate-zone across multiple zones — one command snaps
all the spawns at once.

Verified: 2-zone test where only z1 has populated content correctly
selects z1 alone, snaps 3 creatures + 1 object, exits 0.
2026-05-07 13:42:19 -07:00
Kelsi
ddea06a0e4 feat(editor): add --info-project-audio cross-zone audio rollup
Project-wide companion to --info-zone-audio. Walks every zone in
<projectDir>, reads the audio fields, emits a per-zone yes/no
table for music + ambience plus the volume sliders. Counts how
many zones have music and how many have any ambience set so the
header tells you at a glance how much audio work remains.

Useful for spotting zones still missing audio assignment before a
release pass — rather than opening each zone in the editor to
check.

Verified: 2-zone project where only A has audio configured →
header reports "with music: 1, with ambience: 1" and the per-zone
table shows A=yes/yes, B=no/no.
2026-05-07 13:27:16 -07:00
Kelsi
d1138e283c feat(editor): add --snap-zone-to-ground bulk Z-resnap for spawns
Walks every creature in creatures.json and every object in
objects.json, samples the actual terrain height at each spawn's
(x, y), and writes that into the spawn's Z. Useful after terrain
edits or after --random-populate-zone if the spawn baseZ doesn't
match the carved terrain.

Height lookup: loads every WHM tile listed in zone.json, then for
each spawn finds the chunk containing its (x, y) and uses the
chunk's average heightmap height + base Z. Average rather than
bilinear because spawns don't need sub-yard precision and the
average dodges sampling-induced spikes near chunk seams.

Verified: random-populate-zone followed by snap-zone-to-ground on a
fresh tile snaps 4 creatures + 2 objects without errors. Brings
command count to 235.
2026-05-07 13:12:09 -07:00
Kelsi
ff5f6a9070 feat(editor): extend --gen-mesh with ramp (right-triangular prism)
Right-triangular prism that climbs along +X. Footprint is size×size
on the XY plane (centered in X, +Y from 0 to size); rises from
Z=0 at -X up to Z=size at +X. Useful for ramps onto platforms,
simple roof slopes, cliff-face placeholders.

Five faces emitted as quads (top slope, bottom, back-tall vertical
wall, two side triangles encoded as degenerate quads so the
indexing stays uniform): 20 verts / 10 tris (some side tris are
zero-area; renderers skip them cleanly).

Top-slope normal is computed from the actual rise/run so shading
shows the slope angle correctly. Help text on both --gen-mesh and
--gen-mesh-textured updated to advertise the new shape — primitive
set is now cube/plane/sphere/cylinder/torus/cone/ramp + the
gen-mesh-stairs/-from-heightmap commands.
2026-05-07 12:58:10 -07:00
Kelsi
7a624adada feat(editor): zone audio panel scans Data dir for music + ambience files
The Zone Audio panel previously only offered five hardcoded preset
paths. Replaced with a recursive directory walk of <data>/Sound/
Music and <data>/Sound/Ambience for any .mp3/.wav/.ogg files.
Results cached after the first scan; a Refresh button forces a
rebuild for when the user drops new files in.

Two ImGui combos populate from the scan:
- "Music File"     — picks from Sound/Music
- "Ambience File"  — picks from Sound/Ambience

Each combo has a "(none)" option to clear. Selecting an entry
updates both the manifest field and the existing manual text input
buffer below, so users can fine-tune the path after picking from
the combo.

EditorApp gets a public getDataPath() so the UI can reach the
configured asset root without exposing the rest of the private
state.

Audio playback preview is the remaining piece — needs SDL_mixer
or similar wired into the editor; not in this commit.
2026-05-07 12:43:58 -07:00
Kelsi
0cb6a4c536 feat(editor): add --info-zone-audio for inspecting zone audio config
Prints the music track, day/night ambience tracks, and the two
volume sliders stored in zone.json. Also supports --json. Useful
for spot-checking that the right audio assets are wired up before
bake/export, and for CI to assert the zone has audio configured.

Empty fields render as "(none)" in the human view; JSON form emits
the actual empty string. Brings command count to 234.
2026-05-07 12:30:00 -07:00
Kelsi
8c20f732ce fix(editor): random-populate ground-snaps spawns to actual terrain
Previously every random-populated creature and object dropped at
baseZ (the manifest's flat height), which sat below the carved
terrain on hills and floated above it in valleys. After
generateCompleteZone the terrain is far from flat, so the spawns
showed up clipping or floating.

Added an inline groundZ(x, y) helper that casts a downward ray
from baseZ+500y at the spawn's (x, y) and uses the hit position's
Z. Falls back to baseZ if the ray misses terrain (shouldn't happen
inside the loaded tile bbox but is safer than NaN).

Both creature and object loops now consult groundZ. Snap-to-ground
is the runtime placer's default behavior anyway, so this lines up
with the rest of the editor's expectations.
2026-05-07 12:15:45 -07:00
Kelsi
89dacba666 feat(editor): in-editor "Random Populate" menu mirrors CLI
Adds EditorApp::randomPopulateZone(creatureCount, objectCount, seed)
and a Generate-menu submenu with sliders for both counts plus a
seed input. Same logic and bestiary as the --random-populate-zone
CLI: 12 creature templates with level jitter, 5 placeholder WMO
prop types with randomized rotation/scale.

Spawns are placed within the loaded tile's world bbox, marked
dirty + auto-save pending so the populated content persists across
restarts.

The CLI command remains for CI / scripting / batch workflows; the
menu serves designers who want to populate from the GUI without
context-switching to a terminal.

Same-seed reproducibility: a hover hint reminds the user that the
seed determines the output deterministically.
2026-05-07 12:14:13 -07:00
Kelsi
bdf2497f46 feat(editor): paint roads to/from selected objects/NPCs
The path tool gains a "Append selected <object|NPC> as path point"
button that captures the currently-selected entity's world position
into pathPoints_. Workflow: place an inn + a town hall, select inn
→ click append, select town hall → click append, click Apply Path.
The road is then drawn between the two object positions.

When no object/NPC is selected, the button area shows a hint
instead. The button auto-kicks pathCapture_ from None into Waiting*
on first use so the user doesn't have to remember the setup
sequence.

Works with the multi-point polyline path tool from the previous
commit — append three or more objects to route a road through them
in order.
2026-05-07 11:59:52 -07:00
Kelsi
d96496147f feat(editor): add --random-populate-items seeded loot generator
Seeded random items.json populator. Pulls a quality-banded prefix
("Worn" → "Eternal" depending on rolled quality) plus a noun
("Sword", "Tome", "Cloak"...) for plausible names, then randomizes
itemLevel and stack size around quality-appropriate baselines.

Useful for playtest loot tables that need bulk content without
hand-typing each entry. Reproducible from --seed; respects
--max-quality so a "trash loot" pass won't accidentally drop
artifacts.

Verified on a fresh zone: 8 items rolled with the seed=5 produces
a sensible spread (1 epic Tome, 1 epic Cuirass, 1 uncommon Bow,
5 common/poor) and unique IDs starting at 1. Brings command count
to 233.
2026-05-07 11:57:55 -07:00
Kelsi
9dad8c2aa0 feat(editor): add --random-populate-zone seeded creature/object spawner
Walks <zoneDir>/zone.json to compute the world AABB the zone
occupies, then drops N random creatures and M random objects with
positions inside that bbox. Seeded LCG so the same seed always
produces the same population — useful for reproducible playtest
scenarios and for CI fixtures that need deterministic content.

Default flags: --seed 42 --creatures 20 --objects 10. Creatures
draw from a small built-in bestiary (Wolf/Boar/Bear/Spider/Bandit/
Kobold/Murloc/Skeleton/Wisp/Goblin/Stag/Crab) with level jitter
around a per-name baseline. Objects pick from a generic placeholder
prop set (Tree/Boulder/Bush/Stump/Mushroom). Both lists are inline
so users can extend them without dragging in the asset browser.

Verified: 5-creature 3-object run produces a creatures.json with
real names + level-9 wolves + behavior=2 (Wander) + an objects.json
with real WMO paths, positions inside the zone bbox, randomized
rotation + scale, and unique IDs. Brings command count to 232.
2026-05-07 11:43:03 -07:00
Kelsi
4e4102bf4a feat(editor): river/road tool supports multi-point polylines
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Path capture replaced with std::vector<glm::vec3> pathPoints_. The
flow is now: click "Click Start Point" → click terrain for first
point → keep clicking for additional waypoints → press Apply when
done, or Cancel to scrap. Apply iterates each consecutive pair as a
segment, calling carveRiver + paintAlongPath + fillWaterAlongPath
(or flattenRoad for road mode) per segment.

Hard-capped at 64 captured points so a runaway click handler can't
unboundedly grow the polyline. Toast at end reports the segment
count so users see the polyline took.

PathCapture states extended: None / WaitingStart / WaitingEnd /
WaitingMore. Backwards-compatible getPathStart()/getPathEnd()
return first/last points so the existing path-preview wiring keeps
working.
2026-05-07 10:32:19 -07:00
Kelsi
158ab192f0 feat(editor): river tool now fills water along the carved path
The river path tool used to carve the channel and texture the banks
but never added a water layer — users had to manually run
"Fill Water" afterward, which floods the entire tile. The fix is a
new TerrainEditor::fillWaterAlongPath() method that adds water
layers only to chunks the river segment passes through (within
width + chunk-half-diagonal of the line).

Per-chunk water height is set to the chunk's post-carve minimum
terrain height + 0.5y offset so the water sits visibly in the
channel without overflowing onto banks.

The river apply path now invokes carveRiver → paintAlongPath →
fillWaterAlongPath in sequence. Toast updated to mention all three.

Multi-point rivers are still next on the list — the underlying
math takes a single segment today, so a polyline river needs a UI
revamp to capture N points + a per-segment loop. Ack'd, not done
this commit.
2026-05-07 10:17:36 -07:00
Kelsi
ecba93d4a4 fix(editor): NPCs default to Wander behavior + UI tooltip
CreatureSpawn::behavior defaulted to Stationary, so newly placed
NPCs would never move at runtime. The "NPCs do not patrol or
wander, they stay put" complaint was a default-value issue, not a
missing feature — switching the default to Wander (radius 10y)
gets normal NPCs roaming out of the box.

Added a tooltip on the Behavior combo making it explicit that this
is the runtime mode (the editor's preview doesn't run movement
logic; the behavior kicks in once the zone ships and the server
consumes the exported SQL). Lists what each mode means:
Stationary / Patrol / Wander / Scripted.

Existing zones with explicitly-set Stationary NPCs are preserved
on load — only fresh defaults are affected.
2026-05-07 10:03:17 -07:00
Kelsi
3a6f119f7a fix(editor): NPC nameplates align with NPC + togglable to selected only
Three issues fixed in one pass:

1. Z-offset was 35 yards — nameplates floated way above the actual
   NPC position. Dropped to 3.5 yards (just above an average
   creature's head) so the label reads as attached.

2. Horizontal alignment was a fixed -30 px offset, which only looked
   centered for ~6-character names. Switched to ImGui::CalcTextSize
   and centering on the projected position.

3. Nameplates were always-on, which got noisy fast on populated
   zones. Added "Nameplate on selected only" checkbox in the NPC
   panel (default ON); when unchecked, all NPCs show their names.

Vertical position also slightly tightened (sy - textSz.y - 2 vs
sy - 10) so the text baseline aligns rather than floating below.
2026-05-07 09:48:59 -07:00
Kelsi
0e2b55f9fd fix(editor): crater button now click-to-place on terrain
The "Create Crater at Cursor" button used the brush's last hover
position, which was stale by the time the user clicked the button —
the cursor had to move onto the button itself, dragging the brush
position along with it. Result: crater spawned somewhere between
where the user wanted and the button.

New flow: button arms a "click on terrain to place crater" mode
(captures the user's chosen radius/depth/rim at arm time). The next
left-click anywhere on terrain spawns the crater at the actual click
position, regardless of brush state. Button label changes to "ARMED
— click on terrain to place" while waiting, with a Cancel button +
Esc to dismiss.

Misses on terrain leave the mode armed so the user can retry without
re-pressing the button.
2026-05-07 09:34:17 -07:00
Kelsi
b35c9341ec fix(editor): WASD/QE flycam works while hovering ImGui panels
Camera key events were gated on io.WantCaptureKeyboard, which goes
true whenever any ImGui panel has focus — not just when typing into
a text widget. Hovering the cursor over a side panel silently
disabled the flycam, which read as "WASD doesn't move the camera."

Switched the gate to io.WantTextInput, which is true only while a
text widget is actively accepting input. Now WASD/QE/Shift work
exactly when they should: always, except while you're typing into a
field.
2026-05-07 09:20:01 -07:00
Kelsi
c2eec42eb8 fix(editor): generateCompleteZone honors active biome
Previously generateCompleteZone() applied a hardcoded heightband
texture set (Tanaris sand → Elwynn grass → Barrens rock →
Dragonblight snow) regardless of which biome the user picked in the
"New Terrain" dialog. The biome's textures were correctly applied
by createNewTerrain, then immediately overwritten by the
hardcoded auto-paint pass. Result: every "Create + Generate" run
looked the same, masquerading as "the first biome sticks."

Fix: store the active biome on EditorApp and read it in
generateCompleteZone(), pulling the four-band texture set + slope
accent from getBiomeTextures() instead of hardcoding. Now selecting
Forest vs Desert vs Snow vs ... actually changes the painted
textures.
2026-05-07 09:19:08 -07:00
Kelsi
7ce5aac3e5 feat(editor): add --info-mesh-storage-budget byte-category breakdown
Estimated bytes-per-category breakdown for a single WOM. Numbers
are based on the in-memory struct sizes (vertex 40B, index 4B,
bone 22B, batch 16B, anim keyframe 44B, plus texture path strings)
— not the actual on-disk encoding (which has framing overhead) —
but the relative shares are accurate and help users decide where
shrinking efforts pay off.

Surfaces actionable tips when a category dominates: animations >
vertices → suggest --strip-mesh --anims with the saved KB; bones
non-trivial → suggest --bones for static placement; vertices
dominate → suggest a lower-poly variant.

Verified on procedural cube: 960B vertices (85%) + 144B indices
(13%) + 16B batches + 8B textures, total 1128 estimated vs 1184
on-disk (~5% framing overhead). Tip correctly fires for
vertex-dominated mesh. Brings command count to 231.
2026-05-07 09:00:47 -07:00
Kelsi
ca8c366870 feat(editor): add --export-mesh-heightmap, inverse of gen-mesh-from-heightmap
Extracts a grayscale heightmap PNG from a row-major W×H heightmap
mesh. Y values are normalized to 0..255 using the mesh bounds so
the output PNG uses the full dynamic range.

User supplies W and H explicitly: arbitrary meshes aren't
necessarily heightmap-shaped, and taking the dimensions as args
avoids guessing wrong on a mesh with vertex count W*H but a
different layout.

Round-trips with --gen-mesh-from-heightmap modulo the 1-byte
quantization step.

Verified: noise PNG (32×32) → heightmap mesh → exported PNG (32×32)
chain executes cleanly, height 0..1.984 mapped to 0..255 grayscale,
both PNGs share the same dimensions. Brings command count to 230.
2026-05-07 08:46:50 -07:00
Kelsi
d54440a75b feat(editor): add --smooth-mesh-normals for area-weighted normal recompute
Recomputes per-vertex normals as the area-weighted average of
incident face normals. The cross-product magnitude is twice the
triangle area, so larger faces contribute more to the local
direction — gives a clean smooth-shaded result on curved surfaces.

Use cases:
- Imported geometry has no normals (--import-obj leaves them zero
  or face-flat).
- Custom transforms have desynced normals from positions.
- Faceted-by-construction meshes (cube, stairs) need a smooth
  re-shade for stylistic reasons.

Degenerate verts (unreferenced or with sum that cancels to zero —
e.g., the two poles of a UV sphere) fall back to (0,1,0) rather
than leaving NaN; reported separately so the user sees how many.

Verified: sphere → 219 of 221 normalized + 2 degenerate poles
handled cleanly; minimal triangle → 3/3 normalized. Brings command
count to 229 (kArgRequired 210).
2026-05-07 08:33:20 -07:00
Kelsi
b38dec7a83 feat(editor): add --gen-mesh-from-heightmap for PNG → 3D terrain
Converts a grayscale PNG into a heightmap mesh. Each pixel becomes
one vertex; brightness becomes Y. Mesh is centered on the XZ plane
with X spanning [-W*scaleXZ/2, +W*scaleXZ/2] and Z spanning the
same for H.

Defaults: scaleXZ=0.1 (a 64×64 PNG covers a 6.4×6.4 yard patch),
scaleY=2.0 (full white pixels rise 2 yards above black). Capped at
512×512 source images to keep vertex count bounded (the cap raises
to 262K verts for the largest legal input).

Normals are computed from finite differences against the height
field, giving smooth shading across the surface. Single batch +
one empty texture slot ready for downstream binding via
--add-texture-to-mesh.

Pairs naturally with --gen-texture-noise (synthetic terrain) or
any artist-painted heightmap exported from Photoshop / GIMP.

Adds stb_image.h include — implementation is already linked via
the existing tools/editor/stb_image_impl.cpp.

Verified: 64×64 noise heightmap → 4096 verts (= 64²) + 7938 tris
(= 2×63²), 6.4×6.4 span, height range 0..1.99; 1024×1024 input
rejected with size-cap error. Brings command count to 228.
2026-05-07 08:19:37 -07:00
Kelsi
734e7dc2f0 feat(editor): add --gen-texture-stripes for two-color band patterns
Synthesizes a two-color stripe pattern PNG. Stripe width in pixels,
plus direction (diagonal default, or horizontal/vertical). Color
endpoints share the same RRGGBB / RGB hex syntax as other texture
gens.

Useful for caution tape, marble bands, hazard markers, racing-style
start/finish flags — patterns that --gen-texture's checker/grid
don't capture.

Verified: 128×128 diagonal yellow/black at 32px ('caution tape')
and 256×256 horizontal red/white at 16px both written successfully
with correct headers reported. Brings command count to 227 (and
the texture-synthesis family to 6: solid + checker + grid + linear
gradient + radial gradient + noise + stripes).
2026-05-07 08:05:18 -07:00
Kelsi
3852bac1d8 feat(editor): add --merge-meshes for combining two WOMs into one
Concatenates two WOMs into a single output. Vertex buffer is the
two inputs spliced; the second mesh's indices are offset by the
first mesh's vertex count; texture slots from both are appended;
batches from the second mesh have their indexStart shifted by the
first's index count and their textureIndex shifted by the first's
texture-slot count.

Single-batch / no-batch inputs are auto-promoted to one synthetic
batch each so the merged output is always a well-formed v3.

Bones/animations are NOT merged — that requires skeleton retargeting
which is out of scope. If either input has bones, the merged output
is treated as static (bones cleared, weights reset to identity-on-
bone-0) so renderers don't read mismatched indices.

Verified: cube (24v/12t/1 batch) + sphere translated to +X (221v/
384t/1 batch) → combined (245v/396t/2 batches/2 textures), bounds
union (-0.5..3.5 in X), clean v3 reload via --info-mesh. Brings
command count to 226.
2026-05-07 07:52:17 -07:00
Kelsi
e49d8dd738 feat(editor): add --mirror-mesh for axis mirroring with winding flip
Mirrors every vertex position and normal across the chosen axis
(x, y, or z). Negating just one position component reverses face
winding (the triangle's signed area flips), so the second and
third index of every triangle is also swapped to keep front-faces
forward and lighting correct. Bone pivots mirror too.

Useful for "I have a left arm, mirror it for the right arm" content
reuse — the open-format mesh ecosystem can build symmetric content
from one half-asset without artist round-trips.

Verified: cube translated to (+5,0,0) mirrored across X → bounds
correctly land at (-5.5,-0.5,-0.5)..(-4.5,0.5,0.5); 24 vertices
touched / 12 triangles flipped reported; bad axis 'w' rejected
exit 1. Brings command count to 225.
2026-05-07 07:39:05 -07:00
Kelsi
b773ff53e3 feat(editor): add --export-project-items-csv project-wide spreadsheet rollup
Single CSV with every item across every zone in <projectDir>. The
zone name is the first column so a pivot table can group by it;
remaining columns mirror --export-zone-csv items output (index, id,
name, quality, itemLevel, displayId, stackable).

Saves running the per-zone CSV exporter N times and concatenating
manually. Reuses the existing CSV-escape lambda so commas and
quotes in names round-trip cleanly.

Verified: 2-zone project (A: Sword, "Helm, with comma" / B: Crown)
emits 3 rows with the comma'd name correctly double-quoted; zone
column lets a downstream pivot group correctly. Brings command
count to 224 (kArgRequired 205).
2026-05-07 07:26:00 -07:00
Kelsi
05645b2a79 feat(editor): add --copy-zone-items for cross-zone item reuse
Copies items from one zone to another. Two modes:

Default (replace): destination items.json becomes a verbatim copy
of the source. Useful when zoneB is a fresh copy of zoneA's loot
table.

--merge: appends source items to existing destination items, but
preserves the destination's existing IDs by re-id'ing source
entries on collision. The destination's items take precedence; the
source's entries get fresh IDs picked from the smallest unused
positive integer.

Verified: merge with id-1 collision (dest has Boot id=1; source has
Sword id=1, Helm id=2) → Sword becomes id=2, then Helm becomes id=3
because 2 was just claimed; "re-ided: 2 (id collisions)" reported.
Replace mode wholesale-overwrites as expected. Brings command count
to 223.
2026-05-07 07:12:55 -07:00
Kelsi
a3253eebc6 feat(editor): add --gen-texture-radial for circular gradients
Synthesizes a radial gradient PNG fading from <centerHex> at the
image center to <edgeHex> at the corner. Distance is normalized so
the corner is t=1 (works for non-square images), with a smoothstep
curve giving a soft falloff rather than a harsh disc edge.

Useful for spell glow rings, vignettes, soft-edged decals, and
particle-emitter masks — the common "circular blob" cases that
linear gradients can't produce.

Verified: 64×64 white-center / black-edge "glow" PNG written
successfully; invalid 'notacolor' hex rejected with exit 1.
Brings command count to 222.
2026-05-07 07:00:05 -07:00
Kelsi
2cb079549e feat(editor): add --center-mesh and --flip-mesh-normals quick fixes
Two convenience commands for the common "fix imported mesh" cases:

--center-mesh <wom-base>
  Translates the mesh so the bounds center lands at the origin.
  Useful when an OBJ comes in with its pivot in some weird corner
  and the user wants center-pivoted placement. Pure translation —
  shape unchanged, radius preserved.

--flip-mesh-normals <wom-base>
  Inverts every vertex normal. Use case: an OBJ with flipped
  winding renders inside-out — flipping normals fixes shading
  without re-winding the index buffer (which would also need
  batch-aware care). Also useful for skybox-like inside-facing
  meshes.

Verified: cube translated to (5,0,0) → center brings it back to
±0.5 with shift line "(-5, -0, -0)" reported; flip reports 24
vertices touched. Brings command count to 221 (kArgRequired 202).
2026-05-07 06:47:43 -07:00
Kelsi
488fbda0d4 feat(editor): add --rotate-mesh completing the basic transform set
Rotates every vertex position and normal around the chosen axis
(x, y, or z) by <degrees>. Bone pivots also rotate so the skeleton
stays in sync. Bounds are recomputed from rotated positions
(axis-aligned bbox can grow under non-90° rotations).

Standard right-hand rule: positive degrees rotate counter-clockwise
when looking down the axis from positive infinity. Math is
hand-rolled (no glm::rotate dependency) for clarity at the call
site.

Together with --scale-mesh and --translate-mesh this completes the
basic transform set. Useful for "imported mesh is on its side" or
"the orientation flipped between Blender and Wowee" cleanup.

Verified: 2×2 plane on XY rotated 90° around X correctly lands on
XZ (bounds (-1,0,-1)..(1,0,1)); bad axis 'w' rejected with exit 1.
Brings command count to 219 — kArgRequired entry 200.
2026-05-07 06:34:52 -07:00
Kelsi
9f50e23016 feat(editor): add --gen-texture-noise smooth value-noise PNGs
Synthesizes a deterministic smooth-noise PNG from a seed. Useful
for terrain detail overlays, dirt/grass blends, magic-fog
backdrops — anywhere a "natural-looking" pseudo-random texture
beats a flat color or grid.

Algorithm: 16×16 random lattice (LCG with numerical-recipes
constants), bilinearly interpolated per pixel with smoothstep on
the cell-local coords so seams don't show as bands. Cheaper than
perlin and produces a similar visual signal at this resolution.

Output is grayscale (R==G==B) so users can tint externally with
their own pipeline. Default 256×256, configurable W/H. Identical
seeds produce byte-identical PNGs across platforms (LCG is
dependency-free and platform-stable).

Verified: seed=42 + seed=42 → diff finds zero byte differences;
seed=42 + seed=99 → diff -q reports "differ" as expected. Brings
command count to 218.
2026-05-07 06:22:08 -07:00
Kelsi
b9da18b049 feat(editor): add --strip-mesh for shrinking animated meshes to static-only
Drops bones and/or animations from a WOM in place. Use case: a
model imported with full skeleton + anims that will only ever be
placed as static decoration — there's no point shipping the bone
data, and stripping it shrinks the file substantially.

Flags:
  --bones    drop bones AND animations (anims reference bones, so
             keeping anims with no bones is meaningless). Vertex
             skinning weights reset to identity-on-bone-0 so a
             renderer that expects them doesn't read stale indices.
  --anims    drop only animations (keep bones for posed-but-static
             use)
  --all      shorthand for --bones

Default (no flags) refuses with exit 1 so the user explicitly opts
in to destruction. Unknown flags also fail. Reports before/after
counts and byte delta so the user sees what they saved.

Verified: no-flags refuses; --all on already-static cube reports
0→0 / +0 bytes correctly; --foo rejected. Brings command count
to 217.
2026-05-07 06:09:46 -07:00
Kelsi
f8337ee73f feat(editor): add --scale-mesh and --translate-mesh basic transforms
Two in-place mesh transforms covering the most common authoring
fix-ups:

--scale-mesh <wom-base> <factor>
  Multiplies every vertex position, bone pivot, animation
  translation keyframe, and bounds (min/max/radius) by <factor>.
  Normals are unchanged (uniform scale preserves direction).
  Useful for "I imported this OBJ but it's the wrong size" fixes.
  Factor must be positive + finite.

--translate-mesh <wom-base> <dx> <dy> <dz>
  Offsets vertices, bone pivots, and bounds by (dx, dy, dz).
  Animation keyframes are bone-local so they're left alone — only
  pivots shift. Radius stays constant (rigid translation).

Verified: unit cube scale 3x → bounds ±1.5, radius 2.598;
translate (10, 0, 0) → bounds (8.5,-1.5,-1.5)..(11.5,1.5,1.5);
negative scale (-1) rejected with exit 1. Brings command count
to 216.
2026-05-07 05:57:33 -07:00
Kelsi
a65300d854 feat(editor): add --info-mesh single-mesh detail aggregator
One-shot composite of what --info-batches and per-mesh stats spread
across multiple commands. Reports name, version, counts (vertices/
triangles/bones/anims/batches/textures), bounds (min/max/radius),
per-batch detail (indexStart/indexCount/triangles/blend mode/texture
name), and the full texture-slot table.

Both human and --json output modes. Useful authoring command: pass
a WOM and see everything about it without running three sub-
commands.

Verified on a --gen-mesh-textured cube (size 1.5): correctly
reports v3 / 24 verts / 12 tris / 1 batch / 1 texture, bounds
±0.75, batch 0 wired to cube.png with opaque blend. Brings command
count to 214.
2026-05-07 05:44:37 -07:00
Kelsi
432bd3aac3 feat(editor): add --export-project-items-md project-wide items report
Project-wide companion to --export-zone-items-md. Walks every zone
that has items.json, emits one ITEMS.md document with: project
header + total + zone-with-items count, project-wide quality
histogram, then per-zone sections each containing a table
(ID/name/quality/ilvl/displayId/stack).

Easier to scan than running --export-zone-items-md N times — and
the project-wide histogram surfaces the loot landscape across the
whole project at a glance.

Verified on 2-zone project (1 common in zoneA + 1 legendary in
zoneB): histogram shows both qualities best-first, per-zone
sections render with correct counts. Brings command count to 213.
2026-05-07 05:32:07 -07:00
Kelsi
92e28c3de4 feat(editor): add --gen-texture-gradient for two-color linear PNGs
Synthesizes a linear two-color gradient PNG. Direction is "vertical"
(top→bottom, default) or "horizontal" (left→right). Colors use the
same RRGGBB / RGB hex syntax as --gen-texture, with optional
leading '#'.

Useful for the common "fade" cases that solid/checker/grid don't
cover: sky strips, UI backgrounds, glow rings, dirt-on-grass terrain
blends.

Verified: sky.png (87CEEB→FFFFFF vertical 256×256) and a 64×32
horizontal red→blue both write valid PNGs with correctly-parsed
endpoint RGB values reported back; invalid 'notacolor' hex fails
exit 1. Brings command count to 212.
2026-05-07 05:20:24 -07:00
Kelsi
fb775ac208 feat(editor): extend --gen-mesh with cone shape
Adds a cone with apex at +Y, base at Y=0. radius=size/2,
height=size, 24 side segments → 76 verts / 72 tris.

Side normals are computed from the slope (cos/sin of segment angle
× H/sqrt(H²+r²) for XZ, r/sqrt(H²+r²) for Y) so shading shows the
slant correctly. Apex vertex is duplicated per segment so each
triangle carries the segment-specific normal — the cone shades as
a smooth curved surface rather than a faceted prism.

Bottom cap is a separate fan with flat -Y normal. Base at Y=0
(rather than centered) makes it natural to drop on a surface
without manual offset — matches typical "standing on the floor"
placement.

Useful for spikes, party hats, traffic cones, magic-circle pillars.
Help text on both --gen-mesh and --gen-mesh-textured updated to
advertise the new shape.

Verified: cone 2.0 → 76 verts / 72 tris / bounds (-1,0,-1) to
(1,2,1). The 6-shape primitive set is now cube/plane/sphere/cylinder/
torus/cone.
2026-05-07 05:08:39 -07:00
Kelsi
3dc18d96ab feat(editor): add --validate-project-items orchestrator
Project-wide wrapper around --validate-items. Spawns the binary
per-zone (only zones that have items.json) so each zone's full
error report streams through, then aggregates a final tally. Exit
1 if any zone fails.

Skips zones without items.json — those have nothing to validate
and shouldn't count as failures. Handles the edge case where no
zones in the project have items at all (returns "nothing to
validate" exit 0).

Verified: 3-zone project (2 valid + 1 with quality=99) → per-zone
output streamed, summary "passed: 2 failed: 1", true exit code 1
when any zone fails. Brings command count to 211.
2026-05-07 04:57:03 -07:00
Kelsi
90cd64c88d feat(editor): add --export-zone-items-md markdown items report
Renders items.json as a Markdown report grouped by quality, best
first (Artifact → Legendary → Epic → Rare → Uncommon → Common → Poor).
Header shows total count + zone name; quality breakdown table
summarizes the distribution; per-quality sections list ID / name /
itemLevel / displayId / stack with right-aligned numeric columns.

Drops cleanly into design docs, PR descriptions, and GitHub Pages
— one rendered page communicates the loot landscape better than
scrolling through JSON.

Used find()/iterators instead of operator[] in the per-quality
loops so the bucket map doesn't grow phantom empty entries (caught
by the "qualities (used)" line reporting 7 instead of the actual 3
in the first test run).

Verified: 3-item zone (1 common / 1 uncommon / 1 legendary) →
ITEMS.md sections appear in best-first order, quality count line
reports 3 not 7. Brings command count to 210.
2026-05-07 04:45:25 -07:00
Kelsi
98c9b3c624 feat(editor): add --gen-mesh-stairs procedural staircase
Procedural straight staircase along +X with N steps and configurable
rise/run/width. Each step is a closed box (24 verts / 12 tris) with
flat per-face normals, sharing no vertices with neighbors so shading
reads as crisp steps without smoothing tricks.

Defaults (5 steps / 0.2 / 0.3 / 1.0) yield ~1m tall × 1.5m long ×
1m wide — a believable single flight. Step count clamped to 1..256
so a typo can't accidentally generate a million-step mesh.

Useful for level-design placeholders ("I need a staircase up to
this platform"), test-bench geometry for camera/movement work, and
quick prototyping of stepped terrain.

Verified: 5-step default → 120 verts / 60 tris / 1.5L × 1H × 1W
span; custom 8/0.25/0.4/1.5 → 192 verts / 96 tris / 3.2 × 2.0 × 1.5;
steps=0 → exit 1 with range error. Brings command count to 209.
2026-05-07 04:23:11 -07:00
Kelsi
3404910231 feat(editor): extend --gen-mesh with torus shape
Adds a torus around the Y axis. Major radius (ring center distance
from origin) = size/2, minor radius (tube thickness) = size/8 — the
4:1 ratio reads as a ring rather than a fat donut. 32 ring segments
× 16 tube segments → 561 verts / 1024 tris.

Per-vertex normals point from the tube center outward so shading
shows the curvature smoothly. UVs: u=0..1 around the ring, v=0..1
around the tube cross-section.

Useful for jewelry placeholders (rings, pendants), cog teeth, magic
circles, and any other ring-shaped prop.

Verified: torus 2.0 → 561 verts / 1024 tris / bounds ±1.25 in X/Z
(R+r) and ±0.25 in Y (r), exit 0. Help text updated to advertise
the new shape on both --gen-mesh and --gen-mesh-textured.
2026-05-07 04:11:51 -07:00
Kelsi
fad0884d31 feat(editor): add --add-texture-to-zone for importing existing PNGs
Imports an existing PNG into a zone directory. Companion to
--gen-texture (procedural placeholder) for the "I have an artist-
painted texture, get it into my project" workflow. Optional
<renameTo> argument lets the user store the PNG under a project-
specific name.

Safety:
- Refuses non-.png input with a hint to use --convert-blp-png first
  (so .blp/.tga don't get silently shoved into a tree that won't
  render them)
- Auto-appends .png to renameTo if user supplied just a stem
- Idempotent re-runs: byte-identical destination → no-op exit 0
- Different-content destination → refuses to overwrite, exit 1

Closing line suggests --add-texture-to-mesh as the next step so
the user has the full workflow in front of them.

Verified all five paths: plain copy, rename+auto-ext, idempotent
re-run (no-op exit 0), conflict (different bytes, exit 1), wrong
extension (.blp, exit 1). Brings command count to 208.
2026-05-07 04:00:21 -07:00
Kelsi
6f0a60ce83 feat(editor): extend --gen-mesh with cylinder shape
Adds a capped cylinder primitive along the Y axis: radius=size/2,
height=size, 24 side segments. Smooth enough for pillars and
torches without exploding the vertex count (102 verts / 96 tris).

Per-face normals on the caps point ±Y; side faces get smooth
per-vertex radial normals so the cylinder shades as a curved
surface. UVs: side wraps the texture once around (u=0..1 around the
ring, v=0..1 top-to-bottom); caps map [0..1] from a square sampled
at the disc.

The handler's "shape must be" error message updated to include
cylinder. Help text on both --gen-mesh and --gen-mesh-textured
updated to advertise the new shape.

Verified: pillar.wom written with 102 verts / 96 tris / correct
bounds / single opaque batch; --export-obj round-trip preserves
both counts; bad shape ('triangle') rejected with updated error.
2026-05-07 03:48:58 -07:00
Kelsi
f3578a14cc feat(editor): add --list-project-meshes cross-zone mesh inventory
Project-wide companion to --list-zone-meshes. Walks every zone in
<projectDir>, collects every .wom across all zones, sorts by
triangle count descending, and emits a global per-mesh table with
the originating zone in the first column.

Useful for project-wide outlier detection ("which mesh anywhere in
the project is the heaviest?") and for mesh-sharing audits where
many zones might reference the same heavy model. Both human (table)
and --json output modes.

Verified on 2-zone synthetic project (3 meshes total): table sorted
by triangle count descending (sphere 384 → cube 12 → plane 2),
zone column correctly identifies origin, totals row matches sum of
individual rows. Brings command count to 207.
2026-05-07 03:37:41 -07:00
Kelsi
cf7b5c66e3 feat(editor): add --set-item for in-place item field edits
Edit existing item fields without recreating the record. Lookup is
by id by default, '#N' for index. Only specified flags are changed —
everything else (including any extra hand-added fields) is preserved.

Supported flags: --name, --quality, --displayId, --itemLevel,
--stackable. Each takes one positional value. Range checks mirror
--validate-items (quality 0..6, stackable 1..1000) so saved JSON
stays validator-clean.

Unknown flags fail with a "typo?" hint rather than silently no-op,
and an empty flag list is rejected explicitly so the user sees that
nothing happened.

Verified: name + quality + itemLevel updated together (one line each
in the report); --quality 99 → range error exit 1; --foo bar → unknown
flag exit 1; no flags → no-op error exit 1. Brings command count to
206.
2026-05-07 03:26:38 -07:00
Kelsi
3a2bd64970 feat(editor): add --list-zone-meshes per-mesh breakdown
Per-mesh listing of every .wom in a zone. Complements
--info-zone-models-total (aggregate) by surfacing individual mesh
metrics: version, vertex count, triangle count, bone count, batch
count, texture-slot count, and on-disk bytes.

Sorted by triangle count descending so the heaviest meshes float to
the top of the table — useful for spotting outliers ("which mesh is
using 80% of my triangle budget?") and for content audits.

Both human (table) and --json output modes. Verified on synthetic
3-mesh zone (sphere 384 tris, cube 12, plane 2): table sorted
descending, totals row matches sum of individual rows, sub-dir mesh
shown with its relative path. Brings command count to 205.
2026-05-07 03:15:05 -07:00
Kelsi
0014ed4b31 feat(editor): items.csv now emitted by --export-zone-csv
Extends the existing CSV exporter so items.json gets the same
spreadsheet treatment as creatures/objects/quests. Reads items.json
inline (no dedicated editor class needed yet) and emits items.csv
with columns: index,id,name,quality,itemLevel,displayId,stackable.

Quotes the name field via the existing csvEsc helper so commas in
names don't break the format. Empty-zone error message updated to
include 'items' in the list of expected content.

Verified: zone with creatures.json + items.json (2 entries) emits
both creatures.csv and items.csv, header line present, rows sorted
by insertion order, no duplication when re-running.
2026-05-07 03:04:18 -07:00