Files Origin · The Magnus Institute ● Link Secure Clearance 05 SYS 0451
THE BUREAU obsidian declassified
File OCV-10181958 Clearance 05 · SEALED Vol. I · Jun 14, 2026
READY PLAYER 2

Games

Statement Begins

Open a game note and it fills itself with art: key art, the logo, a cover, the studio and the year, a row of screenshots stacked off to one side. The card pulls the lot into something that reads like a storefront page for a game I actually finished.

The artwork is whatever I linked. The card only frames it, lays out the gallery, and works the score.

Plain text in, a shelf listing out. The one thing it cannot supply is whether the hours were worth it.

Custom Views Code

<div class="gv-card" data-name="{{file.name}}">

  <!-- ===== HERO ===== -->
  <section class="gv-hero">
    <img class="gv-hero-bg" src="{{hero}}" alt="" />
    <div class="gv-hero-scrim"></div>
    <div class="gv-hero-center">
      {% if logo %}<img class="gv-logo" src="{{logo}}" alt="{{file.basename}}" />{% else %}<h1 class="gv-logo-fallback">{{file.basename}}</h1>{% endif %}
    </div>
    <div class="gv-hero-strip">
      <span class="gv-platforms">{{system|split:","|join:"&nbsp;&nbsp;·&nbsp;&nbsp;"}}</span>
      <span class="gv-dot">·</span>
      <span class="gv-year">{{year}}</span>
      <span class="gv-dot">·</span>
      <span class="gv-rating"></span>
    </div>
    <div class="gv-scroll-cue">▾</div>
  </section>

  <!-- ===== DETAIL ===== -->
  <section class="gv-detail">
    <div class="gv-info">
      <h1 class="gv-title">{{file.basename}}</h1>
      <p class="gv-genre">{{genre|split:","|join:"&nbsp;&nbsp;·&nbsp;&nbsp;"}}</p>
      <div class="media-score" data-kind="game"></div>
      <div class="gv-meta">
        <div class="gv-meta-item"><span class="gv-k">Maker</span><span class="gv-v">{{maker|split:","|join:", "}}</span></div>
        <div class="gv-meta-item"><span class="gv-k">Publisher</span><span class="gv-v">{{publisher|split:","|join:", "}}</span></div>
        <div class="gv-meta-item"><span class="gv-k">Released</span><span class="gv-v">{{year}}</span></div>
        <div class="gv-meta-item"><span class="gv-k">Last played</span><span class="gv-v">{{last}}</span></div>
        <div class="gv-meta-item"><span class="gv-k">Platforms</span><span class="gv-v">{{system|split:","|join:", "}}</span></div>
              </div>
      <p class="gv-desc">{{description}}</p>
      <button class="gv-more" type="button">SHOW MORE</button>
      <div class="gv-body"></div>
    </div>
    <aside class="gv-art">
      <img class="gv-cover" src="{{cover}}" alt="" />
    </aside>
  </section>

  <!-- ===== GALLERY (built by js from data-shots) ===== -->
  <section class="gv-gallery-wrap">
    <h2 class="gv-section-title">Screenshots</h2>
    <div class="gv-gallery" data-shots="{{screenshots}}"></div>
  </section>

</div>
.obsidian-custom-view-editable .obsidian-custom-view-render,
.obsidian-custom-view-render {
  padding: 0 !important;
  --line-width: 100%;
  --max-width: none;
}

.gv-card {
  --gv-bg: var(--background-primary);
  --gv-pad: clamp(20px, 6vw, 96px);
  position: relative;
  width: 100%;
  background: var(--gv-bg);
  color: var(--text-normal);
  font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro', sans-serif;
  /* respond to the pane's own width, not the viewport (fixes narrow panes + mobile) */
  container-type: inline-size;
  container-name: gv;
}

/* ===== HERO ===== */
.gv-hero {
  position: relative;
  width: 100%;
  min-height: min(82vh, 760px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  overflow: hidden;
}
.gv-hero-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 22%;
  max-width: none;
}
.gv-hero-scrim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(125% 78% at 50% 28%, transparent 42%, rgba(0,0,0,.5) 100%),
    linear-gradient(to bottom, rgba(0,0,0,.28) 0%, transparent 30%, transparent 52%, var(--gv-bg) 98%);
}
.gv-hero-center {
  position: relative;
  z-index: 2;
  flex: 1;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 7vh 8vw 0;
  box-sizing: border-box;
}
.gv-logo {
  max-width: min(620px, 64%);
  max-height: 44%;
  width: auto;
  object-fit: contain;
  filter: drop-shadow(0 6px 26px rgba(0,0,0,.75));
}
.gv-logo-fallback {
  margin: 0;
  text-align: center;
  font-size: clamp(40px, 7vw, 104px);
  font-weight: 700;
  letter-spacing: -2px;
  color: #fff;
  text-shadow: 0 4px 26px rgba(0,0,0,.85);
}
.gv-hero-strip {
  position: relative;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 8px 10px;
  padding: 0 16px clamp(20px, 4vh, 44px);
  font-size: clamp(12px, 1vw, 16px);
  font-weight: 500;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: rgba(255,255,255,.88);
  text-shadow: 0 2px 12px rgba(0,0,0,.85);
}
.gv-hero-strip .gv-dot { opacity: .45; }
.gv-rating { color: var(--color-accent, #d8b25a); }
.gv-scroll-cue {
  position: absolute;
  z-index: 2;
  bottom: 6px;
  left: 50%;
  font-size: 18px;
  color: rgba(255,255,255,.45);
  transform: translateX(-50%);
  animation: gv-bob 1.8s ease-in-out infinite;
}
@keyframes gv-bob { 0%,100%{transform:translate(-50%,0)} 50%{transform:translate(-50%,6px)} }

/* ===== DETAIL ===== */
.gv-detail {
  position: relative;
  display: grid;
  grid-template-columns: 1fr minmax(220px, 320px);
  gap: clamp(24px, 4vw, 56px);
  align-items: start;
  padding: clamp(28px, 5vh, 60px) var(--gv-pad) clamp(32px, 5vh, 56px);
  background: var(--gv-bg);
}
.gv-info { min-width: 0; display: flex; flex-direction: column; gap: 18px; }
.gv-title {
  margin: 0;
  font-size: clamp(30px, 3.4vw, 52px);
  font-weight: 600;
  letter-spacing: -1.2px;
  line-height: 1.05;
}
.gv-genre {
  margin: 0;
  font-size: clamp(15px, 1.3vw, 20px);
  font-weight: 500;
  letter-spacing: -.3px;
  color: var(--text-muted);
}
.gv-meta {
  display: grid;
  grid-template-columns: repeat(2, minmax(0,1fr));
  gap: 0 36px;
  margin: 4px 0;
  border-top: 1px solid var(--background-modifier-border);
  border-bottom: 1px solid var(--background-modifier-border);
}
.gv-meta-item { display: flex; flex-direction: column; gap: 3px; padding: 11px 0; min-width: 0; }
.gv-k {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-faint);
}
.gv-v { font-size: 15px; color: var(--text-normal); word-break: break-word; }
.gv-desc {
  margin: 0;
  font-size: clamp(15px, 1.15vw, 18px);
  line-height: 1.6;
  letter-spacing: -.2px;
  color: var(--text-muted);
}
.gv-body { color: var(--text-muted); font-size: 16px; line-height: 1.6; }
.gv-body > :first-child { margin-top: 0; }
.gv-body .markdown-rendered-content.markdown-preview-view.markdown-rendered { margin: 0; padding: 0; }

/* cover vertically centered against the content column */
.gv-art { align-self: center; }
.gv-cover {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border-radius: 12px;
  box-shadow: 0 20px 50px rgba(0,0,0,.55);
  max-width: none;
}

/* ===== GALLERY ===== */
.gv-gallery-wrap {
  padding: 4px var(--gv-pad) clamp(40px, 8vh, 90px);
  background: var(--gv-bg);
}
.gv-section-title {
  margin: 0 0 16px;
  font-size: clamp(18px, 1.6vw, 24px);
  font-weight: 600;
  letter-spacing: -.4px;
  color: var(--text-normal);
}
.gv-gallery {
  display: flex;
  gap: 14px;
  overflow-x: auto;
  padding-bottom: 8px;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
}
.gv-shot {
  height: clamp(150px, 22vh, 230px);
  width: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 10px;
  flex: 0 0 auto;
  scroll-snap-align: start;
  cursor: pointer;
  background: var(--background-secondary);
  box-shadow: 0 8px 22px rgba(0,0,0,.4);
  transition: transform .15s ease;
}
.gv-shot:hover { transform: translateY(-2px); }

/* ===== NARROW PANES / MOBILE =====
   Container queries (respond to the card's width) with viewport @media as fallback. */
@container gv (max-width: 760px) {
  .gv-hero { min-height: min(68vh, 560px); }
  .gv-logo { max-width: 82%; max-height: 38%; }
  .gv-detail { grid-template-columns: 1fr; gap: 22px; padding-top: clamp(22px, 4vh, 40px); }
  .gv-art { order: -1; align-self: center; justify-self: center; max-width: 240px; width: 100%; }
}
@container gv (max-width: 560px) {
  .gv-meta { grid-template-columns: 1fr; }
  .gv-art { max-width: 200px; }
  .gv-shot { height: 150px; }
}

@media (max-width: 760px) {
  .gv-hero { min-height: min(68vh, 560px); }
  .gv-logo { max-width: 82%; max-height: 38%; }
  .gv-detail { grid-template-columns: 1fr; gap: 22px; padding-top: clamp(22px, 4vh, 40px); }
  .gv-art { order: -1; align-self: center; justify-self: center; max-width: 240px; width: 100%; }
}
@media (max-width: 560px) {
  .gv-meta { grid-template-columns: 1fr; }
  .gv-art { max-width: 200px; }
  .gv-shot { height: 150px; }
}

/* -- SHOW MORE button + collapsible notes (added 2026-05-30) -- */
.gv-more {
  display: inline-block; align-self: flex-start;
  font: 600 11px/1 -apple-system, BlinkMacSystemFont, sans-serif; letter-spacing: .12em;
  color: var(--text-normal); background: var(--background-modifier-hover);
  border: 1px solid var(--background-modifier-border); padding: 9px 17px;
  border-radius: 999px; cursor: pointer; transition: background .15s ease;
}
.gv-more:hover { background: var(--background-modifier-active-hover); }
.gv-body { display: none; margin-top: 16px; }
.gv-card.notes-open .gv-body { display: block; }
.gv-body > :first-child { margin-top: 0; }
const root = this;

/* build the screenshot gallery from data-shots (robust to any list stringification) */
const gal = root.querySelector(".gv-gallery");
if (gal) {
  const raw = gal.getAttribute("data-shots") || "";
  const shots = raw.match(/https?:\/\/[^\s,"'\]\[]+/g) || [];
  const wrap = root.querySelector(".gv-gallery-wrap");
  if (!shots.length) {
    if (wrap) wrap.remove();
  } else {
    gal.innerHTML = "";
    for (const url of shots) {
      const img = document.createElement("img");
      img.className = "gv-shot";
      img.src = url;
      img.loading = "lazy";
      img.alt = "";
      img.addEventListener("click", () => window.open(url, "_blank"));
      gal.appendChild(img);
    }
  }
}

/* if the hero art fails to load, fall back to the cover so the section is never blank */
const bg = root.querySelector(".gv-hero-bg");
const coverEl = root.querySelector(".gv-cover");
if (bg) {
  bg.addEventListener("error", () => {
    if (coverEl && coverEl.src) bg.src = coverEl.src;
  });
}

/* -- SHOW MORE: render note body (frontmatter + score callout stripped), toggle like Albums -- */
(async () => {
  try {
    const _card = root.querySelector(".gv-card") || root;
    const moreBtn = _card.querySelector(".gv-more");
    const notesEl = _card.querySelector(".gv-body");
    if (!notesEl) return;
    const wantName = _card.dataset.name || "";
    let file = app.workspace.getActiveFile();
    if ((!file && wantName) || (file && wantName && file.name !== wantName)) {
      const byName = app.vault.getFiles().find(f => f.name === wantName);
      if (byName) file = byName;
    }
    const fail = () => { if (moreBtn) moreBtn.remove(); notesEl.remove(); };
    if (!file) return fail();
    let body = await app.vault.read(file);
    body = body.replace(/^---[\s\S]*?\n---\n?/, "");
    body = body.replace(/^\s*((?:>.*\n?)+)\n?/, m => /dataviewjs|\[!abstract\]/.test(m) ? "" : m);
    body = body.trim();
    const meaningful = body.replace(/^#{1,6}\s.*$/gm, "").replace(/^#\S+\s*$/gm, "").replace(/^suggested by.*$/gim, "").replace(/\s+/g, "");
    if (!meaningful) return fail();
    const obs = require("obsidian");
    notesEl.classList.add("markdown-rendered");
    notesEl.innerHTML = "";
    const comp = new obs.Component();
    if (obs.MarkdownRenderer && obs.MarkdownRenderer.render) await obs.MarkdownRenderer.render(app, body, notesEl, file.path, comp);
    else if (obs.MarkdownRenderer) await obs.MarkdownRenderer.renderMarkdown(body, notesEl, file.path, comp);
    if (moreBtn) moreBtn.addEventListener("click", () => {
      const open = _card.classList.toggle("notes-open");
      moreBtn.textContent = open ? "SHOW LESS" : "SHOW MORE";
    });
  } catch (e) { console.error("[Custom Views notes]", e); }
})();


/* -- computed 0-100 score + verdict (mirrors the medium's scoring rubric) -- */
(() => {
  try {
    const _scard = root.querySelector(".gv-card"); if (!_scard) return;
    const el = _scard.querySelector(".media-score");
    let _sf = app.workspace.getActiveFile();
    const wn = _scard.dataset.name || "";
    if (wn && (!_sf || _sf.name !== wn)) { const b = app.vault.getFiles().find(x => x.name === wn); if (b) _sf = b; }
    const fm = _sf ? ((app.metadataCache.getFileCache(_sf) || {}).frontmatter || {}) : {};
    const n = v => (typeof v === "number" && !isNaN(v)) ? v : 0;
    const C = ["gameplay","narrative","art_direction","sound","world_design","replayability","polish","pacing","immersion","personal_score"];
    const sum = C.reduce((a, k) => a + n(fm[k]), 0);
    const count = C.filter(k => n(fm[k])).length;
    const bonus = (fm.favorite?3:0)+(fm.replayed?2:0)+(fm.completed?2:0);
    const total = count >= 6 ? Math.min(100, Math.round(sum / count * 10) + bonus) : null;
    const B = [[100,'A masterpiece - flawless in every respect.'],[95,'An all-time great with only unoverlookable minor flaws.'],[90,'Genre-defining, held back only slightly. An all-timer.'],[85,'A standout of its year - memorable, with very few flaws.'],[80,'Excellent and distinctive. Well worth your time.'],[75,'Above average, with several elevating elements.'],[70,'Solid, lifted by some standout element.'],[65,'Above average, but with noticeable flaws.'],[60,'Decent - some genuinely enjoyable parts.'],[55,'Passable, but disappoints on most fronts.'],[50,'The midpoint - average, forgettable.'],[45,'Subpar; falls short on most fronts.'],[40,'Poor - significant flaws, little to enjoy.'],[35,'Below average; flawed and unoriginal.'],[30,'Flat and typical; forgettable.'],[25,'Disappointing; problems outweigh the positives.'],[20,'Largely unenjoyable; few redeeming qualities.'],[15,'Severely flawed; not worth experiencing.'],[10,'Among the worst of its kind.'],[5,'Abysmal - a complete failure.'],[0,'Not worth mentioning.']];
    const verdict = total === null ? "" : (B.find(b => total >= b[0]) || B[B.length - 1])[1];
    if (el) {
      if (total === null) el.innerHTML = `<span class="media-score-num" data-empty="1">Not yet scored</span><span class="media-verdict">${count}/6 core fields filled</span>`;
      else el.innerHTML = `<span class="media-score-num">${total}</span><span class="media-verdict">${verdict}</span>`;
    }
    const hr = _scard.querySelector(".gv-rating"); if (hr) hr.textContent = (total === null ? "—" : String(total));
  } catch (e) { console.error("[Custom Views score]", e); }
})();

Templater Template

<%*
/**
 * Wikidata + Wikipedia → Video Game Frontmatter (Obsidian Templater)
 * No API key required.
 * - Wikidata search (mwapi) → filtered to items that are video games (P31/P279* → Q7889).
 * - Wikidata detail SPARQL → cover (P18), year (P577), developer (P178), publisher (P123),
 *   genre (P136), platform (P400), short description, Wikipedia article link.
 * - Wikipedia REST summary → richer description if available.
 * Rendered as a card by the Custom Views "Games" view.
 */
const SPARQL = "https://query.wikidata.org/sparql";
const WPSUM  = "https://en.wikipedia.org/api/rest_v1/page/summary";
const today  = tp.date.now("YYYY-MM-DD");
const fileTitle = tp.file.title;

const esc = s => (s || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"');

async function sparql(query) {
  return await tp.web.request(`${SPARQL}?format=json&query=${encodeURIComponent(query)}`);
}

// 1) Prompt for title ---------------------------------------------------
let query = await tp.system.prompt("Enter game name (leave empty for file name):");
if (!query || !query.trim()) query = fileTitle;
query = query.trim();

// 2) Wikidata search (filtered to video games) -------------------------
const searchQ = `
SELECT ?game ?gameLabel ?gameDescription ?year ?image WHERE {
  SERVICE wikibase:mwapi {
    bd:serviceParam wikibase:api "EntitySearch" .
    bd:serviceParam wikibase:endpoint "www.wikidata.org" .
    bd:serviceParam mwapi:search "${esc(query)}" .
    bd:serviceParam mwapi:language "en" .
    ?game wikibase:apiOutputItem mwapi:item .
  }
  ?game wdt:P31/wdt:P279* wd:Q7889 .
  OPTIONAL { ?game wdt:P577 ?date . BIND(YEAR(?date) AS ?year) . }
  OPTIONAL { ?game wdt:P18 ?image . }
  SERVICE wikibase:label { bd:serviceParam wikibase:language "en". }
}
LIMIT 15`;

let searchResults;
try { searchResults = await sparql(searchQ); }
catch (e) { tR += `Wikidata search error: ${e}`; return; }
const rows = (searchResults && searchResults.results && searchResults.results.bindings) || [];
if (!rows.length) { tR += `No video-game results on Wikidata for "${query}".`; return; }

// Dedupe by entity (mwapi can return duplicates through subclass paths)
const seen = new Set();
const candidates = [];
for (const r of rows) {
  const uri = r.game && r.game.value;
  if (!uri || seen.has(uri)) continue;
  seen.add(uri);
  candidates.push({
    qid:   uri.replace("http://www.wikidata.org/entity/", ""),
    label: (r.gameLabel && r.gameLabel.value) || "Unknown",
    desc:  (r.gameDescription && r.gameDescription.value) || "",
    year:  (r.year && r.year.value) || "",
    image: (r.image && r.image.value) || "",
  });
}

const display = candidates.map(c => {
  const y = c.year || "????";
  const d = c.desc ? ` — ${c.desc}` : "";
  return `${c.label} (${y})${d}`;
});

// 3) Pick ---------------------------------------------------------------
const chosen = await tp.system.suggester(display, candidates, true, "Pick a game");
if (!chosen) { tR += "Selection cancelled."; return; }

// 4) Wikidata detail SPARQL --------------------------------------------
const detailQ = `
SELECT ?image ?year ?wdDesc ?wikiUrl
       (GROUP_CONCAT(DISTINCT ?devLabel; separator="|") AS ?devs)
       (GROUP_CONCAT(DISTINCT ?pubLabel; separator="|") AS ?pubs)
       (GROUP_CONCAT(DISTINCT ?genreLabel; separator="|") AS ?genres)
       (GROUP_CONCAT(DISTINCT ?platformLabel; separator="|") AS ?platforms)
WHERE {
  BIND(wd:${chosen.qid} AS ?game) .
  OPTIONAL { ?game wdt:P18 ?image . }
  OPTIONAL { ?game wdt:P577 ?date . BIND(YEAR(?date) AS ?year) . }
  OPTIONAL { ?game schema:description ?wdDesc . FILTER(LANG(?wdDesc)="en") . }
  OPTIONAL { ?wikiUrl schema:about ?game ; schema:isPartOf <https://en.wikipedia.org/> . }
  OPTIONAL { ?game wdt:P178 ?dev . ?dev rdfs:label ?devLabel . FILTER(LANG(?devLabel)="en") . }
  OPTIONAL { ?game wdt:P123 ?pub . ?pub rdfs:label ?pubLabel . FILTER(LANG(?pubLabel)="en") . }
  OPTIONAL { ?game wdt:P136 ?genre . ?genre rdfs:label ?genreLabel . FILTER(LANG(?genreLabel)="en") . }
  OPTIONAL { ?game wdt:P400 ?platform . ?platform rdfs:label ?platformLabel . FILTER(LANG(?platformLabel)="en") . }
}
GROUP BY ?image ?year ?wdDesc ?wikiUrl`;

let detail;
try { detail = await sparql(detailQ); }
catch (e) { tR += `Wikidata detail error: ${e}`; return; }
const dr = ((detail && detail.results && detail.results.bindings) || [])[0] || {};

const split = s => s ? s.split("|").filter(Boolean) : [];
let   image    = (dr.image    && dr.image.value)    || chosen.image || "";
const year     = (dr.year     && dr.year.value)     || chosen.year  || "";
const wdDesc   = (dr.wdDesc   && dr.wdDesc.value)   || chosen.desc  || "";
const wikiUrl  = (dr.wikiUrl  && dr.wikiUrl.value)  || "";
const devs     = split((dr.devs      || {}).value);
const pubs     = split((dr.pubs      || {}).value);
const genres   = split((dr.genres    || {}).value);
const platforms= split((dr.platforms || {}).value).slice(0, 6);

// 5) Wikipedia summary: richer description + cover fallback ------------
// Wikidata's P18 only accepts freely-licensed images, so most modern game
// box art is missing there. Wikipedia's summary endpoint exposes the
// article's fair-use cover via `originalimage.source` — use it as a fallback.
let description = wdDesc;
if (wikiUrl) {
  try {
    const slug = decodeURIComponent((wikiUrl.split("/wiki/")[1] || ""));
    const summary = await tp.web.request(`${WPSUM}/${encodeURIComponent(slug)}`);
    if (summary && summary.extract) description = summary.extract;
    if (!image && summary && summary.originalimage && summary.originalimage.source) {
      image = summary.originalimage.source;
    }
  } catch (e) { /* fall back to wdDesc */ }
}
description = (description || "").replace(/\r?\n+/g, " ").replace(/"/g, '\\"').slice(0, 700);

// Helper: link a company note if one exists
async function noteExists(name) {
  for (const folder of ["References", ""]) {
    const path = folder ? `${folder}/${name}.md` : `${name}.md`;
    try { if (await tp.file.exists(path)) return true; } catch (e) {}
  }
  return false;
}
async function linkOrName(names, alwaysLinkFirst = 2) {
  const out = [];
  for (let i = 0; i < names.length; i++) {
    if (i < alwaysLinkFirst) out.push(`[[${names[i]}]]`);
    else out.push((await noteExists(names[i])) ? `[[${names[i]}]]` : names[i]);
  }
  return out;
}
// 6) Last played ----------------------------------------------
let lastDate = await tp.system.prompt("Last played date (YYYY-MM-DD)", today);
if (!lastDate || !/^\d{4}-\d{2}-\d{2}$/.test(lastDate.trim())) lastDate = today;
lastDate = lastDate.trim();

// 7) Rename to "Games - Title" -----------------------------------------
const gameTitle = chosen.label;
const colonFixed = gameTitle.replace(/:/g, " - ");
let sanitized = colonFixed.replace(/[\/#%&{}<>*?$!'"@+`|=]/g, "").trim();
if (!sanitized) sanitized = fileTitle;
const requiresAlias = sanitized !== gameTitle;
const newName = sanitized;
if (newName !== fileTitle) await tp.file.rename(newName);

// 7b) SteamGridDB art (hero + logo) + Steam screenshots ----------------
// Needs Obsidian's requestUrl (supports the SteamGridDB Authorization header
// and bypasses CORS). Available on desktop via require("obsidian"); if it's
// unavailable the note still gets created from Wikidata, just without art.
const SGDB_KEY = (getComputedStyle(document.body).getPropertyValue("--steamgriddb-api-key") || "").trim().replace(/^["']|["']$/g, "");
let requestUrl = null;
try { requestUrl = require("obsidian").requestUrl; } catch (e) {}
if (!requestUrl && tp.obsidian && tp.obsidian.requestUrl) requestUrl = tp.obsidian.requestUrl;
const normName = s => (s || "").toLowerCase().replace(/[^a-z0-9]/g, "");
let hero = "", logo = "", screenshots = [];
if (requestUrl) {
  const SG = "https://www.steamgriddb.com/api/v2";
  const sgHdr = { Authorization: "Bearer " + SGDB_KEY };
  try {
    const s = await requestUrl({ url: `${SG}/search/autocomplete/${encodeURIComponent(gameTitle)}`, headers: sgHdr });
    const cands = (s.json && s.json.data) || [];
    const g = cands.find(x => normName(x.name) === normName(gameTitle)) || cands[0];
    if (g) {
      const hr = await requestUrl({ url: `${SG}/heroes/game/${g.id}`, headers: sgHdr });
      const heroes = ((hr.json && hr.json.data) || []).slice().sort((a, b) => (b.width || 0) - (a.width || 0));
      if (heroes[0]) hero = heroes[0].url;
      const lr = await requestUrl({ url: `${SG}/logos/game/${g.id}`, headers: sgHdr });
      const order = { official: 0, white: 1, custom: 2 };
      const logos = ((lr.json && lr.json.data) || []).slice().sort((a, b) => (order[a.style] ?? 3) - (order[b.style] ?? 3));
      if (logos[0]) logo = logos[0].url;
    }
  } catch (e) { /* keep going without SGDB art */ }
  try {
    const ss = await requestUrl({ url: `https://store.steampowered.com/api/storesearch/?term=${encodeURIComponent(gameTitle)}&cc=us&l=en` });
    const items = (ss.json && ss.json.items) || [];
    const pick = items.find(x => normName(x.name) === normName(gameTitle)) || items[0];
    if (pick) {
      const det = await requestUrl({ url: `https://store.steampowered.com/api/appdetails?appids=${pick.id}&l=en` });
      const data = det.json && det.json[pick.id] && det.json[pick.id].data;
      if (data && Array.isArray(data.screenshots)) {
        screenshots = data.screenshots.slice(0, 8).map(x => (x.path_full || "").split("?")[0]).filter(Boolean);
      }
    }
  } catch (e) { /* keep going without screenshots */ }
}

// 8) Build YAML ---------------------------------------------------------
// genre / maker / publisher / system are written as PLAIN TEXT (no wikilinks),
// only quoting values that contain YAML-significant characters.
const yv = s => {
  s = (s || "").toString();
  return /[:#\[\]{}&*!|>'"%@`,]|^[\s-]/.test(s)
    ? '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'
    : s;
};
const L = [];
L.push("---");
L.push("connections:");
L.push("categories:");
L.push('  - "[[Games]]"');
if (genres.length) { L.push("genre:"); genres.forEach(g => L.push(`  - ${yv(g)}`)); } else L.push("genre: []");
if (devs.length) { L.push("maker:"); devs.forEach(m => L.push(`  - ${yv(m)}`)); } else L.push("maker: []");
if (pubs.length) { L.push("publisher:"); pubs.forEach(p => L.push(`  - ${yv(p)}`)); } else L.push("publisher: []");
if (platforms.length) { L.push("system:"); platforms.forEach(p => L.push(`  - ${yv(p)}`)); } else L.push("system: []");
L.push(image ? `cover: "${image}"` : "cover: []");
if (hero) L.push(`hero: ${hero}`);
if (logo) L.push(`logo: ${logo}`);
if (screenshots.length) { L.push("screenshots:"); screenshots.forEach(s => L.push(`  - ${s}`)); }
L.push(`description: "${description}"`);
L.push(`year: ${year}`);
L.push(`created: ${today}`);
L.push(`last: ${lastDate}`);
L.push(`qid: ${chosen.qid}`);
if (wikiUrl) L.push(`wiki: "${wikiUrl}"`);

// Scores — each 0–10 ----------------------------------------------------
L.push("gameplay: ");
L.push("narrative: ");
L.push("art_direction: ");
L.push("sound: ");
L.push("world_design: ");
L.push("replayability: ");
L.push("polish: ");
L.push("pacing: ");
L.push("immersion: ");
L.push("personal_score: ");

// Flags ------------------------------------------------------------------
L.push("favorite: false");
L.push("replayed: false");
L.push("completed: false");
L.push("status:");

L.push("---");
const body = [
  "",
  "> [!abstract] Score",
  "> ```dataviewjs",
  "> const p = dv.current();",
  '> const n = v => (typeof v === "number" && !isNaN(v)) ? v : 0;',
  '> const C = ["gameplay","narrative","art_direction","sound","world_design","replayability","polish","pacing","immersion","personal_score"];',
  "> const sum = C.reduce((a,k)=>a+n(p[k]),0);",
  "> const count = C.filter(k=>n(p[k])).length;",
  "> const bonus = (p.favorite?3:0) + (p.replayed?2:0) + (p.completed?2:0);",
  "> const total = count >= 6 ? Math.min(100, Math.round(sum/count*9 + bonus/7*10)) : null;",
  "> const B = [[100,'A masterpiece — flawless in every respect.'],[95,'An all-time great with only unoverlookable minor flaws.'],[90,'Genre-defining, held back only slightly. An all-timer.'],[85,'A standout of its year — memorable, with very few flaws.'],[80,'Excellent and distinctive. Well worth your time.'],[75,'Above average, with several elevating elements.'],[70,'Solid, lifted by some standout element.'],[65,'Above average, but with noticeable flaws.'],[60,'Decent — some genuinely enjoyable parts.'],[55,'Passable, but disappoints on most fronts.'],[50,'The midpoint — average, forgettable.'],[45,'Subpar; falls short on most fronts.'],[40,'Poor — significant flaws, little to enjoy.'],[35,'Below average; flawed and unoriginal.'],[30,'Flat and typical; forgettable.'],[25,'Disappointing; problems outweigh the positives.'],[20,'Largely unenjoyable; few redeeming qualities.'],[15,'Severely flawed; not worth experiencing.'],[10,'Among the worst of its kind.'],[5,'Abysmal — a complete failure.'],[0,'Not worth mentioning.']];",
  "> const verdict = total === null ? '' : (B.find(b => total >= b[0]) || B[B.length-1])[1];",
  "> dv.paragraph(total === null ? `_Not yet scored — ${count}/6 core fields._` : `**${total}** — ${verdict}`);",
  "> ```",
  "",
  "## Premise",
  "",
  "## First Hours",
  "",
  "## Gameplay",
  "",
  "## Story & World",
  "",
  "## Art & Sound",
  "",
  "## Verdict",
  ""
];

tR += L.join("\n") + "\n" + body.join("\n");
-%>
DECLASSIFIED

Field Assembly

Three plugins build this storefront in your vault.

  • Custom Views: the renderer. Make a view, build its rule in the menu to match categories contains Games (the screenshot above), then paste the template, styling, and JS blocks into their fields.
  • Templater: the note-maker. Paste the Templater block into a template file and bind it to a hotkey or a folder template.
  • Stardust Importer: mine, in the community store. It holds the key and publishes it as the --steamgriddb-api-key CSS variable the template reads. Without it, supply your own with a snippet: body { --steamgriddb-api-key: "your-key"; }.

That is the whole apparatus: a rule, a template, a script, and a key on the body.

Statement Ends

▌ Cross-Referenced Files