# Unit standard: what a unit model must be and do

Status: **draft for owner review**, 2026-10-20. Machine-readable twin: [`unit-standard.json`](unit-standard.json). Conflicts between sources: [`OPEN-DECISIONS.md`](OPEN-DECISIONS.md).

This standard is for anyone who builds a unit for **JCOM and Conquer**, a browser real-time strategy game built on TypeScript and Three.js. That includes outside 3D designers, AI model designers and our own agents. It covers what every unit model must contain, how it moves, which states it must show, and how to hand it in.

Related standards:

| Topic | Where |
|---|---|
| Blueprints and reference sheets | [`../standards_blueprints/README.md`](../standards_blueprints/README.md) |
| Topology, triangle budgets, LODs, compression | [`../../design_standards_geometry/README.md`](../../design_standards_geometry/README.md) |
| Materials, textures, lighting | [`../../design_standards_lookdev/README.md`](../../design_standards_lookdev/README.md) |

## How to read this

- Every rule names its source. `GAME:` is the live game tree (`data/`, `skirmish/`). `VALIDATOR:` is the jslc-model-validator repo. `ESTATE:` is the z4yin-files checkout. `LAB:` is the owner's E-2D Wireframe Lab spec. `DR:` is the design-repo lane folder (`svc_skills/`).
- **PROPOSED** marks a rule with no source yet. It is our best suggestion and waits for owner approval.
- **OD-nn** points to an open decision. Until the owner decides, follow the recommended value given there and say in your submission which value you used.
- The game rule files are the source of truth. If this page and a game file disagree, the game file wins. Please report the difference.

---

## 1. Scale, units and axes

| Rule | Value | Source |
|---|---|---|
| Unit of length | metres. 1 model unit = 1 real metre | `GAME:data/rules/unit-part-contract.json:4`; `GAME:data/rules/model-precedence.json` (`scaleIsMetres: true`) |
| Up | **+Y** | `GAME:data/rules/unit-part-contract.json:6`; `VALIDATOR:rules/rules.v1.json:11` |
| Forward (nose, bow, gun) | **-Z** (see OD-01) | `GAME:data/rules/unit-part-contract.json:5`; `ESTATE:docs/assets/DOSSIER-STANDARD.md:61` |
| Starboard (right) | +X | `ESTATE:docs/assets/DOSSIER-STANDARD.md:63` |
| Ground | Wheels, tracks, feet and landing gear touch **Y = 0**, centred on the origin | `GAME:data/rules/unit-structure.json:2`; `ESTATE:docs/assets/DOSSIER-STANDARD.md:67` |
| Ships | Design waterline at Y = 0, keel below (see OD-07) | `ESTATE:docs/assets/DOSSIER-STANDARD.md:65-66` |
| Facing check | Travel direction must be within 0.02 rad of -Z | `GAME:data/rules/unit-structure.json:54` |
| Ground check | Lowest point within 0.05 m or 2% of height of Y = 0 | `GAME:data/rules/unit-structure.json:55-56`; `VALIDATOR:rules/rules.v1.json:13` |

In game, the engine scales metres by `metreToWorld = 0.4`. One map tile is 5 m, or 2 world units (`GAME:data/rules/world-scale.json:9-11`). **Do not pre-scale.** Deliver true metres; the game does the rest. A model that faces another way gets a `yawOffset` in the game manifest. New models should not need one (`GAME:data/rules/bldg-defence-dims.json:4`).

Blender users: Blender is Z-up. The glTF exporter's "+Y Up" option produces the axes above. Model with the nose toward Blender **+Y**; it becomes glTF -Z (`GAME:data/rules/bldg-defence-dims.json:4`).

## 2. Dimensions and real-world sourcing

Every unit has a row in `GAME:data/rules/unit-dimensions.json` (887 entries plus 8 class defaults). A row looks like this:

| Field | Meaning |
|---|---|
| `lengthM`, `widthM`, `heightM` | Real metres. Width is the wingspan for aircraft |
| `fit` | Which axis the game fits to: `length` (longest horizontal body), `wingspan`, or `height` (infantry) |
| `bandMinM`, `bandMaxM` | Accepted range for the fitted axis |
| `source` | Where the figure comes from, with a URL to a primary sheet when there is one |
| `confidence` | `high`, `medium`, `low` or `design` |
| `designSize` | `true` when the figure is a design choice rather than a real vehicle |
| `represents` | The real thing the model stands for, for example "M1A2 SEPv3 Abrams" |

Source: `GAME:data/rules/unit-dimensions.json:2-40`.

Rules:

1. **Model to the row's figures, within 2%.** The model validator fails above 5% and warns above 2% (`VALIDATOR:rules/rules.v1.json:6-7`; see OD-04).
2. Use primary sources first: the manufacturer, the armed service, or NAVAIR/NAVSEA product pages. Each row's `source` field shows the standard expected.
3. If the unit has no row, or the row says `confidence: low` or `UNVERIFIED`, find a primary figure and include it in the dossier (section 11).
4. Infantry stand 1.60-1.90 m and no taller than 2.2 m. A standing rifleman with helmet is 1.85 m (`GAME:data/rules/unit-dimensions.json` class `Rifleman`; `GAME:data/rules/unit-structure.json:62`).
5. Ordinary buildings stay under the NASA VAB height of 160.02 m unless the row names an exception (`GAME:data/rules/unit-dimensions.json:12203-12227`).
6. **Canon look.** Castdb canon renders and blueprints are the visual reference for how a unit should look (owner memory, 2026-10-19). Real dimensions still come from the dimensions file.

## 3. Blueprints

The sheet standard (views, scale bars, pixels per metre) lives in [`../standards_blueprints/README.md`](../standards_blueprints/README.md). No single pixels-per-metre value is set in the game files; it varies by sheet.

Unit-specific needs (**PROPOSED**; no source yet):

- One sheet per **distinct silhouette state**: carrier aircraft folded and spread, gear down for fixed-wing, ramp open for transports, and deployed for artillery and launchers with outriggers.
- Pivot locations for every joint in section 6, marked on the side or top view.
- Waterline marked on ship side views.

## 4. Geometry budgets

Triangle budgets, topology and LOD rules are owned by [`../../design_standards_geometry/README.md`](../../design_standards_geometry/README.md). The sources disagree, so the budget conflict is recorded in OD-02.

Unit-specific facts:

- Each unit class maps to one **budget class**: `infantry`, `vehicle`, `aircraft`, `ship` or `structure` (`GAME:data/rules/unit-structure.json:36-46`; mapping in section 7).
- A unit must not be a box. The forge quality gate needs a minimum triangle count per class: infantry 400, wheeled 1,000, tank 1,200, ship 1,500. It also needs at least 60% of the surface textured and at least 4 colour bins (`GAME:data/rules/unit-forge.json:114-165`).
- Meshes must be closed (open edges under 2%). Moving parts must not be welded into the hull (`GAME:data/rules/unit-structure.json:59-61`).
- **Master and game copies** (owner, 2026-10-12, recorded in `ESTATE:skills/blender-model-builder/SKILL.md:202-225`):
  - Every model has one high-resolution **master**. All modelling, rigging and animation happens on the master, and it is never decimated, merged or compressed. The 105,359-triangle E-2D lab export is an example (`LAB:input/OWNER-SPEC.txt:11`).
  - Game copies (LOD0, LOD1 and LOD2) are **derived by script**. Moving parts stay as separately named nodes with their pivots.
  - The export gate checks that the bounding box is within 1% of the master and that pivots are within 2 mm.
  - Outside designers deliver the master. They may also deliver game copies.
  - Master budgets differ from game budgets; see OD-02.

## 5. Node naming and hierarchy contract

The game finds parts **by node name**. A wrong name means a part that does not move.

### 5.1 Contract names

Source: `GAME:data/rules/unit-part-contract.json:128-146`.

| Node | What it is |
|---|---|
| `hull` | Root mesh. Everything else hangs off it |
| `turret` | Yaw pivot |
| `turret_pitch` | Pitch pivot, child of `turret`. May be an empty node |
| `barrel_01` (`_02`, `_03`) | Gun mesh. Recoils along its own axis toward +Z |
| `muzzle_01` | **Empty.** Projectile spawn and muzzle flash, at the barrel tip |
| `track_l`, `track_r` | Tread sheets. Their UVs scroll with speed |
| `wheel_l_0`, `wheel_l_1`, ... and `wheel_r_0`, ... | Wheels, front to back. Index 0 steers on wheeled vehicles |
| `teamcolor_hull` | Player-colour mesh. Any mesh using material `houseTint` also counts |
| `fx_smoke_01`, ... | **Empty.** Damage smoke points |
| `fx_dust_01`, ... | **Empty.** Track dust points |
| `fx_exhaust_01` | **Empty.** Exhaust point (`GAME:data/rules/unit-forge.json:33-37`; see OD-12) |
| `radar_01` | Ship radar dish. Constant spin |
| `rotor_main`, `rotor_tail` | Rotors. They spin about their thinnest axis at the hub. The main rotor sheds on death |
| `<part>_damaged`, `<part>_destroyed` | Swap meshes at the damage bands (section 8) |

The joint file adds `refuel_probe`, `cargo_hook`, `ramp`, `rotor_hub` and `rotor_pivot` (`GAME:data/rules/unit-joints.json:121-176`). Use `hatch`, `door`, `gear`, `prop` or `propeller`, `launcher` and `radar` inside names for those parts. The structure check matches these words (`GAME:data/rules/unit-structure.json:77-89,91-229`).

### 5.2 Naming rules

- Use lower_snake_case for contract names. Matching ignores case. **PROPOSED:** keep all names in lower_snake_case.
- Do not use number-only names; the validator rejects them (`VALIDATOR:rules/rules.v1.json:42`). Unnamed nodes get a warning (`:43`).
- Names such as `Scene`, `Root`, `RootNode`, `Armature`, `Sketchfab` and `node_12` are treated as generic, not as parts (`VALIDATOR:vendor/model-browser/parts-model.mjs:13`).
- Some names disagree between sources (OD-09). Use `barrel_01` under an empty `barrel_recoil` node, `wheel_l_0`, and for jets `fuselage` plus `gear_nose`.

### 5.3 Pivot placement

Set each moving node's **origin** on the real hinge or axle. Source: `GAME:data/rules/unit-structure.json:77-89`. Pivot tolerance is 20% of part size or 0.05 m, whichever is larger (`:57-58`).

| Part | Pivot |
|---|---|
| turret, hatch, launcher | centred in X and Z, seated at the part's lowest point |
| barrel | centred in X and Y, at the breech (the +Z end) |
| wheel | centred in Y and Z (the axle) |
| rotor, radar, gear | centred in X and Z |
| prop | centred in X and Y |
| door | on its hinge line along X |

More rules from the Opus build reviews (`DR:svc_skills/unit-model-contract/references/ADDENDUM-opus-03.md:13-17`, where `DR` is the design-repo lane):

- Every moving node's origin lies inside its own mesh bounds, at the joint.
- `barrel_01` and tool meshes extend along local -Z. If a tool rests facing the rear, put the 180° yaw on the `turret` node, not on the mesh.
- Pose-test each moving part at its travel limit. Nothing may pass through other geometry.
- Openings such as portals, bays and doors are real recesses at least 2 m deep. A dark panel is not an opening (`:18-20`).
- Every LOD keeps every major moving part, such as rotors, wheels and the gun (`ADDENDUM-opus-01.md:9`; `ADDENDUM-opus-02.md:11`).

### 5.4 Example hierarchy (tank)

```
hull
├── turret                 (yaw, +Y)
│   └── turret_pitch       (pitch, +X)
│       └── barrel_recoil  (empty)
│           └── barrel_01  (recoil, +Z)
│               └── muzzle_01   (empty)
├── track_l / track_r
├── wheel_l_0 … wheel_l_n / wheel_r_0 … wheel_r_n
├── sprocket_l / sprocket_r
├── teamcolor_hull
├── fx_smoke_01, fx_dust_01, fx_exhaust_01   (empties)
└── hull_destroyed, turret_destroyed         (hidden swaps)
```

## 6. Joints and articulation

### 6.1 Joint kinds the game knows

Source: `GAME:data/rules/unit-joints.json:21-176`.

| Kind | Node | Motion | Default limits |
|---|---|---|---|
| turret-yaw | `turret` | rotate about Y | -180° to 180° (full traverse) |
| gun-pitch | `turret_pitch` | rotate about X | -8° to +20° |
| track | `track_l`/`track_r` | UV scroll along X | n/a |
| wheel | `wheel_[lr]_n` | rotate about X | continuous |
| hatch | `*hatch*` | rotate about X | 0° to 90° |
| rotor | `rotor_main`, `rotor_hub`, `rotor_pivot` | rotate about Y | continuous |
| refuel-probe | `refuel_probe` | extend along Z | 0 to 3.4 m |
| cargo-hook | `cargo_hook` | rotate about X | -20° to 20° |
| ramp | `ramp` | rotate about X | 0° to 90° |
| wing-fold | clip with `wing` or `fold` | clip | n/a |
| gear | clip with `gear` | clip | n/a |

### 6.2 Rates the game drives

Source: `GAME:data/rules/unit-part-contract.json:35-116`. Model the limits to match the real vehicle; the game uses these rates.

| Class | Turret yaw °/s | Pitch °/s | Pitch range | Recoil m / return s |
|---|---|---|---|---|
| tank | 180 | 40 | -8° to 20° | 0.35 / 0.45 |
| IFV/APC | 120 | 45 | -8° to 30° | 0.22 / 0.35 |
| artillery | 30 | 20 | 0° to 55° | 0.5 / 0.8 |
| wheeled | 90 | 40 | -6° to 25° | 0.08 / 0.2 |
| ship | 80 | 80 | -5° to 45° | 2.4 / 1.3 |

Rotors run at 1,080 °/s (main) and 2,160 °/s (tail). Ship radar runs at 36 °/s (`:29-30,114`). The front wheel steers up to 28° on wheeled vehicles and artillery.

### 6.3 Declaring joints in the GLB

Write each moving node's joint in `node.extras.actuator`. Source: `VALIDATOR:rules/rules.v1.json:46-53`; `VALIDATOR:src/validate.js:118-141`.

```json
"extras": { "actuator": { "joint": "revolute", "axis": [1,0,0], "pivot": [0,1.2,-0.4], "limits": { "min": -8, "max": 20, "unit": "deg" } } }
```

- `joint` is one of `revolute`, `prismatic`, `continuous` or `fixed`.
- Moving joints need an axis.
- Revolute and prismatic joints need limits.

The game and the rig specs use other words for the same thing (OD-08). This mapping is **PROPOSED**:

| Validator | Game joint file | Rig spec |
|---|---|---|
| revolute | rotate (with limits) | yaw, pitch, fold |
| continuous | rotate (0-360), scroll | spin, roll |
| prismatic | extend | slide, recoil |
| fixed | n/a | fixed |

**One driver per joint.** A node is driven by either a clip or the game's procedural rig, never both (`LAB:input/OWNER-SPEC.txt:128`).

## 7. Unit classes, required parts, states and animations

### 7.1 How a model is classed

The game assigns a class from the unit id. The first match wins, in this order: infantry, submarine, helicopter, jet, ship, artillery, defence, truck, tank, wheeled, building (`GAME:data/rules/unit-structure.json:23-35`).

### 7.2 Required parts by class

Source: `GAME:data/rules/unit-structure.json:91-229`, plus forge required nodes at `GAME:data/rules/unit-forge.json:58-96`.

| Class | Budget class | Must have (separate nodes) |
|---|---|---|
| tank | vehicle | hull; turret; barrel_recoil **and** barrel; tracks + sprocket, **or** 4+ wheels; muzzle_01 |
| IFV/APC | vehicle | as tank (`unit-part-contract.json:52-67,118`) |
| wheeled | vehicle | hull; 4+ wheels |
| truck | vehicle | hull or cab; 4+ wheels |
| artillery / MRL | vehicle | hull; 4+ wheels or tracks; launcher/pod **or** barrel; muzzle_01 |
| helicopter | aircraft | hull/fuselage; rotor_main; rotor_tail **or** a second main rotor |
| jet | aircraft | hull/fuselage/airframe; gear (forge wants `fuselage` and `gear_nose`) |
| ship | ship | hull; at least one of prop, turret, launcher or radar |
| submarine | ship | hull; prop; hatch |
| infantry | infantry | a skinned figure with 3+ joints and 1+ clip; fallback node `static` |
| building | structure | hull/body/shell; door |
| defence | structure | turret; barrel or launcher |

Every class also needs `teamcolor` (a mesh or a `houseTint` material) and a destroyed variant (section 8).

### 7.3 Required states and clips

The game drives **named states** by playing glTF clips whose names match patterns. A clip with a matching name plays; one without is ignored (`GAME:data/rules/unit-states.json`, readme).

**Clip rule.** A pose sets a clip to a fraction: 0 is the clip's first frame (rest) and 1 is its last frame (deployed). So every positional clip must run **one way**, from rest to deployed, with no return motion (`GAME:skirmish/src/render/unitStates.ts:1-2,264-270`; see OD-05). Spinning parts use a looping clip, or a named node the rig spins.

**Rest pose** (the bind pose and the first frame of every clip): gear down, wings spread, hook stowed, flaps up, canopy closed, doors and hatches shut, turret forward, gun level.

#### Land vehicles: tank, IFV, wheeled, truck, artillery

States are `travel` (default), `deployed` and `combat`. Source: `GAME:data/rules/unit-states.json` class `vehicle` (from line 718).

| Clip name contains | travel | deployed | combat |
|---|---|---|---|
| `wheel` or `drive` | loop | 0 | 0 |
| `turret` | 0 | 0 | loop (slow) |
| `hitch` | 0 | 1 | n/a |
| `launcher`, `outrigger`, `stabilis`, `deploy` | 0 | 1 | 1 |
| `radar` | 0 | 1 | loop |

Turret, barrel, wheels and tracks are normally driven by the rig from node names (section 6), so clips are optional for them. A deploying vehicle needs a `deploy` or `outrigger` clip. Examples are a launcher that raises or outriggers that drop.

#### Rotary-wing aircraft (helicopter, tilt-rotor)

Land-based helicopters use states `parked` (default), `taxi` and `airborne`. Ship-based ones use the carrier set below. Source: `GAME:data/rules/unit-states.json` class `land-aircraft` (from line 541).

| Clip name contains | parked | taxi | airborne |
|---|---|---|---|
| `stow` or `fold` (blade fold) | 1 | 0 | 0 |
| `gear` (if retractable) | 0 | 0 | 1 |
| `rotor` | 0 | loop idle | loop full |
| `canopy` | 1 | 0 | 0 |

Rotors may instead be spun by the rig from `rotor_main` and `rotor_tail` nodes. Transports add `ramp`, `cargo_hook` and `refuel_probe` nodes as fitted (`GAME:data/rules/unit-joints.json:199-214`).

#### Fixed-wing aircraft (land-based)

The states and table are the same as for rotary-wing. Use `propeller` for prop aircraft. Gear clip: 0 is down and 1 is retracted.

#### Carrier aircraft with folding wings

States are `hangar-deck`, `flight-deck` (default), `airborne` and `landing`. Source: `GAME:data/rules/unit-states.json:26-120`.

| Clip name contains | hangar-deck | flight-deck | airborne | landing |
|---|---|---|---|---|
| `stow` or `fold` | **1 (folded)** | 0 | 0 | 0 |
| `gear` | 0 (down) | 0 | **1 (up)** | 0 |
| `hook` | 0 | 1 | 0 | **1** |
| `flap` | 0 | 0 | 0 | **1** |
| `propeller`, `propulsion`, `screw` | 0 | loop idle | loop full | loop approach |
| `rotor` | 0 | loop idle | loop full | loop approach |
| `dome` (rotodome) | 0 | 0 | loop | loop |
| `canopy` | 1 | 0 | 0 | 0 |
| `launch` (launch bar) | 0 | 1 | 0 | 0 |

Durations the game uses for transitions: wing 5 s, gear 6 s, flap 4 s, spool-up 2 s (`GAME:data/rules/unit-states.json:13-21`).

**Wing fold detail** (`LAB:input/OWNER-SPEC.txt:120-125`): outer wings fold aft. Ailerons and outboard flaps are children of the folding panel. Nacelles and inboard flaps stay on the fixed centre section. Gear retracts with its doors and linkages into modelled wells, and the wheels stay visible.

Reference clip set from the shipped E-2D: `Wing_Port_StoWing`, `Wing_Starboard_StoWing`, `Main_Gear_Port_retraction`, `Main_Gear_Starboard_retraction`, `Nose_Gear_retraction`, `Tailhook_deploy`, `Launch_bar`, `Flap_*_Fowler`, `Aileron_*`, `Elevator_*`, `Rudder_*`, `Propeller_*_rotation` and `Rotodome_rotation` (`GAME:data/rigs/jslc.aircraft.e-2d-hawkeye.json`, model.animations). Control surfaces (aileron, elevator, rudder) are not used by game states today. Their convention, neutral at fraction 0.5, is **PROPOSED**.

#### Naval: ships and submarines

States are `moored` (default), `underway` and `action-stations`. Source: `GAME:data/rules/unit-states.json` class `ship` (from line 909).

| Clip name contains | moored | underway | action-stations |
|---|---|---|---|
| `propeller`, `propulsion`, `screw` | 0 | loop | loop |
| `radar` | 0 | 0 | loop |
| `rotor` | 0 | 0 | loop |
| `train` (gun traverse) | 0 | 0 | 1 |
| `elevate` | 0 | 0 | 0.4 |
| `door`, `hatch`, `gate` | 0 | 0 | 0 |

Radars can instead be a `radar_01` node, which the rig spins. Carriers and amphibious ships also carry deck elevators and well-deck gates as clips. The Ford and Wasp base ships show the pattern (`GAME:data/rules/unit-states.json` models).

#### Structures and defences

States are `idle` (default), `active` and `alert`. Source: `GAME:data/rules/unit-states.json` class `building` (from line 1138); `GAME:data/rules/model-anim.json`.

| Clip name contains | idle | active | alert |
|---|---|---|---|
| `door`, `hatch`, `gate`, `deploy`, `bolt` | 0 | 1 | 1 |
| `track`, `ignite` | 0 | loop | loop fast |

Buildings also use these clips (`GAME:data/rules/model-anim.json:13-18`):

- Production: any clip matching `produc`, `work`, `active`, `build` or `open` plays while a unit is being built.
- `damage`: plays below 50% health.
- Construction: `construct`, `rise` or `build_up`. Without it, the building rises from the ground over 2.5 s.
- Any other clip loops as idle.

Nodes named `radar`, `dish`, `fan`, `turbine`, `pump` or `flag` animate procedurally when there are no clips.

#### Infantry

The structure gate needs a skinned figure with at least 3 joints and at least 1 named clip (`GAME:data/rules/unit-structure.json:196-205`). No source names the required clips (OD-10). **PROPOSED:** `idle`, `move`, `attack` (loops) and `die` (one-shot), on one skinned mesh of one person. A "team" roster row still uses one body (`GAME:data/rules/unit-dimensions.json` class `Rifleman` notes).

## 8. Damage and destroyed states

Source: `GAME:data/rules/unit-part-contract.json:31-33,120-127,144-145`.

| Health below | What happens |
|---|---|
| 50% | `*_damaged` swaps in if present. A building plays a `damage` clip |
| 35% | Smoke starts at a random `fx_smoke_nn` |
| 15% | `*_destroyed` swaps in. `rotor_main` sheds |

What to model:

- Name swap meshes after the part they replace: `hull_destroyed`, `turret_destroyed`, `hull_damaged`. Keep them **hidden** in the file, at the same pivot as the original.
- At least one destroyed variant is required. Damaged variants are optional (OD-11).
- Put two or more `fx_smoke` empties on the hull, at engine decks, hatches or wing roots.
- If there is no destroyed swap, the game makes a procedural wreck: darkened by 0.32, tilted 0.06-0.3 rad, sunk 0.12, burning for 3.5 s, and kept for about 35 s (`GAME:data/rules/fx.json:121-128`). A modelled wreck replaces that.

## 9. Textures and materials

Owned by [`../../design_standards_lookdev/README.md`](../../design_standards_lookdev/README.md): PBR, texel density, atlases, formats and sizes. Related open decisions are OD-03 (maximum size) and OD-06 (formats).

Checks that belong to the **unit contract**:

- **Team colour.** At least one material is named `houseTint`. The game also accepts `team_colour`, `teamColour`, `team-colour` and `TeamColor`. Or a mesh is named `teamcolor_*`. The game tints it to the player colour, and its base colour factor is white (1,1,1,1). Sources: `GAME:data/rules/unit-structure.json:48-51`; `GAME:data/rules/unit-part-contract.json:11-12`; `GAME:data/rules/unit-forge.json:25-30`.
- **Keep team colour neutral** (`DR:svc_skills/unit-model-contract/references/ADDENDUM-opus-03.md:5-8`):
  - The `houseTint` base colour is near-white (#E0E0E0 or lighter), or the material is untextured with a white factor.
  - Never bake a faction colour into it.
  - Use exactly `houseTint` in every LOD, with no `.001` suffix.
  - Put fixed faction colours on a separate material.

  The game lerps the material colour 55% toward the team colour and never replaces textures (`GAME:data/rules/model-precedence.json:172,292-293`).
- **Tread texture** on `track_l` and `track_r` tiles every 0.76 m along the track, with 4 links per tile, so that the UV scroll matches ground speed (`GAME:data/rules/unit-part-contract.json:13`).
- Textures are embedded in the GLB. There are no external files and no data URIs (`VALIDATOR:rules/rules.v1.json:32`; `VALIDATOR:src/validate.js:41`).

## 10. Export and file format

| Rule | Source |
|---|---|
| glTF 2.0 binary `.glb` with `asset.version` "2.0", one scene, embedded buffers | `VALIDATOR:src/validate.js:40-43` |
| Must pass the Khronos glTF Validator with no errors | `VALIDATOR:rules/rules.v1.json:70-73` |
| Clips are named; no more than 64 clips; none longer than 600 s | `VALIDATOR:rules/rules.v1.json:54-58` |
| Positions are finite; degenerate triangles and duplicate meshes are warned | `VALIDATOR:rules/rules.v1.json:59-64` |
| Real-size dimensions in `asset.extras.unit_dimensions` as `{ "length_m", "width_m", "height_m" }` (OD-14) | `VALIDATOR:src/validate.js:49-55` |
| Up axis in `asset.extras.up_axis = "Y"` (optional) | `VALIDATOR:src/validate.js:69` |
| Optional state map in `asset.extras.asset_management.condition_map`, schema `asset-management/conditions/v1`. Each state names its clips and visible or hidden nodes, and each transition names a clip | `VALIDATOR:vendor/model-browser/conditions.js:6-27` |
| Compression, LOD files and decimation | owned by the geometry standard; OD-06 and OD-13 |

**Do not** bake the current pose into the clips. Keep the rest transforms, and export from the rest pose (`LAB:input/OWNER-SPEC.txt:204,244`).

**File names.** There is an owner rule (2026-10-12): no file name may claim a status such as fixed, final, good, new or latest. Names state what changed and when: `<asset>-<what changed>-<UTC stamp>-<id12>.glb` (`ESTATE:skills/blender-model-builder/SKILL.md:199-200`). Use the roster id as `<asset>`. The game-copy names differ between sources (OD-13).

## 11. Dossier

Every unit has a research dossier that follows `ESTATE:docs/assets/DOSSIER-STANDARD.md` (DOSSIER-STD-2026-V1).

**YAML front matter.** Required fields:

- `generated_at` (UTC ISO 8601)
- `generator`
- `skills`
- `provider`
- `models`
- `query`
- `sources_count`

`asset_id` and `title` are recommended (`:18-51`).

**Five sections** (`:55-93`):

1. Coordinate datum and dimensions: -Z forward, +Y up, ground or waterline at Y = 0, the full metric envelope, and mass.
2. 11-station transverse offset table. This is for hulls; **PROPOSED:** for aircraft and vehicles, give fuselage or hull stations instead.
3. Kinematic scene graph: an ASCII tree of every moving part with its pivot, axis and limits. This maps onto section 5.
4. Sensor siting and symmetry audit.
5. AI video and verification prompt package.

The game turns the dossier into a **rig spec**, `GAME:data/rigs/<roster-id>.json` (schema `rig-spec/1`). The rig spec lists parts (name, role, parent, joint, axis, pivot, limits, clips), sockets (muzzle, smoke, dust, exhaust, launch, wake, rotor_wash, teamcolor) and damage zones. The owner writes free-text notes in `<roster-id>.notes.md` beside it, and agents read those first. The rig specs are drafts generated from dossiers; check them against the GLB.

## 12. Roster entry fields

Every unit is one `RosterEntry` in `GAME:data/roster/{jslc,aoi,penguins}.json`, typed by `GAME:data/roster/schema.ts:212-250`. Designers do not write roster rows. They do need the fields that touch the model:

| Field | Type / values | Why the modeller cares |
|---|---|---|
| `id` | canonical slug, e.g. `jslc.vehicle.paladin-tank` | Name your files and dossier `asset_id` after it |
| `legacyId` | e.g. `coalition.vehicle.paladin-tank` | Older manifests and dimension rows use it |
| `faction` | `jslc`, `aoi`, `penguins` (`:33`) | Faction look; colours from `FactionMetadata.colors` (`:42-46`) |
| `kind` / `category` | unit, structure, upgrade, power / infantry, vehicle, aircraft, naval, building, defense (`:50-61`) | Picks the class family |
| `name`, `tier`, `description` | text, 1-5 | Context |
| `dimensions` | `lengthM`, `widthM`, `heightM`, `massTonnes`, `basis` (`official`, `representative`, `fictional`), optional `foldedFootprint` {L,W,H} and `rotorEnvelope` {L,W} (`:119-136`) | Folded size for carrier aircraft; rotor disc for helicopters. Your folded state must match `foldedFootprint` |
| `locomotion.type` | foot, tracked, wheeled, hover, air, water, ship, amphibious, subsurface, static, orbital (`:65-76`) | Decides tracks vs wheels vs rotors, and so the required parts |
| `locomotion.turnsInPlace` | bool | Tracked pivot turns: both tracks scroll opposite ways |
| `weapons[]` | `id`, `name`, damage, range, `targets` (`:92-108`) | **PROPOSED:** one `muzzle_nn` empty per weapon, in the order listed |
| `subsystems[]` | id or {`id`, `name`, `type`, `isExposed`, `hpSharePercent`} (`:138-145`, `:233-234`) | **PROPOSED:** an exposed subsystem should be a separate, named node so it can show damage |
| `parasite` | `dockType`, `capacity`, `acceptedLaunchCategories` (catobar, stovl, rotorcraft, land), `wellDeckSlots`, `flightDeckSlots` (`:147-163`) | Carriers and transports need doors, ramps, elevators and spots to match |
| `modelLink` | `set`, `key`, `unitKey`, `s3Key`, `sha256` (`:176-202`) | Filled by us at ingest; links the roster row to your GLB |
| `_src` | field path to citation (`:245-249`) | Every figure must be cited, the same rule as this standard |

Note: the roster's `dimensions.basis` (official, representative, fictional) and the dimension file's `confidence` (high, medium, low, design) are two different scales for related ideas.

## 13. Validation: what is checked

Three checkers run on a submitted unit.

**A. Model validator** (`jslc-model-validator`, rules version 1.0.1). Verdict: any fail gives `fail`, else any warning gives `warn`, else `pass` (`VALIDATOR:README.md`).

| Check | Fails when |
|---|---|
| `gltf.valid` | Khronos validator errors, external buffers, no scene or mesh, or (in structural mode) compressed geometry |
| `scale.dimensions` | any axis is more than 5% off the reference (warn above 2%) |
| `orientation.up` | not Y-up, or ground gap above 0.05 m (warn) |
| `budget.profile` | triangles, draw calls or bytes over the profile |
| `textures.images` | missing, not power of two, over 4096, a data URI, or a format outside PNG, JPEG, WebP or KTX2 |
| `rig.nodes` | missing required nodes, number-only names (unnamed nodes are warned) |
| `actuators.defs` | unknown joint type, a moving joint with no axis, or a revolute or prismatic joint with no limits |
| `animation.clips` | an unnamed clip, too many clips, or a clip that is too long |
| `geometry.*` | non-finite positions (degenerate and duplicate meshes are warned) |
| `lod.levels` | LOD0 missing (LODs that do not get smaller are warned) |
| `conditions.embedded` | an embedded state map that names missing clips or nodes |

**B. Game structure check** (`tools/validators/unit-structure.mjs`, rules in `GAME:data/rules/unit-structure.json`). It scores out of 100: parts 35, pivots 15, unwelded 10, closed 10, scale 8, facing 7, ground 5, budget 5, team colour 5. Only **100** earns `STRUCTURED`. The other verdicts are `PARTIAL`, `STATIC` and `BROKEN` (`:2,10,65-76`).

**C. Forge quality gate** (`tools/unit-forge/quality-gate.mjs`, rules in `GAME:data/rules/unit-forge.json:114-165`). It catches box figures, untextured meshes and single flat colours.

## 14. Unit creation checklist

Before you start:

- [ ] Find the unit's row in `unit-dimensions.json` and note `fit`, the figures and `confidence`.
- [ ] Find the castdb canon render or blueprint for the look.
- [ ] Find or write the dossier (section 11).
- [ ] Find the class (section 7.1) and its required parts and states (section 7).

Modelling:

- [ ] Metres, +Y up, nose to -Z, ground or waterline at Y = 0, centred on the origin.
- [ ] Size within 2% of the reference on the fitted axis.
- [ ] Every required part is a separate node with a contract name (section 5).
- [ ] Every moving node's origin sits on its real hinge or axle.
- [ ] Moving parts are not welded to the hull, and meshes are closed.
- [ ] Empties are placed: `muzzle_01` per gun, two or more `fx_smoke`, plus `fx_dust` and `fx_exhaust` where they apply.
- [ ] Team colour is on a `houseTint` material or a `teamcolor_*` mesh.
- [ ] `*_destroyed` swap is present and hidden; `*_damaged` is optional.
- [ ] Triangle count fits the budget class (geometry standard).

Rig and animation:

- [ ] The rest pose is the bind pose: gear down, wings spread, doors shut, gun level and forward.
- [ ] Every positional clip runs one way, from rest to deployed. Clip names contain the keywords for the class's states.
- [ ] Spinning parts have a loop clip or a spin-node name.
- [ ] Each moving node has a joint in `extras.actuator`, with type, axis and limits.
- [ ] Each joint has one driver: a clip or the rig, not both.

Export:

- [ ] Uncompressed GLB, embedded PNG or JPEG textures, glTF 2.0.
- [ ] `asset.extras.unit_dimensions` is filled in.
- [ ] The model validator passes, or each warning is explained.
- [ ] Review stills:
  - a ground plane, a -Z front arrow and a 1.8 m human for scale (`DR:.../ADDENDUM-opus-02.md:9`);
  - a `_posed` still of each moving part at its limit, plus a `_34rear` view (`ADDENDUM-opus-03.md:16,21`);
  - grey-clay stills at 50 m and 120 m framing, and a 393x852 in-game crop (`GAME:data/rules/unit-forge.json:110`);
  - one set per required state. **PROPOSED:** this per-state requirement has no source yet.

Submission:

- [ ] GLB(s), dossier `.md` and the submission sheet (section 15).

## 15. Submitting to the asset library

Today the asset library is the estate model browser and asset manager. It stores files in object storage and registers each version in a database (`ESTATE:docs/assets/PULL-IN.md`, sections 1-3). The public API at `assets.jslc.io` is **read-only** (`VALIDATOR:config/validator.config.json`). Dossier upload goes through an API on the local network only (`ESTATE:docs/assets/DOSSIER-STANDARD.md:97-112`). So an **outside designer cannot upload directly.**

**PROPOSED process:**

1. Hand the files to the project contact. Send:
   - the GLB(s);
   - the dossier;
   - a short submission sheet giving the roster id, class, nose axis, the reference dimensions used and their source, the clip list with the meaning of 0 and 1 for each clip, any open-decision values used, and the licence and author of any borrowed parts.
2. An agent checks the SHA-256 for duplicates (`PULL-IN.md:43-58`). It then adds the model as a new asset version:
   - with the additive ingest tool `ESTATE:tools/model-ingest/README.md` (`ingest-version.cjs`, which never overwrites, writes a receipt, and has a `--verify` step);
   - or with the publish path `DR:svc_skills/model-publish-path/SKILL.md`.

   It uploads the dossier through the dossier API, runs the three checkers in section 13, and records the verdicts. The master is stored as the asset version; game copies are linked derivatives (`ESTATE:skills/blender-model-builder/SKILL.md:225`).
3. The model enters the game only through a manifest entry, after review.

## 16. Sources and known gaps

- Game rule files were read from the live game tree on 2026-10-20. Most are marked `assumedDefault: true` under ADR-010, which means they are tunable defaults, not fixed law.
- The dossier standard sits at `docs/assets/DOSSIER-STANDARD.md` in the z4yin-files **main checkout**. It is not in the `cc-dispatch-pages` worktree.
- The recall index (`tools/recall/ask.mjs`) was built on 2026-10-13 and is 7 days stale. The worktree copy has no index. The estate `estate_find` search returned "Read failed", and `estate_context_search` returned no hits. So "no source found" in this document means not found in the files listed above. It does not prove that no rule exists.
- No source was found for these, so they are marked PROPOSED: infantry clip names, the control-surface clip neutral, game-copy file names, the sheets and stills needed per state, the outside-designer hand-off, and one muzzle per weapon.
