Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f5804e2f1 | ||
|
|
1ca0a64295 | ||
|
|
316716e001 |
@@ -34,7 +34,13 @@ one that matters.
|
||||
| Module | 0.6.0 |
|
||||
| Requires | VANITY **0.10.4+** — the release the seams landed in |
|
||||
| Foundry | v13 minimum, verified 14.365 |
|
||||
| Generator stamp | `GENERATOR_VERSION` 0.1.0 — written into every delve file, **not** the module version |
|
||||
| Generator stamp | `GENERATOR_VERSION` **0.2.0** — written into every delve file, **not** the module version |
|
||||
|
||||
`GENERATOR_VERSION` had sat at 0.1.0 while the emitted object gained the `authored` layer and the
|
||||
content model moved under it, so files claimed a generator that no longer existed — the two
|
||||
samples in [`samples/`](samples/) are both stamped 0.1.0 and only one of them has an `authored`
|
||||
layer. It tracks the *shape of the delve file and what a seed yields*, and the comment on it now
|
||||
says when to bump it. Old files still load.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+7
-1
@@ -9,7 +9,13 @@ import { buildArea, fallbackRoute } from './beat.mjs';
|
||||
import { Pressure } from './pressure.mjs';
|
||||
import { baneBeatFor } from './director.mjs';
|
||||
|
||||
export const GENERATOR_VERSION = '0.1.0';
|
||||
/**
|
||||
* Stamped into every delve file and shown on both surfaces, so a file says which generator made
|
||||
* it. Bump it when the shape of the emitted object changes or when content selection changes
|
||||
* enough that the same seed no longer yields the same delve — not when the module version moves.
|
||||
* 0.2.0: the `authored` layer, motif-scoped decisions/temptations/features, areas as situations.
|
||||
*/
|
||||
export const GENERATOR_VERSION = '0.2.0';
|
||||
|
||||
export function generateDelve(params = {}) {
|
||||
const {
|
||||
|
||||
+19
-2
@@ -1,9 +1,12 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { readFileSync, readdirSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { Rng } from './rng.mjs';
|
||||
import { generateDelve } from './delve.mjs';
|
||||
import { renderMarkdown } from './render.mjs';
|
||||
|
||||
const pack = JSON.parse(readFileSync('./content/barrow.json', 'utf8'));
|
||||
const dir = join(dirname(fileURLToPath(import.meta.url)), 'content');
|
||||
const pack = JSON.parse(readFileSync(join(dir, 'barrow.json'), 'utf8'));
|
||||
let pass = 0, fail = 0;
|
||||
const t = (name, cond, detail = '') => { cond ? pass++ : fail++; console.log(`${cond ? ' ok ' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`); };
|
||||
|
||||
@@ -227,5 +230,19 @@ import { renderWorksheet, renderPlay } from './render-authoring.mjs';
|
||||
`play ${done.split('\n').length} vs worksheet ${renderWorksheet(d).split('\n').length} lines`);
|
||||
}
|
||||
|
||||
// A delve must carry the pack it came from. The Foundry adapter reads `params.theme` to decide
|
||||
// which geometry to stage; when it trusted a boot-time global instead, every non-barrow delve got
|
||||
// a barrow map. The adapter cannot be tested here, so the contract it depends on is tested here.
|
||||
{
|
||||
const ids = readdirSync(dir).filter(f => f.endsWith('.json') && f !== 'index.json')
|
||||
.map(f => f.replace('.json', ''));
|
||||
const wrong = ids.filter(id => {
|
||||
const p = JSON.parse(readFileSync(join(dir, `${id}.json`), 'utf8'));
|
||||
return generateDelve({ pack: p, seed: `theme-${id}` }).params.theme !== id;
|
||||
});
|
||||
t('every delve records the pack it was generated from', wrong.length === 0,
|
||||
wrong.length ? `wrong: ${wrong.join(', ')}` : `${ids.length} packs`);
|
||||
}
|
||||
|
||||
console.log(`\n${pass} passed, ${fail} failed`);
|
||||
process.exit(fail ? 1 : 0);
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
# DELVE — the App and the Rules
|
||||
|
||||
*Draft 1, 2026-08-05. Written against the code at `316716e`, module v0.6.1, generator 0.2.0.*
|
||||
*Every number here was read out of the source, not remembered. Line references are live.*
|
||||
|
||||
---
|
||||
|
||||
## 0. What this document is
|
||||
|
||||
`DESIGN.md` is a design history — it records how DELVE was argued into existence, including the
|
||||
parts that failed. It is the wrong thing to hand someone who asks "what is this and how do I run
|
||||
it?"
|
||||
|
||||
This is that document. Two halves:
|
||||
|
||||
- **The App** — what the software is, the two surfaces, and the workflow at the desk and the table.
|
||||
- **The Rules** — the in-play procedure a GM actually runs, stated as rules rather than as
|
||||
architecture.
|
||||
|
||||
It is a draft. §7 lists what I believe is wrong with it.
|
||||
|
||||
---
|
||||
|
||||
## 1. Design intent
|
||||
|
||||
1. **DELVE is an authoring system, not an adventure generator.** It gets a GM to a strong first
|
||||
draft in a minute instead of an evening. It does not hand over something to run cold.
|
||||
2. **It generates the fiction, the order and the pressure.** It does not generate read-aloud prose
|
||||
or the climax, and it says so on the page where they are missing.
|
||||
3. **The fiction coheres because it is projected from one root draw**, not assembled from four
|
||||
independent tables.
|
||||
4. **Generation and table dice stay apart.** The core decides *when* a roll is due and what it
|
||||
means; the table rolls it. This is what makes the seed promise honest.
|
||||
5. **It is deliberately not a balance engine.** It plans pressure; it does not guarantee a fair
|
||||
fight.
|
||||
|
||||
---
|
||||
|
||||
# PART ONE — THE APP
|
||||
|
||||
## 2. The two surfaces and one working file
|
||||
|
||||
Four independent reviews of generated output converged on the same verdict: *"the handwritten
|
||||
delve still tells the GM what matters faster."* The density was the problem — and it turned out
|
||||
not to be a bug to render away. **Everything on the page is a vice at the table and a virtue at
|
||||
the desk.**
|
||||
|
||||
So there are two surfaces over one file:
|
||||
|
||||
| | For | Carries |
|
||||
|---|---|---|
|
||||
| **Worksheet** | the desk | everything, labelled, reroll commands beside each component, explicit prompts where only a human can write |
|
||||
| **Play sheet** | the table | the GM's own prose, the live leverage point, the decision on one line, the numbers. Nothing else |
|
||||
|
||||
**The JSON file is the document.** It carries an `authored` layer the generator never touches
|
||||
([`authoring.mjs:22`](../../core/authoring.mjs)), so **rerolling a component cannot destroy
|
||||
prose**. Lock what is good, reroll what is not, write the read-aloud, export the play sheet.
|
||||
|
||||
- Authored fields: `readAloud`, `notes`, `nameOverride`
|
||||
- Rerollable components: `situation`, `decision`, `temptation`, `feature`, `name`
|
||||
- A reroll draws from a salted seed and avoids what the rest of the delve already uses; when the
|
||||
pool is exhausted it duplicates another area rather than handing back the same thing, because
|
||||
**a reroll that changes nothing looks broken**
|
||||
|
||||
## 3. At the desk — the CLI
|
||||
|
||||
`core/` is pure JavaScript with no Foundry dependencies, so the whole authoring loop runs in Node.
|
||||
|
||||
```bash
|
||||
node core/cli.mjs --new --seed=my-delve --theme=port --file=my.json
|
||||
node core/cli.mjs --file=my.json # the worksheet (default)
|
||||
node core/cli.mjs --file=my.json --reroll=3:situation
|
||||
node core/cli.mjs --file=my.json --lock=3:decision
|
||||
node core/cli.mjs --file=my.json --write=3:readAloud --text="They have been waiting."
|
||||
node core/cli.mjs --file=my.json --todo # what still needs a human
|
||||
node core/cli.mjs --file=my.json --play # the table-facing sheet
|
||||
node core/cli.mjs --themes # list the 16 packs
|
||||
```
|
||||
|
||||
**A fresh delve is not ready to play, and says so.** `outstanding()` returns the intro, one
|
||||
read-aloud per area, the ending read-aloud, and — when the ending is `authored` — the climax
|
||||
itself. A six-area delve starts with **9 outstanding items**. The play sheet prints a warning
|
||||
banner until they are gone.
|
||||
|
||||
## 4. At the table — the Foundry module
|
||||
|
||||
Three tools in the scene controls:
|
||||
|
||||
| | Tool | Does |
|
||||
|---|---|---|
|
||||
| ⛏ | `delve-forge` | raise a dungeon — opens the panel, generates or loads, stages it in the world |
|
||||
| 🗑 | `delve-remove` | remove one — deletes exactly what it made and nothing else |
|
||||
| ⏩ | `delve-next` | stage the next area of a delve you finished at the desk |
|
||||
|
||||
**Entering an area** ([`vanity-delve.mjs:83`](../../foundry-module/module/vanity-delve.mjs)) does
|
||||
five things in a fixed order:
|
||||
|
||||
1. Builds the scene — `forge.stage({ populate: false, activate: true })`
|
||||
2. Forges the encounter, if the area plans one — with `hoard: false` so loot is decoupled
|
||||
3. Forges the hoard, if the area plans one — at its own tier
|
||||
4. **Posts the read-aloud to everyone** — the GM's prose, or the cue fragments marked *(unwritten)*
|
||||
5. **Posts the GM card, quietly** — situation, decision with resolution, foe block, temptation,
|
||||
and a code line carrying the trigger, the Bane beat and the fallback route
|
||||
|
||||
Players first, then the GM. That ordering is deliberate: the scene is up and the read-aloud is
|
||||
what the table came for.
|
||||
|
||||
**The seams.** DELVE requires VANITY **0.10.4+**. Below that floor the Forge's suppression options
|
||||
do not exist and every delve emits 9 stray chat cards and 3 stray folders. The module detects this
|
||||
at boot and warns. Requires Foundry v13+, verified on 14.365.
|
||||
|
||||
## 5. What the seed promises
|
||||
|
||||
State this plainly in any UI. **A partial seed sold as a full one is worse than no seed.**
|
||||
|
||||
| Reproducible from the seed | Recorded only | Live at the table |
|
||||
|---|---|---|
|
||||
| Fiction, area order, heat, hoard tier, foreshadow chain, situations, decisions, temptations | Encounter composition, mood, hoard contents | The clock, the tab, player choices |
|
||||
|
||||
The gap exists because VANITY's Forge uses `Math.random` internally (36 sites). Until an injectable
|
||||
RNG lands, a seed replays the *delve* but not the *dungeon's population*.
|
||||
|
||||
`GENERATOR_VERSION` (currently **0.2.0**) is stamped into every file. It tracks the shape of the
|
||||
file and what a seed yields — **not** the module version.
|
||||
|
||||
---
|
||||
|
||||
# PART TWO — THE RULES
|
||||
|
||||
*This is the procedure. A GM who has read this section can run a delve.*
|
||||
|
||||
## 6. The kernel — where a delve comes from
|
||||
|
||||
One root draw, everything else projected:
|
||||
|
||||
```
|
||||
APPETITE ──> motif ──> danger, attention trigger, every cue fragment
|
||||
│ ──> prize kind
|
||||
│ ──> who was conscripted
|
||||
└─ + ACCOMMODATION ──> the transgression, the bottom problem, the primary faction
|
||||
```
|
||||
|
||||
An **appetite** is a want — *to be looked at*, *never to age*, *to be first*. An
|
||||
**accommodation** is the monstrous thing someone did to satisfy it — *had the court interred
|
||||
alive*, *had the faces struck off*. Accommodations declare which appetites they fit, because
|
||||
drawing freely produced legible nonsense.
|
||||
|
||||
8 appetites × 6 accommodations × 7 claimants = **336 kernels from 21 authored lines**. Measured:
|
||||
60 generated delves produced **51 distinct kernels**.
|
||||
|
||||
**The GM needs one line of this at the table:** the transgression, the bottom problem, and what
|
||||
the thing at the bottom is now unable to bear.
|
||||
|
||||
## 7. The shape of a delve
|
||||
|
||||
Areas are assigned **arc roles** by position, so a delve has a shape rather than a flat run of
|
||||
rooms. Proportional — works at 3 areas or 12 ([`director.mjs:37`](../../core/director.mjs)):
|
||||
|
||||
| Position | Role | Foreshadow facet |
|
||||
|---|---|---|
|
||||
| first 20% | approach | institution |
|
||||
| to 45% | complication | ritual |
|
||||
| to 65% | turn | demand |
|
||||
| to 85% | descent | wound |
|
||||
| last 15% | threshold | anchor |
|
||||
| — | ending (separate slot) | demand |
|
||||
|
||||
The facet column is the **foreshadow chain**, and it is why area 2's held bows and area 4's
|
||||
scratched-out eyes point at the same faceless queen — they are facets of one motif, dealt in a
|
||||
deliberate order. The progression was proposed independently and turned out to describe the
|
||||
hand-authored paper delve exactly.
|
||||
|
||||
**Approach areas stay quiet.** An empty area is what makes an occupied one land.
|
||||
|
||||
## 8. An area, as the GM runs it
|
||||
|
||||
Every area carries the same parts, in this order on the card:
|
||||
|
||||
**1 · The situation.** Someone doing something, and what changes when the players walk in.
|
||||
> *"The tide watch, turning a glass that has already run through — it asks what the hour is, and
|
||||
> means the tide."*
|
||||
|
||||
It comes with an **offer** — what the players can get out of it — and a **because**, the reason it
|
||||
behaves that way. Situations are drawn from the motif, never a global pool.
|
||||
|
||||
**2 · The decision.** One thing you can do about the situation. Always states three outcomes:
|
||||
|
||||
`[Wits 2]` success · **miss** what failure costs · **or** the alternative that needs no roll
|
||||
|
||||
Four test invariants police this: every decision states success **and** failure, offers an attempt
|
||||
rather than a pick, and **never gates progress without an alternative**. Playtest 1 died on a gate
|
||||
with no fallback.
|
||||
|
||||
**3 · The clue.** Automatic on entry — the cue fragment, plus *"and it was done deliberately"*.
|
||||
The roll buys extra: `[VANITY: Observation — 1 success]` buys **who** did it and **roughly when**.
|
||||
Clues are never gated behind a roll; playtest 1 lost both its clues to single unopposed rolls and
|
||||
the motif never reached the table.
|
||||
|
||||
**4 · The encounter**, where one is planned. Foe counts and atk/def/Grit/Nerve in a table, plus
|
||||
**"harmed by"** stated *before* initiative and **"avoidable"** spelled out.
|
||||
|
||||
**5 · The temptation**, where there is a hoard. The decision lives at **use**, not acceptance —
|
||||
both playtests took every free-acceptance item without discussion, so free acceptance is not a
|
||||
temptation, it is an automatic yes.
|
||||
|
||||
```
|
||||
benefit what it does
|
||||
useCost usually +2 Bane
|
||||
standingDrawback what it costs you for the rest of the delve
|
||||
```
|
||||
|
||||
**6 · The failure case.** Every area states it. Ordinary areas: *"if they skip this, move its cue
|
||||
fragment into the next area so the motif still completes."* The threshold: *"if they skip this,
|
||||
the ending opens with them holding nothing the bottom problem wants."*
|
||||
|
||||
**Nothing repeats inside one delve.** Situations, decisions, temptations and features are dealt
|
||||
without replacement. Picking independently per area repeated a temptation in **75 of 80** test
|
||||
delves.
|
||||
|
||||
## 9. The appeasement move — the way through that is not violence
|
||||
|
||||
The best mechanic in the hand-authored delve turned out to project *exactly* from the appetite.
|
||||
Every appetite implies how it is fed:
|
||||
|
||||
| Appetite | The move | Roll |
|
||||
|---|---|---|
|
||||
| to be looked at | look at it and say plainly what you see | `[Flair 2]` |
|
||||
| to be first | stand aside and let it go ahead of you | `[Poise 2]` |
|
||||
| never to age | tell it that it has not changed | `[Flair 2]` |
|
||||
| never to be alone | sit down and stay a while | `[Poise 2]` |
|
||||
|
||||
**+1 Vanity now, +1 Bane later. Repeatable, in every area.**
|
||||
|
||||
> **Say the Bane aloud when it banks**, so the table can see the route is open.
|
||||
|
||||
This is the delve's social spine. It is always thematically exact, and a party that appeases even
|
||||
once reaches an ending that will *trade* rather than fight.
|
||||
|
||||
## 10. The clock — Wandering the Dark
|
||||
|
||||
**Triggers lead; the timer is a floor.** Playtest 1's clock fired 0 times in 4 rolls on a 3-Turn
|
||||
timer, while the attention trigger fired on the first roll of pass 2.
|
||||
|
||||
Every area emits at least one **attention trigger**:
|
||||
|
||||
| Trigger | Means |
|
||||
|---|---|
|
||||
| `announced` | entered somewhere grandly, announcing yourself |
|
||||
| `seenTwice` | reflected, echoed or witnessed twice over |
|
||||
| `loud` | a raised voice, a broken door, a shout |
|
||||
| `lingering` | stayed too long in one place |
|
||||
| `disturbed` | struck or moved something that had not moved in years |
|
||||
|
||||
**Procedure.** Roll `1d6` on any attention trigger, or when the timer comes round.
|
||||
**1 — something comes.** Telegraph one Turn ahead — the GM voices a cue before it is due. When it
|
||||
fires, roll **Reaction (2d6)** for its mood.
|
||||
|
||||
| Pressure setting | Timer period |
|
||||
|---|---|
|
||||
| slow | every 4 Turns |
|
||||
| standard | every 3 Turns |
|
||||
| hunted | every 2 Turns |
|
||||
|
||||
**Escalation.** If the clock has not fired by the halfway area, the trigger number widens from 1
|
||||
to 2 for the rest of the delve. A clock that never bites is decoration.
|
||||
|
||||
> ⚠ **The escalation and the period do not currently reach the table.** See §7 of the features
|
||||
> draft — this section describes the rule as designed and as implemented in `pressure.mjs`, not as
|
||||
> the Foundry module currently behaves.
|
||||
|
||||
## 11. The tab and the Reckoning
|
||||
|
||||
**Banes are emitted by named beats, never hoped for.** Playtest 1 banked 1 Bane in ~40 rolls
|
||||
because Stumbles are rare. The director offers a Bane beat when the tab falls behind pace — always
|
||||
when behind, and only half the time when on pace, so it does not feel mechanical.
|
||||
|
||||
| Source | Banks a Bane when you… |
|
||||
|---|---|
|
||||
| `flattery` | played to something's vanity to be received |
|
||||
| `noticing` | caught your own reflection and looked |
|
||||
| `relicUse` | spent a relic's favour |
|
||||
| `announcing` | announced yourself rather than slipping in |
|
||||
| `refusal` | refused an offer that would have cost you nothing yet |
|
||||
| `push` | pushed a roll |
|
||||
| `stumble` | stumbled — a failure with two or more ones |
|
||||
|
||||
**Target: ~5 Banes by the ending. THE RECKONING falls at 6.**
|
||||
|
||||
> ⚠ **The target is UNCALIBRATED.** Playtest 2 reached 3. It is a target, not a promise.
|
||||
|
||||
Quiet areas offer only `noticing` and `announcing`; louder areas add `flattery`, `relicUse` and
|
||||
`refusal`.
|
||||
|
||||
## 12. Endings
|
||||
|
||||
The ending is a **separate slot** after the last area, and by default it is **yours**.
|
||||
|
||||
- **`authored`** (default) — DELVE writes the question the place asks and leaves the climax blank.
|
||||
It says so on the card: *"the climax is unwritten. DELVE leaves this to you on purpose —
|
||||
improvise, or stop and write it."*
|
||||
- **`generated`** — the ending gets a real heat (one step above the delve's base) and a vault-tier
|
||||
hoard.
|
||||
|
||||
**Appeased even once → it listens, and will trade the prize.** That is the ending the social spine
|
||||
buys.
|
||||
|
||||
## 13. Parameters
|
||||
|
||||
| Parameter | Range | Default | Confidence |
|
||||
|---|---|---|---|
|
||||
| Areas (excludes the ending slot) | 3–12 | 6 | working default — one theme, one table, two passes |
|
||||
| Theme | 16 packs, 5 geometries | barrow | all validate; **only barrow is playtested** |
|
||||
| Depth | 1–5 | 2 | |
|
||||
| Party | 1–8 | 4 | ≤2 softens the heat, ≥7 hardens it |
|
||||
| Deadliness | forgiving / standard / cruel | standard | **severity only** |
|
||||
| Density | sparse / standard / infested | standard | **frequency only** — 0.20 / 0.34 / 0.55 of areas |
|
||||
| Greed | lean / standard / glutted | standard | shifts the hoard tier one step |
|
||||
| Pressure | slow / standard / hunted | standard | ⚠ **currently inert — see features draft** |
|
||||
| Ending | authored / generated | authored | a default, not a law |
|
||||
|
||||
**Heat** runs `skirmish → fight → battle → nightmare`; **hoards** run
|
||||
`pocket → cache → chest → vault → kingly`. Descent and threshold areas run one step hotter. The
|
||||
area before the ending always carries something — the threshold should have teeth. Treasure is not
|
||||
only behind fights: unless greed is `lean`, one treasureless area gets a bonus hoard.
|
||||
|
||||
## 14. What DELVE will not do
|
||||
|
||||
- **No physically coherent tiled dungeon.** v1 is a pointcrawl.
|
||||
- **No balance guarantee.**
|
||||
- **No sheet writes.** DELVE offers; the players and the GM decide.
|
||||
- **No system-generated climax by default.**
|
||||
- **No full replay from seed** until the Forge's RNG seam lands.
|
||||
- **No generated read-aloud prose.** Cue fragments only, labelled as fragments. This is the honest
|
||||
ceiling, and faking prose would be the one dishonest thing this tool could do.
|
||||
|
||||
---
|
||||
|
||||
## 15. What I think is wrong with this draft
|
||||
|
||||
Flagging these rather than hiding them, since this is going out for review.
|
||||
|
||||
1. **§10 and §13 document a rule the software does not run.** The pressure setting is inert end to
|
||||
end and the escalation never reaches the table. I have written the rule as designed and flagged
|
||||
it twice, but a rules document that describes unimplemented behaviour is a liability. The
|
||||
alternative — documenting the degenerate behaviour — is worse. **The right fix is in the code,
|
||||
not here.**
|
||||
2. **Part Two is written for a GM; Part One is written for a developer.** They may want to be two
|
||||
files. I kept them together because the seed promise and the two surfaces are rules-relevant.
|
||||
3. **No worked example.** A rules document of this kind usually ends with one complete annotated
|
||||
area. I have not included one because the samples in `samples/` are stamped generator 0.1.0 and
|
||||
one lacks the authored layer.
|
||||
4. **The Reckoning is underspecified here.** I state that it falls at 6 because that is what
|
||||
`reckoningDue()` returns, but *what happens* at a Reckoning is VANITY's rule, not DELVE's, and I
|
||||
have not cross-checked the rulebook. Someone should.
|
||||
5. **Nothing here has been tested at a table.** Every claim is read from source or from two paper
|
||||
playtests of a hand-written delve.
|
||||
@@ -0,0 +1,237 @@
|
||||
# DELVE — proposed features
|
||||
|
||||
*Draft 1, 2026-08-05. Against `316716e`, module v0.6.1, generator 0.2.0.*
|
||||
*Ranked. Every "Evidence" line was verified in the source or by running the code, not recalled.*
|
||||
|
||||
---
|
||||
|
||||
## The gate
|
||||
|
||||
**DELVE has never been run at a live table.** Sixteen themes, 39 tests, six releases and two paper
|
||||
playtests of a *hand-written* delve — but no session has ever used the generated article.
|
||||
|
||||
That fact sets the order below. Items 1–4 are things I can justify without a session, because they
|
||||
are defects or they are measurement. Items 5–9 are things I would not start until a table has run
|
||||
one delve, because a session will change what they should be. **The most valuable thing that could
|
||||
happen to this project is four hours and four players, not another feature.**
|
||||
|
||||
---
|
||||
|
||||
# TIER 1 — defects wearing feature costumes
|
||||
|
||||
*These should land before anything new is built. Both are the same root cause.*
|
||||
|
||||
## 1. Wire the live Pressure engine to the table
|
||||
|
||||
**What.** The Foundry module should instantiate `Pressure` from the loaded delve and drive the
|
||||
clock through it, instead of reimplementing a degenerate version inline.
|
||||
|
||||
**Why.** `pressure.mjs` is 130 lines that exist *because two playtests falsified the originals* —
|
||||
escalation, telegraphing, and pace-checking were each added to fix a specific observed failure.
|
||||
None of that reaches a table. The module's clock is:
|
||||
|
||||
```js
|
||||
const fired = roll.total <= 1; // vanity-delve.mjs:147
|
||||
```
|
||||
|
||||
A hardcoded 1. So:
|
||||
|
||||
- **Escalation never happens.** `Pressure.escalate()` widens the trigger from 1 to 2 if the clock
|
||||
has not fired by the halfway area. Playtest 1's central failure was a clock that rolled 1-in-6
|
||||
four times and never fired — the exact scenario escalation was written to prevent — and at the
|
||||
table it still cannot fire.
|
||||
- **The telegraph never fires.** `telegraphDue()` is never called, so the "voice a cue one Turn
|
||||
ahead" rule is documentation only.
|
||||
- **The timer never comes round.** `timerDue()` is never called. `st.turn` increments per area and
|
||||
is otherwise unused.
|
||||
- **Pace-checking is generation-only.** `onPace()` shapes which Bane beats get *planned*, but the
|
||||
live tab (`st.tab`) never feeds back, so a table that banks Banes fast or slow changes nothing.
|
||||
|
||||
**Evidence.** The module imports exactly three core modules — `rng`, `authoring`, `forge-app`
|
||||
([`vanity-delve.mjs:17-19`](../../foundry-module/module/vanity-delve.mjs)). `Pressure`,
|
||||
`timerDue`, `telegraphDue`, `escalate`, `onPace` and `triggerNumber` appear **nowhere** in
|
||||
`foundry-module/`. The only hit for "Pressure" is an `<h3>` in the panel.
|
||||
|
||||
**Cost.** Small. `Pressure` is already pure and serialisable; the module already persists
|
||||
`st.clock` and `st.tab` as world state. This is roughly: construct on load, replay the recorded
|
||||
rolls, call `recordClock()` instead of comparing to a literal, and surface `triggerNumber` in the
|
||||
card.
|
||||
|
||||
**Risk.** Low, but it changes live behaviour mid-campaign for anyone with a delve loaded. Needs a
|
||||
state migration or a version check on the stored state blob.
|
||||
|
||||
**Done when.** A delve that reaches its halfway area without a clock firing shows "something comes
|
||||
on 1–2" on the next card, and the escalation is visible to the GM.
|
||||
|
||||
## 2. Make the pressure setting mean something
|
||||
|
||||
**What.** Record the clock period in the delve file and derive the panel text from it.
|
||||
|
||||
**Why.** `slow` / `standard` / `hunted` is offered in the UI, stored in `lastParams`, and passed
|
||||
into generation — and then discarded. `Pressure.summary()`
|
||||
([`pressure.mjs:119`](../../core/pressure.mjs)) emits `turn`, `clockRolls`, `clockFired`,
|
||||
`triggerNumber`, `escalated`, `tab` and `target` — **but not `period`**. So the one number the
|
||||
setting controls never leaves the constructor.
|
||||
|
||||
**Evidence.** Generating the same seed at all three settings produces byte-identical pressure
|
||||
blocks:
|
||||
|
||||
```
|
||||
slow → {"turn":0,...,"triggerNumber":1,"escalated":false,"tab":6,"target":5}
|
||||
standard → {"turn":0,...,"triggerNumber":1,"escalated":false,"tab":6,"target":5}
|
||||
hunted → {"turn":0,...,"triggerNumber":1,"escalated":false,"tab":6,"target":5}
|
||||
```
|
||||
|
||||
And the panel hardcodes the standard value regardless
|
||||
([`forge-app.mjs:188`](../../foundry-module/module/forge-app.mjs)):
|
||||
|
||||
> `Roll 1d6 on any attention trigger, or every 3 Turns.`
|
||||
|
||||
A GM who picks `hunted` is told 3 Turns and gets 3 Turns.
|
||||
|
||||
**Cost.** Trivial — add `period` to `summary()`, read it in the panel. It is item 1's prerequisite
|
||||
and probably the same commit.
|
||||
|
||||
**Risk.** None beyond item 1. Old delve files lack `period`; default to 3.
|
||||
|
||||
---
|
||||
|
||||
# TIER 2 — build steps designed but never started
|
||||
|
||||
## 3. The overlay catalog *(build step 5)*
|
||||
|
||||
**What.** A curated encounter catalog layered over the Forge's output, with per-entry theme,
|
||||
faction, role, uniqueness — plus two fields that must be **tests, not tags**:
|
||||
|
||||
- **playability** — *can a hero with a mundane weapon damage this at all?*
|
||||
- **survivability** — *can the party's weakest member survive one clean hit and two rounds of being
|
||||
focused?*
|
||||
|
||||
**Why.** Both tests come from a corpse. The Wraith's immunity made a finale literally unwinnable; a
|
||||
5d6 armour-ignoring attack that drains max Grit, against 3 Grit, is not a fight. DELVE currently
|
||||
plans *heat* and hands composition to the Forge, so it cannot promise either property.
|
||||
|
||||
**Cost.** Large — it is authored content per theme plus a selection layer, and it interacts with
|
||||
the roster data already in the packs.
|
||||
|
||||
**Risk.** This is the item most likely to be wrong before a live session. Heat may turn out to be
|
||||
sufficient at a real table, in which case a curated catalog is a lot of authoring for a problem
|
||||
that only appeared in simulation. **I would hold this until after the session.**
|
||||
|
||||
## 4. A Bane calibration harness
|
||||
|
||||
**What.** A pure-core simulator: run N delves against a modelled table, count Banes at the ending,
|
||||
report the distribution. Then set `baneTarget` from data instead of intuition.
|
||||
|
||||
**Why.** The target has been marked **UNCALIBRATED** since draft 5. It is ~5; playtest 2 reached 3.
|
||||
Every Bane-beat decision in `baneBeatFor()` keys off `onPace()`, which keys off that number — so an
|
||||
uncalibrated target silently miscalibrates the whole tab engine.
|
||||
|
||||
**Cost.** Small, and it needs no Foundry and no table. This is the cheapest real answer available
|
||||
right now.
|
||||
|
||||
**Risk.** A simulated table is not a table. The output is a *prior*, not a calibration, and should
|
||||
be labelled as one — the same mistake as "6 areas: validated" would be easy to repeat here.
|
||||
|
||||
**Done when.** DESIGN.md can state a target with a measured distribution behind it, and open
|
||||
question 4 either closes or gets sharper.
|
||||
|
||||
---
|
||||
|
||||
# TIER 3 — worth doing, but after a session
|
||||
|
||||
## 5. The play sheet as a Foundry surface
|
||||
|
||||
**What.** Render the play sheet in-world — a GM window carrying the current area's prose, the
|
||||
decision, the numbers, and the live tab, instead of scrollback.
|
||||
|
||||
**Why.** The play sheet is currently CLI markdown only. At the table the GM card is *chat*, and
|
||||
chat scrolls away. The whole two-surface design exists because table-facing material must be
|
||||
scannable; a surface you have to scroll back through is not.
|
||||
|
||||
**Cost.** Medium. Renderer exists (`render-authoring.mjs`); this is a Foundry Application over it.
|
||||
|
||||
## 6. In-world authoring
|
||||
|
||||
**What.** Write the read-aloud, notes and title inside Foundry — the `authored` layer is already
|
||||
the right shape for it.
|
||||
|
||||
**Why.** Today the only authoring path is the CLI, which means a GM who wants to fix one area's
|
||||
prose leaves the VTT, edits JSON, and reloads. The module can already *detect* unfinished work
|
||||
(`outstanding()` is exposed on `game.delve`) but offers no way to resolve it.
|
||||
|
||||
**Cost.** Medium. Needs a save-back path to `worlds/<id>/delves/`, which the module currently only
|
||||
reads.
|
||||
|
||||
**Risk.** Two writers on one file. Needs a clear rule about which side owns the file.
|
||||
|
||||
## 7. A theme quality audit beyond Barrow
|
||||
|
||||
**What.** Read one generated delve per theme against the barrow benchmark, and add whatever
|
||||
invariant each failure implies.
|
||||
|
||||
**Why.** Sixteen packs pass `validate-pack.mjs`, but validation is structural — it proves a pack
|
||||
*can* fill six areas without repeating, not that the result is any good. Only barrow has ever been
|
||||
read closely against a playtest. **A pack can pass every invariant and still be dull.**
|
||||
|
||||
**Cost.** Medium, and mostly reading rather than coding.
|
||||
|
||||
**Risk.** None. This is the item most likely to find something surprising per hour spent.
|
||||
|
||||
## 8. The growing delve map *(build step 7)*
|
||||
|
||||
**What.** A map that accumulates as areas are staged — the pointcrawl made visible.
|
||||
|
||||
**Why.** v1 is explicitly a pointcrawl and players will ask where they are.
|
||||
|
||||
**Cost.** Large. Browser-only by necessity (`forgeStage` needs `Image`/canvas/`XMLSerializer`), so
|
||||
it cannot be tested in the CLI, which is where all 39 tests live.
|
||||
|
||||
**Risk.** High. This is the feature most likely to consume a week and produce something a GM
|
||||
sketches better on paper. **I would want a session to ask for it before building it.**
|
||||
|
||||
## 9. Injectable RNG in the Forge → full replay
|
||||
|
||||
**What.** Thread a seeded RNG through VANITY's Forge so a seed replays the population too.
|
||||
|
||||
**Why.** It closes the honesty gap in the seed promise (§5 of the app draft).
|
||||
|
||||
**Cost.** Large and it is in the *other* repo — 36 `Math.random` sites across four helpers. It was
|
||||
explicitly deferred as not gating a first release, and that judgment still looks right.
|
||||
|
||||
---
|
||||
|
||||
# TIER 4 — proposed and rejected
|
||||
|
||||
**More themes.** Sixteen is already more than has been validated at a table. Another family adds
|
||||
authored content behind an unmeasured quality bar. **Stop until item 7 runs.**
|
||||
|
||||
**Multi-motif or two-faction delves.** The fiction model coheres *because* it has one root draw.
|
||||
Two motifs is the most plausible way to break the thing that currently works best.
|
||||
|
||||
**Generated read-aloud prose.** Named in DESIGN.md as the one dishonest thing this tool could do.
|
||||
Still true. The cue-fragment ceiling is the honest one.
|
||||
|
||||
**A balance guarantee.** Out of scope by design, and the overlay tests (item 3) are the correct
|
||||
scoped version of this instinct.
|
||||
|
||||
---
|
||||
|
||||
## Suggested sequence
|
||||
|
||||
1. **Items 1 + 2** — one commit, small, fixes a rule that is documented but not running.
|
||||
2. **Item 4** — cheap, no table needed, turns an admitted unknown into a number.
|
||||
3. **Run a session.** Four hours, four players, one barrow delve, generated not hand-written.
|
||||
4. **Item 7** while the session is fresh.
|
||||
5. Re-rank 3, 5, 6, 8 against what the session actually showed. **I expect at least one of them to
|
||||
look wrong afterwards, and I would rather find out which one before building it than after.**
|
||||
|
||||
## Where I am least confident
|
||||
|
||||
- **Item 3's priority.** I have ranked a designed-and-documented build step below a simulation
|
||||
harness and a reading exercise. That is arguable, and the argument turns entirely on whether
|
||||
heat-without-curation survives a real fight.
|
||||
- **Whether items 5 and 6 are one feature.** A play-sheet window that cannot be edited may be half
|
||||
a thing; shipping both at once may be the smaller total change.
|
||||
- **The whole ranking assumes the session happens.** If it realistically will not happen soon, tier
|
||||
3 should be reordered to whatever makes the tool most useful at the desk — probably 6, then 5.
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "vanity-delve",
|
||||
"title": "DELVE — a dungeon layer for VANITY",
|
||||
"description": "Generates a coherent delve and sequences VANITY's Forge to stage it, one area at a time.",
|
||||
"version": "0.6.1",
|
||||
"version": "0.6.2",
|
||||
"compatibility": {
|
||||
"minimum": "13",
|
||||
"verified": "14.365"
|
||||
|
||||
@@ -9,7 +9,13 @@ import { buildArea, fallbackRoute } from './beat.mjs';
|
||||
import { Pressure } from './pressure.mjs';
|
||||
import { baneBeatFor } from './director.mjs';
|
||||
|
||||
export const GENERATOR_VERSION = '0.1.0';
|
||||
/**
|
||||
* Stamped into every delve file and shown on both surfaces, so a file says which generator made
|
||||
* it. Bump it when the shape of the emitted object changes or when content selection changes
|
||||
* enough that the same seed no longer yields the same delve — not when the module version moves.
|
||||
* 0.2.0: the `authored` layer, motif-scoped decisions/temptations/features, areas as situations.
|
||||
*/
|
||||
export const GENERATOR_VERSION = '0.2.0';
|
||||
|
||||
export function generateDelve(params = {}) {
|
||||
const {
|
||||
|
||||
@@ -18,6 +18,33 @@ const cap = s => (s ? s[0].toUpperCase() + s.slice(1) : s);
|
||||
const esc = s => foundry.utils.escapeHTML?.(String(s ?? '')) ?? String(s ?? '');
|
||||
|
||||
const MOD = 'vanity-delve';
|
||||
|
||||
/**
|
||||
* What a forged foe actually is.
|
||||
*
|
||||
* A pack's roster describes the encounter DELVE *planned* — "1 Ghoul and 2 Skeletons", with stats
|
||||
* and, in some themes, "blessed, silvered or magical weapons ONLY". The Forge does not take a
|
||||
* cast: it rolls its own monsters from the heat. So the roster and the actors in the world are two
|
||||
* different lists, and printing the roster's numbers beside the Forge's actors told the GM to run
|
||||
* a fight against foes that were never created.
|
||||
*
|
||||
* Read the numbers off the documents that exist. The plan is still worth showing — it carries
|
||||
* authored tactical guidance — but it has to be labelled as the plan.
|
||||
*/
|
||||
export const foeStats = a => ({
|
||||
name: a.name,
|
||||
uuid: a.uuid,
|
||||
atk: a.system?.attack1?.pool ?? null,
|
||||
def: a.system?.defence?.pool ?? null,
|
||||
grit: a.system?.grit?.value ?? null,
|
||||
nerve: a.system?.nerve ?? null,
|
||||
trick: a.system?.trick ?? '',
|
||||
});
|
||||
|
||||
/** One foe as a line of stats, in the roster's own vocabulary so the two read alike. */
|
||||
export const foeLine = f =>
|
||||
`<b>@UUID[${f.uuid}]{${f.name}}</b> — ${f.atk ?? '?'}/${f.def ?? '?'}/${f.grit ?? '?'}, Nerve ${f.nerve ?? '?'}${f.trick ? `. <i>${f.trick}</i>` : ''}`;
|
||||
|
||||
let THEMES = [{ id: 'barrow', label: 'Barrow' }];
|
||||
|
||||
export function setThemes(list) { THEMES = list; }
|
||||
@@ -123,7 +150,7 @@ export async function raiseDungeon(params = {}) {
|
||||
heat: area.encounter.heat, forStage: area.name,
|
||||
...(seams ? { hoard: false, post: false, folderId: folders.Actor.id } : {}),
|
||||
}).catch(e => { console.error('DELVE | encounter failed', e); return null; });
|
||||
area._foes = (enc?.actors ?? []).map(a => ({ name: a.name, uuid: a.uuid }));
|
||||
area._foes = (enc?.actors ?? []).map(foeStats);
|
||||
}
|
||||
if (area.hoard) {
|
||||
const h = await game.vanity.forge.hoard({ size: area.hoard, ...quiet })
|
||||
@@ -212,11 +239,12 @@ function buildPages(d, scenes) {
|
||||
${rv.failure ? `<li><b>Miss</b> → ${esc(rv.failure)}</li>` : ''}
|
||||
${rv.orElse ? `<li><b>Or</b> ${esc(rv.orElse)}</li>` : ''}
|
||||
</ul>
|
||||
${R ? `<p><b>${esc(cap(a.encounter.heat))} — ${esc(R.line)}.</b> Harmed by ${esc(R.harmedBy)}${R.harmedBy.includes('ONLY') ? ' — <b>say so before initiative</b>' : ''}. <i>${esc(R.avoid)}.</i></p>
|
||||
${a._foes?.length ? `<p><b>${esc(cap(a.encounter.heat))} — in the world.</b> These are the actors the Forge created; run the fight off these.</p>
|
||||
<table><thead><tr><th>Foe</th><th>atk</th><th>def</th><th>Grit</th><th>Nerve</th><th></th></tr></thead><tbody>
|
||||
${R.foes.map(f => `<tr><td>${f.n}× ${esc(f.name)}</td><td>${f.atk}</td><td>${f.def}</td><td>${f.grit}</td><td>${f.nerve}</td><td><i>${esc(f.note)}</i></td></tr>`).join('')}
|
||||
${a._foes.map(f => `<tr><td>@UUID[${f.uuid}]{${esc(f.name)}}</td><td>${f.atk ?? '?'}</td><td>${f.def ?? '?'}</td><td>${f.grit ?? '?'}</td><td>${f.nerve ?? '?'}</td><td><i>${esc(f.trick)}</i></td></tr>`).join('')}
|
||||
</tbody></table>` : ''}
|
||||
${a._foes?.length ? `<p><b>Rolled for you:</b> ${a._foes.map(f => `@UUID[${f.uuid}]{${esc(f.name)}}`).join(' · ')}</p>` : ''}
|
||||
${R ? `<p><b>DELVE planned ${esc(R.line)}</b> — the Forge rolls its own foes, so the plan and the table above are different lists.
|
||||
Its guidance still applies to the scene: harmed by ${esc(R.harmedBy)}${R.harmedBy.includes('ONLY') ? ' — <b>only true if you cast the fight yourself</b>' : ''}. <i>${esc(R.avoid)}.</i></p>` : ''}
|
||||
${a.temptation ? `<p><b>${esc(cap(a.temptation.id))}</b> — ${esc(a.temptation.cue)}: ${esc(a.temptation.benefit)}.<br>
|
||||
<i>Using it costs ${a.temptation.useCost?.bane ? `+${a.temptation.useCost.bane} Bane` : '—'}. While carried, ${esc(a.temptation.standingDrawback)}.</i></p>` : ''}
|
||||
${a._hoard?.length ? `<p><b>Hoard.</b></p><ul>${a._hoard.map(l => `<li>${l}</li>`).join('')}</ul>` : ''}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
*/
|
||||
import { coinSeed, Rng } from './core/rng.mjs';
|
||||
import { newWorkingFile, outstanding, readyToPlay } from './core/authoring.mjs';
|
||||
import { DelveForgeApp, raiseDungeon, listDungeons, removeDungeon, removeDungeonDialog, setThemes } from './forge-app.mjs';
|
||||
import { DelveForgeApp, raiseDungeon, listDungeons, removeDungeon, removeDungeonDialog, setThemes, foeStats, foeLine } from './forge-app.mjs';
|
||||
|
||||
const MOD = 'vanity-delve';
|
||||
const FLAG = 'state';
|
||||
@@ -27,6 +27,29 @@ let loadPack = async () => null;
|
||||
|
||||
const getState = () => game.settings.get(MOD, FLAG) ?? null;
|
||||
const setState = async s => game.settings.set(MOD, FLAG, s);
|
||||
|
||||
/**
|
||||
* The pack a delve was actually authored against.
|
||||
*
|
||||
* PACK is only a boot-time default. A delve brought in from the desk may be any of the sixteen
|
||||
* themes, and staging one with the wrong pack gives a harbour delve a barrow map — the fiction
|
||||
* says quayside and the geometry says burial chamber. Resolved per call rather than once at load,
|
||||
* because the global resets on a page reload while the staged delve in world state does not.
|
||||
*
|
||||
* `params.theme` is written from `pack.id` at generation time, so it is always present and always
|
||||
* right, whatever the caller passed.
|
||||
*/
|
||||
async function packFor(d) {
|
||||
const id = d?.params?.theme ?? 'barrow';
|
||||
if (PACK?.id === id) return PACK;
|
||||
const p = await loadPack(id);
|
||||
if (!p) {
|
||||
ui.notifications.error(`DELVE: could not load the ${id} theme — refusing to stage, it would use the wrong geometry.`);
|
||||
return null;
|
||||
}
|
||||
PACK = p;
|
||||
return p;
|
||||
}
|
||||
const cap = s => (s ? s[0].toUpperCase() + s.slice(1) : s);
|
||||
const list = items => `<ul>${items.filter(Boolean).map(i => `<li>${i}</li>`).join('')}</ul>`;
|
||||
|
||||
@@ -51,6 +74,7 @@ async function load(working) {
|
||||
if (!d?.skeleton || !Array.isArray(d.areas)) return ui.notifications.error('DELVE: that is not a delve file.');
|
||||
|
||||
const todo = outstanding(d);
|
||||
if (!await packFor(d)) return; // before anything is created in the world
|
||||
const folder = await Folder.create({ name: `Delve — ${d.authored?.title ?? d.skeleton.placeName}`, type: 'Actor' });
|
||||
await setState({ delve: d, folderId: folder.id, at: 0, turn: 0, tab: [], clock: [] });
|
||||
|
||||
@@ -75,7 +99,9 @@ async function loadFile(name) {
|
||||
/** Generate an unfinished draft in-world. Convenience only — the desk is the right place. */
|
||||
async function draft(params = {}) {
|
||||
const seed = params.seed || coinSeed(new Rng(String(game.world.id)));
|
||||
const d = newWorkingFile({ pack: PACK, ...params, seed });
|
||||
const pack = await packFor({ params });
|
||||
if (!pack) return;
|
||||
const d = newWorkingFile({ pack, ...params, seed });
|
||||
ui.notifications.warn('DELVE: unfinished draft. Write the read-aloud in the worksheet first.');
|
||||
return load(d);
|
||||
}
|
||||
@@ -89,16 +115,18 @@ async function enter() {
|
||||
const area = d.areas[st.at];
|
||||
const w = d.authored?.areas?.[area.index] ?? {};
|
||||
const name = w.nameOverride ?? area.name;
|
||||
const pack = await packFor(d);
|
||||
if (!pack) return;
|
||||
ui.notifications.info(`DELVE: raising ${name}…`);
|
||||
|
||||
const quiet = seamsPresent ? { post: false, folderId: st.folderId } : {};
|
||||
const stage = await game.vanity.forge.stage({
|
||||
type: PACK.forgeStageType, size: 'medium', name, populate: false, activate: true, ...quiet,
|
||||
type: pack.forgeStageType, size: 'medium', name, populate: false, activate: true, ...quiet,
|
||||
});
|
||||
if (area.encounter) await game.vanity.forge.encounter({
|
||||
const enc = area.encounter ? await game.vanity.forge.encounter({
|
||||
heat: area.encounter.heat, forStage: name,
|
||||
...(seamsPresent ? { hoard: false, post: false, folderId: st.folderId } : {}),
|
||||
});
|
||||
}) : null;
|
||||
if (area.hoard) await game.vanity.forge.hoard({ size: area.hoard, ...(seamsPresent ? { post: false } : {}) });
|
||||
|
||||
// Players first — the scene is up and this is what they came for.
|
||||
@@ -106,13 +134,14 @@ async function enter() {
|
||||
|
||||
// Then the GM, quietly.
|
||||
const R = area.encounter?.roster;
|
||||
const foes = (enc?.actors ?? []).map(foeStats);
|
||||
const rv = area.decision?.resolve ?? {};
|
||||
await gmCard(`⛏ ${area.index} · ${name}`, `${area.role} · ${area.facet}`,
|
||||
`${area.situation ? `<p><b>Here:</b> ${cap(area.situation.occupant)}, ${area.situation.doing} — ${area.situation.onArrival}.<br>
|
||||
<b>They can:</b> ${area.situation.offer}. <i>${cap(area.situation.because)}.</i></p>` : ''}
|
||||
<p><b>${cap(area.decision.cue)}</b>${rv.roll ? ` — [${rv.roll}] ${rv.success}` : ''}${rv.failure ? `<br><b>Miss:</b> ${rv.failure}` : ''}${rv.orElse ? `<br><b>Or:</b> ${rv.orElse}` : ''}</p>
|
||||
${R ? `<p><b>${cap(area.encounter.heat)}:</b> ${R.line}</p>${list(R.foes.map(f => `${f.n}× <b>${f.name}</b> — ${f.atk}/${f.def}/${f.grit}, Nerve ${f.nerve}. <i>${f.note}</i>`))}
|
||||
<p><b>Harmed by ${R.harmedBy}.</b> ${R.avoid}.</p>` : ''}
|
||||
${foes.length ? `<p><b>${cap(area.encounter.heat)} — in the world:</b></p>${list(foes.map(foeLine))}` : ''}
|
||||
${R ? `<p><b>DELVE planned ${R.line}</b> — the Forge rolled its own, so run the block above. ${R.avoid}.</p>` : ''}
|
||||
${area.temptation ? `<p><b>${cap(area.temptation.id)}:</b> ${area.temptation.benefit}. <i>Use: ${area.temptation.useCost?.bane ? `+${area.temptation.useCost.bane} Bane` : '—'}. ${area.temptation.standingDrawback}.</i></p>` : ''}
|
||||
${w.notes ? `<p><b>Your note:</b> ${w.notes}</p>` : ''}
|
||||
<p><code>${area.trigger}${area.baneBeat ? ` · ${area.baneBeat}` : ''} · fallback: ${area.fallback.route}</code></p>`);
|
||||
|
||||
Reference in New Issue
Block a user