CyberMax-tools commited on
Commit
2c94890
·
verified ·
1 Parent(s): 1c07766

Deploy plotwren

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ brand/cover.png filter=lfs diff=lfs merge=lfs -text
37
+ brand/og.png filter=lfs diff=lfs merge=lfs -text
404.html ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ <!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Not found · Plotwren</title><link rel="stylesheet" href="/style.css"><link rel="icon" href="/icon.svg"></head>
2
+ <body class="sub"><header class="bar"><a class="brand" href="/"><img src="/icon.svg" alt="" width="28" height="28"> Plotwren</a></header><main class="wrap"><h1>This street doesn't exist</h1><p><a class="btn" href="/">Back to the town map</a></p></main></body></html>
README.md CHANGED
@@ -1,10 +1,39 @@
1
  ---
2
- title: Plotwren
3
- emoji: 📚
4
- colorFrom: pink
5
- colorTo: red
6
  sdk: static
7
  pinned: false
 
 
 
 
 
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: "Plotwren — the CyberMax maker town"
3
+ emoji: 🏘️
4
+ colorFrom: red
5
+ colorTo: yellow
6
  sdk: static
7
  pinned: false
8
+ license: other
9
+ short_description: "A pixel town where makers claim a plot from $5"
10
+ tags:
11
+ - pixel-art
12
+ - indie-makers
13
+ - startup-promotion
14
+ - canvas
15
+ - game
16
+ - cybermax
17
  ---
18
 
19
+ # Plotwren: put your product on the map from $5
20
+
21
+ [![Plotwren demo](https://cybermaxtools.com/media/demos/plotwren/poster.jpg)](https://plotwren.cybermaxtools.com/?s=hf-plotwren)
22
+
23
+ Plotwren is a pixel town for makers, indie devs and small businesses. Claim a **stall, shop, billboard or landmark** with your logo, one-line pitch and link. Fixed prices, paid once: nobody can outbid you or take your plot over.
24
+
25
+ | Plot | Where | Price |
26
+ |---|---|---|
27
+ | Market stall | Market Row | $5, paid once |
28
+ | Shop | Maker Street | $19, paid once |
29
+ | Billboard | Launch Boulevard | $49, paid once |
30
+ | Landmark (4 in total) | Plotwren Square | $149, paid once |
31
+ | Featured (optional) | Featured strip + glowing outline | $9/month, cancel any time |
32
+
33
+ **This Space is a static viewer** of the live town: it reads the same map data and opens checkout on the real site. Claim a plot at **[plotwren.cybermaxtools.com](https://plotwren.cybermaxtools.com/?s=hf-plotwren)**.
34
+
35
+ - Every plot gets a share page, an "I'm on Plotwren" badge and real visit counts (no bots, no link previews).
36
+ - Every sign is checked automatically before it shows: text, link and image. Adult, illegal, hateful or scam content is refused.
37
+ - 14-day fix-or-refund if it doesn't work as described. [Terms](https://plotwren.cybermaxtools.com/terms/).
38
+
39
+ Plots marked CyberMax are CyberMax's own products. © 2026 CyberMax. All rights reserved.
_headers ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # security-headers v1 (scripts/web/security_headers.py)
2
+ /*
3
+ Strict-Transport-Security: max-age=31536000
4
+ X-Content-Type-Options: nosniff
5
+ X-Frame-Options: SAMEORIGIN
6
+ Referrer-Policy: strict-origin-when-cross-origin
7
+ Cross-Origin-Resource-Policy: cross-origin
8
+ Content-Security-Policy: frame-ancestors 'self'; object-src 'none'; base-uri 'self'; upgrade-insecure-requests
9
+ /embed/*
10
+ ! X-Frame-Options
11
+ ! Content-Security-Policy
12
+ # /security-headers
13
+ /*
14
+ X-Content-Type-Options: nosniff
15
+ Referrer-Policy: strict-origin-when-cross-origin
16
+ /town.js
17
+ Cache-Control: public, max-age=300
18
+ /app.js
19
+ Cache-Control: public, max-age=300
20
+ /style.css
21
+ Cache-Control: public, max-age=300
22
+ /seed/*
23
+ Cache-Control: public, max-age=86400
app.js ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Plotwren town map: a hand-drawn (code-drawn) pixel town on one <canvas>. No libraries, ~20 KB, works with touch,
2
+ // mouse and keyboard. Data: /api/town (live plots), falling back to the house plots when offline or on a static mirror.
3
+ // © 2026 CyberMax. All rights reserved.
4
+ import { WORLD, TIERS, FEATURED, DISTRICTS, ROADS, WATER, FOUNTAIN, PLOTS, YARD, usd } from './town.js';
5
+
6
+ const SITE = 'https://plotwren.cybermaxtools.com';
7
+ const ON_SITE = /plotwren\.cybermaxtools\.com$|plotwren\.cybermax-tools\.workers\.dev$|^localhost$|^127\.0\.0\.1$/.test(location.hostname);
8
+ const BASE = ON_SITE ? '' : SITE; // the static mirror (Hugging Face) reads the live town and links to the real checkout
9
+ const SRC = new URLSearchParams(location.search).get('s') || (ON_SITE ? 'map' : 'mirror');
10
+ const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
11
+
12
+ const cv = document.getElementById('town');
13
+ const cx = cv.getContext('2d');
14
+ const panel = document.getElementById('panel');
15
+ const state = { plots: {}, house: [], loaded: false, checkout: true };
16
+ const cam = { x: 0, y: 0, s: 1 };
17
+ const imgs = new Map();
18
+ let hover = null, selected = null, t0 = performance.now();
19
+
20
+ // ---------------------------------------------------------------- deterministic noise
21
+ function rng(seed) { let s = seed >>> 0; return () => ((s = (s * 1664525 + 1013904223) >>> 0) / 4294967296); }
22
+ const hash = (str) => [...str].reduce((h, c) => (Math.imul(h ^ c.charCodeAt(0), 16777619) >>> 0), 2166136261);
23
+ const inRect = (x, y, r, m = 0) => x >= r.x - m && x < r.x + r.w + m && y >= r.y - m && y < r.y + r.h + m;
24
+
25
+ // ---------------------------------------------------------------- static background (drawn once)
26
+ const PAL = { grass: '#8fd16a', grass2: '#7cc35a', grass3: '#a3dc7e', road: '#6b7280', road2: '#5b616d', line: '#f8e16c', walk: '#d6cfc2', walk2: '#c7bfb0', water: '#4aa3df', water2: '#7cc4f0', dirt: '#c99a5b', dirt2: '#b5864a', ink: '#2b2d42', paper: '#fff8e7', trunk: '#7a4a24', leaf: '#3f8f3a', leaf2: '#55a84b' };
27
+ const bg = document.createElement('canvas');
28
+ bg.width = WORLD.w; bg.height = WORLD.h;
29
+ function drawBackground() {
30
+ const g = bg.getContext('2d');
31
+ const r = rng(7);
32
+ g.fillStyle = PAL.grass; g.fillRect(0, 0, WORLD.w, WORLD.h);
33
+ for (let i = 0; i < 2600; i++) { g.fillStyle = r() < 0.5 ? PAL.grass2 : PAL.grass3; g.fillRect((r() * WORLD.w) | 0, (r() * WORLD.h) | 0, 2, 2); }
34
+ // sidewalks around roads, then the roads
35
+ for (const rd of ROADS) { g.fillStyle = PAL.walk; g.fillRect(rd.x - 4, rd.y - 4, rd.w + 8, rd.h + 8); }
36
+ for (const rd of ROADS) {
37
+ g.fillStyle = PAL.road; g.fillRect(rd.x, rd.y, rd.w, rd.h);
38
+ g.fillStyle = PAL.road2; for (let i = 0; i < (rd.w * rd.h) / 60; i++) g.fillRect(rd.x + ((r() * rd.w) | 0), rd.y + ((r() * rd.h) | 0), 2, 2);
39
+ g.fillStyle = PAL.line;
40
+ if (rd.w > rd.h) for (let x = rd.x + 6; x < rd.x + rd.w - 10; x += 24) g.fillRect(x, rd.y + rd.h / 2 - 1, 12, 2);
41
+ else for (let y = rd.y + 6; y < rd.y + rd.h - 10; y += 24) g.fillRect(rd.x + rd.w / 2 - 1, y, 2, 12);
42
+ }
43
+ // water + a little pier
44
+ g.fillStyle = PAL.water; g.fillRect(WATER.x, WATER.y, WATER.w, WATER.h);
45
+ g.fillStyle = '#e9d8a6'; g.fillRect(0, WATER.y - 10, WORLD.w, 10);
46
+ g.fillStyle = PAL.trunk; g.fillRect(744, WATER.y - 10, 48, 54);
47
+ for (let y = WATER.y - 10; y < WATER.y + 44; y += 6) { g.fillStyle = '#5e3a1c'; g.fillRect(744, y, 48, 1); }
48
+ // fountain on the square
49
+ const f = FOUNTAIN;
50
+ g.fillStyle = '#9aa3ad'; g.fillRect(f.x + 4, f.y + 4, f.w - 8, f.h - 8);
51
+ g.fillStyle = PAL.water; g.fillRect(f.x + 8, f.y + 8, f.w - 16, f.h - 16);
52
+ g.fillStyle = '#e5e7eb'; g.fillRect(f.x + 20, f.y + 16, 8, 16);
53
+ // trees and flowers on empty grass
54
+ const blocked = (x, y) => ROADS.some((rd) => inRect(x, y, rd, 10)) || inRect(x, y, WATER, 14) || [...PLOTS, ...YARD].some((p) => inRect(x, y, p, 8)) || inRect(x, y, FOUNTAIN, 14);
55
+ for (let i = 0; i < 700; i++) {
56
+ const x = (r() * (WORLD.w - 20)) | 0, y = (r() * (WORLD.h - 90)) | 0;
57
+ if (blocked(x, y) || blocked(x + 14, y + 18)) continue;
58
+ if (r() < 0.55) tree(g, x, y, r() < 0.3); else flower(g, x, y, ['#ff5d8f', '#f3a712', '#ffffff', '#7b2cbf'][(r() * 4) | 0]);
59
+ }
60
+ // district plaques
61
+ label(g, DISTRICTS.yard, 72, 44); label(g, DISTRICTS.street, 72, 172); label(g, DISTRICTS.boulevard, 72, 380);
62
+ label(g, DISTRICTS.square, 640, 324); label(g, DISTRICTS.market, 72, 652); label(g, DISTRICTS.market, 840, 652); label(g, DISTRICTS.street, 840, 172); label(g, DISTRICTS.boulevard, 912, 380);
63
+ }
64
+ function tree(g, x, y, round) {
65
+ g.fillStyle = 'rgba(0,0,0,.15)'; g.fillRect(x + 2, y + 16, 14, 4);
66
+ g.fillStyle = PAL.trunk; g.fillRect(x + 7, y + 11, 4, 8);
67
+ g.fillStyle = PAL.leaf; round ? (g.fillRect(x + 2, y + 2, 14, 10), g.fillRect(x + 4, y, 10, 14)) : (g.fillRect(x + 5, y, 8, 4), g.fillRect(x + 3, y + 4, 12, 4), g.fillRect(x + 1, y + 8, 16, 4));
68
+ g.fillStyle = PAL.leaf2; g.fillRect(x + 5, y + 3, 4, 3);
69
+ }
70
+ function flower(g, x, y, c) { g.fillStyle = '#3f8f3a'; g.fillRect(x + 2, y + 3, 1, 3); g.fillStyle = c; g.fillRect(x + 1, y + 1, 3, 3); g.fillStyle = '#f8e16c'; g.fillRect(x + 2, y + 2, 1, 1); }
71
+ function label(g, text, x, y) {
72
+ g.font = 'bold 9px ui-monospace, Menlo, Consolas, monospace';
73
+ const w = Math.ceil(g.measureText(text).width) + 8;
74
+ g.fillStyle = PAL.ink; g.fillRect(x, y - 9, w, 12);
75
+ g.fillStyle = '#fff'; g.fillText(text, x + 4, y);
76
+ }
77
+
78
+ // ---------------------------------------------------------------- plots
79
+ function logoImg(src) {
80
+ if (!src) return null;
81
+ let im = imgs.get(src);
82
+ if (!im) { im = new Image(); im.decoding = 'async'; im.src = src.startsWith('/') ? BASE + src : src; im.onload = () => draw(); imgs.set(src, im); }
83
+ return im.complete && im.naturalWidth ? im : null;
84
+ }
85
+ function signBox(g, x, y, w, h, colour, live) {
86
+ g.fillStyle = PAL.ink; g.fillRect(x - 1, y - 1, w + 2, h + 2);
87
+ g.fillStyle = PAL.paper; g.fillRect(x, y, w, h);
88
+ const im = logoImg(live.logo);
89
+ const s = Math.min(w, h) - 2;
90
+ if (im) { g.imageSmoothingEnabled = true; g.drawImage(im, x + (w - s) / 2, y + (h - s) / 2, s, s); g.imageSmoothingEnabled = false; }
91
+ else {
92
+ g.fillStyle = colour; g.fillRect(x + 1, y + 1, w - 2, h - 2);
93
+ g.fillStyle = '#fff'; g.font = `bold ${Math.max(6, (s * 0.7) | 0)}px ui-monospace, monospace`; g.textAlign = 'center'; g.textBaseline = 'middle';
94
+ g.fillText((live.name || '?').slice(0, 1).toUpperCase(), x + w / 2, y + h / 2 + 1); g.textAlign = 'start'; g.textBaseline = 'alphabetic';
95
+ }
96
+ }
97
+ function drawPlot(g, p, info, now) {
98
+ const { x, y, w, h } = p;
99
+ const tier = p.tier;
100
+ g.fillStyle = 'rgba(0,0,0,.12)'; g.fillRect(x + 2, y + h - 2, w, 4);
101
+ if (!info || info.state === 'free') { // empty lot with a for-sale sign
102
+ g.fillStyle = PAL.dirt; g.fillRect(x, y, w, h);
103
+ g.fillStyle = PAL.dirt2; for (let i = 2; i < w; i += 6) g.fillRect(x + i, y + ((i * 7) % (h - 2)), 2, 2);
104
+ g.strokeStyle = '#8a6a3c'; g.setLineDash([2, 2]); g.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1); g.setLineDash([]);
105
+ const sx = x + w / 2 - 7, sy = y + h / 2 - 9;
106
+ g.fillStyle = PAL.trunk; g.fillRect(sx + 6, sy + 8, 2, 10);
107
+ g.fillStyle = { stall: '#29bf12', shop: '#2e86ab', billboard: '#f3a712', landmark: '#e4572e' }[tier]; g.fillRect(sx, sy, 14, 9);
108
+ g.fillStyle = '#fff'; g.fillRect(sx + 3, sy + 4, 8, 1); g.fillRect(sx + 6, sy + 2, 2, 5);
109
+ return;
110
+ }
111
+ if (info.state === 'reserved' || info.state === 'sold') { // claimed, content coming: scaffolding
112
+ g.fillStyle = '#d9c7a3'; g.fillRect(x, y, w, h);
113
+ g.strokeStyle = '#a0763c'; g.lineWidth = 1;
114
+ for (let i = 0; i <= w; i += 8) { g.beginPath(); g.moveTo(x + i + 0.5, y); g.lineTo(x + i + 0.5, y + h); g.stroke(); }
115
+ for (let j = 0; j <= h; j += 8) { g.beginPath(); g.moveTo(x, y + j + 0.5); g.lineTo(x + w, y + j + 0.5); g.stroke(); }
116
+ g.fillStyle = '#f3a712'; g.fillRect(x + 2, y + h - 6, w - 4, 3);
117
+ return;
118
+ }
119
+ const c = info.colour || '#2e86ab';
120
+ if (tier === 'stall') {
121
+ g.fillStyle = '#c7a26b'; g.fillRect(x + 2, y + 14, w - 4, h - 16);
122
+ for (let i = 0; i < w - 4; i += 6) { g.fillStyle = (i / 6) % 2 ? '#fff' : c; g.fillRect(x + 2 + i, y + 6, 6, 8); }
123
+ g.fillStyle = PAL.ink; g.fillRect(x + 2, y + 14, w - 4, 1);
124
+ signBox(g, x + 9, y + 17, 14, 12, c, info);
125
+ } else if (tier === 'shop' || tier === 'house') {
126
+ g.fillStyle = '#efe3cc'; g.fillRect(x + 3, y + 14, w - 6, h - 16);
127
+ g.fillStyle = c; g.fillRect(x, y + 6, w, 10); g.fillRect(x + 4, y + 2, w - 8, 4);
128
+ g.fillStyle = 'rgba(0,0,0,.2)'; g.fillRect(x, y + 15, w, 1);
129
+ g.fillStyle = '#7fc8f8'; g.fillRect(x + 6, y + h - 14, 10, 8); g.fillRect(x + w - 16, y + h - 14, 10, 8);
130
+ g.fillStyle = PAL.ink; g.fillRect(x + w / 2 - 4, y + h - 14, 8, 12);
131
+ signBox(g, x + w / 2 - 12, y + 17, 24, 16, c, info);
132
+ if (tier === 'house') { g.fillStyle = PAL.ink; g.fillRect(x + w - 18, y, 18, 9); g.fillStyle = '#fff'; g.font = 'bold 7px ui-monospace, monospace'; g.fillText('CMX', x + w - 17, y + 7); }
133
+ } else if (tier === 'billboard') {
134
+ g.fillStyle = '#5e3a1c'; g.fillRect(x + 12, y + 34, 4, 14); g.fillRect(x + w - 16, y + 34, 4, 14);
135
+ g.fillStyle = PAL.ink; g.fillRect(x, y, w, 36);
136
+ g.fillStyle = c; g.fillRect(x + 2, y + 2, w - 4, 32);
137
+ signBox(g, x + 5, y + 5, 26, 26, c, info);
138
+ g.fillStyle = '#fff'; g.font = 'bold 7px ui-monospace, monospace';
139
+ const nm = (info.name || '').slice(0, 8);
140
+ g.fillText(nm, x + 34, y + 16); g.font = '6px ui-monospace, monospace'; g.fillText((info.name || '').slice(8, 16), x + 34, y + 25);
141
+ } else if (tier === 'landmark') {
142
+ g.fillStyle = '#e9e4da'; g.fillRect(x + 14, y + 10, w - 28, h - 12);
143
+ g.fillStyle = c; g.fillRect(x + 10, y + 4, w - 20, 10); g.fillRect(x + 24, y, w - 48, 4);
144
+ g.fillStyle = '#7fc8f8';
145
+ for (let r = 0; r < 4; r++) for (let k = 0; k < 4; k++) g.fillRect(x + 20 + k * 15, y + 54 + r * 9, 8, 5);
146
+ g.fillStyle = PAL.ink; g.fillRect(x + w / 2 - 6, y + h - 14, 12, 12);
147
+ signBox(g, x + w / 2 - 18, y + 16, 36, 34, c, info);
148
+ }
149
+ if (info.featured) {
150
+ const a = reduced ? 0.9 : 0.55 + 0.45 * Math.sin(now / 300);
151
+ g.strokeStyle = `rgba(255, 214, 10, ${a})`; g.lineWidth = 3; g.strokeRect(x - 3, y - 3, w + 6, h + 6); g.lineWidth = 1;
152
+ }
153
+ }
154
+
155
+ // ---------------------------------------------------------------- walkers (decoration only)
156
+ const walkers = Array.from({ length: reduced ? 0 : 26 }, (_, i) => {
157
+ const r = rng(100 + i), rd = ROADS[i % ROADS.length], horiz = rd.w > rd.h;
158
+ return { rd, horiz, p: r(), v: (0.012 + r() * 0.02) * (r() < 0.5 ? 1 : -1), lane: r(), c: ['#e4572e', '#2e86ab', '#7b2cbf', '#29bf12', '#f3a712', '#ff5d8f'][i % 6] };
159
+ });
160
+ function drawWalkers(g, now) {
161
+ for (const w of walkers) {
162
+ const len = w.horiz ? w.rd.w : w.rd.h;
163
+ const pos = (((w.p * len + (now / 1000) * w.v * 600) % len) + len) % len;
164
+ const x = w.horiz ? w.rd.x + pos : w.rd.x - 3 + (w.lane < 0.5 ? 0 : w.rd.w + 2);
165
+ const y = w.horiz ? w.rd.y - 7 + (w.lane < 0.5 ? 0 : w.rd.h + 6) : w.rd.y + pos;
166
+ const step = ((now / 180) | 0) % 2;
167
+ g.fillStyle = '#f2c79b'; g.fillRect(x, y, 3, 3);
168
+ g.fillStyle = w.c; g.fillRect(x - 1, y + 3, 5, 4);
169
+ g.fillStyle = PAL.ink; g.fillRect(x - 1 + step, y + 7, 1, 2); g.fillRect(x + 2 - step, y + 7, 1, 2);
170
+ }
171
+ }
172
+
173
+ // ---------------------------------------------------------------- camera + drawing
174
+ function fit() {
175
+ const r = cv.getBoundingClientRect();
176
+ const dpr = Math.min(devicePixelRatio || 1, 2);
177
+ cv.width = Math.round(r.width * dpr); cv.height = Math.round(r.height * dpr);
178
+ const s = Math.max(r.width / WORLD.w, r.height / WORLD.h) * (r.width < 700 ? 1.6 : 1.05);
179
+ cam.s = s; cam.min = Math.min(r.width / WORLD.w, r.height / WORLD.h); cam.max = 6;
180
+ cam.x = WORLD.w / 2 - r.width / s / 2; cam.y = 420 - r.height / s / 2;
181
+ clamp(); draw();
182
+ }
183
+ function clamp() {
184
+ const r = cv.getBoundingClientRect();
185
+ const vw = r.width / cam.s, vh = r.height / cam.s;
186
+ cam.x = vw >= WORLD.w ? (WORLD.w - vw) / 2 : Math.max(0, Math.min(WORLD.w - vw, cam.x));
187
+ cam.y = vh >= WORLD.h ? (WORLD.h - vh) / 2 : Math.max(0, Math.min(WORLD.h - vh, cam.y));
188
+ }
189
+ function infoOf(p) {
190
+ if (p.tier === 'house') return p.live;
191
+ return state.plots[p.id] || { state: 'free' };
192
+ }
193
+ const HOUSE_PLOTS = () => state.house.map((h) => ({ ...YARD.find((y) => y.id === h.id), tier: 'house', live: { state: 'live', ...h } }));
194
+ let raf = 0;
195
+ function draw() {
196
+ if (raf) return;
197
+ raf = requestAnimationFrame(frame);
198
+ }
199
+ function frame(now) {
200
+ raf = 0;
201
+ const dpr = cv.width / cv.getBoundingClientRect().width;
202
+ cx.setTransform(dpr * cam.s, 0, 0, dpr * cam.s, -cam.x * dpr * cam.s, -cam.y * dpr * cam.s);
203
+ cx.imageSmoothingEnabled = false;
204
+ cx.drawImage(bg, 0, 0);
205
+ if (!reduced) { // water shimmer
206
+ cx.fillStyle = PAL.water2;
207
+ for (let i = 0; i < 40; i++) { const x = (i * 97 + now / 40) % WORLD.w; cx.fillRect(x, WATER.y + 8 + ((i * 13) % 44), 10, 2); }
208
+ }
209
+ for (const p of PLOTS) drawPlot(cx, p, state.plots[p.id], now);
210
+ for (const h of HOUSE_PLOTS()) drawPlot(cx, h, h.live, now);
211
+ drawWalkers(cx, now);
212
+ const hl = (p, col) => { if (!p) return; cx.strokeStyle = col; cx.lineWidth = 2 / cam.s; cx.strokeRect(p.x - 1, p.y - 1, p.w + 2, p.h + 2); };
213
+ hl(hover, 'rgba(255,255,255,.9)'); hl(selected, '#ffd60a');
214
+ if (cam.s >= 2.2) { // names under live signs when zoomed in
215
+ cx.font = 'bold 6px ui-monospace, monospace'; cx.textAlign = 'center';
216
+ for (const p of [...PLOTS, ...HOUSE_PLOTS()]) {
217
+ const i = infoOf(p);
218
+ if (i?.state !== 'live' || p.tier === 'billboard') continue;
219
+ const t = (i.name || '').slice(0, 14), tw = cx.measureText(t).width + 4;
220
+ cx.fillStyle = 'rgba(43,45,66,.85)'; cx.fillRect(p.x + p.w / 2 - tw / 2, p.y + p.h + 1, tw, 8);
221
+ cx.fillStyle = '#fff'; cx.fillText(t, p.x + p.w / 2, p.y + p.h + 7);
222
+ }
223
+ cx.textAlign = 'start';
224
+ }
225
+ if (!reduced && !document.hidden && visible) draw();
226
+ }
227
+ let visible = true;
228
+ new IntersectionObserver(([e]) => { visible = e.isIntersecting; if (visible) draw(); }).observe(cv);
229
+ document.addEventListener('visibilitychange', () => { if (!document.hidden) draw(); });
230
+
231
+ // ---------------------------------------------------------------- input: drag, pinch, wheel, tap, keyboard
232
+ const pts = new Map();
233
+ let drag = null, moved = 0, pinch = null;
234
+ const toWorld = (cxp, cyp) => { const r = cv.getBoundingClientRect(); return { x: cam.x + (cxp - r.left) / cam.s, y: cam.y + (cyp - r.top) / cam.s }; };
235
+ const hit = (w) => [...HOUSE_PLOTS(), ...PLOTS].find((p) => inRect(w.x, w.y, p, 2)) || null;
236
+ function zoomAt(f, cxp, cyp) {
237
+ const before = toWorld(cxp, cyp);
238
+ cam.s = Math.max(cam.min, Math.min(cam.max, cam.s * f));
239
+ const r = cv.getBoundingClientRect();
240
+ cam.x = before.x - (cxp - r.left) / cam.s; cam.y = before.y - (cyp - r.top) / cam.s;
241
+ clamp(); draw();
242
+ }
243
+ cv.addEventListener('pointerdown', (e) => {
244
+ cv.setPointerCapture(e.pointerId);
245
+ pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
246
+ if (pts.size === 1) { drag = { x: e.clientX, y: e.clientY, cx: cam.x, cy: cam.y }; moved = 0; }
247
+ if (pts.size === 2) { const [a, b] = [...pts.values()]; pinch = { d: Math.hypot(a.x - b.x, a.y - b.y), s: cam.s }; moved = 99; }
248
+ });
249
+ cv.addEventListener('pointermove', (e) => {
250
+ if (pts.has(e.pointerId)) pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
251
+ if (pinch && pts.size === 2) {
252
+ const [a, b] = [...pts.values()];
253
+ const f = (pinch.s * Math.hypot(a.x - b.x, a.y - b.y) / pinch.d) / cam.s;
254
+ zoomAt(f, (a.x + b.x) / 2, (a.y + b.y) / 2);
255
+ return;
256
+ }
257
+ if (drag && pts.size === 1) {
258
+ moved = Math.max(moved, Math.hypot(e.clientX - drag.x, e.clientY - drag.y));
259
+ cam.x = drag.cx - (e.clientX - drag.x) / cam.s; cam.y = drag.cy - (e.clientY - drag.y) / cam.s;
260
+ clamp(); draw();
261
+ return;
262
+ }
263
+ if (e.pointerType === 'mouse') {
264
+ const h = hit(toWorld(e.clientX, e.clientY));
265
+ if (h !== hover) { hover = h; cv.style.cursor = h ? 'pointer' : 'grab'; draw(); }
266
+ }
267
+ });
268
+ const up = (e) => {
269
+ pts.delete(e.pointerId);
270
+ if (pts.size < 2) pinch = null;
271
+ if (pts.size === 0) {
272
+ if (drag && moved < 6) { const p = hit(toWorld(e.clientX, e.clientY)); select(p); }
273
+ drag = null;
274
+ }
275
+ };
276
+ cv.addEventListener('pointerup', up);
277
+ cv.addEventListener('pointercancel', (e) => { pts.delete(e.pointerId); drag = null; pinch = null; });
278
+ cv.addEventListener('wheel', (e) => { e.preventDefault(); zoomAt(Math.exp(-e.deltaY * 0.0015), e.clientX, e.clientY); }, { passive: false });
279
+ document.getElementById('zin').onclick = () => { const r = cv.getBoundingClientRect(); zoomAt(1.4, r.left + r.width / 2, r.top + r.height / 2); };
280
+ document.getElementById('zout').onclick = () => { const r = cv.getBoundingClientRect(); zoomAt(1 / 1.4, r.left + r.width / 2, r.top + r.height / 2); };
281
+ cv.addEventListener('keydown', (e) => {
282
+ const step = 40 / cam.s, k = e.key;
283
+ if (k === 'ArrowLeft') cam.x -= step; else if (k === 'ArrowRight') cam.x += step; else if (k === 'ArrowUp') cam.y -= step; else if (k === 'ArrowDown') cam.y += step;
284
+ else if (k === '+' || k === '=') return zoomAt(1.25, ...centre()); else if (k === '-') return zoomAt(0.8, ...centre()); else return;
285
+ e.preventDefault(); clamp(); draw();
286
+ });
287
+ const centre = () => { const r = cv.getBoundingClientRect(); return [r.left + r.width / 2, r.top + r.height / 2]; };
288
+
289
+ function focus(p) {
290
+ const r = cv.getBoundingClientRect();
291
+ cam.s = Math.max(cam.s, r.width < 700 ? 1.8 : 2);
292
+ cam.x = p.x + p.w / 2 - r.width / cam.s / 2; cam.y = p.y + p.h / 2 - r.height / cam.s / 2.6;
293
+ clamp(); draw();
294
+ }
295
+
296
+ // ---------------------------------------------------------------- panel
297
+ const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
298
+ function host(u) { try { return new URL(u).hostname.replace(/^www\./, ''); } catch { return ''; } }
299
+ function select(p) {
300
+ selected = p; draw();
301
+ if (!p) { panel.hidden = true; return; }
302
+ const i = infoOf(p);
303
+ const tier = p.tier === 'house' ? null : TIERS[p.tier];
304
+ const where = p.tier === 'house' ? DISTRICTS.yard : DISTRICTS[p.district];
305
+ let html;
306
+ if (i.state === 'live') {
307
+ const logo = i.logo ? `<img src="${esc(i.logo.startsWith('/') ? BASE + i.logo : i.logo)}" alt="" width="56" height="56">` : `<span class="mono" style="background:${esc(i.colour)}">${esc((i.name || '?')[0])}</span>`;
308
+ html = `<div class="ph">${logo}<div><h3>${esc(i.name)}</h3><p>${esc(i.pitch)}</p></div></div>
309
+ <p class="meta">Plot ${esc(p.id)} · ${esc(tier ? tier.name : 'CyberMax house plot')} · ${esc(where)}${p.tier === 'house' ? ' · <b>one of CyberMax\'s own products</b>' : ''}${i.featured ? ' · <b class="feat">Featured</b>' : ''}</p>
310
+ <p class="row"><a class="btn" href="${BASE}/go/${esc(p.id)}" rel="nofollow ugc" target="_blank">Visit ${esc(host(i.url))}</a><a class="btn ghost" href="${BASE}/p/${esc(p.id)}">Share page</a></p>`;
311
+ } else if (i.state === 'reserved' || i.state === 'sold') {
312
+ html = `<h3>Plot ${esc(p.id)}: ${esc(tier.name)}</h3><p>${i.state === 'sold' ? 'Claimed. The owner is putting up their sign.' : 'Someone is checking out on this plot right now. Try a neighbour.'}</p><p class="meta">${esc(where)}</p>`;
313
+ } else {
314
+ const can = state.checkout;
315
+ html = `<h3>Plot ${esc(p.id)}: ${esc(tier.name)} · <span class="price">${usd(tier.price)}</span> <small>paid once</small></h3><p>${esc(tier.blurb)}</p>
316
+ <p class="meta">${esc(where)} · your logo, name, one-line pitch and link · share page + badge · real visit counts</p>
317
+ <p class="row">${can ? `<a class="btn" id="claim" href="${BASE}/buy/${esc(p.id)}?s=${encodeURIComponent(SRC)}">Claim plot ${esc(p.id)} for ${usd(tier.price)}</a>` : '<span class="btn disabled">Checkout opens soon</span>'}<a class="btn ghost" href="${BASE}/p/${esc(p.id)}">Plot page</a></p>
318
+ <p class="fine">Fixed price, no bidding, nobody can take it over. 14-day fix-or-refund if it doesn't work as described. <a href="${BASE}/terms/">Terms</a></p>`;
319
+ }
320
+ panel.innerHTML = `<button class="x" aria-label="Close" onclick="this.parentElement.hidden=true">×</button>${html}`;
321
+ panel.hidden = false;
322
+ history.replaceState(null, '', `?plot=${p.id}${location.hash}`);
323
+ }
324
+
325
+ // ---------------------------------------------------------------- lists below the map (featured strip, directory, stock)
326
+ function lists() {
327
+ const live = [...PLOTS.map((p) => ({ p, i: state.plots[p.id] })).filter((x) => x.i?.state === 'live'), ...HOUSE_PLOTS().map((p) => ({ p, i: p.live }))];
328
+ live.sort((a, b) => (b.i.featured ? 1 : 0) - (a.i.featured ? 1 : 0) || (b.i.since || 0) - (a.i.since || 0));
329
+ const card = ({ p, i }) => `<li><button type="button" data-plot="${esc(p.id)}">${i.logo ? `<img src="${esc(i.logo.startsWith('/') ? BASE + i.logo : i.logo)}" alt="" width="28" height="28" loading="lazy">` : `<span class="mono" style="background:${esc(i.colour)}">${esc((i.name || '?')[0])}</span>`}<span><b>${esc(i.name)}</b><small>${esc(i.pitch)}</small></span>${p.tier === 'house' ? '<em>CyberMax</em>' : i.featured ? '<em class="feat">Featured</em>' : ''}</button></li>`;
330
+ const dir = document.getElementById('dir');
331
+ dir.innerHTML = live.map(card).join('');
332
+ const feat = live.filter((x) => x.i.featured);
333
+ document.getElementById('featured').innerHTML = feat.length ? feat.map(card).join('') : `<li class="empty">No featured makers yet. Own a plot? Feature it for ${usd(FEATURED.price)}/month.</li>`;
334
+ for (const t of Object.keys(TIERS)) {
335
+ const left = PLOTS.filter((p) => p.tier === t && !state.plots[p.id]).length;
336
+ const el = document.querySelector(`[data-left="${t}"]`);
337
+ if (el) el.textContent = `${left} of ${PLOTS.filter((p) => p.tier === t).length} free`;
338
+ }
339
+ const sold = Object.values(state.plots).filter((x) => x.state === 'live' || x.state === 'sold').length;
340
+ document.getElementById('count').textContent = sold ? `${sold} plot${sold === 1 ? '' : 's'} claimed so far` : 'Opening week: every plot is free to claim';
341
+ }
342
+ document.addEventListener('click', (e) => {
343
+ const b = e.target.closest('[data-plot]');
344
+ if (!b) return;
345
+ const p = [...HOUSE_PLOTS(), ...PLOTS].find((x) => x.id === b.dataset.plot);
346
+ if (p) { cv.scrollIntoView({ behavior: reduced ? 'auto' : 'smooth', block: 'center' }); focus(p); select(p); }
347
+ });
348
+ document.querySelectorAll('[data-pick]').forEach((b) => b.addEventListener('click', () => { // "pick a free X for me"
349
+ const free = PLOTS.filter((p) => p.tier === b.dataset.pick && !state.plots[p.id]);
350
+ if (!free.length) return;
351
+ const p = free[(Math.random() * Math.min(free.length, 6)) | 0];
352
+ cv.scrollIntoView({ behavior: reduced || innerWidth < 700 ? 'auto' : 'smooth', block: 'center' }); focus(p); select(p);
353
+ }));
354
+
355
+ // ---------------------------------------------------------------- boot
356
+ async function load() {
357
+ try {
358
+ const r = await fetch(`${BASE}/api/town`, { cache: 'no-store' });
359
+ if (!r.ok) throw new Error(r.status);
360
+ const j = await r.json();
361
+ state.plots = j.plots || {}; state.house = j.house || []; state.checkout = j.checkout !== false;
362
+ } catch {
363
+ try { const j = await (await fetch('./house.json')).json(); state.house = j.house; } catch { /* map still works */ }
364
+ }
365
+ state.loaded = true;
366
+ lists(); draw();
367
+ const q = new URLSearchParams(location.search).get('plot');
368
+ const p = q && [...HOUSE_PLOTS(), ...PLOTS].find((x) => x.id === q);
369
+ if (p) { focus(p); select(p); }
370
+ }
371
+ drawBackground();
372
+ addEventListener('resize', () => { fit(); });
373
+ fit();
374
+ load();
375
+ window.__plotwren = { state, cam, select: (id) => select([...HOUSE_PLOTS(), ...PLOTS].find((x) => x.id === id)) }; // for tests and the demo recorder
brand/cover.png ADDED

Git LFS Details

  • SHA256: 6b7ac8bc362458a7d038b8f8331fab2cba47ef25b28de72065e64bbde1cb0f23
  • Pointer size: 131 Bytes
  • Size of remote file: 367 kB
brand/icon.png ADDED
brand/og.png ADDED

Git LFS Details

  • SHA256: 6e63adee94fdf48449124151e6e61dfa8da896391648178316773f4154c69c08
  • Pointer size: 131 Bytes
  • Size of remote file: 295 kB
e3a69b3cf2f5db7a1df9abcd5efda221.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ e3a69b3cf2f5db7a1df9abcd5efda221
house.json ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "house": [
3
+ {
4
+ "id": "Y1",
5
+ "name": "CyberMax Store",
6
+ "pitch": "Useful software, games and data tools from $5.",
7
+ "url": "https://cybermaxtools.com/store/",
8
+ "logo": "/seed/cybermax-store.png",
9
+ "colour": "#2e86ab"
10
+ },
11
+ {
12
+ "id": "Y2",
13
+ "name": "Shiftmoor",
14
+ "pitch": "Get an alert when any web page changes. Free for 3 pages.",
15
+ "url": "https://shiftmoor.cybermaxtools.com/",
16
+ "logo": "/seed/shiftmoor.png",
17
+ "colour": "#7b2cbf"
18
+ },
19
+ {
20
+ "id": "Y3",
21
+ "name": "Pricewick",
22
+ "pitch": "Track competitor prices and stock without spreadsheets.",
23
+ "url": "https://pricewick.cybermaxtools.com/",
24
+ "logo": "/seed/pricewick.png",
25
+ "colour": "#e4572e"
26
+ },
27
+ {
28
+ "id": "Y4",
29
+ "name": "Snipbare",
30
+ "pitch": "White-background product photos in your browser.",
31
+ "url": "https://snipbare.cybermaxtools.com/",
32
+ "logo": "/seed/cutout.png",
33
+ "colour": "#00a6a6"
34
+ },
35
+ {
36
+ "id": "Y5",
37
+ "name": "Ledgermoor",
38
+ "pitch": "Bank statement PDFs to Excel and QuickBooks CSV.",
39
+ "url": "https://ledgermoor.cybermaxtools.com/",
40
+ "logo": "/seed/ledgermoor.png",
41
+ "colour": "#29bf12"
42
+ },
43
+ {
44
+ "id": "Y6",
45
+ "name": "Citewren",
46
+ "pitch": "Check if AI search engines can find and cite your site.",
47
+ "url": "https://citewren.cybermaxtools.com/",
48
+ "logo": "/seed/citewren.png",
49
+ "colour": "#f3a712"
50
+ },
51
+ {
52
+ "id": "Y7",
53
+ "name": "Figurewell API",
54
+ "pitch": "Point-in-time SEC financials for quants and researchers.",
55
+ "url": "https://figurewell-api.cybermaxtools.com/",
56
+ "logo": "/seed/figurewell-api.png",
57
+ "colour": "#3d405b"
58
+ },
59
+ {
60
+ "id": "Y8",
61
+ "name": "Pathmoor",
62
+ "pitch": "A free daily path puzzle. New board every day.",
63
+ "url": "https://cybermaxtools.com/games/pathmoor/",
64
+ "logo": "/seed/pathmoor.png",
65
+ "colour": "#ff5d8f"
66
+ }
67
+ ]
68
+ }
icon.svg ADDED
index.html CHANGED
@@ -1,19 +1,116 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
6
+ <title>Plotwren: put your product on the map from $5 | CyberMax maker town</title>
7
+ <meta name="description" content="Plotwren is a pixel town for makers, indie devs and small businesses. Claim a plot, shop sign or billboard with your logo, pitch and link from $5, paid once. No bidding, no takeovers, real visit counts.">
8
+ <link rel="canonical" href="https://plotwren.cybermaxtools.com/">
9
+ <meta property="og:type" content="website">
10
+ <meta property="og:url" content="https://plotwren.cybermaxtools.com/">
11
+ <meta property="og:title" content="Plotwren: put your product on the map from $5">
12
+ <meta property="og:description" content="A pixel town where makers claim a stall, shop, billboard or landmark with their logo and link. Fixed prices, paid once.">
13
+ <meta property="og:image" content="https://plotwren.cybermaxtools.com/brand/og.png">
14
+ <meta name="twitter:card" content="summary_large_image">
15
+ <meta name="theme-color" content="#2b2d42">
16
+ <link rel="icon" href="/icon.svg">
17
+ <link rel="stylesheet" href="style.css">
18
+ <link rel="preload" href="town.js" as="script" crossorigin>
19
+ <script type="application/ld+json">
20
+ {"@context":"https://schema.org","@graph":[
21
+ {"@type":"WebApplication","name":"Plotwren","url":"https://plotwren.cybermaxtools.com/","applicationCategory":"BusinessApplication","operatingSystem":"Any (web browser)",
22
+ "description":"A pixel town map where makers, indie developers and small businesses buy a fixed-price plot with their logo, one-line pitch and link.",
23
+ "publisher":{"@type":"Organization","name":"CyberMax","url":"https://cybermaxtools.com/"},
24
+ "offers":[
25
+ {"@type":"Offer","name":"Market stall","price":"5.00","priceCurrency":"USD"},
26
+ {"@type":"Offer","name":"Shop","price":"19.00","priceCurrency":"USD"},
27
+ {"@type":"Offer","name":"Billboard","price":"49.00","priceCurrency":"USD"},
28
+ {"@type":"Offer","name":"Landmark","price":"149.00","priceCurrency":"USD"}]},
29
+ {"@type":"VideoObject","name":"Plotwren: claim a plot in the CyberMax maker town","description":"A quick tour of the Plotwren pixel town: pan the map, pick a free plot and open a live sign.","thumbnailUrl":"https://cybermaxtools.com/media/demos/plotwren/poster.jpg","contentUrl":"https://cybermaxtools.com/media/demos/plotwren/demo-16x9.mp4","duration":"PT17S","uploadDate":"2026-10-09"},
30
+ {"@type":"FAQPage","mainEntity":[
31
+ {"@type":"Question","name":"Can someone take over my plot?","acceptedAnswer":{"@type":"Answer","text":"No. Every plot has one fixed price and is sold once. There is no bidding, no outbid button and no resale."}},
32
+ {"@type":"Question","name":"How long does my plot stay up?","acceptedAnswer":{"@type":"Answer","text":"For as long as Plotwren runs, and at least 12 months. If we ever closed the town sooner, we would refund the unused share of those 12 months."}},
33
+ {"@type":"Question","name":"Are the visit counts real?","acceptedAnswer":{"@type":"Answer","text":"Yes. Bots, link previews and prefetches are not counted, and each visitor counts once a day per plot."}}]}
34
+ ]}
35
+ </script>
36
+ </head>
37
+ <body>
38
+ <header class="bar">
39
+ <a class="brand" href="/"><img src="icon.svg" alt="" width="28" height="28"> Plotwren</a>
40
+ <span class="tag">the CyberMax maker town</span>
41
+ <a class="btn small" href="#plots">Claim a plot</a>
42
+ </header>
43
+
44
+ <section class="hero">
45
+ <div class="copy">
46
+ <h1>Put your product on the map, from $5</h1>
47
+ <p class="lead">A pixel town for makers, indie devs and small businesses. Claim a stall, shop, billboard or landmark with your <b>logo, one-line pitch and link</b>. Paid once. Nobody can outbid you or take it over.</p>
48
+ <p class="row"><button class="btn" type="button" data-pick="stall">Find me a $5 stall</button><a class="btn ghost" href="#plots">See all plots</a></p>
49
+ <p class="fine" id="count">Loading the town…</p>
50
+ </div>
51
+ <video class="demo" src="https://cybermaxtools.com/media/demos/plotwren/demo.webm" poster="https://cybermaxtools.com/media/demos/plotwren/poster.jpg" autoplay muted loop playsinline preload="metadata" width="480" height="270" aria-label="17-second tour of the Plotwren town"><source src="https://cybermaxtools.com/media/demos/plotwren/demo.webm" type="video/webm"><source src="https://cybermaxtools.com/media/demos/plotwren/demo-16x9.mp4" type="video/mp4"></video>
52
+ </section>
53
+
54
+ <section class="mapwrap" aria-label="Town map">
55
+ <canvas id="town" tabindex="0" aria-label="Plotwren town map. Drag to move, pinch or scroll to zoom, tap a plot to open it. A list of every plot is below the map."></canvas>
56
+ <div class="zoom"><button id="zin" type="button" aria-label="Zoom in">+</button><button id="zout" type="button" aria-label="Zoom out">−</button></div>
57
+ <div class="legend"><span><i class="lg free"></i>Free plot</span><span><i class="lg built"></i>Claimed</span><span><i class="lg feat"></i>Featured</span></div>
58
+ <aside id="panel" class="panel" hidden aria-live="polite"></aside>
59
+ </section>
60
+
61
+ <section class="wrap">
62
+ <h2>Featured makers</h2>
63
+ <ul id="featured" class="cards"></ul>
64
+ </section>
65
+
66
+ <section class="wrap" id="plots">
67
+ <h2>Pick your plot</h2>
68
+ <p class="lead">One fixed price per size, paid once. Your sign shows your logo, name, one-line pitch and link, with its own share page, an "I'm on Plotwren" badge and real visit counts.</p>
69
+ <div class="tiers">
70
+ <article class="tier"><h3>Market stall</h3><p class="price">$5 <small>once</small></p><p>A stall on Market Row. The easiest way into town.</p><p class="left" data-left="stall"></p><button class="btn" type="button" data-pick="stall">Pick a free stall</button></article>
71
+ <article class="tier"><h3>Shop</h3><p class="price">$19 <small>once</small></p><p>A shop on Maker Street with a big sign for your logo.</p><p class="left" data-left="shop"></p><button class="btn" type="button" data-pick="shop">Pick a free shop</button></article>
72
+ <article class="tier hot"><h3>Billboard</h3><p class="price">$49 <small>once</small></p><p>Launch Boulevard, the road everyone crosses. Your name in big letters.</p><p class="left" data-left="billboard"></p><button class="btn" type="button" data-pick="billboard">Pick a free billboard</button></article>
73
+ <article class="tier"><h3>Landmark</h3><p class="price">$149 <small>once</small></p><p>One of only four towers on Plotwren Square, the first thing visitors see.</p><p class="left" data-left="landmark"></p><button class="btn" type="button" data-pick="landmark">Pick a free landmark</button></article>
74
+ </div>
75
+ <p class="fine">Optional: <b>Featured, $9/month</b> pins your plot to the Featured strip, adds a glowing outline on the map and lists you first. Cancel any time. Prices in USD; tax, if any, is shown at checkout.</p>
76
+ </section>
77
+
78
+ <section class="wrap">
79
+ <h2>How it works</h2>
80
+ <ol class="steps">
81
+ <li><b>Pick a free plot</b> on the map (or let us pick one for you).</li>
82
+ <li><b>Pay once</b> with Stripe. The plot is held for you while you check out.</li>
83
+ <li><b>Add your sign</b>: name, one-line pitch, link and logo, from your private edit link. Most signs go live in seconds.</li>
84
+ <li><b>Share it</b>: your plot has its own page, a badge for your site and real visit counts.</li>
85
+ </ol>
86
+ </section>
87
+
88
+ <section class="wrap">
89
+ <h2>Everyone in town</h2>
90
+ <p class="fine">Plots marked CyberMax are our own products.</p>
91
+ <ul id="dir" class="cards"></ul>
92
+ </section>
93
+
94
+ <section class="wrap faq">
95
+ <h2>Questions</h2>
96
+ <details><summary>Can someone take over my plot?</summary><p>No. Each plot has one fixed price and is sold once. There's no bidding, no "outbid" button and no resale.</p></details>
97
+ <details><summary>How long does my plot stay up?</summary><p>For as long as Plotwren runs, and at least 12 months. If we ever closed the town sooner, we'd refund the unused share of those 12 months.</p></details>
98
+ <details><summary>Can I change my sign later?</summary><p>Yes, as often as you like, from your private edit link. Each edit is checked automatically and most go live in seconds.</p></details>
99
+ <details><summary>What isn't allowed?</summary><p>Adult content, illegal goods or services, hate, scams, malware or phishing, and links that hide where they go (shorteners). Every sign is checked automatically and borderline ones get a quick human look. See the <a href="/terms/">terms</a>.</p></details>
100
+ <details><summary>Are the visit counts real?</summary><p>Yes. We don't count bots, link previews or prefetches, and each visitor counts once a day per plot. We don't sell traffic or invent numbers.</p></details>
101
+ <details><summary>What if it doesn't work for me?</summary><p>14-day fix-or-refund: if your plot doesn't work as described, tell us and we'll fix it or refund you.</p></details>
102
+ <details><summary>Why not a launch directory?</summary><p>Launch days last a day. A plot stays on the map, keeps its share page and badge, and costs less than one featured week on most directories.</p></details>
103
+ </section>
104
+
105
+ <section class="wrap cta">
106
+ <h2>Your sign could be up in two minutes</h2>
107
+ <p class="row"><button class="btn" type="button" data-pick="stall">Claim a $5 stall</button><button class="btn ghost" type="button" data-pick="shop">Claim a $19 shop</button></p>
108
+ <p class="fine">Also from CyberMax: <a href="https://cybermaxtools.com/store/?s=plotwren">the CyberMax Store</a>, <a href="https://citewren.cybermaxtools.com/?s=plotwren">Citewren</a> (can AI search cite your site?) and <a href="https://shiftmoor.cybermaxtools.com/?s=plotwren">Shiftmoor</a> (watch competitor pages).</p>
109
+ </section>
110
+
111
+ <div class="buybar"><span>Plots from <b>$5</b>, paid once</span><button class="btn small" type="button" data-pick="stall">Claim one</button></div>
112
+
113
+ <footer class="foot"><a href="/terms/">Terms &amp; refunds</a> · <a href="/llms.txt">llms.txt</a> · <a href="https://cybermaxtools.com/?s=plotwren">CyberMax</a> · © 2026 CyberMax</footer>
114
+ <script type="module" src="app.js"></script>
115
+ </body>
116
  </html>
llms.txt ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Plotwren
2
+
3
+ > Plotwren is the CyberMax maker town: a pixel town map in the browser where makers, indie developers and small businesses claim a fixed-price plot that shows their logo, name, one-line pitch and link. Plots are sold once at a fixed price; there is no bidding, takeover or resale.
4
+
5
+ ## Prices (USD, paid once)
6
+ - Market stall: $5 (Market Row)
7
+ - Shop: $19 (Maker Street)
8
+ - Billboard: $49 (Launch Boulevard)
9
+ - Landmark: $149 (Plotwren Square, four in total)
10
+ - Optional Featured upgrade: $9/month (Featured strip, glowing outline, first in the directory; cancel any time)
11
+
12
+ ## How it works
13
+ 1. Pick a free plot on the map at https://plotwren.cybermaxtools.com/ (or open /buy/<plot id>, e.g. /buy/M001).
14
+ 2. Pay with Stripe Checkout; the plot is held for 30 minutes while you pay.
15
+ 3. Add your sign from the private edit link shown after checkout. Every edit is checked automatically (text, link, image); most go live in seconds.
16
+ 4. Share your plot page (/p/<plot id>) and badge (/badge/<plot id>.svg). Visit counts are real visitors only.
17
+
18
+ ## Pages
19
+ - [Town map](https://plotwren.cybermaxtools.com/): the live pixel town and plot directory
20
+ - [Terms and refunds](https://plotwren.cybermaxtools.com/terms/): content rules, 14-day fix-or-refund
21
+ - [Town data (JSON)](https://plotwren.cybermaxtools.com/api/town): live plots, tiers and prices
22
+
23
+ Plots marked CyberMax are CyberMax's own products. © 2026 CyberMax.
robots.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ User-agent: *
2
+ Allow: /
3
+ Disallow: /buy/
4
+ Disallow: /welcome
5
+ Disallow: /edit/
6
+ Disallow: /go/
7
+ Disallow: /admin/
8
+ Disallow: /api/
9
+
10
+ Sitemap: https://plotwren.cybermaxtools.com/sitemap.xml
seed/citewren.png ADDED
seed/cutout.png ADDED
seed/cybermax-store.png ADDED
seed/figurewell-api.png ADDED
seed/ledgermoor.png ADDED
seed/pathmoor.png ADDED
seed/pricewick.png ADDED
seed/shiftmoor.png ADDED
style.css CHANGED
@@ -1,28 +1,101 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Plotwren. © 2026 CyberMax. All rights reserved. */
2
+ :root { --ink: #2b2d42; --paper: #fff8e7; --grass: #8fd16a; --accent: #e4572e; --gold: #ffd60a; --muted: #5c5f73; --line: #e6dcc6; --card: #fffdf6; }
3
+ * { box-sizing: border-box; }
4
+ html { -webkit-text-size-adjust: 100%; }
5
+ body { margin: 0; font: 16px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; color: var(--ink); background: var(--paper); }
6
+ a { color: #1d5f8a; }
7
+ h1, h2, h3 { font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace; letter-spacing: -0.02em; line-height: 1.15; }
8
+ h1 { font-size: clamp(28px, 5vw, 44px); margin: 0 0 10px; }
9
+ h2 { font-size: 24px; margin: 32px 0 10px; }
10
+ .bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; background: var(--ink); color: #fff; position: sticky; top: 0; z-index: 5; }
11
+ .bar .brand { display: flex; align-items: center; gap: 8px; color: #fff; text-decoration: none; font: 700 18px ui-monospace, monospace; }
12
+ .bar .brand img { image-rendering: pixelated; }
13
+ .bar .tag { color: #c9f2b0; font-size: 13px; flex: 1; }
14
+ .btn { display: inline-flex; align-items: center; justify-content: center; min-height: 44px; padding: 10px 16px; border: 3px solid var(--ink); background: var(--accent); color: #fff; font: 700 15px ui-monospace, monospace; text-decoration: none; cursor: pointer; box-shadow: 3px 3px 0 var(--ink); border-radius: 0; }
15
+ .btn:hover { transform: translate(-1px, -1px); box-shadow: 4px 4px 0 var(--ink); }
16
+ .btn:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--ink); }
17
+ .btn.ghost { background: #fff; color: var(--ink); }
18
+ .btn.small { min-height: 36px; padding: 6px 12px; font-size: 13px; box-shadow: 2px 2px 0 #000; border-color: #000; }
19
+ .btn.disabled { background: #bbb; cursor: not-allowed; }
20
+ .row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
21
+ .lead { font-size: 18px; color: #3a3d55; max-width: 62ch; }
22
+ .fine { font-size: 13px; color: var(--muted); }
23
+ .hero { padding: 22px 16px 8px; max-width: 1100px; margin: 0 auto; display: grid; grid-template-columns: 1fr auto; gap: 24px; align-items: center; }
24
+ .hero .demo { width: 400px; max-width: 100%; height: auto; aspect-ratio: 16 / 9; border: 3px solid var(--ink); box-shadow: 4px 4px 0 var(--ink); background: var(--grass); }
25
+ .mapwrap { position: relative; height: min(68vh, 640px); min-height: 380px; margin: 8px auto 0; max-width: 1400px; border-top: 4px solid var(--ink); border-bottom: 4px solid var(--ink); background: var(--grass); }
26
+ #town { display: block; width: 100%; height: 100%; touch-action: none; cursor: grab; image-rendering: pixelated; outline: none; }
27
+ #town:focus-visible { box-shadow: inset 0 0 0 3px var(--gold); }
28
+ .zoom { position: absolute; right: 12px; top: 12px; display: flex; flex-direction: column; gap: 6px; }
29
+ .zoom button { width: 44px; height: 44px; font: 700 22px ui-monospace, monospace; background: #fff; border: 3px solid var(--ink); box-shadow: 2px 2px 0 var(--ink); cursor: pointer; color: var(--ink); }
30
+ .legend { position: absolute; left: 12px; top: 12px; display: flex; gap: 10px; flex-wrap: wrap; background: rgba(255, 248, 231, .92); border: 2px solid var(--ink); padding: 4px 8px; font-size: 12px; }
31
+ .lg { display: inline-block; width: 12px; height: 12px; margin-right: 4px; vertical-align: -2px; border: 1px solid var(--ink); }
32
+ .lg.free { background: #c99a5b; } .lg.built { background: #2e86ab; } .lg.feat { background: #fff; outline: 2px solid var(--gold); }
33
+ .panel { position: absolute; right: 12px; bottom: 12px; width: min(380px, calc(100% - 24px)); background: var(--card); border: 3px solid var(--ink); box-shadow: 4px 4px 0 var(--ink); padding: 14px 16px; }
34
+ .panel h3 { margin: 0 0 6px; font-size: 18px; padding-right: 28px; }
35
+ .panel .x { position: absolute; right: 6px; top: 4px; width: 36px; height: 36px; border: 0; background: none; font-size: 26px; cursor: pointer; color: var(--ink); }
36
+ .panel .ph { display: flex; gap: 12px; align-items: center; }
37
+ .panel .ph img, .mono { width: 56px; height: 56px; border: 2px solid var(--ink); object-fit: contain; background: #fff; flex: none; }
38
+ .mono { display: inline-flex; align-items: center; justify-content: center; color: #fff; font: 700 26px ui-monospace, monospace; }
39
+ .panel p { margin: 6px 0; }
40
+ .meta { font-size: 13px; color: var(--muted); }
41
+ .price { color: var(--accent); font: 700 1.1em ui-monospace, monospace; }
42
+ .feat { color: #9a7b00; font-style: normal; }
43
+ .wrap { max-width: 1100px; margin: 0 auto; padding: 0 16px; }
44
+ .cards { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; }
45
+ .cards button { width: 100%; display: flex; gap: 10px; align-items: center; text-align: left; background: var(--card); border: 2px solid var(--ink); padding: 8px 10px; cursor: pointer; font: inherit; color: inherit; min-height: 56px; }
46
+ .cards button:hover { background: #fff; }
47
+ .cards img, .cards .mono { width: 32px; height: 32px; font-size: 16px; }
48
+ .cards span { display: flex; flex-direction: column; min-width: 0; flex: 1; }
49
+ .cards small { color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
50
+ .cards em { font-size: 11px; font-style: normal; background: var(--ink); color: #fff; padding: 1px 6px; }
51
+ .cards em.feat { background: var(--gold); color: var(--ink); }
52
+ .cards .empty { color: var(--muted); font-size: 14px; }
53
+ .tiers { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; }
54
+ .tier { background: var(--card); border: 3px solid var(--ink); padding: 14px; box-shadow: 4px 4px 0 var(--ink); display: flex; flex-direction: column; }
55
+ .tier.hot { background: #fff4c2; }
56
+ .tier h3 { margin: 0; }
57
+ .tier .price { font-size: 30px; margin: 6px 0; }
58
+ .tier .price small { font-size: 13px; color: var(--muted); }
59
+ .tier .left { font-size: 13px; color: var(--muted); margin-top: auto; }
60
+ .steps li { margin: 6px 0; }
61
+ .faq details { border-bottom: 2px solid var(--line); padding: 10px 0; }
62
+ .faq summary { cursor: pointer; font-weight: 700; }
63
+ .cta { text-align: center; padding-bottom: 30px; }
64
+ .cta .row { justify-content: center; }
65
+ .foot { text-align: center; font-size: 13px; color: var(--muted); padding: 24px 16px 90px; }
66
+ .buybar { display: none; }
67
+ @media (max-width: 700px) {
68
+ .bar .tag { display: none; }
69
+ .bar .brand { flex: 1; }
70
+ .mapwrap { height: 62vh; }
71
+ .hero { padding: 14px 16px 4px; grid-template-columns: 1fr; gap: 8px; }
72
+ .hero .demo { width: 100%; max-height: 190px; }
73
+ .hero h1 { font-size: 26px; }
74
+ .hero .lead { font-size: 15px; margin: 6px 0; }
75
+ .hero .row .ghost { display: none; }
76
+ .hero .fine { margin: 6px 0; }
77
+ .panel { left: 8px; right: 8px; bottom: 8px; width: auto; max-height: 60%; overflow: auto; }
78
+ .legend { font-size: 11px; }
79
+ .buybar { display: flex; position: fixed; left: 0; right: 0; bottom: 0; z-index: 6; gap: 10px; align-items: center; justify-content: space-between; padding: 8px 16px calc(8px + env(safe-area-inset-bottom)); background: var(--ink); color: #fff; }
80
  }
81
+ /* sub pages (share, edit, terms) */
82
+ body.sub .wrap { padding-top: 20px; padding-bottom: 30px; }
83
+ .card { background: var(--card); border: 3px solid var(--ink); padding: 16px; margin: 16px 0; box-shadow: 4px 4px 0 var(--ink); }
84
+ .plotcard { display: flex; gap: 18px; align-items: flex-start; border-top: 14px solid var(--c, var(--accent)); }
85
+ .plotcard .sign img, .plotcard .sign .mono { width: 96px; height: 96px; font-size: 44px; border: 3px solid var(--ink); background: #fff; object-fit: contain; }
86
+ .plotcard .sign .mono { background: var(--c); }
87
+ .form label { display: block; margin: 10px 0; font-weight: 600; }
88
+ .form input[type=text], .form input:not([type]), .form input[type=url], .form input[type=file], textarea { display: block; width: 100%; margin-top: 4px; padding: 10px; border: 2px solid var(--ink); font: inherit; background: #fff; }
89
+ .form fieldset { border: 2px solid var(--line); margin: 10px 0; display: flex; gap: 8px; flex-wrap: wrap; }
90
+ .form .row { display: flex; gap: 8px; align-items: center; font-weight: 400; }
91
+ .sw { position: relative; display: inline-block; margin: 0 !important; }
92
+ .sw input { position: absolute; opacity: 0; }
93
+ .sw span { display: block; width: 34px; height: 34px; background: var(--c); border: 3px solid var(--ink); }
94
+ .sw input:checked + span { outline: 3px solid var(--gold); outline-offset: 2px; }
95
+ .sw input:focus-visible + span { outline: 3px solid #1d5f8a; }
96
+ .note { padding: 10px 14px; border: 3px solid var(--ink); margin: 12px 0; }
97
+ .note.ok { background: #d8f5c4; } .note.warn { background: #fff1b8; } .note.bad { background: #ffd6cc; }
98
+ .ok { color: #1e7a12; }
99
+ .center { text-align: center; }
100
+ .terms h2 { font-size: 20px; }
101
+ @media (max-width: 700px) { .plotcard { flex-direction: column; } }
terms/index.html ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
3
+ <title>Terms and refund policy · Plotwren</title>
4
+ <meta name="description" content="Plotwren terms: what you buy, how long a plot stays up, content rules, moderation and the 14-day fix-or-refund policy.">
5
+ <link rel="canonical" href="https://plotwren.cybermaxtools.com/terms/"><link rel="icon" href="/icon.svg"><link rel="stylesheet" href="/style.css"></head>
6
+ <body class="sub"><header class="bar"><a class="brand" href="/"><img src="/icon.svg" alt="" width="28" height="28"> Plotwren</a><span class="tag">the CyberMax maker town</span><a class="btn small" href="/#plots">Claim a plot</a></header>
7
+ <main class="wrap terms">
8
+ <h1>Terms and refund policy</h1>
9
+ <p class="fine">Last updated 9 October 2026. Plotwren is run by CyberMax. Questions: <a href="mailto:cybermax.tools@gmail.com">cybermax.tools@gmail.com</a>.</p>
10
+
11
+ <h2>1. What you buy</h2>
12
+ <p>A plot is a display spot on the Plotwren town map that shows the name, one-line pitch, link and logo you provide, plus a share page, a badge and visit counts. It is an advertising placement on our website. It is not land, property, a token or an investment, and it has no resale value. You can't resell or transfer it through Plotwren, and nobody can buy it away from you.</p>
13
+ <ul>
14
+ <li>Market stall: $5, paid once</li>
15
+ <li>Shop: $19, paid once</li>
16
+ <li>Billboard: $49, paid once</li>
17
+ <li>Landmark: $149, paid once</li>
18
+ <li>Featured (optional, for a plot you own): $9 a month, cancel any time from your Stripe receipt or by emailing us; it stays on until the end of the paid month</li>
19
+ </ul>
20
+ <p>Prices are in US dollars. Any sales tax or VAT is shown at checkout. Payments are processed by Stripe; we never see your card number.</p>
21
+
22
+ <h2>2. How long a plot stays up</h2>
23
+ <p>Your plot stays on the map for as long as Plotwren runs, and at least 12 months from purchase. If we ever closed Plotwren within those 12 months, we would refund the unused share of the 12 months, pro rata.</p>
24
+
25
+ <h2>3. Your content</h2>
26
+ <p>You can change your sign as often as you like from your private edit link. Keep that link private: anyone with it can edit your plot. If you lose it, email us from the address on your Stripe receipt and we'll resend it.</p>
27
+ <p>You confirm that you have the right to use the name, logo and link you submit, and you give CyberMax permission to show them on Plotwren, on your plot's share page and badge, and in images of the town that promote Plotwren.</p>
28
+
29
+ <h2>4. Content rules and moderation</h2>
30
+ <p>Every submission is checked automatically before it appears: text limits, a banned-words list, the link (https only, a real public website, no link shorteners, no IP addresses, no hidden credentials, and a check against a public malware list when available) and the image (PNG, JPEG, WebP or GIF only, up to 96 KB and 1024×1024 px). Borderline submissions wait for a quick human review. We refuse, and may remove at any time:</p>
31
+ <ul>
32
+ <li>sexual or adult content;</li>
33
+ <li>illegal goods or services, including drugs, weapons, counterfeit goods, stolen data and hacking services;</li>
34
+ <li>hate, harassment or violence;</li>
35
+ <li>scams, phishing, malware, "guaranteed returns" and get-rich-quick schemes;</li>
36
+ <li>content that infringes someone else's trademark or copyright, or impersonates another person or company;</li>
37
+ <li>links that redirect somewhere other than what the sign says.</li>
38
+ </ul>
39
+ <p>If your edit is refused, your previous sign stays up and you can submit a new one. If a plot breaks these rules after it goes live (for example, the linked site changes), we may hide the sign until it's fixed. If we remove a plot for a serious breach (illegal content, malware, scams), we don't refund it.</p>
40
+
41
+ <h2>5. Visit counts</h2>
42
+ <p>We show real visits only: page views of your share page and clicks from Plotwren to your link. Bots, link previews and prefetches aren't counted, and each visitor counts once a day per plot. We store a daily, salted hash for this, never your visitors' IP addresses. We don't promise any number of visits or sales.</p>
43
+
44
+ <h2>6. Refunds: 14-day fix-or-refund</h2>
45
+ <p>If your plot doesn't work as described (for example, your sign won't publish or your link doesn't work from the map) tell us within 14 days of purchase. We'll fix it, or refund you in full if we can't. If two people pay for the same plot at the same moment, the second buyer gets the nearest free plot of the same size; if none is left, we refund them in full. A refunded plot goes back on sale.</p>
46
+ <p>Featured renews monthly until you cancel. Cancelling stops the next charge; the current month isn't refunded unless Featured didn't work as described.</p>
47
+
48
+ <h2>7. CyberMax house plots</h2>
49
+ <p>Plots in CyberMax Yard, and any plot marked "CyberMax", show CyberMax's own products. They are not customers and are never for sale.</p>
50
+
51
+ <h2>8. Liability</h2>
52
+ <p>Plotwren is provided as is. We're not responsible for the websites you or other makers link to. To the extent the law allows, our total liability for any plot is limited to what you paid for it.</p>
53
+
54
+ <p><a class="btn" href="/#plots">Pick a plot</a></p>
55
+ </main>
56
+ <footer class="foot"><a href="/">Town map</a> · © 2026 CyberMax</footer>
57
+ </body></html>
town.js ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Plotwren town layout: one fixed map, every plot has a fixed tier and a fixed price. Pure data, shared by the Worker and
2
+ // the browser (build copies it to web/town.js). No auctions, no takeovers, no resale: a plot is sold once at its tier price.
3
+ // © 2026 CyberMax. All rights reserved.
4
+
5
+ export const WORLD = { w: 1536, h: 1024, tile: 16 };
6
+
7
+ // Prices in US cents. Featured is an optional monthly upgrade for any sold plot.
8
+ export const TIERS = {
9
+ stall: { name: 'Market stall', price: 500, w: 32, h: 32, blurb: 'A pixel stall on Market Row with your logo, name and link.' },
10
+ shop: { name: 'Shop', price: 1900, w: 48, h: 48, blurb: 'A shop on Maker Street with a big sign, your logo and a one-line pitch.' },
11
+ billboard: { name: 'Billboard', price: 4900, w: 64, h: 48, blurb: 'A billboard on Launch Boulevard, the road everyone crosses.' },
12
+ landmark: { name: 'Landmark', price: 14900, w: 96, h: 96, blurb: 'One of four landmark towers on Plotwren Square, the centre of town.' },
13
+ };
14
+ export const FEATURED = { name: 'Featured', price: 900, interval: 'month', blurb: 'Pinned to the Featured strip, a glowing outline on the map and first in the directory. Cancel any time.' };
15
+
16
+ export const DISTRICTS = {
17
+ square: 'Plotwren Square',
18
+ boulevard: 'Launch Boulevard',
19
+ street: 'Maker Street',
20
+ market: 'Market Row',
21
+ yard: 'CyberMax Yard',
22
+ };
23
+
24
+ // Roads (world px rectangles) drawn by the renderer; plots never overlap them.
25
+ export const ROADS = [
26
+ { x: 0, y: 448, w: 1536, h: 48 }, // Launch Boulevard (east-west)
27
+ { x: 744, y: 0, w: 48, h: 1024 }, // North-south avenue
28
+ { x: 64, y: 240, w: 640, h: 32 }, // Maker Street (west)
29
+ { x: 832, y: 240, w: 640, h: 32 }, // Maker Street (east)
30
+ { x: 64, y: 704, w: 640, h: 24 }, // Market Row lane (west)
31
+ { x: 832, y: 704, w: 640, h: 24 }, // Market Row lane (east)
32
+ ];
33
+ export const WATER = { x: 0, y: 960, w: 1536, h: 64 };
34
+ export const FOUNTAIN = { x: 744, y: 448, w: 48, h: 48 };
35
+
36
+ const pad = (n, l = 2) => String(n).padStart(l, '0');
37
+
38
+ function build() {
39
+ const plots = [];
40
+ const add = (id, tier, district, x, y) => plots.push({ id, tier, district, x, y, w: TIERS[tier].w, h: TIERS[tier].h });
41
+ // Plotwren Square: four landmarks around the fountain crossing.
42
+ add('L1', 'landmark', 'square', 636, 336); add('L2', 'landmark', 'square', 804, 336);
43
+ add('L3', 'landmark', 'square', 636, 508); add('L4', 'landmark', 'square', 804, 508);
44
+ // Launch Boulevard: 6 billboards each side of the square, north side of the road.
45
+ let b = 1;
46
+ for (let i = 0; i < 6; i++) add(`B${pad(b++)}`, 'billboard', 'boulevard', 72 + i * 92, 392);
47
+ for (let i = 0; i < 6; i++) add(`B${pad(b++)}`, 'billboard', 'boulevard', 912 + i * 92, 392);
48
+ // Maker Street: two rows of shops either side of each half of the street.
49
+ let s = 1;
50
+ for (const x0 of [72, 840]) {
51
+ for (let i = 0; i < 10; i++) add(`H${pad(s++)}`, 'shop', 'street', x0 + i * 62, 184);
52
+ for (let i = 0; i < 10; i++) add(`H${pad(s++)}`, 'shop', 'street', x0 + i * 62, 280);
53
+ }
54
+ // Market Row: stalls in two rows either side of each lane, south of the boulevard.
55
+ let m = 1;
56
+ for (const x0 of [72, 840]) {
57
+ for (const y of [664, 736, 776]) for (let i = 0; i < 16; i++) add(`M${pad(m++, 3)}`, 'stall', 'market', x0 + i * 39, y);
58
+ }
59
+ return plots;
60
+ }
61
+
62
+ // CyberMax Yard: the house plots, north-west corner, never for sale (CyberMax's own products, clearly labelled).
63
+ export const YARD = [
64
+ { id: 'Y1', x: 72, y: 56, w: 64, h: 64 }, { id: 'Y2', x: 152, y: 56, w: 64, h: 64 }, { id: 'Y3', x: 232, y: 56, w: 64, h: 64 },
65
+ { id: 'Y4', x: 312, y: 56, w: 64, h: 64 }, { id: 'Y5', x: 392, y: 56, w: 64, h: 64 }, { id: 'Y6', x: 472, y: 56, w: 64, h: 64 },
66
+ { id: 'Y7', x: 552, y: 56, w: 64, h: 64 }, { id: 'Y8', x: 632, y: 56, w: 64, h: 64 },
67
+ ];
68
+
69
+ export const PLOTS = build();
70
+ export const PLOT_BY_ID = Object.fromEntries(PLOTS.map((p) => [p.id, p]));
71
+ export const priceOf = (id) => TIERS[PLOT_BY_ID[id]?.tier]?.price ?? null;
72
+ export const usd = (cents) => `$${(cents / 100).toFixed(cents % 100 ? 2 : 0)}`;