Commit graph

3948 commits

Author SHA1 Message Date
Kelsi
33b231b8a2 feat(editor): add --info-zone-density for spawn distribution analysis
Per-tile content density. Catches sparse zones (5 mobs across 16
tiles → boring) and over-stuffed ones (200 mobs in 1 tile → frame-
rate bomb). Reports overall averages plus per-tile bucket counts:

  wowee_editor --info-zone-density custom_zones/MyZone

  Zone density: custom_zones/MyZone
    tiles      : 4
    totals     : 47 creatures, 23 objects, 8 quests
    per-tile   : 11.75 creatures, 5.75 objects, 2.00 quests

    Per-tile breakdown:
      tile        creatures  objects
      (28, 30)            12        7
      (29, 30)            18        4
      (29, 31)             9        8
      (30, 30)             8        4

Spawn-to-tile bucketing reverses the WoW grid transform from world
position back to tile (tx, ty) coords. Out-of-zone spawns silently
drop (they show up in --check-zone-refs / --check-zone-content as
their own warning class).

Useful for difficulty-curve work ('how packed is this hub vs this
zone-edge?'), perf budgeting ('which tile do I need to lod-out
first?'), and content-pacing reviews ('is the early game too empty?').

JSON mode emits per-tile records for dashboards. Verified on a
1-tile mvp-zone: 1 creature + 1 object + 1 quest, all bucketed
into the correct tile (28, 30).
2026-05-06 17:55:15 -07:00
Kelsi
f4e8623d58 feat(editor): add --info-objects-by-path + --info-objects-by-type analytics
Object-side counterparts to --info-creatures-by-faction/-level. Two
analytics commands for placement audits:

  wowee_editor --info-objects-by-path $Z/objects.json

  Objects by path: ... (47 total, 12 unique)
    count   share   path
       18    38.3%  World/Generic/Tree.m2
        9    19.1%  World/Generic/Lamp.m2
        4     8.5%  World/Building/Inn.wmo
        ...

  wowee_editor --info-objects-by-type $Z/objects.json

  Objects by type: ...
    M2  : 38  (scale 0.80-2.50, avg 1.12)
    WMO : 9   (scale 1.00-1.00, avg 1.00)

--info-objects-by-path: most-used model paths first. Catches
'this looks repetitive, diversify the doodads' design feedback
and surfaces texture-budget hot spots (one model used 50× pulls
its textures into the working set 50×).

--info-objects-by-type: M2 vs WMO split + per-type scale stats.
Catches scale outliers ('this WMO is at 0.001 scale, did you mean
1.0?') and gives composition sense (mostly props vs mostly
buildings).

JSON modes emit per-path / per-type records for dashboards.
Verified on a 4-object seed (3 M2 + 1 WMO with mixed scales):
correctly reports Tree.m2 used 2× (50%), M2 scale range 0.80-1.50
avg 1.10.
2026-05-06 17:46:51 -07:00
Kelsi
1797ffd280 feat(editor): add --info-pack-budget for per-extension WCP byte breakdown
Where --info-wcp shows file counts per category, this drills into
per-extension byte costs so users can spot what's bloating an
archive before shipping:

  wowee_editor --info-pack-budget custom_zones/MyZone.wcp

  WCP budget: custom_zones/MyZone.wcp
    total: 47 file(s), 2.34 MB

    ext           count        bytes      KB    share
    .whm              4      1683456  1644.0   70.3%
    .wob              3       451200   440.6   18.8%
    .wom             12       163840   160.0    6.8%
    .json             8        85120    83.1    3.6%
    .woc              1        12672    12.4    0.5%

Sorted by bytes descending so the heaviest contributors surface
first. Useful for:
- Spotting accidental .glb/.obj inclusion in shipping packs
  (`--pack-wcp` should run after `--strip-zone` to keep
  derived outputs out)
- Capacity budgeting when targeting a max-pack-size
- Comparing pre/post compression ratios

Pairs with --info-wcp (counts), --list-wcp (full file list),
--diff-wcp (compare two packs), --info-pack-budget (this one,
byte costs).

Verified on a freshly-mvp-zone packed WCP: 6 files / 0.17 MB
correctly broken down (whm 84%, wot 14.9%, json 1.1%).
2026-05-06 17:38:28 -07:00
Kelsi
baf54d5e47 feat(editor): add --info-zone-water for water-layer aggregation
Aggregates water-layer stats across every tile in a zone. Useful for
confirming a 'lake zone' actually has water, budgeting water-heavy
zones, or auditing what liquid types appear (water vs ocean vs magma
vs slime affects gameplay rules):

  wowee_editor --info-zone-water custom_zones/Z

  Zone water: custom_zones/Z
    loaded tiles : 1
    water chunks : 0 (out of 256 possible)
    total layers : 0
    (no water in this zone)

  wowee_editor --info-zone-water custom_zones/Stranglethorn

  Zone water: custom_zones/Stranglethorn
    loaded tiles : 4
    water chunks : 387 (out of 1024 possible)
    total layers : 412
    height range : 12.40 to 18.50

    By liquid type:
      water (0): 380 layer(s)
      ocean (1): 28 layer(s)
      magma (2): 4 layer(s)

Per-chunk water can have multiple layers (different liquid types or
height regions overlapping). Liquid types: 0=water, 1=ocean, 2=magma,
3=slime — different gameplay rules apply (oceans are swimable, magma
is damage-over-time, etc.).

JSON mode emits per-type layer counts + height range for programmatic
audit. Verified on a freshly-scaffolded zone: correctly reports 0
water chunks.
2026-05-06 17:30:05 -07:00
Kelsi
8257ae72db feat(editor): add --validate-project for whole-project validation gate
Multi-zone wrapper around --validate-all. Walks every zone in a
project and runs the per-format validators (WOM/WOB/WOC/WHM).
Aggregates pass/fail per zone with file-level breakdown:

  wowee_editor --validate-project custom_zones

  validate-project: custom_zones
    zones        : 12 (1 failed)

    zone                       files  failed  errors  status
    AshenForest                    47       0       0  PASS
    BoneOasis                      31       0       0  PASS
    CrystalCaverns                 28       2      14  FAIL
    ...

    1 zone(s) failed validation

Designed for CI gates before --pack-wcp (or before tagging a release):
one command checks the whole project's binary integrity, exits 1 if
anything's broken with a clear breakdown of which zone went wrong.

Pairs with --validate-all (single-zone, all formats) and the
per-format validators (--validate-wom etc.). Three levels of
granularity now:
  --validate-{wom,wob,woc,whm}  single file
  --validate-all                single zone (or any dir)
  --validate-project            entire project

JSON mode emits per-zone records (totalFiles + failedFiles +
totalErrors) for dashboard consumption. Verified on a 2-zone
project with one tile having .woc built and one without — both
zones PASS, exit 0.
2026-05-06 17:21:57 -07:00
Kelsi
e0ed2ab58e feat(editor): add --info-creatures-by-faction + --info-creatures-by-level
Two analytics commands for combat-balance work. Where --info-creatures
gives totals + behavior counts, these give the distributions:

  wowee_editor --info-creatures-by-faction $Z/creatures.json

  Creatures by faction: ... (47 total)
    faction    count   share
          7        12    25.5%
         14        29    61.7%
         35         6    12.8%
    (factions: 7=human, 14=monster, 35=neutral, etc.)

  wowee_editor --info-creatures-by-level $Z/creatures.json

  Creatures by level: ... (47 total)
    range : 5 to 32 (avg 14.2)

    level   count  bar
        5       4  ████████████████████████████████████████
        6       3  ██████████████████████████████
        ...
       30       1  ██████████

Faction histogram catches single-faction zones (one giant melee) and
mixed-faction tuning issues. Level histogram catches difficulty-curve
problems (cluster at 5, gap, cluster at 30) and outlier spawns
(level-60 boss accidentally placed in starter area).

ASCII bar chart for level distribution since gameplay tuning is
visual — '60% of mobs are levels 8-12 with a long tail' is more
intuitive as a bar than as numbers. Bars scale to longest bin so
small zones still get usable visualization.

JSON mode emits per-faction / per-level records for dashboards.
Verified on a 4-creature seed (3×faction-14 + 1×faction-35; levels
7/8/12/30): faction percentages and level range/avg both correct.
2026-05-06 17:13:44 -07:00
Kelsi
1eb8232bb8 feat(editor): add --validate-cli-help self-check for documentation drift
Self-check that every flag in kArgRequired (the master list of
commands needing positional args) appears in the help text emitted
by printUsage. Catches drift where a handler+arg-check pair gets
added but the help line is forgotten:

  wowee_editor --validate-cli-help

  CLI help self-check
    kArgRequired entries : 126
    PASSED — every kArgRequired flag is documented

If it ever fails, the missing flags are listed:

    FAILED — 2 flag(s) missing from help text:
      - --new-thing-i-forgot-to-document
      - --another-undocumented-flag

CI integration: add this to the test target so a PR that adds a
new command without docs can't merge. The --info-cli-stats command
gives the surface-size view; this gives the surface-completeness
view.

Verified: ran on the current binary (126 kArgRequired entries),
PASSED — every flag is documented.
2026-05-06 17:04:57 -07:00
Kelsi
d048beaa1e feat(editor): add --info-project-tree for multi-zone project overview
Project-level tree view: every zone with quick counts + bake/viewer
artifact status. --info-zone-tree drills into one zone; this gives
the bird's-eye view across the whole project:

  wowee_editor --info-project-tree custom_zones

  custom_zones/  (2 zones, 2 tiles, 1 creatures, 1 objects, 1 quests)
  ├─ Empty/  (tiles=1, creat=0, obj=0, quest=0)
  │  ├─ name      : Empty
  │  ├─ mapName   : Empty
  │  ├─ artifacts : (none)
  │  └─ status    : empty (only terrain)
  └─ Forest/  (tiles=1, creat=1, obj=1, quest=1)
     ├─ name      : Forest
     ├─ mapName   : Forest
     ├─ artifacts : .glb
     └─ status    : populated

Per-zone summary line shows counts; sub-tree shows display name,
map slug, which derived artifacts have been baked (.glb / .obj /
.stl / .html / ZONE.md), and a populated/empty status.

Project-header line aggregates totals across all zones for the
'how big is my project?' answer in one glance.

Pairs with --info-tilemap (spatial coverage) and --zone-stats
(quantitative aggregate) — three different lenses on the project:
spatial, quantitative, and structural.
2026-05-06 16:57:26 -07:00
Kelsi
2152b230c8 feat(editor): add --export-project-md for GitHub-renderable project README
Markdown counterpart to --export-project-html. Generates a README.md
indexing every zone with counts + bake/viewer/doc artifact status.
GitHub renders it natively at the project root:

  wowee_editor --export-project-md custom_zones

# Wowee Project — Zone Index

*Auto-generated. 2 zone(s) discovered in `custom_zones`.*

## Summary

| Metric | Total |
|---|---:|
| Zones      | 2 |
| Tiles      | 2 |
| Creatures  | 2 |
| ...

## Zones

| Zone | Tiles | Creatures | Objects | Quests | Bake | Viewer | Docs |
|---|---:|---:|---:|---:|:---:|:---:|:---:|
| Desert | 1 | 1 | 1 | 1 | — | — | — |
| [Forest](Forest/ZONE.md) | 1 | 1 | 1 | 1 | ✓ | [view](Forest/Forest.html) | [md](Forest/ZONE.md) |

Per-zone row links to its ZONE.md (if --export-zone-summary-md was
run) and its HTML viewer (if --export-zone-html was run). The Bake
column shows ✓ if .glb exists. Status columns make it instantly
visible which zones are bake-ready vs documentation-only.

Pairs with --export-project-html (interactive viewer index) — same
data, different presentation: HTML for browsers, Markdown for
GitHub Pages READMEs and PR descriptions.

Verified on a 2-zone project where one zone had been baked +
viewer-exported + doc-exported and the other hadn't: README.md
correctly shows ✓/links for the baked zone, em-dashes for the
unbaked one.
2026-05-06 16:48:46 -07:00
Kelsi
f3130bbd3d feat(editor): add --info-zone-extents for spatial bounding box analysis
Computes the zone's spatial bounding box: XY world coords from
manifest tile coords (each tile is 533.33 yards), Z height range
across all loaded chunks. Useful for sizing camera frustums,
planning where new tiles can fit contiguously, or quick sanity
checks ('this zone is 4km across? something's wrong'):

  wowee_editor --info-zone-extents custom_zones/MyZone

  Zone extents: custom_zones/MyZone
    tile count   : 3 (3 loaded, 0 missing on disk)
    tile range   : x=[30, 31]  y=[30, 31]
    world box    : (0.0, 0.0, 98.5) - (1066.7, 1066.7, 101.5) yards
    size         : 1066.7 x 1066.7 x 3.0 yards (975m x 975m x 2.7m)

WoW grid math: tile (32, 32) is at world origin; +X tile = -X world
(north convention), +Y tile = -Y world (west convention). The
displayed world coords use the same transform the renderer uses
so they line up with --bake-zone-glb output bounds.

Per-axis size in yards + meters (0.9144 conversion) since some
designers think in metric, others in WoW-canonical yards.

Tracks loaded vs missing tiles in case the manifest references a
tile whose .whm got deleted — surfaces silently bad zones early.

JSON mode emits full bounding box + tile range + size for
programmatic consumption (camera autofit, layout planning).
2026-05-06 16:41:07 -07:00
Kelsi
9b362fc825 feat(editor): add --diff-checksum for SHA256SUMS comparison
Compares two SHA256SUMS files (from --export-zone-checksum). Reports
added / removed / changed entries between two zone snapshots — much
faster than walking the filesystem to recompute hashes of unchanged
content:

  wowee_editor --export-zone-checksum custom_zones/Z /tmp/before.sha256
  ... edits happen ...
  wowee_editor --export-zone-checksum custom_zones/Z /tmp/after.sha256
  wowee_editor --diff-checksum /tmp/before.sha256 /tmp/after.sha256

  Diff: /tmp/before.sha256 vs /tmp/after.sha256
    added   : 1
    removed : 0
    changed : 0
    +  creatures.json

Standard diff-style markers (+/-/~) for added/removed/changed,
sorted within each category alphabetically.

Use cases:
- Audit what an editing session actually touched (snapshot before,
  snapshot after, diff)
- Verify a zone bundle re-extracts identically (transfer integrity
  beyond the per-file PASS/FAIL of sha256sum -c)
- CI gate: fail build if a refactor touches files it shouldn't

Diff family for content/integrity formats:
  --diff-zone      unpacked zone dir vs zone dir (high-level)
  --diff-extract   per-extension counts in two extracts
  --diff-checksum  per-file hash diff between two snapshots  <- new

Verified: scaffold + export checksum, add creature, re-export,
diff correctly reports '+ creatures.json' (added) and exit 1.
2026-05-06 16:32:55 -07:00
Kelsi
dbf973e29e feat(editor): add --mvp-zone for one-command demo zone setup
Quick-start: scaffold a zone AND populate one of each content type
(1 creature, 1 object, 1 quest with objective + XP reward) in a
single command. Goes from empty filesystem to 'something to look at'
without 7 chained --add-* commands:

  wowee_editor --mvp-zone 'Demo Land' 30 30

  Created demo zone: custom_zones/Demo_Land
    tile     : (30, 30)
    contents : 1 creature, 1 object, 1 quest (with objective + reward)
    next     : wowee_editor --info-zone-tree custom_zones/Demo_Land

  Demo Land/
  ├─ Manifest ...
  ├─ Tiles (1) — (30, 30)
  ├─ Creatures (1) — lvl 5 Demo Wolf
  ├─ Objects (1) — m2 World/Generic/Tree.m2
  ├─ Quests (1) — [1] Welcome to Demo Land (lvl 1, 100 XP)
  │     └─ kill ×1 Demo Wolf

Demo content is positioned roughly at tile center (533.33-yard
intervals from origin tile 32/32). Quest references the demo
creature's auto-id so --check-zone-refs passes immediately.

Use cases:
- Smoke-testing the bake/validate pipeline
- Screenshot bait for docs / blog posts
- Editor onboarding (open a zone in the GUI to see the format)
- CI sanity check (does our editor still produce a viewable zone?)

Verified end-to-end: --mvp-zone 'Demo Land' → --info-zone-tree
shows all 4 sections populated correctly, file list matches
expected 6 files.
2026-05-06 16:25:23 -07:00
Kelsi
9c7b6aebfc feat(editor): add --info-quest-graph-stats for chain topology analysis
Where --export-quest-graph visualizes the quest dependency graph,
this quantifies it. Useful for spotting authoring issues (orphan
quests that only appear as one-offs, broken chains via cycles)
and getting a sense of quest density:

  wowee_editor --info-quest-graph-stats $Z/quests.json

  Quest graph: $Z/quests.json
    total quests : 4
    roots        : 2 (no inbound chain — entry points)
    leaves       : 2 (no outbound chain — terminal)
    orphans      : 1 (root AND leaf — one-shot)
    cycles       : 0
    max depth    : 3
    avg depth    : 2.00 (chain length per root)

Definitions:
  roots    = quests no other quest chains TO (player entry points)
  leaves   = quests with no nextQuestId or nextQuestId pointing to
             a missing quest (terminal — chain ends here)
  orphans  = root AND leaf (one-shot quests with no neighbors)
  cycles   = number of roots whose forward walk hits a node twice
  maxDepth = longest path from any root forward through the chain
  avgDepth = mean path length across all roots

Cycle-guarded forward walk uses a visited-set per root, so the
cycle count is bounded even on intentionally-broken inputs.

Exit 1 if cycles > 0 so CI can gate before shipping a broken
chain. JSON mode emits all six stats for dashboard consumption.

Verified on 4-quest zone (Q1→Q2→Q3 chain + Loner orphan):
correctly reports 2 roots, 2 leaves, 1 orphan, 0 cycles, max
depth 3, avg depth 2.00.
2026-05-06 16:17:30 -07:00
Kelsi
cc91a1146f feat(editor): add --bake-project-stl + --bake-project-glb completing the project trio
Three project-bake formats now match the three zone-bake formats —
full project terrain reachable from every universal-3D ecosystem:

  wowee_editor --bake-project-obj  custom_zones    # DCC tools
  wowee_editor --bake-project-stl  custom_zones    # 3D printing
  wowee_editor --bake-project-glb  custom_zones    # web viewers

Shared per-zone walking pass collects vertex+index pools per zone,
then the format-specific tail emits:
  STL → per-triangle 'facet normal'+'outer loop'+vertex×3
  GLB → packed BIN chunk + JSON describing per-zone meshes

GLB output gives one mesh+node per zone (named 'zone_NAME') so
viewers can toggle zones independently — same pattern as
--bake-zone-glb but at project scope. STL is single-solid since
slicers don't have a useful concept of multi-part STL.

Coords align across all three exporters and across zone vs project
scope, so:
- A zone .obj overlaid with its containing project .obj lines up
- A project .glb opened in three.js shows zones at the same coords
  the renderer uses

Verified on a 2-zone project (Forest + Desert):
- project.stl: 2 zones, 2 tiles, 65536 facets
- project.glb: 2 zones, 2 tiles, 41472 verts, 65536 tris, 1.78MB BIN
- --validate-glb on project.glb: PASSED

Bake granularity matrix complete:
                     OBJ              STL              GLB
  single model   --export-obj    --export-stl     --export-glb
  single zone    --bake-zone-obj --bake-zone-stl  --bake-zone-glb
  whole project  --bake-project-obj --bake-project-stl --bake-project-glb
2026-05-06 16:08:44 -07:00
Kelsi
54c309a779 feat(editor): add --bake-project-obj for whole-project terrain export
Project-level OBJ bake — combines every zone's terrain into one
giant OBJ with one 'g zone_NAME' block per zone. Useful for
previewing an entire multi-zone project's terrain in MeshLab/
Blender at once, or for printing the full map:

  wowee_editor --bake-project-obj custom_zones

  Baked custom_zones -> custom_zones/project.obj
    2 zone(s), 3 tiles, 62208 verts, 98304 tris

Layout: single global vertex pool (so OBJ indexing stays valid),
per-zone face groups so designers can hide individual zones in
their viewer for area-by-area inspection. Hole bits respected.
Coords match WoweeCollisionBuilder's outer-grid layout exactly so
zones spatially line up at WoW grid boundaries — adjacent tiles
across zones connect seamlessly.

Pairs with the existing --bake-zone-* family (single zone) and
--export-project-html (web index of per-zone viewers). Three
levels of granularity now available:
  --export-glb / --export-obj / --export-stl     single model/file
  --bake-zone-glb / -obj / -stl                  single zone
  --bake-project-obj                             entire project  <- new

Verified: 2-zone project (Forest 2 tiles + Desert 1 tile) baked
to project.obj with 62208 verts (3 × 20736), 98304 tris (3 ×
32768), 2 'g' blocks correctly named (zone_Desert, zone_Forest).
2026-05-06 15:59:51 -07:00
Kelsi
b628535a91 feat(editor): add --export-zone-checksum for SHA-256 integrity manifests
Emits a SHA-256 manifest of every source file in a zone in the
standard sha256sum format. Lets users verify zone integrity after
download/transfer using the standard system tool — no custom
verifier needed:

  wowee_editor --export-zone-checksum custom_zones/MyZone

  3298c35a...  Z_30_30.whm
  f81e3d37...  Z_30_30.wot
  6a49519f...  creatures.json
  4625e30b...  zone.json

  sha256sum -c custom_zones/MyZone/SHA256SUMS
  Z_30_30.whm: OK
  Z_30_30.wot: OK
  ...

Source-only by design — derived outputs (.glb/.obj/.stl/.html/.png/
ZONE.md/DEPS.md/quests.dot/SHA256SUMS itself/Makefile) are excluded
since they're regeneratable and would invalidate the checksum on
every rebuild.

Includes a self-contained 90-LoC SHA-256 (FIPS 180-4 / RFC 6234)
in an internal namespace — no OpenSSL/Crypto++ dependency added.
Streaming hash (16KB chunks) so it scales to giant terrain WHMs
without holding the whole file in memory.

Verified end-to-end: scaffolded zone with 1 creature → checksum
manifest of 4 source files (zone.json, creatures.json, .whm, .wot)
in standard format → sha256sum -c reports all 4 OK.
2026-05-06 15:51:50 -07:00
Kelsi
4668140eed feat(editor): add --info-cli-help for substring searching the help text
130+ commands. 'Is there a thing for X?' is a common question.
Scrolling --help to find out is slow. This searches:

  wowee_editor --info-cli-help quest

  --add-quest <zoneDir> <title> [giverId] [turnInId] [xp] [level]
                         Append one quest to <zoneDir>/quests.json and exit
  --add-quest-objective <zoneDir> <questIdx> <kill|collect|...> ...
                         Append one objective to a quest by index
  --remove-quest-objective <zoneDir> <questIdx> <objIdx>
                         Remove the objective at given 0-based index from a quest
  --clone-quest <zoneDir> <questIdx> [newTitle]
                         Duplicate a quest (with all objectives + rewards)
  ...

  32 line(s) matched 'quest'

Case-insensitive substring match. Continuation lines (the indented
description right after a flag) are emitted along with their flag
line for context. Match-count summary on stderr so it doesn't
contaminate piped output.

Pairs with --list-commands (full list) and --info-cli-stats (counts
by category). Three meta commands now cover the discovery loop:
'how many?' (--info-cli-stats), 'which ones?' (--list-commands),
'what does X do?' (--info-cli-help X).
2026-05-06 15:43:02 -07:00
Kelsi
f5cb2adbda feat(editor): add --gen-project-makefile for top-level multi-zone builds
Pairs with --gen-makefile (per-zone) — generates a project-level
Makefile that delegates to each zone's per-zone Makefile, enabling
parallel rebuilds across zones:

  wowee_editor --gen-project-makefile custom_zones
  make -C custom_zones -j$(nproc)        # all zones in parallel

  make Forest-bake                       # one zone
  make clean                             # strip every zone
  make validate                          # validate every zone
  make index                             # rebuild project HTML index
  make stats / tilemap                   # project-level reports

Per-zone targets (one set per zone):
  ZONE-bake     -> ensures ZONE/Makefile exists then `make -C ZONE all`
  ZONE-clean    -> --strip-zone ZONE
  ZONE-validate -> --validate-all ZONE

Auto-generates the per-zone Makefile if missing (so the project
Makefile bootstraps a fresh project without an extra setup step).

Top-level utility targets reuse the existing project-level commands:
- index    -> --export-project-html (HTML zone index)
- stats    -> --zone-stats (aggregate counts)
- tilemap  -> --info-tilemap (ADT grid visualization)

Verified: 2-zone project (Forest + Desert) generates a Makefile
with 6 zone-level targets (3 per zone) + 5 top-level targets, sorted
alphabetically by zone name.
2026-05-06 15:37:29 -07:00
Kelsi
dadefab64e feat(editor): add --check-zone-content for content data-quality validation
Sanity-checks creature/object/quest fields for plausible values.
Where --check-zone-refs catches dangling references, this catches
data-quality issues that pass technical validation but break
gameplay:

  wowee_editor --check-zone-content custom_zones/MyZone

  Zone content: custom_zones/MyZone
    creature warnings: 1
    object warnings  : 0
    quest warnings   : 2
    FAILED — 3 total warning(s):
      - creature[0] 'Wolf' has displayId=0 (will render invisibly)
      - quest[0] 'Hunt' has no objectives (uncompletable)
      - quest[0] 'Hunt' has no reward at all

Per-type checks:

Creatures:
  - empty name
  - 0 health (dies on spawn)
  - level 0
  - minDamage > maxDamage (broken combat math)
  - non-positive or non-finite scale
  - displayId=0 (invisible at runtime)

Objects:
  - empty path
  - non-positive or non-finite scale
  - non-finite position

Quests:
  - empty title
  - no objectives (player can never complete)
  - no reward at all (XP=0, items=[], coins all 0)
  - requiredLevel=0

Both --check-zone-refs (link integrity) and --check-zone-content
(data quality) needed — a quest can have valid NPC IDs (refs OK)
AND no objectives (content broken). Run both before --pack-wcp.

Verified end-to-end: zone with displayId=0 creature + objective-
less + rewardless quest reports 3 warnings; after fixing all three,
PASSED.
2026-05-06 15:31:12 -07:00
Kelsi
c9e8ad9930 feat(editor): add --diff-extract for asset-extract directory comparison
Compares two extracted asset directories side-by-side per file
extension. Useful for diffing a fresh asset_extract run against
a previous baseline (did the new MPQ add files? did any get
dropped?), or comparing what each WoW expansion contributes:

  wowee_editor --diff-extract baseline/ new/

  Diff: baseline/ vs new/
    totals: 4 files / 0.0 MB    vs    4 files / 0.0 MB

    Per-extension (count then bytes):
    ext            a count   b count    a bytes      b bytes  status
    .blp                 2         2           0            0
    .dbc                 1         0           0            0  -A
    .m2                  1         2           0            0  DIFF

    2 extension(s) differ

Status column flags imbalance:
  -A   only in A (extension dropped going B-ward)
  +B   only in B (extension added)
  DIFF count differs but both sides have some

Recursive walk so subdirectories aggregate into the parent's
extension counts. JSON mode emits per-extension {count,bytes}
pairs for both sides plus union diff count for CI consumption.

Diff family for directory-shaped formats:
  --diff-zone     unpacked zone dir vs zone dir
  --diff-extract  extracted asset dir vs extract dir  <- new

Verified on synthesized 4-file dirs (a: 2 blp + 1 dbc + 1 m2;
b: 2 blp + 0 dbc + 2 m2): correctly flags -A on .dbc, DIFF on
.m2, exit 1.
2026-05-06 15:25:18 -07:00
Kelsi
0f275cfee7 feat(editor): add --info-cli-stats meta command for surface tracking
130 commands and counting — surface inspection has graduated from
'nice to have' to 'necessary to plan'. This drops a per-category
breakdown:

  wowee_editor --info-cli-stats

  CLI surface stats
    total commands : 130
    longest flag   : 24 chars

    Categories (by verb prefix, sorted by count):
      --info           33
      --export         15
      --list           12
      --validate       11
      --diff            8
      --add             6
      --remove          5
      --convert         5
      --bake            3
      --migrate         3
      --clone           3
      ...

Pulls the command list via the same printUsage-parser as
--list-commands so it auto-tracks new flags. Buckets by the verb
prefix (text between '--' and the next '-') so '--info-zone-tree'
counts under 'info'. JSON mode emits the histogram as an object
for dashboard consumption.

Useful for spotting category imbalances ('we have 33 inspectors
but only 5 add commands — should consider more authoring CRUD?'),
tracking growth over releases, and planning where to invest.
2026-05-06 15:21:24 -07:00
Kelsi
1f20d0c5a2 feat(editor): add --gen-makefile for incremental zone-output rebuilds
Generates a Makefile that rebuilds every derived output for a zone
with proper dependency tracking. Designers can `make` to refresh
glb/obj/stl/html/csv/md from sources after editing creatures.json
or terrain — without remembering which wowee_editor flag does what,
and without rebuilding outputs that haven't changed:

  wowee_editor --gen-makefile custom_zones/MyZone
  cd custom_zones/MyZone && make

  make all       # rebuild everything that's stale
  make glb       # just the glTF bake
  make clean     # nuke derived outputs (calls --strip-zone)
  make validate  # run --validate-all on the zone

Targets generated:
  glb obj stl html docs csv graph all clean validate

Dependency tracking:
  - terrain bakes (.glb/.obj/.stl) depend on zone.json + WHM tiles
  - HTML viewer depends on the .glb (forces glb rebuild first)
  - docs (ZONE.md/DEPS.md) depend on content JSONs
  - csv/graph use '-' prefix so missing-content failures don't
    block 'make all' (zone with no quests still bakes terrain)

Uses /proc/self/exe absolute path so the Makefile works from any
cwd (run via `make -C custom_zones/MyZone` from anywhere). Falls
back to PATH lookup if /proc not available.

Verified end-to-end: scaffolded zone, generated Makefile, ran
`make all` from inside the zone dir — all derived outputs (.glb,
.obj, .stl, .html, ZONE.md, DEPS.md) generated; csv+graph
gracefully skipped due to no content; make exit 0.
2026-05-06 15:17:53 -07:00
Kelsi
5ebd04a953 feat(editor): add --info-extract-tree for hierarchical extract-dir overview
After asset_extract finishes, '142k files across 17 dirs' is hard
to reason about. This groups them visually by top-level subdir +
file format with byte totals — instant orientation:

  wowee_editor --info-extract-tree Data

  Data/  (13 dirs, 284612 files, 31451.1 MB)
  ├─ expansions/  (85141 files, 12871.2 MB)
  │  ├─ .adt         5469 files  6226626.7 KB
  │  ├─ .wav        19517 files  3567936.9 KB
  │  ├─ .m2         26354 files  1477702.1 KB
  │  ├─ .wmo         5195 files   744940.1 KB
  │  ├─ .blp        26911 files   691248.5 KB
  │  ├─ .dbc          282 files    92373.6 KB
  │  ├─ ...
  ├─ terrain/    (...)
  ├─ character/  (...)

Top-level dirs sorted by total bytes descending (heaviest first
so the high-impact directories surface immediately). Per-dir
extension breakdown also sorted by bytes. Walks recursively into
each top-level dir so 'expansions/wotlk/world/...' rolls up into
the expansions/ row.

Companion to --info-extract (counts + sidecar coverage) — that
one's wide-format JSON-friendly; this one's a tall tree-style
quick-look. Verified on a real 31GB extract of Data/ — output
makes the size distribution immediately clear (ADT files are
6GB of 13GB total for expansions/, etc.).
2026-05-06 15:10:45 -07:00
Kelsi
d3b7a085c2 feat(editor): add --validate-blp for proprietary BLP structural check
Companion to --validate-png — validates BLP textures without paying
the DXT decompress cost. Useful for spot-checking thousands of BLPs
in an extract dir:

  wowee_editor --validate-blp Texture.blp

  BLP: Texture.blp
    magic      : BLP2
    size       : 128 x 128
    valid mips : 8
    file bytes : 23044
    PASSED

Checks (header-only, no pixel decode):
- 4-byte magic is 'BLP1' or 'BLP2'
- Width/height non-zero and within 8192 (texture exporter cap)
- mipOffsets[16] / mipSizes[16] tables: each non-zero pair refers
  to a byte range within the file; mismatched (off=0 with size!=0
  or vice versa) flagged
- Stops at first zero offset (BLP convention for unused slots)

Per-version layout differences (BLP1 has compression+alphaBits as
uint32; BLP2 has compression/alphaDepth/alphaEncoding/hasMips as
uint8) handled inline.

Verified on real BLP (pvp-banner-emblem-1.blp): 8 valid mips,
PASSED. Non-BLP file (manifest.json starting with '{'): correctly
flags 'magic is {' error and exits 1.

Format-validator lineup is now exhaustive across both proprietary
and open formats:
  Proprietary: BLP / DBC (via --convert-dbc-json round-trip)
  Open binary: WOM / WOB / WOC / WHM / GLB
  Open text:   JSON DBC / STL / PNG (BLP sidecar)
2026-05-06 15:07:46 -07:00
Kelsi
a98e6e79c4 feat(editor): add --export-project-html for multi-zone web index
Generates an index.html linking to every zone's HTML viewer in a
project. Pairs with --export-zone-html (per-zone) and --bake-zone-glb
(terrain bake). Designed for github-pages / static-hosting style
'all my zones' showcase:

  wowee_editor --export-project-html custom_zones
  # -> custom_zones/index.html

Each zone gets a card showing:
- Display name (or mapName fallback)
- Counts: tiles · creatures · objects · quests (singular/plural
  agreement)
- 'Open viewer →' link if HTML exists
- Helpful nudge if HTML missing ('No HTML viewer (run --export-zone-html)')
- Or 'No .glb (run --bake-zone-glb)' if not even baked yet

Self-contained CSS (dark theme matching --export-zone-html), no
external dependencies. Responsive grid layout (300px-min cards
auto-flowing across viewport).

Verified on a 2-zone project (Forest with .glb+.html + Desert
without): index lists both, Forest gets a working link, Desert
gets the 'run --bake-zone-glb' hint.
2026-05-06 15:04:43 -07:00
Kelsi
f73b377b4d feat(editor): add --info-tilemap for project-wide ADT grid visualization
Renders the WoW 64x64 ADT coordinate grid as ASCII art showing which
tiles are claimed by which zones. Useful for spotting tile-coord
collisions before two zones ship overlapping content, and for
'where am I working?' overview of multi-zone projects:

  wowee_editor --info-tilemap custom_zones

  Tilemap: custom_zones
    zones      : 2
    tiles used : 5
    collisions : 0 (multiple zones claiming same tile)
    legend     : D=Desert F=Forest

         0         1         2         3         4         5         6
         0123456789012345678901234567890123456789012345678901234567890123
    y=30 ..............................FFDD..............................
    y=31 ...............................F................................

Glyphs are first-letter-of-zone uppercased; collision tiles render
as '*'. Empty rows are skipped to keep output bounded for projects
in one corner of the map (skipping all 60+ blank rows of the full
64x64 grid).

Exit 1 on collisions so CI can gate before merging two PRs that
both add tiles in the same area. JSON mode emits per-tile claims
for programmatic consumption.

Verified on a 2-zone project: Forest (3 tiles) + Desert (2 tiles)
correctly rendered, no collisions detected, glyphs F/D placed at
the right grid coords.
2026-05-06 15:01:32 -07:00
Kelsi
1718c2333f feat(editor): add --repair-zone to auto-fix manifest/disk drift
When a zone is hand-edited, partially copied, or modified by tools
that don't re-write zone.json, the manifest can fall out of sync
with the on-disk reality. --repair-zone reconciles them:

  wowee_editor --repair-zone custom_zones/MyZone --dry-run

  would add tile (31, 30) to manifest
  would set hasCreatures: false -> true

  repair-zone: custom_zones/MyZone (dry-run)
    fixes    : 2
    warnings : 0 (manual decision needed)
    re-run without --dry-run to apply

Auto-fixes:
- WHM files on disk matching <mapName>_TX_TY.whm pattern but not
  in manifest tiles[] -> add to tiles
- hasCreatures flag mismatched against actual creatures.json
  presence + non-empty -> sync

Warns (no auto-fix — needs manual decision):
- Tiles in manifest but no .whm on disk (could be in-progress
  work or genuinely deleted; user decides)

--dry-run flag previews changes. Pairs with --strip-zone (cleanup
derived) and --validate (open-format coverage) for the trio of
zone-health-maintenance commands.

Verified: scaffolded zone, hand-copied an extra .whm/.wot pair to
simulate disk-without-manifest drift, added a creature then flipped
hasCreatures=false in zone.json. --repair-zone correctly identifies
2 fixes, dry-run lists them, real run applies them, manifest now
shows correct tiles array + hasCreatures=true.
2026-05-06 14:58:08 -07:00
Kelsi
b82f9827d4 feat(editor): add --info-zone-bytes for per-file size breakdown
Drills into one zone's contents with categorized + sorted file
sizes. --zone-stats aggregates across multiple zones; this answers
'which file is 80% of THIS zone?' and 'how much would --strip-zone
free?':

  wowee_editor --info-zone-bytes custom_zones/MyZone

  Zone bytes: custom_zones/MyZone
    total: 2282178 bytes (2228.7 KB) across 6 file(s)

    Per-file (largest first):
    path                                bytes  category
    Z_30_30.woc                       1212456  terrain
    Z.glb                              891736  3D export (derived)
    Z_30_30.whm                        150540  terrain
    Z_30_30.wot                         26680  terrain
    zone.json                              446  json (source)
    quests.json                            320  json (source)

    Per-category:
    3D export (derived)    1 files    891736 bytes  ( 39.1%)
    json (source)          2 files       766 bytes  (  0.0%)
    terrain                3 files   1389676 bytes  ( 60.9%)

Categories: terrain / model (open|proprietary) / building (open|
proprietary) / texture (open|proprietary) / DBC / json (source) /
3D export (derived) / doc (derived) / other.

Source vs derived split surfaces what --strip-zone would clean up
(any 'derived' category) so capacity planning shows both 'what's
mine' (source) and 'what's regeneratable' (derived).

Recursive walk so subdirs (data/) are included with relative paths.
JSON mode emits per-file records + per-category aggregate for
programmatic consumption.
2026-05-06 14:54:29 -07:00
Kelsi
7dfc0b6333 feat(editor): add --strip-zone for derived-output cleanup
Removes the derived outputs (everything --bake/--export-* generates)
leaving only source files in a zone directory:

  wowee_editor --strip-zone custom_zones/MyZone

  removed: MyZone.obj (1099177 bytes)
  removed: MyZone.glb (891736 bytes)
  removed: DEPS.md (357 bytes)
  removed: ZONE.md (534 bytes)
  removed: quests.dot (198 bytes)

  strip-zone: custom_zones/MyZone
    removed  : 5 file(s)
    freed    : 1945.3 KB

What gets deleted:
- .glb / .obj / .stl  (3D format exports)
- .html               (browser viewer)
- .dot                (Graphviz quest graph)
- .csv                (spreadsheet exports)
- ZONE.md / DEPS.md   (markdown documentation)
- .png at zone root   (heightmap previews — NOT inside data/, those
                       are source BLP→PNG sidecars)

What stays:
- zone.json + creatures.json + objects.json + quests.json
- *.whm / *.wot / *.woc (terrain + collision)
- *.wom / *.wob         (open binary models/buildings)
- data/*.json           (DBC sidecars — source, not derived)

Top-level only — does not recurse into subdirectories so source
sidecars under data/ are untouched.

--dry-run flag previews what would be removed without deleting.
Useful before committing to git so derived blobs don't bloat
history, or before --pack-wcp so the archive doesn't carry
redundant exports.

Verified: scaffolded zone, generated 5 derived files (glb/obj/
ZONE.md/DEPS.md/quests.dot), --dry-run lists all 5 with sizes,
real run deletes them and frees 1945 KB. Source files (whm/wot/
woc/zone.json/quests.json) all preserved.
2026-05-06 14:51:36 -07:00
Kelsi
8f5a3b3d95 feat(editor): add --info-glb-tree for hierarchical glTF structure view
--info-glb gives counts; --info-glb-tree shows the actual scene ->
node -> mesh -> primitive hierarchy with names and accessor refs.
Useful when debugging 'why is this imported model showing up empty
in three.js?' (often the scene's nodes[] points to the wrong node):

  wowee_editor --info-glb-tree Z.glb

  Z.glb
  ├─ asset (v2.0, wowee_editor --bake-zone-glb)
  ├─ buffers (1)
  │  └─ [0] 1781760 bytes
  ├─ bufferViews (3)
  │  ├─ [0] off=0 len=497664 (vertex)
  │  ├─ [1] off=497664 len=497664 (vertex)
  │  └─ [2] off=995328 len=786432 (index)
  ├─ accessors (4)
  │  ├─ [0] f32 VEC3 ×41472 (bv=0)
  │  ├─ [1] f32 VEC3 ×41472 (bv=1)
  │  ├─ [2] u32 SCALAR ×98304 (bv=2)
  │  └─ [3] u32 SCALAR ×98304 (bv=2)
  ├─ meshes (2)
  │  ├─ [0] (1 primitives)
  │  │  └─ [0] TRIANGLES indices=acc#2
  │  └─ [1] (1 primitives)
  │     └─ [0] TRIANGLES indices=acc#3
  ├─ nodes (2)
  │  ├─ [0] tile_30_30 -> mesh#0
  │  └─ [1] tile_31_30 -> mesh#1
  └─ scenes (1, default=0)
     └─ [0] nodes=[0,1] (2 nodes)

Decodes glTF componentTypes (5120-5126 -> i8/u8/i16/u16/u32/f32),
bufferView targets (34962=vertex, 34963=index), primitive modes
(0=POINTS / 1=LINES / 4=TRIANGLES). Node sub-line shows mesh
reference so the scene-graph wiring is visible at a glance.

Pairs with --info-zone-tree (zone content tree) — both use the
same UTF-8 box-drawing pattern for visual consistency.
2026-05-06 14:48:37 -07:00
Kelsi
2904fa0560 feat(editor): add --export-zone-deps-md for shareable dependency tables
Markdown counterpart to --list-zone-deps. PR reviewers see at a
glance whether every referenced model exists in either open or
proprietary form across the conventional asset roots:

  wowee_editor --export-zone-deps-md custom_zones/MyZone
  # -> custom_zones/MyZone/DEPS.md

# Dependencies — MyZone

*Auto-generated. Status is best-effort — checks zone-local, output/,
custom_zones/, Data/ roots in that order.*

## Direct M2 placements (12)

| Refs | Path | Status |
|---:|---|---|
| 8 | `World/Tree.m2`        | open + proprietary |
| 3 | `World/Lamp.m2`        | open only |
| 1 | `World/Banner.m2`      | MISSING |

Status column resolves each path against zone-local + output/ +
custom_zones/ + Data/ roots, trying both .wob/.wmo for buildings
and .wom/.m2 for models. Catches missing assets BEFORE pack-wcp
would silently include broken refs.

GitHub-Flavored Markdown — sortable by Refs column once rendered,
backtick-wrapped paths so URLs/spaces don't confuse the viewer.

Verified: scaffolded zone with 2 M2 placements (one duplicated) +
1 WMO placement → DEPS.md has 3 sections (one per category) with
correct ref counts (Tree.m2 ×2) and MISSING status for paths that
don't resolve in any root.
2026-05-06 14:44:53 -07:00
Kelsi
84f897f0ec feat(editor): add --diff-jsondbc completing the diff family for the last text format
Diff family is now exhaustively complete across every shippable
format:

  --diff-wcp       archive
  --diff-zone      unpacked zone dir
  --diff-glb       glTF binary
  --diff-wom       WOM model
  --diff-wob       WOB building
  --diff-whm       WHM/WOT terrain pair
  --diff-woc       WOC collision
  --diff-jsondbc   JSON DBC sidecar  <- new

Schema-level compare:
  - format tag
  - source filename
  - recordCount + fieldCount (header values)
  - actualRecs (records[] array length)

Useful for catching schema regressions when a sidecar is regenerated
by a different tool version, or for verifying a --migrate-jsondbc
pass actually changed what it claimed.

Verified: same file vs itself reports IDENTICAL exit 0;
2-record vs 3-record (with same format/source/fieldCount) reports
2 DIFFs (recordCount + actualRecs) with exit 1.
2026-05-06 14:41:32 -07:00
Kelsi
e531901de8 feat(editor): add --validate-png for full PNG structural + CRC validation
Goes beyond --info-png (header sniff only) to validate every chunk
and verify CRC32. Catches corruption that browsers silently skip:

  wowee_editor --validate-png Texture.png

  PNG: Texture.png
    size       : 256 x 256
    bit depth  : 8 (color type 6)
    chunks     : 3 (0 CRC mismatches)
    file bytes : 142336
    PASSED

  wowee_editor --validate-png corrupted.png
    chunks     : 1 (1 CRC mismatches)
    FAILED — 4 error(s):
      - chunk 'IHDR' at offset 8: CRC mismatch (stored=0xC33E61CB
        actual=0x8A716D80)
      - missing required IDAT chunk
      - missing required IEND chunk

Checks:
- 8-byte PNG signature (89 50 4E 47 0D 0A 1A 0A)
- Per-chunk length doesn't exceed file
- CRC32 of (chunk type + data) matches stored CRC (PNG spec)
- IHDR is the first chunk
- IHDR / IDAT / IEND all present
- No trailing bytes after IEND

CRC32 implementation uses the standard polynomial 0xEDB88320 with a
runtime-built lookup table — no zlib dependency since we already
have to compute it ourselves anyway.

Verified on real PNG (BLP→PNG conversion of pvp-banner emblem):
PASSED. Hand-corrupted IHDR byte: correctly flags the CRC mismatch
with stored vs actual hex values + cascading missing-chunk errors.
Format-validator lineup is now exhaustive across both proprietary
and open formats:
  Open binary: WOM / WOB / WOC / WHM / GLB
  Open text:   JSON DBC / STL
  Sidecar:     PNG (proprietary BLP -> PNG bridge)
2026-05-06 14:38:17 -07:00
Kelsi
f8f5735d9b feat(editor): add --diff-whm and --diff-woc completing the open-format diff suite
Last two missing entries in the diff family — terrain heightmap pairs
and collision meshes:

  wowee_editor --diff-whm Z1/Z1_30_30 Z2/Z2_31_30
  Diff: ...
                         a              b
    tile         : (  30,  30)    (  31,  30)  DIFF
    loadedChunks :          256          256
    doodadPlace  :            0            0
    wmoPlace     :            0            0
    heightRange  : [-1.50,1.50]  [-1.50,1.50]

  wowee_editor --diff-woc tile_a.woc tile_b.woc
  Diff: ...
                         a              b
    tile        : (  30,  30)    (  31,  30)  DIFF
    triangles   :        32768        32768
    walkable    :        32768        32768
    steep       :            0            0

--diff-whm: tile coord, loaded chunk count, doodad/WMO placement
counts, texture/name table sizes, height range (min/max with float
epsilon). Pointwise height compare intentionally not done — float
perturbation from format round-trips would false-flag.

--diff-woc: tile coord, total triangles, walkable + steep counts.
Catches whether a --regen-collision pass actually changed something.

Diff family is now exhaustively complete for every shippable open
format:
  --diff-wcp    archive vs archive
  --diff-zone   unpacked zone dir vs zone dir
  --diff-glb    glTF binary vs glTF binary
  --diff-wom    WOM model vs WOM model
  --diff-wob    WOB building vs WOB building
  --diff-whm    WHM/WOT terrain pair vs pair
  --diff-woc    WOC collision vs collision

Verified: tile (30,30) vs (31,30) reports tile DIFF + identical
counts (since both are flat scaffolds); same-vs-self reports
IDENTICAL with exit 0.
2026-05-06 14:35:03 -07:00
Kelsi
cc3e85be5a feat(editor): add --migrate-jsondbc to auto-fix common JSON DBC issues
Designers receive JSON DBCs from various tools (older asset_extract
versions, third-party converters) that may omit fields the runtime
now expects. --validate-jsondbc tells you what's wrong; this fixes
the auto-fixable ones:

  wowee_editor --migrate-jsondbc db/Spell.json

  added: format = 'wowee-dbc-json-1.0'
  added: source = 'Spell.dbc'
  fixed: recordCount 99 -> 47882 (matches actual)
  inferred: fieldCount = 234 (from first row)
  Migrated db/Spell.json -> db/Spell.json
    fixes applied: 4

Auto-fixes:
- Missing 'format' tag → add 'wowee-dbc-json-1.0'
- Missing 'source' field → derive from filename stem + '.dbc'
- Missing 'fieldCount' → infer from first row
- recordCount mismatch → recompute from actual records[] length

NOT auto-fixed (data loss risk — surfaced as warnings instead):
- Wrong-width rows (silently padding/truncating could mangle field
  values; the user needs to inspect and decide)

In-place by default (writes back to the input path); accepts an
optional output path for non-destructive migration.

Verified: a JSON missing format/source/fieldCount with mismatched
recordCount=99 (actual 2) → migrate applies 4 fixes →
--validate-jsondbc reports PASSED on the result.
2026-05-06 14:31:35 -07:00
Kelsi
84902316e2 feat(editor): add --export-zone-html for browser-viewable zone preview
Generates a single-file HTML viewer next to the zone .glb. Anyone
with a modern browser can open it — no installs, no Blender, no
node_modules:

  wowee_editor --bake-zone-glb custom_zones/MyZone     # bakes .glb
  wowee_editor --export-zone-html custom_zones/MyZone  # writes .html
  open custom_zones/MyZone/MyZone.html                 # any browser

Uses Google's <model-viewer> web component (loaded from unpkg with
^4.0.0 version pin so a unpkg 'latest' bump can't silently break
older HTML files). The HTML itself is ~1.1KB; the .glb sits beside
it for the viewer to load via relative URL.

Features baked into the page:
- Camera controls (orbit, zoom, pan)
- Auto-rotate at 15deg/sec for the headless preview case
- Shadow casting + neutral environment IBL for non-flat lighting
- Header strip showing display name, map slug, tile count, mapId

Refuses to run if the zone .glb doesn't exist yet (clear error
message points the user at --bake-zone-glb).

Verified: scaffolded zone -> --bake-zone-glb -> --export-zone-html.
1.1KB HTML opens cleanly in browsers, references the sibling .glb.
Missing-glb case correctly errors with exit 1 + helpful next-step
hint.
2026-05-06 14:28:42 -07:00
Kelsi
d618d6a517 feat(editor): add --diff-wob completing the binary-format diff suite
Companion to --diff-wom for buildings. Same count-based shape so
round-trips through OBJ/glTF can be validated without false
positives from float perturbation:

  wowee_editor --diff-wob orig back

  Diff: orig.wob vs back.wob
                         a              b
    groups      :            5            5
    portals     :            3            3
    doodads     :           12           12
    materials   :            8            8
    groupTex    :            7            7
    totalVerts  :         4609         4609
    totalIdx    :        10950        10950
    name        : Stormwind     Stormwind
    boundRadius : match
    IDENTICAL

Compares: groups, portals, doodads, aggregated materials count
(per-group materials summed), aggregated group-texture count,
total verts/indices across all groups, name, boundRadius (with
0.01 epsilon).

Diff family is now complete across every binary format that ships
in a content pack:
  --diff-wcp    archive vs archive
  --diff-zone   unpacked zone dir vs zone dir
  --diff-glb    glTF binary vs glTF binary
  --diff-wom    WOM model vs WOM model
  --diff-wob    WOB building vs WOB building

Verified: identical pair reports IDENTICAL exit 0; pair with extra
group + extra doodad + name change reports 6 DIFFs with exit 1.
2026-05-06 14:25:40 -07:00
Kelsi
a01b5e5e89 feat(editor): add --info-object completing the single-entity inspector trio
Symmetric to --info-creature and --info-quest. Every PlacedObject
field for one entry:

  wowee_editor --info-object $Z/objects.json 3

  Object [3]
    type      : wmo
    path      : World/StaticObject/Stormwind/HumanInn.wmo
    nameId    : 3497721408
    uniqueId  : 4
    position  : (-8412.300, 633.200, 110.500)
    rotation  : (0.00, 90.00, 0.00) deg
    scale     : 1.000

Useful when debugging a misplaced object — quickly see the exact
position/rotation/scale + uniqueId + nameId without having to
grep through objects.json or run --list-objects through awk.

JSON mode emits the structured record. Inspector lineup is now
symmetric across all three content types:
  --info-{creatures,objects,quests}    aggregate counts
  --list-{creatures,objects,quests}    per-entry table
  --info-{creature,object,quest}       single-entry deep dive
2026-05-06 14:22:08 -07:00
Kelsi
b7696d1aa9 feat(editor): add --info-creature and --info-quest single-entity inspectors
The --list-* commands give table views; the --info-* (creatures/objects/
quests) give summary counts. Neither shows every field of a single
entry. These fill that gap:

  wowee_editor --info-creature $Z/creatures.json 0

  Creature [0] 'Captain'
    id            : 1
    displayId     : 11430
    position      : (100.00, 200.00, 50.00)
    orientation   : 0.00 deg
    scale         : 1.00
    level         : 12
    health/mana   : 100 / 0
    damage        : 5-10
    armor         : 0
    faction       : 0
    behavior      : stationary
    wander rad    : 10.0
    aggro rad     : 20.0
    leash rad     : 40.0
    respawn ms    : 300000
    patrol points : 0
    flags         :

  wowee_editor --info-quest $Z/quests.json 0

  Quest [0] 'Hunt Wolves'
    id              : 1
    required level  : 5
    giver NPC id    : 100
    turn-in NPC id  : 100
    next quest id   : 0 (terminal)
    reward          : 250 XP, 0g 0s 0c, 1 item(s)
      item[0]      : Item:Sword
    objectives      : 1
      [0] kill    ×5  Wolf  — Slay 5 Wolf

Useful for digging into 'why is this NPC not behaving like I expect?'
or reviewing one quest's full design in one screen instead of running
3-4 list-* commands. JSON mode emits every field as a structured
record for programmatic consumption.

Inspector lineup is now complete from aggregate to per-entry:
  --info-{creatures,objects,quests}      (counts)
  --list-{creatures,objects,quests}      (table)
  --info-{creature,quest}                (single entry, all fields)
2026-05-06 14:19:17 -07:00
Kelsi
17b83858c1 feat(editor): add --export-zone-csv for spreadsheet design workflows
Designers often prefer spreadsheets over JSON for read-only analysis
('which 5 quests give the most XP?', 'how many lvl 10+ creatures?',
'pivot table by faction'). This emits creatures.csv / objects.csv /
quests.csv in standard CSV that LibreOffice / Excel / Numbers / Python
pandas all consume natively:

  wowee_editor --export-zone-csv custom_zones/MyZone

  wrote custom_zones/MyZone/creatures.csv (47 rows)
  wrote custom_zones/MyZone/objects.csv (12 rows)
  wrote custom_zones/MyZone/quests.csv (8 rows)

CSV columns chosen to be designer-actionable:
- creatures: index/id/name/displayId/level/health/mana/faction/
  position/orientation/scale + hostile/questgiver/vendor/trainer flags
- objects: index/type/path/position/rotation/scale
- quests: index/id/title/requiredLevel/giver/turnIn/reward fields/
  objectiveCount + objectives/itemRewards joined by '; ' for keep-
  one-row-per-quest sortability

RFC 4180 quoting: fields with comma/quote/newline get wrapped in
double quotes with internal quotes doubled. Verified on a creature
named 'Big, Bad Bear' — comes out as '"Big, Bad Bear"'.

Round-trip back into the editor isn't supported yet (would need
schema-aware CSV parsing); this is read-only-export for now.
2026-05-06 14:16:00 -07:00
Kelsi
8dc91adc52 feat(editor): add --diff-wom completing the diff suite for models
Structural compare of two WOM models. Useful for verifying that
--migrate-wom or a round-trip through OBJ/glTF/STL preserved the
right counts:

  wowee_editor --diff-wom orig back

  Diff: orig.wom vs back.wom
                         a              b
    version     :            1            1
    vertices    :            5            5
    indices     :           18           18
    triangles   :            6            6
    textures    :            0            0
    bones       :            0            0
    animations  :            0            0
    batches     :            0            0
    name        : Pyramid       Pyramid
    bounds      : match
    IDENTICAL

Compares sizes only (vertex / index / bone / animation / batch /
texture counts) plus name and bounds. Bounds match-with-epsilon
(0.01 unit slop, generous since positions are typically yards) so
text-format round-trips that perturb the last bit don't false-flag.
Pointwise vertex compare is intentionally not done — it would be
O(n²) and brittle to tiny float diffs from format conversions.

Diff family is now complete:
  --diff-wcp    (archive vs archive)
  --diff-zone   (unpacked zone dir vs zone dir)
  --diff-glb    (glTF binary vs glTF binary)
  --diff-wom    (WOM model vs WOM model)

Verified: identical pair reports IDENTICAL exit 0; pair with 1 extra
vertex + extra triangle + name change correctly reports 4 DIFFs
(verts/indices/triangles/name) with exit 1.
2026-05-06 14:13:07 -07:00
Kelsi
468a1b8ede feat(editor): add --validate-stl for STL structural sanity check
Pairs with --export-stl / --import-stl / --bake-zone-stl. Catches
the corruption modes that crash slicer mesh analyzers:

  wowee_editor --validate-stl Tree.stl

  STL: Tree.stl
    solid name : Tree
    facets     : 6
    vertices   : 18
    PASSED

  wowee_editor --validate-stl truncated.stl

  STL: truncated.stl
    solid name : Truncated
    facets     : 1
    vertices   : 2
    FAILED — 3 error(s):
      - missing 'endsolid' footer
      - 1 unclosed 'facet' (missing 'endfacet')
      - vertex count 2 != 3 * facet count 1

Checks:
- 'solid' header present
- 'endsolid' footer present
- Every 'facet' has matching 'endfacet' (no leaks)
- Every facet has exactly 3 vertices
- Total vertex count = 3 × facet count
- All facet normals + vertex coords are finite (no NaN/inf)
- 'facet normal' has 'normal' subtoken + 3 floats
- 'vertex' has 3 floats

Errors capped (30 listed) so a giant corrupt file with consistent
breakage doesn't drown the report. Exit 1 on any error so CI can
gate. Format-validator lineup is now complete:
  Open binary: WOM / WOB / WOC / WHM / GLB
  Open text:   JSON DBC / STL
Every shippable open format has a CLI validator.
2026-05-06 14:10:07 -07:00
Kelsi
a3333b7b4d feat(editor): add --bake-zone-obj completing the bake-zone trio
OBJ companion to --bake-zone-glb / --bake-zone-stl. Same multi-tile
WHM aggregation, this time as Wavefront OBJ — opens directly in
Blender / MeshLab / 3DS Max for hand-editing the terrain mesh:

  wowee_editor --bake-zone-obj custom_zones/MyZone
  # -> custom_zones/MyZone/MyZone.obj

  Baked custom_zones/MyZone -> custom_zones/MyZone/MyZone.obj
    2 tile(s), 41472 verts, 65536 tris

Each tile becomes its own 'g tile_TX_TY' block so designers can hide
tiles independently in Blender. Single global vertex pool with
per-tile vertex base indices for face emission (OBJ requires verts
before faces, so we collect per-tile face indices in memory then
emit them after all verts are streamed to disk).

Hole bits respected (cave-entrance quads dropped). Coords match
WoweeCollisionBuilder's outer-grid layout exactly so .obj/.glb/.stl
of the same source align spatially when overlaid.

Why three formats for full-zone export: glTF for on-screen 3D
viewers, STL for fabrication, OBJ for DCC editing. Three different
ecosystems, three different format sweet spots.

Verified: 2-tile zone (Z + added tile) baked correctly. 41472 verts
(2 × 20736), 65536 tris (2 × 32768), 2 'g' blocks (tile_30_30 +
tile_31_30) — matches what --bake-zone-glb reports for the same
input.
2026-05-06 14:07:22 -07:00
Kelsi
6113582a7d feat(editor): add --check-glb-bounds for stale-bounds detection
Cross-checks every position accessor's claimed min/max against the
actual data in the BIN chunk. glTF viewers use these bounds for
camera framing and frustum culling; stale values (e.g. from a tool
that edited geometry without recomputing) cause models to vanish
at certain angles or get framed wrong on load.

  wowee_editor --check-glb-bounds Tree.glb

  GLB bounds: Tree.glb
    position accessors checked : 1
    mismatched                 : 0
    PASSED

  wowee_editor --check-glb-bounds bad.glb

  GLB bounds: bad.glb
    position accessors checked : 1
    mismatched                 : 1
    FAILED — 1 error(s):
      - accessor 0 bounds mismatch: claimed [-9999,-9999,-9999]-[9999,
        9999,9999] vs actual [533.3,533.3,98.5]-[1066.7,1066.7,101.5]

Walks the meshes/primitives tree, dedups the POSITION attribute
accessors (multiple primitives can share one), then for each unique
accessor reads the BIN chunk via the bufferView+byteOffset chain
and recomputes the actual min/max. Compares with float epsilon
(1e-3) since perfect equality across float compilers isn't
guaranteed.

Also flags missing min/max — the glTF 2.0 spec REQUIRES position
accessors to declare bounds (validators like Khronos's reference
impl reject .glbs that omit them).

Verified: a fresh --export-whm-glb passes clean. After hand-editing
the JSON to claim bogus bounds (-9999 to 9999 for a 533-1067 range
mesh), --check-glb-bounds correctly reports the mismatch with full
claimed-vs-actual values, exit 1.
2026-05-06 14:04:48 -07:00
Kelsi
06b21884ad feat(editor): add --bake-zone-stl for full-zone 3D-printable terrain
STL companion to --bake-zone-glb. Designers can 3D-print a miniature
of an entire multi-tile zone in one slicer load — useful for tabletop
RPG props, physical playtest references, or just the satisfaction of
holding a zone in your hand:

  wowee_editor --bake-zone-stl custom_zones/MyZone
  # -> custom_zones/MyZone/MyZone.stl

  Baked custom_zones/MyZone -> custom_zones/MyZone/MyZone.stl
    2 tile(s), 65536 facets, 0 hole quads skipped

Streams ASCII STL directly to disk (no in-memory accumulation —
relevant for large multi-tile zones). Per-triangle face normal
computed from cross product since slicers use it for orientation.
Hole bits respected (cave-entrance quads dropped) and counted
separately so users see how much got skipped.

Why STL alongside the existing glTF zone bake: glTF targets
on-screen 3D viewers; STL targets fabrication. Different ecosystems,
different file formats, both now reachable from the same WHM source
with one command each.

Verified: 2-tile zone (Z + added tile) baked correctly. 65536 facets
(2 tiles × 32768 each), 12MB ASCII STL, well-formed solid/endsolid
framing, normals computed (e.g. '0.305 -0.399 -0.864' for the first
sloped facet).
2026-05-06 14:02:13 -07:00
Kelsi
b7b600c177 feat(editor): add --list-commands and --gen-completion for shell ergonomics
The CLI surface is now 103 commands. Tab completion is no longer a
nice-to-have:

  source <(wowee_editor --gen-completion bash)   # one-time setup
  wowee_editor --info-<TAB>                       # all info-* offered

Two complementary commands:

  --list-commands: parses printUsage's own output to extract every
  '--flag' it documents, dedupes, sorts. Auto-tracks new commands
  as they're added (no parallel list to maintain).

  --gen-completion bash|zsh: emits a completion script that re-execs
  --list-commands at completion time, so newly-added flags light up
  without regenerating the script. Bash version uses compgen with
  per-session caching in ; zsh version uses the
  _arguments + compdef framework.

Both completion scripts also fall back to file-path completion in
arg slots (the common case for --info-/--validate-/--export-
commands that take a path).

Verified:
- --list-commands: 103 unique flags emitted alphabetically
- --gen-completion bash: well-formed script using full binary path
  for the cached --list-commands invocation
- --gen-completion zsh: same shape, _arguments-based
- Unknown shell: clear error + exit 1
2026-05-06 13:59:54 -07:00
Kelsi
d82f90dd82 feat(editor): add --diff-glb completing the diff-* family
Structural compare of two glTF 2.0 binaries. Completes the diff
suite alongside --diff-wcp (archive-vs-archive) and --diff-zone
(unpacked-zone-vs-zone):

  wowee_editor --diff-glb a.glb b.glb

  Diff: a.glb vs b.glb
                         a      b
    meshes      :      1      2  DIFF
    primitives  :    256      2  DIFF
    accessors   :    258      4  DIFF
    bufferViews :      3      3
    buffers     :      1      1
    BIN bytes   : 890880 1781760  DIFF

Reports per-category counts side-by-side with a 'DIFF' marker on
mismatches. Compares structure (mesh/primitive/accessor counts +
BIN chunk size), NOT byte-level — JSON key ordering can vary
between tools so a byte diff would have false positives. JSON mode
emits the per-field {a, b} pair plus totalDiffs + identical bool
for CI consumption.

Useful for confirming alternate export paths produce equivalent
output (does --bake-zone-glb match concatenated --export-whm-glbs?
does a tool refactor preserve the same shape?).

Verified: same file vs itself reports IDENTICAL with exit 0.
Single-tile WHM .glb (1 mesh, 256 primitives, 890KB BIN) vs
2-tile bake (2 meshes, 2 primitives, 1.7MB BIN): correctly flags
4 DIFFs with exit 1.
2026-05-06 13:57:25 -07:00
Kelsi
901e48b659 feat(editor): add --info-zone-tree for hierarchical zone overview
At-a-glance comprehension of a zone's contents in a single `tree`-
style view. The other --info-* commands focus on one category each;
this composes them into a unified picture:

  wowee_editor --info-zone-tree custom_zones/MyZone

  MyZone/
  ├─ Manifest
  │  ├─ mapName     : MyZone
  │  ├─ mapId       : 9000
  │  ├─ baseHeight  : 100.0
  │  ├─ biome       : (unset)
  │  └─ flags       :
  ├─ Tiles (1)
  │  └─ (30, 30)
  ├─ Creatures (2)
  │  ├─ lvl 7  Wolf
  │  └─ lvl 12  Bear
  ├─ Objects (1)
  │  └─ m2   World/Tree.m2
  ├─ Quests (1)
  │  └─ [1] Hunt Wolves (lvl 5, 250 XP)
  │     ├─ kill ×5 Wolf
  │     └─ reward: Item:Sword
  └─ Files (6)
     ├─ Z_30_30.whm
     ├─ Z_30_30.wot
     ├─ creatures.json
     ├─ objects.json
     ├─ quests.json
     └─ zone.json

Quest sub-tree includes objectives + rewards bulleted underneath.
Files section shows what physically lives in the zone dir so users
spot orphan files (e.g. .obj exports) at a glance.

UTF-8 box-drawing characters for connectors. No --json mode by
design — the structured equivalent is just running --info-* per
category and concatenating, which already exists.
2026-05-06 13:54:48 -07:00
Kelsi
d65c315465 feat(editor): add --bake-zone-glb for whole-zone glTF export
Bakes every WHM tile in a multi-tile zone into ONE .glb so the
entire zone opens in three.js / model-viewer / Sketchfab with one
file. Each tile becomes its own mesh+node so they can be toggled
independently in the viewer:

  wowee_editor --bake-zone-glb custom_zones/MyZone
  # -> custom_zones/MyZone/MyZone.glb

  Baked custom_zones/MyZone -> custom_zones/MyZone/MyZone.glb
    3 tile(s), 62208 verts, 98304 tris, 3 meshes, 2672640-byte BIN

Implementation:
- Walks ZoneManifest::tiles, loads each tile's WHM/WOT pair via
  WoweeTerrainLoader.
- Same per-chunk 9x9 outer-grid + 8x8 quads layout as
  --export-whm-glb (so tiles align spatially with corresponding
  --export-woc-obj output).
- All tiles share one global vertex+index pool packed into a single
  BIN chunk; per-tile primitives slice their index range via
  byteOffset on the indices accessor.
- One node per tile, named 'tile_TX_TY', so viewers can hide
  individual tiles for area-by-area inspection of large zones.

v1 limitation: terrain only — object/WOB instances are a follow-up
that needs careful per-mesh bufferView slicing across many distinct
loaded models. Listed in --info-glb as 'meshes' so users see the
shape of the output before opening in a viewer.

Verified: 3-tile zone (Z + 2 added tiles) baked correctly. 62208
verts (3 × 20736), 98304 tris (3 × 32768), 3 meshes/primitives.
--validate-glb on the output: PASSED, all chunk types correct,
accessor bufferView refs in range.
2026-05-06 13:52:09 -07:00
Kelsi
a212479424 feat(editor): add --import-stl to round-trip STL back into WOM
Closes the WOM <-> STL bridge for the fabrication ecosystem (Cura,
PrusaSlicer, TinkerCAD, Meshmixer, SolidWorks, Fusion 360). Designers
can now edit prints in any CAD/3D-print tool and bring them back to
the engine:

  asset_extract     # M2 -> WOM (open binary)
  --export-stl      # WOM -> STL (universal fabrication format)
  ... edit in TinkerCAD / sculpt in Meshmixer ...
  --import-stl      # STL -> WOM (back to engine format)
  --validate-wom    # confirm consistency

Parser handles ASCII STL only (binary STL is a follow-up):
- 'solid <name>' header — preserves model name
- 'facet normal nx ny nz' — sets the face normal for following verts
- 'vertex x y z' — accumulates 3 per facet
- 'endfacet' — emits the triangle with the face normal applied to all
  3 verts (STL doesn't carry per-vertex normals, so the round trip is
  faceted-shading by design)
- Dedupes on (pos, normal) so shared vertices on the same face merge
  (a 4-vert square base with 2 tris collapses to 4 verts), but verts
  shared across faces with different normals stay distinct (correct
  for faceted geometry)
- Computes bounds from positions for renderer culling

Round-trip cost: a 5-vert/6-tri pyramid round-trips to 16 verts/6
tris. The vertex inflation is structural (STL faceted-shading
semantics) — geometry preservation is exact. Verified via
WOM -> STL -> WOM -> --validate-wom (PASSED, bounds and tri count
match exactly).
2026-05-06 13:49:02 -07:00