diff --git a/assets/blade-runner-banner-small.webp b/assets/blade-runner-banner-small.webp deleted file mode 100644 index 4743698..0000000 Binary files a/assets/blade-runner-banner-small.webp and /dev/null differ diff --git a/assets/blade-runner-cover.webp b/assets/blade-runner-cover.webp deleted file mode 100644 index 1b998de..0000000 Binary files a/assets/blade-runner-cover.webp and /dev/null differ diff --git a/assets/splash.webp b/assets/splash.webp deleted file mode 100644 index e846607..0000000 Binary files a/assets/splash.webp and /dev/null differ diff --git a/slazero2.mjs b/slazero2.mjs index bb9be73..2b17162 100644 --- a/slazero2.mjs +++ b/slazero2.mjs @@ -8,6 +8,23 @@ const ZERO_ENGINE_CORE_COMBAT = { captured: false }; + +/** + * Show a Dice So Nice animation without ever blocking the roll pipeline. + * DSN can stall (no canvas, GPU issues, module conflicts); rolls must not. + */ +async function dsnSafeShow(roll) { + if (!game.dice3d) return; + try { + await Promise.race([ + game.dice3d.showForRoll(roll, game.user, true), + new Promise(resolve => setTimeout(resolve, 3500)) + ]); + } catch (err) { + console.warn("SLA Zero Two | Dice So Nice display failed:", err); + } +} + function restoreZeroEngineCoreCombatConfig() { const { combatClass, combatantClass, combatTrackerClass } = ZERO_ENGINE_CORE_COMBAT; if (combatClass) CONFIG.Combat.documentClass = combatClass; @@ -391,6 +408,9 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { String(w.system.range ?? "").toLowerCase() === "engaged"; context.meleeWeapons = weapons.filter(w => isMeleeWeapon(w)); context.rangedWeapons = weapons.filter(w => !isMeleeWeapon(w)); + context.naturalWeapons = this.actor.items + .filter(i => i.type === "specialty" && i.system?.isNaturalWeapon === true) + .map(i => i.toObject(false)); context.armors = this.actor.items.filter(i => i.type === "armor").map(i => i.toObject(false)); // Expose equipped armor name for header display context.equippedArmorName = context.armors[0]?.name || null; @@ -421,7 +441,7 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { // EBB user detection — Ebon and Brain Waster are ebb users const race = String(this.actor.system.race || "").toLowerCase(); // Flat string versions for dropdown selected-state matching in templates - context.raceStr = race; + context.raceStr = race.replace(/[\s-]/g, "") === "wraith" ? "wraithraider" : race.replace(/[\s-]/g, ""); const trainingRaw = this.actor.system.training; context.trainingStr = String(Array.isArray(trainingRaw) ? (trainingRaw[0] || "") : (trainingRaw || "")).toLowerCase(); context.isEbbUser = (race === "ebon" || race === "brainwaster"); @@ -1135,7 +1155,7 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { const roll = await (new Roll(`${pool}d6`)).evaluate(); if (game.dice3d) { - await game.dice3d.showForRoll(roll, game.user, true); + await dsnSafeShow(roll); } const successes = (roll.dice?.[0]?.results || []).filter(r => r.result === 6).length; const failed = successes <= 0; @@ -1535,7 +1555,7 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { const roll = new Roll(`${pool}d6`); await roll.evaluate(); - if (game.dice3d) await game.dice3d.showForRoll(roll, game.user, true); + if (game.dice3d) await dsnSafeShow(roll); const results = roll.dice[0]?.results || []; const successes = results.filter(r => r.result === 6).length; @@ -2556,7 +2576,7 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { if (effectiveDice > 0) { armorRoll = await (new Roll(`${effectiveDice}d6`)).evaluate(); if (game.dice3d) { - await game.dice3d.showForRoll(armorRoll, game.user, true); + await dsnSafeShow(armorRoll); } armorSuccesses = (armorRoll.dice?.[0]?.results || []).filter(r => r.result === 6).length; } @@ -2656,7 +2676,7 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { // Show Dice So Nice animation if available if (game.dice3d) { this._applyDiceSoNiceColors(roll, normalDice, stressDice); - await game.dice3d.showForRoll(roll, game.user, true); + await dsnSafeShow(roll); } const ebbInfo = rollOptions.ebbInfo || null; @@ -2923,7 +2943,7 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { // Show Dice So Nice animation if available if (game.dice3d) { this._applyDiceSoNiceColors(reroll, normalRerollCount, stressRerollCount + addedStressDice); - await game.dice3d.showForRoll(reroll, game.user, true); + await dsnSafeShow(reroll); } // Combine locked dice with new results @@ -3158,7 +3178,7 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { await roll.evaluate(); if (game.dice3d) { - await game.dice3d.showForRoll(roll, game.user, true); + await dsnSafeShow(roll); } const rollSuccesses = roll.dice[0].results.filter(r => r.result === 6).length; @@ -3358,7 +3378,7 @@ class ZeroEngineActorSheet extends foundry.appv1.sheets.ActorSheet { const roll = await (new Roll("2d6")).evaluate(); if (game.dice3d) { - await game.dice3d.showForRoll(roll, game.user, true); + await dsnSafeShow(roll); } await table.draw({ roll, displayChat: true }); } @@ -4513,16 +4533,27 @@ async function runAutoCritical(actor, kind) { }, actor); } +// Racial derived-stat modifiers — mirrors SLA_RACES healthMod/resolveMod. +// v2.0.1: these were all zero in Zero Engine 1.7.6, silently dropping every +// racial Health/Resolve bonus from actual play. Keys are canonical race keys. const RACE_DERIVED_MODS = { - human: { health: 0, resolve: 0 }, - ebon: { health: 0, resolve: 0 }, - brainwaster: { health: 0, resolve: 0 }, - stormer: { health: 0, resolve: 0 }, - shaktar: { health: 0, resolve: 0 }, - wraith: { health: 0, resolve: 0 }, - frother: { health: 0, resolve: 0 } + human: { health: 0, resolve: 0 }, + ebon: { health: 0, resolve: 1 }, + brainwaster: { health: 0, resolve: 0 }, + stormer: { health: 2, resolve: 0 }, + shaktar: { health: 1, resolve: 0 }, + wraithraider: { health: 0, resolve: 1 }, + frother: { health: 1, resolve: -1 }, + vevaphon: { health: 2, resolve: -1 } }; +/** Canonicalize a stored race string to a RACE_DERIVED_MODS / SLA_RACES key */ +function canonicalRaceKey(raw) { + const k = String(raw || "").toLowerCase().replace(/[\s-]/g, "").replace("313s", "").trim(); + if (k === "wraith") return "wraithraider"; + return k; +} + const TRAINING_DERIVED_MODS = { scout: { health: 0, resolve: 0 }, investigator: { health: 0, resolve: 0 }, @@ -4569,7 +4600,7 @@ function getSpecialtyDerivedMod(item, stat) { function calculateHealthResolve(actor) { const attrs = actor?.system?.attributes || {}; - const raceKey = String(actor?.system?.race || "").trim().toLowerCase(); + const raceKey = canonicalRaceKey(actor?.system?.race); const trainingKey = String(actor?.system?.training || "").trim().toLowerCase(); const baseHealth = toNumber(attrs?.strength?.value, 0) + toNumber(attrs?.agility?.value, 0); @@ -5293,7 +5324,17 @@ class SLAPCCreator extends Application { this._showStep(html, 3); this._renderAttrTable(html); this._getEl(html, '#gen-back-3').addEventListener('click', () => { this._step = 2; this._bindStep2(html); }); - this._getEl(html, '#gen-next-3').addEventListener('click', () => { this._step = 4; this._bindStep4(html); }); + this._getEl(html, '#gen-next-3').addEventListener('click', (ev) => { + const race = SLA_RACES[this._race]; + const left = this._attrFreePoints - Object.entries(this._attrs).reduce((a,[k,v]) => a+(v-race.baseAttrs[k]),0); + if (left > 0 && !ev.currentTarget.dataset.confirmed) { + ev.currentTarget.dataset.confirmed = "1"; + ev.currentTarget.textContent = `${left} point${left!==1?'s':''} unspent — Next anyway?`; + setTimeout(() => { delete ev.target?.dataset?.confirmed; }, 4000); + return; + } + this._step = 4; this._bindStep4(html); + }); } _renderAttrTable(html) { @@ -5341,7 +5382,18 @@ class SLAPCCreator extends Application { this._showStep(html, 4); this._renderSkillTable(html); this._getEl(html, '#gen-back-4').addEventListener('click', () => { this._step = 3; this._bindStep3(html); }); - this._getEl(html, '#gen-next-4').addEventListener('click', () => { this._step = 5; this._bindStep5(html); }); + this._getEl(html, '#gen-next-4').addEventListener('click', (ev) => { + const trainSkills = SLA_TRAINING[this._training]?.skills || {}; + const spent = Object.entries(this._skills).reduce((a,[k,v]) => a + Math.max(0, v - (trainSkills[k]||0)), 0); + const left = Math.max(0, this._skillPoints - spent); + if (left > 0 && !ev.currentTarget.dataset.confirmed) { + ev.currentTarget.dataset.confirmed = "1"; + ev.currentTarget.textContent = `${left} point${left!==1?'s':''} unspent — Next anyway?`; + setTimeout(() => { delete ev.target?.dataset?.confirmed; }, 4000); + return; + } + this._step = 5; this._bindStep5(html); + }); } _renderSkillTable(html) { @@ -10363,3 +10415,77 @@ Hooks.on('pauseGame', (paused) => { _zeRemovePauseAnimation(); } }); + +// ══════════════════════════════════════════════════════════════════════════════ +// SLA ZERO TWO — FIRST-RUN WORLD SETUP (v2.0.1) +// A brand-new world should greet the GM like SLA Industries, not a blank +// Foundry install: a branded landing scene and a Start Here briefing journal. +// ══════════════════════════════════════════════════════════════════════════════ + +Hooks.once("ready", async () => { + if (!game.user?.isGM) return; + const FLAG_KEY = "firstRunSetupDone"; + if (game.settings.storage.get("world")) { /* settings ready */ } + try { + game.settings.register("slazero2", FLAG_KEY, { + scope: "world", config: false, type: Boolean, default: false + }); + } catch (_) { /* already registered */ } + if (game.settings.get("slazero2", FLAG_KEY)) return; + + // ── Start Here journal ──────────────────────────────────────────────────── + if (!game.journal.getName("SLA ZERO TWO — START HERE")) { + await JournalEntry.create({ + name: "SLA ZERO TWO — START HERE", + pages: [{ + name: "Operative Briefing — Read First", + type: "text", + title: { show: false }, + text: { format: 1, content: ` +
+

WELCOME TO THE WORLD OF PROGRESS

+

You are a Gamemaster of SLA Zero Two. This page is your first shift briefing.

+

1. Recommended module

+

Enable Dice So Nice (Game Settings → Manage Modules) for the custom SLA Zero Two 3D dice — +hazard-yellow Operative dice and black-chrome Stress dice with the blood-red bane face.

+

2. Build the squad

+

In the Actors tab, press Generate PC. The wizard walks each player through race, +training package, attributes, skills, and a name. Set each finished PC's Player Character flag +(Biography tab) so the GM status board tracks them.

+

3. Build the opposition

+

Group NPCs stamps whole gangs with stats, art, weapons and folders — set the 1–10 threat slider +to the fight you want. Generate NPC makes single archetypes.

+

4. Hand out work

+

The BPN button tracks the active Blueprint News file. Credits flow through +Credits / Finance Ledger; every round fired auto-bills the +Bullet Tax (rounds × cost × 2 — ammunition is billed, not counted).

+

5. Run the fight

+

Initiative is on each sheet (AGI+MOB or WIT+OBS). Attacks live on the +Equipment tab — fire modes, ammo types, range, scope. Damage prompts the defender's armour roll. +Conditions tick automatically. When Health or Resolve breaks, the d66 critical roll is automatic.

+

The cameras are always on. Encourage Showing Off. Make them pay for it.

+
` } + }] + }); + console.log("SLA Zero Two | Start Here journal created"); + } + + // ── Branded landing scene ───────────────────────────────────────────────── + if (game.scenes.size === 0 || !game.scenes.find(s => s.name === "Mort — Downtown Grid")) { + const scene = await Scene.create({ + name: "Mort — Downtown Grid", + background: { src: "systems/slazero2/assets/SLA_Assets/Maps/slaguru6666_The_words_SLA_INDUSTRIES_in_neon_80s_grafitti_on__bbbf7678-0361-4632-8a3f-4134e9a764dc_1.png" }, + width: 1920, height: 1080, + padding: 0, + grid: { type: 0 }, + tokenVision: false, + fog: { exploration: false }, + backgroundColor: "#0a0a0a", + navigation: true + }); + if (game.scenes.size === 1) await scene.activate(); + console.log("SLA Zero Two | Landing scene created"); + } + + await game.settings.set("slazero2", FLAG_KEY, true); +}); diff --git a/styles/slazero2.css b/styles/slazero2.css index 90120c0..74fefb4 100644 --- a/styles/slazero2.css +++ b/styles/slazero2.css @@ -6788,3 +6788,45 @@ body > .ze-pause-root .ze-sat-inner { border-radius: 4px; } .slazero2 ::-webkit-scrollbar-thumb:hover { background: var(--sz2-yellow-dim); } + +/* ── V2.0.1 evaluation fixes ─────────────────────────────────────────────── */ + +/* Push button: never wrap mid-word */ +.yze-push-button { + white-space: nowrap; + padding: 8px 14px; +} + +/* Chat dice: even flow, no orphan wrap surprises */ +.yze-dice-display { + display: flex; + flex-wrap: wrap; + gap: 5px; + justify-content: flex-start; +} + +/* GM toolbar chips in the Actor directory: one language, not a sticker sheet */ +#actors .directory-header button, +#actors .header-actions button, +.sla-dir-status-btn, .sla-dir-credit-btn, .sla-dir-ledger-btn, .sla-dir-threat-btn, +.sla-dir-bpn-btn, .sla-dir-cond-btn, .sla-dir-groupnpc-btn, .sla-dir-finance-btn { + background: linear-gradient(180deg, #1d1d1d, #0a0a0a) !important; + border: 1px solid #2e2e2e !important; + border-radius: 3px !important; + color: #d8d8d0 !important; + font-family: "SLA Borg UI", sans-serif !important; + text-transform: uppercase; + letter-spacing: 1px; + transition: border-color .15s, color .15s; +} +#actors .directory-header button:hover, +#actors .header-actions button:hover { + border-color: #e3e300 !important; + color: #e3e300 !important; + box-shadow: 0 0 8px rgba(227, 227, 0, 0.25); +} + +/* Natural weapon rows share the weapon-item look with a claw accent */ +.slazero2 .natural-weapon-item { + border-left: 3px solid #9a9a00; +} diff --git a/templates/actor/character-sheet-mk1.hbs b/templates/actor/character-sheet-mk1.hbs index ac9992c..24fb12a 100644 --- a/templates/actor/character-sheet-mk1.hbs +++ b/templates/actor/character-sheet-mk1.hbs @@ -59,7 +59,7 @@ - + diff --git a/templates/actor/character-sheet-simple.hbs b/templates/actor/character-sheet-simple.hbs index d2781a2..f96593b 100644 --- a/templates/actor/character-sheet-simple.hbs +++ b/templates/actor/character-sheet-simple.hbs @@ -19,7 +19,7 @@ - + diff --git a/templates/actor/character-sheet.hbs b/templates/actor/character-sheet.hbs index ee3d04b..1df2e1a 100644 --- a/templates/actor/character-sheet.hbs +++ b/templates/actor/character-sheet.hbs @@ -59,7 +59,7 @@ - + @@ -602,6 +602,29 @@

Melee Weapons

+ {{#each naturalWeapons}} +
+ +
+
+ {{this.name}} + NATURAL +
+
+ DMG {{this.system.naturalWeaponDamage}} + AP {{this.system.naturalWeaponAP}} + ROF {{this.system.naturalWeaponROF}} + always armed +
+
+
+ +
+
+ {{/each}} {{#each meleeWeapons}}
@@ -633,7 +656,7 @@
{{/each}} - {{#unless meleeWeapons.length}} + {{#unless (or meleeWeapons.length naturalWeapons.length)}}
Drag melee weapons from the compendium.
{{/unless}}
diff --git a/templates/actor/character/character-sheet.hbs b/templates/actor/character/character-sheet.hbs index 44af0fb..00b69d6 100644 --- a/templates/actor/character/character-sheet.hbs +++ b/templates/actor/character/character-sheet.hbs @@ -56,7 +56,7 @@ - +