Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad771ff2b5 | ||
|
|
53481bdfc1 |
@@ -426,6 +426,13 @@ If there is time — and there usually is, because this ends early — she says
|
|||||||
|
|
||||||
## Artwork
|
## Artwork
|
||||||
|
|
||||||
|
**Showing them at the table.** The adventure ships a third journal,
|
||||||
|
**LAST ADMISSION — Show the players**, holding these as eight image pages at player
|
||||||
|
visibility. Right-click a page and *Show Players*: the table gets the picture and
|
||||||
|
nothing else — no heading, no caption, no label naming what they are looking at. The
|
||||||
|
page names are for the GM's sidebar and the players never see them.
|
||||||
|
|
||||||
|
|
||||||
**Style line:** flat editorial vector, deep navy and pale yellow with warm off-white,
|
**Style line:** flat editorial vector, deep navy and pale yellow with warm off-white,
|
||||||
1970s British public-information poster, `--style raw --ar 3:2 --v 6`.
|
1970s British public-information poster, `--style raw --ar 3:2 --v 6`.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2026/08/12-19:01:54.231003 174603000 Delete type=3 #1
|
2026/08/12-19:24:52.815175 17303f000 Delete type=3 #1
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2026/08/12-19:01:54.298648 173df7000 Delete type=3 #1
|
2026/08/12-19:24:52.889991 17384b000 Delete type=3 #1
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2026/08/12-19:01:54.232848 174603000 Delete type=3 #1
|
2026/08/12-19:24:52.817229 17303f000 Delete type=3 #1
|
||||||
|
|||||||
Binary file not shown.
@@ -1 +1 @@
|
|||||||
2026/08/12-19:01:54.282484 173df7000 Delete type=3 #1
|
2026/08/12-19:24:52.871528 174057000 Delete type=3 #1
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2026/08/12-19:01:54.266480 174603000 Delete type=3 #1
|
2026/08/12-19:24:52.853027 17303f000 Delete type=3 #1
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2026/08/12-19:01:54.293373 173df7000 Delete type=3 #1
|
2026/08/12-19:24:52.884011 174057000 Delete type=3 #1
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2026/08/12-19:01:54.252536 174603000 Delete type=3 #1
|
2026/08/12-19:24:52.837710 17303f000 Delete type=3 #1
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2026/08/12-19:01:54.290196 172ddf000 Delete type=3 #1
|
2026/08/12-19:24:52.880282 174057000 Delete type=3 #1
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2026/08/12-19:01:54.286378 174603000 Delete type=3 #1
|
2026/08/12-19:24:52.876362 172833000 Delete type=3 #1
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2026/08/12-19:01:54.297168 1735eb000 Delete type=3 #1
|
2026/08/12-19:24:52.888015 174057000 Delete type=3 #1
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2026/08/12-19:01:54.224954 172ddf000 Delete type=3 #1
|
2026/08/12-19:24:52.808706 17303f000 Delete type=3 #1
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2026/08/12-19:01:54.291920 1735eb000 Delete type=3 #1
|
2026/08/12-19:24:52.882429 174057000 Delete type=3 #1
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2026/08/12-19:01:54.276268 174603000 Delete type=3 #1
|
2026/08/12-19:24:52.863812 17384b000 Delete type=3 #1
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2026/08/12-19:01:54.270666 173df7000 Delete type=3 #1
|
2026/08/12-19:24:52.857469 17384b000 Delete type=3 #1
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2026/08/12-19:01:54.227877 174603000 Delete type=3 #1
|
2026/08/12-19:24:52.811553 17303f000 Delete type=3 #1
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2026/08/12-19:01:54.234705 1735eb000 Delete type=3 #1
|
2026/08/12-19:24:52.819322 17303f000 Delete type=3 #1
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2026/08/12-19:01:54.229633 174603000 Delete type=3 #1
|
2026/08/12-19:24:52.813317 17384b000 Delete type=3 #1
|
||||||
|
|||||||
@@ -1684,3 +1684,104 @@
|
|||||||
.rb-dying { border-left: 3px solid var(--rb-danger); padding-left: 8px; }
|
.rb-dying { border-left: 3px solid var(--rb-danger); padding-left: 8px; }
|
||||||
.rb-stable { border-left: 3px solid var(--rb-yellow); padding-left: 8px; }
|
.rb-stable { border-left: 3px solid var(--rb-yellow); padding-left: 8px; }
|
||||||
.rb-dead { border-left: 3px solid var(--rb-white); padding-left: 8px; }
|
.rb-dead { border-left: 3px solid var(--rb-white); padding-left: 8px; }
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
THE PAUSE — a uniselector, stepping
|
||||||
|
============================================================
|
||||||
|
Foundry's pause is a <figure id="pause"> holding an <img class="fa-spin"> and a
|
||||||
|
<figcaption>. Both are replaced here without touching the markup.
|
||||||
|
|
||||||
|
A uniselector is the stepping switch inside a telephone exchange: an arm that clicks
|
||||||
|
round one contact at a time, hunting for a free line. It is the right object for this
|
||||||
|
game — the department's whole business is a machine that says where things belong,
|
||||||
|
and OPEN DAY is set inside one. Eleven contacts, because eleven minutes is the
|
||||||
|
interval this setting keeps coming back to.
|
||||||
|
|
||||||
|
It STEPS rather than sweeps. A continuous spin reads as a loading throbber; a
|
||||||
|
discrete click every 700ms reads as a machine working through a list, which is what
|
||||||
|
the department is doing while the table is out of the room.
|
||||||
|
|
||||||
|
No new text: Foundry's own caption is localised and this only restyles it. A string
|
||||||
|
hardcoded here would be one the lang file could never reach.
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
#pause {
|
||||||
|
/* Core pins this with BOTH top and bottom, so an auto height stretches it down the
|
||||||
|
whole screen — 180px tall for a 16px caption. Pin it once, here, and size to
|
||||||
|
content. The arm is rotated, so its bounding box is the diagonal of its own square:
|
||||||
|
the panel is laid out from the flex basis, never from getBoundingClientRect. */
|
||||||
|
position: fixed;
|
||||||
|
left: 50%; bottom: 14vh; top: auto; right: auto;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
height: auto; width: max-content;
|
||||||
|
display: flex; align-items: center; gap: 16px;
|
||||||
|
min-width: 320px; padding: 14px 22px 14px 16px;
|
||||||
|
background: color-mix(in srgb, var(--rb-void) 88%, transparent);
|
||||||
|
border: 1px solid var(--rb-line);
|
||||||
|
border-top: 2px solid var(--rb-yellow);
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: 0 10px 40px rgba(0,0,0,.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* the dial: eleven contacts, standing still */
|
||||||
|
#pause::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute; left: 16px; top: 50%;
|
||||||
|
width: 52px; height: 52px; margin-top: -26px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid var(--rb-hairline);
|
||||||
|
background:
|
||||||
|
repeating-conic-gradient(from -90deg,
|
||||||
|
var(--rb-line) 0deg 2.4deg, transparent 2.4deg 32.727deg);
|
||||||
|
-webkit-mask: radial-gradient(circle, transparent 0 15px, #000 16px);
|
||||||
|
mask: radial-gradient(circle, transparent 0 15px, #000 16px);
|
||||||
|
opacity: .95;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* the arm. The <img> keeps its box and loses its pixels: object-position walks the
|
||||||
|
raster out of the frame, which leaves a paintable element without a second wrapper. */
|
||||||
|
#pause img {
|
||||||
|
position: relative; z-index: 1;
|
||||||
|
flex: 0 0 52px; width: 52px; height: 52px; margin: 0;
|
||||||
|
object-fit: none; object-position: -999px -999px;
|
||||||
|
border: none; filter: none;
|
||||||
|
/* the arm, and behind it the contact it has just left — without the trailing sector
|
||||||
|
a 700ms step is easy to miss entirely, and the whole point is that it is stepping */
|
||||||
|
background: conic-gradient(from -92deg,
|
||||||
|
var(--rb-yellow) 0deg 5deg,
|
||||||
|
var(--rb-yellow-dim) 5deg 30deg,
|
||||||
|
transparent 30deg 360deg);
|
||||||
|
-webkit-mask: radial-gradient(circle, #000 0 22px, transparent 23px);
|
||||||
|
mask: radial-gradient(circle, #000 0 22px, transparent 23px);
|
||||||
|
animation: rb-uniselector 7.7s steps(11, end) infinite;
|
||||||
|
}
|
||||||
|
/* Foundry adds fa-spin to that img; ours is the only rotation that should run. */
|
||||||
|
#pause img.fa-spin { animation: rb-uniselector 7.7s steps(11, end) infinite; }
|
||||||
|
|
||||||
|
/* the hub, and the amber that says the record is being written */
|
||||||
|
#pause::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute; left: 38px; top: 50%;
|
||||||
|
width: 8px; height: 8px; margin-top: -4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--rb-yellow);
|
||||||
|
animation: rb-pause-pip 2.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pause figcaption {
|
||||||
|
font-family: var(--rb-display);
|
||||||
|
font-size: 13px; letter-spacing: .22em; text-transform: uppercase;
|
||||||
|
color: var(--rb-yellow);
|
||||||
|
text-shadow: none; padding: 0; margin: 0;
|
||||||
|
border-left: 1px solid var(--rb-hairline); padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rb-uniselector { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||||
|
@keyframes rb-pause-pip { 0%, 100% { opacity: .25; } 50% { opacity: 1; } }
|
||||||
|
|
||||||
|
/* A stepping dial in the corner of the eye is exactly the sort of thing a reduced-motion
|
||||||
|
preference is asking us not to do. Keep the object, stop the movement. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
#pause img, #pause img.fa-spin, #pause::after { animation: none; }
|
||||||
|
#pause img { transform: rotate(-32deg); }
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"id": "ringbrp",
|
"id": "ringbrp",
|
||||||
"title": "The Custodians",
|
"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.",
|
"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.2",
|
"version": "1.0.4",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": 13,
|
"minimum": 13,
|
||||||
"verified": "14.364"
|
"verified": "14.364"
|
||||||
|
|||||||
+50
-3
@@ -461,6 +461,38 @@ function journalDoc(name, key, pages) {
|
|||||||
return { doc: { _id: id, name, pages: pageDocs.map(p => p._id), categories: [], ...BASE() }, pages: pageDocs, id };
|
return { doc: { _id: id, name, pages: pageDocs.map(p => p._id), categories: [], ...BASE() }, pages: pageDocs, id };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A journal of nothing but pictures, for showing to players.
|
||||||
|
*
|
||||||
|
* title.show is FALSE and there is no caption on purpose: when the GM hits Show Players
|
||||||
|
* the table gets the image and nothing else — no heading telling them which room this
|
||||||
|
* is, no label naming the thing they are looking at. The page NAME is for the GM's list
|
||||||
|
* in the sidebar, so they can find the right one in a hurry; the players never see it.
|
||||||
|
*/
|
||||||
|
function imageJournal(name, key, plates) {
|
||||||
|
const id = idFor(`journal:${key}`);
|
||||||
|
const pageDocs = plates.map((p, i) => {
|
||||||
|
// A missing plate is a blank frame on a projector in front of six people.
|
||||||
|
if (!existsSync(path.join(ROOT, p.src))) throw new Error(`${name}: no plate at ${p.src}`);
|
||||||
|
return {
|
||||||
|
_id: idFor(`journal:${key}:${i}`),
|
||||||
|
name: p.name, type: "image",
|
||||||
|
title: { show: false, level: 1 },
|
||||||
|
src: `systems/ringbrp/${p.src}`,
|
||||||
|
image: { caption: "" },
|
||||||
|
text: { format: 1, content: "", markdown: "" },
|
||||||
|
video: { controls: true, volume: 0.5 },
|
||||||
|
system: {}, sort: (i + 1) * 100, ownership: { default: 2 }, flags: {},
|
||||||
|
category: null
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
doc: { _id: id, name, pages: pageDocs.map(p => p._id), categories: [],
|
||||||
|
...BASE(), ownership: { default: 2 }, flags: { ringbrp: { brand: true } } },
|
||||||
|
pages: pageDocs, id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------- writer ---------------- */
|
/* ---------------- writer ---------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -842,6 +874,19 @@ function cullandScene() {
|
|||||||
castDocs.push(actorDoc);
|
castDocs.push(actorDoc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The plates, one per page, for showing at the table. Named for what the GM will be
|
||||||
|
// looking for under pressure, ordered by when it comes up.
|
||||||
|
const showJournal = imageJournal("LAST ADMISSION — Show the players", "la_show", [
|
||||||
|
{ name: "1 · The house, from the drive", src: "art/scenes/la_01_hall.webp" },
|
||||||
|
{ name: "2 · Floor plan — Culland Hall", src: "art/maps/culland-hall.webp" },
|
||||||
|
{ name: "3 · Backstage — the green room", src: "art/scenes/la_06_green.webp" },
|
||||||
|
{ name: "4 · The east corridor", src: "art/scenes/la_03_corridor.webp" },
|
||||||
|
{ name: "5 · The nursery", src: "art/scenes/la_04_nursery.webp" },
|
||||||
|
{ name: "6 · The glasshouse — Old Marrow", src: "art/scenes/la_05_glass.webp" },
|
||||||
|
{ name: "7 · The stairs — the woman standing", src: "art/scenes/la_07_stairs.webp" },
|
||||||
|
{ name: "8 · The car park, afterwards", src: "art/scenes/la_08_carpark.webp" }
|
||||||
|
]);
|
||||||
|
|
||||||
const gmJournal = journalDoc("LAST ADMISSION — Run the prologue", "la_gm", L.LA_PAGES);
|
const gmJournal = journalDoc("LAST ADMISSION — Run the prologue", "la_gm", L.LA_PAGES);
|
||||||
const handouts = journalDoc("LAST ADMISSION — Player handouts", "la_handouts", L.LA_HANDOUTS);
|
const handouts = journalDoc("LAST ADMISSION — Player handouts", "la_handouts", L.LA_HANDOUTS);
|
||||||
handouts.doc.ownership = { default: 2 };
|
handouts.doc.ownership = { default: 2 };
|
||||||
@@ -870,8 +915,9 @@ function cullandScene() {
|
|||||||
actors: castDocs,
|
actors: castDocs,
|
||||||
items: [],
|
items: [],
|
||||||
journal: [
|
journal: [
|
||||||
{ ...gmJournal.doc, pages: gmJournal.pages },
|
{ ...gmJournal.doc, pages: gmJournal.pages },
|
||||||
{ ...handouts.doc, pages: handouts.pages }
|
{ ...handouts.doc, pages: handouts.pages },
|
||||||
|
{ ...showJournal.doc, pages: showJournal.pages }
|
||||||
],
|
],
|
||||||
scenes: [cullandScene()], combats: [], tables: [], macros: [], cards: [], playlists: [], folders: [],
|
scenes: [cullandScene()], combats: [], tables: [], macros: [], cards: [], playlists: [], folders: [],
|
||||||
sort: 0, ...BASE()
|
sort: 0, ...BASE()
|
||||||
@@ -879,7 +925,8 @@ function cullandScene() {
|
|||||||
|
|
||||||
await writePack("lastadmission", [[`!adventures!${advId}`, adventure]]);
|
await writePack("lastadmission", [[`!adventures!${advId}`, adventure]]);
|
||||||
console.log(` lastadm LAST ADMISSION — ${castDocs.length} cast, `
|
console.log(` lastadm LAST ADMISSION — ${castDocs.length} cast, `
|
||||||
+ `${L.LA_PAGES.length} GM pages, ${L.LA_HANDOUTS.length} handouts`);
|
+ `${L.LA_PAGES.length} GM pages, ${L.LA_HANDOUTS.length} handouts, `
|
||||||
|
+ `${showJournal.pages.length} plates to show`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// OPEN DAY — the prologue. A much smaller adventure than BLACK PLATFORM: no case file,
|
// OPEN DAY — the prologue. A much smaller adventure than BLACK PLATFORM: no case file,
|
||||||
|
|||||||
Reference in New Issue
Block a user