forked from phenom/healer-man
updating buff and debuff icons. separated COA classes from WoW classes on the character creation screen
This commit is contained in:
1 parent
b5c9bce1aa
commit
c00f3211ea
39 files changed
+2037
-208
No files matched your search
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AuraDefinition } from "../game/combatAuras";
|
||||
import { auraDefinitionDisplaysInStrip, auraDefinitionIcon } from "./AuraStrip";
|
||||
|
||||
function aura(overrides: Partial<AuraDefinition> = {}): AuraDefinition {
|
||||
return {
|
||||
id: "test-aura",
|
||||
name: "Test Aura",
|
||||
disposition: "debuff",
|
||||
durationMs: 10_000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("aura strip visibility", () => {
|
||||
it("shows ordinary auras and suppresses periodic shadow auras with dedicated timers", () => {
|
||||
expect(auraDefinitionDisplaysInStrip(aura())).toBe(true);
|
||||
expect(auraDefinitionDisplaysInStrip(aura({ hideFromAuraStrip: true }))).toBe(false);
|
||||
});
|
||||
|
||||
it("uses spell artwork while retaining the initial fallback for iconless effects", () => {
|
||||
expect(auraDefinitionIcon(aura({ icon: " /assets/ui/spells/test.png " }))).toBe(
|
||||
"/assets/ui/spells/test.png",
|
||||
);
|
||||
expect(auraDefinitionIcon(aura())).toBeNull();
|
||||
});
|
||||
});
|
||||
+14
-2
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { AuraDisposition } from "../game/combatAuras";
|
||||
import type { AuraDefinition, AuraDisposition } from "../game/combatAuras";
|
||||
import { useCombatStore } from "../game/combatStore";
|
||||
import { formatTimedEffectTimer } from "../game/timedEffectDisplay";
|
||||
|
||||
@@ -9,6 +9,14 @@ interface AuraStripProps {
|
||||
readonly className?: string;
|
||||
}
|
||||
|
||||
export function auraDefinitionDisplaysInStrip(definition: AuraDefinition): boolean {
|
||||
return definition.hideFromAuraStrip !== true;
|
||||
}
|
||||
|
||||
export function auraDefinitionIcon(definition: AuraDefinition): string | null {
|
||||
return definition.icon?.trim() || null;
|
||||
}
|
||||
|
||||
export function AuraStrip({
|
||||
targetId,
|
||||
dispositions = ["buff", "debuff"],
|
||||
@@ -19,6 +27,7 @@ export function AuraStrip({
|
||||
const auras = allAuras.filter((aura) => (
|
||||
aura.targetId === targetId
|
||||
&& dispositions.includes(aura.definition.disposition)
|
||||
&& auraDefinitionDisplaysInStrip(aura.definition)
|
||||
&& (aura.expiresAt === null || aura.expiresAt > now)
|
||||
));
|
||||
|
||||
@@ -36,6 +45,7 @@ export function AuraStrip({
|
||||
const remaining = aura.expiresAt === null ? null : Math.max(0, aura.expiresAt - now);
|
||||
const timer = remaining === null ? "" : formatTimedEffectTimer(remaining);
|
||||
const initial = aura.definition.name.trim().charAt(0).toUpperCase() || "*";
|
||||
const icon = auraDefinitionIcon(aura.definition);
|
||||
return (
|
||||
<span
|
||||
key={aura.instanceId}
|
||||
@@ -43,7 +53,9 @@ export function AuraStrip({
|
||||
aria-label={`${aura.definition.name}, ${aura.definition.disposition}${timer ? `, ${timer} remaining` : ""}`}
|
||||
title={`${aura.definition.name}${timer ? ` · ${timer} remaining` : ""}`}
|
||||
>
|
||||
<span className="aura__initial" aria-hidden="true">{initial}</span>
|
||||
{icon
|
||||
? <img className="aura__icon" src={icon} alt="" draggable={false} />
|
||||
: <span className="aura__initial" aria-hidden="true">{initial}</span>}
|
||||
{aura.stacks > 1 ? <b className="aura__stacks">{aura.stacks}</b> : null}
|
||||
{timer ? <b className="aura__timer" aria-hidden="true">{timer}</b> : null}
|
||||
</span>
|
||||
|
||||
@@ -34,12 +34,15 @@ export function CharacterCreateScreen() {
|
||||
const race = raceById(draft.raceId);
|
||||
const characterClass = classById(draft.classId);
|
||||
const availableRaces = useMemo(() => racesForCategory(draft.categoryId), [draft.categoryId]);
|
||||
const availableClasses = useMemo(() => classesForRace(draft.raceId), [draft.raceId]);
|
||||
const availableClasses = useMemo(
|
||||
() => classesForRace(draft.raceId, draft.categoryId),
|
||||
[draft.categoryId, draft.raceId],
|
||||
);
|
||||
const appearanceLabels = race.labels[draft.gender];
|
||||
const appearanceCounts = race.counts[draft.gender];
|
||||
|
||||
const chooseRace = (raceId: RaceId) => {
|
||||
const nextClasses = classesForRace(raceId);
|
||||
const nextClasses = classesForRace(raceId, draft.categoryId);
|
||||
const nextGenders = supportedGenders(raceId);
|
||||
const classId = nextClasses.some((definition) => definition.id === draft.classId)
|
||||
? draft.classId
|
||||
@@ -154,7 +157,7 @@ export function CharacterCreateScreen() {
|
||||
</div>
|
||||
</main>
|
||||
<section className="creator-classes">
|
||||
<header><span>02</span><div><strong>Class</strong><small>{availableClasses.length} {draft.categoryId === "rom" ? "RuneWaker" : "WoW / Ascension"} classes available to {race.name}</small></div></header>
|
||||
<header><span>02</span><div><strong>Class</strong><small>{availableClasses.length} {draft.categoryId === "rom" ? "RuneWaker" : draft.categoryId === "coa" ? "CoA" : "WoW"} classes available to {race.name}</small></div></header>
|
||||
<div className="class-grid">
|
||||
{availableClasses.map((definition) => (
|
||||
<ControllerButton
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo, useState, type CSSProperties } from "react";
|
||||
import { CLASSES, PLAYABLE_RACES, classById, FACTIONS, raceById } from "../app/characterCatalog";
|
||||
import { CLASSES, PLAYABLE_RACES, classById, contentCategoryForClass, FACTIONS, raceById } from "../app/characterCatalog";
|
||||
import { contentCategoryById } from "../app/contentCategories";
|
||||
import { selectedCharacter, useShellStore } from "../app/shellStore";
|
||||
import { DualDisplayFrame } from "../components/DualDisplayFrame";
|
||||
import { useMenuController, type MenuAction } from "../input/useMenuController";
|
||||
@@ -61,6 +62,7 @@ export function CharacterSelectScreen() {
|
||||
{characters.map((character) => {
|
||||
const raceDefinition = raceById(character.raceId);
|
||||
const classDefinition = classById(character.classId);
|
||||
const categoryDefinition = contentCategoryById(character.categoryId ?? contentCategoryForClass(character.classId));
|
||||
return (
|
||||
<ControllerButton
|
||||
key={character.id}
|
||||
@@ -72,7 +74,7 @@ export function CharacterSelectScreen() {
|
||||
onClick={() => select(character.id)}
|
||||
>
|
||||
<span style={{ "--portrait-accent": raceDefinition.accent } as CSSProperties}>{raceDefinition.sigil}</span>
|
||||
<i><strong>{character.name}</strong><small>{character.categoryId === "rom" ? "RoM" : "WoW"} / Level {character.level} {classDefinition.name}{character.secondaryClassId ? ` + ${classById(character.secondaryClassId).name}` : ""}</small></i>
|
||||
<i><strong>{character.name}</strong><small>{categoryDefinition.label} / Level {character.level} {classDefinition.name}{character.secondaryClassId ? ` + ${classById(character.secondaryClassId).name}` : ""}</small></i>
|
||||
<em>{formatLastPlayed(character.lastPlayedAt)}</em>
|
||||
</ControllerButton>
|
||||
);
|
||||
@@ -110,7 +112,7 @@ export function CharacterSelectScreen() {
|
||||
<h2>{selected.location}</h2>
|
||||
<dl>
|
||||
<div><dt>Level</dt><dd>{selected.level}</dd></div>
|
||||
<div><dt>Content</dt><dd>{selected.categoryId === "rom" ? "Runes of Magic" : "World of Warcraft"}</dd></div>
|
||||
<div><dt>Content</dt><dd>{contentCategoryById(selected.categoryId ?? contentCategoryForClass(selected.classId)).name}</dd></div>
|
||||
<div><dt>Faction</dt><dd>{FACTIONS[race.faction].name}</dd></div>
|
||||
<div><dt>Class role</dt><dd>{characterClass.role}</dd></div>
|
||||
<div><dt>Next step</dt><dd>Choose a game mode</dd></div>
|
||||
|
||||
@@ -78,10 +78,11 @@ function PauseDialog() {
|
||||
</div>
|
||||
<dl className="controls-list">
|
||||
<div><dt>Move</dt><dd>WASD / Left stick</dd></div>
|
||||
<div><dt>Jump</dt><dd>Space / L3</dd></div>
|
||||
<div><dt>Look</dt><dd>Hold right mouse / Arrow keys / Right stick</dd></div>
|
||||
<div><dt>Abilities</dt><dd>1–8 / Face, R1, R2, D-pad left/right</dd></div>
|
||||
<div><dt>Ability layers</dt><dd>Shift / Alt / Hold L1 / Hold L2</dd></div>
|
||||
<div><dt>Targets</dt><dd>Tab / R3 enemy · D-pad up/down party/self · L3 clear</dd></div>
|
||||
<div><dt>Targets</dt><dd>Tab / R3 enemy · D-pad up/down party/self</dd></div>
|
||||
<div><dt>Party orders</dt><dd>F1 Attack / F2 Defend / F3 Stop / F4 Recall</dd></div>
|
||||
<div><dt>Spellbook / {skillMenuLabel}</dt><dd>P / N</dd></div>
|
||||
<div><dt>Inventory / Loot</dt><dd>I / E</dd></div>
|
||||
|
||||
Reference in new issue
Block a user