Release Healer Man 0.1.33
This commit is contained in:
1 parent
6dcb1034e5
commit
05b25c4341
20 files changed
+573
-51
No files matched your search
+156
-25
@@ -92,7 +92,8 @@ 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 = 6;
|
||||
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;
|
||||
@@ -394,6 +395,12 @@ function PartyActors({
|
||||
const hit = world.castRay(ray, distance, true, queryFlags);
|
||||
return !hit || hit.timeOfImpact >= distance - 0.04;
|
||||
}, [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.
|
||||
const navigationRouteSegmentAllowed = navigationGraph?.triangles?.length
|
||||
? routeSurfaceSegmentAllowed
|
||||
: undefined;
|
||||
const actorRefs = useRef<Array<Group | null>>([]);
|
||||
const bodyRefs = useRef<Array<Group | null>>([]);
|
||||
const positionsRef = useRef(new Map<string, MutablePartyWorldPosition>());
|
||||
@@ -472,14 +479,22 @@ function PartyActors({
|
||||
}), [hasStaticLineOfSight, memberIdKey, rapier, world]);
|
||||
const teleportPartyToPlayer = useCallback((player: PartyWorldPosition, now: number): void => {
|
||||
const party = usePartyStore.getState();
|
||||
const navigationRecall = navigationGraphRef.current
|
||||
? findNearestPartyNavigationSurface(
|
||||
navigationGraphRef.current,
|
||||
player,
|
||||
PARTY_NAVIGATION_SNAP_DISTANCE,
|
||||
)
|
||||
: null;
|
||||
const recallPosition = navigationRecall?.position ?? player;
|
||||
party.members.forEach((member, index) => {
|
||||
const position = positionsRef.current.get(member.id) ?? initialMemberPosition(index);
|
||||
// The player's exact point is the only location guaranteed to be on the
|
||||
// same side of a shortcut, doorway, or ledge. The breadcrumb system
|
||||
// spreads the party back into a trail once the player moves.
|
||||
position[0] = player[0];
|
||||
position[1] = player[1];
|
||||
position[2] = player[2];
|
||||
// Attach a recalled squad to the reviewed surface just as initial
|
||||
// spawns do. An exact visual-floor point can sit between Recast stair
|
||||
// vertices and leave the first route connector falsely blocked.
|
||||
position[0] = recallPosition[0];
|
||||
position[1] = recallPosition[1];
|
||||
position[2] = recallPosition[2];
|
||||
positionsRef.current.set(member.id, position);
|
||||
updatePartyRuntimePosition(member.id, position[0], position[1], position[2]);
|
||||
actorRefs.current[index]?.position.set(position[0], presentationY(position), position[2]);
|
||||
@@ -495,7 +510,7 @@ function PartyActors({
|
||||
const firstLiving = party.members.find((member) => member.health > 0);
|
||||
resetPartyBreadcrumbTrail(playerTrailRef.current, player);
|
||||
leaderPlayerCursorRef.current.breadcrumbId = null;
|
||||
resetPartyBreadcrumbTrail(leaderTrailRef.current, player);
|
||||
resetPartyBreadcrumbTrail(leaderTrailRef.current, recallPosition);
|
||||
trailLeaderIdRef.current = firstLiving?.id ?? null;
|
||||
routeRef.current = emptyPartyRouteState();
|
||||
combatRoutesRef.current.clear();
|
||||
@@ -756,12 +771,14 @@ function PartyActors({
|
||||
const leader = leaderMember;
|
||||
let goalKey: string;
|
||||
let hasDestination = true;
|
||||
let leaderLocalFinishDistance = 3;
|
||||
if (targetPosition && leaderTargetId) {
|
||||
const profile = movementProfileFor(leader.id, leaderIndex);
|
||||
const rangeBand = personalizedPartyCombatRangeBand(
|
||||
partyCombatRangeBand(leader),
|
||||
profile,
|
||||
);
|
||||
leaderLocalFinishDistance = Math.max(leaderLocalFinishDistance, rangeBand.maximum);
|
||||
let combatAnchor = combatAnchorsRef.current.get(leader.id);
|
||||
if (!combatAnchor || combatAnchor.targetId !== leaderTargetId) {
|
||||
combatAnchor = {
|
||||
@@ -817,7 +834,6 @@ function PartyActors({
|
||||
&& fullDistance(leaderPosition, leaderDestination) > 0.18;
|
||||
|
||||
if (leaderFollowingPlayer) {
|
||||
routeRef.current = { ...emptyPartyRouteState(), goalKey };
|
||||
const cursor = leaderPlayerCursorRef.current;
|
||||
const profile = movementProfileFor(leader.id, leaderIndex);
|
||||
const baseTrailingDistance = PARTY_PLAYER_TRAILING_DISTANCE[0];
|
||||
@@ -835,6 +851,7 @@ function PartyActors({
|
||||
leaderDestination,
|
||||
);
|
||||
if (directSegmentAllowed(leaderPosition, leaderDestination)) {
|
||||
routeRef.current = { ...emptyPartyRouteState(), goalKey };
|
||||
const wasMoving = directFollowMovingRef.current.get(leader.id) ?? false;
|
||||
leaderHasWaypoint = partyShouldMoveToComfortTarget(
|
||||
leaderPosition,
|
||||
@@ -877,9 +894,63 @@ function PartyActors({
|
||||
leaderHasWaypoint = false;
|
||||
leaderPlayerTrailBlocked = true;
|
||||
}
|
||||
if (!leaderHasWaypoint && navigationGraph) {
|
||||
const navigationFollowDestination = findNearestPartyNavigationSurface(
|
||||
navigationGraph,
|
||||
leaderDestination,
|
||||
PARTY_NAVIGATION_SNAP_DISTANCE,
|
||||
)?.position ?? leaderDestination;
|
||||
if (partyRouteNeedsPlanning(
|
||||
routeRef.current,
|
||||
goalKey,
|
||||
leaderPosition,
|
||||
navigationFollowDestination,
|
||||
routeNow,
|
||||
{
|
||||
replanDistance: 1.25,
|
||||
replanIntervalMs: 350,
|
||||
blockedRetryMs: 800,
|
||||
},
|
||||
)) {
|
||||
routeRef.current = planPartyRoute(
|
||||
navigationGraph,
|
||||
goalKey,
|
||||
leaderPosition,
|
||||
navigationFollowDestination,
|
||||
routeNow,
|
||||
{
|
||||
maxOriginSnapDistance: PARTY_NAVIGATION_SNAP_DISTANCE,
|
||||
maxDestinationSnapDistance: 5,
|
||||
localFinishDistance: 3,
|
||||
replanDistance: 1.25,
|
||||
replanIntervalMs: 350,
|
||||
blockedRetryMs: 800,
|
||||
directSegmentAllowed,
|
||||
routeSegmentAllowed: navigationRouteSegmentAllowed,
|
||||
},
|
||||
);
|
||||
}
|
||||
leaderHasWaypoint = nextPartyRouteWaypoint(
|
||||
routeRef.current,
|
||||
leaderPosition,
|
||||
desired,
|
||||
);
|
||||
if (!leaderHasWaypoint && routeRef.current.blocked) {
|
||||
leaderHasWaypoint = nextPartyRouteRecoveryWaypoint(
|
||||
routeRef.current,
|
||||
leaderPosition,
|
||||
desired,
|
||||
directSegmentAllowed,
|
||||
);
|
||||
}
|
||||
leaderPlayerTrailBlocked = !leaderHasWaypoint && routeRef.current.blocked;
|
||||
}
|
||||
leaderMovementExpected = leaderHasWaypoint
|
||||
|| planarDistance(leaderPosition, playerNavigation)
|
||||
> personalizedTrailingDistance + profile.comfortSlack;
|
||||
|| (
|
||||
(!navigationGraph || leaderPlayerTrailBlocked)
|
||||
&& planarDistance(leaderPosition, playerNavigation)
|
||||
> personalizedTrailingDistance + profile.comfortSlack
|
||||
);
|
||||
}
|
||||
} else if (!hasDestination || fullDistance(leaderPosition, leaderDestination) <= 0.18) {
|
||||
if (
|
||||
@@ -902,11 +973,11 @@ function PartyActors({
|
||||
leaderDestination,
|
||||
routeNow,
|
||||
{
|
||||
maxOriginSnapDistance: 3,
|
||||
maxOriginSnapDistance: PARTY_NAVIGATION_SNAP_DISTANCE,
|
||||
maxDestinationSnapDistance: 5,
|
||||
localFinishDistance: 3,
|
||||
localFinishDistance: leaderLocalFinishDistance,
|
||||
directSegmentAllowed,
|
||||
routeSegmentAllowed: routeSurfaceSegmentAllowed,
|
||||
routeSegmentAllowed: navigationRouteSegmentAllowed,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1082,14 +1153,14 @@ function PartyActors({
|
||||
desired,
|
||||
routeNow,
|
||||
{
|
||||
maxOriginSnapDistance: 3,
|
||||
maxOriginSnapDistance: PARTY_NAVIGATION_SNAP_DISTANCE,
|
||||
maxDestinationSnapDistance: 5,
|
||||
localFinishDistance: 3,
|
||||
localFinishDistance: Math.max(3, rangeBand.maximum),
|
||||
replanDistance: 1.25,
|
||||
replanIntervalMs: 350,
|
||||
blockedRetryMs: 800,
|
||||
directSegmentAllowed,
|
||||
routeSegmentAllowed: routeSurfaceSegmentAllowed,
|
||||
routeSegmentAllowed: navigationRouteSegmentAllowed,
|
||||
},
|
||||
);
|
||||
combatRoutesRef.current.set(member.id, route);
|
||||
@@ -1112,7 +1183,6 @@ function PartyActors({
|
||||
}
|
||||
}
|
||||
} else {
|
||||
combatRoutesRef.current.delete(member.id);
|
||||
let cursor = followerCursorsRef.current.get(member.id);
|
||||
if (!cursor) {
|
||||
cursor = createPartyBreadcrumbCursor();
|
||||
@@ -1137,6 +1207,7 @@ function PartyActors({
|
||||
naturalFollowTarget,
|
||||
);
|
||||
if (directSegmentAllowed(current, naturalFollowTarget)) {
|
||||
combatRoutesRef.current.delete(member.id);
|
||||
const wasMoving = directFollowMovingRef.current.get(member.id) ?? false;
|
||||
hasDesired = partyShouldMoveToComfortTarget(
|
||||
current,
|
||||
@@ -1158,6 +1229,7 @@ function PartyActors({
|
||||
movementExpected = hasDesired;
|
||||
} else {
|
||||
directFollowMovingRef.current.set(member.id, false);
|
||||
let followerTrailBlocked = false;
|
||||
const previousBreadcrumbId = cursor.breadcrumbId;
|
||||
const requiresValidatedRejoin = cursor.breadcrumbId === null
|
||||
|| !leaderTrailRef.current.points.some((point) => point.id === cursor!.breadcrumbId);
|
||||
@@ -1177,11 +1249,68 @@ function PartyActors({
|
||||
) {
|
||||
cursor.breadcrumbId = previousBreadcrumbId;
|
||||
hasDesired = false;
|
||||
frameRouteBlocked = true;
|
||||
followerTrailBlocked = true;
|
||||
}
|
||||
if (!hasDesired && navigationGraph) {
|
||||
const goalKey = `member:${member.id}|leader-follow`;
|
||||
const navigationFollowDestination = findNearestPartyNavigationSurface(
|
||||
navigationGraph,
|
||||
naturalFollowTarget,
|
||||
PARTY_NAVIGATION_SNAP_DISTANCE,
|
||||
)?.position ?? naturalFollowTarget;
|
||||
let route = combatRoutesRef.current.get(member.id) ?? emptyPartyRouteState();
|
||||
if (partyRouteNeedsPlanning(
|
||||
route,
|
||||
goalKey,
|
||||
current,
|
||||
navigationFollowDestination,
|
||||
routeNow,
|
||||
{
|
||||
replanDistance: 1.25,
|
||||
replanIntervalMs: 350,
|
||||
blockedRetryMs: 800,
|
||||
},
|
||||
)) {
|
||||
route = planPartyRoute(
|
||||
navigationGraph,
|
||||
goalKey,
|
||||
current,
|
||||
navigationFollowDestination,
|
||||
routeNow,
|
||||
{
|
||||
maxOriginSnapDistance: PARTY_NAVIGATION_SNAP_DISTANCE,
|
||||
maxDestinationSnapDistance: 5,
|
||||
localFinishDistance: 3,
|
||||
replanDistance: 1.25,
|
||||
replanIntervalMs: 350,
|
||||
blockedRetryMs: 800,
|
||||
directSegmentAllowed,
|
||||
routeSegmentAllowed: navigationRouteSegmentAllowed,
|
||||
},
|
||||
);
|
||||
combatRoutesRef.current.set(member.id, route);
|
||||
}
|
||||
hasDesired = nextPartyRouteWaypoint(route, current, desired);
|
||||
const routePrefixRecovery = !hasDesired && route.blocked
|
||||
? nextPartyRouteRecoveryWaypoint(
|
||||
route,
|
||||
current,
|
||||
desired,
|
||||
directSegmentAllowed,
|
||||
)
|
||||
: false;
|
||||
if (routePrefixRecovery) hasDesired = true;
|
||||
followerTrailBlocked = !hasDesired && route.blocked;
|
||||
} else if (hasDesired) {
|
||||
combatRoutesRef.current.delete(member.id);
|
||||
}
|
||||
if (followerTrailBlocked) frameRouteBlocked = true;
|
||||
movementExpected = hasDesired
|
||||
|| planarDistance(current, leaderPosition ?? playerNavigation)
|
||||
> leaderTrailingDistance + profile.comfortSlack;
|
||||
|| (
|
||||
(!navigationGraph || followerTrailBlocked)
|
||||
&& planarDistance(current, leaderPosition ?? playerNavigation)
|
||||
> leaderTrailingDistance + profile.comfortSlack
|
||||
);
|
||||
}
|
||||
}
|
||||
if (memberTargetPosition) {
|
||||
@@ -1382,6 +1511,8 @@ export function PartyPopulation({
|
||||
const wailingPhase = useWailingEncounterStore((state) => state.phase);
|
||||
const manastormEncounter = useManastormStore((state) => state.currentEncounter);
|
||||
const dungeon = requireDungeonDefinition(activeDungeonId);
|
||||
const navigationReviewed = dungeon.validation.status === "green"
|
||||
|| dungeon.validation.navigation === "green";
|
||||
const manastormPackage = gameMode === "manastorm"
|
||||
? activeManastormStageAssetPackage()
|
||||
: null;
|
||||
@@ -1393,9 +1524,9 @@ export function PartyPopulation({
|
||||
}]
|
||||
// Generated packages preserve complete objective metadata, but their
|
||||
// fragmented Recast islands do not claim autonomous route fidelity. Until
|
||||
// reviewed off-mesh links make the pack green, the player leads and the
|
||||
// party follows/engages locally instead of displaying a false blocked route.
|
||||
: dungeon.validation.status === "green"
|
||||
// reviewed links make navigation green, the player leads and the party
|
||||
// follows/engages locally instead of displaying a false blocked route.
|
||||
: navigationReviewed
|
||||
? activeDungeonId === "wailing-caverns"
|
||||
&& wailingPhase !== "mutanus"
|
||||
&& wailingPhase !== "complete"
|
||||
@@ -1450,7 +1581,7 @@ export function PartyPopulation({
|
||||
navigationGraph={navigationGraph}
|
||||
navigationFailed={navigationFailed || !navigationEnabled}
|
||||
objectives={partyObjectivesForNavigation(Boolean(navigationGraph), objectives)}
|
||||
stackAtEntrance={gameMode === "manastorm" || dungeon.validation.status !== "green"}
|
||||
stackAtEntrance={gameMode === "manastorm" || !navigationReviewed}
|
||||
/>
|
||||
{navigationEnabled && assets.navigation ? (
|
||||
<NavigationAssetBoundary key={assets.navigation} onFailure={rejectNavigationGraph}>
|
||||
|
||||
Reference in new issue
Block a user