talkable tavern NPCs (0.38.0): Pokémon-style speech bubbles

- hover a character (barkeep / two patrons) -> parchment speech bubble with flavor
- clicking routes to Browse / My Booth / Mansion — adds life + feature discovery
- verified bubble renders on hover
This commit is contained in:
artheru 2026-06-10 05:56:15 +08:00
parent a138ff1ebb
commit e711a7c904
5 changed files with 23 additions and 4 deletions

View File

@ -4,7 +4,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0e0a14" />
<meta name="description" content="amerc is the agent mercenary tavern — hire, run, publish, and remotely deliver AI agents across software boundaries. No infrastructure, just skills." />
<meta name="version" content="0.37.0-embed" />
<meta name="version" content="0.38.0-npc" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<!-- Open Graph / social share -->
<meta property="og:type" content="website" />

View File

@ -1,6 +1,6 @@
{
"name": "amerc-site",
"version": "0.37.0-embed",
"version": "0.38.0-npc",
"private": true,
"type": "module",
"scripts": {

View File

@ -7,7 +7,7 @@ import Account from './Account.jsx';
import TavernGame from './TavernGame.jsx';
import { api } from './api.js';
const RELEASE = '0.37.0-embed';
const RELEASE = '0.38.0-npc';
const NAV = [
{ id: 'home', label: 'Tavern' },

View File

@ -108,8 +108,20 @@ export default function TavernGame({ onHotspot }) {
<button className="gm-label gm-label-screen" style={{ left: '50%', top: '18%' }} onClick={() => onHotspot?.('agents')}> Browse Agents</button>
<button className="gm-hot" style={{ left: '44%', top: '50%', width: '14%', height: '24%' }} onClick={() => onHotspot?.('booth')} title="My Booth" />
<button className="gm-label gm-label-booth" style={{ left: '50%', top: '90%' }} onClick={() => onHotspot?.('booth')}> Enter My Booth</button>
<button className="gm-label gm-label-bar" style={{ left: '20%', top: '24%' }} onClick={() => onHotspot?.('agents')}>🍺 The Bar</button>
<Npc x="11%" y="4%" w="13%" h="20%" who="Barkeep" say="What'll it be? The staff roster is up on the board." onClick={() => onHotspot?.('agents')} />
<Npc x="11%" y="52%" w="13%" h="22%" who="Patron" say="I run three mercenaries straight from my booth." onClick={() => onHotspot?.('booth')} />
<Npc x="72%" y="52%" w="13%" h="22%" who="Patron" say="Showcase put my little app on the open internet!" onClick={() => onHotspot?.('mansion')} />
</div>
</div>
);
}
function Npc({ x, y, w, h, who, say, onClick }) {
const [hover, setHover] = useState(false);
return (
<button className="gm-npc" style={{ left: x, top: y, width: w, height: h }}
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} onClick={onClick} aria-label={who}>
{hover && <span className="gm-say"><strong>{who}</strong>{say}</span>}
</button>
);
}

View File

@ -1181,3 +1181,10 @@ button {
.ap-embed > code { display: block; color: #7fe9ff; font-family: ui-monospace, monospace; font-size: 12px; background: #07060c; padding: 10px 12px; border-radius: 8px; word-break: break-all; border: 1px solid #1d2a3a; }
.ap-embed .ap-hint { margin: 8px 0 0; } .ap-embed .ap-hint code { display: inline; padding: 1px 5px; background: #162032; color: #9d8fc0; }
.ap-pty-note { padding: 12px 14px; }
/* tavern NPC speech bubbles (0.38) */
.gm-npc { position: absolute; background: transparent; border: 0; cursor: pointer; border-radius: 8px; transition: box-shadow .14s; }
.gm-npc:hover { box-shadow: 0 0 0 2px rgba(255,220,140,0.5), 0 0 18px rgba(255,200,100,0.35); background: rgba(255,220,140,0.08); }
.gm-say { position: absolute; bottom: 102%; left: 50%; transform: translateX(-50%); width: max-content; max-width: 200px; padding: 8px 11px; font-family: Georgia, serif; font-size: 12px; line-height: 1.5; color: #1c1206; background: #fff0d7; border: 2px solid #3a2410; border-radius: 8px; box-shadow: 0 6px 16px rgba(0,0,0,0.5); z-index: 8; text-align: left; }
.gm-say strong { display: block; color: #7a3d0c; font-size: 11px; margin-bottom: 3px; }
.gm-say::after { content: ''; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); border: 7px solid transparent; border-top-color: #3a2410; }