Release Healer Man 0.1.5
This commit is contained in:
1 parent
f4c9cf356c
commit
55cfd43d66
62 files changed
+3030
-153
No files matched your search
@@ -29,7 +29,7 @@ import {
|
||||
rendererCanvasKey,
|
||||
} from "./sceneLifecycle";
|
||||
import { manastormChaoticLinkRuntimeIds } from "../game/manastormChaoticLink";
|
||||
import { manastormStagePopulation } from "../game/manastormStagePopulation";
|
||||
import { manastormStagePopulation, manastormStageRoamingPacks } from "../game/manastormStagePopulation";
|
||||
import { resolveStagePresentation } from "../game/manastormStagePresentation";
|
||||
import {
|
||||
useWailingEncounterStore,
|
||||
@@ -38,6 +38,8 @@ import {
|
||||
import { WailingNaralexEvent } from "./WailingNaralexEvent";
|
||||
import { GameGltfLoaderLifecycle } from "./useGameGLTF";
|
||||
import { CombatEffects } from "./CombatEffects";
|
||||
import { PLAYER_GRAVITY } from "../game/playerMovement";
|
||||
import { GmPlacementMarkers } from "./GmPlacementMarkers";
|
||||
|
||||
export function GameScene() {
|
||||
const activeDungeonId = useGameStore((state) => state.activeDungeonId);
|
||||
@@ -73,7 +75,10 @@ export function GameScene() {
|
||||
: { entities: dungeon.entities, staticSpawns: dungeon.staticSpawns },
|
||||
[dungeon, gameMode, manastormEncounter],
|
||||
);
|
||||
const roamingPacks = dungeon.roamingPacks.filter((pack) => spawnMatchesDifficulty(pack.spawnMask, difficultyMask));
|
||||
const roamingPacks = manastormStageRoamingPacks(
|
||||
dungeon.roamingPacks.filter((pack) => spawnMatchesDifficulty(pack.spawnMask, difficultyMask)),
|
||||
gameMode === "manastorm" ? manastormEncounter : null,
|
||||
);
|
||||
const difficultySpawns = stagePopulation.staticSpawns.filter(
|
||||
(spawn) => spawnMatchesDifficulty(spawn.spawnMask, difficultyMask),
|
||||
);
|
||||
@@ -223,7 +228,7 @@ export function GameScene() {
|
||||
/>
|
||||
<Physics
|
||||
key={encounterWorldKey(activeDungeonId, sessionRevision)}
|
||||
gravity={[0, -18, 0]}
|
||||
gravity={[0, PLAYER_GRAVITY, 0]}
|
||||
paused={simulationBlocked}
|
||||
timeStep={1 / 60}
|
||||
>
|
||||
@@ -241,6 +246,7 @@ export function GameScene() {
|
||||
/>
|
||||
<WailingNaralexEvent />
|
||||
<ManastormPortal />
|
||||
<GmPlacementMarkers />
|
||||
<CombatEffects active={!simulationBlocked} safeGraphics={safeGraphics} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { useFrame } from "@react-three/fiber";
|
||||
import { useMemo, useRef } from "react";
|
||||
import type { Group } from "three";
|
||||
import { useShellStore } from "../app/shellStore";
|
||||
import { requireDungeonDefinition } from "../game/dungeonRegistry";
|
||||
import type { Vector3Tuple } from "../game/dungeonTypes";
|
||||
import {
|
||||
authoredStageConfigForBoss,
|
||||
derivePartySpawnSlots,
|
||||
isGameMasterSession,
|
||||
resolvedBossAdminCatalog,
|
||||
} from "../game/manastormAdminConfig";
|
||||
import { useManastormAdminStore } from "../game/manastormAdminStore";
|
||||
import { getMobRuntimePosition } from "../game/mobRuntimeRegistry";
|
||||
import { useGameStore } from "../game/store";
|
||||
|
||||
function WorldMarker(props: {
|
||||
readonly position: Vector3Tuple;
|
||||
readonly color: string;
|
||||
readonly scale?: number;
|
||||
readonly runtimeId?: string;
|
||||
}) {
|
||||
const group = useRef<Group>(null);
|
||||
useFrame(() => {
|
||||
if (!props.runtimeId || !group.current) return;
|
||||
const live = getMobRuntimePosition(props.runtimeId);
|
||||
if (live) group.current.position.set(live[0], live[1], live[2]);
|
||||
});
|
||||
return (
|
||||
<group ref={group} position={props.position} scale={props.scale ?? 1}>
|
||||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.04, 0]} renderOrder={20}>
|
||||
<ringGeometry args={[0.55, 0.76, 28]} />
|
||||
<meshBasicMaterial color={props.color} transparent opacity={0.9} depthTest={false} />
|
||||
</mesh>
|
||||
<mesh position={[0, 0.72, 0]} renderOrder={20}>
|
||||
<coneGeometry args={[0.16, 1.25, 10]} />
|
||||
<meshBasicMaterial color={props.color} transparent opacity={0.78} depthTest={false} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
function fallbackMobPosition(dungeonId: string, runtimeId: string, bossPosition: Vector3Tuple): Vector3Tuple {
|
||||
const dungeon = requireDungeonDefinition(dungeonId);
|
||||
const staticSpawn = dungeon.staticSpawns.find((spawn) => spawn.id === runtimeId);
|
||||
if (staticSpawn) return staticSpawn.position;
|
||||
for (const pack of dungeon.roamingPacks) {
|
||||
const member = pack.members.find((candidate) => `${pack.id}:${candidate.id}` === runtimeId);
|
||||
const origin = pack.waypoints[0];
|
||||
if (member && origin) {
|
||||
return [origin[0] + member.formationOffset[0], origin[1] + member.formationOffset[1], origin[2] + member.formationOffset[2]];
|
||||
}
|
||||
}
|
||||
return bossPosition;
|
||||
}
|
||||
|
||||
export function GmPlacementMarkers() {
|
||||
const overlay = useGameStore((state) => state.overlay);
|
||||
const activeDungeonId = useGameStore((state) => state.activeDungeonId);
|
||||
const session = useShellStore((state) => state.session);
|
||||
const draft = useManastormAdminStore((state) => state.draft);
|
||||
const editingBossKey = useManastormAdminStore((state) => state.editingBossKey);
|
||||
const entry = useMemo(
|
||||
() => resolvedBossAdminCatalog(draft).find((candidate) => candidate.key === editingBossKey && candidate.dungeonId === activeDungeonId) ?? null,
|
||||
[activeDungeonId, draft, editingBossKey],
|
||||
);
|
||||
const stage = useMemo(() => entry ? authoredStageConfigForBoss(entry, draft) : null, [draft, entry]);
|
||||
const party = useMemo(() => stage ? derivePartySpawnSlots(stage.groupSpawn, 5) : [], [stage]);
|
||||
|
||||
if (overlay !== "gm" || !isGameMasterSession(session) || !entry || !stage) return null;
|
||||
return (
|
||||
<group name="gm-placement-markers">
|
||||
{party.map((slot, index) => (
|
||||
<WorldMarker key={`party-${index}`} position={slot.footPosition} color="#55e8ff" scale={index === 0 ? 1.15 : 0.8} />
|
||||
))}
|
||||
<WorldMarker position={stage.bossPosition} color="#ff485f" scale={1.4} />
|
||||
<WorldMarker position={stage.portalPosition} color="#c26bff" scale={1.2} />
|
||||
{stage.linkedMobRuntimeIds.map((runtimeId) => (
|
||||
<WorldMarker
|
||||
key={runtimeId}
|
||||
position={fallbackMobPosition(stage.dungeonId, runtimeId, stage.bossPosition)}
|
||||
color="#ffd255"
|
||||
runtimeId={runtimeId}
|
||||
/>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
}
|
||||
@@ -1146,9 +1146,10 @@ function RoamingPack({
|
||||
const cleanups = definition.members.map((member, index) => registerMobRuntime(
|
||||
`${definition.id}:${member.id}`,
|
||||
scratchPositions[index],
|
||||
Math.atan2(scratchForwards[index][0], scratchForwards[index][2]),
|
||||
));
|
||||
return () => cleanups.forEach((cleanup) => cleanup());
|
||||
}, [definition.id, definition.members, scratchPositions]);
|
||||
}, [definition.id, definition.members, scratchForwards, scratchPositions]);
|
||||
|
||||
useFrame((_, delta) => {
|
||||
if (!nearby) return;
|
||||
@@ -1242,16 +1243,18 @@ function RoamingPack({
|
||||
chasePosition[1] = patrolPosition[1];
|
||||
chasePosition[2] = patrolPosition[2];
|
||||
}
|
||||
const yaw = Math.atan2(forward[0], forward[2]);
|
||||
updateMobRuntimePosition(
|
||||
instanceId,
|
||||
position[0],
|
||||
position[1],
|
||||
position[2],
|
||||
yaw,
|
||||
);
|
||||
const object = memberRefs.current[index];
|
||||
if (!object) continue;
|
||||
object.position.set(position[0], position[1], position[2]);
|
||||
object.rotation.y = Math.atan2(forward[0], forward[2]);
|
||||
object.rotation.y = yaw;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1298,7 +1301,7 @@ function StaticPopulationMember({ spawn, entities, active, showLabels, useOrigin
|
||||
const entity = entities[spawn.entityId];
|
||||
const groupRef = useRef<Group>(null);
|
||||
const positionRef = useRef<MutableMobVector3>([...spawn.position]);
|
||||
useEffect(() => registerMobRuntime(spawn.id, spawn.position), [spawn.id, spawn.position]);
|
||||
useEffect(() => registerMobRuntime(spawn.id, spawn.position, spawn.yaw ?? 0), [spawn.id, spawn.position, spawn.yaw]);
|
||||
useFrame((_, delta) => {
|
||||
const mob = useCombatStore.getState().mobs[spawn.id];
|
||||
const position = positionRef.current;
|
||||
@@ -1334,7 +1337,6 @@ function StaticPopulationMember({ spawn, entities, active, showLabels, useOrigin
|
||||
position[1] = spawn.position[1];
|
||||
position[2] = spawn.position[2];
|
||||
}
|
||||
updateMobRuntimePosition(spawn.id, position[0], position[1], position[2]);
|
||||
if (groupRef.current) {
|
||||
groupRef.current.position.set(position[0], position[1], position[2]);
|
||||
if (mob?.engaged) {
|
||||
@@ -1345,6 +1347,13 @@ function StaticPopulationMember({ spawn, entities, active, showLabels, useOrigin
|
||||
);
|
||||
}
|
||||
}
|
||||
updateMobRuntimePosition(
|
||||
spawn.id,
|
||||
position[0],
|
||||
position[1],
|
||||
position[2],
|
||||
groupRef.current?.rotation.y ?? spawn.yaw ?? 0,
|
||||
);
|
||||
});
|
||||
if (!entity) return null;
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import { equipmentItemsForOwner } from "../game/equipment";
|
||||
import type { CharacterCombatAnimationEvent } from "../game/combatAnimation";
|
||||
import { resolveDungeonAssets, resolveManastormStageAssets } from "../game/dungeonAssets";
|
||||
import { activeManastormStageAssetPackage } from "../game/manastormStageLoader";
|
||||
import { derivePartySpawnSlots } from "../game/manastormAdminConfig";
|
||||
import { requireDungeonDefinition } from "../game/dungeonRegistry";
|
||||
import type { DungeonBossObjective } from "../game/dungeonTypes";
|
||||
import { getMobPosition } from "../game/mobRuntimeRegistry";
|
||||
@@ -534,21 +535,13 @@ function PartyActors({
|
||||
|
||||
useEffect(() => {
|
||||
const game = useGameStore.getState();
|
||||
const spawnSlots = derivePartySpawnSlots(game.activeSpawn, members.length + 1);
|
||||
members.forEach((member, index) => {
|
||||
const position = positionsRef.current.get(member.id) ?? initialMemberPosition(index);
|
||||
if (game.gameMode === "manastorm" || stackAtEntrance) {
|
||||
position[0] = game.playerPosition[0];
|
||||
position[1] = game.playerPosition[1];
|
||||
position[2] = game.playerPosition[2];
|
||||
} else {
|
||||
setEntrancePosition(
|
||||
index,
|
||||
game.playerPosition,
|
||||
Math.sin(game.cameraYaw),
|
||||
Math.cos(game.cameraYaw),
|
||||
position,
|
||||
);
|
||||
}
|
||||
const slot = spawnSlots[index + 1] ?? spawnSlots[0];
|
||||
position[0] = slot.footPosition[0];
|
||||
position[1] = slot.footPosition[1];
|
||||
position[2] = slot.footPosition[2];
|
||||
const navigationSpawn = navigationGraphRef.current
|
||||
? findNearestPartyNavigationSurface(navigationGraphRef.current, position, 6)
|
||||
: null;
|
||||
|
||||
+74
-25
@@ -50,6 +50,13 @@ import {
|
||||
updatePlayerJumpTiming,
|
||||
type CharacterVerticalMotion,
|
||||
} from "../game/playerJump";
|
||||
import {
|
||||
PLAYER_CHARACTER_CONTROLLER_OFFSET,
|
||||
PLAYER_GROUND_STICK_VELOCITY,
|
||||
airbornePlayerVerticalVelocity,
|
||||
configurePlayerCharacterController,
|
||||
playerFrameDeltaSeconds,
|
||||
} from "../game/playerMovement";
|
||||
import { activeManastormStageAssetPackage } from "../game/manastormStageLoader";
|
||||
import { useGameStore } from "../game/store";
|
||||
|
||||
@@ -85,11 +92,13 @@ interface PlayerControllerProps extends PlayerRigRefs {
|
||||
|
||||
function placeAtActiveSpawn(body: RapierRigidBody, orbit: OrbitState): void {
|
||||
const spawn = useGameStore.getState().activeSpawn;
|
||||
body.setTranslation({
|
||||
const translation = {
|
||||
x: spawn.footPosition[0],
|
||||
y: spawn.footPosition[1] + PLAYER_CENTER_HEIGHT,
|
||||
z: spawn.footPosition[2],
|
||||
}, true);
|
||||
};
|
||||
body.setTranslation(translation, true);
|
||||
body.setNextKinematicTranslation(translation);
|
||||
body.setLinvel({ x: 0, y: 0, z: 0 }, true);
|
||||
body.setAngvel({ x: 0, y: 0, z: 0 }, true);
|
||||
orbit.yaw = spawn.yaw;
|
||||
@@ -122,8 +131,19 @@ function PlayerController({
|
||||
() => new rapier.Ray({ x: 0, y: 0, z: 0 }, { x: 0, y: -1, z: 0 }),
|
||||
[rapier],
|
||||
);
|
||||
const characterController = useMemo(
|
||||
() => configurePlayerCharacterController(
|
||||
world.createCharacterController(PLAYER_CHARACTER_CONTROLLER_OFFSET),
|
||||
),
|
||||
[world],
|
||||
);
|
||||
const jumpTimingRef = useRef(createPlayerJumpTiming());
|
||||
const verticalPresentationTimingRef = useRef(createCharacterVerticalPresentationTiming());
|
||||
const verticalVelocityRef = useRef(0);
|
||||
|
||||
useEffect(() => () => {
|
||||
world.removeCharacterController(characterController);
|
||||
}, [characterController, world]);
|
||||
|
||||
useEffect(() => {
|
||||
if (bodyRef.current) placeAtActiveSpawn(bodyRef.current, orbitRef.current);
|
||||
@@ -131,6 +151,7 @@ function PlayerController({
|
||||
clearJumpRequest();
|
||||
jumpTimingRef.current = createPlayerJumpTiming(performance.now());
|
||||
verticalPresentationTimingRef.current = createCharacterVerticalPresentationTiming();
|
||||
verticalVelocityRef.current = 0;
|
||||
verticalMotionRef.current = "grounded";
|
||||
}, [bodyRef, orbitRef, resetRevision, spawn, verticalMotionRef]);
|
||||
|
||||
@@ -160,7 +181,6 @@ function PlayerController({
|
||||
orbit.pitch = clampCameraPitch(orbit.pitch + snapshot.lookY * delta * 1.55);
|
||||
}
|
||||
|
||||
const velocity = body.linvel();
|
||||
const translation = body.translation();
|
||||
if (![translation.x, translation.y, translation.z].every(Number.isFinite)) {
|
||||
placeAtActiveSpawn(body, orbit);
|
||||
@@ -181,20 +201,25 @@ function PlayerController({
|
||||
undefined,
|
||||
body,
|
||||
);
|
||||
let grounded = velocity.y <= 0.5 && isWalkableGroundHit(groundHit, groundProbeLength);
|
||||
const groundedBeforeMove = verticalVelocityRef.current <= 0.5
|
||||
&& isWalkableGroundHit(groundHit, groundProbeLength);
|
||||
const requestedAtMs = consumeJumpRequest();
|
||||
const gameplayBlocked = paused || mapOpen || companionOpen || controlledUntil > Date.now();
|
||||
const gameplayBlocked = paused
|
||||
|| mapOpen
|
||||
|| companionOpen
|
||||
|| useCombatStore.getState().health <= 0
|
||||
|| controlledUntil > Date.now();
|
||||
if (gameplayBlocked) {
|
||||
cancelBufferedPlayerJump(jumpTimingRef.current);
|
||||
movingRef.current = false;
|
||||
verticalMotionRef.current = updateCharacterVerticalPresentation(
|
||||
verticalPresentationTimingRef.current,
|
||||
grounded,
|
||||
velocity.y,
|
||||
groundedBeforeMove,
|
||||
verticalVelocityRef.current,
|
||||
nowMs,
|
||||
false,
|
||||
);
|
||||
body.setLinvel({ x: 0, y: velocity.y, z: 0 }, true);
|
||||
body.setNextKinematicTranslation(translation);
|
||||
return;
|
||||
}
|
||||
if (requestedAtMs !== null) bufferPlayerJump(jumpTimingRef.current, requestedAtMs);
|
||||
@@ -202,11 +227,42 @@ function PlayerController({
|
||||
const shouldJump = updatePlayerJumpTiming(
|
||||
jumpTimingRef.current,
|
||||
nowMs,
|
||||
grounded,
|
||||
velocity.y,
|
||||
groundedBeforeMove,
|
||||
verticalVelocityRef.current,
|
||||
);
|
||||
const verticalVelocity = shouldJump ? PLAYER_JUMP_VELOCITY : velocity.y;
|
||||
if (shouldJump) grounded = false;
|
||||
const movementDeltaSeconds = playerFrameDeltaSeconds(delta);
|
||||
let verticalVelocity = shouldJump
|
||||
? PLAYER_JUMP_VELOCITY
|
||||
: groundedBeforeMove
|
||||
? PLAYER_GROUND_STICK_VELOCITY
|
||||
: airbornePlayerVerticalVelocity(verticalVelocityRef.current, movementDeltaSeconds);
|
||||
|
||||
const [worldX, worldZ] = cameraRelativeMovement(
|
||||
snapshot.moveX,
|
||||
snapshot.moveForward,
|
||||
orbit.yaw,
|
||||
);
|
||||
if (body.numColliders() === 0) return;
|
||||
characterController.computeColliderMovement(
|
||||
body.collider(0),
|
||||
{
|
||||
x: worldX * MOVE_SPEED * movementDeltaSeconds,
|
||||
y: verticalVelocity * movementDeltaSeconds,
|
||||
z: worldZ * MOVE_SPEED * movementDeltaSeconds,
|
||||
},
|
||||
rapier.QueryFilterFlags.EXCLUDE_SENSORS | rapier.QueryFilterFlags.EXCLUDE_DYNAMIC,
|
||||
);
|
||||
const computedMovement = characterController.computedMovement();
|
||||
const nextTranslation = {
|
||||
x: translation.x + computedMovement.x,
|
||||
y: translation.y + computedMovement.y,
|
||||
z: translation.z + computedMovement.z,
|
||||
};
|
||||
body.setNextKinematicTranslation(nextTranslation);
|
||||
|
||||
const grounded = !shouldJump && characterController.computedGrounded();
|
||||
if (grounded && verticalVelocity <= 0) verticalVelocity = 0;
|
||||
verticalVelocityRef.current = verticalVelocity;
|
||||
verticalMotionRef.current = updateCharacterVerticalPresentation(
|
||||
verticalPresentationTimingRef.current,
|
||||
grounded,
|
||||
@@ -215,13 +271,6 @@ function PlayerController({
|
||||
shouldJump,
|
||||
);
|
||||
|
||||
const [worldX, worldZ] = cameraRelativeMovement(
|
||||
snapshot.moveX,
|
||||
snapshot.moveForward,
|
||||
orbit.yaw,
|
||||
);
|
||||
body.setLinvel({ x: worldX * MOVE_SPEED, y: verticalVelocity, z: worldZ * MOVE_SPEED }, true);
|
||||
|
||||
const moving = Math.hypot(worldX, worldZ) > 0.05;
|
||||
movingRef.current = moving;
|
||||
if (avatarRef.current && moving) {
|
||||
@@ -235,11 +284,11 @@ function PlayerController({
|
||||
: null;
|
||||
const belowRecoveryPlane = activeManastormPackage
|
||||
? isBelowManastormRecoveryPlane(
|
||||
translation.y,
|
||||
nextTranslation.y,
|
||||
activeManastormPackage.anchors.map((anchor) => anchor.position[1]),
|
||||
dungeon.bounds,
|
||||
)
|
||||
: isBelowDungeonRecoveryPlane(translation.y, dungeon.bounds);
|
||||
: isBelowDungeonRecoveryPlane(nextTranslation.y, dungeon.bounds);
|
||||
if (belowRecoveryPlane) {
|
||||
useGameStore.getState().resetAtActiveSpawn();
|
||||
return;
|
||||
@@ -249,9 +298,9 @@ function PlayerController({
|
||||
if (reportAccumulator.current >= 0.1) {
|
||||
reportAccumulator.current = 0;
|
||||
const footPosition: [number, number, number] = [
|
||||
translation.x,
|
||||
translation.y - PLAYER_CENTER_HEIGHT,
|
||||
translation.z,
|
||||
nextTranslation.x,
|
||||
nextTranslation.y - PLAYER_CENTER_HEIGHT,
|
||||
nextTranslation.z,
|
||||
];
|
||||
const distanceDelta = planarDistance(lastReported.current, footPosition);
|
||||
useGameStore.getState().updatePlayerSnapshot({
|
||||
@@ -268,7 +317,7 @@ function PlayerController({
|
||||
<RigidBody
|
||||
ref={bodyRef}
|
||||
name="player-capsule"
|
||||
type="dynamic"
|
||||
type="kinematicPosition"
|
||||
colliders={false}
|
||||
canSleep={false}
|
||||
enabledRotations={[false, false, false]}
|
||||
|
||||
Reference in new issue
Block a user