-
-
-
Hire agents like mercenaries.
-
Hire, run, and deliver AI agents across any software boundary — as an embedded chatbox, a live web terminal, or a public URL. No infrastructure, just skills.
-
- setRoute('agents')}>Browse Agents →
- setRoute('legion')}>Publish yours
-
- {stats && (
-
- classes
- online
- sessions
-
- )}
-
document.querySelector('.hf')?.scrollIntoView({ behavior: 'smooth' })}>how it works ↓
-
+
+
e.stopPropagation()} role="dialog" aria-label={title}>
+
+
+
+ {tag && {tag} }
+ {title}
+ ✕
+
+
{children}
-
-
-
homeRef.current?.scrollTo({ top: 0, behavior: 'smooth' })} aria-label="Back to top" type="button">↑
+
+ );
+}
+
+function Home({ setRoute, auth }) {
+ const [stats, setStats] = useState(null);
+ const [dlg, setDlg] = useState(null); // 'tour' | 'login' | null
+ useEffect(() => { api('/agents/stats').then(setStats).catch(() => {}); const t = setInterval(() => api('/agents/stats').then(setStats).catch(() => {}), 30000); return () => clearInterval(t); }, []);
+ const act = useCallback((a) => {
+ if (a === 'tour' || a === 'login') setDlg(a);
+ else setRoute(a);
+ }, [setRoute]);
+ return (
+
+
+
+
Hire agents like mercenaries_
+
every fixture in the tavern is a door — walk in, or read the setDlg('tour')}>tour
+ {stats && (
+
+ classes
+ online
+ sessions
+
+ )}
+
+ {dlg === 'tour' && (
+
setDlg(null)}>
+ { setDlg(null); setRoute(r); }} />
+
+ )}
+ {dlg === 'login' && (
+
setDlg(null)}>
+ One account opens every door — tavern, docs, PM and git.
+ { setDlg(null); setRoute('legion'); }} />
+
+ )}
);
}
@@ -477,6 +498,7 @@ function StatusPage() {
}
const CHANGELOG = [
+ { v: '2.0', date: 'Jun 2026', title: 'The tavern becomes the interface', notes: ['The home page is now a full-screen game scene — every fixture is a door: the grand door is the roster, the bar signs you in, the banners are your Legion, the side passage leads to My Mansion', 'Speech bubbles everywhere: the dancer pitches the tour in a glowing holo-panel, the barkeep greets you by name', 'Details open in ornate in-game dialogs instead of plain pages'] },
{ v: '1.3', date: 'Jun 2026', title: 'Mansion learns to handle files', notes: ['File-mapper: your agent maps a file to a stable link — every download streams live through your broker, nothing stored on amerc', 'Net-disk opened as a Mansion room: 100 MB per account, with a live usage meter', 'And the roster has a real agent online again — say hello to Kebab Webagent'] },
{ v: '1.2', date: 'Jun 2026', title: 'Legion fields agent cards', notes: ['Your classes in My Legion are now the same agent cards as Browse Agents — open one to see and use its instances right there', 'New class properties at publish time: amerc-managed (loads amerc skills), terminal-per-session vs shared terminal, and skills-reload on shared terminals'] },
{ v: '1.1', date: 'Jun 2026', title: 'Chat, rebuilt native', notes: ['Agent chat now runs on amerc itself — no external relay, so it works whenever the site is up', 'Send files and images both ways: relayed live with a progress bar, never stored on the server', 'The chat shows which skills the agent has loaded', 'Every session keeps its chatlog — reopen any conversation from My Legion'] },
diff --git a/src/TavernGame.jsx b/src/TavernGame.jsx
index d1dc16c..a65698c 100644
--- a/src/TavernGame.jsx
+++ b/src/TavernGame.jsx
@@ -1,57 +1,102 @@
import React, { useEffect, useRef, useState } from 'react';
-// High-res 2D RPG tavern — 32px LPC "Base Assets" tiles (CC-BY-SA) + animated 64px characters.
-const T = 32, W = 512, H = 352;
+// 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',
};
-// tile picks [col,row] (32px)
-const WALL = [0, 4], FLOOR = [0, 7], DOOR = [[8, 1], [9, 1]];
+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); }
-// LPC character: down-facing row=2 (64px frames), frameCol 0=idle, 1..8 walk
-function chr(ctx, sheet, frameCol, dx, dy) { ctx.drawImage(sheet, frameCol * 64, 2 * 64, 64, 64, dx, dy, 64, 64); }
+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;
- // floor
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);
- // brick wall (rows 0-1)
- for (let y = 0; y < 2; y++) for (let x = 0; x < cols; x++) tile(ctx, S.inside, WALL[0], WALL[1], x * T, y * T);
- // wood beam trim at wall/floor seam
+ 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 on the wall
- for (const wx of [40, 432]) {
+ // 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);
}
- // red rug (9-slice), center
- const rx = 176, ry = 160, rcols = 5, rrows = 4;
+ // 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);
}
- // round tables + chairs (sides)
- for (const tx of [96, 400]) {
- tile(ctx, S.vic, CHAIR[0], CHAIR[1], tx - 30, 232); tile(ctx, S.vic, CHAIR_R[0], CHAIR_R[1], tx + 18, 232);
- tile(ctx, S.vic, TABLE[0], TABLE[1], tx - 16, 244);
+ // 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);
}
- // barrels (corner) + plants + lamps
- tile(ctx, S.barrel, 0, 0, 444, 286); tile(ctx, S.barrel, 1, 0, 470, 292); tile(ctx, S.barrel, 2, 0, 452, 312);
- tile(ctx, S.vic, PLANT[0], PLANT[1], 8, 286); tile(ctx, S.vic, PLANT[0], PLANT[1], 484, 252);
- tile(ctx, S.vic, LAMP[0], LAMP[1], 8, 250);
- // screen frame (cyan) on the wall
- ctx.fillStyle = '#0c2030'; ctx.fillRect(198, 6, 116, 50); ctx.strokeStyle = '#46c8e0'; ctx.lineWidth = 2; ctx.strokeRect(199, 7, 114, 48); ctx.fillStyle = '#16415e'; ctx.fillRect(202, 10, 108, 42);
+ // 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);
}
-export default function TavernGame({ onHotspot, stats }) {
+export default function TavernGame({ onAction, stats, auth }) {
const canvasRef = useRef(null);
const statsRef = useRef(stats);
useEffect(() => { statsRef.current = stats; }, [stats]);
@@ -66,49 +111,52 @@ export default function TavernGame({ onHotspot, stats }) {
if (paused) return;
f++;
ctx.drawImage(buf, 0, 0);
- // bartender (a clothed guard tending bar) behind the bar (idle bob)
+ 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, 56, 18 + bb);
- // bar counter (drawn over bartender's lower half) + bottles
- ctx.fillStyle = '#5e3a22'; ctx.fillRect(24, 92, 168, 26); ctx.fillStyle = '#8a5a32'; ctx.fillRect(24, 92, 168, 4); ctx.fillStyle = '#c79155'; ctx.fillRect(24, 96, 168, 1); ctx.fillStyle = '#2a1810'; ctx.fillRect(24, 116, 168, 4);
- for (let i = 1; i < 6; i++) { ctx.fillStyle = '#3a2417'; ctx.fillRect(24 + i * 28, 98, 1, 18); }
- for (const bxp of [44, 96, 150]) { tile(ctx, S.vic, VASE[0], VASE[1], bxp, 76, 20, 20); }
- // patrons at the side tables (idle bob)
- chr(ctx, S.soldier, 0, 64, 188 + (Math.sin(f * 0.05) > 0.6 ? -2 : 0));
- chr(ctx, S.soldier2, 0, 368, 188 + (Math.sin(f * 0.05 + 2) > 0.6 ? -2 : 0));
- // DANCER (舞女): princess cycling walk frames = dancing, on the rug
+ 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) * 4);
- // spotlight
- const g = ctx.createRadialGradient(256, 230, 6, 256, 236, 56); g.addColorStop(0, 'rgba(255,244,210,0.26)'); g.addColorStop(1, 'rgba(255,244,210,0)'); ctx.fillStyle = g; ctx.beginPath(); ctx.ellipse(256, 244, 40, 44, 0, 0, 7); ctx.fill();
- chr(ctx, S.princess, dframe, 224 + sway, 184);
- // screen content: LIVE platform stats, cycling, over a dim sparkline
+ 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(206 + i * 8, 50 - bh, 5, bh); }
+ 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), 256, 33);
- ctx.fillStyle = `rgba(95,208,232,${fade})`; ctx.font = '7px Georgia, serif';
- ctx.fillText(label, 256, 45);
- ctx.textAlign = 'left'; ctx.font = '10px serif';
- ctx.fillStyle = 'rgba(127,233,255,0.4)'; ctx.fillRect(202, 10 + (f % 42), 108, 1);
- // warm lighting + screen glow
+ 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 [[256, 30, 64, 0.16], [40, 40, 30, 0.18], [432, 40, 30, 0.18]]) { 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(256, 30, 0, 256, 30, 56); cg.addColorStop(0, 'rgba(70,200,235,0.14)'); cg.addColorStop(1, 'rgba(70,200,235,0)'); ctx.fillStyle = cg; ctx.beginPath(); ctx.arc(256, 30, 56, 0, 7); ctx.fill();
+ 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
- const vg = ctx.createRadialGradient(W / 2, H * 0.52, H * 0.36, W / 2, H * 0.5, H * 0.82); vg.addColorStop(0, 'rgba(0,0,0,0)'); vg.addColorStop(1, 'rgba(8,4,10,0.5)'); ctx.fillStyle = vg; ctx.fillRect(0, 0, W, H);
+ // 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 < 12; i++) { const dx = (i * 43 + f * 0.16) % W, dy = (i * 57 + Math.sin(f * 0.02 + i) * 8 + 64) % H; ctx.fillRect(dx | 0, dy | 0, 1, 1); }
+ 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();
- // pause the 60fps loop while the tavern is scrolled out of view (CPU/battery)
io = new IntersectionObserver(([e]) => {
if (e.isIntersecting) { if (paused) { paused = false; render(); } }
else if (!paused) { paused = true; cancelAnimationFrame(raf); }
@@ -119,23 +167,60 @@ export default function TavernGame({ onHotspot, stats }) {
return () => { cancelAnimationFrame(raf); if (io) io.disconnect(); };
}, []);
+ const user = auth?.user;
return (
-
+
{!ready &&
loading tavern…
}
- onHotspot?.('agents')} title="Browse Agents (screen)" />
- onHotspot?.('agents')}>▸ Browse Agents
- onHotspot?.('legion')} title="My Legion" />
- onHotspot?.('legion')}>⌂ Enter My Legion
- onHotspot?.('agents')} />
- onHotspot?.('legion')} />
- onHotspot?.('mansion')} />
+ {/* THE DOOR — browse agents */}
+ onAction?.('agents')} title="Browse Agents" />
+ onAction?.('agents')} float>
+ ⚔ THE ROSTER open the door — the mercenaries come to you
+
+ {/* BARTENDER — sign in / your account */}
+ onAction?.(user ? 'legion' : 'login')} title={user ? 'My Legion' : 'Sign in'} />
+ onAction?.(user ? 'legion' : 'login')} float>
+ 🍺 Barkeep
+ {user ? `welcome back, ${user.handle} — your legion is mustered` : 'new blade? sign the ledger here'}
+
+ {/* THE GIRL — holo tour panel, like a quest-giver */}
+ onAction?.('tour')} title="What is amerc?" />
+ onAction?.('tour')} float>
+ ✦ what this tavern does
+ ◆ Publish Agent ◆ Remote Use ◆ Port Mapping ◆ Delivery Relay ◆ Run Directly
+ tap to open the tour ▸
+
+ {/* BANNERS — my legion */}
+ onAction?.('legion')} title="My Legion" />
+ onAction?.('legion')}>
+ 🚩 MY LEGION
+
+ {/* VILLA EXIT — my mansion */}
+ onAction?.('mansion')} title="My Mansion" />
+ onAction?.('mansion')}>
+ 🏰 MY MANSION
+
+ {/* patrons: hover quips */}
+ onAction?.('legion')} />
+ onAction?.('mansion')} />
+ onAction?.('agents')} />
);
}
+// A diegetic speech bubble pinned over the scene. Variants: wood (tavern sign),
+// holo (the glowing panel, like the elf's pitch), door (roster call).
+function Bubble({ x, y, variant = 'wood', small, float, onClick, children }) {
+ return (
+
+ {children}
+
+
+ );
+}
+
function Npc({ x, y, w, h, who, say, onClick }) {
const [hover, setHover] = useState(false);
return (
diff --git a/src/styles.css b/src/styles.css
index 695454a..858f225 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -1648,3 +1648,67 @@ button {
.mn-quota-label { color: #8fa3b8; font-size: 11.5px; flex: 0 0 auto; }
.mn-fm-meta { color: #6e7e94; font-size: 11px; }
.mn-nd { margin-top: 10px; background: #0b0f1a; border: 1px solid #20283c; border-radius: 10px; padding: 8px; }
+
+/* ===== the game IS the page (1.4) — full-screen diegetic tavern ===== */
+.gm-game { height: calc(100dvh - 52px); display: flex; flex-direction: column; align-items: stretch; justify-content: flex-start; overflow: hidden; padding: 0; background: radial-gradient(ellipse at 50% 30%, #171028 0%, #0a0712 70%); }
+.gm-stage3 { flex: 1; width: 100%; display: flex; align-items: center; justify-content: center; min-height: 0; padding: 6px 8px 0; }
+.gm-bottombar { width: 100%; }
+.gm-stage3 .gm-room { position: relative; aspect-ratio: 16 / 9; width: min(100%, calc((100dvh - 196px) * 1.7778)); box-shadow: 0 0 0 2px #2a2440, 0 0 60px rgba(70, 40, 140, 0.35), 0 24px 80px rgba(0, 0, 0, 0.6); border-radius: 6px; overflow: hidden; }
+.gm-stage3 .gm-canvas { width: 100%; height: 100%; display: block; image-rendering: pixelated; }
+.gm-bottombar { flex: 0 0 auto; text-align: center; padding: 10px 14px 14px; display: flex; flex-direction: column; gap: 4px; align-items: center; }
+.gm-tagline { margin: 0; font-family: Georgia, 'Times New Roman', serif; font-size: clamp(20px, 3.4vw, 34px); color: #f3e9d2; letter-spacing: 0.5px; text-shadow: 0 2px 18px rgba(255, 200, 120, 0.25); }
+.gm-cursor { color: #46c8e0; animation: gmblink 1.1s steps(1) infinite; }
+@keyframes gmblink { 50% { opacity: 0; } }
+.gm-tagsub { margin: 0; color: #8a7bb0; font-size: clamp(11px, 1.5vw, 13.5px); }
+.gm-link { background: none; border: none; color: #7fd9ea; cursor: pointer; font: inherit; text-decoration: underline; padding: 0; }
+.gm-link:hover { color: #cfeff7; }
+
+/* diegetic speech bubbles */
+.gm-bub { position: absolute; transform: translate(-50%, -100%); display: flex; flex-direction: column; gap: 2px; align-items: flex-start; text-align: left; padding: 8px 12px; border-radius: 10px; cursor: pointer; font-family: inherit; z-index: 4; max-width: 220px; transition: transform 0.15s, box-shadow 0.15s; }
+.gm-bub:hover { transform: translate(-50%, -104%) scale(1.04); z-index: 6; }
+.gm-bub strong { font-size: 12px; letter-spacing: 0.6px; }
+.gm-bub span { font-size: 11px; line-height: 1.45; }
+.gm-bub i, .gm-bub em { font-style: normal; font-size: 11px; line-height: 1.5; }
+.gm-bub em { margin-top: 3px; opacity: 0.85; }
+.gm-bub-tail { position: absolute; left: 50%; bottom: -7px; width: 12px; height: 12px; transform: translateX(-50%) rotate(45deg); border-radius: 2px; }
+.gm-bub-float { animation: gmfloat 3.4s ease-in-out infinite; }
+@keyframes gmfloat { 0%, 100% { margin-top: 0; } 50% { margin-top: -5px; } }
+.gm-bub-wood { background: linear-gradient(180deg, #241509f0, #170d05f0); border: 2px solid #8a5a32; color: #f3e3c2; box-shadow: 0 4px 0 rgba(0, 0, 0, 0.45), 0 10px 26px rgba(0, 0, 0, 0.5); }
+.gm-bub-wood strong { color: #ffd75e; }
+.gm-bub-wood .gm-bub-tail { background: #1d1107; border-right: 2px solid #8a5a32; border-bottom: 2px solid #8a5a32; }
+.gm-bub-holo { background: linear-gradient(180deg, rgba(10, 40, 64, 0.88), rgba(8, 24, 44, 0.88)); border: 1.5px solid #46c8e0; color: #bfeefb; box-shadow: 0 0 18px rgba(70, 200, 235, 0.35), inset 0 0 24px rgba(70, 200, 235, 0.12); backdrop-filter: blur(2px); background-image: repeating-linear-gradient(0deg, rgba(127, 233, 255, 0.05) 0 1px, transparent 1px 3px); }
+.gm-bub-holo strong { color: #7fe9ff; text-shadow: 0 0 8px rgba(127, 233, 255, 0.6); }
+.gm-bub-holo em { color: #ffd75e; }
+.gm-bub-holo .gm-bub-tail { background: rgba(9, 30, 52, 0.95); border-right: 1.5px solid #46c8e0; border-bottom: 1.5px solid #46c8e0; }
+.gm-bub-door { background: linear-gradient(180deg, rgba(40, 28, 6, 0.92), rgba(26, 16, 4, 0.92)); border: 2px solid #ffd75e; color: #ffe9b0; box-shadow: 0 0 20px rgba(255, 215, 94, 0.3); }
+.gm-bub-door strong { color: #ffd75e; letter-spacing: 1.5px; }
+.gm-bub-door .gm-bub-tail { background: #1c1204; border-right: 2px solid #ffd75e; border-bottom: 2px solid #ffd75e; }
+.gm-bub-sm { padding: 5px 10px; }
+.gm-bub-sm strong { font-size: 10.5px; }
+
+/* ornate in-game dialog */
+.gd-backdrop { position: fixed; inset: 0; background: rgba(5, 3, 12, 0.72); backdrop-filter: blur(3px); display: flex; align-items: center; justify-content: center; z-index: 60; padding: 18px; animation: gdfade 0.18s ease-out; }
+@keyframes gdfade { from { opacity: 0; } }
+.gd { position: relative; width: min(520px, 94vw); max-height: 86vh; display: flex; flex-direction: column; background: linear-gradient(180deg, #14102a, #0c0918); border: 2px solid #46c8e0; border-radius: 14px; box-shadow: 0 0 0 1px #2a2440, 0 0 50px rgba(70, 200, 235, 0.25), 0 30px 90px rgba(0, 0, 0, 0.7); animation: gdpop 0.22s cubic-bezier(0.2, 1.4, 0.4, 1); }
+.gd-wide { width: min(960px, 96vw); }
+@keyframes gdpop { from { transform: scale(0.92) translateY(10px); opacity: 0; } }
+.gd-gem { position: absolute; width: 11px; height: 11px; background: linear-gradient(135deg, #7fe9ff, #2787c8); transform: rotate(45deg); box-shadow: 0 0 10px rgba(127, 233, 255, 0.8); border-radius: 2px; }
+.gd-gem.tl { top: -6px; left: -6px; } .gd-gem.tr { top: -6px; right: -6px; }
+.gd-gem.bl { bottom: -6px; left: -6px; } .gd-gem.br { bottom: -6px; right: -6px; }
+.gd-head { display: flex; align-items: center; gap: 10px; padding: 14px 18px 10px; border-bottom: 1px solid #2a2440; }
+.gd-tag { font-size: 10px; letter-spacing: 1.4px; color: #7fd9ea; background: rgba(70, 200, 235, 0.08); border: 1px solid #1f4456; padding: 3px 9px; border-radius: 999px; white-space: nowrap; }
+.gd-head h3 { margin: 0; flex: 1; font-family: Georgia, serif; font-size: 19px; color: #f3e9d2; }
+.gd-x { background: none; border: 1px solid #3a3354; color: #b6a7d4; border-radius: 7px; padding: 4px 10px; cursor: pointer; font-family: inherit; }
+.gd-x:hover { border-color: #6a5a9a; color: #fff; }
+.gd-body { overflow-y: auto; padding: 14px 18px 18px; }
+.gd-lead { color: #b6a7d4; font-size: 13px; margin: 0 0 12px; }
+.gd-body .hf { padding: 6px 2px 10px; }
+.gd-body .hf-title { font-size: 22px; margin-top: 0; }
+
+@media (max-width: 700px) {
+ .gm-bub { max-width: 150px; padding: 6px 9px; }
+ .gm-bub i { display: none; }
+ .gm-bub span { font-size: 10px; }
+ .gm-stage3 { padding: 4px; }
+ .gm-tagline { font-size: 19px; }
+}