@@ -18,11 +18,14 @@ import (
1818
1919const boardSize = 25
2020
21+ const worldDots = 200 // most players to plot on the world map
22+
2123// Server renders the dashboard from a read-only view of the state store.
2224type Server struct {
2325 store state.Store
2426 tmpl * template.Template
2527 charTmpl * template.Template
28+ mapTmpl * template.Template
2629 now func () time.Time
2730}
2831
@@ -43,6 +46,7 @@ func New(store state.Store) *Server {
4346 store : store ,
4447 tmpl : template .Must (template .New ("index" ).Funcs (tmplFuncs ).Parse (indexTmpl )),
4548 charTmpl : template .Must (template .New ("char" ).Funcs (tmplFuncs ).Parse (charTmpl )),
49+ mapTmpl : template .Must (template .New ("map" ).Funcs (tmplFuncs ).Parse (mapTmpl )),
4650 now : time .Now ,
4751 }
4852}
@@ -51,6 +55,7 @@ func New(store state.Store) *Server {
5155func (s * Server ) Handler () http.Handler {
5256 mux := http .NewServeMux ()
5357 mux .HandleFunc ("/" , s .index )
58+ mux .HandleFunc ("/map" , s .worldMap )
5459 mux .HandleFunc ("/p/" , s .char )
5560 mux .HandleFunc ("/healthz" , func (w http.ResponseWriter , _ * http.Request ) {
5661 w .WriteHeader (http .StatusOK )
@@ -89,6 +94,19 @@ func (s *Server) index(w http.ResponseWriter, r *http.Request) {
8994 _ = s .tmpl .Execute (w , data )
9095}
9196
97+ // worldMap renders the persistent world map: every placed player + the towns.
98+ func (s * Server ) worldMap (w http.ResponseWriter , r * http.Request ) {
99+ ctx , cancel := context .WithTimeout (r .Context (), 3 * time .Second )
100+ defer cancel ()
101+ world , err := idlerpg .ReadWorld (ctx , s .store , worldDots )
102+ if err != nil {
103+ http .Error (w , "the realm is unreachable right now." , http .StatusServiceUnavailable )
104+ return
105+ }
106+ w .Header ().Set ("Content-Type" , "text/html; charset=utf-8" )
107+ _ = s .mapTmpl .Execute (w , world )
108+ }
109+
92110// char renders one character's sheet at /p/<key>.
93111func (s * Server ) char (w http.ResponseWriter , r * http.Request ) {
94112 key := strings .TrimPrefix (r .URL .Path , "/p/" )
@@ -154,6 +172,7 @@ const indexTmpl = `<!doctype html>
154172</head>
155173<body>
156174<h1>⚔ the idle realm</h1>
175+ <p class="muted"><a href="/map">🗺 the realm map</a> — see where everyone's wandering.</p>
157176{{if .Quest}}
158177<div class="quest">
159178 <strong>A quest is underway.</strong>
@@ -240,3 +259,46 @@ const charTmpl = `<!doctype html>
240259<footer><a href="/">← back to the realm</a></footer>
241260</body>
242261</html>`
262+
263+ const mapTmpl = `<!doctype html>
264+ <html lang="en">
265+ <head>
266+ <meta charset="utf-8">
267+ <meta name="viewport" content="width=device-width, initial-scale=1">
268+ <meta http-equiv="refresh" content="30">
269+ <title>annoybots · the realm map</title>
270+ <style>
271+ :root { color-scheme: dark; }
272+ body { background:#0e0f13; color:#d6d8de; font:15px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace; margin:0; padding:2rem; }
273+ h1 { font-size:1.4rem; margin:0 0 .25rem; color:#e9b949; }
274+ .sub { color:#8aa0c6; margin:0 0 1rem; }
275+ .world { display:block; width:100%; max-width:680px; margin:0 auto; background:#0b0c10; border:1px solid #2a2f3a; border-radius:8px; }
276+ .town { fill:#4a3c14; stroke:#e9b949; stroke-width:2; }
277+ .town-l { fill:#b9912f; font-size:11px; }
278+ .dot { fill:#7fd1a8; stroke:#0e0f13; stroke-width:1.5; }
279+ .dot-l { fill:#9aa0ac; font-size:9px; }
280+ .muted { color:#6b7280; }
281+ a { color:#7fd1a8; text-decoration:none; }
282+ a:hover { text-decoration:underline; }
283+ footer { margin-top:1.5rem; color:#4b5563; font-size:.8rem; text-align:center; }
284+ </style>
285+ </head>
286+ <body>
287+ <h1>🗺 the realm map</h1>
288+ <p class="sub">{{len .Players}} adventurers roaming · towns in gold</p>
289+ <svg class="world" viewBox="-12 -12 {{add .Size 24}} {{add .Size 24}}" role="img" aria-label="world map of player positions">
290+ <rect x="0" y="0" width="{{.Size}}" height="{{.Size}}" fill="#0b0c10" stroke="#20232b" stroke-width="1"/>
291+ {{range .Towns}}
292+ <rect x="{{add .X -5}}" y="{{add .Y -5}}" width="10" height="10" class="town"/>
293+ <text x="{{add .X 9}}" y="{{add .Y 4}}" class="town-l">{{.Name}}</text>
294+ {{end}}
295+ {{range .Players}}
296+ <circle cx="{{.X}}" cy="{{.Y}}" r="4" class="dot"/>
297+ <text x="{{add .X 6}}" y="{{add .Y 3}}" class="dot-l">{{.Name}}</text>
298+ {{else}}
299+ <text x="{{add .Size -250}}" y="250" class="dot-l">no one has wandered onto the map yet.</text>
300+ {{end}}
301+ </svg>
302+ <footer><a href="/">← back to the realm</a> · auto-refreshes every 30s</footer>
303+ </body>
304+ </html>`
0 commit comments