gaming updates
@@ -32,3 +32,10 @@ SITE_URL=https://gregkilmer.com
|
|||||||
# Honeypot / basic abuse control. Maximum submissions per IP per window.
|
# Honeypot / basic abuse control. Maximum submissions per IP per window.
|
||||||
CONTACT_RATE_LIMIT=5
|
CONTACT_RATE_LIMIT=5
|
||||||
CONTACT_RATE_WINDOW_MIN=15
|
CONTACT_RATE_WINDOW_MIN=15
|
||||||
|
|
||||||
|
# --- Gaming box-art tool ----------------------------------------------------
|
||||||
|
# Used by scripts/fetch-box-art.mjs to download box/banner art for Gaming Hall
|
||||||
|
# of Fame entries via the RAWG API. Get a free key at:
|
||||||
|
# https://rawg.io/login?forward=developer
|
||||||
|
RAWG_API_KEY=
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Content Guide
|
# Content Guide
|
||||||
|
|
||||||
How to add and edit content on the site. Two sections use **Content Collections**
|
How to add and edit content on the site. Four sections use **Content Collections**
|
||||||
(typed markdown/MDX); the interest pages are hand-built `.astro` files.
|
(typed markdown/MDX/YAML); the other interest pages are hand-built `.astro` files.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -90,6 +90,93 @@ node -e "const m=require('./node_modules/@iconify-json/mdi/icons.json'); console
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Games (Gaming page)
|
||||||
|
|
||||||
|
The Gaming page (`src/pages/interests/gaming.astro`) has two content-driven
|
||||||
|
sections that read from collections: **Now playing** and the **Gaming Hall of
|
||||||
|
Fame**. The platforms grid and favorite-genre cards are still edited inline on
|
||||||
|
the page.
|
||||||
|
|
||||||
|
### Hall of Fame — one file per game
|
||||||
|
|
||||||
|
Each game is a markdown file in `src/content/games/hall-of-fame/`. Create a new
|
||||||
|
file (e.g. `hades.md`):
|
||||||
|
|
||||||
|
```md
|
||||||
|
---
|
||||||
|
title: 'Hades'
|
||||||
|
platform: 'Steam' # free-form; drives the icon + platform pill
|
||||||
|
year: 2020 # optional, shown as a small detail
|
||||||
|
art: '/images/games/hades.jpg' # optional, public path (see fetch tool below)
|
||||||
|
artAlt: 'Hades key art' # optional, defaults to the title
|
||||||
|
icon: 'sword-cross' # optional, overrides the platform-derived icon
|
||||||
|
blurb: 'A 2-3 sentence case for why it earned a Hall of Fame slot.'
|
||||||
|
order: 15 # lower = shown first, default 100
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `title` | string | yes | Game name |
|
||||||
|
| `platform` | string | yes | e.g. "PlayStation 5", "Steam", "Nintendo Switch" |
|
||||||
|
| `blurb` | string | yes | Why it's a favorite |
|
||||||
|
| `art` | string | no | `/images/games/<slug>.jpg` (public path) |
|
||||||
|
| `artAlt` | string | no | Alt text, defaults to title |
|
||||||
|
| `year` | number | no | Release year |
|
||||||
|
| `icon` | string | no | mdi icon override (else derived from platform) |
|
||||||
|
| `order` | number | no | Sort weight |
|
||||||
|
|
||||||
|
`platform` is matched case-insensitively to pick an icon: `playstation` →
|
||||||
|
`sony-playstation`, `switch` → `nintendo-switch`, `steam`/`pc` → `steam`,
|
||||||
|
`xbox` → `microsoft-xbox`, anything else → `gamepad-variant`. Override with
|
||||||
|
`icon` if you want something specific.
|
||||||
|
|
||||||
|
### Now playing — single list
|
||||||
|
|
||||||
|
Edit `src/content/games/now-playing.yaml`. This is the transient list of games
|
||||||
|
you're actively playing; each entry is a title + platform only:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
games:
|
||||||
|
- title: "Elden Ring"
|
||||||
|
platform: "PlayStation 5"
|
||||||
|
- title: "Balatro"
|
||||||
|
platform: "Steam"
|
||||||
|
```
|
||||||
|
|
||||||
|
If the list is empty, the page shows a TODO placeholder instead.
|
||||||
|
|
||||||
|
### Fetching box art (RAWG)
|
||||||
|
|
||||||
|
`scripts/fetch-box-art.mjs` downloads box/banner art from the
|
||||||
|
[RAWG API](https://rawg.io/apidocs) into `public/images/games/`, then prints the
|
||||||
|
public path to paste into a game file's `art:` field. RAWG covers Steam,
|
||||||
|
PlayStation, Xbox, and Nintendo in one place.
|
||||||
|
|
||||||
|
1. Get a free key at <https://rawg.io/login?forward=developer> and add it to
|
||||||
|
`.env` (copy from `.env.example`):
|
||||||
|
```
|
||||||
|
RAWG_API_KEY=your_key_here
|
||||||
|
```
|
||||||
|
2. Run (Node 20.6+ can auto-load `.env` via `--env-file`):
|
||||||
|
```bash
|
||||||
|
node --env-file=.env scripts/fetch-box-art.mjs "Elden Ring"
|
||||||
|
node --env-file=.env scripts/fetch-box-art.mjs "Hollow Knight" --platform pc
|
||||||
|
node --env-file=.env scripts/fetch-box-art.mjs --id 3328 # by RAWG id
|
||||||
|
node --env-file=.env scripts/fetch-box-art.mjs "Celeste" --out celeste.jpg
|
||||||
|
```
|
||||||
|
3. It prints something like:
|
||||||
|
```
|
||||||
|
✔ Saved: public/images/games/elden-ring.jpg
|
||||||
|
art: "/images/games/elden-ring.jpg"
|
||||||
|
```
|
||||||
|
Paste that `art:` value into the game file's frontmatter.
|
||||||
|
|
||||||
|
> RAWG serves **landscape** banner/screenshot art (not portrait Steam
|
||||||
|
> capsules), which matches the Hall of Fame cards (`aspect-[4/3]`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Interest pages
|
## Interest pages
|
||||||
|
|
||||||
Each interest is a hand-built page in `src/pages/interests/`. They follow the
|
Each interest is a hand-built page in `src/pages/interests/`. They follow the
|
||||||
|
|||||||
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 401 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 658 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 665 KiB |
@@ -0,0 +1,178 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* fetch-box-art.mjs
|
||||||
|
* ---------------------------------------------------------------------------
|
||||||
|
* Support tool for the Gaming Hall of Fame. Downloads box/banner art for a
|
||||||
|
* game from the RAWG API (https://rawg.io/apidocs) and saves it into
|
||||||
|
* `public/images/games/<slug>.jpg`, then prints the public path to paste into
|
||||||
|
* a game file's `art:` field.
|
||||||
|
*
|
||||||
|
* RAWG covers Steam, PlayStation, Xbox, and Nintendo titles in one place, so
|
||||||
|
* this works regardless of platform. Note: RAWG serves landscape banner /
|
||||||
|
* screenshot art (not portrait Steam capsules), which matches the landscape
|
||||||
|
* Hall of Fame cards (aspect-[4/3]).
|
||||||
|
*
|
||||||
|
* PREREQUISITES
|
||||||
|
* 1. Get a free RAWG API key: https://rawg.io/login?forward=developer
|
||||||
|
* 2. Put it in a `.env` file at the project root:
|
||||||
|
* RAWG_API_KEY=your_key_here
|
||||||
|
* (`.env` is gitignored; `.env.example` is the template.)
|
||||||
|
*
|
||||||
|
* USAGE
|
||||||
|
* node scripts/fetch-box-art.mjs "Elden Ring"
|
||||||
|
* node scripts/fetch-box-art.mjs "Hades" --platform pc
|
||||||
|
* node scripts/fetch-box-art.mjs "Celeste" --out celeste.jpg
|
||||||
|
* node scripts/fetch-box-art.mjs --id 3328 # by RAWG id
|
||||||
|
*
|
||||||
|
* Run with `--env-file=.env` to auto-load the key (Node 20.6+):
|
||||||
|
* node --env-file=.env scripts/fetch-box-art.mjs "Elden Ring"
|
||||||
|
*
|
||||||
|
* If you don't use --env-file, export RAWG_API_KEY in your shell first.
|
||||||
|
*/
|
||||||
|
import { mkdir, writeFile } from 'node:fs/promises';
|
||||||
|
import { dirname, extname, join, resolve } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const PROJECT_ROOT = resolve(__dirname, '..');
|
||||||
|
const OUT_DIR = join(PROJECT_ROOT, 'public', 'images', 'games');
|
||||||
|
|
||||||
|
const RAWG_BASE = 'https://api.rawg.io/api';
|
||||||
|
// RAWG parent platforms that map to the --platform flag values.
|
||||||
|
const PLATFORM_IDS = {
|
||||||
|
pc: 1,
|
||||||
|
playstation: 2,
|
||||||
|
xbox: 3,
|
||||||
|
nintendo: 7,
|
||||||
|
mac: 4,
|
||||||
|
linux: 5,
|
||||||
|
ios: 5,
|
||||||
|
android: 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- arg parsing ------------------------------------------------------------
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
|
||||||
|
console.log(`Usage: node scripts/fetch-box-art.mjs "<game title>" [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--platform <p> narrow search to a parent platform: pc, playstation, xbox, nintendo
|
||||||
|
--id <rawgId> fetch a specific game by its RAWG id (skips search)
|
||||||
|
--out <name> output filename (default: slug of the title, .jpg)
|
||||||
|
-h, --help show this help
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
node scripts/fetch-box-art.mjs "Elden Ring"
|
||||||
|
node scripts/fetch-box-art.mjs "Hollow Knight" --platform pc
|
||||||
|
node scripts/fetch-box-art.mjs --id 3328
|
||||||
|
|
||||||
|
Tip: run with \`node --env-file=.env scripts/fetch-box-art.mjs ...\` to auto-load
|
||||||
|
the RAWG_API_KEY from your .env file.`);
|
||||||
|
process.exit(args.length === 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const positional = [];
|
||||||
|
let platform = null;
|
||||||
|
let rawgId = null;
|
||||||
|
let outName = null;
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = args[i];
|
||||||
|
if (a === '--platform') platform = args[++i];
|
||||||
|
else if (a === '--id') rawgId = Number(args[++i]);
|
||||||
|
else if (a === '--out') outName = args[++i];
|
||||||
|
else if (!a.startsWith('--')) positional.push(a);
|
||||||
|
}
|
||||||
|
const title = positional.join(' ').trim();
|
||||||
|
|
||||||
|
// --- helpers ----------------------------------------------------------------
|
||||||
|
const slugify = (s) =>
|
||||||
|
s
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/['’.:]/g, '')
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '');
|
||||||
|
|
||||||
|
const fail = (msg) => {
|
||||||
|
console.error(`✖ ${msg}`);
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildImageUrl = (game) => {
|
||||||
|
// Prefer the wide/landscape screenshot-style image, which suits the
|
||||||
|
// aspect-[4/3] Hall of Fame cards. Fall back through RAWG's image fields.
|
||||||
|
const candidates = [
|
||||||
|
game.background_image,
|
||||||
|
game.background_image_additional,
|
||||||
|
];
|
||||||
|
const url = candidates.find(Boolean);
|
||||||
|
if (!url) return null;
|
||||||
|
// RAWG serves http(s) images; force https.
|
||||||
|
return url.replace(/^http:\/\//i, 'https://');
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- main -------------------------------------------------------------------
|
||||||
|
const API_KEY = process.env.RAWG_API_KEY;
|
||||||
|
if (!API_KEY) {
|
||||||
|
fail(
|
||||||
|
'RAWG_API_KEY is not set. Add it to .env (see .env.example) and run with\n' +
|
||||||
|
' node --env-file=.env scripts/fetch-box-art.mjs "..."',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let game;
|
||||||
|
try {
|
||||||
|
if (rawgId) {
|
||||||
|
console.log(`→ Fetching RAWG game id ${rawgId}…`);
|
||||||
|
const res = await fetch(`${RAWG_BASE}/games/${rawgId}?key=${API_KEY}`);
|
||||||
|
if (!res.ok) {
|
||||||
|
if (res.status === 404) fail(`No RAWG game with id ${rawgId}.`);
|
||||||
|
fail(`RAWG detail request failed: ${res.status} ${res.statusText}`);
|
||||||
|
}
|
||||||
|
game = await res.json();
|
||||||
|
} else {
|
||||||
|
if (!title) fail('No game title provided.');
|
||||||
|
const params = new URLSearchParams({ key: API_KEY, search: title, page_size: '5' });
|
||||||
|
if (platform) {
|
||||||
|
const pid = PLATFORM_IDS[platform.toLowerCase()];
|
||||||
|
if (!pid) {
|
||||||
|
fail(
|
||||||
|
`Unknown --platform "${platform}". Try one of: ${Object.keys(PLATFORM_IDS).join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
params.set('parent_platforms', String(pid));
|
||||||
|
}
|
||||||
|
console.log(`→ Searching RAWG for "${title}"${platform ? ` on ${platform}` : ''}…`);
|
||||||
|
const res = await fetch(`${RAWG_BASE}/games?${params}`);
|
||||||
|
if (!res.ok) fail(`RAWG search failed: ${res.status} ${res.statusText}`);
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.results?.length) fail(`No RAWG results for "${title}".`);
|
||||||
|
game = data.results[0];
|
||||||
|
console.log(` found: "${game.name}" (id ${game.id})${game.released ? `, ${game.released.slice(0, 4)}` : ''}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
fail(err?.message ? `Network error: ${err.message}` : 'Network error contacting RAWG.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageUrl = buildImageUrl(game);
|
||||||
|
if (!imageUrl) {
|
||||||
|
fail(`RAWG has no usable image for "${game.name}". Try another title or --id.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide output filename + extension (keep the source extension if --out omits one).
|
||||||
|
const baseName = outName ?? `${slugify(game.name)}.jpg`;
|
||||||
|
const ext = extname(baseName) || '.jpg';
|
||||||
|
const fileName = baseName.replace(/\.[^.]+$/, '') + ext;
|
||||||
|
const outPath = join(OUT_DIR, fileName);
|
||||||
|
|
||||||
|
console.log(`→ Downloading ${imageUrl}`);
|
||||||
|
const imgRes = await fetch(imageUrl);
|
||||||
|
if (!imgRes.ok) fail(`Image download failed: ${imgRes.status} ${imgRes.statusText}`);
|
||||||
|
const buf = Buffer.from(await imgRes.arrayBuffer());
|
||||||
|
|
||||||
|
await mkdir(OUT_DIR, { recursive: true });
|
||||||
|
await writeFile(outPath, buf);
|
||||||
|
|
||||||
|
const publicPath = `/images/games/${fileName}`;
|
||||||
|
console.log(`\n✔ Saved: ${outPath}`);
|
||||||
|
console.log(` Use this in your game file's frontmatter:`);
|
||||||
|
console.log(` art: "${publicPath}"`);
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* GameCard.astro
|
||||||
|
* ---------------------------------------------------------------------------
|
||||||
|
* Card for a Gaming Hall of Fame entry. Shows the game's banner/box art (or a
|
||||||
|
* placeholder slab when no art is set yet), the title, a platform pill, the
|
||||||
|
* release year, and the blurb. Matches the warm "interest" surface styling and
|
||||||
|
* the existing card idiom (`.card card-interactive`).
|
||||||
|
*
|
||||||
|
* Driven by the `games` collection schema in src/content.config.ts.
|
||||||
|
*/
|
||||||
|
import Icon from '@components/Icon.astro';
|
||||||
|
import { platformToIcon } from '@lib/platforms';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
platform: string;
|
||||||
|
blurb: string;
|
||||||
|
/** Public path to art, e.g. "/images/games/elden-ring.jpg". */
|
||||||
|
art?: string;
|
||||||
|
/** Alt text for the art; defaults to the title. */
|
||||||
|
artAlt?: string;
|
||||||
|
/** Optional release year shown next to the platform pill. */
|
||||||
|
year?: number;
|
||||||
|
/** Optional mdi icon override; defaults to a platform-derived icon. */
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, platform, blurb, art, artAlt, year, icon } = Astro.props;
|
||||||
|
|
||||||
|
const platIcon = icon ?? platformToIcon(platform);
|
||||||
|
const alt = artAlt ?? title;
|
||||||
|
---
|
||||||
|
|
||||||
|
<article class="card card-interactive flex h-full flex-col gap-0 overflow-hidden p-0">
|
||||||
|
<div class="relative aspect-[4/3] w-full overflow-hidden">
|
||||||
|
{
|
||||||
|
art ? (
|
||||||
|
<img
|
||||||
|
src={art}
|
||||||
|
alt={alt}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
class="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
class="flex h-full w-full items-center justify-center"
|
||||||
|
style="background: color-mix(in srgb, var(--color-secondary) 16%, transparent);"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="gamepad-variant"
|
||||||
|
class="h-12 w-12 opacity-50"
|
||||||
|
style="color: var(--color-text-muted);"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-3 p-5">
|
||||||
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
|
<span class="pill text-sm">
|
||||||
|
<Icon name={platIcon} class="h-3.5 w-3.5" />
|
||||||
|
{platform}
|
||||||
|
</span>
|
||||||
|
{year && (
|
||||||
|
<span
|
||||||
|
class="font-mono text-[0.65rem] uppercase tracking-[0.18em]"
|
||||||
|
style="color: var(--color-text-muted);"
|
||||||
|
>
|
||||||
|
{year}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="font-sans text-lg" style="color: var(--color-text);">{title}</h3>
|
||||||
|
|
||||||
|
<p class="grow text-sm leading-relaxed" style="color: var(--color-text-muted);">
|
||||||
|
{blurb}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
* Collections:
|
* Collections:
|
||||||
* - projects: portfolio entries shown on the Professional page
|
* - projects: portfolio entries shown on the Professional page
|
||||||
* - experience: work history entries shown on the Experience timeline
|
* - experience: work history entries shown on the Experience timeline
|
||||||
|
* - games: Gaming Hall of Fame cards (one file per game)
|
||||||
|
* - nowPlaying: currently-playing games (single list file)
|
||||||
*/
|
*/
|
||||||
import { defineCollection } from 'astro:content';
|
import { defineCollection } from 'astro:content';
|
||||||
import { z } from 'zod/v4';
|
import { z } from 'zod/v4';
|
||||||
@@ -50,4 +52,40 @@ const experience = defineCollection({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const collections = { projects, experience };
|
const games = defineCollection({
|
||||||
|
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/games/hall-of-fame' }),
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
/** Platform Greg plays it on, e.g. "PlayStation 5". Drives the platform pill + icon. */
|
||||||
|
platform: z.string(),
|
||||||
|
/** 1-3 sentence blurb on why it's a favorite. */
|
||||||
|
blurb: z.string(),
|
||||||
|
/** Optional public path to box/banner art, e.g. "/images/games/elden-ring.jpg". */
|
||||||
|
art: z.string().optional(),
|
||||||
|
/** Optional alt text for the art. Defaults to the title. */
|
||||||
|
artAlt: z.string().optional(),
|
||||||
|
/** Optional release year, shown as a small detail. */
|
||||||
|
year: z.number().optional(),
|
||||||
|
/**
|
||||||
|
* Optional mdi icon override for the card badge. Defaults to a per-platform
|
||||||
|
* icon derived from `platform` (see `platformIcon`).
|
||||||
|
*/
|
||||||
|
icon: z.string().optional(),
|
||||||
|
/** Sort order; lower comes first. Defaults to 100. */
|
||||||
|
order: z.number().default(100),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const nowPlaying = defineCollection({
|
||||||
|
loader: glob({ pattern: 'now-playing.yaml', base: './src/content/games' }),
|
||||||
|
schema: z.object({
|
||||||
|
games: z.array(
|
||||||
|
z.object({
|
||||||
|
title: z.string(),
|
||||||
|
platform: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { projects, experience, games, nowPlaying };
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: "Age of Empires"
|
||||||
|
platform: "PC"
|
||||||
|
year: 1997
|
||||||
|
art: "/images/games/age-of-empires.jpg" # run: node scripts/fetch-box-art.mjs "Balatro"
|
||||||
|
blurb: >
|
||||||
|
A masterful real time strategy game using historic civilizations from cavemen to castles. My uncle showed me this game and never looked back, I would beg for my family laptop and wake up early to get time in before school. While I could say I learned history playing this game, I mostly learned to run around going "Woo-lu-loo".
|
||||||
|
order: 50
|
||||||
|
---
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: "Ark: Survival Evolved"
|
||||||
|
platform: "Steam"
|
||||||
|
year: 2017
|
||||||
|
art: "/images/games/ark-survival-evolved.jpg" # run: node scripts/fetch-box-art.mjs "Stardew Valley"
|
||||||
|
blurb: >
|
||||||
|
Survival crafting game with dinosaurs. While I do not love the practices of the studio, I do love taming and surviving with dinosaurs. I found an online community here when I was not in the greatest place and learned a lot about myself in the process.
|
||||||
|
order: 50
|
||||||
|
---
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: "Dave the Diver"
|
||||||
|
platform: "Steam"
|
||||||
|
year: 2023
|
||||||
|
art: "/images/games/dave-the-diver.jpg" # run: node scripts/fetch-box-art.mjs "Balatro"
|
||||||
|
blurb: >
|
||||||
|
A casual, singleplayer adventure RPG featuring deep-sea exploration and fishing during the day and sushi restaurant management at night. This game had no right being as good as it was. I was addicted to this game for a weeks straight.
|
||||||
|
order: 50
|
||||||
|
---
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: "Elden Ring"
|
||||||
|
platform: "PlayStation 5"
|
||||||
|
year: 2022
|
||||||
|
art: "/images/games/elden-ring.jpg" # run: node scripts/fetch-box-art.mjs "Elden Ring"
|
||||||
|
blurb: >
|
||||||
|
Fantasy Action RPG. The game that made me finish a Souls-like. So good I did it twice! Arcane Blood build, RoB my darling, and an Intelligence Moonveil build, living out my Ichigo arc.
|
||||||
|
order: 40
|
||||||
|
---
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: "Knights of the Old Republic"
|
||||||
|
platform: "Steam"
|
||||||
|
year: 2003
|
||||||
|
art: "/images/games/star-wars-knights-of-the-old-republic.jpg" # run: node scripts/fetch-box-art.mjs "Stardew Valley"
|
||||||
|
blurb: >
|
||||||
|
Fantastic RPG for peak BioWare. My own lightsaber, my own choices, my own force power and awesome companions? My nerd cup runeth over. Plus one of the greatest reveals in gaming. I wish I could replay it knowing nothing. I loved the second one and the MMO as well.
|
||||||
|
order: 60
|
||||||
|
---
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: "Minecraft"
|
||||||
|
platform: "PC"
|
||||||
|
year: 2010
|
||||||
|
art: "/images/games/minecraft.jpg" # run: node scripts/fetch-box-art.mjs "Stardew Valley"
|
||||||
|
blurb: >
|
||||||
|
The crafting game. I picked up a copy of Infdev minecraft from a highschool girlfriend's cousin, and that is the part of the relationship that changed my life. I have spent untold hours building and surviving and modding an endless world of blocks and I wouldn't trade a second of it.
|
||||||
|
order: 40
|
||||||
|
---
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: "Pokemon"
|
||||||
|
platform: "Gameboy"
|
||||||
|
year: 1998
|
||||||
|
art: "/images/games/pok-mon-crystal-version.jpg" # run: node scripts/fetch-box-art.mjs "Stardew Valley"
|
||||||
|
blurb: >
|
||||||
|
The definitive creature collector and battling series. Pokemon Crystal was the first video game that was mine. And I loved it. While Emerald, Platnum, and even Violet might be my more prefered versions, I am a pokemon fan through and through.
|
||||||
|
order: 10
|
||||||
|
---
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: "Satisfactory"
|
||||||
|
platform: "Steam"
|
||||||
|
year: 2024
|
||||||
|
art: "/images/games/satisfactory.jpg" # run: node scripts/fetch-box-art.mjs "Stardew Valley"
|
||||||
|
blurb: >
|
||||||
|
An openworld automation and management game where the wit of your AI overseer meets the harsh realities of an alien planet. There is just something about beatifully lined up conveyer belts feeding an optimized factory with trains that actually run on schedule that I lose hours to.
|
||||||
|
order: 60
|
||||||
|
---
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: "Sly Cooper"
|
||||||
|
platform: "Playstation 2"
|
||||||
|
year: 2002
|
||||||
|
art: "/images/games/sly-cooper-and-the-thievius-raccoonus.jpg" # run: node scripts/fetch-box-art.mjs "Stardew Valley"
|
||||||
|
blurb: >
|
||||||
|
A wonderful cartoony stealth game with platformer elements. The first game I ever 100% and one of my favorite game series to this day. I will happily drop everything and replay these games at almost any moment.
|
||||||
|
order: 20
|
||||||
|
---
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: "Stardew Valley"
|
||||||
|
platform: "Nintendo Switch"
|
||||||
|
year: 2016
|
||||||
|
art: "/images/games/stardew-valley.jpg" # run: node scripts/fetch-box-art.mjs "Stardew Valley"
|
||||||
|
blurb: >
|
||||||
|
The coziest farming sim I have ever played. I have sunk way too many hours into being a blueberry mogal and firmly believe I could run a farm if I could operate it from a switch controller.
|
||||||
|
order: 30
|
||||||
|
---
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Games Greg is actively playing right now. The "Now playing" section on the
|
||||||
|
# Gaming page reads from this list. Edit freely — add, remove, or reorder.
|
||||||
|
# `platform` drives the icon shown on the card.
|
||||||
|
#
|
||||||
|
# Run `node scripts/fetch-box-art.mjs "<title>"` to pull box/banner art for
|
||||||
|
# Hall of Fame entries (this list is title + platform only).
|
||||||
|
games:
|
||||||
|
- title: "PalWorld"
|
||||||
|
platform: "Steam"
|
||||||
|
- title: "Gloomhaven"
|
||||||
|
platform: "Steam"
|
||||||
|
- title: "Pokopia"
|
||||||
|
platform: "Switch 2"
|
||||||
|
- title: "Slay the Spire 2"
|
||||||
|
platform: "Steam"
|
||||||
|
- title: "Indiana Jones and the Great Circle"
|
||||||
|
platform: "Steam"
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* platforms.ts
|
||||||
|
* ---------------------------------------------------------------------------
|
||||||
|
* Helpers for mapping a free-form platform string (as authored in the games /
|
||||||
|
* nowPlaying content) to an mdi icon name. Kept here so it can be shared
|
||||||
|
* between the gaming page and any future components without coupling to the
|
||||||
|
* content collection config.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an mdi icon name (without the `mdi:` prefix) for a platform string.
|
||||||
|
* Matches case-insensitively on substrings; unknown platforms fall back to a
|
||||||
|
* generic gamepad.
|
||||||
|
*
|
||||||
|
* @example platformToIcon("PlayStation 5") // "sony-playstation"
|
||||||
|
*/
|
||||||
|
export const platformToIcon = (platform: string): string => {
|
||||||
|
const p = platform.toLowerCase();
|
||||||
|
if (p.includes('playstation') || p.includes('ps5') || p.includes('ps4')) return 'sony-playstation';
|
||||||
|
if (p.includes('switch')) return 'nintendo-switch';
|
||||||
|
if (p.includes('steam') || p.includes('pc')) return 'steam';
|
||||||
|
if (p.includes('xbox')) return 'microsoft-xbox';
|
||||||
|
return 'gamepad-variant';
|
||||||
|
};
|
||||||
@@ -9,45 +9,95 @@ import BaseLayout from "@layouts/BaseLayout.astro";
|
|||||||
import Reveal from "@components/motion/Reveal.astro";
|
import Reveal from "@components/motion/Reveal.astro";
|
||||||
import InterestHero from "@components/layout/InterestHero.astro";
|
import InterestHero from "@components/layout/InterestHero.astro";
|
||||||
import InterestCard from "@components/cards/InterestCard.astro";
|
import InterestCard from "@components/cards/InterestCard.astro";
|
||||||
|
import GameCard from "@components/cards/GameCard.astro";
|
||||||
import SectionHeader from "@components/layout/SectionHeader.astro";
|
import SectionHeader from "@components/layout/SectionHeader.astro";
|
||||||
import RelatedInterests from "@components/layout/RelatedInterests.astro";
|
import RelatedInterests from "@components/layout/RelatedInterests.astro";
|
||||||
import Placeholder from "@components/layout/Placeholder.astro";
|
import Placeholder from "@components/layout/Placeholder.astro";
|
||||||
import Icon from "@components/Icon.astro";
|
import Icon from "@components/Icon.astro";
|
||||||
|
import { getCollection, getEntry } from "astro:content";
|
||||||
|
import { platformToIcon } from "@lib/platforms";
|
||||||
|
|
||||||
const platforms = [
|
const platforms = [
|
||||||
{
|
{
|
||||||
icon: "sony-playstation",
|
icon: "sony-playstation",
|
||||||
title: "PlayStation 5",
|
title: "PlayStation 5",
|
||||||
body: "Main console for AAA titles and exclusives.",
|
body: "Mainly for single player and exclusives.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "nintendo-switch",
|
icon: "nintendo-switch",
|
||||||
title: "Nintendo Switch / Switch 2",
|
title: "Nintendo Switch / Switch 2",
|
||||||
body: "Handheld and docked. Nintendo first-party plus indie gems.",
|
body: "Handheld and docked. Pokemon, Mario, and many other Nintendo and indie gems.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "steam",
|
icon: "steam",
|
||||||
title: "Steam",
|
title: "Steam",
|
||||||
body: "The primary PC library. Strategy, RPGs, and an ever-growing backlog.",
|
body: "The primary game library. Strategy, RPGs, Adventure, oh my! And an ever-growing backlog.",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Favorite genres. Each is a small card with an mdi icon and a one-line blurb
|
||||||
|
* on what Greg loves about that genre. Page-local; edit freely.
|
||||||
|
*/
|
||||||
const genres = [
|
const genres = [
|
||||||
"RPGs (both Western and JRPG)",
|
{
|
||||||
"Strategy and tactics",
|
icon: "sword-cross",
|
||||||
"Building and crafting",
|
name: "RPGs (Western & JRPG)",
|
||||||
"Automation",
|
blurb:
|
||||||
"Open-world exploration",
|
"Character progression and stories I can live in for 100+ hours and then turn around and do it again.",
|
||||||
"Indie darlings",
|
},
|
||||||
|
{
|
||||||
|
icon: "pokeball",
|
||||||
|
name: "Creature Collecting",
|
||||||
|
blurb: "If I can catch, train, or befriend a roster, I'm sold.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "chess-king",
|
||||||
|
name: "Strategy & Tactics",
|
||||||
|
blurb:
|
||||||
|
"Turn-based puzzles and tactical challengeswhere the right plan is oh so rewarding.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "hammer",
|
||||||
|
name: "Building & Crafting",
|
||||||
|
blurb:
|
||||||
|
"Give me a grid, resources, and a goal, and watch the evening vanish.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "factory",
|
||||||
|
name: "Automation",
|
||||||
|
blurb: "Optimizing a production line until its perfect is my zen.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "compass-outline",
|
||||||
|
name: "Open-world Exploration",
|
||||||
|
blurb:
|
||||||
|
"The map IS the point. I will try to climb every mountain and I will check every waterfall for treasure.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "star-four-points",
|
||||||
|
name: "Indie Darlings",
|
||||||
|
blurb:
|
||||||
|
"The small, weird, passionate projects that punch far above their size.",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Gaming Hall of Fame — one file per game in src/content/games/hall-of-fame/.
|
||||||
|
const hallOfFame = (await getCollection("games")).sort(
|
||||||
|
(a, b) => a.data.order - b.data.order,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Currently playing — single list in src/content/games/now-playing.yaml.
|
||||||
|
const nowPlayingEntry = await getEntry("nowPlaying", "now-playing");
|
||||||
|
const nowPlaying = nowPlayingEntry?.data.games ?? [];
|
||||||
|
|
||||||
const related = [
|
const related = [
|
||||||
{
|
/*{
|
||||||
href: "/interests/pokemon",
|
href: "/interests/pokemon",
|
||||||
title: "Pokémon",
|
title: "Pokémon",
|
||||||
icon: "pokeball",
|
icon: "pokeball",
|
||||||
blurb: "Nintendo console overlap and a lifelong obsession.",
|
blurb: "Nintendo console overlap and a lifelong obsession.",
|
||||||
},
|
},*/
|
||||||
{
|
{
|
||||||
href: "/interests/ttrpgs",
|
href: "/interests/ttrpgs",
|
||||||
title: "TTRPGs & Tabletop",
|
title: "TTRPGs & Tabletop",
|
||||||
@@ -63,9 +113,9 @@ const related = [
|
|||||||
description="PlayStation 5, Nintendo Switch 2, and a Steam library that has gotten politely out of hand. RPGs, strategy, and deeply layered worlds."
|
description="PlayStation 5, Nintendo Switch 2, and a Steam library that has gotten politely out of hand. RPGs, strategy, and deeply layered worlds."
|
||||||
>
|
>
|
||||||
<InterestHero
|
<InterestHero
|
||||||
icon="sony-playstation"
|
icon="gamepad-variant"
|
||||||
title="Gaming"
|
title="Gaming"
|
||||||
tagline="If it has a screen or a controller, I have probably played it."
|
tagline="Walking in another's shoes, escaping reality, or obsessively completing collections, one game at a time."
|
||||||
image="/images/gaming-setup.jpg"
|
image="/images/gaming-setup.jpg"
|
||||||
imageAlt="A gaming setup with controller and console"
|
imageAlt="A gaming setup with controller and console"
|
||||||
/>
|
/>
|
||||||
@@ -78,11 +128,16 @@ const related = [
|
|||||||
>
|
>
|
||||||
<Reveal
|
<Reveal
|
||||||
><p>
|
><p>
|
||||||
Gaming is my other constant. I span console generations with a
|
Gaming has always been a passion in my life. I span console
|
||||||
PlayStation 5, Nintendo Switch 2, and a Steam library that has gotten
|
generations from the Sega Genesis to the modern era. I play mainly
|
||||||
politely out of hand. I'm particularly drawn to RPGs, strategy games,
|
with a PlayStation 5, Nintendo Switch 2, and a Steam library that has
|
||||||
and anything with a deeply layered world. If a game has lore
|
gotten politely out of hand. I'm particularly drawn to RPGs, strategy
|
||||||
compendiums, I'm reading them.
|
games, and anything with a creature to tame or a deeply layered world
|
||||||
|
to explore. But a good game loop will get me sucked in harder than the
|
||||||
|
sunk cost fallacy of a League of Legends streak. I love to play with
|
||||||
|
friends, but have found myself leaning more towards single-player
|
||||||
|
experiences. Someone should have told younger me that scheduling would
|
||||||
|
be the final boss!
|
||||||
</p></Reveal
|
</p></Reveal
|
||||||
>
|
>
|
||||||
</section>
|
</section>
|
||||||
@@ -119,16 +174,37 @@ const related = [
|
|||||||
/></Reveal
|
/></Reveal
|
||||||
>
|
>
|
||||||
<Reveal delay={80}>
|
<Reveal delay={80}>
|
||||||
<ul class="flex flex-wrap gap-3">
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{
|
{
|
||||||
genres.map((g) => (
|
genres.map((g, i) => (
|
||||||
<li class="pill text-sm">
|
<Reveal delay={i * 60}>
|
||||||
<Icon name="star-four-points" class="h-3.5 w-3.5" />
|
<article class="card card-interactive flex h-full flex-col gap-2 p-4">
|
||||||
{g}
|
<div class="flex items-center gap-2.5">
|
||||||
</li>
|
<span
|
||||||
|
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg"
|
||||||
|
style="background: color-mix(in srgb, var(--color-accent) 16%, transparent); color: var(--color-accent);"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<Icon name={g.icon} class="h-5 w-5" />
|
||||||
|
</span>
|
||||||
|
<h3
|
||||||
|
class="font-sans text-base"
|
||||||
|
style="color: var(--color-text);"
|
||||||
|
>
|
||||||
|
{g.name}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
class="text-sm leading-relaxed"
|
||||||
|
style="color: var(--color-text-muted);"
|
||||||
|
>
|
||||||
|
{g.blurb}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</Reveal>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</ul>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -141,24 +217,63 @@ const related = [
|
|||||||
title="Now playing"
|
title="Now playing"
|
||||||
/></Reveal
|
/></Reveal
|
||||||
>
|
>
|
||||||
<div class="grid gap-4 sm:grid-cols-[1fr_1fr]">
|
{
|
||||||
<figure
|
nowPlaying.length > 0 ? (
|
||||||
class="relative overflow-hidden rounded-xl border"
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
style="border-color: color-mix(in srgb, var(--color-accent) 35%, transparent);"
|
{nowPlaying.map((g, i) => (
|
||||||
|
<Reveal delay={i * 60}>
|
||||||
|
<InterestCard
|
||||||
|
icon={platformToIcon(g.platform)}
|
||||||
|
title={g.title}
|
||||||
|
currently
|
||||||
>
|
>
|
||||||
<img
|
{g.platform}
|
||||||
src="/images/gaming-setup.jpg"
|
</InterestCard>
|
||||||
alt="Gaming setup"
|
</Reveal>
|
||||||
loading="lazy"
|
))}
|
||||||
decoding="async"
|
</div>
|
||||||
class="aspect-[4/3] w-full object-cover"
|
) : (
|
||||||
/>
|
|
||||||
</figure>
|
|
||||||
<Placeholder
|
<Placeholder
|
||||||
label="Currently playing"
|
label="Currently playing"
|
||||||
hint="Add the games you are actively playing right now."
|
hint="Add games to src/content/games/now-playing.yaml."
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Gaming Hall of Fame */}
|
||||||
|
<section class="flex flex-col gap-6">
|
||||||
|
<Reveal
|
||||||
|
><SectionHeader
|
||||||
|
kicker="Hall of Fame"
|
||||||
|
icon="trophy"
|
||||||
|
title="All-time favorites"
|
||||||
|
/></Reveal
|
||||||
|
>
|
||||||
|
{
|
||||||
|
hallOfFame.length > 0 ? (
|
||||||
|
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{hallOfFame.map((g, i) => (
|
||||||
|
<Reveal delay={i * 80}>
|
||||||
|
<GameCard
|
||||||
|
title={g.data.title}
|
||||||
|
platform={g.data.platform}
|
||||||
|
blurb={g.data.blurb}
|
||||||
|
art={g.data.art}
|
||||||
|
artAlt={g.data.artAlt}
|
||||||
|
year={g.data.year}
|
||||||
|
icon={g.data.icon}
|
||||||
|
/>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Placeholder
|
||||||
|
label="Gaming Hall of Fame"
|
||||||
|
hint="Add games to src/content/games/hall-of-fame/."
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<RelatedInterests items={related} />
|
<RelatedInterests items={related} />
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const interests = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/interests/gaming",
|
href: "/interests/gaming",
|
||||||
icon: "sony-playstation",
|
icon: "gamepad-variant",
|
||||||
title: "Gaming",
|
title: "Gaming",
|
||||||
tagline: "We do not talk about my backlog.",
|
tagline: "We do not talk about my backlog.",
|
||||||
blurb:
|
blurb:
|
||||||
|
|||||||
@@ -5,26 +5,57 @@
|
|||||||
* Per-interest template with equipment card grid, a gallery placeholder, and the
|
* Per-interest template with equipment card grid, a gallery placeholder, and the
|
||||||
* spec's connection-to-TTRPGs callout. Related links back to TTRPGs and Gaming.
|
* spec's connection-to-TTRPGs callout. Related links back to TTRPGs and Gaming.
|
||||||
*/
|
*/
|
||||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
import BaseLayout from "@layouts/BaseLayout.astro";
|
||||||
import Reveal from '@components/motion/Reveal.astro';
|
import Reveal from "@components/motion/Reveal.astro";
|
||||||
import InterestHero from '@components/layout/InterestHero.astro';
|
import InterestHero from "@components/layout/InterestHero.astro";
|
||||||
import InterestCard from '@components/cards/InterestCard.astro';
|
import InterestCard from "@components/cards/InterestCard.astro";
|
||||||
import SectionHeader from '@components/layout/SectionHeader.astro';
|
import SectionHeader from "@components/layout/SectionHeader.astro";
|
||||||
import RelatedInterests from '@components/layout/RelatedInterests.astro';
|
import RelatedInterests from "@components/layout/RelatedInterests.astro";
|
||||||
import Placeholder from '@components/layout/Placeholder.astro';
|
import Placeholder from "@components/layout/Placeholder.astro";
|
||||||
import Icon from '@components/Icon.astro';
|
import Icon from "@components/Icon.astro";
|
||||||
import CelestialImage from '@components/mystic/CelestialImage.astro';
|
import CelestialImage from "@components/mystic/CelestialImage.astro";
|
||||||
|
|
||||||
const equipment = [
|
const equipment = [
|
||||||
{ icon: 'palette', title: 'Resin Dice Making', body: 'Custom molds, pressure pot curing, pigment experimentation. Producing dice sets for personal use and gifts.' },
|
{
|
||||||
{ icon: 'printer-3d', title: 'Elegoo Mars 4', body: 'Resin 3D printer. Miniatures, terrain, and high-detail props.' },
|
icon: "dice-multiple",
|
||||||
{ icon: 'printer-3d-nozzle', title: 'Bambu Labs P1S', body: 'FDM 3D printer. Functional prints, terrain pieces, and larger prototypes.' },
|
title: "Resin Dice Making",
|
||||||
{ icon: 'wrench', title: 'General Making', body: 'Always experimenting. If there is a new technique or material, I want to try it.' },
|
body: "Custom molds, pressure pot curing, pigment experimentation. Producing dice sets for personal use and gifts.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "printer-3d",
|
||||||
|
title: "Elegoo Mars 4",
|
||||||
|
body: "Resin 3D printer. Miniatures, terrain, and high-detail props.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "printer-3d-nozzle",
|
||||||
|
title: "Bambu Labs P1S",
|
||||||
|
body: "FDM 3D printer. Functional prints, terrain pieces, and larger prototypes.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "scissors-cutting",
|
||||||
|
title: "Silhouette Cameo 5a",
|
||||||
|
body: "Multi material cutting machine. Silly stickers, custom vinyl decals, and custom/proxy TCG cards.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "wrench",
|
||||||
|
title: "General Making",
|
||||||
|
body: "Always experimenting and hoarding nice cardboard. If there is a new technique or material, I want to try it.",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const related = [
|
const related = [
|
||||||
{ href: '/interests/ttrpgs', title: 'TTRPGs & Tabletop', icon: 'dice-multiple', blurb: 'Much of what I make ends up on the virtual table.' },
|
{
|
||||||
{ href: '/interests/reading', title: 'Reading & Media', icon: 'book-open-page-variant', blurb: 'Displays and props for the stories I love.' },
|
href: "/interests/ttrpgs",
|
||||||
|
title: "TTRPGs & Tabletop",
|
||||||
|
icon: "dice-multiple",
|
||||||
|
blurb: "Much of what I make ends up on the virtual table.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "/interests/reading",
|
||||||
|
title: "Reading & Media",
|
||||||
|
icon: "book-open-page-variant",
|
||||||
|
blurb: "Displays and props for the stories I love.",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -36,58 +67,128 @@ const related = [
|
|||||||
<InterestHero
|
<InterestHero
|
||||||
icon="palette"
|
icon="palette"
|
||||||
title="Making & Creating"
|
title="Making & Creating"
|
||||||
tagline="If it involves layers and precision, I am in."
|
tagline="A magnet for super glue and nice cardboard."
|
||||||
image="/images/making-print.jpg"
|
image="/images/making-print.jpg"
|
||||||
imageAlt="A 3D printer producing a detailed model"
|
imageAlt="A 3D printer producing a detailed model"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="container-prose flex flex-col gap-20 py-12 sm:py-16">
|
<div class="container-prose flex flex-col gap-20 py-12 sm:py-16">
|
||||||
{/* Overview */}
|
{/* Overview */}
|
||||||
<section class="flex flex-col gap-5 text-lg leading-relaxed" style="color: var(--color-text-muted);">
|
<section
|
||||||
<Reveal><p>Making is how I decompress. I cast resin dice using pressure pots and custom molds, print miniatures and props on my Elegoo Mars 4 (resin) and Bambu Labs P1S (FDM), and generally fill my workspace with projects-in-progress. There's something deeply satisfying about taking raw material through a process and holding the finished piece, whether that's a perfect polyhedral set or a printed terrain piece for the next game session.</p></Reveal>
|
class="flex flex-col gap-5 text-lg leading-relaxed"
|
||||||
|
style="color: var(--color-text-muted);"
|
||||||
|
>
|
||||||
|
<Reveal
|
||||||
|
><p>
|
||||||
|
Making is part of me. Ever since I can remember, I had been dreaming
|
||||||
|
of making inventions and things from cardboard and clay. I took things
|
||||||
|
apart to see how they worked, and rarely got them back together. But
|
||||||
|
now I have the tools and the skills to take ideas from my imagination
|
||||||
|
and physically produce them. I cast resin dice using pressure pots and
|
||||||
|
custom molds, print miniatures and props on my Elegoo Mars 4 (resin)
|
||||||
|
and Bambu Labs P1S (FDM), and generally fill my workspace with
|
||||||
|
projects-in-progress. I make beatiful cards as gifts and even make
|
||||||
|
proxy mockups of trading cards to enjoy games casually.
|
||||||
|
</p></Reveal
|
||||||
|
>
|
||||||
|
<Reveal>
|
||||||
|
<p>
|
||||||
|
There's something deeply satisfying about taking raw material through
|
||||||
|
a process and holding the finished piece, whether that's a perfect
|
||||||
|
polyhedral set or a hook that fits just right in that weird spot in
|
||||||
|
the bathroom. The ability to blend my technial skills and my creative
|
||||||
|
impulses into tangible objects is a fantasy come true my child self
|
||||||
|
only dreamed of.
|
||||||
|
</p></Reveal
|
||||||
|
>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Equipment */}
|
{/* Equipment */}
|
||||||
<section class="flex flex-col gap-6">
|
<section class="flex flex-col gap-6">
|
||||||
<Reveal><SectionHeader kicker="Equipment" icon="toolbox" title="What is on the bench" /></Reveal>
|
<Reveal
|
||||||
|
><SectionHeader
|
||||||
|
kicker="Equipment"
|
||||||
|
icon="toolbox"
|
||||||
|
title="What is on the bench"
|
||||||
|
/></Reveal
|
||||||
|
>
|
||||||
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-2">
|
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-2">
|
||||||
{equipment.map((e, i) => (
|
{
|
||||||
|
equipment.map((e, i) => (
|
||||||
<Reveal delay={i * 70}>
|
<Reveal delay={i * 70}>
|
||||||
<InterestCard icon={e.icon} title={e.title}>{e.body}</InterestCard>
|
<InterestCard icon={e.icon} title={e.title}>
|
||||||
|
{e.body}
|
||||||
|
</InterestCard>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
))}
|
))
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Gallery */}
|
{/* Gallery */}
|
||||||
<section class="flex flex-col gap-6">
|
<section class="flex flex-col gap-6">
|
||||||
<Reveal><SectionHeader kicker="Gallery" icon="image-multiple" title="Finished pieces" /></Reveal>
|
<Reveal
|
||||||
|
><SectionHeader
|
||||||
|
kicker="Gallery"
|
||||||
|
icon="image-multiple"
|
||||||
|
title="Finished pieces"
|
||||||
|
/></Reveal
|
||||||
|
>
|
||||||
<Reveal delay={80}>
|
<Reveal delay={80}>
|
||||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
<CelestialImage src="/images/making-resin.jpg" alt="Resin dice set" aspect="4/3" />
|
<CelestialImage
|
||||||
<CelestialImage src="/images/making-print.jpg" alt="3D printed miniature" aspect="4/3" />
|
src="/images/making-resin.jpg"
|
||||||
<CelestialImage src="/images/ttrpg-dice.jpg" alt="Terrain piece and dice" aspect="4/3" />
|
alt="Resin dice set"
|
||||||
|
aspect="4/3"
|
||||||
|
/>
|
||||||
|
<CelestialImage
|
||||||
|
src="/images/making-print.jpg"
|
||||||
|
alt="3D printed miniature"
|
||||||
|
aspect="4/3"
|
||||||
|
/>
|
||||||
|
<CelestialImage
|
||||||
|
src="/images/ttrpg-dice.jpg"
|
||||||
|
alt="Terrain piece and dice"
|
||||||
|
aspect="4/3"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
<Reveal delay={120}>
|
|
||||||
<Placeholder label="Your real photos" hint="Swap these stock stand-ins with photos of your own resin casts and prints." />
|
|
||||||
</Reveal>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Connection to TTRPGs */}
|
{/* Connection to TTRPGs */}
|
||||||
<section class="flex flex-col gap-5">
|
<section class="flex flex-col gap-5">
|
||||||
<Reveal><SectionHeader kicker="Cross-pollination" icon="link-variant" title="Where it all connects" /></Reveal>
|
<Reveal
|
||||||
|
><SectionHeader
|
||||||
|
kicker="Cross-pollination"
|
||||||
|
icon="link-variant"
|
||||||
|
title="Where it all connects"
|
||||||
|
/></Reveal
|
||||||
|
>
|
||||||
<Reveal delay={80}>
|
<Reveal delay={80}>
|
||||||
<a
|
<a
|
||||||
href="/interests/ttrpgs"
|
href="/interests/ttrpgs"
|
||||||
class="card card-interactive group flex items-center gap-4"
|
class="card card-interactive group flex items-center gap-4"
|
||||||
>
|
>
|
||||||
<Icon name="dice-multiple" class="h-8 w-8 shrink-0" style="color: var(--color-accent);" />
|
<Icon
|
||||||
|
name="dice-multiple"
|
||||||
|
class="h-8 w-8 shrink-0"
|
||||||
|
style="color: var(--color-accent);"
|
||||||
|
/>
|
||||||
<span class="flex flex-col gap-1">
|
<span class="flex flex-col gap-1">
|
||||||
<span class="font-sans font-semibold" style="color: var(--color-text);">Much of what I make ends up on the virtual table.</span>
|
<span
|
||||||
<span class="text-sm" style="color: var(--color-text-muted);">Custom tokens, terrain, and props for Foundry VTT campaigns.</span>
|
class="font-sans font-semibold"
|
||||||
|
style="color: var(--color-text);"
|
||||||
|
>Much of what I make ends up on the virtual table.</span
|
||||||
|
>
|
||||||
|
<span class="text-sm" style="color: var(--color-text-muted);"
|
||||||
|
>Custom tokens, terrain, and props for Foundry VTT campaigns.</span
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
<Icon name="arrow-right" class="ml-auto h-5 w-5 shrink-0 transition-transform group-hover:translate-x-1" style="color: var(--color-accent);" />
|
<Icon
|
||||||
|
name="arrow-right"
|
||||||
|
class="ml-auto h-5 w-5 shrink-0 transition-transform group-hover:translate-x-1"
|
||||||
|
style="color: var(--color-accent);"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import Placeholder from '@components/layout/Placeholder.astro';
|
|||||||
import Icon from '@components/Icon.astro';
|
import Icon from '@components/Icon.astro';
|
||||||
|
|
||||||
const related = [
|
const related = [
|
||||||
{ href: '/interests/gaming', title: 'Gaming', icon: 'sony-playstation', blurb: 'Nintendo console overlap, from Game Boy to Switch 2.' },
|
{ href: '/interests/gaming', title: 'Gaming', icon: 'gamepad-variant', blurb: 'Nintendo console overlap, from Game Boy to Switch 2.' },
|
||||||
{ href: '/interests/making', title: 'Making & Creating', icon: 'palette', blurb: 'Displays and props for collections.' },
|
{ href: '/interests/making', title: 'Making & Creating', icon: 'palette', blurb: 'Displays and props for collections.' },
|
||||||
];
|
];
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const highlights = [
|
|||||||
|
|
||||||
const related = [
|
const related = [
|
||||||
{ href: '/interests/making', title: 'Making & Creating', icon: 'palette', blurb: 'Dice and terrain made for campaigns.' },
|
{ href: '/interests/making', title: 'Making & Creating', icon: 'palette', blurb: 'Dice and terrain made for campaigns.' },
|
||||||
{ href: '/interests/gaming', title: 'Gaming', icon: 'sony-playstation', blurb: 'Digital RPGs as another expression of the same impulse.' },
|
{ href: '/interests/gaming', title: 'Gaming', icon: 'gamepad-variant', blurb: 'Digital RPGs as another expression of the same impulse.' },
|
||||||
];
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||