diff --git a/src/components/three/gameplay/RepairGame.tsx b/src/components/three/gameplay/RepairGame.tsx
index c8a990f..615d1de 100644
--- a/src/components/three/gameplay/RepairGame.tsx
+++ b/src/components/three/gameplay/RepairGame.tsx
@@ -143,6 +143,7 @@ export function RepairGame({
{step === "fragmented" ? (
@@ -160,6 +161,7 @@ export function RepairGame({
<>
= {
description:
"Repair the damaged cooling module before relaunching the bike",
modelPath: "/models/ebike/model.gltf",
- modelScale: 0.3,
+ modelScale: EBIKE_WORLD_SCALE,
+ modelRotation: [0, EBIKE_WORLD_ROTATION_Y, 0],
stageUiPath: "/assets/world/UI/ebike-mission-notification.webm",
interactUiPath: REPAIR_INTERACT_UI_PATH,
brokenUiPath: REPAIR_BROKEN_UI_PATH,
diff --git a/src/types/gameplay/repairMission.ts b/src/types/gameplay/repairMission.ts
index 6a5e6d7..ef228a3 100644
--- a/src/types/gameplay/repairMission.ts
+++ b/src/types/gameplay/repairMission.ts
@@ -64,6 +64,13 @@ export interface RepairMissionConfig {
description: string;
modelPath: string;
modelScale?: ModelTransformProps["scale"];
+ /**
+ * World-space rotation applied to the model when mounted by RepairGame
+ * (fragmented + repairing steps). Should match the rotation used by the
+ * source object in the world (e.g. parked Ebike) so the fragmented model
+ * lines up visually with the inspection model.
+ */
+ modelRotation?: Vector3Tuple;
stageUiPath: string;
interactUiPath: string;
brokenUiPath: string;
diff --git a/src/utils/three/ExplodedModel.ts b/src/utils/three/ExplodedModel.ts
index e646ebd..d381a78 100644
--- a/src/utils/three/ExplodedModel.ts
+++ b/src/utils/three/ExplodedModel.ts
@@ -53,13 +53,23 @@ export class ExplodedModel {
}
private createParts(model: THREE.Object3D): ExplodedPart[] {
- const root =
- model.children.length === 1 && model.children[0]
- ? model.children[0]
- : model;
- const directChildren = root.children.filter((child) => hasMesh(child));
+ // Drill down through single-mesh-bearing branches until we find a node
+ // with multiple mesh-bearing children (the natural "explosion group" the
+ // modeler authored). Falls back to flat mesh list only if no such group
+ // exists. This avoids exploding leaves in local space when wrapper nodes
+ // (e.g. "Empty" + "Moto" > "Eclatement") sit above the actual group.
+ let current = model;
+ while (true) {
+ const meshChildren = current.children.filter((child) => hasMesh(child));
+ if (meshChildren.length === 1 && meshChildren[0]) {
+ current = meshChildren[0];
+ continue;
+ }
+ break;
+ }
+ const directChildren = current.children.filter((child) => hasMesh(child));
const sourceObjects =
- directChildren.length > 1 ? directChildren : getMeshes(root);
+ directChildren.length > 1 ? directChildren : getMeshes(current);
if (sourceObjects.length === 0) return [];