Commit graph

3946 commits

Author SHA1 Message Date
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
Kelsi
9b24e0be8a feat(editor): add --export-stl for 3D-printer-compatible WOM export
ASCII STL is the universal 3D printing format — Cura, PrusaSlicer,
Bambu Studio, Slic3r, OctoPrint, MakerBot Print, and basically every
slicer made in the last 25 years opens it natively. Lets WOM models
drive physical prints with no conversion friction beyond one command:

  wowee_editor --export-stl Tree         # -> Tree.stl
  wowee_editor --export-stl Tree out.stl

Per-spec STL ASCII output:
- 'solid <name>' header / 'endsolid <name>' footer (name sanitized
  to alphanum + underscore for slicers that strict-parse)
- Per-triangle 'facet normal nx ny nz' with normal computed from
  cross-product of edges 1 and 2 (most slicers use this for
  orientation hints; falls back to (0,0,1) for degenerate triangles)
- 'outer loop' with three vertex lines per facet
- No shared vertex pool — STL stores every triangle independently

Why STL alongside OBJ + glTF: OBJ targets DCC tools (Blender etc.),
glTF targets web 3D viewers (Sketchfab, three.js), and STL targets
fabrication. Three different ecosystems, three different format
needs — wowee open formats now bridge to all three.

Verified on a 5-vert/6-tri pyramid: STL has 6 facets with correctly
computed normals (0 -1 0 for the bottom faces, computed slopes for
the side triangles), proper solid/endsolid framing, name preserved
('solid Pyramid').
2026-05-06 13:46:25 -07:00
Kelsi
6d79963f24 feat(editor): add --migrate-zone for batch WOM upgrade across a zone
Companion to --migrate-wom (single-file upgrade). Walks a zone dir
recursively and upgrades every legacy WOM (v1/v2 with no batches[])
to WOM3 in-place:

  wowee_editor --migrate-zone custom_zones/MyZone

  upgraded: custom_zones/MyZone/models/v1_0.wom
  upgraded: custom_zones/MyZone/models/v1_1.wom

  migrate-zone: custom_zones/MyZone
    scanned   : 3 WOM file(s)
    upgraded  : 2 (added single-batch entry)
    already v3: 1 (no change needed)

Idempotent: already-migrated files become no-ops, so it's safe to
run inside --for-each-zone over a whole project. Useful when the
editor adds a new WOM3-only feature and legacy zones need a one-shot
upgrade pass.

Returns exit 1 if any file fails to write (separate from 'no upgrade
needed'), so CI can gate on it.

Verified: seeded a dir with 2 v1 WOMs + 1 v3 WOM. First run
upgraded 2, reported 1 as already-v3. Re-run reported all 3 as
already-v3 (no double-batching).
2026-05-06 13:44:16 -07:00