From 4d82fc07a4adaec98e2ec845519e4771ec2c546e Mon Sep 17 00:00:00 2001 From: artheru Date: Wed, 10 Jun 2026 01:57:25 +0800 Subject: [PATCH] tavern: real 2D-RPG game scene from CC0 game tiles (Kenney Tiny Dungeon) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TavernGame renders an actual tile-based RPG tavern: brick walls, floor, bar(吧台)+bartender(酒保)+bottles, tables(圆桌)+patrons, rug, dancer(舞女, spotlit+sway), barrels(酒桶), and the cyan amerc screen(屏幕) — animated fire/screen/dust - tiles downloaded from kenney.nl (CC0), bundled as public/scene2d/dungeon.png - credit added to README --- README.md | 4 + index.html | 2 +- package.json | 2 +- public/scene2d/dungeon-LICENSE.txt | 22 ++++++ public/scene2d/dungeon.png | Bin 0 -> 5294 bytes src/Scene2D.jsx | 35 +++------ src/TavernGame.jsx | 121 +++++++++++++++++++++++++++++ src/styles.css | 20 +++++ 8 files changed, 180 insertions(+), 26 deletions(-) create mode 100644 public/scene2d/dungeon-LICENSE.txt create mode 100644 public/scene2d/dungeon.png create mode 100644 src/TavernGame.jsx diff --git a/README.md b/README.md index e879f31..94f7573 100644 --- a/README.md +++ b/README.md @@ -47,3 +47,7 @@ npm run build # -> dist/ The pixel sprites in `public/scene2d/` are downscaled, nearest-neighbour-upscaled versions of the source art (kept out of this repo). + +## Credits + +Tavern scene tiles: **Kenney Tiny Dungeon** (CC0) — https://kenney.nl/assets/tiny-dungeon diff --git a/index.html b/index.html index 9c968f6..d0c825a 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ name="description" content="amerc is an agent mercenary layer for embedding, bringing, and hosting agents across vertical software boundaries." /> - + diff --git a/package.json b/package.json index 3727b6a..3cb9810 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amerc-site", - "version": "0.23.0-hero", + "version": "0.24.0-game", "private": true, "type": "module", "scripts": { diff --git a/public/scene2d/dungeon-LICENSE.txt b/public/scene2d/dungeon-LICENSE.txt new file mode 100644 index 0000000..1b269f6 --- /dev/null +++ b/public/scene2d/dungeon-LICENSE.txt @@ -0,0 +1,22 @@ + + + Tiny Dungeon (1.0) + + Created/distributed by Kenney (www.kenney.nl) + Creation date: 05-07-2022 + + ------------------------------ + + License: (Creative Commons Zero, CC0) + http://creativecommons.org/publicdomain/zero/1.0/ + + This content is free to use in personal, educational and commercial projects. + Support us by crediting Kenney or www.kenney.nl (this is not mandatory) + + ------------------------------ + + Donate: http://support.kenney.nl + Patreon: http://patreon.com/kenney/ + + Follow on Twitter for updates: + http://twitter.com/KenneyNL \ No newline at end of file diff --git a/public/scene2d/dungeon.png b/public/scene2d/dungeon.png new file mode 100644 index 0000000000000000000000000000000000000000..f6e8b937c99fe45b1b75490cc050ee5929954259 GIT binary patch literal 5294 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 { api('/agents/stats').then(setStats).catch(() => {}); }, []); - const Hot = ({ s, to, title, glow }) => ( - - : null} ); @@ -123,7 +110,7 @@ export default function Scene2DApp() { return (

amerc agent mercenary tavern

- {route !== 'home' && } +
); diff --git a/src/TavernGame.jsx b/src/TavernGame.jsx new file mode 100644 index 0000000..cdb114f --- /dev/null +++ b/src/TavernGame.jsx @@ -0,0 +1,121 @@ +import React, { useEffect, useRef, useState } from 'react'; + +// 2D RPG tavern built from Kenney "Tiny Dungeon" (CC0) tiles — Pokémon/Stardew style. +const T = 16, COLS = 22, ROWS = 14, W = COLS * T, H = ROWS * T; +const SHEET = '/scene2d/dungeon.png'; + +// tile picks [col,row] from the 12-wide sheet +const FLOOR = [0, 0], WALL = [4, 3], FIRE = [5, 2], DOOR1 = [10, 3], DOOR2 = [11, 3]; +const BARREL_A = [7, 5], BARREL_B = [8, 5], CRATE = [1, 6], TABLE = [0, 6], STOOL = [1, 6]; +const BOTTLES = [[5, 9], [6, 9], [7, 9], [8, 9]]; +// characters [col,row] +const BARTENDER = [2, 8], DANCER = [3, 8]; +const PATRONS = [[0, 7], [3, 7], [0, 8], [3, 9], [4, 9], [4, 8]]; + +function blit(ctx, sheet, c, r, x, y, scale = 1) { ctx.drawImage(sheet, c * T, r * T, T, T, x, y, T * scale, T * scale); } + +function buildScene(buf, sheet) { + const ctx = buf.getContext('2d'); ctx.imageSmoothingEnabled = false; + // floor + for (let y = 3; y < ROWS; y++) for (let x = 0; x < COLS; x++) blit(ctx, sheet, FLOOR[0], FLOOR[1], x * T, y * T); + // walls + for (let y = 0; y < 3; y++) for (let x = 0; x < COLS; x++) blit(ctx, sheet, WALL[0], WALL[1], x * T, y * T); + // wood baseboard between wall and floor + ctx.fillStyle = '#7c4e2c'; ctx.fillRect(0, 3 * T - 3, W, 3); ctx.fillStyle = '#a06a3c'; ctx.fillRect(0, 3 * T - 3, W, 1); ctx.fillStyle = '#1a0f08'; ctx.fillRect(0, 3 * T, W, 2); + // red rug (center) + const rx = 7 * T, ry = 7 * T, rw = 9 * T, rh = 6 * T; + ctx.fillStyle = '#6e2030'; ctx.fillRect(rx, ry, rw, rh); + ctx.fillStyle = '#8a2c3c'; ctx.fillRect(rx + 4, ry + 4, rw - 8, rh - 8); + ctx.fillStyle = '#d6a846'; ctx.fillRect(rx, ry, rw, 2); ctx.fillRect(rx, ry + rh - 2, rw, 2); ctx.fillRect(rx, ry, 2, rh); ctx.fillRect(rx + rw - 2, ry, 2, rh); + ctx.strokeStyle = '#d6a846'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(rx + rw / 2, ry + 8); ctx.lineTo(rx + rw - 10, ry + rh / 2); ctx.lineTo(rx + rw / 2, ry + rh - 8); ctx.lineTo(rx + 10, ry + rh / 2); ctx.closePath(); ctx.stroke(); + // wall decor: banners, door, torches, hearth + blit(ctx, sheet, DOOR1[0], DOOR1[1], 19 * T, 0); blit(ctx, sheet, DOOR2[0], DOOR2[1], 20 * T, 0); + for (const [bx, c1, c2] of [[4, '#3cc8f0', '#16607e'], [16, '#e0b040', '#7c5a1e']]) { ctx.fillStyle = c2; ctx.fillRect(bx * T + 5, 2, 6, 2 * T + 4); ctx.fillStyle = c1; ctx.fillRect(bx * T + 6, 2, 4, 2 * T); ctx.beginPath(); ctx.moveTo(bx * T + 5, 2 * T + 4); ctx.lineTo(bx * T + 8, 2 * T + 8); ctx.lineTo(bx * T + 11, 2 * T + 4); ctx.fill(); } + // hearth (left) + wall torches: fire tiles (animated glow added in loop) + blit(ctx, sheet, FIRE[0], FIRE[1], 1 * T, 1 * T); + for (const tx of [7, 13]) blit(ctx, sheet, FIRE[0], FIRE[1], tx * T, 1 * T); + // BAR counter (吧台): solid wood, left side, cols 1-7 row 4 + const bx0 = 1 * T, bw = 7 * T, byA = 4 * T + 4; + ctx.fillStyle = '#5e3a22'; ctx.fillRect(bx0, byA, bw, T + 2); + ctx.fillStyle = '#a06a3c'; ctx.fillRect(bx0, byA, bw, 3); ctx.fillStyle = '#c79155'; ctx.fillRect(bx0, byA + 3, bw, 1); + ctx.fillStyle = '#1a0f08'; ctx.fillRect(bx0, byA + T + 1, bw, 3); + ctx.fillStyle = '#3a2417'; for (let i = 1; i < 7; i++) ctx.fillRect(bx0 + i * T, byA + 5, 1, T - 4); + // bottles on the shelf behind the bar (row 3) + BOTTLES.forEach(([c, r], i) => blit(ctx, sheet, c, r, (2 + i * 1.4) * T | 0, 3 * T - 2)); + // bartender behind bar + blit(ctx, sheet, BARTENDER[0], BARTENDER[1], 4 * T, 3 * T + 2); + // round tables + stools + patrons around the rug + const seats = [[3, 9], [3, 12], [18, 9], [18, 12], [9, 13], [13, 13]]; + seats.forEach(([tx, ty], i) => { + blit(ctx, sheet, TABLE[0], TABLE[1], tx * T, ty * T); + blit(ctx, sheet, STOOL[0], STOOL[1], (tx - 1) * T, ty * T + 4); + const p = PATRONS[i % PATRONS.length]; + blit(ctx, sheet, p[0], p[1], (tx + 1) * T, ty * T); + }); + // barrels (酒桶) right corner + crates left corner + blit(ctx, sheet, BARREL_A[0], BARREL_A[1], 20 * T, 11 * T); blit(ctx, sheet, BARREL_B[0], BARREL_B[1], 19 * T, 11 * T); blit(ctx, sheet, BARREL_A[0], BARREL_A[1], 20 * T, 12 * T); + blit(ctx, sheet, CRATE[0], CRATE[1], 0, 12 * T); blit(ctx, sheet, CRATE[0], CRATE[1], 1 * T, 13 * T); +} + +export default function TavernGame({ onHotspot }) { + const canvasRef = useRef(null); + const [ready, setReady] = useState(false); + useEffect(() => { + const sheet = new Image(); sheet.src = SHEET; + let raf, f = 0, buf; + sheet.onload = () => { + setReady(true); + buf = document.createElement('canvas'); buf.width = W; buf.height = H; buildScene(buf, sheet); + const ctx = canvasRef.current.getContext('2d'); ctx.imageSmoothingEnabled = false; + const render = () => { + f++; + ctx.drawImage(buf, 0, 0); + // spotlight on the dancer + const cxp = 11 * T, cyp = 9 * T; + const sg = ctx.createRadialGradient(cxp + 8, cyp, 4, cxp + 8, cyp + 6, 40); + sg.addColorStop(0, 'rgba(255,240,200,0.22)'); sg.addColorStop(1, 'rgba(255,240,200,0)'); + ctx.fillStyle = sg; ctx.beginPath(); ctx.ellipse(cxp + 8, cyp + 8, 26, 30, 0, 0, 7); ctx.fill(); + // dancer (舞女): sway + bob + periodic flip + const sway = Math.sin(f * 0.14) * 3, bob = Math.abs(Math.sin(f * 0.14)) * 2, flip = Math.sin(f * 0.07) < 0; + ctx.save(); ctx.translate(cxp + 8 + sway, cyp - bob); + if (flip) ctx.scale(-1, 1); ctx.imageSmoothingEnabled = false; + ctx.drawImage(sheet, DANCER[0] * T, DANCER[1] * T, T, T, -8, 0, T, T); ctx.restore(); + // animated fire glow over hearth + torches + ctx.globalCompositeOperation = 'lighter'; + for (const [tx, ty, lr] of [[1, 1, 40], [7, 1, 26], [13, 1, 26]]) { + const a = 0.22 + 0.12 * Math.sin(f * 0.3 + tx); + const g = ctx.createRadialGradient(tx * T + 8, ty * T + 8, 0, tx * T + 8, ty * T + 8, lr); + g.addColorStop(0, `rgba(255,150,60,${a})`); g.addColorStop(1, 'rgba(255,150,60,0)'); + ctx.fillStyle = g; ctx.beginPath(); ctx.arc(tx * T + 8, ty * T + 8, lr, 0, 7); ctx.fill(); + } + // screen content (chart + scanline + cyan glow) + const sx = 9 * T, sy = 6; + for (let i = 0; i < 11; i++) { const bh = 3 + ((Math.sin(f * 0.09 + i) + 1) * 4 + (i % 3)) | 0; ctx.fillStyle = i % 2 ? '#7fe9ff' : '#2f9fd0'; ctx.fillRect(sx + 3 + i * 2.4 | 0, sy + 22 - bh, 2, bh); } + ctx.fillStyle = 'rgba(127,233,255,0.5)'; ctx.fillRect(sx, sy + (f % 26), 3 * T, 1); + const cg = ctx.createRadialGradient(10.5 * T, T, 0, 10.5 * T, T, 40); cg.addColorStop(0, 'rgba(70,200,235,0.16)'); cg.addColorStop(1, 'rgba(70,200,235,0)'); ctx.fillStyle = cg; ctx.beginPath(); ctx.arc(10.5 * T, T, 40, 0, 7); ctx.fill(); + ctx.globalCompositeOperation = 'source-over'; + // vignette + dust + const vg = ctx.createRadialGradient(W / 2, H * 0.52, H * 0.34, W / 2, H * 0.5, H * 0.8); vg.addColorStop(0, 'rgba(0,0,0,0)'); vg.addColorStop(1, 'rgba(8,4,10,0.55)'); ctx.fillStyle = vg; ctx.fillRect(0, 0, W, H); + ctx.fillStyle = 'rgba(255,225,170,0.45)'; + for (let i = 0; i < 12; i++) { const dx = (i * 47 + f * 0.18) % W, dy = (i * 59 + Math.sin(f * 0.02 + i) * 8 + 64) % H; ctx.fillRect(dx | 0, dy | 0, 1, 1); } + raf = requestAnimationFrame(render); + }; + render(); + }; + return () => cancelAnimationFrame(raf); + }, []); + + return ( +
+ + {!ready &&
loading tavern…
} +
+ + + +
+
+ ); +} diff --git a/src/styles.css b/src/styles.css index 6e707db..d97f7cb 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1026,3 +1026,23 @@ button { .hero-acct { position: absolute; right: 2.5%; bottom: 3.5%; font-family: Georgia, serif; font-size: clamp(10px, 1vw, 13px); color: #dfe8ff; background: rgba(8,16,30,0.66); border: 1px solid rgba(120,150,200,0.35); border-radius: 8px; padding: 6px 11px; } .hero-acct button { color: #5fe0ff; background: none; cursor: pointer; font-family: inherit; font-size: inherit; text-decoration: underline; } @media (max-width: 640px) { .hero-live span:nth-child(3) { display: none; } } + +/* ===================================================================== */ +/* 0.24 — 2D RPG game tavern (Kenney Tiny Dungeon CC0 tiles) */ +/* ===================================================================== */ +.gm-home { position: relative; flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; padding: 12px; background: radial-gradient(circle at 50% 30%, #16100e, #070506); overflow: hidden; } +.gm-stage { position: relative; aspect-ratio: 352 / 224; width: min(96vw, calc((100vh - 210px) * 1.571)); box-shadow: 0 14px 44px rgba(0,0,0,0.75), 0 0 0 3px #2a1a10, 0 0 0 7px #120a06, 0 0 0 9px #000; } +.gm-room { position: absolute; inset: 0; } +.gm-canvas { width: 100%; height: 100%; display: block; image-rendering: pixelated; image-rendering: crisp-edges; } +.gm-overlay { position: absolute; inset: 0; } +.gm-hot { position: absolute; background: transparent; border: 0; padding: 0; cursor: pointer; border-radius: 6px; transition: box-shadow .15s ease, background .15s ease; } +.gm-hot:hover { background: rgba(70,200,255,0.12); box-shadow: 0 0 0 2px rgba(80,205,255,0.55), 0 0 22px rgba(70,200,255,0.4); } +.gm-label { position: absolute; transform: translateX(-50%); font-family: Georgia, "Times New Roman", serif; font-size: clamp(10px, 1.25vw, 15px); color: #cfeaff; background: rgba(10,20,34,0.84); border: 1px solid rgba(80,200,255,0.45); border-radius: 7px; padding: 5px 12px; cursor: pointer; white-space: nowrap; box-shadow: 0 2px 9px rgba(0,0,0,0.55); } +.gm-label:hover { background: rgba(16,34,54,0.96); color: #fff; transform: translateX(-50%) translateY(-1px); } +.gm-label-booth { color: #7af0b8; border-color: rgba(80,240,180,0.5); } +.gm-label-bar { color: #f0c070; border-color: rgba(240,190,90,0.45); } +.gm-loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #9a86b8; font-family: Georgia, serif; } +.gm-caption { text-align: center; font-family: Georgia, "Times New Roman", serif; max-width: 760px; padding: 0 12px; } +.gm-caption h1 { font-size: clamp(18px, 2.6vw, 30px); color: #f3d27a; margin: 0 0 4px; text-shadow: 0 2px 7px #000; letter-spacing: 0.5px; } +.gm-caption p { font-size: clamp(11px, 1.3vw, 14px); color: #d8c4a0; margin: 0 0 8px; line-height: 1.5; } +.gm-caption .hero-live { position: static; justify-content: center; }