visual overhaul: painterly top-down tavern + RPG login frame + readable Gitea theme

- TopDownTavern rewritten as smooth high-res canvas (warm lighting, hearth, bar,
  bottle shelves, patterned rug, banners, vignette) cohesive with painted NPCs
- RPG wooden building-block login across docs/pm; hero readability backing
- Gitea: forced readable gitea-dark + minimal amerc accent (fixed unreadable theme)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
artheru 2026-06-09 22:31:27 +08:00
parent f85301c33d
commit af7f9addbf
5 changed files with 129 additions and 115 deletions

View File

@ -7,7 +7,7 @@
name="description" name="description"
content="amerc is an agent mercenary layer for embedding, bringing, and hosting agents across vertical software boundaries." content="amerc is an agent mercenary layer for embedding, bringing, and hosting agents across vertical software boundaries."
/> />
<meta name="version" content="0.21.1-pool" /> <meta name="version" content="0.22.1-tavern" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

View File

@ -1,6 +1,6 @@
{ {
"name": "amerc-site", "name": "amerc-site",
"version": "0.21.1-pool", "version": "0.22.1-tavern",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View File

@ -5,7 +5,7 @@ import { AgentBrowse, BoothDashboard } from './AgentPlatform.jsx';
import TopDownTavern from './TopDownTavern.jsx'; import TopDownTavern from './TopDownTavern.jsx';
import { api } from './api.js'; import { api } from './api.js';
const RELEASE = '0.21.0-pool'; const RELEASE = '0.22.1-tavern';
const NAV = [ const NAV = [
{ id: 'home', label: 'Tavern' }, { id: 'home', label: 'Tavern' },

View File

@ -1,114 +1,130 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
// 3/4 top-down RPG tavern, drawn procedurally on a canvas at a fixed virtual size. // Painterly 3/4 top-down RPG tavern smooth high-res canvas, warm lighting,
// Pixel-chunky: rendered at TILE px then scaled up with image-rendering:pixelated. // cohesive with the painted character sprites.
const COLS = 20, ROWS = 13, TILE = 16; // virtual room = 320x208, scaled up const W = 1200, H = 760;
const W = COLS * TILE, H = ROWS * TILE; const rnd = (s) => { let x = Math.sin(s * 127.1) * 43758.5453; return x - Math.floor(x); };
// tile map legend: . floor # wall = bar T table o stool b barrel r rug f fireplace s shelf c crate p plant w window function roundRect(ctx, x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); }
const MAP = [ function ellipse(ctx, x, y, rx, ry, c) { ctx.fillStyle = c; ctx.beginPath(); ctx.ellipse(x, y, rx, ry, 0, 0, 7); ctx.fill(); }
'####################',
'#wwww##ffff##wwww##s#',
'#..................s#',
'#..bb....rr....cc...#',
'#..bb..rrrrrr..cc...#',
'#......rrrrrr.......#',
'#..T...rrrrrr...T...#',
'#.ooo..........ooo..#',
'#..................##',
'#===================#',
'#...=...=...=...=...##',
'#..................##',
'####################',
];
const PAL = { function buildScene(buf) {
floorA: '#6b4a2b', floorB: '#5e4126', grain: '#4f361f', const ctx = buf.getContext('2d');
wall: '#3a2a3f', wallTop: '#52405a', mortar: '#241a28', // ---- floor: warm wood with radial light + planks ----
rug: '#7a1f2b', rugEdge: '#d8a843', rugMid: '#9c2a38', const fg = ctx.createLinearGradient(0, 180, 0, H);
bar: '#7a4a24', barTop: '#a06a36', barEdge: '#3c220f', fg.addColorStop(0, '#5a3a20'); fg.addColorStop(0.5, '#6e4727'); fg.addColorStop(1, '#4a2e18');
wood: '#8a5a2e', woodHi: '#a87a44', woodLo: '#4a2e16', ctx.fillStyle = fg; ctx.fillRect(0, 180, W, H - 180);
barrel: '#6b4524', barrelBand: '#caa54a', barrelTop: '#a8732f', for (let y = 200; y < H; y += 46) { // plank seams (perspective: wider toward bottom)
stone: '#4a4452', stoneHi: '#6a6276', ctx.strokeStyle = 'rgba(40,24,12,0.55)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
fire: '#ff9f38', fireHot: '#ffd66b', glass: '#2aa9d8', ctx.strokeStyle = 'rgba(150,100,60,0.10)'; ctx.beginPath(); ctx.moveTo(0, y + 2); ctx.lineTo(W, y + 2); ctx.stroke();
}; for (let x = 0; x < W; x += 150) { ctx.strokeStyle = 'rgba(40,24,12,0.4)'; ctx.beginPath(); ctx.moveTo(x + rnd(x + y) * 60, y); ctx.lineTo(x + rnd(x + y) * 60, y + 46); ctx.stroke(); }
}
function px(ctx, x, y, w, h, c) { ctx.fillStyle = c; ctx.fillRect(x, y, w, h); } // ---- rug (center, patterned) ----
const cx = W / 2, ry = 470;
function drawFloor(ctx, x, y) { for (const [rw, rh, col] of [[420, 220, '#5a1a24'], [400, 205, '#7a2330'], [300, 150, '#5a1a24']]) { ctx.fillStyle = col; ctx.beginPath(); ctx.ellipse(cx, ry, rw / 2, rh / 2, 0, 0, 7); ctx.fill(); }
const a = ((x / TILE) + (y / TILE)) % 2 === 0; ctx.strokeStyle = '#d6a846'; ctx.lineWidth = 4; ctx.beginPath(); ctx.ellipse(cx, ry, 195, 98, 0, 0, 7); ctx.stroke();
px(ctx, x, y, TILE, TILE, a ? PAL.floorA : PAL.floorB); ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(cx, ry - 70); ctx.lineTo(cx + 150, ry); ctx.lineTo(cx, ry + 70); ctx.lineTo(cx - 150, ry); ctx.closePath(); ctx.stroke();
ctx.fillStyle = PAL.grain; // ---- back wall (top) stone with gradient + rail ----
ctx.fillRect(x, y + 5, TILE, 1); ctx.fillRect(x, y + 11, TILE, 1); const wg = ctx.createLinearGradient(0, 0, 0, 200);
ctx.fillRect(x + (a ? 4 : 11), y, 1, TILE); wg.addColorStop(0, '#241c30'); wg.addColorStop(1, '#3a2f48');
} ctx.fillStyle = wg; ctx.fillRect(0, 0, W, 190);
function drawWall(ctx, x, y) { for (let y = 0; y < 180; y += 36) for (let x = (Math.floor(y / 36) % 2 ? 0 : -40); x < W; x += 80) { ctx.strokeStyle = 'rgba(18,12,26,0.7)'; ctx.lineWidth = 2; ctx.strokeRect(x, y, 80, 36); ctx.strokeStyle = 'rgba(120,108,140,0.15)'; ctx.beginPath(); ctx.moveTo(x + 2, y + 2); ctx.lineTo(x + 78, y + 2); ctx.stroke(); }
px(ctx, x, y, TILE, TILE, PAL.wall); // wainscot rail
px(ctx, x, y, TILE, 4, PAL.wallTop); // 3/4 top cap const rg = ctx.createLinearGradient(0, 175, 0, 200); rg.addColorStop(0, '#8a5a2e'); rg.addColorStop(1, '#5a3a1c');
ctx.fillStyle = PAL.mortar; ctx.fillStyle = rg; ctx.fillRect(0, 175, W, 22); ctx.fillStyle = 'rgba(180,130,80,0.5)'; ctx.fillRect(0, 175, W, 3);
ctx.fillRect(x, y + 9, TILE, 1); ctx.fillStyle = 'rgba(0,0,0,0.35)'; ctx.fillRect(0, 197, W, 8); // shadow below rail
ctx.fillRect(x + ((y / TILE) % 2 ? 8 : 0), y + 4, 1, 5); // windows (night sky)
ctx.fillRect(x + ((y / TILE) % 2 ? 0 : 8), y + 9, 1, 7); for (const wx of [150, W - 230]) {
} ctx.fillStyle = '#5a3a1c'; roundRect(ctx, wx - 6, 30, 92, 110, 8); ctx.fill();
function drawRug(ctx, x, y, edge, midOnly) { const sg = ctx.createLinearGradient(0, 36, 0, 132); sg.addColorStop(0, '#16243f'); sg.addColorStop(1, '#0c1428');
px(ctx, x, y, TILE, TILE, PAL.rug); ctx.fillStyle = sg; ctx.fillRect(wx, 36, 80, 98);
if (midOnly) { px(ctx, x + 3, y + 3, TILE - 6, TILE - 6, PAL.rugMid); } ctx.fillStyle = '#cfe0ff'; for (let i = 0; i < 9; i++) ctx.fillRect(wx + rnd(i + wx) * 76, 38 + rnd(i * 3) * 92, 2, 2);
ctx.fillStyle = PAL.rugEdge; ctx.fillStyle = '#5a3a1c'; ctx.fillRect(wx + 38, 36, 4, 98); ctx.fillRect(wx, 82, 80, 4);
if (edge.t) ctx.fillRect(x, y, TILE, 2); }
if (edge.b) ctx.fillRect(x, y + TILE - 2, TILE, 2); // hearth (center back)
if (edge.l) ctx.fillRect(x, y, 2, TILE); const hx = cx - 110;
if (edge.r) ctx.fillRect(x + TILE - 2, y, 2, TILE); ctx.fillStyle = '#2a2333'; roundRect(ctx, hx, 8, 220, 188, 10); ctx.fill();
} ctx.fillStyle = '#1a1422'; roundRect(ctx, hx + 24, 50, 172, 130, 8); ctx.fill();
ctx.fillStyle = '#120a08'; roundRect(ctx, hx + 36, 66, 148, 110, 6); ctx.fill(); // fire cavity (fire animated)
function drawTile(ctx, ch, x, y, neigh) { const mg = ctx.createLinearGradient(0, 188, 0, 210); mg.addColorStop(0, '#9a6a36'); mg.addColorStop(1, '#5a3a1c');
switch (ch) { ctx.fillStyle = mg; ctx.fillRect(hx - 14, 188, 248, 18);
case '#': drawWall(ctx, x, y); break; // back-bar shelf with bottles (under rail, sides of hearth)
case 'w': drawWall(ctx, x, y); px(ctx, x + 3, y + 5, TILE - 6, TILE - 8, '#0a1830'); px(ctx, x + 4, y + 6, TILE - 8, TILE - 10, PAL.glass); px(ctx, x + 7, y + 6, 1, TILE - 10, '#0a1830'); break; for (const sx of [40, hx + 250]) {
case 's': drawWall(ctx, x, y); px(ctx, x + 2, y + 4, TILE - 4, 2, PAL.wood); px(ctx, x + 2, y + 8, TILE - 4, 2, PAL.wood); px(ctx, x + 2, y + 12, TILE - 4, 2, PAL.wood); px(ctx, x + 4, y + 1, 2, 3, PAL.woodHi); px(ctx, x + 9, y + 5, 2, 3, PAL.fire); break; ctx.fillStyle = '#4a3018'; ctx.fillRect(sx, 150, 230, 30);
case 'f': drawWall(ctx, x, y); px(ctx, x + 2, y + 6, TILE - 4, TILE - 6, '#1a1014'); break; ctx.fillStyle = 'rgba(0,0,0,0.4)'; ctx.fillRect(sx, 176, 230, 5);
default: drawFloor(ctx, x, y); const cols = ['#4a9a64', '#b46a52', '#6a82c8', '#caa54a', '#8a5ab0'];
if (ch === 'r') { for (let i = 0; i < 11; i++) { const bx = sx + 8 + i * 20, bc = cols[i % 5]; ctx.fillStyle = bc; roundRect(ctx, bx, 124, 9, 30, 3); ctx.fill(); ctx.fillStyle = 'rgba(255,255,255,0.25)'; ctx.fillRect(bx + 1, 126, 2, 22); }
const e = { t: neigh.t !== 'r', b: neigh.b !== 'r', l: neigh.l !== 'r', r: neigh.r !== 'r' }; }
drawRug(ctx, x, y, e, true); // ---- bar counter (front of bartender) ----
} const barY = 360, barW = 520, barX = cx - barW / 2;
if (ch === '=') { // bar counter ctx.fillStyle = 'rgba(0,0,0,0.4)'; ctx.beginPath(); ctx.ellipse(cx, barY + 70, barW / 2 + 10, 30, 0, 0, 7); ctx.fill();
px(ctx, x, y + 3, TILE, TILE - 3, PAL.bar); px(ctx, x, y, TILE, 4, PAL.barTop); px(ctx, x, y + 3, TILE, 1, PAL.barEdge); px(ctx, x, y + TILE - 1, TILE, 1, PAL.barEdge); const bg = ctx.createLinearGradient(0, barY, 0, barY + 64); bg.addColorStop(0, '#a06a38'); bg.addColorStop(0.18, '#7a4a24'); bg.addColorStop(1, '#5a3418');
} ctx.fillStyle = bg; roundRect(ctx, barX, barY, barW, 64, 10); ctx.fill();
if (ch === 'T') { px(ctx, x + 1, y + 2, TILE - 2, TILE - 4, PAL.woodLo); px(ctx, x + 1, y + 1, TILE - 2, 4, PAL.wood); px(ctx, x + 3, y + 2, TILE - 6, 1, PAL.woodHi); } ctx.fillStyle = 'rgba(200,150,90,0.6)'; roundRect(ctx, barX + 6, barY + 4, barW - 12, 8, 4); ctx.fill(); // top sheen
if (ch === 'o') { px(ctx, x + 5, y + 6, 6, 6, PAL.woodLo); px(ctx, x + 5, y + 5, 6, 2, PAL.wood); } for (let i = 1; i < 7; i++) { ctx.strokeStyle = 'rgba(40,24,12,0.5)'; ctx.beginPath(); ctx.moveTo(barX + i * barW / 7, barY + 16); ctx.lineTo(barX + i * barW / 7, barY + 60); ctx.stroke(); }
if (ch === 'b') { px(ctx, x + 3, y + 2, TILE - 6, TILE - 3, PAL.barrel); px(ctx, x + 3, y + 1, TILE - 6, 3, PAL.barrelTop); px(ctx, x + 3, y + 6, TILE - 6, 1, PAL.barrelBand); px(ctx, x + 3, y + 11, TILE - 6, 1, PAL.barrelBand); } for (const mx of [barX + 70, barX + barW - 90]) { ctx.fillStyle = '#caa54a'; roundRect(ctx, mx, barY + 14, 22, 26, 4); ctx.fill(); ctx.fillStyle = '#e8dca0'; roundRect(ctx, mx + 2, barY + 10, 18, 8, 3); ctx.fill(); }
if (ch === 'c') { px(ctx, x + 2, y + 3, TILE - 4, TILE - 4, PAL.wood); px(ctx, x + 2, y + 2, TILE - 4, 2, PAL.woodHi); ctx.fillStyle = PAL.woodLo; ctx.fillRect(x + 2, y + 8, TILE - 4, 1); ctx.fillRect(x + 8, y + 3, 1, TILE - 5); } // ---- tables + stools (sides) ----
for (const [tx, ty] of [[230, 600], [W - 230, 600]]) {
ctx.fillStyle = 'rgba(0,0,0,0.4)'; ellipse(ctx, tx, ty + 22, 70, 24, 'rgba(0,0,0,0.4)');
ellipse(ctx, tx, ty, 64, 40, '#5a3418'); ellipse(ctx, tx, ty - 4, 60, 36, '#7a4a24'); ellipse(ctx, tx - 12, ty - 10, 30, 16, '#9a6a3a');
ctx.fillStyle = '#e8dca0'; ctx.fillRect(tx - 4, ty - 14, 8, 14); // candle
}
// ---- barrels (left) + crates (right) ----
for (const [bx, by] of [[70, 580], [120, 612], [92, 660]]) {
ellipse(ctx, bx + 22, by + 50, 26, 8, 'rgba(0,0,0,0.4)');
const bgr = ctx.createLinearGradient(bx, 0, bx + 44, 0); bgr.addColorStop(0, '#6a4422'); bgr.addColorStop(0.5, '#8a5a30'); bgr.addColorStop(1, '#5a3618');
ctx.fillStyle = bgr; roundRect(ctx, bx, by, 44, 54, 8); ctx.fill();
ctx.fillStyle = '#c8a040'; ctx.fillRect(bx, by + 10, 44, 5); ctx.fillRect(bx, by + 38, 44, 5);
ellipse(ctx, bx + 22, by + 4, 22, 7, '#9a6a3a');
}
for (const [cx2, cy2] of [[W - 130, 600], [W - 78, 632]]) {
ellipse(ctx, cx2 + 26, cy2 + 52, 30, 8, 'rgba(0,0,0,0.4)');
ctx.fillStyle = '#7a4a24'; roundRect(ctx, cx2, cy2, 52, 50, 4); ctx.fill();
ctx.fillStyle = 'rgba(180,130,80,0.5)'; ctx.fillRect(cx2 + 3, cy2 + 3, 46, 4);
ctx.strokeStyle = '#5a3418'; ctx.lineWidth = 3; ctx.strokeRect(cx2 + 3, cy2 + 3, 46, 44); ctx.beginPath(); ctx.moveTo(cx2 + 4, cy2 + 4); ctx.lineTo(cx2 + 48, cy2 + 46); ctx.stroke();
}
// banners
for (const [bx, col] of [[60, '#3cc8f0'], [W - 96, '#d6a846']]) {
ctx.fillStyle = col; ctx.globalAlpha = 0.9; ctx.beginPath(); ctx.moveTo(bx, 6); ctx.lineTo(bx + 36, 6); ctx.lineTo(bx + 36, 110); ctx.lineTo(bx + 18, 124); ctx.lineTo(bx, 110); ctx.closePath(); ctx.fill(); ctx.globalAlpha = 1;
ctx.fillStyle = 'rgba(0,0,0,0.25)'; ctx.fillRect(bx + 24, 6, 12, 104);
ctx.fillStyle = '#d6a846'; roundRect(ctx, bx + 12, 48, 12, 12, 2); ctx.fill();
} }
} }
export default function TopDownTavern({ stage = {}, onHotspot }) { const LIGHTS = [[W / 2, 90, 150, 0.55, '255,170,90'], [W / 2, 360, 220, 0.22, '255,180,100'], [150, 60, 80, 0.3, '255,200,120'], [W - 190, 60, 80, 0.3, '255,200,120'], [230, 586, 70, 0.32, '255,190,110'], [W - 230, 586, 70, 0.32, '255,190,110']];
export default function TopDownTavern({ onHotspot }) {
const canvasRef = useRef(null); const canvasRef = useRef(null);
const [t, setT] = useState(0);
useEffect(() => { useEffect(() => {
const buf = document.createElement('canvas'); buf.width = W; buf.height = H; buildScene(buf);
const ctx = canvasRef.current.getContext('2d'); const ctx = canvasRef.current.getContext('2d');
ctx.imageSmoothingEnabled = false; let raf, f = 0;
let raf, frame = 0;
const at = (c, r) => (MAP[r] && MAP[r][c]) || '.';
const render = () => { const render = () => {
frame++; f++;
// base map ctx.drawImage(buf, 0, 0);
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) { // hearth fire
const ch = at(c, r); const hx = W / 2 - 74;
drawTile(ctx, ch, c * TILE, r * TILE, { t: at(c, r - 1), b: at(c, r + 1), l: at(c - 1, r), r: at(c + 1, r) }); for (let i = 0; i < 6; i++) {
const fl = 0.55 + 0.45 * Math.sin(f * 0.16 + i * 1.3), bx = hx + 8 + i * 22;
const fgr = ctx.createLinearGradient(0, 176, 0, 100); fgr.addColorStop(0, '#ff8a2a'); fgr.addColorStop(1, 'rgba(255,210,90,0)');
ctx.fillStyle = fgr; ctx.beginPath(); ctx.moveTo(bx, 178); ctx.quadraticCurveTo(bx + 8, 150 - fl * 30, bx + 11, 120 - fl * 26); ctx.quadraticCurveTo(bx + 14, 150 - fl * 30, bx + 22, 178); ctx.fill();
} }
// fireplace fire (animated) under the 'f' wall tiles row1 cols6-9 // warm lighting
const flick = 0.6 + 0.4 * Math.sin(frame * 0.2); ctx.globalCompositeOperation = 'lighter';
for (let c = 6; c <= 9; c++) { for (const [lx, ly, lr, la, rgb] of LIGHTS) {
const x = c * TILE, y = 1 * TILE; const a = la * (0.85 + 0.15 * Math.sin(f * 0.09 + lx));
px(ctx, x + 3, y + 8 - flick * 3, TILE - 6, 6 + flick * 3, PAL.fire); const g = ctx.createRadialGradient(lx, ly, 0, lx, ly, lr); g.addColorStop(0, `rgba(${rgb},${a})`); g.addColorStop(1, `rgba(${rgb},0)`);
px(ctx, x + 5, y + 10 - flick * 2, TILE - 10, 4, PAL.fireHot); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(lx, ly, lr, 0, 7); ctx.fill();
} }
// wall lantern glow (animated) near windows ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = 0.10 + 0.05 * Math.sin(frame * 0.15); // vignette
[[3, 2], [16, 2], [10, 1]].forEach(([c, r]) => { ctx.fillStyle = PAL.fire; ctx.beginPath(); ctx.arc(c * TILE + 8, r * TILE + 10, 14, 0, 7); ctx.fill(); }); const vg = ctx.createRadialGradient(W / 2, H * 0.5, H * 0.28, W / 2, H * 0.5, H * 0.82);
ctx.globalAlpha = 1; vg.addColorStop(0, 'rgba(0,0,0,0)'); vg.addColorStop(1, 'rgba(8,4,12,0.7)');
ctx.fillStyle = vg; ctx.fillRect(0, 0, W, H);
// dust
ctx.fillStyle = 'rgba(255,220,160,0.45)';
for (let i = 0; i < 22; i++) { const dx = (i * 67 + f * 0.35) % W, dy = (i * 53 + Math.sin(f * 0.018 + i) * 24 + 220) % H; ctx.fillRect(dx, dy, 2, 2); }
raf = requestAnimationFrame(render); raf = requestAnimationFrame(render);
if (frame % 30 === 0) setT(frame);
}; };
render(); render();
return () => cancelAnimationFrame(raf); return () => cancelAnimationFrame(raf);
@ -118,16 +134,13 @@ export default function TopDownTavern({ stage = {}, onHotspot }) {
<div className="td-room"> <div className="td-room">
<canvas ref={canvasRef} width={W} height={H} className="td-canvas" /> <canvas ref={canvasRef} width={W} height={H} className="td-canvas" />
<div className="td-overlay"> <div className="td-overlay">
{/* characters (sprites as NPCs over the floor) */} <Npc sprite="/scene2d/orc.png" x={50} y={20} h={36} label="Grommash · Bartender" say="Pick a mercenary, friend." onClick={() => onHotspot?.('agents')} />
<Npc sprite="/scene2d/orc.png" x={50} y={56} h={20} label="Grommash · Bartender" say="Pick a mercenary, friend." onClick={() => onHotspot?.('agents')} /> <Npc sprite="/scene2d/dwarf.png" x={20} y={56} h={30} label="Durn · Patron" say="My booth runs three agents." onClick={() => onHotspot?.('booth')} />
<Npc sprite="/scene2d/elf.png" x={73} y={62} h={19} flip label="Aelis · Guide" say="Browse the roster, or open your booth." onClick={() => onHotspot?.('agents')} /> <Npc sprite="/scene2d/elf.png" x={80} y={52} h={33} flip label="Aelis · Guide" say="Browse the roster, or open your booth." onClick={() => onHotspot?.('agents')} />
<Npc sprite="/scene2d/dwarf.png" x={24} y={66} h={17} label="Durn · Patron" say="My booth runs three agents." onClick={() => onHotspot?.('booth')} /> <button className="td-hot td-hot-board" style={{ left: '70%', top: '40%' }} onClick={() => onHotspot?.('agents')}>
{/* hotspots */} <span className="td-hot-tag">NOTICE BOARD</span><strong>Browse Agents </strong>
<button className="td-hot td-hot-board" style={{ left: '72%', top: '20%' }} onClick={() => onHotspot?.('agents')}>
<span className="td-hot-tag">NOTICE BOARD</span>
<strong>Browse Agents </strong>
</button> </button>
<button className="td-hot td-hot-door" style={{ left: '50%', top: '74%' }} onClick={() => onHotspot?.('booth')}>Enter My Booth </button> <button className="td-hot td-hot-door" style={{ left: '50%', top: '90%' }} onClick={() => onHotspot?.('booth')}>Enter My Booth </button>
</div> </div>
</div> </div>
); );

View File

@ -922,14 +922,14 @@ button {
/* ===================================================================== */ /* ===================================================================== */
.td-app { background: #0a0710; } .td-app { background: #0a0710; }
.td-home { position: relative; flex: 1; display: flex; align-items: center; justify-content: center; padding: 18px; overflow: hidden; } .td-home { position: relative; flex: 1; display: flex; align-items: center; justify-content: center; padding: 18px; overflow: hidden; }
.td-stage { position: relative; aspect-ratio: 320 / 208; width: min(96vw, calc((100vh - 150px) * 1.538)); box-shadow: 0 16px 50px rgba(0,0,0,0.6), inset 0 0 0 4px #1a0f08; } .td-stage { position: relative; aspect-ratio: 1200 / 760; width: min(96vw, calc((100vh - 150px) * 1.579)); border-radius: 8px; overflow: hidden; box-shadow: 0 18px 60px rgba(0,0,0,0.7), inset 0 0 0 4px #1a0f08, inset 0 0 0 6px #3a2410; }
.td-room { position: absolute; inset: 0; } .td-room { position: absolute; inset: 0; }
.td-canvas { width: 100%; height: 100%; display: block; image-rendering: pixelated; } .td-canvas { width: 100%; height: 100%; display: block; image-rendering: auto; }
.td-overlay { position: absolute; inset: 0; } .td-overlay { position: absolute; inset: 0; }
.td-npc { position: absolute; transform: translateX(-50%); bottom: auto; background: none; cursor: pointer; padding: 0; filter: drop-shadow(2px 3px 0 rgba(0,0,0,0.5)); } .td-npc { position: absolute; transform: translateX(-50%); bottom: auto; background: none; cursor: pointer; padding: 0; filter: drop-shadow(0 6px 6px rgba(0,0,0,0.55)); }
.td-npc img { height: 100%; image-rendering: pixelated; display: block; animation: td-bob 3.6s ease-in-out infinite; } .td-npc img { height: 100%; display: block; animation: td-bob 4s ease-in-out infinite; }
.td-npc:hover { filter: drop-shadow(0 0 8px rgba(255,200,120,0.7)) drop-shadow(2px 3px 0 rgba(0,0,0,0.5)); z-index: 5; } .td-npc:hover { filter: drop-shadow(0 0 12px rgba(255,200,120,0.85)) drop-shadow(0 6px 6px rgba(0,0,0,0.55)); z-index: 5; }
@keyframes td-bob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-4%)} } @keyframes td-bob { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-4%)} }
.td-npc-shadow { position: absolute; left: 50%; bottom: -4px; width: 60%; height: 8%; transform: translateX(-50%); background: radial-gradient(ellipse, rgba(0,0,0,0.5), transparent 70%); } .td-npc-shadow { position: absolute; left: 50%; bottom: -4px; width: 60%; height: 8%; transform: translateX(-50%); background: radial-gradient(ellipse, rgba(0,0,0,0.5), transparent 70%); }
.td-say { position: absolute; bottom: 102%; left: 50%; transform: translateX(-50%); width: max-content; max-width: 180px; padding: 6px 8px; font-size: 8px; line-height: 1.6; color: #1c1206; background: var(--cream); border: 2px solid #000; box-shadow: 2px 2px 0 #000; text-align: center; z-index: 6; } .td-say { position: absolute; bottom: 102%; left: 50%; transform: translateX(-50%); width: max-content; max-width: 180px; padding: 6px 8px; font-size: 8px; line-height: 1.6; color: #1c1206; background: var(--cream); border: 2px solid #000; box-shadow: 2px 2px 0 #000; text-align: center; z-index: 6; }
@ -942,7 +942,8 @@ button {
.td-hot-door { padding: 8px 14px; font-size: 10px; color: #36f0b0; background: rgba(8,24,18,0.82); border: 2px solid #36f0b0; box-shadow: 0 0 16px rgba(54,240,176,0.35), 3px 3px 0 #000; } .td-hot-door { padding: 8px 14px; font-size: 10px; color: #36f0b0; background: rgba(8,24,18,0.82); border: 2px solid #36f0b0; box-shadow: 0 0 16px rgba(54,240,176,0.35), 3px 3px 0 #000; }
.td-hot-door:hover { background: rgba(12,34,26,0.92); } .td-hot-door:hover { background: rgba(12,34,26,0.92); }
.td-hero { position: absolute; left: 3%; top: 8%; width: min(42%, 360px); z-index: 4; font-family: "Press Start 2P", monospace; } .td-hero { position: absolute; left: 2.5%; top: 6%; width: min(40%, 380px); z-index: 4; font-family: "Press Start 2P", monospace;
padding: 16px 18px; background: linear-gradient(180deg, rgba(12,6,18,0.72), rgba(12,6,18,0.45)); border-radius: 10px; backdrop-filter: blur(2px); }
.td-hero h1 { font-size: clamp(13px, 2.1vw, 22px); line-height: 1.5; color: #fff; text-shadow: 2px 2px 0 #000, 0 0 18px rgba(0,0,0,0.8); margin: 0 0 10px; } .td-hero h1 { font-size: clamp(13px, 2.1vw, 22px); line-height: 1.5; color: #fff; text-shadow: 2px 2px 0 #000, 0 0 18px rgba(0,0,0,0.8); margin: 0 0 10px; }
.td-hero p { font-family: Inter, sans-serif; font-size: clamp(10px, 1.3vw, 13px); line-height: 1.7; color: #e7d4b6; text-shadow: 1px 1px 2px #000; margin: 0 0 12px; } .td-hero p { font-family: Inter, sans-serif; font-size: clamp(10px, 1.3vw, 13px); line-height: 1.7; color: #e7d4b6; text-shadow: 1px 1px 2px #000; margin: 0 0 12px; }
.td-hero-cta { display: flex; gap: 8px; flex-wrap: wrap; } .td-hero-cta { display: flex; gap: 8px; flex-wrap: wrap; }