Release Healer Man 0.1.46

This commit is contained in:
phenom committed 2026-09-16 14:30:58 -04:00
1 parent f82daf1c13
commit 9ff574ac0b
73 files changed
+33486 -174

No files matched your search

+105 -33
View File
@@ -106,6 +106,13 @@ import {
PARTY_FLOOR_RAY_DEPTH,
PARTY_FLOOR_RAY_HEIGHT,
} from "./partyTraversal";
import {
activeDungeonRouteTarget,
routeForActiveDungeon,
type ActiveDungeonRouteTarget,
} from "../game/dungeonRouteRuntime";
import { useDungeonSimulationStore } from "../game/dungeonSimulationStore";
import { useDungeonRouteStore } from "../game/dungeonRouteStore";
const PARTY_MOVE_SPEED = 5.25;
const PARTY_CATCH_UP_SPEED = 7.5;
@@ -363,6 +370,7 @@ function PartyActors({
const movementProfilesRef = useRef(new Map<string, PartyMovementProfile>());
const directFollowMovingRef = useRef(new Map<string, boolean>());
const combatAnchorsRef = useRef(new Map<string, PartyCombatAnchor>());
const lastSimulationElapsedRef = useRef(0);
const memberIdKey = members.map((member) => member.id).join("|");
useEffect(() => registerCombatSpatialProvider({
actor: (actorId) => {
@@ -611,10 +619,15 @@ function PartyActors({
return;
}
const game = useGameStore.getState();
const simulation = useDungeonSimulationStore.getState();
const playerNavigation = game.playerNavigationPosition;
appendPartyBreadcrumb(playerTrailRef.current, playerNavigation);
const routeNow = clock.elapsedTime * 1_000;
const combatNow = Date.now();
const routeNow = simulation.sessionMode === "spectate-live"
? simulation.elapsedMs
: clock.elapsedTime * 1_000;
const combatNow = simulation.sessionMode === "spectate-live"
? simulation.startedAt + simulation.elapsedMs
: Date.now();
for (const member of party.members) {
if (member.health <= 0) {
finishPartyRuntimeRejoin(member.id);
@@ -673,7 +686,13 @@ function PartyActors({
}
const objective = objectives.find((candidate) => candidate.id === party.objectiveBossId) ?? null;
const leaderIndex = party.members.findIndex((member) => member.health > 0 && !partyRuntimeIsRejoining(member.id));
const safeDelta = Math.min(delta, 0.05);
const simulationDelta = simulation.sessionMode === "spectate-live"
? Math.max(0, (simulation.elapsedMs - lastSimulationElapsedRef.current) / 1_000)
: delta;
lastSimulationElapsedRef.current = simulation.elapsedMs;
const safeDelta = simulation.sessionMode === "spectate-live"
? Math.min(simulationDelta, 0.5)
: Math.min(delta, 0.05);
const desired: MutablePartyWorldPosition = [0, 0, 0];
const nextPosition: MutablePartyWorldPosition = [0, 0, 0];
const leaderDestination: MutablePartyWorldPosition = [0, 0, 0];
@@ -696,6 +715,7 @@ function PartyActors({
let leaderFollowingPlayer = false;
let leaderPlayerTrailBlocked = false;
let forceLeaderBreadcrumb = false;
let authoredRouteTarget: ActiveDungeonRouteTarget | null = null;
const leaderMember = leaderIndex >= 0 ? party.members[leaderIndex] : null;
const leaderPosition = leaderMember ? positionsRef.current.get(leaderMember.id) : null;
@@ -763,18 +783,28 @@ function PartyActors({
// Imported source spawns can sit on decorative or discarded Recast
// islands. Route to the nearest retained walkable surface and let the
// normal aggro/combat path take over when the encounter engages.
const objectiveApproach = navigationGraph
authoredRouteTarget = activeDungeonRouteTarget(
game.activeDungeonId,
objective.id,
leaderPosition,
leader.id,
combatNow,
);
const objectiveApproach = authoredRouteTarget?.target ?? (navigationGraph
? findNearestPartyNavigationSurface(
navigationGraph,
objective.position,
PARTY_OBJECTIVE_APPROACH_SNAP_DISTANCE,
)?.position
: null;
: null);
const objectiveDestination = objectiveApproach ?? objective.position;
leaderDestination[0] = objectiveDestination[0];
leaderDestination[1] = objectiveDestination[1];
leaderDestination[2] = objectiveDestination[2];
goalKey = `leader:${leader.id}|objective:${objective.id}`;
goalKey = authoredRouteTarget
? `leader:${leader.id}|route:${authoredRouteTarget.route.hash}|${authoredRouteTarget.leg.id}|${authoredRouteTarget.checkpointIndex}`
: `leader:${leader.id}|objective:${objective.id}`;
if (authoredRouteTarget) leaderLocalFinishDistance = 0.3;
} else {
leaderFollowingPlayer = true;
leaderDestination[0] = playerNavigation[0];
@@ -918,27 +948,38 @@ function PartyActors({
leaderDestination,
routeNow,
)) {
routeRef.current = planPartyRoute(
navigationGraph,
goalKey,
leaderPosition,
leaderDestination,
routeNow,
{
maxOriginSnapDistance: PARTY_NAVIGATION_SNAP_DISTANCE,
maxDestinationSnapDistance: 5,
localFinishDistance: leaderLocalFinishDistance,
directSegmentAllowed,
routeSegmentAllowed: navigationRouteSegmentAllowed,
destinationReached: targetPosition ? (surface) => {
const band = personalizedPartyCombatRangeBand(partyCombatRangeBand(leaderMember),
movementProfileFor(leaderMember.id, leaderIndex));
const distance = fullDistance(surface, targetPosition);
return distance >= band.minimum && distance <= band.maximum
&& hasStaticLineOfSight(surface, targetPosition);
} : undefined,
},
);
routeRef.current = authoredRouteTarget
&& !navigationGraph
&& directSegmentAllowed(leaderPosition, leaderDestination)
? {
goalKey,
destination: [leaderDestination[0], leaderDestination[1], leaderDestination[2]],
waypoints: [[leaderDestination[0], leaderDestination[1], leaderDestination[2]]],
cursor: 0,
plannedAt: routeNow,
blocked: false,
}
: planPartyRoute(
navigationGraph,
goalKey,
leaderPosition,
leaderDestination,
routeNow,
{
maxOriginSnapDistance: PARTY_NAVIGATION_SNAP_DISTANCE,
maxDestinationSnapDistance: 5,
localFinishDistance: leaderLocalFinishDistance,
directSegmentAllowed,
routeSegmentAllowed: navigationRouteSegmentAllowed,
destinationReached: targetPosition ? (surface) => {
const band = personalizedPartyCombatRangeBand(partyCombatRangeBand(leaderMember),
movementProfileFor(leaderMember.id, leaderIndex));
const distance = fullDistance(surface, targetPosition);
return distance >= band.minimum && distance <= band.maximum
&& hasStaticLineOfSight(surface, targetPosition);
} : undefined,
},
);
}
leaderHasWaypoint = nextPartyRouteWaypoint(routeRef.current, leaderPosition, desired);
if (!leaderHasWaypoint && routeRef.current.blocked) {
@@ -960,6 +1001,19 @@ function PartyActors({
desired[2] = leaderDestination[2];
leaderHasWaypoint = true;
}
if (
!leaderHasWaypoint
&& authoredRouteTarget
&& routeRef.current.blocked
&& simulation.sessionMode === "play"
&& directSegmentAllowed(leaderPosition, playerNavigation)
) {
desired[0] = playerNavigation[0];
desired[1] = playerNavigation[1];
desired[2] = playerNavigation[2];
leaderHasWaypoint = true;
leaderFollowingPlayer = true;
}
}
}
if (leaderHasWaypoint) {
@@ -980,12 +1034,12 @@ function PartyActors({
&& !targetPosition
&& leaderPosition
&& !routeRef.current.blocked
&& routeRef.current.waypoints.length > routeRef.current.cursor
&& (Boolean(authoredRouteTarget) || routeRef.current.waypoints.length > routeRef.current.cursor)
) {
updatePartyRuntimeAttackCorridor(
leaderPosition,
routeRef.current.waypoints,
routeRef.current.cursor,
authoredRouteTarget?.remaining ?? routeRef.current.waypoints,
authoredRouteTarget ? 0 : routeRef.current.cursor,
);
} else {
clearPartyRuntimeAttackCorridor();
@@ -998,7 +1052,7 @@ function PartyActors({
&& (
leaderFollowingPlayer
? leaderPlayerTrailBlocked
: navigationFailed || Boolean(navigationGraph && routeRef.current.blocked)
: navigationFailed || Boolean((navigationGraph || authoredRouteTarget) && routeRef.current.blocked)
),
);
@@ -1376,6 +1430,20 @@ function PartyActors({
trailPoints: [...leaderTrailRef.current.points, ...playerTrailRef.current.points].map((point) => point.position),
validate: recoveryPointAllowed,
});
if (landing && simulation.sessionMode === "spectate-live") {
simulation.addFinding({
id: `recovery:${member.id}:${simulation.elapsedMs}`,
atMs: simulation.elapsedMs,
severity: "critical",
code: "position-recovery",
message: `${member.name} required a position-only recovery teleport.`,
position: [current[0], current[1], current[2]],
actorId: member.id,
objectiveId: simulation.objectiveId ?? undefined,
checkpointIndex: simulation.checkpointIndex,
});
simulation.addRecovery();
}
if (landing && applyPartyStuckRecoveryLanding(positionsRef.current, member.id, landing.position, stuckProgress, routeNow)) {
updatePartyRuntimePosition(member.id, current[0], current[1], current[2]);
actor.position.set(current[0], presentationY(current), current[2]);
@@ -1504,6 +1572,8 @@ export function PartyPopulation({
const sessionRevision = useGameStore((state) => state.sessionRevision);
const wailingPhase = useWailingEncounterStore((state) => state.phase);
const manastormEncounter = useManastormStore((state) => state.currentEncounter);
const simulationSessionMode = useDungeonSimulationStore((state) => state.sessionMode);
useDungeonRouteStore((state) => state.approvals);
const dungeon = requireDungeonDefinition(activeDungeonId);
const navigationReviewed = dungeon.validation.status === "green"
|| dungeon.validation.navigation === "green";
@@ -1520,13 +1590,15 @@ export function PartyPopulation({
// fragmented Recast islands do not claim autonomous route fidelity. Until
// reviewed links make navigation green, the player leads and the party
// follows/engages locally instead of displaying a false blocked route.
: navigationReviewed
: simulationSessionMode === "spectate-live" || Boolean(routeForActiveDungeon(activeDungeonId))
? dungeon.objectiveOrder.filter((objective) => !objective.optional)
: navigationReviewed
? activeDungeonId === "wailing-caverns"
&& wailingPhase !== "mutanus"
&& wailingPhase !== "complete"
? dungeon.objectiveOrder.filter((objective) => !objective.eventOnly)
: dungeon.objectiveOrder.filter((objective) => !objective.optional)
: [];
: [];
const assets = useMemo(
() => manastormPackage
? resolveManastormStageAssets(manastormPackage)