Files
Its-Achievable/styles/hud.css
Kaysser Kayyali f2ef1ef4f3 v0.1.0 — initial extraction from battle-focus v0.5.0-alpha.12
Stage 2 of the Hax's Tools split. its-achievable ships as a
standalone module that subscribes to hax-hooks-lib's envelope
stream and provides achievements + custom rules + rewards +
achievement wall + combat HUD.

## What's new

scripts/ — moved from battle-focus/scripts/, MODULE_ID retagged
battle-focus → its-achievable:
- achievement-rules.js (323 lines) — rule engine: OPERATORS,
  TRIGGER_TYPES, evaluateCondition(s), testRule, evaluateRulesFor*
- achievements.js (1150 lines) — 24-entry catalog + award path,
  per-event evaluators, encounter-end + career-update evaluation
- achievement-wall.js (333 lines) — renderAchievementWall,
  getAchievementWallProgress, renderAchievementPopover
- custom-achievements-app.js (270 lines) — GM FormApplication
  for editing custom rules
- hud.js (624 lines) — combat HUD (ApplicationV2 +
  HandlebarsApplicationMixin); removed dead import of
  battle-focus's encounter.js (it was unused even in the
  original)

scripts/main.js — Foundry entry point. Registers settings at
its-achievable.* namespace; exposes the public API on mod.api;
registers chatBubble popover listener + HUD singleton on ready.

templates/ + styles/ — moved verbatim.

tests/PLAN.md — per-project test plan (sections A-F).
tests/test-helpers.mjs — Foundry stub.
tests/verify-achievable-v1.mjs — smoke test, 75 assertions
covering rule engine, catalog, awards, hooks-lib wiring, HUD
payload derivation, and wall/popover rendering. Runs in <2s.

## Architecture

- **Settings namespace**: its-achievable.* (was battle-focus.*).
  No migration (per Kaysser's decision); users with existing
  worlds re-create their custom rules. Documented in README.
- **HUD derives its own state from hooks-lib envelopes.** Stage 2
  keeps the legacy battle-focus:hud-update broadcast subscription
  for now (battle-focus still emits it); Stage 3 will switch the
  HUD to subscribe to hooks-lib directly and remove the
  battle-focus broadcasts.
- **Encounter singleton**: accessed via battle-focus's public
  api.getActiveEncounter() — no direct import of battle-focus's
  encounter.js.

## Dependencies

- hax-hooks-lib ^0.2.0 (declared in module.json relationships).
- battle-focus (soft, runtime) — provides the encounter singleton.

## Tests

- 75/75 smoke assertions pass in 0.07s.
- Module manifest validates: 0 errors, 1 warning (no icon —
  Stage 2+ work).

Push: Gitea only.
2026-06-20 14:04:56 -04:00

355 lines
7.0 KiB
CSS

/* Battle Focus Active Combat HUD styles (slice C).
*
* All rules are scoped under `.bf-hud` to avoid clashing with
* Foundry's own CSS or other modules' CSS. The HUD is a floating
* overlay that sits at one of four configurable positions (top,
* bottom, left, right) — see the `hudPosition` setting.
*
* The HUD uses Foundry's ApplicationV2 framework (frame: false) so
* we draw the chrome ourselves. The .window-app class is still
* applied by Foundry and we override it.
*/
.bf-hud {
--bf-hud-bg: rgba(20, 23, 28, 0.95);
--bf-hud-border: #2d333b;
--bf-hud-text: #e1e4e8;
--bf-hud-text-dim: #8b949e;
--bf-hud-accent: #c97a4a;
--bf-hud-danger: #f85149;
--bf-hud-success: #3fb950;
--bf-hud-warning: #d29922;
--bf-hud-party: #58a6ff;
--bf-hud-foe: #f85149;
--bf-hud-pinned-bg: #1c2128;
--bf-hud-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
position: fixed;
z-index: 95; /* under #ui-top but above the canvas */
background: var(--bf-hud-bg);
color: var(--bf-hud-text);
font-family: 'IM Fell English', 'Georgia', serif;
font-size: 12px;
line-height: 1.4;
border: 1px solid var(--bf-hud-border);
border-radius: 6px;
box-shadow: var(--bf-hud-shadow);
padding: 8px 10px;
min-width: 280px;
max-width: 360px;
pointer-events: auto;
user-select: none;
}
/* Position variants. Default: top center. */
.bf-hud--top {
top: 8px;
left: 50%;
transform: translateX(-50%);
}
.bf-hud--bottom {
bottom: 8px;
left: 50%;
transform: translateX(-50%);
}
.bf-hud--left {
top: 50%;
left: 8px;
transform: translateY(-50%);
}
.bf-hud--right {
top: 50%;
right: 8px;
transform: translateY(-50%);
}
/* Compact view for vertical positions (left/right). */
.bf-hud--left,
.bf-hud--right {
max-width: 260px;
}
/* GM vs player view tinting (subtle, mostly cosmetic). */
.bf-hud--gm {
border-color: var(--bf-hud-accent);
}
.bf-hud--player {
border-color: var(--bf-hud-party);
}
/* ── Header ──────────────────────────────────────────────── */
.bf-hud-header {
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid var(--bf-hud-border);
padding-bottom: 6px;
margin-bottom: 6px;
}
.bf-hud-round {
font-weight: 700;
color: var(--bf-hud-accent);
flex: 0 0 auto;
}
.bf-hud-turn {
display: flex;
align-items: center;
gap: 4px;
flex: 1 1 auto;
min-width: 0;
}
.bf-hud-portrait {
width: 24px;
height: 24px;
border-radius: 4px;
border: 1px solid var(--bf-hud-border);
object-fit: cover;
flex: 0 0 24px;
}
.bf-hud-turn-name {
font-style: italic;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.bf-hud-timer {
flex: 0 0 auto;
color: var(--bf-hud-text-dim);
font-variant-numeric: tabular-nums;
}
.bf-hud-close {
flex: 0 0 auto;
background: transparent;
border: 1px solid var(--bf-hud-border);
color: var(--bf-hud-text-dim);
border-radius: 3px;
width: 20px;
height: 20px;
padding: 0;
cursor: pointer;
font-size: 12px;
line-height: 1;
}
.bf-hud-close:hover {
background: var(--bf-hud-border);
color: var(--bf-hud-text);
}
/* ── Combatants list ─────────────────────────────────────── */
.bf-hud-combatants {
margin-bottom: 6px;
}
.bf-hud-combatants-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.bf-hud-row {
display: flex;
align-items: center;
gap: 6px;
padding: 4px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.03);
border-left: 3px solid var(--bf-hud-border);
}
.bf-hud-row--party {
border-left-color: var(--bf-hud-party);
}
.bf-hud-row--foe {
border-left-color: var(--bf-hud-foe);
}
.bf-hud-row-portrait {
width: 20px;
height: 20px;
border-radius: 3px;
object-fit: cover;
flex: 0 0 20px;
}
.bf-hud-row-body {
flex: 1 1 auto;
min-width: 0;
}
.bf-hud-row-name {
display: flex;
align-items: center;
gap: 4px;
font-weight: 600;
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bf-hud-tag {
font-size: 9px;
padding: 0 4px;
border-radius: 2px;
background: var(--bf-hud-party);
color: #fff;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.bf-hud-tag--foe {
background: var(--bf-hud-foe);
}
.bf-hud-tag--down {
background: var(--bf-hud-warning);
color: #000;
}
.bf-hud-row-stats {
display: flex;
flex-wrap: wrap;
gap: 4px 6px;
font-size: 10px;
color: var(--bf-hud-text-dim);
}
.bf-hud-stat {
font-variant-numeric: tabular-nums;
}
.bf-hud-stat--hp {
color: var(--bf-hud-success);
}
.bf-hud-row[data-token-id=""] {
opacity: 0.5;
}
.bf-hud-empty {
color: var(--bf-hud-text-dim);
font-style: italic;
text-align: center;
padding: 6px 0;
margin: 0;
font-size: 11px;
}
.bf-hud-empty--inline {
display: inline;
padding: 0 0 0 4px;
}
/* ── Dice streak ──────────────────────────────────────────── */
.bf-hud-dice-streak {
display: flex;
align-items: baseline;
gap: 6px;
padding: 4px 0;
border-top: 1px solid var(--bf-hud-border);
border-bottom: 1px solid var(--bf-hud-border);
margin-bottom: 6px;
font-size: 11px;
}
.bf-hud-stat-label {
color: var(--bf-hud-text-dim);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 9px;
}
.bf-hud-stat-value {
font-weight: 700;
font-size: 14px;
color: var(--bf-hud-warning);
font-variant-numeric: tabular-nums;
}
.bf-hud-stat-meta {
color: var(--bf-hud-text-dim);
font-size: 10px;
font-style: italic;
}
.bf-hud-dice-streak[data-streak="0"] .bf-hud-stat-value {
color: var(--bf-hud-text-dim);
}
.bf-hud-dice-streak[data-streak="3"] .bf-hud-stat-value,
.bf-hud-dice-streak[data-streak="4"] .bf-hud-stat-value {
color: var(--bf-hud-warning);
}
.bf-hud-dice-streak[data-streak="5"] .bf-hud-stat-value {
color: var(--bf-hud-danger);
animation: bf-hud-streak-pulse 1s ease-in-out infinite;
}
@keyframes bf-hud-streak-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.15); }
}
/* ── Pinned achievements feed ────────────────────────────── */
.bf-hud-pinned {
display: flex;
flex-direction: column;
gap: 4px;
}
.bf-hud-pinned-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 3px;
}
.bf-hud-pinned-item {
display: flex;
align-items: center;
gap: 6px;
padding: 3px 6px;
background: var(--bf-hud-pinned-bg);
border-radius: 3px;
font-size: 11px;
border-left: 2px solid var(--bf-hud-accent);
animation: bf-hud-toast-in 0.4s ease-out;
}
@keyframes bf-hud-toast-in {
from { transform: translateX(20px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.bf-hud-pinned-icon {
font-size: 14px;
flex: 0 0 auto;
}
.bf-hud-pinned-desc {
color: var(--bf-hud-text-dim);
font-size: 10px;
margin-left: 2px;
font-style: italic;
}