diff --git a/index.html b/index.html index a25b9ff..3cc2ee4 100644 --- a/index.html +++ b/index.html @@ -17,7 +17,7 @@ @media (prefers-reduced-motion: reduce) { .app-loading-gem { animation: none; transform: rotate(45deg); } } - + diff --git a/package.json b/package.json index 818d8a8..e44f391 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amerc-site", - "version": "2.5.0-chatmd", + "version": "3.0.0-painted", "private": true, "type": "module", "scripts": { diff --git a/public/scene2d/painted/bg.webp b/public/scene2d/painted/bg.webp new file mode 100644 index 0000000..dbeb594 Binary files /dev/null and b/public/scene2d/painted/bg.webp differ diff --git a/public/scene2d/painted/dwarf.webp b/public/scene2d/painted/dwarf.webp new file mode 100644 index 0000000..8fc661f Binary files /dev/null and b/public/scene2d/painted/dwarf.webp differ diff --git a/public/scene2d/painted/elf.webp b/public/scene2d/painted/elf.webp new file mode 100644 index 0000000..0200041 Binary files /dev/null and b/public/scene2d/painted/elf.webp differ diff --git a/public/scene2d/painted/orc.webp b/public/scene2d/painted/orc.webp new file mode 100644 index 0000000..17d0b10 Binary files /dev/null and b/public/scene2d/painted/orc.webp differ diff --git a/src/Scene2D.jsx b/src/Scene2D.jsx index 9c4fb27..b4dd919 100644 --- a/src/Scene2D.jsx +++ b/src/Scene2D.jsx @@ -12,7 +12,7 @@ import { api } from './api.js'; const Menu = ({ size = 20 }) => (); const X = ({ size = 20 }) => (); -const RELEASE = '2.5.0-chatmd'; +const RELEASE = '3.0.0-painted'; const NAV = [ { id: 'home', label: 'Tavern' }, @@ -477,6 +477,7 @@ function StatusPage() { } const CHANGELOG = [ + { v: '3.0', date: 'Jun 2026', title: 'A painted tavern', notes: ['The home tavern is now a hand-painted scene — an elf host, an orc bartender and a dwarf patron in a lamplit hall — replacing the old pixel tiles', 'Every painted fixture is still a door: the board opens the roster, the barkeep signs you in, the elf pitches the tour, the side passages lead to your Legion and Mansion'] }, { v: '2.5', date: 'Jun 2026', title: 'Chat speaks markdown', notes: ['Chat messages now render markdown — code blocks, lists, tables and links — so an agent can answer with formatted, clickable replies (it picks its own links, to the netdisk or anywhere)'] }, { v: '2.4', date: 'Jun 2026', title: 'The Quartermaster command post', notes: ['The admin Quartermaster is now amerc’s internal management station — members, agents, companies & products, whiteboards and netdisk all in one panel', 'A command-post dashboard with live counts and quick links to git, docs, the PM workspace and the webagent relay'] }, { v: '2.3', date: 'Jun 2026', title: 'My Mansion gets its villa', notes: ['My Mansion is now a moonlit estate — your villa with glowing windows, and the services (Showcase, File-mapper, Net-disk) are chambers within it'] }, diff --git a/src/TavernGame.jsx b/src/TavernGame.jsx index a65698c..242a962 100644 --- a/src/TavernGame.jsx +++ b/src/TavernGame.jsx @@ -1,232 +1,60 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React from 'react'; -// Full-screen 2D RPG tavern — the tavern IS the interface (diegetic UI): -// the door is Browse Agents, the bar is sign-in, banners are your Legion, -// the side exit leads to My Mansion. 32px LPC tiles + 64px characters on a -// 768x432 stage, CSS-scaled to fill the viewport (pixelated). -const T = 32, W = 768, H = 432; -const L = '/scene2d/lpc/'; -const SRC = { - inside: L + 'inside.png', floors: L + 'castlefloors.png', vic: L + 'victoria.png', barrel: L + 'barrel.png', - princess: L + 'princess.png', soldier: L + 'soldier.png', soldier2: L + 'soldier_altcolor.png', -}; -const WALL = [0, 4], FLOOR = [0, 7]; -const RUG = { tl: [1, 6], t: [2, 6], tr: [3, 6], l: [1, 7], c: [2, 7], r: [3, 7], bl: [1, 8], b: [2, 8], br: [3, 8] }; -const TABLE = [4, 3], CHAIR = [6, 0], CHAIR_R = [7, 0], PLANT = [2, 3], LAMP = [0, 3], VASE = [3, 3], CURTAIN = [8, 0], CURTAIN2 = [9, 0]; - -function tile(ctx, s, c, r, dx, dy, tw = T, th = T) { ctx.drawImage(s, c * T, r * T, tw, th, dx, dy, tw, th); } -function chr(ctx, sheet, frameCol, dx, dy, scale = 1) { ctx.drawImage(sheet, frameCol * 64, 2 * 64, 64, 64, dx, dy, 64 * scale, 64 * scale); } - -// the grand door to the roster, drawn as architecture (diegetic Browse Agents) -function drawDoor(ctx, f) { - const x = 336, w = 96, h = 78; - ctx.fillStyle = '#241a12'; ctx.fillRect(x - 10, 0, w + 20, h + 8); // arch frame - ctx.fillStyle = '#3a2a1a'; ctx.fillRect(x - 6, 0, w + 12, h + 4); - // glowing opening - const pulse = 0.55 + 0.18 * Math.sin(f * 0.04); - const g = ctx.createLinearGradient(0, 0, 0, h); - g.addColorStop(0, `rgba(64, 200, 255, ${pulse})`); g.addColorStop(1, `rgba(20, 60, 120, ${pulse * 0.5})`); - ctx.fillStyle = g; ctx.fillRect(x, 4, w, h - 4); - // door planks ajar - ctx.fillStyle = '#5a3a1c'; ctx.fillRect(x, 4, 16, h - 4); ctx.fillRect(x + w - 16, 4, 16, h - 4); - ctx.fillStyle = '#2a1810'; ctx.fillRect(x + 14, 4, 2, h - 4); ctx.fillRect(x + w - 16, 4, 2, h - 4); - // steps - ctx.fillStyle = '#4a4a55'; ctx.fillRect(x - 4, h + 2, w + 8, 5); - ctx.fillStyle = '#33333d'; ctx.fillRect(x - 8, h + 7, w + 16, 5); - // sparkles drifting out of the door - ctx.fillStyle = `rgba(170, 240, 255, ${pulse})`; - for (let i = 0; i < 7; i++) { const sx = x + 10 + ((i * 37 + f * 0.6) % (w - 20)); const sy = 8 + ((i * 23 + f * 0.9) % (h - 14)); ctx.fillRect(sx | 0, sy | 0, 2, 2); } -} - -// legion banners on the right wall (diegetic My Legion) -function drawBanner(ctx, x, hue, f) { - ctx.fillStyle = '#3a2a1a'; ctx.fillRect(x - 3, 2, 38, 4); // rod - const sway = Math.sin(f * 0.03 + x) * 1.5; - ctx.fillStyle = `hsl(${hue} 55% 38%)`; - ctx.beginPath(); ctx.moveTo(x, 6); ctx.lineTo(x + 32, 6); ctx.lineTo(x + 32, 54); ctx.lineTo(x + 16 + sway, 66); ctx.lineTo(x, 54); ctx.closePath(); ctx.fill(); - ctx.fillStyle = `hsl(${hue} 70% 60%)`; ctx.fillRect(x + 4, 10, 24, 3); - ctx.fillStyle = '#ffd75e'; // emblem - ctx.fillRect(x + 13, 24, 6, 14); ctx.fillRect(x + 9, 28, 14, 5); -} - -// villa exit on the right (diegetic My Mansion) -function drawVillaExit(ctx) { - const x = 712, y = 150; - ctx.fillStyle = '#241a12'; ctx.fillRect(x, y, 56, 86); - ctx.fillStyle = '#102a18'; ctx.fillRect(x + 5, y + 5, 51, 76); - // little villa silhouette through the passage - ctx.fillStyle = '#2c4a2e'; ctx.fillRect(x + 10, y + 42, 40, 34); - ctx.fillStyle = '#d8c79a'; ctx.fillRect(x + 18, y + 50, 26, 26); - ctx.fillStyle = '#7a3326'; ctx.beginPath(); ctx.moveTo(x + 14, y + 52); ctx.lineTo(x + 31, y + 38); ctx.lineTo(x + 48, y + 52); ctx.closePath(); ctx.fill(); - ctx.fillStyle = '#2c2417'; ctx.fillRect(x + 27, y + 62, 8, 14); - ctx.fillStyle = 'rgba(255, 230, 150, 0.8)'; ctx.fillRect(x + 20, y + 56, 5, 5); ctx.fillRect(x + 38, y + 56, 5, 5); -} - -function buildStatic(buf, S) { - const ctx = buf.getContext('2d'); ctx.imageSmoothingEnabled = false; - const cols = W / T, rows = H / T; - for (let y = 2; y < rows; y++) for (let x = 0; x < cols; x++) tile(ctx, S.floors, FLOOR[0], FLOOR[1], x * T, y * T); - for (let y = 0; y < 3; y++) for (let x = 0; x < cols; x++) if (y < 2 || x < 2 || x > cols - 3) tile(ctx, S.inside, WALL[0], WALL[1], x * T, y * T); - // beam trim - ctx.fillStyle = '#6b4326'; ctx.fillRect(0, 2 * T - 5, W, 7); ctx.fillStyle = '#8a5a32'; ctx.fillRect(0, 2 * T - 5, W, 2); ctx.fillStyle = '#2a1810'; ctx.fillRect(0, 2 * T + 2, W, 2); - // windows + curtains - for (const wx of [56, 240, 520]) { - ctx.fillStyle = '#13233f'; ctx.fillRect(wx, 8, 40, 44); ctx.fillStyle = '#cfe0ff'; - for (let i = 0; i < 8; i++) ctx.fillRect(wx + 4 + (i * 11) % 36, 10 + (i * 7) % 40, 2, 2); - ctx.fillStyle = '#5a3a1c'; ctx.fillRect(wx - 3, 6, 46, 3); ctx.fillRect(wx + 18, 8, 3, 44); - tile(ctx, S.vic, CURTAIN[0], CURTAIN[1], wx - 18, 2); tile(ctx, S.vic, CURTAIN2[0], CURTAIN2[1], wx + 28, 2); - } - // grand rug - const rx = 304, ry = 220, rcols = 6, rrows = 4; - for (let j = 0; j < rrows; j++) for (let i = 0; i < rcols; i++) { - const k = i === 0 ? 'l' : i === rcols - 1 ? 'r' : 'c'; const kk = j === 0 ? (k === 'l' ? 'tl' : k === 'r' ? 'tr' : 't') : j === rrows - 1 ? (k === 'l' ? 'bl' : k === 'r' ? 'br' : 'b') : k; - tile(ctx, S.floors, RUG[kk][0], RUG[kk][1], rx + i * T, ry + j * T); - } - // tables + chairs - for (const tx of [150, 620]) { - tile(ctx, S.vic, CHAIR[0], CHAIR[1], tx - 30, 296); tile(ctx, S.vic, CHAIR_R[0], CHAIR_R[1], tx + 18, 296); - tile(ctx, S.vic, TABLE[0], TABLE[1], tx - 16, 308); - } - // props - tile(ctx, S.barrel, 0, 0, 700, 366); tile(ctx, S.barrel, 1, 0, 726, 372); tile(ctx, S.barrel, 2, 0, 708, 392); - tile(ctx, S.vic, PLANT[0], PLANT[1], 10, 366); tile(ctx, S.vic, PLANT[0], PLANT[1], 740, 300); - tile(ctx, S.vic, LAMP[0], LAMP[1], 10, 320); tile(ctx, S.vic, LAMP[0], LAMP[1], 290, 96); - // stats screen on the wall (left of door) - ctx.fillStyle = '#0c2030'; ctx.fillRect(120, 6, 116, 50); ctx.strokeStyle = '#46c8e0'; ctx.lineWidth = 2; ctx.strokeRect(121, 7, 114, 48); ctx.fillStyle = '#16415e'; ctx.fillRect(124, 10, 108, 42); - drawVillaExit(ctx); -} +// Painted tavern hero — a layered illustration (backdrop + character cutouts) +// with the diegetic UI on top: the bar signs you in, the elf host pitches the +// tour from her bubble, the board opens the roster, side hotspots lead to your +// Legion and Mansion. No canvas, no tiles — just the painted scene. +const P = '/scene2d/painted/'; export default function TavernGame({ onAction, stats, auth }) { - const canvasRef = useRef(null); - const statsRef = useRef(stats); - useEffect(() => { statsRef.current = stats; }, [stats]); - const [ready, setReady] = useState(false); - useEffect(() => { - const keys = Object.keys(SRC); const S = {}; let loaded = 0, raf, f = 0, buf, paused = false, io; - const start = () => { - setReady(true); - buf = document.createElement('canvas'); buf.width = W; buf.height = H; buildStatic(buf, S); - const ctx = canvasRef.current.getContext('2d'); ctx.imageSmoothingEnabled = false; - const render = () => { - if (paused) return; - f++; - ctx.drawImage(buf, 0, 0); - drawDoor(ctx, f); - for (const [bx, hue] of [[560, 265], [620, 195], [680, 25]]) drawBanner(ctx, bx, hue, f); - // bartender behind the bar - const bb = Math.round(Math.sin(f * 0.06) * 1); - chr(ctx, S.soldier, 0, 60, 52 + bb); - // bar counter + bottles - ctx.fillStyle = '#5e3a22'; ctx.fillRect(24, 126, 196, 28); ctx.fillStyle = '#8a5a32'; ctx.fillRect(24, 126, 196, 4); ctx.fillStyle = '#c79155'; ctx.fillRect(24, 130, 196, 1); ctx.fillStyle = '#2a1810'; ctx.fillRect(24, 150, 196, 4); - for (let i = 1; i < 7; i++) { ctx.fillStyle = '#3a2417'; ctx.fillRect(24 + i * 28, 132, 1, 18); } - for (const bxp of [48, 104, 162]) { tile(ctx, S.vic, VASE[0], VASE[1], bxp, 108, 20, 20); } - // patrons - chr(ctx, S.soldier, 0, 118, 252 + (Math.sin(f * 0.05) > 0.6 ? -2 : 0)); - chr(ctx, S.soldier2, 0, 588, 252 + (Math.sin(f * 0.05 + 2) > 0.6 ? -2 : 0)); - // greeter by the door - chr(ctx, S.soldier2, 0, 452, 76 + (Math.sin(f * 0.045 + 1) > 0.7 ? -2 : 0)); - // DANCER: cycling walk frames on the rug, spotlight - const dframe = 1 + (Math.floor(f / 7) % 8); - const sway = Math.round(Math.sin(f * 0.12) * 5); - const g = ctx.createRadialGradient(400, 290, 6, 400, 296, 66); g.addColorStop(0, 'rgba(255,244,210,0.26)'); g.addColorStop(1, 'rgba(255,244,210,0)'); ctx.fillStyle = g; ctx.beginPath(); ctx.ellipse(400, 304, 48, 52, 0, 0, 7); ctx.fill(); - chr(ctx, S.princess, dframe, 368 + sway, 244); - // wall screen: live platform stats - const st = statsRef.current; - const items = st ? [['CLASSES', st.classes], ['ONLINE', st.online], ['SESSIONS', st.sessions]] : [['LIVE', '·']]; - const phase = Math.floor(f / 150) % items.length; - const [label, val] = items[phase]; - for (let i = 0; i < 13; i++) { const bh = 3 + ((Math.sin(f * 0.09 + i) + 1) * 4 + (i % 3)) | 0; ctx.fillStyle = 'rgba(70,170,210,0.30)'; ctx.fillRect(128 + i * 8, 50 - bh, 5, bh); } - const pf = (f % 150) / 150, fade = Math.min(1, Math.min(pf, 1 - pf) * 8 + 0.25); - ctx.textAlign = 'center'; - ctx.fillStyle = `rgba(174,242,255,${fade})`; ctx.font = 'bold 18px Georgia, serif'; ctx.fillText(String(val), 178, 33); - ctx.fillStyle = `rgba(95,208,232,${fade})`; ctx.font = '7px Georgia, serif'; ctx.fillText(label, 178, 45); - ctx.textAlign = 'left'; - ctx.fillStyle = 'rgba(127,233,255,0.4)'; ctx.fillRect(124, 10 + (f % 42), 108, 1); - // warm light pools + door glow - ctx.globalCompositeOperation = 'lighter'; - for (const [lx, ly, lr, a] of [[384, 40, 80, 0.15], [56, 40, 30, 0.18], [240, 40, 28, 0.16], [520, 40, 28, 0.16], [26, 330, 34, 0.14], [306, 110, 30, 0.14]]) { - const aa = a * (0.85 + 0.15 * Math.sin(f * 0.1 + lx)); const gg = ctx.createRadialGradient(lx, ly, 0, lx, ly, lr); - gg.addColorStop(0, `rgba(255,180,90,${aa})`); gg.addColorStop(1, 'rgba(255,170,80,0)'); ctx.fillStyle = gg; ctx.beginPath(); ctx.arc(lx, ly, lr, 0, 7); ctx.fill(); - } - const cg = ctx.createRadialGradient(384, 44, 0, 384, 44, 90); cg.addColorStop(0, 'rgba(70,200,235,0.16)'); cg.addColorStop(1, 'rgba(70,200,235,0)'); ctx.fillStyle = cg; ctx.beginPath(); ctx.arc(384, 44, 90, 0, 7); ctx.fill(); - ctx.globalCompositeOperation = 'source-over'; - // vignette + dust motes - const vg = ctx.createRadialGradient(W / 2, H * 0.52, H * 0.4, W / 2, H * 0.5, H * 0.92); vg.addColorStop(0, 'rgba(0,0,0,0)'); vg.addColorStop(1, 'rgba(8,4,10,0.52)'); ctx.fillStyle = vg; ctx.fillRect(0, 0, W, H); - ctx.fillStyle = 'rgba(255,225,170,0.4)'; - for (let i = 0; i < 16; i++) { const dx = (i * 53 + f * 0.16) % W, dy = (i * 67 + Math.sin(f * 0.02 + i) * 8 + 64) % H; ctx.fillRect(dx | 0, dy | 0, 1, 1); } - raf = requestAnimationFrame(render); - }; - render(); - io = new IntersectionObserver(([e]) => { - if (e.isIntersecting) { if (paused) { paused = false; render(); } } - else if (!paused) { paused = true; cancelAnimationFrame(raf); } - }, { threshold: 0 }); - io.observe(canvasRef.current); - }; - keys.forEach((k) => { const im = new Image(); im.onload = () => { if (++loaded === keys.length) start(); }; im.src = SRC[k]; S[k] = im; }); - return () => { cancelAnimationFrame(raf); if (io) io.disconnect(); }; - }, []); - const user = auth?.user; return ( -
- - {!ready &&
loading tavern…
} -
- {/* THE DOOR — browse agents */} -
+
+ The amerc mercenary tavern +