claude-code-sounds 1.1.0 → 1.1.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/LICENSE +21 -0
- package/README.md +155 -8
- package/bin/cli.js +379 -58
- package/package.json +1 -1
- package/themes/wc3-peon/download.sh +2 -1
- package/themes/wc3-peon/theme.json +5 -2
- package/themes/zelda-oot/download.sh +67 -0
- package/themes/zelda-oot/theme.json +110 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ryparker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
Plays sound effects when sessions start, prompts are submitted, responses finish, errors occur, and more.
|
|
10
10
|
|
|
11
|
-
Ships with a **WC3 Orc Peon** theme. Bring your own sounds or create new themes.
|
|
11
|
+
Ships with a **WC3 Orc Peon** theme and a **Zelda: Ocarina of Time** theme. Bring your own sounds or create new themes.
|
|
12
12
|
|
|
13
13
|
*"Something need doing?"*
|
|
14
14
|
|
|
@@ -55,7 +55,7 @@ npx claude-code-sounds --help # Show help
|
|
|
55
55
|
|
|
56
56
|
## WC3 Orc Peon Theme
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
57 sounds from Warcraft 3 Orc units mapped across 11 Claude Code lifecycle events.
|
|
59
59
|
|
|
60
60
|
> After installing, preview all sounds with `./preview.sh` or a specific category with `./preview.sh start`
|
|
61
61
|
|
|
@@ -88,11 +88,12 @@ npx claude-code-sounds --help # Show help
|
|
|
88
88
|
</details>
|
|
89
89
|
|
|
90
90
|
<details>
|
|
91
|
-
<summary><b>stop</b> — Claude finished responding (
|
|
91
|
+
<summary><b>stop</b> — Claude finished responding (9 sounds)</summary>
|
|
92
92
|
|
|
93
93
|
| Sound | Quote | Unit |
|
|
94
94
|
|---|---|---|
|
|
95
|
-
| `zug-zug.wav` | *"Zug zug"* |
|
|
95
|
+
| `zug-zug.wav` | *"Zug zug"* | Grunt |
|
|
96
|
+
| `get-em.wav` | *"Get 'em!"* | Peon |
|
|
96
97
|
| `ok.wav` | *"OK"* | Peon |
|
|
97
98
|
| `i-can-do-that.wav` | *"I can do that"* | Peon |
|
|
98
99
|
| `be-happy-to.wav` | *"Be happy to"* | Peon |
|
|
@@ -117,12 +118,13 @@ npx claude-code-sounds --help # Show help
|
|
|
117
118
|
</details>
|
|
118
119
|
|
|
119
120
|
<details>
|
|
120
|
-
<summary><b>subagent</b> — Spawning a subagent (
|
|
121
|
+
<summary><b>subagent</b> — Spawning a subagent (7 sounds)</summary>
|
|
121
122
|
|
|
122
123
|
| Sound | Quote | Unit |
|
|
123
124
|
|---|---|---|
|
|
124
125
|
| `work-work.wav` | *"Work, work"* | Peon |
|
|
125
|
-
| `zug-zug.wav` | *"Zug zug"* |
|
|
126
|
+
| `zug-zug.wav` | *"Zug zug"* | Grunt |
|
|
127
|
+
| `get-em.wav` | *"Get 'em!"* | Peon |
|
|
126
128
|
| `ill-try.wav` | *"I'll try"* | Peon |
|
|
127
129
|
| `why-not.wav` | *"Why not?"* | Peon |
|
|
128
130
|
| `for-the-horde.wav` | *"For the Horde!"* | Grunt |
|
|
@@ -202,6 +204,147 @@ npx claude-code-sounds --help # Show help
|
|
|
202
204
|
|
|
203
205
|
</details>
|
|
204
206
|
|
|
207
|
+
## Zelda: Ocarina of Time Theme
|
|
208
|
+
|
|
209
|
+
47 sounds from Navi, Link, Ganondorf, and iconic OOT jingles mapped across 11 Claude Code lifecycle events.
|
|
210
|
+
|
|
211
|
+
*"Hey! Listen!"*
|
|
212
|
+
|
|
213
|
+
<details open>
|
|
214
|
+
<summary><b>start</b> — Session starting, Navi greets you (5 sounds)</summary>
|
|
215
|
+
|
|
216
|
+
| Sound | Description | Source |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| `hey.wav` | *"Hey!"* | Navi |
|
|
219
|
+
| `hello.wav` | *"Hello!"* | Navi |
|
|
220
|
+
| `listen.wav` | *"Listen!"* | Navi |
|
|
221
|
+
| `great-fairy-laugh.wav` | *(magical laugh)* | Great Fairy |
|
|
222
|
+
| `open-chest.wav` | *(opening big chest)* | SFX |
|
|
223
|
+
|
|
224
|
+
</details>
|
|
225
|
+
|
|
226
|
+
<details>
|
|
227
|
+
<summary><b>prompt</b> — User submitted a prompt, adventure continues (5 sounds)</summary>
|
|
228
|
+
|
|
229
|
+
| Sound | Description | Source |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `hey.wav` | *"Hey!"* | Navi |
|
|
232
|
+
| `look.wav` | *"Look!"* | Navi |
|
|
233
|
+
| `menu-select.wav` | *(menu select)* | SFX |
|
|
234
|
+
| `dialogue-next.wav` | *(continue dialogue)* | SFX |
|
|
235
|
+
| `get-heart.wav` | *(get heart)* | SFX |
|
|
236
|
+
|
|
237
|
+
</details>
|
|
238
|
+
|
|
239
|
+
<details>
|
|
240
|
+
<summary><b>stop</b> — Claude finished responding (5 sounds)</summary>
|
|
241
|
+
|
|
242
|
+
| Sound | Description | Source |
|
|
243
|
+
|---|---|---|
|
|
244
|
+
| `secret-discovered.wav` | *(secret jingle)* | SFX |
|
|
245
|
+
| `get-item.wav` | *(get small item)* | SFX |
|
|
246
|
+
| `get-rupee.wav` | *(get rupee)* | SFX |
|
|
247
|
+
| `open-small-chest.wav` | *(open small chest)* | SFX |
|
|
248
|
+
| `song-correct.wav` | *(song played correctly)* | SFX |
|
|
249
|
+
|
|
250
|
+
</details>
|
|
251
|
+
|
|
252
|
+
<details>
|
|
253
|
+
<summary><b>permission</b> — Permission prompt, waiting for approval (5 sounds)</summary>
|
|
254
|
+
|
|
255
|
+
| Sound | Description | Source |
|
|
256
|
+
|---|---|---|
|
|
257
|
+
| `watch-out.wav` | *"Watch out!"* | Navi |
|
|
258
|
+
| `hey.wav` | *"Hey!"* | Navi |
|
|
259
|
+
| `listen.wav` | *"Listen!"* | Navi |
|
|
260
|
+
| `pause-menu.wav` | *(pause menu open)* | SFX |
|
|
261
|
+
| `z-target.wav` | *(Z-target enemy)* | SFX |
|
|
262
|
+
|
|
263
|
+
</details>
|
|
264
|
+
|
|
265
|
+
<details>
|
|
266
|
+
<summary><b>subagent</b> — Spawning a subagent (5 sounds)</summary>
|
|
267
|
+
|
|
268
|
+
| Sound | Description | Source |
|
|
269
|
+
|---|---|---|
|
|
270
|
+
| `sword-draw.wav` | *(draw sword)* | SFX |
|
|
271
|
+
| `link-attack.wav` | *(attack shout)* | Adult Link |
|
|
272
|
+
| `link-strong-attack.wav` | *(strong attack)* | Adult Link |
|
|
273
|
+
| `hey.wav` | *"Hey!"* | Navi |
|
|
274
|
+
| `spin-attack.wav` | *(spin attack)* | SFX |
|
|
275
|
+
|
|
276
|
+
</details>
|
|
277
|
+
|
|
278
|
+
<details>
|
|
279
|
+
<summary><b>idle</b> — Waiting for user input (5 sounds)</summary>
|
|
280
|
+
|
|
281
|
+
| Sound | Description | Source |
|
|
282
|
+
|---|---|---|
|
|
283
|
+
| `hey.wav` | *"Hey!"* | Navi |
|
|
284
|
+
| `listen.wav` | *"Listen!"* | Navi |
|
|
285
|
+
| `watch-out.wav` | *"Watch out!"* | Navi |
|
|
286
|
+
| `low-health.wav` | *(low health beep)* | SFX |
|
|
287
|
+
| `snore.wav` | *(Talon snoring)* | Talon |
|
|
288
|
+
|
|
289
|
+
</details>
|
|
290
|
+
|
|
291
|
+
<details>
|
|
292
|
+
<summary><b>error</b> — Tool call failed (4 sounds)</summary>
|
|
293
|
+
|
|
294
|
+
| Sound | Description | Source |
|
|
295
|
+
|---|---|---|
|
|
296
|
+
| `error.wav` | *(error sound)* | SFX |
|
|
297
|
+
| `song-error.wav` | *(wrong note)* | SFX |
|
|
298
|
+
| `link-hurt.wav` | *(Link hurt)* | Adult Link |
|
|
299
|
+
| `ganondorf-laugh.wav` | *(Ganondorf laughing)* | Ganondorf |
|
|
300
|
+
|
|
301
|
+
</details>
|
|
302
|
+
|
|
303
|
+
<details>
|
|
304
|
+
<summary><b>end</b> — Session ending (3 sounds)</summary>
|
|
305
|
+
|
|
306
|
+
| Sound | Description | Source |
|
|
307
|
+
|---|---|---|
|
|
308
|
+
| `item-fanfare.wav` | *(item fanfare)* | SFX |
|
|
309
|
+
| `secret-discovered.wav` | *(secret jingle)* | SFX |
|
|
310
|
+
| `dialogue-done.wav` | *(dialogue finished)* | SFX |
|
|
311
|
+
|
|
312
|
+
</details>
|
|
313
|
+
|
|
314
|
+
<details>
|
|
315
|
+
<summary><b>task-completed</b> — Task marked done (2 sounds)</summary>
|
|
316
|
+
|
|
317
|
+
| Sound | Description | Source |
|
|
318
|
+
|---|---|---|
|
|
319
|
+
| `item-fanfare.wav` | *(item fanfare)* | SFX |
|
|
320
|
+
| `secret-discovered.wav` | *(secret jingle)* | SFX |
|
|
321
|
+
|
|
322
|
+
</details>
|
|
323
|
+
|
|
324
|
+
<details>
|
|
325
|
+
<summary><b>compact</b> — Context compaction, health running low (4 sounds)</summary>
|
|
326
|
+
|
|
327
|
+
| Sound | Description | Source |
|
|
328
|
+
|---|---|---|
|
|
329
|
+
| `low-health.wav` | *(low health beep)* | SFX |
|
|
330
|
+
| `pause-close.wav` | *(pause menu close)* | SFX |
|
|
331
|
+
| `watch-out.wav` | *"Watch out!"* | Navi |
|
|
332
|
+
| `error.wav` | *(error sound)* | SFX |
|
|
333
|
+
|
|
334
|
+
</details>
|
|
335
|
+
|
|
336
|
+
<details>
|
|
337
|
+
<summary><b>teammate-idle</b> — Teammate went idle (4 sounds)</summary>
|
|
338
|
+
|
|
339
|
+
| Sound | Description | Source |
|
|
340
|
+
|---|---|---|
|
|
341
|
+
| `hey.wav` | *"Hey!"* | Navi |
|
|
342
|
+
| `listen.wav` | *"Listen!"* | Navi |
|
|
343
|
+
| `watch-out.wav` | *"Watch out!"* | Navi |
|
|
344
|
+
| `low-health.wav` | *(low health beep)* | SFX |
|
|
345
|
+
|
|
346
|
+
</details>
|
|
347
|
+
|
|
205
348
|
## Hook Events
|
|
206
349
|
|
|
207
350
|
| Event | Hook | When |
|
|
@@ -230,6 +373,7 @@ Defines metadata and maps source files to hook categories:
|
|
|
230
373
|
{
|
|
231
374
|
"name": "My Theme",
|
|
232
375
|
"description": "A short description",
|
|
376
|
+
"srcBase": "MyTheme",
|
|
233
377
|
"sounds": {
|
|
234
378
|
"start": {
|
|
235
379
|
"description": "Session starting",
|
|
@@ -241,13 +385,16 @@ Defines metadata and maps source files to hook categories:
|
|
|
241
385
|
}
|
|
242
386
|
```
|
|
243
387
|
|
|
388
|
+
- **`srcBase`** — subdirectory under the temp dir where downloaded files live (e.g., `"Orc"`, `"OOT"`)
|
|
389
|
+
- **`src`** — path relative to `$2/<srcBase>/` where the file is found after download
|
|
390
|
+
|
|
244
391
|
### `download.sh`
|
|
245
392
|
|
|
246
393
|
Downloads the sound files. Receives two arguments:
|
|
247
394
|
- `$1` — target sounds directory (`~/.claude/sounds`)
|
|
248
395
|
- `$2` — temp directory for downloads
|
|
249
396
|
|
|
250
|
-
The script should download and extract files so they're accessible at `$2
|
|
397
|
+
The script should download and extract files so they're accessible at `$2/<srcBase>/<src path>` (matching the `srcBase` and `src` values in `theme.json`).
|
|
251
398
|
|
|
252
399
|
## How It Works
|
|
253
400
|
|
|
@@ -279,4 +426,4 @@ This removes all sound files, the hook script, and the hooks config from `settin
|
|
|
279
426
|
|
|
280
427
|
## Disclaimer
|
|
281
428
|
|
|
282
|
-
Sound files are downloaded from third-party sources at install time and are not included in this repository.
|
|
429
|
+
Sound files are downloaded from third-party sources at install time and are not included in this repository. Warcraft audio is property of Blizzard Entertainment. Zelda audio is property of Nintendo.
|
package/bin/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ const SOUNDS_DIR = path.join(CLAUDE_DIR, "sounds");
|
|
|
14
14
|
const HOOKS_DIR = path.join(CLAUDE_DIR, "hooks");
|
|
15
15
|
const SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
|
|
16
16
|
const THEMES_DIR = path.join(PKG_DIR, "themes");
|
|
17
|
+
const INSTALLED_PATH = path.join(SOUNDS_DIR, ".installed.json");
|
|
17
18
|
|
|
18
19
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
19
20
|
|
|
@@ -66,6 +67,53 @@ function hasCommand(name) {
|
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
function readInstalled() {
|
|
71
|
+
if (fs.existsSync(INSTALLED_PATH)) {
|
|
72
|
+
return JSON.parse(fs.readFileSync(INSTALLED_PATH, "utf-8"));
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeInstalled(themeName) {
|
|
78
|
+
mkdirp(SOUNDS_DIR);
|
|
79
|
+
fs.writeFileSync(INSTALLED_PATH, JSON.stringify({ theme: themeName }, null, 2) + "\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Check if sounds are already installed.
|
|
84
|
+
* Returns { theme, themeDisplay, totalEnabled, totalAvailable, categories } or null.
|
|
85
|
+
*/
|
|
86
|
+
function getExistingInstall() {
|
|
87
|
+
const installed = readInstalled();
|
|
88
|
+
if (!installed) return null;
|
|
89
|
+
|
|
90
|
+
const themeJsonPath = path.join(THEMES_DIR, installed.theme, "theme.json");
|
|
91
|
+
if (!fs.existsSync(themeJsonPath)) return null;
|
|
92
|
+
|
|
93
|
+
const theme = JSON.parse(fs.readFileSync(themeJsonPath, "utf-8"));
|
|
94
|
+
let totalEnabled = 0;
|
|
95
|
+
const totalAvailable = Object.values(theme.sounds).reduce((sum, c) => sum + c.files.length, 0);
|
|
96
|
+
|
|
97
|
+
for (const cat of Object.keys(theme.sounds)) {
|
|
98
|
+
const catDir = path.join(SOUNDS_DIR, cat);
|
|
99
|
+
try {
|
|
100
|
+
for (const f of fs.readdirSync(catDir)) {
|
|
101
|
+
if (f.endsWith(".wav") || f.endsWith(".mp3")) totalEnabled++;
|
|
102
|
+
}
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (totalEnabled === 0) return null;
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
theme: installed.theme,
|
|
110
|
+
themeDisplay: theme.name,
|
|
111
|
+
themeDescription: theme.description,
|
|
112
|
+
totalEnabled,
|
|
113
|
+
totalAvailable,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
69
117
|
// ─── ANSI helpers ────────────────────────────────────────────────────────────
|
|
70
118
|
|
|
71
119
|
const CSI = "\x1b[";
|
|
@@ -191,29 +239,61 @@ function select(title, options) {
|
|
|
191
239
|
|
|
192
240
|
/**
|
|
193
241
|
* Multi-select checklist with toggle, preview, and confirm.
|
|
194
|
-
* Returns array of selected indices.
|
|
242
|
+
* Returns array of selected indices, or null if back was pressed.
|
|
195
243
|
*/
|
|
196
|
-
function multiSelect(title, items, defaults, previewDir) {
|
|
244
|
+
function multiSelect(title, items, defaults, previewDir, { allowBack = false } = {}) {
|
|
197
245
|
return new Promise((resolve) => {
|
|
198
246
|
let cursor = 0;
|
|
247
|
+
let scrollTop = 0;
|
|
199
248
|
const checked = items.map((_, i) => defaults.includes(i));
|
|
200
|
-
|
|
249
|
+
|
|
250
|
+
// Calculate scrolling dimensions
|
|
251
|
+
const termRows = process.stdout.rows || 24;
|
|
252
|
+
const maxItemRows = Math.max(5, termRows - 5); // 5 = title + blank + hint + 2 buffer
|
|
253
|
+
const needsScroll = items.length > maxItemRows;
|
|
254
|
+
// When scrolling, reserve 2 rows for ▲/▼ indicators (always present for stable line count)
|
|
255
|
+
const visibleCount = needsScroll ? maxItemRows - 2 : items.length;
|
|
256
|
+
const lineCount = needsScroll ? maxItemRows + 3 : items.length + 3;
|
|
257
|
+
|
|
258
|
+
function adjustScroll() {
|
|
259
|
+
if (!needsScroll) return;
|
|
260
|
+
if (cursor < scrollTop) scrollTop = cursor;
|
|
261
|
+
if (cursor >= scrollTop + visibleCount) scrollTop = cursor - visibleCount + 1;
|
|
262
|
+
}
|
|
201
263
|
|
|
202
264
|
function render(initial) {
|
|
203
265
|
if (!initial) moveCursorUp(lineCount);
|
|
204
266
|
print(` ${title}\n`);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
267
|
+
|
|
268
|
+
if (needsScroll) {
|
|
269
|
+
const above = scrollTop;
|
|
270
|
+
const below = items.length - scrollTop - visibleCount;
|
|
271
|
+
print(above > 0 ? `${DIM} ▲ ${above} more${RESET}` : "");
|
|
272
|
+
for (let i = scrollTop; i < scrollTop + visibleCount; i++) {
|
|
273
|
+
const pointer = i === cursor ? `${CYAN} ❯ ` : " ";
|
|
274
|
+
const box = checked[i] ? `${GREEN}[✓]${RESET}` : `${DIM}[ ]${RESET}`;
|
|
275
|
+
const label = items[i].label;
|
|
276
|
+
const desc = items[i].description ? ` ${DIM}${items[i].description}${RESET}` : "";
|
|
277
|
+
print(`${pointer}${RESET}${box} ${label}${desc}`);
|
|
278
|
+
}
|
|
279
|
+
print(below > 0 ? `${DIM} ▼ ${below} more${RESET}` : "");
|
|
280
|
+
} else {
|
|
281
|
+
for (let i = 0; i < items.length; i++) {
|
|
282
|
+
const pointer = i === cursor ? `${CYAN} ❯ ` : " ";
|
|
283
|
+
const box = checked[i] ? `${GREEN}[✓]${RESET}` : `${DIM}[ ]${RESET}`;
|
|
284
|
+
const label = items[i].label;
|
|
285
|
+
const desc = items[i].description ? ` ${DIM}${items[i].description}${RESET}` : "";
|
|
286
|
+
print(`${pointer}${RESET}${box} ${label}${desc}`);
|
|
287
|
+
}
|
|
211
288
|
}
|
|
289
|
+
|
|
212
290
|
const previewHint = previewDir ? " · p preview" : "";
|
|
213
|
-
|
|
291
|
+
const backHint = allowBack ? "← back · " : "";
|
|
292
|
+
print(`${DIM} ${backHint}↑↓ navigate · space toggle · a all${previewHint} · →/enter confirm${RESET}`);
|
|
214
293
|
}
|
|
215
294
|
|
|
216
295
|
process.stdout.write(HIDE_CURSOR);
|
|
296
|
+
adjustScroll();
|
|
217
297
|
render(true);
|
|
218
298
|
|
|
219
299
|
process.stdin.setRawMode(true);
|
|
@@ -230,14 +310,29 @@ function multiSelect(title, items, defaults, previewDir) {
|
|
|
230
310
|
return;
|
|
231
311
|
}
|
|
232
312
|
|
|
313
|
+
// Left arrow — go back
|
|
314
|
+
if (allowBack && key === "\x1b[D") {
|
|
315
|
+
process.stdin.setRawMode(false);
|
|
316
|
+
process.stdin.pause();
|
|
317
|
+
process.stdin.removeListener("data", onKey);
|
|
318
|
+
killPreview();
|
|
319
|
+
moveCursorUp(lineCount);
|
|
320
|
+
clearLines(lineCount);
|
|
321
|
+
process.stdout.write(SHOW_CURSOR);
|
|
322
|
+
resolve(null);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
233
326
|
if (key === "\x1b[A" || key === "k") {
|
|
234
327
|
cursor = (cursor - 1 + items.length) % items.length;
|
|
328
|
+
adjustScroll();
|
|
235
329
|
render(false);
|
|
236
330
|
return;
|
|
237
331
|
}
|
|
238
332
|
|
|
239
333
|
if (key === "\x1b[B" || key === "j") {
|
|
240
334
|
cursor = (cursor + 1) % items.length;
|
|
335
|
+
adjustScroll();
|
|
241
336
|
render(false);
|
|
242
337
|
return;
|
|
243
338
|
}
|
|
@@ -249,6 +344,14 @@ function multiSelect(title, items, defaults, previewDir) {
|
|
|
249
344
|
return;
|
|
250
345
|
}
|
|
251
346
|
|
|
347
|
+
// a — toggle all
|
|
348
|
+
if (key === "a") {
|
|
349
|
+
const allChecked = checked.every(Boolean);
|
|
350
|
+
checked.fill(!allChecked);
|
|
351
|
+
render(false);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
252
355
|
// p — preview sound
|
|
253
356
|
if (key === "p" && previewDir && items[cursor].file) {
|
|
254
357
|
const soundPath = path.join(previewDir, items[cursor].file);
|
|
@@ -256,8 +359,8 @@ function multiSelect(title, items, defaults, previewDir) {
|
|
|
256
359
|
return;
|
|
257
360
|
}
|
|
258
361
|
|
|
259
|
-
// Enter — confirm
|
|
260
|
-
if (key === "\r" || key === "\n") {
|
|
362
|
+
// Enter or right arrow — confirm
|
|
363
|
+
if (key === "\r" || key === "\n" || key === "\x1b[C") {
|
|
261
364
|
process.stdin.setRawMode(false);
|
|
262
365
|
process.stdin.pause();
|
|
263
366
|
process.stdin.removeListener("data", onKey);
|
|
@@ -375,6 +478,183 @@ function uninstall() {
|
|
|
375
478
|
print("");
|
|
376
479
|
}
|
|
377
480
|
|
|
481
|
+
// ─── Sound Item Builder ─────────────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Build the full list of sound items for a category.
|
|
485
|
+
* Native sounds (from this category's theme.json entry) come first,
|
|
486
|
+
* then borrowed sounds from all other categories, deduplicated by filename.
|
|
487
|
+
*/
|
|
488
|
+
function buildCategoryItems(theme, category) {
|
|
489
|
+
const config = theme.sounds[category];
|
|
490
|
+
const categories = Object.keys(theme.sounds);
|
|
491
|
+
const items = [];
|
|
492
|
+
const seen = new Set();
|
|
493
|
+
|
|
494
|
+
// Build a map of filename -> list of hooks it appears in
|
|
495
|
+
const hookMap = {};
|
|
496
|
+
for (const cat of categories) {
|
|
497
|
+
for (const f of theme.sounds[cat].files) {
|
|
498
|
+
if (!hookMap[f.name]) hookMap[f.name] = [];
|
|
499
|
+
if (!hookMap[f.name].includes(cat)) hookMap[f.name].push(cat);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Native sounds first
|
|
504
|
+
for (const f of config.files) {
|
|
505
|
+
seen.add(f.name);
|
|
506
|
+
items.push({
|
|
507
|
+
label: f.name.replace(/\.(wav|mp3)$/, ""),
|
|
508
|
+
description: hookMap[f.name].join(", "),
|
|
509
|
+
file: f.name,
|
|
510
|
+
src: f.src,
|
|
511
|
+
native: true,
|
|
512
|
+
originCat: category,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Borrowed sounds from other categories
|
|
517
|
+
for (const otherCat of categories) {
|
|
518
|
+
if (otherCat === category) continue;
|
|
519
|
+
for (const f of theme.sounds[otherCat].files) {
|
|
520
|
+
if (seen.has(f.name)) continue;
|
|
521
|
+
seen.add(f.name);
|
|
522
|
+
items.push({
|
|
523
|
+
label: f.name.replace(/\.(wav|mp3)$/, ""),
|
|
524
|
+
description: hookMap[f.name].join(", "),
|
|
525
|
+
file: f.name,
|
|
526
|
+
src: f.src,
|
|
527
|
+
native: false,
|
|
528
|
+
originCat: otherCat,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return items;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Resolve a sound file's source from download (tmpDir/<srcBase>/...).
|
|
538
|
+
*/
|
|
539
|
+
function resolveDownloadSrc(srcBase, src) {
|
|
540
|
+
if (src.startsWith("@soundfxcenter/")) {
|
|
541
|
+
return path.join(srcBase, path.basename(src));
|
|
542
|
+
}
|
|
543
|
+
return path.join(srcBase, src);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ─── Reconfigure Flow ───────────────────────────────────────────────────────
|
|
547
|
+
|
|
548
|
+
async function reconfigure(existingInstall) {
|
|
549
|
+
const themeDir = path.join(THEMES_DIR, existingInstall.theme);
|
|
550
|
+
const theme = JSON.parse(fs.readFileSync(path.join(themeDir, "theme.json"), "utf-8"));
|
|
551
|
+
const categories = Object.keys(theme.sounds);
|
|
552
|
+
const tmpDirs = [];
|
|
553
|
+
|
|
554
|
+
try {
|
|
555
|
+
let catIdx = 0;
|
|
556
|
+
while (catIdx < categories.length) {
|
|
557
|
+
const cat = categories[catIdx];
|
|
558
|
+
const config = theme.sounds[cat];
|
|
559
|
+
const catDir = path.join(SOUNDS_DIR, cat);
|
|
560
|
+
const disabledDir = path.join(catDir, ".disabled");
|
|
561
|
+
const items = buildCategoryItems(theme, cat);
|
|
562
|
+
|
|
563
|
+
// Determine current state: checked if file exists in category dir
|
|
564
|
+
const defaults = [];
|
|
565
|
+
for (let i = 0; i < items.length; i++) {
|
|
566
|
+
if (fs.existsSync(path.join(catDir, items[i].file))) {
|
|
567
|
+
defaults.push(i);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Build preview dir with all sounds from all possible locations
|
|
572
|
+
const previewDir = fs.mkdtempSync(path.join(os.tmpdir(), `claude-preview-`));
|
|
573
|
+
tmpDirs.push(previewDir);
|
|
574
|
+
for (const item of items) {
|
|
575
|
+
const originCatDir = path.join(SOUNDS_DIR, item.originCat);
|
|
576
|
+
const originDisabledDir = path.join(originCatDir, ".disabled");
|
|
577
|
+
const searchDirs = [catDir, disabledDir, originCatDir, originDisabledDir];
|
|
578
|
+
for (const dir of searchDirs) {
|
|
579
|
+
const p = path.join(dir, item.file);
|
|
580
|
+
if (fs.existsSync(p)) {
|
|
581
|
+
fs.copyFileSync(p, path.join(previewDir, item.file));
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const selected = await multiSelect(
|
|
588
|
+
`${BOLD}${cat}${RESET} ${DIM}— ${config.description}${RESET}`,
|
|
589
|
+
items,
|
|
590
|
+
defaults,
|
|
591
|
+
previewDir,
|
|
592
|
+
{ allowBack: catIdx > 0 }
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
// Back was pressed — go to previous category
|
|
596
|
+
if (selected === null) {
|
|
597
|
+
catIdx--;
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Apply changes
|
|
602
|
+
for (let i = 0; i < items.length; i++) {
|
|
603
|
+
const item = items[i];
|
|
604
|
+
const isSelected = selected.includes(i);
|
|
605
|
+
const enabledPath = path.join(catDir, item.file);
|
|
606
|
+
|
|
607
|
+
if (item.native) {
|
|
608
|
+
const disabledPath = path.join(disabledDir, item.file);
|
|
609
|
+
if (isSelected && !fs.existsSync(enabledPath) && fs.existsSync(disabledPath)) {
|
|
610
|
+
fs.renameSync(disabledPath, enabledPath);
|
|
611
|
+
} else if (!isSelected && fs.existsSync(enabledPath)) {
|
|
612
|
+
mkdirp(disabledDir);
|
|
613
|
+
fs.renameSync(enabledPath, disabledPath);
|
|
614
|
+
}
|
|
615
|
+
} else {
|
|
616
|
+
// Borrowed sound: copy in or delete
|
|
617
|
+
if (isSelected && !fs.existsSync(enabledPath)) {
|
|
618
|
+
const previewFile = path.join(previewDir, item.file);
|
|
619
|
+
if (fs.existsSync(previewFile)) {
|
|
620
|
+
fs.copyFileSync(previewFile, enabledPath);
|
|
621
|
+
}
|
|
622
|
+
} else if (!isSelected && fs.existsSync(enabledPath)) {
|
|
623
|
+
fs.unlinkSync(enabledPath);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
catIdx++;
|
|
629
|
+
}
|
|
630
|
+
} finally {
|
|
631
|
+
for (const dir of tmpDirs) {
|
|
632
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Summary
|
|
637
|
+
let total = 0;
|
|
638
|
+
print(` ${GREEN}✓${RESET} Configuration updated!`);
|
|
639
|
+
print(" ─────────────────────────────────────");
|
|
640
|
+
|
|
641
|
+
for (const cat of categories) {
|
|
642
|
+
const catDir = path.join(SOUNDS_DIR, cat);
|
|
643
|
+
let count = 0;
|
|
644
|
+
try {
|
|
645
|
+
for (const f of fs.readdirSync(catDir)) {
|
|
646
|
+
if (f.endsWith(".wav") || f.endsWith(".mp3")) count++;
|
|
647
|
+
}
|
|
648
|
+
} catch {}
|
|
649
|
+
total += count;
|
|
650
|
+
print(` ${cat} (${count}) — ${theme.sounds[cat].description}`);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
print("");
|
|
654
|
+
print(` ${total} sound files across ${categories.length} events.`);
|
|
655
|
+
print("");
|
|
656
|
+
}
|
|
657
|
+
|
|
378
658
|
// ─── Install Flow ───────────────────────────────────────────────────────────
|
|
379
659
|
|
|
380
660
|
async function interactiveInstall(autoYes) {
|
|
@@ -383,6 +663,31 @@ async function interactiveInstall(autoYes) {
|
|
|
383
663
|
print(" ──────────────────────────────");
|
|
384
664
|
print("");
|
|
385
665
|
|
|
666
|
+
// ── Detect existing install ───────────────────────────────────────────────
|
|
667
|
+
|
|
668
|
+
const existingInstall = getExistingInstall();
|
|
669
|
+
|
|
670
|
+
if (existingInstall && !autoYes) {
|
|
671
|
+
print(` ${GREEN}✓${RESET} Already installed — ${BOLD}${existingInstall.themeDisplay}${RESET}`);
|
|
672
|
+
print(` ${existingInstall.totalEnabled}/${existingInstall.totalAvailable} sounds enabled\n`);
|
|
673
|
+
|
|
674
|
+
const actionIdx = await select("What would you like to do?", [
|
|
675
|
+
{ label: "Reconfigure", description: "Choose which sounds are enabled" },
|
|
676
|
+
{ label: "Reinstall", description: "Re-download and start fresh" },
|
|
677
|
+
{ label: "Uninstall", description: "Remove all sounds and hooks" },
|
|
678
|
+
]);
|
|
679
|
+
|
|
680
|
+
if (actionIdx === 0) {
|
|
681
|
+
await reconfigure(existingInstall);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (actionIdx === 2) {
|
|
685
|
+
uninstall();
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
// actionIdx === 1 falls through to full install
|
|
689
|
+
}
|
|
690
|
+
|
|
386
691
|
// ── Step 1: Dependency Check ──────────────────────────────────────────────
|
|
387
692
|
|
|
388
693
|
const deps = ["afplay", "curl", "unzip"];
|
|
@@ -469,10 +774,14 @@ async function interactiveInstall(autoYes) {
|
|
|
469
774
|
|
|
470
775
|
// ── Step 4: Customize or Accept Defaults ──────────────────────────────
|
|
471
776
|
|
|
472
|
-
// Build
|
|
777
|
+
// Build items and selections for each category (includes all theme sounds)
|
|
778
|
+
const categoryItems = {};
|
|
473
779
|
const selections = {};
|
|
474
780
|
for (const cat of categories) {
|
|
475
|
-
|
|
781
|
+
const items = buildCategoryItems(theme, cat);
|
|
782
|
+
categoryItems[cat] = items;
|
|
783
|
+
// Default: select only native sounds
|
|
784
|
+
selections[cat] = items.map((item, i) => item.native ? i : -1).filter(i => i >= 0);
|
|
476
785
|
}
|
|
477
786
|
|
|
478
787
|
if (!autoYes) {
|
|
@@ -483,29 +792,21 @@ async function interactiveInstall(autoYes) {
|
|
|
483
792
|
const customizeIdx = await select("Customize sounds for each hook?", customizeOptions);
|
|
484
793
|
|
|
485
794
|
if (customizeIdx === 1) {
|
|
486
|
-
|
|
487
|
-
|
|
795
|
+
const srcBase = path.join(tmpDir, theme.srcBase || "Orc");
|
|
796
|
+
let catIdx = 0;
|
|
797
|
+
|
|
798
|
+
while (catIdx < categories.length) {
|
|
799
|
+
const cat = categories[catIdx];
|
|
488
800
|
const config = theme.sounds[cat];
|
|
489
|
-
const items =
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}));
|
|
494
|
-
const defaults = config.files.map((_, i) => i); // all selected by default
|
|
495
|
-
|
|
496
|
-
// Build preview dir: sounds are in tmpDir/Orc/... but we need them by name
|
|
497
|
-
// Copy files to a temp preview dir first
|
|
801
|
+
const items = categoryItems[cat];
|
|
802
|
+
const defaults = selections[cat];
|
|
803
|
+
|
|
804
|
+
// Build preview dir with ALL theme sounds
|
|
498
805
|
const previewDir = path.join(tmpDir, "_preview", cat);
|
|
499
806
|
mkdirp(previewDir);
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if (file.src.startsWith("@soundfxcenter/")) {
|
|
504
|
-
srcFile = path.join(srcBase, path.basename(file.src));
|
|
505
|
-
} else {
|
|
506
|
-
srcFile = path.join(srcBase, file.src);
|
|
507
|
-
}
|
|
508
|
-
const destFile = path.join(previewDir, file.name);
|
|
807
|
+
for (const item of items) {
|
|
808
|
+
const srcFile = resolveDownloadSrc(srcBase, item.src);
|
|
809
|
+
const destFile = path.join(previewDir, item.file);
|
|
509
810
|
if (fs.existsSync(srcFile)) {
|
|
510
811
|
fs.copyFileSync(srcFile, destFile);
|
|
511
812
|
}
|
|
@@ -515,9 +816,17 @@ async function interactiveInstall(autoYes) {
|
|
|
515
816
|
`${BOLD}${cat}${RESET} ${DIM}— ${config.description}${RESET}`,
|
|
516
817
|
items,
|
|
517
818
|
defaults,
|
|
518
|
-
previewDir
|
|
819
|
+
previewDir,
|
|
820
|
+
{ allowBack: catIdx > 0 }
|
|
519
821
|
);
|
|
822
|
+
|
|
823
|
+
if (selected === null) {
|
|
824
|
+
catIdx--;
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
|
|
520
828
|
selections[cat] = selected;
|
|
829
|
+
catIdx++;
|
|
521
830
|
}
|
|
522
831
|
}
|
|
523
832
|
}
|
|
@@ -526,40 +835,54 @@ async function interactiveInstall(autoYes) {
|
|
|
526
835
|
|
|
527
836
|
print(" Installing sounds...");
|
|
528
837
|
|
|
529
|
-
// Clear existing sounds
|
|
838
|
+
// Clear existing sounds and .disabled dirs
|
|
530
839
|
for (const cat of categories) {
|
|
531
840
|
const catDir = path.join(SOUNDS_DIR, cat);
|
|
532
841
|
for (const f of fs.readdirSync(catDir)) {
|
|
533
|
-
|
|
534
|
-
|
|
842
|
+
const fp = path.join(catDir, f);
|
|
843
|
+
if (f === ".disabled") {
|
|
844
|
+
fs.rmSync(fp, { recursive: true, force: true });
|
|
845
|
+
} else if (f.endsWith(".wav") || f.endsWith(".mp3")) {
|
|
846
|
+
fs.unlinkSync(fp);
|
|
535
847
|
}
|
|
536
848
|
}
|
|
537
849
|
}
|
|
538
850
|
|
|
539
|
-
// Copy
|
|
540
|
-
const srcBase = path.join(tmpDir, "Orc");
|
|
851
|
+
// Copy files from download based on selections
|
|
852
|
+
const srcBase = path.join(tmpDir, theme.srcBase || "Orc");
|
|
541
853
|
let total = 0;
|
|
542
|
-
for (const
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
854
|
+
for (const cat of categories) {
|
|
855
|
+
const items = categoryItems[cat];
|
|
856
|
+
const selectedIndices = selections[cat];
|
|
857
|
+
const catDir = path.join(SOUNDS_DIR, cat);
|
|
858
|
+
const disabledDir = path.join(catDir, ".disabled");
|
|
859
|
+
|
|
860
|
+
for (let i = 0; i < items.length; i++) {
|
|
861
|
+
const item = items[i];
|
|
862
|
+
const srcFile = resolveDownloadSrc(srcBase, item.src);
|
|
863
|
+
|
|
864
|
+
if (!fs.existsSync(srcFile)) {
|
|
865
|
+
if (item.native) {
|
|
866
|
+
print(` ${YELLOW}⚠${RESET} ${item.src} not found, skipping`);
|
|
867
|
+
}
|
|
868
|
+
continue;
|
|
551
869
|
}
|
|
552
870
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
fs.copyFileSync(srcFile, destFile);
|
|
871
|
+
if (selectedIndices.includes(i)) {
|
|
872
|
+
fs.copyFileSync(srcFile, path.join(catDir, item.file));
|
|
556
873
|
total++;
|
|
557
|
-
} else {
|
|
558
|
-
|
|
874
|
+
} else if (item.native) {
|
|
875
|
+
// Save unselected native sounds to .disabled for future reconfigure
|
|
876
|
+
mkdirp(disabledDir);
|
|
877
|
+
fs.copyFileSync(srcFile, path.join(disabledDir, item.file));
|
|
559
878
|
}
|
|
879
|
+
// Unselected borrowed sounds: skip (no need to store)
|
|
560
880
|
}
|
|
561
881
|
}
|
|
562
882
|
|
|
883
|
+
// Write install marker
|
|
884
|
+
writeInstalled(selectedTheme.name);
|
|
885
|
+
|
|
563
886
|
// Copy play-sound.sh hook
|
|
564
887
|
const hookSrc = path.join(PKG_DIR, "hooks", "play-sound.sh");
|
|
565
888
|
const hookDest = path.join(HOOKS_DIR, "play-sound.sh");
|
|
@@ -576,11 +899,9 @@ async function interactiveInstall(autoYes) {
|
|
|
576
899
|
print(` ${GREEN}✓${RESET} Installed! Here's what you'll hear:`);
|
|
577
900
|
print(" ─────────────────────────────────────");
|
|
578
901
|
|
|
579
|
-
for (const
|
|
902
|
+
for (const cat of categories) {
|
|
580
903
|
const count = selections[cat].length;
|
|
581
|
-
|
|
582
|
-
const suffix = count < totalAvailable ? ` (${count}/${totalAvailable})` : ` (${count})`;
|
|
583
|
-
print(` ${cat}${suffix} — ${config.description}`);
|
|
904
|
+
print(` ${cat} (${count}) — ${theme.sounds[cat].description}`);
|
|
584
905
|
}
|
|
585
906
|
|
|
586
907
|
print("");
|
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
#
|
|
6
6
|
set -e
|
|
7
7
|
|
|
8
|
+
# shellcheck disable=SC2034 # passed by install.sh, reserved for theme use
|
|
8
9
|
SOUNDS_DIR="$1"
|
|
9
10
|
TMP_DIR="$2"
|
|
10
11
|
|
|
@@ -14,7 +15,7 @@ curl -sL -o "$ZIP" "https://sounds.spriters-resource.com/media/assets/422/425494
|
|
|
14
15
|
|
|
15
16
|
if ! file "$ZIP" | grep -q "Zip"; then
|
|
16
17
|
echo " Error: Failed to download WC3 sound pack."
|
|
17
|
-
|
|
18
|
+
exit 1
|
|
18
19
|
fi
|
|
19
20
|
|
|
20
21
|
unzip -qo "$ZIP" \
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"name": "WC3 Orc Peon",
|
|
3
3
|
"description": "Warcraft 3 Orc unit soundbites — Peons, Grunts, Shamans, and more",
|
|
4
4
|
"author": "ryparker",
|
|
5
|
+
"srcBase": "Orc",
|
|
5
6
|
"sounds": {
|
|
6
7
|
"start": {
|
|
7
8
|
"description": "Session starting — being summoned",
|
|
@@ -34,7 +35,8 @@
|
|
|
34
35
|
"stop": {
|
|
35
36
|
"description": "Done responding — acknowledgment",
|
|
36
37
|
"files": [
|
|
37
|
-
{ "src": "
|
|
38
|
+
{ "src": "Grunt/GruntYes4.wav", "name": "zug-zug.wav" },
|
|
39
|
+
{ "src": "Peon/PeonYesAttack2.wav", "name": "get-em.wav" },
|
|
38
40
|
{ "src": "Peon/PeonYesAttack1.wav", "name": "ok.wav" },
|
|
39
41
|
{ "src": "Peon/PeonYes1.wav", "name": "i-can-do-that.wav" },
|
|
40
42
|
{ "src": "Peon/PeonYes2.wav", "name": "be-happy-to.wav" },
|
|
@@ -52,7 +54,8 @@
|
|
|
52
54
|
{ "src": "Hellscream/GromWarcry1.wav", "name": "taste-the-fury.wav" },
|
|
53
55
|
{ "src": "Peon/PeonYesAttack3.wav", "name": "ill-try.wav" },
|
|
54
56
|
{ "src": "Peon/PeonWarcry1.wav", "name": "why-not.wav" },
|
|
55
|
-
{ "src": "
|
|
57
|
+
{ "src": "Grunt/GruntYes4.wav", "name": "zug-zug.wav" },
|
|
58
|
+
{ "src": "Peon/PeonYesAttack2.wav", "name": "get-em.wav" }
|
|
56
59
|
]
|
|
57
60
|
},
|
|
58
61
|
"idle": {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Downloads sound files for the zelda-oot theme.
|
|
4
|
+
# Called by install.sh with $1 = target sounds directory, $2 = temp directory.
|
|
5
|
+
#
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
# shellcheck disable=SC2034 # passed by install.sh, reserved for theme use
|
|
9
|
+
SOUNDS_DIR="$1"
|
|
10
|
+
TMP_DIR="$2"
|
|
11
|
+
OOT_DIR="$TMP_DIR/OOT"
|
|
12
|
+
|
|
13
|
+
mkdir -p "$OOT_DIR"
|
|
14
|
+
|
|
15
|
+
BASE_URL="https://noproblo.dayjo.org/zeldasounds/oot"
|
|
16
|
+
|
|
17
|
+
# All unique sound files needed by this theme
|
|
18
|
+
FILES=(
|
|
19
|
+
OOT_Navi_Hey1.wav
|
|
20
|
+
OOT_Navi_Hello1.wav
|
|
21
|
+
OOT_Navi_Listen1.wav
|
|
22
|
+
OOT_Navi_Look1.wav
|
|
23
|
+
OOT_Navi_WatchOut1.wav
|
|
24
|
+
OOT_GreatFairy_Laugh1.wav
|
|
25
|
+
OOT_Chest_Big.wav
|
|
26
|
+
OOT_Chest_Small.wav
|
|
27
|
+
OOT_MainMenu_Select.wav
|
|
28
|
+
OOT_Dialogue_Next.wav
|
|
29
|
+
OOT_Dialogue_Done_Mono.wav
|
|
30
|
+
OOT_Get_Heart.wav
|
|
31
|
+
OOT_Get_SmallItem1.wav
|
|
32
|
+
OOT_Get_Rupee.wav
|
|
33
|
+
OOT_Secret_Mono.wav
|
|
34
|
+
OOT_Song_Correct_Mono.wav
|
|
35
|
+
OOT_Song_Error.wav
|
|
36
|
+
OOT_PauseMenu_Open_Mono.wav
|
|
37
|
+
OOT_PauseMenu_Close_Mono.wav
|
|
38
|
+
OOT_ZTarget_Enemy.wav
|
|
39
|
+
OOT_Sword_Draw.wav
|
|
40
|
+
OOT_Sword_Spin.wav
|
|
41
|
+
OOT_AdultLink_Attack1.wav
|
|
42
|
+
OOT_AdultLink_StrongAttack1.wav
|
|
43
|
+
OOT_AdultLink_Hurt1.wav
|
|
44
|
+
OOT_Fanfare_SmallItem.wav
|
|
45
|
+
OOT_Error.wav
|
|
46
|
+
OOT_LowHealth.wav
|
|
47
|
+
OOT_Talon_Snore.wav
|
|
48
|
+
OOT_Ganondorf_Heheh.wav
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
echo " Downloading Zelda: Ocarina of Time sounds..."
|
|
52
|
+
|
|
53
|
+
FAILED=0
|
|
54
|
+
for FILE in "${FILES[@]}"; do
|
|
55
|
+
curl -sL -o "$OOT_DIR/$FILE" "$BASE_URL/$FILE"
|
|
56
|
+
if ! file "$OOT_DIR/$FILE" | grep -q "WAVE\|RIFF"; then
|
|
57
|
+
echo " Warning: Failed to download $FILE"
|
|
58
|
+
rm -f "$OOT_DIR/$FILE"
|
|
59
|
+
FAILED=$((FAILED + 1))
|
|
60
|
+
fi
|
|
61
|
+
done
|
|
62
|
+
|
|
63
|
+
if [ "$FAILED" -gt 0 ]; then
|
|
64
|
+
echo " Warning: $FAILED file(s) failed to download."
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
echo " Download complete."
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Zelda: Ocarina of Time",
|
|
3
|
+
"description": "Navi, Link, and iconic OOT jingles — Hey! Listen!",
|
|
4
|
+
"author": "ryparker",
|
|
5
|
+
"srcBase": "OOT",
|
|
6
|
+
"sounds": {
|
|
7
|
+
"start": {
|
|
8
|
+
"description": "Session starting — Navi greets you",
|
|
9
|
+
"files": [
|
|
10
|
+
{ "src": "OOT_Navi_Hey1.wav", "name": "hey.wav" },
|
|
11
|
+
{ "src": "OOT_Navi_Hello1.wav", "name": "hello.wav" },
|
|
12
|
+
{ "src": "OOT_Navi_Listen1.wav", "name": "listen.wav" },
|
|
13
|
+
{ "src": "OOT_GreatFairy_Laugh1.wav", "name": "great-fairy-laugh.wav" },
|
|
14
|
+
{ "src": "OOT_Chest_Big.wav", "name": "open-chest.wav" }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"end": {
|
|
18
|
+
"description": "Session over — quest complete",
|
|
19
|
+
"files": [
|
|
20
|
+
{ "src": "OOT_Fanfare_SmallItem.wav", "name": "item-fanfare.wav" },
|
|
21
|
+
{ "src": "OOT_Secret_Mono.wav", "name": "secret-discovered.wav" },
|
|
22
|
+
{ "src": "OOT_Dialogue_Done_Mono.wav", "name": "dialogue-done.wav" }
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"permission": {
|
|
26
|
+
"description": "Permission prompt — Navi warns you",
|
|
27
|
+
"files": [
|
|
28
|
+
{ "src": "OOT_Navi_WatchOut1.wav", "name": "watch-out.wav" },
|
|
29
|
+
{ "src": "OOT_Navi_Hey1.wav", "name": "hey.wav" },
|
|
30
|
+
{ "src": "OOT_Navi_Listen1.wav", "name": "listen.wav" },
|
|
31
|
+
{ "src": "OOT_PauseMenu_Open_Mono.wav", "name": "pause-menu.wav" },
|
|
32
|
+
{ "src": "OOT_ZTarget_Enemy.wav", "name": "z-target.wav" }
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"stop": {
|
|
36
|
+
"description": "Done responding — loot collected",
|
|
37
|
+
"files": [
|
|
38
|
+
{ "src": "OOT_Secret_Mono.wav", "name": "secret-discovered.wav" },
|
|
39
|
+
{ "src": "OOT_Get_SmallItem1.wav", "name": "get-item.wav" },
|
|
40
|
+
{ "src": "OOT_Get_Rupee.wav", "name": "get-rupee.wav" },
|
|
41
|
+
{ "src": "OOT_Chest_Small.wav", "name": "open-small-chest.wav" },
|
|
42
|
+
{ "src": "OOT_Song_Correct_Mono.wav", "name": "song-correct.wav" }
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"subagent": {
|
|
46
|
+
"description": "Spawning subagent — Link attacks",
|
|
47
|
+
"files": [
|
|
48
|
+
{ "src": "OOT_Sword_Draw.wav", "name": "sword-draw.wav" },
|
|
49
|
+
{ "src": "OOT_AdultLink_Attack1.wav", "name": "link-attack.wav" },
|
|
50
|
+
{ "src": "OOT_AdultLink_StrongAttack1.wav", "name": "link-strong-attack.wav" },
|
|
51
|
+
{ "src": "OOT_Navi_Hey1.wav", "name": "hey.wav" },
|
|
52
|
+
{ "src": "OOT_Sword_Spin.wav", "name": "spin-attack.wav" }
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"idle": {
|
|
56
|
+
"description": "Waiting for input — Navi pesters you",
|
|
57
|
+
"files": [
|
|
58
|
+
{ "src": "OOT_Navi_Hey1.wav", "name": "hey.wav" },
|
|
59
|
+
{ "src": "OOT_Navi_Listen1.wav", "name": "listen.wav" },
|
|
60
|
+
{ "src": "OOT_Navi_WatchOut1.wav", "name": "watch-out.wav" },
|
|
61
|
+
{ "src": "OOT_LowHealth.wav", "name": "low-health.wav" },
|
|
62
|
+
{ "src": "OOT_Talon_Snore.wav", "name": "snore.wav" }
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
"error": {
|
|
66
|
+
"description": "Tool failure — Link takes damage",
|
|
67
|
+
"files": [
|
|
68
|
+
{ "src": "OOT_Error.wav", "name": "error.wav" },
|
|
69
|
+
{ "src": "OOT_Song_Error.wav", "name": "song-error.wav" },
|
|
70
|
+
{ "src": "OOT_AdultLink_Hurt1.wav", "name": "link-hurt.wav" },
|
|
71
|
+
{ "src": "OOT_Ganondorf_Heheh.wav", "name": "ganondorf-laugh.wav" }
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"prompt": {
|
|
75
|
+
"description": "User submitted prompt — adventure continues",
|
|
76
|
+
"files": [
|
|
77
|
+
{ "src": "OOT_Navi_Hey1.wav", "name": "hey.wav" },
|
|
78
|
+
{ "src": "OOT_Navi_Look1.wav", "name": "look.wav" },
|
|
79
|
+
{ "src": "OOT_MainMenu_Select.wav", "name": "menu-select.wav" },
|
|
80
|
+
{ "src": "OOT_Dialogue_Next.wav", "name": "dialogue-next.wav" },
|
|
81
|
+
{ "src": "OOT_Get_Heart.wav", "name": "get-heart.wav" }
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
"task-completed": {
|
|
85
|
+
"description": "Task finished — treasure acquired",
|
|
86
|
+
"files": [
|
|
87
|
+
{ "src": "OOT_Fanfare_SmallItem.wav", "name": "item-fanfare.wav" },
|
|
88
|
+
{ "src": "OOT_Secret_Mono.wav", "name": "secret-discovered.wav" }
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
"compact": {
|
|
92
|
+
"description": "Context compaction — health running low",
|
|
93
|
+
"files": [
|
|
94
|
+
{ "src": "OOT_LowHealth.wav", "name": "low-health.wav" },
|
|
95
|
+
{ "src": "OOT_PauseMenu_Close_Mono.wav", "name": "pause-close.wav" },
|
|
96
|
+
{ "src": "OOT_Navi_WatchOut1.wav", "name": "watch-out.wav" },
|
|
97
|
+
{ "src": "OOT_Error.wav", "name": "error.wav" }
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
"teammate-idle": {
|
|
101
|
+
"description": "Teammate went idle — Navi nags",
|
|
102
|
+
"files": [
|
|
103
|
+
{ "src": "OOT_Navi_Hey1.wav", "name": "hey.wav" },
|
|
104
|
+
{ "src": "OOT_Navi_Listen1.wav", "name": "listen.wav" },
|
|
105
|
+
{ "src": "OOT_Navi_WatchOut1.wav", "name": "watch-out.wav" },
|
|
106
|
+
{ "src": "OOT_LowHealth.wav", "name": "low-health.wav" }
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|