Straining the Weave (§17) — v0.9.8
Repeated castings of the same spell in one scene now raise its Threshold by +1 per prior cast, as the rules have always said. The sheet shows the current Threshold with a +N badge, "Settle the Weave" clears the scene's strain, and both rests settle it automatically. Implemented in the VRPG source; this repo carries the published system. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7f6c5af39a
commit
3b7d6e778d
@@ -376,6 +376,20 @@
|
||||
font-style: normal;
|
||||
margin-left: 4px;
|
||||
}
|
||||
/* Straining the Weave (§17) — repeats in a scene raise the Threshold */
|
||||
.vanity-sheet .strain-badge {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--v-plum);
|
||||
border: 1px solid var(--v-plum);
|
||||
border-radius: 3px;
|
||||
padding: 0 3px;
|
||||
margin-left: 3px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.vanity-sheet .bar a.strain-reset { float: right; font-size: 11px; letter-spacing: 0.04em; }
|
||||
.vanity-roll-dialog .strained { color: var(--v-plum); font-style: italic; }
|
||||
|
||||
.vanity-sheet a.item-attack i, .vanity-sheet a.item-cast i { color: var(--v-teal-d); font-size: 15px; }
|
||||
.vanity-sheet a.item-attack:hover i, .vanity-sheet a.item-cast:hover i { color: var(--v-gold-d); text-shadow: 0 0 6px rgba(176,135,47,.7); }
|
||||
.vanity-sheet a.item-edit i, .vanity-sheet a.item-delete i, .vanity-sheet a.item-spent i, .vanity-sheet a.item-equip i {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"id": "vanity",
|
||||
"title": "VANITY — the Vanity Role-Playing Game",
|
||||
"description": "A generic d6 RPG where ego is a resource and the universe keeps score. Pool = Attribute + Knack + situation; 5–6 = a Success; fail with two 1s = a Stumble that banks a Bane of Karma (Draft 13). No hit points — Grit. Vanity, Karma and Glory drive play. Includes the full Draft 13 rulebook, all spells and gear, the starter bestiary, a 100-item Treasures & Relics library, twenty ready-to-play characters (each with cameo art, token and backstory), ten animal companions with the d10 Bond table and the one-shot adventure The Vain Crown.",
|
||||
"version": "0.9.7",
|
||||
"version": "0.9.8",
|
||||
"compatibility": {
|
||||
"minimum": 13,
|
||||
"verified": "14.364"
|
||||
|
||||
@@ -244,7 +244,9 @@
|
||||
{{#if hasSpells}}
|
||||
<div class="tab" data-tab="magic" data-group="primary">
|
||||
<div class="sect">
|
||||
<div class="bar">Spells & Nature Tricks · Threshold 1·2·3</div>
|
||||
<div class="bar">Spells & Nature Tricks · Threshold 1·2·3
|
||||
{{#if strained}}<a class="strain-reset" title="The Weave is strained — clear this scene's repeats"><i class="fa-solid fa-rotate-left"></i> Settle the Weave</a>{{/if}}
|
||||
</div>
|
||||
<div class="body">
|
||||
<table class="v-table">
|
||||
<thead><tr><th></th><th>Spell</th><th>Thr</th><th>Effect · scaling</th><th class="ctrl"></th></tr></thead>
|
||||
@@ -253,7 +255,7 @@
|
||||
<tr data-item-id="{{item.id}}" class="{{#if item.system.spent}}spent{{/if}}">
|
||||
<td class="ctrl"><a class="item-cast" title="Cast"><i class="fa-solid fa-wand-sparkles"></i></a></td>
|
||||
<td class="item-name">{{item.name}} <em class="listtag">{{listLabel}}</em></td>
|
||||
<td>{{item.system.threshold}}</td>
|
||||
<td>{{threshold}}{{#if strain}} <span class="strain-badge" title="Cast {{strain}}× this scene — Threshold {{item.system.threshold}} +{{strain}}">+{{strain}}</span>{{/if}}</td>
|
||||
<td class="props">{{item.system.effect}}{{#if item.system.scaling}} · <em>{{item.system.scaling}}</em>{{/if}}</td>
|
||||
<td class="ctrl">
|
||||
<a class="item-spent" title="Toggle spent"><i class="fa-solid {{#if item.system.spent}}fa-battery-empty{{else}}fa-battery-full{{/if}}"></i></a>
|
||||
|
||||
+63
-7
@@ -401,6 +401,45 @@ async function rollPool(actor, pool, label, options = {}) {
|
||||
if (banked) await actor.addBanes(1, `a Stumble on “${label}”`);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Straining the Weave (§17) — the spam brake */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Strain is per scene: cast the same spell again and its Threshold rises by
|
||||
* +1 per prior casting, resetting when the scene changes or on a rest.
|
||||
*
|
||||
* The count lives in a flag stamped with the scene it belongs to, so a change
|
||||
* of scene resets it on read — no hook, nothing to miss if a GM swaps scenes
|
||||
* mid-fight or reloads.
|
||||
*/
|
||||
function strainScene() {
|
||||
return game.scenes?.current?.id ?? game.scenes?.viewed?.id ?? "no-scene";
|
||||
}
|
||||
|
||||
/** Prior castings of this spell in the current scene. */
|
||||
function strainCount(item) {
|
||||
const s = item.getFlag("vanity", "strain");
|
||||
return s?.scene === strainScene() ? Number(s.casts) || 0 : 0;
|
||||
}
|
||||
|
||||
/** Threshold as it stands now: printed Threshold + 1 per prior casting. */
|
||||
function effectiveThreshold(item) {
|
||||
return (Number(item.system.threshold) || 1) + strainCount(item);
|
||||
}
|
||||
|
||||
/** Remember a casting. Called only once the dice have actually been rolled. */
|
||||
async function addStrain(item) {
|
||||
return item.setFlag("vanity", "strain", { scene: strainScene(), casts: strainCount(item) + 1 });
|
||||
}
|
||||
|
||||
/** Let the Weave settle — clears strain on every spell an actor carries. */
|
||||
async function clearStrain(actor) {
|
||||
const strained = actor.items.filter(i => i.type === "spell" && i.getFlag("vanity", "strain"));
|
||||
for (const i of strained) await i.unsetFlag("vanity", "strain");
|
||||
return strained.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* The pool-builder dialog: situational dice, Vanity spend, Rattled penalty.
|
||||
*/
|
||||
@@ -616,8 +655,10 @@ class VanityActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
ctx.armour = items.filter(i => i.type === "armour");
|
||||
ctx.gear = items.filter(i => i.type === "gear");
|
||||
ctx.spells = items.filter(i => i.type === "spell").map(s => ({
|
||||
item: s, listLabel: SPELL_LISTS[s.system.list]?.label ?? s.system.list
|
||||
item: s, listLabel: SPELL_LISTS[s.system.list]?.label ?? s.system.list,
|
||||
strain: strainCount(s), threshold: effectiveThreshold(s)
|
||||
}));
|
||||
ctx.strained = ctx.spells.some(s => s.strain > 0);
|
||||
ctx.edges = items.filter(i => i.type === "edge");
|
||||
ctx.vices = items.filter(i => i.type === "vice");
|
||||
ctx.hasSpells = ctx.spells.length > 0;
|
||||
@@ -768,7 +809,7 @@ class VanityActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
});
|
||||
|
||||
// Spell cast
|
||||
html.find(".item-cast").click(ev => {
|
||||
html.find(".item-cast").click(async ev => {
|
||||
const item = actor.items.get(ev.currentTarget.closest("[data-item-id]").dataset.itemId);
|
||||
if (!item) return;
|
||||
if (item.system.spent) return ui.notifications.warn(`${item.name} is spent — recover it with a rest or ritual.`);
|
||||
@@ -777,13 +818,16 @@ class VanityActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
const locked = actor.system.locked;
|
||||
const knackDice = Number(locked.rank) || 0;
|
||||
const flavor = (item.system.description ?? "").match(/<i>(.*?)<\/i>/)?.[1] ?? "";
|
||||
promptAndRoll(actor, {
|
||||
const prior = strainCount(item);
|
||||
const threshold = effectiveThreshold(item);
|
||||
const rolled = await promptAndRoll(actor, {
|
||||
title: `Cast ${item.name}`,
|
||||
base: attr + knackDice,
|
||||
baseLabel: `${ATTRIBUTES[listDef.attr].label} ${attr} + ${locked.name || listDef.knack} ${knackDice} · Threshold ${item.system.threshold}`,
|
||||
baseLabel: `${ATTRIBUTES[listDef.attr].label} ${attr} + ${locked.name || listDef.knack} ${knackDice} · Threshold ${threshold}`
|
||||
+ (prior ? ` <span class="strained">(the Weave strains — +${prior})</span>` : ""),
|
||||
options: {
|
||||
context: "spell",
|
||||
threshold: Number(item.system.threshold) || 1,
|
||||
threshold,
|
||||
spellUuid: item.uuid,
|
||||
spell: {
|
||||
img: item.img,
|
||||
@@ -795,6 +839,16 @@ class VanityActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
}
|
||||
}
|
||||
});
|
||||
// Only a casting that actually reached the dice strains the Weave —
|
||||
// a cancelled dialog costs nothing.
|
||||
if (rolled) await addStrain(item);
|
||||
});
|
||||
|
||||
// Let the Weave settle — clear this scene's strain (§17)
|
||||
html.find(".strain-reset").click(async () => {
|
||||
const n = await clearStrain(actor);
|
||||
if (!n) return ui.notifications.info(`The Weave is already quiet around ${actor.name}.`);
|
||||
ui.notifications.info(`The Weave settles — strain cleared on ${n} spell${n > 1 ? "s" : ""}.`);
|
||||
});
|
||||
|
||||
// Spell recover / spend toggle
|
||||
@@ -828,9 +882,10 @@ class VanityActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
const g = actor.system.grit;
|
||||
if (g.value >= g.max) return ui.notifications.info(`${actor.name} is already at full Grit.`);
|
||||
await actor.update({ "system.grit.value": Math.min(g.max, g.value + 2) });
|
||||
const settled = await clearStrain(actor);
|
||||
await ChatMessage.create({
|
||||
speaker: ChatMessage.getSpeaker({ actor }),
|
||||
content: `<div class="vanity-roll vanity-forge-card"><p><b>${actor.name}</b> catches their breath — <b>+2 Grit</b> (${actor.system.grit.value}/${g.max}).</p></div>`
|
||||
content: `<div class="vanity-roll vanity-forge-card"><p><b>${actor.name}</b> catches their breath — <b>+2 Grit</b> (${actor.system.grit.value}/${g.max})${settled ? `, and <b>the Weave settles</b>` : ""}.</p></div>`
|
||||
});
|
||||
});
|
||||
|
||||
@@ -841,9 +896,10 @@ class VanityActorSheet extends foundry.appv1.sheets.ActorSheet {
|
||||
await actor.update({ "system.grit.value": g.max, "system.karma": karma });
|
||||
const spent = actor.items.filter(i => i.type === "spell" && i.system.spent);
|
||||
if (spent.length) await actor.updateEmbeddedDocuments("Item", spent.map(i => ({ _id: i.id, "system.spent": false })));
|
||||
const settled = await clearStrain(actor);
|
||||
await ChatMessage.create({
|
||||
speaker: ChatMessage.getSpeaker({ actor }),
|
||||
content: `<div class="vanity-roll vanity-forge-card"><p><b>${actor.name}</b> takes a proper rest: <b>Grit restored</b> (${g.max}/${g.max}), <b>1 Bane cleared</b> (tab ${karma})${spent.length ? `, <b>${spent.length} spell${spent.length > 1 ? "s" : ""} recovered</b>` : ""}.</p></div>`
|
||||
content: `<div class="vanity-roll vanity-forge-card"><p><b>${actor.name}</b> takes a proper rest: <b>Grit restored</b> (${g.max}/${g.max}), <b>1 Bane cleared</b> (tab ${karma})${spent.length ? `, <b>${spent.length} spell${spent.length > 1 ? "s" : ""} recovered</b>` : ""}${settled ? `, <b>the Weave settled</b>` : ""}.</p></div>`
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user