@onnie81/murdoku-spor 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Caddyfile ADDED
@@ -0,0 +1,8 @@
1
+ # Automatic HTTPS for Murdoku. Caddy obtains + renews a Let's Encrypt
2
+ # certificate for the domain and reverse-proxies to the app container.
3
+ # The domain comes from $MURDOKU_DOMAIN (default games.onniehex.dev).
4
+
5
+ {$MURDOKU_DOMAIN:games.onniehex.dev} {
6
+ encode zstd gzip
7
+ reverse_proxy murdoku:8080
8
+ }
package/README.md CHANGED
@@ -30,8 +30,17 @@ victim** is the killer.
30
30
  long-press to mark X, keyboard shortcuts (1-9, N, X, U, R, E).
31
31
  - **Multilingual** — English and Spanish shipped; clues are structured data
32
32
  rendered through per-language templates, so adding a language is one file.
33
- - **Original art** — 12 hand-drawn SVG suspect portraits, 16 furniture icons,
34
- 8 scene themes, logo no external assets, no fonts to load.
33
+ - **Furniture that matters** — blockers, seats you can be *seated on*
34
+ (armchairs, two-square sofas), beds you can be *lying in*, and wall-mounted
35
+ windows/paintings/mirrors that live on the walls themselves (windows count
36
+ from both sides). Clues use all of it: "was seated on the sofa", "was not
37
+ lying in any bed", "was beside a window".
38
+ - **Original art & sound** — 12 hand-drawn SVG suspect portraits, colorful
39
+ furniture sprites, 8 scene themes; synthesized noir soundtrack and sound
40
+ effects (WebAudio, zero assets, all original).
41
+ - **Phone-first play screen** — fullscreen mode, an always-visible lateral
42
+ suspect overlay that turns translucent while you place, floating controls,
43
+ and a clue bottom-sheet.
35
44
 
36
45
  ## Quick start (Docker)
37
46
 
@@ -50,16 +59,38 @@ docker run --rm -v murdoku-data:/data -v "$PWD:/out" debian \
50
59
 
51
60
  ### HTTPS / domain
52
61
 
53
- Uncomment the `caddy` service in `docker-compose.yml`, set your domain, remove
54
- the direct port mapping, and `docker compose up -d`. Caddy provisions TLS
55
- automatically. (Any reverse proxy works; the app sets `trust proxy`.)
62
+ The stack ships a **Caddy** service that terminates TLS and reverse-proxies the
63
+ app. On `docker compose up -d` it automatically obtains and renews a **Let's
64
+ Encrypt** certificate for the domain no manual certs. Requirements:
65
+
66
+ - ports **80 and 443** open to the internet (Caddy needs 80/443 for the ACME challenge),
67
+ - a DNS **A record** `games.onniehex.dev` → the server's public IP, **DNS-only**
68
+ (not proxied behind another CDN, or the challenge can't reach Caddy).
69
+
70
+ Override the domain with the `MURDOKU_DOMAIN` env var. Port `5000` stays mapped
71
+ straight to the app for direct testing / health checks. The app sets
72
+ `trust proxy`, so `Secure` cookies and client IPs work correctly behind Caddy.
73
+
74
+ ### Cloud delivery (npm)
75
+
76
+ Published as `@onnie81/murdoku-spor`. First-time server setup:
77
+
78
+ ```bash
79
+ curl -fsSL https://registry.npmjs.org/@onnie81/murdoku-spor/-/murdoku-spor-<version>.tgz \
80
+ | tar xz && sudo bash package/scripts/npm-server-bootstrap.sh
81
+ ```
82
+
83
+ This installs Docker/git if missing, unpacks a real git repo (from the bundled
84
+ history) into `/opt/murdoku`, builds, and installs a systemd timer that polls
85
+ npm every 60s and rebuilds on each new published version.
56
86
 
57
87
  ### Configuration
58
88
 
59
89
  | Env | Default | Meaning |
60
90
  |---|---|---|
61
- | `PORT` | `8080` | HTTP port |
91
+ | `PORT` | `8080` | app HTTP port (inside the container) |
62
92
  | `DATA_DIR` | `/data` (container) / `./data` (bare) | SQLite location |
93
+ | `MURDOKU_DOMAIN` | `games.onniehex.dev` | domain Caddy requests a cert for |
63
94
 
64
95
  ## Development
65
96
 
@@ -1,158 +1,219 @@
1
- // Original furniture / object icons, drawn to sit inside a board cell.
1
+ // Colorful original furniture art.
2
+ // FloorIcon — single-cell pieces, viewBox 0 0 100 100
3
+ // WideIcon — sofa & bed spanning two cells, viewBox 0 0 200 100
4
+ // WallIcon — wall-mounted pieces (window/painting/mirror), viewBox 0 0 100 36
2
5
  import React from 'react';
3
6
 
4
- const S = { fill: 'none', stroke: 'currentColor', strokeWidth: 5, strokeLinecap: 'round', strokeLinejoin: 'round' };
5
- const F = { fill: 'currentColor', stroke: 'none' };
7
+ const INK = '#241e33';
8
+ const O = { stroke: INK, strokeWidth: 2.5, strokeLinejoin: 'round', strokeLinecap: 'round' };
9
+ const WOOD = '#8a5a33', WOOD_D = '#6d4426', BRASS = '#c9a227', CREAM = '#f4efe2';
6
10
 
7
- const ICONS = {
11
+ const FLOOR = {
8
12
  piano: (
9
13
  <>
10
- <path {...F} d="M18,30 L66,30 Q84,34 84,52 Q84,70 62,70 L18,70 Z" opacity="0.85" />
11
- <rect {...F} x="18" y="30" width="10" height="40" opacity="0.5" />
12
- <path {...S} strokeWidth="3" d="M28,36 L28,64" opacity="0.6" />
13
- <path {...F} d="M18,74 L60,74 L60,80 L18,80 Z" opacity="0.5" />
14
+ <path {...O} d="M20,28 L70,28 Q86,32 86,52 Q86,72 62,72 L20,72 Z" fill="#2b2436" />
15
+ <path d="M24,32 L66,32 Q80,35 80,50 Q80,66 60,66 L24,66 Z" fill="#3a3350" opacity="0.8" />
16
+ <rect {...O} x="14" y="60" width="46" height="14" rx="3" fill={CREAM} />
17
+ {[20, 27, 34, 41, 48, 55].map(x => <rect key={x} x={x} y="60" width="3" height="8" fill={INK} />)}
18
+ <rect x="20" y="74" width="6" height="12" fill={WOOD_D} />
19
+ <rect x="52" y="74" width="6" height="12" fill={WOOD_D} />
20
+ <circle cx="74" cy="30" r="3" fill={BRASS} />
14
21
  </>
15
22
  ),
16
23
  armchair: (
17
24
  <>
18
- <path {...F} d="M26,34 Q50,24 74,34 L74,58 L26,58 Z" opacity="0.75" />
19
- <rect {...F} x="16" y="46" width="14" height="26" rx="6" opacity="0.9" />
20
- <rect {...F} x="70" y="46" width="14" height="26" rx="6" opacity="0.9" />
21
- <rect {...F} x="26" y="58" width="48" height="14" rx="4" opacity="0.6" />
25
+ <path {...O} d="M24,30 Q50,18 76,30 L76,60 L24,60 Z" fill="#2e7d6e" />
26
+ <rect {...O} x="28" y="52" width="44" height="16" rx="6" fill="#3f9683" />
27
+ <rect {...O} x="14" y="42" width="14" height="30" rx="7" fill="#256a5d" />
28
+ <rect {...O} x="72" y="42" width="14" height="30" rx="7" fill="#256a5d" />
29
+ <rect x="30" y="72" width="7" height="12" fill={WOOD} />
30
+ <rect x="62" y="72" width="7" height="12" fill={WOOD} />
22
31
  </>
23
32
  ),
24
33
  bookshelf: (
25
34
  <>
26
- <rect {...S} x="24" y="18" width="52" height="64" />
27
- <line {...S} strokeWidth="4" x1="24" y1="40" x2="76" y2="40" />
28
- <line {...S} strokeWidth="4" x1="24" y1="60" x2="76" y2="60" />
29
- {[30, 37, 44, 52, 59, 66].map((x, i) => (
30
- <rect key={i} {...F} x={x} y={i % 2 ? 24 : 26} width="5" height={i % 2 ? 13 : 11} opacity="0.7" />
35
+ <rect {...O} x="22" y="14" width="56" height="72" rx="3" fill={WOOD} />
36
+ <rect x="27" y="20" width="46" height="26" fill="#513a20" />
37
+ <rect x="27" y="52" width="46" height="26" fill="#513a20" />
38
+ {[['#e5484d', 29], ['#0090ff', 37], ['#ffc53d', 45], ['#46a758', 52], ['#6e56cf', 60]].map(([f, x], i) => (
39
+ <rect key={i} x={x} y={i % 2 ? 26 : 24} width="7" height={i % 2 ? 20 : 22} fill={f} stroke={INK} strokeWidth="1.5" />
31
40
  ))}
32
- {[32, 41, 50, 59, 66].map((x, i) => (
33
- <rect key={i} {...F} x={x} y={i % 2 ? 45 : 47} width="5" height={i % 2 ? 12 : 10} opacity="0.55" />
41
+ {[['#12a594', 30], ['#e93d82', 38], ['#c9a227', 46], ['#ad7f58', 54], ['#0090ff', 62]].map(([f, x], i) => (
42
+ <rect key={i} x={x} y={i % 2 ? 58 : 56} width="7" height={i % 2 ? 20 : 22} fill={f} stroke={INK} strokeWidth="1.5" />
34
43
  ))}
35
44
  </>
36
45
  ),
37
46
  plant: (
38
47
  <>
39
- <path {...F} d="M34,62 L66,62 L60,84 L40,84 Z" opacity="0.8" />
40
- <path {...S} strokeWidth="4" d="M50,62 L50,42" />
41
- <path {...F} d="M50,44 Q34,42 30,26 Q48,28 50,44 Z" opacity="0.7" />
42
- <path {...F} d="M50,44 Q66,42 70,26 Q52,28 50,44 Z" opacity="0.7" />
43
- <path {...F} d="M50,38 Q50,20 50,16 Q58,26 50,40 Z" opacity="0.7" />
48
+ <path {...O} d="M50,52 Q30,48 26,26 Q46,30 50,52 Z" fill="#46a758" />
49
+ <path {...O} d="M50,52 Q70,48 74,26 Q54,30 50,52 Z" fill="#5a9e66" />
50
+ <path {...O} d="M50,50 Q48,26 50,14 Q58,28 50,50 Z" fill="#3f8d50" />
51
+ <path {...O} d="M34,58 L66,58 L60,84 L40,84 Z" fill="#b5623a" />
52
+ <rect {...O} x="31" y="54" width="38" height="8" rx="3" fill="#c9744a" />
44
53
  </>
45
54
  ),
46
55
  clock: (
47
56
  <>
48
- <rect {...S} x="34" y="14" width="32" height="72" rx="4" />
49
- <circle {...S} strokeWidth="4" cx="50" cy="32" r="11" />
50
- <path {...S} strokeWidth="3" d="M50,32 L50,25 M50,32 L55,34" />
51
- <line {...S} strokeWidth="3" x1="50" y1="48" x2="50" y2="70" opacity="0.7" />
52
- <circle {...F} cx="50" cy="72" r="5" opacity="0.8" />
57
+ <rect {...O} x="34" y="12" width="32" height="76" rx="5" fill={WOOD_D} />
58
+ <rect x="38" y="46" width="24" height="34" rx="3" fill="#513a20" />
59
+ <circle {...O} cx="50" cy="29" r="12" fill={CREAM} />
60
+ <path d="M50,29 L50,21 M50,29 L56,32" stroke={INK} strokeWidth="2.5" strokeLinecap="round" />
61
+ <line x1="50" y1="48" x2="50" y2="70" stroke={BRASS} strokeWidth="3" />
62
+ <circle cx="50" cy="72" r="6" fill={BRASS} stroke={INK} strokeWidth="2" />
53
63
  </>
54
64
  ),
55
65
  chest: (
56
66
  <>
57
- <path {...F} d="M22,42 Q22,26 50,26 Q78,26 78,42 L78,50 L22,50 Z" opacity="0.75" />
58
- <rect {...F} x="22" y="50" width="56" height="26" rx="3" opacity="0.9" />
59
- <rect {...F} x="45" y="44" width="10" height="14" rx="2" opacity="0.5" />
60
- <line {...S} strokeWidth="4" x1="32" y1="28" x2="32" y2="76" opacity="0.45" />
61
- <line {...S} strokeWidth="4" x1="68" y1="28" x2="68" y2="76" opacity="0.45" />
67
+ <path {...O} d="M20,44 Q20,26 50,26 Q80,26 80,44 L80,52 L20,52 Z" fill="#9a6537" />
68
+ <rect {...O} x="20" y="52" width="60" height="26" rx="3" fill={WOOD} />
69
+ <rect x="30" y="28" width="6" height="49" fill={BRASS} stroke={INK} strokeWidth="1.5" />
70
+ <rect x="64" y="28" width="6" height="49" fill={BRASS} stroke={INK} strokeWidth="1.5" />
71
+ <rect {...O} x="44" y="46" width="12" height="14" rx="2" fill={BRASS} />
72
+ <circle cx="50" cy="53" r="2" fill={INK} />
62
73
  </>
63
74
  ),
64
75
  stove: (
65
76
  <>
66
- <rect {...F} x="24" y="38" width="52" height="42" rx="4" opacity="0.85" />
67
- <rect {...S} strokeWidth="4" x="32" y="48" width="24" height="22" opacity="0.7" />
68
- <circle {...F} cx="68" cy="46" r="3.5" opacity="0.6" />
69
- <circle {...F} cx="68" cy="56" r="3.5" opacity="0.6" />
70
- <path {...S} strokeWidth="5" d="M62,38 L62,22 Q62,16 68,16" opacity="0.8" />
71
- </>
72
- ),
73
- bed: (
74
- <>
75
- <path {...S} d="M20,32 L20,76 M80,44 L80,76" />
76
- <path {...F} d="M20,52 L80,52 L80,66 L20,66 Z" opacity="0.7" />
77
- <rect {...F} x="24" y="44" width="18" height="9" rx="4" opacity="0.9" />
78
- <path {...F} d="M20,32 Q26,26 32,32 L32,44 L20,44 Z" opacity="0.5" />
77
+ <rect {...O} x="24" y="36" width="52" height="44" rx="5" fill="#3a3644" />
78
+ <rect {...O} x="32" y="46" width="24" height="22" rx="3" fill="#f76b15" />
79
+ <path d="M36,64 Q40,52 44,64 Q48,52 52,64 Z" fill="#ffc53d" />
80
+ <circle cx="66" cy="46" r="3.5" fill={BRASS} />
81
+ <circle cx="66" cy="56" r="3.5" fill={BRASS} />
82
+ <path {...O} d="M62,36 L62,20 Q62,14 70,14" fill="none" strokeWidth="5" stroke="#3a3644" />
83
+ <rect x="28" y="80" width="8" height="7" fill={INK} />
84
+ <rect x="64" y="80" width="8" height="7" fill={INK} />
79
85
  </>
80
86
  ),
81
87
  desk: (
82
88
  <>
83
- <rect {...F} x="18" y="36" width="64" height="8" rx="2" opacity="0.9" />
84
- <rect {...S} strokeWidth="4" x="24" y="44" width="22" height="18" opacity="0.8" />
85
- <circle {...F} cx="35" cy="53" r="2.5" opacity="0.8" />
86
- <path {...S} d="M76,44 L76,78 M24,62 L24,78" />
87
- <rect {...F} x="52" y="26" width="20" height="10" rx="1" opacity="0.5" transform="rotate(-6 62 31)" />
89
+ <rect {...O} x="16" y="34" width="68" height="9" rx="2" fill={WOOD} />
90
+ <rect {...O} x="22" y="43" width="26" height="24" fill={WOOD_D} />
91
+ <rect x="26" y="47" width="18" height="7" fill={WOOD} stroke={INK} strokeWidth="1.5" />
92
+ <circle cx="35" cy="50.5" r="1.8" fill={BRASS} />
93
+ <rect x="26" y="57" width="18" height="7" fill={WOOD} stroke={INK} strokeWidth="1.5" />
94
+ <circle cx="35" cy="60.5" r="1.8" fill={BRASS} />
95
+ <path d="M76,43 L76,78 M22,67 L22,78" stroke={INK} strokeWidth="4" strokeLinecap="round" />
96
+ <rect x="50" y="27" width="22" height="8" rx="1" fill={CREAM} stroke={INK} strokeWidth="1.5" transform="rotate(-5 61 31)" />
97
+ <rect x="52" y="35" width="26" height="4" fill="#3f5c57" />
88
98
  </>
89
99
  ),
90
100
  statue: (
91
101
  <>
92
- <rect {...F} x="32" y="70" width="36" height="12" opacity="0.85" />
93
- <rect {...F} x="38" y="62" width="24" height="8" opacity="0.6" />
94
- <path {...F} d="M44,62 Q40,48 46,40 Q42,34 47,28 Q44,20 50,18 Q56,20 53,28 Q58,34 54,40 Q60,48 56,62 Z" opacity="0.8" />
102
+ <rect {...O} x="30" y="72" width="40" height="12" fill="#7d7691" />
103
+ <rect {...O} x="36" y="62" width="28" height="10" fill="#9a93b0" />
104
+ <path {...O} d="M44,62 Q40,48 46,40 Q42,34 47,28 Q44,20 50,18 Q56,20 53,28 Q58,34 54,40 Q60,48 56,62 Z" fill="#b8b2c4" />
105
+ <circle cx="50" cy="22" r="1.5" fill="#8d87a0" />
95
106
  </>
96
107
  ),
97
108
  telescope: (
98
109
  <>
99
- <path {...F} d="M28,58 L64,26 L72,36 L36,68 Z" opacity="0.85" />
100
- <path {...F} d="M62,20 L78,34 L74,39 L58,25 Z" opacity="0.6" />
101
- <path {...S} d="M46,60 L36,84 M46,60 L58,84" />
102
- <circle {...F} cx="47" cy="59" r="6" opacity="0.9" />
110
+ <path {...O} d="M28,58 L62,28 L72,40 L38,68 Z" fill={BRASS} />
111
+ <path {...O} d="M60,22 L78,36 L73,42 L56,28 Z" fill="#a3821a" />
112
+ <rect x="40" y="48" width="14" height="6" fill="#a3821a" transform="rotate(-41 47 51)" />
113
+ <path d="M46,62 L34,86 M46,62 L60,86" stroke={INK} strokeWidth="5" strokeLinecap="round" />
114
+ <circle {...O} cx="47" cy="60" r="7" fill="#3a3644" />
115
+ <circle cx="76" cy="33" r="2.5" fill="#e6f4ff" />
103
116
  </>
104
117
  ),
105
118
  lamp: (
106
119
  <>
107
- <path {...F} d="M34,18 L66,18 L58,42 L42,42 Z" opacity="0.8" />
108
- <line {...S} x1="50" y1="42" x2="50" y2="78" />
109
- <path {...S} d="M36,84 Q50,76 64,84" />
120
+ <ellipse cx="50" cy="34" rx="26" ry="10" fill="#ffd76a" opacity="0.35" />
121
+ <path {...O} d="M32,16 L68,16 L59,40 L41,40 Z" fill="#ffc53d" />
122
+ <line x1="50" y1="40" x2="50" y2="78" stroke={BRASS} strokeWidth="4" />
123
+ <path {...O} d="M34,84 Q50,76 66,84 Z" fill={WOOD_D} />
110
124
  </>
111
125
  ),
112
- mirror: (
126
+ crate: (
113
127
  <>
114
- <ellipse {...S} cx="50" cy="42" rx="20" ry="26" />
115
- <ellipse {...F} cx="50" cy="42" rx="14" ry="20" opacity="0.25" />
116
- <path {...S} strokeWidth="3" d="M42,36 Q46,28 54,28" opacity="0.7" />
117
- <path {...S} d="M40,70 L36,84 M60,70 L64,84 M38,80 L62,80" />
128
+ <rect {...O} x="22" y="28" width="56" height="50" fill="#c9a06a" />
129
+ <line x1="22" y1="45" x2="78" y2="45" stroke={WOOD_D} strokeWidth="3" />
130
+ <line x1="22" y1="62" x2="78" y2="62" stroke={WOOD_D} strokeWidth="3" />
131
+ <rect x="38" y="32" width="24" height="9" rx="1.5" fill={CREAM} stroke={INK} strokeWidth="1.5" />
132
+ {[27, 73].map(x => [52, 69].map(y => <circle key={x + ':' + y} cx={x} cy={y} r="1.8" fill={WOOD_D} />))}
118
133
  </>
119
134
  ),
120
- painting: (
135
+ harp: (
121
136
  <>
122
- <rect {...S} x="22" y="24" width="56" height="46" />
123
- <rect {...S} strokeWidth="2.5" x="29" y="31" width="42" height="32" opacity="0.6" />
124
- <path {...S} strokeWidth="3" d="M32,56 L44,42 L52,50 L60,38 L68,50" opacity="0.8" />
125
- <circle {...F} cx="62" cy="38" r="3" opacity="0.6" />
137
+ <path {...O} d="M30,82 L30,30 Q30,12 50,14 Q72,16 74,44 L74,82 Z" fill="none" stroke={BRASS} strokeWidth="7" />
138
+ <path {...O} d="M24,80 L80,80 L76,88 L28,88 Z" fill={BRASS} />
139
+ {[40, 48, 56, 64].map((x, i) => (
140
+ <line key={x} x1={x} y1={24 + i * 4} x2={x} y2="80" stroke={CREAM} strokeWidth="2" />
141
+ ))}
142
+ </>
143
+ )
144
+ };
145
+
146
+ const WIDE = {
147
+ sofa: (
148
+ <>
149
+ <path {...O} d="M28,26 Q100,12 172,26 L172,56 L28,56 Z" fill="#a04848" />
150
+ <rect {...O} x="34" y="46" width="64" height="20" rx="7" fill="#b85c5c" />
151
+ <rect {...O} x="102" y="46" width="64" height="20" rx="7" fill="#b85c5c" />
152
+ <rect {...O} x="14" y="34" width="18" height="36" rx="9" fill="#8d3d3d" />
153
+ <rect {...O} x="168" y="34" width="18" height="36" rx="9" fill="#8d3d3d" />
154
+ <rect x="36" y="70" width="8" height="12" fill={WOOD_D} />
155
+ <rect x="156" y="70" width="8" height="12" fill={WOOD_D} />
156
+ <path d="M60,32 Q66,38 60,44 M136,32 Q142,38 136,44" stroke="#7d3535" strokeWidth="2.5" fill="none" />
126
157
  </>
127
158
  ),
128
- crate: (
159
+ bed: (
129
160
  <>
130
- <rect {...S} x="24" y="28" width="52" height="48" />
131
- <line {...S} strokeWidth="4" x1="24" y1="44" x2="76" y2="44" opacity="0.8" />
132
- <line {...S} strokeWidth="4" x1="24" y1="60" x2="76" y2="60" opacity="0.8" />
133
- <rect {...F} x="40" y="32" width="20" height="7" rx="2" opacity="0.6" />
134
- <circle {...F} cx="31" cy="52" r="2.5" opacity="0.7" />
135
- <circle {...F} cx="69" cy="52" r="2.5" opacity="0.7" />
136
- <circle {...F} cx="31" cy="68" r="2.5" opacity="0.7" />
137
- <circle {...F} cx="69" cy="68" r="2.5" opacity="0.7" />
161
+ <rect {...O} x="14" y="18" width="16" height="58" rx="4" fill={WOOD} />
162
+ <rect {...O} x="176" y="30" width="12" height="46" rx="3" fill={WOOD} />
163
+ <rect {...O} x="28" y="40" width="150" height="30" rx="6" fill={CREAM} />
164
+ <path {...O} d="M74,40 L178,40 L178,70 L74,70 Q66,55 74,40 Z" fill="#41609e" />
165
+ <path d="M84,48 L170,48 M80,56 L170,56" stroke="#33518b" strokeWidth="2.5" />
166
+ <rect {...O} x="36" y="44" width="30" height="16" rx="7" fill="#ffffff" />
167
+ <rect x="32" y="70" width="8" height="10" fill={WOOD_D} />
168
+ <rect x="168" y="70" width="8" height="10" fill={WOOD_D} />
169
+ </>
170
+ )
171
+ };
172
+
173
+ const WALL = {
174
+ window: (
175
+ <>
176
+ <rect x="6" y="2" width="88" height="32" rx="4" fill={CREAM} stroke={INK} strokeWidth="3" />
177
+ <rect x="12" y="7" width="36" height="22" fill="#2b3a5c" stroke={INK} strokeWidth="2" />
178
+ <rect x="52" y="7" width="36" height="22" fill="#33456b" stroke={INK} strokeWidth="2" />
179
+ <circle cx="72" cy="13" r="4" fill="#f4efe2" opacity="0.9" />
180
+ <circle cx="22" cy="12" r="1.3" fill="#cfe0ff" />
181
+ <circle cx="36" cy="22" r="1" fill="#cfe0ff" />
182
+ <path d="M52,26 L64,10" stroke="#8fa8d8" strokeWidth="1.5" opacity="0.7" />
138
183
  </>
139
184
  ),
140
- harp: (
185
+ painting: (
141
186
  <>
142
- <path {...S} d="M30,80 L30,30 Q30,14 48,16 Q68,18 70,44 L70,80" />
143
- <path {...F} d="M26,78 L74,78 L70,86 L30,86 Z" opacity="0.85" />
144
- {[38, 46, 54, 62].map((x, i) => (
145
- <line key={i} {...S} strokeWidth="2.5" x1={x} y1={26 + i * 3} x2={x} y2="78" opacity="0.7" />
146
- ))}
187
+ <rect x="8" y="2" width="84" height="32" rx="3" fill={BRASS} stroke={INK} strokeWidth="3" />
188
+ <rect x="15" y="8" width="70" height="20" fill="#8fb8d8" />
189
+ <path d="M15,24 L34,13 L46,22 L60,10 L85,24 L85,28 L15,28 Z" fill="#46a758" stroke={INK} strokeWidth="1" />
190
+ <circle cx="70" cy="13" r="3" fill="#ffd76a" />
191
+ </>
192
+ ),
193
+ mirror: (
194
+ <>
195
+ <ellipse cx="50" cy="18" rx="42" ry="16" fill={BRASS} stroke={INK} strokeWidth="3" />
196
+ <ellipse cx="50" cy="18" rx="34" ry="11" fill="#cfd8e8" stroke={INK} strokeWidth="1.5" />
197
+ <path d="M30,22 L44,11 M40,24 L52,14" stroke="#f2f6ff" strokeWidth="2.5" opacity="0.8" />
147
198
  </>
148
199
  )
149
200
  };
150
201
 
151
- export function ObjectIcon({ type, size = 40, className = '', title }) {
202
+ export function FloorIcon({ type, size = 40, className = '' }) {
152
203
  return (
153
- <svg viewBox="0 0 100 100" width={size} height={size} className={`obj-icon ${className}`}
154
- role="img" aria-label={title || type}>
155
- {ICONS[type] || <circle {...S} cx="50" cy="50" r="26" />}
204
+ <svg viewBox="0 0 100 100" width={size} height={size} className={`obj-icon ${className}`} role="img" aria-label={type}>
205
+ {FLOOR[type] || FLOOR.crate}
156
206
  </svg>
157
207
  );
158
208
  }
209
+
210
+ export const hasWide = (type) => !!WIDE[type];
211
+ export const hasWall = (type) => !!WALL[type];
212
+ export const wideArt = (type) => WIDE[type] || WIDE.sofa;
213
+ export const wallArt = (type) => WALL[type] || WALL.window;
214
+ export const floorArt = (type) => FLOOR[type] || FLOOR.crate;
215
+
216
+ // kept for any legacy callers
217
+ export function ObjectIcon(props) {
218
+ return <FloorIcon {...props} />;
219
+ }