Skip to content

Production art v1: portable rigged character and PBR package pipeline - #121

Open
BrandDead wants to merge 2 commits into
main-tL2525from
feat/production-art-package-v1
Open

Production art v1: portable rigged character and PBR package pipeline#121
BrandDead wants to merge 2 commits into
main-tL2525from
feat/production-art-package-v1

Conversation

@BrandDead

@BrandDead BrandDead commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Summary

This PR begins the next production-art milestone after merged PRs #119 and #120 without making a paid purchase. It proves the portable character and block-material contracts with legally redistributable CC0 sources while preserving the shared combat, possession, camera, and result authority.

What changed

  • Adds one optimized 1.36 MB rigged Quaternius CC0 character GLB with a preserved creator license, strict package manifest, 65 named joints, and 12 selected gameplay clips.
  • Builds the runtime GLB deterministically from matching base and animation rigs, with explicit skeleton and required-clip validation.
  • Loads the GLB once through a Babylon AssetContainer, then instantiates independent skeletons and animation groups for every combatant.
  • Maps named bones to invisible physical head, torso, arm, and leg hit volumes and attaches the presentation weapon to hand_r.
  • Drives idle, movement, aim, fire, reload, hit, and downed presentation from existing combat events and snapshot state.
  • Adds CC0 ambientCG asphalt, concrete, and brick PBR color/normal/roughness maps for the 1208 block.
  • Extends package/schema validation and the global 20 MB asset audit so GLBs, PBR maps, manifests, and licenses cannot bypass CI accounting.
  • Adds a shipped-package unit test, reproducible source/checksum ledger, production package specification, visual review, and final browser evidence.
  • Makes the browser gauntlet branch-relative and requires the production package ID before accepting TPS/FPS/tactical evidence.

Verification

  • ESLint: 0 errors; 194 pre-existing warnings.
  • TypeScript: passed.
  • Vitest: 708/708 tests across 48 files.
  • Package validation: 5 packages against 4 schemas.
  • Runtime assets: 110 manifest assets plus 13 package files; 7.38 MB / 20 MB; 0 errors; 0 warnings.
  • Production build: passed in 23.22 seconds after stopping temporary browser/acquisition processes.
  • Backend offline pytest: 42/42.
  • Live WebGL2: production package active, TPS, FPS, tactical commander, possession transfer Lil Dre → Kilo, physical fire, retreat result, one result application, and return to strategy; 0 fatal local runtime exceptions.

Visual boundary

The free standard character is intentionally a pipeline stand-in, not final member art. Its superhero costume does not match the streetwear vision. Two procedural bone-attached clothing experiments were tested and removed because they degraded the live TPS silhouette. This PR therefore proves that a commissioned or licensed clothed hero/rival can replace the GLB without rewriting combat, cameras, possession, hit resolution, or persistence; it does not claim target-image fidelity.

The next asset decision is defined in docs/PRODUCTION_ART_PACKAGE_V1.md: approve one authored clothed hero and one rival only after they pass the same strict package, TPS/FPS/tactical, and physical-hit gates.

Evidence

See:

  • docs/PRODUCTION_ART_PACKAGE_V1.md
  • docs/PRODUCTION_ART_SOURCES.md
  • docs/PRODUCTION_ART_V1_VERIFICATION.md
  • docs/PRODUCTION_ART_VISUAL_REVIEW.md
  • docs/evidence/production-art-v1/

Note

Medium Risk
Touches 3D combat presentation, physical hit proxies, and asset loading with a fallback path; scope is large but gated by tests, schema validation, and unchanged gameplay contracts.

Overview
Introduces production art package v1: a strict, schema-validated character pipeline and CC0 PBR street materials for the 1208 Las Olas Modern Ops client, without changing combat, possession, or result authority.

Character runtime loads character.universal-male.pipeline-v1 before the scene is ready—a Quaternius CC0 base plus 12 gameplay clips merged into one ~1.36 MB GLB via build-character-package.mjs. The loader parses the GLB once into a Babylon AssetContainer, clones rigs per combatant, attaches bone-local hit proxies and a hand_r weapon, and falls back to the articulated procedural actor if validation fails. Environment swaps flat street colors for ambientCG asphalt/concrete/brick WebP maps with a pbr-sources.json provenance lock.

Tooling and gates add @gltf-transform dev deps, PBR prep script, package files in the global 20 MB asset audit, and gauntlet checks that require the production package ID. Docs (ASSETS.md, package spec, sources, verification, visual review) record CC0 acquisition, explicit “pipeline stand-in not final art,” and acceptance criteria for commissioned heroes.

Lifecycle fixes from review: transient animation end callbacks only restart idle if the group is still active; downed, tactical, and first-person camera sync now toggles hit proxy pickability with visible meshes so rays cannot hit hidden bodies.

Reviewed by Cursor Bugbot for commit 20a610a. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Aug 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
slide Ready Ready Preview Aug 29, 2026 3:16am

Request Review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-29T03:15:19.097545Z 285ce57 PR opened
🔒 Security Review Completed 2026-08-29T03:07:04.703092Z 285ce57 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Interrupted clips leave idle running
    • Added a check to ensure the animation group is still active before starting idle, preventing nested idle animations when transient clips are interrupted.
  • ✅ Fixed: Downed hit proxies stay pickable
    • Added pickability toggles for hitMeshes in both syncSnapshot and updateCamera to ensure downed actors' collision proxies become unpickable.

Create PR

Or push these changes by commenting:

@cursor push 256ef8a86b
Preview (256ef8a86b)
diff --git a/frontend/src/game/ops/OpsPackagedAssetLoader.ts b/frontend/src/game/ops/OpsPackagedAssetLoader.ts
--- a/frontend/src/game/ops/OpsPackagedAssetLoader.ts
+++ b/frontend/src/game/ops/OpsPackagedAssetLoader.ts
@@ -233,9 +233,11 @@
         nextGroup.start(loop);
         if (transient) {
           nextGroup.onAnimationGroupEndObservable.addOnce(() => {
-            transient = false;
-            activeState = undefined;
-            playAnimation('idle');
+            if (activeGroup === nextGroup) {
+              transient = false;
+              activeState = undefined;
+              playAnimation('idle');
+            }
           });
         }
       };

diff --git a/frontend/src/game/ops/OpsWorld.ts b/frontend/src/game/ops/OpsWorld.ts
--- a/frontend/src/game/ops/OpsWorld.ts
+++ b/frontend/src/game/ops/OpsWorld.ts
@@ -525,6 +525,9 @@
         mesh.visibility = actor.isDown ? 0.42 : 1;
         mesh.isPickable = !actor.isDown;
       });
+      visual.hitMeshes?.forEach((mesh) => {
+        mesh.isPickable = !actor.isDown;
+      });
     });
   }
 
@@ -780,6 +783,9 @@
         mesh.visibility = selected.isDown ? 0.42 : 1;
         mesh.isPickable = !selected.isDown;
       });
+      actor.hitMeshes?.forEach((mesh) => {
+        mesh.isPickable = !selected.isDown;
+      });
       return;
     }
 
@@ -790,6 +796,9 @@
         mesh.visibility = 0;
         mesh.isPickable = false;
       });
+      actor.hitMeshes?.forEach((mesh) => {
+        mesh.isPickable = false;
+      });
       return;
     }
 
@@ -803,6 +812,9 @@
       mesh.visibility = selected.isDown ? 0.42 : 1;
       mesh.isPickable = !selected.isDown;
     });
+    actor.hitMeshes?.forEach((mesh) => {
+      mesh.isPickable = !selected.isDown;
+    });
   }
 
   private updateEffects(delta: number): void {

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 285ce57. Configure here.

Comment thread frontend/src/game/ops/OpsPackagedAssetLoader.ts
Comment thread frontend/src/game/ops/OpsWorld.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 285ce57619

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 524 to 526
visual.meshes.forEach((mesh) => {
mesh.visibility = actor.isDown ? 0.42 : 1;
mesh.isPickable = !actor.isDown;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Disable packaged hit proxies when actors go down

When a production-GLB actor is downed, this updates only visual.meshes; the separate visual.hitMeshes created by createHitProxies() remain pickable. In FPS/TPS, rays therefore continue selecting the invisible hit boxes around a corpse, and validateAimFireRay() rejects the downed actor candidate instead of firing at a living target behind it. This differs from the articulated fallback, whose hit geometry is disabled here, so the packaged path should also disable its hit proxies.

Useful? React with 👍 / 👎.

Comment on lines +105 to +107
return HIT_PROXY_SPECS.flatMap((spec) => {
const bone = findBone(entries, packageDefinition.hitZones[spec.packageKey]);
if (!bone) return [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject packages without every physical hit bone

For any schema-valid replacement package with a misspelled hit-zone bone—or no skinned mesh—this silently omits some or all hit proxies while still accepting the template and marking the package active. Because the visible GLB meshes are deliberately non-pickable, a package missing all proxies makes every actor impossible to shoot in FPS/TPS. Validate the skinned mesh and every declared hit-zone bone while loading the template so malformed packages trigger the articulated fallback instead.

Useful? React with 👍 / 👎.

];

const io = new NodeIO();
const targetDocument = await io.read(baseModelPath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize the official texture URIs before reading the model

With the exact Quaternius archive this pipeline is documented to consume, docs/PRODUCTION_ART_SOURCES.md records that the source glTF requests T_Hair_1_Normal_png.png and T_Eye_Normal_png.png, while the archive contains differently named files. The script calls io.read(baseModelPath) before performing any URI repair or creating aliases, so its advertised command cannot rebuild the asset directly from the official sources without an undocumented manual pre-step. Automate that normalization before reading the model.

Useful? React with 👍 / 👎.

// budget. Excluding them let ~31 MB of raw PNG land on main while
// the gate still reported 5.35/20 MB.
const isPackageFile = p.startsWith(PACKAGE_DIR + path.sep);
if (!isPackageFile) warn('W_ORPHAN', `Runtime file not in manifest: ${path.relative(FRONTEND, p)}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run package validation in the checked CI workflow

In the checked .github/workflows/ci.yml frontend job, only npm run assets:audit and the Vite build run; npm run assets:packages is never called. This new branch suppresses orphan warnings for everything under public/assets/packages on the assumption that validate-packages.mjs handles it, while Vite merely copies public files, so a malformed package manifest or missing referenced GLB can pass CI and deploy with the runtime silently falling back. Add the package validator to that workflow rather than relying on the unused aggregate validate script.

Useful? React with 👍 / 👎.

Comment on lines +17 to +19
const albedo = new Texture(`${PBR_ROOT}/${surfaceId}-color.webp`, scene, true, false);
const normal = new Texture(`${PBR_ROOT}/${surfaceId}-normal.webp`, scene, true, false);
const roughness = new Texture(`${PBR_ROOT}/${surfaceId}-roughness.webp`, scene, true, false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enable mipmaps for the tiled PBR surfaces

The third Texture constructor argument is noMipmap, so passing true disables mip generation for every new PBR map. These textures are tiled up to 10× and are heavily minified in the tactical and oblique cameras; without mipmaps, the albedo, roughness, and normal detail will alias and shimmer during camera or actor movement. Pass false or omit noMipmap so the production street materials use mipmapped sampling.

Useful? React with 👍 / 👎.

Comment on lines +37 to 38
await world.initializeProductionAssets();
await scene.whenReadyAsync();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound or cancel the blocking production-asset load

The render loop and onReady callback are not started until this awaited package load and the subsequent scene-ready wait complete, but neither operation has a timeout or an abort signal. If the package JSON, 1.36 MB GLB, or one of its embedded resources stalls rather than rejecting, Modern Ops remains on its loading state indefinitely and the articulated fallback is never displayed. Apply a bounded abort signal or start rendering the fallback while production assets load asynchronously.

Useful? React with 👍 / 👎.

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