Update Healer Man server

This commit is contained in:
phenom committed 2026-10-02 10:38:12 -04:00
1 parent 4c768fbec6
commit b16821e92e
330 files changed
+35406 -1760

No files matched your search

+55
View File
@@ -0,0 +1,55 @@
import type { CSSProperties } from "react";
import {
EMPTY_DAMAGE_TEXT_POPUPS,
formatDamageText,
useDamageTextStore,
type DamageTextPopup,
} from "../game/floatingCombatText";
import { DungeonOccludedHtml } from "../scene/DungeonOccludedHtml";
export function damageTextClassName(popup: DamageTextPopup): string {
return `damage-text__number damage-text__number--${popup.category}${popup.critical ? " is-critical" : ""}`;
}
function DamageTextNumbers({ popups }: { readonly popups: readonly DamageTextPopup[] }) {
return (
<span className="damage-text" aria-hidden="true">
{popups.map((popup) => (
<span
key={popup.key}
className={damageTextClassName(popup)}
style={{
"--damage-x": `${[-40, 40, -40, 40][popup.slot]}px`,
"--damage-y": `${[-4, -24, -44, -64][popup.slot]}px`,
} as CSSProperties}
>{formatDamageText(popup.amount)}</span>
))}
</span>
);
}
export function WorldDamageText({
targetActorId,
position,
offsetX = 0,
}: {
readonly targetActorId: string;
readonly position: readonly [number, number, number];
readonly offsetX?: number;
}) {
const popups = useDamageTextStore((state) => state.byTarget[targetActorId] ?? EMPTY_DAMAGE_TEXT_POPUPS);
if (popups.length === 0) return null;
return (
<DungeonOccludedHtml center position={position} zIndexRange={[80, 0]} style={{ pointerEvents: "none" }}>
<span className="damage-text__world-anchor" style={{ left: offsetX }}>
<DamageTextNumbers popups={popups} />
</span>
</DungeonOccludedHtml>
);
}
export function FrameDamageText({ targetActorId }: { readonly targetActorId: string }) {
const popups = useDamageTextStore((state) => state.byTarget[targetActorId] ?? EMPTY_DAMAGE_TEXT_POPUPS);
if (popups.length === 0) return null;
return <span className="party-frame__damage-popups"><DamageTextNumbers popups={popups} /></span>;
}
+18
View File
@@ -65,4 +65,22 @@ describe("combat visual mode option", () => {
expect(markup).toContain("Stored on this device. Enable the pack");
expect(markup).toContain(">Enable</button>");
});
it("offers independent damage and healing number switches", () => {
const markup = renderOptions(DEFAULT_COMBAT_PRESENTATION_SETTINGS.vfxMode);
expect(markup).toContain("Your damage");
expect(markup).toContain("Ally damage");
expect(markup).toContain("Incoming damage");
expect(markup).toContain("Party healing numbers");
});
it("shows dungeon graphics controls with the saved defaults", () => {
const markup = renderOptions(DEFAULT_COMBAT_PRESENTATION_SETTINGS.vfxMode);
expect(markup).toContain("Render resolution");
expect(markup).toContain("Texture filtering");
expect(markup).toContain("Shadows");
expect(markup).toContain("Ambient fill");
expect(markup).toContain(">Adaptive</button>");
expect(markup).toContain(">Standard</button>");
});
});
+45 -2
View File
@@ -20,19 +20,30 @@ import {
type GameplayFrameRateLimit,
type PreviewFrameRateLimit,
} from "../game/frameRateSettings";
import {
AMBIENT_FILL_LEVELS,
RENDER_SCALES,
SHADOW_QUALITIES,
TEXTURE_FILTERING_LEVELS,
nextGraphicsChoice,
type GraphicsSettings,
} from "../game/graphicsSettings";
import {
combatMediaPackReady,
downloadCombatMediaPack,
removeCombatMediaPack,
} from "../game/combatMediaPack";
export interface OptionsPanelSettings {
export interface OptionsPanelSettings extends GraphicsSettings {
showMobHealthBars: boolean;
showMobAggroRanges: boolean;
showTargetFrame: boolean;
showThreatMeter: boolean;
threatMeterPosition: NormalizedHudPosition;
showFloatingCombatText: boolean;
showOwnDamageText: boolean;
showAllyDamageText: boolean;
showIncomingDamageText: boolean;
autoTargetNearest: boolean;
enableScreenShake: boolean;
masterVolume: number;
@@ -55,7 +66,7 @@ export interface OptionsPanelProps {
interface ToggleOption {
id: keyof Pick<OptionsPanelSettings,
"showMobHealthBars" | "showMobAggroRanges" | "showTargetFrame" | "showThreatMeter" | "showFloatingCombatText" | "autoTargetNearest" | "enableScreenShake">;
"showMobHealthBars" | "showMobAggroRanges" | "showTargetFrame" | "showThreatMeter" | "showFloatingCombatText" | "showOwnDamageText" | "showAllyDamageText" | "showIncomingDamageText" | "autoTargetNearest" | "enableScreenShake">;
label: string;
description: string;
}
@@ -66,6 +77,9 @@ const TOGGLES: readonly ToggleOption[] = [
{ id: "showTargetFrame", label: "Target frame", description: "Show detailed status for your current target." },
{ id: "showThreatMeter", label: "Threat meter", description: "Show the compact movable threat table for your selected enemy." },
{ id: "showFloatingCombatText", label: "Party healing numbers", description: "Float effective healing amounts over party frames during combat." },
{ id: "showOwnDamageText", label: "Your damage", description: "Float your damage over the enemy you hit." },
{ id: "showAllyDamageText", label: "Ally damage", description: "Float companion and online ally damage over enemies." },
{ id: "showIncomingDamageText", label: "Incoming damage", description: "Float damage over party characters and your party frame." },
{ id: "autoTargetNearest", label: "Automatic targeting", description: "Pick the nearest valid enemy when an offensive ability needs one." },
{ id: "enableScreenShake", label: "Screen shake", description: "Use restrained camera impact for major hits." },
] as const;
@@ -167,6 +181,10 @@ function OptionsDialog({
{ id: "option-keybinds", run: () => useGameStore.getState().openOverlay("keybinds") },
{ id: "option-gameplay-frame-rate", run: cycleGameplayFrameRate },
{ id: "option-preview-frame-rate", run: cyclePreviewFrameRate },
{ id: "option-render-scale", run: () => onChange({ renderScale: nextGraphicsChoice(settings.renderScale, RENDER_SCALES) }) },
{ id: "option-texture-filtering", run: () => onChange({ textureFiltering: nextGraphicsChoice(settings.textureFiltering, TEXTURE_FILTERING_LEVELS) }) },
{ id: "option-shadow-quality", run: () => onChange({ shadowQuality: nextGraphicsChoice(settings.shadowQuality, SHADOW_QUALITIES) }) },
{ id: "option-ambient-fill", run: () => onChange({ ambientFill: nextGraphicsChoice(settings.ambientFill, AMBIENT_FILL_LEVELS) }) },
...TOGGLES.map((option) => ({
id: `option-${option.id}`,
run: () => onChange({ [option.id]: !settings[option.id] }),
@@ -242,6 +260,31 @@ function OptionsDialog({
cyclePreviewFrameRate,
)}
</div>
<h3 className="options-panel__section-title">Dungeon graphics</h3>
<div className="option-row">
<span><strong>Render resolution</strong><small>Adaptive adjusts sharpness for frame rate. Fixed 100–200% keeps your chosen pixel density.</small></span>
{optionButton("option-render-scale", settings.renderScale === "adaptive" ? "Adaptive" : `${Math.round(settings.renderScale * 100)}%`, () => onChange({
renderScale: nextGraphicsChoice(settings.renderScale, RENDER_SCALES),
}))}
</div>
<div className="option-row">
<span><strong>Texture filtering</strong><small>Sharpen floor and wall textures viewed at an angle. Source texture resolution stays the same.</small></span>
{optionButton("option-texture-filtering", `${settings.textureFiltering}×`, () => onChange({
textureFiltering: nextGraphicsChoice(settings.textureFiltering, TEXTURE_FILTERING_LEVELS),
}))}
</div>
<div className="option-row">
<span><strong>Shadows</strong><small>Standard preserves the current lightweight shadows. High adds dungeon and creature shadows and uses a sharper map.</small></span>
{optionButton("option-shadow-quality", settings.shadowQuality[0].toUpperCase() + settings.shadowQuality.slice(1), () => onChange({
shadowQuality: nextGraphicsChoice(settings.shadowQuality, SHADOW_QUALITIES),
}))}
</div>
<div className="option-row">
<span><strong>Ambient fill</strong><small>Change indirect brightness while preserving each dungeon's authored light colors and local lights.</small></span>
{optionButton("option-ambient-fill", settings.ambientFill[0].toUpperCase() + settings.ambientFill.slice(1), () => onChange({
ambientFill: nextGraphicsChoice(settings.ambientFill, AMBIENT_FILL_LEVELS),
}))}
</div>
<h3 className="options-panel__section-title">Gameplay and interface</h3>
{TOGGLES.map((option) => (
<div key={option.id} className="option-row">
+2
View File
@@ -21,6 +21,7 @@ import { partyResurrectionBlockedByBoss, resurrectDeadPartyMembers } from "../ga
import { ResurrectionDestination, useResurrectionCheckpoints } from "./ResurrectionDestination";
import { useGameStore } from "../game/store";
import { useHudSurface } from "./HudSurface";
import { FrameDamageText } from "./FloatingDamageText";
import {
PARTY_HEAL_POPUP_LIFETIME_MS,
addedPartyHealPopups,
@@ -400,6 +401,7 @@ export function PartyFrames() {
/>
</span>
<HealPopups popups={healPopups} targetId={PLAYER_AGGRO_ID} />
<FrameDamageText targetActorId={PLAYER_AGGRO_ID} />
</button>
{onlineMembers.map((member) => {
const actorId = onlinePlayerActorId(member.accountId);
+55 -8
View File
@@ -1,9 +1,14 @@
import { PLAYER_AGGRO_ID } from "../game/aggro";
import { useCombatStore } from "../game/combatStore";
export function SummonFrames({ now }: { readonly now: number }) {
const summons = useCombatStore((state) => state.summons);
const selectedFriendlyActorId = useCombatStore((state) => state.selectedFriendlyActorId);
const selectedTargetId = useCombatStore((state) => state.selectedTargetId);
const selectFriendlyActor = useCombatStore((state) => state.selectFriendlyActor);
const commandHunterPet = useCombatStore((state) => state.commandHunterPet);
const setHunterPetReactMode = useCombatStore((state) => state.setHunterPetReactMode);
const setHunterPetGrowlAutocast = useCombatStore((state) => state.setHunterPetGrowlAutocast);
if (!summons.length) return null;
return (
<section className="summon-frames" aria-label="Pets, totems, and summons">
@@ -12,18 +17,60 @@ export function SummonFrames({ now }: { readonly now: number }) {
const remaining = summon.expiresAt >= Number.MAX_SAFE_INTEGER
? "∞"
: `${Math.max(0, Math.ceil((summon.expiresAt - now) / 1_000))}s`;
const playerHunterPet = Boolean(summon.hunterPet && summon.ownerActorId === PLAYER_AGGRO_ID);
const command = summon.hunterPetCommand ?? "follow";
const reactMode = summon.hunterPetReactMode ?? "defensive";
return (
<button
type="button"
<div
className={`summon-frame summon-frame--${summon.kind}${selectedFriendlyActorId === summon.id ? " is-selected" : ""}`}
key={summon.id}
aria-pressed={selectedFriendlyActorId === summon.id}
onClick={() => selectFriendlyActor(selectedFriendlyActorId === summon.id ? null : summon.id)}
>
<header><strong>{summon.name}</strong><span>{summon.kind} · {remaining}</span></header>
<div><i style={{ width: `${healthPercent}%` }} /></div>
<small>{Math.ceil(summon.health)} / {summon.maxHealth}</small>
</button>
<button
type="button"
className="summon-frame__select"
aria-pressed={selectedFriendlyActorId === summon.id}
onClick={() => selectFriendlyActor(selectedFriendlyActorId === summon.id ? null : summon.id)}
>
<header><strong>{summon.name}</strong><span>{summon.kind} · {remaining}</span></header>
<div><i style={{ width: `${healthPercent}%` }} /></div>
<small>{Math.ceil(summon.health)} / {summon.maxHealth}</small>
</button>
{playerHunterPet && (
<div className="hunter-pet-controls" aria-label={`${summon.name} controls`}>
<div className="hunter-pet-controls__commands" role="group" aria-label="Pet command">
{(["attack", "follow", "stay"] as const).map((petCommand) => (
<button
key={petCommand}
type="button"
aria-pressed={command === petCommand}
disabled={petCommand === "attack" && !selectedTargetId}
onClick={() => commandHunterPet(summon.id, petCommand)}
>{petCommand}</button>
))}
</div>
<label className="hunter-pet-controls__react">
<span>React</span>
<select
aria-label="Pet reaction"
value={reactMode}
onChange={(event) => setHunterPetReactMode(summon.id, event.currentTarget.value as "defensive" | "aggressive" | "passive")}
>
<option value="defensive">Defensive</option>
<option value="aggressive">Aggressive</option>
<option value="passive">Passive</option>
</select>
</label>
<label className="hunter-pet-controls__growl">
<input
type="checkbox"
checked={Boolean(summon.hunterPetGrowlAutocastEnabled)}
onChange={(event) => setHunterPetGrowlAutocast(summon.id, event.currentTarget.checked)}
/>
Growl auto
</label>
</div>
)}
</div>
);
})}
</section>