@umicat/three-sdk 0.12.0 → 0.14.0
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/Input3D.d.ts +20 -2
- package/dist/Input3D.js +89 -21
- package/package.json +1 -1
package/dist/Input3D.d.ts
CHANGED
|
@@ -27,9 +27,10 @@ export interface Input3DOptions {
|
|
|
27
27
|
/** Where key events are read from. */
|
|
28
28
|
target?: Window;
|
|
29
29
|
/**
|
|
30
|
-
* On-screen thumbstick
|
|
30
|
+
* On-screen thumbstick and buttons. `'auto'` (the default) adds them when
|
|
31
31
|
* the device reports coarse pointers and no fine one — i.e. a phone, not a
|
|
32
|
-
* laptop with a touchscreen.
|
|
32
|
+
* laptop with a touchscreen. What the cluster CONTAINS is a separate
|
|
33
|
+
* question — see `jump` and `actions`.
|
|
33
34
|
*/
|
|
34
35
|
touch?: boolean | 'auto';
|
|
35
36
|
/**
|
|
@@ -52,6 +53,21 @@ export interface Input3DOptions {
|
|
|
52
53
|
/** A shape for the JUMP button, in place of its `▲`. Same reasoning as
|
|
53
54
|
* `Input3DAction.icon`, for the one button the platform owns outright. */
|
|
54
55
|
jumpIcon?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Whether this game HAS a jump. Default `true`.
|
|
58
|
+
*
|
|
59
|
+
* Jumping is not universal — a game played by walking around a board does
|
|
60
|
+
* not want a jump button, and it was getting one anyway because the button
|
|
61
|
+
* came bundled with the thumbstick under `touch`. Turning `touch` off to be
|
|
62
|
+
* rid of it takes the stick with it, which is not a trade anyone would make.
|
|
63
|
+
*
|
|
64
|
+
* `false` removes the on-screen button AND makes `jump` permanently false,
|
|
65
|
+
* which also hands **`Space` back to the game**. Those two belong together:
|
|
66
|
+
* Space is read here as jump, so a game with no jump that binds Space to
|
|
67
|
+
* something of its own would otherwise have the hero hop every time the
|
|
68
|
+
* player used it.
|
|
69
|
+
*/
|
|
70
|
+
jump?: boolean;
|
|
55
71
|
/**
|
|
56
72
|
* How the thumbstick behaves on touch.
|
|
57
73
|
*
|
|
@@ -111,6 +127,8 @@ export declare class Input3D {
|
|
|
111
127
|
private touchJump;
|
|
112
128
|
private actions;
|
|
113
129
|
private jumpIcon?;
|
|
130
|
+
/** Whether this game has a jump at all — see `Input3DOptions.jump`. */
|
|
131
|
+
private readonly hasJump;
|
|
114
132
|
/** The glyph inside each on-screen button, by action id (`'jump'` for the
|
|
115
133
|
* platform's own). Kept so an icon can be CHANGED while the game runs —
|
|
116
134
|
* which is the point of it for anything the player switches: an attack
|
package/dist/Input3D.js
CHANGED
|
@@ -71,7 +71,7 @@ export class Input3D {
|
|
|
71
71
|
this.onDown = (e) => {
|
|
72
72
|
this.keysDown.add(e.code);
|
|
73
73
|
if (!e.repeat) {
|
|
74
|
-
if (e.code === 'Space')
|
|
74
|
+
if (e.code === 'Space' && this.hasJump)
|
|
75
75
|
this.beginPress('jump');
|
|
76
76
|
for (const a of this.actions)
|
|
77
77
|
if (a.keys?.includes(e.code))
|
|
@@ -120,6 +120,7 @@ export class Input3D {
|
|
|
120
120
|
this.target.addEventListener('blur', this.onBlur);
|
|
121
121
|
this.actions = opts.actions ?? [];
|
|
122
122
|
this.jumpIcon = opts.jumpIcon;
|
|
123
|
+
this.hasJump = opts.jump ?? true;
|
|
123
124
|
this.stickMode = opts.stick ?? 'floating';
|
|
124
125
|
this.wantLook = opts.look ?? true;
|
|
125
126
|
this.topInset = opts.controlsTopInset ?? 'max(64px, 12%)';
|
|
@@ -206,6 +207,8 @@ export class Input3D {
|
|
|
206
207
|
/** Whether the jump control is held right now. Pass it to the controller —
|
|
207
208
|
* coyote time and buffering live there, not here. */
|
|
208
209
|
get jump() {
|
|
210
|
+
if (!this.hasJump)
|
|
211
|
+
return false;
|
|
209
212
|
return this.enabled && this.held_('jump', this.touchJump || this.isDown('Space'));
|
|
210
213
|
}
|
|
211
214
|
/**
|
|
@@ -278,7 +281,7 @@ export class Input3D {
|
|
|
278
281
|
if (this.keysDown.has(code))
|
|
279
282
|
return; // held, not re-pressed
|
|
280
283
|
this.keysDown.add(code);
|
|
281
|
-
if (code === 'Space')
|
|
284
|
+
if (code === 'Space' && this.hasJump)
|
|
282
285
|
this.beginPress('jump');
|
|
283
286
|
for (const a of this.actions)
|
|
284
287
|
if (a.keys?.includes(code))
|
|
@@ -406,10 +409,9 @@ export class Input3D {
|
|
|
406
409
|
transform: 'translate(-50%, -50%)', pointerEvents: 'none',
|
|
407
410
|
});
|
|
408
411
|
pad.appendChild(knob);
|
|
409
|
-
// ONE cluster holds every button,
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
// was; actions stack to its left and wrap upward.
|
|
412
|
+
// ONE cluster holds every button, because the bug this replaces was two of
|
|
413
|
+
// them independently choosing "bottom right" and landing on top of each
|
|
414
|
+
// other. What it does with them is `layOnArc` below.
|
|
413
415
|
const cluster = document.createElement('div');
|
|
414
416
|
Object.assign(cluster.style, {
|
|
415
417
|
position: 'absolute', right: '6vmin', bottom: '7vmin',
|
|
@@ -417,6 +419,53 @@ export class Input3D {
|
|
|
417
419
|
flexWrap: 'wrap-reverse', justifyContent: 'flex-start',
|
|
418
420
|
gap: '3vmin', maxWidth: '52vmin', pointerEvents: 'none',
|
|
419
421
|
});
|
|
422
|
+
/**
|
|
423
|
+
* Put the buttons on the arc a THUMB actually sweeps.
|
|
424
|
+
*
|
|
425
|
+
* They were a wrapping flex row, which is how they ended up in a straight
|
|
426
|
+
* line across the bottom of the screen the moment a game had exactly two —
|
|
427
|
+
* the third button had been the only thing forcing a wrap, so removing it
|
|
428
|
+
* silently changed the layout of the two that were left. A row is also the
|
|
429
|
+
* wrong shape regardless: a thumb is hinged at the bottom corner and moves
|
|
430
|
+
* on an arc around it, so buttons at the same HEIGHT are at different
|
|
431
|
+
* reaches, and the far one is the one that gets missed.
|
|
432
|
+
*
|
|
433
|
+
* So: a quarter circle centred on the corner. Index 0 sits low and to the
|
|
434
|
+
* left, and each one after it steps up and in toward the edge, every one of
|
|
435
|
+
* them the same distance from the thumb's hinge.
|
|
436
|
+
*
|
|
437
|
+
* The radius is DERIVED, not chosen: neighbours must be at least a button
|
|
438
|
+
* apart along the chord, so a wider sweep — which is what more buttons
|
|
439
|
+
* need — pushes the arc out. Past three the radius it asks for has the
|
|
440
|
+
* buttons halfway up the screen, so the old wrapping row is kept for that
|
|
441
|
+
* case rather than pretending an arc still fits.
|
|
442
|
+
*/
|
|
443
|
+
const layOnArc = (els) => {
|
|
444
|
+
const S = 20; // vmin — the button size, below
|
|
445
|
+
const [A0, A1] = [16, 74]; // degrees, the comfortable sweep
|
|
446
|
+
if (els.length > 3)
|
|
447
|
+
return; // the flex row stays as it is
|
|
448
|
+
const step = els.length > 1 ? (A1 - A0) / (els.length - 1) : 0;
|
|
449
|
+
// Chord between neighbours = 2R·sin(step/2), and it may not be smaller
|
|
450
|
+
// than a button plus a little air.
|
|
451
|
+
const need = els.length > 1 ? (S * 1.12) / (2 * Math.sin((step * Math.PI) / 360)) : S * 1.3;
|
|
452
|
+
const R = Math.max(S * 1.3, need);
|
|
453
|
+
cluster.style.display = 'block';
|
|
454
|
+
cluster.style.width = '0';
|
|
455
|
+
cluster.style.height = '0';
|
|
456
|
+
els.forEach((el, i) => {
|
|
457
|
+
const t = ((A0 + step * i) * Math.PI) / 180;
|
|
458
|
+
// From the corner, in vmin, then back off by half a button so the
|
|
459
|
+
// ARC passes through each button's middle rather than its corner.
|
|
460
|
+
const dx = (R * Math.cos(t)).toFixed(2);
|
|
461
|
+
const dy = (R * Math.sin(t)).toFixed(2);
|
|
462
|
+
Object.assign(el.style, {
|
|
463
|
+
position: 'absolute',
|
|
464
|
+
right: `calc(${dx}vmin - min(${S / 2}vmin, 60px))`,
|
|
465
|
+
bottom: `calc(${dy}vmin - min(${S / 2}vmin, 60px))`,
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
};
|
|
420
469
|
/** Returns the button; its glyph, if it has one, is left in `glyph`. */
|
|
421
470
|
let glyph = null;
|
|
422
471
|
const makeButton = (label, icon) => {
|
|
@@ -465,17 +514,33 @@ export class Input3D {
|
|
|
465
514
|
return el;
|
|
466
515
|
};
|
|
467
516
|
this.glyphs.clear();
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
517
|
+
// ONE LIST, and the game decides what is in it.
|
|
518
|
+
//
|
|
519
|
+
// Jump used to be built here on its own, before the loop, which is what
|
|
520
|
+
// made it the one button a game could not decline — it arrived with the
|
|
521
|
+
// thumbstick whether the game had a jump or not. It is an entry in the
|
|
522
|
+
// list now, present unless `jump: false`, and everything after it is
|
|
523
|
+
// whatever the game declared. What this class owns is PLACING them.
|
|
524
|
+
const buttons = [
|
|
525
|
+
...(this.hasJump ? [{ id: 'jump', label: '▲', icon: this.jumpIcon }] : []),
|
|
526
|
+
...this.actions.map((a) => ({ id: a.id, label: a.label ?? a.id.slice(0, 1).toUpperCase(), icon: a.icon })),
|
|
527
|
+
];
|
|
528
|
+
let jumpBtn = null;
|
|
529
|
+
const made = [];
|
|
530
|
+
for (const b of buttons) {
|
|
531
|
+
const el = makeButton(b.label, b.icon);
|
|
474
532
|
if (glyph)
|
|
475
|
-
this.glyphs.set(
|
|
533
|
+
this.glyphs.set(b.id, glyph);
|
|
476
534
|
cluster.append(el);
|
|
477
|
-
|
|
535
|
+
made.push(el);
|
|
536
|
+
// Jump is the platform's own: its press is read through `get jump()`
|
|
537
|
+
// rather than through the action latch, so it is wired below instead.
|
|
538
|
+
if (b.id === 'jump')
|
|
539
|
+
jumpBtn = el;
|
|
540
|
+
else
|
|
541
|
+
this.wireButton(el, b.id);
|
|
478
542
|
}
|
|
543
|
+
layOnArc(made);
|
|
479
544
|
root.append(zone, lookZone, pad, cluster);
|
|
480
545
|
container.appendChild(root);
|
|
481
546
|
this.root = root;
|
|
@@ -613,13 +678,16 @@ export class Input3D {
|
|
|
613
678
|
on(grip, ev, (e) => { if (e.pointerId === stickId)
|
|
614
679
|
reset(); });
|
|
615
680
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
681
|
+
if (jumpBtn) {
|
|
682
|
+
const jb = jumpBtn;
|
|
683
|
+
on(jb, 'pointerdown', (e) => {
|
|
684
|
+
jb.setPointerCapture(e.pointerId);
|
|
685
|
+
this.touchJump = true;
|
|
686
|
+
this.beginPress('jump');
|
|
687
|
+
});
|
|
688
|
+
for (const ev of ['pointerup', 'pointercancel', 'lostpointercapture']) {
|
|
689
|
+
on(jb, ev, () => { this.touchJump = false; });
|
|
690
|
+
}
|
|
623
691
|
}
|
|
624
692
|
}
|
|
625
693
|
wireButton(el, id) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umicat/three-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Three.js runtime for Umicat games: the scene3d design format, its loader with physics, a kinematic character controller, and the Umicat platform via @umicat/platform-sdk.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|