update: feedback repair model and improve repair case interaction feedback
This commit is contained in:
@@ -71,6 +71,7 @@ export function RepairGame({
|
|||||||
|
|
||||||
useRepairFragmentationInput({
|
useRepairFragmentationInput({
|
||||||
enabled: mainState === mission && readyForFragmentation,
|
enabled: mainState === mission && readyForFragmentation,
|
||||||
|
keyboardEnabled: false,
|
||||||
onFragment: () => setMissionStep(mission, "fragmented"),
|
onFragment: () => setMissionStep(mission, "fragmented"),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -143,6 +144,11 @@ export function RepairGame({
|
|||||||
open={step === "repairing"}
|
open={step === "repairing"}
|
||||||
zoomed={step === "repairing"}
|
zoomed={step === "repairing"}
|
||||||
showFragmentationPrompt={readyForFragmentation}
|
showFragmentationPrompt={readyForFragmentation}
|
||||||
|
onInteract={
|
||||||
|
readyForFragmentation
|
||||||
|
? () => setMissionStep(mission, "fragmented")
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import {
|
|||||||
type RepairCasePlaceholder,
|
type RepairCasePlaceholder,
|
||||||
} from "@/components/three/gameplay/RepairCaseModel";
|
} from "@/components/three/gameplay/RepairCaseModel";
|
||||||
import { RepairPromptVideo } from "@/components/three/gameplay/RepairPromptVideo";
|
import { RepairPromptVideo } from "@/components/three/gameplay/RepairPromptVideo";
|
||||||
|
import { TriggerObject } from "@/components/three/interaction/TriggerObject";
|
||||||
import {
|
import {
|
||||||
REPAIR_CASE_FOCUS_POSITION,
|
REPAIR_CASE_FOCUS_POSITION,
|
||||||
REPAIR_CASE_FOCUS_SCALE,
|
REPAIR_CASE_FOCUS_SCALE,
|
||||||
REPAIR_CASE_MODEL_PATH,
|
REPAIR_CASE_MODEL_PATH,
|
||||||
} from "@/data/gameplay/repairCaseConfig";
|
} from "@/data/gameplay/repairCaseConfig";
|
||||||
import type { RepairMissionConfig } from "@/data/gameplay/repairMissions";
|
import type { RepairMissionConfig } from "@/data/gameplay/repairMissions";
|
||||||
|
import type { Vector3Tuple } from "@/types/three/three";
|
||||||
|
|
||||||
interface RepairMissionCaseProps {
|
interface RepairMissionCaseProps {
|
||||||
config: RepairMissionConfig;
|
config: RepairMissionConfig;
|
||||||
@@ -20,6 +22,7 @@ interface RepairMissionCaseProps {
|
|||||||
open?: boolean;
|
open?: boolean;
|
||||||
zoomed?: boolean;
|
zoomed?: boolean;
|
||||||
showFragmentationPrompt?: boolean;
|
showFragmentationPrompt?: boolean;
|
||||||
|
onInteract?: (() => void) | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RepairMissionCase({
|
export function RepairMissionCase({
|
||||||
@@ -30,14 +33,23 @@ export function RepairMissionCase({
|
|||||||
open = false,
|
open = false,
|
||||||
zoomed = false,
|
zoomed = false,
|
||||||
showFragmentationPrompt = false,
|
showFragmentationPrompt = false,
|
||||||
|
onInteract,
|
||||||
}: RepairMissionCaseProps): React.JSX.Element {
|
}: RepairMissionCaseProps): React.JSX.Element {
|
||||||
const casePosition = zoomed
|
const casePosition = zoomed
|
||||||
? REPAIR_CASE_FOCUS_POSITION
|
? REPAIR_CASE_FOCUS_POSITION
|
||||||
: config.case.position;
|
: config.case.position;
|
||||||
const caseScale = zoomed ? REPAIR_CASE_FOCUS_SCALE : config.case.scale;
|
const caseScale = zoomed ? REPAIR_CASE_FOCUS_SCALE : config.case.scale;
|
||||||
|
const modelPosition: Vector3Tuple = onInteract ? [0, 0, 0] : casePosition;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
|
{onInteract ? (
|
||||||
|
<TriggerObject
|
||||||
|
position={casePosition}
|
||||||
|
colliders="ball"
|
||||||
|
label={`Ouvrir ${config.label}`}
|
||||||
|
onTrigger={onInteract}
|
||||||
|
>
|
||||||
<RepairCaseModel
|
<RepairCaseModel
|
||||||
modelPath={REPAIR_CASE_MODEL_PATH}
|
modelPath={REPAIR_CASE_MODEL_PATH}
|
||||||
exiting={exiting}
|
exiting={exiting}
|
||||||
@@ -45,10 +57,24 @@ export function RepairMissionCase({
|
|||||||
onPlaceholdersChange={onPlaceholdersChange}
|
onPlaceholdersChange={onPlaceholdersChange}
|
||||||
open={open}
|
open={open}
|
||||||
floating={!zoomed}
|
floating={!zoomed}
|
||||||
position={casePosition}
|
position={modelPosition}
|
||||||
rotation={config.case.rotation}
|
rotation={config.case.rotation}
|
||||||
scale={caseScale}
|
scale={caseScale}
|
||||||
/>
|
/>
|
||||||
|
</TriggerObject>
|
||||||
|
) : (
|
||||||
|
<RepairCaseModel
|
||||||
|
modelPath={REPAIR_CASE_MODEL_PATH}
|
||||||
|
exiting={exiting}
|
||||||
|
onExitComplete={onExitComplete}
|
||||||
|
onPlaceholdersChange={onPlaceholdersChange}
|
||||||
|
open={open}
|
||||||
|
floating={!zoomed}
|
||||||
|
position={modelPosition}
|
||||||
|
rotation={config.case.rotation}
|
||||||
|
scale={caseScale}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{showFragmentationPrompt && !exiting ? (
|
{showFragmentationPrompt && !exiting ? (
|
||||||
<RepairPromptVideo
|
<RepairPromptVideo
|
||||||
src={config.interactUiPath}
|
src={config.interactUiPath}
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ const BROKEN_PART_START_OFFSETS: Vector3Tuple[] = [
|
|||||||
[1.35, 0.55, -0.85],
|
[1.35, 0.55, -0.85],
|
||||||
];
|
];
|
||||||
const REPAIR_INSTALL_RADIUS = 1.1;
|
const REPAIR_INSTALL_RADIUS = 1.1;
|
||||||
|
const VALID_PART_COLOR = "#22c55e";
|
||||||
|
const INVALID_PART_COLOR = "#ef4444";
|
||||||
|
const STORED_BROKEN_PART_COLOR = "#38bdf8";
|
||||||
|
|
||||||
interface RepairRepairingStepProps {
|
interface RepairRepairingStepProps {
|
||||||
brokenParts: readonly RepairScannedBrokenPart[];
|
brokenParts: readonly RepairScannedBrokenPart[];
|
||||||
@@ -50,6 +53,10 @@ interface RepairPlaceholderMarkersProps {
|
|||||||
positions: readonly Vector3Tuple[];
|
positions: readonly Vector3Tuple[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RepairPartPlacementFeedbackProps {
|
||||||
|
state: "valid" | "invalid" | "stored" | null;
|
||||||
|
}
|
||||||
|
|
||||||
export function RepairRepairingStep({
|
export function RepairRepairingStep({
|
||||||
brokenParts,
|
brokenParts,
|
||||||
config,
|
config,
|
||||||
@@ -167,6 +174,12 @@ export function RepairRepairingStep({
|
|||||||
const placeholderPosition =
|
const placeholderPosition =
|
||||||
placeholderPositions[index % placeholderPositions.length] ??
|
placeholderPositions[index % placeholderPositions.length] ??
|
||||||
placeholderPositions[0]!;
|
placeholderPositions[0]!;
|
||||||
|
const isPlaced = Boolean(placedPartIds[part.id]);
|
||||||
|
const feedbackState = getReplacementFeedbackState(
|
||||||
|
part.id,
|
||||||
|
config.requiredReplacementPartId,
|
||||||
|
isPlaced,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GrabbableObject
|
<GrabbableObject
|
||||||
@@ -185,11 +198,14 @@ export function RepairRepairingStep({
|
|||||||
snapRadius={REPAIR_CASE_PLACEHOLDER_SNAP_RADIUS}
|
snapRadius={REPAIR_CASE_PLACEHOLDER_SNAP_RADIUS}
|
||||||
snapTargets={placeholderPositions}
|
snapTargets={placeholderPositions}
|
||||||
>
|
>
|
||||||
|
<group>
|
||||||
<RepairObjectModel
|
<RepairObjectModel
|
||||||
label={part.label}
|
label={part.label}
|
||||||
modelPath={part.modelPath ?? config.modelPath}
|
modelPath={part.modelPath ?? config.modelPath}
|
||||||
scale={0.36}
|
scale={0.36}
|
||||||
/>
|
/>
|
||||||
|
<RepairPartPlacementFeedback state={feedbackState} />
|
||||||
|
</group>
|
||||||
</GrabbableObject>
|
</GrabbableObject>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -207,6 +223,7 @@ export function RepairRepairingStep({
|
|||||||
part,
|
part,
|
||||||
placeholderTargets,
|
placeholderTargets,
|
||||||
);
|
);
|
||||||
|
const isDeposited = Boolean(depositedBrokenPartIds[part.id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GrabbableObject
|
<GrabbableObject
|
||||||
@@ -235,6 +252,9 @@ export function RepairRepairingStep({
|
|||||||
<sphereGeometry args={[0.11, 16, 16]} />
|
<sphereGeometry args={[0.11, 16, 16]} />
|
||||||
<meshBasicMaterial color="#ef4444" transparent opacity={0.85} />
|
<meshBasicMaterial color="#ef4444" transparent opacity={0.85} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
<RepairPartPlacementFeedback
|
||||||
|
state={isDeposited ? "stored" : null}
|
||||||
|
/>
|
||||||
</group>
|
</group>
|
||||||
</GrabbableObject>
|
</GrabbableObject>
|
||||||
);
|
);
|
||||||
@@ -296,6 +316,46 @@ function RepairPlaceholderMarkers({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function RepairPartPlacementFeedback({
|
||||||
|
state,
|
||||||
|
}: RepairPartPlacementFeedbackProps): React.JSX.Element | null {
|
||||||
|
if (!state) return null;
|
||||||
|
|
||||||
|
const color = getPlacementFeedbackColor(state);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group position={[0, 0.72, 0]}>
|
||||||
|
<mesh rotation={[Math.PI / 2, 0, 0]}>
|
||||||
|
<torusGeometry args={[0.48, 0.035, 12, 64]} />
|
||||||
|
<meshBasicMaterial color={color} transparent opacity={0.85} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, 0.08, 0]}>
|
||||||
|
<sphereGeometry args={[0.1, 16, 16]} />
|
||||||
|
<meshBasicMaterial color={color} transparent opacity={0.9} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlacementFeedbackColor(
|
||||||
|
state: NonNullable<RepairPartPlacementFeedbackProps["state"]>,
|
||||||
|
): string {
|
||||||
|
if (state === "valid") return VALID_PART_COLOR;
|
||||||
|
if (state === "stored") return STORED_BROKEN_PART_COLOR;
|
||||||
|
|
||||||
|
return INVALID_PART_COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReplacementFeedbackState(
|
||||||
|
partId: string,
|
||||||
|
requiredPartId: string,
|
||||||
|
isPlaced: boolean,
|
||||||
|
): RepairPartPlacementFeedbackProps["state"] {
|
||||||
|
if (!isPlaced) return null;
|
||||||
|
|
||||||
|
return partId === requiredPartId ? "valid" : "invalid";
|
||||||
|
}
|
||||||
|
|
||||||
function getPlaceholderTargets(
|
function getPlaceholderTargets(
|
||||||
placeholders: readonly RepairCasePlaceholder[],
|
placeholders: readonly RepairCasePlaceholder[],
|
||||||
): readonly RepairCasePlaceholder[] {
|
): readonly RepairCasePlaceholder[] {
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import { useBothFistsHold } from "@/hooks/handTracking/useBothFistsHold";
|
|||||||
|
|
||||||
interface UseRepairFragmentationInputOptions {
|
interface UseRepairFragmentationInputOptions {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
keyboardEnabled?: boolean;
|
||||||
onFragment: () => void;
|
onFragment: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useRepairFragmentationInput({
|
export function useRepairFragmentationInput({
|
||||||
enabled,
|
enabled,
|
||||||
|
keyboardEnabled = true,
|
||||||
onFragment,
|
onFragment,
|
||||||
}: UseRepairFragmentationInputOptions): void {
|
}: UseRepairFragmentationInputOptions): void {
|
||||||
const completedRef = useRef(false);
|
const completedRef = useRef(false);
|
||||||
@@ -29,7 +31,7 @@ export function useRepairFragmentationInput({
|
|||||||
}, [enabled, onFragment]);
|
}, [enabled, onFragment]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled) return undefined;
|
if (!enabled || !keyboardEnabled) return undefined;
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||||
if (event.key.toLowerCase() !== INTERACT_KEY) return;
|
if (event.key.toLowerCase() !== INTERACT_KEY) return;
|
||||||
@@ -43,7 +45,7 @@ export function useRepairFragmentationInput({
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("keydown", handleKeyDown);
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [enabled, fragment]);
|
}, [enabled, fragment, keyboardEnabled]);
|
||||||
|
|
||||||
useBothFistsHold({
|
useBothFistsHold({
|
||||||
enabled,
|
enabled,
|
||||||
|
|||||||
Reference in New Issue
Block a user