diff --git a/css/mosh.css b/css/mosh.css index 27e3b2d..932f93b 100644 --- a/css/mosh.css +++ b/css/mosh.css @@ -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, diff --git a/module/actor/actor-sheet.js b/module/actor/actor-sheet.js index 8505c76..3e222c4 100644 --- a/module/actor/actor-sheet.js +++ b/module/actor/actor-sheet.js @@ -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); }); diff --git a/module/actor/actor.js b/module/actor/actor.js index a0d1846..88de531 100644 --- a/module/actor/actor.js +++ b/module/actor/actor.js @@ -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 + `
${skillList}
` + 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(); diff --git a/module/sla-species-balance.js b/module/sla-species-balance.js index 99ae4e8..9b18ed6 100644 --- a/module/sla-species-balance.js +++ b/module/sla-species-balance.js @@ -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 = "") { diff --git a/module/windows/sla-character-generator.js b/module/windows/sla-character-generator.js index 8996fe0..fdea192 100644 --- a/module/windows/sla-character-generator.js +++ b/module/windows/sla-character-generator.js @@ -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 ?? ""); } diff --git a/template.json b/template.json index 2e2611e..6ee5131 100644 --- a/template.json +++ b/template.json @@ -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 @@ } } } -} +} \ No newline at end of file diff --git a/templates/actor/actor-sheet.html b/templates/actor/actor-sheet.html index ba9c8d4..104f2ff 100644 --- a/templates/actor/actor-sheet.html +++ b/templates/actor/actor-sheet.html @@ -381,6 +381,7 @@ Traits Special Rules {{#if isEbbUser}}Ebb{{/if}} + {{#if isVevaphon}}Morph{{/if}} {{ localize 'Mosh.Bio' }} {{ localize 'Mosh.Notes' }} @@ -412,6 +413,7 @@ {{system.sla.bpn.value}} SCL {{system.sla.scl.value}} {{#if isEbbUser}}Ebb {{system.sla.ebbRating.value}}{{/if}} + {{#if isVevaphon}}Instability {{system.sla.vevaphonInstability.value}}/12{{/if}} @@ -814,6 +816,80 @@ {{/if}} + {{#if isVevaphon}} +
+
+
+
+
Stormer Vevaphon
+
Polymorphic Form Control
+
+
+ Form: {{system.sla.morphForm.value}} + Instability {{system.sla.vevaphonInstability.value}} / 12 + Sanity {{system.stats.sanity.value}} +
+
+ +
+
+
Instability Track
+
+
+
+
+ {{#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}} +
+
+ + +
+
At 12, the Vevaphon is lost — immediate retirement or death.
+
+
+
Active Morph Form
+
{{system.sla.morphForm.value}}
+
Each morph shift costs 1 Instability. Changing form mid-scene requires a Sanity save at Instability 6+.
+
+ + + +
+
+
+ +
+
+
Morph Panic Reference
+
Triggered at Instability 10+ on a failed Sanity save. Roll 1d6:
+
+
1. Cascade Shift — random morph form, +1 Instability.
+
2. Identity Bleed — forgets name and callsign until end of scene.
+
3. Partial Lock — one limb freezes. -10 physical rolls using that limb.
+
4. Tissue Rejection — 1d5 damage, armour does not apply.
+
5. Threat Imprint — locked on nearest visible target.
+
6. Full Reversion — base form, lose morph bonuses, -1 Instability.
+
+
+
+
Morph Notes
+ +
+
+
+
+ {{/if}} {{!-- Owned Items Tab --}}
{{#if drugAlerts.length}}