updating buff and debuff icons. separated COA classes from WoW classes on the character creation screen

This commit is contained in:
phenom committed 2026-08-16 11:16:51 -04:00
1 parent b5c9bce1aa
commit c00f3211ea
39 files changed
+2037 -208

No files matched your search

+27
View File
@@ -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
View File
@@ -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>
+6 -3
View File
@@ -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
+5 -3
View File
@@ -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>
+2 -1
View File
@@ -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>