Update Healer Man server

This commit is contained in:
phenom committed 2026-09-23 16:14:19 -04:00
1 parent f90ba7d989
commit 189b94d6b7
51 files changed
+3448 -154

No files matched your search

+1 -1
View File
@@ -183,7 +183,7 @@ export function CharacterCreateScreen() {
<div className="class-summary">
<strong style={{ color: characterClass.color }}>{characterClass.name}</strong>
<span>{characterClass.description}</span>
<small>{characterClass.armor} / {characterClass.resourceSummary} / {characterClass.specializations.length ? characterClass.specializations.join(" · ") : "Automatic skill ranks"}</small>
<small>{characterClass.armor} / {characterClass.resourceSummary} / {characterClass.specializations.length ? characterClass.specializations.join(" · ") : "Skills unlock by level"}</small>
</div>
{specializationOptions.length > 0 && (
<div className="creator-specializations" aria-label="Specialization">
+4 -1
View File
@@ -68,12 +68,15 @@ export function DungeonSelectScreen() {
const enterDungeon = useShellStore((state) => state.enterDungeon);
const returnToMainMenu = useShellStore((state) => state.returnToMainMenu);
const notice = useShellStore((state) => state.notice);
const dungeonSelectionId = useShellStore((state) => state.dungeonSelectionId);
const session = useShellStore((state) => state.session);
const onlineGroup = useOnlineGroupStore((state) => state.snapshot.group);
const updateOnlineMember = useOnlineGroupStore((state) => state.updateMember);
const startOnlineActivity = useOnlineGroupStore((state) => state.startActivity);
const character = activeCharacter ?? rosterCharacter;
const firstAvailable = DUNGEON_CATALOG.find((dungeon) => dungeon.available) ?? DUNGEON_CATALOG[0];
const firstAvailable = DUNGEON_CATALOG.find((dungeon) => dungeon.id === dungeonSelectionId)
?? DUNGEON_CATALOG.find((dungeon) => dungeon.available)
?? DUNGEON_CATALOG[0];
const [selectedCategory, setSelectedCategory] = useState<DungeonCategoryId>(firstAvailable?.category ?? "wow");
const [selectedDungeonId, setSelectedDungeonId] = useState<DungeonId | "">(firstAvailable?.id ?? "");
const [rolePickerOpen, setRolePickerOpen] = useState(false);
+3 -3
View File
@@ -83,9 +83,9 @@ export function GameplayPanels() {
known: !abilityTalentNodeId(catalogAbility) || Boolean(learnedTalentRank),
talentLocked: Boolean(abilityTalentNodeId(catalogAbility)),
passive: ability.passive,
rank: activeRank?.rank,
rankCount: catalogAbility.ranks?.length,
ranks: catalogAbility.ranks?.map((rank) => ({
rank: ability.source === "runewaker" ? undefined : activeRank?.rank,
rankCount: ability.source === "runewaker" ? undefined : catalogAbility.ranks?.length,
ranks: ability.source === "runewaker" ? undefined : catalogAbility.ranks?.map((rank) => ({
rank: rank.rank,
level: rank.level,
spellId: rank.spellId,
+43 -16
View File
@@ -1,4 +1,4 @@
import { lazy, Suspense, useMemo, useState } from "react";
import { lazy, Suspense, useCallback, useMemo, useState } from "react";
import { classById } from "../app/characterCatalog";
import { selectedCharacter, useShellStore } from "../app/shellStore";
import { DualDisplayFrame } from "../components/DualDisplayFrame";
@@ -28,8 +28,22 @@ const GAME_MODES = [
available: true,
},
{
id: "randomized-bite-sized",
id: "random-dungeon",
number: "02",
name: "Random Dungeon",
description: "Let fate choose an available full dungeon from WoW or RoM.",
available: true,
},
{
id: "random-bite-sized",
number: "03",
name: "Random Regular Bite-Sized Dungeon",
description: "Choose a short authored dungeon with its regular enemies and boss rules.",
available: true,
},
{
id: "randomized-bite-sized",
number: "04",
name: "Randomized Bite-Sized",
description: "Run a short dungeon with every mob and the final boss rerolled.",
available: true,
@@ -114,6 +128,8 @@ function MainMenuDestinations({ onOpenDownloads, onOpenReports, onOpenCards, onO
const activeCharacter = useShellStore((state) => state.activeCharacter);
const rosterCharacter = useShellStore(selectedCharacter);
const openDungeons = useShellStore((state) => state.openDungeons);
const openRandomDungeon = useCallback(() => openDungeons("random"), [openDungeons]);
const openRandomBiteSizedDungeon = useCallback(() => openDungeons("random-bite-sized"), [openDungeons]);
const openManastorms = useShellStore((state) => state.openManastorms);
const openArenas = useShellStore((state) => state.openArenas);
const openClassHall = useShellStore((state) => state.openClassHall);
@@ -137,12 +153,22 @@ function MainMenuDestinations({ onOpenDownloads, onOpenReports, onOpenCards, onO
{
id: "mode-dungeons",
run: openDungeons,
neighbors: { up: "back-to-roster", right: "mode-randomized-bite-sized" },
neighbors: { up: "back-to-roster", right: "mode-random-dungeon", down: "mode-random-bite-sized" },
},
{
id: "mode-random-dungeon",
run: openRandomDungeon,
neighbors: { up: "back-to-roster", left: "mode-dungeons", down: "mode-randomized-bite-sized" },
},
{
id: "mode-random-bite-sized",
run: openRandomBiteSizedDungeon,
neighbors: { up: "mode-dungeons", right: "mode-randomized-bite-sized" },
},
{
id: "mode-randomized-bite-sized",
run: onOpenRandomizedBiteSized,
neighbors: { up: "back-to-roster", left: "mode-dungeons" },
neighbors: { up: "mode-random-dungeon", left: "mode-random-bite-sized" },
},
...(character ? [
{ id: "mode-class-hall", run: openClassHall },
@@ -158,7 +184,7 @@ function MainMenuDestinations({ onOpenDownloads, onOpenReports, onOpenCards, onO
] : []),
{ id: "mode-downloads", run: onOpenDownloads },
{ id: "back-to-roster", run: returnToCharacters, neighbors: { down: "mode-dungeons" } },
], [character, gmMenuOpen, isGm, onOpenBalanceLab, onOpenBalanceTuning, onOpenBossTesting, onOpenCards, onOpenDownloads, onOpenEditor, onOpenRandomizedBiteSized, onOpenReadiness, onOpenReports, openArenas, openClassHall, openDungeons, openGmAdmin, openManastorms, returnToCharacters]);
], [character, gmMenuOpen, isGm, onOpenBalanceLab, onOpenBalanceTuning, onOpenBossTesting, onOpenCards, onOpenDownloads, onOpenEditor, onOpenRandomizedBiteSized, onOpenReadiness, onOpenReports, openArenas, openClassHall, openDungeons, openGmAdmin, openManastorms, openRandomBiteSizedDungeon, openRandomDungeon, returnToCharacters]);
const controller = useMenuController(actions, {
columns: 2,
initialId: gmMenuOpen ? (isGm ? "gm-menu-gm-tools" : "gm-menu-boss-testing") : "mode-dungeons",
@@ -168,6 +194,15 @@ function MainMenuDestinations({ onOpenDownloads, onOpenReports, onOpenCards, onO
const characterClass = character ? classById(character.classId) : null;
const specialization = specializationById(character?.specializationId);
const secondaryClass = character?.secondaryClassId ? classById(character.secondaryClassId) : null;
const gameModeAction = (id: string) => {
if (id === "manastorms") return openManastorms;
if (id === "arenas") return openArenas;
if (id === "downloads") return onOpenDownloads;
if (id === "random-dungeon") return openRandomDungeon;
if (id === "random-bite-sized") return openRandomBiteSizedDungeon;
if (id === "randomized-bite-sized") return onOpenRandomizedBiteSized;
return () => openDungeons();
};
const renderGameMode = (mode: (typeof GAME_MODES)[number]) => mode.available ? (
<ControllerButton
key={mode.id}
@@ -176,15 +211,7 @@ function MainMenuDestinations({ onOpenDownloads, onOpenReports, onOpenCards, onO
select={controller.select}
className="front-button main-menu-mode main-menu-mode--available"
type="button"
onClick={mode.id === "manastorms"
? openManastorms
: mode.id === "arenas"
? openArenas
: mode.id === "downloads"
? onOpenDownloads
: mode.id === "randomized-bite-sized"
? onOpenRandomizedBiteSized
: openDungeons}
onClick={gameModeAction(mode.id)}
>
<b aria-hidden="true">{mode.number}</b>
<span><strong>{mode.name}</strong><small>{mode.description}</small></span>
@@ -247,7 +274,7 @@ function MainMenuDestinations({ onOpenDownloads, onOpenReports, onOpenCards, onO
<section className="main-menu-card main-menu-card--adventures" aria-labelledby="main-menu-adventures-title">
<header><span>01</span><strong id="main-menu-adventures-title">Adventures</strong></header>
<div className="main-menu-card__grid">
{GAME_MODES.filter((mode) => mode.id === "dungeons" || mode.id === "randomized-bite-sized").map(renderGameMode)}
{GAME_MODES.filter((mode) => ["dungeons", "random-dungeon", "random-bite-sized", "randomized-bite-sized"].includes(mode.id)).map(renderGameMode)}
</div>
</section>
@@ -280,7 +307,7 @@ function MainMenuDestinations({ onOpenDownloads, onOpenReports, onOpenCards, onO
className="front-button main-menu-mode main-menu-mode--available main-menu-mode--gm-menu" type="button" onClick={() => setGmMenuOpen(true)}>
<b aria-hidden="true">GM</b><span><strong>GM Menu</strong><small>Testing, readiness, reports, and balance tools.</small></span><em>Open</em>
</ControllerButton>
{GAME_MODES.filter((mode) => mode.id !== "dungeons" && mode.id !== "randomized-bite-sized").map(renderGameMode)}
{GAME_MODES.filter((mode) => !["dungeons", "random-dungeon", "random-bite-sized", "randomized-bite-sized"].includes(mode.id)).map(renderGameMode)}
</div>
</section>
</nav>
+4 -4
View File
@@ -1,6 +1,6 @@
import { useMemo } from "react";
import { classById } from "../app/characterCatalog";
import { abilityAtLevel, abilityRankForLevel, type AbilityDefinition } from "../game/abilityCatalog";
import { abilityAtLevel, type AbilityDefinition } from "../game/abilityCatalog";
import { useCombatStore } from "../game/combatStore";
import { useGameStore } from "../game/store";
import { useMenuController, type MenuAction } from "../input/useMenuController";
@@ -41,11 +41,11 @@ export function RomSkillsPanel({ open, onClose }: { open?: boolean; onClose?: ()
<div className="modal-backdrop game-panel-backdrop" role="presentation">
<section className="game-panel rom-skills-panel" role="dialog" aria-modal="true" aria-labelledby="rom-skills-title">
<header className="game-panel__header"><div><p>Runes of Magic / Automatic progression</p><h2 id="rom-skills-title">Skills</h2></div><ControllerButton controlId="rom-skills-close" selectedId={controller.selectedId} select={controller.select} type="button" onClick={close}>Close</ControllerButton></header>
<p className="rom-skills-panel__intro"><strong>{primary.name}{secondary ? ` / ${secondary.name}` : ""}</strong><span>Ranks automatically use the highest source rank available at level {level}. No training-point allocation is required.</span></p>
<p className="rom-skills-panel__intro"><strong>{primary.name}{secondary ? ` / ${secondary.name}` : ""}</strong><span>Skills unlock automatically as you level. There are no talent points or spell ranks to assign.</span></p>
<div className="rom-skills-groups">
{GROUPS.map((group) => {
{GROUPS.filter((group) => abilities.some((ability) => visibleGroup(ability, classId) === group.id)).map((group) => {
const entries = abilities.filter((ability) => visibleGroup(ability, classId) === group.id);
return <section key={group.id}><header><h3>{group.label}</h3><span>{entries.filter((ability) => level >= ability.unlockLevel).length} / {entries.length}</span></header><div>{entries.map((catalogAbility) => { const ability = abilityAtLevel(catalogAbility, level); const rank = abilityRankForLevel(catalogAbility, level); const locked = level < catalogAbility.unlockLevel; return <ControllerButton key={ability.id} controlId={`rom-skill-${ability.id}`} selectedId={controller.selectedId} select={controller.select} className={`rom-skill-card ${locked ? "is-locked" : ""}`} type="button"><img src={ability.icon} alt=""/><span><strong>{ability.name}</strong><small>{locked ? `Unlocks at level ${ability.unlockLevel}` : `Rank ${rank?.rank ?? 1}${ability.secondaryClassId ? ` / ${secondary?.name ?? "Elite"}` : ""}`}</small></span><em>{ability.passive ? "Passive" : ability.cost.amount ? `${ability.cost.amount} ${ability.cost.resource}` : "No cost"}</em></ControllerButton>; })}</div></section>;
return <section key={group.id}><header><h3>{group.label}</h3><span>{entries.filter((ability) => level >= ability.unlockLevel).length} / {entries.length}</span></header><div>{entries.map((catalogAbility) => { const ability = abilityAtLevel(catalogAbility, level); const locked = level < catalogAbility.unlockLevel; return <ControllerButton key={ability.id} controlId={`rom-skill-${ability.id}`} selectedId={controller.selectedId} select={controller.select} className={`rom-skill-card ${locked ? "is-locked" : ""}`} type="button"><img src={ability.icon} alt=""/><span><strong>{ability.name}</strong><small>{locked ? `Unlocks at level ${ability.unlockLevel}` : ability.secondaryClassId ? `Elite / ${secondary?.name ?? "Secondary class"}` : "Learned"}</small></span><em>{ability.passive ? "Passive" : ability.cost.amount ? `${ability.cost.amount} ${ability.cost.resource}` : "No cost"}</em></ControllerButton>; })}</div></section>;
})}
</div>
</section>
+1 -1
View File
@@ -59,7 +59,7 @@ describe("gameplay panel loading integration", () => {
it("groups primary adventures separately and keeps testing utilities in the GM menu", () => {
expect(mainMenuSource).toContain('className="main-menu-card main-menu-card--adventures"');
expect(mainMenuSource).toContain('mode.id === "dungeons" || mode.id === "randomized-bite-sized"');
expect(mainMenuSource).toContain('["dungeons", "random-dungeon", "random-bite-sized", "randomized-bite-sized"]');
expect(mainMenuSource).toContain('controlId="mode-gm-menu"');
for (const controlId of [
"gm-menu-gm-tools",