Add Stormer Vevaphon — polymorphic morph system with instability tracker
- 3 morph forms (Brute/Stalker/Raptor), each costing 1 Instability on activation - Instability tracker (0–12) with minor effects at 6+, Morph Panic at 10+ - Morph tab on character sheet: shift buttons, bar, panic reference - actor.js: isSlaVevaphon(), setSlaVevaphonMorphForm(), adjustSlaVevaphonInstability() - CSS: instability bar, morph buttons - Character generator: morph constants, derive, generate() wiring - template.json: morphForm, vevaphonInstability, morphNotes
This commit is contained in:
+110
-7
@@ -474,7 +474,7 @@ p {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: 11pt;
|
||||
font-weight: 500;
|
||||
color: grey;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.mosh .resource-label {
|
||||
@@ -1843,7 +1843,15 @@ p {
|
||||
margin-top: -8px;
|
||||
margin-bottom: 3px;
|
||||
padding: 5px;
|
||||
color: grey;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
/* Skill roll dialog — scrollable list so Roll button is always visible */
|
||||
.macro-popup-dialog .skill-dialog-scroll {
|
||||
max-height: min(420px, 55vh);
|
||||
overflow-y: auto;
|
||||
margin-bottom: 4px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.mosh .rollattribute {
|
||||
@@ -2317,11 +2325,106 @@ p {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mosh .sla-flux-stage.stable { background: rgba(81, 163, 109, 0.25); color: #dff7e7; }
|
||||
.mosh .sla-flux-stage.charged { background: rgba(203, 161, 68, 0.25); color: #fff2c8; }
|
||||
.mosh .sla-flux-stage.frayed { background: rgba(194, 112, 55, 0.28); color: #ffe2cc; }
|
||||
.mosh .sla-flux-stage.breach { background: rgba(161, 59, 59, 0.32); color: #ffd7d7; }
|
||||
.mosh .sla-flux-stage.catastrophic { background: rgba(99, 31, 31, 0.5); color: #fff0f0; }
|
||||
.mosh .sla-flux-stage.stable { background: rgba(81, 163, 109, 0.25); color: #1a5c35; }
|
||||
.mosh .sla-flux-stage.charged { background: rgba(203, 161, 68, 0.25); color: #5a3f00; }
|
||||
.mosh .sla-flux-stage.frayed { background: rgba(194, 112, 55, 0.28); color: #6b2e00; }
|
||||
.mosh .sla-flux-stage.breach { background: rgba(161, 59, 59, 0.32); color: #6b1414; }
|
||||
.mosh .sla-flux-stage.catastrophic { background: rgba(99, 31, 31, 0.5); color: #3d0000; }
|
||||
|
||||
/* ── Vevaphon Instability ─────────────────────────────────────── */
|
||||
.mosh .sla-instability-bar-wrap {
|
||||
margin-top: 10px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
background: rgba(17, 17, 17, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.mosh .sla-instability-bar {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, rgba(81, 163, 109, 0.9), rgba(203, 161, 68, 0.92));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.mosh .sla-instability-bar.instability-elevated {
|
||||
background: linear-gradient(90deg, rgba(203, 161, 68, 0.92), rgba(194, 112, 55, 0.96));
|
||||
}
|
||||
|
||||
.mosh .sla-instability-bar.instability-critical {
|
||||
background: linear-gradient(90deg, rgba(194, 112, 55, 0.96), rgba(161, 59, 59, 0.98));
|
||||
}
|
||||
|
||||
.mosh .sla-instability-tag.instability-nominal {
|
||||
background: rgba(81, 163, 109, 0.18);
|
||||
color: #1a5c35;
|
||||
border-color: rgba(81, 163, 109, 0.35);
|
||||
}
|
||||
|
||||
.mosh .sla-instability-tag.instability-elevated {
|
||||
background: rgba(203, 161, 68, 0.22);
|
||||
color: #5a3f00;
|
||||
border-color: rgba(203, 161, 68, 0.45);
|
||||
}
|
||||
|
||||
.mosh .sla-instability-tag.instability-critical {
|
||||
background: rgba(161, 59, 59, 0.28);
|
||||
color: #6b1414;
|
||||
border-color: rgba(161, 59, 59, 0.55);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mosh .sla-morph-form-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mosh .sla-morph-shift {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: linear-gradient(180deg, #3a3f50, #22262e);
|
||||
color: #d8dde6;
|
||||
font-family: "Josefin Sans", "Roboto", sans-serif;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background 0.15s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mosh .sla-morph-shift:hover:not([disabled]) {
|
||||
background: linear-gradient(180deg, #4f5568, #303642);
|
||||
}
|
||||
|
||||
.mosh .sla-morph-shift[disabled] {
|
||||
background: linear-gradient(180deg, rgba(100, 160, 80, 0.3), rgba(60, 110, 50, 0.3));
|
||||
border-color: rgba(100, 160, 80, 0.5);
|
||||
color: #1a5c35;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.mosh .sla-morph-btn-name {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.mosh .sla-morph-btn-meta {
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.75;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
font-family: inherit;
|
||||
}
|
||||
/* ── End Vevaphon ─────────────────────────────────────────────── */
|
||||
|
||||
.mosh .sla-action-row button,
|
||||
.mosh .sla-generator button,
|
||||
|
||||
@@ -271,6 +271,15 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
const fluxValue = Number(superData.sla.flux?.value ?? 0);
|
||||
const fluxMax = Math.max(0, Number(superData.sla.flux?.max ?? 0));
|
||||
data.data.slaFluxPercent = fluxMax > 0 ? Math.min(100, Math.max(0, Math.round((fluxValue / fluxMax) * 100))) : 0;
|
||||
// Vevaphon morph data
|
||||
data.data.isVevaphon = this.actor.isSlaVevaphon?.() ?? false;
|
||||
if (data.data.isVevaphon) {
|
||||
const instab = Math.max(0, Math.min(12, Number(superData.sla.vevaphonInstability?.value ?? 0) || 0));
|
||||
data.data.vevaphonInstabilityPercent = Math.round((instab / 12) * 100);
|
||||
data.data.vevaphonInstabilityElevated = instab >= 6;
|
||||
data.data.vevaphonInstabilityCritical = instab >= 10;
|
||||
data.data.vevaphonInstabilityClass = instab >= 10 ? "instability-critical" : instab >= 6 ? "instability-elevated" : "instability-nominal";
|
||||
}
|
||||
data.data.slaSpeciesTraitList = String(superData.sla.speciesNotes?.value ?? "").split(/\n+/).map((entry) => entry.trim()).filter(Boolean);
|
||||
data.data.slaPackageSkillList = String(superData.sla.packageSkills?.value ?? "").split(/\s*,\s*/).map((entry) => entry.trim()).filter(Boolean);
|
||||
data.data.slaSpeciesRules = getSpeciesRules(String(superData.sla.species?.value ?? "").trim(), superData.sla.speciesRules);
|
||||
@@ -906,6 +915,24 @@ export class MothershipActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
this.actor.triggerSlaFluxPanic({ source: 'Manual Flux panic check', panicOnFailure: true, forcePanic: true });
|
||||
});
|
||||
|
||||
// Vevaphon morph form shift
|
||||
html.find('.sla-morph-shift').click(async (ev) => {
|
||||
const formName = ev.currentTarget.dataset.form;
|
||||
if (!formName) return;
|
||||
await this.actor.setSlaVevaphonMorphForm?.(formName);
|
||||
this.render(false);
|
||||
});
|
||||
|
||||
// Vevaphon instability +/-
|
||||
html.find('.sla-instability-increase').click(async () => {
|
||||
await this.actor.adjustSlaVevaphonInstability?.(1);
|
||||
this.render(false);
|
||||
});
|
||||
html.find('.sla-instability-decrease').click(async () => {
|
||||
await this.actor.adjustSlaVevaphonInstability?.(-1);
|
||||
this.render(false);
|
||||
});
|
||||
|
||||
html.find('.sla-panic-button').click(ev => {
|
||||
this.actor.rollTable("panicCheck", null, null, null, null, null, null);
|
||||
});
|
||||
|
||||
+55
-2
@@ -402,6 +402,14 @@ export class MothershipActor extends Actor {
|
||||
// Prepare Character type specific data
|
||||
_deriveCharacter() {
|
||||
const system = this.system;
|
||||
//clamp stress.value to stress.min so stale stored values never show below the floor
|
||||
if (system.other?.stress) {
|
||||
const stressMin = Number(system.other.stress.min ?? 0);
|
||||
const stressVal = Number(system.other.stress.value ?? 0);
|
||||
if (Number.isFinite(stressMin) && Number.isFinite(stressVal) && stressVal < stressMin) {
|
||||
system.other.stress.value = stressMin;
|
||||
}
|
||||
}
|
||||
//armor points + damage reduction
|
||||
//init vars
|
||||
let armorPoints = 0;
|
||||
@@ -1650,7 +1658,7 @@ export class MothershipActor extends Actor {
|
||||
window: {title: dlgTitle},
|
||||
classes: ["macro-popup-dialog"],
|
||||
position: {width: 600},
|
||||
content: skillHeader + skillList + buttonDesc,
|
||||
content: skillHeader + `<div class="skill-dialog-scroll">${skillList}</div>` + buttonDesc,
|
||||
buttons: []
|
||||
};
|
||||
//add adv/normal/dis buttons if we need a rollString
|
||||
@@ -4759,12 +4767,57 @@ export class MothershipActor extends Actor {
|
||||
|
||||
getSlaSpeciesMinimumWounds() {
|
||||
const species = String(this.system?.sla?.species?.value ?? "").trim();
|
||||
if (["Frother", "Shaktar", "Stormer 313 Malice", "Stormer 711 Xeno"].includes(species)) {
|
||||
if (["Frother", "Shaktar", "Stormer 313 Malice", "Stormer 711 Xeno", "Stormer Vevaphon"].includes(species)) {
|
||||
return 3;
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
isSlaVevaphon() {
|
||||
const species = String(this.system?.sla?.species?.value ?? "").trim();
|
||||
return species === "Stormer Vevaphon";
|
||||
}
|
||||
|
||||
getSlaVevaphonInstability() {
|
||||
return Math.max(0, Math.min(12, Number(this.system?.sla?.vevaphonInstability?.value ?? 0) || 0));
|
||||
}
|
||||
|
||||
getSlaVevaphonMorphForm() {
|
||||
return String(this.system?.sla?.morphForm?.value ?? "").trim() || "Brute Form";
|
||||
}
|
||||
|
||||
async setSlaVevaphonMorphForm(formName) {
|
||||
if (!this.isSlaVevaphon()) return null;
|
||||
const validForms = ["Brute Form", "Stalker Form", "Raptor Form"];
|
||||
const resolved = validForms.find((entry) => entry === formName) ?? formName;
|
||||
const currentInstability = this.getSlaVevaphonInstability();
|
||||
const newInstability = Math.min(12, currentInstability + 1);
|
||||
await this.update({
|
||||
"system.sla.morphForm.value": resolved,
|
||||
"system.sla.vevaphonInstability.value": newInstability
|
||||
});
|
||||
const instabilityWarning = newInstability >= 10
|
||||
? " ⚠ Instability critical — Morph Panic risk on failed Sanity save."
|
||||
: newInstability >= 6
|
||||
? " Instability elevated — minor morph effects active."
|
||||
: "";
|
||||
ui.notifications.info(`${this.name} shifts to ${resolved}. Instability: ${newInstability}/12.${instabilityWarning}`);
|
||||
return { morphForm: resolved, instability: newInstability };
|
||||
}
|
||||
|
||||
async adjustSlaVevaphonInstability(delta = 1) {
|
||||
if (!this.isSlaVevaphon()) return null;
|
||||
const current = this.getSlaVevaphonInstability();
|
||||
const next = Math.max(0, Math.min(12, current + delta));
|
||||
await this.update({ "system.sla.vevaphonInstability.value": next });
|
||||
if (next >= 12) {
|
||||
ui.notifications.warn(`${this.name} has reached maximum Instability (12). The Vevaphon is lost — immediate retirement or death.`);
|
||||
} else if (next >= 10) {
|
||||
ui.notifications.warn(`${this.name} — Instability ${next}/12. Morph Panic risk: failed Sanity save triggers a Morph Panic event.`);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
async enforceSlaSpeciesWounds() {
|
||||
if (this.type !== "character") return null;
|
||||
const minimumWounds = this.getSlaSpeciesMinimumWounds();
|
||||
|
||||
@@ -8,7 +8,9 @@ export const SLA_SPECIES_STAT_ADJUSTMENTS = {
|
||||
"Wraith Raider": { sanity: 15 },
|
||||
Shaktar: { body: 10, fear: 20 },
|
||||
"Stormer 313 Malice": { fear: 25 },
|
||||
"Stormer 711 Xeno": { fear: 25 }
|
||||
"Stormer 711 Xeno": { fear: 25 },
|
||||
// Vevaphon: strong, fast, resilient biogenetic chassis — psychologically shattered
|
||||
"Stormer Vevaphon": { strength: 10, combat: 10, body: 15, speed: 5, sanity: -10 }
|
||||
};
|
||||
|
||||
export function getSlaSpeciesStatAdjustments(speciesName = "") {
|
||||
|
||||
@@ -69,6 +69,10 @@ const SPECIES_DOSSIER = {
|
||||
"Advanced Carrien": {
|
||||
identity: "Cunning, dangerous mutants with strong survival instincts and an uneasy fit inside official structures.",
|
||||
fieldPressure: "They thrive in broken districts and dirty jobs, but prejudice follows them into every briefing room."
|
||||
},
|
||||
"Stormer Vevaphon": {
|
||||
identity: "Sleight Industries' cancelled polymorphic Stormer line: a shape-shifting bioweapon that can be anything the mission needs — and that is precisely the problem.",
|
||||
fieldPressure: "They are terrifying in infiltration, ambush, and close quarters, but every form change costs them a piece of who they are. Long ops risk unravelling them entirely."
|
||||
}
|
||||
};
|
||||
|
||||
@@ -76,7 +80,8 @@ const SPECIES_MIN_WOUNDS = {
|
||||
Frother: 3,
|
||||
Shaktar: 3,
|
||||
"Stormer 313 Malice": 3,
|
||||
"Stormer 711 Xeno": 3
|
||||
"Stormer 711 Xeno": 3,
|
||||
"Stormer Vevaphon": 3
|
||||
};
|
||||
|
||||
const SKILL_IMAGE_OVERRIDES = {
|
||||
@@ -201,6 +206,19 @@ const SPECIES_MOTHERSHIP_PROFILES = {
|
||||
health: "1d10+11"
|
||||
},
|
||||
description: "Clever survivor profile. Strong Intellect and workable Strength make Carrien dangerous problem-solvers with uneven social acceptance."
|
||||
},
|
||||
"Stormer Vevaphon": {
|
||||
rolls: {
|
||||
strength: "2d10+35",
|
||||
speed: "2d10+28",
|
||||
intellect: "2d10+20",
|
||||
combat: "2d10+35",
|
||||
sanity: "2d10+12",
|
||||
fear: "2d10+23",
|
||||
body: "2d10+22",
|
||||
health: "1d10+12"
|
||||
},
|
||||
description: "Polymorphic bioweapon with excellent combat and physical resilience, above-average speed, and severely degraded Sanity reflecting continuous identity diffusion from shapeshifting."
|
||||
}
|
||||
};
|
||||
|
||||
@@ -293,9 +311,60 @@ const SPECIES_WEAPONS = {
|
||||
"Wraith Raider": ["Wraith Raider Claws"],
|
||||
Shaktar: ["Shaktar Claws"],
|
||||
"Stormer 313 Malice": ["Stormer Claws"],
|
||||
"Stormer 711 Xeno": ["Stormer Claws"]
|
||||
"Stormer 711 Xeno": ["Stormer Claws"],
|
||||
"Stormer Vevaphon": ["Morphic Strike"]
|
||||
};
|
||||
|
||||
// Vevaphon morph form abilities — one is chosen at character creation as starting form.
|
||||
// Each form grants stat bonuses and a special ability but costs 1 Instability when activated.
|
||||
const VEVAPHON_MORPH_STARTERS = [
|
||||
{
|
||||
name: "Brute Form",
|
||||
summary: "A hulking, armoured chassis. STR+10, BODY+10. Immune to Knockdown. Cannot be concealed.",
|
||||
statMods: { strength: 10, body: 10 },
|
||||
special: "Knockdown Immunity",
|
||||
instabilityCost: 1,
|
||||
description: "The Vevaphon expands its frame into a dense combat shell. Slabs of biogenetic armour plate reinforce the torso and limbs. Effective in close-quarters breach work. Every activation costs 1 Instability."
|
||||
},
|
||||
{
|
||||
name: "Stalker Form",
|
||||
summary: "A lean, predatory chassis built for silence. SPD+10, Stealth+20. Cannot wear armour.",
|
||||
statMods: { speed: 10 },
|
||||
skillMods: { Stealth: 20 },
|
||||
special: "Armour Incompatible",
|
||||
instabilityCost: 1,
|
||||
description: "The Vevaphon elongates and flattens, redistributing mass for near-silent movement. Standard armour cannot bond to the shifting surface. Ideal for covert approach and ambush. Every activation costs 1 Instability."
|
||||
},
|
||||
{
|
||||
name: "Raptor Form",
|
||||
summary: "A fast striking chassis built around offensive output. COM+10, SPD+5. Attacks score one extra wound on a critical.",
|
||||
statMods: { combat: 10, speed: 5 },
|
||||
special: "Critical Wound Bonus",
|
||||
instabilityCost: 1,
|
||||
description: "The Vevaphon reshapes its strike limbs into natural weapons optimised for penetrating force. Designed for rapid target elimination at close range. Every activation costs 1 Instability."
|
||||
}
|
||||
];
|
||||
|
||||
// Instability minor effects (triggered at 6+ Instability, rolled each scene)
|
||||
const VEVAPHON_INSTABILITY_MINOR = [
|
||||
"Involuntary surface texturing — skin shifts pattern mid-conversation. Social rolls at -10.",
|
||||
"Limb proportions drift. Fine motor tasks at -10 until end of scene.",
|
||||
"Voice shifts register unexpectedly. Communications may be misidentified.",
|
||||
"Brief facial blurring. Anyone attempting to describe the Vevaphon gives contradictory reports.",
|
||||
"Pain from realignment. Take 1 point of Stress.",
|
||||
"Form flickers. One randomly selected stat drops by 5 until the Vevaphon rests."
|
||||
];
|
||||
|
||||
// Instability Morph Panic table (triggered at 10+ Instability on a failed SANITY save)
|
||||
const VEVAPHON_INSTABILITY_TABLE = [
|
||||
{ roll: 1, result: "Cascade Shift", effect: "Vevaphon immediately shifts to a random morph form, even mid-combat. Costs 1 additional Instability." },
|
||||
{ roll: 2, result: "Identity Bleed", effect: "Forgets assigned name and call sign until end of scene. Acts on instinct — GM adjudicates behaviour." },
|
||||
{ roll: 3, result: "Partial Lock", effect: "One limb freezes in incorrect form. -10 to all physical rolls using that limb." },
|
||||
{ roll: 4, result: "Tissue Rejection", effect: "Takes 1d5 damage as the body fights itself. Armour does not apply." },
|
||||
{ roll: 5, result: "Threat Imprint", effect: "Becomes locked on nearest visible target. Must make a Sanity save to perform any action not directed at that target." },
|
||||
{ roll: 6, result: "Full Reversion", effect: "Collapses to base form, losing all morph bonuses. Spends next action recovering. Lose 1 Instability." }
|
||||
];
|
||||
|
||||
const NAME_PARTS = {
|
||||
first: [
|
||||
"Ari", "Bex", "Cade", "Dax", "Eris", "Finn", "Galen", "Hex", "Iris", "Jax",
|
||||
@@ -610,6 +679,10 @@ export class SLAMothershipGenerator extends FormApplication {
|
||||
itemPayloads.push(...this.buildEbbItems(species.name));
|
||||
}
|
||||
|
||||
if (species.name === "Stormer Vevaphon") {
|
||||
itemPayloads.push(...this.buildVevaphonItems(derived.startingMorphForm));
|
||||
}
|
||||
|
||||
const uniquePayloads = uniqueEmbeddedItems(itemPayloads);
|
||||
const packageLoadout = getPackageLoadout(trainingPackage?.name ?? "", data.equipment.packageLoadouts ?? {});
|
||||
const packageIssueKit = getPackageIssueKit(trainingPackage?.name ?? "");
|
||||
@@ -664,11 +737,18 @@ export class SLAMothershipGenerator extends FormApplication {
|
||||
"system.sla.ebbRating.value": derived.ebbRating,
|
||||
"system.other.stressdesc.value": buildTraumaResponseText(species, trainingPackage),
|
||||
"system.sla.fluxNotes.value": this.isEbbSpecies(species.name)
|
||||
? `Starting Flux reserve: ${derived.fluxMax}. Ebb users begin with at least 20 Flux and spend from that reserve when channeling. Fear is used as the control save.`
|
||||
? `Starting Flux reserve: ${derived.fluxMax} (rolled 20 + 1d10). Flux is randomised at creation (range 21–31). Spend from this reserve when channeling; Fear is the control save.`
|
||||
: "",
|
||||
"system.sla.ebbNotes.value": this.isEbbSpecies(species.name)
|
||||
? `Starter discipline package: ${(EBB_STARTERS[species.name] ?? []).join(", ")}`
|
||||
: "",
|
||||
...(species.name === "Stormer Vevaphon" ? {
|
||||
"system.sla.morphForm.value": derived.startingMorphForm ?? "Brute Form",
|
||||
"system.sla.vevaphonInstability.value": 0,
|
||||
"system.sla.vevaphonInstability.max": 12,
|
||||
"system.sla.vevaphonInstability.label": "Instability",
|
||||
"system.sla.morphNotes.value": buildVevaphonMorphNotes(derived.startingMorphForm)
|
||||
} : {}),
|
||||
"system.biography": description,
|
||||
"system.notes": buildNotes(species, trainingPackage, packageLoadout, packageIssueKit)
|
||||
};
|
||||
@@ -815,6 +895,40 @@ export class SLAMothershipGenerator extends FormApplication {
|
||||
.map((item) => toEmbeddedData(item));
|
||||
}
|
||||
|
||||
static buildVevaphonItems(startingMorphForm) {
|
||||
const items = [];
|
||||
// Try to add all three morph form abilities from the world item compendium.
|
||||
// They may or may not exist as world items — we fall back to building minimal
|
||||
// ability documents directly if not found.
|
||||
for (const morph of VEVAPHON_MORPH_STARTERS) {
|
||||
const worldItem = findWorldItem(morph.name, "ability");
|
||||
if (worldItem) {
|
||||
const embedded = toEmbeddedData(worldItem);
|
||||
items.push(embedded);
|
||||
} else {
|
||||
// Build a minimal ability document so the character isn't empty-handed.
|
||||
items.push({
|
||||
name: morph.name,
|
||||
type: "ability",
|
||||
img: "icons/svg/upgrade.svg",
|
||||
system: {
|
||||
description: morph.description,
|
||||
summary: morph.summary,
|
||||
special: morph.special,
|
||||
sla: {
|
||||
morphForm: true,
|
||||
instabilityCost: morph.instabilityCost,
|
||||
statMods: morph.statMods ?? {},
|
||||
skillMods: morph.skillMods ?? {},
|
||||
isStartingForm: morph.name === startingMorphForm
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
static isEbbSpecies(speciesName) {
|
||||
return ["Ebon", "Brain Waster"].includes(String(speciesName ?? ""));
|
||||
}
|
||||
@@ -952,7 +1066,16 @@ function deriveProfile(rolls, packageName, speciesName) {
|
||||
|
||||
if (["Ebon", "Brain Waster"].includes(speciesName)) {
|
||||
profile.ebbRating = clamp(Math.ceil(profile.intellect / 20) + Number(mods.ebbRating ?? 0), 1, 8);
|
||||
profile.fluxMax = clamp(Math.max(20, Math.floor(profile.intellect / 4)), 20, 40);
|
||||
// Starting flux: 20 + 1d10 (range 21–31), randomised at character creation
|
||||
const fluxRoll = Math.floor(Math.random() * 11) + 1; // 1–11 → simulates d10 with extended top end
|
||||
profile.fluxMax = 20 + fluxRoll; // 21–31
|
||||
}
|
||||
|
||||
if (speciesName === "Stormer Vevaphon") {
|
||||
// Instability starts at 0. Pick a random starting morph form.
|
||||
profile.morphInstability = 0;
|
||||
const morphIndex = Math.floor(Math.random() * VEVAPHON_MORPH_STARTERS.length);
|
||||
profile.startingMorphForm = VEVAPHON_MORPH_STARTERS[morphIndex].name;
|
||||
}
|
||||
|
||||
return profile;
|
||||
@@ -1032,6 +1155,26 @@ function getFluxStage(flux, maxFlux = 0) {
|
||||
return { label: "Stable", text: "Flux reserves are healthy. Ebb use remains disciplined and contained." };
|
||||
}
|
||||
|
||||
function buildVevaphonMorphNotes(startingMorphForm) {
|
||||
const form = VEVAPHON_MORPH_STARTERS.find((entry) => entry.name === startingMorphForm) ?? VEVAPHON_MORPH_STARTERS[0];
|
||||
const allForms = VEVAPHON_MORPH_STARTERS.map((entry) => `${entry.name}: ${entry.summary}`).join("\n");
|
||||
return [
|
||||
`Starting Morph Form: ${form.name}`,
|
||||
`${form.description}`,
|
||||
``,
|
||||
`Available Forms:`,
|
||||
allForms,
|
||||
``,
|
||||
`Instability (0–12): Each morph activation costs 1 Instability. At 6+, minor effects trigger each scene. At 10+, failed Sanity saves trigger Morph Panic. At 12, the Vevaphon is lost — immediate retirement or death.`,
|
||||
``,
|
||||
`Minor Effects (6+ Instability):`,
|
||||
VEVAPHON_INSTABILITY_MINOR.join("\n"),
|
||||
``,
|
||||
`Morph Panic Table (10+ Instability, failed Sanity):`,
|
||||
VEVAPHON_INSTABILITY_TABLE.map((entry) => `${entry.roll}. ${entry.result}: ${entry.effect}`).join("\n")
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function buildTraumaResponseText(species, trainingPackage) {
|
||||
return buildSlaTraumaResponseText(species?.name ?? "", trainingPackage?.name ?? "");
|
||||
}
|
||||
|
||||
+63
-53
@@ -11,24 +11,24 @@
|
||||
"value": 10,
|
||||
"min": 0,
|
||||
"max": 10,
|
||||
"label" : "Health"
|
||||
"label": "Health"
|
||||
},
|
||||
"hits": {
|
||||
"value": 0,
|
||||
"min": 0,
|
||||
"max" : 2,
|
||||
"label" : "Wounds"
|
||||
"max": 2,
|
||||
"label": "Wounds"
|
||||
},
|
||||
"netHP": {
|
||||
"value": 20,
|
||||
"min": 0,
|
||||
"max" : 20,
|
||||
"label" : "Net HP"
|
||||
"max": 20,
|
||||
"label": "Net HP"
|
||||
},
|
||||
"bleeding": {
|
||||
"value": 0,
|
||||
"min": 0,
|
||||
"label" : "Bleeding"
|
||||
"label": "Bleeding"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -38,9 +38,9 @@
|
||||
],
|
||||
"biography": "",
|
||||
"notes": "",
|
||||
"weight" : {
|
||||
"current" : 0,
|
||||
"capacity" : 0
|
||||
"weight": {
|
||||
"current": 0,
|
||||
"capacity": 0
|
||||
},
|
||||
"class": {
|
||||
"value": ""
|
||||
@@ -59,8 +59,8 @@
|
||||
},
|
||||
"xp": {
|
||||
"value": 0,
|
||||
"html" : 0,
|
||||
"selectedSkill" : ""
|
||||
"html": 0,
|
||||
"selectedSkill": ""
|
||||
},
|
||||
"attributes": {
|
||||
"level": {
|
||||
@@ -140,13 +140,13 @@
|
||||
"value": 2,
|
||||
"min": 2,
|
||||
"max": 20,
|
||||
"label" : "Stress"
|
||||
"label": "Stress"
|
||||
},
|
||||
"resolve": {
|
||||
"value": 0,
|
||||
"min": 0,
|
||||
"max": 15,
|
||||
"label" : "Resolve"
|
||||
"label": "Resolve"
|
||||
},
|
||||
"stressdesc": {
|
||||
"value": ""
|
||||
@@ -216,6 +216,17 @@
|
||||
},
|
||||
"ebbNotes": {
|
||||
"value": ""
|
||||
},
|
||||
"morphForm": {
|
||||
"value": ""
|
||||
},
|
||||
"vevaphonInstability": {
|
||||
"value": 0,
|
||||
"max": 12,
|
||||
"label": "Instability"
|
||||
},
|
||||
"morphNotes": {
|
||||
"value": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -237,7 +248,7 @@
|
||||
"max": 99,
|
||||
"label": "Combat",
|
||||
"rollLabel": "Combat Check",
|
||||
"enabled" : true
|
||||
"enabled": true
|
||||
},
|
||||
"instinct": {
|
||||
"value": 10,
|
||||
@@ -245,7 +256,7 @@
|
||||
"max": 99,
|
||||
"label": "Instinct",
|
||||
"rollLabel": "Instinct Check",
|
||||
"enabled" : true
|
||||
"enabled": true
|
||||
},
|
||||
"speed": {
|
||||
"value": 10,
|
||||
@@ -253,7 +264,7 @@
|
||||
"max": 99,
|
||||
"label": "Speed",
|
||||
"rollLabel": "Speed Check",
|
||||
"enabled" : false
|
||||
"enabled": false
|
||||
},
|
||||
"loyalty": {
|
||||
"value": 10,
|
||||
@@ -261,7 +272,7 @@
|
||||
"max": 99,
|
||||
"label": "Loyalty",
|
||||
"rollLabel": "Loyalty Check",
|
||||
"enabled" : false
|
||||
"enabled": false
|
||||
},
|
||||
"armor": {
|
||||
"value": 0,
|
||||
@@ -272,7 +283,7 @@
|
||||
"cover": "none",
|
||||
"label": "Armor",
|
||||
"rollLabel": "Armor Save",
|
||||
"enabled" : false
|
||||
"enabled": false
|
||||
},
|
||||
"sanity": {
|
||||
"value": 10,
|
||||
@@ -280,7 +291,7 @@
|
||||
"max": 99,
|
||||
"label": "Sanity",
|
||||
"rollLabel": "Sanity Save",
|
||||
"enabled" : false
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -295,18 +306,18 @@
|
||||
"xp": {
|
||||
"value": 1
|
||||
},
|
||||
"images" : {
|
||||
"layout" : "systems/sla-mothership/images/Galloway_Top.png",
|
||||
"beauty" : "systems/sla-mothership/images/Galloway_Top.png"
|
||||
"images": {
|
||||
"layout": "systems/sla-mothership/images/Galloway_Top.png",
|
||||
"beauty": "systems/sla-mothership/images/Galloway_Top.png"
|
||||
},
|
||||
"runSetup" : true,
|
||||
"megadamage" : {
|
||||
"html" : "",
|
||||
"hits" : [],
|
||||
"menu" : {
|
||||
"html" : ""
|
||||
"runSetup": true,
|
||||
"megadamage": {
|
||||
"html": "",
|
||||
"hits": [],
|
||||
"menu": {
|
||||
"html": ""
|
||||
},
|
||||
"open" : false
|
||||
"open": false
|
||||
},
|
||||
"stats": {
|
||||
"armor": {
|
||||
@@ -370,9 +381,9 @@
|
||||
"hull": {
|
||||
"value": 10,
|
||||
"max": 10,
|
||||
"75" : 0,
|
||||
"50" : 0,
|
||||
"25" : 0
|
||||
"75": 0,
|
||||
"50": 0,
|
||||
"25": 0
|
||||
},
|
||||
"fuel": {
|
||||
"value": 10,
|
||||
@@ -400,10 +411,10 @@
|
||||
},
|
||||
"upgrades": {
|
||||
"value": 0,
|
||||
"max" : 0
|
||||
"max": 0
|
||||
}
|
||||
},
|
||||
"weapon-stats" : {
|
||||
"weapon-stats": {
|
||||
"weapons": {
|
||||
"value": 0,
|
||||
"max": 1
|
||||
@@ -416,7 +427,6 @@
|
||||
"value": 0,
|
||||
"max": 1
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -447,7 +457,7 @@
|
||||
"quantity": 1,
|
||||
"weight": 0,
|
||||
"carryState": "carried",
|
||||
"cost" : 0,
|
||||
"cost": 0,
|
||||
"sla": {
|
||||
"category": "",
|
||||
"power": "",
|
||||
@@ -495,7 +505,7 @@
|
||||
"ammo": 10,
|
||||
"shots": 1,
|
||||
"curShots": 0,
|
||||
"shotsPerFire" : 1,
|
||||
"shotsPerFire": 1,
|
||||
"useAmmo": false,
|
||||
"ammoType": "",
|
||||
"ammoCalibre": "",
|
||||
@@ -511,22 +521,22 @@
|
||||
"ammoReserveAp": 0,
|
||||
"ammoReserveHe": 0,
|
||||
"ammoReserveHeap": 0,
|
||||
"wound":"",
|
||||
"wound": "",
|
||||
"critDmg": "",
|
||||
"woundEffect": "",
|
||||
"bonus": 0,
|
||||
"weight": 0,
|
||||
"carryState": "equipped",
|
||||
"equipped": true,
|
||||
"cost" : 0,
|
||||
"cost": 0,
|
||||
"ranges": {
|
||||
"short": 0,
|
||||
"medium": 0,
|
||||
"long": 0,
|
||||
"value" : ""
|
||||
"value": ""
|
||||
},
|
||||
"settings" : {
|
||||
"firstEdition" : false
|
||||
"settings": {
|
||||
"firstEdition": false
|
||||
},
|
||||
"sla": {
|
||||
"weaponType": "",
|
||||
@@ -548,14 +558,14 @@
|
||||
"base"
|
||||
],
|
||||
"armorPoints": 1,
|
||||
"damageReduction":0,
|
||||
"damageReduction": 0,
|
||||
"speed": "",
|
||||
"oxygenMax": 0,
|
||||
"oxygenCurrent": 0,
|
||||
"oxygenCurrent": 0,
|
||||
"weight": 0,
|
||||
"carryState": "equipped",
|
||||
"equipped": true,
|
||||
"cost" : 0,
|
||||
"cost": 0,
|
||||
"sla": {
|
||||
"burden": "",
|
||||
"coverage": "",
|
||||
@@ -572,9 +582,9 @@
|
||||
"base"
|
||||
],
|
||||
"severity": 1,
|
||||
"treatment" : {
|
||||
"value" : 0,
|
||||
"html" : ""
|
||||
"treatment": {
|
||||
"value": 0,
|
||||
"html": ""
|
||||
},
|
||||
"sla": {
|
||||
"category": "",
|
||||
@@ -622,7 +632,7 @@
|
||||
"base"
|
||||
],
|
||||
"text": "",
|
||||
"job" : ""
|
||||
"job": ""
|
||||
},
|
||||
"repair": {
|
||||
"templates": [
|
||||
@@ -637,9 +647,9 @@
|
||||
],
|
||||
"quantity": 1,
|
||||
"hull": 1,
|
||||
"totalHull" : 1,
|
||||
"feature" : "",
|
||||
"offline" : false
|
||||
"totalHull": 1,
|
||||
"feature": "",
|
||||
"offline": false
|
||||
},
|
||||
"class": {
|
||||
"templates": [
|
||||
@@ -702,4 +712,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,6 +381,7 @@
|
||||
<a class="tab-select" data-tab="traits">Traits</a>
|
||||
<a class="tab-select" data-tab="special-rules">Special Rules</a>
|
||||
{{#if isEbbUser}}<a class="tab-select" data-tab="ebb">Ebb</a>{{/if}}
|
||||
{{#if isVevaphon}}<a class="tab-select" data-tab="morph">Morph</a>{{/if}}
|
||||
<a class="tab-select" data-tab="description">{{ localize 'Mosh.Bio' }}</a>
|
||||
<a class="tab-select" data-tab="notes">{{ localize 'Mosh.Notes' }}</a>
|
||||
</nav>
|
||||
@@ -412,6 +413,7 @@
|
||||
<span class="sla-tag">{{system.sla.bpn.value}}</span>
|
||||
<span class="sla-tag">SCL {{system.sla.scl.value}}</span>
|
||||
{{#if isEbbUser}}<span class="sla-tag">Ebb {{system.sla.ebbRating.value}}</span>{{/if}}
|
||||
{{#if isVevaphon}}<span class="sla-tag sla-instability-tag {{vevaphonInstabilityClass}}">Instability {{system.sla.vevaphonInstability.value}}/12</span>{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -814,6 +816,80 @@
|
||||
</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>
|
||||
<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}}
|
||||
|
||||
Reference in New Issue
Block a user