castle-web-cli 0.4.181 → 0.4.183
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/shell/assets/{index-C5DS_7UM.js → index-DnNy-Z5u.js} +90 -90
- package/dist/shell/assets/index-cdsH2lna.css +1 -0
- package/dist/shell/index.html +2 -2
- package/kits/physics-2d/CLAUDE.md +1 -0
- package/kits/physics-2d/castle.json +1 -1
- package/kits/physics-2d/engine/fields/fields.jsx +475 -0
- package/kits/physics-2d/engine/fields/fields.module.css +330 -0
- package/kits/physics-2d/engine/paletteField.jsx +2 -8
- package/kits/physics-2d/engine/scene.js +12 -3
- package/kits/physics-2d/engine/spriteField.jsx +2 -11
- package/kits/physics-2d/engine/ui.jsx +24 -448
- package/kits/physics-2d/engine/ui.module.css +2 -375
- package/kits/physics-2d/package.json +1 -1
- package/package.json +3 -2
- package/dist/shell/assets/index-CoU3ETYM.css +0 -1
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/* Styles for the field widgets beside this file. The canonical copy is
|
|
2
|
+
shared/fields/fields.module.css; scripts/sync-shared.mjs copies it into each
|
|
3
|
+
consumer of the widgets, and `check` fails when a copy drifts. Edit it there. */
|
|
4
|
+
.fieldRow {
|
|
5
|
+
display: grid;
|
|
6
|
+
grid-template-columns: minmax(82px, 0.5fr) minmax(0, 1fr);
|
|
7
|
+
align-items: center;
|
|
8
|
+
gap: 12px;
|
|
9
|
+
padding-bottom: 12px;
|
|
10
|
+
margin-bottom: 0;
|
|
11
|
+
min-height: 28px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.fieldRow:last-child {
|
|
15
|
+
margin-bottom: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* `MAX_ENEMY_SPEED` is a single unbreakable token to the line breaker -- no
|
|
19
|
+
space, and an underscore is not a break opportunity -- so a long param name
|
|
20
|
+
has to be allowed to break mid-word or it overflows the label column. */
|
|
21
|
+
.fieldLabel {
|
|
22
|
+
color: var(--castle-inspector-text);
|
|
23
|
+
font-size: 14px;
|
|
24
|
+
min-width: 0;
|
|
25
|
+
overflow-wrap: anywhere;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Instance override: purple label + input border, plus a "Default: X [Reset]"
|
|
29
|
+
sub-line rendered below the control (see FieldRow). */
|
|
30
|
+
.fieldLabelOverridden {
|
|
31
|
+
color: var(--castle-override);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* A custom property rather than `.fieldRowOverridden .input`, so a control styled
|
|
35
|
+
in another CSS module (ui.module.css's .select and .textarea) takes the border
|
|
36
|
+
too -- a class selector can't reach a class from a different module. */
|
|
37
|
+
.fieldRowOverridden {
|
|
38
|
+
--field-border: var(--castle-override-border);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.fieldOverride {
|
|
42
|
+
padding-bottom: 12px;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.fieldOverride .fieldRow {
|
|
46
|
+
padding-bottom: 4px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* A muted line of explanation under a field, sitting in the control column the
|
|
50
|
+
same way the override sub-line above does. For a prop whose name can't carry
|
|
51
|
+
its own meaning -- see `hint` in a behavior's propertyMeta. */
|
|
52
|
+
.fieldNote {
|
|
53
|
+
display: grid;
|
|
54
|
+
grid-template-columns: minmax(82px, 0.5fr) minmax(0, 1fr);
|
|
55
|
+
gap: 12px;
|
|
56
|
+
margin-top: -6px;
|
|
57
|
+
padding-bottom: 12px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.fieldNoteInner {
|
|
61
|
+
grid-column: 2;
|
|
62
|
+
min-width: 0;
|
|
63
|
+
font-size: 12px;
|
|
64
|
+
line-height: 1.35;
|
|
65
|
+
color: var(--castle-inspector-muted);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.fieldDefault {
|
|
69
|
+
display: grid;
|
|
70
|
+
grid-template-columns: minmax(82px, 0.5fr) minmax(0, 1fr);
|
|
71
|
+
gap: 12px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.fieldDefaultInner {
|
|
75
|
+
grid-column: 2;
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
gap: 8px;
|
|
79
|
+
min-width: 0;
|
|
80
|
+
font-size: 12px;
|
|
81
|
+
color: var(--castle-inspector-muted);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.fieldDefaultInner > span {
|
|
85
|
+
min-width: 0;
|
|
86
|
+
overflow: hidden;
|
|
87
|
+
text-overflow: ellipsis;
|
|
88
|
+
white-space: nowrap;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.fieldResetBtn {
|
|
92
|
+
margin-left: auto;
|
|
93
|
+
border: 0;
|
|
94
|
+
padding: 0;
|
|
95
|
+
background: transparent;
|
|
96
|
+
color: var(--castle-override);
|
|
97
|
+
font-size: 12px;
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.fieldResetBtn:hover {
|
|
102
|
+
color: var(--castle-inspector-text);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.input {
|
|
106
|
+
width: 100%;
|
|
107
|
+
border: 1px solid var(--field-border, var(--castle-inspector-border));
|
|
108
|
+
border-radius: var(--castle-radius);
|
|
109
|
+
background: var(--castle-inspector-input-bg);
|
|
110
|
+
color: var(--castle-inspector-text);
|
|
111
|
+
padding: 5px 8px;
|
|
112
|
+
outline: 0;
|
|
113
|
+
font: inherit;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.numberField {
|
|
117
|
+
position: relative;
|
|
118
|
+
width: 100%;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.numberInput {
|
|
122
|
+
appearance: textfield;
|
|
123
|
+
-moz-appearance: textfield;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.numberInput::-webkit-inner-spin-button,
|
|
127
|
+
.numberInput::-webkit-outer-spin-button {
|
|
128
|
+
-webkit-appearance: none;
|
|
129
|
+
appearance: none;
|
|
130
|
+
margin: 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Number fields are drag scrubbers with a tap-to-open custom numpad (no native
|
|
134
|
+
keyboard). A bounded field (min AND max) is a slider with fill + thumb; an
|
|
135
|
+
unbounded one shows a grip and scrubs into negatives. touch-action: pan-y (set
|
|
136
|
+
inline) lets a vertical drag scroll the field list while horizontal scrubs. */
|
|
137
|
+
.numberScrub {
|
|
138
|
+
position: relative;
|
|
139
|
+
display: flex;
|
|
140
|
+
align-items: center;
|
|
141
|
+
gap: 6px;
|
|
142
|
+
overflow: hidden;
|
|
143
|
+
padding: 5px 8px;
|
|
144
|
+
cursor: ew-resize;
|
|
145
|
+
user-select: none;
|
|
146
|
+
-webkit-user-select: none;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.numberSlider {
|
|
150
|
+
cursor: pointer;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.numberValue {
|
|
154
|
+
position: relative;
|
|
155
|
+
z-index: 1;
|
|
156
|
+
flex: 1 1 auto;
|
|
157
|
+
min-width: 0;
|
|
158
|
+
text-align: right;
|
|
159
|
+
font-variant-numeric: tabular-nums;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.scrubGrip {
|
|
163
|
+
position: relative;
|
|
164
|
+
z-index: 1;
|
|
165
|
+
flex: 0 0 auto;
|
|
166
|
+
color: var(--castle-inspector-muted);
|
|
167
|
+
font-size: 12px;
|
|
168
|
+
line-height: 1;
|
|
169
|
+
letter-spacing: -2px;
|
|
170
|
+
opacity: 0.4;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* Value bubble that floats above the finger while scrubbing (the finger covers the
|
|
174
|
+
field, so the live number needs to show somewhere visible). */
|
|
175
|
+
.scrubBubble {
|
|
176
|
+
position: fixed;
|
|
177
|
+
z-index: 1001;
|
|
178
|
+
transform: translate(-50%, -100%);
|
|
179
|
+
padding: 3px 10px;
|
|
180
|
+
border-radius: 8px;
|
|
181
|
+
background: rgba(20, 24, 30, 0.96);
|
|
182
|
+
border: 1px solid var(--castle-inspector-border);
|
|
183
|
+
color: var(--castle-inspector-text);
|
|
184
|
+
font-size: 15px;
|
|
185
|
+
font-variant-numeric: tabular-nums;
|
|
186
|
+
pointer-events: none;
|
|
187
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.sliderFill {
|
|
191
|
+
position: absolute;
|
|
192
|
+
left: 0;
|
|
193
|
+
top: 0;
|
|
194
|
+
bottom: 0;
|
|
195
|
+
background: rgba(90, 140, 210, 0.3);
|
|
196
|
+
pointer-events: none;
|
|
197
|
+
z-index: 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.sliderThumb {
|
|
201
|
+
position: absolute;
|
|
202
|
+
top: 50%;
|
|
203
|
+
width: 3px;
|
|
204
|
+
height: 60%;
|
|
205
|
+
border-radius: 2px;
|
|
206
|
+
background: rgba(150, 190, 240, 0.95);
|
|
207
|
+
transform: translate(-50%, -50%);
|
|
208
|
+
pointer-events: none;
|
|
209
|
+
z-index: 1;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Custom numeric pad, shown as a bottom sheet. */
|
|
213
|
+
.numpadBackdrop {
|
|
214
|
+
position: fixed;
|
|
215
|
+
inset: 0;
|
|
216
|
+
z-index: 1000;
|
|
217
|
+
display: flex;
|
|
218
|
+
flex-direction: column;
|
|
219
|
+
justify-content: flex-end;
|
|
220
|
+
background: rgba(0, 0, 0, 0.35);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.numpadSheet {
|
|
224
|
+
background: var(--castle-inspector-bg, #16181d);
|
|
225
|
+
border-top: 1px solid var(--castle-inspector-border);
|
|
226
|
+
border-radius: 14px 14px 0 0;
|
|
227
|
+
padding: 10px 10px calc(10px + env(safe-area-inset-bottom, 0px));
|
|
228
|
+
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.4);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.numpadValueRow {
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: baseline;
|
|
234
|
+
justify-content: space-between;
|
|
235
|
+
gap: 12px;
|
|
236
|
+
padding: 6px 8px 12px;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* Label and number are one value line, not a caption plus a value — same size
|
|
240
|
+
and color. A long label (e.g. "Friction Static") truncates rather than
|
|
241
|
+
pushing the number off, which is why the number never shrinks. */
|
|
242
|
+
.numpadValueLabel {
|
|
243
|
+
color: var(--castle-inspector-text);
|
|
244
|
+
font-size: 22px;
|
|
245
|
+
min-width: 0;
|
|
246
|
+
overflow: hidden;
|
|
247
|
+
text-overflow: ellipsis;
|
|
248
|
+
white-space: nowrap;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.numpadValueNum {
|
|
252
|
+
flex: 0 0 auto;
|
|
253
|
+
color: var(--castle-inspector-text);
|
|
254
|
+
font-size: 22px;
|
|
255
|
+
font-variant-numeric: tabular-nums;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.numpadGrid {
|
|
259
|
+
display: grid;
|
|
260
|
+
grid-template-columns: repeat(4, 1fr);
|
|
261
|
+
gap: 8px;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.numpadKey {
|
|
265
|
+
height: 52px;
|
|
266
|
+
border: 1px solid var(--castle-inspector-border);
|
|
267
|
+
border-radius: 10px;
|
|
268
|
+
background: var(--castle-inspector-input-bg);
|
|
269
|
+
color: var(--castle-inspector-text);
|
|
270
|
+
font-size: 20px;
|
|
271
|
+
cursor: pointer;
|
|
272
|
+
touch-action: manipulation;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.numpadKey:active {
|
|
276
|
+
background: var(--castle-inspector-divider);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.numpadKeyZero {
|
|
280
|
+
grid-column: span 3;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.numpadKeyDone {
|
|
284
|
+
background: rgba(90, 140, 210, 0.9);
|
|
285
|
+
border-color: transparent;
|
|
286
|
+
color: #fff;
|
|
287
|
+
font-size: 16px;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.colorInput {
|
|
291
|
+
width: 100%;
|
|
292
|
+
height: 28px;
|
|
293
|
+
border: 1px solid var(--field-border, var(--castle-inspector-border));
|
|
294
|
+
border-radius: var(--castle-radius);
|
|
295
|
+
background: var(--castle-inspector-input-bg);
|
|
296
|
+
padding: 2px;
|
|
297
|
+
outline: 0;
|
|
298
|
+
cursor: pointer;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.checkbox {
|
|
302
|
+
width: 20px;
|
|
303
|
+
height: 20px;
|
|
304
|
+
flex: 0 0 auto;
|
|
305
|
+
justify-self: end;
|
|
306
|
+
border: 1px solid var(--field-border, var(--castle-inspector-border));
|
|
307
|
+
border-radius: var(--castle-radius);
|
|
308
|
+
background: var(--castle-inspector-input-bg);
|
|
309
|
+
padding: 0;
|
|
310
|
+
cursor: pointer;
|
|
311
|
+
display: flex;
|
|
312
|
+
align-items: center;
|
|
313
|
+
justify-content: center;
|
|
314
|
+
box-shadow: none;
|
|
315
|
+
color: var(--castle-text);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.checkboxOn {
|
|
319
|
+
background: var(--castle-black);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.checkboxMark {
|
|
323
|
+
display: none;
|
|
324
|
+
width: 12px;
|
|
325
|
+
height: 12px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.checkboxOn .checkboxMark {
|
|
329
|
+
display: block;
|
|
330
|
+
}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
pickerPageIndexFor,
|
|
6
6
|
pickerPagesFor,
|
|
7
7
|
} from './palettes';
|
|
8
|
-
import { cx, FieldRow, IconButton, styles } from './ui';
|
|
8
|
+
import { cx, FieldRow, followAnchor, IconButton, styles } from './ui';
|
|
9
9
|
import { attachDismissListeners } from './popoverDismiss';
|
|
10
10
|
|
|
11
11
|
export function sameHex(a, b) {
|
|
@@ -118,13 +118,7 @@ export function PalettePopover({ open, anchorRef, onClose, children }) {
|
|
|
118
118
|
setPosition({ top, left });
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
positionPopover
|
|
122
|
-
window.addEventListener('resize', positionPopover);
|
|
123
|
-
window.addEventListener('scroll', positionPopover, true);
|
|
124
|
-
return () => {
|
|
125
|
-
window.removeEventListener('resize', positionPopover);
|
|
126
|
-
window.removeEventListener('scroll', positionPopover, true);
|
|
127
|
-
};
|
|
121
|
+
return followAnchor(positionPopover);
|
|
128
122
|
}, [open, anchorRef]);
|
|
129
123
|
|
|
130
124
|
useEffect(() => {
|
|
@@ -33,6 +33,15 @@ function resolveSceneFileKey(name) {
|
|
|
33
33
|
return candidates.find((candidate) => initialFiles[candidate] !== undefined) ?? key;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// A behavior instance for one actor. `this.props` is what the constructor
|
|
37
|
+
// assigns; a behavior written without one would read `this.props` as undefined
|
|
38
|
+
// and throw on its first frame, so it is filled in here when missing.
|
|
39
|
+
function makeBehavior(Behavior, props) {
|
|
40
|
+
const instance = new Behavior(props);
|
|
41
|
+
if (instance.props === undefined) instance.props = props;
|
|
42
|
+
return instance;
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
// `$enabled: false` turns a behavior off while keeping its props. Absent means
|
|
37
46
|
// on, so it is only ever stored when false, and every existing scene reads the
|
|
38
47
|
// same. `$` marks a prop the ENGINE owns -- behaviors must not define one.
|
|
@@ -371,7 +380,7 @@ export class SceneRuntime {
|
|
|
371
380
|
if (!Behavior) return;
|
|
372
381
|
ctx.save();
|
|
373
382
|
ctx.globalAlpha *= 0.5;
|
|
374
|
-
|
|
383
|
+
makeBehavior(Behavior, props).draw?.(actor, this, ctx, options);
|
|
375
384
|
ctx.restore();
|
|
376
385
|
drawDisabledSpriteLabel(ctx, actor);
|
|
377
386
|
}
|
|
@@ -389,7 +398,7 @@ export class SceneRuntime {
|
|
|
389
398
|
const hook = on ? 'onEnable' : 'onDisable';
|
|
390
399
|
// Constructed from the props it still has -- the whole point of
|
|
391
400
|
// disabling is that they survive.
|
|
392
|
-
|
|
401
|
+
makeBehavior(Behavior, props ?? {})[hook]?.(actor, this);
|
|
393
402
|
}
|
|
394
403
|
}
|
|
395
404
|
|
|
@@ -422,7 +431,7 @@ export class SceneRuntime {
|
|
|
422
431
|
if (!isBehaviorEnabled(props)) continue;
|
|
423
432
|
const Behavior = this.behaviors.get(behaviorName);
|
|
424
433
|
if (!Behavior) continue;
|
|
425
|
-
callback(
|
|
434
|
+
callback(makeBehavior(Behavior, props));
|
|
426
435
|
}
|
|
427
436
|
}
|
|
428
437
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import { ArtThumbnail } from './artThumbnail';
|
|
4
|
-
import { cx, FieldRow, Icon, styles } from './ui';
|
|
4
|
+
import { cx, FieldRow, followAnchor, Icon, styles } from './ui';
|
|
5
5
|
import { attachDismissListeners } from './popoverDismiss';
|
|
6
6
|
|
|
7
7
|
// Visual sprite / image picker for inspector fields that point at art files.
|
|
@@ -56,16 +56,7 @@ function SpritePickerPopover({ paths, current, sprites, anchorRef, onPick, onClo
|
|
|
56
56
|
|
|
57
57
|
useLayoutEffect(() => {
|
|
58
58
|
if (!anchorRef.current || !listRef.current) return undefined;
|
|
59
|
-
|
|
60
|
-
setPos(placePopover(anchorRef.current, listRef.current));
|
|
61
|
-
}
|
|
62
|
-
place();
|
|
63
|
-
window.addEventListener('resize', place);
|
|
64
|
-
window.addEventListener('scroll', place, true);
|
|
65
|
-
return () => {
|
|
66
|
-
window.removeEventListener('resize', place);
|
|
67
|
-
window.removeEventListener('scroll', place, true);
|
|
68
|
-
};
|
|
59
|
+
return followAnchor(() => setPos(placePopover(anchorRef.current, listRef.current)));
|
|
69
60
|
}, [anchorRef, paths.length]);
|
|
70
61
|
|
|
71
62
|
useEffect(() => {
|