Merge branch 'develop' into feat/polish-mission-2
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled

This commit is contained in:
math-pixel
2026-06-03 01:52:20 +02:00
31 changed files with 661 additions and 857 deletions
+5 -1
View File
@@ -11,6 +11,7 @@ import {
isMapModelVisible,
useMapPerformanceStore,
} from "@/managers/stores/useMapPerformanceStore";
import { useRepairFocusStore } from "@/managers/stores/useRepairFocusStore";
import { SkyModel } from "@/components/three/world/SkyModel";
import { CloudSystem } from "@/world/clouds/CloudSystem";
import { FogSystem } from "@/world/fog/FogSystem";
@@ -24,6 +25,9 @@ export function Environment(): React.JSX.Element {
const groups = useMapPerformanceStore((state) => state.groups);
const models = useMapPerformanceStore((state) => state.models);
const showSky = isMapModelVisible("sky", { groups, models });
// Hide vegetation while the repair focus bubble is active so the cocoon
// shroud is not pierced by tall trees / bushes around the repair model.
const repairFocusActive = useRepairFocusStore((state) => state.active);
if (sceneMode === "physics") {
return (
@@ -52,7 +56,7 @@ export function Environment(): React.JSX.Element {
<WaterSystem />
<CloudSystem />
<GrassSystem />
<VegetationSystem />
{repairFocusActive ? null : <VegetationSystem />}
</>
);
}
+9 -2
View File
@@ -250,7 +250,10 @@ export function animateCameraTransformTransition(
targetRotation: Vector3Tuple,
duration: number = 1,
onComplete?: () => void,
options: { lockInput?: boolean } = {},
): void {
const { lockInput = true } = options;
if (!globalCamera) {
logger.warn("GameCinematics", "Camera not found for transition");
onComplete?.();
@@ -260,7 +263,9 @@ export function animateCameraTransformTransition(
const camera = globalCamera;
cameraTransitionTimeline?.kill();
useGameStore.getState().setCinematicPlaying(true);
if (lockInput) {
useGameStore.getState().setCinematicPlaying(true);
}
// Convert target rotation in degrees to quaternion
const targetEuler = new THREE.Euler(
@@ -282,7 +287,9 @@ export function animateCameraTransformTransition(
},
onComplete: () => {
cameraTransitionTimeline = null;
useGameStore.getState().setCinematicPlaying(false);
if (lockInput) {
useGameStore.getState().setCinematicPlaying(false);
}
onComplete?.();
},
});
+6 -8
View File
@@ -1,5 +1,6 @@
import { Ebike } from "@/components/ebike/Ebike";
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
import { RepairFocusBubble } from "@/components/three/gameplay/RepairFocusBubble";
import { RepairGame } from "@/components/three/gameplay/RepairGame";
import { FarmNarrativeFlow } from "@/components/gameplay/farm/FarmNarrativeFlow";
import { PylonDownedPylon } from "@/components/gameplay/pylon/PylonDownedPylon";
@@ -17,6 +18,7 @@ import {
OUTRO_STAGE_ANCHOR,
} from "@/data/gameplay/gameStageAnchors";
import { useGameStore } from "@/managers/stores/useGameStore";
import { useRepairFocusStore } from "@/managers/stores/useRepairFocusStore";
import { useRepairMissionAnchorStore } from "@/managers/stores/useRepairMissionAnchorStore";
import {
isFarmNarrativeStep,
@@ -25,13 +27,7 @@ import {
import type { RepairMissionTriggerConfig } from "@/types/gameplay/repairMission";
import type { Vector3Tuple } from "@/types/three/three";
import { getRepairMissionPosition } from "@/utils/gameplay/repairMissionPosition";
import {
EBIKE_WORLD_POSITION,
EBIKE_WORLD_ROTATION_Y,
EBIKE_WORLD_SCALE,
} from "@/data/ebike/ebikeConfig";
const EBIKE_CONFIG_KEY = `${EBIKE_WORLD_POSITION.join(",")}:${EBIKE_WORLD_ROTATION_Y}:${EBIKE_WORLD_SCALE}`;
import { EBIKE_WORLD_POSITION } from "@/data/ebike/ebikeConfig";
interface StageAnchorProps {
color: string;
@@ -96,6 +92,7 @@ export function GameStageContent(): React.JSX.Element {
const mainState = useGameStore((state) => state.mainState);
const pylonStep = useGameStore((state) => state.pylon.currentStep);
const anchors = useRepairMissionAnchorStore((state) => state.anchors);
const repairFocusActive = useRepairFocusStore((state) => state.active);
const farmStep = useGameStore((state) => state.farm.currentStep);
@@ -110,7 +107,7 @@ export function GameStageContent(): React.JSX.Element {
<Ebike position={EBIKE_WORLD_POSITION} />
<PylonLightingEffect />
<PylonDownedPylon />
{isDebugEnabled() ? (
{isDebugEnabled() && !repairFocusActive ? (
<>
<ZoneDebugVisual zone={PYLON_APPROACH_ZONE} active={false} />
<ZoneDebugVisual zone={PYLON_ARRIVED_ZONE} active={false} />
@@ -131,6 +128,7 @@ export function GameStageContent(): React.JSX.Element {
<RepairMissionTrigger key={config.mission} config={config} />
))}
{mainState === "outro" ? <StageAnchor {...OUTRO_STAGE_ANCHOR} /> : null}
<RepairFocusBubble />
</>
);
}
+9 -3
View File
@@ -6,6 +6,7 @@ import {
} from "@/data/player/playerConfig";
import { LA_FABRIK_INITIAL_LOOK_AT } from "@/data/world/laFabrikConfig";
import { useCameraMode } from "@/hooks/debug/useCameraMode";
import { useDebugStore } from "@/hooks/debug/useDebugStore";
import { useEnvironmentDebug } from "@/hooks/debug/useEnvironmentDebug";
import { useMapPerformanceDebug } from "@/hooks/debug/useMapPerformanceDebug";
import { useCharacterDebug } from "@/hooks/debug/useCharacterDebug";
@@ -32,7 +33,6 @@ import { CharacterSystem } from "@/world/characters/CharacterSystem";
import { Player } from "@/world/player/Player";
import { TestMap } from "@/world/debug/TestMap";
import type { SceneLoadingChangeHandler } from "@/types/world/sceneLoading";
import type { HandTrackingGloveHandedness } from "@/hooks/handTracking/useHandTrackingGloveStatus";
import type { HandTrackingHand } from "@/types/handTracking/handTracking";
interface WorldProps {
@@ -41,7 +41,7 @@ interface WorldProps {
function hasTrackedHand(
hands: HandTrackingHand[],
handedness: HandTrackingGloveHandedness,
handedness: "left" | "right",
): boolean {
return hands.some((hand) => hand.handedness.toLowerCase() === handedness);
}
@@ -60,6 +60,9 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
(state) => state.showPlayerModel,
);
const showDebugOctree = useDebugVisualsStore((state) => state.showOctree);
const showHandTrackingModel = useDebugStore((debug) =>
debug.getShowHandTrackingModel(),
);
const { hands, status, usageStatus } = useHandTrackingSnapshot();
const {
octree,
@@ -74,7 +77,10 @@ export function World({ onLoadingStateChange }: WorldProps): React.JSX.Element {
? PLAYER_SPAWN_POSITION_GAME
: PLAYER_SPAWN_POSITION_PHYSICS;
const showHandTrackingGloves =
status === "connected" && usageStatus !== "inactive" && hands.length > 0;
showHandTrackingModel &&
status === "connected" &&
usageStatus !== "inactive" &&
hands.length > 0;
const showLeftHandTrackingGlove =
showHandTrackingGloves && hasTrackedHand(hands, "left");
const showRightHandTrackingGlove =
+7
View File
@@ -3,7 +3,9 @@ import { Component, useRef, useState, useEffect } from "react";
import * as THREE from "three";
import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier";
import { Line } from "@react-three/drei";
import { Ebike } from "@/components/ebike/Ebike";
import { RepairGame } from "@/components/three/gameplay/RepairGame";
import { RepairFocusBubble } from "@/components/three/gameplay/RepairFocusBubble";
import { GrabbableObject } from "@/components/three/interaction/GrabbableObject";
import { AnimatedModel } from "@/components/three/models/AnimatedModel";
import { TriggerObject } from "@/components/three/interaction/TriggerObject";
@@ -239,11 +241,16 @@ export function TestMap({ onOctreeReady }: TestMapProps): React.JSX.Element {
<group position={zone.position}>
<RepairPlaygroundZoneMarker color={zone.color} />
</group>
{zone.mission === "ebike" ? (
<Ebike position={zone.position} snapToTerrain={false} />
) : null}
<RepairGame mission={zone.mission} position={zone.position} />
</group>
))}
</Physics>
<RepairFocusBubble />
{/* Dynamic Futuristic 3D GPS Dashboard Preview */}
<group
position={TEST_SCENE_GPS_PREVIEW_POSITION}
+1 -24
View File
@@ -23,7 +23,6 @@ import {
PLAYER_MAX_DELTA,
PLAYER_XZ_DAMPING_FACTOR,
} from "@/data/player/playerConfig";
import { useRepairMovementLocked } from "@/hooks/gameplay/useRepairMovementLocked";
import { useTerrainHeightSampler } from "@/hooks/three/useTerrainHeight";
import { InteractionManager } from "@/managers/InteractionManager";
import { useGameStore } from "@/managers/stores/useGameStore";
@@ -154,9 +153,7 @@ export function PlayerController({
}: PlayerControllerProps): null {
const camera = useThree((state) => state.camera);
const sceneMode = useSceneMode();
const movementLocked = useRepairMovementLocked();
const terrainHeight = useTerrainHeightSampler();
const movementLockedRef = useRef(movementLocked);
const keys = useRef<Keys>({ ...DEFAULT_KEYS });
const velocity = useRef(new THREE.Vector3());
const fallDuration = useRef(0);
@@ -249,17 +246,6 @@ export function PlayerController({
initializedRef.current = true;
}, [camera, initialLookAt, spawnPosition]);
useEffect(() => {
movementLockedRef.current = movementLocked;
if (!movementLocked) return;
keys.current = { ...DEFAULT_KEYS };
wantsJump.current = false;
velocity.current.setX(0);
velocity.current.setZ(0);
}, [movementLocked]);
useEffect(() => {
const interaction = InteractionManager.getInstance();
@@ -267,20 +253,11 @@ export function PlayerController({
if (isPlayerInputLocked()) return;
if (setMovementKey(keys.current, event.key, true)) {
if (movementLockedRef.current) {
keys.current = { ...DEFAULT_KEYS };
}
event.preventDefault();
return;
}
if (event.key === JUMP_KEY) {
if (movementLockedRef.current) {
wantsJump.current = false;
event.preventDefault();
return;
}
wantsJump.current = true;
event.preventDefault();
return;
@@ -386,7 +363,7 @@ export function PlayerController({
}
_wishDir.set(0, 0, 0);
if (!movementLocked && !isEbikeBreakdown) {
if (!isEbikeBreakdown) {
if (keys.current.forward) _wishDir.add(_forward);
if (keys.current.backward) _wishDir.sub(_forward);
if (!isEbikeMounted) {