Release Healer Man 0.1.46
This commit is contained in:
1 parent
f82daf1c13
commit
9ff574ac0b
73 files changed
+33486
-174
No files matched your search
+105
-33
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user