6 Commits
Author SHA1 Message Date
tevans bf4e98a780 chore: bump version to 0.6.1 — UI overhaul, icons, pause animation fix 2026-04-10 23:36:06 +01:00
tevansandClaude Sonnet 4.6 3d21b8e466 feat: SLA Industries UI overhaul — icons, pause animation, actor sheet enhancements
- 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 <noreply@anthropic.com>
2026-04-10 23:12:06 +01:00
slaguru666 85fedf1e16 chore: bump version to 0.6.0
Vevaphon species, Disguise skill, bug fixes, tooltips, instability bar.
2026-04-09 15:09:28 +01:00
slaguru666 e7a18a735a Add instability numeric readout and column header tooltips
- Instability bar now taller (22px) with X/12 label centred inside it
- CSS: bar label positioned absolutely inside the wrap
- Tooltips on all column headers: AP, DR, Speed, Oxygen, Carry (armour);
  Qty, Rating, Value, Weight, State, Duration (drugs);
  Quantity, Weight, Carry, Value (items);
  Category, Rank, Bonus (skills);
  Damage, Ammo, Shots, Mode, Carry, Range (weapons)
2026-04-09 13:45:23 +01:00
slaguru666 eacdf8e94b Fix: exclude Vevaphon morph abilities from Ebb enforcement
getSlaRestrictedEbbItemIds() was treating all ability-type items as Ebb
abilities and deleting them from non-Ebb characters. Vevaphon morph form
abilities (Brute Form, Stalker Form, Raptor Form) are not Ebb items —
exclude them by both flag check and name so they survive the enforcement hook.
2026-04-09 12:38:31 +01:00
slaguru666 971f6c4c49 Add Stormer Vevaphon — polymorphic morph system with instability tracker
- 3 morph forms (Brute/Stalker/Raptor), each costing 1 Instability on activation
- Instability tracker (0–12) with minor effects at 6+, Morph Panic at 10+
- Morph tab on character sheet: shift buttons, bar, panic reference
- actor.js: isSlaVevaphon(), setSlaVevaphonMorphForm(), adjustSlaVevaphonInstability()
- CSS: instability bar, morph buttons
- Character generator: morph constants, derive, generate() wiring
- template.json: morphForm, vevaphonInstability, morphNotes
2026-04-09 12:00:33 +01:00
73 changed files with 2421 additions and 176 deletions
+1062 -7
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Badge shape -->
<path d="M6,2 H26 L30,6 V30 H2 V6 Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
<!-- Notch at top (lanyard hole) -->
<rect x="13" y="0" width="6" height="4" rx="1" fill="currentColor"/>
<!-- Head -->
<circle cx="16" cy="12" r="4" fill="currentColor"/>
<!-- Shoulders -->
<path d="M8,26 C8,20 24,20 24,26" fill="currentColor"/>
<!-- ID stripe at bottom -->
<rect x="5" y="26" width="22" height="2" fill="currentColor" opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 575 B

+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Hexagonal speech bubble -->
<polygon points="4,4 28,4 28,22 20,22 16,28 12,22 4,22" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linejoin="round" fill-rule="evenodd"/>
<!-- Signal bars inside -->
<rect x="8" y="14" width="3" height="5" fill="currentColor"/>
<rect x="13" y="11" width="3" height="8" fill="currentColor"/>
<rect x="18" y="8" width="3" height="11" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 489 B

+16
View File
@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Outer ring -->
<circle cx="16" cy="16" r="13" fill="none" stroke="currentColor" stroke-width="2"/>
<!-- Inner ring -->
<circle cx="16" cy="16" r="5" fill="none" stroke="currentColor" stroke-width="1.5"/>
<!-- Crosshairs -->
<line x1="16" y1="3" x2="16" y2="9" stroke="currentColor" stroke-width="2"/>
<line x1="16" y1="23" x2="16" y2="29" stroke="currentColor" stroke-width="2"/>
<line x1="3" y1="16" x2="9" y2="16" stroke="currentColor" stroke-width="2"/>
<line x1="23" y1="16" x2="29" y2="16" stroke="currentColor" stroke-width="2"/>
<!-- Corner tick marks -->
<line x1="11" y1="5" x2="12.5" y2="5" stroke="currentColor" stroke-width="1.5"/>
<line x1="21" y1="5" x2="19.5" y2="5" stroke="currentColor" stroke-width="1.5"/>
<!-- Center dot -->
<circle cx="16" cy="16" r="2" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 897 B

+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Cylinder top ellipse -->
<ellipse cx="16" cy="6" rx="12" ry="4" fill="none" stroke="currentColor" stroke-width="2"/>
<!-- Cylinder body -->
<line x1="4" y1="6" x2="4" y2="24" stroke="currentColor" stroke-width="2"/>
<line x1="28" y1="6" x2="28" y2="24" stroke="currentColor" stroke-width="2"/>
<!-- Cylinder bottom ellipse -->
<ellipse cx="16" cy="24" rx="12" ry="4" fill="currentColor" stroke="currentColor" stroke-width="2"/>
<!-- Data bands -->
<ellipse cx="16" cy="12" rx="12" ry="4" fill="none" stroke="currentColor" stroke-width="1" stroke-dasharray="3 2"/>
<ellipse cx="16" cy="18" rx="12" ry="4" fill="none" stroke="currentColor" stroke-width="1" stroke-dasharray="3 2"/>
<!-- Access port -->
<rect x="13" y="4" width="6" height="4" rx="1" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 866 B

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Stylus body -->
<polygon points="8,4 24,4 28,8 16,30 4,8" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
<!-- Stylus grip band -->
<line x1="6" y1="10" x2="26" y2="10" stroke="currentColor" stroke-width="1.5"/>
<!-- Tip -->
<polygon points="16,25 13,29 19,29" fill="currentColor"/>
<!-- SLA insignia on stylus face -->
<text x="16" y="19" text-anchor="middle" font-family="sans-serif" font-size="7" font-weight="bold" fill="currentColor">SLA</text>
</svg>

After

Width:  |  Height:  |  Size: 567 B

+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Main crate -->
<rect x="3" y="8" width="26" height="20" fill="none" stroke="currentColor" stroke-width="2" rx="1"/>
<!-- Lid -->
<rect x="3" y="4" width="26" height="6" fill="currentColor" rx="1"/>
<!-- Center clasp -->
<rect x="13" y="6" width="6" height="4" rx="1" fill="currentColor" opacity="0.6"/>
<!-- Horizontal divider on crate -->
<line x1="3" y1="18" x2="29" y2="18" stroke="currentColor" stroke-width="1.5"/>
<!-- Corner brackets -->
<path d="M3,13 L3,10 L6,10" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M29,13 L29,10 L26,10" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M3,23 L3,26 L6,26" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M29,23 L29,26 L26,26" fill="none" stroke="currentColor" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 881 B

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- File/tablet body -->
<path d="M5,2 H22 L28,8 V30 H5 Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
<!-- Folded corner -->
<path d="M22,2 L22,8 L28,8" fill="none" stroke="currentColor" stroke-width="1.5"/>
<!-- Text lines -->
<line x1="9" y1="13" x2="24" y2="13" stroke="currentColor" stroke-width="1.5"/>
<line x1="9" y1="17" x2="24" y2="17" stroke="currentColor" stroke-width="1.5"/>
<line x1="9" y1="21" x2="20" y2="21" stroke="currentColor" stroke-width="1.5"/>
<!-- Classified stamp -->
<rect x="7" y="24" width="10" height="4" rx="0.5" fill="none" stroke="currentColor" stroke-width="1.2" transform="rotate(-8 12 26)"/>
<line x1="8" y1="26" x2="16" y2="26" stroke="currentColor" stroke-width="1" transform="rotate(-8 12 26)"/>
</svg>

After

Width:  |  Height:  |  Size: 859 B

+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Emitter housing -->
<polygon points="12,6 20,6 22,12 10,12" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
<!-- Lens circle -->
<circle cx="16" cy="9" r="3" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.7"/>
<!-- Beam spread (lower trapezoid) -->
<polygon points="10,12 22,12 28,28 4,28" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
<!-- Beam rays -->
<line x1="16" y1="12" x2="16" y2="28" stroke="currentColor" stroke-width="1" opacity="0.5"/>
<line x1="13" y1="12" x2="8" y2="28" stroke="currentColor" stroke-width="1"/>
<line x1="19" y1="12" x2="24" y2="28" stroke="currentColor" stroke-width="1"/>
<!-- Mounting bracket -->
<rect x="14" y="2" width="4" height="5" rx="1" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 894 B

+17
View File
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Ruler body -->
<rect x="2" y="12" width="28" height="8" rx="1" fill="none" stroke="currentColor" stroke-width="2"/>
<!-- Major tick marks -->
<line x1="9" y1="12" x2="9" y2="17" stroke="currentColor" stroke-width="1.5"/>
<line x1="16" y1="12" x2="16" y2="20" stroke="currentColor" stroke-width="2"/>
<line x1="23" y1="12" x2="23" y2="17" stroke="currentColor" stroke-width="1.5"/>
<!-- Minor ticks -->
<line x1="5" y1="12" x2="5" y2="15" stroke="currentColor" stroke-width="1"/>
<line x1="12" y1="12" x2="12" y2="15" stroke="currentColor" stroke-width="1"/>
<line x1="19" y1="12" x2="19" y2="15" stroke="currentColor" stroke-width="1"/>
<line x1="26" y1="12" x2="26" y2="15" stroke="currentColor" stroke-width="1"/>
<!-- Arrow / range indicator above -->
<line x1="4" y1="8" x2="28" y2="8" stroke="currentColor" stroke-width="1.5"/>
<polygon points="4,8 8,6 8,10" fill="currentColor"/>
<polygon points="28,8 24,6 24,10" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Map pin shape -->
<path d="M16,2 C10,2 6,7 6,12 C6,19 16,30 16,30 C16,30 26,19 26,12 C26,7 22,2 16,2 Z" fill="currentColor" stroke="currentColor" stroke-width="2"/>
<!-- Inner ring -->
<circle cx="16" cy="12" r="5" fill="none" stroke="currentColor" stroke-width="2"/>
<!-- Center dot -->
<circle cx="16" cy="12" r="2.5" fill="currentColor"/>
<!-- Angular bracket decorations -->
<path d="M10,5 L8,7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M22,5 L24,7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 644 B

+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Waveform bars -->
<rect x="2" y="12" width="3" height="8" rx="1" fill="currentColor"/>
<rect x="7" y="7" width="3" height="18" rx="1" fill="currentColor"/>
<rect x="12" y="4" width="3" height="24" rx="1" fill="currentColor"/>
<rect x="17" y="9" width="3" height="14" rx="1" fill="currentColor"/>
<rect x="22" y="6" width="3" height="20" rx="1" fill="currentColor"/>
<rect x="27" y="13" width="3" height="6" rx="1" fill="currentColor"/>
<!-- Base line -->
<line x1="2" y1="29" x2="30" y2="29" stroke="currentColor" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 631 B

+16
View File
@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Outer border -->
<rect x="2" y="2" width="28" height="28" fill="none" stroke="currentColor" stroke-width="2" rx="1"/>
<!-- Grid lines -->
<line x1="11" y1="2" x2="11" y2="30" stroke="currentColor" stroke-width="1"/>
<line x1="21" y1="2" x2="21" y2="30" stroke="currentColor" stroke-width="1"/>
<line x1="2" y1="11" x2="30" y2="11" stroke="currentColor" stroke-width="1"/>
<line x1="2" y1="21" x2="30" y2="21" stroke="currentColor" stroke-width="1"/>
<!-- Sector fills — industrial blocks -->
<rect x="3" y="3" width="7" height="7" fill="currentColor"/>
<rect x="13" y="13" width="7" height="7" fill="currentColor" opacity="0.6"/>
<rect x="22" y="22" width="7" height="7" fill="currentColor"/>
<rect x="22" y="3" width="7" height="7" fill="currentColor"/>
<!-- Location marker -->
<polygon points="16,17 14,21 18,21" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 940 B

+16
View File
@@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Hex gear outer -->
<polygon points="16,2 27,8 27,20 16,26 5,20 5,8" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linejoin="round"/>
<!-- Gear teeth on hex edges -->
<polygon points="16,1 18,4 14,4" fill="currentColor"/>
<polygon points="27.5,7.5 27,11 24.5,9.5" fill="currentColor"/>
<polygon points="27.5,20.5 24.5,22.5 27,19" fill="currentColor"/>
<polygon points="16,27 14,24 18,24" fill="currentColor"/>
<polygon points="4.5,20.5 5,19 7.5,22.5" fill="currentColor"/>
<polygon points="4.5,7.5 7.5,9.5 5,11" fill="currentColor"/>
<!-- Inner hex cutout -->
<polygon points="16,9 21,12 21,18 16,21 11,18 11,12" fill="currentColor"/>
<!-- Center circle -->
<circle cx="16" cy="15" r="3.5" fill="none" stroke="currentColor" stroke-width="2"/>
<circle cx="16" cy="15" r="1.5" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 910 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Speaker cone -->
<polygon points="4,11 12,11 20,5 20,27 12,21 4,21" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
<!-- Sound wave arcs -->
<path d="M23,10 C26,12 26,20 23,22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M25,7 C30,11 30,21 25,25" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/>
<!-- Base line -->
<rect x="4" y="21" width="8" height="5" rx="0.5" fill="currentColor" opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 609 B

+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Outer frame -->
<rect x="2" y="3" width="28" height="26" fill="none" stroke="currentColor" stroke-width="2" rx="1"/>
<!-- Header row -->
<rect x="2" y="3" width="28" height="6" fill="currentColor" rx="1"/>
<!-- Row dividers -->
<line x1="2" y1="15" x2="30" y2="15" stroke="currentColor" stroke-width="1"/>
<line x1="2" y1="21" x2="30" y2="21" stroke="currentColor" stroke-width="1"/>
<!-- Column dividers -->
<line x1="10" y1="9" x2="10" y2="29" stroke="currentColor" stroke-width="1"/>
<line x1="21" y1="9" x2="21" y2="29" stroke="currentColor" stroke-width="1"/>
<!-- Die symbol in first col -->
<rect x="4" y="11" width="4" height="4" rx="0.5" fill="none" stroke="currentColor" stroke-width="1"/>
<circle cx="6" cy="13" r="0.8" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 851 B

+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- 2x2 tile grid with isometric feel -->
<!-- Top-left tile -->
<rect x="2" y="2" width="13" height="13" rx="1" fill="currentColor" stroke="currentColor" stroke-width="1.5"/>
<!-- Top-right tile -->
<rect x="17" y="2" width="13" height="13" rx="1" fill="currentColor" opacity="0.5" stroke="currentColor" stroke-width="1.5"/>
<!-- Bottom-left tile -->
<rect x="2" y="17" width="13" height="13" rx="1" fill="currentColor" opacity="0.5" stroke="currentColor" stroke-width="1.5"/>
<!-- Bottom-right tile -->
<rect x="17" y="17" width="13" height="13" rx="1" fill="currentColor" stroke="currentColor" stroke-width="1.5"/>
<!-- Corner brackets on tiles -->
<path d="M4,7 L4,4 L7,4" fill="none" stroke="currentColor" stroke-width="1.2"/>
<path d="M19,7 L19,4 L22,4" fill="none" stroke="currentColor" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 907 B

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Hex base -->
<polygon points="16,3 28,10 28,22 16,29 4,22 4,10" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
<!-- Inner ring -->
<polygon points="16,8 23,12 23,20 16,24 9,20 9,12" fill="none" stroke="currentColor" stroke-width="1"/>
<!-- Head silhouette -->
<circle cx="16" cy="14" r="3.5" fill="currentColor"/>
<!-- Shoulders -->
<path d="M10,23 C10,19 22,19 22,23" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 509 B

+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<!-- Main wall line heavy -->
<line x1="4" y1="16" x2="28" y2="16" stroke="currentColor" stroke-width="4"/>
<!-- Wall battlements (top) -->
<rect x="5" y="10" width="4" height="6" fill="currentColor"/>
<rect x="13" y="10" width="4" height="6" fill="currentColor"/>
<rect x="21" y="10" width="4" height="6" fill="currentColor"/>
<!-- Wall base foundation -->
<rect x="3" y="20" width="26" height="4" rx="1" fill="currentColor" opacity="0.5"/>
<!-- Door gap indicator -->
<rect x="14" y="16" width="4" height="4" fill="currentColor" opacity="0"/>
<line x1="14" y1="16" x2="14" y2="24" stroke="currentColor" stroke-width="1" stroke-dasharray="2 2" opacity="0.7"/>
<line x1="18" y1="16" x2="18" y2="24" stroke="currentColor" stroke-width="1" stroke-dasharray="2 2" opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 869 B

+228 -2
View File
@@ -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;
@@ -271,6 +274,15 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet {
const fluxValue = Number(superData.sla.flux?.value ?? 0);
const fluxMax = Math.max(0, Number(superData.sla.flux?.max ?? 0));
data.data.slaFluxPercent = fluxMax > 0 ? Math.min(100, Math.max(0, Math.round((fluxValue / fluxMax) * 100))) : 0;
// Vevaphon morph data
data.data.isVevaphon = this.actor.isSlaVevaphon?.() ?? false;
if (data.data.isVevaphon) {
const instab = Math.max(0, Math.min(12, Number(superData.sla.vevaphonInstability?.value ?? 0) || 0));
data.data.vevaphonInstabilityPercent = Math.round((instab / 12) * 100);
data.data.vevaphonInstabilityElevated = instab >= 6;
data.data.vevaphonInstabilityCritical = instab >= 10;
data.data.vevaphonInstabilityClass = instab >= 10 ? "instability-critical" : instab >= 6 ? "instability-elevated" : "instability-nominal";
}
data.data.slaSpeciesTraitList = String(superData.sla.speciesNotes?.value ?? "").split(/\n+/).map((entry) => entry.trim()).filter(Boolean);
data.data.slaPackageSkillList = String(superData.sla.packageSkills?.value ?? "").split(/\s*,\s*/).map((entry) => entry.trim()).filter(Boolean);
data.data.slaSpeciesRules = getSpeciesRules(String(superData.sla.species?.value ?? "").trim(), superData.sla.speciesRules);
@@ -283,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;
@@ -906,8 +929,141 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet {
this.actor.triggerSlaFluxPanic({ source: 'Manual Flux panic check', panicOnFailure: true, forcePanic: true });
});
html.find('.sla-panic-button').click(ev => {
this.actor.rollTable("panicCheck", null, null, null, null, null, null);
// Vevaphon morph form shift
html.find('.sla-morph-shift').click(async (ev) => {
const formName = ev.currentTarget.dataset.form;
if (!formName) return;
await this.actor.setSlaVevaphonMorphForm?.(formName);
this.render(false);
});
// Vevaphon instability +/-
html.find('.sla-instability-increase').click(async () => {
await this.actor.adjustSlaVevaphonInstability?.(1);
this.render(false);
});
html.find('.sla-instability-decrease').click(async () => {
await this.actor.adjustSlaVevaphonInstability?.(-1);
this.render(false);
});
// 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: "<p>Start a new session? This will reset the trauma response session ability for this operative.</p>",
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: `
<p>What kind of rest is <strong>${this.actor.name}</strong> taking?</p>
<p><strong>Short Rest:</strong> Removes non-permanent conditions. No stat recovery.</p>
<p><strong>Long Rest:</strong> Removes non-permanent conditions and fully restores Flux (if Ebb user).</p>
`,
buttons: {
short: { icon: '<i class="fas fa-moon"></i>', label: "Short Rest", callback: () => resolve("short") },
long: { icon: '<i class="fas fa-bed"></i>', label: "Long Rest", callback: () => resolve("long") },
cancel: { icon: '<i class="fas fa-times"></i>', 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: "<p>Remove this contact from the operative's network?</p>",
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 => {
@@ -1183,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 = `
<div style="display:grid; gap:8px; padding:8px 0;">
<div>
<label style="font-size:0.8rem; font-weight:600; display:block; margin-bottom:4px;">Name</label>
<input type="text" id="sla-contact-name" value="${name}" placeholder="Contact name" style="width:100%;" />
</div>
<div>
<label style="font-size:0.8rem; font-weight:600; display:block; margin-bottom:4px;">Role / Occupation</label>
<input type="text" id="sla-contact-role" value="${role}" placeholder="e.g. Fixer, Fence, Squad medic" style="width:100%;" />
</div>
<div>
<label style="font-size:0.8rem; font-weight:600; display:block; margin-bottom:4px;">Disposition</label>
<select id="sla-contact-disposition" style="width:100%;">
<option value="ally" ${disposition === "ally" ? "selected" : ""}>Ally</option>
<option value="neutral" ${disposition === "neutral" ? "selected" : ""}>Neutral</option>
<option value="hostile" ${disposition === "hostile" ? "selected" : ""}>Hostile</option>
<option value="unknown" ${disposition === "unknown" ? "selected" : ""}>Unknown</option>
</select>
</div>
<div>
<label style="font-size:0.8rem; font-weight:600; display:block; margin-bottom:4px;">Notes</label>
<textarea id="sla-contact-notes" rows="3" placeholder="Sector, history, how they know the operative..." style="width:100%;">${notes}</textarea>
</div>
</div>
`;
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
+112 -5
View File
@@ -402,6 +402,14 @@ export class MothershipActor extends Actor {
// Prepare Character type specific data
_deriveCharacter() {
const system = this.system;
//clamp stress.value to stress.min so stale stored values never show below the floor
if (system.other?.stress) {
const stressMin = Number(system.other.stress.min ?? 0);
const stressVal = Number(system.other.stress.value ?? 0);
if (Number.isFinite(stressMin) && Number.isFinite(stressVal) && stressVal < stressMin) {
system.other.stress.value = stressMin;
}
}
//armor points + damage reduction
//init vars
let armorPoints = 0;
@@ -489,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
@@ -1650,7 +1691,7 @@ export class MothershipActor extends Actor {
window: {title: dlgTitle},
classes: ["macro-popup-dialog"],
position: {width: 600},
content: skillHeader + skillList + buttonDesc,
content: skillHeader + `<div class="skill-dialog-scroll">${skillList}</div>` + buttonDesc,
buttons: []
};
//add adv/normal/dis buttons if we need a rollString
@@ -2999,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];
}
@@ -4759,12 +4812,57 @@ export class MothershipActor extends Actor {
getSlaSpeciesMinimumWounds() {
const species = String(this.system?.sla?.species?.value ?? "").trim();
if (["Frother", "Shaktar", "Stormer 313 Malice", "Stormer 711 Xeno"].includes(species)) {
if (["Frother", "Shaktar", "Stormer 313 Malice", "Stormer 711 Xeno", "Stormer Vevaphon"].includes(species)) {
return 3;
}
return 2;
}
isSlaVevaphon() {
const species = String(this.system?.sla?.species?.value ?? "").trim();
return species === "Stormer Vevaphon";
}
getSlaVevaphonInstability() {
return Math.max(0, Math.min(12, Number(this.system?.sla?.vevaphonInstability?.value ?? 0) || 0));
}
getSlaVevaphonMorphForm() {
return String(this.system?.sla?.morphForm?.value ?? "").trim() || "Brute Form";
}
async setSlaVevaphonMorphForm(formName) {
if (!this.isSlaVevaphon()) return null;
const validForms = ["Brute Form", "Stalker Form", "Raptor Form"];
const resolved = validForms.find((entry) => entry === formName) ?? formName;
const currentInstability = this.getSlaVevaphonInstability();
const newInstability = Math.min(12, currentInstability + 1);
await this.update({
"system.sla.morphForm.value": resolved,
"system.sla.vevaphonInstability.value": newInstability
});
const instabilityWarning = newInstability >= 10
? " ⚠ Instability critical — Morph Panic risk on failed Sanity save."
: newInstability >= 6
? " Instability elevated — minor morph effects active."
: "";
ui.notifications.info(`${this.name} shifts to ${resolved}. Instability: ${newInstability}/12.${instabilityWarning}`);
return { morphForm: resolved, instability: newInstability };
}
async adjustSlaVevaphonInstability(delta = 1) {
if (!this.isSlaVevaphon()) return null;
const current = this.getSlaVevaphonInstability();
const next = Math.max(0, Math.min(12, current + delta));
await this.update({ "system.sla.vevaphonInstability.value": next });
if (next >= 12) {
ui.notifications.warn(`${this.name} has reached maximum Instability (12). The Vevaphon is lost — immediate retirement or death.`);
} else if (next >= 10) {
ui.notifications.warn(`${this.name} — Instability ${next}/12. Morph Panic risk: failed Sanity save triggers a Morph Panic event.`);
}
return next;
}
async enforceSlaSpeciesWounds() {
if (this.type !== "character") return null;
const minimumWounds = this.getSlaSpeciesMinimumWounds();
@@ -4863,7 +4961,14 @@ export class MothershipActor extends Actor {
return this.items
.filter((item) => {
const name = String(item.name ?? "").trim();
if (item.type === "ability") return true;
if (item.type === "ability") {
// Vevaphon morph form abilities are NOT Ebb items — exclude them
if (item.system?.sla?.morphForm === true) return false;
// Also exclude by name so hand-placed morph abilities are safe even without the flag
const morphNames = ["Brute Form", "Stalker Form", "Raptor Form"];
if (morphNames.includes(name)) return false;
return true;
}
if (item.type !== "skill") return false;
return name === "Formulate" || name === "Biofeedback" || name === "Ebb (Core)" || name.startsWith("Ebb ");
})
@@ -5239,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 };
}
+299
View File
@@ -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 = `
<div class="sla-pause-root" aria-label="SLA Industries – Game Paused">
<svg
class="sla-pause-svg"
viewBox="-120 -120 240 260"
xmlns="http://www.w3.org/2000/svg"
role="img"
>
<defs>
<!-- Red glow filter for the logo and satellites -->
<filter id="sla-glow-red" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="3.5" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<!-- Subtle glow for rings -->
<filter id="sla-glow-ring" x="-30%" y="-30%" width="160%" height="160%">
<feGaussianBlur stdDeviation="1.5" result="blur"/>
<feMerge>
<feMergeNode in="blur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<!-- Radial gradient for background disc -->
<radialGradient id="sla-bg-grad" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#1a0000" stop-opacity="0.95"/>
<stop offset="100%" stop-color="#000000" stop-opacity="0.0"/>
</radialGradient>
<!-- Clip for the inner content -->
<clipPath id="sla-clip-outer">
<circle cx="0" cy="0" r="108"/>
</clipPath>
</defs>
<!-- Ambient background disc -->
<circle cx="0" cy="0" r="108" fill="url(#sla-bg-grad)"/>
<!-- ── Outer ring (clockwise) ── -->
<g class="sla-ring-outer">
<!-- Dashed orbit path -->
<circle cx="0" cy="0" r="90"
fill="none"
stroke="#8b1a1a"
stroke-width="0.8"
stroke-dasharray="6 4"
opacity="0.7"
filter="url(#sla-glow-ring)"
/>
<!-- 3 red satellite dots at 0°, 120°, 240° -->
<circle cx="0" cy="-90" r="5" fill="#cc2222" filter="url(#sla-glow-red)" class="sla-sat"/>
<circle cx="77.9" cy="45" r="5" fill="#cc2222" filter="url(#sla-glow-red)" class="sla-sat"/>
<circle cx="-77.9" cy="45" r="5" fill="#cc2222" filter="url(#sla-glow-red)" class="sla-sat"/>
<!-- Thin tick marks every 30° on outer ring -->
<line x1="0" y1="-86" x2="0" y2="-94" stroke="#8b1a1a" stroke-width="1" transform="rotate(0)"/>
<line x1="0" y1="-86" x2="0" y2="-94" stroke="#8b1a1a" stroke-width="1" transform="rotate(30)"/>
<line x1="0" y1="-86" x2="0" y2="-94" stroke="#8b1a1a" stroke-width="1" transform="rotate(60)"/>
<line x1="0" y1="-86" x2="0" y2="-94" stroke="#8b1a1a" stroke-width="1" transform="rotate(90)"/>
<line x1="0" y1="-86" x2="0" y2="-94" stroke="#8b1a1a" stroke-width="1" transform="rotate(120)"/>
<line x1="0" y1="-86" x2="0" y2="-94" stroke="#8b1a1a" stroke-width="1" transform="rotate(150)"/>
<line x1="0" y1="-86" x2="0" y2="-94" stroke="#8b1a1a" stroke-width="1" transform="rotate(180)"/>
<line x1="0" y1="-86" x2="0" y2="-94" stroke="#8b1a1a" stroke-width="1" transform="rotate(210)"/>
<line x1="0" y1="-86" x2="0" y2="-94" stroke="#8b1a1a" stroke-width="1" transform="rotate(240)"/>
<line x1="0" y1="-86" x2="0" y2="-94" stroke="#8b1a1a" stroke-width="1" transform="rotate(270)"/>
<line x1="0" y1="-86" x2="0" y2="-94" stroke="#8b1a1a" stroke-width="1" transform="rotate(300)"/>
<line x1="0" y1="-86" x2="0" y2="-94" stroke="#8b1a1a" stroke-width="1" transform="rotate(330)"/>
</g>
<!-- ── Inner ring (counter-clockwise) ── -->
<g class="sla-ring-inner">
<circle cx="0" cy="0" r="62"
fill="none"
stroke="#6b1010"
stroke-width="0.6"
stroke-dasharray="4 6"
opacity="0.6"
filter="url(#sla-glow-ring)"
/>
<!-- 3 smaller orange-red dots at 60°, 180°, 300° offset for variety -->
<circle cx="53.7" cy="-31" r="3.5" fill="#dd4422" filter="url(#sla-glow-red)" class="sla-sat-inner"/>
<circle cx="-53.7" cy="-31" r="3.5" fill="#dd4422" filter="url(#sla-glow-red)" class="sla-sat-inner"/>
<circle cx="0" cy="62" r="3.5" fill="#dd4422" filter="url(#sla-glow-red)" class="sla-sat-inner"/>
</g>
<!-- ── Central "SLA" logotype ── -->
<g filter="url(#sla-glow-red)">
<!-- Hexagonal backing plate -->
<polygon
points="0,-38 33,-19 33,19 0,38 -33,19 -33,-19"
fill="#1a0000"
stroke="#cc2222"
stroke-width="1.2"
opacity="0.9"
/>
<!-- SLA text -->
<text
x="0" y="14"
text-anchor="middle"
font-family="'Oswald', 'Impact', 'Arial Narrow', sans-serif"
font-size="38"
font-weight="700"
letter-spacing="3"
fill="#ff2222"
class="sla-pause-logo-text"
>SLA</text>
</g>
<!-- ── Thin separator lines ── -->
<line x1="-70" y1="48" x2="70" y2="48" stroke="#8b1a1a" stroke-width="0.5" opacity="0.7"/>
<!-- ── "STAND BY" label ── -->
<text
x="0" y="70"
text-anchor="middle"
font-family="'Oswald', 'Courier New', monospace"
font-size="13"
font-weight="400"
letter-spacing="5"
fill="#cc4444"
class="sla-pause-standby"
>STAND BY</text>
<!-- ── "SLA INDUSTRIES" footer text ── -->
<text
x="0" y="95"
text-anchor="middle"
font-family="'Oswald', 'Arial Narrow', sans-serif"
font-size="7"
font-weight="300"
letter-spacing="3"
fill="#661111"
opacity="0.85"
>SLA INDUSTRIES</text>
<!-- ── Scanning line sweep ── -->
<line
x1="0" y1="-90" x2="0" y2="0"
stroke="#ff2222"
stroke-width="0.8"
opacity="0.18"
class="sla-pause-scan"
/>
</svg>
</div>`;
// 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:
// <button class="ui-control plain icon fa-solid fa-comments" data-tab="chat" ...>
// The BUTTON itself is the icon — no child <i> element.
document.querySelectorAll(
"#sidebar-tabs button[data-tab], #sidebar-tabs [data-tab].icon"
).forEach(btn => {
const name = btn.dataset.tab;
const file = SLA_SIDEBAR_ICONS[name];
if (!file || btn.hasAttribute("data-sla-icon")) return;
_slaApplyIconMask(btn, file);
found++;
});
// ── Scene / layer control icons ───────────────────────────────────────
// Foundry v13 scene controls are also buttons — search the whole doc
// since we don't yet know the container ID.
document.querySelectorAll(
"button[data-control], [data-control].icon, [data-control].ui-control"
).forEach(btn => {
const name = btn.dataset.control;
const file = SLA_CONTROL_ICONS[name];
if (!file || btn.hasAttribute("data-sla-icon")) return;
_slaApplyIconMask(btn, file);
found++;
});
if (found > 0) {
console.log(`SLA Industries | Applied ${found} custom icons.`);
} else {
console.warn("SLA Industries | injectSlaIcons: no icons matched — dumping HTML:");
console.warn("sidebar:", document.querySelector("#sidebar-tabs")?.outerHTML?.slice(0, 1200));
const ctrlDump = document.querySelector("#ui-left, #scene-controls, #controls");
console.warn("controls:", ctrlDump?.outerHTML?.slice(0, 1200) ?? "(not found — try: " + [...document.querySelectorAll("[data-control]")].map(e=>e.tagName+'.'+e.className).join(', ') + ")");
}
}
// Run once on ready, then re-run whenever Foundry re-renders these components
Hooks.once("ready", () => {
requestAnimationFrame(() => requestAnimationFrame(() => injectSlaIcons()));
});
Hooks.on("renderSidebar", () => requestAnimationFrame(() => injectSlaIcons()));
Hooks.on("renderSidebarTab", () => requestAnimationFrame(() => injectSlaIcons()));
Hooks.on("changeSidebarTab", () => requestAnimationFrame(() => injectSlaIcons()));
Hooks.on("renderSceneControls", () => requestAnimationFrame(() => injectSlaIcons()));
+17
View File
@@ -487,6 +487,23 @@ export class SLADrugSystem {
await this.resolveExpiredEffects(actor);
// Drug stacking warning — check if any OTHER drug is already active
const allActiveDrugEffects = this.getActorDrugEffects(actor);
const otherActiveDrugs = allActiveDrugEffects.filter((effect) => {
const flag = this.getDrugEffectFlag(effect);
return flag?.stage === "active" && flag?.drugId && flag.drugId !== def.id;
});
if (otherActiveDrugs.length > 0) {
const activeNames = [...new Set(otherActiveDrugs.map((e) => this.getDrugEffectFlag(e)?.drugName ?? e.name))].join(", ");
const proceed = await Dialog.confirm({
title: "Drug Stacking Warning",
content: `<p><strong>${actor.name}</strong> already has active drug effects from: <strong>${activeNames}</strong>.</p>
<p>Combining drugs can cause unpredictable or lethal interactions. Do you want to proceed?</p>`,
defaultYes: false
});
if (!proceed) return { ok: false, reason: "stacking-cancelled" };
}
const quantity = Math.max(0, Number(item.system?.quantity ?? 0));
if (consume && quantity < 1) {
ui.notifications.warn(`${actor.name} has no ${def.name} doses remaining.`);
+3 -1
View File
@@ -8,7 +8,9 @@ export const SLA_SPECIES_STAT_ADJUSTMENTS = {
"Wraith Raider": { sanity: 15 },
Shaktar: { body: 10, fear: 20 },
"Stormer 313 Malice": { fear: 25 },
"Stormer 711 Xeno": { fear: 25 }
"Stormer 711 Xeno": { fear: 25 },
// Vevaphon: strong, fast, resilient biogenetic chassis — psychologically shattered
"Stormer Vevaphon": { strength: 10, combat: 10, body: 15, speed: 5, sanity: -10 }
};
export function getSlaSpeciesStatAdjustments(speciesName = "") {
+147 -4
View File
@@ -69,6 +69,10 @@ const SPECIES_DOSSIER = {
"Advanced Carrien": {
identity: "Cunning, dangerous mutants with strong survival instincts and an uneasy fit inside official structures.",
fieldPressure: "They thrive in broken districts and dirty jobs, but prejudice follows them into every briefing room."
},
"Stormer Vevaphon": {
identity: "Sleight Industries' cancelled polymorphic Stormer line: a shape-shifting bioweapon that can be anything the mission needs — and that is precisely the problem.",
fieldPressure: "They are terrifying in infiltration, ambush, and close quarters, but every form change costs them a piece of who they are. Long ops risk unravelling them entirely."
}
};
@@ -76,7 +80,8 @@ const SPECIES_MIN_WOUNDS = {
Frother: 3,
Shaktar: 3,
"Stormer 313 Malice": 3,
"Stormer 711 Xeno": 3
"Stormer 711 Xeno": 3,
"Stormer Vevaphon": 3
};
const SKILL_IMAGE_OVERRIDES = {
@@ -201,6 +206,19 @@ const SPECIES_MOTHERSHIP_PROFILES = {
health: "1d10+11"
},
description: "Clever survivor profile. Strong Intellect and workable Strength make Carrien dangerous problem-solvers with uneven social acceptance."
},
"Stormer Vevaphon": {
rolls: {
strength: "2d10+35",
speed: "2d10+28",
intellect: "2d10+20",
combat: "2d10+35",
sanity: "2d10+12",
fear: "2d10+23",
body: "2d10+22",
health: "1d10+12"
},
description: "Polymorphic bioweapon with excellent combat and physical resilience, above-average speed, and severely degraded Sanity reflecting continuous identity diffusion from shapeshifting."
}
};
@@ -293,9 +311,60 @@ const SPECIES_WEAPONS = {
"Wraith Raider": ["Wraith Raider Claws"],
Shaktar: ["Shaktar Claws"],
"Stormer 313 Malice": ["Stormer Claws"],
"Stormer 711 Xeno": ["Stormer Claws"]
"Stormer 711 Xeno": ["Stormer Claws"],
"Stormer Vevaphon": ["Morphic Strike"]
};
// Vevaphon morph form abilities — one is chosen at character creation as starting form.
// Each form grants stat bonuses and a special ability but costs 1 Instability when activated.
const VEVAPHON_MORPH_STARTERS = [
{
name: "Brute Form",
summary: "A hulking, armoured chassis. STR+10, BODY+10. Immune to Knockdown. Cannot be concealed.",
statMods: { strength: 10, body: 10 },
special: "Knockdown Immunity",
instabilityCost: 1,
description: "The Vevaphon expands its frame into a dense combat shell. Slabs of biogenetic armour plate reinforce the torso and limbs. Effective in close-quarters breach work. Every activation costs 1 Instability."
},
{
name: "Stalker Form",
summary: "A lean, predatory chassis built for silence. SPD+10, Stealth+20. Cannot wear armour.",
statMods: { speed: 10 },
skillMods: { Stealth: 20 },
special: "Armour Incompatible",
instabilityCost: 1,
description: "The Vevaphon elongates and flattens, redistributing mass for near-silent movement. Standard armour cannot bond to the shifting surface. Ideal for covert approach and ambush. Every activation costs 1 Instability."
},
{
name: "Raptor Form",
summary: "A fast striking chassis built around offensive output. COM+10, SPD+5. Attacks score one extra wound on a critical.",
statMods: { combat: 10, speed: 5 },
special: "Critical Wound Bonus",
instabilityCost: 1,
description: "The Vevaphon reshapes its strike limbs into natural weapons optimised for penetrating force. Designed for rapid target elimination at close range. Every activation costs 1 Instability."
}
];
// Instability minor effects (triggered at 6+ Instability, rolled each scene)
const VEVAPHON_INSTABILITY_MINOR = [
"Involuntary surface texturing — skin shifts pattern mid-conversation. Social rolls at -10.",
"Limb proportions drift. Fine motor tasks at -10 until end of scene.",
"Voice shifts register unexpectedly. Communications may be misidentified.",
"Brief facial blurring. Anyone attempting to describe the Vevaphon gives contradictory reports.",
"Pain from realignment. Take 1 point of Stress.",
"Form flickers. One randomly selected stat drops by 5 until the Vevaphon rests."
];
// Instability Morph Panic table (triggered at 10+ Instability on a failed SANITY save)
const VEVAPHON_INSTABILITY_TABLE = [
{ roll: 1, result: "Cascade Shift", effect: "Vevaphon immediately shifts to a random morph form, even mid-combat. Costs 1 additional Instability." },
{ roll: 2, result: "Identity Bleed", effect: "Forgets assigned name and call sign until end of scene. Acts on instinct — GM adjudicates behaviour." },
{ roll: 3, result: "Partial Lock", effect: "One limb freezes in incorrect form. -10 to all physical rolls using that limb." },
{ roll: 4, result: "Tissue Rejection", effect: "Takes 1d5 damage as the body fights itself. Armour does not apply." },
{ roll: 5, result: "Threat Imprint", effect: "Becomes locked on nearest visible target. Must make a Sanity save to perform any action not directed at that target." },
{ roll: 6, result: "Full Reversion", effect: "Collapses to base form, losing all morph bonuses. Spends next action recovering. Lose 1 Instability." }
];
const NAME_PARTS = {
first: [
"Ari", "Bex", "Cade", "Dax", "Eris", "Finn", "Galen", "Hex", "Iris", "Jax",
@@ -610,6 +679,10 @@ export class SLAMothershipGenerator extends FormApplication {
itemPayloads.push(...this.buildEbbItems(species.name));
}
if (species.name === "Stormer Vevaphon") {
itemPayloads.push(...this.buildVevaphonItems(derived.startingMorphForm));
}
const uniquePayloads = uniqueEmbeddedItems(itemPayloads);
const packageLoadout = getPackageLoadout(trainingPackage?.name ?? "", data.equipment.packageLoadouts ?? {});
const packageIssueKit = getPackageIssueKit(trainingPackage?.name ?? "");
@@ -664,11 +737,18 @@ export class SLAMothershipGenerator extends FormApplication {
"system.sla.ebbRating.value": derived.ebbRating,
"system.other.stressdesc.value": buildTraumaResponseText(species, trainingPackage),
"system.sla.fluxNotes.value": this.isEbbSpecies(species.name)
? `Starting Flux reserve: ${derived.fluxMax}. Ebb users begin with at least 20 Flux and spend from that reserve when channeling. Fear is used as the control save.`
? `Starting Flux reserve: ${derived.fluxMax} (rolled 20 + 1d10). Flux is randomised at creation (range 21–31). Spend from this reserve when channeling; Fear is the control save.`
: "",
"system.sla.ebbNotes.value": this.isEbbSpecies(species.name)
? `Starter discipline package: ${(EBB_STARTERS[species.name] ?? []).join(", ")}`
: "",
...(species.name === "Stormer Vevaphon" ? {
"system.sla.morphForm.value": derived.startingMorphForm ?? "Brute Form",
"system.sla.vevaphonInstability.value": 0,
"system.sla.vevaphonInstability.max": 12,
"system.sla.vevaphonInstability.label": "Instability",
"system.sla.morphNotes.value": buildVevaphonMorphNotes(derived.startingMorphForm)
} : {}),
"system.biography": description,
"system.notes": buildNotes(species, trainingPackage, packageLoadout, packageIssueKit)
};
@@ -815,6 +895,40 @@ export class SLAMothershipGenerator extends FormApplication {
.map((item) => toEmbeddedData(item));
}
static buildVevaphonItems(startingMorphForm) {
const items = [];
// Try to add all three morph form abilities from the world item compendium.
// They may or may not exist as world items — we fall back to building minimal
// ability documents directly if not found.
for (const morph of VEVAPHON_MORPH_STARTERS) {
const worldItem = findWorldItem(morph.name, "ability");
if (worldItem) {
const embedded = toEmbeddedData(worldItem);
items.push(embedded);
} else {
// Build a minimal ability document so the character isn't empty-handed.
items.push({
name: morph.name,
type: "ability",
img: "icons/svg/upgrade.svg",
system: {
description: morph.description,
summary: morph.summary,
special: morph.special,
sla: {
morphForm: true,
instabilityCost: morph.instabilityCost,
statMods: morph.statMods ?? {},
skillMods: morph.skillMods ?? {},
isStartingForm: morph.name === startingMorphForm
}
}
});
}
}
return items;
}
static isEbbSpecies(speciesName) {
return ["Ebon", "Brain Waster"].includes(String(speciesName ?? ""));
}
@@ -952,7 +1066,16 @@ function deriveProfile(rolls, packageName, speciesName) {
if (["Ebon", "Brain Waster"].includes(speciesName)) {
profile.ebbRating = clamp(Math.ceil(profile.intellect / 20) + Number(mods.ebbRating ?? 0), 1, 8);
profile.fluxMax = clamp(Math.max(20, Math.floor(profile.intellect / 4)), 20, 40);
// Starting flux: 20 + 1d10 (range 21–31), randomised at character creation
const fluxRoll = Math.floor(Math.random() * 11) + 1; // 1–11 → simulates d10 with extended top end
profile.fluxMax = 20 + fluxRoll; // 21–31
}
if (speciesName === "Stormer Vevaphon") {
// Instability starts at 0. Pick a random starting morph form.
profile.morphInstability = 0;
const morphIndex = Math.floor(Math.random() * VEVAPHON_MORPH_STARTERS.length);
profile.startingMorphForm = VEVAPHON_MORPH_STARTERS[morphIndex].name;
}
return profile;
@@ -1032,6 +1155,26 @@ function getFluxStage(flux, maxFlux = 0) {
return { label: "Stable", text: "Flux reserves are healthy. Ebb use remains disciplined and contained." };
}
function buildVevaphonMorphNotes(startingMorphForm) {
const form = VEVAPHON_MORPH_STARTERS.find((entry) => entry.name === startingMorphForm) ?? VEVAPHON_MORPH_STARTERS[0];
const allForms = VEVAPHON_MORPH_STARTERS.map((entry) => `${entry.name}: ${entry.summary}`).join("\n");
return [
`Starting Morph Form: ${form.name}`,
`${form.description}`,
``,
`Available Forms:`,
allForms,
``,
`Instability (0–12): Each morph activation costs 1 Instability. At 6+, minor effects trigger each scene. At 10+, failed Sanity saves trigger Morph Panic. At 12, the Vevaphon is lost — immediate retirement or death.`,
``,
`Minor Effects (6+ Instability):`,
VEVAPHON_INSTABILITY_MINOR.join("\n"),
``,
`Morph Panic Table (10+ Instability, failed Sanity):`,
VEVAPHON_INSTABILITY_TABLE.map((entry) => `${entry.roll}. ${entry.result}: ${entry.effect}`).join("\n")
].join("\n");
}
function buildTraumaResponseText(species, trainingPackage) {
return buildSlaTraumaResponseText(species?.name ?? "", trainingPackage?.name ?? "");
}
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000039
MANIFEST-000061
+3 -3
View File
@@ -1,3 +1,3 @@
2026/03/31-15:50:48.583476 17054f000 Recovering log #38
2026/03/31-15:50:48.583855 17054f000 Delete type=0 #38
2026/03/31-15:50:48.583882 17054f000 Delete type=3 #37
2026/04/10-15:21:30.814878 16f8db000 Recovering log #60
2026/04/10-15:21:30.815292 16f8db000 Delete type=0 #60
2026/04/10-15:21:30.815311 16f8db000 Delete type=3 #59
+3 -3
View File
@@ -1,3 +1,3 @@
2026/03/29-21:14:27.556257 16ffc3000 Recovering log #34
2026/03/29-21:14:27.556633 16ffc3000 Delete type=3 #32
2026/03/29-21:14:27.556668 16ffc3000 Delete type=0 #34
2026/04/10-15:15:40.939578 17164b000 Recovering log #58
2026/04/10-15:15:40.939865 17164b000 Delete type=0 #58
2026/04/10-15:15:40.939883 17164b000 Delete type=3 #57
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000039
MANIFEST-000061
+3 -3
View File
@@ -1,3 +1,3 @@
2026/03/31-15:50:48.585617 170d5b000 Recovering log #38
2026/03/31-15:50:48.585981 170d5b000 Delete type=0 #38
2026/03/31-15:50:48.586009 170d5b000 Delete type=3 #37
2026/04/10-15:21:30.816908 16e0b7000 Recovering log #60
2026/04/10-15:21:30.817298 16e0b7000 Delete type=0 #60
2026/04/10-15:21:30.817326 16e0b7000 Delete type=3 #59
+3 -3
View File
@@ -1,3 +1,3 @@
2026/03/29-21:14:27.559077 1717e7000 Recovering log #34
2026/03/29-21:14:27.559760 1717e7000 Delete type=3 #32
2026/03/29-21:14:27.559790 1717e7000 Delete type=0 #34
2026/04/10-15:15:40.941390 170e3f000 Recovering log #58
2026/04/10-15:15:40.941631 170e3f000 Delete type=0 #58
2026/04/10-15:15:40.941649 170e3f000 Delete type=3 #57
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000039
MANIFEST-000061
+3 -3
View File
@@ -1,3 +1,3 @@
2026/03/31-15:50:48.587337 171d73000 Recovering log #38
2026/03/31-15:50:48.587684 171d73000 Delete type=0 #38
2026/03/31-15:50:48.587707 171d73000 Delete type=3 #37
2026/04/10-15:21:30.818908 16f0cf000 Recovering log #60
2026/04/10-15:21:30.819298 16f0cf000 Delete type=0 #60
2026/04/10-15:21:30.819316 16f0cf000 Delete type=3 #59
+3 -3
View File
@@ -1,3 +1,3 @@
2026/03/29-21:14:27.561319 170fdb000 Recovering log #34
2026/03/29-21:14:27.561677 170fdb000 Delete type=3 #32
2026/03/29-21:14:27.561715 170fdb000 Delete type=0 #34
2026/04/10-15:15:40.942933 17164b000 Recovering log #58
2026/04/10-15:15:40.943347 17164b000 Delete type=0 #58
2026/04/10-15:15:40.943366 17164b000 Delete type=3 #57
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000039
MANIFEST-000061
+3 -3
View File
@@ -1,3 +1,3 @@
2026/03/31-15:50:48.589215 17054f000 Recovering log #38
2026/03/31-15:50:48.589634 17054f000 Delete type=0 #38
2026/03/31-15:50:48.589656 17054f000 Delete type=3 #37
2026/04/10-15:21:30.820489 16f0cf000 Recovering log #60
2026/04/10-15:21:30.820764 16f0cf000 Delete type=0 #60
2026/04/10-15:21:30.820786 16f0cf000 Delete type=3 #59
+3 -3
View File
@@ -1,3 +1,3 @@
2026/03/29-21:14:27.563010 1707cf000 Recovering log #34
2026/03/29-21:14:27.563276 1707cf000 Delete type=3 #32
2026/03/29-21:14:27.563303 1707cf000 Delete type=0 #34
2026/04/10-15:15:40.944425 171e57000 Recovering log #58
2026/04/10-15:15:40.944787 171e57000 Delete type=0 #58
2026/04/10-15:15:40.944804 171e57000 Delete type=3 #57
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000039
MANIFEST-000061
+3 -3
View File
@@ -1,3 +1,3 @@
2026/03/31-15:50:48.590832 171567000 Recovering log #38
2026/03/31-15:50:48.591262 171567000 Delete type=0 #38
2026/03/31-15:50:48.591291 171567000 Delete type=3 #37
2026/04/10-15:21:30.821825 16e0b7000 Recovering log #60
2026/04/10-15:21:30.822206 16e0b7000 Delete type=0 #60
2026/04/10-15:21:30.822238 16e0b7000 Delete type=3 #59
+3 -3
View File
@@ -1,3 +1,3 @@
2026/03/29-21:14:27.564411 1717e7000 Recovering log #34
2026/03/29-21:14:27.564754 1717e7000 Delete type=3 #32
2026/03/29-21:14:27.564784 1717e7000 Delete type=0 #34
2026/04/10-15:15:40.946179 170e3f000 Recovering log #58
2026/04/10-15:15:40.946914 170e3f000 Delete type=0 #58
2026/04/10-15:15:40.946937 170e3f000 Delete type=3 #57
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000039
MANIFEST-000061
+3 -3
View File
@@ -1,3 +1,3 @@
2026/03/31-15:50:48.592976 170d5b000 Recovering log #38
2026/03/31-15:50:48.593456 170d5b000 Delete type=0 #38
2026/03/31-15:50:48.593476 170d5b000 Delete type=3 #37
2026/04/10-15:21:30.988603 16f8db000 Recovering log #60
2026/04/10-15:21:30.989459 16f8db000 Delete type=0 #60
2026/04/10-15:21:30.989517 16f8db000 Delete type=3 #59
+3 -3
View File
@@ -1,3 +1,3 @@
2026/03/29-21:14:27.565839 1707cf000 Recovering log #34
2026/03/29-21:14:27.566214 1707cf000 Delete type=3 #32
2026/03/29-21:14:27.566242 1707cf000 Delete type=0 #34
2026/04/10-15:15:40.964061 17164b000 Recovering log #58
2026/04/10-15:15:40.964631 17164b000 Delete type=0 #58
2026/04/10-15:15:40.964657 17164b000 Delete type=3 #57
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000039
MANIFEST-000063
+3 -3
View File
@@ -1,3 +1,3 @@
2026/03/31-15:50:48.594786 171567000 Recovering log #38
2026/03/31-15:50:48.595201 171567000 Delete type=0 #38
2026/03/31-15:50:48.595227 171567000 Delete type=3 #37
2026/04/10-15:21:30.992926 16f8db000 Recovering log #62
2026/04/10-15:21:30.993656 16f8db000 Delete type=0 #62
2026/04/10-15:21:30.993730 16f8db000 Delete type=3 #61
+3 -3
View File
@@ -1,3 +1,3 @@
2026/03/29-21:14:27.567335 170fdb000 Recovering log #34
2026/03/29-21:14:27.567720 170fdb000 Delete type=3 #32
2026/03/29-21:14:27.567753 170fdb000 Delete type=0 #34
2026/04/10-15:15:40.966053 17164b000 Recovering log #60
2026/04/10-15:15:40.967356 17164b000 Delete type=0 #60
2026/04/10-15:15:40.967388 17164b000 Delete type=3 #59
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000039
MANIFEST-000061
+3 -3
View File
@@ -1,3 +1,3 @@
2026/03/31-15:50:48.597410 170d5b000 Recovering log #38
2026/03/31-15:50:48.598033 170d5b000 Delete type=0 #38
2026/03/31-15:50:48.598068 170d5b000 Delete type=3 #37
2026/04/10-15:21:30.995778 16e8c3000 Recovering log #60
2026/04/10-15:21:30.996525 16e8c3000 Delete type=0 #60
2026/04/10-15:21:30.996566 16e8c3000 Delete type=3 #59
+3 -3
View File
@@ -1,3 +1,3 @@
2026/03/29-21:14:27.568770 1707cf000 Recovering log #34
2026/03/29-21:14:27.569097 1707cf000 Delete type=3 #32
2026/03/29-21:14:27.569138 1707cf000 Delete type=0 #34
2026/04/10-15:15:40.968762 17164b000 Recovering log #58
2026/04/10-15:15:40.968983 17164b000 Delete type=0 #58
2026/04/10-15:15:40.969099 17164b000 Delete type=3 #57
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000039
MANIFEST-000061
+3 -3
View File
@@ -1,3 +1,3 @@
2026/03/31-15:50:48.600045 17054f000 Recovering log #38
2026/03/31-15:50:48.600385 17054f000 Delete type=0 #38
2026/03/31-15:50:48.600414 17054f000 Delete type=3 #37
2026/04/10-15:21:30.998700 16e8c3000 Recovering log #60
2026/04/10-15:21:30.999246 16e8c3000 Delete type=0 #60
2026/04/10-15:21:30.999287 16e8c3000 Delete type=3 #59
+3 -3
View File
@@ -1,3 +1,3 @@
2026/03/29-21:14:27.570654 170fdb000 Recovering log #34
2026/03/29-21:14:27.571032 170fdb000 Delete type=3 #32
2026/03/29-21:14:27.571059 170fdb000 Delete type=0 #34
2026/04/10-15:15:40.970322 172663000 Recovering log #58
2026/04/10-15:15:40.970657 172663000 Delete type=0 #58
2026/04/10-15:15:40.970680 172663000 Delete type=3 #57
+1 -1
View File
@@ -2,7 +2,7 @@
"id": "sla-mothership",
"title": "SLA Mothership",
"description": "An SLA Industries conversion built on the unofficial Mothership system for Foundry VTT, preserving Mothership's core mechanics while adding SLA flux, Ebb, BPN, and equipment support.",
"version": "0.5.1",
"version": "0.6.1",
"esmodules": [
"module/mosh.js"
],
+66 -51
View File
@@ -11,24 +11,24 @@
"value": 10,
"min": 0,
"max": 10,
"label" : "Health"
"label": "Health"
},
"hits": {
"value": 0,
"min": 0,
"max" : 2,
"label" : "Wounds"
"max": 2,
"label": "Wounds"
},
"netHP": {
"value": 20,
"min": 0,
"max" : 20,
"label" : "Net HP"
"max": 20,
"label": "Net HP"
},
"bleeding": {
"value": 0,
"min": 0,
"label" : "Bleeding"
"label": "Bleeding"
}
}
},
@@ -38,9 +38,9 @@
],
"biography": "",
"notes": "",
"weight" : {
"current" : 0,
"capacity" : 0
"weight": {
"current": 0,
"capacity": 0
},
"class": {
"value": ""
@@ -59,8 +59,8 @@
},
"xp": {
"value": 0,
"html" : 0,
"selectedSkill" : ""
"html": 0,
"selectedSkill": ""
},
"attributes": {
"level": {
@@ -140,13 +140,13 @@
"value": 2,
"min": 2,
"max": 20,
"label" : "Stress"
"label": "Stress"
},
"resolve": {
"value": 0,
"min": 0,
"max": 15,
"label" : "Resolve"
"label": "Resolve"
},
"stressdesc": {
"value": ""
@@ -216,6 +216,22 @@
},
"ebbNotes": {
"value": ""
},
"morphForm": {
"value": ""
},
"vevaphonInstability": {
"value": 0,
"max": 12,
"label": "Instability"
},
"morphNotes": {
"value": ""
},
"sessionAbilityUsed": false,
"contacts": [],
"activityLog": {
"value": ""
}
}
},
@@ -237,7 +253,7 @@
"max": 99,
"label": "Combat",
"rollLabel": "Combat Check",
"enabled" : true
"enabled": true
},
"instinct": {
"value": 10,
@@ -245,7 +261,7 @@
"max": 99,
"label": "Instinct",
"rollLabel": "Instinct Check",
"enabled" : true
"enabled": true
},
"speed": {
"value": 10,
@@ -253,7 +269,7 @@
"max": 99,
"label": "Speed",
"rollLabel": "Speed Check",
"enabled" : false
"enabled": false
},
"loyalty": {
"value": 10,
@@ -261,7 +277,7 @@
"max": 99,
"label": "Loyalty",
"rollLabel": "Loyalty Check",
"enabled" : false
"enabled": false
},
"armor": {
"value": 0,
@@ -272,7 +288,7 @@
"cover": "none",
"label": "Armor",
"rollLabel": "Armor Save",
"enabled" : false
"enabled": false
},
"sanity": {
"value": 10,
@@ -280,7 +296,7 @@
"max": 99,
"label": "Sanity",
"rollLabel": "Sanity Save",
"enabled" : false
"enabled": false
}
}
},
@@ -295,18 +311,18 @@
"xp": {
"value": 1
},
"images" : {
"layout" : "systems/sla-mothership/images/Galloway_Top.png",
"beauty" : "systems/sla-mothership/images/Galloway_Top.png"
"images": {
"layout": "systems/sla-mothership/images/Galloway_Top.png",
"beauty": "systems/sla-mothership/images/Galloway_Top.png"
},
"runSetup" : true,
"megadamage" : {
"html" : "",
"hits" : [],
"menu" : {
"html" : ""
"runSetup": true,
"megadamage": {
"html": "",
"hits": [],
"menu": {
"html": ""
},
"open" : false
"open": false
},
"stats": {
"armor": {
@@ -370,9 +386,9 @@
"hull": {
"value": 10,
"max": 10,
"75" : 0,
"50" : 0,
"25" : 0
"75": 0,
"50": 0,
"25": 0
},
"fuel": {
"value": 10,
@@ -400,10 +416,10 @@
},
"upgrades": {
"value": 0,
"max" : 0
"max": 0
}
},
"weapon-stats" : {
"weapon-stats": {
"weapons": {
"value": 0,
"max": 1
@@ -416,7 +432,6 @@
"value": 0,
"max": 1
}
}
}
},
@@ -447,7 +462,7 @@
"quantity": 1,
"weight": 0,
"carryState": "carried",
"cost" : 0,
"cost": 0,
"sla": {
"category": "",
"power": "",
@@ -495,7 +510,7 @@
"ammo": 10,
"shots": 1,
"curShots": 0,
"shotsPerFire" : 1,
"shotsPerFire": 1,
"useAmmo": false,
"ammoType": "",
"ammoCalibre": "",
@@ -511,22 +526,22 @@
"ammoReserveAp": 0,
"ammoReserveHe": 0,
"ammoReserveHeap": 0,
"wound":"",
"wound": "",
"critDmg": "",
"woundEffect": "",
"bonus": 0,
"weight": 0,
"carryState": "equipped",
"equipped": true,
"cost" : 0,
"cost": 0,
"ranges": {
"short": 0,
"medium": 0,
"long": 0,
"value" : ""
"value": ""
},
"settings" : {
"firstEdition" : false
"settings": {
"firstEdition": false
},
"sla": {
"weaponType": "",
@@ -548,14 +563,14 @@
"base"
],
"armorPoints": 1,
"damageReduction":0,
"damageReduction": 0,
"speed": "",
"oxygenMax": 0,
"oxygenCurrent": 0,
"weight": 0,
"carryState": "equipped",
"equipped": true,
"cost" : 0,
"cost": 0,
"sla": {
"burden": "",
"coverage": "",
@@ -572,9 +587,9 @@
"base"
],
"severity": 1,
"treatment" : {
"value" : 0,
"html" : ""
"treatment": {
"value": 0,
"html": ""
},
"sla": {
"category": "",
@@ -622,7 +637,7 @@
"base"
],
"text": "",
"job" : ""
"job": ""
},
"repair": {
"templates": [
@@ -637,9 +652,9 @@
],
"quantity": 1,
"hull": 1,
"totalHull" : 1,
"feature" : "",
"offline" : false
"totalHull": 1,
"feature": "",
"offline": false
},
"class": {
"templates": [
+185 -39
View File
@@ -12,7 +12,7 @@
from 1 to 12 and will create that number of columns. --}}
<div class="header">
<img class="profile" src="{{img}}" data-edit="img" title="{{name}}" height="150"
<img class="profile sla-portrait-static" src="{{img}}" data-edit="img" title="{{name}}" height="150"
width="150" />
<div class="headergrid">
@@ -29,7 +29,7 @@
<div class="headerinputtext">L.A.D.</div>
<label class="sla-header-checkbox" title="Life After Death">
<input type="checkbox" name="system.sla.lad" data-dtype="Boolean" {{checked system.sla.lad}} />
<span>Life After Death</span>
<span>L.A.D.</span>
</label>
</div>
<div class="header-field header-field-credits">
@@ -52,22 +52,26 @@
value="{{system.rank.value}}" placeholder="" /></div>
</div>
<div class="header-field header-field-sla-rank header-field-compact">
<div class="headerinputtext">SLA Rank</div>
<div class="header-field header-field-sla-rank">
<div class="headerinputtext">S.C.L</div>
<div class="headerinputfield sla-header-input charname"><input name="system.attributes.level.value" class="noborder"
type="text" value="{{system.attributes.level.value}}" placeholder="" /></div>
</div>
<div class="header-field header-field-height header-field-compact">
<div class="header-field header-field-height">
<div class="headerinputtext">Height</div>
<div class="headerinputfield sla-header-input charname"><input name="system.sla.height.value" class="noborder" type="text"
value="{{system.sla.height.value}}" placeholder="" /></div>
</div>
<div class="header-field header-field-weight header-field-compact">
<div class="header-field header-field-weight">
<div class="headerinputtext">Weight</div>
<div class="headerinputfield sla-header-input charname"><input name="system.sla.weightText.value" class="noborder" type="text"
value="{{system.sla.weightText.value}}" placeholder="" /></div>
</div>
<div class="header-field header-field-rest">
<div class="headerinputtext">&nbsp;</div>
<button type="button" class="sla-sheet-action sla-rest-button sla-header-rest-pill" title="Take a short or long rest">Rest</button>
</div>
</div>
</div>
@@ -203,7 +207,16 @@
{{/if}}
<!-- Trauma Response -->
<div class="" style="margin-left: 6px; margin-right: 0px; flex: 0; grid-column-start: 1; grid-column-end: 3;">
<div class="sla-trauma-header-row">
<div class="resource-label sla-stat-heading sla-trauma-heading">{{ localize 'Mosh.TraumaResponse' }}</div>
<div class="sla-session-ability-row">
<label class="sla-session-ability-label" title="Check when the session trauma ability has been used">
<input type="checkbox" class="sla-session-ability-used" {{#if system.sla.sessionAbilityUsed}}checked{{/if}} />
<span>Session ability used</span>
</label>
<button type="button" class="sla-new-session" title="Start a new session — resets session ability">New Session</button>
</div>
</div>
<textarea name="system.other.stressdesc.value" rows="4" class="textarea-input trauma-response-box" type="text"
data-target="system.other.stressdesc.value" placeholder="Trauma response">{{system.other.stressdesc.value}}</textarea>
</div>
@@ -381,6 +394,8 @@
<a class="tab-select" data-tab="traits">Traits</a>
<a class="tab-select" data-tab="special-rules">Special Rules</a>
{{#if isEbbUser}}<a class="tab-select" data-tab="ebb">Ebb</a>{{/if}}
{{#if isVevaphon}}<a class="tab-select" data-tab="morph">Morph</a>{{/if}}
<a class="tab-select" data-tab="contacts">Contacts</a>
<a class="tab-select" data-tab="description">{{ localize 'Mosh.Bio' }}</a>
<a class="tab-select" data-tab="notes">{{ localize 'Mosh.Notes' }}</a>
</nav>
@@ -388,6 +403,48 @@
{{!-- Sheet Body --}}
<section class="sheet-body">
{{!-- Contacts Tab --}}
<div class="tab items" data-group="primary" data-tab="contacts">
<div class="sla-panel">
<div class="sla-banner">
<div>
<div class="sla-kicker">Operative Network</div>
<div class="sla-title">Contacts & Associates</div>
</div>
<div class="sla-tags">
<span class="sla-tag">{{slaContactCount}} contact{{#unless (compare 'int' slaContactCount '===' 1)}}s{{/unless}}</span>
</div>
</div>
<div class="sla-contacts-toolbar">
<button type="button" class="sla-sheet-action sla-contact-add"><i class="fas fa-plus"></i> Add Contact</button>
</div>
{{#if slaContacts.length}}
<div class="sla-contacts-list">
{{#each slaContacts as |contact|}}
<div class="sla-contact-card" data-contact-id="{{contact.id}}">
<div class="sla-contact-header">
<div class="sla-contact-disposition sla-contact-disposition-{{contact.disposition}}"></div>
<div class="sla-contact-name">{{contact.name}}</div>
<div class="sla-contact-role">{{contact.role}}</div>
<div class="sla-contact-controls">
<button type="button" class="sla-contact-edit" data-contact-id="{{contact.id}}" title="Edit contact"><i class="fas fa-edit"></i></button>
<button type="button" class="sla-contact-delete" data-contact-id="{{contact.id}}" title="Delete contact"><i class="fas fa-trash"></i></button>
</div>
</div>
{{#if contact.notes}}
<div class="sla-contact-notes">{{contact.notes}}</div>
{{/if}}
</div>
{{/each}}
</div>
{{else}}
<div class="sla-note-pill sla-note-pill-wide">No contacts yet. Add allies, fixers, squad members, and other NPCs the operative knows.</div>
{{/if}}
</div>
</div>
{{!-- Biography Tab --}}
<div class="tab biography" data-group="primary" data-tab="description">
{{editor enriched.biography target="system.biography" button=true editable=true}}
@@ -399,6 +456,11 @@
<div class="sla-card-copy">Total spent: {{system.sla.ammoSpendTotal.value}} cR</div>
<textarea readonly rows="6" class="textarea-input">{{system.sla.ammoLedger.value}}</textarea>
</div>
<div class="sla-card sla-activity-log-card" style="margin-bottom: 12px;">
<div class="sla-card-label">Operative Activity Log</div>
<div class="sla-card-copy">Rolls, rests, drug use, panic — automatically recorded. Newest first.</div>
<textarea readonly rows="8" class="textarea-input sla-activity-log-textarea">{{system.sla.activityLog.value}}</textarea>
</div>
{{editor enriched.notes target="system.notes" button=true editable=true}}
</div>
<div class="tab items" data-group="primary" data-tab="sla">
@@ -412,6 +474,7 @@
<span class="sla-tag">{{system.sla.bpn.value}}</span>
<span class="sla-tag">SCL {{system.sla.scl.value}}</span>
{{#if isEbbUser}}<span class="sla-tag">Ebb {{system.sla.ebbRating.value}}</span>{{/if}}
{{#if isVevaphon}}<span class="sla-tag sla-instability-tag {{vevaphonInstabilityClass}}">Instability {{system.sla.vevaphonInstability.value}}/12</span>{{/if}}
</div>
</div>
@@ -476,7 +539,15 @@
</div>
<div class="resource">
<label class="resource-label">SCL</label>
<input type="text" name="system.sla.scl.value" value="{{system.sla.scl.value}}" data-dtype="String" />
<div class="sla-scl-control">
<input type="text" name="system.sla.scl.value" value="{{system.sla.scl.value}}" data-dtype="String" class="sla-scl-input" />
<div class="sla-scl-buttons">
<button type="button" class="sla-scl-adjust" data-delta="1" title="+1 SCL">+1</button>
<button type="button" class="sla-scl-adjust" data-delta="-1" title="-1 SCL">-1</button>
<button type="button" class="sla-scl-adjust" data-delta="0.5" title="+0.5 SCL">+½</button>
<button type="button" class="sla-scl-adjust" data-delta="-0.5" title="-0.5 SCL">-½</button>
</div>
</div>
</div>
<div class="resource">
<label class="resource-label">Employer / Squad</label>
@@ -814,6 +885,81 @@
</ol>
</div>
{{/if}}
{{#if isVevaphon}}
<div class="tab items" data-group="primary" data-tab="morph">
<div class="sla-panel">
<div class="sla-banner">
<div>
<div class="sla-kicker">Stormer Vevaphon</div>
<div class="sla-title">Polymorphic Form Control</div>
</div>
<div class="sla-tags">
<span class="sla-tag">Form: {{system.sla.morphForm.value}}</span>
<span class="sla-tag sla-instability-tag {{vevaphonInstabilityClass}}">Instability {{system.sla.vevaphonInstability.value}} / 12</span>
<span class="sla-tag">Sanity {{system.stats.sanity.value}}</span>
</div>
</div>
<div class="grid grid-2col sla-card-grid">
<div class="sla-card">
<div class="sla-card-label">Instability Track</div>
<div class="sla-instability-bar-wrap">
<div class="sla-instability-bar {{vevaphonInstabilityClass}}" style="width: {{vevaphonInstabilityPercent}}%;"></div>
<div class="sla-instability-bar-label {{vevaphonInstabilityClass}}">{{system.sla.vevaphonInstability.value}} / 12</div>
</div>
<div class="sla-card-copy">
{{#if vevaphonInstabilityCritical}}⚠ CRITICAL — Morph Panic risk on every failed Sanity save.{{/if}}
{{#if vevaphonInstabilityElevated}}Instability elevated — minor morph effects trigger each scene.{{/if}}
{{#unless vevaphonInstabilityElevated}}Instability nominal. Morphing is available without additional risk.{{/unless}}
</div>
<div class="sla-action-row" style="gap: 6px; flex-wrap: wrap;">
<button type="button" class="sla-instability-increase">+1 Instability</button>
<button type="button" class="sla-instability-decrease">-1 Instability</button>
</div>
<div class="sla-card-copy" style="margin-top: 8px;">At 12, the Vevaphon is lost — immediate retirement or death.</div>
</div>
<div class="sla-card">
<div class="sla-card-label">Active Morph Form</div>
<div class="sla-card-title">{{system.sla.morphForm.value}}</div>
<div class="sla-card-copy">Each morph shift costs 1 Instability. Changing form mid-scene requires a Sanity save at Instability 6+.</div>
<div class="sla-morph-form-buttons">
<button type="button" class="sla-morph-shift" data-form="Brute Form" {{#if (compare 'str' system.sla.morphForm.value '===' 'Brute Form')}}disabled{{/if}}>
<span class="sla-morph-btn-name">Brute Form</span>
<span class="sla-morph-btn-meta">STR+10, BODY+10 | Knockdown Immune</span>
</button>
<button type="button" class="sla-morph-shift" data-form="Stalker Form" {{#if (compare 'str' system.sla.morphForm.value '===' 'Stalker Form')}}disabled{{/if}}>
<span class="sla-morph-btn-name">Stalker Form</span>
<span class="sla-morph-btn-meta">SPD+10, Stealth+20 | Armour Incompatible</span>
</button>
<button type="button" class="sla-morph-shift" data-form="Raptor Form" {{#if (compare 'str' system.sla.morphForm.value '===' 'Raptor Form')}}disabled{{/if}}>
<span class="sla-morph-btn-name">Raptor Form</span>
<span class="sla-morph-btn-meta">COM+10, SPD+5 | Critical Wound Bonus</span>
</button>
</div>
</div>
</div>
<div class="grid grid-2col sla-card-grid">
<div class="sla-card">
<div class="sla-card-label">Morph Panic Reference</div>
<div class="sla-card-copy">Triggered at Instability 10+ on a failed Sanity save. Roll 1d6:</div>
<div class="sla-note-list">
<div class="sla-note-pill sla-note-pill-wide">1. Cascade Shift — random morph form, +1 Instability.</div>
<div class="sla-note-pill sla-note-pill-wide">2. Identity Bleed — forgets name and callsign until end of scene.</div>
<div class="sla-note-pill sla-note-pill-wide">3. Partial Lock — one limb freezes. -10 physical rolls using that limb.</div>
<div class="sla-note-pill sla-note-pill-wide">4. Tissue Rejection — 1d5 damage, armour does not apply.</div>
<div class="sla-note-pill sla-note-pill-wide">5. Threat Imprint — locked on nearest visible target.</div>
<div class="sla-note-pill sla-note-pill-wide">6. Full Reversion — base form, lose morph bonuses, -1 Instability.</div>
</div>
</div>
<div class="sla-card">
<div class="sla-card-label">Morph Notes</div>
<textarea name="system.sla.morphNotes.value" rows="10" class="textarea-input" data-target="system.sla.morphNotes.value">{{system.sla.morphNotes.value}}</textarea>
</div>
</div>
</div>
</div>
{{/if}}
{{!-- Owned Items Tab --}}
<div class="tab items" data-group="primary" data-tab="items">
{{#if drugAlerts.length}}
@@ -840,11 +986,11 @@
<li class="item flexrow item-header" role="rowgroup">
<div class="item-image" role="cell"></div>
<div class="skill-stat" style="flex-grow: 2.5;" role="cell">{{ localize 'Mosh.ArmorName' }}</div>
<div class="skill-stat" role="cell">{{ localize 'Mosh.AP' }}</div>
<div class="skill-stat" role="cell">{{ localize 'Mosh.DR' }}</div>
<div class="skill-stat" role="cell">{{ localize 'Mosh.Speed' }}</div>
<div class="skill-stat" role="cell">{{ localize 'Mosh.Oxygen' }}</div>
<div class="skill-stat" role="cell">Carry</div>
<div class="skill-stat" role="cell" title="Armour Protection — reduces incoming damage before it reaches your wounds. Higher is better.">{{ localize 'Mosh.AP' }}</div>
<div class="skill-stat" role="cell" title="Damage Reduction — subtracts directly from wound damage after AP is applied. Each point absorbed is one less point of damage taken.">{{ localize 'Mosh.DR' }}</div>
<div class="skill-stat" role="cell" title="Speed penalty — how much this armour slows movement. Expressed as a % penalty to physical speed checks.">{{ localize 'Mosh.Speed' }}</div>
<div class="skill-stat" role="cell" title="Oxygen supply — current / maximum charges for sealed suits and vacuum armour. N/A means the suit is not airtight.">{{ localize 'Mosh.Oxygen' }}</div>
<div class="skill-stat" role="cell" title="Carry state — Eqp means wearing it now (weight counts against your equipped load). Stow means packed away (counts as stowed weight).">Carry</div>
<div class="item-controls" role="cell">
<a class="item-control item-create" title="{{ localize 'Mosh.CreateArmor' }}" data-type="armor"><i class="fas fa-plus"></i> {{ localize 'Mosh.Add' }}</a>
</div>
@@ -881,10 +1027,10 @@
<li class="item flexrow item-header" role="rowgroup">
<div class="item-image" role="cell"></div>
<div class="skill-stat" style="flex-grow: 2.5;" role="cell">{{ localize 'Mosh.ArmorName' }}</div>
<div class="skill-stat" role="cell">{{ localize 'Mosh.SaveBonus' }}</div>
<div class="skill-stat" role="cell">{{ localize 'Mosh.Speed' }}</div>
<div class="skill-stat" role="cell">{{ localize 'Mosh.Oxygen' }}</div>
<div class="skill-stat" role="cell">Carry</div>
<div class="skill-stat" role="cell" title="Save Bonus — this value is added to your Body save when checking to resist physical damage while wearing this armour.">{{ localize 'Mosh.SaveBonus' }}</div>
<div class="skill-stat" role="cell" title="Speed penalty — how much this armour slows movement. Expressed as a % penalty to physical speed checks.">{{ localize 'Mosh.Speed' }}</div>
<div class="skill-stat" role="cell" title="Oxygen supply — current / maximum charges for sealed suits and vacuum armour. N/A means the suit is not airtight.">{{ localize 'Mosh.Oxygen' }}</div>
<div class="skill-stat" role="cell" title="Carry state — Eqp means wearing it now (weight counts against your equipped load). Stow means packed away (counts as stowed weight).">Carry</div>
<div class="item-controls" role="cell">
<a class="item-control item-create" title="{{ localize 'Mosh.CreateArmor' }}" data-type="armor"><i class="fas fa-plus"></i> {{ localize 'Mosh.Add' }}</a>
</div>
@@ -921,13 +1067,13 @@
<li class="item flexrow item-header">
<div class="item-image"></div>
<div class="skill-stat" style="flex-grow: 1.6;">Drug</div>
<div class="skill-stat">Qty</div>
<div class="skill-stat">Rating</div>
<div class="skill-stat">Value</div>
<div class="skill-stat">Weight</div>
<div class="skill-stat">Carry</div>
<div class="skill-stat">State</div>
<div class="skill-stat">Duration</div>
<div class="skill-stat" title="Quantity — number of doses currently carried.">Qty</div>
<div class="skill-stat" title="Rating — the potency tier of this drug. Higher ratings mean stronger effects and harder addiction saves.">Rating</div>
<div class="skill-stat" title="Value — street price per dose in credits (cR).">Value</div>
<div class="skill-stat" title="Weight — total carry weight for all doses of this drug.">Weight</div>
<div class="skill-stat" title="Carry state — Eqp means on your person and immediately accessible. Stow means packed away in your kit.">Carry</div>
<div class="skill-stat" title="State — current drug status. Ready means unused. Active means the drug is currently in effect. Crashed means the effect has ended and the character may be experiencing comedown penalties.">State</div>
<div class="skill-stat" title="Duration — how many rounds or minutes of effect remain. Blank or 0 means the drug is not currently active.">Duration</div>
<div class="item-controls">
<a class="item-control item-create" title="Create Drug" data-type="drug"><i class="fas fa-plus"></i> Add</a>
</div>
@@ -962,11 +1108,11 @@
<ol class="items-list">
<li class="item flexrow item-header">
<div class="item-image"></div>
<div class="skill-stat" style="flex-grow: 1.5;">{{ localize 'Mosh.ItemName' }}</div>
<div class="skill-stat">{{ localize 'Mosh.Quantity' }}</div>
<div class="skill-stat">{{ localize 'Mosh.Weight' }}</div>
<div class="skill-stat">Carry</div>
<div class="skill-stat">{{ localize 'Mosh.Value' }}</div>
<div class="skill-stat" style="flex-grow: 1.5;" title="Hover over any item name to see its description.">{{ localize 'Mosh.ItemName' }}</div>
<div class="skill-stat" title="Quantity — how many of this item you are carrying.">{{ localize 'Mosh.Quantity' }}</div>
<div class="skill-stat" title="Weight — total carry weight for all units of this item (kg).">{{ localize 'Mosh.Weight' }}</div>
<div class="skill-stat" title="Carry state — Eqp means on your person and ready to use. Stow means packed away. Equipped items count against your active carry limit; stowed items count against your total carry limit.">Carry</div>
<div class="skill-stat" title="Value — purchase price in credits (cR). Sell price is typically half.">{{ localize 'Mosh.Value' }}</div>
<div class="item-controls">
<a class="item-control item-create" title="{{ localize 'Mosh.CreateItem' }}" data-type="item"><i class="fas fa-plus"></i> Add</a>
</div>
@@ -1008,10 +1154,10 @@
<ol class="items-list" style="margin-bottom : 10px;">
<li class="item flexrow item-header">
<div class="item-image"></div>
<div class="skill-stat" style="flex-grow: 1.4;">{{ localize 'Mosh.SkillName' }}</div>
<div class="skill-stat">Category</div>
<div class="skill-stat">{{ localize 'Mosh.SkillRank' }}</div>
<div class="skill-stat">{{ localize 'Mosh.SkillBonus' }}</div>
<div class="skill-stat" style="flex-grow: 1.4;" title="Click a skill name to roll it against the relevant stat. Your bonus is added to a d100 roll — you succeed by rolling under the total.">{{ localize 'Mosh.SkillName' }}</div>
<div class="skill-stat" title="Category — broad skill grouping (Combat, Technical, Social, etc.) used for package sorting and Ebb discipline filtering.">Category</div>
<div class="skill-stat" title="Rank — your proficiency level in this skill. Untrained (no bonus), Basic (+1–9), Trained (+10–14), Expert (+15–19), Master (+20+).">{{ localize 'Mosh.SkillRank' }}</div>
<div class="skill-stat" title="Bonus — total modifier added to d100 rolls with this skill. Breakdown shown in small text below the value. Higher is better — you need to roll under (stat + bonus).">{{ localize 'Mosh.SkillBonus' }}</div>
<div class="item-controls">
<a class="item-control skill-create" title="{{ localize 'Mosh.CreateSkill' }}" data-type="skill"><i class="fas fa-plus"></i> {{ localize 'Mosh.Add' }}</a>
</div>
@@ -1108,13 +1254,13 @@
<ol class="items-list">
<li class="item flexrow item-header">
<div class="item-image"></div>
<div class="skill-stat" style="flex-grow: 2;">{{ localize 'Mosh.WeaponName' }}</div>
<div class="skill-stat">{{ localize 'Mosh.Damage' }}</div>
<div class="skill-stat">{{ localize 'Mosh.Ammo' }}</div>
<div class="skill-stat">{{ localize 'Mosh.Shots' }}</div>
<div class="skill-stat">Mode</div>
<div class="skill-stat">Carry</div>
<div class="skill-stat">{{ localize 'Mosh.Range' }}</div>
<div class="skill-stat" style="flex-grow: 2;" title="Click a weapon name to make an attack roll using your Combat stat. Calibre and current ammo state shown below the name.">{{ localize 'Mosh.WeaponName' }}</div>
<div class="skill-stat" title="Damage — rolled on a successful hit. Melee weapons may add your Strength modifier. Anti-Armour (AA) weapons halve target armour before applying damage.">{{ localize 'Mosh.Damage' }}</div>
<div class="skill-stat" title="Ammo — currently loaded type (STD / AP / HE / HEAP) and reserve count. STD is standard. AP is armour-piercing (halves target armour, -1 damage). HE is high explosive (+1d5 damage, explosive wounds). HEAP combines AP and HE effects.">{{ localize 'Mosh.Ammo' }}</div>
<div class="skill-stat" title="Shots — current magazine / maximum magazine capacity. Click the reload icon to reload or swap ammo type.">{{ localize 'Mosh.Shots' }}</div>
<div class="skill-stat" title="Fire Mode — click to cycle between available modes. Single fires one shot. Burst fires several rounds for bonus damage but uses more ammo. Auto lays down suppressing fire.">Mode</div>
<div class="skill-stat" title="Carry state — Eqp means the weapon is drawn and ready. Stow means holstered or packed. Only equipped weapons can be used in combat without an action to draw.">Carry</div>
<div class="skill-stat" title="Range — maximum effective distance. At short range you roll normally. At medium range you take a penalty. At long range the penalty increases significantly. Beyond max range the weapon cannot reach.">{{ localize 'Mosh.Range' }}</div>
<div class="item-controls">
<a class="item-control weapon-create" title="{{ localize 'Mosh.CreateWeapon' }}" data-type="weapon"><i class="fas fa-plus"></i> {{ localize 'Mosh.Add' }}</a>
</div>