new help section explaining classes and their mechanics

This commit is contained in:
Warren H
2026-07-18 12:05:34 -04:00
parent 638aef22b2
commit 40aa23be46
32 changed files with 1423 additions and 134 deletions
+36 -3
View File
@@ -10,9 +10,38 @@ import { hockeyPvpDampeningPercent } from "../game/hockeyHealingPvp";
import { blockbreakerTimeMultiplier } from "../game/blockbreaker";
import { RpgRunOverlay } from "./rpgRoguelike/RpgRunOverlay";
import type { CharacterAppearanceV1 } from "../game/characterAppearance";
import { healerMaxResource, isBeaconOfLightTarget } from "../game/healerMechanics";
const GameScene = memo(lazy(() => import("./GameScene").then((module) => ({ default: module.GameScene }))));
GameScene.displayName = "MemoizedGameScene";
const CONVICTION_PIPS = [0, 1, 2] as const;
const MAX_CONVICTION = healerMaxResource("paladin");
function ConvictionMeter() {
const healerClassId = useGameStore((state) => state.healerClassId);
const runMode = useGameStore((state) => state.runMode);
const encounterConviction = useGameStore((state) => state.healerMechanic.resource);
const rpgConviction = useGameStore((state) => state.rpgSpellResources.conviction);
if (healerClassId !== "paladin") return null;
const conviction = Math.max(0, Math.min(MAX_CONVICTION, runMode === "rpg-roguelike" ? rpgConviction : encounterConviction));
return (
<div
className={`top-conviction-meter ${conviction === MAX_CONVICTION ? "is-full" : ""}`}
role="meter"
aria-label="Conviction"
aria-valuemin={0}
aria-valuemax={MAX_CONVICTION}
aria-valuenow={conviction}
aria-valuetext={`${conviction} of ${MAX_CONVICTION} Conviction`}
title={`${conviction} of ${MAX_CONVICTION} Conviction`}
>
<span><b>Conviction</b><small>{conviction} / {MAX_CONVICTION}</small></span>
<span className="top-conviction-pips" aria-hidden="true">
{CONVICTION_PIPS.map((pip) => <i className={pip < conviction ? "is-filled" : ""} key={pip} />)}
</span>
</div>
);
}
function CompactParty() {
const party = useGameStore((state) => state.party);
@@ -24,17 +53,19 @@ function CompactParty() {
const selectMember = useGameStore((state) => state.selectMember);
const mana = useGameStore((state) => state.mana);
const maxMana = useGameStore((state) => state.maxMana);
const healerMechanic = useGameStore((state) => state.healerMechanic);
return (
<div className="top-party" aria-label="Party health">
<div className="top-party" aria-label="Party health and player resources">
{party.map((member) => {
const health = (member.hp / member.maxHp) * 100;
const shield = (member.absorb / member.maxHp) * 100;
const beaconed = isBeaconOfLightTarget(member.id, healerMechanic, time);
return (
<button
className={`top-party-member ${selected === member.id ? "is-selected" : ""}`}
className={`top-party-member ${selected === member.id ? "is-selected" : ""} ${beaconed ? "is-beacon" : ""}`}
key={member.id}
onClick={() => selectMember(member.id)}
aria-label={`Target ${member.name}, ${Math.ceil(member.hp)} health`}
aria-label={`Target ${member.name}, ${Math.ceil(member.hp)} health${beaconed ? ", Beacon of Light" : ""}`}
>
<span className="portrait-dot" style={{ background: member.color }}>{member.name[0]}</span>
<span className="top-party-copy"><strong>{member.name}</strong><small>{member.role}</small></span>
@@ -48,6 +79,7 @@ function CompactParty() {
</span>
)}
<span className="top-effects">
{beaconed && <em className="beacon-pip" title={`Beacon of Light: ${Math.max(0, healerMechanic.beaconExpiresAt - time).toFixed(1)} seconds`}></em>}
{member.healingEffects.some((effect) => effect.expiresAt > time) && <em className="renew-pip">H</em>}
{member.reactiveHeal && member.reactiveHeal.expiresAt > time && <em className="renew-pip">E{member.reactiveHeal.charges}</em>}
{member.debuffs.length > 0 && <em className="debuff-pip">!</em>}
@@ -59,6 +91,7 @@ function CompactParty() {
</button>
);
})}
<ConvictionMeter />
</div>
);
}