1 Commits
Author SHA1 Message Date
slaguru666andClaude Opus 5 ab619f4b73 Remove the dice textures and the whole Dice So Nice path
Deleted: the seven PNGs in dice/, tools/make-dice-textures.mjs, the `npm run
dice` script, the RB_DICE colourset and texture tables, diceSetFor, the
diceSoNiceReady hook, and rbDressDice — which was tagging ten roll sites with
coloursets that no longer existed. 99 lines of module, plus the assets.
README and DEPLOY updated; they still advertised all of it.

Recoverable with `git revert` if it is ever wanted back.

This also explains the 01-that-was-12. The RESULT was never wrong: skillRoll
rolls one 1d100 and grades on roll.total, and the chat card derives the tens and
units it prints from that same total. Checked all 100 values of percentileFaces
and 60 live rolls — the printed faces reconstruct the graded number every time,
with no mismatches.

What disagreed was the ANIMATION. The custom addDicePreset labels registered for
d100 and d10 remapped which physical face means what, so Dice So Nice could land
showing 01 while the roll behind it was 12. Nothing downstream ever read the
animated faces, so no roll was ever adjudicated on the wrong number — but a
table watching the dice had every reason to believe otherwise, which is its own
kind of wrong. Disabling the skins in 1.0.6 already removed it; deleting them
makes it permanent.

Verified in a running Foundry after removal: skill roll, weapon damage, hit
location, major wound and Coherence all resolve and post their cards, no errors,
and game.ringbrp.diceSetFor is gone from the API.

Version 1.0.7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 22:45:52 +01:00
47 changed files with 119 additions and 398 deletions
+7 -7
View File
@@ -28,7 +28,8 @@ git clone https://gitea.timevans.uk/slaguru666/RingBRP.git ringbrp
```
Requires **Foundry v13+** (verified against v14.364). No modules are required. Dice So
Nice is supported but optional — the system registers four of its own dice textures.
Nice works if you have it, using your own dice — the system ships no dice skins and does
not touch anybody's dice settings.
### Deploying to a remote Foundry server
@@ -47,7 +48,7 @@ packs or regenerate art on that machine:
```bash
npm install # pulls classic-level, used to write the LevelDB packs
npm run build # rebuilds packs/ from tools/content.mjs
npm run check # the three guards; the build refuses to run if they fail
npm run check # the five guards; the build refuses to run if they fail
```
To update a deployed server: `git pull` and restart Foundry. If the pull touches
@@ -152,13 +153,12 @@ template.json data model for every Actor and Item type
templates/ Handlebars sheets
styles/ one stylesheet
lang/en.json all user-facing text
icons/ dice/ fonts/ generated art
icons/ fonts/ art/ generated art
packs/ built LevelDB compendia (committed — see Deploying)
tools/
content.mjs the catalogue: skills, weapons, armour, gear, vehicles, NPCs
build-packs.mjs builds packs/ — runs the three guards first and refuses on failure
build-packs.mjs builds packs/ — runs the five guards first and refuses on failure
make-icons.mjs draws all 231 icons
make-dice-textures.mjs
rules-text.mjs generates the rules journal FROM rules.mjs
mission.mjs the case generator
mission-tables.mjs its content tables
@@ -176,11 +176,11 @@ stops being identity-equal to what it aliases, if a constant is re-declared as a
literal, or if an exported rule has no spot-check. The packs used to be built under
one set of numbers and played under another; this makes that impossible to ship.
The build runs all three guards before it writes anything:
The build runs all five guards before it writes anything:
<!-- guards:start -->
```
check-rules: OK — 87 rules, 21 files scanned, 3 aliases + 41 constant sets checked, 275 formulas verified
check-rules: OK — 87 rules, 20 files scanned, 3 aliases + 41 constant sets checked, 275 formulas verified
check-kits: OK — 25 roles, 10 trades, 218 catalogue items, every kit key resolves, every posting can use what it carries, every loadout distinct
check-lang: OK — en.json, 945 keys, no leaf/branch collisions
check-behaviour: OK — 46 behavioural tests
Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 826 B

+1 -2
View File
@@ -33,10 +33,9 @@ Only if you intend to edit the catalogue there:
```bash
npm install # classic-level, for writing LevelDB
npm run check # the three guards
npm run check # the five guards
npm run build # rebuilds packs/
node tools/make-icons.mjs # redraws all icons
node tools/make-dice-textures.mjs # regenerates the Dice So Nice textures
```
`npm run build` runs the guards first and aborts if any fails, so a broken catalogue
-1
View File
@@ -6,7 +6,6 @@
"scripts": {
"build": "node tools/build-packs.mjs && node tools/update-readme.mjs",
"icons": "node tools/make-icons.mjs",
"dice": "node tools/make-dice-textures.mjs",
"check": "node tools/check-rules.mjs && node tools/check-kits.mjs && node tools/check-lang.mjs && node tools/check-templates.mjs && node tools/check-behaviour.mjs",
"test": "npm run check",
"readme": "node tools/update-readme.mjs"
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-20:33:19.258824 175abb000 Recovering log #3
2026/08/12-20:33:19.258964 175abb000 Level-0 table #5: started
2026/08/12-20:33:19.259113 175abb000 Level-0 table #5: 4591 bytes OK
2026/08/12-20:33:19.259390 175abb000 Delete type=0 #3
2026/08/12-20:33:19.259432 175abb000 Delete type=3 #2
2026/08/12-22:44:48.920895 172817000 Recovering log #3
2026/08/12-22:44:48.921047 172817000 Level-0 table #5: started
2026/08/12-22:44:48.921223 172817000 Level-0 table #5: 4591 bytes OK
2026/08/12-22:44:48.921517 172817000 Delete type=0 #3
2026/08/12-22:44:48.921554 172817000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-20:33:03.500947 17385b000 Delete type=3 #1
2026/08/12-22:44:33.287733 16f5db000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-20:33:19.281748 1772df000 Recovering log #3
2026/08/12-20:33:19.282130 1772df000 Level-0 table #5: started
2026/08/12-20:33:19.282266 1772df000 Level-0 table #5: 4129 bytes OK
2026/08/12-20:33:19.282510 1772df000 Delete type=0 #3
2026/08/12-20:33:19.282542 1772df000 Delete type=3 #2
2026/08/12-22:44:48.946376 17382f000 Recovering log #3
2026/08/12-22:44:48.946637 17382f000 Level-0 table #5: started
2026/08/12-22:44:48.946788 17382f000 Level-0 table #5: 4129 bytes OK
2026/08/12-22:44:48.947029 17382f000 Delete type=0 #3
2026/08/12-22:44:48.947065 17382f000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-20:33:03.577064 17385b000 Delete type=3 #1
2026/08/12-22:44:33.362147 16edcf000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-20:33:19.260549 1772df000 Recovering log #3
2026/08/12-20:33:19.260715 1772df000 Level-0 table #5: started
2026/08/12-20:33:19.261020 1772df000 Level-0 table #5: 31614 bytes OK
2026/08/12-20:33:19.261257 1772df000 Delete type=0 #3
2026/08/12-20:33:19.261283 1772df000 Delete type=3 #2
2026/08/12-22:44:48.922471 173023000 Recovering log #3
2026/08/12-22:44:48.923346 173023000 Level-0 table #5: started
2026/08/12-22:44:48.923621 173023000 Level-0 table #5: 31614 bytes OK
2026/08/12-22:44:48.923809 173023000 Delete type=0 #3
2026/08/12-22:44:48.923836 173023000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-20:33:03.503203 174067000 Delete type=3 #1
2026/08/12-22:44:33.290003 16f5db000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-20:33:19.273026 175abb000 Recovering log #3
2026/08/12-20:33:19.273575 175abb000 Level-0 table #5: started
2026/08/12-20:33:19.273963 175abb000 Level-0 table #5: 45532 bytes OK
2026/08/12-20:33:19.274265 175abb000 Delete type=0 #3
2026/08/12-20:33:19.274316 175abb000 Delete type=3 #2
2026/08/12-22:44:48.936635 17200b000 Recovering log #3
2026/08/12-22:44:48.937301 17200b000 Level-0 table #5: started
2026/08/12-22:44:48.937794 17200b000 Level-0 table #5: 45532 bytes OK
2026/08/12-22:44:48.938219 17200b000 Delete type=0 #3
2026/08/12-22:44:48.938275 17200b000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-20:33:03.557918 172843000 Delete type=3 #1
2026/08/12-22:44:33.345401 16e5c3000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-20:33:19.245248 1762c7000 Recovering log #3
2026/08/12-20:33:19.246727 1762c7000 Level-0 table #5: started
2026/08/12-20:33:19.248205 1762c7000 Level-0 table #5: 225945 bytes OK
2026/08/12-20:33:19.248498 1762c7000 Delete type=0 #3
2026/08/12-20:33:19.248546 1762c7000 Delete type=3 #2
2026/08/12-22:44:48.907196 173023000 Recovering log #3
2026/08/12-22:44:48.908927 173023000 Level-0 table #5: started
2026/08/12-22:44:48.910392 173023000 Level-0 table #5: 225945 bytes OK
2026/08/12-22:44:48.910874 173023000 Delete type=0 #3
2026/08/12-22:44:48.910934 173023000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-20:33:03.540045 172843000 Delete type=3 #1
2026/08/12-22:44:33.327015 16f5db000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-20:33:19.255310 175abb000 Recovering log #3
2026/08/12-20:33:19.255554 175abb000 Level-0 table #5: started
2026/08/12-20:33:19.255695 175abb000 Level-0 table #5: 3295 bytes OK
2026/08/12-20:33:19.255978 175abb000 Delete type=0 #3
2026/08/12-20:33:19.256022 175abb000 Delete type=3 #2
2026/08/12-22:44:48.917345 172817000 Recovering log #3
2026/08/12-22:44:48.917660 172817000 Level-0 table #5: started
2026/08/12-22:44:48.917794 172817000 Level-0 table #5: 3295 bytes OK
2026/08/12-22:44:48.918046 172817000 Delete type=0 #3
2026/08/12-22:44:48.918081 172817000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-20:33:03.569801 17304f000 Delete type=3 #1
2026/08/12-22:44:33.356982 16edcf000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-20:33:19.232393 175abb000 Recovering log #3
2026/08/12-20:33:19.235807 175abb000 Level-0 table #5: started
2026/08/12-20:33:19.239193 175abb000 Level-0 table #5: 545293 bytes OK
2026/08/12-20:33:19.239934 175abb000 Delete type=0 #3
2026/08/12-20:33:19.240034 175abb000 Delete type=3 #2
2026/08/12-22:44:48.896053 17200b000 Recovering log #3
2026/08/12-22:44:48.898901 17200b000 Level-0 table #5: started
2026/08/12-22:44:48.902279 17200b000 Level-0 table #5: 545293 bytes OK
2026/08/12-22:44:48.902701 17200b000 Delete type=0 #3
2026/08/12-22:44:48.902787 17200b000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-20:33:03.524869 17385b000 Delete type=3 #1
2026/08/12-22:44:33.311753 16e5c3000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-20:33:19.263420 1762c7000 Recovering log #3
2026/08/12-20:33:19.263582 1762c7000 Level-0 table #5: started
2026/08/12-20:33:19.263817 1762c7000 Level-0 table #5: 21613 bytes OK
2026/08/12-20:33:19.264044 1762c7000 Delete type=0 #3
2026/08/12-20:33:19.264079 1762c7000 Delete type=3 #2
2026/08/12-22:44:48.925841 17382f000 Recovering log #3
2026/08/12-22:44:48.926073 17382f000 Level-0 table #5: started
2026/08/12-22:44:48.926304 17382f000 Level-0 table #5: 21613 bytes OK
2026/08/12-22:44:48.926557 17382f000 Delete type=0 #3
2026/08/12-22:44:48.926598 17382f000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-20:33:03.565946 17304f000 Delete type=3 #1
2026/08/12-22:44:33.353318 16edcf000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-20:33:19.275275 175abb000 Recovering log #3
2026/08/12-20:33:19.275814 175abb000 Level-0 table #5: started
2026/08/12-20:33:19.276182 175abb000 Level-0 table #5: 43953 bytes OK
2026/08/12-20:33:19.276489 175abb000 Delete type=0 #3
2026/08/12-20:33:19.276529 175abb000 Delete type=3 #2
2026/08/12-22:44:48.939398 17200b000 Recovering log #3
2026/08/12-22:44:48.940505 17200b000 Level-0 table #5: started
2026/08/12-22:44:48.941454 17200b000 Level-0 table #5: 43953 bytes OK
2026/08/12-22:44:48.942198 17200b000 Delete type=0 #3
2026/08/12-22:44:48.942249 17200b000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-20:33:03.562330 17385b000 Delete type=3 #1
2026/08/12-22:44:33.349699 16f5db000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-20:33:19.264705 176ad3000 Recovering log #3
2026/08/12-20:33:19.265215 176ad3000 Level-0 table #5: started
2026/08/12-20:33:19.265531 176ad3000 Level-0 table #5: 33484 bytes OK
2026/08/12-20:33:19.265770 176ad3000 Delete type=0 #3
2026/08/12-20:33:19.265802 176ad3000 Delete type=3 #2
2026/08/12-22:44:48.927292 172817000 Recovering log #3
2026/08/12-22:44:48.927647 172817000 Level-0 table #5: started
2026/08/12-22:44:48.928175 172817000 Level-0 table #5: 33484 bytes OK
2026/08/12-22:44:48.928522 172817000 Delete type=0 #3
2026/08/12-22:44:48.928579 172817000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-20:33:03.575038 17304f000 Delete type=3 #1
2026/08/12-22:44:33.360431 16e5c3000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-20:33:19.251838 175abb000 Recovering log #3
2026/08/12-20:33:19.252367 175abb000 Level-0 table #5: started
2026/08/12-20:33:19.252611 175abb000 Level-0 table #5: 7590 bytes OK
2026/08/12-20:33:19.252941 175abb000 Delete type=0 #3
2026/08/12-20:33:19.252982 175abb000 Delete type=3 #2
2026/08/12-22:44:48.913614 172817000 Recovering log #3
2026/08/12-22:44:48.914288 172817000 Level-0 table #5: started
2026/08/12-22:44:48.914458 172817000 Level-0 table #5: 7590 bytes OK
2026/08/12-22:44:48.914739 172817000 Delete type=0 #3
2026/08/12-22:44:48.914773 172817000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-20:33:03.494044 17304f000 Delete type=3 #1
2026/08/12-22:44:33.281949 16edcf000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-20:33:19.277727 175abb000 Recovering log #3
2026/08/12-20:33:19.278352 175abb000 Level-0 table #5: started
2026/08/12-20:33:19.278612 175abb000 Level-0 table #5: 16902 bytes OK
2026/08/12-20:33:19.278891 175abb000 Delete type=0 #3
2026/08/12-20:33:19.278925 175abb000 Delete type=3 #2
2026/08/12-22:44:48.943128 17200b000 Recovering log #3
2026/08/12-22:44:48.943292 17200b000 Level-0 table #5: started
2026/08/12-22:44:48.943546 17200b000 Level-0 table #5: 16902 bytes OK
2026/08/12-22:44:48.943901 17200b000 Delete type=0 #3
2026/08/12-22:44:48.943932 17200b000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-20:33:03.567928 17385b000 Delete type=3 #1
2026/08/12-22:44:33.355279 16e5c3000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-20:33:19.268400 175abb000 Recovering log #3
2026/08/12-20:33:19.269480 175abb000 Level-0 table #5: started
2026/08/12-20:33:19.270081 175abb000 Level-0 table #5: 82299 bytes OK
2026/08/12-20:33:19.270432 175abb000 Delete type=0 #3
2026/08/12-20:33:19.270471 175abb000 Delete type=3 #2
2026/08/12-22:44:48.931234 17200b000 Recovering log #3
2026/08/12-22:44:48.932452 17200b000 Level-0 table #5: started
2026/08/12-22:44:48.933139 17200b000 Level-0 table #5: 82299 bytes OK
2026/08/12-22:44:48.933493 17200b000 Delete type=0 #3
2026/08/12-22:44:48.933538 17200b000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-20:33:03.550640 174067000 Delete type=3 #1
2026/08/12-22:44:33.337758 16f5db000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-20:33:19.280144 176ad3000 Recovering log #3
2026/08/12-20:33:19.280500 176ad3000 Level-0 table #5: started
2026/08/12-20:33:19.280605 176ad3000 Level-0 table #5: 3739 bytes OK
2026/08/12-20:33:19.280818 176ad3000 Delete type=0 #3
2026/08/12-20:33:19.280857 176ad3000 Delete type=3 #2
2026/08/12-22:44:48.944808 172817000 Recovering log #3
2026/08/12-22:44:48.944973 172817000 Level-0 table #5: started
2026/08/12-22:44:48.945086 172817000 Level-0 table #5: 3739 bytes OK
2026/08/12-22:44:48.945317 172817000 Delete type=0 #3
2026/08/12-22:44:48.945349 172817000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-20:33:03.544720 17385b000 Delete type=3 #1
2026/08/12-22:44:33.331276 16e5c3000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-20:33:19.253872 1762c7000 Recovering log #3
2026/08/12-20:33:19.254006 1762c7000 Level-0 table #5: started
2026/08/12-20:33:19.254155 1762c7000 Level-0 table #5: 5770 bytes OK
2026/08/12-20:33:19.254377 1762c7000 Delete type=0 #3
2026/08/12-20:33:19.254415 1762c7000 Delete type=3 #2
2026/08/12-22:44:48.915783 173023000 Recovering log #3
2026/08/12-22:44:48.915922 173023000 Level-0 table #5: started
2026/08/12-22:44:48.916069 173023000 Level-0 table #5: 5770 bytes OK
2026/08/12-22:44:48.916313 173023000 Delete type=0 #3
2026/08/12-22:44:48.916348 173023000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-20:33:03.496700 174067000 Delete type=3 #1
2026/08/12-22:44:33.284515 16e5c3000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-20:33:19.262214 175abb000 Recovering log #3
2026/08/12-20:33:19.262355 175abb000 Level-0 table #5: started
2026/08/12-20:33:19.262536 175abb000 Level-0 table #5: 6939 bytes OK
2026/08/12-20:33:19.262774 175abb000 Delete type=0 #3
2026/08/12-20:33:19.262806 175abb000 Delete type=3 #2
2026/08/12-22:44:48.924550 17200b000 Recovering log #3
2026/08/12-22:44:48.924684 17200b000 Level-0 table #5: started
2026/08/12-22:44:48.924837 17200b000 Level-0 table #5: 6939 bytes OK
2026/08/12-22:44:48.925120 17200b000 Delete type=0 #3
2026/08/12-22:44:48.925163 17200b000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-20:33:03.505366 174067000 Delete type=3 #1
2026/08/12-22:44:33.292038 16fde7000 Delete type=3 #1
+5 -5
View File
@@ -1,5 +1,5 @@
2026/08/12-20:33:19.257047 1772df000 Recovering log #3
2026/08/12-20:33:19.257521 1772df000 Level-0 table #5: started
2026/08/12-20:33:19.257788 1772df000 Level-0 table #5: 14767 bytes OK
2026/08/12-20:33:19.258070 1772df000 Delete type=0 #3
2026/08/12-20:33:19.258099 1772df000 Delete type=3 #2
2026/08/12-22:44:48.918709 173023000 Recovering log #3
2026/08/12-22:44:48.919008 173023000 Level-0 table #5: started
2026/08/12-22:44:48.919255 173023000 Level-0 table #5: 14767 bytes OK
2026/08/12-22:44:48.919627 173023000 Delete type=0 #3
2026/08/12-22:44:48.919674 173023000 Delete type=3 #2
+1 -1
View File
@@ -1 +1 @@
2026/08/12-20:33:03.498976 172843000 Delete type=3 #1
2026/08/12-22:44:33.286188 16e5c3000 Delete type=3 #1
+8 -109
View File
@@ -1075,7 +1075,6 @@ export async function skillRoll({
}
);
rbDressDice(roll, actor);
await roll.toMessage({
speaker: speaker ?? ChatMessage.getSpeaker({ actor }),
content,
@@ -1108,7 +1107,7 @@ export async function majorWoundRoll({ actor, woundType = "blunt", monofilament
console.error("Custodians | majorWoundRoll called without an actor");
return null;
}
const r = rbDressDice(await new Roll("1d10").evaluate(), actor);
const r = await new Roll("1d10").evaluate();
let result = r.total;
if (monofilament && result >= 4) result = 10; // spec §3.3 shift
else if (blunt && result === 10) result = 9;
@@ -1171,7 +1170,7 @@ export async function hitLocationRoll({ target, damage, mode = "ranged",
}
const sys = target.system;
const speciesId = sys.speciesProfile || "baseline";
const r = rbDressDice(await new Roll("1d20").evaluate(), target);
const r = await new Roll("1d20").evaluate();
const loc = locationFor(r.total, speciesId, mode);
const live = (sys.locations ?? []).find(l => l.id === loc.id);
const max = live?.max ?? locationMaxHp(sys.hp?.max ?? 1, loc.frac);
@@ -1280,7 +1279,7 @@ export async function rollReaction(combatant) {
: reactionPrintedFrom(rb.base, f.loadOverride);
const mod = reactionModifier({ aim: f.aim ?? 0, edge: !!f.edge, drag: !!f.drag });
const roll = rbDressDice(await new Roll(`1d${RINGBRP.reaction.die}`).evaluate(), actor);
const roll = await new Roll(`1d${RINGBRP.reaction.die}`).evaluate();
const raw = roll.total;
const coherence = actor.system?.coherence?.value ?? 10;
const shown = fluxDie(raw, coherence);
@@ -2384,7 +2383,7 @@ export async function fall(actor, metres, { soft = false, controlled = false } =
content: `<div class="ringbrp-chat">${game.i18n.format("RINGBRP.Fall.Short", { m: metres })}</div>` });
return { damage: 0 };
}
const roll = rbDressDice(await new Roll(plan.formula).evaluate(), actor);
const roll = await new Roll(plan.formula).evaluate();
await roll.toMessage({ speaker: ChatMessage.getSpeaker({ actor }),
flavor: game.i18n.format("RINGBRP.Fall.Flavor", { m: metres, dice: plan.formula }) });
// armourApplied true: the fall ignores armour, and this is how we say so.
@@ -2417,7 +2416,7 @@ export async function asphyxiate(actor, { held = 0, prepared = true } = {}) {
export async function burn(actor, bandId = "clothes", { round = 1 } = {}) {
if (!actor) return null;
const band = fireBandFor(bandId);
const roll = rbDressDice(await new Roll(band.damage).evaluate(), actor);
const roll = await new Roll(band.damage).evaluate();
// Armour on the round you catch, and never after. A sealed suit is the exception
// and the GM should say so by running it as a `spark` that never spreads.
const armour = (band.armour && round === 1) ? (Number(actor.system.armourPoints) || 0) : 0;
@@ -2436,7 +2435,7 @@ export async function burn(actor, bandId = "clothes", { round = 1 } = {}) {
* armour is only ever half of one, at contact.
*/
export async function detonate({ formula = "4d6", radius = 6, targets = [] } = {}) {
const roll = rbDressDice(await new Roll(formula).evaluate(), null);
const roll = await new Roll(formula).evaluate();
await roll.toMessage({ flavor: game.i18n.format("RINGBRP.Blast.Flavor",
{ formula, r: radius }) });
const out = [];
@@ -4292,7 +4291,7 @@ export async function rollCharacteristicsDialog({ actorName = "" } = {}) {
const rollOne = async k => {
const cell = el.querySelector(`tr[data-ch="${k}"] .rb-roll-value`);
if (!cell || cell.dataset.value) return; // rolled is rolled
const roll = rbDressDice(await new Roll(CHARACTERISTIC_DICE[k]).evaluate());
const roll = await new Roll(CHARACTERISTIC_DICE[k]).evaluate();
await roll.toMessage({
flavor: game.i18n.format("RINGBRP.Gen.RolledChar",
{ ch: game.i18n.localize(`RINGBRP.Char.${k}`) })
@@ -4538,105 +4537,6 @@ Hooks.on("preCreateItem", (item, data) => {
* The colourset is chosen per roll from the roller's Coherence, so the dice
* themselves report the one number the players are trying not to look at.
* ============================================================ */
const RB_DICE = {
textures: {
"custodians-registry": { name: "Registry", file: "custodians-registry.png" },
"custodians-drift": { name: "Drift", file: "custodians-drift.png" },
"custodians-displaced": { name: "Displaced", file: "custodians-displaced.png" },
"custodians-agency": { name: "Agency", file: "custodians-agency.png" }
},
// one colourset per Coherence band, plus the plain agency set
sets: [
{ name: "custodians-anchored", description: "Custodians — Anchored",
foreground: "#f2ea72", background: "#18268d", outline: "#0c125b",
edge: "#6674d8", texture: "custodians-registry", material: "metal" },
{ name: "custodians-loose", description: "Custodians — Loose",
foreground: "#f2ea72", background: "#16215f", outline: "#0c125b",
edge: "#6674d8", texture: "custodians-registry", material: "plastic" },
{ name: "custodians-adrift", description: "Custodians — Adrift",
foreground: "#ecefee", background: "#141e78", outline: "#0c125b",
edge: "#7a6cc8", texture: "custodians-drift", material: "plastic" },
{ name: "custodians-unmoored", description: "Custodians — Unmoored",
foreground: "#ff7a5c", background: "#101a6e", outline: "#08103f",
edge: "#ff7a5c", texture: "custodians-drift", material: "glass" },
{ name: "custodians-displaced", description: "Custodians — Displaced",
foreground: "#c9b6ff", background: "#0c0e3c", outline: "#000000",
edge: "#8a6cc0", texture: "custodians-displaced", material: "glass" },
{ name: "custodians-agency", description: "Custodians — Agency issue",
foreground: "#f2ea72", background: "#16215f", outline: "#0c125b",
edge: "#a8b2e3", texture: "custodians-agency", material: "metal" }
]
};
/** Which set an agent rolls, from the band they are in. */
export function diceSetFor(actor) {
const band = coherenceBand(actor?.system?.coherence?.value ?? 10);
return `custodians-${band?.id ?? "anchored"}`;
}
/** Tag a Roll so Dice So Nice draws it in the roller's colours. */
function rbDressDice(roll, actor) {
if (!roll?.dice?.length) return roll;
const colorset = diceSetFor(actor);
for (const d of roll.dice) {
d.options.colorset = colorset;
d.options.appearance = { ...(d.options.appearance ?? {}), colorset };
}
return roll;
}
/**
* The custom Dice So Nice skins are OFF.
*
* Registering them with "preferred" makes them the default for everybody in the world,
* which overrides whatever dice a player had already chosen for themselves — the system
* helping itself to a setting that belongs to the user. Turned off at the author's
* request; flip this to true to put them back, and everything below still works.
*
* The textures in dice/ and the generator behind `npm run dice` are left alone. Nothing
* reads them while this is false, and deleting work to turn a feature off is a poor
* trade.
*/
const RB_DICE_ENABLED = false;
Hooks.once("diceSoNiceReady", async (dice3d) => {
if (!RB_DICE_ENABLED) return;
try {
dice3d.addSystem({ id: "custodians", name: "The Custodians" }, "preferred");
// addTexture returns a Promise and the colourset that references a texture must
// not be registered before it resolves, or the die renders untextured.
await Promise.all(Object.entries(RB_DICE.textures).map(([id, t]) =>
dice3d.addTexture(id, {
name: t.name, composite: "source-over",
source: `systems/ringbrp/dice/${t.file}`
})));
for (const set of RB_DICE.sets) {
dice3d.addColorset({
...set, category: "The Custodians",
font: "Michroma", visibility: "visible"
}, set.name === "custodians-anchored" ? "default" : "showcase");
}
// The percentile pair is the game's core roll, so it is the one worth dressing.
dice3d.addDicePreset({
type: "d100", system: "custodians",
labels: ["00", "10", "20", "30", "40", "50", "60", "70", "80", "90"]
});
dice3d.addDicePreset({ type: "d10", system: "custodians",
labels: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] });
for (const t of ["d4", "d6", "d8", "d12", "d20"]) {
const n = Number(t.slice(1));
dice3d.addDicePreset({ type: t, system: "custodians",
labels: Array.from({ length: n }, (_, i) => String(i + 1)) });
}
console.log("Custodians | dice registered with Dice So Nice");
} catch (err) {
console.warn("Custodians | could not register dice", err);
}
});
Hooks.on("renderItemDirectory", (app, html) => {
const root = html instanceof HTMLElement ? html : html?.[0];
if (!root || !game.user.isGM || root.querySelector(".rb-genbar")) return;
@@ -5890,7 +5790,6 @@ export async function rollWeaponDamage({ actor = null, weapon, level = "success"
roll = await new Roll(usesDm ? formula + dm : formula).evaluate();
}
rbDressDice(roll, actor);
const note = level === "critical" ? game.i18n.localize("RINGBRP.Chat.DamageCritical")
: level === "special" ? game.i18n.localize("RINGBRP.Chat.DamageSpecial")
: "";
@@ -6204,7 +6103,7 @@ Hooks.once("init", () => {
usePower, grantPower, ARTS, powerCostFor, crossBack,
generateMissionCase, missionDialog, salvageFrom,
crossingArtPoolFor, crossingGrantsArt, crossingCostFor, returnCrossing,
rollWeaponDamage, diceSetFor,
rollWeaponDamage,
restFor, endOfCase, gearBonusFor,
hasTalent, talentAvailable, useTalent, requisition, armCleanScene, resetTalents,
generateCharacter, generateNPC, generateDialog, installContent, grantFullSkillList,
+1 -1
View File
@@ -2,7 +2,7 @@
"id": "ringbrp",
"title": "The Custodians",
"description": "A Basic Roleplaying game of agents who cross the crossing to contain what should not be there — and who come back a little less from here each time.",
"version": "1.0.6",
"version": "1.0.7",
"compatibility": {
"minimum": 13,
"verified": "14.364"
-176
View File
@@ -1,176 +0,0 @@
/**
* Dice textures for The Custodians, generated rather than drawn.
*
* Written as raw RGBA and encoded to PNG with node:zlib, so the repo gains no
* image dependency and the textures can be regenerated deterministically. Every
* pattern is tileable — Dice So Nice wraps these across a die face, and a seam
* shows up immediately on a d20.
*
* The set is the setting: the Custodians keep a filing system, and the game is
* about that filing system being wrong. So the dice carry a ruled grid, and the
* further an agent drifts from their own reality the more the grid stops agreeing
* with itself.
*
* node tools/make-dice-textures.mjs
*/
import { deflateSync } from "node:zlib";
import { writeFile, mkdir } from "node:fs/promises";
const SIZE = 256;
/* ---------- a minimal PNG encoder: RGBA, no interlace ---------- */
function crc32(buf) {
let c, table = crc32.table;
if (!table) {
table = crc32.table = new Int32Array(256);
for (let n = 0; n < 256; n++) {
c = n;
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
table[n] = c;
}
}
c = -1;
for (let i = 0; i < buf.length; i++) c = table[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
return (c ^ -1) >>> 0;
}
function chunk(type, data) {
const len = Buffer.alloc(4); len.writeUInt32BE(data.length);
const td = Buffer.concat([Buffer.from(type, "ascii"), data]);
const crc = Buffer.alloc(4); crc.writeUInt32BE(crc32(td));
return Buffer.concat([len, td, crc]);
}
function png(width, height, rgba) {
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0); ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8; ihdr[9] = 6; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
// each scanline is prefixed with filter byte 0
const raw = Buffer.alloc(height * (width * 4 + 1));
for (let y = 0; y < height; y++) {
raw[y * (width * 4 + 1)] = 0;
rgba.copy(raw, y * (width * 4 + 1) + 1, y * width * 4, (y + 1) * width * 4);
}
return Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
chunk("IHDR", ihdr),
chunk("IDAT", deflateSync(raw, { level: 9 })),
chunk("IEND", Buffer.alloc(0))
]);
}
/* ---------- deterministic noise, so a rebuild is byte-identical ---------- */
let seed = 20260810;
const rnd = () => (seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296;
const canvas = () => Buffer.alloc(SIZE * SIZE * 4);
const put = (buf, x, y, r, g, b, a = 255) => {
const i = ((y & (SIZE - 1)) * SIZE + (x & (SIZE - 1))) * 4;
buf[i] = r; buf[i + 1] = g; buf[i + 2] = b; buf[i + 3] = a;
};
const fill = (buf, r, g, b, a = 255) => {
for (let i = 0; i < buf.length; i += 4) { buf[i] = r; buf[i + 1] = g; buf[i + 2] = b; buf[i + 3] = a; }
};
/* ============================================================
1. REGISTRY — the ruled grid of a filing system, kept true.
============================================================ */
function registry() {
const b = canvas();
fill(b, 24, 38, 141); // ultramarine ground
for (let i = 0; i < SIZE; i++) {
for (const step of [32]) {
for (let k = 0; k < SIZE; k += step) {
put(b, i, k, 102, 116, 216); // --rb-line
put(b, k, i, 102, 116, 216);
}
}
for (let k = 16; k < SIZE; k += 32) { // fainter half-lines
put(b, i, k, 60, 78, 175);
put(b, k, i, 60, 78, 175);
}
}
// registry ticks at the intersections
for (let x = 0; x < SIZE; x += 64) for (let y = 0; y < SIZE; y += 64) {
for (let d = -3; d <= 3; d++) { put(b, x + d, y, 242, 234, 114); put(b, x, y + d, 242, 234, 114); }
}
return b;
}
/* ============================================================
2. DRIFT — the same grid, no longer agreeing with itself.
Used for agents who have come loose from their own reality.
============================================================ */
function drift() {
const b = canvas();
fill(b, 20, 30, 120);
const wobble = y => Math.round(6 * Math.sin(y / 14) + 3 * Math.sin(y / 5.5));
for (let i = 0; i < SIZE; i++) {
for (let k = 0; k < SIZE; k += 32) {
put(b, i + wobble(i), k, 122, 108, 200);
put(b, k, i + wobble(i), 122, 108, 200);
}
}
for (let n = 0; n < 900; n++) { // grain where it has torn
const x = (rnd() * SIZE) | 0, y = (rnd() * SIZE) | 0;
put(b, x, y, 200, 120, 150, 255);
}
for (let x = 0; x < SIZE; x += 64) for (let y = 0; y < SIZE; y += 64) {
const ox = ((rnd() * 7) | 0) - 3, oy = ((rnd() * 7) | 0) - 3;
for (let d = -3; d <= 3; d++) { put(b, x + d + ox, y + oy, 255, 122, 92); put(b, x + ox, y + d + oy, 255, 122, 92); }
}
return b;
}
/* ============================================================
3. DISPLACED — the grid is gone. What is left is not filed.
============================================================ */
function displaced() {
const b = canvas();
fill(b, 12, 14, 60);
for (let n = 0; n < 5200; n++) {
const x = (rnd() * SIZE) | 0, y = (rnd() * SIZE) | 0;
const v = 40 + ((rnd() * 90) | 0);
put(b, x, y, v, v * 0.5, v * 0.7);
}
// a few surviving fragments of rule, going nowhere
for (let n = 0; n < 26; n++) {
const x = (rnd() * SIZE) | 0, y = (rnd() * SIZE) | 0, len = 6 + ((rnd() * 22) | 0);
const horiz = rnd() > 0.5;
for (let d = 0; d < len; d++) {
if (horiz) put(b, x + d, y, 150, 120, 190); else put(b, x, y + d, 150, 120, 190);
}
}
return b;
}
/* ============================================================
4. AGENCY — brushed plate, for the agency-issue set.
============================================================ */
function agency() {
const b = canvas();
for (let y = 0; y < SIZE; y++) {
for (let x = 0; x < SIZE; x++) {
const grain = 6 * Math.sin(x / 1.7 + y / 90) + (rnd() * 7 - 3.5);
const v = 30 + grain;
put(b, x, y, 22 + v, 33 + v, 95 + v);
}
}
for (let k = 0; k < SIZE; k += 48) for (let i = 0; i < SIZE; i++) put(b, i, k, 90, 104, 190);
return b;
}
const TEXTURES = {
"custodians-registry": registry,
"custodians-drift": drift,
"custodians-displaced": displaced,
"custodians-agency": agency
};
await mkdir(new URL("../dice/", import.meta.url), { recursive: true });
for (const [name, make] of Object.entries(TEXTURES)) {
const buf = png(SIZE, SIZE, make());
await writeFile(new URL(`../dice/${name}.png`, import.meta.url), buf);
console.log(` ${name}.png ${SIZE}x${SIZE} ${(buf.length / 1024).toFixed(1)} kB`);
}
console.log("Custodians | dice textures written");