Map Editor Documentation

Custom Units & 3D Meshy Models

How to generate, rig, animate, and wire custom 3D models and units into Shards of Stone using Meshy.ai, Blender, and data overrides.

Page 14 of 17

Shards of Stone features a dual-presentation architecture: a pure, deterministic lockstep simulation running on a background Web Worker, rendered in the browser through a high-performance Three.js 3D viewport (with an automatic 2D canvas sprite fallback).

Every unit, building, and prop in the 3D world is driven by standard glTF / GLB assets loaded on demand through ModelManager. Using AI generation tools like Meshy.ai, standard DCC packages like Blender, and the game's Data Overrides system, modders and AI agents can create completely new, fully animated, custom 3D units without editing any engine source code.

flowchart LR
    A["Meshy.ai / Blender\n(3D Gen & Rig)"] --> B["Dual GLB Pipeline\n(character + animations)"]
    B --> C["Local / R2 Storage\n(public/assets/models/...)"]
    C --> D["ModelManager\n(Clip Normalization & LODs)"]
    D --> E["Data Overrides\n(defOverrides.units)"]
    E --> F["Three.js 3D Viewport\n(Sockets, VFX & Lockstep Sync)"]

1. Meshy.ai 3D Generation Workflows

Modern generative AI makes producing RTS-ready 3D models fast and accessible. When generating models via Meshy.ai (Text-to-3D or Image-to-3D), adhering to these guidelines ensures your models integrate cleanly into the engine's lighting, LOD, and animation systems.

Prompt Engineering for RTS Readability

Units in an RTS are viewed from an isometric, elevated camera (typically 45° pitch, zoomed out). Subtle surface details wash out at distance; strong silhouettes and high-contrast materials are essential.

  • Style Keywords: Include stylized fantasy RTS, hand-painted textures, bold chunky proportions, clean silhouette, high contrast PBR, low-poly game asset.
  • Proportions: Exaggerate weapons, pauldrons, heads, and hands by 20–30% so they read clearly at distance. Thin weapons or delicate antennae disappear when zoomed out.
  • Color Zoning: Reserve clear areas on armor or cloaks for faction/team colors. Avoid muddy gradients or hyper-detailed noisy noise maps.

Polycount & Detail Targets

The game engine features an automatic mesh-decimation and Level of Detail (LOD) pipeline. Author your high-tier source models to the following polygon budgets:

Unit CategoryTarget TrianglesUse CasesRecommended LOD Ladder
Heroes & Titans~25,000 – 30,000Faction leaders, massive siege golems, dragons, elite avatars30k (source) → 15k → 5k → 500
Standard Combat Units~12,000 – 16,000Ironguards, brawlers, crossbowmen, cavalry, shamans15k (source) → 8k → 3k → 500
Swarm Units & Workers~6,000 – 10,000Ratmen clanrats, goblin peons, skink workers10k (source) → 5k → 1k → 500
Buildings & Towers~15,000 – 25,000Barracks, town halls, guard towers, stone walls20k (source) → 10k → 3k → 500

[!TIP] Do not author models above 35k triangles. The runtime normalizes 1x1 ground units to a baseline height of 24–32 world pixels (UNIT_MODEL_HEIGHT_PIXELS). Extreme polycounts waste GPU bandwidth and memory without improving visual fidelity.

Textures & PBR Materials

  • Channels: Standard glTF Metallic-Roughness PBR. Use Base Color (Albedo), Roughness, Metallic, and Normal maps.
  • Texture Dimensions: Source textures should be 1024×1024 pixels. The asset pipeline automatically optimizes these into compressed KTX2 / Basis Universal tiers (1024, 512, 256, 128) during build.
  • Topology: Ensure manifold geometry with clean edge loops around bending joints (knees, elbows, hips, shoulders) to prevent mesh tearing when animated.
  • Origin & Anchoring: The model's feet must rest on the ground plane at Y = 0 (or Z = 0 in Blender before export) centered at (0, 0). The pivot point is the unit's ground center.

2. The Two Export Pipelines

Shards of Stone uses two distinct export paths depending on whether the entity requires skeletal deformation or rigid/procedural motion:

Pipeline A: Static Units & Buildings (Single GLB)
└── <race>/units/<id>/character.glb   (OR <race>/buildings/<id>.glb)
    └── Rigid mesh, materials, no skinned skeleton.
 
Pipeline B: Rigged Humanoids & Bipeds (Dual GLBs)
├── <race>/units/<id>/character.glb   (Skinned mesh + skeleton bind pose)
└── <race>/units/<id>/animations.glb  (AnimationClips on identical armature)

Pipeline A: Static Units & Buildings (Single GLB)

Not all units require skeletal rigs. In RTS gameplay, mechanical and naval entities look best with rigid bodies:

  • Who Uses This?:
    • All Buildings (town halls, towers, barracks, workshops, walls).
    • Siege Engines (battering rams, steam cannons, demolisher wagons, spark cannons).
    • Naval Vessels (ironclads, dreadnoughts, barges, submarines, transport ships).
    • Rigid Flyers (gyrocopters, sky shredders, zeppelins).
    • Terrain Props & Doodads (gold rocks, runic monoliths, trees).
  • File Structure: A single flat GLB file. For units: character.glb. For buildings: <buildingId>.glb.
  • No animations.glb Required: The game engine evaluates unitHasAnimations(def). Units with roles siege, trader, or naval_trader, or with flags isFlying: true or isNaval: true, or whose IDs end in _ram, _cannon, _wagon, _wheel, _tank, _engine, _shredder, _chariot, etc., automatically bypass animations.glb loading.
  • Procedural Animation: The 3D renderer handles these dynamically:
    • Naval vessels rock realistically with water currents and leave particle wakes.
    • Siege engines recoil procedurally upon firing.
    • Wheeled vehicles roll in sync with movement velocity.
    • Units with deathStyle: 'shatter' dynamically shatter into physics chunks.

Pipeline B: Rigged Humanoids & Bipeds (Dual GLBs)

For humanoid warriors, monsters, workers, and quadrupeds, the engine uses a dual-file architecture:

  1. character.glb: Contains the textured 3D mesh, the vertex skin weights, and the bone armature in its rest / bind pose (or a basic idle clip).
  2. animations.glb: Contains only the animation tracks bound to the exact same bone names. Mesh geometry is stripped out to save memory and allow multiple skins to share one animation set.

Armature & Bone Conventions

Meshy and Mixamo standard humanoid biped armatures are supported out of the box. The standard hierarchy:

Root / Armature
└── Hips
    ├── Spine ── Spine1 ── Spine2 (Chest) ── Neck ── Head
    │   ├── LeftShoulder ── LeftArm ── LeftForeArm ── LeftHand
    │   └── RightShoulder ── RightArm ── RightForeArm ── RightHand (weapon_r)
    ├── LeftUpLeg ── LeftLeg ── LeftFoot ── LeftToeBase
    └── RightUpLeg ── RightLeg ── RightFoot ── RightToeBase

[!IMPORTANT] When exporting character.glb and animations.glb, the bone hierarchy, bone names, and rest poses must match identically. If a bone named RightHand exists in character.glb but is named Hand.R in animations.glb, that limb will fail to animate.


3. The 9 Canonical Animation States & Substring Matching

In traditional game engines, an animator must explicitly wire every clip into a complex state machine graph. Shards of Stone uses an intelligent, zero-configuration substring-matching resolver inside ModelManager.

When a model's animations.glb loads:

  1. Every clip name is normalized: converted to lowercase and stripped of all punctuation, spaces, underscores, and symbols:
    function normalizeClipName(name: string): string {
      return name.toLowerCase().replace(/[^a-z0-9]/g, '');
    }
    Example: "Armature|Walk_Cycle.001""armaturewalkcycle001".
  2. The normalized name is substring-tested against the alias table for each of the 9 Canonical Animation States.
  3. All clips matching a state are pooled into clipPools.get(state).
flowchart TD
    Clip["Clip in animations.glb\ne.g. 'Armature|Attack_Overhead'"] --> Norm["Normalize Name\n'armatureattackoverhead'"]
    Norm --> Match{"Substring Match\nAgainst STATE_ALIASES"}
    Match -->|Contains 'attack'| Atk["clipPools['attack']\n(Random Combat Selection)"]
    Match -->|Contains 'walk'| Walk["clipPools['walk']\n(Movement Loop)"]
    Match -->|Contains 'idle'| Idle["clipPools['idle']\n(Ambient Flavour Cycle)"]
    Match -->|Contains 'spell1'| Sp1["clipPools['special_1']\n(Ability Slot 1)"]

The 9 Canonical States

Below is the authoritative list of supported animation states, their aliases, loop rules, and gameplay triggers:

1. idle

  • Aliases: idle, confusedscratch
  • Loop Config: Multi-clip LoopOnce (cycles variants), single-clip LoopRepeat
  • Behavior: Plays while the unit is stationary without an active attack target or build order. If your model has multiple idle variations (e.g. idle_breathe, idle_scratch, idle_look_around), ModelManager automatically loops through them in random sequence, giving units organic ambient life!

2. walk

  • Aliases: walk, walking, walkcycle, move, moving, baselayer, unrealtake, unreal
  • Loop Config: LoopRepeat, clamp false
  • Behavior: Plays whenever the unit has an active move command and its velocity is greater than 0. The animation playback speed automatically scales with the unit's speed attribute. Note: Meshy frequently exports single-loop animations named "Armature|Unreal Take|baselayer". The aliases baselayer and unreal ensure raw Meshy exports work out of the box.

3. attack

  • Aliases: attack, attacking, hit, swing, strike
  • Loop Config: LoopOnce, clamp true
  • Behavior: Triggered when the unit attacks an enemy. On every swing or shot, Model3DRenderer picks a random variant from the attack pool. Clips named attack_slash, attack_thrust, strike_down, or hit_2 will automatically alternate during battle!

4. death

  • Aliases: death, die, dying, dead
  • Loop Config: LoopOnce, clamp true
  • Behavior: Plays once when the unit's HP reaches 0. The animation clamps and freezes at its final frame, where the unit lies as a physical corpse. The corpse remains until its decay timer expires (or until an undead worker consumes/resurrects it), after which it sinks through the terrain.

5. gather

  • Aliases: gather, harvest, spellcast, magespellcast, magesoellcast, soellcast, mage
  • Loop Config: LoopRepeat, clamp false
  • Behavior: Plays when a worker unit is harvesting resources (quarrying stone, mining gold, or felling timber). Also used as a generic channeling posture for spellcasters.

6. hop

  • Aliases: jump, leap
  • Loop Config: LoopOnce, clamp true
  • Behavior: Dedicated jumping animation for assault units and jetpack infantry (such as the Dwarf Ironjumper) equipped with canHop: true. Clamps during mid-air flight and blends back to walk/idle upon touchdown.

7. special_1

  • Aliases: special1, ability1, spell1, cast1
  • Loop Config: LoopOnce, clamp true
  • Behavior: Primary designer-triggered ability slot. Triggered by unit spellcasting, battle cries, or hero ability 1.

8. special_2

  • Aliases: special2, ability2, spell2, cast2
  • Loop Config: LoopOnce, clamp true
  • Behavior: Secondary designer-triggered ability slot. Used for secondary spells, defensive stances, or hero ability 2.

9. special_3

  • Aliases: special3, ability3, spell3, cast3
  • Loop Config: LoopOnce, clamp true
  • Behavior: Tertiary designer-triggered ability slot. Used for hero ultimates, massive area-of-effect rituals, or summonings.

Summary Matrix

State NameAliases MatchedLoop ModeClamped?Multi-Clip Pool Behavior
idleidle, confusedscratchLoopOnce*NoCycles ambient flavour clips randomly
walkwalk, walking, walkcycle, move, moving, baselayer, unrealLoopRepeatNoPlays first matched clip
attackattack, attacking, hit, swing, strikeLoopOnceYesRandomly picks variant per swing
deathdeath, die, dying, deadLoopOnceYesOne-shot, freezes at final frame
gathergather, harvest, spellcast, magespellcast, mageLoopRepeatNoLoops during resource extraction
hopjump, leapLoopOnceYesHeld during cliff / obstacle vaults
special_1special1, ability1, spell1, cast1LoopOnceYesAbility / Spell Slot 1
special_2special2, ability2, spell2, cast2LoopOnceYesAbility / Spell Slot 2
special_3special3, ability3, spell3, cast3LoopOnceYesAbility / Spell Slot 3 (Ultimate)

* Note: If idle contains only 1 clip, it automatically switches to LoopRepeat.


4. Can Custom Animations Be Added?

Yes, completely and without modifying engine code.

Because ModelManager populates dynamic arrays (THREE.AnimationClip[]) for each state pool, you can add as many animations as you want to your animations.glb. The engine automatically leverages them through three core mechanics:

1. Ambient Flavour Idles

If you export multiple idle clips in animations.glb:

  • Armature|Idle
  • Armature|Idle_Scratch
  • Armature|Idle_Taunt
  • Armature|Idle_Dance

All four clips contain the substring idle and are added to the unit's idlePool. When the unit is idle, it plays one clip. Upon completion (mixer.addEventListener('finished')), the engine selects another random idle from the pool. Units will stand guard, occasionally stretch, glance around, or scratch their beards!

2. Dynamic Combat Variations

If your unit has multiple attack clips:

  • Armature|Attack_Chop
  • Armature|Attack_Stab
  • Armature|Attack_SpinStrike

Because each clip contains attack or strike, they are all registered into actionPools.attack. On every attack interval, the combat system executes:

const pool = instance.actionPools.attack;
const clip = pool[Math.floor(Math.random() * pool.length)];

Your unit will alternate between slashing, thrusting, and power strikes during melee engagements.

3. Dedicated Special Abilities (special_1..3)

By naming clips with special1, spell1, ability2, or cast3, you populate the three dedicated special slots. These can be fired in-game via:

  • Hero Spells: Spells assigned to the unit's Spellcaster component.
  • Triggers: Using trigger actions (cast_spell or custom trigger events).
  • Presentation Calls: Model3DRenderer.triggerSpecial(entityId, 1 | 2 | 3).

The CLI Animation Merger Tool

If you download an animation library containing separate .glb files from Meshy or Mixamo, you can merge them into a production-ready animations.glb using the included tool:

node tools/merge_meshy_animations.js \
  --src path/to/raw_clips \
  --out public/assets/models/dwarf/units/dwarf_warrior \
  --caster \
  --fun-idles
  • --caster: Keeps spell/cast clips and assigns them to special_1, special_2, and special_3.
  • --fun-idles: Automatically promotes otherwise-discarded flavour clips (dance, taunt, cheer, jump, wave) into the idle pool as idle_<name>.
  • --worker: Maps harvest/dig/mining motions to the gather state.

5. Projectile Sockets, Mount Nodes & VFX Anchors

To ensure projectile trails, spell bursts, and muzzle flashes emerge from the right points in 3D space, Model3DRenderer resolves named sockets on your model.

Sockets can be bones in your armature or Empty / Dummy objects parented to bones. Names are matched case-insensitively with punctuation removed.

flowchart TD
    Model["Loaded 3D Character"] --> Detect{"Traverse Nodes\nMatch Aliases"}
    Detect -->|'muzzle' / 'barreltip'| Muzzle["'muzzle' Socket\n(Projectile Spawns & Muzzle Flashes)"]
    Detect -->|'weapontip' / 'swordtip'| WTip["'weapon_tip' Socket\n(Melee Swing Ribbons & Sparks)"]
    Detect -->|'handr' / 'righthand'| HandR["'hand_r' Socket\n(Casting Glows & Weapon Grips)"]
    Detect -->|'spine2' / 'chest'| Torso["Torso Attachment Bone\n(HP Bars, Auras & Mounts)"]

Supported Socket Aliases

  1. muzzle (Ranged Weapon Exit Point):
    • Aliases: muzzle, muzzleflash, barreltip, guntip, weapontip
    • Usage: The exact origin point where arrows, bullets, fireballs, and catapult stones spawn. Also anchors the 4-ray starburst muzzle flash particle effect.
  2. weapon_tip (Melee Weapon End):
    • Aliases: weapontip, swordtip, bladetip, stafftip, tipweapon
    • Usage: Used for ribbon trails during melee swings and tip glows on magic staves.
  3. hand_r (Right Hand / Casting Anchor):
    • Aliases: handr, righthand, mixamorigrighthand, mixamorighthand, handright, rhand
    • Usage: Anchors charged spell effects, glowing runes, torches, and one-handed weapons.
  4. Humanoid Torso & Mount Bones:
    • Bones Checked: mount_head, spine2, spine02, chest, uppertorso, spine1, spine
    • Usage: Used by EntityUI3D for overhead health bars, level badges, status crowns, and seating points for cavalry riders.

[!TIP] Authoring in Blender: Add an Empty (Plain Axes) object, parent it to your character's right hand bone (or rifle barrel), and name it muzzle or weapon_tip. When exported into character.glb, the engine finds it automatically. If no socket is found, the engine uses an intelligent procedural fallback offset based on the unit's bounding box and facing direction.


6. Wiring into UnitDef / defOverrides

Once your 3D assets are exported, you wire them into your custom map using Data Overrides in The Forge (or directly in your map's .play.json file).

Data Override Types: Patch vs. Clone

  • Patch: Modifies an existing unit's stats or appearance in-place.
  • Clone: Creates a brand new unit type that inherits defaults from a baseId.
{
  "units": [
    {
      "id": "dwarf_steam_juggernaut",
      "baseId": "dwarf_ironguard",
      "name": "Steam Juggernaut",
      "modelId": "dwarf_steam_juggernaut",
      "spriteKey": "dwarf_ironguard",
      "portraitKey": "dwarf_ironguard",
      "icon": "icon_ironguard",
      "hp": 350,
      "damage": 38,
      "armor": 6,
      "range": 1.2,
      "attackSpeed": 0.9,
      "speed": 2.2,
      "gridWidth": 2,
      "gridHeight": 2,
      "renderScale": 1.4,
      "baseRotationY": 0.0,
      "cost": { "gold": 220, "lumber": 60, "stone": 40, "oil": 0 }
    }
  ]
}

Full UnitDef Override Property Schema

The following table details all visual and gameplay properties available when defining custom 3D units:

PropertyTypeDescription & RTS Impact
idstringUnique identifier for this unit definition (e.g. "dwarf_valkyrie").
baseIdstring?Base archetype to inherit from (e.g. "dwarf_ironguard"). Required for clones.
namestringDisplay name shown in selection cards, tooltips, and death logs.
modelIdstring?3D Asset Key. Points to public/assets/models/<race>/units/<modelId>/.
spriteKeystring2D sprite fallback key used when 3D mode is disabled or if GLB fails to load.
portraitKeystringAnimated HUD portrait key for the bottom-left selection inspector.
iconstring128×128 UI icon for worker build menus and building production queues.
hpnumberMaximum hit points.
damagenumberCombat attack damage per strike.
rangenumberAttack range in grid tiles (1.0–1.5 for melee, 5.0–10.0 for ranged).
attackSpeednumberAttacks per second (e.g. 1.2 = one attack every 0.83 seconds).
speednumberMovement speed across the grid (tiles per second). Typically 2.0 to 4.5.
gridWidthnumber?Pathfinding footprint width in tiles (1 for standard infantry, 2 for large beasts/tanks).
gridHeightnumber?Pathfinding footprint height in tiles. Multi-tile units scale visually by footprint.
isFlyingboolean?If true, unit hovers aloft, ignores terrain cliffs, and avoids ground collision.
isNavalboolean?If true, paths only on deep water tiles and generates dynamic water wakes.
renderScalenumber?Visual scale multiplier (0.2 to 4.0). Does NOT change collision or footprint.
baseRotationYnumber?Yaw correction in radians. Compensates for models whose native export faces sideways.
selectionRadiusnumber?Radius of the selection ring in pixels. Defaults to (max(gridW, gridH) * 32) / 2 + 3.
canHopboolean?Allows unit to jump over cliffs/walls using its hop animation clip.
hopDistancenumber?Maximum tiles the unit can hop over in a single leap (default 3).

Concrete Copy-Paste Examples

Example 1: Elite Melee Hero / Titan (dwarf_runic_golem)

A massive 2x2 mechanical titan with custom scale, heavy armor, and high health:

{
  "id": "dwarf_runic_golem",
  "baseId": "dwarf_ironguard",
  "name": "Runic Iron Golem",
  "modelId": "dwarf_runic_golem",
  "spriteKey": "dwarf_ironguard",
  "portraitKey": "dwarf_ironguard",
  "icon": "icon_ironguard",
  "role": "heavy",
  "hp": 900,
  "damage": 75,
  "armor": 8,
  "range": 1.4,
  "attackSpeed": 0.75,
  "speed": 1.8,
  "gridWidth": 2,
  "gridHeight": 2,
  "renderScale": 1.35,
  "baseRotationY": 0.0,
  "cost": { "gold": 400, "lumber": 0, "stone": 150, "oil": 50 },
  "buildTime": 45,
  "populationCost": 4
}

Example 2: Flying Siege Gunship (goblin_sky_terror)

A flying goblin war balloon that bombards ground defenses from the air:

{
  "id": "goblin_sky_terror",
  "baseId": "goblin_demolisher",
  "name": "Sky Terror Zeppelin",
  "modelId": "goblin_sky_terror",
  "spriteKey": "goblin_demolisher",
  "portraitKey": "goblin_demolisher",
  "icon": "icon_demolisher",
  "role": "flyer",
  "hp": 480,
  "damage": 55,
  "armor": 2,
  "range": 8.0,
  "attackSpeed": 0.5,
  "speed": 2.6,
  "isFlying": true,
  "canAttackGround": true,
  "canAttackAir": false,
  "gridWidth": 2,
  "gridHeight": 2,
  "renderScale": 1.2,
  "baseRotationY": 1.5708,
  "cost": { "gold": 300, "lumber": 120, "stone": 0, "oil": 40 }
}

[!NOTE] In Example 2, baseRotationY: 1.5708 ($\pi/2$ radians) rotates the model 90 degrees clockwise at load time. Use this if your Meshy export faced right (+X) instead of forward (+Y / down).

Example 3: Caster Hero with Dedicated Abilities (high_elf_archmage)

An elven caster wielding special_1 (Fireball) and special_2 (Blizzard):

{
  "id": "high_elf_archmage",
  "baseId": "dwarf_cleric",
  "name": "High Archmage",
  "modelId": "high_elf_archmage",
  "spriteKey": "dwarf_cleric",
  "portraitKey": "dwarf_cleric",
  "icon": "icon_cleric",
  "role": "caster",
  "hp": 420,
  "damage": 28,
  "armor": 1,
  "range": 7.0,
  "attackSpeed": 1.1,
  "speed": 3.0,
  "mana": 300,
  "renderScale": 1.05,
  "cost": { "gold": 280, "lumber": 80, "stone": 0, "oil": 0 }
}

7. Asset Directory Layout & Cloudflare R2 CDN

To make your custom models accessible to the game engine, place them in the standard asset hierarchy:

Local Development File Paths

public/assets/models/
├── dwarf/
│   ├── units/
│   │   └── dwarf_runic_golem/
│   │       ├── character.glb     <-- Skinned mesh + skeleton
│   │       └── animations.glb    <-- AnimationClips
│   └── buildings/
│       └── dwarf_obelisk.glb     <-- Single rigid GLB
├── goblin/
│   └── units/
│       └── goblin_sky_terror/
│           └── character.glb     <-- Static flyer (no animations.glb needed)
└── neutral/
    └── units/
        └── neutral_dragon/
            ├── character.glb
            └── animations.glb

Cloudflare R2 CDN Integration

In production and local development, assets are served from the Cloudflare R2 CDN bucket (assets.shardsofstone.com). The asset pipeline automatically handles atlas packaging, texture compression (AVIF/WebP and KTX2 Basis Universal), and content-hash cache-busting.

Syncing Assets to R2

When adding new 3D assets to your local public/assets/models/ folder:

  1. Run the asset pipeline build:
    npm run assets:build
  2. Sync the updated files to Cloudflare R2:
    ./tools/sync-r2.sh
    (To preview uploads without modifying the remote bucket, pass --dry-run).

Graceful Degradation & Fallbacks

You never have to worry about missing models crashing a match:

  • If a custom 3D model fails to download or returns a 404, ModelManager catches the error internally, logs a non-fatal warning, and falls back immediately to the 2D canvas sprite (spriteKey).
  • The lockstep game simulation remains 100% synchronized regardless of whether a client is rendering high-polygon 3D meshes or 2D pixel-art sprites.

8. The Modder's Step-by-Step Practical Recipe

Follow this step-by-step checklist to take a concept from prompt to playable 3D unit:

Step 1: Generate Model in Meshy.ai
        • Prompt: "Stylized dwarf iron warrior, heavy runic plate armor, warhammer"
        • Target 15k–25k triangles. Download the Character GLB and Animation Pack.
 
Step 2: Inspect & Clean in Blender
        • Check pivot: Feet must stand on ground plane (Z=0 in Blender).
        • Facing: Unit should face front (-Y in Blender).
        • Add an Empty object parented to right hand named "weapon_tip" or "muzzle".
 
Step 3: Merge Animations
        • Run: node tools/merge_meshy_animations.js --src meshy/warrior --out public/assets/models/dwarf/units/dwarf_warrior --fun-idles
        • Verify character.glb and animations.glb are produced.
 
Step 4: Create Data Override in The Forge
        • Open The Forge (Map Editor) and switch to the Data tab (F9).
        • Create a Clone of "dwarf_ironguard", set id to "dwarf_warrior".
        • Set modelId to "dwarf_warrior".
        • Adjust HP, damage, speed, and renderScale.
 
Step 5: Test in Real-Time
        • Press Ctrl + F9 (or Ctrl + Enter) to launch instant lockstep playtest.
        • Spawn your unit and observe idle cycling, walking, combat swinging, and death!

Previous: AI Creators & LLM Modding. Next: Engine Hooks & Event Bus.