diff --git a/docs/CONTENT.md b/docs/CONTENT.md index 76bd321..88739af 100644 --- a/docs/CONTENT.md +++ b/docs/CONTENT.md @@ -177,6 +177,63 @@ PlayStation, Xbox, and Nintendo in one place. --- +## Board games (TTRPGs page) + +The "Board games" section on the TTRPGs page reads from a JSON file and renders +an interactive shelf: a list of games on the left, and a tall image on the +right that cross-fades to a game's BGG cover when you hover its name. Clicking a +name opens the game on BoardGameGeek. + +### The list — `src/data/boardgames.json` + +A flat JSON array. Each entry needs a `name` and a `bggId`; `note` and `image` +are optional: + +```json +[ + { "name": "Wingspan", "bggId": 266192, "note": "Engine builder" }, + { "name": "Brass: Birmingham", "bggId": 224517 } +] +``` + +| Field | Type | Required | Notes | +|---|---|---|---| +| `name` | string | yes | Display label + link text | +| `bggId` | number | yes | BoardGameGeek numeric id (from its URL) | +| `note` | string | no | One-line caption shown under the name | +| `image` | string | no | Cover path/URL; populated by the fetch tool | + +Find a game's `bggId` in its BoardGameGeek URL: +`https://boardgamegeek.com/boardgame//`. + +The shelf photo at rest is `/images/ttrpg-boardgame-shelf.jpg` — drop a real +photo of your shelf at that path. The image is a **progressive enhancement**: +without JS the list still works (links + shelf photo); games without an `image` +just keep the shelf photo on hover. + +### Fetching covers (BoardGameGeek) + +`scripts/fetch-bgg-covers.mjs` fills in the `image` field for any game missing +one, using BGG's public XML2 API (no key needed): + +```bash +# Default: download covers to public/images/boardgames/.jpg (recommended) +node scripts/fetch-bgg-covers.mjs + +# Instead hotlink BGG's CDN URLs (no download, depends on their CDN) +node scripts/fetch-bgg-covers.mjs --remote + +# Re-fetch even games that already have an image +node scripts/fetch-bgg-covers.mjs --force + +# Slow down between BGG calls (they rate-limit; default 1200ms) +node scripts/fetch-bgg-covers.mjs --sleep 2000 +``` + +It's idempotent — run it again any time you add games to the JSON. + +--- + ## Interest pages Each interest is a hand-built page in `src/pages/interests/`. They follow the diff --git a/public/images/boardgames/.gitkeep b/public/images/boardgames/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/images/boardgames/wyrmspan.jpg b/public/images/boardgames/wyrmspan.jpg new file mode 100644 index 0000000..406fef1 Binary files /dev/null and b/public/images/boardgames/wyrmspan.jpg differ diff --git a/public/images/ttrpg-boardgame-shelf.jpg b/public/images/ttrpg-boardgame-shelf.jpg new file mode 100644 index 0000000..6d33332 Binary files /dev/null and b/public/images/ttrpg-boardgame-shelf.jpg differ diff --git a/scripts/fetch-bgg-covers.mjs b/scripts/fetch-bgg-covers.mjs new file mode 100644 index 0000000..826f771 --- /dev/null +++ b/scripts/fetch-bgg-covers.mjs @@ -0,0 +1,168 @@ +#!/usr/bin/env node +/** + * fetch-bgg-covers.mjs + * --------------------------------------------------------------------------- + * Support tool for the Board games section on the TTRPGs page. For each game + * in src/data/boardgames.json that is missing an `image`, it looks the game up + * on the BoardGameGeek XML2 API (public, no key — just a User-Agent), and + * either: + * + * (default) downloads the cover to public/images/boardgames/.jpg and + * writes the local public path back into the JSON, OR + * --remote writes BGG's remote cover URL straight into the JSON (hotlinked; + * no download, but depends on BGG's CDN staying up). + * + * Idempotent: games that already have an `image` are skipped. Be gentle on BGG + * — they rate-limit, so there's a small delay between requests. + * + * USAGE + * node scripts/fetch-bgg-covers.mjs # download covers locally + * node scripts/fetch-bgg-covers.mjs --remote # hotlink BGG URLs instead + * node scripts/fetch-bgg-covers.mjs --force # re-fetch even if image set + * node scripts/fetch-bgg-covers.mjs --sleep 1500 # ms between BGG calls + * node scripts/fetch-bgg-covers.mjs --help + * + * Edit the list itself in src/data/boardgames.json: + * { "name": "Wingspan", "bggId": 266192 } + * Find a game's bggId in its BoardGameGeek URL: + * https://boardgamegeek.com/boardgame// + */ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const PROJECT_ROOT = resolve(__dirname, '..'); +const DATA_FILE = join(PROJECT_ROOT, 'src', 'data', 'boardgames.json'); +const OUT_DIR = join(PROJECT_ROOT, 'public', 'images', 'boardgames'); +const BGG_API = 'https://boardgamegeek.com/xmlapi2'; + +// --- args ------------------------------------------------------------------- +const args = process.argv.slice(2); +if (args.includes('-h') || args.includes('--help')) { + console.log(`Usage: node scripts/fetch-bgg-covers.mjs [options] + +Reads src/data/boardgames.json and fills in the "image" field for any game +missing one, using BoardGameGeek's public XML2 API (no key required). + +Options: + --remote write BGG's remote cover URL into the JSON instead of downloading + --force re-fetch even for games that already have an image + --sleep delay between BGG API calls (default 1200; BGG rate-limits) + -h, --help show this help + +Examples: + node scripts/fetch-bgg-covers.mjs + node scripts/fetch-bgg-covers.mjs --remote --sleep 2000`); + process.exit(0); +} + +const remoteMode = args.includes('--remote'); +const force = args.includes('--force'); +const sleepIdx = args.indexOf('--sleep'); +const sleepMs = sleepIdx !== -1 ? Number(args[sleepIdx + 1]) || 1200 : 1200; + +// --- helpers ---------------------------------------------------------------- +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +const fail = (msg, code = 1) => { + console.error(`✖ ${msg}`); + process.exit(code); +}; + +/** Extract text of the first element from an XML string. */ +const xmlText = (xml, tagName) => { + const re = new RegExp(`<${tagName}[^>]*>([^<]*)`, 'i'); + const m = xml.match(re); + return m ? m[1].trim() : null; +}; + +// --- load data -------------------------------------------------------------- +let games; +try { + const raw = await readFile(DATA_FILE, 'utf8'); + games = JSON.parse(raw); +} catch (err) { + if (err?.code === 'ENOENT') fail(`Could not read ${DATA_FILE}. Does it exist?`); + fail(`Could not parse ${DATA_FILE}: ${err.message}`); +} +if (!Array.isArray(games)) fail(`${DATA_FILE} is not a JSON array.`); + +console.log( + `→ ${games.length} games in ${DATA_FILE.split(/[\\/]/).pop()}` + + (remoteMode ? ' (remote mode: hotlink BGG URLs)' : ' (local mode: download covers)'), +); + +// --- process each game ------------------------------------------------------ +let changed = false; +const UA = 'GregKilmerProfileSite/1.0 (board-game-cover fetcher)'; +await mkdir(OUT_DIR, { recursive: true }); + +for (let i = 0; i < games.length; i++) { + const g = games[i]; + if (typeof g.bggId !== 'number') { + console.warn(` ! "${g.name}" has no bggId; skipping.`); + continue; + } + if (g.image && !force) { + console.log(` ✓ ${g.name} — already has image, skipping.`); + continue; + } + + // BGG rate-limits; be polite between calls. + if (i > 0) await sleep(sleepMs); + + const url = `${BGG_API}/thing?id=${g.bggId}&type=boardgame`; + console.log(`→ [${i + 1}/${games.length}] ${g.name} (${url})`); + let res; + try { + res = await fetch(url, { headers: { 'User-Agent': UA, Accept: 'application/xml' } }); + } catch (err) { + console.warn(` ! network error: ${err?.message ?? err}; skipping.`); + continue; + } + if (res.status === 429) { + console.warn(' ! rate-limited by BGG (429). Wait a bit and re-run; skipping rest.'); + break; + } + if (!res.ok) { + console.warn(` ! BGG returned ${res.status} ${res.statusText}; skipping.`); + continue; + } + const xml = await res.text(); + const remoteImage = xmlText(xml, 'image'); // BGG's primary box-art URL + if (!remoteImage) { + console.warn(` ! no in BGG response for bggId ${g.bggId}; skipping.`); + continue; + } + + if (remoteMode) { + g.image = remoteImage; + changed = true; + console.log(` ✔ ${g.name} → ${remoteImage}`); + } else { + const ext = (remoteImage.match(/\.(jpe?g|png|webp)$/i)?.[1] ?? 'jpg').toLowerCase(); + const fileName = `${g.bggId}.${ext === 'jpeg' ? 'jpg' : ext}`; + let buf; + try { + const imgRes = await fetch(remoteImage, { headers: { 'User-Agent': UA } }); + if (!imgRes.ok) throw new Error(`${imgRes.status} ${imgRes.statusText}`); + buf = Buffer.from(await imgRes.arrayBuffer()); + } catch (err) { + console.warn(` ! image download failed (${err?.message ?? err}); skipping.`); + continue; + } + await writeFile(join(OUT_DIR, fileName), buf); + g.image = `/images/boardgames/${fileName}`; + changed = true; + console.log(` ✔ ${g.name} → ${g.image}`); + } +} + +// --- write back ------------------------------------------------------------- +if (changed) { + await writeFile(DATA_FILE, JSON.stringify(games, null, 2) + '\n', 'utf8'); + console.log(`\n✔ Updated ${DATA_FILE}`); +} else { + console.log('\n— Nothing to update (all games already had an image).'); +} diff --git a/src/components/ttrpgs/BoardGameShelf.tsx b/src/components/ttrpgs/BoardGameShelf.tsx new file mode 100644 index 0000000..8139902 --- /dev/null +++ b/src/components/ttrpgs/BoardGameShelf.tsx @@ -0,0 +1,188 @@ +/** + * BoardGameShelf.tsx + * --------------------------------------------------------------------------- + * Two-column "Board games" island for the TTRPGs page. + * + * Left: a list of game names, each a real to its BoardGameGeek page. + * Hovering (or keyboard-focusing) a name selects it. + * Right: a tall image frame. At rest it shows Greg's board-game shelf photo; + * when a name is selected, that game's BGG cover cross-fades in on + * top of the shelf, with the game's name + note as a caption. + * + * Progressive enhancement: without JS the list is still fully usable — the + * links work and the shelf photo stays put. The cross-fade is the enhancement. + * framer-motion honors prefers-reduced-motion automatically. + * + * Data shape (src/data/boardgames.json): + * { name: string; bggId: number; note?: string; image?: string } + * `image` is optional — populated by scripts/fetch-bgg-covers.mjs. Rows without + * an `image` simply keep the shelf photo on hover (graceful fallback). + */ +import { useState } from 'react'; +import { AnimatePresence, motion } from 'framer-motion'; + +export interface BoardGame { + name: string; + /** BoardGameGeek numeric game id; drives the link + cover lookup. */ + bggId: number; + /** Optional one-line note shown as the caption. */ + note?: string; + /** Optional cover image URL/path. If absent, hovering won't swap images. */ + image?: string; +} + +interface Props { + games: BoardGame[]; + /** Public path to the default shelf photo, e.g. "/images/ttrpg-boardgame-shelf.jpg". */ + shelfImage: string; + /** Alt text for the shelf photo. */ + shelfAlt: string; + /** Optional caption shown at rest over the shelf photo. */ + shelfCaption?: string; +} + +/** + * Fixed height for BOTH columns. The list shows up to 8 rows in a scroll + * container; the image frame is pinned to this same height so the two + * columns stay aligned regardless of how many games are in the list. + * ~3.25rem per row x 8 rows. + */ +const SHELF_HEIGHT = '44rem'; + +const crossfade = { + initial: { opacity: 0 }, + animate: { opacity: 1 }, + exit: { opacity: 0 }, + transition: { duration: 0.35, ease: [0.16, 1, 0.3, 1] as [number, number, number, number] }, +}; + +export default function BoardGameShelf({ + games, + shelfImage, + shelfAlt, + shelfCaption = 'The shelf', +}: Props) { + // Index of the active game, or null when at rest (showing the shelf). + const [active, setActive] = useState(null); + const activeGame = active !== null ? games[active] : null; + // Only swap to the cover if the active game actually has an image. + const showCover = activeGame?.image; + + return ( +
+ {/* LEFT: game list — capped at the shelf height with internal scroll */} + + + {/* RIGHT: tall image frame, shelf at rest, cover on hover. Pinned to the + same height as the list so the two columns line up. */} +
setActive(null)} + > + {/* Base layer: shelf photo (always present; covers cross-fade on top). */} + {shelfAlt} + {/* Gradient + caption overlay for legibility. */} +
+
+ ); +} diff --git a/src/data/boardgames.json b/src/data/boardgames.json new file mode 100644 index 0000000..74b125c --- /dev/null +++ b/src/data/boardgames.json @@ -0,0 +1,302 @@ +[ + { + "name": "Wyrmspan", + "bggId": 410201, + "note": "Engine-building with dragons. A Wingspan reimplementation with slightly more complex mechanics.", + "image": "https://cf.geekdo-images.com/original/pic7947338.png" + }, + { + "name": "Spirit Island", + "bggId": 162886, + "note": "Cooperative area control. Play as spirits defending an island from invaders. Highly complex and strategic.", + "image": "https://cf.geekdo-images.com/original/pic7013651.jpg" + }, + { + "name": "Codenames", + "bggId": 178900, + "note": "Party word game. Teams compete to find their agents using one-word clues.", + "image": "https://cf.geekdo-images.com/original/pic2856478.jpg" + }, + { + "name": "Codenames: Duet", + "bggId": 224037, + "note": "Two-player cooperative version of Codenames. Work together to find all the agents.", + "image": "https://cf.geekdo-images.com/original/pic3550436.jpg" + }, + { + "name": "Coup", + "bggId": 131357, + "note": "Deduction and bluffing game. Each player has two roles; claim actions and challenge others.", + "image": "https://cf.geekdo-images.com/original/pic1892875.jpg" + }, + { + "name": "The Resistance", + "bggId": 41114, + "note": "Social deduction hidden roles game. Resistance members vs spies trying to sabotage missions.", + "image": "https://cf.geekdo-images.com/cAPTkL2BR3snLO71dkH8rw__imagepage/img/IOY562LZXFjsmcVioY5c_r70Ebc=/fit-in/900x600/filters:no_upscale():strip_icc()/pic2576459.jpg" + }, + { + "name": "Ticket to Ride", + "bggId": 9209, + "note": "Route-building set collection. Collect train cards to claim railway routes across the country.", + "image": "https://cf.geekdo-images.com/original/pic1000656.jpg" + }, + { + "name": "7 Wonders", + "bggId": 68448, + "note": "Card-drafting civilization building. Build one of the seven wonders through three ages.", + "image": "https://cf.geekdo-images.com/original/pic869179.jpg" + }, + { + "name": "Pandemic", + "bggId": 30549, + "note": "Cooperative strategy. Work together to stop global disease outbreaks before they spread too far.", + "image": "https://cf.geekdo-images.com/original/pic398633.jpg" + }, + { + "name": "King of Tokyo", + "bggId": 70323, + "note": "Dice-rolling monster combat. Be the monster who earns 20 points or eliminates the competition.", + "image": "https://cf.geekdo-images.com/original/pic1142193.jpg" + }, + { + "name": "Carcassonne", + "bggId": 822, + "note": "Tile-placement medieval city building. Place tiles to build the city and claim features for points.", + "image": "https://cf.geekdo-images.com/original/pic830344.jpg" + }, + { + "name": "Sagrada", + "bggId": 199561, + "note": "Dice-drafting stained glass window building. Draft colored dice to complete patterns on your window.", + "image": "https://cf.geekdo-images.com/original/pic3297563.jpg" + }, + { + "name": "Small World", + "bggId": 40692, + "note": "Area control fantasy civilization. Races rise and fall as you compete for territory across the map.", + "image": "https://cf.geekdo-images.com/original/pic699500.jpg" + }, + { + "name": "Dead of Winter", + "bggId": 150376, + "note": "Cooperative survival with betrayal. Survive the zombie apocalypse while completing secret objectives.", + "image": "https://cf.geekdo-images.com/g4mV4BH-ZrhMUVgil-yV1A__imagepage/img/CJ6erNGmKfFGhR6ZbxXbBI7XaoE=/fit-in/900x600/filters:no_upscale():strip_icc()/pic3016500.jpg" + }, + { + "name": "Betrayal at the House on the Hill", + "bggId": 10547, + "note": "Traitor horror exploration. Explore a haunted mansion until one betrays the others in a unique haunt.", + "image": "https://cf.geekdo-images.com/original/pic302310.jpg" + }, + { + "name": "Betrayal at Baldur's Gate", + "bggId": 228660, + "note": "Traitor D&D horror. Betrayal set in the Forgotten Realms with fantasy-themed haunts.", + "image": "https://cf.geekdo-images.com/O6TfqmDWCkJvZPZw6pcxpg__imagepage/img/Zif6XP4Ij1qxBH2vHB6V2jBXFHk=/fit-in/900x600/filters:no_upscale():strip_icc()/pic6552099.png" + }, + { + "name": "Lords of Waterdeep", + "bggId": 110327, + "note": "Worker placement D&D city management. Assign adventurers to quests to score points for your secret lord.", + "image": "https://cf.geekdo-images.com/original/pic1204394.jpg" + }, + { + "name": "Mysterium", + "bggId": 181304, + "note": "Cooperative murder mystery. One ghost communicates clues through abstract art; others guess the murderer.", + "image": "https://cf.geekdo-images.com/1nQ3ZKudtDeAP7IiKE-kNg__imagepage/img/scbgJc5EdSpHJGxGbV9dgpxj9hQ=/fit-in/900x600/filters:no_upscale():strip_icc()/pic8625343.jpg" + }, + { + "name": "Hanabi", + "bggId": 98778, + "note": "Cooperative card game where you see everyone's cards but your own. Work together to build the perfect firework display.", + "image": "https://cf.geekdo-images.com/original/pic1598898.jpg" + }, + { + "name": "Forbidden Island", + "bggId": 65244, + "note": "Cooperative adventure survival. Work together to collect treasures from a sinking island before it's too late.", + "image": "https://cf.geekdo-images.com/original/pic711958.jpg" + }, + { + "name": "Sushi Go Deluxe", + "bggId": 172563, + "note": "Card-drafting sushi set collection. Pick sushi cards to build the best meal; deluxe version with more cards.", + "image": "https://cf.geekdo-images.com/original/pic3246378.jpg" + }, + { + "name": "Evolution (with climate)", + "bggId": 182134, + "note": "Species evolution strategy. Build species with traits to adapt to changing food sources and climate.", + "image": "https://cf.geekdo-images.com/original/pic2696435.jpg" + }, + { + "name": "Quacks of Quedlinburg (Deluxe)", + "bggId": 244521, + "note": "Push-your luck potion brewing. Draw ingredient chips from your bag without exploding your cauldron.", + "image": "https://cf.geekdo-images.com/original/pic3953887.jpg" + }, + { + "name": "Dragonfire", + "bggId": 221217, + "note": "D&D deck-building cooperative. Build your deck and defend the North from dragons and monsters.", + "image": "https://cf.geekdo-images.com/original/pic3519244.jpg" + }, + { + "name": "Mistborn: The Deckbuilding Game", + "bggId": 422780, + "note": "Cosmere deck-building strategy. Burn metals in fast-paced allomantic battles in the Final Empire.", + "image": "https://cf.geekdo-images.com/original/pic8148911.jpg" + }, + { + "name": "Doomlings", + "bggId": 356834, + "note": "Card game set collection with extinction events. Build species traits through ages until doomsday arrives.", + "image": "https://cf.geekdo-images.com/original/pic6574236.jpg" + }, + { + "name": "Dragonology Gems", + "bggId": 329381, + "note": "Dragon-themed set collection. Collect gems to complete dragon research sets.", + "image": "https://cf.geekdo-images.com/original/pic6043991.jpg" + }, + { + "name": "Heroes of Barcadia", + "bggId": 341027, + "note": "Dungeon crawl party game. Progress through arcade-themed dungeon rooms and drink along the way.", + "image": "https://cf.geekdo-images.com/htK1yU55RTMFIpT2ivdN2w__imagepage/img/kQBLS2DHkRtJfR0MgrhsNdvGBlU=/fit-in/900x600/filters:no_upscale():strip_icc()/pic8505535.jpg" + }, + { + "name": "Icarus", + "bggId": 374025, + "note": "Storytelling civilization collapse. Build a civilization through history, then try to prevent its fall.", + "image": "https://cf.geekdo-images.com/original/pic6860915.jpg" + }, + { + "name": "Epic Spellwars of the Battle Wizards: Mt. Skullzfire", + "bggId": 124742, + "note": "Over-the-top card combat wizard battle. Combine spell cards to attack other wizards in gory absurdity.", + "image": "https://cf.geekdo-images.com/original/pic1409236.jpg" + }, + { + "name": "Lost Cities: Duel", + "bggId": 192544, + "note": "Two-player expedition card game. Compete to complete expeditions worth more than your opponent's.", + "image": "https://cf.geekdo-images.com/original/pic2798708.jpg" + }, + { + "name": "The Fox in the Forest", + "bggId": 233077, + "note": "Two-player trick-taking. Win tricks to score points, but too many wins can hurt your total.", + "image": "https://cf.geekdo-images.com/original/pic3698833.jpg" + }, + { + "name": "One Night Werewolf", + "bggId": 92696, + "note": "Fast-paced social deduction. Single night of werewolf chaos; vote to eliminate the werewolf.", + "image": "https://cf.geekdo-images.com/original/pic1212691.jpg" + }, + { + "name": "What Do You Meme?", + "bggId": 234699, + "note": "Party game matching caption cards to meme photos. Create the funniest meme combinations.", + "image": "https://cf.geekdo-images.com/original/pic3749917.jpg" + }, + { + "name": "If You Know You Know", + "bggId": 318265, + "note": "Pop culture trivia party game. Guess phrases from partial information in categories.", + "image": "https://cf.geekdo-images.com/original/pic5496066.jpg" + }, + { + "name": "Cards Against Humanity", + "bggId": 50381, + "note": "Offensive humor party game. Fill in the blank with the most inappropriate card from your hand.", + "image": "https://cf.geekdo-images.com/original/pic899330.jpg" + }, + { + "name": "Hues and Cues", + "bggId": 326447, + "note": "Color communication party game. Give one- or two-word cues for others to guess a specific color.", + "image": "https://cf.geekdo-images.com/original/pic5903863.jpg" + }, + { + "name": "Wavelength", + "bggId": 250349, + "note": "Telepathy party game. Give clues to help your team locate a hidden concept on a spectrum.", + "image": "https://cf.geekdo-images.com/original/pic4181983.jpg" + }, + { + "name": "Lost in Translation", + "bggId": 204238, + "note": "Word communication party game. Describe words while avoiding forbidden translations and synonyms.", + "image": "https://cf.geekdo-images.com/original/pic3073849.jpg" + }, + { + "name": "Risk Legacy", + "bggId": 107907, + "note": "Legacy strategy global conquest. Write on the board, open sealed packets, and evolve the game over plays.", + "image": "https://cf.geekdo-images.com/original/pic1086743.jpg" + }, + { + "name": "Risk", + "bggId": 181, + "note": "Classic strategy global domination. Conquer territories and eliminate opponents in military world conquest.", + "image": "https://cf.geekdo-images.com/original/pic1059899.jpg" + }, + { + "name": "Age of Mythology: The Board Game", + "bggId": 464, + "note": "Area control mythological warfare. Recruit armies, build structures, and invoke gods' favor.", + "image": "https://cf.geekdo-images.com/original/pic224600.jpg" + }, + { + "name": "Tsuro", + "bggId": 22138, + "note": "Tile placement path following. Lay tiles to create paths; stay on the board while others fall off.", + "image": "https://cf.geekdo-images.com/original/pic335329.jpg" + }, + { + "name": "Kings Cup", + "bggId": 48967, + "note": "Drinking game with card-based rules. Draw cards and follow the associated drinking challenges.", + "image": "https://cf.geekdo-images.com/original/pic886320.jpg" + }, + { + "name": "Pocket Farkle", + "bggId": 36139, + "note": "Dice game push your luck. Roll dice to score points, but risk losing everything if you push too far.", + "image": "https://cf.geekdo-images.com/original/pic338874.jpg" + }, + { + "name": "Pokertiles", + "bggId": 13327, + "note": "Abstract tile strategy game. Form poker hands using numbered tiles on a board.", + "image": "https://cf.geekdo-images.com/original/pic113334.jpg" + }, + { + "name": "The Perilous Parlor Game", + "bggId": 227503, + "note": "Victorian mystery party game. Deduce the culprit through clues and interactions in a parlor setting.", + "image": "https://cf.geekdo-images.com/original/pic3552892.jpg" + }, + { + "name": "Deception: Murder in Hong Kong", + "bggId": 156129, + "note": "Social deduction murder mystery. Forensic scientists identify the killer while one investigator secretly helps.", + "image": "https://cf.geekdo-images.com/original/pic2037351.jpg" + }, + { + "name": "Deception: Undercover Allies", + "bggId": 290617, + "note": "Expansion for Deception: Murder in Hong Kong. Adds new roles and additional investigator options.", + "image": "https://cf.geekdo-images.com/original/pic4759191.jpg" + }, + { + "name": "Settlers of Catan (expansion)", + "bggId": 5602, + "note": "Expansion for Settlers of Catan. Adds additional gameplay content to the base Catan experience.", + "image": "https://cf.geekdo-images.com/original/pic135031.jpg" + } +] \ No newline at end of file diff --git a/src/pages/interests/index.astro b/src/pages/interests/index.astro index 62fe862..fc93c89 100644 --- a/src/pages/interests/index.astro +++ b/src/pages/interests/index.astro @@ -49,8 +49,7 @@ const interests = [ href: "/interests/making", icon: "palette", title: "Making & Creating", - tagline: - "If it involves really nice cardboard or mastering a new craft, I am in.", + tagline: "A magnet for super glue and nice cardboard.", blurb: "Resin dice in a pressure pot, fun and functional prints on the P1s, custom TCG proxies and decks. Passion and utility.", image: "/images/making-resin.jpg", diff --git a/src/pages/interests/reading.astro b/src/pages/interests/reading.astro index 4e363a3..649766b 100644 --- a/src/pages/interests/reading.astro +++ b/src/pages/interests/reading.astro @@ -5,31 +5,95 @@ * Per-interest template with the spec's reading highlights table (status pills * mapped from spec emoji statuses), anime favorites, and movies/TV note. */ -import BaseLayout from '@layouts/BaseLayout.astro'; -import Reveal from '@components/motion/Reveal.astro'; -import InterestHero from '@components/layout/InterestHero.astro'; -import InterestCard from '@components/cards/InterestCard.astro'; -import SectionHeader from '@components/layout/SectionHeader.astro'; -import RelatedInterests from '@components/layout/RelatedInterests.astro'; -import Icon from '@components/Icon.astro'; +import BaseLayout from "@layouts/BaseLayout.astro"; +import Reveal from "@components/motion/Reveal.astro"; +import InterestHero from "@components/layout/InterestHero.astro"; +import InterestCard from "@components/cards/InterestCard.astro"; +import SectionHeader from "@components/layout/SectionHeader.astro"; +import RelatedInterests from "@components/layout/RelatedInterests.astro"; +import Icon from "@components/Icon.astro"; // Spec reading highlights, with status mapped from the spec's emoji statuses. const reading = [ - { icon: 'check-decagram', title: 'Brandon Sanderson — Cosmere', status: 'Complete', body: 'Complete. Every novel, every era. The gold standard for me.' }, - { icon: 'check-decagram', title: 'Frank Herbert — Dune', status: 'Complete', body: 'Books 1-3 complete.' }, - { icon: 'autorenew', title: 'Douglas Preston & Lincoln Child — Pendergast', status: 'Rereading', body: 'Currently rereading. Up to Reliquary.' }, - { icon: 'autorenew', title: 'Sarah J. Maas — Throne of Glass', status: 'Reading', body: 'Currently on Heir of Fire (Book 3).' }, - { icon: 'new-box', title: 'Wistoria: Wand and Sword', status: 'New', body: 'Recently added to the reading list.' }, + { + icon: "check-decagram", + title: "Brandon Sanderson — Cosmere", + status: "Complete", + body: "Complete. Every novel, every era. The gold standard for me.", + }, + { + icon: "check-decagram", + title: "Frank Herbert — Dune", + status: "Complete", + body: "Books 1-3 complete.", + }, + { + icon: "bookmark", + title: "Emily Rodda — Deltora Quest", + status: "Reread-pending", + body: "Planning to listen to audiobooks to reread.", + }, + { + icon: "volume-high", + title: "J.R.R. Tolkein — The Lord of the Rings", + status: "Listening", + body: "Rereading by listening to the Andy Serkis audiobooks.", + }, + { + icon: "autorenew", + title: "Douglas Preston & Lincoln Child — Pendergast", + status: "Rereading", + body: "Currently rereading. Up to Cabinet of Curiosities.", + }, + { + icon: "autorenew", + title: "Sarah J. Maas — Throne of Glass", + status: "Reading", + body: "Currently Tandem reading Heir of Fire (Book 3) and Tower of Dawn (Book 4).", + }, + { + icon: "bookmark", + title: "Fonda Lee — Green Bone Saga", + status: "Next", + body: "Next Series to Read.", + }, + { + icon: "bookmark", + title: "That Time I Got Reincarnated as a Slime", + status: "New", + body: "Read up to Volume 13 before, planning to finish now that it is complete.", + }, + { + icon: "new-box", + title: "Wistoria: Wand and Sword", + status: "New", + body: "Recently added to the reading list. After finishing season 2", + }, ]; const anime = [ - 'One Piece', 'Bleach', 'Gundam (all series)', 'Code Geass', - 'Magi', 'That Time I Got Reincarnated as a Slime', 'Overlord', + "One Piece", + "Bleach", + "Gundam (all series)", + "Code Geass", + "Magi", + "That Time I Got Reincarnated as a Slime", + "Overlord", ]; const related = [ - { href: '/interests/ttrpgs', title: 'TTRPGs & Tabletop', icon: 'dice-multiple', blurb: 'Worldbuilding inspiration flows both ways.' }, - { href: '/interests/making', title: 'Making & Creating', icon: 'palette', blurb: 'Props and displays for the stories I love.' }, + { + href: "/interests/ttrpgs", + title: "TTRPGs & Tabletop", + icon: "dice-multiple", + blurb: "Worldbuilding inspiration flows both ways.", + }, + { + href: "/interests/making", + title: "Making & Creating", + icon: "palette", + blurb: "Props and displays for the stories I love.", + }, ]; --- @@ -48,54 +112,104 @@ const related = [
{/* Overview */} -
-

I read like it's a competitive sport. Fantasy is my native language, Brandon Sanderson's Cosmere is fully consumed and eagerly awaiting the next era. Frank Herbert's Dune trilogy shaped how I think about worldbuilding. Douglas Preston & Lincoln Child's Pendergast saga is my comfort reread. And I'm currently working through Sarah J. Maas's Throne of Glass series.

-

Beyond novels, I read light novels, comic books, manhwa, and manga. I watch anime and film extensively. If a story exists in any format, I'm probably consuming it.

+
+

+ I read like it's a competitive sport. Fantasy is my native language, + Brandon Sanderson's Cosmere is fully consumed and I am eagerly + awaiting the next era. Frank Herbert's Dune trilogy shaped how I think + about worldbuilding. Douglas Preston & Lincoln Child's Pendergast saga + showed me that a well-crafted narrative can keep me engaged outside of + a Tolkein-esque fantasy world. And I'm currently working through Sarah + J. Maas's Throne of Glass series. +

+

+ Beyond novels, I read light novels, comic books, manhwa, and manga. I + watch anime and film extensively. If a story exists in any format, I'm + probably consuming it in some capacity. +

{/* Reading highlights */}
- +
- {reading.map((r, i) => ( - - {r.body} - - ))} + { + reading.map((r, i) => ( + + + {r.body} + + + )) + }
{/* Anime */}
- +
    - {anime.map((a) => ( -
  • - - {a} -
  • - ))} + { + anime.map((a) => ( +
  • + + {a} +
  • + )) + }
{/* Movies & TV */}
- +
- Watches extensively across genres. Particularly drawn to sci-fi, fantasy, and animated film. + Watches extensively across genres. Particularly drawn to sci-fi, + fantasy, and animated film.
- Fantasy and media collection + Fantasy and media collection
diff --git a/src/pages/interests/ttrpgs.astro b/src/pages/interests/ttrpgs.astro index 9e022d5..83a245f 100644 --- a/src/pages/interests/ttrpgs.astro +++ b/src/pages/interests/ttrpgs.astro @@ -6,25 +6,67 @@ * (narrative, verbatim), Highlights (card grid, spec emojis -> mdi), Currently, * then the Board Games sub-section, then RelatedInterests cross-links. */ -import BaseLayout from '@layouts/BaseLayout.astro'; -import Reveal from '@components/motion/Reveal.astro'; -import InterestHero from '@components/layout/InterestHero.astro'; -import InterestCard from '@components/cards/InterestCard.astro'; -import SectionHeader from '@components/layout/SectionHeader.astro'; -import RelatedInterests from '@components/layout/RelatedInterests.astro'; -import Placeholder from '@components/layout/Placeholder.astro'; +import BaseLayout from "@layouts/BaseLayout.astro"; +import Reveal from "@components/motion/Reveal.astro"; +import InterestHero from "@components/layout/InterestHero.astro"; +import InterestCard from "@components/cards/InterestCard.astro"; +import SectionHeader from "@components/layout/SectionHeader.astro"; +import RelatedInterests from "@components/layout/RelatedInterests.astro"; +import Placeholder from "@components/layout/Placeholder.astro"; +import BoardGameShelf from "@components/ttrpgs/BoardGameShelf.tsx"; +import boardgames from "@/data/boardgames.json"; const highlights = [ - { icon: 'castle', title: 'Dungeons & Dragons', body: 'Long-running campaign on Foundry VTT. Focus on rich worldbuilding, meaningful NPC relationships, and player-driven narrative.' }, - { icon: 'sword-cross', title: 'Daggerheart', body: "Darrington Press's dice-building RPG. Currently running a campaign and exploring how its mechanics create different story rhythms than D&D." }, - { icon: 'bird', title: 'Crows (Playtest)', body: "MCDM's tactical RPG in active playtest. Hex-based travel, revised travel roles, Miasma mechanics. Contributing playtest feedback." }, - { icon: 'monitor', title: 'Foundry VTT', body: 'Self-hosted on the home server (Cosmos Lenovo Tiny). Building custom modules, maps, and automation for campaigns.' }, - { icon: 'target', title: 'Draw Steel', body: "MCDM's upcoming tactical combat game. Interested in running it when it releases." }, + { + icon: "castle", + title: "Dungeons & Dragons", + body: "Lately 5th Edition. Long-running campaigns on and off Foundry VTT. Focus on rich worldbuilding, meaningful NPC relationships, and player-driven narrative.", + }, + { + icon: "sword-cross", + title: "Daggerheart", + body: "Darrington Press's dice-building RPG. Currently running a campaign and exploring how its mechanics create different story rhythms than D&D.", + }, + { + icon: "target", + title: "Draw Steel", + body: "MCDM's fantasy tactical cinematic combat game. Interested in running it when I get a chance.", + }, + { + icon: "shield-moon-outline", + title: "Cypher System", + body: "Monte Cook's narrative-focused system. Played a few one-shots and short form campaigns.", + }, + { + icon: "knife-military", + title: "Blades in the Dark", + body: "A gritty, cinematic system for heists and criminal enterprises. Interested in playing short campaigns in it one day.", + }, + { + icon: "weather-windy", + title: "Avatar the Last Airbender RPG", + body: "A narrative-focused system set in the world of Avatar. Interested in exploring bending mechanics and character-driven stories.", + }, + { + icon: "monitor", + title: "Foundry VTT", + body: "Self-hosted on the home server. Building custom modules, maps, and automation for campaigns.", + }, ]; const related = [ - { href: '/interests/making', title: 'Making & Creating', icon: 'palette', blurb: 'Dice and terrain made for campaigns.' }, - { href: '/interests/gaming', title: 'Gaming', icon: 'gamepad-variant', blurb: 'Digital RPGs as another expression of the same impulse.' }, + { + href: "/interests/making", + title: "Making & Creating", + icon: "palette", + blurb: "Dice and terrain made for campaigns.", + }, + { + href: "/interests/gaming", + title: "Gaming", + icon: "gamepad-variant", + blurb: "Digital RPGs as another expression of the same impulse.", + }, ]; --- @@ -43,24 +85,72 @@ const related = [
{/* Overview */} -
-

Tabletop gaming isn't a hobby for me, it's a practice. I run Dungeons & Dragons and Daggerheart campaigns that span years of real-time play, maintaining worlds, NPCs, and narrative threads that my players remember years later. I host everything on Foundry VTT, which lets me build the kind of dynamic, visually rich experiences that keep players invested session after session.

-

Right now, I'm also playtesting MCDM's Crows, Matt Colville's forthcoming RPG built around hex-crawling, faction play, and hard travel rules. The playtest has already influenced how I think about overland encounters in my own campaigns.

+
+

+ I love playing tabletop games from trading card games to role-playing + games. I run Dungeons & Dragons and Daggerheart campaigns that span + years of real-time play, maintaining worlds, NPCs, and narrative + threads that my players remember years later. I host everything on + Foundry VTT, which lets me build the kind of dynamic, visually rich + experiences that keep players invested session after session and lets + me flex my technical skills. +

+

+ I also love board games, from strategy to social deduction. My + collection is growing, and I enjoy hosting game nights with friends. I + have a particular fondness for games that challenge players to think + critically and creatively, and I appreciate the social aspect of + gaming as much as the gameplay itself. Although, I do love the ability + to play some of the engine building and deck-building games solo. + Spirit Island and Wyrmspan have been my favorites lately, and I am + always on the lookout for new games to add to my collection. +

+

+ I have played a few trading card games and collected many cards. I + have been a lifelong Pokémon and Magic: The Gathering fan, and I enjoy + the strategy and collection aspects of both games in different ways. + While I mostly enjoy looking at Pokemon cards, I spend most of my time + with Magic deck building and playing Elder Dragon Highlander (or + Commander). I recently picked up Riftbound and have had my eye on the + Gundam TCG game as well. The mechanical differences of these games are + fascinating, and I enjoy exploring the different strategies and + deck-building techniques that each game offers. +

{/* Highlights */}
- +
- {highlights.map((h, i) => ( - - {h.body} - - ))} + { + highlights.map((h, i) => ( + + + {h.body} + + + )) + }
- {/* Currently */} + { + /* Currently
@@ -69,42 +159,36 @@ const related = [
+ */ + } {/* Board games */}
- +
Beyond RPGs, I maintain a growing board and card game collection. - Favorites span strategy, deck-building, and social deduction. Game night - is a regular event, and yes, there's always room for one more. + Favorites span strategy, deck-building, and social deduction. Game + night is a regular event, and yes, there's always room for one more. + Hover a title to preview its cover; click to open it on BoardGameGeek.
-
-
- Beyond RPGs, I maintain a growing board and card game collection. - Favorites span strategy, deck-building, and social deduction. Game night - is a regular event, and yes, there's always room for one more. -
-
- Dice and game pieces -
-
-
- -