castle-web-cli 0.4.166 → 0.4.168
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.d.ts +1 -2
- package/dist/agent.js +22 -13
- package/dist/api.js +16 -1
- package/dist/castle-host/host.d.ts +2 -0
- package/dist/castle-host/host.js +17 -3
- package/dist/imports.js +19 -4
- package/dist/localPaths.d.ts +2 -0
- package/dist/localPaths.js +6 -0
- package/dist/platformDoc.js +1 -1
- package/dist/save-deck.js +13 -1
- package/dist/serve.js +7 -1
- package/dist/serveSecurity.d.ts +12 -0
- package/dist/serveSecurity.js +42 -0
- package/dist/shell/assets/index-CVHj503j.css +1 -0
- package/dist/shell/assets/index-D-qJ5VGp.js +425 -0
- package/dist/shell/index.html +2 -2
- package/kits/base/castle.json +64 -2
- package/kits/base/editors/ImageViewer.js +259 -0
- package/kits/base/editors/MediaPlayer.js +90 -0
- package/kits/base/editors/imageViewer.css +95 -0
- package/kits/base/editors/mediaPlayer.css +58 -0
- package/kits/base/sdk/README.md +3 -1
- package/kits/base/sdk/castle.d.ts +1 -0
- package/kits/base/sdk/commands.d.ts +7 -0
- package/kits/base/sdk/multiplayer.d.ts +6 -0
- package/kits/base/sdk/multiplayer.js +6 -0
- package/kits/blank/.castle/agent/children.json +1 -0
- package/kits/blank/.castle/agent/messages.json +9 -0
- package/kits/blank/.castle/agent/pending-sends.json +1 -0
- package/kits/blank/.castle/agent/tasks/index.md +2 -0
- package/kits/blank/.castle/docs/platform.md +36 -0
- package/kits/blank/node_modules/.vite/deps/_metadata.json +8 -0
- package/kits/blank/node_modules/.vite/deps/package.json +3 -0
- package/kits/multiplayer-2d/CLAUDE.md +6 -0
- package/kits/multiplayer-2d/castle.json +3 -3
- package/kits/multiplayer-2d/code/systems/multiplayer.js +5 -0
- package/kits/multiplayer-2d/package-lock.json +4 -1
- package/kits/multiplayer-3d/CLAUDE.md +6 -1
- package/kits/multiplayer-3d/castle.json +3 -3
- package/kits/multiplayer-3d/code/systems/multiplayer.js +5 -0
- package/kits/multiplayer-3d/package-lock.json +1 -1
- package/kits/physics-2d/behaviors/Joints.jsx +11 -5
- package/kits/physics-2d/behaviors/Sprite.jsx +6 -21
- package/kits/physics-2d/castle.json +1 -61
- package/kits/physics-2d/editors/BlueprintLibrary.jsx +8 -68
- package/kits/physics-2d/editors/SingleEditor.jsx +3 -10
- package/kits/physics-2d/editors/editorRegistry.jsx +36 -2
- package/kits/physics-2d/engine/art.js +25 -1
- package/kits/physics-2d/engine/artThumbnail.jsx +92 -0
- package/kits/physics-2d/engine/files.js +4 -15
- package/kits/physics-2d/engine/spriteField.jsx +176 -0
- package/kits/physics-2d/engine/ui.module.css +136 -0
- package/kits/physics-2d/package-lock.json +5 -0
- package/kits/physics-2d/pnpm-lock.yaml +96 -3
- package/kits/physics-3d/castle.json +1 -1
- package/kits/physics-3d/package-lock.json +1 -1
- package/kits/physics-3d/pnpm-lock.yaml +88 -3
- package/kits/real-time/CLAUDE.md +5 -0
- package/kits/real-time/castle.json +2 -2
- package/kits/real-time/code/client/connection.js +4 -0
- package/kits/real-time/code/client/gameHooks.js +3 -0
- package/kits/real-time/code/client/soloBadge.js +71 -0
- package/kits/real-time/package.json +3 -0
- package/kits/turn-based/castle.json +1 -1
- package/kits/turn-based/package-lock.json +1 -1
- package/package.json +1 -1
- package/dist/shell/assets/index-CG9TrvCm.js +0 -444
- package/dist/shell/assets/index-CIRBt1qf.css +0 -1
- package/kits/base/node_modules/.package-lock.json +0 -28
- package/kits/base/node_modules/castle-web-fonts/README.md +0 -114
- package/kits/base/node_modules/castle-web-fonts/dist/catalog.d.ts +0 -18
- package/kits/base/node_modules/castle-web-fonts/dist/catalog.js +0 -142
- package/kits/base/node_modules/castle-web-fonts/dist/face.d.ts +0 -1
- package/kits/base/node_modules/castle-web-fonts/dist/face.js +0 -52
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Bore.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Bore.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/CourierPrime.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/CourierPrime.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/DMSans.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/DMSans.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Glacier.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Glacier.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/HelicoCentrica.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/HelicoCentrica.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Piazzolla.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Piazzolla.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Synco.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Synco.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Tektur.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/Tektur.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/faces/YatraOne.d.ts +0 -3
- package/kits/base/node_modules/castle-web-fonts/dist/faces/YatraOne.js +0 -10
- package/kits/base/node_modules/castle-web-fonts/dist/index.d.ts +0 -16
- package/kits/base/node_modules/castle-web-fonts/dist/index.js +0 -15
- package/kits/base/node_modules/castle-web-fonts/licenses/CC-BY-ND-4.0.txt +0 -393
- package/kits/base/node_modules/castle-web-fonts/licenses/FONTS.md +0 -64
- package/kits/base/node_modules/castle-web-fonts/licenses/OFL-1.1.txt +0 -95
- package/kits/base/node_modules/castle-web-fonts/package.json +0 -47
- package/kits/base/node_modules/castle-web-fonts/woff2/Bore.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/CourierPrime.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/DMSans.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/Glacier.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/HelicoCentrica.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/Piazzolla.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/SOURCE.txt +0 -2
- package/kits/base/node_modules/castle-web-fonts/woff2/Synco.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/Tektur.woff2 +0 -0
- package/kits/base/node_modules/castle-web-fonts/woff2/YatraOne.woff2 +0 -0
- package/kits/physics-2d/editors/ImageViewer.jsx +0 -206
- package/kits/physics-2d/editors/MediaPlayer.jsx +0 -57
- package/kits/physics-2d/editors/mediaFile.js +0 -31
- package/kits/physics-2d/editors/mediaViewer.module.css +0 -128
package/dist/shell/index.html
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
<link rel="icon" type="image/png" sizes="32x32" href="/__castle/ide/favicon-32x32.png" />
|
|
11
11
|
<link rel="icon" type="image/png" sizes="16x16" href="/__castle/ide/favicon-16x16.png" />
|
|
12
12
|
<link rel="icon" href="/__castle/ide/favicon.ico" sizes="any" />
|
|
13
|
-
<script type="module" crossorigin src="/__castle/ide/assets/index-
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/__castle/ide/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/__castle/ide/assets/index-D-qJ5VGp.js"></script>
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/__castle/ide/assets/index-CVHj503j.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
package/kits/base/castle.json
CHANGED
|
@@ -1,13 +1,75 @@
|
|
|
1
1
|
{
|
|
2
2
|
"editor": {
|
|
3
3
|
"hiddenPaths": [],
|
|
4
|
-
"visiblePaths": []
|
|
4
|
+
"visiblePaths": [],
|
|
5
|
+
"fileTypes": [
|
|
6
|
+
{
|
|
7
|
+
"ext": ".png",
|
|
8
|
+
"label": "Image",
|
|
9
|
+
"icon": "image",
|
|
10
|
+
"editor": "editors/ImageViewer.js"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"ext": ".jpg",
|
|
14
|
+
"label": "Image",
|
|
15
|
+
"icon": "image",
|
|
16
|
+
"editor": "editors/ImageViewer.js"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"ext": ".jpeg",
|
|
20
|
+
"label": "Image",
|
|
21
|
+
"icon": "image",
|
|
22
|
+
"editor": "editors/ImageViewer.js"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"ext": ".gif",
|
|
26
|
+
"label": "Image",
|
|
27
|
+
"icon": "image",
|
|
28
|
+
"editor": "editors/ImageViewer.js"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"ext": ".webp",
|
|
32
|
+
"label": "Image",
|
|
33
|
+
"icon": "image",
|
|
34
|
+
"editor": "editors/ImageViewer.js"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"ext": ".svg",
|
|
38
|
+
"label": "Image",
|
|
39
|
+
"icon": "image",
|
|
40
|
+
"editor": "editors/ImageViewer.js"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"ext": ".mp3",
|
|
44
|
+
"label": "Audio",
|
|
45
|
+
"icon": "music",
|
|
46
|
+
"editor": "editors/MediaPlayer.js"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"ext": ".wav",
|
|
50
|
+
"label": "Audio",
|
|
51
|
+
"icon": "music",
|
|
52
|
+
"editor": "editors/MediaPlayer.js"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"ext": ".m4a",
|
|
56
|
+
"label": "Audio",
|
|
57
|
+
"icon": "music",
|
|
58
|
+
"editor": "editors/MediaPlayer.js"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"ext": ".mp4",
|
|
62
|
+
"label": "Video",
|
|
63
|
+
"icon": "film",
|
|
64
|
+
"editor": "editors/MediaPlayer.js"
|
|
65
|
+
}
|
|
66
|
+
]
|
|
5
67
|
},
|
|
6
68
|
"autoUpdateWhenImported": true,
|
|
7
69
|
"title": "base",
|
|
8
70
|
"deckId": "yRcmH4_aYllE",
|
|
9
71
|
"cardId": "zkFdingmIm3m",
|
|
10
|
-
"publishedVersion": "2026-09-
|
|
72
|
+
"publishedVersion": "2026-09-05T01:31:11.287Z",
|
|
11
73
|
"provides": [
|
|
12
74
|
"castle-web-sdk"
|
|
13
75
|
]
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// Read-only image viewer for the base kit. Opened for the image extensions
|
|
2
|
+
// declared in this kit's castle.json, so an uploaded .png/.jpg/.svg opens as a
|
|
3
|
+
// picture instead of a wall of binary in the code editor — including on bare
|
|
4
|
+
// (`--kit none`) decks that have no framework kit.
|
|
5
|
+
//
|
|
6
|
+
// Framework-free mount editor (see editorHost.js): default export is
|
|
7
|
+
// `mount(el, props)`. Physics-2d's DeclaredEditor wraps that shape when it
|
|
8
|
+
// loads an import's module.
|
|
9
|
+
//
|
|
10
|
+
// It VIEWS: fit / zoom / pan, at 1:1 when you want to count pixels. Nothing
|
|
11
|
+
// here writes.
|
|
12
|
+
|
|
13
|
+
import { fileUrl, onFilesChanged } from 'castle-web-sdk';
|
|
14
|
+
import './imageViewer.css';
|
|
15
|
+
|
|
16
|
+
const MIN_SCALE = 0.05;
|
|
17
|
+
const MAX_SCALE = 64;
|
|
18
|
+
const PIXELATE_ABOVE = 2.5;
|
|
19
|
+
const WHEEL_ZOOM_PER_PX = 0.0016;
|
|
20
|
+
|
|
21
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
22
|
+
|
|
23
|
+
function basename(path) {
|
|
24
|
+
const i = path.lastIndexOf('/');
|
|
25
|
+
return i < 0 ? path : path.slice(i + 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fitScale(stage, natural) {
|
|
29
|
+
if (!stage.width || !stage.height || !natural.width || !natural.height) return 1;
|
|
30
|
+
const margin = 24;
|
|
31
|
+
const sx = (stage.width - margin) / natural.width;
|
|
32
|
+
const sy = (stage.height - margin) / natural.height;
|
|
33
|
+
return clamp(Math.min(sx, sy), MIN_SCALE, MAX_SCALE);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function zoomAbout(view, factor, px, py) {
|
|
37
|
+
const scale = clamp(view.scale * factor, MIN_SCALE, MAX_SCALE);
|
|
38
|
+
const k = scale / view.scale;
|
|
39
|
+
return { scale, x: px - (px - view.x) * k, y: py - (py - view.y) * k };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function mediaUrl(path, version) {
|
|
43
|
+
const base = fileUrl(path);
|
|
44
|
+
return version === 0 ? base : `${base}&v=${version}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {HTMLElement} el
|
|
49
|
+
* @param {{ path: string }} props
|
|
50
|
+
*/
|
|
51
|
+
export default function mount(el, { path }) {
|
|
52
|
+
el.textContent = '';
|
|
53
|
+
el.style.cssText = 'display:flex;flex:1;width:100%;height:100%;min-height:100vh;';
|
|
54
|
+
|
|
55
|
+
const root = document.createElement('div');
|
|
56
|
+
root.className = 'castle-image-viewer';
|
|
57
|
+
root.innerHTML = `
|
|
58
|
+
<div class="castle-image-viewer-toolbar">
|
|
59
|
+
<button type="button" data-act="zoom-out" aria-label="Zoom out">−</button>
|
|
60
|
+
<button type="button" data-act="zoom-in" aria-label="Zoom in">+</button>
|
|
61
|
+
<button type="button" data-act="fit">Fit</button>
|
|
62
|
+
<button type="button" data-act="one">1:1</button>
|
|
63
|
+
<span class="castle-image-viewer-readout" data-readout></span>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="castle-image-viewer-stage" data-stage>
|
|
66
|
+
<img class="castle-image-viewer-image" alt="" data-image />
|
|
67
|
+
<div class="castle-image-viewer-message" data-message>Loading…</div>
|
|
68
|
+
</div>
|
|
69
|
+
`;
|
|
70
|
+
el.appendChild(root);
|
|
71
|
+
|
|
72
|
+
const stage = root.querySelector('[data-stage]');
|
|
73
|
+
const image = root.querySelector('[data-image]');
|
|
74
|
+
const message = root.querySelector('[data-message]');
|
|
75
|
+
const readout = root.querySelector('[data-readout]');
|
|
76
|
+
|
|
77
|
+
let view = { scale: 1, x: 0, y: 0 };
|
|
78
|
+
let natural = { width: 0, height: 0 };
|
|
79
|
+
let touched = false;
|
|
80
|
+
let dragging = false;
|
|
81
|
+
let version = 0;
|
|
82
|
+
let status = 'loading';
|
|
83
|
+
const stageSize = { width: 0, height: 0 };
|
|
84
|
+
|
|
85
|
+
const applyTransform = () => {
|
|
86
|
+
image.style.width = natural.width ? `${natural.width}px` : '';
|
|
87
|
+
image.style.height = natural.height ? `${natural.height}px` : '';
|
|
88
|
+
image.style.transform = `translate(-50%, -50%) translate(${view.x}px, ${view.y}px) scale(${view.scale})`;
|
|
89
|
+
image.style.visibility = status === 'ready' ? 'visible' : 'hidden';
|
|
90
|
+
image.classList.toggle('pixelated', view.scale >= PIXELATE_ABOVE);
|
|
91
|
+
stage.classList.toggle('dragging', dragging);
|
|
92
|
+
readout.textContent =
|
|
93
|
+
status === 'ready' ? `${natural.width}×${natural.height} · ${Math.round(view.scale * 100)}%` : '';
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const setStatus = (next) => {
|
|
97
|
+
status = next;
|
|
98
|
+
if (next === 'loading') {
|
|
99
|
+
message.textContent = 'Loading…';
|
|
100
|
+
message.className = 'castle-image-viewer-message';
|
|
101
|
+
message.hidden = false;
|
|
102
|
+
} else if (next === 'error') {
|
|
103
|
+
message.textContent = `Could not load ${basename(path)}.`;
|
|
104
|
+
message.className = 'castle-image-viewer-message error';
|
|
105
|
+
message.hidden = false;
|
|
106
|
+
} else {
|
|
107
|
+
message.hidden = true;
|
|
108
|
+
}
|
|
109
|
+
applyTransform();
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const fit = () => {
|
|
113
|
+
touched = false;
|
|
114
|
+
view = { scale: fitScale(stageSize, natural), x: 0, y: 0 };
|
|
115
|
+
applyTransform();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const zoomAt = (factor, clientX, clientY) => {
|
|
119
|
+
const rect = stage.getBoundingClientRect();
|
|
120
|
+
const px = clientX - rect.left - rect.width / 2;
|
|
121
|
+
const py = clientY - rect.top - rect.height / 2;
|
|
122
|
+
touched = true;
|
|
123
|
+
view = zoomAbout(view, factor, px, py);
|
|
124
|
+
applyTransform();
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const zoomBy = (factor) => {
|
|
128
|
+
touched = true;
|
|
129
|
+
view = zoomAbout(view, factor, 0, 0);
|
|
130
|
+
applyTransform();
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const setScale = (scale) => {
|
|
134
|
+
touched = true;
|
|
135
|
+
view = zoomAbout(view, scale / view.scale, 0, 0);
|
|
136
|
+
applyTransform();
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const measure = () => {
|
|
140
|
+
stageSize.width = stage.clientWidth;
|
|
141
|
+
stageSize.height = stage.clientHeight;
|
|
142
|
+
if (!touched && status === 'ready') fit();
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const load = () => {
|
|
146
|
+
setStatus('loading');
|
|
147
|
+
image.alt = basename(path);
|
|
148
|
+
image.src = mediaUrl(path, version);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
image.onload = () => {
|
|
152
|
+
natural = {
|
|
153
|
+
width: image.naturalWidth || 300,
|
|
154
|
+
height: image.naturalHeight || 300,
|
|
155
|
+
};
|
|
156
|
+
setStatus('ready');
|
|
157
|
+
fit();
|
|
158
|
+
};
|
|
159
|
+
image.onerror = () => setStatus('error');
|
|
160
|
+
|
|
161
|
+
root.querySelector('[data-act="zoom-out"]').onclick = () => zoomBy(1 / 1.4);
|
|
162
|
+
root.querySelector('[data-act="zoom-in"]').onclick = () => zoomBy(1.4);
|
|
163
|
+
root.querySelector('[data-act="fit"]').onclick = () => fit();
|
|
164
|
+
root.querySelector('[data-act="one"]').onclick = () => setScale(1);
|
|
165
|
+
|
|
166
|
+
const onWheel = (event) => {
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
zoomAt(Math.exp(-event.deltaY * WHEEL_ZOOM_PER_PX), event.clientX, event.clientY);
|
|
169
|
+
};
|
|
170
|
+
stage.addEventListener('wheel', onWheel, { passive: false });
|
|
171
|
+
|
|
172
|
+
stage.onpointerdown = (event) => {
|
|
173
|
+
if (event.pointerType === 'touch' || event.button !== 0) return;
|
|
174
|
+
stage.setPointerCapture(event.pointerId);
|
|
175
|
+
dragging = true;
|
|
176
|
+
applyTransform();
|
|
177
|
+
};
|
|
178
|
+
stage.onpointermove = (event) => {
|
|
179
|
+
if (!dragging) return;
|
|
180
|
+
touched = true;
|
|
181
|
+
view = { ...view, x: view.x + event.movementX, y: view.y + event.movementY };
|
|
182
|
+
applyTransform();
|
|
183
|
+
};
|
|
184
|
+
const endDrag = () => {
|
|
185
|
+
dragging = false;
|
|
186
|
+
applyTransform();
|
|
187
|
+
};
|
|
188
|
+
stage.onpointerup = endDrag;
|
|
189
|
+
stage.onpointercancel = endDrag;
|
|
190
|
+
stage.ondblclick = () => fit();
|
|
191
|
+
|
|
192
|
+
// Two-finger pinch / pan on touch.
|
|
193
|
+
const active = new Map();
|
|
194
|
+
let lastPinch = null;
|
|
195
|
+
stage.addEventListener(
|
|
196
|
+
'pointerdown',
|
|
197
|
+
(event) => {
|
|
198
|
+
if (event.pointerType !== 'touch') return;
|
|
199
|
+
active.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
200
|
+
if (active.size === 2) {
|
|
201
|
+
const [a, b] = [...active.values()];
|
|
202
|
+
lastPinch = {
|
|
203
|
+
dist: Math.hypot(b.x - a.x, b.y - a.y),
|
|
204
|
+
cx: (a.x + b.x) / 2,
|
|
205
|
+
cy: (a.y + b.y) / 2,
|
|
206
|
+
};
|
|
207
|
+
endDrag();
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
true,
|
|
211
|
+
);
|
|
212
|
+
stage.addEventListener(
|
|
213
|
+
'pointermove',
|
|
214
|
+
(event) => {
|
|
215
|
+
if (!active.has(event.pointerId)) return;
|
|
216
|
+
active.set(event.pointerId, { x: event.clientX, y: event.clientY });
|
|
217
|
+
if (active.size !== 2 || !lastPinch) return;
|
|
218
|
+
event.stopPropagation();
|
|
219
|
+
const [a, b] = [...active.values()];
|
|
220
|
+
const dist = Math.hypot(b.x - a.x, b.y - a.y);
|
|
221
|
+
const cx = (a.x + b.x) / 2;
|
|
222
|
+
const cy = (a.y + b.y) / 2;
|
|
223
|
+
const factor = lastPinch.dist > 0 ? dist / lastPinch.dist : 1;
|
|
224
|
+
zoomAt(factor, cx, cy);
|
|
225
|
+
touched = true;
|
|
226
|
+
view = { ...view, x: view.x + (cx - lastPinch.cx), y: view.y + (cy - lastPinch.cy) };
|
|
227
|
+
applyTransform();
|
|
228
|
+
lastPinch = { dist, cx, cy };
|
|
229
|
+
},
|
|
230
|
+
true,
|
|
231
|
+
);
|
|
232
|
+
const endTouch = (event) => {
|
|
233
|
+
active.delete(event.pointerId);
|
|
234
|
+
if (active.size < 2) lastPinch = null;
|
|
235
|
+
};
|
|
236
|
+
stage.addEventListener('pointerup', endTouch, true);
|
|
237
|
+
stage.addEventListener('pointercancel', endTouch, true);
|
|
238
|
+
|
|
239
|
+
const resizeObserver =
|
|
240
|
+
typeof ResizeObserver !== 'undefined' ? new ResizeObserver(measure) : null;
|
|
241
|
+
resizeObserver?.observe(stage);
|
|
242
|
+
measure();
|
|
243
|
+
|
|
244
|
+
const offFiles = onFilesChanged((event) => {
|
|
245
|
+
if (event.changes.some((change) => change.path === path)) {
|
|
246
|
+
version += 1;
|
|
247
|
+
load();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
load();
|
|
252
|
+
|
|
253
|
+
return () => {
|
|
254
|
+
offFiles?.();
|
|
255
|
+
resizeObserver?.disconnect();
|
|
256
|
+
stage.removeEventListener('wheel', onWheel);
|
|
257
|
+
el.textContent = '';
|
|
258
|
+
};
|
|
259
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Read-only audio / video player for the base kit. Opened for the sound and
|
|
2
|
+
// video extensions declared in this kit's castle.json, so an uploaded .mp3 or
|
|
3
|
+
// .mp4 can be played in place — including on bare (`--kit none`) decks.
|
|
4
|
+
//
|
|
5
|
+
// Framework-free mount editor (see editorHost.js). The transport is the
|
|
6
|
+
// browser's `<audio controls>` / `<video controls>`; this file is the frame
|
|
7
|
+
// around that element. Nothing here writes.
|
|
8
|
+
|
|
9
|
+
import { fileUrl, onFilesChanged } from 'castle-web-sdk';
|
|
10
|
+
import './mediaPlayer.css';
|
|
11
|
+
|
|
12
|
+
const VIDEO_EXTS = ['.mp4'];
|
|
13
|
+
|
|
14
|
+
function basename(path) {
|
|
15
|
+
const i = path.lastIndexOf('/');
|
|
16
|
+
return i < 0 ? path : path.slice(i + 1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isVideoPath(path) {
|
|
20
|
+
const lower = path.toLowerCase();
|
|
21
|
+
return VIDEO_EXTS.some((ext) => lower.endsWith(ext));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function mediaUrl(path, version) {
|
|
25
|
+
const base = fileUrl(path);
|
|
26
|
+
return version === 0 ? base : `${base}&v=${version}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {HTMLElement} el
|
|
31
|
+
* @param {{ path: string }} props
|
|
32
|
+
*/
|
|
33
|
+
export default function mount(el, { path }) {
|
|
34
|
+
el.textContent = '';
|
|
35
|
+
el.style.cssText = 'display:flex;flex:1;width:100%;height:100%;min-height:100vh;';
|
|
36
|
+
|
|
37
|
+
const video = isVideoPath(path);
|
|
38
|
+
const root = document.createElement('div');
|
|
39
|
+
root.className = 'castle-media-player';
|
|
40
|
+
root.innerHTML = `
|
|
41
|
+
<div class="castle-media-player-panel ${video ? 'video' : 'audio'}">
|
|
42
|
+
<div class="castle-media-player-name" data-name></div>
|
|
43
|
+
<${video ? 'video' : 'audio'}
|
|
44
|
+
class="castle-media-player-${video ? 'video' : 'audio'}"
|
|
45
|
+
data-media
|
|
46
|
+
controls
|
|
47
|
+
playsinline
|
|
48
|
+
></${video ? 'video' : 'audio'}>
|
|
49
|
+
<div class="castle-media-player-error" data-error hidden></div>
|
|
50
|
+
</div>
|
|
51
|
+
`;
|
|
52
|
+
el.appendChild(root);
|
|
53
|
+
|
|
54
|
+
const nameEl = root.querySelector('[data-name]');
|
|
55
|
+
const media = root.querySelector('[data-media]');
|
|
56
|
+
const errorEl = root.querySelector('[data-error]');
|
|
57
|
+
let version = 0;
|
|
58
|
+
|
|
59
|
+
// Metadata only for audio: opening shouldn't pull the whole file until play.
|
|
60
|
+
// Video preloads enough for a first-frame poster so the panel isn't black.
|
|
61
|
+
media.preload = video ? 'auto' : 'metadata';
|
|
62
|
+
|
|
63
|
+
const load = () => {
|
|
64
|
+
nameEl.textContent = basename(path);
|
|
65
|
+
errorEl.hidden = true;
|
|
66
|
+
errorEl.textContent = '';
|
|
67
|
+
media.src = mediaUrl(path, version);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
media.onerror = () => {
|
|
71
|
+
errorEl.textContent = `Could not play ${basename(path)} — this browser may not support its format.`;
|
|
72
|
+
errorEl.hidden = false;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const offFiles = onFilesChanged((event) => {
|
|
76
|
+
if (event.changes.some((change) => change.path === path)) {
|
|
77
|
+
version += 1;
|
|
78
|
+
load();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
load();
|
|
83
|
+
|
|
84
|
+
return () => {
|
|
85
|
+
offFiles?.();
|
|
86
|
+
media.removeAttribute('src');
|
|
87
|
+
media.load();
|
|
88
|
+
el.textContent = '';
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/* Read-only image viewer chrome: toolbar over a stage that fills the panel. */
|
|
2
|
+
|
|
3
|
+
.castle-image-viewer {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
width: 100%;
|
|
7
|
+
height: 100%;
|
|
8
|
+
min-height: 100vh;
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
color: #e6e6ea;
|
|
11
|
+
background: #161616;
|
|
12
|
+
font: 13px system-ui, sans-serif;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.castle-image-viewer-toolbar {
|
|
16
|
+
flex: 0 0 auto;
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 8px;
|
|
20
|
+
padding: 6px 10px;
|
|
21
|
+
border-bottom: 1px solid #2a2a33;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.castle-image-viewer-toolbar button {
|
|
25
|
+
appearance: none;
|
|
26
|
+
border: 1px solid #3a3a44;
|
|
27
|
+
background: #22222a;
|
|
28
|
+
color: inherit;
|
|
29
|
+
border-radius: 6px;
|
|
30
|
+
padding: 6px 10px;
|
|
31
|
+
font: inherit;
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.castle-image-viewer-toolbar button:active {
|
|
36
|
+
background: #2c2c36;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.castle-image-viewer-readout {
|
|
40
|
+
margin-left: auto;
|
|
41
|
+
font-size: 12px;
|
|
42
|
+
opacity: 0.7;
|
|
43
|
+
font-variant-numeric: tabular-nums;
|
|
44
|
+
white-space: nowrap;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.castle-image-viewer-stage {
|
|
48
|
+
position: relative;
|
|
49
|
+
flex: 1 1 auto;
|
|
50
|
+
min-height: 0;
|
|
51
|
+
overflow: hidden;
|
|
52
|
+
background: repeating-conic-gradient(#1b1b21 0% 25%, #23232b 0% 50%) 50% / 20px 20px;
|
|
53
|
+
touch-action: none;
|
|
54
|
+
cursor: grab;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.castle-image-viewer-stage.dragging {
|
|
58
|
+
cursor: grabbing;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.castle-image-viewer-image {
|
|
62
|
+
position: absolute;
|
|
63
|
+
top: 50%;
|
|
64
|
+
left: 50%;
|
|
65
|
+
transform-origin: center;
|
|
66
|
+
user-select: none;
|
|
67
|
+
-webkit-user-drag: none;
|
|
68
|
+
pointer-events: none;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.castle-image-viewer-image.pixelated {
|
|
72
|
+
image-rendering: pixelated;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.castle-image-viewer-message {
|
|
76
|
+
position: absolute;
|
|
77
|
+
inset: 0;
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
justify-content: center;
|
|
81
|
+
padding: 16px;
|
|
82
|
+
text-align: center;
|
|
83
|
+
opacity: 0.7;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Author `display: flex` above beats the UA `[hidden] { display: none }`, so
|
|
87
|
+
the loading label stayed painted after the image was ready. */
|
|
88
|
+
.castle-image-viewer-message[hidden] {
|
|
89
|
+
display: none;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.castle-image-viewer-message.error {
|
|
93
|
+
color: #e86a73;
|
|
94
|
+
opacity: 1;
|
|
95
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* Read-only audio / video player chrome. Native <audio>/<video controls>
|
|
2
|
+
handle transport; this only frames them. */
|
|
3
|
+
|
|
4
|
+
.castle-media-player {
|
|
5
|
+
display: flex;
|
|
6
|
+
flex: 1;
|
|
7
|
+
width: 100%;
|
|
8
|
+
height: 100%;
|
|
9
|
+
min-height: 100vh;
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
padding: 20px;
|
|
14
|
+
color: #e6e6ea;
|
|
15
|
+
background: #161616;
|
|
16
|
+
font: 13px system-ui, sans-serif;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.castle-media-player-panel {
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
gap: 10px;
|
|
23
|
+
min-width: 0;
|
|
24
|
+
width: 100%;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.castle-media-player-panel.audio {
|
|
28
|
+
max-width: 420px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.castle-media-player-panel.video {
|
|
32
|
+
max-width: 100%;
|
|
33
|
+
max-height: 100%;
|
|
34
|
+
min-height: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.castle-media-player-name {
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
text-overflow: ellipsis;
|
|
40
|
+
white-space: nowrap;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.castle-media-player-audio {
|
|
44
|
+
width: 100%;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.castle-media-player-video {
|
|
48
|
+
min-height: 0;
|
|
49
|
+
max-width: 100%;
|
|
50
|
+
max-height: 100%;
|
|
51
|
+
background: #000;
|
|
52
|
+
border-radius: 4px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.castle-media-player-error {
|
|
56
|
+
font-size: 12px;
|
|
57
|
+
color: #e86a73;
|
|
58
|
+
}
|
package/kits/base/sdk/README.md
CHANGED
|
@@ -55,7 +55,9 @@ metadata distinguishes a server `bye`, a replaced server version, an explicit
|
|
|
55
55
|
|
|
56
56
|
When Castle selects solo fallback, the returned connection has `isSolo === true`,
|
|
57
57
|
a one-player roster, and no server process. Calls to `send` are accepted but do
|
|
58
|
-
nothing.
|
|
58
|
+
nothing. `soloReason` says why: `private_deck` or `no_server_bundle` are the
|
|
59
|
+
creator's to fix (change the deck's visibility, or publish a deck that has a
|
|
60
|
+
server entry); anything else reports `unavailable`. Client→server frames are limited to 16 KB on the wire.
|
|
59
61
|
|
|
60
62
|
### Binary messages
|
|
61
63
|
|
|
@@ -9,6 +9,7 @@ export { Lifecycle } from "./lifecycle";
|
|
|
9
9
|
export type { CastleLifecycleApi } from "./lifecycle";
|
|
10
10
|
export { Multiplayer } from "./multiplayer";
|
|
11
11
|
export type { CastleMultiplayerApi, MultiplayerConnection, MultiplayerConnectionState, MultiplayerJoinOptions, MultiplayerRoster, MultiplayerStateMetadata, } from "./multiplayer";
|
|
12
|
+
export type { MultiplayerSoloReason } from "./commands";
|
|
12
13
|
export { MAX_CLIENT_FRAME_BYTES, MAX_PLAYERS_CAP } from "./multiplayerProtocol";
|
|
13
14
|
export type { ClientByeFrame, ClientHelloFrame, ClientMessageFrame, ClientPingFrame, ClientPongFrame, ClientReplacedFrame, ClientRosterFrame, ClientToPlatformFrame, ClientToPlatformMessageFrame, MultiplayerPlayer, PlayerIdentity, PlatformToClientFrame, SessionPlayer, SessionType, } from "./multiplayerProtocol";
|
|
14
15
|
export { Pass } from "./passes";
|
|
@@ -66,11 +66,18 @@ export interface MultiplayerGetSessionParams {
|
|
|
66
66
|
mode: MultiplayerMode;
|
|
67
67
|
key?: string | null;
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Why a join fell back to solo. `private_deck` and `no_server_bundle` are the
|
|
71
|
+
* creator's to fix; anything else the platform reports as `unavailable`.
|
|
72
|
+
*/
|
|
73
|
+
export type MultiplayerSoloReason = "private_deck" | "no_server_bundle" | "unavailable";
|
|
69
74
|
export interface MultiplayerGetSessionResult {
|
|
70
75
|
status: "ok" | "solo" | "unavailable";
|
|
71
76
|
url: string | null;
|
|
72
77
|
nonce: string | null;
|
|
73
78
|
sessionId: string | null;
|
|
79
|
+
/** Set when `status` is "solo"; null otherwise. */
|
|
80
|
+
soloReason: MultiplayerSoloReason | null;
|
|
74
81
|
}
|
|
75
82
|
export interface CommandParams {
|
|
76
83
|
"deckStorage.load": Record<string, never>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MultiplayerSoloReason } from "./commands";
|
|
1
2
|
import { CastleError } from "./errors";
|
|
2
3
|
import { type MultiplayerPlayer } from "./multiplayerProtocol";
|
|
3
4
|
export type MultiplayerJoinOptions = {
|
|
@@ -31,6 +32,11 @@ export interface MultiplayerConnection {
|
|
|
31
32
|
readonly playerId: string;
|
|
32
33
|
readonly sessionId: string;
|
|
33
34
|
readonly isSolo: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Why the join fell back to solo; null while multiplayer is live.
|
|
37
|
+
* `private_deck` and `no_server_bundle` are the creator's to fix.
|
|
38
|
+
*/
|
|
39
|
+
readonly soloReason: MultiplayerSoloReason | null;
|
|
34
40
|
/** Sends byte payloads as binary frames and all other payloads as JSON. */
|
|
35
41
|
send(data: unknown): void;
|
|
36
42
|
/** Receives binary payloads as Uint8Array values and JSON payloads otherwise. */
|
|
@@ -25,6 +25,7 @@ class MultiplayerConnectionImpl {
|
|
|
25
25
|
currentPlayerId = "";
|
|
26
26
|
currentSessionId = "";
|
|
27
27
|
solo = false;
|
|
28
|
+
currentSoloReason = null;
|
|
28
29
|
baseUrl = "";
|
|
29
30
|
reconnectToken = "";
|
|
30
31
|
socket = null;
|
|
@@ -52,6 +53,9 @@ class MultiplayerConnectionImpl {
|
|
|
52
53
|
get isSolo() {
|
|
53
54
|
return this.solo;
|
|
54
55
|
}
|
|
56
|
+
get soloReason() {
|
|
57
|
+
return this.currentSoloReason;
|
|
58
|
+
}
|
|
55
59
|
async start() {
|
|
56
60
|
try {
|
|
57
61
|
await this.acquire();
|
|
@@ -114,12 +118,14 @@ class MultiplayerConnectionImpl {
|
|
|
114
118
|
throw multiplayerError("MULTIPLAYER_UNAVAILABLE", "Multiplayer is unavailable for this deck.");
|
|
115
119
|
}
|
|
116
120
|
this.solo = false;
|
|
121
|
+
this.currentSoloReason = null;
|
|
117
122
|
this.baseUrl = result.url;
|
|
118
123
|
this.transition("connecting");
|
|
119
124
|
await this.openSocket("nonce", result.nonce);
|
|
120
125
|
}
|
|
121
126
|
connectSolo(result) {
|
|
122
127
|
this.solo = true;
|
|
128
|
+
this.currentSoloReason = result.soloReason ?? "unavailable";
|
|
123
129
|
this.currentPlayerId = "solo";
|
|
124
130
|
this.currentSessionId = result.sessionId ?? "solo";
|
|
125
131
|
const you = {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[]
|