Commit graph

4025 commits

Author SHA1 Message Date
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
Kelsi
ff1a974e3a feat(editor): add --info-item single-item detail view
Detail view for one record from items.json. Lookup is by id by
default; prefix the argument with '#' (e.g., "#3") to look up by
0-based array index instead. Useful for inspecting all fields
without sifting through the full --list-items table.

Surfaces any extra fields the user added by hand (description, lore,
flavor, custom flags) in a separate section so the command stays
useful as the schema evolves without code changes here.

Verified all three paths: id=7 → finds Sword, shows quality "rare",
surfaces extra "description" field; "#1" → finds Helm at index 1
("epic"); id=999 → no match, exit 1 with clear error. Brings
command count to 204.
2026-05-07 02:53:50 -07:00
Kelsi
8588279a88 feat(editor): add --add-texture-to-mesh manual texture binder
Manual companion to --gen-mesh-textured. Binds an existing PNG into
a WOM's texturePaths and points the chosen batch (default 0) at it.
Useful when the texture wasn't synthesized — e.g., an artist-painted
PNG, a converted .blp, or a texture shared across multiple meshes.

Slot dedup: if the leaf path already exists in texturePaths, the
existing slot is reused rather than appended, so repeated invocations
don't bloat the table. Warns (but doesn't fail) if the PNG isn't
sitting next to the WOM, since runtime path resolution is relative
to the WOM's own directory.

Verified: bind once → slot 1 added (slot 0 is the placeholder from
--gen-mesh), batch wired; rebind same path → slot 1 reused, count
stays at 2; missing PNG → exit 1 with clear error. Brings command
count to 203.
2026-05-07 02:43:32 -07:00
Kelsi
b882709d30 feat(editor): add --gen-mesh-textured one-shot mesh+texture composer
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
Pairs --gen-mesh and --gen-texture into a single call, then patches
the resulting WOM so its texturePaths[0] points at the freshly-
written PNG sidecar. Output is a textured model that renders out of
the box — no need to chain three commands by hand and remember the
relative-path conventions.

Layout:
  <wom-base>.wom    — mesh with texturePaths[0] = "<base>.png"
  <wom-base>.png    — 256x256 texture (color or pattern spec)

The PNG path stored in the WOM is just the leaf — the runtime
resolves textures relative to the model file's own directory, so
the bound model is portable as long as the .wom and .png ship
together.

Verified end-to-end: textured cube emits both files, --info-batches
shows "check.png" in the texture column for batch 0 (proving the
binding is permanent in the saved WOM, not just in memory). Brings
command count to 202.
2026-05-07 02:32:43 -07:00
Kelsi
099da16f0b feat(editor): add --info-project-items cross-zone item rollup
Project-wide companion to --list-items. Walks every zone in
<projectDir>, sums item counts and per-zone quality histograms,
emits a project-wide quality histogram plus a per-zone breakdown
table.

Useful for "do my zones have enough loot variety?" capacity checks
and balance-passes — surfacing that one zone has 50 commons and 0
rares is the kind of thing only the rollup makes obvious.

Verified on 2-zone project (4 items): project totals correct,
quality histogram (2 common / 1 uncommon / 1 legendary) matches
per-zone counts, JSON form parses cleanly. Brings command count
to 201.
2026-05-07 02:22:26 -07:00
Kelsi
f4e90b387c feat(editor): add --validate-items schema check + 200-command milestone
Catches the issues --add-item / --clone-item only enforce on
insertion (e.g., duplicate ids if items.json was hand-edited
outside the CLI), plus general field-range issues:
  - missing/zero/non-uint id
  - missing or empty name
  - quality outside 0..6
  - itemLevel > 1000 (suspicious typo)
  - stackable == 0 or > 1000 (out of range)
  - duplicate id across multiple item indices

Reports per-error lines with item index and the offending field;
exit 1 if any error.

Verified: clean 2-item zone → PASS exit 0; injected file with one
clean entry plus one with all-five-error-types fields → 5 errors
detected (4 field-level + 1 duplicate-id), exit 1.

Brings command count to 200 — round-number milestone for the
headless CLI surface.
2026-05-07 02:12:35 -07:00
Kelsi
86b4527eb2 feat(editor): add --clone-item for items.json parity with clone-creature/object/quest
Duplicates the entry at given 0-based index. Auto-assigns the
smallest unused positive id so numbering stays contiguous. Optional
<newName> argument overrides the cloned name; without it the new
entry appends " (copy)" to the original name (matches the
established convention from --clone-creature).

Useful for variant items: clone "Iron Sword" → "Steel Sword" and
edit just the displayId, instead of typing every field again.

Verified: clone with no name override → "Iron Sword (copy)" id=2;
clone with explicit "Steel Sword" → id=3 (next free); fields
(quality, ilvl, displayId) preserved verbatim; out-of-range index
99 fails with exit 1. Brings command count to 199.
2026-05-07 02:02:35 -07:00
Kelsi
a7ba6bf711 feat(editor): add --remove-item completing the items CRUD set
Removes the entry at given 0-based index from <zoneDir>/items.json.
Mirrors --remove-creature/--remove-object/--remove-quest semantics:
bounds-checked, file rewrites on success, exit 1 on out-of-range or
malformed JSON. Reports the removed item's name + id in the success
line so the user has feedback on what they just dropped.

Items CRUD set is now complete: --add-item / --list-items /
--remove-item. Together they form the items.json half of the new
content-creation direction (textures/meshes/items) the user asked
for in the prior batch.

Verified: 3-item zone → remove idx 1 ('Bread', id=2) leaves 2 items
intact and renumbered; out-of-range index 99 fails with exit 1 and
clear error. Brings command count to 198.
2026-05-07 01:52:43 -07:00
Kelsi
6eeac1c861 feat(editor): add --list-items companion to --add-item
Pretty-prints every record in <zoneDir>/items.json as a table with
idx / id / itemLevel / stackable / quality (named) / displayId /
name columns. Also supports --json for machine-readable output
(emits the items array verbatim).

Verified on a 3-item zone: table output aligns columns, quality
labels resolve correctly (common/uncommon/legendary), --json mode
emits a clean array consumable by jq or downstream tools. Brings
command count to 197.
2026-05-07 01:43:12 -07:00
Kelsi
dd36182cd3 feat(editor): add --add-item, introducing zone items.json content type
Introduces a new per-zone content file alongside creatures.json /
objects.json / quests.json. Schema: {"items": [{id, name, quality,
displayId, itemLevel, stackable}, ...]}. Inline JSON manipulation
via nlohmann::json — items are simple records and don't yet need
NpcSpawner-style infrastructure.

ID assignment: pass 0 (or omit) to auto-pick the smallest unused
positive integer so numbering stays contiguous. Explicit IDs are
honored. Duplicate IDs rejected with exit 1 so collisions are
visible.

Quality is 0..6 (poor/common/uncommon/rare/epic/legendary/artifact)
— summary line maps the number to the human name so users see what
they wrote.

Verified: auto-id sequential (1, 2) → explicit id (99) honored →
duplicate id rejected with exit 1; JSON schema stable, quality
labels correct, file auto-created on first call. Brings command
count to 196.
2026-05-07 01:33:58 -07:00
Kelsi
d28094593c feat(editor): add --gen-mesh for procedural WOM primitives
Synthesizes a procedural WOM model with proper per-face normals,
planar UVs, a bounding box, and a single batch covering all indices
so the model renders immediately in the editor without further
processing.

Three shapes:
- cube   — 24 verts / 12 tris, axis-aligned, ±size/2 (per-face flat
           normals, requires duplicated verts at edges)
- plane  — 4 verts / 2 tris on the XY plane (Z=0), ±size/2
- sphere — UV sphere, 16 segments × 12 stacks, radius=size/2
           (221 verts / 384 tris)

Default size=1.0 (unit cube/plane/unit-diameter sphere). Pair with
--gen-texture to make a ready-to-place model from scratch.

Verified all three shapes write valid v3 WOM files with correct
vertex/triangle counts, correct bounds, and a single opaque batch;
cube.wom round-trips cleanly through --export-obj (24 verts in →
24 verts out). Brings command count to 195.
2026-05-07 01:24:01 -07:00
Kelsi
6cf2043de4 feat(editor): add --gen-texture for synthesizing placeholder PNG textures
Lets users add a working texture to a project without firing up an
external image editor. Useful for prototyping new meshes, filling
out a zone before art is final, or generating CI test fixtures.

Three spec modes:
- "RRGGBB" or "RGB" hex (case-insensitive, optional leading '#') →
  solid fill
- "checker" → 32x32 black/white checkerboard
- "grid" → black background with white 1-px grid every 16

Default 256x256, configurable via optional W H positional args
(clamped to 1..8192). Writes via stbi_write_png so the output is a
standard PNG every WoW open-format pipeline already accepts.

First commit in the new "add content" direction (items / textures /
meshes) the user requested. Verified: solid hex (ff0000), 3-char
hex (8a3), checker, grid all produce valid PNGs of correct sizes;
'notacolor' input fails with exit 1 and a clear error. Brings
command count to 194.
2026-05-07 00:52:29 -07:00
Kelsi
61fc3847c3 feat(editor): add --export-data-tree-md migration progress markdown report
Generates MIGRATION.md with: status badge ("100% migrated", "Mostly
migrated", "Partially migrated", "Migration pending"), summary
counts, per-pair table with shares, and recommended next steps as
copy-pasteable wowee_editor invocations. Drops cleanly into PR
descriptions, CI artifacts, or GitHub Pages status pages.

Verified: 50%-migrated test tree → "Partially migrated" badge,
correct per-pair shares (.m2→.wom 100%, .blp→.png 0%), recommended
steps point at the actual srcDir. Brings command count to 193.
2026-05-07 00:42:55 -07:00
Kelsi
6057190a62 feat(editor): add --list-data-tree-largest for migration prioritization
Top-N largest proprietary files (.m2/.wmo/.blp/.dbc) in <srcDir>.
Helps users decide what to migrate first when sequencing the work
across a large extracted Data tree — convert the heaviest files
first to free the most disk space soonest.

Each row annotates whether an open sidecar already exists ("migrate")
or is still pending ("pending"), so the heavy hitters that are
already migrated are visible at a glance.

Default N = 20 (one terminal page); pass an explicit N for a
different cutoff. Header reports total proprietary bytes plus the
share captured by the top-N rows so users know how much of the work
the displayed list represents.

Verified: 3-file mixed tree (100/50/10 KB) → ranked descending by
size, .m2 (with sidecar) shows "migrate", others show "pending",
total/shown bytes match. Brings command count to 192.
2026-05-07 00:33:34 -07:00
Kelsi
7c0edbb421 feat(editor): add --bench-migrate-data-tree wall-clock perf benchmark
Times each step of --migrate-data-tree (m2/wmo/blp/dbc) end-to-end
and reports wall-clock per step plus the total. Useful for capacity
planning ("how long will the full extracted Data tree take?") and
regression detection (a recent change shouldn't make M2 conversion
2x slower).

Sub-batches dispatched the same way --migrate-data-tree dispatches
them, so the timings are exactly what the user will experience
running the migration. Both human (table with share %) and --json
output modes.

Verified: 4-format synthetic tree → all 4 steps timed individually,
share percentages sum to 100, total reported in both ms and seconds.
M2 + WMO dominate the share even on empty inputs (AssetManager init
overhead surfaces here, useful insight). Brings command count to 191.
2026-05-07 00:24:28 -07:00
Kelsi
97519645a6 feat(editor): add --audit-data-tree CI gate for migration completeness
Non-destructive CI gate that exits 1 if any proprietary file
(.m2/.wmo/.blp/.dbc) lacks a matching open sidecar at the same
(parent, stem). The pre-strip safety check: don't run --strip-data-
tree until this returns exit 0.

Lists the missing sidecars (capped at 50) so the user can re-run
--migrate-data-tree to fill the gaps. Per-extension breakdown
identifies which converter to investigate if specific formats are
underrepresented.

Completes the data-tree workflow:
  --info-data-tree     visibility
  --migrate-data-tree  fill sidecars
  --audit-data-tree    confirm 100% before stripping
  --strip-data-tree    delete migrated proprietary originals

Verified: empty tree → PASS exit 0; fully migrated → PASS exit 0;
gaps (1 .m2 + 1 .blp lacking sidecars) → FAIL exit 1 with both
files listed sorted, per-ext counts correct. Brings command count to
190.
2026-05-07 00:15:02 -07:00
Kelsi
97d52802b7 feat(editor): add --strip-data-tree to delete migrated proprietary files
Destructive cleanup that completes the data-tree migration workflow.
Walks <srcDir>, finds every proprietary file (.m2/.wmo/.blp/.dbc)
that already has a matching open sidecar at the same (parent, stem),
and deletes the proprietary file. Files without sidecars are
preserved (still need migration).

Honors --dry-run for safe previews. Defaults to actually delete
(matches --strip-zone convention).

Recommended workflow:
  --info-data-tree       see migration share
  --migrate-data-tree    fill in missing sidecars
  --strip-data-tree --dry-run    confirm kill list
  --strip-data-tree              apply

Verified: 11-file mixed tree → dry-run preserves all 11; apply
removes exactly the 4 that have sidecars (foo.m2, castle.wmo,
sky.blp, Spell.dbc); the 3 unmatched proprietary files
(bar.m2, grass.blp, Item.dbc) are correctly kept. Brings command
count to 189.
2026-05-07 00:05:54 -07:00
Kelsi
8b01ccd77f feat(editor): add --info-data-tree for non-destructive migration progress
Companion to --migrate-data-tree. Walks <srcDir> recursively, counts
files per format pair (.m2 vs .wom, .wmo vs .wob, .blp vs .png, .dbc
vs .json), and reports per-pair counts plus an overall migration
share — the fraction of source files that already have an open
sidecar present.

A "sidecar" is matched by parent dir + stem (case-insensitive ext),
so the comparison is sound across nested trees. Orphan open files
(present without a matching proprietary) are also reported — those
are the candidates for keeping after stripping the originals.

Designed to drop into CI dashboards: a 100% migration share means
every proprietary asset has a deterministic open counterpart on disk
and the originals are safe to delete.

Verified on a mixed tree: 8 proprietary / 4 sidecars (50% share)
correctly reported per pair, plus 1 orphan .json detected. Brings
command count to 188.
2026-05-06 23:57:13 -07:00
Kelsi
b4ae83e639 feat(editor): add --migrate-data-tree end-to-end open-format orchestrator
Composes the four bulk converters into a single one-shot migration
of an extracted Data tree. Runs --convert-m2-batch → --convert-wmo-
batch → --convert-blp-batch → --convert-dbc-batch in order, streams
each step's full output through, then emits an aggregate summary
with per-step PASS/FAIL.

The headline open-format command: point it at a freshly extracted
Data dir and every .m2/.wmo/.blp/.dbc gets converted to its open
counterpart in one call. Idempotent — re-running on a partially-
migrated tree just re-attempts the originals (sidecars are
deterministic).

Verified: 4-format synthetic tree → all four sub-batches dispatched,
aggregate summary correctly reports per-step rc, exit 1 when any
sub-converter reports failures (verified with true exit code, not
shell pipeline gotcha). Brings command count to 187.
2026-05-06 23:44:23 -07:00
Kelsi
d93bf30978 feat(editor): add --convert-dbc-batch for bulk DBC→JSON migration
Final commit in the four-format batch-converter set. Walks <srcDir>
recursively for every .dbc file (case-insensitive) and re-invokes
--convert-dbc-json per file, writing a .json sidecar next to each
source.

The batch converters now cover the full proprietary→open transition:
  --convert-m2-batch   .m2  → .wom
  --convert-wmo-batch  .wmo → .wob (skipping _NNN group files)
  --convert-blp-batch  .blp → .png
  --convert-dbc-batch  .dbc → .json
Run all four against an extracted Data tree to migrate it end-to-end
to the open format ecosystem.

Verified: 3 .dbc files (case-insensitive, sub-dir) discovered;
.json file in same tree skipped; exit 1 on failures. Brings command
count to 186.
2026-05-06 23:31:38 -07:00
Kelsi
8f890ef1f3 feat(editor): add --convert-blp-batch for bulk BLP→PNG migration
Walks <srcDir> recursively for every .blp file (case-insensitive) and
re-invokes --convert-blp-png per file. The single-file converter
writes the .png as a sidecar next to the source by default, so a
batched run mirrors the standard "PNG sidecar everywhere" layout
that the editor's open-format runtime expects.

Verified discovery: 3 placeholder .blp files (lowercase, uppercase,
sub-dir) found correctly; .png file in same tree skipped; all fail
conversion as expected for empty bytes; exit 1 on failures. Brings
command count to 185.
2026-05-06 23:19:38 -07:00
Kelsi
3e2580e8b5 feat(editor): add --convert-wmo-batch for bulk WMO→WOB migration
Sibling to --convert-m2-batch. Walks <srcDir> recursively for every
.wmo file (case-insensitive) and re-invokes --convert-wmo per file
via a child process so the existing root-WMO + group-loading logic
is reused verbatim.

Skips group files (e.g. Stormwind_001.wmo) since the root WMO
converter already pulls those in transitively. Detection: stem ends
in _NNN with NNN being three digits.

Verified: 5 .wmo files → 2 root candidates, 3 group files correctly
skipped, summary line accurate. Brings command count to 184.
2026-05-06 23:07:34 -07:00
Kelsi
5efd5b157d feat(editor): add --convert-m2-batch for bulk M2→WOM migration
Walks <srcDir> recursively for every .m2 file (case-insensitive,
including subdirs) and re-invokes --convert-m2 per file via a child
process so the existing single-file logic — AssetManager init, skin
file resolution, fromM2() pipeline — is reused verbatim. Per-file
[ok]/[FAIL] line streamed live; aggregate summary at the end.

Designed to migrate an entire creature/world model dump in one go.
This is the headline open-format conversion: every .m2 in a
proprietary data tree gets turned into a .wom open-format model. Pair
with the upcoming --convert-wmo-batch / --convert-blp-batch /
--convert-dbc-batch to migrate a complete extracted Data tree.

Verified discovery: 3 placeholder .m2 files (one in subdir, one
.M2 uppercase) found correctly; .txt skipped; all fail conversion as
expected for empty bytes; exit 1 on any failure. Brings command
count to 183.
2026-05-06 22:55:39 -07:00
Kelsi
f1bd7b7f1f feat(editor): add --remove-project-orphans for pre-pack cleanup
Destructive companion to --list-project-orphans. Reuses the same
reference-collection + orphan-detection logic, then deletes the
resulting .wom/.wob files. Honors --dry-run for safe previews.

Completes the list/remove cycle for unreferenced model files: run
--list-project-orphans to audit, then --remove-project-orphans
--dry-run to confirm, then drop --dry-run to actually clean. Useful
right before --pack-wcp so the archive doesn't carry dead weight.

Verified: dry-run preserves files (3 reported, all still present);
real run on a copy removes all 3 with no failures, freeing 1.2 KB.
Brings command count to 182.
2026-05-06 22:42:29 -07:00
Kelsi
0eb20a4069 feat(editor): add --list-project-orphans to find unreferenced models
Inverse of --list-zone-deps. Walks every zone in <projectDir>,
collects the set of .wom/.wob files on disk plus the set of paths
actually referenced by objects.json placements + WOB doodad lists,
and reports any model files in the first set but not the second.

Useful pre-pack cleanup — orphans bloat .wcp archives without
contributing to gameplay. The output table shows zone + path + bytes
so users can decide which to delete.

Comparison normalizes paths by stripping extensions and matching both
the full relative path and the leaf basename, so unqualified refs in
objects.json (e.g. just "cube_a" without ".wom") still resolve.

Verified: empty objects.json → 3 orphans across 2 zones; after
--add-object cube_a → only 2 orphans remain (cube_a correctly removed
from the list). Brings command count to 181.
2026-05-06 22:30:10 -07:00