@umicat/three-sdk 0.5.0 → 0.7.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 -1
- package/dist/Input3D.js +83 -11
- 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/Tint.d.ts +16 -0
- package/dist/Tint.js +62 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/scene3d.d.ts +22 -0
- package/package.json +1 -1
package/dist/Input3D.d.ts
CHANGED
|
@@ -33,6 +33,18 @@ export interface Input3DOptions {
|
|
|
33
33
|
* platform places the controls, so the platform has to place ALL of them.
|
|
34
34
|
*/
|
|
35
35
|
actions?: Input3DAction[];
|
|
36
|
+
/**
|
|
37
|
+
* How the thumbstick behaves on touch.
|
|
38
|
+
*
|
|
39
|
+
* `'floating'` (the default) is the phone convention Roblox made standard:
|
|
40
|
+
* nothing is drawn until a thumb lands on the left half of the screen, and
|
|
41
|
+
* then the stick appears exactly there. A fixed stick makes the player find
|
|
42
|
+
* a target before they can move, and on a screen you cannot feel, that is a
|
|
43
|
+
* thumb-sized target somewhere the hand is not.
|
|
44
|
+
*
|
|
45
|
+
* `'fixed'` keeps the old always-visible pad at the bottom left.
|
|
46
|
+
*/
|
|
47
|
+
stick?: 'floating' | 'fixed';
|
|
36
48
|
}
|
|
37
49
|
/**
|
|
38
50
|
* Movement and jump, from a keyboard or a thumb, behind one interface.
|
|
@@ -58,6 +70,7 @@ export declare class Input3D {
|
|
|
58
70
|
private readonly stick;
|
|
59
71
|
private touchJump;
|
|
60
72
|
private actions;
|
|
73
|
+
private stickMode;
|
|
61
74
|
private readonly touchHeld;
|
|
62
75
|
private readonly pressedAt;
|
|
63
76
|
private readonly unconsumed;
|
|
@@ -85,7 +98,13 @@ export declare class Input3D {
|
|
|
85
98
|
consume(id: string): boolean;
|
|
86
99
|
private held_;
|
|
87
100
|
private beginPress;
|
|
88
|
-
/** Test seam: drive the controller without synthesising DOM events.
|
|
101
|
+
/** Test seam: drive the controller without synthesising DOM events.
|
|
102
|
+
*
|
|
103
|
+
* This latches exactly as a real keydown does. It used to only add the code
|
|
104
|
+
* to the held set, so `consume()` never saw it — a seam that behaved
|
|
105
|
+
* differently from the thing it stands in for, which made a test report
|
|
106
|
+
* that an attack did not land when the only thing that had not happened was
|
|
107
|
+
* the press. */
|
|
89
108
|
press(code: string): void;
|
|
90
109
|
release(code: string): void;
|
|
91
110
|
dispose(): void;
|
package/dist/Input3D.js
CHANGED
|
@@ -52,6 +52,7 @@ export class Input3D {
|
|
|
52
52
|
this.stick = { x: 0, z: 0 };
|
|
53
53
|
this.touchJump = false;
|
|
54
54
|
this.actions = [];
|
|
55
|
+
this.stickMode = 'floating';
|
|
55
56
|
this.touchHeld = new Set();
|
|
56
57
|
this.pressedAt = new Map();
|
|
57
58
|
this.unconsumed = new Set();
|
|
@@ -66,6 +67,7 @@ export class Input3D {
|
|
|
66
67
|
// Losing focus mid-press would otherwise leave the character walking.
|
|
67
68
|
this.target.addEventListener('blur', this.onBlur);
|
|
68
69
|
this.actions = opts.actions ?? [];
|
|
70
|
+
this.stickMode = opts.stick ?? 'floating';
|
|
69
71
|
const wantTouch = opts.touch === undefined || opts.touch === 'auto' ? isTouchDevice() : opts.touch;
|
|
70
72
|
if (wantTouch && typeof document !== 'undefined') {
|
|
71
73
|
this.mountTouch(opts.container ?? document.body);
|
|
@@ -113,8 +115,23 @@ export class Input3D {
|
|
|
113
115
|
this.pressedAt.set(id, now());
|
|
114
116
|
this.unconsumed.add(id);
|
|
115
117
|
}
|
|
116
|
-
/** Test seam: drive the controller without synthesising DOM events.
|
|
117
|
-
|
|
118
|
+
/** Test seam: drive the controller without synthesising DOM events.
|
|
119
|
+
*
|
|
120
|
+
* This latches exactly as a real keydown does. It used to only add the code
|
|
121
|
+
* to the held set, so `consume()` never saw it — a seam that behaved
|
|
122
|
+
* differently from the thing it stands in for, which made a test report
|
|
123
|
+
* that an attack did not land when the only thing that had not happened was
|
|
124
|
+
* the press. */
|
|
125
|
+
press(code) {
|
|
126
|
+
if (this.keysDown.has(code))
|
|
127
|
+
return; // held, not re-pressed
|
|
128
|
+
this.keysDown.add(code);
|
|
129
|
+
if (code === 'Space')
|
|
130
|
+
this.beginPress('jump');
|
|
131
|
+
for (const a of this.actions)
|
|
132
|
+
if (a.keys?.includes(code))
|
|
133
|
+
this.beginPress(a.id);
|
|
134
|
+
}
|
|
118
135
|
release(code) { this.keysDown.delete(code); }
|
|
119
136
|
dispose() {
|
|
120
137
|
this.target.removeEventListener('keydown', this.onDown);
|
|
@@ -140,8 +157,28 @@ export class Input3D {
|
|
|
140
157
|
position: 'fixed', inset: '0', pointerEvents: 'none',
|
|
141
158
|
touchAction: 'none', userSelect: 'none', zIndex: '10',
|
|
142
159
|
});
|
|
160
|
+
const floating = this.stickMode === 'floating';
|
|
161
|
+
// The zone a thumb may land on to summon the stick. It is the LEFT HALF,
|
|
162
|
+
// not the pad: the whole point is that the player does not have to find
|
|
163
|
+
// anything. It deliberately stops short of the buttons on the right.
|
|
164
|
+
const zone = document.createElement('div');
|
|
165
|
+
Object.assign(zone.style, {
|
|
166
|
+
position: 'absolute', left: '0', top: '0', width: '50%', height: '100%',
|
|
167
|
+
pointerEvents: floating ? 'auto' : 'none',
|
|
168
|
+
});
|
|
143
169
|
const pad = document.createElement('div');
|
|
144
|
-
Object.assign(pad.style, {
|
|
170
|
+
Object.assign(pad.style, floating ? {
|
|
171
|
+
position: 'absolute', width: '30vmin', height: '30vmin',
|
|
172
|
+
maxWidth: '180px', maxHeight: '180px',
|
|
173
|
+
borderRadius: '50%', background: 'rgba(255,255,255,0.14)',
|
|
174
|
+
border: '2px solid rgba(255,255,255,0.35)',
|
|
175
|
+
// Never interactive when floating: the ZONE owns the pointer, and a pad
|
|
176
|
+
// that also captured it would steal the very first move event as the
|
|
177
|
+
// thumb crosses its edge.
|
|
178
|
+
pointerEvents: 'none', display: 'none',
|
|
179
|
+
transform: 'translate(-50%, -50%)',
|
|
180
|
+
transition: 'opacity 120ms linear',
|
|
181
|
+
} : {
|
|
145
182
|
position: 'absolute', left: '5vmin', bottom: '5vmin',
|
|
146
183
|
width: '30vmin', height: '30vmin', maxWidth: '180px', maxHeight: '180px',
|
|
147
184
|
borderRadius: '50%', background: 'rgba(255,255,255,0.14)',
|
|
@@ -185,17 +222,37 @@ export class Input3D {
|
|
|
185
222
|
cluster.append(el);
|
|
186
223
|
this.wireButton(el, a.id);
|
|
187
224
|
}
|
|
188
|
-
root.append(pad, cluster);
|
|
225
|
+
root.append(zone, pad, cluster);
|
|
189
226
|
container.appendChild(root);
|
|
190
227
|
this.root = root;
|
|
191
228
|
// Track by pointerId so a thumb on the stick and a thumb on the button do
|
|
192
229
|
// not fight over one piece of state.
|
|
193
230
|
let stickId = null;
|
|
194
|
-
|
|
231
|
+
// Where the stick is centred. When floating this is wherever the thumb
|
|
232
|
+
// landed, so it is remembered rather than read back off the element —
|
|
233
|
+
// reading the rect would make the origin drift with the pad's own
|
|
234
|
+
// transform and the stick would feel like it was sliding away.
|
|
235
|
+
let originX = 0, originY = 0, radius = 0;
|
|
236
|
+
const measurePad = () => {
|
|
195
237
|
const r = pad.getBoundingClientRect();
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
238
|
+
radius = r.width / 2;
|
|
239
|
+
if (!floating) {
|
|
240
|
+
originX = r.left + radius;
|
|
241
|
+
originY = r.top + r.height / 2;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
const showAt = (x, y) => {
|
|
245
|
+
originX = x;
|
|
246
|
+
originY = y;
|
|
247
|
+
pad.style.display = 'block';
|
|
248
|
+
pad.style.left = `${x}px`;
|
|
249
|
+
pad.style.top = `${y}px`;
|
|
250
|
+
measurePad();
|
|
251
|
+
};
|
|
252
|
+
const setFromEvent = (e) => {
|
|
253
|
+
if (!radius)
|
|
254
|
+
measurePad();
|
|
255
|
+
let dx = (e.clientX - originX) / radius, dy = (e.clientY - originY) / radius;
|
|
199
256
|
const len = Math.hypot(dx, dy);
|
|
200
257
|
if (len > 1) {
|
|
201
258
|
dx /= len;
|
|
@@ -212,19 +269,34 @@ export class Input3D {
|
|
|
212
269
|
this.stick.z = 0;
|
|
213
270
|
knob.style.left = '50%';
|
|
214
271
|
knob.style.top = '50%';
|
|
272
|
+
if (floating)
|
|
273
|
+
pad.style.display = 'none';
|
|
215
274
|
};
|
|
216
275
|
const on = (el, ev, fn) => {
|
|
217
276
|
const h = fn;
|
|
218
277
|
el.addEventListener(ev, h);
|
|
219
278
|
this.cleanups.push(() => el.removeEventListener(ev, h));
|
|
220
279
|
};
|
|
221
|
-
|
|
222
|
-
|
|
280
|
+
// The element that owns the gesture differs by mode, but the handlers do
|
|
281
|
+
// not — which is the point: `direction()` reads the same either way.
|
|
282
|
+
const grip = floating ? zone : pad;
|
|
283
|
+
on(grip, 'pointerdown', (e) => {
|
|
284
|
+
// One thumb drives the stick. A second finger landing in the zone must
|
|
285
|
+
// not move the origin out from under the first.
|
|
286
|
+
if (stickId !== null)
|
|
287
|
+
return;
|
|
288
|
+
stickId = e.pointerId;
|
|
289
|
+
grip.setPointerCapture(e.pointerId);
|
|
290
|
+
if (floating)
|
|
291
|
+
showAt(e.clientX, e.clientY);
|
|
292
|
+
setFromEvent(e);
|
|
293
|
+
});
|
|
294
|
+
on(grip, 'pointermove', (e) => { if (e.pointerId === stickId)
|
|
223
295
|
setFromEvent(e); });
|
|
224
296
|
// pointercancel too: a system gesture steals the pointer without an up, and
|
|
225
297
|
// the character would walk forever.
|
|
226
298
|
for (const ev of ['pointerup', 'pointercancel', 'lostpointercapture']) {
|
|
227
|
-
on(
|
|
299
|
+
on(grip, ev, (e) => { if (e.pointerId === stickId)
|
|
228
300
|
reset(); });
|
|
229
301
|
}
|
|
230
302
|
on(btn, 'pointerdown', (e) => {
|
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/Tint.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
/**
|
|
3
|
+
* Flash `object` for `ms` milliseconds. Call `updateTints` each frame.
|
|
4
|
+
*
|
|
5
|
+
* Flashing something already flashing restarts it rather than stacking, so a
|
|
6
|
+
* fast combo does not leave a character permanently red.
|
|
7
|
+
*/
|
|
8
|
+
export declare function flashTint(object: THREE.Object3D, opts?: {
|
|
9
|
+
color?: THREE.ColorRepresentation;
|
|
10
|
+
ms?: number;
|
|
11
|
+
intensity?: number;
|
|
12
|
+
}): void;
|
|
13
|
+
/** Restore anything whose flash has expired. Call once per frame. */
|
|
14
|
+
export declare function updateTints(objects: Iterable<THREE.Object3D>): void;
|
|
15
|
+
/** Whether this object is mid-flash — handy for tests and for not stacking. */
|
|
16
|
+
export declare function isTinted(object: THREE.Object3D): boolean;
|
package/dist/Tint.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
const states = new WeakMap();
|
|
3
|
+
const now = () => (typeof performance !== 'undefined' ? performance.now() : Date.now());
|
|
4
|
+
/** Give this object its own materials, so tinting it tints only it. */
|
|
5
|
+
function isolate(object) {
|
|
6
|
+
const out = [];
|
|
7
|
+
object.traverse((o) => {
|
|
8
|
+
const mesh = o;
|
|
9
|
+
if (!mesh.isMesh)
|
|
10
|
+
return;
|
|
11
|
+
const list = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
12
|
+
const cloned = list.map((m) => {
|
|
13
|
+
const c = m.clone();
|
|
14
|
+
return c;
|
|
15
|
+
});
|
|
16
|
+
mesh.material = Array.isArray(mesh.material) ? cloned : cloned[0];
|
|
17
|
+
for (const m of cloned) {
|
|
18
|
+
if (!m.emissive)
|
|
19
|
+
continue;
|
|
20
|
+
out.push({ mat: m, emissive: m.emissive.clone(), intensity: m.emissiveIntensity ?? 1 });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Flash `object` for `ms` milliseconds. Call `updateTints` each frame.
|
|
27
|
+
*
|
|
28
|
+
* Flashing something already flashing restarts it rather than stacking, so a
|
|
29
|
+
* fast combo does not leave a character permanently red.
|
|
30
|
+
*/
|
|
31
|
+
export function flashTint(object, opts = {}) {
|
|
32
|
+
let st = states.get(object);
|
|
33
|
+
if (!st) {
|
|
34
|
+
st = { materials: isolate(object), until: 0 };
|
|
35
|
+
states.set(object, st);
|
|
36
|
+
}
|
|
37
|
+
const color = new THREE.Color(opts.color ?? 0xff3020);
|
|
38
|
+
for (const m of st.materials) {
|
|
39
|
+
m.mat.emissive.copy(color);
|
|
40
|
+
m.mat.emissiveIntensity = opts.intensity ?? 0.9;
|
|
41
|
+
}
|
|
42
|
+
st.until = now() + (opts.ms ?? 140);
|
|
43
|
+
}
|
|
44
|
+
/** Restore anything whose flash has expired. Call once per frame. */
|
|
45
|
+
export function updateTints(objects) {
|
|
46
|
+
const t = now();
|
|
47
|
+
for (const o of objects) {
|
|
48
|
+
const st = states.get(o);
|
|
49
|
+
if (!st || st.until === 0 || t < st.until)
|
|
50
|
+
continue;
|
|
51
|
+
st.until = 0;
|
|
52
|
+
for (const m of st.materials) {
|
|
53
|
+
m.mat.emissive.copy(m.emissive);
|
|
54
|
+
m.mat.emissiveIntensity = m.intensity;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Whether this object is mid-flash — handy for tests and for not stacking. */
|
|
59
|
+
export function isTinted(object) {
|
|
60
|
+
const st = states.get(object);
|
|
61
|
+
return !!st && st.until > now();
|
|
62
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
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 { flashTint, updateTints, isTinted } from './Tint.js';
|
|
13
|
+
export type { Attachment } from './Sockets.js';
|
|
11
14
|
export type { LoadedScene3D, LoadSceneOptions } from './SceneLoader3D.js';
|
|
12
15
|
export { ORIENTATION_DIMENSIONS } from '@umicat/platform-sdk/orientation.js';
|
|
13
16
|
export type { Orientation } from '@umicat/platform-sdk/orientation.js';
|
package/dist/index.js
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
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';
|
|
13
|
+
export { flashTint, updateTints, isTinted } from './Tint.js';
|
|
12
14
|
// Re-exported so a game imports one package for the common case. A game should
|
|
13
15
|
// not have to know that identity and saves come from a different package than
|
|
14
16
|
// 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.7.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",
|