castle-web-cli 0.4.128 → 0.4.130
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/dist/agent-prompts.js +4 -4
- package/dist/filesChanged.d.ts +2 -0
- package/dist/filesChanged.js +6 -2
- package/dist/ide.js +15 -4
- package/dist/importBrowse.js +1 -0
- package/dist/index.js +10 -4
- package/dist/init.js +63 -15
- package/dist/native/loop.js +1 -1
- package/dist/native/tools.js +1 -1
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +20 -1
- package/dist/shell/assets/index-BpOLUyVO.js +441 -0
- package/dist/shell/assets/{index-CAq6f9B5.css → index-D6K-0YDB.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/kits/physics-2d/CLAUDE.md +40 -23
- package/kits/physics-2d/behaviors/Joints.jsx +4 -3
- package/kits/physics-2d/behaviors/SoundPlayer.jsx +266 -0
- package/kits/physics-2d/behaviors/Sprite.jsx +3 -3
- package/kits/physics-2d/behaviors/Video.jsx +4 -3
- package/kits/physics-2d/castle.json +1 -1
- package/kits/physics-2d/editors/PlayOnly.jsx +15 -6
- package/kits/physics-2d/editors/PxArtEditor.jsx +95 -31
- package/kits/physics-2d/editors/SceneEditor.jsx +5 -5
- package/kits/physics-2d/editors/SingleEditor.jsx +37 -8
- package/kits/physics-2d/editors/StyleEditor.jsx +7 -4
- package/kits/physics-2d/editors/pixelEditorChrome.jsx +7 -7
- package/kits/physics-2d/editors/pixelInspector.jsx +13 -7
- package/kits/physics-2d/editors/pxArtTimeline.jsx +29 -11
- package/kits/physics-2d/editors/pxArtTimeline.module.css +11 -0
- package/kits/physics-2d/engine/audioContext.js +41 -0
- package/kits/physics-2d/engine/autoInspector.jsx +50 -33
- package/kits/physics-2d/engine/blueprint.js +15 -3
- package/kits/physics-2d/engine/liveReload.js +33 -21
- package/kits/physics-2d/engine/media.js +29 -39
- package/kits/physics-2d/engine/sound.js +316 -0
- package/kits/physics-2d/engine/systemRegistry.js +1 -1
- package/kits/physics-2d/engine/ui.jsx +274 -5
- package/kits/physics-2d/engine/ui.module.css +137 -16
- package/kits/physics-2d/scripts/testsounds.mjs +84 -0
- package/kits/physics-2d/systems/media.js +31 -15
- package/kits/physics-3d/CLAUDE.md +109 -0
- package/kits/physics-3d/behaviors/Body.jsx +51 -0
- package/kits/physics-3d/behaviors/Door.jsx +31 -0
- package/kits/physics-3d/behaviors/Lookable.jsx +11 -0
- package/kits/physics-3d/behaviors/Model.jsx +14 -0
- package/kits/physics-3d/behaviors/Pickup.jsx +39 -0
- package/kits/physics-3d/behaviors/Player.jsx +172 -0
- package/kits/physics-3d/behaviors/Portal.jsx +19 -0
- package/kits/physics-3d/behaviors/Shape.jsx +75 -0
- package/kits/physics-3d/behaviors/Solid.jsx +11 -0
- package/kits/physics-3d/behaviors/Transform.jsx +24 -0
- package/kits/physics-3d/blueprints/barrel.scene +15 -0
- package/kits/physics-3d/blueprints/crate.scene +29 -0
- package/kits/physics-3d/blueprints/door.scene +35 -0
- package/kits/physics-3d/blueprints/gem.scene +26 -0
- package/kits/physics-3d/blueprints/pillar.scene +25 -0
- package/kits/physics-3d/blueprints/platform.scene +23 -0
- package/kits/physics-3d/blueprints/player.scene +29 -0
- package/kits/physics-3d/blueprints/portal.scene +21 -0
- package/kits/physics-3d/blueprints/rock.scene +14 -0
- package/kits/physics-3d/blueprints/statue.scene +26 -0
- package/kits/physics-3d/blueprints/tree.scene +14 -0
- package/kits/physics-3d/blueprints/wall.scene +26 -0
- package/kits/{basic-2d → physics-3d}/castle.json +23 -5
- package/kits/physics-3d/docs/pxmodel-format.md +111 -0
- package/kits/physics-3d/drawings/crate.pxart +26 -0
- package/kits/physics-3d/drawings/door-edge.pxart +26 -0
- package/kits/physics-3d/drawings/door.pxart +27 -0
- package/kits/physics-3d/drawings/face.pxart +26 -0
- package/kits/physics-3d/drawings/floor.pxart +27 -0
- package/kits/physics-3d/drawings/platform.pxart +27 -0
- package/kits/physics-3d/drawings/statue.pxart +27 -0
- package/kits/physics-3d/drawings/wall.pxart +27 -0
- package/kits/physics-3d/engine3d/PlayOnly3D.jsx +32 -0
- package/kits/physics-3d/engine3d/PxModelEditor.jsx +619 -0
- package/kits/physics-3d/engine3d/Scene3DEditor.jsx +618 -0
- package/kits/physics-3d/engine3d/Scene3DPlayer.jsx +309 -0
- package/kits/physics-3d/engine3d/editor3dData.js +114 -0
- package/kits/physics-3d/engine3d/editor3dInspector.jsx +404 -0
- package/kits/physics-3d/engine3d/editorChrome.jsx +218 -0
- package/kits/physics-3d/engine3d/editorWorld.js +249 -0
- package/kits/physics-3d/engine3d/materials.js +174 -0
- package/kits/physics-3d/engine3d/meshops.js +123 -0
- package/kits/physics-3d/engine3d/modelEditorWorld.js +439 -0
- package/kits/physics-3d/engine3d/modelFiles.js +139 -0
- package/kits/physics-3d/engine3d/pxmodel.js +265 -0
- package/kits/physics-3d/engine3d/thumbnails.js +137 -0
- package/kits/physics-3d/engine3d/world3d.js +216 -0
- package/kits/{basic-2d → physics-3d}/eslint.config.js +10 -25
- package/kits/physics-3d/index.html +20 -0
- package/kits/physics-3d/main.jsx +30 -0
- package/kits/physics-3d/models/barrel.pxmodel +10 -0
- package/kits/physics-3d/models/player.pxmodel +222 -0
- package/kits/physics-3d/models/rock.pxmodel +30 -0
- package/kits/physics-3d/models/tree.pxmodel +10 -0
- package/kits/{basic-2d → physics-3d}/package-lock.json +140 -119
- package/kits/{basic-2d → physics-3d}/package.json +11 -9
- package/kits/{basic-2d → physics-3d}/pnpm-lock.yaml +162 -137
- package/kits/physics-3d/scenes/main.scene +414 -0
- package/kits/physics-3d/scenes/model-lab.scene +127 -0
- package/kits/physics-3d/systems/physics3d.js +356 -0
- package/package.json +5 -2
- package/dist/shell/assets/index-DKu9ejyh.js +0 -436
- package/kits/basic-2d/CLAUDE.md +0 -221
- package/kits/basic-2d/behaviors/Camera.jsx +0 -43
- package/kits/basic-2d/behaviors/Collider.jsx +0 -213
- package/kits/basic-2d/behaviors/Layout.jsx +0 -53
- package/kits/basic-2d/behaviors/Sprite.jsx +0 -357
- package/kits/basic-2d/behaviors/tint.js +0 -47
- package/kits/basic-2d/blueprints/cauldron.scene +0 -20
- package/kits/basic-2d/docs/pxart-format.md +0 -377
- package/kits/basic-2d/drawings/cauldron.pxart +0 -113
- package/kits/basic-2d/editors/BlueprintLibrary.jsx +0 -270
- package/kits/basic-2d/editors/ErrorBoundary.jsx +0 -59
- package/kits/basic-2d/editors/PlayOnly.jsx +0 -31
- package/kits/basic-2d/editors/PxArtEditor.jsx +0 -1092
- package/kits/basic-2d/editors/SceneEditor.jsx +0 -1780
- package/kits/basic-2d/editors/SelectionOverlay.jsx +0 -909
- package/kits/basic-2d/editors/SingleEditor.jsx +0 -122
- package/kits/basic-2d/editors/behaviorRegistry.js +0 -34
- package/kits/basic-2d/editors/editorHistory.js +0 -157
- package/kits/basic-2d/editors/inspectorSheet.js +0 -13
- package/kits/basic-2d/editors/pixelCanvas.js +0 -11
- package/kits/basic-2d/editors/pixelEditorChrome.jsx +0 -74
- package/kits/basic-2d/editors/pixelGeometry.js +0 -140
- package/kits/basic-2d/editors/pixelInspector.jsx +0 -633
- package/kits/basic-2d/editors/pxArtEditorModel.js +0 -732
- package/kits/basic-2d/editors/pxArtPlayback.js +0 -92
- package/kits/basic-2d/editors/pxArtTimeline.jsx +0 -752
- package/kits/basic-2d/editors/pxArtTimeline.module.css +0 -506
- package/kits/basic-2d/editors/pxArtTools.js +0 -232
- package/kits/basic-2d/editors/useArtboardFit.js +0 -105
- package/kits/basic-2d/engine/ScenePlayer.jsx +0 -209
- package/kits/basic-2d/engine/SceneUI.jsx +0 -59
- package/kits/basic-2d/engine/assets.js +0 -15
- package/kits/basic-2d/engine/autoInspector.jsx +0 -70
- package/kits/basic-2d/engine/behaviorExtensions.js +0 -32
- package/kits/basic-2d/engine/blueprint.js +0 -557
- package/kits/basic-2d/engine/collider.js +0 -200
- package/kits/basic-2d/engine/files.js +0 -141
- package/kits/basic-2d/engine/liveReload.js +0 -88
- package/kits/basic-2d/engine/pxart.js +0 -1032
- package/kits/basic-2d/engine/pxartSmooth.js +0 -222
- package/kits/basic-2d/engine/scene.js +0 -696
- package/kits/basic-2d/engine/spriteGeometry.js +0 -32
- package/kits/basic-2d/engine/systemRegistry.js +0 -16
- package/kits/basic-2d/engine/ui.jsx +0 -695
- package/kits/basic-2d/engine/ui.module.css +0 -2287
- package/kits/basic-2d/index.html +0 -24
- package/kits/basic-2d/main.jsx +0 -24
- package/kits/basic-2d/scenes/main.scene +0 -16
- package/kits/basic-2d/scripts/draw.mjs +0 -121
- package/kits/physics-2d/behaviors/Sound.jsx +0 -163
- package/kits/physics-2d/behaviors/Tone.jsx +0 -95
- package/kits/physics-2d/engine/tone.js +0 -112
- /package/kits/{basic-2d → physics-3d}/.prettierrc +0 -0
- /package/kits/{basic-2d → physics-3d}/vite.config.js +0 -0
package/kits/basic-2d/index.html
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
|
|
5
|
-
<meta name="color-scheme" content="dark" />
|
|
6
|
-
<title>Castle Basic 2D JS Kit</title>
|
|
7
|
-
<!-- Declare the dark scheme + opaque dark background statically, before the
|
|
8
|
-
JS-imported CSS module loads. Otherwise this frame paints with the
|
|
9
|
-
default (light) scheme first and only switches to dark once the module
|
|
10
|
-
applies -- and Safari fails to repaint a composited iframe on a
|
|
11
|
-
color-scheme change (WebKit bug 309097), leaving static panels like the
|
|
12
|
-
file list blank until a forced repaint (e.g. opening devtools). -->
|
|
13
|
-
<style>
|
|
14
|
-
html {
|
|
15
|
-
color-scheme: dark;
|
|
16
|
-
background: #0a0a0a;
|
|
17
|
-
}
|
|
18
|
-
</style>
|
|
19
|
-
</head>
|
|
20
|
-
<body>
|
|
21
|
-
<div id="root"></div>
|
|
22
|
-
<script type="module" src="/main.jsx"></script>
|
|
23
|
-
</body>
|
|
24
|
-
</html>
|
package/kits/basic-2d/main.jsx
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { createRoot } from 'react-dom/client';
|
|
3
|
-
import { isEdit, setup } from 'castle-web-sdk';
|
|
4
|
-
import { PlayOnly } from './editors/PlayOnly';
|
|
5
|
-
import { ErrorBoundary } from './editors/ErrorBoundary';
|
|
6
|
-
import { SingleEditor } from './editors/SingleEditor';
|
|
7
|
-
document.body.dataset.castleScreenshotTarget = 'viewport';
|
|
8
|
-
setup();
|
|
9
|
-
const root = document.getElementById('root');
|
|
10
|
-
if (!root) throw new Error('Missing root element');
|
|
11
|
-
// Route on URL params so the shell can mount each piece in its own panel iframe.
|
|
12
|
-
// File browsing and the code/text editor are now builtin shell panels, so the
|
|
13
|
-
// kit only renders the deck (play) and its rich per-file editors (scene/pxart):
|
|
14
|
-
// (no edit / ?edit=0) -> play the deck (playtest panel)
|
|
15
|
-
// ?file=<path>[&editor=<id>] -> a single rich editor for that file
|
|
16
|
-
const params = new URLSearchParams(window.location.search);
|
|
17
|
-
function pick() {
|
|
18
|
-
const initialScene = params.get('scene') ?? undefined;
|
|
19
|
-
if (!isEdit()) return <PlayOnly initialScene={initialScene} />;
|
|
20
|
-
const file = params.get('file');
|
|
21
|
-
if (file) return <SingleEditor path={file} editor={params.get('editor') ?? undefined} />;
|
|
22
|
-
return <PlayOnly initialScene={initialScene} />;
|
|
23
|
-
}
|
|
24
|
-
createRoot(root).render(<ErrorBoundary>{pick()}</ErrorBoundary>);
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
// `npm run draw -- <name>` — turn a terse svg-rect emission into a sprite.
|
|
2
|
-
//
|
|
3
|
-
// Reads an <svg> with one <rect> per pixel from stdin (or `--from <file>`),
|
|
4
|
-
// decodes + palette-quantizes it onto the agent 16-color subset, and writes a
|
|
5
|
-
// single-frame `drawings/<name>.pxart` in the deck (the script's cwd).
|
|
6
|
-
//
|
|
7
|
-
// This is a kit-layer concern: the `.pxart` format lives entirely in the kit
|
|
8
|
-
// (engine/pxart.js); the harness has no knowledge of it.
|
|
9
|
-
|
|
10
|
-
import * as fs from 'fs';
|
|
11
|
-
import * as path from 'path';
|
|
12
|
-
import {
|
|
13
|
-
svgRectToPxArt,
|
|
14
|
-
serializeCompact,
|
|
15
|
-
AGENT_PALETTE_16,
|
|
16
|
-
DEFAULT_RESOLUTION,
|
|
17
|
-
} from '../engine/pxart.js';
|
|
18
|
-
|
|
19
|
-
// Read all of stdin as a UTF-8 string. Resolves '' if stdin is a TTY with no
|
|
20
|
-
// piped input so we can give a clear "no input" error instead of hanging.
|
|
21
|
-
function readStdin() {
|
|
22
|
-
return new Promise((resolve, reject) => {
|
|
23
|
-
if (process.stdin.isTTY) {
|
|
24
|
-
resolve('');
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
const chunks = [];
|
|
28
|
-
process.stdin.on('data', (c) => chunks.push(c));
|
|
29
|
-
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
30
|
-
process.stdin.on('error', reject);
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Normalize a requested sprite name into a relative `drawings/<...>.pxart`
|
|
35
|
-
// path. Tolerates a leading `drawings/` and/or a trailing `.pxart`/`.px.json`
|
|
36
|
-
// so `draw ship`, `draw ship.pxart`, and `draw drawings/ship.pxart` all land in
|
|
37
|
-
// the same place.
|
|
38
|
-
function resolveSpritePath(dir, name) {
|
|
39
|
-
let rel = name.trim().replace(/\\/g, '/');
|
|
40
|
-
rel = rel.replace(/^\.?\/*/, '');
|
|
41
|
-
if (rel.startsWith('drawings/')) rel = rel.slice('drawings/'.length);
|
|
42
|
-
rel = rel.replace(/\.px\.json$/i, '').replace(/\.pxart$/i, '');
|
|
43
|
-
if (!rel) return null;
|
|
44
|
-
|
|
45
|
-
const drawingsDir = path.resolve(dir, 'drawings');
|
|
46
|
-
const filepath = path.resolve(drawingsDir, `${rel}.pxart`);
|
|
47
|
-
const within = path.relative(drawingsDir, filepath);
|
|
48
|
-
if (within.startsWith('..') || path.isAbsolute(within)) return null;
|
|
49
|
-
return filepath;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function getFlagValue(flag) {
|
|
53
|
-
const idx = process.argv.indexOf(flag);
|
|
54
|
-
return idx >= 0 ? process.argv[idx + 1] : undefined;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// All positional (non-flag) args after the script, in order.
|
|
58
|
-
function positionals() {
|
|
59
|
-
const args = process.argv.slice(2);
|
|
60
|
-
const withValues = new Set(['--from']);
|
|
61
|
-
const out = [];
|
|
62
|
-
for (let i = 0; i < args.length; i++) {
|
|
63
|
-
if (args[i].startsWith('--')) {
|
|
64
|
-
if (withValues.has(args[i])) i++;
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
out.push(args[i]);
|
|
68
|
-
}
|
|
69
|
-
return out;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function main() {
|
|
73
|
-
const name = positionals()[0];
|
|
74
|
-
if (!name) {
|
|
75
|
-
console.error('Usage: npm run draw -- <name> [--from FILE] (svg-rect on stdin -> drawings/<name>.pxart)');
|
|
76
|
-
process.exit(1);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// The script's cwd is the deck; sprites land under <deck>/drawings/.
|
|
80
|
-
const dir = process.cwd();
|
|
81
|
-
const filepath = resolveSpritePath(dir, name);
|
|
82
|
-
if (!filepath) {
|
|
83
|
-
console.error(`Invalid sprite name: ${name}`);
|
|
84
|
-
process.exit(1);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const fromFile = getFlagValue('--from');
|
|
88
|
-
let svg;
|
|
89
|
-
if (fromFile) {
|
|
90
|
-
try {
|
|
91
|
-
svg = fs.readFileSync(path.resolve(fromFile), 'utf-8');
|
|
92
|
-
} catch (e) {
|
|
93
|
-
console.error(`Could not read --from file ${fromFile}: ${e instanceof Error ? e.message : String(e)}`);
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
} else {
|
|
97
|
-
svg = await readStdin();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (!svg.trim()) {
|
|
101
|
-
console.error('No svg-rect input. Pipe an <svg> with <rect> cells via stdin or pass --from <file>.');
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const art = svgRectToPxArt(svg, { resolution: DEFAULT_RESOLUTION, palette: AGENT_PALETTE_16 });
|
|
106
|
-
if (!art) {
|
|
107
|
-
console.error('Could not decode any pixels from the svg-rect input (no usable <rect fill="...">). Emit one <rect> per filled pixel on a 16x16 viewBox.');
|
|
108
|
-
process.exit(1);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
|
112
|
-
fs.writeFileSync(filepath, serializeCompact(art), 'utf-8');
|
|
113
|
-
|
|
114
|
-
const rel = path.relative(path.resolve(dir), filepath);
|
|
115
|
-
console.log(`Wrote ${rel}`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
main().catch((e) => {
|
|
119
|
-
console.error(e instanceof Error ? e.message : String(e));
|
|
120
|
-
process.exit(1);
|
|
121
|
-
});
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { resolveDeckFile } from 'castle-web-sdk';
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { mediaElement, playMedia, playOneShot, setMediaPan, stopMedia } from '../engine/media';
|
|
4
|
-
import { mediaFilesOfKind } from '../engine/files';
|
|
5
|
-
import { Panel, SelectField } from '../engine/ui';
|
|
6
|
-
import { AutoFields, overrideProps } from '../engine/autoInspector';
|
|
7
|
-
import { PAN, UNIT } from '../engine/propertyRanges';
|
|
8
|
-
|
|
9
|
-
// Plays a sound file (.mp3 / .wav / .m4a) from an actor.
|
|
10
|
-
//
|
|
11
|
-
// Two ways to use it. Set `playOnStart` and it plays when the scene starts --
|
|
12
|
-
// music, ambience, a jingle on a game-over screen. Or leave it off and trigger
|
|
13
|
-
// it from a behavior, through the handle this puts on the actor:
|
|
14
|
-
//
|
|
15
|
-
// scene.getActor('sfx').runtime.sound.play(); // from the top
|
|
16
|
-
// await actor.runtime.sound.play(); // ...and wait for it to end
|
|
17
|
-
// actor.runtime.sound.stop();
|
|
18
|
-
//
|
|
19
|
-
// `play()` on a `polyphonic` sound overlaps copies instead of cutting the last
|
|
20
|
-
// one off, which is what a sound effect fired twice in a row should do. Without
|
|
21
|
-
// it, a second play restarts the one element, which is what music wants.
|
|
22
|
-
//
|
|
23
|
-
// Sound only plays during play, never in the editor: `update` is what wires the
|
|
24
|
-
// element up, and the editor doesn't run it. Pressing Stop silences everything,
|
|
25
|
-
// as does `scene.sound.stopAll()` (see systems/media.js).
|
|
26
|
-
//
|
|
27
|
-
// Browsers refuse to play audio until the player has touched the page. A sound
|
|
28
|
-
// that starts before that isn't lost -- it's queued and starts on the first tap
|
|
29
|
-
// or key (engine/media.js), which for a game is usually the first input anyway.
|
|
30
|
-
export class Sound {
|
|
31
|
-
static behaviorName = 'Sound';
|
|
32
|
-
|
|
33
|
-
static defaultProps = {
|
|
34
|
-
file: '',
|
|
35
|
-
volume: 1,
|
|
36
|
-
playbackRate: 1,
|
|
37
|
-
pan: 0,
|
|
38
|
-
loop: false,
|
|
39
|
-
polyphonic: false,
|
|
40
|
-
playOnStart: true,
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// Mirrors the clamps `readSettings` already applies at runtime, so the
|
|
44
|
-
// inspector can't author a value the sound would silently ignore -- including
|
|
45
|
-
// playbackRate's 0.01..10, where the classic editor's ceiling lives.
|
|
46
|
-
static propertyMeta = {
|
|
47
|
-
volume: UNIT,
|
|
48
|
-
playbackRate: { min: 0.01, max: 10, step: 0.05 },
|
|
49
|
-
pan: PAN,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
constructor(props) {
|
|
53
|
-
this.props = props;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
update(actor) {
|
|
57
|
-
if (!actor.runtime || actor.runtime.collected) return;
|
|
58
|
-
const file = resolveDeckFile(this.props.file);
|
|
59
|
-
const element = mediaElement(actor.id, 'audio', file);
|
|
60
|
-
if (!element) {
|
|
61
|
-
actor.runtime.sound = null;
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
// Props are live: turning the volume down, the pitch up, or the loop on in
|
|
65
|
-
// the inspector takes effect on the sound already playing.
|
|
66
|
-
const settings = readSettings(this.props);
|
|
67
|
-
element.volume = settings.volume;
|
|
68
|
-
element.playbackRate = settings.playbackRate;
|
|
69
|
-
element.loop = settings.loop;
|
|
70
|
-
if (settings.pan !== 0) setMediaPan(element, settings.pan);
|
|
71
|
-
|
|
72
|
-
const handle = actor.runtime.sound;
|
|
73
|
-
if (!handle || handle.file !== file) {
|
|
74
|
-
actor.runtime.sound = makeHandle(file, element, () => readSettings(this.props));
|
|
75
|
-
if (this.props.playOnStart) void actor.runtime.sound.play();
|
|
76
|
-
} else {
|
|
77
|
-
handle.settings = settings;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
static Inspector({ component, setComponent, override }) {
|
|
82
|
-
return (
|
|
83
|
-
<Panel title="Sound" overridden={override?.anyOverridden()}>
|
|
84
|
-
<SelectField
|
|
85
|
-
label="File"
|
|
86
|
-
value={component.file}
|
|
87
|
-
onChange={(file) => setComponent({ file })}
|
|
88
|
-
options={mediaFilesOfKind('audio')}
|
|
89
|
-
{...overrideProps(override, 'file')}
|
|
90
|
-
/>
|
|
91
|
-
<AutoFields
|
|
92
|
-
defaultProps={Sound.defaultProps}
|
|
93
|
-
meta={Sound.propertyMeta}
|
|
94
|
-
component={component}
|
|
95
|
-
setComponent={setComponent}
|
|
96
|
-
only={['volume', 'playbackRate', 'pan', 'loop', 'polyphonic', 'playOnStart']}
|
|
97
|
-
override={override}
|
|
98
|
-
/>
|
|
99
|
-
</Panel>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function clamp(value, min, max, fallback) {
|
|
105
|
-
return Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function readSettings(props) {
|
|
109
|
-
return {
|
|
110
|
-
volume: clamp(props.volume, 0, 1, 1),
|
|
111
|
-
// The same ceiling the classic editor used: past 10x a sound is a click.
|
|
112
|
-
playbackRate: clamp(props.playbackRate, 0.01, 10, 1),
|
|
113
|
-
pan: clamp(props.pan, -1, 1, 0),
|
|
114
|
-
loop: Boolean(props.loop),
|
|
115
|
-
polyphonic: Boolean(props.polyphonic),
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// What a behavior gets at `actor.runtime.sound`. Plain methods over the element,
|
|
120
|
-
// so game code never has to know an <audio> is involved.
|
|
121
|
-
//
|
|
122
|
-
// `play()` returns a promise that resolves when the sound ends, so a behavior
|
|
123
|
-
// can `await` one before doing the next thing. A looping sound resolves right
|
|
124
|
-
// away rather than never -- awaiting something that by definition doesn't end
|
|
125
|
-
// would hang the caller forever, and that is never what was meant.
|
|
126
|
-
function makeHandle(file, element, currentSettings) {
|
|
127
|
-
const handle = {
|
|
128
|
-
file,
|
|
129
|
-
element,
|
|
130
|
-
settings: currentSettings(),
|
|
131
|
-
play() {
|
|
132
|
-
const s = handle.settings;
|
|
133
|
-
if (s.polyphonic) {
|
|
134
|
-
return playOneShot(file, { volume: s.volume, playbackRate: s.playbackRate, pan: s.pan });
|
|
135
|
-
}
|
|
136
|
-
try {
|
|
137
|
-
element.currentTime = 0;
|
|
138
|
-
} catch {
|
|
139
|
-
// Not seekable yet (still loading) -- it plays from the start anyway.
|
|
140
|
-
}
|
|
141
|
-
playMedia(element);
|
|
142
|
-
if (s.loop) return Promise.resolve();
|
|
143
|
-
return new Promise((resolve) => {
|
|
144
|
-
const done = () => resolve();
|
|
145
|
-
element.addEventListener('ended', done, { once: true });
|
|
146
|
-
element.addEventListener('error', done, { once: true });
|
|
147
|
-
});
|
|
148
|
-
},
|
|
149
|
-
resume() {
|
|
150
|
-
playMedia(element);
|
|
151
|
-
},
|
|
152
|
-
pause() {
|
|
153
|
-
element.pause();
|
|
154
|
-
},
|
|
155
|
-
stop() {
|
|
156
|
-
stopMedia(element);
|
|
157
|
-
},
|
|
158
|
-
get playing() {
|
|
159
|
-
return !element.paused;
|
|
160
|
-
},
|
|
161
|
-
};
|
|
162
|
-
return handle;
|
|
163
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { playTone, WAVEFORMS } from '../engine/tone';
|
|
3
|
-
import { Panel, SelectField } from '../engine/ui';
|
|
4
|
-
import { AutoFields, overrideProps } from '../engine/autoInspector';
|
|
5
|
-
import { PAN, POSITIVE, UNIT } from '../engine/propertyRanges';
|
|
6
|
-
|
|
7
|
-
// Plays a synthesized note -- no audio file involved. This is the cheapest sound
|
|
8
|
-
// a deck can make: a blip on a pickup, a thud on a miss, a rising scale as a
|
|
9
|
-
// counter climbs. Nothing is downloaded and nothing is stored.
|
|
10
|
-
//
|
|
11
|
-
// actor.runtime.tone.play(); // the note as configured
|
|
12
|
-
// actor.runtime.tone.play({ note: 72 }); // ...or override it per play
|
|
13
|
-
// await actor.runtime.tone.play(); // wait for the release to finish
|
|
14
|
-
//
|
|
15
|
-
// `note` is a MIDI number: 60 is middle C, 72 an octave up, +1 a semitone. The
|
|
16
|
-
// envelope is attack (silence up to full) then release (full down to silence),
|
|
17
|
-
// so the note's whole length is attack + release seconds.
|
|
18
|
-
export class Tone {
|
|
19
|
-
static behaviorName = 'Tone';
|
|
20
|
-
|
|
21
|
-
static defaultProps = {
|
|
22
|
-
note: 60,
|
|
23
|
-
waveform: 'square',
|
|
24
|
-
attack: 0,
|
|
25
|
-
release: 0.3,
|
|
26
|
-
volume: 0.5,
|
|
27
|
-
pan: 0,
|
|
28
|
-
playOnStart: false,
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// `note` is MIDI, which is genuinely 0..127. `attack`/`release` are seconds,
|
|
32
|
-
// so they get a floor but no ceiling.
|
|
33
|
-
static propertyMeta = {
|
|
34
|
-
note: { min: 0, max: 127, step: 1 },
|
|
35
|
-
attack: POSITIVE,
|
|
36
|
-
release: POSITIVE,
|
|
37
|
-
volume: UNIT,
|
|
38
|
-
pan: PAN,
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
constructor(props) {
|
|
42
|
-
this.props = props;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
update(actor) {
|
|
46
|
-
if (!actor.runtime || actor.runtime.collected) return;
|
|
47
|
-
const handle = actor.runtime.tone;
|
|
48
|
-
if (!handle) {
|
|
49
|
-
actor.runtime.tone = makeHandle(() => this.props);
|
|
50
|
-
if (this.props.playOnStart) void actor.runtime.tone.play();
|
|
51
|
-
} else {
|
|
52
|
-
handle.props = this.props;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
static Inspector({ component, setComponent, override }) {
|
|
57
|
-
return (
|
|
58
|
-
<Panel title="Tone" overridden={override?.anyOverridden()}>
|
|
59
|
-
<SelectField
|
|
60
|
-
label="Waveform"
|
|
61
|
-
value={component.waveform}
|
|
62
|
-
onChange={(waveform) => setComponent({ waveform })}
|
|
63
|
-
options={WAVEFORMS}
|
|
64
|
-
{...overrideProps(override, 'waveform')}
|
|
65
|
-
/>
|
|
66
|
-
<AutoFields
|
|
67
|
-
defaultProps={Tone.defaultProps}
|
|
68
|
-
meta={Tone.propertyMeta}
|
|
69
|
-
component={component}
|
|
70
|
-
setComponent={setComponent}
|
|
71
|
-
only={['note', 'attack', 'release', 'volume', 'pan', 'playOnStart']}
|
|
72
|
-
override={override}
|
|
73
|
-
/>
|
|
74
|
-
</Panel>
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// `actor.runtime.tone`. `play(overrides)` takes the same fields as the props, so
|
|
80
|
-
// one Tone actor can cover a whole scale without a component per note.
|
|
81
|
-
function makeHandle(currentProps) {
|
|
82
|
-
const handle = {
|
|
83
|
-
props: currentProps(),
|
|
84
|
-
voice: null,
|
|
85
|
-
play(overrides) {
|
|
86
|
-
handle.voice = playTone({ ...handle.props, ...overrides });
|
|
87
|
-
return handle.voice.finished;
|
|
88
|
-
},
|
|
89
|
-
stop() {
|
|
90
|
-
handle.voice?.stop();
|
|
91
|
-
handle.voice = null;
|
|
92
|
-
},
|
|
93
|
-
};
|
|
94
|
-
return handle;
|
|
95
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
// A synthesized note: one oscillator through an attack/release envelope. This
|
|
2
|
-
// is the sound a deck can make with no audio file at all -- a blip on a pickup,
|
|
3
|
-
// a beep on a wrong answer -- and it costs nothing to ship.
|
|
4
|
-
//
|
|
5
|
-
// Deliberately one voice and two envelope stages, matching what the classic
|
|
6
|
-
// Castle editor's "tone" sample offers. Anything richer is an instrument, and an
|
|
7
|
-
// instrument belongs in a music system, not here.
|
|
8
|
-
|
|
9
|
-
import { getAudioContext, resumeAudioContext } from './audioContext';
|
|
10
|
-
|
|
11
|
-
export const WAVEFORMS = ['square', 'sawtooth', 'sine', 'triangle', 'noise'];
|
|
12
|
-
|
|
13
|
-
// Voices currently sounding, so play mode can be silenced on Stop.
|
|
14
|
-
const voices = new Set();
|
|
15
|
-
|
|
16
|
-
// MIDI note number to Hz. 69 is A440, 12 notes to the octave.
|
|
17
|
-
export function noteToFrequency(note) {
|
|
18
|
-
const midi = Number.isFinite(note) ? note : 60;
|
|
19
|
-
return 440 * Math.pow(2, (midi - 69) / 12);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// A short burst of white noise, made once and reused. Noise is the one waveform
|
|
23
|
-
// an oscillator can't produce, and it's what a percussive hit wants.
|
|
24
|
-
let noiseBuffer = null;
|
|
25
|
-
function getNoiseBuffer(ctx) {
|
|
26
|
-
if (noiseBuffer && noiseBuffer.sampleRate === ctx.sampleRate) return noiseBuffer;
|
|
27
|
-
const frames = Math.floor(ctx.sampleRate * 2);
|
|
28
|
-
const buffer = ctx.createBuffer(1, frames, ctx.sampleRate);
|
|
29
|
-
const data = buffer.getChannelData(0);
|
|
30
|
-
for (let i = 0; i < frames; i++) data[i] = Math.random() * 2 - 1;
|
|
31
|
-
noiseBuffer = buffer;
|
|
32
|
-
return buffer;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function makeSource(ctx, waveform, frequency) {
|
|
36
|
-
if (waveform === 'noise') {
|
|
37
|
-
const source = ctx.createBufferSource();
|
|
38
|
-
source.buffer = getNoiseBuffer(ctx);
|
|
39
|
-
source.loop = true;
|
|
40
|
-
return source;
|
|
41
|
-
}
|
|
42
|
-
const osc = ctx.createOscillator();
|
|
43
|
-
osc.type = WAVEFORMS.includes(waveform) ? waveform : 'square';
|
|
44
|
-
osc.frequency.value = frequency;
|
|
45
|
-
return osc;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Play one note. Returns `{ stop, finished }` -- `finished` resolves when the
|
|
49
|
-
// release has run out, so a caller can wait for it the way a Sound can.
|
|
50
|
-
export function playTone({ note = 60, waveform = 'square', attack = 0, release = 0.4, volume = 1, pan = 0 } = {}) {
|
|
51
|
-
const ctx = getAudioContext();
|
|
52
|
-
if (!ctx) return { stop() {}, finished: Promise.resolve() };
|
|
53
|
-
resumeAudioContext();
|
|
54
|
-
|
|
55
|
-
const level = Number.isFinite(volume) ? Math.min(1, Math.max(0, volume)) : 1;
|
|
56
|
-
const rise = Math.max(0, Number.isFinite(attack) ? attack : 0);
|
|
57
|
-
// A note with no release at all clicks; give it a floor short enough to still
|
|
58
|
-
// read as instant.
|
|
59
|
-
const fall = Math.max(0.01, Number.isFinite(release) ? release : 0.4);
|
|
60
|
-
const start = ctx.currentTime;
|
|
61
|
-
const end = start + rise + fall;
|
|
62
|
-
|
|
63
|
-
const source = makeSource(ctx, waveform, noteToFrequency(note));
|
|
64
|
-
const gain = ctx.createGain();
|
|
65
|
-
gain.gain.setValueAtTime(0, start);
|
|
66
|
-
gain.gain.linearRampToValueAtTime(level, start + rise);
|
|
67
|
-
gain.gain.linearRampToValueAtTime(0, end);
|
|
68
|
-
|
|
69
|
-
let tail = gain;
|
|
70
|
-
if (pan && ctx.createStereoPanner) {
|
|
71
|
-
const panner = ctx.createStereoPanner();
|
|
72
|
-
panner.pan.value = Math.min(1, Math.max(-1, pan));
|
|
73
|
-
gain.connect(panner);
|
|
74
|
-
tail = panner;
|
|
75
|
-
}
|
|
76
|
-
source.connect(gain);
|
|
77
|
-
tail.connect(ctx.destination);
|
|
78
|
-
source.start(start);
|
|
79
|
-
source.stop(end);
|
|
80
|
-
|
|
81
|
-
const voice = { source, gain };
|
|
82
|
-
voices.add(voice);
|
|
83
|
-
const finished = new Promise((resolve) => {
|
|
84
|
-
source.addEventListener('ended', () => {
|
|
85
|
-
voices.delete(voice);
|
|
86
|
-
resolve();
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
return {
|
|
90
|
-
stop() {
|
|
91
|
-
voices.delete(voice);
|
|
92
|
-
try {
|
|
93
|
-
source.stop();
|
|
94
|
-
} catch {
|
|
95
|
-
// Already stopped -- nothing to do.
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
finished,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Silence every sounding note. Play mode ending must not leave one ringing.
|
|
103
|
-
export function stopAllTones() {
|
|
104
|
-
for (const voice of voices) {
|
|
105
|
-
try {
|
|
106
|
-
voice.source.stop();
|
|
107
|
-
} catch {
|
|
108
|
-
// Already stopped.
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
voices.clear();
|
|
112
|
-
}
|
|
File without changes
|
|
File without changes
|