@umicat/three-sdk 0.4.0 → 0.6.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 +36 -1
- package/dist/Input3D.js +110 -19
- package/dist/SceneLoader3D.d.ts +15 -0
- package/dist/SceneLoader3D.js +22 -0
- package/dist/Sockets.d.ts +38 -0
- package/dist/Sockets.js +48 -0
- package/dist/ThreeUmicat.js +28 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/scene3d.d.ts +22 -0
- package/package.json +1 -1
package/dist/Input3D.d.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/** An extra on-screen button, and the keys that do the same thing. */
|
|
2
|
+
export interface Input3DAction {
|
|
3
|
+
/** What the game calls it: `input.held('attack')`, `input.consume('attack')`. */
|
|
4
|
+
id: string;
|
|
5
|
+
/** What the button shows. Keep it one glyph. */
|
|
6
|
+
label?: string;
|
|
7
|
+
/** Keyboard equivalents, e.g. `['KeyJ']`. */
|
|
8
|
+
keys?: string[];
|
|
9
|
+
}
|
|
1
10
|
export interface Input3DOptions {
|
|
2
11
|
/** Where key events are read from. */
|
|
3
12
|
target?: Window;
|
|
@@ -14,6 +23,16 @@ export interface Input3DOptions {
|
|
|
14
23
|
* this layer belongs to the platform.
|
|
15
24
|
*/
|
|
16
25
|
container?: HTMLElement | null;
|
|
26
|
+
/**
|
|
27
|
+
* Extra action buttons — attack, interact, whatever the game has.
|
|
28
|
+
*
|
|
29
|
+
* These exist because a game that mounts its own button has no idea where
|
|
30
|
+
* the jump button is, and the first one to try landed exactly on top of it:
|
|
31
|
+
* same corner, and this layer sits above, so every tap in that area jumped
|
|
32
|
+
* and the attack button could not be pressed at all. Nothing errored. The
|
|
33
|
+
* platform places the controls, so the platform has to place ALL of them.
|
|
34
|
+
*/
|
|
35
|
+
actions?: Input3DAction[];
|
|
17
36
|
}
|
|
18
37
|
/**
|
|
19
38
|
* Movement and jump, from a keyboard or a thumb, behind one interface.
|
|
@@ -31,13 +50,17 @@ export interface Input3DOptions {
|
|
|
31
50
|
* has to branch on input source.
|
|
32
51
|
*/
|
|
33
52
|
export declare class Input3D {
|
|
34
|
-
private readonly
|
|
53
|
+
private readonly keysDown;
|
|
35
54
|
private readonly target;
|
|
36
55
|
private readonly onDown;
|
|
37
56
|
private readonly onUp;
|
|
38
57
|
private readonly onBlur;
|
|
39
58
|
private readonly stick;
|
|
40
59
|
private touchJump;
|
|
60
|
+
private actions;
|
|
61
|
+
private readonly touchHeld;
|
|
62
|
+
private readonly pressedAt;
|
|
63
|
+
private readonly unconsumed;
|
|
41
64
|
private root;
|
|
42
65
|
private readonly cleanups;
|
|
43
66
|
constructor(options?: Input3DOptions | Window);
|
|
@@ -51,9 +74,21 @@ export declare class Input3D {
|
|
|
51
74
|
/** Whether the jump control is held right now. Pass it to the controller —
|
|
52
75
|
* coyote time and buffering live there, not here. */
|
|
53
76
|
get jump(): boolean;
|
|
77
|
+
/** Whether an action is held right now — button or bound key, same answer. */
|
|
78
|
+
held(id: string): boolean;
|
|
79
|
+
/** True exactly once per press: one tap is one swing.
|
|
80
|
+
*
|
|
81
|
+
* Edge detection done in the game reads the button's STATE and compares it
|
|
82
|
+
* to last frame's, which means a press that begins and ends between two
|
|
83
|
+
* frames never happened. This latches at the event, so it cannot be missed
|
|
84
|
+
* — and it clears on read, so holding the button does not chain. */
|
|
85
|
+
consume(id: string): boolean;
|
|
86
|
+
private held_;
|
|
87
|
+
private beginPress;
|
|
54
88
|
/** Test seam: drive the controller without synthesising DOM events. */
|
|
55
89
|
press(code: string): void;
|
|
56
90
|
release(code: string): void;
|
|
57
91
|
dispose(): void;
|
|
58
92
|
private mountTouch;
|
|
93
|
+
private wireButton;
|
|
59
94
|
}
|
package/dist/Input3D.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/** How long a press stays readable, however briefly it was actually made.
|
|
2
|
+
*
|
|
3
|
+
* A tap whose down and up both land between two frames is invisible to a
|
|
4
|
+
* loop that polls state — the game samples `false`, `false`, and the player
|
|
5
|
+
* swears the button did nothing. Holding it for a few frames costs nothing
|
|
6
|
+
* and makes a quick tap always count. */
|
|
7
|
+
const MIN_PRESS_MS = 80;
|
|
8
|
+
const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
|
|
1
9
|
const isTouchDevice = () => typeof matchMedia === 'function' &&
|
|
2
10
|
matchMedia('(pointer: coarse)').matches &&
|
|
3
11
|
!matchMedia('(pointer: fine)').matches;
|
|
@@ -18,18 +26,35 @@ const isTouchDevice = () => typeof matchMedia === 'function' &&
|
|
|
18
26
|
*/
|
|
19
27
|
export class Input3D {
|
|
20
28
|
constructor(options = {}) {
|
|
21
|
-
this.
|
|
29
|
+
this.keysDown = new Set();
|
|
22
30
|
this.onDown = (e) => {
|
|
23
|
-
this.
|
|
31
|
+
this.keysDown.add(e.code);
|
|
32
|
+
if (!e.repeat) {
|
|
33
|
+
if (e.code === 'Space')
|
|
34
|
+
this.beginPress('jump');
|
|
35
|
+
for (const a of this.actions)
|
|
36
|
+
if (a.keys?.includes(e.code))
|
|
37
|
+
this.beginPress(a.id);
|
|
38
|
+
}
|
|
24
39
|
// Space scrolls the page, which in an embedded game scrolls the HOST page
|
|
25
40
|
// out from under the player.
|
|
26
41
|
if (e.code === 'Space')
|
|
27
42
|
e.preventDefault();
|
|
28
43
|
};
|
|
29
|
-
this.onUp = (e) => { this.
|
|
30
|
-
this.onBlur = () => {
|
|
44
|
+
this.onUp = (e) => { this.keysDown.delete(e.code); };
|
|
45
|
+
this.onBlur = () => {
|
|
46
|
+
this.keysDown.clear();
|
|
47
|
+
this.stick.x = 0;
|
|
48
|
+
this.stick.z = 0;
|
|
49
|
+
this.touchJump = false;
|
|
50
|
+
this.touchHeld.clear();
|
|
51
|
+
};
|
|
31
52
|
this.stick = { x: 0, z: 0 };
|
|
32
53
|
this.touchJump = false;
|
|
54
|
+
this.actions = [];
|
|
55
|
+
this.touchHeld = new Set();
|
|
56
|
+
this.pressedAt = new Map();
|
|
57
|
+
this.unconsumed = new Set();
|
|
33
58
|
this.root = null;
|
|
34
59
|
this.cleanups = [];
|
|
35
60
|
// A Window here is the pre-touch signature; keep it working.
|
|
@@ -40,12 +65,13 @@ export class Input3D {
|
|
|
40
65
|
this.target.addEventListener('keyup', this.onUp);
|
|
41
66
|
// Losing focus mid-press would otherwise leave the character walking.
|
|
42
67
|
this.target.addEventListener('blur', this.onBlur);
|
|
68
|
+
this.actions = opts.actions ?? [];
|
|
43
69
|
const wantTouch = opts.touch === undefined || opts.touch === 'auto' ? isTouchDevice() : opts.touch;
|
|
44
70
|
if (wantTouch && typeof document !== 'undefined') {
|
|
45
71
|
this.mountTouch(opts.container ?? document.body);
|
|
46
72
|
}
|
|
47
73
|
}
|
|
48
|
-
isDown(...codes) { return codes.some((c) => this.
|
|
74
|
+
isDown(...codes) { return codes.some((c) => this.keysDown.has(c)); }
|
|
49
75
|
/** Camera-relative would need the camera; this is world-axis movement.
|
|
50
76
|
* Keyboard and stick are merged, so both work on a device with both. */
|
|
51
77
|
direction() {
|
|
@@ -64,10 +90,32 @@ export class Input3D {
|
|
|
64
90
|
}
|
|
65
91
|
/** Whether the jump control is held right now. Pass it to the controller —
|
|
66
92
|
* coyote time and buffering live there, not here. */
|
|
67
|
-
get jump() { return this.touchJump || this.isDown('Space'); }
|
|
93
|
+
get jump() { return this.held_('jump', this.touchJump || this.isDown('Space')); }
|
|
94
|
+
/** Whether an action is held right now — button or bound key, same answer. */
|
|
95
|
+
held(id) {
|
|
96
|
+
const a = this.actions.find((x) => x.id === id);
|
|
97
|
+
return this.held_(id, this.touchHeld.has(id) || (a?.keys ? this.isDown(...a.keys) : false));
|
|
98
|
+
}
|
|
99
|
+
/** True exactly once per press: one tap is one swing.
|
|
100
|
+
*
|
|
101
|
+
* Edge detection done in the game reads the button's STATE and compares it
|
|
102
|
+
* to last frame's, which means a press that begins and ends between two
|
|
103
|
+
* frames never happened. This latches at the event, so it cannot be missed
|
|
104
|
+
* — and it clears on read, so holding the button does not chain. */
|
|
105
|
+
consume(id) { return this.unconsumed.delete(id); }
|
|
106
|
+
held_(id, physical) {
|
|
107
|
+
if (physical)
|
|
108
|
+
return true;
|
|
109
|
+
const t = this.pressedAt.get(id);
|
|
110
|
+
return t !== undefined && now() - t < MIN_PRESS_MS;
|
|
111
|
+
}
|
|
112
|
+
beginPress(id) {
|
|
113
|
+
this.pressedAt.set(id, now());
|
|
114
|
+
this.unconsumed.add(id);
|
|
115
|
+
}
|
|
68
116
|
/** Test seam: drive the controller without synthesising DOM events. */
|
|
69
|
-
press(code) { this.
|
|
70
|
-
release(code) { this.
|
|
117
|
+
press(code) { this.keysDown.add(code); }
|
|
118
|
+
release(code) { this.keysDown.delete(code); }
|
|
71
119
|
dispose() {
|
|
72
120
|
this.target.removeEventListener('keydown', this.onDown);
|
|
73
121
|
this.target.removeEventListener('keyup', this.onUp);
|
|
@@ -77,7 +125,7 @@ export class Input3D {
|
|
|
77
125
|
this.cleanups.length = 0;
|
|
78
126
|
this.root?.remove();
|
|
79
127
|
this.root = null;
|
|
80
|
-
this.
|
|
128
|
+
this.keysDown.clear();
|
|
81
129
|
}
|
|
82
130
|
// ── on-screen controls ────────────────────────────────────────────────────
|
|
83
131
|
mountTouch(container) {
|
|
@@ -106,17 +154,38 @@ export class Input3D {
|
|
|
106
154
|
transform: 'translate(-50%, -50%)', pointerEvents: 'none',
|
|
107
155
|
});
|
|
108
156
|
pad.appendChild(knob);
|
|
109
|
-
|
|
110
|
-
|
|
157
|
+
// ONE cluster holds every button, laid out by flexbox, because the bug this
|
|
158
|
+
// replaces was two buttons independently choosing "bottom right" and
|
|
159
|
+
// landing on top of each other. Jump sits at the corner where it always
|
|
160
|
+
// was; actions stack to its left and wrap upward.
|
|
161
|
+
const cluster = document.createElement('div');
|
|
162
|
+
Object.assign(cluster.style, {
|
|
111
163
|
position: 'absolute', right: '6vmin', bottom: '7vmin',
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
116
|
-
color: 'rgba(255,255,255,0.85)', font: '600 4vmin/1 system-ui, sans-serif',
|
|
164
|
+
display: 'flex', flexDirection: 'row-reverse', alignItems: 'flex-end',
|
|
165
|
+
flexWrap: 'wrap-reverse', justifyContent: 'flex-start',
|
|
166
|
+
gap: '3vmin', maxWidth: '52vmin', pointerEvents: 'none',
|
|
117
167
|
});
|
|
118
|
-
|
|
119
|
-
|
|
168
|
+
const makeButton = (label) => {
|
|
169
|
+
const el = document.createElement('div');
|
|
170
|
+
Object.assign(el.style, {
|
|
171
|
+
width: '20vmin', height: '20vmin', maxWidth: '120px', maxHeight: '120px',
|
|
172
|
+
flex: '0 0 auto',
|
|
173
|
+
borderRadius: '50%', background: 'rgba(255,255,255,0.2)',
|
|
174
|
+
border: '2px solid rgba(255,255,255,0.4)', pointerEvents: 'auto',
|
|
175
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
176
|
+
color: 'rgba(255,255,255,0.85)', font: '600 4vmin/1 system-ui, sans-serif',
|
|
177
|
+
});
|
|
178
|
+
el.textContent = label;
|
|
179
|
+
return el;
|
|
180
|
+
};
|
|
181
|
+
const btn = makeButton('▲');
|
|
182
|
+
cluster.append(btn);
|
|
183
|
+
for (const a of this.actions) {
|
|
184
|
+
const el = makeButton(a.label ?? a.id.slice(0, 1).toUpperCase());
|
|
185
|
+
cluster.append(el);
|
|
186
|
+
this.wireButton(el, a.id);
|
|
187
|
+
}
|
|
188
|
+
root.append(pad, cluster);
|
|
120
189
|
container.appendChild(root);
|
|
121
190
|
this.root = root;
|
|
122
191
|
// Track by pointerId so a thumb on the stick and a thumb on the button do
|
|
@@ -158,9 +227,31 @@ export class Input3D {
|
|
|
158
227
|
on(pad, ev, (e) => { if (e.pointerId === stickId)
|
|
159
228
|
reset(); });
|
|
160
229
|
}
|
|
161
|
-
on(btn, 'pointerdown', (e) => {
|
|
230
|
+
on(btn, 'pointerdown', (e) => {
|
|
231
|
+
btn.setPointerCapture(e.pointerId);
|
|
232
|
+
this.touchJump = true;
|
|
233
|
+
this.beginPress('jump');
|
|
234
|
+
});
|
|
162
235
|
for (const ev of ['pointerup', 'pointercancel', 'lostpointercapture']) {
|
|
163
236
|
on(btn, ev, () => { this.touchJump = false; });
|
|
164
237
|
}
|
|
165
238
|
}
|
|
239
|
+
wireButton(el, id) {
|
|
240
|
+
const on = (ev, fn) => {
|
|
241
|
+
const h = fn;
|
|
242
|
+
el.addEventListener(ev, h);
|
|
243
|
+
this.cleanups.push(() => el.removeEventListener(ev, h));
|
|
244
|
+
};
|
|
245
|
+
on('pointerdown', (e) => {
|
|
246
|
+
e.preventDefault();
|
|
247
|
+
el.setPointerCapture(e.pointerId);
|
|
248
|
+
this.touchHeld.add(id);
|
|
249
|
+
this.beginPress(id);
|
|
250
|
+
});
|
|
251
|
+
// pointercancel included: a system gesture steals the pointer with no `up`,
|
|
252
|
+
// and the button would read as held for the rest of the run.
|
|
253
|
+
for (const ev of ['pointerup', 'pointercancel', 'lostpointercapture']) {
|
|
254
|
+
on(ev, () => { this.touchHeld.delete(id); });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
166
257
|
}
|
package/dist/SceneLoader3D.d.ts
CHANGED
|
@@ -46,4 +46,19 @@ export interface LoadedScene3D {
|
|
|
46
46
|
* settled and shipped for 2D, and it is the reason an editor can render a scene
|
|
47
47
|
* without starting a game.
|
|
48
48
|
*/
|
|
49
|
+
/** One model from the manifest, loaded on its own.
|
|
50
|
+
*
|
|
51
|
+
* `loadScene3D` only fetches what the SCENE references, which is everything a
|
|
52
|
+
* game needs until the first thing that appears at runtime — a weapon picked
|
|
53
|
+
* up, a projectile, a dropped item. Without this, a game reaches for
|
|
54
|
+
* GLTFLoader itself and re-implements asset-base resolution and importScale
|
|
55
|
+
* slightly differently from the loader beside it.
|
|
56
|
+
*
|
|
57
|
+
* Returns a fresh object each call, so two swords do not share one transform. */
|
|
58
|
+
export declare function loadModelAsset(manifest: Manifest3D, modelAssetId: string, opts?: {
|
|
59
|
+
assetBase?: string;
|
|
60
|
+
}): Promise<{
|
|
61
|
+
object: THREE.Object3D;
|
|
62
|
+
clips: THREE.AnimationClip[];
|
|
63
|
+
}>;
|
|
49
64
|
export declare function loadScene3D(scene3d: Scene3D, manifest: Manifest3D, opts?: LoadSceneOptions): Promise<LoadedScene3D>;
|
package/dist/SceneLoader3D.js
CHANGED
|
@@ -45,6 +45,28 @@ function makePrimitive(e) {
|
|
|
45
45
|
* settled and shipped for 2D, and it is the reason an editor can render a scene
|
|
46
46
|
* without starting a game.
|
|
47
47
|
*/
|
|
48
|
+
/** One model from the manifest, loaded on its own.
|
|
49
|
+
*
|
|
50
|
+
* `loadScene3D` only fetches what the SCENE references, which is everything a
|
|
51
|
+
* game needs until the first thing that appears at runtime — a weapon picked
|
|
52
|
+
* up, a projectile, a dropped item. Without this, a game reaches for
|
|
53
|
+
* GLTFLoader itself and re-implements asset-base resolution and importScale
|
|
54
|
+
* slightly differently from the loader beside it.
|
|
55
|
+
*
|
|
56
|
+
* Returns a fresh object each call, so two swords do not share one transform. */
|
|
57
|
+
export async function loadModelAsset(manifest, modelAssetId, opts = {}) {
|
|
58
|
+
const asset = (manifest.models ?? []).find((m) => m.id === modelAssetId);
|
|
59
|
+
if (!asset) {
|
|
60
|
+
const have = (manifest.models ?? []).map((m) => m.id).join(', ');
|
|
61
|
+
throw new Error(`[umicat] manifest has no model '${modelAssetId}'. It has: ${have || '(none)'}`);
|
|
62
|
+
}
|
|
63
|
+
const gltf = await new GLTFLoader().loadAsync((opts.assetBase ?? '') + asset.path);
|
|
64
|
+
const object = gltf.scene;
|
|
65
|
+
if (asset.importScale !== undefined)
|
|
66
|
+
object.scale.setScalar(asset.importScale);
|
|
67
|
+
object.userData.modelAssetId = modelAssetId;
|
|
68
|
+
return { object, clips: gltf.animations };
|
|
69
|
+
}
|
|
48
70
|
export async function loadScene3D(scene3d, manifest, opts = {}) {
|
|
49
71
|
const problems = validateScene(scene3d);
|
|
50
72
|
if (problems.length) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import type { Socket3D } from './scene3d';
|
|
3
|
+
/**
|
|
4
|
+
* Put a thing in a character's hand.
|
|
5
|
+
*
|
|
6
|
+
* A held object is not a child of the character — it is a child of a BONE, so
|
|
7
|
+
* it inherits that bone's animated transform and follows the swing for free.
|
|
8
|
+
* Parenting to the model root instead produces a sword that hovers politely
|
|
9
|
+
* beside a character doing all the work.
|
|
10
|
+
*
|
|
11
|
+
* The catch, and the reason this is a platform primitive rather than four
|
|
12
|
+
* lines in each game: **nothing guarantees a rig has a hand bone.** The
|
|
13
|
+
* character every 3D Umicat game ships with has seven bones — root, two legs,
|
|
14
|
+
* torso, two arms, head — and no wrist at all. So "the hand" is a position
|
|
15
|
+
* along the arm bone, found by tuning, and it is a property of that model.
|
|
16
|
+
* Every game re-deriving it by eye is how you get twelve games with the sword
|
|
17
|
+
* in twelve slightly different places.
|
|
18
|
+
*/
|
|
19
|
+
export interface Attachment {
|
|
20
|
+
/** The bone it ended up on — useful when you want to check your work. */
|
|
21
|
+
readonly bone: THREE.Bone;
|
|
22
|
+
/** The wrapper that carries the socket offset; the object is its child. */
|
|
23
|
+
readonly holder: THREE.Object3D;
|
|
24
|
+
detach(): void;
|
|
25
|
+
}
|
|
26
|
+
/** Find a bone by exact name anywhere under `root`. */
|
|
27
|
+
export declare function findBone(root: THREE.Object3D, name: string): THREE.Bone | null;
|
|
28
|
+
/** Every bone name under `root` — what an error message should offer. */
|
|
29
|
+
export declare function boneNames(root: THREE.Object3D): string[];
|
|
30
|
+
/**
|
|
31
|
+
* Attach `object` to a socket on `root`.
|
|
32
|
+
*
|
|
33
|
+
* Throws if the bone is missing, and says which bones DO exist. A silent miss
|
|
34
|
+
* here is the worst outcome available: the weapon stays parented where it was,
|
|
35
|
+
* usually at the world origin, and the character walks around empty-handed
|
|
36
|
+
* while every line of the game's equip logic reports success.
|
|
37
|
+
*/
|
|
38
|
+
export declare function attachToSocket(root: THREE.Object3D, socket: Socket3D, object: THREE.Object3D): Attachment;
|
package/dist/Sockets.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
/** Find a bone by exact name anywhere under `root`. */
|
|
3
|
+
export function findBone(root, name) {
|
|
4
|
+
let found = null;
|
|
5
|
+
root.traverse((o) => { if (!found && o.isBone && o.name === name)
|
|
6
|
+
found = o; });
|
|
7
|
+
return found;
|
|
8
|
+
}
|
|
9
|
+
/** Every bone name under `root` — what an error message should offer. */
|
|
10
|
+
export function boneNames(root) {
|
|
11
|
+
const out = [];
|
|
12
|
+
root.traverse((o) => { if (o.isBone)
|
|
13
|
+
out.push(o.name); });
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Attach `object` to a socket on `root`.
|
|
18
|
+
*
|
|
19
|
+
* Throws if the bone is missing, and says which bones DO exist. A silent miss
|
|
20
|
+
* here is the worst outcome available: the weapon stays parented where it was,
|
|
21
|
+
* usually at the world origin, and the character walks around empty-handed
|
|
22
|
+
* while every line of the game's equip logic reports success.
|
|
23
|
+
*/
|
|
24
|
+
export function attachToSocket(root, socket, object) {
|
|
25
|
+
const bone = findBone(root, socket.bone);
|
|
26
|
+
if (!bone) {
|
|
27
|
+
throw new Error(`[umicat] no bone "${socket.bone}" on this model. It has: ${boneNames(root).join(', ') || '(no bones at all)'}`);
|
|
28
|
+
}
|
|
29
|
+
// A holder, not the object itself: the socket offset and whatever transform
|
|
30
|
+
// the object arrives with are different things, and baking them together
|
|
31
|
+
// means a game cannot move the object within the hand without losing the
|
|
32
|
+
// socket. It also makes detaching exact.
|
|
33
|
+
const holder = new THREE.Object3D();
|
|
34
|
+
holder.name = `socket:${socket.bone}`;
|
|
35
|
+
if (socket.position)
|
|
36
|
+
holder.position.set(socket.position.x, socket.position.y, socket.position.z);
|
|
37
|
+
if (socket.rotation)
|
|
38
|
+
holder.rotation.set(socket.rotation.x, socket.rotation.y, socket.rotation.z);
|
|
39
|
+
if (socket.scale !== undefined)
|
|
40
|
+
holder.scale.setScalar(socket.scale);
|
|
41
|
+
holder.add(object);
|
|
42
|
+
bone.add(holder);
|
|
43
|
+
return {
|
|
44
|
+
bone,
|
|
45
|
+
holder,
|
|
46
|
+
detach() { holder.removeFromParent(); },
|
|
47
|
+
};
|
|
48
|
+
}
|
package/dist/ThreeUmicat.js
CHANGED
|
@@ -21,6 +21,33 @@ export class ThreeUmicat extends UmicatCore {
|
|
|
21
21
|
this.dialogue = new DialogueModule(this.saves, () => transport.locale ?? 'en', () => transport.user?.name ?? null);
|
|
22
22
|
}
|
|
23
23
|
static async init(options = {}) {
|
|
24
|
-
|
|
24
|
+
const game = new ThreeUmicat(await UmicatCore.connect(SDK_VERSION, options));
|
|
25
|
+
announceScaleMode();
|
|
26
|
+
return game;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Tell the host this game fills the frame.
|
|
31
|
+
*
|
|
32
|
+
* The host letterboxes to the game's authored aspect UNLESS the game says
|
|
33
|
+
* otherwise, and it reads silence as "built before the SDK announced anything"
|
|
34
|
+
* — which every 3D game was, because only the Phaser SDK ever sent this. So a
|
|
35
|
+
* 3D game on a phone rendered into a 16:9 box with black down the side, losing
|
|
36
|
+
* a third of the screen, and the only symptom was a black band nobody could
|
|
37
|
+
* attribute to anything.
|
|
38
|
+
*
|
|
39
|
+
* A 3D game is always `resize`: the camera's aspect follows the window, so
|
|
40
|
+
* there is no authored pixel canvas to preserve. That is a property of drawing
|
|
41
|
+
* in 3D, not a per-game preference, which is why this is not an option.
|
|
42
|
+
*/
|
|
43
|
+
function announceScaleMode() {
|
|
44
|
+
try {
|
|
45
|
+
// Safe no-op when not embedded — the same game runs standalone in dev.
|
|
46
|
+
if (typeof window !== 'undefined' && window.parent && window.parent !== window) {
|
|
47
|
+
window.parent.postMessage({ type: 'umicat:scaleMode', mode: 'resize' }, '*');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* cross-origin parent access can throw in odd embeds — non-fatal */
|
|
25
52
|
}
|
|
26
53
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
export { ThreeUmicat } from './ThreeUmicat.js';
|
|
2
2
|
export type { UmicatInitOptions } from './ThreeUmicat.js';
|
|
3
3
|
export * from './scene3d.js';
|
|
4
|
-
export { loadScene3D } from './SceneLoader3D.js';
|
|
4
|
+
export { loadScene3D, loadModelAsset } from './SceneLoader3D.js';
|
|
5
5
|
export { CharacterController3D } from './CharacterController3D.js';
|
|
6
6
|
export type { CharacterOptions, CharacterState, CharacterInput } from './CharacterController3D.js';
|
|
7
7
|
export { CharacterAnimator } from './CharacterAnimator.js';
|
|
8
8
|
export type { ClipMap, CharacterAnimatorOptions } from './CharacterAnimator.js';
|
|
9
9
|
export { Input3D } from './Input3D.js';
|
|
10
|
-
export type { Input3DOptions } from './Input3D.js';
|
|
10
|
+
export type { Input3DOptions, Input3DAction } from './Input3D.js';
|
|
11
|
+
export { attachToSocket, findBone, boneNames } from './Sockets.js';
|
|
12
|
+
export type { Attachment } from './Sockets.js';
|
|
11
13
|
export type { LoadedScene3D, LoadSceneOptions } from './SceneLoader3D.js';
|
|
12
14
|
export { ORIENTATION_DIMENSIONS } from '@umicat/platform-sdk/orientation.js';
|
|
13
15
|
export type { Orientation } from '@umicat/platform-sdk/orientation.js';
|
package/dist/index.js
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
// that turns it into a three.js scene with physics, and the runtime glue.
|
|
6
6
|
export { ThreeUmicat } from './ThreeUmicat.js';
|
|
7
7
|
export * from './scene3d.js';
|
|
8
|
-
export { loadScene3D } from './SceneLoader3D.js';
|
|
8
|
+
export { loadScene3D, loadModelAsset } from './SceneLoader3D.js';
|
|
9
9
|
export { CharacterController3D } from './CharacterController3D.js';
|
|
10
10
|
export { CharacterAnimator } from './CharacterAnimator.js';
|
|
11
11
|
export { Input3D } from './Input3D.js';
|
|
12
|
+
export { attachToSocket, findBone, boneNames } from './Sockets.js';
|
|
12
13
|
// Re-exported so a game imports one package for the common case. A game should
|
|
13
14
|
// not have to know that identity and saves come from a different package than
|
|
14
15
|
// the renderer.
|
package/dist/scene3d.d.ts
CHANGED
|
@@ -92,6 +92,26 @@ export interface Entity3D {
|
|
|
92
92
|
* model calls its walk cycle `Walk` fails the same way, only more quietly.
|
|
93
93
|
*/
|
|
94
94
|
export type AnimationMap = Record<string, string>;
|
|
95
|
+
/**
|
|
96
|
+
* Where a held object sits on a character, named by what it is FOR rather
|
|
97
|
+
* than by which bone it happens to hang off.
|
|
98
|
+
*
|
|
99
|
+
* Rigs do not necessarily have a hand bone — this character's has seven bones
|
|
100
|
+
* and the arm is one of them, so a sword hangs off `arm-right` with an offset
|
|
101
|
+
* that puts it where a hand would be. That offset is a property of the MODEL,
|
|
102
|
+
* not of any game, so it travels in the manifest: tune it once and every game
|
|
103
|
+
* using this character gets a sword in the right place.
|
|
104
|
+
*/
|
|
105
|
+
export interface Socket3D {
|
|
106
|
+
/** Bone to hang from. Names are exact; the loader reports what it found. */
|
|
107
|
+
bone: string;
|
|
108
|
+
/** Offset in the bone's local space. */
|
|
109
|
+
position?: Vec3;
|
|
110
|
+
/** Euler angles in radians, applied in the bone's local space. */
|
|
111
|
+
rotation?: Vec3;
|
|
112
|
+
/** Uniform scale for whatever is attached. */
|
|
113
|
+
scale?: number;
|
|
114
|
+
}
|
|
95
115
|
export interface ModelAsset3D {
|
|
96
116
|
id: string;
|
|
97
117
|
/** Resolved by the runtime against the project's asset base url. */
|
|
@@ -99,6 +119,8 @@ export interface ModelAsset3D {
|
|
|
99
119
|
animations?: AnimationMap;
|
|
100
120
|
/** Authoring correction applied at load: most models are not born 1 unit tall. */
|
|
101
121
|
importScale?: number;
|
|
122
|
+
/** Named attachment points — `{ "hand-right": { bone: "arm-right", ... } }`. */
|
|
123
|
+
sockets?: Record<string, Socket3D>;
|
|
102
124
|
}
|
|
103
125
|
export interface Light3D {
|
|
104
126
|
id: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umicat/three-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|