Skip to content

feat(art): Synty weapons for the player and the bots#28

Merged
ralyodio merged 1 commit into
mainfrom
feat/synty-weapons
Jul 26, 2026
Merged

feat(art): Synty weapons for the player and the bots#28
ralyodio merged 1 commit into
mainfrom
feat/synty-weapons

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Replaces the generated carbine viewmodel with the licensed Synty rifle, arms the bots, and makes every licensed conversion reproducible from one command.

What ships

wep_rifle player viewmodel + Directorate bots
wep_smg Nightcell bots
wep_sniper converted, available
synty_weapons.webp 2 KB — the whole armoury

Two silhouettes rather than one, so which side a figure is on is legible before the team tint on its marker resolves. The generated carbine is still built and still ships as a fallback that does not need the licensed pack.

Shell budget 7.13 MB against the 9 MB guard.

Assembled, not converted

Synty ships a weapon modular — the preset is a receiver, and the magazine, sight, trigger, slide and charging handle are separate FBX files sharing its origin. import_synty_weapon.py globs the parts, joins and welds them, and skips bipods and fold-out foregrips, which hang below the barrel and read as damage at viewmodel distance.

SOCKET_MUZZLE is measured, not assumed

CLAUDE.md requires one on every weapon, and the pack is not consistent about which way a weapon faces. The barrel is the longest axis; which end is the muzzle is decided by the pistol grip, which is the lowest part of a firearm and always sits behind the barrel.

The first heuristic — "the muzzle end is the thinner end" — holds for the rifle and the sniper and fails on the SMG, whose folding stock is a thin rod. Its socket ended up on the stock, where muzzle flash and tracers would have spawned behind the shooter's shoulder. Rendering a marker at the socket is what caught it; the coordinates alone looked reasonable for all three. assets.test.ts now guards each weapon's socket position.

Three bugs the screenshots forced out

  • The FBX import transform is now baked into the mesh. Left alone it is a 0.01 scale and a −90° X rotation that survives export as a node transform, making every child coordinate centimetres in a rotated basis — the muzzle socket read 72.0 rather than 0.72.
  • placeAll gained a unique option. It returns hardware instances by default, and assigning to an InstancedMesh's material is a silent no-op — so every viewmodel material change was landing on nothing. Forcing the weapon bright red changed nothing on screen, which is what finally identified it.
  • synty_weapons is barely metallic. "Gun metal, so make it metallic" renders it black: a metal is lit almost entirely by what it reflects, this atlas ships no metallic or roughness map, and the environment contribution is turned down for close-range work. Synty paint their metal into the albedo instead.

Reproducibility

tools/art/import-synty.mjs records the source file, material slot, decimation ratio and atlas for every licensed asset. Those parameters previously lived only in one session's shell history. The props and vehicles it regenerates come back byte-identical — which is how the missing atlas arguments were found. synty_atlas.webp is re-encoded from source so the driver reproduces it too (+39 KB).

Not wired into pnpm assets:build: that runs in CI, and this needs the 406 MB purchased pack.

Verification

Typecheck, lint, 202 tests and the full build all pass. The viewmodel and bot weapons were checked by driving the built game in a browser and screenshotting /play, not by eye over the source.

🤖 Generated with Claude Code

Replaces the generated carbine viewmodel with the licensed Synty rifle, arms
the bots, and makes every licensed conversion reproducible from one command.

Weapons are assembled rather than converted: Synty ships a preset as a receiver
plus separate magazine, sight, trigger, slide and charging handle sharing one
origin, so `import_synty_weapon.py` globs the parts, joins and welds them, and
skips the bipods and fold-out foregrips that read as damage up close.

SOCKET_MUZZLE (CLAUDE.md) is placed by measuring the mesh, because the pack is
not consistent about which way a weapon faces. The barrel is the longest axis;
which end is the muzzle is decided by the pistol grip, which is the lowest part
of a firearm and always sits behind the barrel. The first heuristic — "the
muzzle end is thinner" — is true of the rifle and the sniper and false of the
SMG, whose folding stock is a thin rod, and put that socket on the stock.
Rendering a marker at the socket caught it; the coordinates looked reasonable
for all three. assets.test.ts now guards each weapon's socket position.

Three fixes the screenshots forced out:

- The FBX import transform is baked into the mesh. Left alone it is a 0.01
  scale and a -90° X rotation that survives export as a node transform, making
  every child coordinate centimetres in a rotated basis — the muzzle socket
  read 72.0 rather than 0.72.
- `placeAll` gained a `unique` option. It returns hardware instances by
  default, and assigning to an InstancedMesh's material is a silent no-op, so
  every viewmodel material change was landing on nothing. Forcing the weapon
  bright red changed nothing on screen, which is what identified it.
- `synty_weapons` is barely metallic. "Gun metal, so make it metallic" renders
  it black: a metal is lit by what it reflects, this atlas ships no metallic or
  roughness map, and the environment contribution is turned down for
  close-range work. Synty paint metal into the albedo instead.

tools/art/import-synty.mjs now records the source file, material slot,
decimation ratio and atlas for every licensed asset. Those parameters
previously lived only in shell history; the props and vehicles it regenerates
come back byte-identical, which is how the missing atlas arguments were found.
synty_atlas.webp is re-encoded from source so the driver reproduces it too.

Shell budget 7.13 MB against the 9 MB guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

vu1nz Security Review

0 finding(s) in PR #?

No security issues found.

@ralyodio
ralyodio merged commit 8fb62aa into main Jul 26, 2026
6 checks passed
@ralyodio
ralyodio deleted the feat/synty-weapons branch July 26, 2026 17:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant