Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions apps/game/public/assets/PROVENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,19 @@ material-slot or `COL_` conventions the generated props use;

## Synty POLYGON Military

| File | Source | Licence |
| ------------------------------ | ------------------------------------------------------- | ------------------------------- |
| `models/fighter_insurgent.glb` | Synty POLYGON Military — `SK_Chr_Insurgent_Male_01` | Synty Store licence (purchased) |
| `models/fighter_soldier.glb` | Synty POLYGON Military — `SK_Chr_Soldier_Male_01` | Synty Store licence (purchased) |
| `textures/synty_atlas.webp` | Synty POLYGON Military — `PolygonMilitary_Texture_01_A` | Synty Store licence (purchased) |
| File | Source | Licence |
| ------------------------------ | ----------------------------------------------------------- | ------------------------------- |
| `models/fighter_insurgent.glb` | Synty POLYGON Military — `SK_Chr_Insurgent_Male_01` | Synty Store licence (purchased) |
| `models/fighter_soldier.glb` | Synty POLYGON Military — `SK_Chr_Soldier_Male_01` | Synty Store licence (purchased) |
| `models/veh_armored_car.glb` | Synty POLYGON Military — `SM_Veh_Light_Armored_Car_01` | Synty Store licence (purchased) |
| `models/veh_technical.glb` | Synty POLYGON Military — `SM_Veh_Pickup_Technical_01` | Synty Store licence (purchased) |
| `models/prop_barrel.glb` | Synty POLYGON Military — `SM_Prop_Barrel_01` | Synty Store licence (purchased) |
| `models/prop_barrel_stack.glb` | Synty POLYGON Military — `SM_Prop_Barrel_Stack_01` | Synty Store licence (purchased) |
| `models/prop_ammo_box.glb` | Synty POLYGON Military — `SM_Prop_AmmoBox_01` | Synty Store licence (purchased) |
| `models/prop_barrier.glb` | Synty POLYGON Military — `SM_Prop_Barrier_Tall_01` | Synty Store licence (purchased) |
| `models/prop_water_tank.glb` | Synty POLYGON Military — `SM_Prop_WaterTank_02` | Synty Store licence (purchased) |
| `textures/synty_atlas.webp` | Synty POLYGON Military — `PolygonMilitary_Texture_01_A` | Synty Store licence (purchased) |
| `textures/synty_vehicles.webp` | Synty POLYGON Military — `PolygonMilitary_Land_Vehicles_03` | Synty Store licence (purchased) |

Purchased from https://syntystore.com. The licence grants perpetual,
royalty-free commercial use in unlimited titles and permits modification; it
Expand All @@ -131,3 +139,31 @@ plain glTF path cannot:

Kept clips: `Idle_Gun`, `Idle_Gun_Shoot`, `Walk`, `Run`, `Run_Shoot`, `Death`,
`HitRecieve`.

### Vehicles and props (static meshes)

The vehicles and props carry no rig, so none of the character retarget applies.
They are converted with `tools/art/blender/import_synty_prop.py`, which is the
easy path the characters could not take:

- **Shared atlases, so props cost no texture at all.** Every prop was authored
against `PolygonMilitary_Texture_01_A`, which already ships as
`synty_atlas.webp` for the characters, so the props reuse it and add only
geometry. The vehicles use Synty's Land_Vehicles atlas; the pack ships ten
recolour variants over one shared UV layout, so **one** desert variant
(`Land_Vehicles_03`) is shipped as `synty_vehicles.webp` (76 KB) and bound to
every vehicle mesh, rather than a separate 2 MB texture per vehicle. Glass was
authored against Texture_01_A, so glass meshes are routed to `synty_atlas` by
mesh name at convert time.
- **Decimated to fit the shell budget.** Synty's vehicle FBX is far denser than
the game needs for set-dressing — the armoured car imports at 24k triangles /
1.5 MB uncompressed. The two vehicles are decimated (car 0.4, pickup 0.5) to
land near 0.5 MB and 0.4 MB, verified by preview render to be indistinguishable
at gameplay distance. Props ship undecimated; they are already 12–143 KB.
- **No embedded textures**, exactly like the characters and generated props: the
GLB names its material slot and the engine binds the atlas by that name.

These are licensed static meshes, so — like the characters — they do not follow
the generated props' material-slot or `COL_` conventions;
`apps/game/src/assets.test.ts` exempts them explicitly. They are cosmetic
set-dressing and carry no collision proxy.
18 changes: 13 additions & 5 deletions apps/game/public/assets/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"generator": "tools/art/build-assets.mjs",
"blender": "Blender 4.5.12 LTS",
"commit": "af4fbdcb5b822384fb6f8c3f84e98141465453aa",
"commit": "dfb11e7362e0b5e6f91388e6af2457edb96d0764",
"license": "Original work, © NIGHTCELL 7. No third-party assets.",
"source": "Generated procedurally from the scripts in tools/art. No asset is downloaded, photographed, traced, or derived from another game.",
"textureSize": 1024,
Expand All @@ -16,8 +16,15 @@
"hardpoint.glb",
"lamp_mast.glb",
"pipe_rack.glb",
"prop_ammo_box.glb",
"prop_barrel.glb",
"prop_barrel_stack.glb",
"prop_barrier.glb",
"prop_water_tank.glb",
"stair.glb",
"tank.glb",
"veh_armored_car.glb",
"veh_technical.glb",
"wall.glb"
],
"textures": [
Expand All @@ -43,7 +50,8 @@
"steel_albedo.webp",
"steel_normal.webp",
"steel_orm.webp",
"synty_atlas.webp"
"synty_atlas.webp",
"synty_vehicles.webp"
],
"audio": [
"ambience_yard.mp3",
Expand Down Expand Up @@ -78,9 +86,9 @@
"throngva/the-wolf-called-want.mp3"
],
"bytes": {
"models": 2724980,
"textures": 2328964,
"models": 3905884,
"textures": 2405506,
"audio": 323166,
"total": 5377110
"total": 6634556
}
}
Binary file added apps/game/public/assets/models/prop_ammo_box.glb
Binary file not shown.
Binary file added apps/game/public/assets/models/prop_barrel.glb
Binary file not shown.
Binary file not shown.
Binary file added apps/game/public/assets/models/prop_barrier.glb
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added apps/game/public/assets/models/veh_technical.glb
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 15 additions & 1 deletion apps/game/src/assets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,21 @@ describe("generated models", () => {
* apply. Their provenance is a licence recorded in PROVENANCE.md rather than
* a generator script.
*/
const LICENSED = new Set(["fighter_insurgent", "fighter_soldier"]);
const LICENSED = new Set([
"fighter_insurgent",
"fighter_soldier",
// Synty POLYGON Military static meshes: licensed, and bound by their own
// atlas slots (`synty_vehicles` / `synty_atlas`) rather than the generated
// material set. Their provenance is the Synty licence in PROVENANCE.md, and
// they are cosmetic set-dressing that never collides, so no COL_ proxy.
"veh_armored_car",
"veh_technical",
"prop_barrel",
"prop_barrel_stack",
"prop_ammo_box",
"prop_barrier",
"prop_water_tank",
]);

it("only uses material slots the engine can bind", () => {
// A slot the engine does not know about is not an error at load time: the
Expand Down
27 changes: 27 additions & 0 deletions apps/game/src/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ export const MODELS = [
// generated props follow — see apps/game/public/assets/PROVENANCE.md.
"fighter_insurgent",
"fighter_soldier",
// Licensed Synty POLYGON Military static meshes. Vehicles bind the shared
// `synty_vehicles` atlas; props reuse the character atlas (`synty_atlas`),
// since both were authored against Synty's Texture_01_A. No new texture ships
// for the props at all — see apps/game/public/assets/PROVENANCE.md.
"veh_armored_car",
"veh_technical",
"prop_barrel",
"prop_barrel_stack",
"prop_ammo_box",
"prop_barrier",
"prop_water_tank",
] as const;

export type ModelName = (typeof MODELS)[number];
Expand Down Expand Up @@ -148,6 +159,22 @@ export function createMaterials(scene: Scene): Map<string, PBRMaterial | Standar
synty.environmentIntensity = 0.35;
materials.set("synty_atlas", synty);

// Synty vehicles share one Land_Vehicles atlas (a desert recolour that fits
// the Kaviran setting), bound once here the same way the character atlas is.
// The pack's vehicle glass was authored against Texture_01_A, so those meshes
// are bound to `synty_atlas` at convert time rather than here.
const vehicles = new PBRMaterial("synty_vehicles", scene);
vehicles.albedoTexture = loadTexture(scene, "synty_vehicles.webp", true);
// Painted sheet metal: matte, barely metallic. The camo reads as diffuse.
vehicles.metallic = 0.05;
vehicles.roughness = 0.8;
// Same hot-yard correction as the character atlas: the unscaled tan would
// clip past the 0.62 bloom threshold under hemispheric 4.05 / exposure 2.05.
vehicles.albedoColor = new Color3(0.36, 0.36, 0.36);
vehicles.environmentIntensity = 0.35;
vehicles.maxSimultaneousLights = 6;
materials.set("synty_vehicles", vehicles);

// Lamp lenses are the one unlit surface: they are a light source, and
// shading them would make the fitting darker than the pool of light it casts.
const lens = new StandardMaterial("lamp_glass", scene);
Expand Down
53 changes: 53 additions & 0 deletions apps/game/src/world.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,59 @@ export async function buildWorld(
lamp.falloffType = PointLight.FALLOFF_PHYSICAL;
});

// ------------------------------------------------- cosmetic set-dressing
//
// Licensed Synty vehicles and props, staged in the two spawn zones and the
// back corners against the perimeter — the band beyond the outermost
// collision volumes (|x| > 32, |z| > 44) and inside the walls.
//
// These are the one deliberate exception to rule 1 at the top of this file:
// they are NOT generated from collision data and carry no collider, so a
// player can walk through them. That is why they are confined to the
// protected spawn ends and never to the three combat lanes — nothing here can
// become fake cover in a firefight. Promoting any of them to real cover is a
// deliberate change to the server map (`packages/multiplayer-sim/src/map.ts`)
// and its checksum, not something to smuggle in as art. One vehicle sits at
// each end so the two factions read as having staged the yard from opposite
// sides.
put("veh_armored_car", "veh_car", [{ position: new Vector3(-31, 0, -52), rotationY: 0.6 }]);
put("veh_technical", "veh_tech", [{ position: new Vector3(31, 0, 52), rotationY: -2.4 }]);

// Supply depots flanking each vehicle, plus a checkpoint line of T-wall
// barriers along each back wall behind the spawns.
put("prop_barrel_stack", "barrels_n", [
{ position: new Vector3(-34.5, 0, -49.5), rotationY: 0.3 },
]);
put("prop_barrel_stack", "barrels_s", [
{ position: new Vector3(34.5, 0, 49.5), rotationY: -2.9 },
]);
put("prop_barrel", "barrel", [
{ position: new Vector3(-30, 0, -54), rotationY: 1.1 },
{ position: new Vector3(-28.5, 0, -52.5), rotationY: -0.4 },
{ position: new Vector3(29, 0, 54), rotationY: 2.2 },
{ position: new Vector3(27.5, 0, 52.5), rotationY: 0.7 },
]);
put("prop_ammo_box", "ammo", [
{ position: new Vector3(-33, 0, -53), rotationY: 0.5 },
{ position: new Vector3(-35, 0, -46), rotationY: -0.8 },
{ position: new Vector3(33, 0, 53), rotationY: -2.4 },
{ position: new Vector3(35, 0, 46), rotationY: 2.5 },
]);
put("prop_water_tank", "watertank", [
{ position: new Vector3(35, 0, -52), rotationY: -2.2 },
{ position: new Vector3(-35, 0, 52), rotationY: 0.9 },
]);
put("prop_barrier", "barrier", [
{ position: new Vector3(-6, 0, -56), rotationY: 0 },
{ position: new Vector3(-2, 0, -56), rotationY: 0 },
{ position: new Vector3(2, 0, -56), rotationY: 0 },
{ position: new Vector3(6, 0, -56), rotationY: 0 },
{ position: new Vector3(-6, 0, 56), rotationY: Math.PI },
{ position: new Vector3(-2, 0, 56), rotationY: Math.PI },
{ position: new Vector3(2, 0, 56), rotationY: Math.PI },
{ position: new Vector3(6, 0, 56), rotationY: Math.PI },
]);

// Two cold accent lights mark the opposing spawn ends, echoing the split
// palette the whole product is built on.
const southMark = new PointLight("mark-south", new Vector3(0, 5, 50), scene);
Expand Down
50 changes: 35 additions & 15 deletions docs/HANDOFF-synty.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ oversight.
The Synty characters are committed, load fine, are textured, and are **not
shipped**, because their animation retarget is broken.

| Thing | State |
| ----------------------------------------------------- | ------------------------------------------- |
| `models/fighter_insurgent.glb`, `fighter_soldier.glb` | committed, load, textured, animation wrong |
| `textures/synty_atlas.webp` | working, 357 KB, shared by both |
| `tools/art/blender/import_synty.py` | converter; maths fixed, export broken |
| Vehicles, weapons, props | **none integrated** — 2 of 1,954 files used |
| Shell budget | 5.13 MB against a 9 MB guard |
| Thing | State |
| ----------------------------------------------------- | ----------------------------------------------- |
| `models/fighter_insurgent.glb`, `fighter_soldier.glb` | committed, load, textured, animation wrong |
| `textures/synty_atlas.webp` | working, 357 KB, shared by characters + props |
| `tools/art/blender/import_synty.py` | character converter; maths fixed, export broken |
| `tools/art/blender/import_synty_prop.py` | static-mesh converter; **working, shipped** |
| 2 vehicles + 5 props (`veh_*`, `prop_*`) | **integrated** — converted, textured, placed |
| `textures/synty_vehicles.webp` | working, 76 KB, shared by all vehicles |
| Weapons | **none integrated** — still the next win |
| Shell budget | 6.33 MB against a 9 MB guard |

Source pack is at `~/src/nightcell7-assets/SourceFiles/` (406 MB, outside the
repo, not committed).
Expand Down Expand Up @@ -78,15 +81,29 @@ single animation named `target_rig`** — after the object, not the actions.
names change from lowercase (`walk`/`run`/`idle`/`death`) to Synty's
(`Walk`/`Run`/`Idle_Gun`/`Death`).

4. **Then vehicles and props.** `SM_Veh_*` and `SM_Prop_*` are **static
meshes** — no retargeting, so far easier than the characters and a bigger
visual win. Good candidates: `SM_Veh_Pickup_Technical_01`,
`SM_Veh_Truck_01_Tanker`, `SM_Veh_Light_Armored_Car_01`,
`SM_Veh_Helicopter_Attack_02`. Textures come from
`PolygonMilitary_Land_Vehicles_*` and `Veh_Heli_*`.
4. ~~**Then vehicles and props.**~~ **Done.** `import_synty_prop.py` converts
the static `SM_Veh_*` / `SM_Prop_*` meshes: no retarget, just unit apply,
recentre-to-ground, per-mesh material naming, weld and (for vehicles)
decimate. Shipped: `veh_armored_car`, `veh_technical` and five props
(`prop_barrel`, `prop_barrel_stack`, `prop_ammo_box`, `prop_barrier`,
`prop_water_tank`). Two lessons for weapons next:
- **The FBX is already metres and uses one placeholder material.** No 0.01
scale (that was the _skeletal_ path); split body vs glass by mesh name.
- **Land_Vehicles_NN are one UV layout in ten recolours,** so one variant
(`03`, desert) is bound to every vehicle as `synty_vehicles`. Props reuse
`synty_atlas` (Texture_01_A) and cost no new texture at all.
- **Vehicles decimate hard without visible loss** (car 24k→~10k tris,
1.5 MB→0.5 MB). Verified by preview render, not by eye.

Placement is cosmetic set-dressing in the spawn zones and back corners
(`world.ts`, "cosmetic set-dressing" block) — deliberately non-colliding and
kept out of the three lanes, so rule 1 still holds for the play space.
Promoting any to real cover is a `map.ts` collision + checksum change.

5. **Then weapons.** `SM_Wep_*` with the `PolygonMilitary_Weapons_*` atlases,
replacing the generated carbine.
replacing the generated carbine. `import_synty_prop.py` is the right starting
point, but a weapon needs a `SOCKET_MUZZLE` node and is exempt from the
`COL_` rule — see how `assets.test.ts` handles the carbine.

## Traps already hit — do not repeat

Expand All @@ -103,7 +120,10 @@ single animation named `target_rig`** — after the object, not the actions.
- **Blender appends `.001`** if an action name is taken, and the engine looks
clips up by exact name. Rename the source out of the way first, then delete
the renamed sources before export or they ship alongside and double the file.
- **Synty FBX is centimetres**; scale by 0.01 on import.
- **The _skeletal_ (`SK_Chr_*`) FBX imports at centimetres**; scale by 0.01.
The **static** (`SM_*`) FBX does not — Blender reads its unit metadata and
lands it at metres already (`import_synty_prop.py` only _applies_ the
importer's transform, and range-checks the result). Do not blanket-scale.
- **Budget accounting counts flat `.mp3` files in the audio root only.** Music
lives in `audio/music/<artist>/` and is streamed, deliberately outside the
shell budget.
Expand Down
Loading
Loading