diff --git a/styles/zero-engine.css b/styles/zero-engine.css
index 7b44d3d..eadd5df 100644
--- a/styles/zero-engine.css
+++ b/styles/zero-engine.css
@@ -4671,3 +4671,1847 @@ body > .ze-pause-root .ze-sat-inner {
font-size: 10px; padding: 0 1px; line-height: 1;
}
.gm-status-window .gsw-remove-cond:hover { color: #ff0000; }
+
+
+/* ══════════════════════════════════════════════════════════════════════════
+ SLA GM TOOLS — v1.7.0
+ ══════════════════════════════════════════════════════════════════════════ */
+
+
+/* ── 💳 CREDIT DISTRIBUTION TOOL ──────────────────────────────────────────────────── */
+/* ============================================================
+ SLA Credit Distribution Tool — .credit-tool-window scoped
+ Dark sci-fi aesthetic: navy/midnight backgrounds, cyan + orange neon
+ ============================================================ */
+
+.credit-tool-window .window-content {
+ background: #0d1117;
+ color: #c9d1d9;
+ padding: 0;
+ font-family: "SLA Borg UI", "Rajdhani", "Oxanium", monospace, sans-serif;
+}
+
+/* ── Wrapper ─────────────────────────────────────────────── */
+.credit-tool-window .ctw-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+}
+
+/* ── Section containers ──────────────────────────────────── */
+.credit-tool-window .ctw-section {
+ padding: 10px 12px;
+ border-bottom: 1px solid #1e2d3d;
+}
+
+.credit-tool-window .ctw-section:last-child {
+ border-bottom: none;
+}
+
+.credit-tool-window .ctw-section-title {
+ font-size: 10px;
+ font-weight: 600;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: #00d4ff;
+ margin: 0 0 8px 0;
+ padding-bottom: 5px;
+ border-bottom: 1px solid rgba(0, 212, 255, 0.25);
+}
+
+/* ── Table ───────────────────────────────────────────────── */
+.credit-tool-window .ctw-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 11px;
+}
+
+.credit-tool-window .ctw-header-row {
+ background: #111927;
+}
+
+.credit-tool-window .ctw-th {
+ padding: 5px 6px;
+ text-align: center;
+ font-size: 9px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: #7d8fa1;
+ border-bottom: 1px solid #1e2d3d;
+ white-space: nowrap;
+}
+
+.credit-tool-window .ctw-th-name {
+ text-align: left;
+}
+
+.credit-tool-window .ctw-row {
+ transition: background 0.15s ease;
+ border-bottom: 1px solid #161d27;
+}
+
+.credit-tool-window .ctw-row:hover {
+ background: rgba(0, 212, 255, 0.04);
+}
+
+.credit-tool-window .ctw-row:last-child {
+ border-bottom: none;
+}
+
+.credit-tool-window .ctw-cell {
+ padding: 5px 6px;
+ text-align: center;
+ vertical-align: middle;
+ font-family: "Courier New", "Consolas", monospace;
+ font-size: 11px;
+}
+
+/* ── Name cell ───────────────────────────────────────────── */
+.credit-tool-window .ctw-name {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 5px 6px;
+ text-align: left;
+ white-space: nowrap;
+}
+
+.credit-tool-window .ctw-portrait {
+ width: 28px;
+ height: 28px;
+ border-radius: 3px;
+ object-fit: cover;
+ border: 1px solid #2a3a4a;
+ flex-shrink: 0;
+}
+
+.credit-tool-window .ctw-actor-name {
+ font-size: 11px;
+ font-weight: 600;
+ color: #e6edf3;
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 120px;
+}
+
+.credit-tool-window .ctw-scl {
+ font-size: 9px;
+ color: #7d8fa1;
+ font-family: "Courier New", monospace;
+ flex-shrink: 0;
+}
+
+/* ── Credits input ───────────────────────────────────────── */
+.credit-tool-window .ctw-credits-input {
+ width: 70px;
+ background: #0d1117;
+ border: 1px solid #2a3a4a;
+ border-radius: 3px;
+ color: #00d4ff;
+ font-family: "Courier New", "Consolas", monospace;
+ font-size: 11px;
+ font-weight: 700;
+ text-align: center;
+ padding: 3px 4px;
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
+}
+
+.credit-tool-window .ctw-credits-input:focus {
+ outline: none;
+ border-color: #00d4ff;
+ box-shadow: 0 0 0 2px rgba(0, 212, 255, 0.2);
+}
+
+/* Remove browser spin buttons */
+.credit-tool-window .ctw-credits-input::-webkit-inner-spin-button,
+.credit-tool-window .ctw-credits-input::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+
+/* ── Colour helpers ──────────────────────────────────────── */
+.credit-tool-window .ctw-positive {
+ color: #3fb950;
+ font-weight: 700;
+}
+
+.credit-tool-window .ctw-negative {
+ color: #f85149;
+ font-weight: 700;
+}
+
+.credit-tool-window .ctw-greyed {
+ color: #7d8fa1;
+}
+
+.credit-tool-window .ctw-debt {
+ color: #ff6b35;
+ font-weight: 700;
+}
+
+/* ── Empty state ─────────────────────────────────────────── */
+.credit-tool-window .ctw-empty {
+ text-align: center;
+ padding: 18px 12px;
+ color: #7d8fa1;
+ font-size: 11px;
+ font-style: italic;
+}
+
+/* ── Buttons ─────────────────────────────────────────────── */
+.credit-tool-window .ctw-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 4px 10px;
+ border-radius: 3px;
+ border: 1px solid transparent;
+ font-family: "SLA Borg UI", "Rajdhani", monospace, sans-serif;
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ white-space: nowrap;
+}
+
+.credit-tool-window .ctw-btn-save {
+ background: rgba(0, 212, 255, 0.1);
+ border-color: rgba(0, 212, 255, 0.4);
+ color: #00d4ff;
+}
+
+.credit-tool-window .ctw-btn-save:hover {
+ background: rgba(0, 212, 255, 0.22);
+ border-color: #00d4ff;
+ box-shadow: 0 0 6px rgba(0, 212, 255, 0.3);
+}
+
+.credit-tool-window .ctw-btn-pay-all {
+ background: rgba(63, 185, 80, 0.12);
+ border-color: rgba(63, 185, 80, 0.5);
+ color: #3fb950;
+}
+
+.credit-tool-window .ctw-btn-pay-all:hover {
+ background: rgba(63, 185, 80, 0.25);
+ border-color: #3fb950;
+ box-shadow: 0 0 6px rgba(63, 185, 80, 0.3);
+}
+
+.credit-tool-window .ctw-btn-danger {
+ background: rgba(248, 81, 73, 0.1);
+ border-color: rgba(248, 81, 73, 0.4);
+ color: #f85149;
+}
+
+.credit-tool-window .ctw-btn-danger:hover {
+ background: rgba(248, 81, 73, 0.22);
+ border-color: #f85149;
+ box-shadow: 0 0 6px rgba(248, 81, 73, 0.3);
+}
+
+.credit-tool-window .ctw-btn:active {
+ transform: scale(0.96);
+}
+
+/* ── Bulk panel ──────────────────────────────────────────── */
+.credit-tool-window .ctw-bulk-panel {
+ background: rgba(0, 212, 255, 0.02);
+}
+
+.credit-tool-window .ctw-bulk-controls {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 10px;
+}
+
+.credit-tool-window .ctw-label {
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: #7d8fa1;
+ white-space: nowrap;
+}
+
+.credit-tool-window .ctw-bulk-amount {
+ width: 80px;
+}
+
+.credit-tool-window .ctw-radio-group {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+
+.credit-tool-window .ctw-radio-label {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 10px;
+ font-weight: 600;
+ color: #c9d1d9;
+ cursor: pointer;
+ letter-spacing: 0.04em;
+ white-space: nowrap;
+}
+
+.credit-tool-window .ctw-radio-label input[type="radio"] {
+ accent-color: #ff6b35;
+ width: 12px;
+ height: 12px;
+ cursor: pointer;
+}
+
+.credit-tool-window .ctw-radio-label:hover {
+ color: #ff6b35;
+}
+
+.credit-tool-window .ctw-bulk-buttons {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ margin-left: auto;
+}
+
+/* ── Scrollable tbody if many actors ─────────────────────── */
+.credit-tool-window .ctw-table tbody {
+ display: table-row-group;
+}
+
+.credit-tool-window.credit-tool-window .window-content {
+ overflow-y: auto;
+}
+
+/* ── 📒 SHIFT LEDGER TOOL ──────────────────────────────────────────────────── */
+/* ═══════════════════════════════════════════════════════════════════════════
+ SLA Shift Ledger — temp_ledger.css
+ All rules scoped to .shift-ledger-window
+ ═══════════════════════════════════════════════════════════════════════════ */
+
+.shift-ledger-window .window-content {
+ padding: 0;
+ background: #0d0d1a;
+ color: #c8d0e0;
+ font-family: 'Signika', 'Palatino Linotype', serif;
+}
+
+/* ── Root container ────────────────────────────────────────────────────────── */
+.shift-ledger-window .slw-root {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ min-height: 100%;
+}
+
+/* ── Shared colour utilities ───────────────────────────────────────────────── */
+.shift-ledger-window .slw-positive { color: #44cc66; }
+.shift-ledger-window .slw-negative { color: #cc2222; }
+.shift-ledger-window .slw-debt { color: #ff6b35; }
+.shift-ledger-window .slw-credits { color: #00d4ff; }
+.shift-ledger-window .slw-neutral { color: #888; }
+.shift-ledger-window .slw-mono { font-family: 'Courier New', Courier, monospace; }
+
+/* ── Summary bar ───────────────────────────────────────────────────────────── */
+.shift-ledger-window .slw-summary-bar {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0;
+ background: linear-gradient(135deg, #16213e 0%, #1a1a2e 100%);
+ border-bottom: 2px solid #00d4ff44;
+ padding: 10px 14px;
+}
+
+.shift-ledger-window .slw-summary-item {
+ flex: 1 1 120px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 6px 10px;
+ border-right: 1px solid #ffffff12;
+}
+
+.shift-ledger-window .slw-summary-item:last-child {
+ border-right: none;
+}
+
+.shift-ledger-window .slw-summary-label {
+ font-size: 0.68em;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: #8899aa;
+ margin-bottom: 3px;
+ white-space: nowrap;
+}
+
+.shift-ledger-window .slw-summary-value {
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 1.25em;
+ font-weight: bold;
+}
+
+/* ── PC list container ─────────────────────────────────────────────────────── */
+.shift-ledger-window .slw-pc-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ padding: 10px 10px 4px 10px;
+}
+
+.shift-ledger-window .slw-empty {
+ color: #667;
+ font-style: italic;
+ text-align: center;
+ padding: 20px;
+}
+
+/* ── Individual PC card ────────────────────────────────────────────────────── */
+.shift-ledger-window .slw-pc-card {
+ background: #12122a;
+ border: 1px solid #2a2a4a;
+ border-left: 3px solid #00d4ff55;
+ border-radius: 4px;
+ margin-bottom: 8px;
+ overflow: hidden;
+ transition: border-left-color 0.15s ease;
+}
+
+.shift-ledger-window .slw-pc-card:hover {
+ border-left-color: #00d4ff;
+}
+
+/* PC card header */
+.shift-ledger-window .slw-pc-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 7px 12px;
+ background: linear-gradient(90deg, #1a1a3a 0%, #16213e 100%);
+ border-bottom: 1px solid #2a2a4a;
+}
+
+.shift-ledger-window .slw-pc-name {
+ flex: 1;
+ font-size: 1em;
+ font-weight: bold;
+ color: #e0e8f0;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.shift-ledger-window .slw-scl-badge {
+ background: #ff6b3522;
+ color: #ff6b35;
+ border: 1px solid #ff6b3566;
+ border-radius: 3px;
+ font-size: 0.72em;
+ font-weight: bold;
+ padding: 2px 7px;
+ letter-spacing: 0.06em;
+ white-space: nowrap;
+}
+
+.shift-ledger-window .slw-pc-credits {
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 1.15em;
+ font-weight: bold;
+ color: #00d4ff;
+ white-space: nowrap;
+}
+
+/* PC card body — three columns */
+.shift-ledger-window .slw-pc-body {
+ display: flex;
+ gap: 0;
+ padding: 8px 0 10px 0;
+}
+
+.shift-ledger-window .slw-finance-col {
+ flex: 1;
+ padding: 0 12px;
+ border-right: 1px solid #1e1e3a;
+}
+
+.shift-ledger-window .slw-finance-col:last-child {
+ border-right: none;
+}
+
+.shift-ledger-window .slw-finance-col--summary {
+ flex: 0 0 160px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.shift-ledger-window .slw-col-title {
+ font-size: 0.7em;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: #667788;
+ margin-bottom: 5px;
+ padding-bottom: 3px;
+ border-bottom: 1px solid #1e2a3a;
+}
+
+/* Finance rows */
+.shift-ledger-window .slw-finance-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ padding: 1px 0;
+ font-size: 0.8em;
+}
+
+.shift-ledger-window .slw-finance-label {
+ color: #8899aa;
+ white-space: nowrap;
+ margin-right: 8px;
+}
+
+.shift-ledger-window .slw-finance-val {
+ text-align: right;
+ min-width: 52px;
+ color: #b0bcc8;
+}
+
+/* Total row emphasis */
+.shift-ledger-window .slw-total-row {
+ border-top: 1px solid #2a2a4a;
+ margin-top: 3px;
+ padding-top: 3px;
+ font-weight: bold;
+}
+
+.shift-ledger-window .slw-total-row .slw-finance-label {
+ color: #aabbc8;
+}
+
+/* ── Buttons ───────────────────────────────────────────────────────────────── */
+.shift-ledger-window .slw-btn {
+ display: inline-block;
+ padding: 5px 10px;
+ font-size: 0.78em;
+ font-family: 'Signika', sans-serif;
+ font-weight: bold;
+ letter-spacing: 0.04em;
+ border: none;
+ border-radius: 3px;
+ cursor: pointer;
+ transition: filter 0.12s ease, transform 0.08s ease;
+ white-space: nowrap;
+}
+
+.shift-ledger-window .slw-btn:hover {
+ filter: brightness(1.2);
+ transform: translateY(-1px);
+}
+
+.shift-ledger-window .slw-btn:active {
+ transform: translateY(0);
+ filter: brightness(0.9);
+}
+
+/* Per-PC process button */
+.shift-ledger-window .slw-btn--process {
+ background: linear-gradient(135deg, #1a3a2a 0%, #224433 100%);
+ color: #44cc66;
+ border: 1px solid #44cc6655;
+ margin-top: 8px;
+ width: 100%;
+}
+
+.shift-ledger-window .slw-btn--process:hover {
+ background: linear-gradient(135deg, #224433 0%, #2a5540 100%);
+ border-color: #44cc66aa;
+}
+
+/* ── Action bar (bottom) ───────────────────────────────────────────────────── */
+.shift-ledger-window .slw-action-bar {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+ padding: 8px 12px 10px 12px;
+ background: #0d0d1a;
+ border-top: 1px solid #1e1e3a;
+}
+
+/* Process all */
+.shift-ledger-window .slw-btn--all {
+ background: linear-gradient(135deg, #1a2a3a 0%, #162030 100%);
+ color: #00d4ff;
+ border: 1px solid #00d4ff55;
+}
+
+.shift-ledger-window .slw-btn--all:hover {
+ background: linear-gradient(135deg, #1e3040 0%, #1a2838 100%);
+ border-color: #00d4ffaa;
+}
+
+/* Export to chat */
+.shift-ledger-window .slw-btn--export {
+ background: linear-gradient(135deg, #2a1a2a 0%, #221422 100%);
+ color: #cc88ff;
+ border: 1px solid #cc88ff55;
+}
+
+.shift-ledger-window .slw-btn--export:hover {
+ background: linear-gradient(135deg, #311e31 0%, #291828 100%);
+ border-color: #cc88ffaa;
+}
+
+/* ── Scrollbar theming ─────────────────────────────────────────────────────── */
+.shift-ledger-window .window-content::-webkit-scrollbar {
+ width: 6px;
+}
+
+.shift-ledger-window .window-content::-webkit-scrollbar-track {
+ background: #0d0d1a;
+}
+
+.shift-ledger-window .window-content::-webkit-scrollbar-thumb {
+ background: #2a2a4a;
+ border-radius: 3px;
+}
+
+.shift-ledger-window .window-content::-webkit-scrollbar-thumb:hover {
+ background: #00d4ff44;
+}
+
+/* ── ☠ NPC THREAT BOARD ──────────────────────────────────────────────────── */
+/* ──────────────────────────────────────────────────────────────────────────────
+ SLANPCThreatBoard — dark sci-fi NPC tactical overview
+ All rules scoped to .npc-threat-board
+ ────────────────────────────────────────────────────────────────────────────── */
+
+/* ── Window shell ─────────────────────────────────────────────────────────── */
+.npc-threat-board {
+ background: #0d0d1a;
+ color: #c8d0e0;
+ font-family: 'Helvetica Neue', Arial, sans-serif;
+ font-size: 12px;
+}
+
+.npc-threat-board .window-content {
+ background: #0d0d1a;
+ padding: 0;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+/* ── Outer wrapper ────────────────────────────────────────────────────────── */
+.npc-threat-board .ntb-wrapper {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
+}
+
+/* ── Filter bar ───────────────────────────────────────────────────────────── */
+.npc-threat-board .ntb-filterbar {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px 10px;
+ align-items: center;
+ padding: 6px 8px;
+ background: #111126;
+ border-bottom: 1px solid #2a2a4a;
+ flex-shrink: 0;
+}
+
+.npc-threat-board .ntb-filter-group {
+ display: flex;
+ align-items: center;
+ gap: 3px;
+}
+
+.npc-threat-board .ntb-sort-label {
+ color: #6a7090;
+ font-size: 11px;
+ margin-right: 2px;
+}
+
+.npc-threat-board .ntb-filter-btn,
+.npc-threat-board .ntb-threat-filter,
+.npc-threat-board .ntb-sort-btn {
+ background: #1a1a2e;
+ border: 1px solid #2e2e50;
+ color: #8890aa;
+ border-radius: 3px;
+ padding: 2px 7px;
+ font-size: 11px;
+ cursor: pointer;
+ transition: background 0.15s, color 0.15s, border-color 0.15s;
+ white-space: nowrap;
+ line-height: 1.6;
+}
+
+.npc-threat-board .ntb-filter-btn:hover,
+.npc-threat-board .ntb-threat-filter:hover,
+.npc-threat-board .ntb-sort-btn:hover {
+ background: #22224a;
+ color: #c8d0e0;
+ border-color: #4a4a88;
+}
+
+.npc-threat-board .ntb-filter-btn.ntb-active,
+.npc-threat-board .ntb-threat-filter.ntb-active,
+.npc-threat-board .ntb-sort-btn.ntb-active {
+ background: #2a2a5c;
+ color: #e0e8ff;
+ border-color: #5555aa;
+}
+
+/* Active threat filters — color-coded active state */
+.npc-threat-board .ntb-threat-filter[data-threat="low"].ntb-active { background: #1a3a22; border-color: #44cc66; color: #7deeaa; }
+.npc-threat-board .ntb-threat-filter[data-threat="med"].ntb-active { background: #3a2e10; border-color: #ffaa00; color: #ffd055; }
+.npc-threat-board .ntb-threat-filter[data-threat="high"].ntb-active { background: #3a1212; border-color: #cc2222; color: #ff6666; }
+.npc-threat-board .ntb-threat-filter[data-threat="elite"].ntb-active{ background: #2a1040; border-color: #9933cc; color: #cc77ff; }
+
+/* Dead toggle active */
+.npc-threat-board .ntb-toggle-dead.ntb-active {
+ background: #2a1010;
+ border-color: #882222;
+ color: #ff8888;
+}
+
+/* ── Scrollable card grid ─────────────────────────────────────────────────── */
+.npc-threat-board .ntb-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ padding: 8px;
+ overflow-y: auto;
+ flex: 1 1 0;
+ align-content: flex-start;
+}
+
+/* Thin scrollbar */
+.npc-threat-board .ntb-grid::-webkit-scrollbar { width: 6px; }
+.npc-threat-board .ntb-grid::-webkit-scrollbar-track { background: #0d0d1a; }
+.npc-threat-board .ntb-grid::-webkit-scrollbar-thumb { background: #2a2a4a; border-radius: 3px; }
+.npc-threat-board .ntb-grid::-webkit-scrollbar-thumb:hover { background: #4a4a88; }
+
+/* ── Empty state ──────────────────────────────────────────────────────────── */
+.npc-threat-board .ntb-empty {
+ width: 100%;
+ text-align: center;
+ padding: 40px 20px;
+ color: #4a5070;
+ font-style: italic;
+ font-size: 13px;
+}
+
+/* ── NPC Card ─────────────────────────────────────────────────────────────── */
+.npc-threat-board .ntb-card {
+ position: relative;
+ background: #13132a;
+ border: 1px solid #2a2a4a;
+ border-radius: 5px;
+ padding: 7px 8px 6px;
+ width: 175px;
+ flex-shrink: 0;
+ cursor: pointer;
+ transition: background 0.15s, border-color 0.2s, transform 0.1s;
+ overflow: hidden;
+}
+
+.npc-threat-board .ntb-card:hover {
+ background: #1a1a38;
+ border-color: #4a4a88;
+ transform: translateY(-1px);
+}
+
+/* Card threat border accent — left bar */
+.npc-threat-board .ntb-card::before {
+ content: '';
+ position: absolute;
+ left: 0; top: 0; bottom: 0;
+ width: 3px;
+ border-radius: 5px 0 0 5px;
+}
+.npc-threat-board .ntb-card.threat-low::before { background: #44cc66; }
+.npc-threat-board .ntb-card.threat-med::before { background: #ffaa00; }
+.npc-threat-board .ntb-card.threat-high::before { background: #cc3333; }
+.npc-threat-board .ntb-card.threat-elite::before { background: #9933cc; }
+
+/* Dead card dimming */
+.npc-threat-board .ntb-card.ntb-dead {
+ opacity: 0.55;
+ filter: grayscale(60%);
+}
+
+/* Dead skull overlay */
+.npc-threat-board .ntb-skull-overlay {
+ position: absolute;
+ top: 4px; right: 5px;
+ font-size: 16px;
+ pointer-events: none;
+ opacity: 0.8;
+ z-index: 2;
+}
+
+/* ── Card header: portrait + name + threat badge ──────────────────────────── */
+.npc-threat-board .ntb-card-header {
+ display: flex;
+ align-items: flex-start;
+ gap: 7px;
+ margin-bottom: 6px;
+}
+
+.npc-threat-board .ntb-portrait-wrap {
+ flex-shrink: 0;
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ border: 2px solid #2a2a4a;
+ overflow: hidden;
+}
+
+/* Portrait border color mirrors threat */
+.npc-threat-board .ntb-border-threat-low { border-color: #44cc66; }
+.npc-threat-board .ntb-border-threat-med { border-color: #ffaa00; }
+.npc-threat-board .ntb-border-threat-high { border-color: #cc3333; }
+.npc-threat-board .ntb-border-threat-elite { border-color: #9933cc; }
+
+.npc-threat-board .ntb-portrait {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+
+.npc-threat-board .ntb-name-block {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding-top: 1px;
+}
+
+.npc-threat-board .ntb-name {
+ font-weight: bold;
+ font-size: 12px;
+ color: #d8e0f0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ line-height: 1.2;
+}
+
+/* Scene indicator badge */
+.npc-threat-board .ntb-scene-badge {
+ display: inline-block;
+ margin-left: 4px;
+ color: #44aaff;
+ font-size: 10px;
+ vertical-align: middle;
+}
+
+/* ── Threat badge ─────────────────────────────────────────────────────────── */
+.npc-threat-board .ntb-threat-badge {
+ display: inline-block;
+ font-size: 10px;
+ font-weight: bold;
+ padding: 1px 5px;
+ border-radius: 3px;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ line-height: 1.5;
+ align-self: flex-start;
+}
+
+.npc-threat-board .ntb-threat-badge.threat-low { background: #1a3a22; color: #7deeaa; border: 1px solid #44cc66; }
+.npc-threat-board .ntb-threat-badge.threat-med { background: #3a2e10; color: #ffd055; border: 1px solid #ffaa00; }
+.npc-threat-board .ntb-threat-badge.threat-high { background: #3a1212; color: #ff7070; border: 1px solid #cc3333; }
+.npc-threat-board .ntb-threat-badge.threat-elite { background: #2a1040; color: #cc77ff; border: 1px solid #9933cc; }
+
+/* ── HP bar + controls ────────────────────────────────────────────────────── */
+.npc-threat-board .ntb-hp-row {
+ margin-bottom: 5px;
+}
+
+.npc-threat-board .ntb-hp-track {
+ width: 100%;
+ height: 6px;
+ background: #0a0a18;
+ border-radius: 3px;
+ overflow: hidden;
+ margin-bottom: 3px;
+ border: 1px solid #1e1e38;
+}
+
+.npc-threat-board .ntb-hp-fill {
+ height: 100%;
+ border-radius: 3px;
+ transition: width 0.3s ease, background 0.3s ease;
+ min-width: 1px;
+}
+
+.npc-threat-board .ntb-hp-controls {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ justify-content: center;
+}
+
+.npc-threat-board .ntb-hp-text {
+ font-size: 11px;
+ color: #9090b8;
+ min-width: 40px;
+ text-align: center;
+ flex: 1;
+}
+
+.npc-threat-board .ntb-hp-btn {
+ background: #1c1c38;
+ border: 1px solid #2e2e50;
+ color: #8890aa;
+ border-radius: 3px;
+ width: 18px;
+ height: 18px;
+ font-size: 14px;
+ line-height: 1;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ padding: 0;
+ transition: background 0.15s, color 0.15s;
+}
+
+.npc-threat-board .ntb-hp-btn:hover {
+ background: #28284e;
+ color: #d0d8ee;
+ border-color: #5555aa;
+}
+
+.npc-threat-board .ntb-hp-dec:hover { color: #ff6666; border-color: #882222; }
+.npc-threat-board .ntb-hp-inc:hover { color: #66dd88; border-color: #226644; }
+
+/* ── Attribute mini-grid ──────────────────────────────────────────────────── */
+.npc-threat-board .ntb-attrs {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 3px;
+ margin-bottom: 5px;
+ background: #0f0f22;
+ border-radius: 3px;
+ padding: 4px 2px;
+ border: 1px solid #1e1e38;
+}
+
+.npc-threat-board .ntb-attr {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1px;
+}
+
+.npc-threat-board .ntb-attr-label {
+ font-size: 9px;
+ color: #5a6080;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ line-height: 1;
+}
+
+.npc-threat-board .ntb-attr-val {
+ font-size: 13px;
+ font-weight: bold;
+ color: #b8c8e0;
+ line-height: 1.1;
+}
+
+/* ── Damage / Armor badges + status dots ──────────────────────────────────── */
+.npc-threat-board .ntb-badges {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ flex-wrap: wrap;
+}
+
+.npc-threat-board .ntb-badge {
+ display: inline-block;
+ font-size: 10px;
+ padding: 1px 5px;
+ border-radius: 3px;
+ line-height: 1.5;
+ white-space: nowrap;
+}
+
+.npc-threat-board .ntb-badge-dmg {
+ background: #2a1010;
+ color: #ff9090;
+ border: 1px solid #551111;
+}
+
+.npc-threat-board .ntb-badge-armor {
+ background: #10202a;
+ color: #70bbee;
+ border: 1px solid #11334a;
+}
+
+.npc-threat-board .ntb-statuses {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 3px;
+ align-items: center;
+}
+
+.npc-threat-board .ntb-status-dot {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: #aa8800;
+ border: 1px solid #ffcc44;
+ cursor: default;
+}
+
+/* ── 📋 BPN TRACKER ──────────────────────────────────────────────────── */
+/* ============================================================
+ BPN Tracker — scoped to .bpn-tracker-window
+ Dark sci-fi aesthetic, SLA Industries
+ ============================================================ */
+
+.bpn-tracker-window .window-content {
+ background: #0f1117;
+ color: #c8cdd8;
+ font-family: 'Roboto', 'Arial', sans-serif;
+ font-size: 13px;
+ padding: 0;
+ overflow-y: auto;
+}
+
+/* ── Inner wrapper ─────────────────────────────────────────── */
+.bpn-tracker-window .bpn-tracker-inner {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 12px;
+}
+
+/* ── Empty state ───────────────────────────────────────────── */
+.bpn-tracker-window .bpn-empty-state {
+ color: #555;
+ font-style: italic;
+ text-align: center;
+ padding: 20px 0;
+}
+
+/* ── Mission card ──────────────────────────────────────────── */
+.bpn-tracker-window .bpn-mission-card {
+ background: #14181f;
+ border: 1px solid #2a2d35;
+ border-left: 4px solid #3399ff; /* overridden inline per type */
+ border-radius: 4px;
+ padding: 10px 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+/* Card header row */
+.bpn-tracker-window .bpn-card-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+/* BPN code — monospace, prominent */
+.bpn-tracker-window .bpn-code {
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 14px;
+ font-weight: 700;
+ color: #e0e6f0;
+ letter-spacing: 0.04em;
+ flex-shrink: 0;
+}
+
+/* Type badge */
+.bpn-tracker-window .bpn-type-badge {
+ font-size: 11px;
+ font-weight: 600;
+ padding: 2px 7px;
+ border-radius: 3px;
+ border: 1px solid;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ flex-shrink: 0;
+}
+
+/* Status badge */
+.bpn-tracker-window .bpn-status-badge {
+ font-size: 11px;
+ font-weight: 600;
+ padding: 2px 7px;
+ border-radius: 3px;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ flex-shrink: 0;
+}
+
+.bpn-tracker-window .bpn-status-pending { background: #2a2a2a; color: #888; border: 1px solid #555; }
+.bpn-tracker-window .bpn-status-active { background: #0d2a2a; color: #00e5e5; border: 1px solid #00bfbf; }
+.bpn-tracker-window .bpn-status-complete { background: #0d2a1a; color: #44dd66; border: 1px solid #33aa44; }
+.bpn-tracker-window .bpn-status-failed { background: #2a0d0d; color: #ff5555; border: 1px solid #cc2222; }
+
+/* Clear button — right-aligned */
+.bpn-tracker-window .bpn-clear-btn {
+ margin-left: auto;
+ background: transparent;
+ border: 1px solid #443333;
+ color: #885555;
+ font-size: 11px;
+ padding: 2px 8px;
+ border-radius: 3px;
+ cursor: pointer;
+ transition: background 0.15s, color 0.15s;
+ flex-shrink: 0;
+}
+.bpn-tracker-window .bpn-clear-btn:hover {
+ background: #3a1a1a;
+ color: #ff6666;
+ border-color: #cc2222;
+}
+
+/* ── Card rows ─────────────────────────────────────────────── */
+.bpn-tracker-window .bpn-card-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.bpn-tracker-window .bpn-label {
+ font-size: 11px;
+ color: #666;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ flex-shrink: 0;
+}
+
+.bpn-tracker-window .bpn-label-type {
+ margin-left: 8px;
+}
+
+.bpn-tracker-window .bpn-status-select,
+.bpn-tracker-window .bpn-type-select {
+ background: #1a1e26;
+ border: 1px solid #2e323c;
+ color: #c8cdd8;
+ font-size: 12px;
+ padding: 3px 6px;
+ border-radius: 3px;
+ cursor: pointer;
+}
+.bpn-tracker-window .bpn-status-select:focus,
+.bpn-tracker-window .bpn-type-select:focus {
+ outline: none;
+ border-color: #3399ff;
+}
+
+/* Reward row */
+.bpn-tracker-window .bpn-reward-row {
+ align-items: center;
+}
+
+.bpn-tracker-window .bpn-reward-label {
+ font-size: 12px;
+ color: #8899aa;
+}
+.bpn-tracker-window .bpn-reward-label strong {
+ color: #c8b46e;
+ font-family: 'Courier New', Courier, monospace;
+}
+
+/* Pay Out button — gold/amber */
+.bpn-tracker-window .bpn-payout-btn {
+ background: #3a2a00;
+ border: 1px solid #8a6a00;
+ color: #ffc233;
+ font-size: 11px;
+ font-weight: 600;
+ padding: 3px 10px;
+ border-radius: 3px;
+ cursor: pointer;
+ letter-spacing: 0.04em;
+ transition: background 0.15s;
+ margin-left: auto;
+}
+.bpn-tracker-window .bpn-payout-btn:hover {
+ background: #5a4400;
+ border-color: #ccaa00;
+ color: #ffe066;
+}
+
+/* ── Description / Objectives fields ──────────────────────── */
+.bpn-tracker-window .bpn-card-field {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.bpn-tracker-window .bpn-textarea {
+ background: #0d1014;
+ border: 1px solid #252830;
+ color: #b0b8c8;
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 12px;
+ line-height: 1.5;
+ padding: 6px 8px;
+ border-radius: 3px;
+ resize: vertical;
+ width: 100%;
+ box-sizing: border-box;
+ transition: border-color 0.15s;
+}
+.bpn-tracker-window .bpn-textarea:focus {
+ outline: none;
+ border-color: #3399ff;
+ background: #0f1318;
+}
+.bpn-tracker-window .bpn-textarea::placeholder {
+ color: #3a3f4a;
+}
+
+/* ── Card footer ───────────────────────────────────────────── */
+.bpn-tracker-window .bpn-card-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 8px;
+ padding-top: 4px;
+ border-top: 1px solid #1e2228;
+}
+
+/* PC badge strip */
+.bpn-tracker-window .bpn-pc-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 5px;
+}
+
+.bpn-tracker-window .bpn-pc-badge {
+ background: #1a2030;
+ border: 1px solid #2e3a50;
+ color: #7899cc;
+ font-size: 11px;
+ padding: 2px 8px;
+ border-radius: 10px;
+ letter-spacing: 0.03em;
+}
+
+.bpn-tracker-window .bpn-pc-badge-unassigned {
+ background: #1a1a1a;
+ border-color: #333;
+ color: #555;
+}
+
+/* Save button */
+.bpn-tracker-window .bpn-save-btn {
+ background: #0d2233;
+ border: 1px solid #1a5080;
+ color: #55aadd;
+ font-size: 11px;
+ font-weight: 600;
+ padding: 3px 12px;
+ border-radius: 3px;
+ cursor: pointer;
+ letter-spacing: 0.03em;
+ transition: background 0.15s;
+ flex-shrink: 0;
+}
+.bpn-tracker-window .bpn-save-btn:hover {
+ background: #163348;
+ border-color: #3399ff;
+ color: #88ccff;
+}
+
+/* ── Unassigned section ────────────────────────────────────── */
+.bpn-tracker-window .bpn-unassigned-section {
+ background: #111318;
+ border: 1px dashed #2a2a2a;
+ border-radius: 4px;
+ padding: 8px 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.bpn-tracker-window .bpn-unassigned-label {
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 0.07em;
+ color: #444;
+}
+
+/* ── Assign BPN Panel ──────────────────────────────────────── */
+.bpn-tracker-window .bpn-assign-panel {
+ background: #111520;
+ border: 1px solid #1e2535;
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.bpn-tracker-window .bpn-assign-header {
+ background: #151a28;
+ border-bottom: 1px solid #1e2535;
+ color: #6688bb;
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.09em;
+ text-transform: uppercase;
+ padding: 7px 12px;
+}
+
+.bpn-tracker-window .bpn-assign-body {
+ display: flex;
+ gap: 12px;
+ padding: 10px 12px;
+}
+
+/* PC checklist — left column */
+.bpn-tracker-window .bpn-assign-pcs {
+ min-width: 130px;
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.bpn-tracker-window .bpn-pc-checklist {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ max-height: 160px;
+ overflow-y: auto;
+ padding-right: 4px;
+}
+
+.bpn-tracker-window .bpn-pc-check {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ color: #8899aa;
+ cursor: pointer;
+}
+.bpn-tracker-window .bpn-pc-check:hover {
+ color: #aabbcc;
+}
+.bpn-tracker-window .bpn-pc-check input[type="checkbox"] {
+ accent-color: #3399ff;
+ cursor: pointer;
+}
+
+/* Fields — right column */
+.bpn-tracker-window .bpn-assign-fields {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 7px;
+}
+
+.bpn-tracker-window .bpn-assign-row {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ flex-wrap: wrap;
+}
+
+.bpn-tracker-window .bpn-assign-code,
+.bpn-tracker-window .bpn-assign-reward {
+ background: #0d1014;
+ border: 1px solid #252830;
+ color: #c8cdd8;
+ font-family: 'Courier New', Courier, monospace;
+ font-size: 12px;
+ padding: 4px 8px;
+ border-radius: 3px;
+ flex: 1;
+ min-width: 80px;
+}
+.bpn-tracker-window .bpn-assign-code:focus,
+.bpn-tracker-window .bpn-assign-reward:focus {
+ outline: none;
+ border-color: #3399ff;
+}
+
+.bpn-tracker-window .bpn-assign-type,
+.bpn-tracker-window .bpn-assign-status {
+ background: #1a1e26;
+ border: 1px solid #2e323c;
+ color: #c8cdd8;
+ font-size: 12px;
+ padding: 3px 6px;
+ border-radius: 3px;
+ cursor: pointer;
+ flex: 1;
+ min-width: 70px;
+}
+.bpn-tracker-window .bpn-assign-type:focus,
+.bpn-tracker-window .bpn-assign-status:focus {
+ outline: none;
+ border-color: #3399ff;
+}
+
+.bpn-tracker-window .bpn-assign-submit-row {
+ justify-content: flex-end;
+ padding-top: 4px;
+ border-top: 1px solid #1e2228;
+ margin-top: 2px;
+}
+
+/* Assign BPN submit button */
+.bpn-tracker-window .bpn-assign-btn {
+ background: #0d2a18;
+ border: 1px solid #1a6640;
+ color: #44cc88;
+ font-size: 12px;
+ font-weight: 700;
+ letter-spacing: 0.05em;
+ padding: 5px 16px;
+ border-radius: 3px;
+ cursor: pointer;
+ text-transform: uppercase;
+ transition: background 0.15s;
+}
+.bpn-tracker-window .bpn-assign-btn:hover {
+ background: #164030;
+ border-color: #33cc77;
+ color: #88ffbb;
+}
+
+/* ── Scrollbar styling (webkit) ────────────────────────────── */
+.bpn-tracker-window .window-content::-webkit-scrollbar,
+.bpn-tracker-window .bpn-pc-checklist::-webkit-scrollbar {
+ width: 5px;
+}
+.bpn-tracker-window .window-content::-webkit-scrollbar-track,
+.bpn-tracker-window .bpn-pc-checklist::-webkit-scrollbar-track {
+ background: #0a0c10;
+}
+.bpn-tracker-window .window-content::-webkit-scrollbar-thumb,
+.bpn-tracker-window .bpn-pc-checklist::-webkit-scrollbar-thumb {
+ background: #2a3040;
+ border-radius: 3px;
+}
+.bpn-tracker-window .window-content::-webkit-scrollbar-thumb:hover,
+.bpn-tracker-window .bpn-pc-checklist::-webkit-scrollbar-thumb:hover {
+ background: #3a4455;
+}
+
+/* ── ⚡ CONDITION APPLICATOR ──────────────────────────────────────────────────── */
+/* ============================================================
+ SLA Condition Applicator — temp_condapp.css
+ Scoped to .condition-applicator-window
+ ============================================================ */
+
+.condition-applicator-window .window-content {
+ padding: 0;
+ background: #0d0d1a;
+ color: #c8d0e0;
+ font-family: 'Roboto Condensed', 'Arial Narrow', Arial, sans-serif;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+/* ---- Wrapper ---- */
+.condition-applicator-window .cap-wrapper {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
+}
+
+/* ============================================================
+ HEADER
+ ============================================================ */
+.condition-applicator-window .cap-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 12px;
+ background: #13132b;
+ border-bottom: 1px solid #00d4ff33;
+ flex-shrink: 0;
+ gap: 8px;
+}
+
+.condition-applicator-window .cap-header-info {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+}
+
+.condition-applicator-window .cap-header-label {
+ font-size: 0.7rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: #6a7a9a;
+}
+
+.condition-applicator-window .cap-selected-name {
+ font-size: 0.95rem;
+ font-weight: 700;
+ color: #00d4ff;
+ text-shadow: 0 0 8px #00d4ff88;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 180px;
+}
+
+.condition-applicator-window .cap-header-actions {
+ display: flex;
+ gap: 6px;
+ flex-shrink: 0;
+}
+
+/* ---- Buttons ---- */
+.condition-applicator-window .cap-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ padding: 4px 10px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ border: 1px solid #2a2a4a;
+ border-radius: 3px;
+ background: #1a1a2e;
+ color: #8a9bbf;
+ cursor: pointer;
+ transition: background 0.15s, border-color 0.15s, color 0.15s;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.condition-applicator-window .cap-btn:hover {
+ background: #22224a;
+ border-color: #00d4ff55;
+ color: #c8d0e0;
+}
+
+.condition-applicator-window .cap-btn--toggle.cap-btn--active {
+ background: #0a2a3a;
+ border-color: #00d4ff;
+ color: #00d4ff;
+ box-shadow: 0 0 6px #00d4ff44;
+}
+
+.condition-applicator-window .cap-btn--danger {
+ border-color: #3a1a1a;
+ color: #c05050;
+}
+
+.condition-applicator-window .cap-btn--danger:hover {
+ background: #2a0f0f;
+ border-color: #ff4444;
+ color: #ff6666;
+}
+
+/* ============================================================
+ BODY — two-column layout
+ ============================================================ */
+.condition-applicator-window .cap-body {
+ display: flex;
+ flex: 1 1 0;
+ overflow: hidden;
+}
+
+/* ============================================================
+ LEFT PANEL — PC Roster (30%)
+ ============================================================ */
+.condition-applicator-window .cap-pc-list {
+ width: 30%;
+ min-width: 140px;
+ display: flex;
+ flex-direction: column;
+ border-right: 1px solid #1e1e3a;
+ background: #0a0a18;
+ flex-shrink: 0;
+}
+
+.condition-applicator-window .cap-pc-list-header {
+ padding: 6px 10px;
+ font-size: 0.65rem;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: #4a5a7a;
+ background: #0d0d22;
+ border-bottom: 1px solid #1a1a30;
+ flex-shrink: 0;
+}
+
+.condition-applicator-window .cap-pc-list-scroll {
+ overflow-y: auto;
+ flex: 1 1 0;
+ scrollbar-width: thin;
+ scrollbar-color: #1e2a4a #0a0a18;
+}
+
+.condition-applicator-window .cap-pc-list-scroll::-webkit-scrollbar {
+ width: 4px;
+}
+.condition-applicator-window .cap-pc-list-scroll::-webkit-scrollbar-track {
+ background: #0a0a18;
+}
+.condition-applicator-window .cap-pc-list-scroll::-webkit-scrollbar-thumb {
+ background: #1e2a4a;
+ border-radius: 2px;
+}
+
+/* PC Row */
+.condition-applicator-window .cap-pc-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 7px 10px;
+ cursor: pointer;
+ border-bottom: 1px solid #111128;
+ border-left: 3px solid transparent;
+ transition: background 0.12s, border-left-color 0.12s;
+ gap: 4px;
+}
+
+.condition-applicator-window .cap-pc-row:hover {
+ background: #12122a;
+ border-left-color: #3a3a6a;
+}
+
+.condition-applicator-window .cap-pc-row--selected {
+ background: #141430;
+ border-left-color: #ff8800;
+}
+
+.condition-applicator-window .cap-pc-row--selected .cap-pc-name {
+ color: #ffaa44;
+}
+
+.condition-applicator-window .cap-pc-name {
+ font-size: 0.82rem;
+ font-weight: 600;
+ color: #a8b8d8;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ flex: 1 1 0;
+}
+
+.condition-applicator-window .cap-pc-dots {
+ display: flex;
+ gap: 3px;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ flex-shrink: 0;
+ max-width: 60px;
+}
+
+.condition-applicator-window .cap-cond-dot {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: #00d4ff22;
+ border: 1px solid #00d4ff66;
+ font-size: 0.45rem;
+ color: #00d4ff;
+}
+
+.condition-applicator-window .cap-no-pcs {
+ padding: 12px 10px;
+ font-size: 0.75rem;
+ color: #4a5a7a;
+ font-style: italic;
+}
+
+/* ============================================================
+ RIGHT PANEL — Condition Grid (70%)
+ ============================================================ */
+.condition-applicator-window .cap-cond-panel {
+ flex: 1 1 0;
+ overflow-y: auto;
+ padding: 8px;
+ background: #0d0d1a;
+ scrollbar-width: thin;
+ scrollbar-color: #1e2a4a #0d0d1a;
+}
+
+.condition-applicator-window .cap-cond-panel::-webkit-scrollbar {
+ width: 4px;
+}
+.condition-applicator-window .cap-cond-panel::-webkit-scrollbar-track {
+ background: #0d0d1a;
+}
+.condition-applicator-window .cap-cond-panel::-webkit-scrollbar-thumb {
+ background: #1e2a4a;
+ border-radius: 2px;
+}
+
+.condition-applicator-window .cap-cond-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ align-content: flex-start;
+}
+
+/* Condition Tile */
+.condition-applicator-window .cap-cond-tile {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 80px;
+ height: 80px;
+ padding: 6px 4px 4px;
+ border-radius: 4px;
+ background: #111128;
+ border: 1px solid #1e1e3a;
+ cursor: pointer;
+ transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
+ gap: 4px;
+ overflow: hidden;
+ text-align: center;
+}
+
+.condition-applicator-window .cap-cond-tile:hover {
+ background: #181838;
+ border-color: #3a3a7a;
+}
+
+.condition-applicator-window .cap-cond-tile--active {
+ background: #062030;
+ border: 1px solid #00d4ff;
+ box-shadow: 0 0 8px #00d4ff44, inset 0 0 12px #00d4ff18;
+}
+
+.condition-applicator-window .cap-cond-tile--active .cap-cond-icon,
+.condition-applicator-window .cap-cond-tile--active .cap-cond-label {
+ color: #00d4ff;
+ text-shadow: 0 0 6px #00d4ff88;
+}
+
+.condition-applicator-window .cap-cond-icon {
+ font-size: 1.4rem;
+ color: #6a7a9a;
+ line-height: 1;
+ transition: color 0.15s, text-shadow 0.15s;
+}
+
+.condition-applicator-window .cap-cond-label {
+ font-size: 0.6rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: #7a8aaa;
+ line-height: 1.2;
+ word-break: break-word;
+ transition: color 0.15s, text-shadow 0.15s;
+}
+
+/* Dice Modifier Badge */
+.condition-applicator-window .cap-mod-badge {
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ font-size: 0.5rem;
+ font-weight: 700;
+ background: #8b0000;
+ color: #ffaaaa;
+ border-radius: 2px;
+ padding: 1px 3px;
+ line-height: 1.3;
+ white-space: nowrap;
+ border: 1px solid #cc222233;
+ text-transform: uppercase;
+ letter-spacing: 0.02em;
+}
+
+.condition-applicator-window .cap-no-conds {
+ padding: 16px;
+ font-size: 0.8rem;
+ color: #4a5a7a;
+ font-style: italic;
+}
+
+/* ============================================================
+ STATUS BAR
+ ============================================================ */
+.condition-applicator-window .cap-statusbar {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 5px 10px;
+ background: #0a0a18;
+ border-top: 1px solid #1a1a30;
+ flex-shrink: 0;
+ min-height: 30px;
+ overflow: hidden;
+}
+
+.condition-applicator-window .cap-statusbar-label {
+ font-size: 0.65rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: #4a5a7a;
+ flex-shrink: 0;
+}
+
+.condition-applicator-window .cap-statusbar-tags {
+ display: flex;
+ flex-wrap: nowrap;
+ gap: 5px;
+ overflow-x: auto;
+ scrollbar-width: none;
+ align-items: center;
+}
+
+.condition-applicator-window .cap-statusbar-tags::-webkit-scrollbar {
+ display: none;
+}
+
+.condition-applicator-window .cap-status-tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 7px;
+ border-radius: 10px;
+ background: #062030;
+ border: 1px solid #00d4ff55;
+ color: #00d4ff;
+ font-size: 0.65rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ white-space: nowrap;
+}
+
+.condition-applicator-window .cap-status-none {
+ font-size: 0.7rem;
+ color: #3a4a6a;
+ font-style: italic;
+}
+
+.condition-applicator-window .cap-status-mode {
+ font-size: 0.7rem;
+ color: #00d4ff88;
+ font-style: italic;
+}
diff --git a/system.json b/system.json
index edca695..4c141b5 100644
--- a/system.json
+++ b/system.json
@@ -2,7 +2,7 @@
"id": "zero-engine",
"title": "Zero Engine (SLA Industries)",
"description": "A basic game system for SLA Industries using the Zero Engine rules.",
- "version": "1.6.5",
+ "version": "1.7.0",
"compatibility": {
"minimum": 13,
"verified": "14.359"
diff --git a/zero-engine.mjs b/zero-engine.mjs
index 6d7ec16..deeab84 100644
--- a/zero-engine.mjs
+++ b/zero-engine.mjs
@@ -3741,34 +3741,34 @@ Hooks.once("setup", () => {
/**
* Ready hook
*/
-// ── GM SCENE CONTROLS: PC STATUS BUTTON ──────────────────────────────────────
-// Opens the draggable SLAGMStatusWindow (GM only).
+// ── GM SCENE CONTROLS: SLA TOOLS ─────────────────────────────────────────────
+// Adds all SLA GM tool buttons to the scene controls (GM only).
// Foundry v14: controls is an object; v13: controls is an array.
Hooks.on("getSceneControlButtons", (controls) => {
if (!game.user?.isGM) return;
- const openStatus = () => SLAGMStatusWindow.open();
-
- const tool = {
- name: "sla-pc-status",
- title: "PC Status Board",
- icon: "fas fa-clipboard-list",
- visible: true,
- button: true,
- onChange: openStatus, // v14
- onClick: openStatus // v13 compat
- };
+ const SLA_TOOLS = [
+ { name: "sla-pc-status", title: "PC Status Board", icon: "fas fa-clipboard-list", fn: () => SLAGMStatusWindow.open() },
+ { name: "sla-credit", title: "Credit Distribution", icon: "fas fa-credit-card", fn: () => SLACreditDistributionTool.open() },
+ { name: "sla-ledger", title: "Shift Ledger", icon: "fas fa-book-open", fn: () => SLAShiftLedger.open() },
+ { name: "sla-threat", title: "NPC Threat Board", icon: "fas fa-skull", fn: () => SLANPCThreatBoard.open() },
+ { name: "sla-bpn", title: "BPN Tracker", icon: "fas fa-clipboard-check", fn: () => SLABPNTracker.open() },
+ { name: "sla-condapp", title: "Condition Applicator", icon: "fas fa-bolt", fn: () => SLAConditionApplicator.open() },
+ ];
// Foundry v14: controls is a plain object keyed by group name
if (controls && !Array.isArray(controls)) {
- controls["sla-gm-tools"] = {
- name: "sla-gm-tools",
- title: "SLA GM Tools",
- icon: "fas fa-clipboard-list",
- visible: true,
- button: true,
- onChange: openStatus
- };
+ const grp = controls.tokens ?? controls.token ?? null;
+ if (grp) {
+ grp.tools ??= {};
+ for (const t of SLA_TOOLS) {
+ grp.tools[t.name] = {
+ name: t.name, title: t.title, icon: t.icon,
+ visible: true, button: true, toggle: false,
+ onChange: t.fn
+ };
+ }
+ }
return;
}
@@ -3776,8 +3776,15 @@ Hooks.on("getSceneControlButtons", (controls) => {
const tokenGroup = controls.find(c => c.name === "token" || c.name === "tokens");
if (tokenGroup) {
tokenGroup.tools ??= [];
- if (Array.isArray(tokenGroup.tools)) tokenGroup.tools.push(tool);
- else tokenGroup.tools["sla-pc-status"] = tool;
+ for (const t of SLA_TOOLS) {
+ const entry = {
+ name: t.name, title: t.title, icon: t.icon,
+ visible: true, button: true,
+ onChange: t.fn, onClick: t.fn
+ };
+ if (Array.isArray(tokenGroup.tools)) tokenGroup.tools.push(entry);
+ else tokenGroup.tools[t.name] = entry;
+ }
}
});
@@ -7050,6 +7057,1596 @@ class SLAGMStatusWindow extends Application {
});
this._hooks = [];
SLAGMStatusWindow._inst = null;
+
+// ── 💳 CREDIT DISTRIBUTION ────────────────────────────────────────────────────────────
+class SLACreditDistributionTool extends Application {
+ constructor(...args) {
+ super(...args);
+ this._hooks = [];
+ }
+
+ static get defaultOptions() {
+ return foundry.utils.mergeObject(super.defaultOptions, {
+ id: 'sla-credit-tool',
+ title: '💳 Credit Distribution',
+ width: 560,
+ height: 'auto',
+ resizable: true,
+ classes: ['zero-engine', 'credit-tool-window']
+ });
+ }
+
+ static open() {
+ if (!SLACreditDistributionTool._inst) SLACreditDistributionTool._inst = new SLACreditDistributionTool();
+ SLACreditDistributionTool._inst.render(true);
+ return SLACreditDistributionTool._inst;
+ }
+
+ _pcActors() {
+ return (game.actors ?? []).filter(a => a.type === 'character' && a.system?.details?.isPlayerCharacter);
+ }
+
+ _calcIncome(actor) {
+ const fin = actor.system?.finances?.income ?? {};
+ return (fin.salary ?? 0) + (fin.bpnReward ?? 0) + (fin.other ?? 0);
+ }
+
+ _calcExpenses(actor) {
+ const exp = actor.system?.finances?.expenses ?? {};
+ return (exp.accommodation ?? 0) + (exp.drugs ?? 0) + (exp.subscriptions ?? 0) + (exp.other ?? 0) + (exp.bulletTax ?? 0);
+ }
+
+ async _renderInner() {
+ const pcs = this._pcActors();
+
+ // Build rows for each PC
+ const rows = pcs.map(actor => {
+ const credits = actor.system?.details?.credits ?? 0;
+ const scl = actor.system?.details?.scl ?? 0;
+ const income = this._calcIncome(actor);
+ const expenses = this._calcExpenses(actor);
+ const debt = actor.system?.finances?.debt ?? 0;
+ const net = income - expenses;
+
+ const netClass = net >= 0 ? 'ctw-positive' : 'ctw-negative';
+ const debtClass = debt > 0 ? 'ctw-debt' : '';
+ const imgSrc = actor.img ?? 'icons/svg/mystery-man.svg';
+
+ return `
+
+
+
+ ${actor.name}
+ SCL ${scl}
+ |
+
+
+ |
+ ${income} |
+ ${expenses} |
+ ${debt} |
+ ${net} |
+
+
+ |
+
`;
+ }).join('');
+
+ const emptyMsg = pcs.length === 0
+ ? `| No player characters found. Mark actors as Player Characters on their sheets. |
`
+ : '';
+
+ const html = `
+
+
+
+ Player Credit Ledger
+
+
+
+
+
+ ${rows}
+ ${emptyMsg}
+
+
+
+
+
+
+
`;
+
+ return $(html);
+ }
+
+ async _render(force, options) {
+ await super._render(force, options);
+ if (!this._hooks.length) {
+ const refresh = () => { if (this.rendered) this.render(false); };
+ this._hooks.push(
+ Hooks.on('updateActor', refresh)
+ );
+ }
+ }
+
+ async close(options) {
+ this._hooks.forEach((id) => {
+ Hooks.off('updateActor', id);
+ });
+ this._hooks = [];
+ SLACreditDistributionTool._inst = null;
+ return super.close(options);
+ }
+
+ activateListeners(html) {
+ super.activateListeners(html);
+
+ // --- Save individual credits ---
+ html.find('.ctw-btn-save').on('click', async (ev) => {
+ const actorId = ev.currentTarget.dataset.actorId;
+ const actor = game.actors.get(actorId);
+ if (!actor) return ui.notifications.error(`Actor not found: ${actorId}`);
+
+ const input = html.find(`#credits-input-${actorId}`);
+ const newValue = parseInt(input.val(), 10);
+
+ if (isNaN(newValue) || newValue < 0) {
+ return ui.notifications.error(`Invalid credit value for ${actor.name}.`);
+ }
+
+ try {
+ await actor.update({ 'system.details.credits': newValue });
+ ui.notifications.info(`${actor.name}: credits set to ¢${newValue}.`);
+ } catch (err) {
+ console.error('SLACreditDistributionTool | save error', err);
+ ui.notifications.error(`Failed to update credits for ${actor.name}.`);
+ }
+ });
+
+ // --- Pay All PCs ---
+ html.find('#ctw-pay-all-btn').on('click', async () => {
+ const pcs = this._pcActors();
+ if (pcs.length === 0) return ui.notifications.error('No player characters found.');
+
+ const amount = parseInt(html.find('#ctw-bulk-amount').val(), 10);
+ if (isNaN(amount) || amount <= 0) {
+ return ui.notifications.error('Enter a valid amount greater than 0.');
+ }
+
+ const payType = html.find('input[name="ctw-pay-type"]:checked').val();
+
+ let successCount = 0;
+ for (const actor of pcs) {
+ try {
+ if (payType === 'custom') {
+ // Custom: add directly to credits balance
+ const currentCredits = actor.system?.details?.credits ?? 0;
+ await actor.update({ 'system.details.credits': currentCredits + amount });
+ } else if (payType === 'salary') {
+ await actor.update({ 'system.finances.income.salary': amount });
+ } else if (payType === 'bpnReward') {
+ await actor.update({ 'system.finances.income.bpnReward': amount });
+ }
+ successCount++;
+ } catch (err) {
+ console.error(`SLACreditDistributionTool | pay error for ${actor.name}`, err);
+ ui.notifications.error(`Failed to pay ${actor.name}.`);
+ }
+ }
+
+ if (successCount > 0) {
+ const label = payType === 'custom' ? `¢${amount} credits` : `${payType} set to ¢${amount}`;
+ ui.notifications.info(`Paid ${successCount} PC(s): ${label}.`);
+ }
+ });
+
+ // --- Deduct All Expenses ---
+ html.find('#ctw-deduct-expenses-btn').on('click', async () => {
+ const pcs = this._pcActors();
+ if (pcs.length === 0) return ui.notifications.error('No player characters found.');
+
+ let successCount = 0;
+ for (const actor of pcs) {
+ try {
+ const currentCredits = actor.system?.details?.credits ?? 0;
+ const totalExpenses = this._calcExpenses(actor);
+ const newCredits = Math.max(0, currentCredits - totalExpenses);
+ await actor.update({ 'system.details.credits': newCredits });
+ successCount++;
+ } catch (err) {
+ console.error(`SLACreditDistributionTool | deduct error for ${actor.name}`, err);
+ ui.notifications.error(`Failed to deduct expenses for ${actor.name}.`);
+ }
+ }
+
+ if (successCount > 0) {
+ ui.notifications.info(`Expenses deducted from ${successCount} PC(s).`);
+ }
+ });
+ }
+}
+
+SLACreditDistributionTool._inst = null;
+
+// ── 📒 SHIFT LEDGER ────────────────────────────────────────────────────────────
+class SLAShiftLedger extends Application {
+ constructor(...args) {
+ super(...args);
+ this._hooks = [];
+ }
+
+ static get defaultOptions() {
+ return foundry.utils.mergeObject(super.defaultOptions, {
+ id: 'sla-shift-ledger',
+ title: '📒 Shift Ledger',
+ width: 700,
+ height: 'auto',
+ resizable: true,
+ classes: ['zero-engine', 'shift-ledger-window']
+ });
+ }
+
+ static open() {
+ if (!SLAShiftLedger._inst) SLAShiftLedger._inst = new SLAShiftLedger();
+ SLAShiftLedger._inst.render(true);
+ return SLAShiftLedger._inst;
+ }
+
+ _pcActors() {
+ return (game.actors ?? []).filter(a => a.type === 'character' && a.system?.details?.isPlayerCharacter);
+ }
+
+ _calcNet(actor) {
+ const fin = actor.system?.finances ?? {};
+ const inc = fin.income ?? {};
+ const exp = fin.expenses ?? {};
+ const totalIncome = (inc.salary || 0) + (inc.bpnReward || 0) + (inc.other || 0);
+ const totalExpenses = (exp.accommodation || 0) + (exp.drugs || 0) + (exp.subscriptions || 0) + (exp.other || 0) + (exp.bulletTax || 0);
+ return { totalIncome, totalExpenses, net: totalIncome - totalExpenses };
+ }
+
+ async _processShift(actor) {
+ const { totalIncome, totalExpenses } = this._calcNet(actor);
+ const currentCredits = actor.system?.details?.credits ?? 0;
+ const currentDebt = actor.system?.finances?.debt ?? 0;
+
+ let newCredits = currentCredits + totalIncome - totalExpenses;
+ let newDebt = currentDebt;
+
+ if (newCredits < 0) {
+ newDebt = currentDebt + Math.abs(newCredits);
+ newCredits = 0;
+ }
+
+ try {
+ await actor.update({
+ 'system.details.credits': newCredits,
+ 'system.finances.debt': newDebt
+ });
+ ui.notifications.info(`Shift processed for ${actor.name}: ${newCredits}¢ credits, ${newDebt}¢ debt.`);
+ } catch (err) {
+ console.error('SLAShiftLedger | Failed to process shift for', actor.name, err);
+ ui.notifications.error(`Failed to process shift for ${actor.name}. See console for details.`);
+ }
+ }
+
+ async _processAllShifts() {
+ const pcs = this._pcActors();
+ if (!pcs.length) {
+ ui.notifications.error('No player characters found to process.');
+ return;
+ }
+ for (const actor of pcs) {
+ await this._processShift(actor);
+ }
+ ui.notifications.info(`Shift processed for all ${pcs.length} player character(s).`);
+ }
+
+ async _exportToChat() {
+ const pcs = this._pcActors();
+ if (!pcs.length) {
+ ui.notifications.error('No player characters found to export.');
+ return;
+ }
+
+ let lines = ['📒 Shift Ledger Summary
'];
+ lines.push('| Character | SCL | Credits | Income | Expenses | Net | Debt |
');
+
+ let totalCredits = 0, totalIncome = 0, totalExpenses = 0, totalDebt = 0;
+
+ for (const actor of pcs) {
+ const { totalIncome: inc, totalExpenses: exp, net } = this._calcNet(actor);
+ const credits = actor.system?.details?.credits ?? 0;
+ const scl = actor.system?.details?.scl ?? '–';
+ const debt = actor.system?.finances?.debt ?? 0;
+ totalCredits += credits;
+ totalIncome += inc;
+ totalExpenses += exp;
+ totalDebt += debt;
+ const netColor = net >= 0 ? '#44cc66' : '#cc2222';
+ const debtColor = debt > 0 ? '#cc2222' : 'inherit';
+ lines.push(
+ `` +
+ `| ${actor.name} | ` +
+ `${scl} | ` +
+ `${credits}¢ | ` +
+ `+${inc}¢ | ` +
+ `-${exp}¢ | ` +
+ `${net >= 0 ? '+' : ''}${net}¢ | ` +
+ `${debt}¢ | ` +
+ `
`
+ );
+ }
+
+ const groupNet = totalIncome - totalExpenses;
+ const groupNetColor = groupNet >= 0 ? '#44cc66' : '#cc2222';
+ lines.push(
+ `` +
+ `| GROUP TOTALS | ` +
+ `${totalCredits}¢ | ` +
+ `+${totalIncome}¢ | ` +
+ `-${totalExpenses}¢ | ` +
+ `${groupNet >= 0 ? '+' : ''}${groupNet}¢ | ` +
+ `${totalDebt}¢ | ` +
+ `
`
+ );
+ lines.push('
');
+
+ try {
+ await ChatMessage.create({
+ content: lines.join(''),
+ whisper: [game.user.id],
+ speaker: { alias: 'Shift Ledger' }
+ });
+ ui.notifications.info('Shift ledger exported to chat (GM only).');
+ } catch (err) {
+ console.error('SLAShiftLedger | Failed to export to chat:', err);
+ ui.notifications.error('Failed to export ledger to chat. See console for details.');
+ }
+ }
+
+ async _renderInner() {
+ const pcs = this._pcActors();
+
+ // ── Aggregate totals ──────────────────────────────────────────────────────
+ let aggCredits = 0, aggIncome = 0, aggExpenses = 0, aggDebt = 0;
+ for (const actor of pcs) {
+ const { totalIncome, totalExpenses } = this._calcNet(actor);
+ aggCredits += actor.system?.details?.credits ?? 0;
+ aggIncome += totalIncome;
+ aggExpenses += totalExpenses;
+ aggDebt += actor.system?.finances?.debt ?? 0;
+ }
+ const aggNet = aggIncome - aggExpenses;
+
+ // ── Summary bar ──────────────────────────────────────────────────────────
+ const summaryBar = `
+
+
+ Group Credits
+ ${aggCredits}¢
+
+
+ Total Income
+ +${aggIncome}¢
+
+
+ Total Expenses
+ -${aggExpenses}¢
+
+
+ Net This Shift
+ ${aggNet >= 0 ? '+' : ''}${aggNet}¢
+
+
+ Total Debt
+ ${aggDebt}¢
+
+
`;
+
+ // ── Per-PC cards ─────────────────────────────────────────────────────────
+ let pcCards = '';
+ if (pcs.length === 0) {
+ pcCards = 'No player characters found. Mark actors as Player Characters in their sheet details.
';
+ } else {
+ for (const actor of pcs) {
+ const det = actor.system?.details ?? {};
+ const fin = actor.system?.finances ?? {};
+ const inc = fin.income ?? {};
+ const exp = fin.expenses ?? {};
+ const { totalIncome, totalExpenses, net } = this._calcNet(actor);
+ const credits = det.credits ?? 0;
+ const scl = det.scl ?? '–';
+ const debt = fin.debt ?? 0;
+
+ pcCards += `
+
+
+
+
+
+
Income
+
+ Salary
+ ${inc.salary ?? 0}¢
+
+
+ BPN Reward
+ ${inc.bpnReward ?? 0}¢
+
+
+ Other
+ ${inc.other ?? 0}¢
+
+
+ Total Income
+ +${totalIncome}¢
+
+
+
+
+
Expenses
+
+ Accommodation
+ ${exp.accommodation ?? 0}¢
+
+
+ Drugs
+ ${exp.drugs ?? 0}¢
+
+
+ Subscriptions
+ ${exp.subscriptions ?? 0}¢
+
+
+ Other
+ ${exp.other ?? 0}¢
+
+
+ Bullet Tax
+ ${exp.bulletTax ?? 0}¢
+
+
+ Total Expenses
+ -${totalExpenses}¢
+
+
+
+
+
Summary
+
+ Net Balance
+ ${net >= 0 ? '+' : ''}${net}¢
+
+
+ Debt
+ ${debt}¢
+
+
+
+
+
`;
+ }
+ }
+
+ // ── Global action bar ────────────────────────────────────────────────────
+ const actionBar = `
+
+
+
+
`;
+
+ const html = `
+
+ ${summaryBar}
+
+ ${pcCards}
+
+ ${actionBar}
+
`;
+
+ return $(html);
+ }
+
+ async _render(force, options) {
+ await super._render(force, options);
+ if (!this._hooks.length) {
+ const refresh = () => { if (this.rendered) this.render(false); };
+ this._hooks.push(Hooks.on('updateActor', refresh));
+ }
+ }
+
+ async close(options) {
+ this._hooks.forEach(id => Hooks.off('updateActor', id));
+ this._hooks = [];
+ SLAShiftLedger._inst = null;
+ return super.close(options);
+ }
+
+ activateListeners(html) {
+ super.activateListeners(html);
+
+ // Per-PC process shift buttons
+ html.find('.slw-btn--process').on('click', async (ev) => {
+ const actorId = ev.currentTarget.dataset.actorId;
+ const actor = game.actors.get(actorId);
+ if (!actor) {
+ ui.notifications.error('Actor not found.');
+ return;
+ }
+ await this._processShift(actor);
+ });
+
+ // Process all shifts button
+ html.find('#slw-process-all').on('click', async () => {
+ await this._processAllShifts();
+ });
+
+ // Export to chat button
+ html.find('#slw-export-chat').on('click', async () => {
+ await this._exportToChat();
+ });
+ }
+}
+
+SLAShiftLedger._inst = null;
+
+// ── ☠ NPC THREAT BOARD ────────────────────────────────────────────────────────────
+// ──────────────────────────────────────────────────────────────────────────────
+// SLANPCThreatBoard — GM tactical overview of all NPC actors
+// ──────────────────────────────────────────────────────────────────────────────
+
+function threatLabel(val) {
+ if (!val || val <= 2) return { label: 'Low', cls: 'threat-low' };
+ if (val <= 3) return { label: 'Med', cls: 'threat-med' };
+ if (val <= 5) return { label: 'High', cls: 'threat-high' };
+ return { label: 'Elite', cls: 'threat-elite' };
+}
+
+function hpColor(cur, max) {
+ const pct = max > 0 ? cur / max : 1;
+ if (pct > 0.50) return '#44cc66';
+ if (pct > 0.25) return '#ffaa00';
+ return '#cc2222';
+}
+
+class SLANPCThreatBoard extends Application {
+ constructor(...args) {
+ super(...args);
+ this._hooks = [];
+ this._filterThreat = 'all'; // 'all' | 'low' | 'med' | 'high' | 'elite'
+ this._filterScene = false; // show only actors present in the active scene
+ this._showDead = false; // include dead actors
+ this._sortBy = 'threat'; // 'name' | 'threat' | 'health'
+ }
+
+ // ── AppV1 config ─────────────────────────────────────────────────────────────
+
+ static get defaultOptions() {
+ return foundry.utils.mergeObject(super.defaultOptions, {
+ id: 'sla-npc-threat-board',
+ title: '☠ NPC Threat Board',
+ width: 640,
+ height: 500,
+ resizable: true,
+ classes: ['zero-engine', 'npc-threat-board']
+ });
+ }
+
+ static open() {
+ if (!SLANPCThreatBoard._inst) SLANPCThreatBoard._inst = new SLANPCThreatBoard();
+ SLANPCThreatBoard._inst.render(true);
+ return SLANPCThreatBoard._inst;
+ }
+
+ // ── Data helpers ──────────────────────────────────────────────────────────────
+
+ _npcActors() {
+ return (game.actors ?? []).filter(
+ a => a.type === 'npc' || (a.type === 'character' && !a.system?.details?.isPlayerCharacter)
+ );
+ }
+
+ /** Returns a Set of actorIds present in the currently active scene. */
+ _sceneActorIds() {
+ const ids = new Set();
+ const tokens = game.scenes?.active?.tokens ?? [];
+ for (const t of tokens) {
+ if (t.actorId) ids.add(t.actorId);
+ }
+ return ids;
+ }
+
+ // ── HTML builder ──────────────────────────────────────────────────────────────
+
+ async _renderInner() {
+ const sceneIds = this._sceneActorIds();
+ let actors = this._npcActors();
+
+ // ── Filter: scene ────────────────────────────────────────────────────────
+ if (this._filterScene) {
+ actors = actors.filter(a => sceneIds.has(a.id));
+ }
+
+ // ── Filter: dead ─────────────────────────────────────────────────────────
+ if (!this._showDead) {
+ actors = actors.filter(a => {
+ const dead = a.statuses?.has('dead') ?? false;
+ const hpVal = a.system?.health?.value ?? 0;
+ return !dead && hpVal > 0;
+ });
+ }
+
+ // ── Filter: threat tier ──────────────────────────────────────────────────
+ if (this._filterThreat !== 'all') {
+ actors = actors.filter(a => {
+ const t = a.system?.threat || 0;
+ const { cls } = threatLabel(t);
+ return cls === `threat-${this._filterThreat}`;
+ });
+ }
+
+ // ── Sort ─────────────────────────────────────────────────────────────────
+ if (this._sortBy === 'name') {
+ actors.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
+ } else if (this._sortBy === 'threat') {
+ actors.sort((a, b) => (b.system?.threat || 0) - (a.system?.threat || 0));
+ } else if (this._sortBy === 'health') {
+ actors.sort((a, b) => {
+ const pctA = (a.system?.health?.max || 0) > 0
+ ? (a.system.health.value || 0) / a.system.health.max : 0;
+ const pctB = (b.system?.health?.max || 0) > 0
+ ? (b.system.health.value || 0) / b.system.health.max : 0;
+ return pctA - pctB; // lowest HP first — most urgent
+ });
+ }
+
+ // ── Build card HTML ──────────────────────────────────────────────────────
+ const cards = actors.map(actor => {
+ const hpCur = actor.system?.health?.value ?? 0;
+ const hpMax = actor.system?.health?.max ?? 0;
+ const threat = actor.system?.threat ?? 0;
+ const str = actor.system?.attributes?.strength ?? 0;
+ const agi = actor.system?.attributes?.agility ?? 0;
+ const wit = actor.system?.attributes?.wits ?? 0;
+ const emp = actor.system?.attributes?.empathy ?? 0;
+ const dmg = actor.system?.damage ?? '—';
+ const armor = actor.system?.armor ?? 0;
+ const imgSrc = actor.img ?? 'icons/svg/mystery-man.svg';
+
+ const { label: tLabel, cls: tCls } = threatLabel(threat);
+ const hpPct = hpMax > 0 ? Math.max(0, Math.min(1, hpCur / hpMax)) : 0;
+ const hpBarPct = Math.round(hpPct * 100);
+ const barColor = hpColor(hpCur, hpMax);
+
+ const isDead = (actor.statuses?.has('dead') ?? false) || hpCur <= 0;
+
+ // Status dots (all active statuses other than 'dead')
+ const statuses = actor.statuses ? [...actor.statuses].filter(s => s !== 'dead') : [];
+ const statusDots = statuses.slice(0, 6).map(s =>
+ ``
+ ).join('');
+
+ // Scene indicator
+ const inScene = sceneIds.has(actor.id)
+ ? `◉`
+ : '';
+
+ return `
+
+ ${isDead ? '
💀
' : ''}
+
+
+
+
+
+
+
+ ${hpCur} / ${hpMax}
+
+
+
+
+
+
STR${str}
+
AGI${agi}
+
WIT${wit}
+
EMP${emp}
+
+
+
+
⚔ ${dmg}
+
🛡 ${armor}
+
${statusDots}
+
+
`;
+ }).join('');
+
+ const emptyMsg = actors.length === 0
+ ? `No NPCs match the current filters.
`
+ : '';
+
+ // ── Filter / sort bar HTML ────────────────────────────────────────────────
+ const html = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sort:
+
+
+
+
+
+
+
+
+ ${cards}
+ ${emptyMsg}
+
+
+
`;
+
+ return $(html);
+ }
+
+ // ── Lifecycle ─────────────────────────────────────────────────────────────────
+
+ async _render(force, options) {
+ await super._render(force, options);
+ if (!this._hooks.length) {
+ const refresh = () => { if (this.rendered) this.render(false); };
+ this._hooks.push(
+ Hooks.on('updateActor', refresh),
+ Hooks.on('createActor', refresh),
+ Hooks.on('deleteActor', refresh)
+ );
+ }
+ }
+
+ async close(options) {
+ const evts = ['updateActor', 'createActor', 'deleteActor'];
+ this._hooks.forEach((id, i) => Hooks.off(evts[i], id));
+ this._hooks = [];
+ SLANPCThreatBoard._inst = null;
+ return super.close(options);
+ }
+
+ // ── Listeners ─────────────────────────────────────────────────────────────────
+
+ activateListeners(html) {
+ super.activateListeners(html);
+
+ // ── Scene filter buttons ─────────────────────────────────────────────────
+ html.find('[data-filter-scene]').on('click', (ev) => {
+ this._filterScene = ev.currentTarget.dataset.filterScene === 'true';
+ this.render(false);
+ });
+
+ // ── Dead toggle ──────────────────────────────────────────────────────────
+ html.find('.ntb-toggle-dead').on('click', () => {
+ this._showDead = !this._showDead;
+ this.render(false);
+ });
+
+ // ── Threat filter buttons ────────────────────────────────────────────────
+ html.find('.ntb-threat-filter').on('click', (ev) => {
+ this._filterThreat = ev.currentTarget.dataset.threat || 'all';
+ this.render(false);
+ });
+
+ // ── Sort buttons ─────────────────────────────────────────────────────────
+ html.find('.ntb-sort-btn').on('click', (ev) => {
+ this._sortBy = ev.currentTarget.dataset.sort || 'threat';
+ this.render(false);
+ });
+
+ // ── HP decrement ─────────────────────────────────────────────────────────
+ html.find('.ntb-hp-dec').on('click', async (ev) => {
+ ev.stopPropagation();
+ const actorId = ev.currentTarget.dataset.actorId;
+ const actor = game.actors?.get(actorId);
+ if (!actor) return;
+ const cur = actor.system?.health?.value ?? 0;
+ const newVal = cur - 1;
+ try {
+ await actor.update({ 'system.health.value': newVal });
+ } catch (err) {
+ console.error('SLANPCThreatBoard | HP decrement error', err);
+ ui.notifications?.error(`Failed to update HP for ${actor.name}.`);
+ }
+ });
+
+ // ── HP increment ─────────────────────────────────────────────────────────
+ html.find('.ntb-hp-inc').on('click', async (ev) => {
+ ev.stopPropagation();
+ const actorId = ev.currentTarget.dataset.actorId;
+ const actor = game.actors?.get(actorId);
+ if (!actor) return;
+ const cur = actor.system?.health?.value ?? 0;
+ const hpMax = actor.system?.health?.max ?? 0;
+ const newVal = hpMax > 0 ? Math.min(hpMax, cur + 1) : cur + 1;
+ try {
+ await actor.update({ 'system.health.value': newVal });
+ } catch (err) {
+ console.error('SLANPCThreatBoard | HP increment error', err);
+ ui.notifications?.error(`Failed to update HP for ${actor.name}.`);
+ }
+ });
+
+ // ── Open actor sheet on card click ───────────────────────────────────────
+ html.find('.ntb-card').on('click', (ev) => {
+ // Don't open sheet if a HP button was clicked
+ if (ev.target.closest('.ntb-hp-btn')) return;
+ const actorId = ev.currentTarget.dataset.actorId;
+ const actor = game.actors?.get(actorId);
+ if (actor) actor.sheet.render(true);
+ });
+ }
+}
+
+SLANPCThreatBoard._inst = null;
+
+// ── 📋 BPN TRACKER ────────────────────────────────────────────────────────────
+class SLABPNTracker extends Application {
+ constructor(...args) {
+ super(...args);
+ this._hooks = [];
+ this._editMode = false;
+ }
+
+ static get defaultOptions() {
+ return foundry.utils.mergeObject(super.defaultOptions, {
+ id: 'sla-bpn-tracker',
+ title: '📋 BPN Tracker',
+ width: 520,
+ height: 'auto',
+ resizable: true,
+ classes: ['zero-engine', 'bpn-tracker-window']
+ });
+ }
+
+ static open() {
+ if (!SLABPNTracker._inst) SLABPNTracker._inst = new SLABPNTracker();
+ SLABPNTracker._inst.render(true);
+ return SLABPNTracker._inst;
+ }
+
+ _pcActors() {
+ return (game.actors ?? []).filter(a => a.type === 'character' && a.system?.details?.isPlayerCharacter);
+ }
+
+ _groupByBPN(actors) {
+ const groups = new Map();
+ for (const actor of actors) {
+ const code = actor.system?.bpn?.code?.trim() || '';
+ const key = code === '' ? '__unassigned__' : code;
+ if (!groups.has(key)) groups.set(key, []);
+ groups.get(key).push(actor);
+ }
+ return groups;
+ }
+
+ _bpnTypeColors() {
+ return {
+ 'Blue': { bg: '#1a3a6e', border: '#3399ff', text: '#88ccff' },
+ 'White': { bg: '#3a3a3a', border: '#cccccc', text: '#ffffff' },
+ 'Green': { bg: '#1a3a1a', border: '#44cc66', text: '#88ff88' },
+ 'Red': { bg: '#3a1a1a', border: '#cc2222', text: '#ff6666' },
+ 'Black': { bg: '#0d0d0d', border: '#555555', text: '#aaaaaa' },
+ 'Silver': { bg: '#2a2a3a', border: '#aaaacc', text: '#ccccff' }
+ };
+ }
+
+ _buildTypeBadge(bpnType) {
+ const colors = this._bpnTypeColors();
+ const c = colors[bpnType] ?? { bg: '#222', border: '#666', text: '#ccc' };
+ return `${bpnType || 'Unknown'}`;
+ }
+
+ _buildStatusBadge(status) {
+ const cls = {
+ 'Active': 'bpn-status-active',
+ 'Pending': 'bpn-status-pending',
+ 'Complete': 'bpn-status-complete',
+ 'Failed': 'bpn-status-failed'
+ }[status] ?? 'bpn-status-pending';
+ return `${status || 'Pending'}`;
+ }
+
+ _buildMissionCard(code, actors) {
+ const rep = actors[0];
+ const bpn = rep.system?.bpn ?? {};
+ const bpnType = bpn.type || 'Unknown';
+ const status = bpn.status || 'Pending';
+ const reward = bpn.reward ?? 0;
+ const description = bpn.description || '';
+ const objectives = bpn.objectives || '';
+
+ const colors = this._bpnTypeColors();
+ const c = colors[bpnType] ?? { bg: '#1a1a1a', border: '#555', text: '#aaa' };
+
+ const actorIds = actors.map(a => a.id).join(',');
+
+ const pcBadges = actors.map(a =>
+ `${a.name}`
+ ).join('');
+
+ const statusOptions = ['Pending', 'Active', 'Complete', 'Failed'].map(s =>
+ ``
+ ).join('');
+
+ const typeOptions = ['Blue', 'White', 'Green', 'Red', 'Black', 'Silver', 'Unknown'].map(t =>
+ ``
+ ).join('');
+
+ return `
+
+
+
+
+
+
+
+
+
+
+
+ 💳 Reward: ${reward}¢
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
+ }
+
+ _buildAssignPanel(actors) {
+ const pcCheckboxes = actors.map(a =>
+ ``
+ ).join('');
+
+ const typeOptions = ['Blue', 'White', 'Green', 'Red', 'Black', 'Silver'].map(t =>
+ ``
+ ).join('');
+
+ const statusOptions = ['Pending', 'Active', 'Complete', 'Failed'].map(s =>
+ ``
+ ).join('');
+
+ return `
+
+
+
+
+
+
${pcCheckboxes}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
`;
+ }
+
+ async _renderInner() {
+ const actors = this._pcActors();
+ const groups = this._groupByBPN(actors);
+
+ let cardsHtml = '';
+
+ // Render assigned BPNs first (all non-unassigned keys)
+ for (const [key, group] of groups.entries()) {
+ if (key === '__unassigned__') continue;
+ cardsHtml += this._buildMissionCard(key, group);
+ }
+
+ // Render unassigned section
+ const unassigned = groups.get('__unassigned__') ?? [];
+ let unassignedHtml = '';
+ if (unassigned.length > 0) {
+ const badges = unassigned.map(a => `${a.name}`).join('');
+ unassignedHtml = `
+
+
Unassigned PCs
+
${badges}
+
`;
+ }
+
+ const assignPanel = this._buildAssignPanel(actors);
+
+ const html = `
+
+
+ ${cardsHtml || '
No active BPNs. Assign one below.
'}
+
+ ${unassignedHtml}
+ ${assignPanel}
+
`;
+
+ return $(html);
+ }
+
+ async _render(force, options) {
+ await super._render(force, options);
+ if (!this._hooks.length) {
+ const refresh = () => { if (this.rendered) this.render(false); };
+ this._hooks.push(Hooks.on('updateActor', refresh));
+ this._hooks.push(Hooks.on('createActor', refresh));
+ this._hooks.push(Hooks.on('deleteActor', refresh));
+ }
+ }
+
+ async close(options) {
+ this._hooks.forEach(id => Hooks.off('updateActor', id));
+ this._hooks = [];
+ SLABPNTracker._inst = null;
+ return super.close(options);
+ }
+
+ activateListeners(html) {
+ super.activateListeners(html);
+
+ // Status change — update all PCs on this BPN
+ html.on('change', '.bpn-status-select', async (ev) => {
+ const card = ev.currentTarget.closest('.bpn-mission-card');
+ const actorIds = card.dataset.actorIds.split(',').filter(Boolean);
+ const newStatus = ev.currentTarget.value;
+ for (const id of actorIds) {
+ const actor = game.actors.get(id);
+ if (!actor) continue;
+ try {
+ await actor.update({ 'system.bpn.status': newStatus });
+ } catch (err) {
+ console.error(`BPN Tracker | Failed to update status for ${actor.name}:`, err);
+ }
+ }
+ // Update the badge in-place without full re-render
+ const badge = card.querySelector('.bpn-status-badge');
+ if (badge) {
+ badge.className = 'bpn-status-badge ' + ({
+ 'Active': 'bpn-status-active',
+ 'Pending': 'bpn-status-pending',
+ 'Complete': 'bpn-status-complete',
+ 'Failed': 'bpn-status-failed'
+ }[newStatus] ?? 'bpn-status-pending');
+ badge.textContent = newStatus;
+ }
+ ui.notifications.info(`BPN status updated to "${newStatus}".`);
+ });
+
+ // Type change — update all PCs on this BPN
+ html.on('change', '.bpn-type-select', async (ev) => {
+ const card = ev.currentTarget.closest('.bpn-mission-card');
+ const actorIds = card.dataset.actorIds.split(',').filter(Boolean);
+ const newType = ev.currentTarget.value;
+ const colors = this._bpnTypeColors();
+ const c = colors[newType] ?? { bg: '#222', border: '#666', text: '#ccc' };
+ for (const id of actorIds) {
+ const actor = game.actors.get(id);
+ if (!actor) continue;
+ try {
+ await actor.update({ 'system.bpn.type': newType });
+ } catch (err) {
+ console.error(`BPN Tracker | Failed to update type for ${actor.name}:`, err);
+ }
+ }
+ // Update badge and border color in-place
+ const badge = card.querySelector('.bpn-type-badge');
+ if (badge) {
+ badge.style.background = c.bg;
+ badge.style.borderColor = c.border;
+ badge.style.color = c.text;
+ badge.textContent = newType;
+ }
+ card.style.borderLeftColor = c.border;
+ ui.notifications.info(`BPN type updated to "${newType}".`);
+ });
+
+ // Pay Out — add reward to each PC's credits
+ html.on('click', '[data-action="payout"]', async (ev) => {
+ const card = ev.currentTarget.closest('.bpn-mission-card');
+ const actorIds = card.dataset.actorIds.split(',').filter(Boolean);
+ let paid = 0;
+ for (const id of actorIds) {
+ const actor = game.actors.get(id);
+ if (!actor) continue;
+ const reward = actor.system?.bpn?.reward ?? 0;
+ const cur = actor.system?.details?.credits ?? 0;
+ try {
+ await actor.update({ 'system.details.credits': cur + reward });
+ paid++;
+ } catch (err) {
+ console.error(`BPN Tracker | Failed to pay out reward to ${actor.name}:`, err);
+ }
+ }
+ ui.notifications.info(`Reward paid out to ${paid} PC(s).`);
+ });
+
+ // Save description + objectives back to all PCs on this BPN
+ html.on('click', '[data-action="save-bpn"]', async (ev) => {
+ const card = ev.currentTarget.closest('.bpn-mission-card');
+ const actorIds = card.dataset.actorIds.split(',').filter(Boolean);
+ const description = card.querySelector('.bpn-description').value;
+ const objectives = card.querySelector('.bpn-objectives').value;
+ for (const id of actorIds) {
+ const actor = game.actors.get(id);
+ if (!actor) continue;
+ try {
+ await actor.update({
+ 'system.bpn.description': description,
+ 'system.bpn.objectives': objectives
+ });
+ } catch (err) {
+ console.error(`BPN Tracker | Failed to save BPN fields for ${actor.name}:`, err);
+ }
+ }
+ ui.notifications.info('BPN description and objectives saved.');
+ });
+
+ // Clear BPN — remove code and status from all PCs on this BPN
+ html.on('click', '[data-action="clear-bpn"]', async (ev) => {
+ const card = ev.currentTarget.closest('.bpn-mission-card');
+ const actorIds = card.dataset.actorIds.split(',').filter(Boolean);
+ for (const id of actorIds) {
+ const actor = game.actors.get(id);
+ if (!actor) continue;
+ try {
+ await actor.update({ 'system.bpn.code': '', 'system.bpn.status': '' });
+ } catch (err) {
+ console.error(`BPN Tracker | Failed to clear BPN for ${actor.name}:`, err);
+ }
+ }
+ ui.notifications.info('BPN cleared from assigned PCs.');
+ if (this.rendered) this.render(false);
+ });
+
+ // Assign BPN — write BPN data to selected PCs
+ html.on('click', '[data-action="assign-bpn"]', async (ev) => {
+ const panel = ev.currentTarget.closest('.bpn-assign-panel');
+ const selectedIds = [...panel.querySelectorAll('.bpn-assign-pc-check:checked')].map(el => el.value);
+ if (selectedIds.length === 0) {
+ ui.notifications.warn('No PCs selected for BPN assignment.');
+ return;
+ }
+ const code = panel.querySelector('.bpn-assign-code').value.trim();
+ if (!code) {
+ ui.notifications.warn('Please enter a BPN code.');
+ return;
+ }
+ const bpnType = panel.querySelector('.bpn-assign-type').value;
+ const status = panel.querySelector('.bpn-assign-status').value;
+ const reward = Number(panel.querySelector('.bpn-assign-reward').value) || 0;
+ const description = panel.querySelector('.bpn-assign-desc').value;
+ const objectives = panel.querySelector('.bpn-assign-obj').value;
+
+ for (const id of selectedIds) {
+ const actor = game.actors.get(id);
+ if (!actor) continue;
+ try {
+ await actor.update({
+ 'system.bpn.code': code,
+ 'system.bpn.type': bpnType,
+ 'system.bpn.status': status,
+ 'system.bpn.reward': reward,
+ 'system.bpn.description': description,
+ 'system.bpn.objectives': objectives
+ });
+ } catch (err) {
+ console.error(`BPN Tracker | Failed to assign BPN to ${actor.name}:`, err);
+ }
+ }
+ ui.notifications.info(`BPN "${code}" assigned to ${selectedIds.length} PC(s).`);
+
+ // Clear the form
+ panel.querySelector('.bpn-assign-code').value = '';
+ panel.querySelector('.bpn-assign-reward').value = '0';
+ panel.querySelector('.bpn-assign-desc').value = '';
+ panel.querySelector('.bpn-assign-obj').value = '';
+ panel.querySelectorAll('.bpn-assign-pc-check').forEach(el => el.checked = false);
+
+ if (this.rendered) this.render(false);
+ });
+ }
+}
+
+SLABPNTracker._inst = null;
+
+// ── ⚡ CONDITION APPLICATOR ────────────────────────────────────────────────────────────
+class SLAConditionApplicator extends Application {
+ constructor(...args) {
+ super(...args);
+ this._hooks = [];
+ this._selectedActorId = null;
+ this._allPCsMode = false;
+ }
+
+ static get defaultOptions() {
+ return foundry.utils.mergeObject(super.defaultOptions, {
+ id: 'sla-condition-applicator',
+ title: '⚡ Condition Applicator',
+ width: 600,
+ height: 520,
+ resizable: true,
+ classes: ['zero-engine', 'condition-applicator-window']
+ });
+ }
+
+ static open() {
+ if (!SLAConditionApplicator._inst) SLAConditionApplicator._inst = new SLAConditionApplicator();
+ SLAConditionApplicator._inst.render(true);
+ return SLAConditionApplicator._inst;
+ }
+
+ _pcActors() {
+ return (game.actors ?? []).filter(a => a.type === 'character' && a.system?.details?.isPlayerCharacter);
+ }
+
+ async _renderInner() {
+ const pcs = this._pcActors();
+
+ // Default selected actor to first PC if none set or stale
+ if (!this._selectedActorId || !pcs.find(a => a.id === this._selectedActorId)) {
+ this._selectedActorId = pcs.length ? pcs[0].id : null;
+ }
+
+ const selActor = pcs.find(a => a.id === this._selectedActorId) ?? null;
+
+ // --- Build PC roster rows ---
+ let pcRowsHtml = '';
+ for (const actor of pcs) {
+ const activeConditions = SLA_CONDITIONS.filter(c => actor.statuses?.has(c.id));
+ const dotBadges = activeConditions.map(c =>
+ ``
+ ).join('');
+ const isSelected = actor.id === this._selectedActorId && !this._allPCsMode;
+ pcRowsHtml += `
+
+ ${actor.name}
+ ${dotBadges}
+
`;
+ }
+ if (!pcRowsHtml) {
+ pcRowsHtml = 'No PC actors found.
';
+ }
+
+ // --- Build condition grid tiles ---
+ let condTilesHtml = '';
+ for (const cond of SLA_CONDITIONS) {
+ let isActive = false;
+ if (this._allPCsMode) {
+ // Active if ALL PCs have it (useful visual: show partial state too)
+ const pcCount = pcs.length;
+ const hasCount = pcs.filter(a => a.statuses?.has(cond.id)).length;
+ if (hasCount === pcCount && pcCount > 0) isActive = true;
+ } else if (selActor) {
+ isActive = selActor.statuses?.has(cond.id) ?? false;
+ }
+
+ // Build dice modifier badge text
+ let modBadge = '';
+ if (cond.diceModifiers && Object.keys(cond.diceModifiers).length) {
+ const parts = Object.entries(cond.diceModifiers).map(([key, val]) => {
+ const sign = val >= 0 ? '+' : '−';
+ const abs = Math.abs(val);
+ return `${sign}${abs} ${key}`;
+ });
+ modBadge = `${parts.join(' ')}`;
+ }
+
+ const descAttr = cond.description ? ` title="${cond.description.replace(/"/g, '"')}"` : '';
+
+ condTilesHtml += `
+
+ ${modBadge}
+
+ ${cond.label}
+
`;
+ }
+ if (!condTilesHtml) {
+ condTilesHtml = 'No conditions defined.
';
+ }
+
+ // --- Active conditions status bar ---
+ let statusBarHtml = '';
+ if (this._allPCsMode) {
+ statusBarHtml = 'All PCs mode active';
+ } else if (selActor) {
+ const activeConds = SLA_CONDITIONS.filter(c => selActor.statuses?.has(c.id));
+ if (activeConds.length) {
+ statusBarHtml = activeConds.map(c =>
+ ` ${c.label}`
+ ).join('');
+ } else {
+ statusBarHtml = 'No active conditions';
+ }
+ } else {
+ statusBarHtml = 'No PC selected';
+ }
+
+ const selectedLabel = this._allPCsMode
+ ? 'All PCs'
+ : (selActor ? selActor.name : '—');
+
+ const html = `
+
+
+
+
+
+
+
Active:
+
+ ${statusBarHtml}
+
+
+
`;
+
+ return $(html);
+ }
+
+ async _render(force, options) {
+ await super._render(force, options);
+ if (!this._hooks.length) {
+ const refresh = () => { if (this.rendered) this.render(false); };
+ this._hooks.push(
+ Hooks.on('updateActor', refresh),
+ Hooks.on('createActiveEffect', refresh),
+ Hooks.on('deleteActiveEffect', refresh),
+ Hooks.on('updateActiveEffect', refresh)
+ );
+ }
+ }
+
+ async close(options) {
+ const evts = ['updateActor', 'createActiveEffect', 'deleteActiveEffect', 'updateActiveEffect'];
+ this._hooks.forEach((id, i) => Hooks.off(evts[i], id));
+ this._hooks = [];
+ SLAConditionApplicator._inst = null;
+ return super.close(options);
+ }
+
+ activateListeners(html) {
+ super.activateListeners(html);
+
+ // PC row click — select actor
+ html.find('.cap-pc-row').on('click', (ev) => {
+ const actorId = ev.currentTarget.dataset.actorId;
+ if (actorId) {
+ this._selectedActorId = actorId;
+ this._allPCsMode = false;
+ this.render(false);
+ }
+ });
+
+ // Condition tile click — toggle condition
+ html.find('.cap-cond-tile').on('click', async (ev) => {
+ const condId = ev.currentTarget.dataset.condId;
+ const isActive = ev.currentTarget.dataset.active === 'true';
+ const pcs = this._pcActors();
+
+ if (this._allPCsMode) {
+ for (const actor of pcs) {
+ try {
+ await actor.toggleStatusEffect(condId, { active: !isActive });
+ } catch (err) {
+ console.error(`SLAConditionApplicator | Failed to toggle ${condId} on ${actor.name}:`, err);
+ ui.notifications.warn(`Could not toggle condition on ${actor.name}.`);
+ }
+ }
+ const verb = isActive ? 'Removed' : 'Applied';
+ const condLabel = SLA_CONDITIONS.find(c => c.id === condId)?.label ?? condId;
+ ui.notifications.info(`${verb} "${condLabel}" on all PCs.`);
+ } else {
+ const selActor = pcs.find(a => a.id === this._selectedActorId);
+ if (!selActor) {
+ ui.notifications.warn('No PC selected.');
+ return;
+ }
+ try {
+ await selActor.toggleStatusEffect(condId, { active: !isActive });
+ const verb = isActive ? 'Removed' : 'Applied';
+ const condLabel = SLA_CONDITIONS.find(c => c.id === condId)?.label ?? condId;
+ ui.notifications.info(`${verb} "${condLabel}" on ${selActor.name}.`);
+ } catch (err) {
+ console.error(`SLAConditionApplicator | Failed to toggle ${condId} on ${selActor.name}:`, err);
+ ui.notifications.warn(`Could not toggle condition on ${selActor.name}.`);
+ }
+ }
+ });
+
+ // Toggle All PCs mode
+ html.find('[data-action="toggle-all-pcs"]').on('click', () => {
+ this._allPCsMode = !this._allPCsMode;
+ this.render(false);
+ });
+
+ // Clear All conditions
+ html.find('[data-action="clear-all"]').on('click', async () => {
+ const pcs = this._pcActors();
+ const targets = this._allPCsMode
+ ? pcs
+ : pcs.filter(a => a.id === this._selectedActorId);
+
+ if (!targets.length) {
+ ui.notifications.warn('No PC selected.');
+ return;
+ }
+
+ for (const actor of targets) {
+ for (const cond of SLA_CONDITIONS) {
+ if (actor.statuses?.has(cond.id)) {
+ try {
+ await actor.toggleStatusEffect(cond.id, { active: false });
+ } catch (err) {
+ console.error(`SLAConditionApplicator | Failed to clear ${cond.id} on ${actor.name}:`, err);
+ }
+ }
+ }
+ }
+
+ const targetLabel = this._allPCsMode ? 'all PCs' : (targets[0]?.name ?? 'selected PC');
+ ui.notifications.info(`Cleared all conditions from ${targetLabel}.`);
+ });
+ }
+}
+
+SLAConditionApplicator._inst = null;
return super.close(options);
}
@@ -7124,6 +8721,25 @@ Hooks.on('renderActorDirectory', (app, html) => {
game.slaGroupNPCTool.render(true);
});
actions.append(grpBtn);
+
+ // ── SLA GM Tool buttons ─────────────────────────────────────────────────
+ const gmToolDefs = [
+ { cls: 'sla-dir-status-btn', icon: 'fas fa-clipboard-list', label: 'PC Status', color: '#00d4ff', fn: () => SLAGMStatusWindow.open() },
+ { cls: 'sla-dir-credit-btn', icon: 'fas fa-credit-card', label: 'Credits', color: '#00d4ff', fn: () => SLACreditDistributionTool.open() },
+ { cls: 'sla-dir-ledger-btn', icon: 'fas fa-book-open', label: 'Ledger', color: '#aa88ff', fn: () => SLAShiftLedger.open() },
+ { cls: 'sla-dir-threat-btn', icon: 'fas fa-skull', label: 'Threats', color: '#ff4444', fn: () => SLANPCThreatBoard.open() },
+ { cls: 'sla-dir-bpn-btn', icon: 'fas fa-clipboard-check', label: 'BPN', color: '#ffcc00', fn: () => SLABPNTracker.open() },
+ { cls: 'sla-dir-condapp-btn', icon: 'fas fa-bolt', label: 'Conditions', color: '#ff6b35', fn: () => SLAConditionApplicator.open() },
+ ];
+ for (const def of gmToolDefs) {
+ if (actions.querySelector(`.${def.cls}`)) continue;
+ const btn = document.createElement('button');
+ btn.className = def.cls;
+ btn.innerHTML = ` ${def.label}`;
+ btn.style.cssText = `font-size:11px;padding:2px 6px;margin-left:3px;background:rgba(0,0,0,0.3);border:1px solid ${def.color};color:${def.color};border-radius:3px;cursor:pointer;`;
+ btn.addEventListener('click', def.fn);
+ actions.append(btn);
+ }
}
actions.append(pcBtn, npcBtn);