Update Healer Man server

This commit is contained in:
phenom committed 2026-10-04 13:13:35 -04:00
1 parent 39458d5de6
commit 6cc1b9217f
203 files changed
+133929 -31271

No files matched your search

+8 -11
View File
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { useShallow } from "zustand/react/shallow";
import { usePresentationClock } from "./usePresentationClock";
import type { AuraDisposition } from "../game/combatAuras";
import { useCombatStore } from "../game/combatStore";
import { formatTimedEffectTimer } from "../game/timedEffectDisplay";
@@ -38,24 +39,20 @@ export function AuraStrip({
onInspect,
tooltipId,
}: AuraStripProps) {
const allAuras = useCombatStore((state) => state.auras);
const [now, setNow] = useState(() => Date.now());
const requestedTargetIds = targetIds ?? [targetId];
const auras = groupAurasForDisplay(allAuras.filter((aura) => (
const matchingAuras = useCombatStore(useShallow((state) => state.auras.filter((aura) => (
requestedTargetIds.includes(aura.targetId)
&& dispositions.includes(aura.definition.disposition)
&& auraDefinitionDisplaysInStrip(aura.definition)
&& (!hideLongTerm || !auraDefinitionIsLongTerm(aura.definition))
&& (!showOnlyLongTerm || auraDefinitionIsLongTerm(aura.definition))
&& (aura.expiresAt === null || aura.expiresAt > now)
))));
const hasTimedAuras = matchingAuras.some((aura) => aura.expiresAt !== null);
const now = usePresentationClock(hasTimedAuras);
const auras = groupAurasForDisplay(matchingAuras.filter((aura) => (
aura.expiresAt === null || aura.expiresAt > now
)));
useEffect(() => {
if (!auras.some((aura) => aura.expiresAt !== null)) return undefined;
const timer = window.setInterval(() => setNow(Date.now()), 100);
return () => window.clearInterval(timer);
}, [auras]);
if (!auras.length) return null;
return (
+6 -1
View File
@@ -1,6 +1,6 @@
import { Bounds, useBounds } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { useCallback } from "react";
import { useCallback, useMemo } from "react";
import type { CharacterDraft, CharacterProfile } from "../app/types";
import { CharacterModel } from "../avatar/CharacterModel";
import { ProceduralAvatar } from "../avatar/ProceduralAvatar";
@@ -11,6 +11,7 @@ import {
} from "../game/frameRateSettings";
import { useDocumentVisibility } from "../platform/useDocumentVisibility";
import { CappedFrameLoop } from "../scene/CappedFrameLoop";
import { equipmentItemsForOwner, PLAYER_EQUIPMENT_OWNER_ID } from "../game/equipment";
function FittedCharacter({
characterClassColor,
@@ -22,12 +23,16 @@ function FittedCharacter({
turnDegrees: number;
}) {
const bounds = useBounds();
const equipment = useMemo(() => "inventory" in source
? equipmentItemsForOwner(source.equipment, source.inventory, PLAYER_EQUIPMENT_OWNER_ID)
: [], [source]);
const refit = useCallback(() => {
bounds.refresh().clip().fit();
}, [bounds]);
return (
<CharacterModel
identity={source}
equipment={equipment}
onReady={refit}
preview
turnDegrees={turnDegrees}
+8 -2
View File
@@ -4,17 +4,23 @@ import { formatDungeonDuration } from "../game/dungeonRunTypes";
import { dungeonRaceAffix } from "../game/dungeonRaceAffixes";
import { raceAffixBoonUntil } from "../game/dungeonRaceAffixBuffs";
import { useGameStore } from "../game/store";
import { useDocumentVisibility } from "../platform/useDocumentVisibility";
import "./dungeonRace.css";
export function DungeonRaceHud({ region }: { readonly region: "status" | "overlays" }) {
const race = useDungeonRaceStore();
const activeDungeonId = useGameStore((state) => state.activeDungeonId);
const documentVisible = useDocumentVisibility();
const raceVisible = race.inGame && race.opponent !== null && race.dungeonId === activeDungeonId;
const clockActive = raceVisible && documentVisible;
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
if (!clockActive) return;
setNow(Date.now());
const interval = window.setInterval(() => setNow(Date.now()), 100);
return () => window.clearInterval(interval);
}, []);
if (!race.inGame || !race.opponent || race.dungeonId !== activeDungeonId) return null;
}, [clockActive]);
if (!raceVisible || !race.opponent) return null;
const serverNow = now + race.serverOffset;
const remaining = race.startAt ? Math.max(0, Math.ceil((race.startAt - serverNow) / 1000)) : null;
const elapsed = race.startAt ? Math.max(0, serverNow - race.startAt) : 0;
+11 -5
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState, type CSSProperties } from "react";
import { classById, type ClassId, type GenderId, type RaceId } from "../app/characterCatalog";
import { classById, isRomClassId, type ClassId, type GenderId, type RaceId } from "../app/characterCatalog";
import { useShellStore } from "../app/shellStore";
import {
EQUIPMENT_SLOTS,
@@ -165,7 +165,7 @@ function characterRows(stats: CharacterStats, page: StatPage): readonly { label:
{ label: "Healing points", value: displayNumber(native.healing.healPoint) },
];
return [
{ label: "Physical defense (DEF)", value: `${displayNumber(native.physicalDefense)} · ${stats.armorReductionPercent.toFixed(2)}% shared armor rule` },
{ label: "Physical defense (DEF)", value: displayNumber(native.physicalDefense) },
{ label: "Magical defense (MDEF)", value: displayNumber(native.magicalDefense) },
{ label: "Physical dodge (points)", value: displayNumber(native.physicalDodge, 2) },
{ label: "Magical dodge (points)", value: displayNumber(native.magicalDodge, 2) },
@@ -291,6 +291,7 @@ function EquipmentDialog({ readOnly = false }: { readOnly?: boolean }) {
}, [inspectedId, inventory]);
const owner = owners.find((candidate) => candidate.id === ownerId) ?? owners[0];
const automaticGearLabel = isRomClassId(owner.classId) ? "Training gear" : "Heirloom";
const inspected = inventory.find((item) => item.instanceId === inspectedId) ?? inventory.at(-1) ?? null;
const ownerById = useMemo(() => new Map(owners.map((candidate) => [candidate.id, candidate])), [owners]);
const heirloomContext: HeirloomContext = {
@@ -447,7 +448,10 @@ function EquipmentDialog({ readOnly = false }: { readOnly?: boolean }) {
))}
</div>
{owner.statRuleset === "rom" && (
<p className="equipment-character-stats__note">Native RoM stats · shared dungeon combat rules</p>
<p className="equipment-character-stats__note">Native RoM stats</p>
)}
{isRomClassId(owner.classId) && (
<p className="equipment-character-stats__note">Training gear scales automatically through level 79.</p>
)}
<dl className="equipment-character-stats" aria-label={`${owner.name} ${statPage} stats`}>
{visibleCharacterRows.map((row) => (
@@ -477,7 +481,7 @@ function EquipmentDialog({ readOnly = false }: { readOnly?: boolean }) {
onClick={() => earned && useCombatStore.getState().unequipItem(owner.id, slot)}
>
<span><small>{SLOT_LABELS[slot]}</small><strong>{item.name}</strong></span>
<em>{earned ? "Unequip" : `Heirloom ${item.itemLevel}`}</em>
<em>{earned ? "Unequip" : `${automaticGearLabel} ${item.itemLevel}`}</em>
</ControllerButton>
) : (
<div key={slot} className="equipment-slot is-empty">
@@ -520,7 +524,9 @@ function EquipmentDialog({ readOnly = false }: { readOnly?: boolean }) {
})}
{!inventory.length && (
<p className="game-panel__empty">
No loot collected yet. Boss drops are added here automatically when the boss is defeated.
{isRomClassId(owner.classId)
? "Training gear is supplied automatically. No equipment is stored in this inventory."
: "No loot collected yet. Boss drops are added here automatically when the boss is defeated."}
</p>
)}
</div>
+2 -7
View File
@@ -4,7 +4,6 @@ import type { HudElementId, HudElementLayout } from "../game/uiLayout";
import { DungeonInteractionPrompt } from "./DungeonInteractionPrompt";
import {
useCallback,
useEffect,
useMemo,
useState,
useSyncExternalStore,
@@ -47,6 +46,7 @@ import { DesktopActionBars } from "./DesktopActionBars";
import { currentUiDevice, useUiLayoutStore } from "../game/uiLayoutStore";
import { SummonFrames } from "./SummonFrames";
import { useOnlineSessionStore } from "../app/onlineSessionStore";
import { usePresentationClock } from "./usePresentationClock";
import { onlineAccountIdFromActor, onlinePlayerActorId } from "../game/onlineSessionRuntime";
import {
activeDungeonEncounterRevision,
@@ -234,7 +234,7 @@ export function Hud({ surface = "top", onLayoutChange, completion, onFinish, onR
const playerRole = usePartyStore((state) => state.playerRole);
const settings = useCombatStore((state) => state.settings);
const onlineSession = useOnlineSessionStore((state) => state.snapshot);
const [clock, setClock] = useState(() => Date.now());
const clock = usePresentationClock();
useSyncExternalStore(
subscribeDungeonEncounterSnapshots,
activeDungeonEncounterRevision,
@@ -244,11 +244,6 @@ export function Hud({ surface = "top", onLayoutChange, completion, onFinish, onR
const finalBossDefeated = runProgress?.dungeonId === activeDungeonId && runProgress.finishedAt !== null;
const dungeonComplete = completion ?? (gameMode === "dungeon" && (finalBossDefeated || activeDungeonRunIsComplete(activeDungeonId)));
useEffect(() => {
const timer = window.setInterval(() => setClock(Date.now()), 100);
return () => window.clearInterval(timer);
}, []);
const updateThreatMeterPosition = useCallback((threatMeterPosition: typeof settings.threatMeterPosition) => {
useCombatStore.getState().updateSettings({ threatMeterPosition });
}, []);
+5 -11
View File
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
import { useShallow } from "zustand/react/shallow";
import { usePresentationClock } from "./usePresentationClock";
import { abilityById } from "../game/abilityCatalog";
import { useCombatStore, type TimedCombatEffect } from "../game/combatStore";
import {
@@ -33,13 +34,7 @@ function LiveTimedEffectStrip({
readonly onInspect?: StatusEffectInspectHandler;
readonly tooltipId?: string;
}) {
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
setNow(Date.now());
const timer = window.setInterval(() => setNow(Date.now()), 100);
return () => window.clearInterval(timer);
}, []);
const now = usePresentationClock();
const visibleEffects = groupTimedEffectsForDisplay(effects, now);
@@ -99,11 +94,10 @@ export function TimedEffectStrip({
onInspect,
tooltipId,
}: TimedEffectStripProps) {
const allEffects = useCombatStore((state) => state.effects);
const requestedTargetIds = targetIds ?? [targetId];
const effects = allEffects.filter((effect) => (
const effects = useCombatStore(useShallow((state) => state.effects.filter((effect) => (
requestedTargetIds.includes(effect.targetId) && kinds.includes(effect.kind)
));
))));
if (!effects.length) return null;
return (
+22
View File
@@ -0,0 +1,22 @@
import { useEffect, useState } from "react";
import { useOnlineSessionStore } from "../app/onlineSessionStore";
import { useGameStore } from "../game/store";
import { useDocumentVisibility } from "../platform/useDocumentVisibility";
/** Visible timers follow local pause; shared online play continues behind menus. */
export function usePresentationClock(active = true): number {
const locallyBlocked = useGameStore((state) => state.paused || state.mapOpen || state.companionOpen);
const sharedOnline = useOnlineSessionStore((state) => state.snapshot !== null);
const visible = useDocumentVisibility();
const ticking = active && visible && (sharedOnline || !locallyBlocked);
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
if (!ticking) return;
setNow(Date.now());
const timer = window.setInterval(() => setNow(Date.now()), 100);
return () => window.clearInterval(timer);
}, [ticking]);
return now;
}