Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96896ba92f | ||
|
|
6557ef82ad | ||
|
|
17c64ec0de | ||
|
|
64bef08fe0 | ||
|
|
d81034f6f4 | ||
|
|
f66ac84c0b | ||
|
|
24f513baf8 | ||
|
|
9269348bf4 | ||
|
|
2758be6096 | ||
|
|
b1cfb636df | ||
|
|
0d77741a41 | ||
|
|
6508a7e883 | ||
|
|
8cbea7c237 | ||
|
|
d89671e707 | ||
|
|
d0d907bd98 | ||
|
|
f3264d60e2 | ||
|
|
63da87f372 | ||
|
|
4a5561a82f | ||
|
|
9fb7867069 | ||
|
|
b64125e637 | ||
|
|
c728f7bb6d | ||
|
|
19b472de00 | ||
|
|
bf4e98a780 | ||
|
|
3d21b8e466 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<rect x="14" y="12" width="36" height="40" rx="6" stroke="#F4F7FA" stroke-width="4"/>
|
||||
<circle cx="24" cy="27" r="5" stroke="#F4F7FA" stroke-width="4"/>
|
||||
<path d="M19 40c2-4 5-6 9-6s7 2 9 6M36 24h8M36 32h8M36 40h8" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 358 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<path d="M20 50V18l12-8 12 8v32" stroke="#F4F7FA" stroke-width="4" stroke-linejoin="round"/>
|
||||
<path d="M26 24h3M35 24h3M26 31h3M35 31h3M26 38h3M35 38h3" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M16 50h32" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 376 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<path d="M39 10c-8 4-13 12-13 22 0 10 5 18 13 22-13-1.5-24-11.8-24-27S26 11.5 39 10Z" fill="#F4F7FA"/>
|
||||
<path d="m40 21 8 11-8 11" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 297 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<path d="M10 32c5.7-9 13.1-13.5 22-13.5S48.3 23 54 32c-5.7 9-13.1 13.5-22 13.5S15.7 41 10 32Z" stroke="#F4F7FA" stroke-width="4" stroke-linejoin="round"/>
|
||||
<circle cx="32" cy="32" r="7" stroke="#F4F7FA" stroke-width="4"/>
|
||||
<circle cx="32" cy="32" r="2.5" fill="#F4F7FA"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 356 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<path d="M23 18h18l5 8-3 15H21l-3-15 5-8Z" stroke="#F4F7FA" stroke-width="4" stroke-linejoin="round"/>
|
||||
<circle cx="26" cy="29" r="2.5" fill="#F4F7FA"/>
|
||||
<circle cx="38" cy="29" r="2.5" fill="#F4F7FA"/>
|
||||
<path d="M26 37c2 2 4 3 6 3s4-1 6-3" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 385 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<path d="M18 12h20l8 8v28a4 4 0 0 1-4 4H18a4 4 0 0 1-4-4V16a4 4 0 0 1 4-4Z" stroke="#F4F7FA" stroke-width="4" stroke-linejoin="round"/>
|
||||
<path d="M38 12v10h10M24 30h12M24 38h16" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 320 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<path d="M32 16v28M24 44h16" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round"/>
|
||||
<path d="M22 22c2.5-3.5 6-5 10-5s7.5 1.5 10 5M16 16c4.3-5.3 9.6-8 16-8s11.7 2.7 16 8M16 28c4.3 5.3 9.6 8 16 8s11.7-2.7 16-8" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 356 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<path d="M18 48 28 16M30 50l8-26M42 48l6-18" stroke="#F4F7FA" stroke-width="5" stroke-linecap="round"/>
|
||||
<path d="M18 48c2-4 6-7 10-8M30 50c2-4 5-7 8-8M42 48c1.5-3 4-5.5 7-6.5" stroke="#F4F7FA" stroke-width="3.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 321 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<path d="M32 8 48 14v14c0 11-6.4 20.8-16 28-9.6-7.2-16-17-16-28V14L32 8Z" stroke="#F4F7FA" stroke-width="4" stroke-linejoin="round"/>
|
||||
<path d="M32 18v24M22 30h20" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 306 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<path d="M24 10h16l8 14-8 14H24l-8-14 8-14Z" stroke="#F4F7FA" stroke-width="4" stroke-linejoin="round"/>
|
||||
<circle cx="32" cy="24" r="4" fill="#F4F7FA"/>
|
||||
<path d="M32 28v18M32 38h10M32 46H22" stroke="#F4F7FA" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 335 B |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -141,6 +141,30 @@ function decorateSkillWithTraitPreview(skill, traitRows = []) {
|
||||
return skill;
|
||||
}
|
||||
|
||||
function getPurchasableItemTotalCost(itemData = {}) {
|
||||
const system = itemData?.system ?? itemData?.data ?? {};
|
||||
const unitCost = Number(system.cost ?? 0) || 0;
|
||||
const quantity = Math.max(1, Number(system.quantity ?? 1) || 1);
|
||||
return Math.round(unitCost * quantity * 100) / 100;
|
||||
}
|
||||
|
||||
function isAmmoPurchaseItem(itemData = {}) {
|
||||
const system = itemData?.system ?? itemData?.data ?? {};
|
||||
const sla = system?.sla ?? {};
|
||||
const name = String(itemData?.name ?? "").trim().toLowerCase();
|
||||
const category = String(sla.category ?? "").trim().toLowerCase();
|
||||
const calibre = String(sla.calibre ?? "").trim();
|
||||
const ammoTag = String(sla.ammoTag ?? "").trim();
|
||||
return category === "ammunition" || Boolean((calibre && ammoTag) || name.startsWith("ammo:"));
|
||||
}
|
||||
|
||||
function formatSlaTimestamp() {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short"
|
||||
}).format(new Date());
|
||||
}
|
||||
|
||||
const SLA_SPECIES_RULES = {
|
||||
Human: [
|
||||
{ key: "adaptable", label: "Adaptable", summary: "Once per session, reroll one Stat check and take the better result.", optional: false, default: true }
|
||||
@@ -258,6 +282,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;
|
||||
@@ -279,6 +306,19 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
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.vevaphonInstabilityDesc = instab >= 10
|
||||
? "Instability critical. Full reversion risk on every shift."
|
||||
: instab >= 6
|
||||
? "Instability elevated. Sanity saves required when morphing mid-scene."
|
||||
: "Instability nominal. Morphing is available without additional risk.";
|
||||
data.data.vevaphonMorphPanic = [
|
||||
{ roll: 1, label: "Cascade Shift", desc: "Random morph form, +1 Instability." },
|
||||
{ roll: 2, label: "Identity Bleed", desc: "Forgets name and callsign until end of scene." },
|
||||
{ roll: 3, label: "Partial Lock", desc: "One limb freezes. −10 physical rolls using that limb." },
|
||||
{ roll: 4, label: "Tissue Rejection", desc: "1d5 damage, armour does not apply." },
|
||||
{ roll: 5, label: "Threat Imprint", desc: "Locked on nearest visible target." },
|
||||
{ roll: 6, label: "Full Reversion", desc: "Base form, lose morph bonuses, −1 Instability." }
|
||||
];
|
||||
}
|
||||
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);
|
||||
@@ -292,6 +332,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;
|
||||
@@ -406,6 +457,7 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
const armors = [];
|
||||
const conditions = [];
|
||||
const abilities = [];
|
||||
const morphForms = [];
|
||||
const persTraits = [];
|
||||
|
||||
let curWeight = 0;
|
||||
@@ -512,7 +564,11 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
|
||||
conditions.push(i);
|
||||
} else if (i.type === 'ability') {
|
||||
abilities.push(i);
|
||||
if (i.system?.sla?.morphForm) {
|
||||
morphForms.push(i);
|
||||
} else {
|
||||
abilities.push(i);
|
||||
}
|
||||
} else if (i.type === 'persTrait') {
|
||||
persTraits.push(i);
|
||||
}
|
||||
@@ -549,6 +605,9 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
actorData.weapons = weapons;
|
||||
actorData.conditions = conditions;
|
||||
actorData.abilities = abilities;
|
||||
const activeMorphFormName = String(actorData.system?.sla?.morphForm?.value ?? "").trim();
|
||||
morphForms.forEach(f => { f.slaIsActiveMorphForm = f.name === activeMorphFormName; });
|
||||
actorData.slaMorphForms = morphForms;
|
||||
actorData.persTraits = persTraits.sort((left, right) => String(left?.name ?? "").localeCompare(String(right?.name ?? "")));
|
||||
|
||||
}
|
||||
@@ -663,19 +722,26 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
//Quantity adjuster
|
||||
html.on('mousedown', '.item-quantity', ev => {
|
||||
const li = ev.currentTarget.closest(".item");
|
||||
//const item = duplicate(this.actor.getEmbeddedDocument("Item", li.dataset.itemId))
|
||||
var item;
|
||||
item = foundry.utils.duplicate(this.actor.getEmbeddedDocument("Item", li.dataset.itemId));
|
||||
|
||||
let amount = item.system.quantity;
|
||||
const sourceItem = this.actor.getEmbeddedDocument("Item", li.dataset.itemId);
|
||||
if (!sourceItem) return;
|
||||
const item = foundry.utils.duplicate(sourceItem);
|
||||
const amount = Math.max(0, Number(item.system?.quantity ?? 0) || 0);
|
||||
let delta = 0;
|
||||
|
||||
if (event.button == 0) {
|
||||
item.system.quantity = Number(amount) + 1;
|
||||
} else if (event.button == 2) {
|
||||
item.system.quantity = Number(amount) - 1;
|
||||
if (ev.button === 0) {
|
||||
delta = 1;
|
||||
} else if (ev.button === 2) {
|
||||
delta = -1;
|
||||
}
|
||||
|
||||
this.actor.updateEmbeddedDocuments('Item', [item]);
|
||||
if (!delta) return;
|
||||
item.system.quantity = Math.max(0, amount + delta);
|
||||
|
||||
this.actor.updateEmbeddedDocuments('Item', [item]).then(async () => {
|
||||
if (delta > 0) {
|
||||
await this._applySlaQuantityIncreaseDebit(sourceItem, delta);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//Severity adjuster
|
||||
@@ -760,7 +826,7 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
var item;
|
||||
item = foundry.utils.duplicate(this.actor.getEmbeddedDocument("Item", li.dataset.itemId));
|
||||
|
||||
this.actor.rollCheck(null, null, null, item.name, item.system.bonus, null);
|
||||
this.actor.rollCheck(null, null, null, item.name, item.system.bonus, null, null, { promptModifier: true });
|
||||
});
|
||||
|
||||
html.find('.sla-ebb-skill-roll').click(ev => {
|
||||
@@ -792,7 +858,7 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
//const item = duplicate(this.actor.getEmbeddedDocument("Item", li.dataset.itemId));
|
||||
var item;
|
||||
item = foundry.utils.duplicate(this.actor.getEmbeddedDocument("Item", li.dataset.itemId));
|
||||
this.actor.rollWeaponAttack(item);
|
||||
this.actor.rollWeaponAttack(item, { promptModifier: true });
|
||||
});
|
||||
|
||||
// Rollable Damage
|
||||
@@ -933,8 +999,123 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
this.render(false);
|
||||
});
|
||||
|
||||
html.find('.sla-panic-button').click(ev => {
|
||||
this.actor.rollTable("panicCheck", null, null, null, null, null, null);
|
||||
// SCL fractional adjustment buttons
|
||||
html.find('.sla-scl-adjust').click(async (ev) => {
|
||||
ev.preventDefault();
|
||||
const delta = parseFloat(ev.currentTarget.dataset.delta ?? 0);
|
||||
if (!delta) return;
|
||||
const current = parseFloat(String(this.actor.system?.sla?.scl?.value ?? "0").replace(",", ".")) || 0;
|
||||
const next = Math.max(0, Math.round((current + delta) * 10) / 10);
|
||||
const formatted = Number.isInteger(next) ? String(next) : next.toFixed(1);
|
||||
await this.actor.update({ "system.sla.scl.value": formatted });
|
||||
this.render(false);
|
||||
});
|
||||
|
||||
// Session ability tracking — checkbox
|
||||
html.find('.sla-session-ability-used').on('change', async (ev) => {
|
||||
ev.stopPropagation();
|
||||
await this.actor.update({ "system.sla.sessionAbilityUsed": ev.currentTarget.checked });
|
||||
});
|
||||
|
||||
// New Session reset button
|
||||
html.find('.sla-new-session').click(async (ev) => {
|
||||
ev.preventDefault();
|
||||
const confirmed = await Dialog.confirm({
|
||||
title: "New Session",
|
||||
content: "<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 => {
|
||||
@@ -1068,6 +1249,91 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
async _onDrop(event) {
|
||||
const data = TextEditor.getDragEventData(event);
|
||||
if (data?.type !== "Item") return super._onDrop(event);
|
||||
|
||||
const droppedItem = await Item.implementation.fromDropData(data).catch(() => null);
|
||||
if (!droppedItem) return super._onDrop(event);
|
||||
|
||||
if (droppedItem.parent?.uuid === this.actor.uuid) {
|
||||
return super._onDrop(event);
|
||||
}
|
||||
|
||||
const shouldDebitPurchase = !droppedItem.parent || droppedItem.parent.documentName !== "Actor";
|
||||
const itemData = droppedItem.toObject();
|
||||
delete itemData._id;
|
||||
delete itemData.folder;
|
||||
const created = await this.actor.createEmbeddedDocuments("Item", [itemData], {
|
||||
slaPurchaseDebit: true,
|
||||
slaPurchaseSource: data.uuid ?? droppedItem.uuid ?? ""
|
||||
});
|
||||
if (shouldDebitPurchase) {
|
||||
await this._applySlaPurchaseDebit(created);
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
async _applySlaPurchaseDebit(createdDocs = []) {
|
||||
const documents = Array.isArray(createdDocs) ? createdDocs : [createdDocs];
|
||||
const total = Math.round(documents.reduce((sum, doc) => sum + getPurchasableItemTotalCost(doc?.toObject?.() ?? doc), 0) * 100) / 100;
|
||||
if (!total) return;
|
||||
|
||||
const currentCredits = Number(this.actor.system?.credits?.value ?? 0) || 0;
|
||||
const newCredits = Math.round((currentCredits - total) * 100) / 100;
|
||||
const updateData = { "system.credits.value": newCredits };
|
||||
|
||||
const itemNames = documents.map((doc) => doc?.name).filter(Boolean).join(", ");
|
||||
const ammoDocs = documents
|
||||
.map((doc) => doc?.toObject?.() ?? doc)
|
||||
.filter((doc) => isAmmoPurchaseItem(doc));
|
||||
if (ammoDocs.length) {
|
||||
const stamp = formatSlaTimestamp();
|
||||
const currentLedger = String(this.actor.system?.sla?.ammoLedger?.value ?? "").trim();
|
||||
const purchaseEntries = ammoDocs.map((doc) => {
|
||||
const system = doc?.system ?? doc?.data ?? {};
|
||||
const quantity = Math.max(1, Number(system.quantity ?? 1) || 1);
|
||||
const unitCost = Number(system.cost ?? 0) || 0;
|
||||
const lineCost = Math.round(unitCost * quantity * 100) / 100;
|
||||
return `[${stamp}] PURCHASE | ${doc?.name ?? "Ammo"} | ${quantity} round${quantity === 1 ? "" : "s"} | -${lineCost} cR | balance ${newCredits} cR`;
|
||||
});
|
||||
const ledgerLines = [...purchaseEntries, ...currentLedger.split("\n").filter(Boolean)].slice(0, 60);
|
||||
updateData["system.sla.ammoLedger.value"] = ledgerLines.join("\n");
|
||||
}
|
||||
|
||||
await this.actor.update(updateData);
|
||||
|
||||
ui.notifications.info(`${this.actor.name}: deducted ${total} cR for ${itemNames || "purchased item"}. Credits now ${newCredits} cR.`);
|
||||
await this.actor.addSlaActivityLog?.(`Purchased ${itemNames || "item"} for ${total} cR. Credits now ${newCredits} cR.`);
|
||||
}
|
||||
|
||||
async _applySlaQuantityIncreaseDebit(itemDoc, increase = 0) {
|
||||
const added = Math.max(0, Number(increase ?? 0) || 0);
|
||||
if (!added || !itemDoc) return;
|
||||
|
||||
const unitCost = Number(itemDoc.system?.cost ?? 0) || 0;
|
||||
const total = Math.round(unitCost * added * 100) / 100;
|
||||
if (!total) return;
|
||||
|
||||
const currentCredits = Number(this.actor.system?.credits?.value ?? 0) || 0;
|
||||
const newCredits = Math.round((currentCredits - total) * 100) / 100;
|
||||
const updateData = { "system.credits.value": newCredits };
|
||||
|
||||
if (isAmmoPurchaseItem(itemDoc)) {
|
||||
const stamp = formatSlaTimestamp();
|
||||
const currentLedger = String(this.actor.system?.sla?.ammoLedger?.value ?? "").trim();
|
||||
const entry = `[${stamp}] PURCHASE | ${itemDoc.name ?? "Ammo"} | ${added} round${added === 1 ? "" : "s"} | -${total} cR | balance ${newCredits} cR`;
|
||||
const ledgerLines = [entry, ...currentLedger.split("\n").filter(Boolean)].slice(0, 60);
|
||||
updateData["system.sla.ammoLedger.value"] = ledgerLines.join("\n");
|
||||
}
|
||||
|
||||
await this.actor.update(updateData);
|
||||
ui.notifications.info(`${this.actor.name}: deducted ${total} cR for ${added} more ${itemDoc.name ?? "item"}. Credits now ${newCredits} cR.`);
|
||||
await this.actor.addSlaActivityLog?.(`Purchased +${added} ${itemDoc.name ?? "item"} for ${total} cR. Credits now ${newCredits} cR.`);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
|
||||
* @param {Event} event The originating click event
|
||||
@@ -1210,6 +1476,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
|
||||
|
||||
@@ -4,6 +4,21 @@ import { SLADrugSystem } from "../sla-drug-system.js";
|
||||
import { SLA_SPECIES_BALANCE_VERSION, getSlaSpeciesStatAdjustments } from "../sla-species-balance.js";
|
||||
import { buildSlaTraumaResponseText } from "../sla-trauma.js";
|
||||
|
||||
// Global event delegation to handle interactive range slider value updates
|
||||
// in a way that is fully compatible with Foundry VTT's HTML sanitization.
|
||||
if (typeof document !== "undefined") {
|
||||
document.addEventListener("input", (event) => {
|
||||
const target = event.target;
|
||||
if (target && target.tagName === "INPUT" && target.name === "rollModifier") {
|
||||
const value = Number(target.value) || 0;
|
||||
const label = target.closest(".resource")?.querySelector(".slider-value");
|
||||
if (label) {
|
||||
label.innerText = (value >= 0 ? "+" : "") + value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const SLA_AMMO_TYPES = {
|
||||
STD: { label: "Standard", multiplier: 1, damageBonus: "", woundEffect: "", summary: "Standard ammunition." },
|
||||
AP: { label: "AP", multiplier: 2, damageBonus: "-1", woundEffect: "Gunshot [-]", summary: "Armour-piercing ammunition. Treat target armour as halved and reduce damage by 1." },
|
||||
@@ -402,14 +417,6 @@ 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;
|
||||
@@ -607,9 +614,123 @@ export class MothershipActor extends Actor {
|
||||
return rollStringParsed;
|
||||
}
|
||||
|
||||
clampSlaRollTarget(rollTarget, rollString = "") {
|
||||
const numericTarget = Number(rollTarget);
|
||||
if (!Number.isFinite(numericTarget)) return rollTarget;
|
||||
const usesD100 = String(rollString ?? "").includes("1d100");
|
||||
if (!usesD100) return numericTarget;
|
||||
return Math.min(98, numericTarget);
|
||||
}
|
||||
|
||||
isSlaAutomaticFailure(total, rollString = "") {
|
||||
const numericTotal = Number(total);
|
||||
if (!Number.isFinite(numericTotal)) return false;
|
||||
const usesD100 = String(rollString ?? "").includes("1d100");
|
||||
return usesD100 && numericTotal === 99;
|
||||
}
|
||||
|
||||
getSlaChatVariant(context = {}) {
|
||||
const actorClass = String(this.system?.class?.value ?? "").trim();
|
||||
const employer = String(this.system?.sla?.employer?.value ?? "").trim();
|
||||
const sector = String(this.system?.sla?.sector?.value ?? "").trim();
|
||||
const skill = String(context?.skill ?? "").trim();
|
||||
const skillCategory = String(context?.skillCategory ?? "").trim();
|
||||
const tableName = String(context?.tableName ?? "").trim();
|
||||
const header = String(context?.msgHeader ?? "").trim();
|
||||
const weaponName = String(context?.weapon?.name ?? "").trim();
|
||||
const itemName = String(context?.item?.name ?? "").trim();
|
||||
const extra = String(context?.extra ?? "").trim();
|
||||
|
||||
const haystack = [
|
||||
actorClass,
|
||||
employer,
|
||||
sector,
|
||||
skill,
|
||||
skillCategory,
|
||||
tableName,
|
||||
header,
|
||||
weaponName,
|
||||
itemName,
|
||||
extra
|
||||
].join(" ").toLowerCase();
|
||||
|
||||
if (context?.isSlaEbbSkill || /\bebb\b|ebon|brain\swaste|flux|telekinesis|blast|awareness/.test(haystack)) {
|
||||
return { key: "ebb", label: "Ebb", emblem: "EBB" };
|
||||
}
|
||||
if (/\bdark\s?night\b/.test(haystack)) {
|
||||
return { key: "darknight", label: "DarkNight", emblem: "DN" };
|
||||
}
|
||||
if (/\bshiver\b/.test(haystack)) {
|
||||
return { key: "shiver", label: "Shiver", emblem: "SV" };
|
||||
}
|
||||
if (/\bmort\b|mortuary|undertaker|corpse|cadaver/.test(haystack)) {
|
||||
return { key: "mort", label: "Mort", emblem: "MT" };
|
||||
}
|
||||
if (/\btek\b|technical|engineer|engineering|medic|medical|science|research/.test(haystack)) {
|
||||
return { key: "tek", label: "Tek", emblem: "TK" };
|
||||
}
|
||||
if (/\bops\b|operations|opscon|ops\scon|operative|squad/.test(haystack)) {
|
||||
return { key: "ops", label: "Ops", emblem: "OP" };
|
||||
}
|
||||
return { key: "sla", label: "SLA", emblem: "SLA" };
|
||||
}
|
||||
|
||||
getSlaSeverityMeta({ severity = null, tableName = "", specialRoll = "", tableResultNumber = null } = {}) {
|
||||
const rawSeverity = Number(severity);
|
||||
if (Number.isFinite(rawSeverity)) {
|
||||
if (rawSeverity >= 5) return { key: "critical", label: "Critical", cssClass: "sla-severity-critical" };
|
||||
if (rawSeverity >= 3) return { key: "severe", label: "Severe", cssClass: "sla-severity-severe" };
|
||||
return { key: "minor", label: "Minor", cssClass: "sla-severity-minor" };
|
||||
}
|
||||
|
||||
const name = String(tableName ?? "").toLowerCase();
|
||||
const special = String(specialRoll ?? "").toLowerCase();
|
||||
const numericResult = Number(tableResultNumber);
|
||||
|
||||
if (!Number.isFinite(numericResult)) {
|
||||
return { key: "minor", label: "Minor", cssClass: "sla-severity-minor" };
|
||||
}
|
||||
|
||||
const isPanic = special === "paniccheck" || name.includes("panic");
|
||||
const isWound = name.includes("wound");
|
||||
|
||||
if (isPanic) {
|
||||
if (numericResult >= 16) return { key: "critical", label: "Catastrophic", cssClass: "sla-severity-critical" };
|
||||
if (numericResult >= 9) return { key: "severe", label: "Severe", cssClass: "sla-severity-severe" };
|
||||
return { key: "minor", label: "Minor", cssClass: "sla-severity-minor" };
|
||||
}
|
||||
|
||||
if (isWound) {
|
||||
if (numericResult >= 8) return { key: "critical", label: "Critical", cssClass: "sla-severity-critical" };
|
||||
if (numericResult >= 4) return { key: "severe", label: "Severe", cssClass: "sla-severity-severe" };
|
||||
return { key: "minor", label: "Minor", cssClass: "sla-severity-minor" };
|
||||
}
|
||||
|
||||
return { key: "minor", label: "Minor", cssClass: "sla-severity-minor" };
|
||||
}
|
||||
|
||||
async addSlaActivityLog(entry, { maxEntries = 40 } = {}) {
|
||||
const text = String(entry ?? "").trim();
|
||||
if (!text || this.type !== "character") return null;
|
||||
|
||||
const timestamp = new Date().toLocaleString("en-GB", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit"
|
||||
});
|
||||
const currentLog = String(this.system?.sla?.activityLog?.value ?? "").trim();
|
||||
const existingLines = currentLog ? currentLog.split("\n").filter(Boolean) : [];
|
||||
const nextLines = [`[${timestamp}] ${text}`, ...existingLines].slice(0, Math.max(1, maxEntries));
|
||||
await this.update({ "system.sla.activityLog.value": nextLines.join("\n") });
|
||||
return nextLines[0];
|
||||
}
|
||||
|
||||
//central roll parsing function | TAKES '1d100',[Foundry roll object],true,true,41,'<' | RETURNS enriched Foundry roll object
|
||||
parseRollResult(rollString, rollResult, zeroBased, checkCrit, rollTarget, comparison, specialRoll) {
|
||||
//init vars
|
||||
rollTarget = this.clampSlaRollTarget(rollTarget, rollString);
|
||||
let doubles = new Set([0, 11, 22, 33, 44, 55, 66, 77, 88, 99]);
|
||||
let enrichedRollResult = rollResult;
|
||||
let rollFormula = enrichedRollResult.formula;
|
||||
@@ -678,29 +799,29 @@ export class MothershipActor extends Actor {
|
||||
if (rollString.includes("[")) {
|
||||
//set whether each die succeeded
|
||||
//die 0
|
||||
if (comparison === '<' && die0value < rollTarget && die0value < 90) {
|
||||
if (comparison === '<' && die0value < rollTarget && !this.isSlaAutomaticFailure(die0value, rollString)) {
|
||||
die0success = true;
|
||||
}
|
||||
if (comparison === '<=' && die0value <= rollTarget && die0value < 90) {
|
||||
if (comparison === '<=' && die0value <= rollTarget && !this.isSlaAutomaticFailure(die0value, rollString)) {
|
||||
die0success = true;
|
||||
}
|
||||
if (comparison === '>' && die0value > rollTarget && die0value < 90) {
|
||||
if (comparison === '>' && die0value > rollTarget && !this.isSlaAutomaticFailure(die0value, rollString)) {
|
||||
die0success = true;
|
||||
}
|
||||
if (comparison === '>=' && die0value >= rollTarget && die0value < 90) {
|
||||
if (comparison === '>=' && die0value >= rollTarget && !this.isSlaAutomaticFailure(die0value, rollString)) {
|
||||
die0success = true;
|
||||
}
|
||||
//die 1
|
||||
if (comparison === '<' && die1value < rollTarget && die1value < 90) {
|
||||
if (comparison === '<' && die1value < rollTarget && !this.isSlaAutomaticFailure(die1value, rollString)) {
|
||||
die1success = true;
|
||||
}
|
||||
if (comparison === '<=' && die1value <= rollTarget && die1value < 90) {
|
||||
if (comparison === '<=' && die1value <= rollTarget && !this.isSlaAutomaticFailure(die1value, rollString)) {
|
||||
die1success = true;
|
||||
}
|
||||
if (comparison === '>' && die1value > rollTarget && die1value < 90) {
|
||||
if (comparison === '>' && die1value > rollTarget && !this.isSlaAutomaticFailure(die1value, rollString)) {
|
||||
die1success = true;
|
||||
}
|
||||
if (comparison === '>=' && die1value >= rollTarget && die1value < 90) {
|
||||
if (comparison === '>=' && die1value >= rollTarget && !this.isSlaAutomaticFailure(die1value, rollString)) {
|
||||
die1success = true;
|
||||
}
|
||||
//set whether each die are a crit
|
||||
@@ -842,8 +963,8 @@ export class MothershipActor extends Actor {
|
||||
//add data point: detect success/failure
|
||||
if (rollTarget || rollTarget === 0) {
|
||||
//check for auto failure
|
||||
if (enrichedRollResult.total >= 90) {
|
||||
//result >= 90 is a failure
|
||||
if (this.isSlaAutomaticFailure(enrichedRollResult.total, rollString)) {
|
||||
// result 99 on d100 is always a failure
|
||||
enrichedRollResult.success = false;
|
||||
} else {
|
||||
//compare values based on compararison setting
|
||||
@@ -918,7 +1039,7 @@ export class MothershipActor extends Actor {
|
||||
compareIcon = '<i class="fas fa-greater-than-equal"></i>';
|
||||
}
|
||||
//prepare formula
|
||||
if (rollTarget) {
|
||||
if (rollTarget || rollTarget === 0) {
|
||||
//show dice against target
|
||||
diceFormula = rollString + ' ' + compareIcon + ' ' + rollTarget;
|
||||
} else {
|
||||
@@ -1343,6 +1464,12 @@ export class MothershipActor extends Actor {
|
||||
}
|
||||
//generate chat message
|
||||
//prepare data
|
||||
const severityMeta = this.getSlaSeverityMeta({
|
||||
tableName,
|
||||
specialRoll,
|
||||
tableResultNumber
|
||||
});
|
||||
|
||||
let messageData = {
|
||||
actor: this,
|
||||
tableResult: tableResult,
|
||||
@@ -1357,7 +1484,14 @@ export class MothershipActor extends Actor {
|
||||
woundText: woundText,
|
||||
secondRoll: secondRoll,
|
||||
parsedRollResult2: parsedRollResult2,
|
||||
specialRoll: specialRoll
|
||||
specialRoll: specialRoll,
|
||||
severityMeta: severityMeta,
|
||||
chatStyle: this.getSlaChatVariant({
|
||||
tableName,
|
||||
msgHeader: tableName,
|
||||
isSlaEbbSkill: false,
|
||||
extra: specialRoll
|
||||
})
|
||||
};
|
||||
//prepare template
|
||||
messageTemplate = 'systems/sla-mothership/templates/chat/rollTable.html';
|
||||
@@ -1376,6 +1510,7 @@ export class MothershipActor extends Actor {
|
||||
}, {
|
||||
keepId: true
|
||||
});
|
||||
await this.addSlaActivityLog?.(`${tableName}: ${parsedRollResult.success ? "success" : "failure"} on ${rollString}${tableResultNumber !== null ? ` -> ${tableResultNumber}` : ""}.`);
|
||||
if (game.modules.get("dice-so-nice") && game.modules.get("dice-so-nice").active) {
|
||||
//log what was done
|
||||
slaDebug(`Rolled on table ID: ${tableId}, with: rollString:${rollString}, aimFor:${aimFor}, zeroBased:${zeroBased}, checkCrit:${checkCrit}, rollAgainst:${rollAgainst}, comparison:${comparison}`);
|
||||
@@ -1609,8 +1744,6 @@ export class MothershipActor extends Actor {
|
||||
let skillList = ``;
|
||||
//create skill counter
|
||||
let skillCount = 0;
|
||||
//create dialog pixel counter
|
||||
let dialogHeight = 232;
|
||||
//loop through and create skill rows
|
||||
const filteredSkills = playerItems.filter((item) => item.type === "skill" && this.isSkillRelevantForContext(item, context));
|
||||
const skillSource = filteredSkills.length
|
||||
@@ -1638,13 +1771,9 @@ export class MothershipActor extends Actor {
|
||||
skillList = skillList + tempRow;
|
||||
//increment skill count
|
||||
skillCount++;
|
||||
//increment pixel counter
|
||||
dialogHeight = dialogHeight + 77;
|
||||
}
|
||||
//check if there are no skills, and adjust prompt height accordingly
|
||||
//check if there are no skills
|
||||
if (skillCount === 0) {
|
||||
//set window height
|
||||
dialogHeight = 170;
|
||||
skillList = `<div class="macro_window"><div class="macro_desc" style="padding: 12px 14px;">${requiresCombatSkills ? "No combat-tagged skills are available for this action." : "No matching skills are available for this action."}</div></div>`;
|
||||
}
|
||||
//create button header if needed
|
||||
@@ -1653,12 +1782,24 @@ export class MothershipActor extends Actor {
|
||||
} else {
|
||||
buttonDesc = ``;
|
||||
}
|
||||
//wrap skill list in a scrollable container so long lists don't push buttons off-screen
|
||||
const scrollableSkillList = `<div class="sla-skill-scroll-list" style="max-height: 340px; overflow-y: auto; padding-right: 4px; margin-bottom: 4px;">${skillList}</div>`;
|
||||
//create final dialog data
|
||||
const showModifier = !foundry.utils.hasProperty(context.attackContext ?? {}, "rollModifier");
|
||||
const modifierHtml = showModifier ? `
|
||||
<div class="resource" style="margin-top: 15px; margin-bottom: 10px; border-top: 1px dashed rgba(255,255,255,0.15); padding-top: 10px; padding-left: 10px; padding-right: 10px;">
|
||||
<label class="resource-label" style="display: flex; justify-content: space-between; font-weight: bold; margin-bottom: 8px;">
|
||||
<span>Roll Modifier</span>
|
||||
<span class="slider-value" style="font-weight: bold; color: #cc2828;">0</span>
|
||||
</label>
|
||||
<input type="range" name="rollModifier" min="-30" max="30" step="10" value="0" style="width: 100%; cursor: pointer; accent-color: #cc2828;" oninput="this.previousElementSibling.querySelector('.slider-value').innerText = (this.value >= 0 ? '+' : '') + this.value">
|
||||
</div>
|
||||
` : "";
|
||||
const dialogData = {
|
||||
window: {title: dlgTitle},
|
||||
classes: ["macro-popup-dialog"],
|
||||
position: {width: 600},
|
||||
content: skillHeader + `<div class="skill-dialog-scroll">${skillList}</div>` + buttonDesc,
|
||||
content: skillHeader + scrollableSkillList + modifierHtml + buttonDesc,
|
||||
buttons: []
|
||||
};
|
||||
//add adv/normal/dis buttons if we need a rollString
|
||||
@@ -1672,8 +1813,9 @@ export class MothershipActor extends Actor {
|
||||
rollString = `1d100 [+]`;
|
||||
skill = button.form.querySelector("input[name='skill']:checked")?.getAttribute("id");
|
||||
skillValue = button.form.querySelector("input[name='skill']:checked")?.getAttribute("value");
|
||||
resolve([rollString, skill, skillValue]);
|
||||
slaDebug(`User left the chooseSkill dialog with: rollString:${rollString}, skill:${skill}, skillValue:${skillValue}`);
|
||||
const rollModifier = showModifier ? (Number(button.form.querySelector("input[name='rollModifier']")?.value ?? 0) || 0) : (Number(context.attackContext?.rollModifier ?? 0) || 0);
|
||||
resolve([rollString, skill, skillValue, rollModifier]);
|
||||
slaDebug(`User left the chooseSkill dialog with: rollString:${rollString}, skill:${skill}, skillValue:${skillValue}, modifier:${rollModifier}`);
|
||||
},
|
||||
icon: `fas fa-angle-double-up`
|
||||
},
|
||||
@@ -1684,8 +1826,9 @@ export class MothershipActor extends Actor {
|
||||
rollString = `1d100`;
|
||||
skill = button.form.querySelector("input[name='skill']:checked")?.getAttribute("id");
|
||||
skillValue = button.form.querySelector("input[name='skill']:checked")?.getAttribute("value");
|
||||
resolve([rollString, skill, skillValue]);
|
||||
slaDebug(`User left the chooseSkill dialog with: rollString:${rollString}, skill:${skill}, skillValue:${skillValue}`);
|
||||
const rollModifier = showModifier ? (Number(button.form.querySelector("input[name='rollModifier']")?.value ?? 0) || 0) : (Number(context.attackContext?.rollModifier ?? 0) || 0);
|
||||
resolve([rollString, skill, skillValue, rollModifier]);
|
||||
slaDebug(`User left the chooseSkill dialog with: rollString:${rollString}, skill:${skill}, skillValue:${skillValue}, modifier:${rollModifier}`);
|
||||
},
|
||||
icon: `fas fa-minus`
|
||||
},
|
||||
@@ -1696,8 +1839,9 @@ export class MothershipActor extends Actor {
|
||||
rollString = `1d100 [-]`;
|
||||
skill = button.form.querySelector("input[name='skill']:checked")?.getAttribute("id");
|
||||
skillValue = button.form.querySelector("input[name='skill']:checked")?.getAttribute("value");
|
||||
resolve([rollString, skill, skillValue]);
|
||||
slaDebug(`User left the chooseSkill dialog with: rollString:${rollString}, skill:${skill}, skillValue:${skillValue}`);
|
||||
const rollModifier = showModifier ? (Number(button.form.querySelector("input[name='rollModifier']")?.value ?? 0) || 0) : (Number(context.attackContext?.rollModifier ?? 0) || 0);
|
||||
resolve([rollString, skill, skillValue, rollModifier]);
|
||||
slaDebug(`User left the chooseSkill dialog with: rollString:${rollString}, skill:${skill}, skillValue:${skillValue}, modifier:${rollModifier}`);
|
||||
},
|
||||
icon: `fas fa-angle-double-down`
|
||||
}
|
||||
@@ -1711,8 +1855,9 @@ export class MothershipActor extends Actor {
|
||||
callback: (event, button, dialog) => {
|
||||
skill = button.form.querySelector("input[name='skill']:checked")?.getAttribute("id");
|
||||
skillValue = button.form.querySelector("input[name='skill']:checked")?.getAttribute("value");
|
||||
resolve([rollString, skill, skillValue]);
|
||||
slaDebug(`User left the chooseSkill dialog with: rollString:${rollString}, skill:${skill}, skillValue:${skillValue}`);
|
||||
const rollModifier = showModifier ? (Number(button.form.querySelector("input[name='rollModifier']")?.value ?? 0) || 0) : (Number(context.attackContext?.rollModifier ?? 0) || 0);
|
||||
resolve([rollString, skill, skillValue, rollModifier]);
|
||||
slaDebug(`User left the chooseSkill dialog with: rollString:${rollString}, skill:${skill}, skillValue:${skillValue}, modifier:${rollModifier}`);
|
||||
},
|
||||
icon: `fas fa-chevron-circle-right`
|
||||
}
|
||||
@@ -1733,19 +1878,29 @@ export class MothershipActor extends Actor {
|
||||
let dieAdv = die + ` [+]`;
|
||||
let dieDis = die + ` [-]`;
|
||||
//create final dialog data
|
||||
const modifierHtml = `
|
||||
<div class="resource" style="margin-top: 15px; margin-bottom: 10px; border-top: 1px dashed rgba(255,255,255,0.15); padding-top: 10px; padding-left: 10px; padding-right: 10px;">
|
||||
<label class="resource-label" style="display: flex; justify-content: space-between; font-weight: bold; margin-bottom: 8px;">
|
||||
<span>Roll Modifier</span>
|
||||
<span class="slider-value" style="font-weight: bold; color: #cc2828;">0</span>
|
||||
</label>
|
||||
<input type="range" name="rollModifier" min="-30" max="30" step="10" value="0" style="width: 100%; cursor: pointer; accent-color: #cc2828;" oninput="this.previousElementSibling.querySelector('.slider-value').innerText = (this.value >= 0 ? '+' : '') + this.value">
|
||||
</div>
|
||||
`;
|
||||
const dialogData = {
|
||||
window: {title: dlgTitle},
|
||||
classes: ["macro-popup-dialog"],
|
||||
position: {width: 600},
|
||||
content: `<div class="macro_prompt">` + game.i18n.localize("Mosh.SelectYourRollType") + `:</div>`,
|
||||
content: `<form style="padding: 5px 10px;"><div class="macro_prompt" style="margin-bottom:10px;">` + game.i18n.localize("Mosh.SelectYourRollType") + `:</div>` + modifierHtml + `</form>`,
|
||||
buttons: [
|
||||
{
|
||||
label: game.i18n.localize("Mosh.Advantage"),
|
||||
action: `action_advantage`,
|
||||
callback: (event, button, dialog) => {
|
||||
rollString = dieAdv;
|
||||
resolve([rollString]);
|
||||
slaDebug(`User left the chooseAdvantage dialog with: rollString:${rollString}`);
|
||||
const rollModifier = Number(button.form.querySelector("input[name='rollModifier']")?.value ?? 0) || 0;
|
||||
resolve([rollString, rollModifier]);
|
||||
slaDebug(`User left the chooseAdvantage dialog with: rollString:${rollString}, modifier:${rollModifier}`);
|
||||
},
|
||||
icon: `fas fa-angle-double-up`
|
||||
},
|
||||
@@ -1754,8 +1909,9 @@ export class MothershipActor extends Actor {
|
||||
action: `action_normal`,
|
||||
callback: (event, button, dialog) => {
|
||||
rollString = die;
|
||||
resolve([rollString]);
|
||||
slaDebug(`User left the chooseAdvantage dialog with: rollString:${rollString}`);
|
||||
const rollModifier = Number(button.form.querySelector("input[name='rollModifier']")?.value ?? 0) || 0;
|
||||
resolve([rollString, rollModifier]);
|
||||
slaDebug(`User left the chooseAdvantage dialog with: rollString:${rollString}, modifier:${rollModifier}`);
|
||||
},
|
||||
icon: `fas fa-minus`
|
||||
},
|
||||
@@ -1764,8 +1920,9 @@ export class MothershipActor extends Actor {
|
||||
action: `action_disadvantage`,
|
||||
callback: (event, button, dialog) => {
|
||||
rollString = dieDis;
|
||||
resolve([rollString]);
|
||||
slaDebug(`User left the chooseAdvantage dialog with: rollString:${rollString}`);
|
||||
const rollModifier = Number(button.form.querySelector("input[name='rollModifier']")?.value ?? 0) || 0;
|
||||
resolve([rollString, rollModifier]);
|
||||
slaDebug(`User left the chooseAdvantage dialog with: rollString:${rollString}, modifier:${rollModifier}`);
|
||||
},
|
||||
icon: `fas fa-angle-double-down`
|
||||
}
|
||||
@@ -1874,19 +2031,29 @@ export class MothershipActor extends Actor {
|
||||
if (rawFormula === "Str/10") {
|
||||
rawFormula = `floor((${this.system.stats.strength.value} + ${(Number(this.system.stats.strength.mod) || 0)}) / 10)`;
|
||||
}
|
||||
const mult = Number(weapon?.system?.sla?.damageMultiplier ?? weapon?.system?.damageMultiplier ?? 1) || 1;
|
||||
if (mult > 1) {
|
||||
if (/^\d+d\d+/i.test(rawFormula)) {
|
||||
rawFormula = rawFormula.replace(/^(\d+)(d\d+)/i, (match, dice, rest) => {
|
||||
return `${Number(dice) * mult}${rest}`;
|
||||
});
|
||||
} else {
|
||||
rawFormula = `(${rawFormula}) * ${mult}`;
|
||||
}
|
||||
}
|
||||
const critDamageMode = game.settings.get('sla-mothership', 'critDamage');
|
||||
|
||||
if (isCritical) {
|
||||
if (critDamageMode === 'advantage') {
|
||||
rawFormula = `{${parsedDamageString},${parsedDamageString}}kh`;
|
||||
rawFormula = `{${rawFormula},${rawFormula}}kh`;
|
||||
} else if (critDamageMode === 'doubleDamage') {
|
||||
rawFormula = `(${parsedDamageString}) * 2`;
|
||||
rawFormula = `(${rawFormula}) * 2`;
|
||||
} else if (critDamageMode === 'doubleDice') {
|
||||
rawFormula = `${parsedDamageString} + ${parsedDamageString}`;
|
||||
rawFormula = `${rawFormula} + ${rawFormula}`;
|
||||
} else if (critDamageMode === 'maxDamage') {
|
||||
rawFormula = parsedDamageString.replaceAll('d', ' * ');
|
||||
rawFormula = rawFormula.replaceAll('d', ' * ');
|
||||
} else if (critDamageMode === 'weaponValue') {
|
||||
rawFormula = `${parsedDamageString} + ${weapon?.system?.critDmg || 0}`;
|
||||
rawFormula = `${rawFormula} + ${weapon?.system?.critDmg || 0}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2358,6 +2525,9 @@ export class MothershipActor extends Actor {
|
||||
rollString = chosenSkills[0];
|
||||
skill = chosenSkills[1];
|
||||
skillValue = chosenSkills[2];
|
||||
if (chosenSkills[3] !== undefined) {
|
||||
attackContext.rollModifier = chosenSkills[3];
|
||||
}
|
||||
}
|
||||
//if rollString is STILL blank, redirect player to choose the roll
|
||||
if (!rollString) {
|
||||
@@ -2365,6 +2535,9 @@ export class MothershipActor extends Actor {
|
||||
let chosenRollType = await this.chooseAdvantage(this.system.stats[attribute].rollLabel, '1d100');
|
||||
//set variables
|
||||
rollString = chosenRollType[0];
|
||||
if (chosenRollType[1] !== undefined) {
|
||||
attackContext.rollModifier = chosenRollType[1];
|
||||
}
|
||||
}
|
||||
//if this is a weapon roll
|
||||
if (weapon) {
|
||||
@@ -2424,7 +2597,15 @@ export class MothershipActor extends Actor {
|
||||
//if this is a damage roll
|
||||
if (specialRoll === 'damage') {
|
||||
//parse the roll string
|
||||
let damageRollString = weapon.system.damage
|
||||
let damageRollString = weapon.system.damage;
|
||||
if (weapon && !overrideDamagaRollString) {
|
||||
const mult = Number(weapon.system?.sla?.damageMultiplier ?? weapon.system?.damageMultiplier ?? 1) || 1;
|
||||
if (mult > 1) {
|
||||
damageRollString = damageRollString.replace(/^(\d+)(d\d+)/i, (match, dice, rest) => {
|
||||
return `${Number(dice) * mult}${rest}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
if(overrideDamagaRollString){
|
||||
damageRollString = overrideDamagaRollString;
|
||||
}
|
||||
@@ -2443,8 +2624,10 @@ export class MothershipActor extends Actor {
|
||||
}
|
||||
//prepare flavortext
|
||||
if (weapon.system.damage === "Str/10" && this.type === 'character') {
|
||||
const mult = Number(weapon.system?.sla?.damageMultiplier ?? weapon.system?.damageMultiplier ?? 1) || 1;
|
||||
const multStr = mult > 1 ? `${mult} * ` : '';
|
||||
//determine the damage string
|
||||
flavorText = 'You strike your target for <strong>[[floor((' + this.system.stats.strength.value + ' + ' + (Number(this.system.stats.strength.mod) || 0) + ')/10)]] damage</strong>.';
|
||||
flavorText = 'You strike your target for <strong>[[' + multStr + 'floor((' + this.system.stats.strength.value + ' + ' + (Number(this.system.stats.strength.mod) || 0) + ')/10)]] damage</strong>.';
|
||||
} else {
|
||||
flavorText = 'You inflict [[' + parsedDamageString + '[' + dsnTheme + ']' + critMod + ']] points of damage.';
|
||||
}
|
||||
@@ -2480,7 +2663,13 @@ export class MothershipActor extends Actor {
|
||||
targetingResult: targetingResult,
|
||||
needsDesc: needsDesc,
|
||||
woundEffect: woundEffect,
|
||||
specialRoll: specialRoll
|
||||
specialRoll: specialRoll,
|
||||
chatStyle: this.getSlaChatVariant({
|
||||
weapon,
|
||||
msgHeader,
|
||||
isSlaEbbSkill: false,
|
||||
extra: ammoContext?.label ?? ""
|
||||
})
|
||||
};
|
||||
let chatData = {
|
||||
user: game.user.id,
|
||||
@@ -2495,6 +2684,7 @@ export class MothershipActor extends Actor {
|
||||
const content = await foundry.applications.handlebars.renderTemplate(template, messageData);
|
||||
chatData.content = content;
|
||||
await ChatMessage.create(chatData);
|
||||
await this.addSlaActivityLog?.(`${weapon.name}: damage roll resolved.${targetingResult ? ` ${targetingResult.targetName} took ${targetingResult.finalDamage} damage.` : ""}`);
|
||||
//log what was done
|
||||
console.log(`Rolled damage on:${weapon.name}`);
|
||||
//return messageData
|
||||
@@ -2512,6 +2702,25 @@ export class MothershipActor extends Actor {
|
||||
} else {
|
||||
rollTarget = rollTargetOverride;
|
||||
}
|
||||
const modifier = Number(attackContext?.rollModifier ?? 0) || 0;
|
||||
if (modifier) {
|
||||
rollTarget += modifier;
|
||||
if (effectiveRollBreakdown) {
|
||||
effectiveRollBreakdown.parts.push({
|
||||
label: "Roll Modifier",
|
||||
value: modifier
|
||||
});
|
||||
effectiveRollBreakdown.total += modifier;
|
||||
effectiveRollBreakdown.formulaText += ` ${modifier >= 0 ? "+" : ""}${modifier}`;
|
||||
}
|
||||
}
|
||||
rollTarget = this.clampSlaRollTarget(rollTarget, rollString);
|
||||
if (effectiveRollBreakdown && (String(rollString ?? "").includes("1d100"))) {
|
||||
effectiveRollBreakdown = {
|
||||
...effectiveRollBreakdown,
|
||||
total: rollTarget
|
||||
};
|
||||
}
|
||||
//roll the dice
|
||||
//parse the roll string
|
||||
let parsedRollString = this.parseRollString(rollString, aimFor);
|
||||
@@ -2674,9 +2883,9 @@ export class MothershipActor extends Actor {
|
||||
}
|
||||
if (ammoUsage) {
|
||||
const roundLabel = ammoUsage.shots === 1 ? 'round' : 'rounds';
|
||||
const ammoPrefix = `<strong>${ammoUsage.label}</strong> fire with <strong>${ammoUsage.ammoLabel}</strong> spent <strong>${ammoUsage.shots}</strong> ${roundLabel}. Magazine <strong>${ammoUsage.loaded}/${weapon.system.shots}</strong>. Reserve <strong>${ammoUsage.reserve}</strong>. Ammo cost <strong>${ammoUsage.totalCost}</strong> cR. ${ammoUsage.ammoRule}`;
|
||||
const ammoPrefix = `<strong>${ammoUsage.label}</strong> fire with <strong>${ammoUsage.ammoLabel}</strong> spent <strong>${ammoUsage.shots}</strong> ${roundLabel}. Magazine <strong>${ammoUsage.loaded}/${weapon.system.shots}</strong>. Reserve <strong>${ammoUsage.reserve}</strong>. Firing spend <strong>${ammoUsage.totalCost}</strong> cR. ${ammoUsage.ammoRule}`;
|
||||
const accountingText = ammoAccounting
|
||||
? ` Credits now <strong>${ammoAccounting.newCredits}</strong> cR. Running ammo spend <strong>${ammoAccounting.spendTotal}</strong> cR.`
|
||||
? ` Credits now <strong>${ammoAccounting.newCredits}</strong> cR. Running firing spend <strong>${ammoAccounting.spendTotal}</strong> cR.`
|
||||
: "";
|
||||
flavorText = flavorText ? `${ammoPrefix}${accountingText}<br>${flavorText}` : `${ammoPrefix}${accountingText}`;
|
||||
}
|
||||
@@ -2966,7 +3175,14 @@ export class MothershipActor extends Actor {
|
||||
targetingResult: targetingResult,
|
||||
firstEdition: game.settings.get('sla-mothership', 'firstEdition'),
|
||||
useCalm: game.settings.get('sla-mothership', 'useCalm'),
|
||||
androidPanic: game.settings.get('sla-mothership', 'androidPanic')
|
||||
androidPanic: game.settings.get('sla-mothership', 'androidPanic'),
|
||||
chatStyle: this.getSlaChatVariant({
|
||||
weapon,
|
||||
skill,
|
||||
skillCategory: String(attackContext?.skillCategory ?? "").trim(),
|
||||
msgHeader,
|
||||
isSlaEbbSkill
|
||||
})
|
||||
};
|
||||
//prepare template
|
||||
messageTemplate = 'systems/sla-mothership/templates/chat/rollCheck.html';
|
||||
@@ -2985,6 +3201,11 @@ export class MothershipActor extends Actor {
|
||||
}, {
|
||||
keepId: true
|
||||
});
|
||||
const skillLabel = String(skill ?? "").trim();
|
||||
const actionLabel = weapon
|
||||
? weapon.name
|
||||
: (skillLabel ? `${attributeLabel} / ${skillLabel}` : attributeLabel);
|
||||
await this.addSlaActivityLog?.(`${actionLabel}: ${parsedRollResult.success ? "success" : "failure"} (${parsedRollResult.total} vs ${rollTarget}).`);
|
||||
//is DSN active?
|
||||
if (game.modules.get("dice-so-nice") && game.modules.get("dice-so-nice").active) {
|
||||
//log what was done
|
||||
@@ -3863,10 +4084,58 @@ export class MothershipActor extends Actor {
|
||||
return { total, parts };
|
||||
}
|
||||
|
||||
async chooseWeaponAttackOptions(weapon) {
|
||||
async promptRollModifier(title = "Roll Modifier") {
|
||||
return new Promise((resolve) => {
|
||||
const dialogData = {
|
||||
window: { title },
|
||||
content: `
|
||||
<form style="padding: 5px 10px;">
|
||||
<div class="resource" style="margin-bottom: 15px;">
|
||||
<label class="resource-label" style="display: flex; justify-content: space-between; font-weight: bold; margin-bottom: 8px;">
|
||||
<span>Modifier Value</span>
|
||||
<span class="slider-value" style="font-weight: bold; color: #cc2828;">0</span>
|
||||
</label>
|
||||
<input type="range" name="rollModifier" min="-30" max="30" step="10" value="0" style="width: 100%; cursor: pointer; accent-color: #cc2828;">
|
||||
<div class="slider-ticks" style="display: flex; justify-content: space-between; margin-top: 5px; font-size: 0.85em; color: rgba(255,255,255,0.6); padding: 0 8px;">
|
||||
<span>-30</span>
|
||||
<span>-20</span>
|
||||
<span>-10</span>
|
||||
<span style="color: #cc2828; font-weight: bold;">0</span>
|
||||
<span>+10</span>
|
||||
<span>+20</span>
|
||||
<span>+30</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
`,
|
||||
buttons: [
|
||||
{
|
||||
label: "Roll",
|
||||
action: "roll",
|
||||
icon: "fas fa-dice-d20",
|
||||
callback: (_event, button) => {
|
||||
const modifier = Number(button.form.querySelector("input[name='rollModifier']")?.value ?? 0) || 0;
|
||||
resolve(modifier);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Cancel",
|
||||
action: "cancel",
|
||||
icon: "fas fa-times",
|
||||
callback: () => resolve(null)
|
||||
}
|
||||
],
|
||||
close: () => resolve(null)
|
||||
};
|
||||
new foundry.applications.api.DialogV2(dialogData).render({ force: true });
|
||||
});
|
||||
}
|
||||
|
||||
async chooseWeaponAttackOptions(weapon, attackContext = {}) {
|
||||
const fireModes = this.getWeaponFireModes(weapon);
|
||||
const activeFireMode = this.getActiveWeaponFireMode(weapon);
|
||||
const ammoChoices = this.getWeaponAmmoChoices(weapon);
|
||||
const showModifier = Boolean(attackContext?.promptModifier);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const fireOptions = fireModes.map((mode) => `
|
||||
@@ -3876,20 +4145,40 @@ export class MothershipActor extends Actor {
|
||||
<option value="${choice.tag}" ${choice.selected ? "selected" : ""}>${choice.label}</option>
|
||||
`).join("");
|
||||
|
||||
const modifierHtml = showModifier ? `
|
||||
<div class="resource" style="margin-top: 15px; margin-bottom: 10px; border-top: 1px dashed rgba(255,255,255,0.15); padding-top: 10px;">
|
||||
<label class="resource-label" style="display: flex; justify-content: space-between; font-weight: bold; margin-bottom: 8px;">
|
||||
<span>Roll Modifier</span>
|
||||
<span class="slider-value" style="font-weight: bold; color: #cc2828;">0</span>
|
||||
</label>
|
||||
<input type="range" name="rollModifier" min="-30" max="30" step="10" value="0" style="width: 100%; cursor: pointer; accent-color: #cc2828;">
|
||||
<div class="slider-ticks" style="display: flex; justify-content: space-between; margin-top: 5px; font-size: 0.85em; color: rgba(255,255,255,0.6); padding: 0 8px;">
|
||||
<span>-30</span>
|
||||
<span>-20</span>
|
||||
<span>-10</span>
|
||||
<span style="color: #cc2828; font-weight: bold;">0</span>
|
||||
<span>+10</span>
|
||||
<span>+20</span>
|
||||
<span>+30</span>
|
||||
</div>
|
||||
</div>
|
||||
` : "";
|
||||
|
||||
const dialogData = {
|
||||
window: {
|
||||
title: `${weapon.name} Attack Setup`
|
||||
},
|
||||
content: `
|
||||
<form>
|
||||
<form style="padding: 5px 10px;">
|
||||
<div class="resource" style="margin-bottom: 10px;">
|
||||
<label class="resource-label">Fire Mode</label>
|
||||
<select name="fireMode">${fireOptions}</select>
|
||||
<label class="resource-label" style="font-weight: bold; margin-bottom: 5px; display: block;">Fire Mode</label>
|
||||
<select name="fireMode" style="width: 100%;">${fireOptions}</select>
|
||||
</div>
|
||||
<div class="resource" style="margin-bottom: 10px;">
|
||||
<label class="resource-label">Ammo Type</label>
|
||||
<select name="ammoTag">${ammoOptions}</select>
|
||||
<label class="resource-label" style="font-weight: bold; margin-bottom: 5px; display: block;">Ammo Type</label>
|
||||
<select name="ammoTag" style="width: 100%;">${ammoOptions}</select>
|
||||
</div>
|
||||
${modifierHtml}
|
||||
</form>
|
||||
`,
|
||||
buttons: [
|
||||
@@ -3901,10 +4190,12 @@ export class MothershipActor extends Actor {
|
||||
const fireLabel = button.form.querySelector("select[name='fireMode']")?.value ?? activeFireMode.label;
|
||||
const fireMode = fireModes.find((mode) => mode.label === fireLabel) ?? activeFireMode;
|
||||
const ammoTag = button.form.querySelector("select[name='ammoTag']")?.value ?? ammoChoices[0]?.tag ?? "STD";
|
||||
const rollModifier = showModifier ? (Number(button.form.querySelector("input[name='rollModifier']")?.value ?? 0) || 0) : 0;
|
||||
resolve({
|
||||
fireMode: fireMode.label,
|
||||
shotsPerFire: fireMode.shots,
|
||||
ammoTag
|
||||
ammoTag,
|
||||
rollModifier
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -3921,10 +4212,20 @@ export class MothershipActor extends Actor {
|
||||
});
|
||||
}
|
||||
|
||||
async rollWeaponAttack(weapon) {
|
||||
const attackContext = weapon?.system?.useAmmo ? await this.chooseWeaponAttackOptions(weapon) : {};
|
||||
if (weapon?.system?.useAmmo && !attackContext) return;
|
||||
return this.rollCheck(null, 'low', 'combat', null, null, weapon, null, attackContext ?? {});
|
||||
async rollWeaponAttack(weapon, attackContext = {}) {
|
||||
let context = { ...attackContext };
|
||||
if (weapon?.system?.useAmmo) {
|
||||
const ammoContext = await this.chooseWeaponAttackOptions(weapon, context);
|
||||
if (!ammoContext) return null;
|
||||
context = { ...context, ...ammoContext };
|
||||
} else {
|
||||
if (context.promptModifier) {
|
||||
const rollMod = await this.promptRollModifier(`${weapon.name} Attack Roll`);
|
||||
if (rollMod === null) return null;
|
||||
context.rollModifier = rollMod;
|
||||
}
|
||||
}
|
||||
return this.rollCheck(null, 'low', 'combat', null, null, weapon, null, context);
|
||||
}
|
||||
|
||||
async cycleWeaponFireMode(itemId, direction = 1) {
|
||||
@@ -4767,57 +5068,12 @@ export class MothershipActor extends Actor {
|
||||
|
||||
getSlaSpeciesMinimumWounds() {
|
||||
const species = String(this.system?.sla?.species?.value ?? "").trim();
|
||||
if (["Frother", "Shaktar", "Stormer 313 Malice", "Stormer 711 Xeno", "Stormer Vevaphon"].includes(species)) {
|
||||
if (["Frother", "Shaktar", "Stormer 313 Malice", "Stormer 711 Xeno"].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();
|
||||
@@ -4912,16 +5168,31 @@ export class MothershipActor extends Actor {
|
||||
return ["Ebon", "Brain Waster"].includes(species);
|
||||
}
|
||||
|
||||
isSlaVevaphon() {
|
||||
return String(this.system?.sla?.species?.value ?? "").trim() === "Stormer Vevaphon";
|
||||
}
|
||||
|
||||
async setSlaVevaphonMorphForm(formName) {
|
||||
if (!formName) return;
|
||||
await this.update({ "system.sla.morphForm.value": String(formName) });
|
||||
}
|
||||
|
||||
async adjustSlaVevaphonInstability(delta) {
|
||||
const current = Math.max(0, Math.min(12, Number(this.system?.sla?.vevaphonInstability?.value ?? 0) || 0));
|
||||
const next = Math.max(0, Math.min(12, current + (Number(delta) || 0)));
|
||||
await this.update({ "system.sla.vevaphonInstability.value": next });
|
||||
}
|
||||
|
||||
getSlaRestrictedEbbItemIds() {
|
||||
return this.items
|
||||
.filter((item) => {
|
||||
const name = String(item.name ?? "").trim();
|
||||
const sla = item.system?.sla ?? {};
|
||||
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;
|
||||
// Only flag abilities that are explicitly Ebb abilities.
|
||||
// Morph forms (Vevaphon) get abilityType "morph" from the generator and are never removed.
|
||||
// morphForm flag is a belt-and-suspenders guard in case abilityType is missing.
|
||||
if (sla.morphForm || String(sla.abilityType ?? "ebb").trim() !== "ebb") return false;
|
||||
return true;
|
||||
}
|
||||
if (item.type !== "skill") return false;
|
||||
@@ -4942,13 +5213,20 @@ export class MothershipActor extends Actor {
|
||||
"system.sla.ebbRating.value": 0
|
||||
};
|
||||
await this.update(updateData);
|
||||
if (restrictedIds.length) {
|
||||
await this.deleteEmbeddedDocuments("Item", restrictedIds);
|
||||
const validIds = restrictedIds.filter(id => this.items.get(id) !== undefined);
|
||||
let removed = 0;
|
||||
if (validIds.length) {
|
||||
try {
|
||||
await this.deleteEmbeddedDocuments("Item", validIds);
|
||||
removed = validIds.length;
|
||||
} catch (err) {
|
||||
// Concurrent enforcement calls may have already deleted these items — safe to ignore
|
||||
}
|
||||
}
|
||||
if (notify && restrictedIds.length) {
|
||||
if (notify && removed) {
|
||||
ui.notifications.warn(`${this.name} is not an Ebb user. Restricted Ebb skills and abilities were removed.`);
|
||||
}
|
||||
return { removed: restrictedIds.length };
|
||||
return { removed };
|
||||
}
|
||||
|
||||
const currentMax = Math.max(0, Number(this.system?.sla?.flux?.max ?? 0) || 0);
|
||||
@@ -5484,7 +5762,8 @@ export class MothershipActor extends Actor {
|
||||
insert: rollInsert,
|
||||
onlyDesc: true,
|
||||
swapNameDesc,
|
||||
swapName
|
||||
swapName,
|
||||
severityMeta: this.getSlaSeverityMeta({ severity: item?.system?.severity })
|
||||
};
|
||||
|
||||
const chatData = {
|
||||
|
||||
@@ -79,6 +79,9 @@ export class MothershipCreatureSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
if (data.data.system.settings == null) {
|
||||
data.data.system.settings = {};
|
||||
}
|
||||
if (data.data.system.slaNpc == null) {
|
||||
data.data.system.slaNpc = {};
|
||||
}
|
||||
data.data.system.settings.useCalm = game.settings.get("sla-mothership", "useCalm");
|
||||
data.data.system.settings.hideWeight = game.settings.get("sla-mothership", "hideWeight");
|
||||
data.data.system.settings.firstEdition = game.settings.get("sla-mothership", "firstEdition");
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
import { SLADrugSystem } from "../sla-drug-system.js";
|
||||
|
||||
function isAmmoPurchaseItem(itemData = {}) {
|
||||
const system = itemData?.system ?? {};
|
||||
const sla = system?.sla ?? {};
|
||||
const name = String(itemData?.name ?? "").trim().toLowerCase();
|
||||
const category = String(sla.category ?? "").trim().toLowerCase();
|
||||
const calibre = String(sla.calibre ?? "").trim();
|
||||
const ammoTag = String(sla.ammoTag ?? "").trim();
|
||||
return category === "ammunition" || Boolean((calibre && ammoTag) || name.startsWith("ammo:"));
|
||||
}
|
||||
|
||||
function formatSlaTimestamp() {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short"
|
||||
}).format(new Date());
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the basic ItemSheet with some very simple modifications
|
||||
* @extends {ItemSheet}
|
||||
@@ -128,11 +145,38 @@ export class MothershipItemSheet extends foundry.appv1.sheets.ItemSheet {
|
||||
|
||||
async _updateObject(event, formData) {
|
||||
const updateData = foundry.utils.expandObject(formData);
|
||||
const currentQty = Math.max(0, Number(this.item.system?.quantity ?? 0) || 0);
|
||||
if (Object.prototype.hasOwnProperty.call(updateData?.system ?? {}, "weight")) {
|
||||
const parsed = Number(updateData.system.weight ?? 0) || 0;
|
||||
updateData.system.weight = Math.round(parsed * 10) / 10;
|
||||
}
|
||||
await this.item.update(updateData, { diff: false });
|
||||
|
||||
const nextQtyRaw = updateData?.system?.quantity;
|
||||
const nextQty = nextQtyRaw === undefined ? currentQty : Math.max(0, Number(nextQtyRaw ?? 0) || 0);
|
||||
const increase = Math.max(0, nextQty - currentQty);
|
||||
const actor = this.item.parent;
|
||||
if (!increase || actor?.documentName !== "Actor") return;
|
||||
|
||||
const unitCost = Number(updateData?.system?.cost ?? this.item.system?.cost ?? 0) || 0;
|
||||
const total = Math.round(unitCost * increase * 100) / 100;
|
||||
if (!total) return;
|
||||
|
||||
const currentCredits = Number(actor.system?.credits?.value ?? 0) || 0;
|
||||
const newCredits = Math.round((currentCredits - total) * 100) / 100;
|
||||
const updateActorData = { "system.credits.value": newCredits };
|
||||
|
||||
if (isAmmoPurchaseItem(this.item.toObject())) {
|
||||
const stamp = formatSlaTimestamp();
|
||||
const currentLedger = String(actor.system?.sla?.ammoLedger?.value ?? "").trim();
|
||||
const entry = `[${stamp}] PURCHASE | ${this.item.name ?? "Ammo"} | ${increase} round${increase === 1 ? "" : "s"} | -${total} cR | balance ${newCredits} cR`;
|
||||
const ledgerLines = [entry, ...currentLedger.split("\n").filter(Boolean)].slice(0, 60);
|
||||
updateActorData["system.sla.ammoLedger.value"] = ledgerLines.join("\n");
|
||||
}
|
||||
|
||||
await actor.update(updateActorData);
|
||||
ui.notifications.info(`${actor.name}: deducted ${total} cR for ${increase} more ${this.item.name ?? "item"}. Credits now ${newCredits} cR.`);
|
||||
await actor.addSlaActivityLog?.(`Purchased +${increase} ${this.item.name ?? "item"} for ${total} cR. Credits now ${newCredits} cR.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { MothershipSkillSheet } from "./item/skill-sheet.js";
|
||||
import { SLADrugSystem } from "./sla-drug-system.js";
|
||||
import { slaDebug } from "./logger.js";
|
||||
import { SLAMothershipGenerator } from "./windows/sla-character-generator.js";
|
||||
import { SLANpcGeneratorApp, generateSlaNpc } from "./windows/sla-npc-generator.js";
|
||||
import { SLAWorldToolsApp, installSlaSidebarButtons } from "./windows/sla-world-tools.js";
|
||||
|
||||
import {
|
||||
@@ -50,6 +51,8 @@ Hooks.once('init', async function () {
|
||||
closeDrug: (actor, itemId, drug = "") => SLADrugSystem.closeDrug({ actor, itemId, drug }),
|
||||
restartDrugReminders: () => SLADrugSystem.startReminderLoop(),
|
||||
openGenerator: (actor = null, options = {}) => SLAMothershipGenerator.open(actor, options),
|
||||
openNpcDesk: (options = {}) => SLANpcGeneratorApp.open(options),
|
||||
createNpc: (state = {}) => generateSlaNpc(state),
|
||||
createRandomOperative: (options = {}) => SLAMothershipGenerator.createRandomOperative(options),
|
||||
createStarterSquad: (options = {}) => SLAMothershipGenerator.createStarterSquad(options),
|
||||
openTools: () => SLAWorldToolsApp.open(),
|
||||
@@ -144,11 +147,38 @@ Hooks.once('init', async function () {
|
||||
await SLADrugSystem.handleEffectDeleted(effect, options);
|
||||
});
|
||||
|
||||
Hooks.on("createItem", async (item) => {
|
||||
// Debounce map: actorId → pending timeout handle.
|
||||
// Prevents enforceSlaEbbEligibility from being called once per item when the
|
||||
// character generator creates many items in rapid succession (each createItem
|
||||
// hook call would otherwise race to delete the same Ebb items concurrently).
|
||||
const _pendingEbbEnforcement = new Map();
|
||||
Hooks.on("createItem", async (item, options, userId) => {
|
||||
// ── Unarmed Combat Auto-Skill Drop ──
|
||||
const actor = item.parent;
|
||||
if (actor && actor.documentName === "Actor" && item.type === "weapon" && String(item.name).trim().toLowerCase() === "unarmed combat") {
|
||||
const hasSkill = actor.items.some(i => i.type === "skill" && String(i.name).trim().toLowerCase() === "unarmed combat");
|
||||
if (!hasSkill) {
|
||||
await actor.createEmbeddedDocuments("Item", [{
|
||||
name: "Unarmed Combat",
|
||||
type: "skill",
|
||||
img: "icons/svg/fist.svg",
|
||||
system: {
|
||||
rank: "Trained",
|
||||
bonus: 10,
|
||||
sla: { category: "combat", skillFamily: "", source: "Unarmed Item Drop" }
|
||||
}
|
||||
}]);
|
||||
ui.notifications.info(`Added 'Unarmed Combat' skill to ${actor.name}.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!actor || actor.type !== "character" || typeof actor.enforceSlaEbbEligibility !== "function") return;
|
||||
if (actor.isSlaEbbUser?.()) return;
|
||||
await actor.enforceSlaEbbEligibility({ notify: true, refillFlux: false });
|
||||
if (_pendingEbbEnforcement.has(actor.id)) clearTimeout(_pendingEbbEnforcement.get(actor.id));
|
||||
_pendingEbbEnforcement.set(actor.id, setTimeout(async () => {
|
||||
_pendingEbbEnforcement.delete(actor.id);
|
||||
await actor.enforceSlaEbbEligibility({ notify: true, refillFlux: false });
|
||||
}, 300));
|
||||
});
|
||||
|
||||
Hooks.on("updateActor", async (actor, changes, options) => {
|
||||
@@ -175,16 +205,37 @@ Hooks.once('init', async function () {
|
||||
|
||||
Hooks.on("renderActorDirectory", (app, html) => {
|
||||
installSlaSidebarButtons(app, html);
|
||||
if (!game.user?.isGM) enforceSlaPlayerDirectoryRestrictions("actors", html);
|
||||
});
|
||||
|
||||
Hooks.on("renderSidebarTab", (app, html) => {
|
||||
const isActorsTab = app?.tabName === "actors" || app?.options?.id === "actors";
|
||||
if (!isActorsTab) return;
|
||||
installSlaSidebarButtons(app, html);
|
||||
if (isActorsTab) installSlaSidebarButtons(app, html);
|
||||
if (!game.user?.isGM) {
|
||||
hideSlaRestrictedSidebarTabs();
|
||||
enforceSlaPlayerSidebarTabRestrictions(app, html);
|
||||
}
|
||||
});
|
||||
|
||||
Hooks.on("renderSceneDirectory", (_app, html) => {
|
||||
if (!game.user?.isGM) enforceSlaPlayerDirectoryRestrictions("scenes", html);
|
||||
});
|
||||
|
||||
Hooks.on("renderJournalDirectory", (_app, html) => {
|
||||
if (!game.user?.isGM) enforceSlaPlayerDirectoryRestrictions("journal", html);
|
||||
});
|
||||
|
||||
Hooks.on("renderRollTableDirectory", (_app, html) => {
|
||||
if (!game.user?.isGM) enforceSlaPlayerDirectoryRestrictions("tables", html);
|
||||
});
|
||||
|
||||
Hooks.on("renderCompendiumDirectory", (_app, html) => {
|
||||
if (!game.user?.isGM) enforceSlaPlayerDirectoryRestrictions("compendium", html);
|
||||
});
|
||||
|
||||
Hooks.on("renderCombatTracker", (app, html) => {
|
||||
installSlaCombatTrackerControls(app, html);
|
||||
if (!game.user?.isGM) enforceSlaPlayerDirectoryRestrictions("combat", html);
|
||||
});
|
||||
|
||||
Hooks.on("updateCombat", async (combat, changed) => {
|
||||
@@ -247,9 +298,62 @@ Hooks.once("ready", async function () {
|
||||
}
|
||||
|
||||
patchSlaInitiativeSystem();
|
||||
if (!game.user?.isGM) hideSlaRestrictedSidebarTabs();
|
||||
|
||||
// GM Conditions Summary — whisper to all GMs every 30 minutes
|
||||
if (game.user?.isGM) {
|
||||
setInterval(_sendGMConditionsSummary, 30 * 60 * 1000);
|
||||
console.log("SLA MOTHERSHIP | GM conditions summary timer started (every 30 minutes).");
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Scans all actors for active condition items and whispers a summary to GM(s).
|
||||
* Fires every 30 minutes when logged in as GM.
|
||||
*/
|
||||
function _sendGMConditionsSummary() {
|
||||
if (!game.user?.isGM) return;
|
||||
|
||||
const rows = [];
|
||||
for (const actor of game.actors) {
|
||||
const conditions = actor.items?.filter(i => i.type === "condition") ?? [];
|
||||
if (conditions.length === 0) continue;
|
||||
const conditionList = conditions.map(c => {
|
||||
const sev = c.system?.severity ?? "";
|
||||
return `<span style="color:#ff4444;font-weight:bold;">${c.name}${sev ? ` (${sev})` : ""}</span>`;
|
||||
}).join(", ");
|
||||
rows.push(`<tr><td style="padding:2px 8px;font-weight:bold;color:#eee;">${actor.name}</td><td style="padding:2px 8px;">${conditionList}</td></tr>`);
|
||||
}
|
||||
|
||||
let content;
|
||||
if (rows.length === 0) {
|
||||
content = `<div style="font-family:monospace;color:#888;padding:4px 8px;">
|
||||
<strong style="color:#ff4444;">⚕ CONDITIONS REPORT</strong> — No active conditions.
|
||||
</div>`;
|
||||
} else {
|
||||
content = `<div style="font-family:monospace;padding:4px 0;">
|
||||
<div style="background:#1a0000;border:1px solid #8b0000;border-radius:4px;padding:4px 8px;margin-bottom:6px;">
|
||||
<strong style="color:#ff4444;font-size:1em;">⚕ CONDITIONS REPORT</strong>
|
||||
<span style="color:#888;font-size:0.8em;margin-left:8px;">${new Date().toLocaleTimeString()}</span>
|
||||
</div>
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<thead><tr style="border-bottom:1px solid #8b0000;">
|
||||
<th style="padding:2px 8px;color:#aaa;text-align:left;font-size:0.8em;">ACTOR</th>
|
||||
<th style="padding:2px 8px;color:#aaa;text-align:left;font-size:0.8em;">CONDITIONS</th>
|
||||
</tr></thead>
|
||||
<tbody>${rows.join("")}</tbody>
|
||||
</table>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
ChatMessage.create({
|
||||
content,
|
||||
whisper: ChatMessage.getWhisperRecipients("GM"),
|
||||
speaker: { alias: "SLA System" }
|
||||
});
|
||||
}
|
||||
|
||||
Hooks.on("renderChatMessageHTML", (app, html) => {
|
||||
const root = html?.[0] ?? html;
|
||||
if (!root?.querySelectorAll) return;
|
||||
@@ -272,6 +376,129 @@ Hooks.on("renderChatMessageHTML", (app, html) => {
|
||||
await actor.rollSlaWoundEffect(effectKey, modifier);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Critical Hit? Button Handler ──
|
||||
root.querySelectorAll(".sla-crit-hit-btn").forEach((button) => {
|
||||
button.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const actorId = button.dataset.actorId;
|
||||
const itemId = button.dataset.itemId;
|
||||
const actor = actorId ? game.actors?.get(actorId) : null;
|
||||
if (!actor) {
|
||||
ui.notifications.warn("The actor for this critical hit could not be found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const weapon = itemId ? actor.items.get(itemId) : null;
|
||||
|
||||
// Determine wound setting key based on weapon and damage type
|
||||
let settingKey = 'table1eWoundBluntForce';
|
||||
let tableLabel = 'Blunt Force';
|
||||
|
||||
if (weapon) {
|
||||
const name = String(weapon.name ?? "").toLowerCase();
|
||||
const damageType = String(weapon.system?.sla?.damageType ?? "").toLowerCase();
|
||||
|
||||
if (damageType.includes("blunt") || damageType.includes("impact")) {
|
||||
settingKey = 'table1eWoundBluntForce';
|
||||
tableLabel = 'Blunt Force';
|
||||
} else if (damageType.includes("penetrating") || damageType.includes("pierce") || damageType.includes("stab") || damageType.includes("bullet") || damageType.includes("shot")) {
|
||||
settingKey = 'table1eWoundGunshot';
|
||||
tableLabel = 'Gunshot';
|
||||
} else if (damageType.includes("slash") || damageType.includes("rend") || damageType.includes("bleed") || damageType.includes("cut")) {
|
||||
settingKey = 'table1eWoundBleeding';
|
||||
tableLabel = 'Bleeding';
|
||||
} else if (damageType.includes("fire") || damageType.includes("explos") || damageType.includes("acid") || damageType.includes("burn")) {
|
||||
settingKey = 'table1eWoundFireExplosives';
|
||||
tableLabel = 'Fire & Explosives';
|
||||
} else if (name.includes("pistol") || name.includes("smg") || name.includes("rifle") || name.includes("shotgun") || name.includes("blunderbuss") || name.includes("gun") || name.includes("harpoon") || name.includes("bullet") || name.includes("projectile") || name.includes("crossbow")) {
|
||||
settingKey = 'table1eWoundGunshot';
|
||||
tableLabel = 'Gunshot';
|
||||
} else if (name.includes("machete") || name.includes("cleaver") || name.includes("whip") || name.includes("blade") || name.includes("knife") || name.includes("sword") || name.includes("claws") || name.includes("bite") || name.includes("razor") || name.includes("teeth")) {
|
||||
settingKey = 'table1eWoundBleeding';
|
||||
tableLabel = 'Bleeding';
|
||||
} else if (name.includes("club") || name.includes("pipe") || name.includes("hammer") || name.includes("prod") || name.includes("unarmed") || name.includes("fist") || name.includes("strike") || name.includes("blunt") || name.includes("brawl") || name.includes("cuff")) {
|
||||
settingKey = 'table1eWoundBluntForce';
|
||||
tableLabel = 'Blunt Force';
|
||||
} else if (name.includes("bomb") || name.includes("acid") || name.includes("spit") || name.includes("fire") || name.includes("flame") || name.includes("grenade") || name.includes("explosive")) {
|
||||
settingKey = 'table1eWoundFireExplosives';
|
||||
tableLabel = 'Fire & Explosives';
|
||||
}
|
||||
}
|
||||
|
||||
const tableId = game.settings.get('sla-mothership', settingKey);
|
||||
if (!tableId) {
|
||||
ui.notifications.warn(`No wound table is configured for ${tableLabel}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve the table
|
||||
let resolved = game.tables.get(tableId) || game.tables.getName(tableId);
|
||||
if (!resolved) {
|
||||
for (const pack of game.packs) {
|
||||
if (pack.metadata.type === "RollTable") {
|
||||
const index = await pack.getIndex();
|
||||
const entry = index.find(e => e.id === tableId || e.name === tableId || e.name === `${tableLabel} Wound` || e.name === tableLabel.replace(/&/g, "and"));
|
||||
if (entry) {
|
||||
resolved = await pack.getDocument(entry._id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!resolved) {
|
||||
ui.notifications.warn(`The ${tableLabel} wound table could not be found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await actor.rollTable(resolved.uuid ?? resolved.id ?? tableId, resolved.formula ?? "1d10", "low", true, false, null, null);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Critical Success Option Button Handler ──
|
||||
root.querySelectorAll(".sla-crit-option-btn").forEach((button) => {
|
||||
button.addEventListener("click", async (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const actorId = button.dataset.actorId;
|
||||
const optionText = button.dataset.optionText;
|
||||
const actor = actorId ? game.actors?.get(actorId) : null;
|
||||
const actorName = actor ? actor.name : "An operative";
|
||||
|
||||
const chatContent = await foundry.applications.handlebars.renderTemplate(
|
||||
'systems/sla-mothership/templates/chat/sla-critical-selection.html',
|
||||
{
|
||||
actorName: actorName,
|
||||
img: actor?.img ?? 'icons/svg/mystery-man.svg',
|
||||
optionText: optionText
|
||||
}
|
||||
);
|
||||
|
||||
await ChatMessage.create({
|
||||
user: game.user.id,
|
||||
content: chatContent,
|
||||
speaker: ChatMessage.getSpeaker({ actor: actor })
|
||||
});
|
||||
|
||||
// Disable all options in the card
|
||||
const container = button.closest(".sla-crit-success-options");
|
||||
if (container) {
|
||||
container.querySelectorAll(".sla-crit-option-btn").forEach(btn => {
|
||||
btn.disabled = true;
|
||||
btn.style.opacity = 0.6;
|
||||
btn.style.cursor = "default";
|
||||
if (btn === button) {
|
||||
btn.style.background = "#0f300f";
|
||||
btn.style.border = "1px solid #1ebd1e";
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
//add custom damage dice for MOSH
|
||||
@@ -1249,3 +1476,366 @@ export function formatCreditsNumber(num) {
|
||||
return num.toString() + 'cr';
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// SLA Industries Pause Screen Animation
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
let slaPauseRefreshHandle = null;
|
||||
|
||||
function injectSlaPauseAnimation() {
|
||||
document.body?.classList.add("sla-pause-active");
|
||||
if (document.querySelector('.sla-pause-root')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const svgHTML = `
|
||||
<div class="sla-pause-root" aria-label="SLA Industries – Game Paused">
|
||||
<div class="sla-pause-stage">
|
||||
<svg class="sla-pause-svg" viewBox="-120 -120 240 260" xmlns="http://www.w3.org/2000/svg" role="img">
|
||||
<defs>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<clipPath id="sla-clip-outer">
|
||||
<circle cx="0" cy="0" r="108"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<circle cx="0" cy="0" r="108" fill="url(#sla-bg-grad)" class="sla-pause-core-glow"/>
|
||||
<g class="sla-ring-outer">
|
||||
<animateTransform attributeName="transform" type="rotate" from="0 0 0" to="360 0 0" dur="18s" repeatCount="indefinite"/>
|
||||
<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)"
|
||||
/>
|
||||
<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"/>
|
||||
<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>
|
||||
<g class="sla-ring-inner">
|
||||
<animateTransform attributeName="transform" type="rotate" from="360 0 0" to="0 0 0" dur="13s" repeatCount="indefinite"/>
|
||||
<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)"
|
||||
/>
|
||||
<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>
|
||||
<g filter="url(#sla-glow-red)" class="sla-pause-logo-core">
|
||||
<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"
|
||||
/>
|
||||
<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>
|
||||
<line x1="-70" y1="48" x2="70" y2="48" stroke="#8b1a1a" stroke-width="0.5" opacity="0.7"/>
|
||||
<text
|
||||
x="0" y="68"
|
||||
text-anchor="middle"
|
||||
font-family="'Oswald', 'Courier New', monospace"
|
||||
font-size="11.5"
|
||||
font-weight="500"
|
||||
letter-spacing="3.2"
|
||||
fill="#ff7b7b"
|
||||
class="sla-pause-standby"
|
||||
>PLEASE STAND BY</text>
|
||||
<text
|
||||
x="0" y="94"
|
||||
text-anchor="middle"
|
||||
font-family="'Oswald', 'Arial Narrow', sans-serif"
|
||||
font-size="8"
|
||||
font-weight="300"
|
||||
letter-spacing="3"
|
||||
fill="#b13b3b"
|
||||
opacity="0.92"
|
||||
>SLA INDUSTRIES</text>
|
||||
<line
|
||||
x1="0" y1="-90" x2="0" y2="0"
|
||||
stroke="#ff2222"
|
||||
stroke-width="0.8"
|
||||
opacity="0.18"
|
||||
class="sla-pause-scan"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body?.insertAdjacentHTML('beforeend', svgHTML);
|
||||
}
|
||||
|
||||
function removeSlaPauseAnimation() {
|
||||
document.querySelector(".sla-pause-root")?.remove();
|
||||
document.body?.classList.remove("sla-pause-active");
|
||||
}
|
||||
|
||||
function queueSlaPauseRefresh(delay = 0) {
|
||||
window.clearTimeout(slaPauseRefreshHandle);
|
||||
slaPauseRefreshHandle = window.setTimeout(() => {
|
||||
if (!game?.paused) return;
|
||||
injectSlaPauseAnimation();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
Hooks.once('ready', () => {
|
||||
if (game.paused) {
|
||||
queueSlaPauseRefresh(0);
|
||||
queueSlaPauseRefresh(60);
|
||||
queueSlaPauseRefresh(180);
|
||||
}
|
||||
});
|
||||
|
||||
Hooks.on('pauseGame', (paused) => {
|
||||
if (paused) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => injectSlaPauseAnimation()));
|
||||
queueSlaPauseRefresh(0);
|
||||
queueSlaPauseRefresh(60);
|
||||
queueSlaPauseRefresh(180);
|
||||
queueSlaPauseRefresh(360);
|
||||
} else {
|
||||
removeSlaPauseAnimation();
|
||||
}
|
||||
});
|
||||
|
||||
function getSlaHtmlRoot(html) {
|
||||
return html instanceof HTMLElement ? html : html?.[0] ?? null;
|
||||
}
|
||||
|
||||
function hideSlaRestrictedSidebarTabs() {
|
||||
[
|
||||
"#sidebar-tabs [data-tab='tables']",
|
||||
"#sidebar-tabs [aria-controls='tables']",
|
||||
"#sidebar-tabs [data-tab='compendium']",
|
||||
"#sidebar-tabs [aria-controls='compendium']"
|
||||
].forEach((selector) => {
|
||||
document.querySelectorAll(selector).forEach((el) => {
|
||||
el.closest("li")?.remove();
|
||||
if (el instanceof HTMLElement) el.remove();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeSlaElements(root, selectors = []) {
|
||||
selectors.forEach((selector) => {
|
||||
root.querySelectorAll(selector).forEach((el) => el.remove());
|
||||
});
|
||||
}
|
||||
|
||||
function pruneEmptySlaFolders(root) {
|
||||
const folders = Array.from(root.querySelectorAll(".directory-item.folder"));
|
||||
folders.reverse().forEach((folder) => {
|
||||
const visibleDocuments = Array.from(folder.querySelectorAll(".directory-item.document, li.document")).filter((node) => node.offsetParent !== null);
|
||||
const visibleFolders = Array.from(folder.querySelectorAll(":scope > .directory-list > .directory-item.folder, :scope > ol > li.folder")).filter((node) => node.offsetParent !== null);
|
||||
if (!visibleDocuments.length && !visibleFolders.length) folder.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function shouldShowSlaPlayerJournal(entry) {
|
||||
if (!entry) return false;
|
||||
const name = String(entry.name ?? "").trim();
|
||||
if (/^operative handbook$/i.test(name)) return true;
|
||||
if (entry.getFlag?.("sla-mothership", "playerVisible") === true) return true;
|
||||
return entry.testUserPermission?.(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER) ?? false;
|
||||
}
|
||||
|
||||
function filterSlaJournalDirectory(root) {
|
||||
const rows = Array.from(root.querySelectorAll("[data-entry-id], [data-document-id]"));
|
||||
rows.forEach((row) => {
|
||||
const id = row.dataset.entryId ?? row.dataset.documentId;
|
||||
if (!id) return;
|
||||
const entry = game.journal?.get?.(id);
|
||||
if (!shouldShowSlaPlayerJournal(entry)) row.remove();
|
||||
});
|
||||
pruneEmptySlaFolders(root);
|
||||
}
|
||||
|
||||
function enforceSlaPlayerDirectoryRestrictions(kind, html) {
|
||||
const root = getSlaHtmlRoot(html);
|
||||
if (!root || game.user?.isGM) return;
|
||||
|
||||
removeSlaElements(root, [
|
||||
".directory-header .header-actions",
|
||||
".directory-header .action-buttons",
|
||||
".sla-sidebar-actions",
|
||||
".qbm-scene-directory-actions",
|
||||
".qbm-launch",
|
||||
".sla-world-tools-launch",
|
||||
".sla-create-operative",
|
||||
".sla-create-random-operative",
|
||||
".sla-refresh-content",
|
||||
".sla-gear-cache-open",
|
||||
".sla-briefing-desk-open",
|
||||
".sla-bpn-dispatch-open",
|
||||
".sla-npc-director-open",
|
||||
".sla-npc-director-actor-open",
|
||||
".sla-ops-clock-open",
|
||||
".npp-generate-sidebar-btn"
|
||||
]);
|
||||
|
||||
if (kind === "journal") {
|
||||
filterSlaJournalDirectory(root);
|
||||
}
|
||||
|
||||
if (kind === "tables" || kind === "compendium") {
|
||||
root.innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
function enforceSlaPlayerSidebarTabRestrictions(app, html) {
|
||||
const tabName = app?.tabName ?? app?.options?.id ?? "";
|
||||
if (tabName === "actors") return enforceSlaPlayerDirectoryRestrictions("actors", html);
|
||||
if (tabName === "scenes") return enforceSlaPlayerDirectoryRestrictions("scenes", html);
|
||||
if (tabName === "journal") return enforceSlaPlayerDirectoryRestrictions("journal", html);
|
||||
if (tabName === "tables") return enforceSlaPlayerDirectoryRestrictions("tables", html);
|
||||
if (tabName === "compendium") return enforceSlaPlayerDirectoryRestrictions("compendium", html);
|
||||
if (tabName === "combat") return enforceSlaPlayerDirectoryRestrictions("combat", html);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 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()));
|
||||
|
||||
@@ -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.`);
|
||||
|
||||
@@ -904,6 +904,13 @@ export class SLAMothershipGenerator extends FormApplication {
|
||||
const worldItem = findWorldItem(morph.name, "ability");
|
||||
if (worldItem) {
|
||||
const embedded = toEmbeddedData(worldItem);
|
||||
// abilityType "morph" is schema-defined and guaranteed to be stored —
|
||||
// this prevents the Ebb enforcement sweep from ever deleting morph forms.
|
||||
embedded.system ??= {};
|
||||
embedded.system.sla ??= {};
|
||||
embedded.system.sla.abilityType = "morph";
|
||||
embedded.system.sla.morphForm = true;
|
||||
embedded.system.sla.isStartingForm = morph.name === startingMorphForm;
|
||||
items.push(embedded);
|
||||
} else {
|
||||
// Build a minimal ability document so the character isn't empty-handed.
|
||||
@@ -916,6 +923,7 @@ export class SLAMothershipGenerator extends FormApplication {
|
||||
summary: morph.summary,
|
||||
special: morph.special,
|
||||
sla: {
|
||||
abilityType: "morph",
|
||||
morphForm: true,
|
||||
instabilityCost: morph.instabilityCost,
|
||||
statMods: morph.statMods ?? {},
|
||||
|
||||
@@ -0,0 +1,853 @@
|
||||
import { SLAMothershipGenerator } from "./sla-character-generator.js";
|
||||
|
||||
const SYSTEM_ID = "sla-mothership";
|
||||
const ROOT_ACTOR_FOLDER = "SLA Mothership";
|
||||
const NPC_FOLDER = "NPCs";
|
||||
|
||||
const NPC_ARCHETYPES = {
|
||||
"Civilian Witness": {
|
||||
summary: "Clerks, residents, dock workers, minor officials, and bystanders with enough identity to question or endanger.",
|
||||
stats: { combat: 18, instinct: 32, speed: 24, loyalty: 46, sanity: 36, armor: 0, damageReduction: 0, health: 8, hits: 1 },
|
||||
enabled: { combat: true, instinct: true, speed: true, loyalty: true, sanity: true, armor: false },
|
||||
skills: [
|
||||
{ name: "Persuade", bonus: 20 },
|
||||
{ name: "Spot Hidden", bonus: 25 },
|
||||
{ name: "Drive (Civilian)", bonus: 20 },
|
||||
{ name: "Bureaucracy", bonus: 15 }
|
||||
],
|
||||
loadouts: {
|
||||
None: [],
|
||||
Civilian: ["Street Clothes / Leather Jacket", "UV / Multi-Spectrum Torch", "Oyster Card (ITB Card)"]
|
||||
},
|
||||
defaultLoadout: "Civilian",
|
||||
defaultRole: "Witness / Local",
|
||||
defaultAffiliation: "Mort Civilian",
|
||||
defaultDepartment: "Local District"
|
||||
},
|
||||
"Street Ganger": {
|
||||
summary: "Street muscle, lookouts, turf enforcers, and volatile crew members from Mort's lower tiers.",
|
||||
stats: { combat: 34, instinct: 34, speed: 30, loyalty: 24, sanity: 24, armor: 0, damageReduction: 0, health: 10, hits: 2 },
|
||||
enabled: { combat: true, instinct: true, speed: true, loyalty: true, sanity: false, armor: false },
|
||||
skills: [
|
||||
{ name: "Brawl", bonus: 25 },
|
||||
{ name: "Streetwise", bonus: 25 },
|
||||
{ name: "Intimidate", bonus: 20 },
|
||||
{ name: "Dodge", bonus: 20 },
|
||||
{ name: "Firearm (Pistol)", bonus: 20 }
|
||||
],
|
||||
loadouts: {
|
||||
None: [],
|
||||
"Street Melee": ["Collapsible Baton", "Standard Combat Knife", "Street Clothes / Leather Jacket"],
|
||||
Sidearm: ["FEN 603 Auto-Pistol", "Standard Combat Knife", "Street Clothes / Leather Jacket"]
|
||||
},
|
||||
defaultLoadout: "Sidearm",
|
||||
defaultRole: "Street Gang Enforcer",
|
||||
defaultAffiliation: "Local Street Gang",
|
||||
defaultDepartment: "Turf Crew"
|
||||
},
|
||||
"Shiver Patrol": {
|
||||
summary: "Street cops, precinct enforcers, and cordon teams good for patrol scenes, arrests, and fast escalation.",
|
||||
stats: { combat: 44, instinct: 38, speed: 34, loyalty: 58, sanity: 36, armor: 3, damageReduction: 1, health: 12, hits: 2 },
|
||||
enabled: { combat: true, instinct: true, speed: true, loyalty: true, sanity: true, armor: true },
|
||||
skills: [
|
||||
{ name: "Firearm (Pistol)", bonus: 30 },
|
||||
{ name: "Firearm (SMG)", bonus: 25 },
|
||||
{ name: "Athletics", bonus: 20 },
|
||||
{ name: "Command", bonus: 20 },
|
||||
{ name: "Spot Hidden", bonus: 25 }
|
||||
],
|
||||
loadouts: {
|
||||
None: [],
|
||||
Patrol: ["FEN 401 Shiver Pistol", "Collapsible Baton", "Shiver Flak Vest"],
|
||||
"SMG Team": ["FEN 209 Machine Pistol", "Collapsible Baton", "Full Shiver Armour"]
|
||||
},
|
||||
defaultLoadout: "Patrol",
|
||||
defaultRole: "Shiver Patrol Officer",
|
||||
defaultAffiliation: "SLA Industries",
|
||||
defaultDepartment: "Shiver Precinct"
|
||||
},
|
||||
"Corp Operative": {
|
||||
summary: "Standard corporate assets, contract staff, or in-house operatives for Mort departments and sponsor interests.",
|
||||
stats: { combat: 40, instinct: 40, speed: 32, loyalty: 50, sanity: 40, armor: 2, damageReduction: 0, health: 11, hits: 2 },
|
||||
enabled: { combat: true, instinct: true, speed: true, loyalty: true, sanity: true, armor: true },
|
||||
skills: [
|
||||
{ name: "Firearm (Pistol)", bonus: 25 },
|
||||
{ name: "Command", bonus: 20 },
|
||||
{ name: "Spot Hidden", bonus: 20 },
|
||||
{ name: "Bureaucracy", bonus: 20 },
|
||||
{ name: "Persuade", bonus: 15 }
|
||||
],
|
||||
loadouts: {
|
||||
None: [],
|
||||
Sidearm: ["FEN 603 Auto-Pistol", "Street Clothes / Leather Jacket"],
|
||||
"Field Agent": ["FEN 209 Machine Pistol", "Shiver Flak Vest", "Klippo Multi-Band Communicator"]
|
||||
},
|
||||
defaultLoadout: "Field Agent",
|
||||
defaultAffiliationType: "Corporation",
|
||||
defaultRole: "Corporate Operative",
|
||||
defaultAffiliation: "SLA Industries",
|
||||
defaultDepartment: "Operations",
|
||||
defaultSector: "Mort City"
|
||||
},
|
||||
Media: {
|
||||
summary: "Reporters, propaganda staff, image handlers, and camera teams who shape the public story while staying very close to the danger.",
|
||||
stats: { combat: 20, instinct: 42, speed: 28, loyalty: 44, sanity: 42, armor: 1, damageReduction: 0, health: 9, hits: 1 },
|
||||
enabled: { combat: true, instinct: true, speed: true, loyalty: true, sanity: true, armor: true },
|
||||
skills: [
|
||||
{ name: "Persuade", bonus: 30 },
|
||||
{ name: "Fast Talk", bonus: 25 },
|
||||
{ name: "SLA Info", bonus: 25 },
|
||||
{ name: "Spot Hidden", bonus: 20 },
|
||||
{ name: "Intimidate", bonus: 15 }
|
||||
],
|
||||
loadouts: {
|
||||
None: [],
|
||||
Press: ["Klippo Multi-Band Communicator", "UV / Multi-Spectrum Torch", "Street Clothes / Leather Jacket"],
|
||||
"Embedded Crew": ["FEN 603 Auto-Pistol", "Klippo Multi-Band Communicator", "Street Clothes / Leather Jacket"]
|
||||
},
|
||||
defaultLoadout: "Press",
|
||||
defaultAffiliationType: "Corporation",
|
||||
defaultRole: "Media Correspondent",
|
||||
defaultAffiliation: "SLA Media",
|
||||
defaultDepartment: "Editorial / Field Unit",
|
||||
defaultSector: "Downtown Mort"
|
||||
},
|
||||
"Internal Affairs": {
|
||||
summary: "Cold, suspicious investigators and auditors used for leak hunts, corruption probes, and quiet pressure on operatives or departments.",
|
||||
stats: { combat: 34, instinct: 48, speed: 28, loyalty: 56, sanity: 44, armor: 2, damageReduction: 0, health: 10, hits: 2 },
|
||||
enabled: { combat: true, instinct: true, speed: true, loyalty: true, sanity: true, armor: true },
|
||||
skills: [
|
||||
{ name: "Bureaucracy", bonus: 30 },
|
||||
{ name: "Psychology", bonus: 25 },
|
||||
{ name: "Intimidate", bonus: 25 },
|
||||
{ name: "Command", bonus: 20 },
|
||||
{ name: "Firearm (Pistol)", bonus: 20 }
|
||||
],
|
||||
loadouts: {
|
||||
None: [],
|
||||
Audit: ["FEN 603 Auto-Pistol", "Klippo Multi-Band Communicator", "Street Clothes / Leather Jacket"],
|
||||
Pressure: ["FEN 401 Shiver Pistol", "Shiver Flak Vest", "Klippo Multi-Band Communicator"]
|
||||
},
|
||||
defaultLoadout: "Audit",
|
||||
defaultAffiliationType: "Department",
|
||||
defaultRole: "Internal Affairs Investigator",
|
||||
defaultAffiliation: "SLA Industries",
|
||||
defaultDepartment: "Internal Affairs",
|
||||
defaultSector: "Cannibal Sectors"
|
||||
},
|
||||
"DarkNight Agent": {
|
||||
summary: "Hard, covert, and unpleasant professionals for pressure scenes, black-bag work, and very bad conversations.",
|
||||
stats: { combat: 52, instinct: 48, speed: 42, loyalty: 42, sanity: 44, armor: 4, damageReduction: 2, health: 13, hits: 2 },
|
||||
enabled: { combat: true, instinct: true, speed: true, loyalty: true, sanity: true, armor: true },
|
||||
skills: [
|
||||
{ name: "Stealth", bonus: 30 },
|
||||
{ name: "Dodge", bonus: 25 },
|
||||
{ name: "Spot Hidden", bonus: 30 },
|
||||
{ name: "Firearm (SMG)", bonus: 30 },
|
||||
{ name: "Intimidate", bonus: 25 }
|
||||
],
|
||||
loadouts: {
|
||||
None: [],
|
||||
"Strike Agent": ["FEN 209 Machine Pistol", "Standard Combat Knife", "Stealth Suit"],
|
||||
Breach: ["FEN AR Assault Rifle", "Fragmentation Grenade", "Crackshot Armour"]
|
||||
},
|
||||
defaultLoadout: "Strike Agent",
|
||||
defaultAffiliationType: "Squad",
|
||||
defaultRole: "DarkNight Agent",
|
||||
defaultAffiliation: "DarkNight",
|
||||
defaultDepartment: "Black Unit",
|
||||
defaultSector: "Cannibal Sectors"
|
||||
},
|
||||
"Tek Specialist": {
|
||||
summary: "Engineers, locks-and-systems staff, data handlers, and machine-facing support assets.",
|
||||
stats: { combat: 26, instinct: 42, speed: 28, loyalty: 46, sanity: 40, armor: 1, damageReduction: 0, health: 9, hits: 1 },
|
||||
enabled: { combat: true, instinct: true, speed: true, loyalty: true, sanity: true, armor: true },
|
||||
skills: [
|
||||
{ name: "Computer Systems", bonus: 30 },
|
||||
{ name: "Tech (Computers & AI)", bonus: 25 },
|
||||
{ name: "Tech (Electronics)", bonus: 25 },
|
||||
{ name: "Spot Hidden", bonus: 20 },
|
||||
{ name: "Firearm (Pistol)", bonus: 15 }
|
||||
],
|
||||
loadouts: {
|
||||
None: [],
|
||||
Utility: ["FEN 603 Auto-Pistol", "Klippo Multi-Band Communicator", "UV / Multi-Spectrum Torch"],
|
||||
"Field Tek": ["FEN 603 Auto-Pistol", "BOOPA Medical Kit (Standard)", "Klippo Multi-Band Communicator"]
|
||||
},
|
||||
defaultLoadout: "Utility",
|
||||
defaultAffiliationType: "Department",
|
||||
defaultRole: "Tek Specialist",
|
||||
defaultAffiliation: "Tek Division",
|
||||
defaultDepartment: "Systems",
|
||||
defaultSector: "Industrial Edge"
|
||||
},
|
||||
"Ebb Operative": {
|
||||
summary: "Psychic intervention assets, occult handlers, and dangerous minds kept near the edge of tolerable doctrine.",
|
||||
stats: { combat: 30, instinct: 46, speed: 24, loyalty: 30, sanity: 46, armor: 0, damageReduction: 0, health: 9, hits: 2 },
|
||||
enabled: { combat: true, instinct: true, speed: true, loyalty: false, sanity: true, armor: false },
|
||||
skills: [
|
||||
{ name: "Biofeedback", bonus: 25 },
|
||||
{ name: "Formulate", bonus: 25 },
|
||||
{ name: "Ebb (Core)", bonus: 25 },
|
||||
{ name: "Spot Hidden", bonus: 20 },
|
||||
{ name: "Firearm (Pistol)", bonus: 15 }
|
||||
],
|
||||
loadouts: {
|
||||
None: [],
|
||||
Focus: ["FEN 603 Auto-Pistol", "BOOPA Medical Kit (Standard)", "Klippo Multi-Band Communicator"]
|
||||
},
|
||||
defaultLoadout: "Focus",
|
||||
defaultAffiliationType: "Department",
|
||||
defaultRole: "Ebb Handler",
|
||||
defaultAffiliation: "SLA Industries",
|
||||
defaultDepartment: "Ebb / Black",
|
||||
defaultSector: "Mort City"
|
||||
},
|
||||
"Predator / Beast": {
|
||||
summary: "Mutants, monsters, feral biogen threats, or engineered horrors that need only enough structure to fight and stalk.",
|
||||
stats: { combat: 46, instinct: 56, speed: 40, loyalty: 12, sanity: 10, armor: 2, damageReduction: 1, health: 14, hits: 3 },
|
||||
enabled: { combat: true, instinct: true, speed: true, loyalty: false, sanity: false, armor: true },
|
||||
skills: [
|
||||
{ name: "Brawl", bonus: 30 },
|
||||
{ name: "Athletics", bonus: 25 },
|
||||
{ name: "Dodge", bonus: 20 },
|
||||
{ name: "Spot Hidden", bonus: 25 }
|
||||
],
|
||||
loadouts: {
|
||||
None: []
|
||||
},
|
||||
defaultLoadout: "None",
|
||||
defaultAffiliationType: "Independent",
|
||||
defaultRole: "Hunting Threat",
|
||||
defaultAffiliation: "Hostile Specimen",
|
||||
defaultDepartment: "Wild / Uncontrolled",
|
||||
defaultSector: "Waste District"
|
||||
}
|
||||
};
|
||||
|
||||
const QUALITY_MODS = {
|
||||
Rabble: { stat: -8, skill: -10, health: -2, hits: 0 },
|
||||
Standard: { stat: 0, skill: 0, health: 0, hits: 0 },
|
||||
Veteran: { stat: 6, skill: 5, health: 1, hits: 0 },
|
||||
Elite: { stat: 10, skill: 10, health: 2, hits: 1 },
|
||||
Boss: { stat: 14, skill: 15, health: 4, hits: 1 }
|
||||
};
|
||||
|
||||
const DISPOSITIONS = ["Neutral", "Professional", "Hostile", "Nervous", "Fanatical", "Cornered", "Corrupt", "Helpful"];
|
||||
const THREAT_TIERS = ["Low", "Moderate", "High", "Extreme"];
|
||||
const AFFILIATION_TYPES = ["Department", "Gang", "Corporation", "Cult", "Squad", "Civic", "Independent"];
|
||||
|
||||
const ROLE_SNIPPETS = {
|
||||
"Civilian Witness": ["Transit clerk", "Dock handler", "Apartment resident", "Witness under pressure", "Small trader"],
|
||||
"Street Ganger": ["Lookout", "Enforcer", "Runner", "Debt collector", "Back-alley bruiser"],
|
||||
"Shiver Patrol": ["Patrol officer", "Checkpoint lead", "Riot response cop", "Beat sergeant", "Scene controller"],
|
||||
"Corp Operative": ["Case handler", "Recovery agent", "Sponsor fixer", "Quiet escort", "Internal security operative"],
|
||||
Media: ["Field correspondent", "Embedded camera lead", "Image handler", "Narrative producer", "Live scene reporter"],
|
||||
"Internal Affairs": ["Audit investigator", "Leak hunter", "Corruption examiner", "Interrogation lead", "Compliance agent"],
|
||||
"DarkNight Agent": ["Pressure asset", "Black-bag specialist", "Interdiction hunter", "Silent breach lead", "Cleanup professional"],
|
||||
"Tek Specialist": ["Systems breach tech", "Maintenance lead", "Field engineer", "Data recovery tech", "Signals operator"],
|
||||
"Ebb Operative": ["Control asset", "Psi observer", "Occult investigator", "Flux-sensitive handler", "Discreet interventionist"],
|
||||
"Predator / Beast": ["Tunnel stalker", "Waste-zone hunter", "Escaped specimen", "Territory alpha", "Feral ambusher"]
|
||||
};
|
||||
|
||||
const AFFILIATION_SNIPPETS = {
|
||||
Department: ["Mort Department", "Ops Con", "Shiver Precinct", "Tek Division", "Cannibal Sector Authority"],
|
||||
Gang: ["Chrome Jacks", "Sump Dogs", "Red Wire Crew", "Murder Mile Kings", "Ash-Sector Runners"],
|
||||
Corporation: ["SLA Industries", "FEN", "Wraithen Security", "MediCore", "Tek Division"],
|
||||
Cult: ["The True Noise", "Church of White Fire", "The Bent Halo", "The Last Broadcast", "Ash Choir"],
|
||||
Squad: ["Blue BPN Hold Team", "Black Transit Unit", "Sweep Detail", "Strike Cell Seven", "Special Recovery Team"],
|
||||
Civic: ["Transit Maintenance", "Block Committee", "Waste Processing", "Public Utility", "Relocation Office"],
|
||||
Independent: ["Freelance crew", "Mercenary pair", "Broker network", "Private fixer", "Unaffiliated contact"]
|
||||
};
|
||||
|
||||
const DEPARTMENT_SNIPPETS = {
|
||||
"Civilian Witness": ["Hab Block", "Transit", "Utility", "Street Market", "Dock Quarter"],
|
||||
"Street Ganger": ["East Cut", "Underline", "Recycling Stack", "Canal Row", "Tunnel Nine"],
|
||||
"Shiver Patrol": ["Precinct 88", "Riot Control", "Street Division", "Transit Policing", "Evidence Escort"],
|
||||
"Corp Operative": ["Internal Security", "Investigations", "Sponsor Liaison", "Logistics Recovery", "Operations"],
|
||||
Media: ["Editorial Desk", "Live Scene Unit", "Mort Broadcast", "Image Control", "Public Feed Team"],
|
||||
"Internal Affairs": ["Compliance Review", "Leak Office", "Audit Section", "Disciplinary Unit", "Special Oversight"],
|
||||
"DarkNight Agent": ["Silent Entry", "Black Archive", "Pressure Cell", "Removal Team", "Interdiction"],
|
||||
"Tek Specialist": ["Signals", "Maintenance", "Surveillance", "Field Systems", "Machine Control"],
|
||||
"Ebb Operative": ["Occult Oversight", "Black Psi", "Flux Watch", "Intervention Cell", "Containment"],
|
||||
"Predator / Beast": ["Den", "Nest", "Territory", "Spore Cluster", "Hunting Ground"]
|
||||
};
|
||||
|
||||
export class SLANpcGeneratorApp extends FormApplication {
|
||||
constructor(options = {}) {
|
||||
super(options);
|
||||
this.formState = null;
|
||||
}
|
||||
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
id: "sla-npc-generator",
|
||||
classes: ["mosh", "sheet", "sla-generator", "sla-npc-generator"],
|
||||
template: "systems/sla-mothership/templates/dialogs/sla-npc-generator.html",
|
||||
width: 780,
|
||||
height: "auto",
|
||||
submitOnChange: false,
|
||||
closeOnSubmit: false
|
||||
});
|
||||
}
|
||||
|
||||
get title() {
|
||||
return "SLA NPC Desk";
|
||||
}
|
||||
|
||||
async getData() {
|
||||
await SLAMothershipGenerator.ensureSeedData();
|
||||
const seed = await SLAMothershipGenerator.loadSeedData();
|
||||
const speciesOptions = seed.species.species.map((entry) => entry.name);
|
||||
const state = this.formState ?? this.defaultState(speciesOptions);
|
||||
this.formState = state;
|
||||
|
||||
const archetype = NPC_ARCHETYPES[state.archetype] ?? NPC_ARCHETYPES["Street Ganger"];
|
||||
const loadoutOptions = Object.keys(archetype.loadouts ?? { None: [] });
|
||||
|
||||
return {
|
||||
state,
|
||||
accentKey: getArchetypeAccentKey(state.archetype),
|
||||
archetypes: Object.entries(NPC_ARCHETYPES).map(([name, entry]) => ({ name, summary: entry.summary })),
|
||||
speciesOptions,
|
||||
qualityOptions: Object.keys(QUALITY_MODS),
|
||||
dispositionOptions: DISPOSITIONS,
|
||||
threatOptions: THREAT_TIERS,
|
||||
affiliationTypes: AFFILIATION_TYPES,
|
||||
loadoutOptions,
|
||||
summary: archetype.summary,
|
||||
preview: buildNpcPreview(state, archetype)
|
||||
};
|
||||
}
|
||||
|
||||
defaultState(speciesOptions = []) {
|
||||
const archetype = "Street Ganger";
|
||||
const config = NPC_ARCHETYPES[archetype];
|
||||
return {
|
||||
name: SLAMothershipGenerator.randomName(),
|
||||
archetype,
|
||||
quality: "Standard",
|
||||
species: speciesOptions.includes("Human") ? "Human" : (speciesOptions[0] ?? "Human"),
|
||||
affiliationType: config.defaultAffiliationType ?? "Gang",
|
||||
affiliation: config.defaultAffiliation,
|
||||
department: config.defaultDepartment,
|
||||
role: config.defaultRole,
|
||||
sector: config.defaultSector ?? "Mort City",
|
||||
disposition: "Hostile",
|
||||
threat: "Moderate",
|
||||
loadout: config.defaultLoadout ?? "None",
|
||||
assignPortrait: true,
|
||||
openSheet: true,
|
||||
notes: ""
|
||||
};
|
||||
}
|
||||
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
|
||||
html.find(".sla-npc-random-name").on("click", () => {
|
||||
html.find("[name='name']").val(SLAMothershipGenerator.randomName());
|
||||
});
|
||||
|
||||
html.find(".sla-npc-randomize").on("click", async () => {
|
||||
const state = this.collectState(html);
|
||||
const randomized = this.randomizeState(state);
|
||||
this.formState = randomized;
|
||||
await this.render(false);
|
||||
});
|
||||
|
||||
html.find("[name='archetype']").on("change", async () => {
|
||||
const current = this.collectState(html);
|
||||
this.formState = this.rethemeStateForArchetype(current, current.archetype);
|
||||
await this.render(false);
|
||||
});
|
||||
|
||||
const rerenderSelectors = [
|
||||
"[name='quality']",
|
||||
"[name='species']",
|
||||
"[name='disposition']",
|
||||
"[name='threat']",
|
||||
"[name='loadout']",
|
||||
"[name='affiliationType']"
|
||||
];
|
||||
|
||||
for (const selector of rerenderSelectors) {
|
||||
html.find(selector).on("change", async () => {
|
||||
this.formState = this.collectState(html);
|
||||
await this.render(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
collectState(html) {
|
||||
return {
|
||||
name: String(html.find("[name='name']").val() ?? "").trim(),
|
||||
archetype: String(html.find("[name='archetype']").val() ?? "Street Ganger"),
|
||||
quality: String(html.find("[name='quality']").val() ?? "Standard"),
|
||||
species: String(html.find("[name='species']").val() ?? "Human"),
|
||||
affiliationType: String(html.find("[name='affiliationType']").val() ?? "Department"),
|
||||
affiliation: String(html.find("[name='affiliation']").val() ?? "").trim(),
|
||||
department: String(html.find("[name='department']").val() ?? "").trim(),
|
||||
role: String(html.find("[name='role']").val() ?? "").trim(),
|
||||
sector: String(html.find("[name='sector']").val() ?? "").trim(),
|
||||
disposition: String(html.find("[name='disposition']").val() ?? "Neutral"),
|
||||
threat: String(html.find("[name='threat']").val() ?? "Moderate"),
|
||||
loadout: String(html.find("[name='loadout']").val() ?? "None"),
|
||||
assignPortrait: Boolean(html.find("[name='assignPortrait']").prop("checked")),
|
||||
openSheet: Boolean(html.find("[name='openSheet']").prop("checked")),
|
||||
notes: String(html.find("[name='notes']").val() ?? "").trim()
|
||||
};
|
||||
}
|
||||
|
||||
randomizeState(current) {
|
||||
const state = { ...current };
|
||||
const archetypeName = state.archetype || randomFrom(Object.keys(NPC_ARCHETYPES)) || "Street Ganger";
|
||||
const archetype = NPC_ARCHETYPES[archetypeName] ?? NPC_ARCHETYPES["Street Ganger"];
|
||||
const themed = this.rethemeStateForArchetype(state, archetypeName);
|
||||
themed.affiliation = randomFrom(AFFILIATION_SNIPPETS[themed.affiliationType] ?? [archetype.defaultAffiliation]) ?? archetype.defaultAffiliation;
|
||||
themed.department = randomFrom(DEPARTMENT_SNIPPETS[archetypeName] ?? [archetype.defaultDepartment]) ?? archetype.defaultDepartment;
|
||||
themed.role = randomFrom(ROLE_SNIPPETS[archetypeName] ?? [archetype.defaultRole]) ?? archetype.defaultRole;
|
||||
themed.sector = randomFrom(getSectorOptionsForArchetype(archetypeName, archetype.defaultSector)) ?? (archetype.defaultSector ?? "Mort City");
|
||||
themed.disposition = randomFrom(DISPOSITIONS) ?? "Neutral";
|
||||
themed.threat = randomFrom(THREAT_TIERS) ?? "Moderate";
|
||||
themed.loadout = randomFrom(Object.keys(archetype.loadouts ?? { None: [] })) ?? "None";
|
||||
themed.name = SLAMothershipGenerator.randomName();
|
||||
return themed;
|
||||
}
|
||||
|
||||
rethemeStateForArchetype(current, archetypeName) {
|
||||
const archetype = NPC_ARCHETYPES[archetypeName] ?? NPC_ARCHETYPES["Street Ganger"];
|
||||
const next = { ...current, archetype: archetypeName };
|
||||
next.affiliationType = archetype.defaultAffiliationType ?? current.affiliationType ?? "Department";
|
||||
next.affiliation = archetype.defaultAffiliation ?? "";
|
||||
next.department = archetype.defaultDepartment ?? "";
|
||||
next.role = archetype.defaultRole ?? "";
|
||||
next.sector = archetype.defaultSector ?? "Mort City";
|
||||
next.loadout = archetype.defaultLoadout ?? "None";
|
||||
next.disposition = getDefaultDispositionForArchetype(archetypeName);
|
||||
next.threat = getDefaultThreatForArchetype(archetypeName);
|
||||
next.species = getDefaultSpeciesForArchetype(archetypeName, current.species);
|
||||
return next;
|
||||
}
|
||||
|
||||
async _updateObject(_event, formData) {
|
||||
const expanded = foundry.utils.expandObject(formData);
|
||||
const state = {
|
||||
name: String(expanded.name ?? "").trim(),
|
||||
archetype: String(expanded.archetype ?? "Street Ganger"),
|
||||
quality: String(expanded.quality ?? "Standard"),
|
||||
species: String(expanded.species ?? "Human"),
|
||||
affiliationType: String(expanded.affiliationType ?? "Department"),
|
||||
affiliation: String(expanded.affiliation ?? "").trim(),
|
||||
department: String(expanded.department ?? "").trim(),
|
||||
role: String(expanded.role ?? "").trim(),
|
||||
sector: String(expanded.sector ?? "").trim(),
|
||||
disposition: String(expanded.disposition ?? "Neutral"),
|
||||
threat: String(expanded.threat ?? "Moderate"),
|
||||
loadout: String(expanded.loadout ?? "None"),
|
||||
assignPortrait: Boolean(expanded.assignPortrait),
|
||||
openSheet: Boolean(expanded.openSheet),
|
||||
notes: String(expanded.notes ?? "").trim()
|
||||
};
|
||||
|
||||
this.formState = state;
|
||||
const result = await generateSlaNpc(state);
|
||||
if (result?.actor && state.openSheet) {
|
||||
result.actor.sheet?.render(true);
|
||||
}
|
||||
ui.notifications.info(`SLA NPC ready: ${result.actor.name}`);
|
||||
this.render(false);
|
||||
}
|
||||
|
||||
static open(options = {}) {
|
||||
return new SLANpcGeneratorApp(options).render({ force: true });
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateSlaNpc(state = {}) {
|
||||
await SLAMothershipGenerator.ensureSeedData();
|
||||
const seed = await SLAMothershipGenerator.loadSeedData();
|
||||
|
||||
const archetype = NPC_ARCHETYPES[state.archetype] ?? NPC_ARCHETYPES["Street Ganger"];
|
||||
const quality = QUALITY_MODS[state.quality] ?? QUALITY_MODS.Standard;
|
||||
const speciesName = String(state.species ?? "Human").trim() || "Human";
|
||||
const resolvedName = String(state.name ?? "").trim() || SLAMothershipGenerator.randomName();
|
||||
const folder = await ensureNpcFolder();
|
||||
|
||||
const actor = await Actor.create({
|
||||
name: resolvedName,
|
||||
type: "creature",
|
||||
folder: folder.id
|
||||
});
|
||||
|
||||
const stats = applyQualityToStats(archetype.stats, quality);
|
||||
const enabled = archetype.enabled ?? {};
|
||||
const description = buildNpcDescription({ state, archetype, speciesName });
|
||||
const biography = buildNpcBiography({ state, archetype, speciesName });
|
||||
|
||||
await actor.update({
|
||||
img: SLAMothershipGenerator.resolveSpeciesImage(speciesName),
|
||||
"system.description": description,
|
||||
"system.biography": biography,
|
||||
"system.notes": String(state.notes ?? "").trim(),
|
||||
"system.health.value": stats.health,
|
||||
"system.health.max": stats.health,
|
||||
"system.hits.value": 0,
|
||||
"system.hits.max": stats.hits,
|
||||
"system.stats.combat.value": stats.combat,
|
||||
"system.stats.instinct.value": stats.instinct,
|
||||
"system.stats.speed.value": stats.speed,
|
||||
"system.stats.loyalty.value": stats.loyalty,
|
||||
"system.stats.sanity.value": stats.sanity,
|
||||
"system.stats.armor.mod": stats.armor,
|
||||
"system.stats.armor.damageReduction": stats.damageReduction,
|
||||
"system.stats.combat.enabled": Boolean(enabled.combat),
|
||||
"system.stats.instinct.enabled": Boolean(enabled.instinct),
|
||||
"system.stats.speed.enabled": Boolean(enabled.speed),
|
||||
"system.stats.loyalty.enabled": Boolean(enabled.loyalty),
|
||||
"system.stats.sanity.enabled": Boolean(enabled.sanity),
|
||||
"system.stats.armor.enabled": Boolean(enabled.armor),
|
||||
"system.slaNpc.archetype.value": state.archetype,
|
||||
"system.slaNpc.species.value": speciesName,
|
||||
"system.slaNpc.affiliationType.value": state.affiliationType,
|
||||
"system.slaNpc.affiliation.value": String(state.affiliation ?? "").trim(),
|
||||
"system.slaNpc.department.value": String(state.department ?? "").trim(),
|
||||
"system.slaNpc.role.value": String(state.role ?? "").trim(),
|
||||
"system.slaNpc.sector.value": String(state.sector ?? "").trim(),
|
||||
"system.slaNpc.disposition.value": String(state.disposition ?? "").trim(),
|
||||
"system.slaNpc.threat.value": String(state.threat ?? "").trim(),
|
||||
"system.slaNpc.summary.value": archetype.summary
|
||||
});
|
||||
|
||||
const itemPayloads = [];
|
||||
itemPayloads.push(...buildNpcSkillItems(archetype.skills ?? [], quality, seed.skills.skills));
|
||||
itemPayloads.push(...buildLoadoutItems(archetype, state.loadout));
|
||||
|
||||
if (state.archetype === "Ebb Operative" && SLAMothershipGenerator.isEbbSpecies(speciesName)) {
|
||||
itemPayloads.push(...SLAMothershipGenerator.buildEbbSkillItems(speciesName));
|
||||
itemPayloads.push(...SLAMothershipGenerator.buildEbbItems(speciesName));
|
||||
}
|
||||
|
||||
if (state.archetype === "Predator / Beast") {
|
||||
itemPayloads.push(...SLAMothershipGenerator.buildSpeciesItems(speciesName));
|
||||
}
|
||||
|
||||
const uniquePayloads = uniqueEmbeddedItems(itemPayloads);
|
||||
if (uniquePayloads.length) {
|
||||
await actor.createEmbeddedDocuments("Item", uniquePayloads);
|
||||
}
|
||||
|
||||
if (state.assignPortrait && game.npcPortraitPack?.assignPortraitToActor) {
|
||||
await game.npcPortraitPack.assignPortraitToActor(actor);
|
||||
}
|
||||
|
||||
return { actor };
|
||||
}
|
||||
|
||||
async function ensureNpcFolder() {
|
||||
let root = game.folders.find((folder) => folder.type === "Actor" && folder.name === ROOT_ACTOR_FOLDER && !folder.folder);
|
||||
if (!root) {
|
||||
root = await Folder.create({ name: ROOT_ACTOR_FOLDER, type: "Actor", color: "#163447" });
|
||||
}
|
||||
|
||||
let folder = game.folders.find((entry) => entry.type === "Actor" && entry.name === NPC_FOLDER && entry.folder?.id === root.id);
|
||||
if (!folder) {
|
||||
folder = await Folder.create({ name: NPC_FOLDER, type: "Actor", folder: root.id, color: "#5b3349" });
|
||||
}
|
||||
return folder;
|
||||
}
|
||||
|
||||
function applyQualityToStats(baseStats, quality) {
|
||||
return {
|
||||
combat: clamp98((baseStats.combat ?? 10) + (quality.stat ?? 0)),
|
||||
instinct: clamp98((baseStats.instinct ?? 10) + (quality.stat ?? 0)),
|
||||
speed: clamp98((baseStats.speed ?? 10) + (quality.stat ?? 0)),
|
||||
loyalty: clamp98((baseStats.loyalty ?? 10) + (quality.stat ?? 0)),
|
||||
sanity: clamp98((baseStats.sanity ?? 10) + (quality.stat ?? 0)),
|
||||
armor: Math.max(0, Number(baseStats.armor ?? 0)),
|
||||
damageReduction: Math.max(0, Number(baseStats.damageReduction ?? 0)),
|
||||
health: Math.max(1, Number(baseStats.health ?? 8) + Number(quality.health ?? 0)),
|
||||
hits: Math.max(1, Number(baseStats.hits ?? 1) + Number(quality.hits ?? 0))
|
||||
};
|
||||
}
|
||||
|
||||
function buildNpcSkillItems(skillDefs, quality, allSkillData = []) {
|
||||
return (skillDefs ?? [])
|
||||
.map((entry) => {
|
||||
const baseBonus = Number(entry.bonus ?? 10) + Number(quality.skill ?? 0);
|
||||
const worldItem = findWorldItem(entry.name, "skill");
|
||||
if (worldItem) {
|
||||
const embedded = toEmbeddedData(worldItem);
|
||||
embedded.system.bonus = clamp98(baseBonus);
|
||||
embedded.system.rank = rankFromBonus(embedded.system.bonus);
|
||||
embedded.system.sla ??= {};
|
||||
embedded.system.sla.baseBonus = Number(worldItem.system?.bonus ?? 0) || 0;
|
||||
embedded.system.sla.packageBonus = Math.max(0, embedded.system.bonus - Number(embedded.system.sla.baseBonus ?? 0));
|
||||
return embedded;
|
||||
}
|
||||
|
||||
const data = allSkillData.find((skill) => normalizeText(skill.name) === normalizeText(entry.name)) ?? {};
|
||||
return {
|
||||
name: entry.name,
|
||||
type: "skill",
|
||||
img: "icons/svg/upgrade.svg",
|
||||
system: {
|
||||
description: "",
|
||||
rank: rankFromBonus(baseBonus),
|
||||
bonus: clamp98(baseBonus),
|
||||
prerequisite_ids: [],
|
||||
sla: {
|
||||
category: data.categoryRef ?? "",
|
||||
skillFamily: data.categoryRef ?? "",
|
||||
source: "SLA NPC Desk"
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function buildLoadoutItems(archetype, loadoutName) {
|
||||
const loadout = archetype.loadouts?.[loadoutName] ?? [];
|
||||
return loadout
|
||||
.map((name) => findWorldItem(name))
|
||||
.filter(Boolean)
|
||||
.map((item) => toEmbeddedData(item));
|
||||
}
|
||||
|
||||
function buildNpcDescription({ state, archetype, speciesName }) {
|
||||
return [
|
||||
`${state.role || archetype.defaultRole}.`,
|
||||
`${state.affiliation || archetype.defaultAffiliation}${state.department ? ` / ${state.department}` : ""}.`,
|
||||
`${state.disposition || "Neutral"} presence with ${state.threat || "Moderate"} threat pressure.`,
|
||||
`${speciesName} profile.`
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
function buildNpcBiography({ state, archetype, speciesName }) {
|
||||
return `
|
||||
<p><strong>Role:</strong> ${escapeHtml(state.role || archetype.defaultRole)}</p>
|
||||
<p><strong>Affiliation:</strong> ${escapeHtml(state.affiliation || archetype.defaultAffiliation)}</p>
|
||||
<p><strong>Department / Cell:</strong> ${escapeHtml(state.department || archetype.defaultDepartment)}</p>
|
||||
<p><strong>Species:</strong> ${escapeHtml(speciesName)}</p>
|
||||
<p><strong>Disposition:</strong> ${escapeHtml(state.disposition || "Neutral")} | <strong>Threat:</strong> ${escapeHtml(state.threat || "Moderate")}</p>
|
||||
<p><strong>Sector:</strong> ${escapeHtml(state.sector || "Mort City")}</p>
|
||||
<p>${escapeHtml(archetype.summary)}</p>
|
||||
`.trim();
|
||||
}
|
||||
|
||||
function buildNpcPreview(state, archetype) {
|
||||
const quality = QUALITY_MODS[state.quality] ?? QUALITY_MODS.Standard;
|
||||
const loadoutNames = archetype.loadouts?.[state.loadout] ?? [];
|
||||
const statline = applyQualityToStats(archetype.stats, quality);
|
||||
return {
|
||||
iconPath: getArchetypeIconPath(state.archetype),
|
||||
iconLabel: getArchetypeBadgeShort(state.archetype),
|
||||
badge: getArchetypeBadge(state.archetype),
|
||||
strap: getArchetypeStrapline(state.archetype),
|
||||
identity: `${state.role || archetype.defaultRole} / ${state.species}`,
|
||||
brief: `${state.affiliation || archetype.defaultAffiliation}${state.department ? ` / ${state.department}` : ""} operating in ${state.sector || archetype.defaultSector || "Mort City"}.`,
|
||||
posture: `${state.disposition || "Neutral"} disposition with ${state.threat || "Moderate"} threat expectations.`,
|
||||
statline: [
|
||||
`Combat ${statline.combat}`,
|
||||
`Instinct ${statline.instinct}`,
|
||||
`Speed ${statline.speed}`,
|
||||
`Health ${statline.health}`,
|
||||
`Wounds ${statline.hits}`
|
||||
],
|
||||
loadout: loadoutNames.length ? loadoutNames : ["No preset issue kit selected."],
|
||||
notes: archetype.summary
|
||||
};
|
||||
}
|
||||
|
||||
function clamp98(value) {
|
||||
return Math.max(1, Math.min(98, Number(value ?? 0) || 0));
|
||||
}
|
||||
|
||||
function rankFromBonus(bonus) {
|
||||
if (bonus >= 40) return "Master";
|
||||
if (bonus >= 25) return "Expert";
|
||||
return "Trained";
|
||||
}
|
||||
|
||||
function findWorldItem(name, type = null) {
|
||||
return game.items.find((item) => normalizeText(item.name) === normalizeText(name) && (!type || item.type === type)) ?? null;
|
||||
}
|
||||
|
||||
function toEmbeddedData(document) {
|
||||
return foundry.utils.duplicate(document.toObject());
|
||||
}
|
||||
|
||||
function uniqueEmbeddedItems(items = []) {
|
||||
const seen = new Set();
|
||||
const out = [];
|
||||
for (const item of items) {
|
||||
const key = `${item.type}::${normalizeText(item.name)}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
out.push(item);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value ?? "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function randomFrom(list = []) {
|
||||
if (!list.length) return null;
|
||||
return list[Math.floor(Math.random() * list.length)] ?? null;
|
||||
}
|
||||
|
||||
function getDefaultSpeciesForArchetype(archetypeName, currentSpecies = "Human") {
|
||||
if (archetypeName === "Ebb Operative") return "Ebon";
|
||||
if (archetypeName === "Predator / Beast") return "Advanced Carrien";
|
||||
return currentSpecies || "Human";
|
||||
}
|
||||
|
||||
function getDefaultDispositionForArchetype(archetypeName) {
|
||||
switch (archetypeName) {
|
||||
case "Civilian Witness": return "Nervous";
|
||||
case "Street Ganger": return "Hostile";
|
||||
case "Shiver Patrol": return "Professional";
|
||||
case "Corp Operative": return "Professional";
|
||||
case "Media": return "Corrupt";
|
||||
case "Internal Affairs": return "Professional";
|
||||
case "DarkNight Agent": return "Hostile";
|
||||
case "Tek Specialist": return "Professional";
|
||||
case "Ebb Operative": return "Cornered";
|
||||
case "Predator / Beast": return "Fanatical";
|
||||
default: return "Neutral";
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultThreatForArchetype(archetypeName) {
|
||||
switch (archetypeName) {
|
||||
case "Civilian Witness": return "Low";
|
||||
case "Street Ganger": return "Moderate";
|
||||
case "Shiver Patrol": return "Moderate";
|
||||
case "Corp Operative": return "Moderate";
|
||||
case "Media": return "Low";
|
||||
case "Internal Affairs": return "Moderate";
|
||||
case "DarkNight Agent": return "High";
|
||||
case "Tek Specialist": return "Moderate";
|
||||
case "Ebb Operative": return "High";
|
||||
case "Predator / Beast": return "Extreme";
|
||||
default: return "Moderate";
|
||||
}
|
||||
}
|
||||
|
||||
function getSectorOptionsForArchetype(archetypeName, fallback = "Mort City") {
|
||||
switch (archetypeName) {
|
||||
case "Street Ganger":
|
||||
return ["Cannibal Sectors", "Transit Underlevels", "Waste District", "Mort City"];
|
||||
case "Shiver Patrol":
|
||||
return ["Mort City", "Downtown Mort", "Transit Underlevels", "Cannibal Sectors"];
|
||||
case "Corp Operative":
|
||||
return ["Mort City", "Downtown Mort", "Industrial Edge", "Cannibal Sectors"];
|
||||
case "Media":
|
||||
return ["Downtown Mort", "Mort City", "Cannibal Sectors", "Transit Underlevels"];
|
||||
case "Internal Affairs":
|
||||
return ["Cannibal Sectors", "Mort City", "Downtown Mort", "Industrial Edge"];
|
||||
case "DarkNight Agent":
|
||||
return ["Cannibal Sectors", "Transit Underlevels", "Mort City", "Waste District"];
|
||||
case "Tek Specialist":
|
||||
return ["Industrial Edge", "Mort City", "Transit Underlevels", "Downtown Mort"];
|
||||
case "Ebb Operative":
|
||||
return ["Mort City", "Cannibal Sectors", "Downtown Mort"];
|
||||
case "Predator / Beast":
|
||||
return ["Waste District", "Cannibal Sectors", "Transit Underlevels", "Industrial Edge"];
|
||||
default:
|
||||
return [fallback, "Mort City", "Downtown Mort"];
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function getArchetypeAccentKey(archetypeName) {
|
||||
switch (archetypeName) {
|
||||
case "Shiver Patrol": return "shiver";
|
||||
case "DarkNight Agent": return "darknight";
|
||||
case "Tek Specialist": return "tek";
|
||||
case "Ebb Operative": return "ebb";
|
||||
case "Media": return "media";
|
||||
case "Internal Affairs": return "internal";
|
||||
case "Street Ganger": return "gang";
|
||||
case "Predator / Beast": return "predator";
|
||||
case "Civilian Witness": return "civilian";
|
||||
case "Corp Operative": return "corp";
|
||||
default: return "corp";
|
||||
}
|
||||
}
|
||||
|
||||
function getArchetypeBadge(archetypeName) {
|
||||
switch (archetypeName) {
|
||||
case "Shiver Patrol": return "Shiver Patrol";
|
||||
case "DarkNight Agent": return "DarkNight";
|
||||
case "Tek Specialist": return "Tek Division";
|
||||
case "Ebb Operative": return "Ebb Control";
|
||||
case "Media": return "Media Feed";
|
||||
case "Internal Affairs": return "Internal Affairs";
|
||||
case "Street Ganger": return "Street Threat";
|
||||
case "Predator / Beast": return "Hostile Specimen";
|
||||
case "Civilian Witness": return "Civilian Contact";
|
||||
case "Corp Operative": return "Corporate Asset";
|
||||
default: return archetypeName;
|
||||
}
|
||||
}
|
||||
|
||||
function getArchetypeBadgeShort(archetypeName) {
|
||||
switch (archetypeName) {
|
||||
case "Shiver Patrol": return "SV";
|
||||
case "DarkNight Agent": return "DN";
|
||||
case "Tek Specialist": return "TK";
|
||||
case "Ebb Operative": return "EB";
|
||||
case "Media": return "MD";
|
||||
case "Internal Affairs": return "IA";
|
||||
case "Street Ganger": return "SG";
|
||||
case "Predator / Beast": return "HX";
|
||||
case "Civilian Witness": return "CV";
|
||||
case "Corp Operative": return "CO";
|
||||
default: return "NPC";
|
||||
}
|
||||
}
|
||||
|
||||
function getArchetypeIconPath(archetypeName) {
|
||||
const root = "systems/sla-mothership/icons/ui/npc-badges";
|
||||
switch (archetypeName) {
|
||||
case "Shiver Patrol": return `${root}/shiver.svg`;
|
||||
case "DarkNight Agent": return `${root}/darknight.svg`;
|
||||
case "Tek Specialist": return `${root}/tek.svg`;
|
||||
case "Ebb Operative": return `${root}/ebb.svg`;
|
||||
case "Media": return `${root}/media.svg`;
|
||||
case "Internal Affairs": return `${root}/internal.svg`;
|
||||
case "Street Ganger": return `${root}/gang.svg`;
|
||||
case "Predator / Beast": return `${root}/predator.svg`;
|
||||
case "Civilian Witness": return `${root}/civilian.svg`;
|
||||
case "Corp Operative": return `${root}/corp.svg`;
|
||||
default: return `${root}/corp.svg`;
|
||||
}
|
||||
}
|
||||
|
||||
function getArchetypeStrapline(archetypeName) {
|
||||
switch (archetypeName) {
|
||||
case "Shiver Patrol": return "Precinct issue / patrol doctrine / fast response";
|
||||
case "DarkNight Agent": return "Black operations / pressure asset / covert response";
|
||||
case "Tek Specialist": return "Systems breach / maintenance / machine-facing support";
|
||||
case "Ebb Operative": return "Occult intervention / flux risk / psychic control";
|
||||
case "Media": return "Image control / live coverage / narrative pressure";
|
||||
case "Internal Affairs": return "Audit pressure / disciplinary review / quiet threat";
|
||||
case "Street Ganger": return "Turf violence / local intimidation / disposable muscle";
|
||||
case "Predator / Beast": return "Ferality / pursuit / close-quarters threat";
|
||||
case "Civilian Witness": return "Collateral witness / local knowledge / brittle pressure";
|
||||
case "Corp Operative": return "Corporate recovery / sponsor discipline / field oversight";
|
||||
default: return "Operational contact / generated dossier";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SLAMothershipGenerator } from "./sla-character-generator.js";
|
||||
import { SLANpcGeneratorApp } from "./sla-npc-generator.js";
|
||||
|
||||
const SYSTEM_ID = "sla-mothership";
|
||||
|
||||
@@ -22,6 +23,7 @@ export class SLAWorldToolsApp extends FormApplication {
|
||||
async getData() {
|
||||
const seededItems = game.items.filter((item) => item.flags?.[SYSTEM_ID]?.slaSeed?.key);
|
||||
const actors = game.actors.filter((actor) => actor.type === "character");
|
||||
const creatures = game.actors.filter((actor) => actor.type === "creature");
|
||||
const ebbActors = actors.filter((actor) => ["Ebon", "Brain Waster"].includes(String(actor.system?.sla?.species?.value ?? "").trim()));
|
||||
|
||||
return {
|
||||
@@ -30,6 +32,7 @@ export class SLAWorldToolsApp extends FormApplication {
|
||||
weaponCount: seededItems.filter((item) => item.type === "weapon").length,
|
||||
gearCount: seededItems.filter((item) => item.flags?.[SYSTEM_ID]?.slaSeed?.kind === "gear").length,
|
||||
actorCount: actors.length,
|
||||
creatureCount: creatures.length,
|
||||
starterCount: actors.filter((actor) => actor.flags?.[SYSTEM_ID]?.slaStarter).length,
|
||||
ebbActorCount: ebbActors.length,
|
||||
quickBattleMapBuilderReady: Boolean(game.modules?.get("quick-battlemap-builder")?.active && game.quickBattleMapBuilder?.open)
|
||||
@@ -53,6 +56,10 @@ export class SLAWorldToolsApp extends FormApplication {
|
||||
game.slaMothership.openGenerator();
|
||||
});
|
||||
|
||||
html.find(".open-npc-desk").on("click", () => {
|
||||
game.slaMothership.openNpcDesk();
|
||||
});
|
||||
|
||||
html.find(".generate-random").on("click", async () => {
|
||||
await game.slaMothership.createRandomOperative({ openSheet: true });
|
||||
this.render(false);
|
||||
@@ -127,7 +134,14 @@ export function installSlaSidebarButtons(app, html) {
|
||||
await game.slaMothership.seedWorldContent({ overwrite: true, notify: true });
|
||||
});
|
||||
|
||||
toolContainer.append(randomButton, createButton, toolsButton, refreshButton);
|
||||
const npcButton = $(`
|
||||
<button type="button" class="sla-open-npc-desk">
|
||||
<i class="fas fa-id-badge"></i> NPC Desk
|
||||
</button>
|
||||
`);
|
||||
npcButton.on("click", () => SLANpcGeneratorApp.open());
|
||||
|
||||
toolContainer.append(randomButton, createButton, npcButton, toolsButton, refreshButton);
|
||||
}
|
||||
|
||||
function findToolContainer(root) {
|
||||
|
||||
@@ -1 +1 @@
|
||||
MANIFEST-000039
|
||||
MANIFEST-000085
|
||||
|
||||
@@ -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/05/19-23:19:40.611150 173b87000 Recovering log #84
|
||||
2026/05/19-23:19:40.611608 173b87000 Delete type=3 #83
|
||||
2026/05/19-23:19:40.611656 173b87000 Delete type=0 #84
|
||||
|
||||
@@ -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/16-22:16:27.606022 16eb7b000 Recovering log #82
|
||||
2026/04/16-22:16:27.612707 16eb7b000 Delete type=3 #81
|
||||
2026/04/16-22:16:27.612921 16eb7b000 Delete type=0 #82
|
||||
|
||||
@@ -1 +1 @@
|
||||
MANIFEST-000039
|
||||
MANIFEST-000085
|
||||
|
||||
@@ -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/05/19-23:19:40.613309 172363000 Recovering log #84
|
||||
2026/05/19-23:19:40.613576 172363000 Delete type=3 #83
|
||||
2026/05/19-23:19:40.613610 172363000 Delete type=0 #84
|
||||
|
||||
@@ -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/16-22:16:27.615553 16f387000 Recovering log #82
|
||||
2026/04/16-22:16:27.616188 16f387000 Delete type=3 #81
|
||||
2026/04/16-22:16:27.616243 16f387000 Delete type=0 #82
|
||||
|
||||
@@ -1 +1 @@
|
||||
MANIFEST-000039
|
||||
MANIFEST-000085
|
||||
|
||||
@@ -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/05/19-23:19:40.614698 172b6f000 Recovering log #84
|
||||
2026/05/19-23:19:40.614916 172b6f000 Delete type=3 #83
|
||||
2026/05/19-23:19:40.614942 172b6f000 Delete type=0 #84
|
||||
|
||||
@@ -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/16-22:16:27.618035 16fb93000 Recovering log #82
|
||||
2026/04/16-22:16:27.618455 16fb93000 Delete type=3 #81
|
||||
2026/04/16-22:16:27.618488 16fb93000 Delete type=0 #82
|
||||
|
||||
@@ -1 +1 @@
|
||||
MANIFEST-000039
|
||||
MANIFEST-000085
|
||||
|
||||
@@ -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/05/19-23:19:40.615928 17337b000 Recovering log #84
|
||||
2026/05/19-23:19:40.616184 17337b000 Delete type=3 #83
|
||||
2026/05/19-23:19:40.616214 17337b000 Delete type=0 #84
|
||||
|
||||
@@ -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/16-22:16:27.619986 16e36f000 Recovering log #82
|
||||
2026/04/16-22:16:27.620339 16e36f000 Delete type=3 #81
|
||||
2026/04/16-22:16:27.620370 16e36f000 Delete type=0 #82
|
||||
|
||||
@@ -1 +1 @@
|
||||
MANIFEST-000039
|
||||
MANIFEST-000085
|
||||
|
||||
@@ -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/05/19-23:19:40.617145 17337b000 Recovering log #84
|
||||
2026/05/19-23:19:40.617442 17337b000 Delete type=3 #83
|
||||
2026/05/19-23:19:40.617486 17337b000 Delete type=0 #84
|
||||
|
||||
@@ -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/16-22:16:27.621457 16e36f000 Recovering log #82
|
||||
2026/04/16-22:16:27.621716 16e36f000 Delete type=3 #81
|
||||
2026/04/16-22:16:27.621743 16e36f000 Delete type=0 #82
|
||||
|
||||
@@ -1 +1 @@
|
||||
MANIFEST-000039
|
||||
MANIFEST-000085
|
||||
|
||||
@@ -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/05/19-23:19:40.618390 17337b000 Recovering log #84
|
||||
2026/05/19-23:19:40.618663 17337b000 Delete type=3 #83
|
||||
2026/05/19-23:19:40.618694 17337b000 Delete type=0 #84
|
||||
|
||||
@@ -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/16-22:16:27.622897 16e36f000 Recovering log #82
|
||||
2026/04/16-22:16:27.623174 16e36f000 Delete type=3 #81
|
||||
2026/04/16-22:16:27.623240 16e36f000 Delete type=0 #82
|
||||
|
||||
@@ -1 +1 @@
|
||||
MANIFEST-000039
|
||||
MANIFEST-000087
|
||||
|
||||
@@ -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/05/19-23:19:40.619688 17337b000 Recovering log #86
|
||||
2026/05/19-23:19:40.620003 17337b000 Delete type=3 #85
|
||||
2026/05/19-23:19:40.620026 17337b000 Delete type=0 #86
|
||||
|
||||
@@ -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/16-22:16:27.624570 16e36f000 Recovering log #84
|
||||
2026/04/16-22:16:27.625146 16e36f000 Delete type=3 #83
|
||||
2026/04/16-22:16:27.625212 16e36f000 Delete type=0 #84
|
||||
|
||||
@@ -1 +1 @@
|
||||
MANIFEST-000039
|
||||
MANIFEST-000085
|
||||
|
||||
@@ -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/05/19-23:19:40.621197 17337b000 Recovering log #84
|
||||
2026/05/19-23:19:40.621497 17337b000 Delete type=3 #83
|
||||
2026/05/19-23:19:40.621534 17337b000 Delete type=0 #84
|
||||
|
||||
@@ -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/16-22:16:27.626995 16e36f000 Recovering log #82
|
||||
2026/04/16-22:16:27.627395 16e36f000 Delete type=3 #81
|
||||
2026/04/16-22:16:27.627527 16e36f000 Delete type=0 #82
|
||||
|
||||
@@ -1 +1 @@
|
||||
MANIFEST-000039
|
||||
MANIFEST-000085
|
||||
|
||||
@@ -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/05/19-23:19:40.622748 173b87000 Recovering log #84
|
||||
2026/05/19-23:19:40.622969 173b87000 Delete type=3 #83
|
||||
2026/05/19-23:19:40.622990 173b87000 Delete type=0 #84
|
||||
|
||||
@@ -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/16-22:16:27.629071 16fb93000 Recovering log #82
|
||||
2026/04/16-22:16:27.629605 16fb93000 Delete type=3 #81
|
||||
2026/04/16-22:16:27.629821 16fb93000 Delete type=0 #82
|
||||
|
||||
@@ -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.6.0",
|
||||
"version": "0.6.9",
|
||||
"esmodules": [
|
||||
"module/mosh.js"
|
||||
],
|
||||
@@ -160,7 +160,7 @@
|
||||
},
|
||||
"compatibility": {
|
||||
"minimum": "13",
|
||||
"maximum": "13",
|
||||
"verified": "13"
|
||||
"verified": "14",
|
||||
"maximum": "14"
|
||||
}
|
||||
}
|
||||
@@ -227,6 +227,11 @@
|
||||
},
|
||||
"morphNotes": {
|
||||
"value": ""
|
||||
},
|
||||
"sessionAbilityUsed": false,
|
||||
"contacts": [],
|
||||
"activityLog": {
|
||||
"value": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -241,6 +246,38 @@
|
||||
"value": 1,
|
||||
"html": ""
|
||||
},
|
||||
"slaNpc": {
|
||||
"archetype": {
|
||||
"value": ""
|
||||
},
|
||||
"species": {
|
||||
"value": ""
|
||||
},
|
||||
"affiliationType": {
|
||||
"value": ""
|
||||
},
|
||||
"affiliation": {
|
||||
"value": ""
|
||||
},
|
||||
"department": {
|
||||
"value": ""
|
||||
},
|
||||
"role": {
|
||||
"value": ""
|
||||
},
|
||||
"sector": {
|
||||
"value": ""
|
||||
},
|
||||
"disposition": {
|
||||
"value": ""
|
||||
},
|
||||
"threat": {
|
||||
"value": ""
|
||||
},
|
||||
"summary": {
|
||||
"value": ""
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"combat": {
|
||||
"value": 10,
|
||||
@@ -712,4 +749,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,26 +17,19 @@
|
||||
|
||||
<div class="headergrid">
|
||||
<div class="sla-sheet-wordmark">SLA Industries</div>
|
||||
<!-- <img class="profile-img" src="{{actor.img}}" data-edit="img" title="{{actor.name}}" height="150" width="150"/> -->
|
||||
|
||||
{{!-- REST button — top-right, above Race --}}
|
||||
<div class="header-field header-field-rest">
|
||||
<button type="button" class="sla-sheet-action sla-rest-button sla-header-rest-pill" title="Take a short or long rest">REST</button>
|
||||
</div>
|
||||
|
||||
{{!-- ROW 2: Name / Class / Race --}}
|
||||
<!-- PLAYER NAME -->
|
||||
<div class="headernamegrid header-field header-field-name">
|
||||
<div class="headerinputtext">{{ localize 'Mosh.Name' }}</div>
|
||||
<div class="headerinputfield sla-header-input charname"><input name="name" class="noborder" type="text"
|
||||
value="{{name}}" placeholder="{{ localize 'Mosh.Name' }}" /></div>
|
||||
</div>
|
||||
<div class="header-field header-field-lad">
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-field header-field-credits">
|
||||
<div class="headerinputtext">{{ localize 'Mosh.Credits' }}</div>
|
||||
<div class="headerinputfield sla-header-input charname"><input name="system.credits.value" class="noborder" type="text"
|
||||
value="{{system.credits.value}}" placeholder="" /></div>
|
||||
</div>
|
||||
|
||||
<!-- CLASS -->
|
||||
<div class="header-field header-field-class">
|
||||
@@ -45,30 +38,43 @@
|
||||
value="{{system.class.value}}" placeholder="" /></div>
|
||||
</div>
|
||||
|
||||
<!-- RANK -->
|
||||
<!-- RACE -->
|
||||
<div class="header-field header-field-race">
|
||||
<div class="headerinputtext">Race</div>
|
||||
<div class="headerinputfield sla-header-input charname"><input name="system.rank.value" class="noborder" type="text"
|
||||
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="headerinputfield sla-header-input charname"><input name="system.attributes.level.value" class="noborder"
|
||||
type="text" value="{{system.attributes.level.value}}" placeholder="" /></div>
|
||||
{{!-- ROW 3: Bottom row — all equal width, same font --}}
|
||||
<div class="sla-header-bottom-row">
|
||||
<div class="header-field header-field-lad">
|
||||
<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>L.A.D.</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-field header-field-credits">
|
||||
<div class="headerinputtext">{{ localize 'Mosh.Credits' }}</div>
|
||||
<div class="headerinputfield sla-header-input charname"><input name="system.credits.value" class="noborder" type="text"
|
||||
value="{{system.credits.value}}" placeholder="" /></div>
|
||||
</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">
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<div class="header-field header-field-height header-field-compact">
|
||||
<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="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>
|
||||
</div>
|
||||
|
||||
@@ -202,8 +208,15 @@
|
||||
</div>
|
||||
{{/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-panel" style="margin-left: 6px; margin-right: 0px; flex: 0; grid-column-start: 1; grid-column-end: 3;">
|
||||
<div class="resource-label sla-stat-heading sla-trauma-heading">{{ localize 'Mosh.TraumaResponse' }}</div>
|
||||
<div class="sla-trauma-controls">
|
||||
<label class="sla-trauma-toggle" title="Mark the trauma response as used this session">
|
||||
<input type="checkbox" class="sla-session-ability-used" {{checked system.sla.sessionAbilityUsed}} />
|
||||
<span>Used This Session</span>
|
||||
</label>
|
||||
<button type="button" class="sla-inline-mini-button sla-new-session" title="Reset trauma response use for a new session">New Session</button>
|
||||
</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>
|
||||
@@ -305,6 +318,14 @@
|
||||
<div class="sla-mainstat-action-strip">
|
||||
<button type="button" class="sla-sheet-action sla-wound-button">Wound</button>
|
||||
</div>
|
||||
{{#if conditions.length}}
|
||||
<div class="sla-active-conditions-summary">
|
||||
<span class="sla-conditions-label">CONDITIONS:</span>
|
||||
{{#each conditions as |condition|}}
|
||||
<span class="sla-active-condition">{{condition.name}}{{#if condition.system.severity}} {{condition.system.severity}}{{/if}}</span>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<!-- SAVES -->
|
||||
{{#if system.settings.firstEdition}}
|
||||
@@ -377,11 +398,12 @@
|
||||
<a class="tab-select" data-tab="skills">{{ localize 'Mosh.Skills' }}</a>
|
||||
<a class="tab-select" data-tab="weapons">{{ localize 'Mosh.Weapons' }}</a>
|
||||
<a class="tab-select" data-tab="items">{{ localize 'Mosh.Items' }}</a>
|
||||
<a class="tab-select" data-tab="drugs">Drugs</a>
|
||||
<a class="tab-select" data-tab="sla">SLA</a>
|
||||
<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}}
|
||||
{{#if isEbbUser}}<a class="tab-select" data-tab="ebb">Ebb</a>{{/if}}
|
||||
<a class="tab-select" data-tab="description">{{ localize 'Mosh.Bio' }}</a>
|
||||
<a class="tab-select" data-tab="notes">{{ localize 'Mosh.Notes' }}</a>
|
||||
</nav>
|
||||
@@ -396,8 +418,13 @@
|
||||
{{!-- Notes Tab --}}
|
||||
<div class="tab biography" data-group="primary" data-tab="notes">
|
||||
<div class="sla-card" style="margin-bottom: 12px;">
|
||||
<div class="sla-card-label">Ammo Spend Ledger</div>
|
||||
<div class="sla-card-copy">Total spent: {{system.sla.ammoSpendTotal.value}} cR</div>
|
||||
<div class="sla-card-label">Operation Log</div>
|
||||
<div class="sla-card-copy">Recent rolls, outcomes, and field actions for this operative.</div>
|
||||
<textarea readonly rows="10" class="textarea-input">{{system.sla.activityLog.value}}</textarea>
|
||||
</div>
|
||||
<div class="sla-card" style="margin-bottom: 12px;">
|
||||
<div class="sla-card-label">Ammo Economy Ledger</div>
|
||||
<div class="sla-card-copy">Firing spend total: {{system.sla.ammoSpendTotal.value}} cR. Purchases are logged below as separate entries.</div>
|
||||
<textarea readonly rows="6" class="textarea-input">{{system.sla.ammoLedger.value}}</textarea>
|
||||
</div>
|
||||
{{editor enriched.notes target="system.notes" button=true editable=true}}
|
||||
@@ -413,7 +440,6 @@
|
||||
<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>
|
||||
|
||||
@@ -710,8 +736,76 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{#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 {{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">
|
||||
{{!-- Instability Track --}}
|
||||
<div class="sla-card">
|
||||
<div class="sla-card-label">Instability Track</div>
|
||||
<div class="sla-morph-track-bar">
|
||||
<div class="sla-morph-track-fill {{vevaphonInstabilityClass}}" style="width: {{vevaphonInstabilityPercent}}%;"></div>
|
||||
<span class="sla-morph-track-label">{{data.sla.vevaphonInstability.value}} / 12</span>
|
||||
</div>
|
||||
<div class="sla-card-copy">{{vevaphonInstabilityDesc}}</div>
|
||||
<div class="sla-action-row" style="margin-top: 10px;">
|
||||
<button type="button" class="sla-instability-increase sla-morph-btn-instab">+1 Instability</button>
|
||||
<button type="button" class="sla-instability-decrease sla-morph-btn-instab sla-morph-btn-instab-down">−1 Instability</button>
|
||||
</div>
|
||||
<div class="sla-card-copy" style="margin-top: 12px; font-style: italic;">At 12, the Vevaphon is lost — immediate retirement or death.</div>
|
||||
</div>
|
||||
|
||||
{{!-- Active Morph Form --}}
|
||||
<div class="sla-card">
|
||||
<div class="sla-card-label">Active Morph Form</div>
|
||||
<div class="sla-card-copy" style="margin-bottom: 10px;">Each morph shift costs 1 Instability. Changing form mid-scene requires a Sanity save at Instability 6+.</div>
|
||||
{{#each slaMorphForms as |form|}}
|
||||
<button type="button"
|
||||
class="sla-morph-shift sla-morph-form-btn {{#if form.slaIsActiveMorphForm}}sla-morph-active{{/if}}"
|
||||
data-form="{{form.name}}">
|
||||
<strong class="sla-morph-form-name">{{form.name}}</strong>
|
||||
{{#if form.system.summary}}<div class="sla-morph-form-summary">{{form.system.summary}}</div>{{/if}}
|
||||
</button>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-2col sla-card-grid" style="margin-top: 12px;">
|
||||
{{!-- Morph Panic Reference --}}
|
||||
<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" style="margin-top: 8px;">
|
||||
{{#each vevaphonMorphPanic as |entry|}}
|
||||
<div class="sla-note-pill sla-note-pill-wide">{{entry.roll}}. {{entry.label}} — {{entry.desc}}</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Morph Notes --}}
|
||||
<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}}
|
||||
{{#if isEbbUser}}
|
||||
<div class="tab items" data-group="primary" data-tab="ebb">
|
||||
<div class="sla-panel">
|
||||
@@ -816,112 +910,19 @@
|
||||
</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}}
|
||||
<div class="sla-drug-alerts">
|
||||
{{#each drugAlerts as |alert id|}}
|
||||
<div class="sla-drug-alert {{alert.state}}">
|
||||
<div class="sla-drug-alert-main">
|
||||
<div class="sla-drug-alert-name">{{alert.name}}</div>
|
||||
<div class="sla-drug-alert-meta">
|
||||
<span class="sla-drug-alert-state">{{alert.stateLabel}}</span>
|
||||
{{#if alert.duration}}<span class="sla-drug-alert-duration">{{alert.duration}}</span>{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="sla-drug-alert-summary">{{alert.summary}}</div>
|
||||
<button type="button" class="drug-close-btn drug-close" data-item-id="{{alert.itemId}}" data-drug-id="{{alert.drugId}}">Close</button>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if system.settings.firstEdition}}
|
||||
<!-- ARMOR -->
|
||||
<ol class="items-list" role="table">
|
||||
<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" 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="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="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>
|
||||
@@ -958,10 +959,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" 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="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="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>
|
||||
@@ -993,57 +994,15 @@
|
||||
</ol>
|
||||
{{/if}}
|
||||
|
||||
<!-- DRUGS -->
|
||||
<ol class="items-list sla-drug-list">
|
||||
<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" 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>
|
||||
</li>
|
||||
{{#each drugRows as |drug id|}}
|
||||
<li class="item flexrow dropitem" data-item-id="{{drug.itemId}}" data-drug-id="{{drug.drugId}}">
|
||||
<div class="item-image"><img src="{{drug.img}}" title="{{drug.name}}" width="24" height="24" /></div>
|
||||
<div class="skill-name list-roll description-roll drug-item-summary" style="flex-grow: 1.6;">{{drug.name}}<span>{{drug.summary}}</span></div>
|
||||
<div class="skill-stat item-quantity">{{drug.quantity}}</div>
|
||||
<div class="skill-stat">{{drug.rating}}</div>
|
||||
<div class="skill-stat">{{drug.cost}}</div>
|
||||
<div class="skill-stat">{{drug.slaCarry.totalWeight}} kg</div>
|
||||
<div class="skill-stat">
|
||||
<div class="sla-carry-toggle-row">
|
||||
<button type="button" class="sla-carry-toggle {{#if (compare 'str' drug.slaCarry.state '===' 'equipped')}}active{{/if}}" data-state="equipped">Eqp</button>
|
||||
<button type="button" class="sla-carry-toggle {{#if (compare 'str' drug.slaCarry.state '===' 'stowed')}}active{{/if}}" data-state="stowed">Stow</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skill-stat">{{drug.statusLabel}}</div>
|
||||
<div class="skill-stat">{{drug.duration}}</div>
|
||||
<div class="item-controls">
|
||||
<button type="button" class="drug-use-btn drug-use" {{#unless drug.canUse}}disabled{{/unless}}>Use</button>
|
||||
<button type="button" class="drug-close-btn drug-close" {{#unless drug.canClose}}disabled{{/unless}}>Close</button>
|
||||
<a class="item-control item-edit" title="Edit Drug"><i class="fas fa-edit"></i></a>
|
||||
<a class="item-control item-delete" title="Delete Drug"><i class="fas fa-trash"></i></a>
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
|
||||
<!-- ITEMS -->
|
||||
<ol class="items-list">
|
||||
<li class="item flexrow item-header">
|
||||
<div class="item-image"></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="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="item-controls">
|
||||
<a class="item-control item-create" title="{{ localize 'Mosh.CreateItem' }}" data-type="item"><i class="fas fa-plus"></i> Add</a>
|
||||
</div>
|
||||
@@ -1075,6 +1034,66 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="tab items" data-group="primary" data-tab="drugs">
|
||||
{{#if drugAlerts.length}}
|
||||
<div class="sla-drug-alerts">
|
||||
{{#each drugAlerts as |alert id|}}
|
||||
<div class="sla-drug-alert {{alert.state}}">
|
||||
<div class="sla-drug-alert-main">
|
||||
<div class="sla-drug-alert-name">{{alert.name}}</div>
|
||||
<div class="sla-drug-alert-meta">
|
||||
<span class="sla-drug-alert-state">{{alert.stateLabel}}</span>
|
||||
{{#if alert.duration}}<span class="sla-drug-alert-duration">{{alert.duration}}</span>{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="sla-drug-alert-summary">{{alert.summary}}</div>
|
||||
<button type="button" class="drug-close-btn drug-close" data-item-id="{{alert.itemId}}" data-drug-id="{{alert.drugId}}">Close</button>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<ol class="items-list sla-drug-list">
|
||||
<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="item-controls">
|
||||
<a class="item-control item-create" title="Create Drug" data-type="drug"><i class="fas fa-plus"></i> Add</a>
|
||||
</div>
|
||||
</li>
|
||||
{{#each drugRows as |drug id|}}
|
||||
<li class="item flexrow dropitem" data-item-id="{{drug.itemId}}" data-drug-id="{{drug.drugId}}">
|
||||
<div class="item-image"><img src="{{drug.img}}" title="{{drug.name}}" width="24" height="24" /></div>
|
||||
<div class="skill-name list-roll description-roll drug-item-summary" style="flex-grow: 1.6;">{{drug.name}}<span>{{drug.summary}}</span></div>
|
||||
<div class="skill-stat item-quantity">{{drug.quantity}}</div>
|
||||
<div class="skill-stat">{{drug.rating}}</div>
|
||||
<div class="skill-stat">{{drug.cost}}</div>
|
||||
<div class="skill-stat">{{drug.slaCarry.totalWeight}} kg</div>
|
||||
<div class="skill-stat">
|
||||
<div class="sla-carry-toggle-row">
|
||||
<button type="button" class="sla-carry-toggle {{#if (compare 'str' drug.slaCarry.state '===' 'equipped')}}active{{/if}}" data-state="equipped">Eqp</button>
|
||||
<button type="button" class="sla-carry-toggle {{#if (compare 'str' drug.slaCarry.state '===' 'stowed')}}active{{/if}}" data-state="stowed">Stow</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skill-stat">{{drug.statusLabel}}</div>
|
||||
<div class="skill-stat">{{drug.duration}}</div>
|
||||
<div class="item-controls">
|
||||
<button type="button" class="drug-use-btn drug-use" {{#unless drug.canUse}}disabled{{/unless}}>Use</button>
|
||||
<button type="button" class="drug-close-btn drug-close" {{#unless drug.canClose}}disabled{{/unless}}>Close</button>
|
||||
<a class="item-control item-edit" title="Edit Drug"><i class="fas fa-edit"></i></a>
|
||||
<a class="item-control item-delete" title="Delete Drug"><i class="fas fa-trash"></i></a>
|
||||
</div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
{{!-- Owned Skills Tab --}}
|
||||
<div class="tab items" data-group="primary" data-tab="skills">
|
||||
<div class="sla-card" style="margin-bottom: 12px;">
|
||||
@@ -1085,10 +1104,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;" 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="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="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>
|
||||
@@ -1147,7 +1166,7 @@
|
||||
<li class="item flexrow dropitem" data-item-id="{{condition._id}}">
|
||||
<div class="item-image"><img src="{{condition.img}}" title="{{condition.name}}" width="24" height="24"/></div>
|
||||
<div class="skill-name description-roll list-roll">{{condition.name}}</div>
|
||||
<div class="skill-stat list-roll severity">{{condition.system.severity}}</div>
|
||||
<div class="skill-stat list-roll severity" data-severity="{{condition.system.severity}}">{{condition.system.severity}}</div>
|
||||
<div class="skill-stat">
|
||||
{{#if condition.system.treatment.html}}
|
||||
<div class="treatment-button list-roll flex" style="margin: 0; position: relative; top: 50%; -ms-transform: translateY(-50%); transform: translateY(-50%);" data-key="condition" data-label="condition" data-max="3" data-min="0">
|
||||
@@ -1175,7 +1194,7 @@
|
||||
<div class="tab items" data-group="primary" data-tab="weapons">
|
||||
<div class="sla-card" style="margin-bottom: 12px;">
|
||||
<div class="sla-card-label">Weapons Readiness</div>
|
||||
<div class="sla-card-copy">Ammo stacks: {{slaAmmoInventory.stackCount}} | Total loose rounds: {{slaAmmoInventory.roundCount}} | Running ammo spend: {{system.sla.ammoSpendTotal.value}} cR</div>
|
||||
<div class="sla-card-copy">Ammo stacks: {{slaAmmoInventory.stackCount}} | Total loose rounds: {{slaAmmoInventory.roundCount}} | Running firing spend: {{system.sla.ammoSpendTotal.value}} cR</div>
|
||||
<div class="sla-card-copy">Carry: {{slaCarry.current}} / {{slaCarry.capacity}} kg | {{slaCarry.state}}</div>
|
||||
<div class="sla-action-row" style="margin-top: 8px;">
|
||||
<button type="button" class="sla-sheet-action weapon-reload-all">Reload All</button>
|
||||
@@ -1185,13 +1204,13 @@
|
||||
<ol class="items-list">
|
||||
<li class="item flexrow item-header">
|
||||
<div class="item-image"></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="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="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>
|
||||
|
||||
@@ -97,6 +97,46 @@
|
||||
</div>
|
||||
<div class="whiteline"></div>
|
||||
</header>
|
||||
<section class="sla-creature-dossier">
|
||||
<div class="sla-creature-dossier-grid">
|
||||
<div class="resource">
|
||||
<label class="resource-label">Role</label>
|
||||
<input type="text" name="system.slaNpc.role.value" value="{{system.slaNpc.role.value}}" />
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Archetype</label>
|
||||
<input type="text" name="system.slaNpc.archetype.value" value="{{system.slaNpc.archetype.value}}" />
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Species</label>
|
||||
<input type="text" name="system.slaNpc.species.value" value="{{system.slaNpc.species.value}}" />
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Affiliation</label>
|
||||
<input type="text" name="system.slaNpc.affiliation.value" value="{{system.slaNpc.affiliation.value}}" />
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Department / Cell</label>
|
||||
<input type="text" name="system.slaNpc.department.value" value="{{system.slaNpc.department.value}}" />
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Sector</label>
|
||||
<input type="text" name="system.slaNpc.sector.value" value="{{system.slaNpc.sector.value}}" />
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Disposition</label>
|
||||
<input type="text" name="system.slaNpc.disposition.value" value="{{system.slaNpc.disposition.value}}" />
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Threat</label>
|
||||
<input type="text" name="system.slaNpc.threat.value" value="{{system.slaNpc.threat.value}}" />
|
||||
</div>
|
||||
<div class="resource sla-creature-dossier-wide">
|
||||
<label class="resource-label">Snapshot</label>
|
||||
<input type="text" name="system.slaNpc.summary.value" value="{{system.slaNpc.summary.value}}" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="creature-description-grid">
|
||||
<div class="creaturedescription">
|
||||
<div class="grid grid-3col">
|
||||
@@ -391,7 +431,7 @@
|
||||
<li class="item flexrow dropitem" data-item-id="{{condition._id}}">
|
||||
<div class="item-image"><img src="{{condition.img}}" title="{{condition.name}}" width="24" height="24"/></div>
|
||||
<div class="skill-name description-roll list-roll">{{condition.name}}</div>
|
||||
<div class="skill-stat list-roll severity">{{condition.system.severity}}</div>
|
||||
<div class="skill-stat list-roll severity" data-severity="{{condition.system.severity}}">{{condition.system.severity}}</div>
|
||||
<div class="skill-stat">
|
||||
{{#if condition.system.treatment.html}}
|
||||
<div class="treatment-button list-roll flex" style="margin: 0; position: relative; top: 50%; -ms-transform: translateY(-50%); transform: translateY(-50%);" data-key="condition" data-label="condition" data-max="3" data-min="0">
|
||||
|
||||
@@ -1,88 +1,97 @@
|
||||
<div class="mosh" {{#if actor._id}}data-actor-id="{{actor._id}}" {{/if}}
|
||||
<div class="mosh sla-chat-shell sla-chat-shell-item" {{#if actor._id}}data-actor-id="{{actor._id}}" {{/if}}
|
||||
{{#if item._id}}data-item-id="{{item._id}}" {{/if}} {{#if tokenId}}data-token-id="{{tokenId}}" {{/if}}>
|
||||
<div class="showinfo"></div>
|
||||
<div class="showlessinfo"></div>
|
||||
<div class="rollcontainer">
|
||||
<div class="flexrow">
|
||||
{{#if swapNameDesc}}
|
||||
<div class="rollweaponh1" >{{swapName}}</div>
|
||||
{{else}}
|
||||
{{#if (compare 'int' item.name.length '>=' 22)}}
|
||||
<div class="rollweaponh1" style="font-size: 0.75rem; padding-top: 7px;">{{item.name}}</div>
|
||||
{{else}}
|
||||
<div class="rollweaponh1" >{{item.name}}</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if (compare 'str' item.type '===' 'ability')}}
|
||||
{{#if (compare 'str' actor.img '===' 'icons/svg/mystery-man.svg')}}
|
||||
<div style="text-align: right"><img class="roll-image" src="systems/sla-mothership/images/icons/ui/attributes/instinct.png" /></div>
|
||||
{{else}}
|
||||
<div style="text-align: right"><img class="roll-image" src="{{actor.img}}" /></div>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div style="text-align: right"><img class="roll-image" src="{{item.img}}" /></div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="sla-chat-hero">
|
||||
<div class="sla-chat-hero-main">
|
||||
<div class="sla-chat-kicker-row">
|
||||
<div class="sla-chat-kicker">Item Detail</div>
|
||||
<div class="sla-chat-meta-row">
|
||||
{{#if (compare 'str' item.system.sla.impact '===' 'heal')}}
|
||||
<span class="sla-chat-type-badge sla-chat-type-heal">HLG</span>
|
||||
{{else if item.system.roll}}
|
||||
<span class="sla-chat-type-badge sla-chat-type-attack">ATK</span>
|
||||
{{else if (compare 'str' item.type '===' 'ability')}}
|
||||
<span class="sla-chat-type-badge sla-chat-type-ebb">ABL</span>
|
||||
{{else}}
|
||||
<span class="sla-chat-type-badge sla-chat-type-item">ITM</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{#if swapNameDesc}}
|
||||
<div class="rollweaponh1 sla-chat-hero-title">{{swapName}}</div>
|
||||
{{else}}
|
||||
{{#if (compare 'int' item.name.length '>=' 22)}}
|
||||
<div class="rollweaponh1 sla-chat-hero-title sla-chat-hero-title-tight">{{item.name}}</div>
|
||||
{{else}}
|
||||
<div class="rollweaponh1 sla-chat-hero-title">{{item.name}}</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="sla-chat-hero-media">
|
||||
{{#if (compare 'str' item.type '===' 'ability')}}
|
||||
{{#if (compare 'str' actor.img '===' 'icons/svg/mystery-man.svg')}}
|
||||
<img class="roll-image" src="systems/sla-mothership/images/icons/ui/attributes/instinct.png" />
|
||||
{{else}}
|
||||
<img class="roll-image" src="{{actor.img}}" />
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<img class="roll-image" src="{{item.img}}" />
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if item.system.roll}}
|
||||
<div class="description">
|
||||
<div class="description sla-chat-section {{#if (compare 'str' item.system.sla.impact '===' 'heal')}}sla-chat-section-heal{{else}}sla-chat-section-damage{{/if}}">
|
||||
{{#if (compare 'str' item.system.sla.impact '===' 'heal')}}
|
||||
<div class="body">You restore <strong>[[{{item.system.roll}}[damage]]]</strong> points of healing.</div>
|
||||
{{else}}
|
||||
<div class="body">You inflict <strong>[[{{item.system.roll}}[damage]]]</strong> points of damage.</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div style="margin-bottom : 10px;"></div>
|
||||
<div style="border-bottom: 1px solid #999; height: 0; width: 100%;"></div>
|
||||
<div style="margin-bottom : 5px;"></div>
|
||||
{{/if}}
|
||||
|
||||
{{#if item.system.severity}}
|
||||
<div class="description">
|
||||
<div class="body"><strong>{{ localize 'Mosh.Severity' }}: </strong>{{item.system.severity}}</div>
|
||||
<div class="description sla-chat-section {{severityMeta.cssClass}}" data-severity="{{item.system.severity}}">
|
||||
<div class="sla-chat-section-heading">
|
||||
<span class="sla-chat-severity-pill {{severityMeta.cssClass}}">{{severityMeta.label}}</span>
|
||||
</div>
|
||||
<div class="body"><strong>{{ localize 'Mosh.Severity' }}:</strong> {{item.system.severity}}</div>
|
||||
</div>
|
||||
<div style="margin-bottom : 5px;"></div>
|
||||
<div style="border-bottom: 1px solid #999; height: 0; width: 100%;"></div>
|
||||
<div style="margin-bottom : 5px;"></div>
|
||||
{{/if}}
|
||||
|
||||
{{#if item.system.armorPoints}}
|
||||
<div class="description">
|
||||
<div class="body"><strong>{{ localize 'Mosh.ArmorPoints' }}: </strong>{{item.system.armorPoints}}</div>
|
||||
<div class="description sla-chat-section">
|
||||
<div class="body"><strong>{{ localize 'Mosh.ArmorPoints' }}:</strong> {{item.system.armorPoints}}</div>
|
||||
</div>
|
||||
<div style="margin-bottom : 5px;"></div>
|
||||
<div style="border-bottom: 1px solid #999; height: 0; width: 100%;"></div>
|
||||
<div style="margin-bottom : 5px;"></div>
|
||||
{{/if}}
|
||||
|
||||
{{#if item.system.damageReduction}}
|
||||
<div class="description">
|
||||
<div class="body"><strong>{{ localize 'Mosh.DMGReduction' }}: </strong>{{item.system.damageReduction}}</div>
|
||||
<div class="description sla-chat-section">
|
||||
<div class="body"><strong>{{ localize 'Mosh.DMGReduction' }}:</strong> {{item.system.damageReduction}}</div>
|
||||
</div>
|
||||
<div style="margin-bottom : 5px;"></div>
|
||||
<div style="border-bottom: 1px solid #999; height: 0; width: 100%;"></div>
|
||||
<div style="margin-bottom : 5px;"></div>
|
||||
{{/if}}
|
||||
|
||||
{{#if swapNameDesc}}
|
||||
<div class="description">
|
||||
<div class="description sla-chat-section">
|
||||
<div class="body">{{{item.name}}}</div>
|
||||
</div>
|
||||
<div style="margin-bottom : 16px;"></div>
|
||||
{{else if (compare 'str' item.system.type '===' 'repair')}}
|
||||
<div class="description">
|
||||
<div class="description sla-chat-section">
|
||||
{{#if item.system.major}}
|
||||
<div class="body"><strong>{{ localize 'Mosh.MajorRepair' }}</strong></div>
|
||||
{{else}}
|
||||
<div class="body"><strong>{{ localize 'Mosh.MinorRepair' }}</strong></div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div style="margin-bottom : 5px;"></div>
|
||||
<div style="border-bottom: 1px solid #999; height: 0; width: 100%;"></div>
|
||||
<div style="margin-bottom : 5px;"></div>
|
||||
<div class="description">
|
||||
<div class="description sla-chat-section">
|
||||
<div class="body">{{{item.name}}}</div>
|
||||
</div>
|
||||
<div style="margin-bottom : 16px;"></div>
|
||||
{{else}}
|
||||
<div class="description">
|
||||
<div class="description sla-chat-section">
|
||||
<div class="body">{{{item.system.description}}}</div>
|
||||
</div>
|
||||
<div style="margin-bottom : 16px;"></div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
<div class="mosh" {{#if actor._id}}data-actor-id="{{actor._id}}" {{/if}}
|
||||
<div class="mosh sla-chat-shell sla-chat-shell-actor-update" {{#if actor._id}}data-actor-id="{{actor._id}}" {{/if}}
|
||||
{{#if item._id}}data-item-id="{{item._id}}" {{/if}} {{#if tokenId}}data-token-id="{{tokenId}}" {{/if}}>
|
||||
<div class="rollcontainer">
|
||||
<div class="flexrow" style="margin-bottom: 5px;">
|
||||
{{#if (compare 'int' msgHeader.length '>=' 22)}}
|
||||
<div class="rollweaponh1" style="font-size: 0.75rem; padding-top: 7px;">{{{msgHeader}}}</div>
|
||||
{{else}}
|
||||
<div class="rollweaponh1" >{{{msgHeader}}}</div>
|
||||
{{/if}}
|
||||
<div style="text-align: right"><img class="roll-image" src="{{{msgImgPath}}}" /></div>
|
||||
<div class="sla-chat-hero">
|
||||
<div class="sla-chat-hero-main">
|
||||
<div class="sla-chat-kicker">Actor Update</div>
|
||||
{{#if (compare 'int' msgHeader.length '>=' 22)}}
|
||||
<div class="rollweaponh1 sla-chat-hero-title sla-chat-hero-title-tight">{{{msgHeader}}}</div>
|
||||
{{else}}
|
||||
<div class="rollweaponh1 sla-chat-hero-title">{{{msgHeader}}}</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="sla-chat-hero-media">
|
||||
<img class="roll-image" src="{{{msgImgPath}}}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="description"" style="margin-bottom: 20px;">
|
||||
<div class="description sla-chat-section">
|
||||
<div class="body">
|
||||
{{{msgFlavor}}}
|
||||
{{#if modRollString}}
|
||||
<div class="description" style="margin-bottom: 10px;"></div>
|
||||
{{{parsedRollResult.rollHtml}}}
|
||||
{{else}}
|
||||
<br><br>
|
||||
<div class="sla-chat-rollframe">{{{parsedRollResult.rollHtml}}}</div>
|
||||
{{/if}}
|
||||
{{{msgOutcome}}}
|
||||
{{#if halfDamage}}
|
||||
<br><br>@UUID[Compendium.mosh.macros_hotbar_0e.eyGvbBi1BhWSgfUP]{Make a Panic Check}
|
||||
<div class="sla-chat-section sla-chat-section-danger">
|
||||
<div class="body">@UUID[Compendium.mosh.macros_hotbar_0e.eyGvbBi1BhWSgfUP]{Make a Panic Check}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
<div class="mosh" {{#if actor._id}}data-actor-id="{{actor._id}}" {{/if}}
|
||||
<div class="mosh sla-chat-shell sla-chat-shell-item-update" {{#if actor._id}}data-actor-id="{{actor._id}}" {{/if}}
|
||||
{{#if item._id}}data-item-id="{{item._id}}" {{/if}} {{#if tokenId}}data-token-id="{{tokenId}}" {{/if}}>
|
||||
<div class="rollcontainer">
|
||||
<div class="flexrow" style="margin-bottom : 5px;">
|
||||
{{#if (compare 'int' msgHeader.length '>=' 22)}}
|
||||
<div class="rollweaponh1" style="font-size: 0.75rem; padding-top: 7px;">{{{msgHeader}}}</div>
|
||||
{{else}}
|
||||
<div class="rollweaponh1" >{{{msgHeader}}}</div>
|
||||
{{/if}}
|
||||
<div style="text-align: right"><img class="roll-image" src="{{{msgImgPath}}}" /></div>
|
||||
<div class="sla-chat-hero">
|
||||
<div class="sla-chat-hero-main">
|
||||
<div class="sla-chat-kicker-row">
|
||||
<div class="sla-chat-kicker">Item Update</div>
|
||||
<div class="sla-chat-meta-row">
|
||||
<span class="sla-chat-type-badge sla-chat-type-item">UPD</span>
|
||||
</div>
|
||||
</div>
|
||||
{{#if (compare 'int' msgHeader.length '>=' 22)}}
|
||||
<div class="rollweaponh1 sla-chat-hero-title sla-chat-hero-title-tight">{{{msgHeader}}}</div>
|
||||
{{else}}
|
||||
<div class="rollweaponh1 sla-chat-hero-title">{{{msgHeader}}}</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="sla-chat-hero-media">
|
||||
<img class="roll-image" src="{{{msgImgPath}}}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="description" style="margin-bottom: 10px;">
|
||||
<div class="description sla-chat-section sla-chat-section-target">
|
||||
<div class="body">{{{flavorText}}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
<div class="mosh" {{#if actor._id}}data-actor-id="{{actor._id}}" {{/if}}
|
||||
<div class="mosh sla-chat-shell sla-chat-shell-reload" {{#if actor._id}}data-actor-id="{{actor._id}}" {{/if}}
|
||||
{{#if item._id}}data-item-id="{{item._id}}" {{/if}} {{#if tokenId}}data-token-id="{{tokenId}}" {{/if}}>
|
||||
<div class="rollcontainer">
|
||||
<div class="flexrow" style="margin-bottom : 5px;">
|
||||
{{#if (compare 'int' item.name.length '>=' 22)}}
|
||||
<div class="rollweaponh1" style="font-size: 0.75rem; padding-top: 7px;">{{{item.name}}}</div>
|
||||
{{else}}
|
||||
<div class="rollweaponh1" >{{{item.name}}}</div>
|
||||
{{/if}}
|
||||
<div style="text-align: right"><img class="roll-image" src="systems/sla-mothership/images/icons/ui/macros/reload.png" /></div>
|
||||
<div class="sla-chat-hero">
|
||||
<div class="sla-chat-hero-main">
|
||||
<div class="sla-chat-kicker-row">
|
||||
<div class="sla-chat-kicker">Reload Action</div>
|
||||
<div class="sla-chat-meta-row">
|
||||
<span class="sla-chat-type-badge sla-chat-type-reload">RLD</span>
|
||||
</div>
|
||||
</div>
|
||||
{{#if (compare 'int' item.name.length '>=' 22)}}
|
||||
<div class="rollweaponh1 sla-chat-hero-title sla-chat-hero-title-tight">{{{item.name}}}</div>
|
||||
{{else}}
|
||||
<div class="rollweaponh1 sla-chat-hero-title">{{{item.name}}}</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="sla-chat-hero-media">
|
||||
<img class="roll-image" src="systems/sla-mothership/images/icons/ui/macros/reload.png" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="description">
|
||||
<div class="description sla-chat-section sla-chat-section-target">
|
||||
<div class="body">{{{msgBody}}}</div>
|
||||
</div>
|
||||
<div style="margin-bottom : 16px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,38 +1,58 @@
|
||||
<div class="mosh{{#if isSlaEbbSkill}} sla-ebb-chat{{/if}}" {{#if actor._id}}data-actor-id="{{actor._id}}" {{/if}}
|
||||
<div class="mosh sla-chat-shell sla-chat-shell-check{{#if isSlaEbbSkill}} sla-chat-shell-ebb{{/if}} sla-chat-variant-{{chatStyle.key}}{{#if parsedRollResult.success}} sla-chat-shell-success{{else}} sla-chat-shell-failure{{/if}}{{#if parsedRollResult.critical}} sla-chat-shell-critical{{/if}}" {{#if actor._id}}data-actor-id="{{actor._id}}" {{/if}}
|
||||
{{#if weapon._id}}data-item-id="{{weapon._id}}" {{/if}} {{#if tokenId}}data-token-id="{{tokenId}}" {{/if}}>
|
||||
<div class="rollcontainer">
|
||||
<div class="flexrow" style="margin-bottom : 5px;">
|
||||
{{#if (compare 'int' msgHeader.length '>=' 22)}}
|
||||
<div class="rollweaponh1" style="font-size: 0.75rem; padding-top: 7px;">{{{msgHeader}}}</div>
|
||||
{{else}}
|
||||
<div class="rollweaponh1" >{{{msgHeader}}}</div>
|
||||
{{/if}}
|
||||
<div style="text-align: right"><img class="roll-image" src="{{{msgImgPath}}}" /></div>
|
||||
<div class="sla-chat-hero">
|
||||
<div class="sla-chat-hero-main">
|
||||
<div class="sla-chat-kicker-row">
|
||||
<div class="sla-chat-kicker">{{#if (compare 'str' specialRoll '===' 'damage')}}Attack Roll{{else if isSlaEbbSkill}}Ebb Check{{else}}Normal Roll{{/if}}</div>
|
||||
<div class="sla-chat-meta-row">
|
||||
{{#if (compare 'str' specialRoll '===' 'damage')}}
|
||||
<span class="sla-chat-type-badge sla-chat-type-attack">ATK</span>
|
||||
{{else if isSlaEbbSkill}}
|
||||
<span class="sla-chat-type-badge sla-chat-type-ebb">EBB</span>
|
||||
{{else}}
|
||||
<span class="sla-chat-type-badge sla-chat-type-normal">CHK</span>
|
||||
{{/if}}
|
||||
<div class="sla-chat-deptline"><span class="sla-chat-emblem">{{chatStyle.emblem}}</span><span class="sla-chat-dept">{{chatStyle.label}}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
{{#if (compare 'int' msgHeader.length '>=' 22)}}
|
||||
<div class="rollweaponh1 sla-chat-hero-title sla-chat-hero-title-tight">{{{msgHeader}}}</div>
|
||||
{{else}}
|
||||
<div class="rollweaponh1 sla-chat-hero-title">{{{msgHeader}}}</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="sla-chat-hero-media">
|
||||
<img class="roll-image" src="{{{msgImgPath}}}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if (compare 'str' specialRoll '!==' 'damage')}}
|
||||
{{{parsedRollResult.outcomeHtml}}}
|
||||
<div class="description" style="margin-bottom: 10px;">
|
||||
<div class="body">
|
||||
{{#if (compare 'int' parsedRollResult.total '>=' 90)}}
|
||||
<span class="high-failure">{{ localize 'Mosh.YouRolled90OrHigher' }}</span>
|
||||
{{else}}
|
||||
{{#if isSlaEbbSkill}}
|
||||
<div class="sla-chat-summary">
|
||||
{{#if (compare 'int' parsedRollResult.total '>=' 90)}}
|
||||
<span class="high-failure">{{ localize 'Mosh.YouRolled90OrHigher' }}</span>
|
||||
{{else}}
|
||||
{{#if isSlaEbbSkill}}
|
||||
{{#if (compare 'int' skillValue '>' 0)}}
|
||||
You {{{outcomeVerb}}} {{{comparisonText}}} your <strong>{{{attribute}}}</strong> plus the <strong>{{{skill}}}</strong> discipline.
|
||||
{{else}}
|
||||
You {{{outcomeVerb}}} {{{comparisonText}}} your <strong>{{{attribute}}}</strong> while channeling <strong>{{{skill}}}</strong>.
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{else}}
|
||||
{{#if (compare 'int' skillValue '>' 0)}}
|
||||
You {{{outcomeVerb}}} {{{comparisonText}}} your <strong>{{{attribute}}}</strong> plus <strong>{{{skill}}}</strong> skill bonus.
|
||||
{{else}}
|
||||
You {{{outcomeVerb}}} {{{comparisonText}}} your <strong>{{{attribute}}}</strong>.
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{{parsedRollResult.rollHtml}}}
|
||||
|
||||
<div class="sla-chat-rollframe">
|
||||
{{{parsedRollResult.outcomeHtml}}}
|
||||
{{{parsedRollResult.rollHtml}}}
|
||||
</div>
|
||||
|
||||
{{#if isSlaEbbSkill}}
|
||||
<div class="sla-chat-section sla-chat-section-ebb-meta">
|
||||
<div class="sla-chat-section-title">Ebb Discipline</div>
|
||||
@@ -50,79 +70,73 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<div class="description" style="margin-bottom : 20px;">
|
||||
{{#if (compare 'str' specialRoll '===' 'damage')}}
|
||||
<div class="description sla-chat-section">{{{flavorText}}}</div>
|
||||
{{#if needsDesc}}
|
||||
<div style="margin-bottom : 10px;"></div>
|
||||
<div class="grid grid-2col" style="grid-template-columns: 10px 75px auto">
|
||||
<div style="border-bottom:1px solid #999; height: 50%;"></div>
|
||||
<div style= "font-size:0.65rem; text-align:center;">DESCRIPTION</div>
|
||||
<div style="border-bottom:1px solid #999; height: 50%;"></div>
|
||||
</div>
|
||||
<div style="margin-bottom : 10px;"></div>
|
||||
<div class="description">
|
||||
<div class="body">{{{weapon.system.description}}}</div>
|
||||
{{#if woundEffect}}
|
||||
<div class="body">
|
||||
<br><strong>{{ localize 'Mosh.WoundEffect' }}</strong>
|
||||
<div style="margin-bottom : 3px;"></div>
|
||||
{{{woundEffect}}}
|
||||
{{#unless isSlaEbbSkill}}
|
||||
{{#if rollBreakdown}}
|
||||
<div class="sla-chat-section sla-chat-section-meta" style="margin-top: 5px; border-top: 1px dashed rgba(255,255,255,0.15); padding-top: 5px;">
|
||||
<div class="body"><strong>Target Number:</strong> {{#each rollBreakdown.parts as |part index|}}{{#if index}} + {{/if}}{{part.label}} {{part.value}}{{/each}} = <strong>{{rollBreakdown.total}}</strong></div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
{{#if (compare 'str' specialRoll '===' 'damage')}}
|
||||
<div class="sla-chat-section sla-chat-section-damage">{{{flavorText}}}</div>
|
||||
<div class="sla-chat-section sla-chat-section-crit-btn" style="margin-top: 6px;">
|
||||
<button class="sla-crit-hit-btn" data-actor-id="{{actor._id}}" data-item-id="{{weapon._id}}" style="background: linear-gradient(185deg, #a80000 0%, #5c0000 100%); color: #ffffff; border: 1px solid #ff4444; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-family: 'Barlow Condensed', sans-serif; font-size: 11pt; font-weight: bold; width: 100%; text-shadow: 0 1px 2px #000; box-shadow: 0 0 6px rgba(255, 0, 0, 0.4); transition: all 0.2s ease;">
|
||||
<i class="fas fa-skull-crossbones"></i> CRITICAL HIT?
|
||||
</button>
|
||||
</div>
|
||||
<div style="margin-bottom : 16px;"></div>
|
||||
{{#if needsDesc}}
|
||||
<div class="sla-chat-divider">Description</div>
|
||||
<div class="description sla-chat-section">
|
||||
<div class="body">{{{weapon.system.description}}}</div>
|
||||
{{#if woundEffect}}
|
||||
<div class="body sla-chat-wound-block">
|
||||
<strong>{{ localize 'Mosh.WoundEffect' }}</strong>
|
||||
<div class="sla-chat-wound-link">{{{woundEffect}}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if parsedRollResult.success}}
|
||||
{{#if flavorText}}<div class="sla-chat-section sla-chat-section-success">{{{flavorText}}}</div>{{/if}}
|
||||
{{#if weapon}}
|
||||
<div class="description sla-chat-section">{{{flavorText}}}</div>
|
||||
{{#if criticalSuccessOptions.length}}
|
||||
<div class="sla-chat-section sla-chat-section-success">
|
||||
<div class="sla-chat-section-title">Critical Success Options</div>
|
||||
<ol class="sla-chat-list">
|
||||
{{#each criticalSuccessOptions as |option|}}
|
||||
<li>{{option}}</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
<div class="sla-chat-section sla-chat-section-crit-btn" style="margin-top: 6px; margin-bottom: 6px;">
|
||||
<button class="sla-crit-hit-btn" data-actor-id="{{actor._id}}" data-item-id="{{weapon._id}}" style="background: linear-gradient(185deg, #a80000 0%, #5c0000 100%); color: #ffffff; border: 1px solid #ff4444; border-radius: 4px; padding: 6px 12px; cursor: pointer; font-family: 'Barlow Condensed', sans-serif; font-size: 11pt; font-weight: bold; width: 100%; text-shadow: 0 1px 2px #000; box-shadow: 0 0 6px rgba(255, 0, 0, 0.4); transition: all 0.2s ease;">
|
||||
<i class="fas fa-skull-crossbones"></i> CRITICAL HIT?
|
||||
</button>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if criticalSuccessOptions.length}}
|
||||
<div class="sla-chat-section sla-chat-section-success">
|
||||
<div class="sla-chat-section-title"><i class="fas fa-crown"></i> Critical Success Options</div>
|
||||
<div class="sla-crit-success-options" style="margin-top: 5px;">
|
||||
{{#each criticalSuccessOptions as |option|}}
|
||||
<button class="sla-crit-option-btn" data-actor-id="{{../actor._id}}" data-option-text="{{option}}" style="background: linear-gradient(180deg, #1f421f 0%, #0c260c 100%); color: #bdf2bd; border: 1px solid #3fa63f; border-radius: 4px; padding: 6px 10px; margin-bottom: 6px; cursor: pointer; font-family: 'Barlow Condensed', sans-serif; font-size: 10.5pt; text-align: left; width: 100%; transition: all 0.2s ease; display: flex; align-items: flex-start; gap: 6px; text-shadow: 0 1px 2px #000;">
|
||||
<i class="fas fa-chevron-right" style="margin-top: 3px; font-size: 8pt; color: #2ebd2e;"></i>
|
||||
<span>{{option}}</span>
|
||||
</button>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if weapon}}
|
||||
{{#if needsDesc}}
|
||||
<div style="margin-bottom : 10px;"></div>
|
||||
<div class="grid grid-2col" style="grid-template-columns: 10px 75px auto">
|
||||
<div style="border-bottom:1px solid #999; height: 50%;"></div>
|
||||
<div style= "font-size:0.65rem; text-align:center;">{{ localize 'Mosh.Description' }}</div>
|
||||
<div style="border-bottom:1px solid #999; height: 50%;"></div>
|
||||
</div>
|
||||
<div style="margin-bottom : 10px;"></div>
|
||||
<div class="description">
|
||||
<div class="sla-chat-divider">{{ localize 'Mosh.Description' }}</div>
|
||||
<div class="description sla-chat-section">
|
||||
<div class="body">{{{weapon.system.description}}}</div>
|
||||
{{#if woundEffect}}
|
||||
<div class="body">
|
||||
<br><strong>{{ localize 'Mosh.WoundEffect' }}</strong>
|
||||
<div style="margin-bottom : 3px;"></div>
|
||||
{{{woundEffect}}}
|
||||
<div class="body sla-chat-wound-block">
|
||||
<strong>{{ localize 'Mosh.WoundEffect' }}</strong>
|
||||
<div class="sla-chat-wound-link">{{{woundEffect}}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div style="margin-bottom : 16px;"></div>
|
||||
{{else}}
|
||||
<div class="sla-chat-section">{{{flavorText}}}</div>
|
||||
{{#if criticalSuccessOptions.length}}
|
||||
<div class="sla-chat-section sla-chat-section-success">
|
||||
<div class="sla-chat-section-title">Critical Success Options</div>
|
||||
<ol class="sla-chat-list">
|
||||
{{#each criticalSuccessOptions as |option|}}
|
||||
<li>{{option}}</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if flavorText}}<div class="sla-chat-section">{{{flavorText}}}</div>{{/if}}
|
||||
{{#if flavorText}}<div class="sla-chat-section sla-chat-section-danger">{{{flavorText}}}</div>{{/if}}
|
||||
{{#if criticalFailureTable}}
|
||||
<div class="sla-chat-section sla-chat-section-danger">
|
||||
<div class="sla-chat-section-title">{{criticalFailureTable.label}}</div>
|
||||
@@ -132,15 +146,18 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if critFail}}
|
||||
<br><br>@UUID[Compendium.mosh.macros_hotbar_1e.IgWvaabVqzKDzLL4]{Make a Panic Check}
|
||||
<div class="sla-chat-section sla-chat-section-danger">
|
||||
<div class="body">@UUID[Compendium.mosh.macros_hotbar_1e.IgWvaabVqzKDzLL4]{Make a Panic Check}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if targetingResult}}
|
||||
<div class="sla-chat-section sla-chat-section-target">
|
||||
<div class="sla-chat-section-title">Target Resolution</div>
|
||||
<div class="body"><strong>{{targetingResult.targetName}}</strong> took <strong>{{targetingResult.finalDamage}}</strong> damage.</div>
|
||||
<div class="body">Rolled {{targetingResult.rawDamage}}. Armour reduced {{targetingResult.armourReduced}}{{#if targetingResult.armourPiercing}} after AP/anti-armour effects{{/if}}.</div>
|
||||
<div class="body">Rolled {{targetingResult.rawDamage}}. Armour reduced {{targetingResult.armourReduced}}{{#if targetingResult.armourPiercing}} after AP or anti-armour effects{{/if}}.</div>
|
||||
{{#if targetingResult.additionalTargets}}
|
||||
<div class="body">{{targetingResult.additionalTargets}} additional targeted token(s) were ignored for this automatic application.</div>
|
||||
{{/if}}
|
||||
@@ -152,6 +169,5 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,45 @@
|
||||
<div class="mosh" {{#if actor._id}}data-actor-id="{{actor._id}}" {{/if}}
|
||||
<div class="mosh sla-chat-shell sla-chat-shell-table sla-chat-variant-{{chatStyle.key}}{{#if parsedRollResult.success}} sla-chat-shell-success{{else}} sla-chat-shell-failure{{/if}}" {{#if actor._id}}data-actor-id="{{actor._id}}" {{/if}}
|
||||
{{#if item._id}}data-item-id="{{item._id}}" {{/if}} {{#if tokenId}}data-token-id="{{tokenId}}" {{/if}}>
|
||||
<div class="rollcontainer">
|
||||
<div class="flexrow" style="margin-bottom : 5px;">
|
||||
<div class="rollweaponh1">{{{tableName}}}</div>
|
||||
<div style="text-align: right"><img class="roll-image" src="{{{tableImg}}}" /></div>
|
||||
<div class="sla-chat-hero">
|
||||
<div class="sla-chat-hero-main">
|
||||
<div class="sla-chat-kicker-row">
|
||||
<div class="sla-chat-kicker">Table Roll</div>
|
||||
<div class="sla-chat-meta-row">
|
||||
{{#if (compare 'str' specialRoll '===' 'panicCheck')}}
|
||||
<span class="sla-chat-type-badge sla-chat-type-panic">PNC</span>
|
||||
{{else if (compare 'str' tableName '===' 'Gunshot Wound')}}
|
||||
<span class="sla-chat-type-badge sla-chat-type-wound">WND</span>
|
||||
{{else if (compare 'str' tableName '===' 'Bleeding Wound')}}
|
||||
<span class="sla-chat-type-badge sla-chat-type-wound">WND</span>
|
||||
{{else if (compare 'str' tableName '===' 'Fire & Explosives Wound')}}
|
||||
<span class="sla-chat-type-badge sla-chat-type-wound">WND</span>
|
||||
{{else if (compare 'str' tableName '===' 'Gore & Massive Wound')}}
|
||||
<span class="sla-chat-type-badge sla-chat-type-wound">WND</span>
|
||||
{{else}}
|
||||
<span class="sla-chat-type-badge sla-chat-type-table">TBL</span>
|
||||
{{/if}}
|
||||
<div class="sla-chat-deptline"><span class="sla-chat-emblem">{{chatStyle.emblem}}</span><span class="sla-chat-dept">{{chatStyle.label}}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rollweaponh1 sla-chat-hero-title">{{{tableName}}}</div>
|
||||
</div>
|
||||
<div class="sla-chat-hero-media">
|
||||
<img class="roll-image" src="{{{tableImg}}}" />
|
||||
</div>
|
||||
</div>
|
||||
{{{parsedRollResult.outcomeHtml}}}
|
||||
<div class="description sla-chat-section" style="margin-bottom: 10px;">
|
||||
|
||||
<div class="description sla-chat-section">
|
||||
<div class="body">{{{msgDesc}}}</div>
|
||||
</div>
|
||||
{{{parsedRollResult.rollHtml}}}
|
||||
|
||||
<div class="sla-chat-rollframe">
|
||||
{{{parsedRollResult.outcomeHtml}}}
|
||||
{{{parsedRollResult.rollHtml}}}
|
||||
</div>
|
||||
|
||||
{{#if secondRoll}}
|
||||
<div class="description sla-chat-section" style="margin-bottom: 10px;">
|
||||
<div class="description sla-chat-section">
|
||||
{{#if (compare 'str' specialRoll '===' 'panicCheck')}}
|
||||
<div class="body">{{ localize 'Mosh.RollingForPanicEffect' }}</div>
|
||||
{{/if}}
|
||||
@@ -19,34 +47,57 @@
|
||||
<div class="body">{{ localize 'Mosh.RollingForMaintenanceIssue' }}</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{{parsedRollResult2.rollHtml}}}
|
||||
<div class="sla-chat-rollframe">
|
||||
{{{parsedRollResult2.rollHtml}}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if parsedRollResult.success}}
|
||||
<div class="description sla-chat-section sla-chat-section-success" style="margin-bottom : 20px;">{{{flavorText}}}</div>
|
||||
<div class="description sla-chat-section sla-chat-section-success">{{{flavorText}}}</div>
|
||||
{{else}}
|
||||
{{#if woundText}}
|
||||
{{{woundText}}}<br><br>
|
||||
<div class="sla-chat-section sla-chat-section-danger {{severityMeta.cssClass}}">
|
||||
<div class="sla-chat-section-heading">
|
||||
<span class="sla-chat-severity-pill {{severityMeta.cssClass}}">{{severityMeta.label}}</span>
|
||||
</div>
|
||||
{{{woundText}}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (compare 'str' tableResultType '===' 'document')}}
|
||||
<ol class="table-results">
|
||||
{{#each tableResult}}
|
||||
<li class="table-result flexrow" data-result-id="{{{this._id}}}">
|
||||
<img class="result-image" src="{{{this.img}}}">
|
||||
<div class="result-text"><a class="content-link broken" draggable="true" data-type="Item" data-uuid="undefined"><i class="fas fa-unlink"></i>{{{this.description}}}</a></div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
<div class="sla-chat-section sla-chat-section-target">
|
||||
<div class="sla-chat-section-title">Linked Results</div>
|
||||
<ol class="table-results">
|
||||
{{#each tableResult}}
|
||||
<li class="table-result flexrow" data-result-id="{{{this._id}}}">
|
||||
<img class="result-image" src="{{{this.img}}}">
|
||||
<div class="result-text"><a class="content-link broken" draggable="true" data-type="Item" data-uuid="undefined"><i class="fas fa-unlink"></i>{{{this.description}}}</a></div>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if (compare 'str' tableResultType '===' 'text')}}
|
||||
{{#if tableResultEdited}}
|
||||
<div class="description sla-chat-section sla-chat-section-danger" style="margin-bottom : 20px;">{{{tableResultEdited}}}{{{tableResultFooter}}}</div>
|
||||
<div class="description sla-chat-section sla-chat-section-danger {{severityMeta.cssClass}}">
|
||||
<div class="sla-chat-section-heading">
|
||||
<span class="sla-chat-severity-pill {{severityMeta.cssClass}}">{{severityMeta.label}}</span>
|
||||
</div>
|
||||
{{{tableResultEdited}}}{{{tableResultFooter}}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="description sla-chat-section sla-chat-section-danger" style="margin-bottom : 20px;">{{{tableResult.[0].description}}}{{{tableResultFooter}}}</div>
|
||||
<div class="description sla-chat-section sla-chat-section-danger {{severityMeta.cssClass}}">
|
||||
<div class="sla-chat-section-heading">
|
||||
<span class="sla-chat-severity-pill {{severityMeta.cssClass}}">{{severityMeta.label}}</span>
|
||||
</div>
|
||||
{{{tableResult.[0].description}}}{{{tableResultFooter}}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if (compare 'str' specialRoll '===' 'maintenanceCheck')}}
|
||||
{{#if critFail}}
|
||||
<br><br>@UUID[Compendium.mosh.macros_hotbar_1e.IgWvaabVqzKDzLL4]{Make a Panic Check}
|
||||
<div class="sla-chat-section sla-chat-section-danger">
|
||||
<div class="body">@UUID[Compendium.mosh.macros_hotbar_1e.IgWvaabVqzKDzLL4]{Make a Panic Check}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<div class="mosh sla-chat-shell sla-chat-shell-check sla-chat-shell-success" style="border-left: 4px solid #2ebd2e; padding-left: 8px;">
|
||||
<div class="rollcontainer">
|
||||
<div class="sla-chat-hero" style="background: rgba(46, 189, 46, 0.15); display: flex; align-items: center; padding: 6px; border-radius: 4px 4px 0 0;">
|
||||
<div class="sla-chat-hero-main" style="flex-grow: 1;">
|
||||
<div class="sla-chat-kicker-row">
|
||||
<div class="sla-chat-kicker" style="color: #85e085; font-size: 8pt; font-weight: bold; letter-spacing: 0.1em;">TACTICAL DECISION</div>
|
||||
</div>
|
||||
<div class="rollweaponh1 sla-chat-hero-title" style="font-size: 14pt; margin: 0; color: #ffffff;">{{{actorName}}}</div>
|
||||
</div>
|
||||
<div class="sla-chat-hero-media" style="width: 32px; height: 32px; border-radius: 4px; overflow: hidden; margin-left: 8px; flex-shrink: 0; border: 1px solid #2ebd2e;">
|
||||
<img class="roll-image" src="{{{img}}}" style="width: 100%; height: 100%; object-fit: cover;" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="sla-chat-section sla-chat-section-success" style="padding: 10px; background: rgba(0,0,0,0.3); border-radius: 0 0 4px 4px; font-family: 'Cormorant Garamond', serif; font-size: 13pt; line-height: 1.3;">
|
||||
<i class="fas fa-check-circle" style="color: #2ebd2e; margin-right: 4px;"></i> <strong>Combat Critical Choice:</strong>
|
||||
<div style="margin-top: 6px; color: #e0fde0; font-style: italic;">"{{{optionText}}}"</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,154 @@
|
||||
<form class="{{cssClass}} sla-npc-generator sla-npc-accent-{{accentKey}}" autocomplete="off">
|
||||
<p>Create lightweight SLA NPCs with enough identity for play, then open the creature sheet to adjust portrait, items, weapons, skills, and notes.</p>
|
||||
|
||||
<div class="sla-card sla-npc-brief-card" style="padding: 12px; margin-bottom: 12px;">
|
||||
<div class="grid grid-2col" style="gap: 10px;">
|
||||
<div class="resource">
|
||||
<label class="resource-label">Name</label>
|
||||
<div class="flexrow" style="gap: 8px;">
|
||||
<input type="text" name="name" value="{{state.name}}" />
|
||||
<button type="button" class="sla-npc-random-name"><i class="fas fa-dice"></i> Name</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Archetype</label>
|
||||
<select name="archetype">
|
||||
{{#each archetypes}}
|
||||
<option value="{{this.name}}" {{#if (compare "str" ../state.archetype "===" this.name)}}selected{{/if}}>{{this.name}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Species</label>
|
||||
<select name="species">
|
||||
{{#each speciesOptions}}
|
||||
<option value="{{this}}" {{#if (compare "str" ../state.species "===" this)}}selected{{/if}}>{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Quality</label>
|
||||
<select name="quality">
|
||||
{{#each qualityOptions}}
|
||||
<option value="{{this}}" {{#if (compare "str" ../state.quality "===" this)}}selected{{/if}}>{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sla-card-copy" style="margin-top: 10px;">{{summary}}</div>
|
||||
</div>
|
||||
|
||||
<section class="sla-card sla-npc-preview-panel" style="margin-bottom: 12px;">
|
||||
<div class="sla-npc-preview-header">
|
||||
<div class="sla-npc-preview-heading">
|
||||
<div class="sla-npc-preview-badge" aria-hidden="true">
|
||||
<img class="sla-npc-preview-icon" src="{{preview.iconPath}}" alt="" />
|
||||
<span>{{preview.iconLabel}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="sla-kicker">{{preview.badge}}</div>
|
||||
<div class="sla-title">{{state.name}}</div>
|
||||
<div class="sla-card-copy">{{preview.strap}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sla-npc-preview-pill">{{state.quality}}</div>
|
||||
</div>
|
||||
<div class="sla-npc-preview-grid">
|
||||
<div class="sla-npc-preview-block">
|
||||
<div class="sla-card-label">Profile</div>
|
||||
<div class="sla-card-copy"><strong>{{preview.identity}}</strong></div>
|
||||
<div class="sla-card-copy">{{preview.brief}}</div>
|
||||
<div class="sla-card-copy">{{preview.posture}}</div>
|
||||
</div>
|
||||
<div class="sla-npc-preview-block">
|
||||
<div class="sla-card-label">Field Readout</div>
|
||||
<div class="sla-tags">
|
||||
{{#each preview.statline}}
|
||||
<span class="sla-tag">{{this}}</span>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="sla-npc-preview-block sla-npc-preview-wide">
|
||||
<div class="sla-card-label">Issue Kit</div>
|
||||
<div class="sla-note-list">
|
||||
{{#each preview.loadout}}
|
||||
<span class="sla-note-pill">{{this}}</span>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="grid grid-2col" style="gap: 10px;">
|
||||
<div class="resource">
|
||||
<label class="resource-label">Affiliation Type</label>
|
||||
<select name="affiliationType">
|
||||
{{#each affiliationTypes}}
|
||||
<option value="{{this}}" {{#if (compare "str" ../state.affiliationType "===" this)}}selected{{/if}}>{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Affiliation</label>
|
||||
<input type="text" name="affiliation" value="{{state.affiliation}}" />
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Department / Cell / Turf</label>
|
||||
<input type="text" name="department" value="{{state.department}}" />
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Role / Title</label>
|
||||
<input type="text" name="role" value="{{state.role}}" />
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Sector / Location</label>
|
||||
<input type="text" name="sector" value="{{state.sector}}" />
|
||||
</div>
|
||||
<div class="grid grid-2col" style="gap: 10px;">
|
||||
<div class="resource">
|
||||
<label class="resource-label">Disposition</label>
|
||||
<select name="disposition">
|
||||
{{#each dispositionOptions}}
|
||||
<option value="{{this}}" {{#if (compare "str" ../state.disposition "===" this)}}selected{{/if}}>{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Threat</label>
|
||||
<select name="threat">
|
||||
{{#each threatOptions}}
|
||||
<option value="{{this}}" {{#if (compare "str" ../state.threat "===" this)}}selected{{/if}}>{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Loadout Preset</label>
|
||||
<select name="loadout">
|
||||
{{#each loadoutOptions}}
|
||||
<option value="{{this}}" {{#if (compare "str" ../state.loadout "===" this)}}selected{{/if}}>{{this}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Generator Notes</label>
|
||||
<textarea name="notes" rows="4">{{state.notes}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sla-action-row" style="margin-top: 12px; display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
|
||||
<label class="checkbox" style="display:inline-flex; gap:6px; align-items:center;">
|
||||
<input type="checkbox" name="assignPortrait" {{checked state.assignPortrait}} />
|
||||
Assign portrait if available
|
||||
</label>
|
||||
<label class="checkbox" style="display:inline-flex; gap:6px; align-items:center;">
|
||||
<input type="checkbox" name="openSheet" {{checked state.openSheet}} />
|
||||
Open sheet after creation
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="sla-action-row" style="margin-top: 12px; display: flex; gap: 10px; justify-content: flex-end; flex-wrap: wrap;">
|
||||
<button type="button" class="sla-npc-randomize"><i class="fas fa-shuffle"></i> Randomize Brief</button>
|
||||
<button type="submit"><i class="fas fa-user-plus"></i> Create NPC</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -10,6 +10,10 @@
|
||||
<label class="resource-label">Character Actors</label>
|
||||
<div>{{actorCount}}</div>
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Creature NPCs</label>
|
||||
<div>{{creatureCount}}</div>
|
||||
</div>
|
||||
<div class="resource">
|
||||
<label class="resource-label">Skills / Weapons</label>
|
||||
<div>{{skillCount}} / {{weaponCount}}</div>
|
||||
@@ -28,6 +32,7 @@
|
||||
<button type="button" class="seed-world"><i class="fas fa-box-open"></i> Seed SLA Content</button>
|
||||
<button type="button" class="refresh-world"><i class="fas fa-sync"></i> Refresh Seeded Content</button>
|
||||
<button type="button" class="open-generator"><i class="fas fa-user-plus"></i> Open Generator</button>
|
||||
<button type="button" class="open-npc-desk"><i class="fas fa-id-badge"></i> Open NPC Desk</button>
|
||||
<button type="button" class="generate-random"><i class="fas fa-dice"></i> Create Random Operative</button>
|
||||
<button type="button" class="generate-random-standard"><i class="fas fa-user"></i> Create Standard Operative</button>
|
||||
<button type="button" class="generate-random-ebb"><i class="fas fa-brain"></i> Create Random Ebb User</button>
|
||||
|
||||