Release Healer Man 0.1.6
This commit is contained in:
1 parent
55cfd43d66
commit
b12fb84859
96 files changed
+10874
-609
No files matched your search
+10
-4
@@ -20,6 +20,7 @@ import { useGameStore } from "../game/store";
|
||||
import { DungeonEnvironment } from "./DungeonEnvironment";
|
||||
import { MobPopulation } from "./MobPopulation";
|
||||
import { PartyPopulation } from "./PartyPopulation";
|
||||
import { GroundEffectTelegraphs } from "./GroundEffectTelegraphs";
|
||||
import { PlayerRig } from "./PlayerRig";
|
||||
import { useManastormStore } from "../game/manastormStore";
|
||||
import { ManastormPortal } from "./ManastormPortal";
|
||||
@@ -40,6 +41,9 @@ import { GameGltfLoaderLifecycle } from "./useGameGLTF";
|
||||
import { CombatEffects } from "./CombatEffects";
|
||||
import { PLAYER_GRAVITY } from "../game/playerMovement";
|
||||
import { GmPlacementMarkers } from "./GmPlacementMarkers";
|
||||
import { useOnlineGroupStore } from "../app/onlineGroupStore";
|
||||
import { useOnlineSessionStore } from "../app/onlineSessionStore";
|
||||
import { OnlinePlayerPopulation } from "./OnlinePlayerPopulation";
|
||||
|
||||
export function GameScene() {
|
||||
const activeDungeonId = useGameStore((state) => state.activeDungeonId);
|
||||
@@ -88,6 +92,8 @@ export function GameScene() {
|
||||
const paused = useGameStore((state) => state.paused);
|
||||
const mapOpen = useGameStore((state) => state.mapOpen);
|
||||
const companionOpen = useGameStore((state) => state.companionOpen);
|
||||
const sharedOnline = useOnlineGroupStore((state) => state.activeActivity !== null);
|
||||
const onlineAuthority = useOnlineSessionStore((state) => state.snapshot?.authority ?? true);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [readySessionRevision, setReadySessionRevision] = useState<number | null>(null);
|
||||
const [safeGraphics, setSafeGraphics] = useState(false);
|
||||
@@ -96,9 +102,7 @@ export function GameScene() {
|
||||
const worldReady = encounterWorldIsReady(readySessionRevision, sessionRevision);
|
||||
const simulationBlocked = graphicsRecovery !== null
|
||||
|| !worldReady
|
||||
|| paused
|
||||
|| mapOpen
|
||||
|| companionOpen;
|
||||
|| (!sharedOnline && (paused || mapOpen || companionOpen));
|
||||
const allowedRuntimeIds = useMemo(
|
||||
() => gameMode === "manastorm" && manastormEncounter
|
||||
? manastormChaoticLinkRuntimeIds(manastormEncounter, manastormPhase)
|
||||
@@ -236,7 +240,9 @@ export function GameScene() {
|
||||
{worldReady && (
|
||||
<>
|
||||
<PlayerRig />
|
||||
<PartyPopulation active={!simulationBlocked} />
|
||||
<OnlinePlayerPopulation active={!simulationBlocked} />
|
||||
<PartyPopulation active={!simulationBlocked} simulate={!sharedOnline || onlineAuthority} />
|
||||
<GroundEffectTelegraphs />
|
||||
<MobPopulation
|
||||
active={!simulationBlocked}
|
||||
entities={stagePopulation.entities}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { DoubleSide } from "three";
|
||||
import { useCombatStore } from "../game/combatStore";
|
||||
|
||||
/** Presentation-only rings; placement and ticking remain in the combat runtime. */
|
||||
export function GroundEffectTelegraphs() {
|
||||
const effects = useCombatStore((state) => state.areaEffects);
|
||||
const pending = useCombatStore((state) => state.pendingGroundTarget);
|
||||
return (
|
||||
<group name="combat-ground-effects">
|
||||
{pending ? (
|
||||
<mesh
|
||||
name="ground-target-reticle"
|
||||
position={[pending.position[0], pending.position[1] + 0.045, pending.position[2]]}
|
||||
rotation={[-Math.PI / 2, 0, 0]}
|
||||
renderOrder={5}
|
||||
>
|
||||
<ringGeometry args={[Math.max(0.1, pending.radius - 0.16), pending.radius, 64]} />
|
||||
<meshBasicMaterial
|
||||
color={pending.valid ? "#78dfac" : "#e55f57"}
|
||||
transparent
|
||||
opacity={0.78}
|
||||
depthWrite={false}
|
||||
side={DoubleSide}
|
||||
/>
|
||||
</mesh>
|
||||
) : null}
|
||||
{effects.map((effect) => (
|
||||
<mesh
|
||||
key={effect.id}
|
||||
position={[effect.position[0], effect.position[1] + 0.035, effect.position[2]]}
|
||||
rotation={[-Math.PI / 2, 0, 0]}
|
||||
renderOrder={4}
|
||||
>
|
||||
<ringGeometry args={[Math.max(0.1, effect.radius - 0.12), effect.radius, 64]} />
|
||||
<meshBasicMaterial color="#e9c95c" transparent opacity={0.58} depthWrite={false} side={DoubleSide} />
|
||||
</mesh>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { MANASTORM_BUBBLE_DEFAULT_RADIUS } from "../game/manastormClientOverlay"
|
||||
import { advanceManastormSession } from "../game/manastormSession";
|
||||
import { useManastormStore } from "../game/manastormStore";
|
||||
import { useGameStore } from "../game/store";
|
||||
import { requestOnlineInteraction } from "../game/onlineSessionRuntime";
|
||||
|
||||
const STORM_PARTICLES = [
|
||||
[-0.9, 1.4, 0.4],
|
||||
@@ -101,6 +102,10 @@ export function ManastormPortal() {
|
||||
useGameStore.getState().playerPosition,
|
||||
);
|
||||
if (inside && !wasInsideRef.current) {
|
||||
if (requestOnlineInteraction("portal")) {
|
||||
wasInsideRef.current = true;
|
||||
return;
|
||||
}
|
||||
transitionLatchedRef.current = true;
|
||||
transitionStartedAtRef.current = Date.now();
|
||||
displayedCountdownRef.current = NEXT_WAVE_COUNTDOWN_SECONDS;
|
||||
|
||||
@@ -73,6 +73,7 @@ import {
|
||||
resolveMobAnimationInterval,
|
||||
} from "./mobAnimationCadence";
|
||||
import { useGameGLTF } from "./useGameGLTF";
|
||||
import { onlineSynchronizedMobTransform, requestOnlineInteraction } from "../game/onlineSessionRuntime";
|
||||
|
||||
const MOVEMENT_LOCKING_MOB_STATUSES: ReadonlySet<string> = new Set([
|
||||
"stun", "root", "sleep", "knockdown", "freeze", "charm", "confuse",
|
||||
@@ -916,7 +917,9 @@ function ProxyMob({
|
||||
if (event.button !== PRIMARY_MOUSE_BUTTON) return;
|
||||
event.stopPropagation();
|
||||
if (dead) {
|
||||
useCombatStore.getState().lootMob(instanceId);
|
||||
if (!requestOnlineInteraction("loot", instanceId)) {
|
||||
useCombatStore.getState().lootMob(instanceId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
usePartyStore.getState().clearSelection();
|
||||
@@ -1188,6 +1191,25 @@ function RoamingPack({
|
||||
const forward = scratchForwards[index];
|
||||
const member = definition.members[index];
|
||||
const instanceId = `${definition.id}:${member.id}`;
|
||||
const synchronizedTransform = onlineSynchronizedMobTransform(instanceId);
|
||||
if (synchronizedTransform) {
|
||||
const object = memberRefs.current[index];
|
||||
if (object) {
|
||||
object.position.set(...synchronizedTransform.position);
|
||||
object.rotation.y = synchronizedTransform.yaw;
|
||||
}
|
||||
chasePositions[index][0] = synchronizedTransform.position[0];
|
||||
chasePositions[index][1] = synchronizedTransform.position[1];
|
||||
chasePositions[index][2] = synchronizedTransform.position[2];
|
||||
updateMobRuntimePosition(
|
||||
instanceId,
|
||||
synchronizedTransform.position[0],
|
||||
synchronizedTransform.position[1],
|
||||
synchronizedTransform.position[2],
|
||||
synchronizedTransform.yaw,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const mob = combat.mobs[instanceId];
|
||||
const chasePosition = chasePositions[index];
|
||||
const engaged = Boolean(mob?.engaged && !mob.dead);
|
||||
@@ -1303,6 +1325,24 @@ function StaticPopulationMember({ spawn, entities, active, showLabels, useOrigin
|
||||
const positionRef = useRef<MutableMobVector3>([...spawn.position]);
|
||||
useEffect(() => registerMobRuntime(spawn.id, spawn.position, spawn.yaw ?? 0), [spawn.id, spawn.position, spawn.yaw]);
|
||||
useFrame((_, delta) => {
|
||||
const synchronizedTransform = onlineSynchronizedMobTransform(spawn.id);
|
||||
if (synchronizedTransform) {
|
||||
positionRef.current[0] = synchronizedTransform.position[0];
|
||||
positionRef.current[1] = synchronizedTransform.position[1];
|
||||
positionRef.current[2] = synchronizedTransform.position[2];
|
||||
if (groupRef.current) {
|
||||
groupRef.current.position.set(...synchronizedTransform.position);
|
||||
groupRef.current.rotation.y = synchronizedTransform.yaw;
|
||||
}
|
||||
updateMobRuntimePosition(
|
||||
spawn.id,
|
||||
synchronizedTransform.position[0],
|
||||
synchronizedTransform.position[1],
|
||||
synchronizedTransform.position[2],
|
||||
synchronizedTransform.yaw,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const mob = useCombatStore.getState().mobs[spawn.id];
|
||||
const position = positionRef.current;
|
||||
const player = useGameStore.getState().playerPosition;
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { Html } from "@react-three/drei";
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useMemo, useRef, type CSSProperties } from "react";
|
||||
import type { Group } from "three";
|
||||
import {
|
||||
classById,
|
||||
clampAppearance,
|
||||
createDefaultAppearance,
|
||||
type ClassId,
|
||||
type GenderId,
|
||||
type RaceId,
|
||||
} from "../app/characterCatalog";
|
||||
import { useOnlineSessionStore } from "../app/onlineSessionStore";
|
||||
import type { OnlineSessionPlayer } from "../app/onlineSessionTypes";
|
||||
import { CharacterModel, type AvatarIdentity } from "../avatar/CharacterModel";
|
||||
import { ProceduralAvatar } from "../avatar/ProceduralAvatar";
|
||||
import { useCombatStore } from "../game/combatStore";
|
||||
import { onlinePlayerActorId } from "../game/onlineSessionRuntime";
|
||||
import { usePartyStore } from "../game/partyStore";
|
||||
|
||||
const labelStyle: CSSProperties = {
|
||||
color: "#d8f6ff",
|
||||
fontFamily: "system-ui, sans-serif",
|
||||
fontSize: "11px",
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.1,
|
||||
pointerEvents: "none",
|
||||
textAlign: "center",
|
||||
textShadow: "0 1px 2px #000, 0 0 5px #000",
|
||||
userSelect: "none",
|
||||
whiteSpace: "nowrap",
|
||||
};
|
||||
|
||||
function validAppearance(player: OnlineSessionPlayer): AvatarIdentity {
|
||||
const raceId = (player.character?.raceId ?? "human") as RaceId;
|
||||
const gender = (player.character?.gender ?? "male") as GenderId;
|
||||
const source = player.character?.appearance;
|
||||
const appearance = source && [source.skinColor, source.face, source.hairStyle, source.hairColor, source.feature].every(Number.isFinite)
|
||||
? source
|
||||
: createDefaultAppearance();
|
||||
return {
|
||||
raceId,
|
||||
gender,
|
||||
classId: (player.character?.classId ?? "priest") as ClassId,
|
||||
appearance: clampAppearance(raceId, gender, appearance),
|
||||
};
|
||||
}
|
||||
|
||||
function RemoteOnlinePlayer({ player, active }: {
|
||||
readonly player: OnlineSessionPlayer;
|
||||
readonly active: boolean;
|
||||
}) {
|
||||
const rootRef = useRef<Group>(null);
|
||||
const movingRef = useRef(false);
|
||||
const animationEventRef = useRef(player.animationEvent);
|
||||
animationEventRef.current = player.animationEvent;
|
||||
const previousTargetRef = useRef<[number, number, number]>([...player.position]);
|
||||
const identity = useMemo(() => validAppearance(player), [player.character]);
|
||||
const characterClass = classById(identity.classId);
|
||||
const actorId = onlinePlayerActorId(player.accountId);
|
||||
const selected = useCombatStore((state) => state.selectedFriendlyActorId === actorId);
|
||||
|
||||
useFrame((_, delta) => {
|
||||
const root = rootRef.current;
|
||||
if (!root) return;
|
||||
const target = player.position;
|
||||
const previousTarget = previousTargetRef.current;
|
||||
movingRef.current = Math.hypot(
|
||||
target[0] - previousTarget[0],
|
||||
target[2] - previousTarget[2],
|
||||
) > 0.015;
|
||||
previousTarget[0] = target[0];
|
||||
previousTarget[1] = target[1];
|
||||
previousTarget[2] = target[2];
|
||||
const alpha = 1 - Math.exp(-18 * Math.min(delta, 0.05));
|
||||
root.position.x += (target[0] - root.position.x) * alpha;
|
||||
root.position.y += (target[1] - root.position.y) * alpha;
|
||||
root.position.z += (target[2] - root.position.z) * alpha;
|
||||
const yawDelta = Math.atan2(Math.sin(player.yaw - root.rotation.y), Math.cos(player.yaw - root.rotation.y));
|
||||
root.rotation.y += yawDelta * alpha;
|
||||
});
|
||||
|
||||
return (
|
||||
<group
|
||||
ref={rootRef}
|
||||
name={`online-player-${player.accountId}`}
|
||||
position={player.position}
|
||||
rotation={[0, player.yaw, 0]}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.nativeEvent.stopPropagation();
|
||||
if (event.button !== 0 || !active || player.health <= 0) return;
|
||||
useCombatStore.getState().clearTarget();
|
||||
usePartyStore.getState().clearSelection();
|
||||
useCombatStore.getState().selectFriendlyActor(actorId);
|
||||
}}
|
||||
>
|
||||
<mesh position={[0, 1.05, 0]}>
|
||||
<capsuleGeometry args={[0.48, 1.2, 6, 10]} />
|
||||
<meshBasicMaterial transparent opacity={0} colorWrite={false} depthWrite={false} />
|
||||
</mesh>
|
||||
<CharacterModel
|
||||
identity={identity}
|
||||
equipment={player.equipment}
|
||||
active={active}
|
||||
movingRef={movingRef}
|
||||
animationEventRef={animationEventRef}
|
||||
fallback={<ProceduralAvatar accent={characterClass.color} />}
|
||||
/>
|
||||
{selected && player.health > 0 ? (
|
||||
<mesh position={[0, 0.035, 0]} rotation={[Math.PI / 2, 0, 0]}>
|
||||
<torusGeometry args={[0.62, 0.055, 6, 28]} />
|
||||
<meshBasicMaterial color="#8deaff" transparent opacity={0.95} depthWrite={false} />
|
||||
</mesh>
|
||||
) : null}
|
||||
<Html center position={[0, 2.45, 0]} zIndexRange={[5, 0]} style={{
|
||||
...labelStyle,
|
||||
opacity: player.health > 0 ? 1 : 0.55,
|
||||
}}>
|
||||
<div>{player.character?.name ?? player.username}<br /><small>Online player · {player.role ?? "party"}</small></div>
|
||||
</Html>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export function OnlinePlayerPopulation({ active = true }: { readonly active?: boolean }) {
|
||||
const snapshot = useOnlineSessionStore((state) => state.snapshot);
|
||||
if (!snapshot) return null;
|
||||
const remotePlayers = snapshot.players.filter((player) => (
|
||||
player.accountId !== snapshot.localAccountId && player.character
|
||||
));
|
||||
if (!remotePlayers.length) return null;
|
||||
return (
|
||||
<group name="online-player-population">
|
||||
{remotePlayers.map((player) => (
|
||||
<RemoteOnlinePlayer key={player.accountId} player={player} active={active} />
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
@@ -65,6 +65,7 @@ import { partyCombatRangeBand } from "../game/partyRuntime";
|
||||
import { useWailingEncounterStore } from "../game/wailingCavernsEncounter";
|
||||
import {
|
||||
clearPartyRuntimeAttackCorridor,
|
||||
getPartyRuntimePosition,
|
||||
registerPartyRuntimePosition,
|
||||
updatePartyRuntimeAttackCorridor,
|
||||
updatePartyRuntimePosition,
|
||||
@@ -75,6 +76,8 @@ import {
|
||||
import { usePartyStore } from "../game/partyStore";
|
||||
import { useGameStore } from "../game/store";
|
||||
import { useManastormStore } from "../game/manastormStore";
|
||||
import { registerCombatSpatialProvider } from "../game/combatSpatial";
|
||||
import { PLAYER_AGGRO_ID } from "../game/aggro";
|
||||
|
||||
const PARTY_MOVE_SPEED = 5.25;
|
||||
const PARTY_CATCH_UP_SPEED = 7.5;
|
||||
@@ -266,12 +269,14 @@ function initialMemberPosition(index: number): MutablePartyWorldPosition {
|
||||
|
||||
function PartyActors({
|
||||
active,
|
||||
simulate,
|
||||
navigationGraph,
|
||||
navigationFailed,
|
||||
objectives,
|
||||
stackAtEntrance,
|
||||
}: {
|
||||
readonly active: boolean;
|
||||
readonly simulate: boolean;
|
||||
readonly navigationGraph: PartyNavigationGraph | null;
|
||||
readonly navigationFailed: boolean;
|
||||
readonly objectives: readonly DungeonBossObjective[];
|
||||
@@ -442,6 +447,56 @@ function PartyActors({
|
||||
const directFollowMovingRef = useRef(new Map<string, boolean>());
|
||||
const combatAnchorsRef = useRef(new Map<string, PartyCombatAnchor>());
|
||||
const memberIdKey = members.map((member) => member.id).join("|");
|
||||
useEffect(() => registerCombatSpatialProvider({
|
||||
actor: (actorId) => {
|
||||
const gameState = useGameStore.getState();
|
||||
if (actorId === PLAYER_AGGRO_ID) {
|
||||
return { id: actorId, position: gameState.playerPosition, yaw: gameState.cameraYaw };
|
||||
}
|
||||
const memberIndex = usePartyStore.getState().members.findIndex((member) => member.id === actorId);
|
||||
const memberPosition = positionsRef.current.get(actorId);
|
||||
if (memberPosition) {
|
||||
return {
|
||||
id: actorId,
|
||||
position: memberPosition,
|
||||
yaw: actorRefs.current[memberIndex]?.rotation.y ?? gameState.cameraYaw,
|
||||
};
|
||||
}
|
||||
const summon = useCombatStore.getState().summons.find((candidate) => candidate.id === actorId);
|
||||
if (summon) return { id: actorId, position: summon.position, yaw: gameState.cameraYaw };
|
||||
const mobPosition = getMobPosition(actorId);
|
||||
if (!mobPosition) return null;
|
||||
const mob = useCombatStore.getState().mobs[actorId];
|
||||
const targetPosition = mob?.targetActorId === PLAYER_AGGRO_ID
|
||||
? gameState.playerPosition
|
||||
: mob?.targetActorId
|
||||
? positionsRef.current.get(mob.targetActorId)
|
||||
?? useCombatStore.getState().summons.find((candidate) => candidate.id === mob.targetActorId)?.position
|
||||
: null;
|
||||
const yaw = targetPosition
|
||||
? Math.atan2(targetPosition[0] - mobPosition[0], targetPosition[2] - mobPosition[2])
|
||||
: 0;
|
||||
return { id: actorId, position: mobPosition, yaw };
|
||||
},
|
||||
hasLineOfSight: hasStaticLineOfSight,
|
||||
projectGroundPoint: (requested, maximumDistance, requestedOrigin) => {
|
||||
const origin = requestedOrigin ?? useGameStore.getState().playerPosition;
|
||||
const dx = requested[0] - origin[0];
|
||||
const dz = requested[2] - origin[2];
|
||||
const planar = Math.hypot(dx, dz);
|
||||
const scale = planar > maximumDistance && planar > 0 ? maximumDistance / planar : 1;
|
||||
const x = origin[0] + dx * scale;
|
||||
const z = origin[2] + dz * scale;
|
||||
const ray = new rapier.Ray({ x, y: requested[1] + 8, z }, { x: 0, y: -1, z: 0 });
|
||||
const hit = world.castRay(
|
||||
ray,
|
||||
20,
|
||||
true,
|
||||
rapier.QueryFilterFlags.EXCLUDE_SENSORS | rapier.QueryFilterFlags.EXCLUDE_DYNAMIC,
|
||||
);
|
||||
return hit ? [x, ray.origin.y - hit.timeOfImpact, z] : null;
|
||||
},
|
||||
}), [hasStaticLineOfSight, memberIdKey, rapier, world]);
|
||||
const teleportPartyToPlayer = useCallback((player: PartyWorldPosition, now: number): void => {
|
||||
const party = usePartyStore.getState();
|
||||
party.members.forEach((member, index) => {
|
||||
@@ -606,6 +661,21 @@ function PartyActors({
|
||||
if (!active) return;
|
||||
const party = usePartyStore.getState();
|
||||
if (!party.members.length) return;
|
||||
if (!simulate) {
|
||||
party.members.forEach((member, index) => {
|
||||
const synchronized = getPartyRuntimePosition(member.id);
|
||||
if (!synchronized) return;
|
||||
const position = positionsRef.current.get(member.id) ?? initialMemberPosition(index);
|
||||
position[0] = synchronized[0];
|
||||
position[1] = synchronized[1];
|
||||
position[2] = synchronized[2];
|
||||
positionsRef.current.set(member.id, position);
|
||||
actorRefs.current[index]?.position.set(position[0], presentationY(position), position[2]);
|
||||
const movingSignal = movingSignalsRef.current.get(member.id);
|
||||
if (movingSignal) movingSignal.current = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
const game = useGameStore.getState();
|
||||
const playerNavigation = game.playerNavigationPosition;
|
||||
appendPartyBreadcrumb(playerTrailRef.current, playerNavigation);
|
||||
@@ -1320,7 +1390,13 @@ function PartyActors({
|
||||
);
|
||||
}
|
||||
|
||||
export function PartyPopulation({ active = true }: { readonly active?: boolean }) {
|
||||
export function PartyPopulation({
|
||||
active = true,
|
||||
simulate = true,
|
||||
}: {
|
||||
readonly active?: boolean;
|
||||
readonly simulate?: boolean;
|
||||
}) {
|
||||
const activeDungeonId = useGameStore((state) => state.activeDungeonId);
|
||||
const gameMode = useGameStore((state) => state.gameMode);
|
||||
const sessionRevision = useGameStore((state) => state.sessionRevision);
|
||||
@@ -1383,6 +1459,7 @@ export function PartyPopulation({ active = true }: { readonly active?: boolean }
|
||||
<PartyActors
|
||||
key={activeDungeonId}
|
||||
active={active}
|
||||
simulate={simulate}
|
||||
navigationGraph={navigationGraph}
|
||||
navigationFailed={navigationFailed || !assets.navigation}
|
||||
objectives={objectives}
|
||||
|
||||
+13
-3
@@ -180,6 +180,15 @@ function PlayerController({
|
||||
orbit.yaw = cameraYawAfterHorizontalLook(orbit.yaw, snapshot.lookX * delta * 2.25);
|
||||
orbit.pitch = clampCameraPitch(orbit.pitch + snapshot.lookY * delta * 1.55);
|
||||
}
|
||||
const groundTargetActive = useCombatStore.getState().pendingGroundTarget !== null;
|
||||
if (groundTargetActive && !paused && !mapOpen && !companionOpen) {
|
||||
useCombatStore.getState().moveGroundTarget(
|
||||
snapshot.moveX,
|
||||
snapshot.moveForward,
|
||||
orbit.yaw,
|
||||
delta,
|
||||
);
|
||||
}
|
||||
|
||||
const translation = body.translation();
|
||||
if (![translation.x, translation.y, translation.z].every(Number.isFinite)) {
|
||||
@@ -222,7 +231,8 @@ function PlayerController({
|
||||
body.setNextKinematicTranslation(translation);
|
||||
return;
|
||||
}
|
||||
if (requestedAtMs !== null) bufferPlayerJump(jumpTimingRef.current, requestedAtMs);
|
||||
if (requestedAtMs !== null && !groundTargetActive) bufferPlayerJump(jumpTimingRef.current, requestedAtMs);
|
||||
if (groundTargetActive) cancelBufferedPlayerJump(jumpTimingRef.current);
|
||||
|
||||
const shouldJump = updatePlayerJumpTiming(
|
||||
jumpTimingRef.current,
|
||||
@@ -238,8 +248,8 @@ function PlayerController({
|
||||
: airbornePlayerVerticalVelocity(verticalVelocityRef.current, movementDeltaSeconds);
|
||||
|
||||
const [worldX, worldZ] = cameraRelativeMovement(
|
||||
snapshot.moveX,
|
||||
snapshot.moveForward,
|
||||
groundTargetActive ? 0 : snapshot.moveX,
|
||||
groundTargetActive ? 0 : snapshot.moveForward,
|
||||
orbit.yaw,
|
||||
);
|
||||
if (body.numColliders() === 0) return;
|
||||
|
||||
Reference in new issue
Block a user