@onnie81/murdoku-spor 1.1.0 → 1.2.1

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/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
 
@@ -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
+ }
@@ -0,0 +1,287 @@
1
+ // Original audio: synthesized SFX + a generative noir music loop.
2
+ // Everything is produced with WebAudio oscillators/noise — no assets, no
3
+ // copyright entanglements. Autoplay-safe: the context starts on first gesture.
4
+
5
+ let ctx = null;
6
+ let master, sfxBus, musicBus, analyser;
7
+ let enabled = { sfx: true, music: true };
8
+ let musicOn = false;
9
+ let schedTimer = null;
10
+
11
+ function ensureCtx() {
12
+ if (ctx) return ctx;
13
+ const AC = window.AudioContext || window.webkitAudioContext;
14
+ if (!AC) return null;
15
+ ctx = new AC();
16
+ master = ctx.createGain();
17
+ master.gain.value = 0.9;
18
+ master.connect(ctx.destination);
19
+ analyser = ctx.createAnalyser();
20
+ analyser.fftSize = 2048;
21
+ master.connect(analyser);
22
+ sfxBus = ctx.createGain();
23
+ sfxBus.gain.value = 0.5;
24
+ sfxBus.connect(master);
25
+ musicBus = ctx.createGain();
26
+ musicBus.gain.value = 0.0;
27
+ // gentle echo on the music bus
28
+ const delay = ctx.createDelay(1.0);
29
+ delay.delayTime.value = 0.31;
30
+ const fb = ctx.createGain();
31
+ fb.gain.value = 0.25;
32
+ const wet = ctx.createGain();
33
+ wet.gain.value = 0.22;
34
+ musicBus.connect(master);
35
+ musicBus.connect(delay);
36
+ delay.connect(fb);
37
+ fb.connect(delay);
38
+ delay.connect(wet);
39
+ wet.connect(master);
40
+ return ctx;
41
+ }
42
+
43
+ export function initAudioOnGesture() {
44
+ const kick = () => {
45
+ const c = ensureCtx();
46
+ if (!c) return;
47
+ const go = () => { if (enabled.music) startMusic(); };
48
+ if (c.state === 'suspended') c.resume().then(go).catch(() => {});
49
+ else go();
50
+ window.removeEventListener('pointerdown', kick);
51
+ window.removeEventListener('keydown', kick);
52
+ };
53
+ window.addEventListener('pointerdown', kick);
54
+ window.addEventListener('keydown', kick);
55
+ }
56
+
57
+ export function setAudioPrefs({ sfx, music }) {
58
+ if (typeof sfx === 'boolean') enabled.sfx = sfx;
59
+ if (typeof music === 'boolean') {
60
+ enabled.music = music;
61
+ if (ctx) {
62
+ if (music) startMusic();
63
+ else stopMusic();
64
+ }
65
+ }
66
+ }
67
+
68
+ function env(node, t0, a, peak, d, sustain = 0.0001) {
69
+ node.gain.cancelScheduledValues(t0);
70
+ node.gain.setValueAtTime(0.0001, t0);
71
+ node.gain.exponentialRampToValueAtTime(Math.max(peak, 0.0002), t0 + a);
72
+ node.gain.exponentialRampToValueAtTime(sustain, t0 + a + d);
73
+ }
74
+
75
+ function tone(freq, { type = 'sine', a = 0.005, d = 0.2, peak = 0.5, bus = null, glide = null, at = 0 } = {}) {
76
+ const c = ensureCtx();
77
+ if (!c) return;
78
+ const t0 = c.currentTime + at;
79
+ const o = c.createOscillator();
80
+ const g = c.createGain();
81
+ o.type = type;
82
+ o.frequency.setValueAtTime(freq, t0);
83
+ if (glide) o.frequency.exponentialRampToValueAtTime(glide, t0 + a + d);
84
+ env(g, t0, a, peak, d);
85
+ o.connect(g);
86
+ g.connect(bus || sfxBus);
87
+ o.start(t0);
88
+ o.stop(t0 + a + d + 0.1);
89
+ }
90
+
91
+ function noise({ a = 0.001, d = 0.08, peak = 0.3, freq = 2200, q = 1, at = 0, type = 'bandpass' } = {}) {
92
+ const c = ensureCtx();
93
+ if (!c) return;
94
+ const t0 = c.currentTime + at;
95
+ const len = Math.ceil(c.sampleRate * (a + d + 0.05));
96
+ const buf = c.createBuffer(1, len, c.sampleRate);
97
+ const data = buf.getChannelData(0);
98
+ for (let i = 0; i < len; i++) data[i] = Math.random() * 2 - 1;
99
+ const src = c.createBufferSource();
100
+ src.buffer = buf;
101
+ const f = c.createBiquadFilter();
102
+ f.type = type;
103
+ f.frequency.value = freq;
104
+ f.Q.value = q;
105
+ const g = c.createGain();
106
+ env(g, t0, a, peak, d);
107
+ src.connect(f);
108
+ f.connect(g);
109
+ g.connect(sfxBus);
110
+ src.start(t0);
111
+ src.stop(t0 + a + d + 0.05);
112
+ }
113
+
114
+ export function sfx(name) {
115
+ if (!enabled.sfx) return;
116
+ const c = ensureCtx();
117
+ if (!c) return;
118
+ if (c.state === 'suspended') c.resume();
119
+ switch (name) {
120
+ case 'place': // soft wooden tap + warm blip
121
+ noise({ d: 0.05, peak: 0.25, freq: 900, q: 0.8, type: 'lowpass' });
122
+ tone(392, { type: 'triangle', d: 0.14, peak: 0.22 });
123
+ tone(587.3, { type: 'sine', d: 0.18, peak: 0.12, at: 0.02 });
124
+ break;
125
+ case 'pickup':
126
+ tone(523, { type: 'triangle', d: 0.09, peak: 0.15, glide: 740 });
127
+ break;
128
+ case 'thud': // illegal square
129
+ tone(110, { type: 'sine', d: 0.16, peak: 0.3, glide: 70 });
130
+ noise({ d: 0.06, peak: 0.12, freq: 300, type: 'lowpass' });
131
+ break;
132
+ case 'x':
133
+ noise({ d: 0.07, peak: 0.2, freq: 1800, q: 2 });
134
+ noise({ d: 0.07, peak: 0.16, freq: 1400, q: 2, at: 0.07 });
135
+ break;
136
+ case 'tick': // pencil mark
137
+ noise({ d: 0.03, peak: 0.18, freq: 3200, q: 3 });
138
+ break;
139
+ case 'paper': // clue strike
140
+ noise({ d: 0.12, peak: 0.16, freq: 2400, q: 0.7 });
141
+ break;
142
+ case 'click':
143
+ tone(880, { type: 'square', d: 0.03, peak: 0.05 });
144
+ break;
145
+ case 'chime': // hint / board complete
146
+ tone(659.3, { type: 'sine', d: 0.3, peak: 0.18 });
147
+ tone(987.8, { type: 'sine', d: 0.4, peak: 0.14, at: 0.09 });
148
+ break;
149
+ case 'wrong':
150
+ tone(196, { type: 'sawtooth', d: 0.25, peak: 0.14, glide: 155 });
151
+ tone(185, { type: 'sawtooth', d: 0.3, peak: 0.12, at: 0.05, glide: 147 });
152
+ break;
153
+ case 'reveal': // accusation moment
154
+ [220, 261.6, 311.1].forEach((f, i) => tone(f, { type: 'triangle', d: 0.5, peak: 0.12, at: i * 0.05 }));
155
+ break;
156
+ case 'win': { // noir sting: Dm(add9) arpeggio up + sparkle
157
+ [146.8, 220, 293.7, 349.2, 440, 587.3].forEach((f, i) =>
158
+ tone(f, { type: 'triangle', d: 0.55, peak: 0.16, at: i * 0.09 }));
159
+ tone(1174.7, { type: 'sine', d: 0.8, peak: 0.1, at: 0.6 });
160
+ noise({ d: 0.5, peak: 0.05, freq: 6000, q: 0.5, at: 0.55, type: 'highpass' });
161
+ break;
162
+ }
163
+ default:
164
+ break;
165
+ }
166
+ }
167
+
168
+ // ---------- generative noir loop ----------
169
+ // D natural-minor palette. A slow bass walk, sparse vibraphone phrases,
170
+ // an occasional brushed-cymbal breath. Nothing repeats exactly.
171
+
172
+ const BASS = [73.42, 87.31, 98.0, 110.0, 65.41, 87.31]; // D2 F2 G2 A2 C2 F2
173
+ const VIBES = [293.66, 349.23, 392.0, 440.0, 523.25, 587.33, 698.46]; // D4 F4 G4 A4 C5 D5 F5
174
+ let step = 0;
175
+
176
+ function playBass(t, f) {
177
+ const c = ctx;
178
+ const o = c.createOscillator();
179
+ const g = c.createGain();
180
+ const flt = c.createBiquadFilter();
181
+ o.type = 'triangle';
182
+ o.frequency.value = f;
183
+ flt.type = 'lowpass';
184
+ flt.frequency.value = 260;
185
+ g.gain.setValueAtTime(0.0001, t);
186
+ g.gain.exponentialRampToValueAtTime(0.34, t + 0.04);
187
+ g.gain.exponentialRampToValueAtTime(0.0001, t + 1.55);
188
+ o.connect(flt); flt.connect(g); g.connect(musicBus);
189
+ o.start(t); o.stop(t + 1.7);
190
+ }
191
+
192
+ function playVibe(t, f, vel = 0.12) {
193
+ const c = ctx;
194
+ const o = c.createOscillator();
195
+ const o2 = c.createOscillator();
196
+ const g = c.createGain();
197
+ o.type = 'sine'; o.frequency.value = f;
198
+ o2.type = 'sine'; o2.frequency.value = f * 2.01;
199
+ const g2 = c.createGain(); g2.gain.value = 0.25;
200
+ g.gain.setValueAtTime(0.0001, t);
201
+ g.gain.exponentialRampToValueAtTime(vel, t + 0.015);
202
+ g.gain.exponentialRampToValueAtTime(0.0001, t + 1.9);
203
+ o.connect(g); o2.connect(g2); g2.connect(g); g.connect(musicBus);
204
+ o.start(t); o.stop(t + 2); o2.start(t); o2.stop(t + 2);
205
+ }
206
+
207
+ function playBrush(t) {
208
+ const c = ctx;
209
+ const len = Math.ceil(c.sampleRate * 0.5);
210
+ const buf = c.createBuffer(1, len, c.sampleRate);
211
+ const d = buf.getChannelData(0);
212
+ for (let i = 0; i < len; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / len);
213
+ const src = c.createBufferSource();
214
+ src.buffer = buf;
215
+ const f = c.createBiquadFilter();
216
+ f.type = 'highpass'; f.frequency.value = 5000;
217
+ const g = c.createGain();
218
+ g.gain.setValueAtTime(0.0001, t);
219
+ g.gain.exponentialRampToValueAtTime(0.045, t + 0.05);
220
+ g.gain.exponentialRampToValueAtTime(0.0001, t + 0.45);
221
+ src.connect(f); f.connect(g); g.connect(musicBus);
222
+ src.start(t); src.stop(t + 0.5);
223
+ }
224
+
225
+ let nextBeat = 0;
226
+ const BEAT = 0.86; // ~70bpm
227
+
228
+ function scheduler() {
229
+ const c = ctx;
230
+ if (!c || !musicOn) return;
231
+ const horizon = c.currentTime + 1.6;
232
+ while (nextBeat < horizon) {
233
+ const t = nextBeat;
234
+ const bar = Math.floor(step / 4) % BASS.length;
235
+ if (step % 2 === 0) playBass(t, BASS[bar]);
236
+ if (Math.random() < 0.55) {
237
+ const n = VIBES[Math.floor(Math.random() * VIBES.length)];
238
+ playVibe(t + (Math.random() < 0.4 ? BEAT / 2 : 0) + Math.random() * 0.03, n, 0.07 + Math.random() * 0.07);
239
+ if (Math.random() < 0.25) playVibe(t + BEAT * 0.75, n * 1.335, 0.05); // fourth up
240
+ }
241
+ if (step % 8 === 6 && Math.random() < 0.7) playBrush(t + BEAT / 2);
242
+ step++;
243
+ nextBeat += BEAT;
244
+ }
245
+ }
246
+
247
+ export function startMusic() {
248
+ const c = ensureCtx();
249
+ if (!c) return;
250
+ musicOn = true;
251
+ if (c.state === 'suspended') return; // the gesture handler calls again after resume
252
+ nextBeat = c.currentTime + 0.1;
253
+ step = 0;
254
+ musicBus.gain.cancelScheduledValues(c.currentTime);
255
+ musicBus.gain.setValueAtTime(0.0001, c.currentTime);
256
+ musicBus.gain.exponentialRampToValueAtTime(0.5, c.currentTime + 2.5);
257
+ clearInterval(schedTimer);
258
+ schedTimer = setInterval(scheduler, 400);
259
+ }
260
+
261
+ export function stopMusic() {
262
+ musicOn = false;
263
+ clearInterval(schedTimer);
264
+ if (ctx && musicBus) {
265
+ musicBus.gain.cancelScheduledValues(ctx.currentTime);
266
+ musicBus.gain.setValueAtTime(musicBus.gain.value, ctx.currentTime);
267
+ musicBus.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.8);
268
+ }
269
+ }
270
+
271
+ // diagnostics hook (used by the e2e test and handy in devtools)
272
+ if (typeof window !== 'undefined') {
273
+ window.__mdkAudio = {
274
+ state: () => (ctx ? ctx.state : 'none'),
275
+ musicOn: () => musicOn,
276
+ enabled: () => ({ ...enabled }),
277
+ rms() {
278
+ if (!analyser) return 0;
279
+ const a = new Float32Array(analyser.fftSize);
280
+ analyser.getFloatTimeDomainData(a);
281
+ let s = 0;
282
+ for (let i = 0; i < a.length; i++) s += a[i] * a[i];
283
+ return Math.sqrt(s / a.length);
284
+ },
285
+ ping: () => sfx('chime')
286
+ };
287
+ }