3 Commits
6 changed files with 388 additions and 26 deletions
+180 -13
View File
@@ -4775,11 +4775,21 @@ p {
.mosh.sla-chat-shell .sla-chat-section-danger.sla-severity-minor {
background: linear-gradient(180deg, rgba(92, 57, 18, 0.96), rgba(55, 34, 12, 0.96));
border-color: rgba(255, 190, 74, 0.5);
color: #ffeacc;
}
.mosh.sla-chat-shell .sla-chat-section-danger.sla-severity-minor strong {
color: #ffffff;
}
.mosh.sla-chat-shell .sla-chat-section-danger.sla-severity-severe {
background: linear-gradient(180deg, rgba(90, 18, 26, 0.97), rgba(52, 10, 15, 0.97));
border-color: rgba(255, 91, 91, 0.52);
color: #ffdada;
}
.mosh.sla-chat-shell .sla-chat-section-danger.sla-severity-severe strong {
color: #ffffff;
}
.mosh.sla-chat-shell .sla-chat-section-danger.sla-severity-critical {
@@ -4788,6 +4798,11 @@ p {
linear-gradient(180deg, rgba(102, 8, 28, 0.98), rgba(45, 8, 21, 0.98));
border-color: rgba(204, 237, 255, 0.84);
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.06), 0 0 14px rgba(162, 231, 255, 0.08);
color: #ffffff;
}
.mosh.sla-chat-shell .sla-chat-section-danger.sla-severity-critical strong {
color: #ffffff;
}
.mosh.sla-chat-shell .sla-chat-section-target {
@@ -4983,26 +4998,25 @@ p {
font-weight: 800;
}
.mosh .sla-severity-minor,
.mosh .sla-chat-severity-pill.sla-severity-minor {
background: linear-gradient(180deg, rgba(255, 216, 140, 0.32), rgba(255, 189, 79, 0.18));
border-color: rgba(224, 152, 24, 0.42);
color: #9b5b00;
background: linear-gradient(180deg, #ffc866 0%, #ff9d00 100%);
border-color: #ffbe4a;
color: #1a1000 !important;
text-shadow: none;
}
.mosh .sla-severity-severe,
.mosh .sla-chat-severity-pill.sla-severity-severe {
background: linear-gradient(180deg, rgba(255, 145, 145, 0.24), rgba(233, 63, 63, 0.14));
border-color: rgba(179, 27, 27, 0.34);
color: #9e1717;
background: linear-gradient(180deg, #ff8080 0%, #cc0000 100%);
border-color: #ff5b5b;
color: #ffffff !important;
text-shadow: 0 1px 1px rgba(0,0,0,0.5);
}
.mosh .sla-severity-critical,
.mosh .sla-chat-severity-pill.sla-severity-critical {
background: linear-gradient(180deg, rgba(232, 247, 255, 0.94), rgba(196, 233, 255, 0.82));
border-color: rgba(84, 145, 199, 0.42);
color: #611120;
box-shadow: 0 0 14px rgba(126, 196, 245, 0.16);
background: linear-gradient(180deg, #9ae6ff 0%, #0099ff 100%);
border-color: #54a5d6;
color: #001a33 !important;
text-shadow: none;
}
.mosh .sla-chat-section-target {
@@ -5473,3 +5487,156 @@ p {
border-radius: 3px;
background: rgba(139, 0, 0, 0.2);
}
/* =============================================================================
SLA Industries — Sidebar Tabs and Scene Controls Modern Overhaul
============================================================================= */
/* ── Scene Controls (Left Panel) ────────────────────────────────────────── */
#controls {
pointer-events: none !important;
}
#controls ol.main-controls,
#controls ol.sub-controls {
pointer-events: auto !important;
background: rgba(10, 12, 16, 0.88) !important;
border: 1px solid rgba(0, 229, 255, 0.2) !important;
border-radius: 10px !important;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.65), 0 0 14px rgba(0, 229, 255, 0.08) !important;
backdrop-filter: blur(12px) !important;
padding: 5px !important;
gap: 5px !important;
display: flex !important;
flex-direction: column !important;
margin-bottom: 10px !important;
}
#controls li.scene-control,
#controls li.control-tool {
position: relative !important;
border: 1px solid rgba(255, 255, 255, 0.06) !important;
background: rgba(255, 255, 255, 0.02) !important;
color: #8fa4b8 !important; /* Premium muted steel-blue */
border-radius: 8px !important;
width: 36px !important;
height: 36px !important;
margin: 0 !important;
padding: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
transition: all 0.22s cubic-bezier(0.4, 0, 0.2, 1) !important;
cursor: pointer !important;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02) !important;
}
/* Hover State */
#controls li.scene-control:hover,
#controls li.control-tool:hover {
color: #00e5ff !important; /* Vibrant Neon Cyan */
background: rgba(0, 229, 255, 0.1) !important;
border-color: rgba(0, 229, 255, 0.5) !important;
box-shadow: 0 0 14px rgba(0, 229, 255, 0.35), inset 0 1px 0 rgba(0, 229, 255, 0.15) !important;
transform: translateY(-1px) scale(1.04) !important;
}
/* Active State */
#controls li.scene-control.active,
#controls li.control-tool.active {
color: #00e5ff !important;
background: rgba(0, 229, 255, 0.16) !important;
border-color: #00e5ff !important;
box-shadow: 0 0 20px rgba(0, 229, 255, 0.5), inset 0 1px 0 rgba(0, 229, 255, 0.25) !important;
}
/* Active side indicator bar */
#controls li.scene-control.active::before,
#controls li.control-tool.active::before {
content: "" !important;
position: absolute !important;
left: -2px !important;
top: 15% !important;
height: 70% !important;
width: 2px !important;
background: #00e5ff !important;
box-shadow: 0 0 8px #00e5ff, 0 0 15px #00e5ff !important;
border-radius: 2px !important;
}
/* Sub-controls offset container glow adjustment */
#controls ol.sub-controls {
border-color: rgba(0, 229, 255, 0.14) !important;
background: rgba(14, 18, 24, 0.82) !important;
}
/* ── Right Sidebar Tabs (Navigation Panel) ─────────────────────────────── */
#sidebar-tabs {
background: rgba(10, 12, 16, 0.88) !important;
border-bottom: 2px solid rgba(0, 229, 255, 0.25) !important;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.05) !important;
backdrop-filter: blur(12px) !important;
border-radius: 8px 8px 0 0 !important;
padding: 3px 5px !important;
gap: 4px !important;
display: flex !important;
margin: 0 !important;
border-top: none !important;
}
#sidebar-tabs button[data-tab],
#sidebar-tabs [data-tab].icon,
#sidebar-tabs .item {
position: relative !important;
border: 1px solid rgba(255, 255, 255, 0.06) !important;
background: rgba(255, 255, 255, 0.02) !important;
color: #8fa4b8 !important;
border-radius: 6px !important;
margin: 2px 0 !important;
height: 32px !important;
transition: all 0.22s cubic-bezier(0.4, 0, 0.2, 1) !important;
cursor: pointer !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02) !important;
flex: 1 !important;
}
/* Hover State */
#sidebar-tabs button[data-tab]:hover,
#sidebar-tabs [data-tab].icon:hover,
#sidebar-tabs .item:hover {
color: #00e5ff !important;
background: rgba(0, 229, 255, 0.1) !important;
border-color: rgba(0, 229, 255, 0.5) !important;
box-shadow: 0 0 14px rgba(0, 229, 255, 0.35), inset 0 1px 0 rgba(0, 229, 255, 0.15) !important;
transform: translateY(-1px) scale(1.04) !important;
}
/* Active State */
#sidebar-tabs button[data-tab].active,
#sidebar-tabs [data-tab].icon.active,
#sidebar-tabs .item.active {
color: #00e5ff !important;
background: rgba(0, 229, 255, 0.16) !important;
border-color: #00e5ff !important;
box-shadow: 0 0 18px rgba(0, 229, 255, 0.45), inset 0 1px 0 rgba(0, 229, 255, 0.25) !important;
}
/* Glowing bottom line indicator on active tabs */
#sidebar-tabs button[data-tab].active::after,
#sidebar-tabs [data-tab].icon.active::after,
#sidebar-tabs .item.active::after {
content: "" !important;
position: absolute !important;
bottom: -3px !important;
left: 15% !important;
width: 70% !important;
height: 2px !important;
background: #00e5ff !important;
box-shadow: 0 0 8px #00e5ff, 0 0 15px #00e5ff !important;
border-radius: 2px !important;
}
+27 -7
View File
@@ -2031,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}`;
}
}
@@ -2587,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;
}
@@ -2606,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.';
}
+142 -1
View File
@@ -152,8 +152,26 @@ Hooks.once('init', async function () {
// 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", (item) => {
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;
if (_pendingEbbEnforcement.has(actor.id)) clearTimeout(_pendingEbbEnforcement.get(actor.id));
@@ -358,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
+1 -1
View File
@@ -2,7 +2,7 @@
"id": "sla-mothership",
"title": "SLA Mothership",
"description": "An SLA Industries conversion built on the unofficial Mothership system for Foundry VTT, preserving Mothership's core mechanics while adding SLA flux, Ebb, BPN, and equipment support.",
"version": "0.6.4",
"version": "0.6.9",
"esmodules": [
"module/mosh.js"
],
+19 -4
View File
@@ -81,6 +81,11 @@
{{#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>
{{#if needsDesc}}
<div class="sla-chat-divider">Description</div>
<div class="description sla-chat-section">
@@ -96,14 +101,24 @@
{{else}}
{{#if parsedRollResult.success}}
{{#if flavorText}}<div class="sla-chat-section sla-chat-section-success">{{{flavorText}}}</div>{{/if}}
{{#if weapon}}
<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">Critical Success Options</div>
<ol class="sla-chat-list">
<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|}}
<li>{{option}}</li>
<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}}
</ol>
</div>
</div>
{{/if}}
{{#if weapon}}
@@ -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>