// PROCITY Lane B — credits.js ⟨R41 §41.5, THE CREDITS SURFACE⟩ // // PROCITY had no attribution surface at all. It ships OpenStreetMap data (ODbL 1.0 — "keep the // © OpenStreetMap contributors credit visible", web/assets/towns/SOURCES.md's own words) and a // vendored MIT three.js, and both obligations were live and undischarged. It is also the gate in // front of 4,472 more licence-clean motion clips on the fleet (100STYLE CC BY 4.0 · CMU · ACCAD // CC BY 3.0) that R41 could not spend for exactly this reason (ROUND41 ruling 2). So: build the // surface, then a later round can spend them by appending rows to a data file. // // ── THE THREE PROPERTIES THIS FILE IS BUILT TO HOLD ────────────────────────────────────────────── // // 1. ZERO DRAWS, structurally. Everything here is DOM. The renderer never sees a credits object, // there is no geometry, no material, no texture and no render pass — so the street draw budget // (291/300 at night, ROUND41 ruling 4: nine draws of margin, no lane spends any) cannot move. // It is the same mechanism the R19 town selector and the R39 fog map use: chrome the composer // never renders. Measured as an A/B against a control tree anyway — see B-progress R41. // // 2. CLASSIC-GATED BY CONSTRUCTION, not by a flag test. There is NO `classic` check in this file. // The shell dynamic-imports this module only on a non-classic boot and hands the result to // `hud.setCredits()`; under `?classic=1` the module is never fetched, `setCredits` is never // called, and the HUD's DOM, key handlers and fetch surface are byte-identical to R40. That is // the null-provider idiom the R40 `#pc-street` row rides, and the R40 playtest harness before it. // // 3. LAZY, so an unopened panel costs nothing. `assets/credits.json` is fetched on FIRST OPEN, not // at boot: a boot that never opens the panel makes no request at all, so the default boot's fetch // surface is unchanged too. Under `?noassets=1` the asset law forbids the fetch outright and the // panel says so honestly rather than fetching anyway or inventing a built-in copy — a boot with // no assets loaded has, correctly, almost nothing to attribute. // // ── THE DATA FILE IS THE PRODUCT ───────────────────────────────────────────────────────────────── // `web/assets/credits.json`, schema `procity-credits/1`. EVERY lane appends its own entries there; // no lane needs to touch this file to be credited. The full schema, the field rules and a paste-ready // row for the three CC-BY motion libraries are in docs/LANES/LANE_B_NOTES.md §41. // // This renderer makes exactly two editorial decisions and both are licence-driven: // • entries whose attribution is a LICENCE OBLIGATION (`required: true`) sort to the top and are // rendered with the required text verbatim, in a box, so it cannot be skimmed past; // • entries carrying a `flag` (an open licence question — an unverified source, or art a human // must clear before release) are rendered in amber. A flag that only exists in a lane note is a // flag nobody sees; R25's lesson (the licence line that went silent while the gate still said OK) // is that the print IS the mechanism. // Everything else is presented as the data file states it. This file never decides a licence. const CREDITS_URL = 'assets/credits.json'; const CSS = ` #pc-credits{position:fixed;inset:0;z-index:40;display:none;align-items:center;justify-content:center; background:rgba(8,6,4,.72);font:13px/1.55 -apple-system,Segoe UI,Roboto,sans-serif;color:#f4efe6} #pc-credits.on{display:flex} #pc-credits-box{width:min(760px,92vw);max-height:86vh;overflow-y:auto;background:rgba(20,16,10,.97); border:1px solid rgba(255,215,94,.35);border-radius:14px;padding:18px 20px 22px;pointer-events:auto; box-shadow:0 18px 60px rgba(0,0,0,.6)} #pc-credits h2{font-size:15px;color:#ffd75e;margin:0 0 2px;font-weight:600;letter-spacing:.06em} #pc-credits .pc-cr-sub{opacity:.72;margin:0 0 14px;font-size:12px} #pc-credits .pc-cr-note{opacity:.85;margin:0 0 16px;padding-bottom:14px;border-bottom:1px solid rgba(255,244,230,.13)} #pc-credits .pc-cr-e{padding:11px 0;border-top:1px solid rgba(255,244,230,.09)} #pc-credits .pc-cr-e:first-of-type{border-top:0} #pc-credits .pc-cr-h{display:flex;gap:9px;align-items:baseline;flex-wrap:wrap} #pc-credits .pc-cr-n{font-weight:600;color:#ffe9b0} #pc-credits .pc-cr-k{font-size:10px;letter-spacing:.09em;text-transform:uppercase;opacity:.5} #pc-credits .pc-cr-lic{font-size:12px;opacity:.8} #pc-credits .pc-cr-u{opacity:.82;margin:4px 0 0} #pc-credits .pc-cr-src{font-size:12px;opacity:.62;margin:3px 0 0} #pc-credits .pc-cr-att{margin:7px 0 0;padding:7px 10px;border-left:3px solid #ffd75e; background:rgba(255,215,94,.09);border-radius:0 6px 6px 0} #pc-credits .pc-cr-att b{color:#ffd75e;font-size:10px;letter-spacing:.09em;display:block;margin-bottom:2px} #pc-credits .pc-cr-flag{margin:7px 0 0;padding:7px 10px;border-left:3px solid #ff9a6a; background:rgba(255,154,106,.09);border-radius:0 6px 6px 0;color:#ffcdb4;font-size:12px} #pc-credits .pc-cr-flag b{color:#ff9a6a;font-size:10px;letter-spacing:.09em;display:block;margin-bottom:2px} #pc-credits .pc-cr-foot{margin-top:16px;padding-top:12px;border-top:1px solid rgba(255,244,230,.13); font-size:12px;opacity:.6;display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap} #pc-credits button{background:rgba(20,16,10,.7);color:#f4efe6;border:1px solid rgba(255,215,94,.35); border-radius:8px;padding:4px 12px;font:12px/1.3 -apple-system,Segoe UI,Roboto,sans-serif;cursor:pointer} #pc-credits button:hover{border-color:rgba(255,215,94,.7);color:#ffd75e} `; const KIND_LABEL = { engine: 'engine', data: 'map data', motion: 'motion', mesh: 'models', texture: 'textures', audio: 'audio', stock: 'cover art', tool: 'tools', font: 'type', }; const esc = (s) => String(s == null ? '' : s) .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); /** * createCredits({ noassets, releasePointer }) → { toggle, open, close, dispose, get isOpen, state } * Builds nothing but a hidden overlay shell; the list is rendered on first open, after the fetch. * Never throws: a missing, 404ing or malformed data file degrades to a stated line, not an error. * * `releasePointer` frees the pointer lock on open, so the panel is clickable and WASD stops moving * a player who is reading. It is the SAME contract the dig card, the sell card and the F8 note ride, * which is why the shell's `unlock` handler carries one more term: an unlock that is us opening the * credits must not walk you out of a shop (index.html, [Lane B R41]). F2/Esc work locked or not. */ export function createCredits({ noassets = false, releasePointer = null } = {}) { if (!document.getElementById('pc-credits-style')) { const st = document.createElement('style'); st.id = 'pc-credits-style'; st.textContent = CSS; document.head.appendChild(st); } const root = document.createElement('div'); root.id = 'pc-credits'; root.innerHTML = '
'; document.body.appendChild(root); const box = root.querySelector('#pc-credits-box'); let open = false, loaded = false, data = null, err = null; // one fetch per session, on first open. `force-cache` matches the towns-index discipline. function load() { if (loaded) return Promise.resolve(); loaded = true; if (noassets) { err = 'noassets'; return Promise.resolve(); } return fetch(CREDITS_URL, { cache: 'force-cache' }) .then((r) => (r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status)))) .then((j) => { if (!j || !Array.isArray(j.entries)) throw new Error('bad shape'); data = j; }) .catch((e) => { err = String((e && e.message) || e); }); // stated in the panel, never thrown } function entryHTML(e) { const kind = KIND_LABEL[e.kind] || e.kind || ''; const lic = e.licence === 'unverified' ? 'licence unverified' : e.licence; const n = Number.isFinite(e.count) && e.count > 0 ? ` · ${e.count}` : ''; let h = '${esc(data.notice || '')}
` + es.map(entryHTML).join('') + 'This boot is running with ?noassets=1 — no textures, models,' + ' audio or town data have been loaded, and the asset law forbids fetching the credits file' + ' either. There is nothing on screen to attribute beyond three.js (MIT, vendored) and the' + ' game\'s own code. The full list ships at web/assets/credits.json.
'; } else { body = `The credits file could not be read (${esc(err || 'unknown')}).` + ' It ships at web/assets/credits.json; nothing has been guessed to fill the gap.
'; } box.innerHTML = 'What PROCITY is built from, and what each source asks for in return.
' + body + '