forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
39458d5de6
commit
6cc1b9217f
203 files changed
+133929
-31271
No files matched your search
+8
-11
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 });
|
||||
}, []);
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user