From 3d21b8e46645216b64adfdd3d6da74402493df62 Mon Sep 17 00:00:00 2001 From: "Tim Evans (slaguru666)" Date: Fri, 10 Apr 2026 23:10:55 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20SLA=20Industries=20UI=20overhaul=20?= =?UTF-8?q?=E2=80=94=20icons,=20pause=20animation,=20actor=20sheet=20enhan?= =?UTF-8?q?cements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Custom SLA Industries pause-screen animation (orbital rings, SLA hex logo, radar sweep, STAND BY blink) - Pause animation now correctly disappears on unpause (fixed display:flex !important conflict) - 18 custom SLA Industries SVG icons for all sidebar tabs and scene controls (icons/ui/) - JavaScript DOM injection for Foundry v13 icon replacement (button-based structure) - SCL fractional controls (+1/-1/+½/-½ step buttons) - Session ability tracking checkbox + New Session button - Contacts tab with card-based CRUD - Drug stacking warning dialog - Rest button (Short/Long rest with condition clear + flux restore) - Activity log in Notes tab (rolls, rests, drugs, panic — newest first, 10KB cap) - Portrait static neon blue border - Header restructured to 8-column grid (LAD, Credits, SCL, Height, Weight, Rest) - S.C.L label rename Co-Authored-By: Claude Sonnet 4.6 --- css/mosh.css | 928 ++++++++++++++++++ icons/ui/actors.svg | 12 + icons/ui/chat.svg | 8 + icons/ui/combat.svg | 16 + icons/ui/compendium.svg | 14 + icons/ui/drawings.svg | 10 + icons/ui/items.svg | 15 + icons/ui/journal.svg | 13 + icons/ui/lighting.svg | 14 + icons/ui/measure.svg | 17 + icons/ui/notes.svg | 11 + icons/ui/playlists.svg | 11 + icons/ui/scenes.svg | 16 + icons/ui/settings.svg | 16 + icons/ui/sounds.svg | 9 + icons/ui/tables.svg | 15 + icons/ui/tiles.svg | 14 + icons/ui/token.svg | 10 + icons/ui/walls.svg | 14 + module/actor/actor-sheet.js | 203 +++- module/actor/actor.js | 51 +- module/mosh.js | 299 ++++++ module/sla-drug-system.js | 17 + .../conditions_0e/{000040.log => 000062.log} | 0 packs/conditions_0e/CURRENT | 2 +- packs/conditions_0e/LOG | 6 +- packs/conditions_0e/LOG.old | 6 +- .../{MANIFEST-000039 => MANIFEST-000061} | Bin 153 -> 153 bytes .../conditions_1e/{000040.log => 000062.log} | 0 packs/conditions_1e/CURRENT | 2 +- packs/conditions_1e/LOG | 6 +- packs/conditions_1e/LOG.old | 6 +- .../{MANIFEST-000039 => MANIFEST-000061} | Bin 153 -> 153 bytes .../{000040.log => 000062.log} | 0 packs/items_maintenance_1e/CURRENT | 2 +- packs/items_maintenance_1e/LOG | 6 +- packs/items_maintenance_1e/LOG.old | 6 +- .../{MANIFEST-000039 => MANIFEST-000061} | Bin 154 -> 154 bytes .../{000040.log => 000062.log} | 0 packs/macros_hotbar_0e/CURRENT | 2 +- packs/macros_hotbar_0e/LOG | 6 +- packs/macros_hotbar_0e/LOG.old | 6 +- .../{MANIFEST-000039 => MANIFEST-000061} | Bin 156 -> 156 bytes .../{000040.log => 000062.log} | 0 packs/macros_hotbar_1e/CURRENT | 2 +- packs/macros_hotbar_1e/LOG | 6 +- packs/macros_hotbar_1e/LOG.old | 6 +- .../{MANIFEST-000039 => MANIFEST-000061} | Bin 156 -> 156 bytes .../{000040.log => 000062.log} | 0 packs/macros_triggered_0e/CURRENT | 2 +- packs/macros_triggered_0e/LOG | 6 +- packs/macros_triggered_0e/LOG.old | 6 +- .../{MANIFEST-000039 => MANIFEST-000061} | Bin 157 -> 157 bytes .../{000040.log => 000064.log} | 0 packs/macros_triggered_1e/CURRENT | 2 +- packs/macros_triggered_1e/LOG | 6 +- packs/macros_triggered_1e/LOG.old | 6 +- .../{MANIFEST-000039 => MANIFEST-000063} | Bin 158 -> 158 bytes .../rolltables_0e/{000040.log => 000062.log} | 0 packs/rolltables_0e/CURRENT | 2 +- packs/rolltables_0e/LOG | 6 +- packs/rolltables_0e/LOG.old | 6 +- .../{MANIFEST-000039 => MANIFEST-000061} | Bin 208 -> 208 bytes .../rolltables_1e/{000040.log => 000062.log} | 0 packs/rolltables_1e/CURRENT | 2 +- packs/rolltables_1e/LOG | 6 +- packs/rolltables_1e/LOG.old | 6 +- .../{MANIFEST-000039 => MANIFEST-000061} | Bin 208 -> 208 bytes template.json | 5 + templates/actor/actor-sheet.html | 85 +- 70 files changed, 1874 insertions(+), 75 deletions(-) create mode 100644 icons/ui/actors.svg create mode 100644 icons/ui/chat.svg create mode 100644 icons/ui/combat.svg create mode 100644 icons/ui/compendium.svg create mode 100644 icons/ui/drawings.svg create mode 100644 icons/ui/items.svg create mode 100644 icons/ui/journal.svg create mode 100644 icons/ui/lighting.svg create mode 100644 icons/ui/measure.svg create mode 100644 icons/ui/notes.svg create mode 100644 icons/ui/playlists.svg create mode 100644 icons/ui/scenes.svg create mode 100644 icons/ui/settings.svg create mode 100644 icons/ui/sounds.svg create mode 100644 icons/ui/tables.svg create mode 100644 icons/ui/tiles.svg create mode 100644 icons/ui/token.svg create mode 100644 icons/ui/walls.svg rename packs/conditions_0e/{000040.log => 000062.log} (100%) rename packs/conditions_0e/{MANIFEST-000039 => MANIFEST-000061} (83%) rename packs/conditions_1e/{000040.log => 000062.log} (100%) rename packs/conditions_1e/{MANIFEST-000039 => MANIFEST-000061} (83%) rename packs/items_maintenance_1e/{000040.log => 000062.log} (100%) rename packs/items_maintenance_1e/{MANIFEST-000039 => MANIFEST-000061} (83%) rename packs/macros_hotbar_0e/{000040.log => 000062.log} (100%) rename packs/macros_hotbar_0e/{MANIFEST-000039 => MANIFEST-000061} (65%) rename packs/macros_hotbar_1e/{000040.log => 000062.log} (100%) rename packs/macros_hotbar_1e/{MANIFEST-000039 => MANIFEST-000061} (82%) rename packs/macros_triggered_0e/{000040.log => 000062.log} (100%) rename packs/macros_triggered_0e/{MANIFEST-000039 => MANIFEST-000061} (81%) rename packs/macros_triggered_1e/{000040.log => 000064.log} (100%) rename packs/macros_triggered_1e/{MANIFEST-000039 => MANIFEST-000063} (81%) rename packs/rolltables_0e/{000040.log => 000062.log} (100%) rename packs/rolltables_0e/{MANIFEST-000039 => MANIFEST-000061} (92%) rename packs/rolltables_1e/{000040.log => 000062.log} (100%) rename packs/rolltables_1e/{MANIFEST-000039 => MANIFEST-000061} (92%) diff --git a/css/mosh.css b/css/mosh.css index 75ead34..bebf858 100644 --- a/css/mosh.css +++ b/css/mosh.css @@ -4483,3 +4483,931 @@ p { background: #1a120c; border: 1px solid #8a5a31; } + +/* ============================================================ + SLA PASS 4 — SCL Controls, Session Tracking, Contacts, Rest + ============================================================ */ + +/* SCL fractional control */ +.sla-scl-control { + display: flex; + flex-direction: column; + gap: 4px; +} + +.sla-scl-input { + width: 100%; +} + +.sla-scl-buttons { + display: flex; + gap: 3px; +} + +.sla-scl-buttons .sla-scl-adjust { + flex: 1; + padding: 2px 0; + font-size: 0.72rem; + font-weight: 700; + border: 1px solid rgba(170, 2, 0, 0.4); + background: rgba(170, 2, 0, 0.12); + color: #cc3333; + border-radius: 3px; + cursor: pointer; + transition: background 0.15s; + line-height: 1.4; +} + +.sla-scl-buttons .sla-scl-adjust:hover { + background: rgba(170, 2, 0, 0.28); + color: #f0f0f0; +} + +/* Trauma Response / Session Ability Tracking */ +.sla-trauma-header-row { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 4px; +} + +.sla-session-ability-row { + display: flex; + align-items: center; + gap: 8px; +} + +.sla-session-ability-label { + display: flex; + align-items: center; + gap: 5px; + font-size: 0.75rem; + color: #aaa; + cursor: pointer; + user-select: none; +} + +.sla-session-ability-label input[type="checkbox"] { + margin: 0; + cursor: pointer; +} + +.sla-session-ability-label span { + white-space: nowrap; +} + +.sla-new-session { + font-size: 0.72rem; + padding: 2px 8px; + border: 1px solid rgba(170, 2, 0, 0.4); + background: rgba(170, 2, 0, 0.12); + color: #cc3333; + border-radius: 3px; + cursor: pointer; + transition: background 0.15s; + white-space: nowrap; +} + +.sla-new-session:hover { + background: rgba(170, 2, 0, 0.28); + color: #f0f0f0; +} + +/* Rest button — in save-action-strip */ +.sla-rest-button { + /* inherits .sla-sheet-action but add a distinct colour */ + background: rgba(40, 90, 150, 0.18) !important; + border-color: rgba(60, 120, 200, 0.45) !important; + color: #7ab8f5 !important; +} + +.sla-rest-button:hover { + background: rgba(40, 90, 150, 0.36) !important; + color: #d8eaff !important; +} + +/* Contacts Tab */ +.sla-contacts-toolbar { + display: flex; + gap: 8px; + margin-bottom: 12px; +} + +.sla-contacts-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.sla-contact-card { + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 6px; + padding: 10px 12px; +} + +.sla-contact-header { + display: flex; + align-items: center; + gap: 10px; +} + +.sla-contact-disposition { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; +} + +.sla-contact-disposition-ally { + background: #4eb765; + box-shadow: 0 0 6px rgba(78, 183, 101, 0.5); +} + +.sla-contact-disposition-neutral { + background: #aaa; +} + +.sla-contact-disposition-hostile { + background: #cc5050; + box-shadow: 0 0 6px rgba(204, 80, 80, 0.5); +} + +.sla-contact-disposition-unknown { + background: #777; + border: 1px dashed #aaa; +} + +.sla-contact-name { + font-weight: 700; + font-size: 0.9rem; + color: #f0f0f0; + flex: 1; +} + +.sla-contact-role { + font-size: 0.78rem; + color: #999; + font-style: italic; + flex: 1; +} + +.sla-contact-controls { + display: flex; + gap: 6px; + margin-left: auto; +} + +.sla-contact-controls button { + background: transparent; + border: none; + color: #888; + cursor: pointer; + padding: 2px 4px; + font-size: 0.8rem; + transition: color 0.15s; +} + +.sla-contact-controls button:hover { + color: #f0f0f0; +} + +.sla-contact-controls .sla-contact-delete:hover { + color: #cc5050; +} + +.sla-contact-notes { + margin-top: 6px; + font-size: 0.8rem; + color: #bbb; + line-height: 1.5; + border-top: 1px solid rgba(255, 255, 255, 0.06); + padding-top: 6px; +} + +/* LAD + Rest button row in header */ +.sla-lad-rest-row { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.sla-header-rest-button { + font-size: 0.72rem !important; + padding: 2px 9px !important; + border-radius: 3px !important; + font-weight: 600 !important; + cursor: pointer !important; + white-space: nowrap !important; + line-height: 1.5 !important; +} + +/* ============================================================ + SLA — Header grid: LAD · Credits · SCL · Height · Weight · Rest + ============================================================ */ + +/* 8-column grid — col 8 holds the Rest button at the far right */ +.mosh .headergrid { + grid-template-columns: 1.1fr 1.3fr 0.85fr 0.85fr 0.85fr 0.85fr 0.85fr 0.9fr !important; +} + +/* Row 2: Name (1-4), Class (5-6), Race (7-8) */ +.mosh .headernamegrid, +.mosh .header-field-name { + grid-column: 1 / span 4 !important; + grid-row: 2; +} +.mosh .header-field-class { + grid-column: 5 / span 2 !important; + grid-row: 2; +} +.mosh .header-field-race { + grid-column: 7 / span 2 !important; + grid-row: 2; +} + +/* Row 3: LAD (1) · Credits (2) · SCL (3) · Height (4) · Weight (5) · Rest (6-8) */ +.mosh .header-field-lad { + grid-column: 1 !important; + grid-row: 3; + align-self: start; +} +.mosh .header-field-credits { + grid-column: 2 !important; + grid-row: 3; +} +.mosh .header-field-sla-rank { + grid-column: 3 !important; + grid-row: 3; +} +.mosh .header-field-height { + grid-column: 4 !important; + grid-row: 3; +} +.mosh .header-field-weight { + grid-column: 5 !important; + grid-row: 3; +} +.mosh .header-field-rest { + grid-column: 6 / span 3 !important; + grid-row: 3; + align-self: end; +} + +/* Rest header pill — blue rounded, matching Wound/Panic shape */ +.mosh .sla-header-rest-pill { + background: linear-gradient(180deg, #3a8fd1, #1a5fa0) !important; + color: #e8f4ff !important; + border-color: rgba(100, 180, 255, 0.3) !important; + min-width: 0 !important; + width: 100%; + padding: 6px 10px !important; + font-size: 0.82rem !important; +} +.mosh .sla-header-rest-pill:hover { + background: linear-gradient(180deg, #4ea8f0, #2472bc) !important; + box-shadow: 0 0 12px rgba(60, 140, 255, 0.4) !important; + color: #fff !important; +} + +/* Dossier sheet overrides */ +.mosh.sla-dossier-sheet .header-field-lad { + grid-column: 1 !important; +} +.mosh.sla-dossier-sheet .header-field-credits { + grid-column: 2 !important; +} +.mosh.sla-dossier-sheet .header-field-rest { + grid-column: 6 / span 3 !important; +} + +/* ============================================================ + Portrait — static neon blue border (no animation) + ============================================================ */ +.mosh .sla-portrait-static { + outline: 2px solid #3a9fd6; + outline-offset: 2px; + box-shadow: 0 0 8px 2px rgba(58, 159, 214, 0.55); + border-radius: 6px; +} + +/* ============================================================ + Activity log textarea + ============================================================ */ +.sla-activity-log-textarea { + font-family: 'Roboto Mono', 'Courier New', monospace !important; + font-size: 0.75rem !important; + line-height: 1.55 !important; + overflow-y: auto !important; + resize: vertical !important; +} + +/* ============================================================ + SLA Industries Pause Screen Animation + ============================================================ */ + +/* Make the pause overlay a centered flex column — only when visible. + Do NOT set display here globally: Foundry controls show/hide via + its own display:none and we must not override that with !important. */ +#pause:not([hidden]):not(.hidden) { + display: flex !important; + flex-direction: column !important; + align-items: center !important; + justify-content: center !important; +} + +/* Nuke all of Foundry's default pause children */ +#pause > *:not(.sla-pause-root) { + display: none !important; +} +#pause figure, +#pause h3, +#pause img, +#pause canvas, +#pause figure *, +#pause .fas, +#pause .fa-cog { + display: none !important; +} + +/* Root container for the injected SVG — full centered column */ +.sla-pause-root { + display: flex !important; + align-items: center !important; + justify-content: center !important; + width: 100% !important; + height: 100% !important; + position: absolute !important; + top: 0 !important; + left: 0 !important; +} + +/* Main SVG — large and prominent */ +.sla-pause-svg { + width: min(360px, 60vw) !important; + height: auto !important; + overflow: visible !important; +} + +/* ── Outer ring: rotates clockwise ── */ +.sla-ring-outer { + transform-origin: 0px 0px; + animation: sla-spin-cw 12s linear infinite; +} + +/* ── Inner ring: rotates counter-clockwise ── */ +.sla-ring-inner { + transform-origin: 0px 0px; + animation: sla-spin-ccw 8s linear infinite; +} + +/* ── Scan line: sweeps clockwise like a radar ── */ +.sla-pause-scan { + transform-origin: 0px 0px; + animation: sla-spin-cw 4s linear infinite; +} + +/* ── Logo pulse ── */ +.sla-pause-logo-text { + animation: sla-logo-pulse 3s ease-in-out infinite; +} + +/* ── "STAND BY" blinking ── */ +.sla-pause-standby { + animation: sla-standby-blink 1.8s step-start infinite; +} + +/* ─── Keyframes ─────────────────────────────────────────── */ + +@keyframes sla-spin-cw { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +@keyframes sla-spin-ccw { + from { transform: rotate(0deg); } + to { transform: rotate(-360deg); } +} + +@keyframes sla-logo-pulse { + 0%, 100% { fill: #ff2222; filter: drop-shadow(0 0 4px #ff2222); } + 50% { fill: #ff5555; filter: drop-shadow(0 0 10px #ff4444); } +} + +@keyframes sla-standby-blink { + 0%, 60% { opacity: 1; } + 61%, 100% { opacity: 0; } +} +/* ============================================================ + SLA Industries — Custom UI Icon Set (v2 — scoped selectors) + Replaces Foundry's Font Awesome icons via CSS mask-image + ============================================================ */ + +/* ── Sidebar Tabs ─────────────────────────────────────────── */ + +/* chat — chat */ +#sidebar-tabs [data-tab="chat"] i, +#sidebar-tabs a[data-tab="chat"] i, +nav#sidebar-tabs [data-tab="chat"] i { + display: inline-block !important; + position: relative !important; + width: 1.25em !important; + height: 1.25em !important; + vertical-align: middle !important; +} +#sidebar-tabs [data-tab="chat"] i::before, +#sidebar-tabs a[data-tab="chat"] i::before, +nav#sidebar-tabs [data-tab="chat"] i::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background-color: currentColor !important; + -webkit-mask-image: url('../icons/ui/chat.svg') !important; + mask-image: url('../icons/ui/chat.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; +} + +/* combat — combat */ +#sidebar-tabs [data-tab="combat"] i, +#sidebar-tabs a[data-tab="combat"] i, +nav#sidebar-tabs [data-tab="combat"] i { + display: inline-block !important; + position: relative !important; + width: 1.25em !important; + height: 1.25em !important; + vertical-align: middle !important; +} +#sidebar-tabs [data-tab="combat"] i::before, +#sidebar-tabs a[data-tab="combat"] i::before, +nav#sidebar-tabs [data-tab="combat"] i::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background-color: currentColor !important; + -webkit-mask-image: url('../icons/ui/combat.svg') !important; + mask-image: url('../icons/ui/combat.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; +} + +/* scenes — scenes */ +#sidebar-tabs [data-tab="scenes"] i, +#sidebar-tabs a[data-tab="scenes"] i, +nav#sidebar-tabs [data-tab="scenes"] i { + display: inline-block !important; + position: relative !important; + width: 1.25em !important; + height: 1.25em !important; + vertical-align: middle !important; +} +#sidebar-tabs [data-tab="scenes"] i::before, +#sidebar-tabs a[data-tab="scenes"] i::before, +nav#sidebar-tabs [data-tab="scenes"] i::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background-color: currentColor !important; + -webkit-mask-image: url('../icons/ui/scenes.svg') !important; + mask-image: url('../icons/ui/scenes.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; +} + +/* actors — actors */ +#sidebar-tabs [data-tab="actors"] i, +#sidebar-tabs a[data-tab="actors"] i, +nav#sidebar-tabs [data-tab="actors"] i { + display: inline-block !important; + position: relative !important; + width: 1.25em !important; + height: 1.25em !important; + vertical-align: middle !important; +} +#sidebar-tabs [data-tab="actors"] i::before, +#sidebar-tabs a[data-tab="actors"] i::before, +nav#sidebar-tabs [data-tab="actors"] i::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background-color: currentColor !important; + -webkit-mask-image: url('../icons/ui/actors.svg') !important; + mask-image: url('../icons/ui/actors.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; +} + +/* items — items */ +#sidebar-tabs [data-tab="items"] i, +#sidebar-tabs a[data-tab="items"] i, +nav#sidebar-tabs [data-tab="items"] i { + display: inline-block !important; + position: relative !important; + width: 1.25em !important; + height: 1.25em !important; + vertical-align: middle !important; +} +#sidebar-tabs [data-tab="items"] i::before, +#sidebar-tabs a[data-tab="items"] i::before, +nav#sidebar-tabs [data-tab="items"] i::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background-color: currentColor !important; + -webkit-mask-image: url('../icons/ui/items.svg') !important; + mask-image: url('../icons/ui/items.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; +} + +/* journal — journal */ +#sidebar-tabs [data-tab="journal"] i, +#sidebar-tabs a[data-tab="journal"] i, +nav#sidebar-tabs [data-tab="journal"] i { + display: inline-block !important; + position: relative !important; + width: 1.25em !important; + height: 1.25em !important; + vertical-align: middle !important; +} +#sidebar-tabs [data-tab="journal"] i::before, +#sidebar-tabs a[data-tab="journal"] i::before, +nav#sidebar-tabs [data-tab="journal"] i::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background-color: currentColor !important; + -webkit-mask-image: url('../icons/ui/journal.svg') !important; + mask-image: url('../icons/ui/journal.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; +} + +/* tables — tables */ +#sidebar-tabs [data-tab="tables"] i, +#sidebar-tabs a[data-tab="tables"] i, +nav#sidebar-tabs [data-tab="tables"] i { + display: inline-block !important; + position: relative !important; + width: 1.25em !important; + height: 1.25em !important; + vertical-align: middle !important; +} +#sidebar-tabs [data-tab="tables"] i::before, +#sidebar-tabs a[data-tab="tables"] i::before, +nav#sidebar-tabs [data-tab="tables"] i::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background-color: currentColor !important; + -webkit-mask-image: url('../icons/ui/tables.svg') !important; + mask-image: url('../icons/ui/tables.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; +} + +/* playlists — playlists */ +#sidebar-tabs [data-tab="playlists"] i, +#sidebar-tabs a[data-tab="playlists"] i, +nav#sidebar-tabs [data-tab="playlists"] i { + display: inline-block !important; + position: relative !important; + width: 1.25em !important; + height: 1.25em !important; + vertical-align: middle !important; +} +#sidebar-tabs [data-tab="playlists"] i::before, +#sidebar-tabs a[data-tab="playlists"] i::before, +nav#sidebar-tabs [data-tab="playlists"] i::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background-color: currentColor !important; + -webkit-mask-image: url('../icons/ui/playlists.svg') !important; + mask-image: url('../icons/ui/playlists.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; +} + +/* compendium — compendium */ +#sidebar-tabs [data-tab="compendium"] i, +#sidebar-tabs a[data-tab="compendium"] i, +nav#sidebar-tabs [data-tab="compendium"] i { + display: inline-block !important; + position: relative !important; + width: 1.25em !important; + height: 1.25em !important; + vertical-align: middle !important; +} +#sidebar-tabs [data-tab="compendium"] i::before, +#sidebar-tabs a[data-tab="compendium"] i::before, +nav#sidebar-tabs [data-tab="compendium"] i::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background-color: currentColor !important; + -webkit-mask-image: url('../icons/ui/compendium.svg') !important; + mask-image: url('../icons/ui/compendium.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; +} + +/* settings — settings */ +#sidebar-tabs [data-tab="settings"] i, +#sidebar-tabs a[data-tab="settings"] i, +nav#sidebar-tabs [data-tab="settings"] i { + display: inline-block !important; + position: relative !important; + width: 1.25em !important; + height: 1.25em !important; + vertical-align: middle !important; +} +#sidebar-tabs [data-tab="settings"] i::before, +#sidebar-tabs a[data-tab="settings"] i::before, +nav#sidebar-tabs [data-tab="settings"] i::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background-color: currentColor !important; + -webkit-mask-image: url('../icons/ui/settings.svg') !important; + mask-image: url('../icons/ui/settings.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; +} + +/* ── Scene Controls (left toolbar) ────────────────────────── */ + +/* token — token */ +#controls [data-control="token"] > i, +#controls li[data-control="token"] > i, +#controls .scene-control[data-control="token"] > i, +ol#controls [data-control="token"] > i { + display: inline-block !important; + position: relative !important; + width: 1.25em !important; + height: 1.25em !important; + vertical-align: middle !important; +} +#controls [data-control="token"] > i::before, +#controls li[data-control="token"] > i::before, +#controls .scene-control[data-control="token"] > i::before, +ol#controls [data-control="token"] > i::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background-color: currentColor !important; + -webkit-mask-image: url('../icons/ui/token.svg') !important; + mask-image: url('../icons/ui/token.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; +} + +/* measure — measure */ +#controls [data-control="measure"] > i, +#controls li[data-control="measure"] > i, +#controls .scene-control[data-control="measure"] > i, +ol#controls [data-control="measure"] > i { + display: inline-block !important; + position: relative !important; + width: 1.25em !important; + height: 1.25em !important; + vertical-align: middle !important; +} +#controls [data-control="measure"] > i::before, +#controls li[data-control="measure"] > i::before, +#controls .scene-control[data-control="measure"] > i::before, +ol#controls [data-control="measure"] > i::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background-color: currentColor !important; + -webkit-mask-image: url('../icons/ui/measure.svg') !important; + mask-image: url('../icons/ui/measure.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; +} + +/* tiles — tiles */ +#controls [data-control="tiles"] > i, +#controls li[data-control="tiles"] > i, +#controls .scene-control[data-control="tiles"] > i, +ol#controls [data-control="tiles"] > i { + display: inline-block !important; + position: relative !important; + width: 1.25em !important; + height: 1.25em !important; + vertical-align: middle !important; +} +#controls [data-control="tiles"] > i::before, +#controls li[data-control="tiles"] > i::before, +#controls .scene-control[data-control="tiles"] > i::before, +ol#controls [data-control="tiles"] > i::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background-color: currentColor !important; + -webkit-mask-image: url('../icons/ui/tiles.svg') !important; + mask-image: url('../icons/ui/tiles.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; +} + +/* drawings — drawings */ +#controls [data-control="drawings"] > i, +#controls li[data-control="drawings"] > i, +#controls .scene-control[data-control="drawings"] > i, +ol#controls [data-control="drawings"] > i { + display: inline-block !important; + position: relative !important; + width: 1.25em !important; + height: 1.25em !important; + vertical-align: middle !important; +} +#controls [data-control="drawings"] > i::before, +#controls li[data-control="drawings"] > i::before, +#controls .scene-control[data-control="drawings"] > i::before, +ol#controls [data-control="drawings"] > i::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background-color: currentColor !important; + -webkit-mask-image: url('../icons/ui/drawings.svg') !important; + mask-image: url('../icons/ui/drawings.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; +} + +/* walls — walls */ +#controls [data-control="walls"] > i, +#controls li[data-control="walls"] > i, +#controls .scene-control[data-control="walls"] > i, +ol#controls [data-control="walls"] > i { + display: inline-block !important; + position: relative !important; + width: 1.25em !important; + height: 1.25em !important; + vertical-align: middle !important; +} +#controls [data-control="walls"] > i::before, +#controls li[data-control="walls"] > i::before, +#controls .scene-control[data-control="walls"] > i::before, +ol#controls [data-control="walls"] > i::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background-color: currentColor !important; + -webkit-mask-image: url('../icons/ui/walls.svg') !important; + mask-image: url('../icons/ui/walls.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; +} + +/* lighting — lighting */ +#controls [data-control="lighting"] > i, +#controls li[data-control="lighting"] > i, +#controls .scene-control[data-control="lighting"] > i, +ol#controls [data-control="lighting"] > i { + display: inline-block !important; + position: relative !important; + width: 1.25em !important; + height: 1.25em !important; + vertical-align: middle !important; +} +#controls [data-control="lighting"] > i::before, +#controls li[data-control="lighting"] > i::before, +#controls .scene-control[data-control="lighting"] > i::before, +ol#controls [data-control="lighting"] > i::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background-color: currentColor !important; + -webkit-mask-image: url('../icons/ui/lighting.svg') !important; + mask-image: url('../icons/ui/lighting.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; +} + +/* sounds — sounds */ +#controls [data-control="sounds"] > i, +#controls li[data-control="sounds"] > i, +#controls .scene-control[data-control="sounds"] > i, +ol#controls [data-control="sounds"] > i { + display: inline-block !important; + position: relative !important; + width: 1.25em !important; + height: 1.25em !important; + vertical-align: middle !important; +} +#controls [data-control="sounds"] > i::before, +#controls li[data-control="sounds"] > i::before, +#controls .scene-control[data-control="sounds"] > i::before, +ol#controls [data-control="sounds"] > i::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background-color: currentColor !important; + -webkit-mask-image: url('../icons/ui/sounds.svg') !important; + mask-image: url('../icons/ui/sounds.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; +} + +/* notes — notes */ +#controls [data-control="notes"] > i, +#controls li[data-control="notes"] > i, +#controls .scene-control[data-control="notes"] > i, +ol#controls [data-control="notes"] > i { + display: inline-block !important; + position: relative !important; + width: 1.25em !important; + height: 1.25em !important; + vertical-align: middle !important; +} +#controls [data-control="notes"] > i::before, +#controls li[data-control="notes"] > i::before, +#controls .scene-control[data-control="notes"] > i::before, +ol#controls [data-control="notes"] > i::before { + content: '' !important; + position: absolute !important; + inset: 0 !important; + background-color: currentColor !important; + -webkit-mask-image: url('../icons/ui/notes.svg') !important; + mask-image: url('../icons/ui/notes.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; +} diff --git a/icons/ui/actors.svg b/icons/ui/actors.svg new file mode 100644 index 0000000..36586fb --- /dev/null +++ b/icons/ui/actors.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/icons/ui/chat.svg b/icons/ui/chat.svg new file mode 100644 index 0000000..1b2ad97 --- /dev/null +++ b/icons/ui/chat.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/icons/ui/combat.svg b/icons/ui/combat.svg new file mode 100644 index 0000000..b394956 --- /dev/null +++ b/icons/ui/combat.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/icons/ui/compendium.svg b/icons/ui/compendium.svg new file mode 100644 index 0000000..68aac21 --- /dev/null +++ b/icons/ui/compendium.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/icons/ui/drawings.svg b/icons/ui/drawings.svg new file mode 100644 index 0000000..9005007 --- /dev/null +++ b/icons/ui/drawings.svg @@ -0,0 +1,10 @@ + + + + + + + + + SLA + \ No newline at end of file diff --git a/icons/ui/items.svg b/icons/ui/items.svg new file mode 100644 index 0000000..7b33bc3 --- /dev/null +++ b/icons/ui/items.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/icons/ui/journal.svg b/icons/ui/journal.svg new file mode 100644 index 0000000..b1d1792 --- /dev/null +++ b/icons/ui/journal.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/icons/ui/lighting.svg b/icons/ui/lighting.svg new file mode 100644 index 0000000..6daac2e --- /dev/null +++ b/icons/ui/lighting.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/icons/ui/measure.svg b/icons/ui/measure.svg new file mode 100644 index 0000000..62cbb82 --- /dev/null +++ b/icons/ui/measure.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/icons/ui/notes.svg b/icons/ui/notes.svg new file mode 100644 index 0000000..8788182 --- /dev/null +++ b/icons/ui/notes.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/icons/ui/playlists.svg b/icons/ui/playlists.svg new file mode 100644 index 0000000..a3b7c0f --- /dev/null +++ b/icons/ui/playlists.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/icons/ui/scenes.svg b/icons/ui/scenes.svg new file mode 100644 index 0000000..d1b54a2 --- /dev/null +++ b/icons/ui/scenes.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/icons/ui/settings.svg b/icons/ui/settings.svg new file mode 100644 index 0000000..96d1900 --- /dev/null +++ b/icons/ui/settings.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/icons/ui/sounds.svg b/icons/ui/sounds.svg new file mode 100644 index 0000000..221be0c --- /dev/null +++ b/icons/ui/sounds.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/icons/ui/tables.svg b/icons/ui/tables.svg new file mode 100644 index 0000000..e6d0cd1 --- /dev/null +++ b/icons/ui/tables.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/icons/ui/tiles.svg b/icons/ui/tiles.svg new file mode 100644 index 0000000..9e752d6 --- /dev/null +++ b/icons/ui/tiles.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/icons/ui/token.svg b/icons/ui/token.svg new file mode 100644 index 0000000..5899384 --- /dev/null +++ b/icons/ui/token.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/icons/ui/walls.svg b/icons/ui/walls.svg new file mode 100644 index 0000000..f57c009 --- /dev/null +++ b/icons/ui/walls.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/module/actor/actor-sheet.js b/module/actor/actor-sheet.js index 3e222c4..2009de0 100644 --- a/module/actor/actor-sheet.js +++ b/module/actor/actor-sheet.js @@ -258,6 +258,9 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet { superData.sla.fluxNotes ??= { value: "" }; superData.sla.ebbNotes ??= { value: "" }; superData.sla.speciesRules ??= {}; + superData.sla.sessionAbilityUsed ??= false; + superData.sla.contacts ??= []; + superData.sla.activityLog ??= { value: "" }; const activeTheme = this.actor.getFlag("sla-mothership", "sheetTheme") || "modern"; data.slaSheetTheme = activeTheme; data.data.slaSheetTheme = activeTheme; @@ -292,6 +295,17 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet { disadvantage: String(this._slaTraitPickerSelection?.disadvantage ?? "").trim() }; data.data.slaPrometheusStatus = this.actor.getSlaPrometheusStatus?.() ?? { enabled: false, inCombat: false, currentRound: 0, roundsUntilPulse: null, pulseThisRound: false }; + + // Contacts + const rawContacts = Array.isArray(superData.sla.contacts) ? superData.sla.contacts : []; + data.data.slaContacts = rawContacts.map((c) => ({ + id: String(c.id ?? foundry.utils.randomID()), + name: String(c.name ?? "Unknown"), + role: String(c.role ?? ""), + notes: String(c.notes ?? ""), + disposition: ["ally", "neutral", "hostile", "unknown"].includes(c.disposition) ? c.disposition : "neutral" + })); + data.data.slaContactCount = data.data.slaContacts.length; const traitRows = buildSlaTraitRows(data.data.persTraits ?? []); data.data.persTraitsEnhanced = traitRows; data.data.traitCount = traitRows.length; @@ -933,8 +947,123 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet { this.render(false); }); - html.find('.sla-panic-button').click(ev => { - this.actor.rollTable("panicCheck", null, null, null, null, null, null); + // SCL fractional adjustment buttons + html.find('.sla-scl-adjust').click(async (ev) => { + ev.preventDefault(); + const delta = parseFloat(ev.currentTarget.dataset.delta ?? 0); + if (!delta) return; + const current = parseFloat(String(this.actor.system?.sla?.scl?.value ?? "0").replace(",", ".")) || 0; + const next = Math.max(0, Math.round((current + delta) * 10) / 10); + const formatted = Number.isInteger(next) ? String(next) : next.toFixed(1); + await this.actor.update({ "system.sla.scl.value": formatted }); + this.render(false); + }); + + // Session ability tracking — checkbox + html.find('.sla-session-ability-used').on('change', async (ev) => { + ev.stopPropagation(); + await this.actor.update({ "system.sla.sessionAbilityUsed": ev.currentTarget.checked }); + }); + + // New Session reset button + html.find('.sla-new-session').click(async (ev) => { + ev.preventDefault(); + const confirmed = await Dialog.confirm({ + title: "New Session", + content: "

Start a new session? This will reset the trauma response session ability for this operative.

", + defaultYes: true + }); + if (!confirmed) return; + await this.actor.update({ "system.sla.sessionAbilityUsed": false }); + ui.notifications.info(`${this.actor.name}: session ability reset for new session.`); + this.render(false); + }); + + // Rest button — short or long rest dialog + html.find('.sla-rest-button').click(async (ev) => { + ev.preventDefault(); + const restType = await new Promise((resolve) => { + new Dialog({ + title: "Take a Rest", + content: ` +

What kind of rest is ${this.actor.name} taking?

+

Short Rest: Removes non-permanent conditions. No stat recovery.

+

Long Rest: Removes non-permanent conditions and fully restores Flux (if Ebb user).

+ `, + buttons: { + short: { icon: '', label: "Short Rest", callback: () => resolve("short") }, + long: { icon: '', label: "Long Rest", callback: () => resolve("long") }, + cancel: { icon: '', label: "Cancel", callback: () => resolve(null) } + }, + default: "short" + }).render(true); + }); + if (!restType) return; + + const updates = {}; + // Remove temporary conditions (items of type "condition") + const conditionIds = this.actor.items + .filter((item) => item.type === "condition") + .map((item) => item.id); + if (conditionIds.length > 0) { + await this.actor.deleteEmbeddedDocuments("Item", conditionIds); + } + + if (restType === "long") { + // Restore Flux to max for Ebb users + const isEbb = this.actor.isSlaEbbUser?.() ?? false; + if (isEbb) { + const fluxMax = Math.max(0, Number(this.actor.system?.sla?.flux?.max ?? 0)); + updates["system.sla.flux.value"] = fluxMax; + await this.actor.updateSlaFluxState?.(fluxMax, { save: true }); + } + } + + if (Object.keys(updates).length > 0) { + await this.actor.update(updates); + } + + const condMsg = conditionIds.length > 0 ? ` Cleared ${conditionIds.length} condition(s).` : ""; + const fluxMsg = restType === "long" && (this.actor.isSlaEbbUser?.() ?? false) ? " Flux restored." : ""; + ui.notifications.info(`${this.actor.name}: ${restType === "long" ? "Long" : "Short"} rest complete.${condMsg}${fluxMsg}`); + await this.actor.addSlaActivityLog?.(`${restType === "long" ? "Long" : "Short"} rest.${condMsg}${fluxMsg}`); + this.render(false); + }); + + // Contact add button + html.find('.sla-contact-add').click(async (ev) => { + ev.preventDefault(); + await this._slaContactDialog(null); + }); + + // Contact edit button + html.find('.sla-contact-edit').click(async (ev) => { + ev.preventDefault(); + const contactId = String(ev.currentTarget.dataset.contactId ?? "").trim(); + if (!contactId) return; + await this._slaContactDialog(contactId); + }); + + // Contact delete button + html.find('.sla-contact-delete').click(async (ev) => { + ev.preventDefault(); + const contactId = String(ev.currentTarget.dataset.contactId ?? "").trim(); + if (!contactId) return; + const confirmed = await Dialog.confirm({ + title: "Delete Contact", + content: "

Remove this contact from the operative's network?

", + defaultYes: false + }); + if (!confirmed) return; + const contacts = Array.isArray(this.actor.system?.sla?.contacts) ? [...this.actor.system.sla.contacts] : []; + const updated = contacts.filter((c) => c.id !== contactId); + await this.actor.update({ "system.sla.contacts": updated }); + this.render(false); + }); + + html.find('.sla-panic-button').click(async ev => { + await this.actor.rollTable("panicCheck", null, null, null, null, null, null); + await this.actor.addSlaActivityLog?.("Panic check triggered."); }); html.find('.sla-wound-button').click(ev => { @@ -1210,6 +1339,76 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet { }); } + /** + * Show a dialog for adding or editing a contact + * @param {string|null} contactId null = add new, string = edit existing + */ + async _slaContactDialog(contactId) { + const contacts = Array.isArray(this.actor.system?.sla?.contacts) ? [...this.actor.system.sla.contacts] : []; + const existing = contactId ? contacts.find((c) => c.id === contactId) : null; + const isNew = !existing; + + const name = existing?.name ?? ""; + const role = existing?.role ?? ""; + const notes = existing?.notes ?? ""; + const disposition = existing?.disposition ?? "neutral"; + + const content = ` +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ `; + + const result = await Dialog.prompt({ + title: isNew ? "Add Contact" : "Edit Contact", + content, + label: isNew ? "Add" : "Save", + callback: (html) => { + const nameVal = String(html.find("#sla-contact-name").val() ?? "").trim(); + if (!nameVal) return null; + return { + id: existing?.id ?? foundry.utils.randomID(), + name: nameVal, + role: String(html.find("#sla-contact-role").val() ?? "").trim(), + disposition: String(html.find("#sla-contact-disposition").val() ?? "neutral"), + notes: String(html.find("#sla-contact-notes").val() ?? "").trim() + }; + }, + rejectClose: false + }); + + if (!result) return; + + let updated; + if (isNew) { + updated = [...contacts, result]; + } else { + updated = contacts.map((c) => c.id === contactId ? result : c); + } + await this.actor.update({ "system.sla.contacts": updated }); + this.render(false); + } + /** * Extend and override the sheet header buttons * @override diff --git a/module/actor/actor.js b/module/actor/actor.js index d1b3053..1b43f51 100644 --- a/module/actor/actor.js +++ b/module/actor/actor.js @@ -497,11 +497,44 @@ export class MothershipActor extends Actor { } async useSlaDrug(itemId) { - return SLADrugSystem.useDrug({ actor: this, itemId }); + const result = await SLADrugSystem.useDrug({ actor: this, itemId }); + if (result?.ok !== false) { + const item = this.items.get(itemId); + if (item) await this.addSlaActivityLog(`Drug used: ${item.name}.`); + } + return result; } async closeSlaDrug(itemId = "", drug = "") { - return SLADrugSystem.closeDrug({ actor: this, itemId, drug }); + const result = await SLADrugSystem.closeDrug({ actor: this, itemId, drug }); + if (result?.ok !== false) { + const drugName = drug || (itemId ? (this.items.get(itemId)?.name ?? "Unknown drug") : "Unknown drug"); + await this.addSlaActivityLog(`Drug effect ended: ${drugName}.`); + } + return result; + } + + /** + * Append a timestamped line to the operative's activity log. + * Keeps newest entries at the top and trims to ~10 KB. + */ + async addSlaActivityLog(entry) { + try { + const now = new Date(); + const ts = now.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" }); + const line = `[${ts}] ${entry}`; + const current = String(this.system?.sla?.activityLog?.value ?? ""); + const combined = current ? `${line}\n${current}` : line; + // Trim oldest lines until under 10 KB + const MAX = 10240; + if (combined.length > MAX) { + const lines = combined.split("\n"); + while (lines.join("\n").length > MAX && lines.length > 1) lines.pop(); + await this.update({ "system.sla.activityLog.value": lines.join("\n") }); + } else { + await this.update({ "system.sla.activityLog.value": combined }); + } + } catch (_) { /* log failures are non-fatal */ } } //central flavor text library for all chat messages @@ -3007,6 +3040,18 @@ export class MothershipActor extends Actor { await this.rollTable("panicCheck", null, null, null, null, null, null); } } + // Activity log + if (this.type === "character" && specialRoll !== "damage") { + try { + const rollLabel = skill && skill !== "none" ? skill : (attribute ? String(attribute).charAt(0).toUpperCase() + String(attribute).slice(1) : "Roll"); + const resultNum = Number(parsedRollResult?.total ?? 0); + const outcome = parsedRollResult?.success + ? (parsedRollResult.critical ? "Critical success" : "Pass") + : (parsedRollResult?.critical ? "Critical failure" : "Fail"); + const critTag = parsedRollResult?.critical ? " ★" : ""; + await this.addSlaActivityLog(`${rollLabel}: rolled ${resultNum} vs ${rollTarget} — ${outcome}${critTag}`); + } catch (_) {} + } return [messageData]; } @@ -5299,6 +5344,8 @@ export class MothershipActor extends Actor { if (shouldPanic) { await this.rollTable("panicCheck", null, null, null, null, null, null); } + const saveOutcome = saveResult ? (saveResult.success ? "Pass" : "Fail") : "n/a"; + await this.addSlaActivityLog(`Flux ${forcePanic ? "Panic forced" : "Save"} — Fear save ${saveOutcome}. Flux ${this.system?.sla?.flux?.value ?? 0}/${this.system?.sla?.flux?.max ?? 0}.${shouldPanic ? " Panic triggered." : ""}`); return { saveResult, state, panicTriggered: shouldPanic }; } diff --git a/module/mosh.js b/module/mosh.js index 07ba6b3..3915934 100644 --- a/module/mosh.js +++ b/module/mosh.js @@ -1249,3 +1249,302 @@ export function formatCreditsNumber(num) { return num.toString() + 'cr'; } } + +// ───────────────────────────────────────────────────────────────────────────── +// SLA Industries Pause Screen Animation +// ───────────────────────────────────────────────────────────────────────────── + +function injectSlaPauseAnimation() { + const pauseEl = document.querySelector('#pause'); + if (!pauseEl) return; + + // Don't inject twice + if (pauseEl.querySelector('.sla-pause-root')) return; + + // Aggressively hide ALL of Foundry's default pause content + // Hide every direct child that isn't our injected element + Array.from(pauseEl.children).forEach(child => { + child.style.setProperty('display', 'none', 'important'); + }); + // Also kill any descendant with animation or image content + pauseEl.querySelectorAll('figure, img, svg, h3, p, .fas, .fa-cog, canvas').forEach(el => { + el.style.setProperty('display', 'none', 'important'); + }); + + const svgHTML = ` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SLA + + + + + + + STAND BY + + + SLA INDUSTRIES + + + + +
`; + + // Inject directly into the pause element (not into figure, which we hid) + pauseEl.insertAdjacentHTML('beforeend', svgHTML); +} + +Hooks.once('ready', () => { + // Inject if game is already paused on load + if (game.paused) injectSlaPauseAnimation(); +}); + +// Use the pauseGame hook exclusively — don't use a MutationObserver +// which could fight Foundry's own show/hide logic on the #pause element. +Hooks.on('pauseGame', (paused) => { + if (paused) { + // Two-frame delay — Foundry renders the pause figure asynchronously + requestAnimationFrame(() => requestAnimationFrame(() => injectSlaPauseAnimation())); + } else { + // On unpause: remove our injected content so it can be re-injected cleanly next time + const slaRoot = document.querySelector('#pause .sla-pause-root'); + if (slaRoot) slaRoot.remove(); + // Restore Foundry's original pause children (they were hidden inline, unhide them) + const pauseEl = document.querySelector('#pause'); + if (pauseEl) { + pauseEl.querySelectorAll(':scope > *:not(.sla-pause-root)').forEach(child => { + child.style.removeProperty('display'); + }); + } + } +}); + +// ───────────────────────────────────────────────────────────────────────────── +// SLA Industries — UI Icon Injection +// Replaces Foundry's Font Awesome icons in the sidebar and scene controls +// by walking the live DOM and applying inline CSS mask-image styles. +// ───────────────────────────────────────────────────────────────────────────── + +const SLA_ICON_BASE = "systems/sla-mothership/icons/ui/"; + +const SLA_SIDEBAR_ICONS = { + chat: "chat", + combat: "combat", + scenes: "scenes", + actors: "actors", + items: "items", + journal: "journal", + tables: "tables", + playlists: "playlists", + compendium: "compendium", + settings: "settings", +}; + +const SLA_CONTROL_ICONS = { + token: "token", + measure: "measure", + tiles: "tiles", + drawings: "drawings", + walls: "walls", + lighting: "lighting", + sounds: "sounds", + notes: "notes", +}; + +function _slaApplyIconMask(el, iconFile) { + const url = `${SLA_ICON_BASE}${iconFile}.svg`; + // Measure font size BEFORE zeroing it — FA icons scale with font-size + const fs = parseFloat(getComputedStyle(el).fontSize) || 18; + const sz = Math.round(fs * 1.15) + "px"; + + // font-size:0 → collapses the ::before FA glyph to nothing + // currentColor → inherits the button's text colour (white/grey/active from Foundry) + // mask → cuts that fill into our SVG shape + el.style.setProperty("font-size", "0", "important"); + el.style.setProperty("line-height", "0", "important"); + el.style.setProperty("width", sz, "important"); + el.style.setProperty("height", sz, "important"); + el.style.setProperty("display", "inline-flex", "important"); + el.style.setProperty("align-items", "center", "important"); + el.style.setProperty("justify-content", "center", "important"); + el.style.setProperty("background-color", "currentColor", "important"); + el.style.setProperty("-webkit-mask", `url('${url}') center/contain no-repeat`, "important"); + el.style.setProperty("mask", `url('${url}') center/contain no-repeat`, "important"); + el.setAttribute("data-sla-icon", iconFile); +} + +function injectSlaIcons() { + let found = 0; + + // ── Sidebar tab icons ───────────────────────────────────────────────── + // Foundry v13 confirmed structure: + // + @@ -203,7 +207,16 @@ {{/if}}
-
{{ localize 'Mosh.TraumaResponse' }}
+
+
{{ localize 'Mosh.TraumaResponse' }}
+
+ + +
+
@@ -382,6 +395,7 @@ Special Rules {{#if isEbbUser}}Ebb{{/if}} {{#if isVevaphon}}Morph{{/if}} + Contacts {{ localize 'Mosh.Bio' }} {{ localize 'Mosh.Notes' }} @@ -389,6 +403,48 @@ {{!-- Sheet Body --}}
+ {{!-- Contacts Tab --}} +
+
+
+
+
Operative Network
+
Contacts & Associates
+
+
+ {{slaContactCount}} contact{{#unless (compare 'int' slaContactCount '===' 1)}}s{{/unless}} +
+
+ +
+ +
+ + {{#if slaContacts.length}} +
+ {{#each slaContacts as |contact|}} +
+
+
+
{{contact.name}}
+
{{contact.role}}
+
+ + +
+
+ {{#if contact.notes}} +
{{contact.notes}}
+ {{/if}} +
+ {{/each}} +
+ {{else}} +
No contacts yet. Add allies, fixers, squad members, and other NPCs the operative knows.
+ {{/if}} +
+
+ {{!-- Biography Tab --}}
{{editor enriched.biography target="system.biography" button=true editable=true}} @@ -400,6 +456,11 @@
Total spent: {{system.sla.ammoSpendTotal.value}} cR
+
+
Operative Activity Log
+
Rolls, rests, drug use, panic — automatically recorded. Newest first.
+ +
{{editor enriched.notes target="system.notes" button=true editable=true}}
@@ -478,7 +539,15 @@
- +
+ +
+ + + + +
+