fix(repair): drill explosion to natural group + apply mission rotation
- ExplodedModel.createParts now descends recursively through single mesh-bearing wrapper nodes (e.g. Scene > Moto > Eclatement) until reaching a node with multiple mesh-bearing children. Previously the first wrapper was used as root, so models with extra Empty/group parents fell back to flat leaf meshes lerping in local space. - Add optional modelRotation field on RepairMissionConfig so fragmented + repairing models can match the world-space rotation of the source inspection model (parked Ebike). - Ebike mission now uses EBIKE_WORLD_ROTATION_Y/EBIKE_WORLD_SCALE directly so the fragmented bike lines up with the parked bike.
This commit is contained in:
@@ -143,6 +143,7 @@ export function RepairGame({
|
|||||||
{step === "fragmented" ? (
|
{step === "fragmented" ? (
|
||||||
<ExplodableModel
|
<ExplodableModel
|
||||||
modelPath={config.modelPath}
|
modelPath={config.modelPath}
|
||||||
|
rotation={config.modelRotation ?? [0, 0, 0]}
|
||||||
scale={config.modelScale ?? 1}
|
scale={config.modelScale ?? 1}
|
||||||
split
|
split
|
||||||
/>
|
/>
|
||||||
@@ -160,6 +161,7 @@ export function RepairGame({
|
|||||||
<>
|
<>
|
||||||
<ExplodableModel
|
<ExplodableModel
|
||||||
modelPath={config.modelPath}
|
modelPath={config.modelPath}
|
||||||
|
rotation={config.modelRotation ?? [0, 0, 0]}
|
||||||
scale={config.modelScale ?? 1}
|
scale={config.modelScale ?? 1}
|
||||||
split
|
split
|
||||||
hideNodeNames={brokenNodeNames}
|
hideNodeNames={brokenNodeNames}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import type {
|
|||||||
RepairMissionConfig,
|
RepairMissionConfig,
|
||||||
RepairMissionId,
|
RepairMissionId,
|
||||||
} from "@/types/gameplay/repairMission";
|
} from "@/types/gameplay/repairMission";
|
||||||
|
import {
|
||||||
|
EBIKE_WORLD_ROTATION_Y,
|
||||||
|
EBIKE_WORLD_SCALE,
|
||||||
|
} from "@/data/ebike/ebikeConfig";
|
||||||
|
|
||||||
const REPAIR_INTERACT_UI_PATH = "/assets/world/UI/interagir.webm";
|
const REPAIR_INTERACT_UI_PATH = "/assets/world/UI/interagir.webm";
|
||||||
const REPAIR_BROKEN_UI_PATH = "/assets/world/UI/cassé.webm";
|
const REPAIR_BROKEN_UI_PATH = "/assets/world/UI/cassé.webm";
|
||||||
@@ -20,7 +24,8 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
|
|||||||
description:
|
description:
|
||||||
"Repair the damaged cooling module before relaunching the bike",
|
"Repair the damaged cooling module before relaunching the bike",
|
||||||
modelPath: "/models/ebike/model.gltf",
|
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",
|
stageUiPath: "/assets/world/UI/ebike-mission-notification.webm",
|
||||||
interactUiPath: REPAIR_INTERACT_UI_PATH,
|
interactUiPath: REPAIR_INTERACT_UI_PATH,
|
||||||
brokenUiPath: REPAIR_BROKEN_UI_PATH,
|
brokenUiPath: REPAIR_BROKEN_UI_PATH,
|
||||||
|
|||||||
@@ -64,6 +64,13 @@ export interface RepairMissionConfig {
|
|||||||
description: string;
|
description: string;
|
||||||
modelPath: string;
|
modelPath: string;
|
||||||
modelScale?: ModelTransformProps["scale"];
|
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;
|
stageUiPath: string;
|
||||||
interactUiPath: string;
|
interactUiPath: string;
|
||||||
brokenUiPath: string;
|
brokenUiPath: string;
|
||||||
|
|||||||
@@ -53,13 +53,23 @@ export class ExplodedModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createParts(model: THREE.Object3D): ExplodedPart[] {
|
private createParts(model: THREE.Object3D): ExplodedPart[] {
|
||||||
const root =
|
// Drill down through single-mesh-bearing branches until we find a node
|
||||||
model.children.length === 1 && model.children[0]
|
// with multiple mesh-bearing children (the natural "explosion group" the
|
||||||
? model.children[0]
|
// modeler authored). Falls back to flat mesh list only if no such group
|
||||||
: model;
|
// exists. This avoids exploding leaves in local space when wrapper nodes
|
||||||
const directChildren = root.children.filter((child) => hasMesh(child));
|
// (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 =
|
const sourceObjects =
|
||||||
directChildren.length > 1 ? directChildren : getMeshes(root);
|
directChildren.length > 1 ? directChildren : getMeshes(current);
|
||||||
|
|
||||||
if (sourceObjects.length === 0) return [];
|
if (sourceObjects.length === 0) return [];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user