@worldmesh/runtime 0.1.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/LICENSE +21 -0
- package/README.md +79 -0
- package/dist/abilities/abilities.d.ts +9 -0
- package/dist/abilities/abilities.d.ts.map +1 -0
- package/dist/abilities/abilities.js +25 -0
- package/dist/abilities/abilities.js.map +1 -0
- package/dist/camera/cameraRig.d.ts +39 -0
- package/dist/camera/cameraRig.d.ts.map +1 -0
- package/dist/camera/cameraRig.js +88 -0
- package/dist/camera/cameraRig.js.map +1 -0
- package/dist/controls/input.d.ts +64 -0
- package/dist/controls/input.d.ts.map +1 -0
- package/dist/controls/input.js +236 -0
- package/dist/controls/input.js.map +1 -0
- package/dist/controls/keymap.d.ts +8 -0
- package/dist/controls/keymap.d.ts.map +1 -0
- package/dist/controls/keymap.js +21 -0
- package/dist/controls/keymap.js.map +1 -0
- package/dist/controls/touch.d.ts +43 -0
- package/dist/controls/touch.d.ts.map +1 -0
- package/dist/controls/touch.js +329 -0
- package/dist/controls/touch.js.map +1 -0
- package/dist/core/events.d.ts +9 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/events.js +27 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/worldmesh.d.ts +10 -0
- package/dist/core/worldmesh.d.ts.map +1 -0
- package/dist/core/worldmesh.js +283 -0
- package/dist/core/worldmesh.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/movement/collision.d.ts +39 -0
- package/dist/movement/collision.d.ts.map +1 -0
- package/dist/movement/collision.js +93 -0
- package/dist/movement/collision.js.map +1 -0
- package/dist/movement/controller.d.ts +53 -0
- package/dist/movement/controller.d.ts.map +1 -0
- package/dist/movement/controller.js +200 -0
- package/dist/movement/controller.js.map +1 -0
- package/dist/net/adapter.d.ts +22 -0
- package/dist/net/adapter.d.ts.map +1 -0
- package/dist/net/adapter.js +8 -0
- package/dist/net/adapter.js.map +1 -0
- package/dist/player/avatar.d.ts +29 -0
- package/dist/player/avatar.d.ts.map +1 -0
- package/dist/player/avatar.js +420 -0
- package/dist/player/avatar.js.map +1 -0
- package/dist/player/player.d.ts +30 -0
- package/dist/player/player.d.ts.map +1 -0
- package/dist/player/player.js +76 -0
- package/dist/player/player.js.map +1 -0
- package/dist/portals/portals.d.ts +34 -0
- package/dist/portals/portals.d.ts.map +1 -0
- package/dist/portals/portals.js +132 -0
- package/dist/portals/portals.js.map +1 -0
- package/dist/types.d.ts +228 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/overlay.d.ts +19 -0
- package/dist/ui/overlay.d.ts.map +1 -0
- package/dist/ui/overlay.js +370 -0
- package/dist/ui/overlay.js.map +1 -0
- package/package.json +63 -0
- package/src/abilities/abilities.ts +30 -0
- package/src/camera/cameraRig.ts +106 -0
- package/src/controls/input.ts +252 -0
- package/src/controls/keymap.ts +23 -0
- package/src/controls/touch.ts +378 -0
- package/src/core/events.ts +31 -0
- package/src/core/worldmesh.ts +313 -0
- package/src/index.ts +46 -0
- package/src/movement/collision.ts +109 -0
- package/src/movement/controller.ts +248 -0
- package/src/net/adapter.ts +28 -0
- package/src/player/avatar.ts +495 -0
- package/src/player/player.ts +91 -0
- package/src/portals/portals.ts +173 -0
- package/src/types.ts +230 -0
- package/src/ui/overlay.ts +398 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { Vector3 } from 'three';
|
|
2
|
+
import type { Input } from '../controls/input.js';
|
|
3
|
+
import type { Abilities, MovementTuning } from '../types.js';
|
|
4
|
+
import type { CollisionWorld } from './collision.js';
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_TUNING: MovementTuning = {
|
|
7
|
+
walkSpeed: 5.2,
|
|
8
|
+
sprintSpeed: 9,
|
|
9
|
+
crouchSpeed: 2.4,
|
|
10
|
+
flySpeed: 12,
|
|
11
|
+
jumpSpeed: 7.6,
|
|
12
|
+
gravity: 24,
|
|
13
|
+
groundAccel: 14,
|
|
14
|
+
airAccel: 4,
|
|
15
|
+
dashSpeed: 18,
|
|
16
|
+
dashCooldown: 1.2,
|
|
17
|
+
maxFallSpeed: 55,
|
|
18
|
+
fallLimit: -60,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface ControllerOptions {
|
|
22
|
+
input: Input;
|
|
23
|
+
collision: CollisionWorld;
|
|
24
|
+
abilities: Abilities;
|
|
25
|
+
tuning?: Partial<MovementTuning>;
|
|
26
|
+
height: number;
|
|
27
|
+
radius: number;
|
|
28
|
+
spawn: Vector3;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The one movement implementation every WorldMesh world shares.
|
|
33
|
+
* Optional capabilities are branches on `abilities` flags, never subclasses:
|
|
34
|
+
* that is what keeps "declare it in config" honest.
|
|
35
|
+
*/
|
|
36
|
+
export class MovementController {
|
|
37
|
+
/** Player feet position in world space. */
|
|
38
|
+
readonly position = new Vector3();
|
|
39
|
+
readonly velocity = new Vector3();
|
|
40
|
+
onGround = false;
|
|
41
|
+
crouching = false;
|
|
42
|
+
flying = false;
|
|
43
|
+
/** Current standing height, shrinks while crouching. */
|
|
44
|
+
currentHeight: number;
|
|
45
|
+
|
|
46
|
+
readonly tuning: MovementTuning;
|
|
47
|
+
|
|
48
|
+
private input: Input;
|
|
49
|
+
private collision: CollisionWorld;
|
|
50
|
+
private abilities: Abilities;
|
|
51
|
+
private baseHeight: number;
|
|
52
|
+
private radius: number;
|
|
53
|
+
private jumpsUsed = 0;
|
|
54
|
+
private dashCooldown = 0;
|
|
55
|
+
private dashTime = 0;
|
|
56
|
+
private dashDirection = new Vector3();
|
|
57
|
+
private scratch = new Vector3();
|
|
58
|
+
private groundProbe = new Vector3();
|
|
59
|
+
|
|
60
|
+
constructor(options: ControllerOptions) {
|
|
61
|
+
this.input = options.input;
|
|
62
|
+
this.collision = options.collision;
|
|
63
|
+
this.abilities = options.abilities;
|
|
64
|
+
this.tuning = { ...DEFAULT_TUNING, ...options.tuning };
|
|
65
|
+
this.baseHeight = options.height;
|
|
66
|
+
this.currentHeight = options.height;
|
|
67
|
+
this.radius = options.radius;
|
|
68
|
+
this.position.copy(options.spawn);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get height(): number {
|
|
72
|
+
return this.currentHeight;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
reset(position: Vector3): void {
|
|
76
|
+
this.position.copy(position);
|
|
77
|
+
this.velocity.set(0, 0, 0);
|
|
78
|
+
this.onGround = false;
|
|
79
|
+
this.flying = false;
|
|
80
|
+
this.crouching = false;
|
|
81
|
+
this.jumpsUsed = 0;
|
|
82
|
+
this.dashTime = 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Advance one simulation step. `yaw` is the camera heading in radians. */
|
|
86
|
+
step(dt: number, yaw: number, controlsEnabled: boolean): void {
|
|
87
|
+
const axis = controlsEnabled ? this.input.getMoveAxis() : { x: 0, z: 0 };
|
|
88
|
+
|
|
89
|
+
this.updateModes(controlsEnabled);
|
|
90
|
+
|
|
91
|
+
// Rotate local input into world space using the camera heading.
|
|
92
|
+
const sin = Math.sin(yaw);
|
|
93
|
+
const cos = Math.cos(yaw);
|
|
94
|
+
const wishX = axis.x * cos + axis.z * sin;
|
|
95
|
+
const wishZ = -axis.x * sin + axis.z * cos;
|
|
96
|
+
|
|
97
|
+
const speed = this.currentSpeed();
|
|
98
|
+
const targetX = wishX * speed;
|
|
99
|
+
const targetZ = wishZ * speed;
|
|
100
|
+
|
|
101
|
+
if (this.flying) {
|
|
102
|
+
this.stepFlying(dt, targetX, targetZ, controlsEnabled);
|
|
103
|
+
} else {
|
|
104
|
+
this.stepGrounded(dt, targetX, targetZ, controlsEnabled);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.stepDash(dt, controlsEnabled, wishX, wishZ, yaw);
|
|
108
|
+
this.applyMotion(dt);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private updateModes(controlsEnabled: boolean): void {
|
|
112
|
+
if (this.abilities.flying && controlsEnabled && this.input.consume('fly')) {
|
|
113
|
+
this.flying = !this.flying;
|
|
114
|
+
if (this.flying) {
|
|
115
|
+
this.velocity.y = 0;
|
|
116
|
+
this.onGround = false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const wantsCrouch = this.abilities.crouching && controlsEnabled && this.input.isDown('crouch');
|
|
121
|
+
this.crouching = wantsCrouch && !this.flying;
|
|
122
|
+
const targetHeight = this.crouching ? this.baseHeight * 0.55 : this.baseHeight;
|
|
123
|
+
// Smooth so the camera does not snap between eye heights.
|
|
124
|
+
this.currentHeight += (targetHeight - this.currentHeight) * 0.35;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private currentSpeed(): number {
|
|
128
|
+
const tuning = this.tuning;
|
|
129
|
+
if (this.flying) return tuning.flySpeed;
|
|
130
|
+
if (this.crouching) return tuning.crouchSpeed;
|
|
131
|
+
return this.input.isDown('sprint') ? tuning.sprintSpeed : tuning.walkSpeed;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private stepGrounded(dt: number, targetX: number, targetZ: number, controlsEnabled: boolean): void {
|
|
135
|
+
const tuning = this.tuning;
|
|
136
|
+
const accel = this.onGround ? tuning.groundAccel : tuning.airAccel;
|
|
137
|
+
const blend = 1 - Math.exp(-accel * dt);
|
|
138
|
+
this.velocity.x += (targetX - this.velocity.x) * blend;
|
|
139
|
+
this.velocity.z += (targetZ - this.velocity.z) * blend;
|
|
140
|
+
|
|
141
|
+
this.velocity.y -= tuning.gravity * dt;
|
|
142
|
+
if (this.velocity.y < -tuning.maxFallSpeed) this.velocity.y = -tuning.maxFallSpeed;
|
|
143
|
+
|
|
144
|
+
if (controlsEnabled && this.input.consume('jump')) {
|
|
145
|
+
const maxJumps = this.abilities.doubleJump ? 2 : 1;
|
|
146
|
+
if (this.onGround) {
|
|
147
|
+
this.velocity.y = tuning.jumpSpeed;
|
|
148
|
+
this.jumpsUsed = 1;
|
|
149
|
+
this.onGround = false;
|
|
150
|
+
} else if (this.jumpsUsed < maxJumps) {
|
|
151
|
+
this.velocity.y = tuning.jumpSpeed * 0.9;
|
|
152
|
+
this.jumpsUsed++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private stepFlying(dt: number, targetX: number, targetZ: number, controlsEnabled: boolean): void {
|
|
158
|
+
const blend = 1 - Math.exp(-this.tuning.groundAccel * dt);
|
|
159
|
+
this.velocity.x += (targetX - this.velocity.x) * blend;
|
|
160
|
+
this.velocity.z += (targetZ - this.velocity.z) * blend;
|
|
161
|
+
|
|
162
|
+
const up = controlsEnabled && this.input.isDown('jump') ? 1 : 0;
|
|
163
|
+
const down = controlsEnabled && this.abilities.crouching && this.input.isDown('crouch') ? 1 : 0;
|
|
164
|
+
const targetY = (up - down) * this.tuning.flySpeed;
|
|
165
|
+
this.velocity.y += (targetY - this.velocity.y) * blend;
|
|
166
|
+
this.onGround = false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private stepDash(
|
|
170
|
+
dt: number,
|
|
171
|
+
controlsEnabled: boolean,
|
|
172
|
+
wishX: number,
|
|
173
|
+
wishZ: number,
|
|
174
|
+
yaw: number,
|
|
175
|
+
): void {
|
|
176
|
+
if (!this.abilities.dash) return;
|
|
177
|
+
this.dashCooldown = Math.max(0, this.dashCooldown - dt);
|
|
178
|
+
|
|
179
|
+
if (controlsEnabled && this.input.consume('dash') && this.dashCooldown === 0) {
|
|
180
|
+
// Dash along the input direction, or straight ahead when standing still.
|
|
181
|
+
if (wishX === 0 && wishZ === 0) {
|
|
182
|
+
this.dashDirection.set(-Math.sin(yaw), 0, -Math.cos(yaw));
|
|
183
|
+
} else {
|
|
184
|
+
this.dashDirection.set(wishX, 0, wishZ).normalize();
|
|
185
|
+
}
|
|
186
|
+
this.dashTime = 0.18;
|
|
187
|
+
this.dashCooldown = this.tuning.dashCooldown;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (this.dashTime > 0) {
|
|
191
|
+
this.dashTime = Math.max(0, this.dashTime - dt);
|
|
192
|
+
this.velocity.x = this.dashDirection.x * this.tuning.dashSpeed;
|
|
193
|
+
this.velocity.z = this.dashDirection.z * this.tuning.dashSpeed;
|
|
194
|
+
if (!this.flying && this.velocity.y < 0) this.velocity.y = 0;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private applyMotion(dt: number): void {
|
|
199
|
+
const horizontal = this.scratch.set(this.velocity.x * dt, 0, this.velocity.z * dt);
|
|
200
|
+
const resolved = this.collision.resolveHorizontal(
|
|
201
|
+
this.position,
|
|
202
|
+
horizontal,
|
|
203
|
+
this.radius,
|
|
204
|
+
this.currentHeight,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// Feed the resolved slide back into velocity so we do not keep pushing
|
|
208
|
+
// into a wall and accelerating along it.
|
|
209
|
+
if (dt > 0) {
|
|
210
|
+
this.velocity.x = resolved.x / dt;
|
|
211
|
+
this.velocity.z = resolved.z / dt;
|
|
212
|
+
}
|
|
213
|
+
this.position.x += resolved.x;
|
|
214
|
+
this.position.z += resolved.z;
|
|
215
|
+
|
|
216
|
+
const previousY = this.position.y;
|
|
217
|
+
this.position.y += this.velocity.y * dt;
|
|
218
|
+
|
|
219
|
+
// Sample from where the feet *were*, and look down far enough to cover the
|
|
220
|
+
// distance just fallen. Sampling from the new position would miss the floor
|
|
221
|
+
// on the frame the player crosses it and drop them through the world.
|
|
222
|
+
const fallDistance = Math.max(0, previousY - this.position.y);
|
|
223
|
+
const stepHeight = this.onGround ? 0.6 : 0.05;
|
|
224
|
+
this.groundProbe.set(this.position.x, previousY, this.position.z);
|
|
225
|
+
const ground = this.collision.sampleGround(
|
|
226
|
+
this.groundProbe,
|
|
227
|
+
stepHeight,
|
|
228
|
+
fallDistance + 4,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
if (this.flying) {
|
|
232
|
+
if (this.position.y < ground.y) {
|
|
233
|
+
this.position.y = ground.y;
|
|
234
|
+
this.velocity.y = Math.max(0, this.velocity.y);
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (this.position.y <= ground.y + 1e-3 && this.velocity.y <= 0) {
|
|
240
|
+
this.position.y = ground.y;
|
|
241
|
+
this.velocity.y = 0;
|
|
242
|
+
this.onGround = true;
|
|
243
|
+
this.jumpsUsed = 0;
|
|
244
|
+
} else {
|
|
245
|
+
this.onGround = false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { NetworkAdapter, PlayerState } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Multiplayer is intentionally NOT implemented. This file exists to pin down
|
|
5
|
+
* the seam so the runtime stays easy to network later:
|
|
6
|
+
*
|
|
7
|
+
* - the local player is simulated from serializable `PlayerState`, so a peer's
|
|
8
|
+
* state can drive an identical avatar with no movement-code changes;
|
|
9
|
+
* - the runtime exposes `update(dt)` separately from rendering, so a fixed
|
|
10
|
+
* network tick can drive it;
|
|
11
|
+
* - nothing in movement/camera/portals reaches for a global connection.
|
|
12
|
+
*
|
|
13
|
+
* Expected eventual topologies, in order of how much WorldMesh has to host:
|
|
14
|
+
* 1. peer-to-peer WebRTC, worlds exchange state directly;
|
|
15
|
+
* 2. a multiplayer server run by the world's own creator;
|
|
16
|
+
* 3. optional WorldMesh signaling/discovery only (no game traffic);
|
|
17
|
+
* 4. an optional hosted fallback, last resort.
|
|
18
|
+
*/
|
|
19
|
+
export type { NetworkAdapter, PlayerState };
|
|
20
|
+
|
|
21
|
+
/** Round trip a player state over the wire. Kept tiny on purpose. */
|
|
22
|
+
export function serializePlayerState(state: PlayerState): string {
|
|
23
|
+
return JSON.stringify(state);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function deserializePlayerState(payload: string): PlayerState {
|
|
27
|
+
return JSON.parse(payload) as PlayerState;
|
|
28
|
+
}
|