Release Healer Man 0.1.34

This commit is contained in:
phenom committed 2026-09-12 15:14:56 -04:00
1 parent 05b25c4341
commit 46c272f889
19 files changed
+568 -121

No files matched your search

+24 -100
View File
@@ -14,7 +14,7 @@ import {
type ErrorInfo,
type ReactNode,
} from "react";
import { Quaternion, type Group } from "three";
import { type Group } from "three";
import { classById, raceById, type GenderId, type RaceId } from "../app/characterCatalog";
import { CharacterModel } from "../avatar/CharacterModel";
import { ProceduralAvatar } from "../avatar/ProceduralAvatar";
@@ -25,7 +25,7 @@ import { resolveDungeonAssets, resolveManastormStageAssets } from "../game/dunge
import { activeManastormStageAssetPackage } from "../game/manastormStageLoader";
import { derivePartySpawnSlots } from "../game/partySpawnSlots";
import { requireDungeonDefinition } from "../game/dungeonRegistry";
import type { DungeonBossObjective } from "../game/dungeonTypes";
import type { DungeonBossObjective, DungeonTraversalRamp } from "../game/dungeonTypes";
import { getMobPosition } from "../game/mobRuntimeRegistry";
import {
appendPartyBreadcrumb,
@@ -84,20 +84,19 @@ import { registerCombatSpatialProvider } from "../game/combatSpatial";
import { PLAYER_AGGRO_ID } from "../game/aggro";
import { hasDungeonLineOfSight } from "./dungeonLineOfSight";
import { registerCombatEffectAnchor } from "./combatEffectAnchorRegistry";
import {
createPartyDirectSegmentValidator,
createPartyRouteSegmentValidator,
PARTY_FLOOR_RAY_DEPTH,
PARTY_FLOOR_RAY_HEIGHT,
partyTerrainQueryFlags,
} from "./partyTraversal";
const PARTY_MOVE_SPEED = 5.25;
const PARTY_CATCH_UP_SPEED = 7.5;
const BREADCRUMB_REACHED_DISTANCE = 0.04;
const PARTY_COLLISION_RADIUS = 0.34;
const PARTY_COLLISION_HALF_HEIGHT = 0.48;
const PARTY_COLLISION_CENTER_HEIGHT = PARTY_COLLISION_RADIUS + PARTY_COLLISION_HALF_HEIGHT + 0.04;
const PARTY_WALKABLE_STEP_HEIGHT = 0.32;
const PARTY_OBJECTIVE_APPROACH_SNAP_DISTANCE = 15;
const PARTY_NAVIGATION_SNAP_DISTANCE = 6;
const PARTY_FLOOR_SAMPLE_SPACING = 0.6;
const PARTY_FLOOR_RAY_HEIGHT = 1.25;
const PARTY_FLOOR_RAY_DEPTH = 2;
const PARTY_MAX_FLOOR_DELTA = 0.55;
const PARTY_MAX_GROUND_CORRECTION = 0.85;
const EMPTY_NAVIGATION_LINKS = [] as const;
@@ -201,16 +200,18 @@ class NavigationAssetBoundary extends Component<NavigationAssetBoundaryProps, Na
function PartyNavigationLoader({
url,
offMeshLinks,
ramps,
onReady,
}: {
readonly url: string;
readonly offMeshLinks: ReturnType<typeof requireDungeonDefinition>["navigationLinks"];
readonly ramps: readonly DungeonTraversalRamp[];
readonly onReady: (graph: PartyNavigationGraph) => void;
}) {
const gltf = useGLTF(resolveContentUrl(url));
const graph = useMemo(
() => preparePartyNavigationGraph(url, gltf.scene, offMeshLinks),
[gltf.scene, offMeshLinks, url],
() => preparePartyNavigationGraph(url, gltf.scene, offMeshLinks, ramps),
[gltf.scene, offMeshLinks, ramps, url],
);
useEffect(() => onReady(graph), [graph, onReady]);
@@ -289,16 +290,6 @@ function PartyActors({
);
const resetRevision = useGameStore((state) => state.resetRevision);
const { rapier, world } = useRapier();
const navigationCapsule = useMemo(
// Trim the lower leg band while keeping the capsule top at normal head
// height. This supplies step clearance without demanding extra ceiling.
() => new rapier.Capsule(
PARTY_COLLISION_HALF_HEIGHT - PARTY_WALKABLE_STEP_HEIGHT * 0.5,
PARTY_COLLISION_RADIUS,
),
[rapier],
);
const navigationRotation = useMemo(() => new Quaternion(), []);
const presentationY = useCallback((position: PartyWorldPosition): number => {
const ray = new rapier.Ray({
x: position[0],
@@ -309,92 +300,23 @@ function PartyActors({
ray,
PARTY_FLOOR_RAY_DEPTH,
true,
rapier.QueryFilterFlags.EXCLUDE_SENSORS | rapier.QueryFilterFlags.EXCLUDE_DYNAMIC,
partyTerrainQueryFlags(rapier),
);
const floorY = floor ? ray.origin.y - floor.timeOfImpact : null;
return partyPresentationY(position[1], floorY, PARTY_MAX_GROUND_CORRECTION);
}, [rapier, world]);
const directSegmentAllowed = useCallback((
start: PartyWorldPosition,
end: PartyWorldPosition,
): boolean => {
const dx = end[0] - start[0];
const dy = end[1] - start[1];
const dz = end[2] - start[2];
const distance = Math.hypot(dx, dy, dz);
if (!Number.isFinite(distance)) return false;
const planarDistance = Math.hypot(dx, dz);
// Collision and navigation surfaces commonly disagree by a few centimeters.
// Treat a same-column height correction as grounding, not a blocked move.
if (planarDistance <= 0.04) {
return Math.abs(dy) <= PARTY_WALKABLE_STEP_HEIGHT;
}
const queryFlags = rapier.QueryFilterFlags.EXCLUDE_SENSORS
| rapier.QueryFilterFlags.EXCLUDE_DYNAMIC;
const obstacle = world.castShape(
{
x: start[0],
// Raise only the trimmed lower band over walkable relief. The floor
// samples below still reject real ledges and excessive elevation.
y: start[1] + PARTY_COLLISION_CENTER_HEIGHT + PARTY_WALKABLE_STEP_HEIGHT * 0.5,
z: start[2],
},
navigationRotation,
{ x: dx / distance, y: dy / distance, z: dz / distance },
navigationCapsule,
0.025,
distance,
false,
queryFlags,
);
if (obstacle && obstacle.time_of_impact < distance - 0.04) return false;
// A clear chest-height cast is insufficient across a ledge. Require the
// collision floor to remain close to the planned segment at short samples.
const sampleCount = Math.max(1, Math.ceil(distance / PARTY_FLOOR_SAMPLE_SPACING));
for (let sample = 0; sample <= sampleCount; sample += 1) {
const progress = sample / sampleCount;
const expectedY = start[1] + dy * progress;
const ray = new rapier.Ray({
x: start[0] + dx * progress,
y: expectedY + PARTY_FLOOR_RAY_HEIGHT,
z: start[2] + dz * progress,
}, { x: 0, y: -1, z: 0 });
const floor = world.castRay(ray, PARTY_FLOOR_RAY_DEPTH, true, queryFlags);
if (!floor) return false;
const floorY = ray.origin.y - floor.timeOfImpact;
if (Math.abs(floorY - expectedY) > PARTY_MAX_FLOOR_DELTA) return false;
}
return true;
}, [navigationCapsule, navigationRotation, rapier, world]);
const directSegmentAllowed = useMemo(
() => createPartyDirectSegmentValidator(rapier, world),
[rapier, world],
);
const hasStaticLineOfSight = useCallback((
start: PartyWorldPosition,
end: PartyWorldPosition,
): boolean => hasDungeonLineOfSight(rapier, world, start, end), [rapier, world]);
const routeSurfaceSegmentAllowed = useCallback((
start: PartyWorldPosition,
end: PartyWorldPosition,
): boolean => {
const dx = end[0] - start[0];
const dy = end[1] - start[1];
const dz = end[2] - start[2];
const distance = Math.hypot(dx, dy, dz);
if (!Number.isFinite(distance)) return false;
if (distance <= 0.04) return true;
const queryFlags = rapier.QueryFilterFlags.EXCLUDE_SENSORS
| rapier.QueryFilterFlags.EXCLUDE_DYNAMIC;
// Portal-centered paths already stay inside walkable triangles. One
// body-center ray catches a true wall/overhang without treating the floor
// or a tessellation edge as missing agent-radius clearance. Failed
// triangles are excluded and A* searches another corridor.
const ray = new rapier.Ray({
x: start[0],
y: start[1] + 0.9,
z: start[2],
}, { x: dx / distance, y: dy / distance, z: dz / distance });
const hit = world.castRay(ray, distance, true, queryFlags);
return !hit || hit.timeOfImpact >= distance - 0.04;
}, [rapier, world]);
const routeSurfaceSegmentAllowed = useMemo(
() => createPartyRouteSegmentValidator(rapier, world),
[rapier, world],
);
// Authored-link graphs use their eroded Recast node edges directly. Their
// boundary vertices are already agent-radius safe, while a center ray can
// falsely read the stair floor as a wall across a short vertical seam.
@@ -1542,6 +1464,7 @@ export function PartyPopulation({
const navigationEnabled = Boolean(assets.navigation)
&& shouldLoadPartyNavigation(gameMode, dungeon, manastormPackage);
const navigationLinks = manastormPackage ? EMPTY_NAVIGATION_LINKS : dungeon.navigationLinks;
const traversalRamps = manastormPackage ? EMPTY_NAVIGATION_LINKS : dungeon.traversalRamps ?? EMPTY_NAVIGATION_LINKS;
const [navigationState, setNavigationState] = useState<{
readonly url: string;
readonly graph: PartyNavigationGraph;
@@ -1589,6 +1512,7 @@ export function PartyPopulation({
<PartyNavigationLoader
url={assets.navigation}
offMeshLinks={navigationLinks}
ramps={traversalRamps}
onReady={acceptNavigationGraph}
/>
</Suspense>