pf2e-subsystems 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,31 @@
1
+ This LICENSE applies to the application CODE of pf2e-subsystems only.
2
+
3
+ The Pathfinder game content bundled in this package (data/subsystems.generated.js, data/reference.generated.js
4
+ and the inlined copy in dist/index.html — the subsystem, condition and action text)
5
+ is owned by Paizo Inc. and is licensed separately under the ORC and OGL licenses.
6
+ The MIT grant below does NOT cover that content. See notice.md for those terms and
7
+ the required attribution.
8
+
9
+ ----------------------------------------------------------------------
10
+
11
+ MIT License
12
+
13
+ Copyright (c) 2026 Whyte Erminae
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a copy
16
+ of this software and associated documentation files (the "Software"), to deal
17
+ in the Software without restriction, including without limitation the rights
18
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
+ copies of the Software, and to permit persons to whom the Software is
20
+ furnished to do so, subject to the following conditions:
21
+
22
+ The above copyright notice and this permission notice shall be included in all
23
+ copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # PF2e Subsystems
2
+
3
+ A GM-side runner for **Pathfinder 2e (Remaster)** GM Core's Victory Point
4
+ subsystems — the chase, the infiltration, the influence encounter, the research
5
+ montage, and the bare framework underneath all of them.
6
+
7
+ These are rules that get abandoned mid-session, not because they're hard but
8
+ because tracking them by hand is tedious and easy to get wrong. This keeps the
9
+ bookkeeping honest: awareness ticks up on its own at the end of every round,
10
+ leftover chase points are discarded at each obstacle rather than carried, a
11
+ research source stops paying out once it's spent, and every one of those moves
12
+ can be undone.
13
+
14
+ Single self-contained `dist/index.html` — no framework, no build step to *use*
15
+ it, nothing sent anywhere. Open it from a file, host it, or install it as an app.
16
+
17
+ ## The five modes
18
+
19
+ | Mode | Tracks | The thing it gets right |
20
+ |------|--------|-------------------------|
21
+ | **Chase** | Chase Points | Excess points are discarded at each obstacle, not carried to the next |
22
+ | **Infiltration** | Infiltration + Awareness | A failure costs 0 IP **and** hands over 1 AP; every round adds another |
23
+ | **Influence** | Influence Points | Which of the NPC's resistances and weaknesses the party has actually Discovered |
24
+ | **Research** | Research Points | Each source has a maximum, after which it gives nothing |
25
+ | **Victory Points** | whatever you name them | Accumulating and diminishing scales, seeded from GM Core's own table |
26
+
27
+ The fifth is the escape hatch. Reputation, duels, leadership and hexploration
28
+ ship as reference text rather than runners — name your own points, set your own
29
+ thresholds, and the same board runs them.
30
+
31
+ ### At the table
32
+
33
+ - **One tap per PC per round.** Four buttons, one per degree of success, and the
34
+ app works out what moves.
35
+ - **Undo.** Every action is reversible, including across an obstacle boundary
36
+ where points were discarded and no arithmetic could put them back.
37
+ - **42 sample obstacles.** GM Core's Underground, Urban and Wilderness chase
38
+ tables, with their real skills and DCs, pickable when you're building a chase.
39
+ - **Prep and run are separate.** Resetting a run hands the prepped chase back
40
+ untouched; an export code carries the prep and none of last week's dice.
41
+ - **Where the book says "the GM decides", the app asks.** Obstacle costs, the
42
+ awareness failure point, round lengths and thresholds are prefilled with the
43
+ book's rule of thumb and left editable.
44
+
45
+ ## Run it
46
+
47
+ ```sh
48
+ python3 tools/build.py # assemble dist/index.html
49
+ # then either:
50
+ open dist/index.html # straight from the file, fully offline
51
+ npx pf2e-subsystems # serve at http://localhost:8727 and open the browser
52
+ ```
53
+
54
+ ## Where the rules text comes from
55
+
56
+ Every word of quoted rules text is Paizo's own, generated from the open-source
57
+ Foundry VTT pf2e data so it can't drift from the book. The subsystems chapter
58
+ lives in the GM Screen journal; the conditions and actions come from their own
59
+ packs. The subsystem pages are on the `v14-dev` branch:
60
+
61
+ ```sh
62
+ git clone --depth 1 --filter=blob:none --sparse -b v14-dev \
63
+ https://github.com/foundryvtt/pf2e.git pf2e-data
64
+ cd pf2e-data && git sparse-checkout set packs/pf2e/journals \
65
+ packs/pf2e/conditions packs/pf2e/actions && cd ..
66
+
67
+ python3 tools/build_subsystems.py \
68
+ --src pf2e-data/packs/pf2e/journals/gm-screen.json \
69
+ --out data/subsystems.generated.js
70
+ python3 tools/build_reference.py --src pf2e-data/packs/pf2e \
71
+ --out data/reference.generated.js
72
+ python3 tools/build.py
73
+ ```
74
+
75
+ The Influence page in particular is *only* a pointer to the **Influence** and
76
+ **Discover** actions — which is why `build_reference.py` here pulls
77
+ `actions/subsystems/` as well as the four folders the other tools use. Without
78
+ it that page renders as two bare headings. The test suite asserts that every
79
+ action the rules text names actually resolves.
80
+
81
+ The explanations *around* that text, the run board, and every judgement about
82
+ what to enforce and what to ask are ours.
83
+
84
+ ## Tests
85
+
86
+ ```sh
87
+ npm install # jsdom
88
+ node tools/test.mjs
89
+ ```
90
+
91
+ 107 assertions over the built file. They cover the arithmetic rather than the
92
+ rendering: every degree-of-success scale, the chase discard at an obstacle
93
+ boundary, awareness accruing at end of round, the 2× failure threshold and its
94
+ quarter marks, individual versus group obstacles, Edge Point spends, research
95
+ caps, thresholds firing exactly once against the right count, both Victory
96
+ Point scales, undo restoring exactly across an obstacle advance, and that the
97
+ two generated data files line up with each other.
98
+
99
+ ## The other three tools
100
+
101
+ - [PF2e Primer](https://thewhytewolf.github.io/pf2eprimer/) — an interactive introduction for new players.
102
+ - [PF2e Spellbook](https://thewhytewolf.github.io/pf2espellcards/) — player-side spell preparation and casting.
103
+ - [PF2e Party Tracker](https://thewhytewolf.github.io/pf2epartytracker/) — GM-side passive stats, secret checks and the exploration board.
104
+
105
+ ## Licensing
106
+
107
+ Application code is MIT (see [`LICENSE`](./LICENSE)). Bundled Pathfinder
108
+ subsystem, condition and action text is Paizo content under the ORC and OGL
109
+ licenses — see [`notice.md`](./notice.md). This is an unofficial fan tool, not
110
+ affiliated with Paizo.
package/bin/cli.mjs ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ // Serves the built subsystems app from dist/ on localhost and opens the browser.
3
+ // The port is fixed by default: localStorage (your prepped encounters) is
4
+ // keyed by origin, so a stable http://localhost:8727 keeps your encounters
5
+ // across package updates. localhost is a secure context, so the service
6
+ // worker and the "Install app" button work too. (8727, one above the
7
+ // primer's 8726, the party tracker's 8725 and the spellbook's 8724, so the
8
+ // four apps never share an origin.)
9
+ import http from 'node:http';
10
+ import { readFile } from 'node:fs/promises';
11
+ import { spawn } from 'node:child_process';
12
+
13
+ const DEFAULT_PORT = 8727;
14
+
15
+ const args = process.argv.slice(2);
16
+ if (args.includes('--help') || args.includes('-h')) {
17
+ console.log(`Usage: pf2e-subsystems [--port N] [--no-open]
18
+
19
+ Serves the PF2e Subsystems at http://localhost:${DEFAULT_PORT} and opens your browser.
20
+
21
+ --port N listen on port N instead of ${DEFAULT_PORT}
22
+ --no-open don't open the browser, just serve`);
23
+ process.exit(0);
24
+ }
25
+ const portIdx = args.indexOf('--port');
26
+ const port = portIdx !== -1 ? Number(args[portIdx + 1]) : DEFAULT_PORT;
27
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
28
+ console.error(`pf2e-subsystems: invalid --port ${args[portIdx + 1]}`);
29
+ process.exit(1);
30
+ }
31
+ const noOpen = args.includes('--no-open');
32
+
33
+ const distDir = new URL('../dist/', import.meta.url);
34
+ const FILES = {
35
+ '/': ['index.html', 'text/html; charset=utf-8'],
36
+ '/index.html': ['index.html', 'text/html; charset=utf-8'],
37
+ '/sw.js': ['sw.js', 'text/javascript; charset=utf-8'],
38
+ '/manifest.webmanifest': ['manifest.webmanifest', 'application/manifest+json'],
39
+ '/icon.svg': ['icon.svg', 'image/svg+xml'],
40
+ };
41
+
42
+ const server = http.createServer(async (req, res) => {
43
+ const entry = FILES[new URL(req.url, 'http://localhost').pathname];
44
+ if (!entry || (req.method !== 'GET' && req.method !== 'HEAD')) {
45
+ res.writeHead(404, { 'Content-Type': 'text/plain' }).end('Not found');
46
+ return;
47
+ }
48
+ const [file, type] = entry;
49
+ try {
50
+ const body = await readFile(new URL(file, distDir));
51
+ // no-cache so package updates (new sw.js/index.html) are picked up on reload
52
+ res.writeHead(200, {
53
+ 'Content-Type': type,
54
+ 'Content-Length': body.byteLength,
55
+ 'Cache-Control': 'no-cache',
56
+ });
57
+ res.end(req.method === 'HEAD' ? undefined : body);
58
+ } catch {
59
+ res.writeHead(500, { 'Content-Type': 'text/plain' }).end('Read error');
60
+ }
61
+ });
62
+
63
+ function openBrowser(url) {
64
+ const [cmd, cmdArgs] =
65
+ process.platform === 'darwin' ? ['open', [url]]
66
+ : process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]]
67
+ : ['xdg-open', [url]];
68
+ spawn(cmd, cmdArgs, { stdio: 'ignore', detached: true }).on('error', () => {
69
+ console.error(`Couldn't open a browser — go to ${url}`);
70
+ }).unref();
71
+ }
72
+
73
+ const url = `http://localhost:${port}/`;
74
+ server.on('error', (err) => {
75
+ if (err.code === 'EADDRINUSE') {
76
+ console.log(`Port ${port} is in use — assuming the subsystems app is already running at ${url}`);
77
+ if (!noOpen) openBrowser(url);
78
+ process.exit(0);
79
+ }
80
+ console.error(`pf2e-subsystems: ${err.message}`);
81
+ process.exit(1);
82
+ });
83
+ server.listen(port, '127.0.0.1', () => {
84
+ console.log(`PF2e Subsystems running at ${url} (Ctrl+C to stop)`);
85
+ if (!noOpen) openBrowser(url);
86
+ });