@jgengine/core 0.7.0 → 0.8.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/CHANGELOG.md +47 -0
- package/README.md +2 -0
- package/dist/ai/spawnDirector.d.ts +5 -0
- package/dist/ai/spawnDirector.js +13 -1
- package/dist/anim/easing.d.ts +14 -0
- package/dist/anim/easing.js +55 -0
- package/dist/behaviour/behaviour.d.ts +136 -0
- package/dist/behaviour/behaviour.js +388 -0
- package/dist/cards/cardPile.d.ts +1 -0
- package/dist/cards/cardPile.js +12 -3
- package/dist/combat/abilityKit.d.ts +5 -0
- package/dist/combat/abilityKit.js +10 -0
- package/dist/combat/effects.d.ts +1 -1
- package/dist/combat/effects.js +9 -6
- package/dist/combat/projectiles.d.ts +42 -5
- package/dist/combat/projectiles.js +98 -12
- package/dist/combat/telegraph.d.ts +5 -1
- package/dist/combat/telegraph.js +14 -0
- package/dist/commands/commandRegistry.d.ts +1 -1
- package/dist/commands/commandRegistry.js +2 -1
- package/dist/data/dataSource.d.ts +26 -0
- package/dist/data/dataSource.js +93 -0
- package/dist/data/devProxy.d.ts +11 -0
- package/dist/data/devProxy.js +38 -0
- package/dist/data/fetchJson.d.ts +20 -0
- package/dist/data/fetchJson.js +43 -0
- package/dist/data/jsonDataSource.d.ts +4 -0
- package/dist/data/jsonDataSource.js +6 -0
- package/dist/devtools/devtools.d.ts +157 -0
- package/dist/devtools/devtools.js +484 -0
- package/dist/devtools/transformTunables.d.ts +14 -0
- package/dist/devtools/transformTunables.js +38 -0
- package/dist/game/chat.d.ts +66 -0
- package/dist/game/chat.js +165 -0
- package/dist/game/defineGame.d.ts +8 -2
- package/dist/game/defineGame.js +2 -1
- package/dist/game/events.d.ts +25 -0
- package/dist/game/levelSequence.d.ts +44 -0
- package/dist/game/levelSequence.js +78 -0
- package/dist/game/playableGame.d.ts +237 -6
- package/dist/game/playableGame.js +6 -1
- package/dist/game/progression.d.ts +1 -0
- package/dist/game/progression.js +23 -2
- package/dist/game/race.d.ts +5 -0
- package/dist/game/race.js +20 -0
- package/dist/game/social.d.ts +42 -0
- package/dist/game/social.js +122 -0
- package/dist/game/spawnPoints.d.ts +19 -0
- package/dist/game/spawnPoints.js +23 -0
- package/dist/game/worldItem.d.ts +3 -0
- package/dist/game/worldItem.js +12 -6
- package/dist/input/actionBindings.d.ts +10 -0
- package/dist/input/actionBindings.js +14 -0
- package/dist/input/gestureSurface.d.ts +31 -0
- package/dist/input/gestureSurface.js +120 -0
- package/dist/input/pointer.d.ts +45 -0
- package/dist/input/pointer.js +48 -0
- package/dist/input/touchScheme.d.ts +83 -0
- package/dist/input/touchScheme.js +115 -0
- package/dist/item/affix.d.ts +1 -1
- package/dist/item/affix.js +1 -19
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +37 -1
- package/dist/movement/movementModel.d.ts +36 -3
- package/dist/movement/movementModel.js +81 -5
- package/dist/movement/voxelController.d.ts +61 -0
- package/dist/movement/voxelController.js +229 -0
- package/dist/multiplayer/chatContract.d.ts +42 -0
- package/dist/multiplayer/chatContract.js +57 -0
- package/dist/multiplayer/identity.d.ts +13 -0
- package/dist/multiplayer/identity.js +20 -0
- package/dist/multiplayer/poseSyncGate.d.ts +1 -0
- package/dist/multiplayer/poseSyncGate.js +13 -1
- package/dist/multiplayer/presenceModel.d.ts +3 -0
- package/dist/multiplayer/presenceModel.js +15 -1
- package/dist/multiplayer/voiceContract.d.ts +44 -0
- package/dist/multiplayer/voiceContract.js +108 -0
- package/dist/nav/navConstrain.d.ts +14 -0
- package/dist/nav/navConstrain.js +26 -0
- package/dist/nav/navFromEnvironment.d.ts +11 -0
- package/dist/nav/navFromEnvironment.js +16 -0
- package/dist/nav/navGrid.d.ts +10 -0
- package/dist/nav/navGrid.js +18 -1
- package/dist/nav/pathFollow.d.ts +4 -2
- package/dist/nav/pathFollow.js +5 -3
- package/dist/physics/ballisticSweep.d.ts +18 -0
- package/dist/physics/ballisticSweep.js +49 -0
- package/dist/physics/forceVolume.js +5 -5
- package/dist/physics/physicsWorld.d.ts +41 -2
- package/dist/physics/physicsWorld.js +232 -40
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +5 -0
- package/dist/physics/vehicleBody.d.ts +6 -0
- package/dist/physics/vehicleBody.js +16 -17
- package/dist/puzzle/cellGrid.d.ts +26 -0
- package/dist/puzzle/cellGrid.js +110 -0
- package/dist/puzzle/fallingPiece.d.ts +24 -0
- package/dist/puzzle/fallingPiece.js +51 -0
- package/dist/random/nameGen.d.ts +18 -0
- package/dist/random/nameGen.js +35 -0
- package/dist/random/rng.d.ts +2 -0
- package/dist/random/rng.js +23 -0
- package/dist/runtime/adapter.d.ts +35 -0
- package/dist/runtime/adapter.js +37 -1
- package/dist/runtime/cameraDirector.d.ts +10 -0
- package/dist/runtime/cameraDirector.js +25 -0
- package/dist/runtime/gameContext.d.ts +44 -2
- package/dist/runtime/gameContext.js +100 -4
- package/dist/runtime/hostPersistence.d.ts +21 -0
- package/dist/runtime/hostPersistence.js +50 -0
- package/dist/runtime/inputSnapshot.d.ts +7 -0
- package/dist/runtime/inputSnapshot.js +12 -0
- package/dist/runtime/motionIntents.d.ts +25 -0
- package/dist/runtime/motionIntents.js +25 -0
- package/dist/runtime/transport.d.ts +32 -0
- package/dist/scene/entityStore.d.ts +8 -0
- package/dist/scene/entityStore.js +25 -2
- package/dist/scene/movementSpeed.d.ts +35 -0
- package/dist/scene/movementSpeed.js +20 -0
- package/dist/scene/objectQuery.d.ts +18 -0
- package/dist/scene/objectQuery.js +108 -0
- package/dist/scene/objectStore.d.ts +11 -0
- package/dist/scene/objectStore.js +109 -4
- package/dist/scene/paintLayer.d.ts +15 -0
- package/dist/scene/paintLayer.js +53 -0
- package/dist/scene/spatial.d.ts +21 -0
- package/dist/scene/spatial.js +63 -1
- package/dist/sensor/concealment.d.ts +21 -0
- package/dist/sensor/concealment.js +63 -0
- package/dist/sensor/freezeMonitor.d.ts +17 -0
- package/dist/sensor/freezeMonitor.js +35 -0
- package/dist/session/roles.d.ts +6 -0
- package/dist/session/roles.js +32 -0
- package/dist/session/roundState.d.ts +41 -17
- package/dist/session/roundState.js +67 -38
- package/dist/tactics/fallingGrid.d.ts +62 -0
- package/dist/tactics/fallingGrid.js +158 -0
- package/dist/tactics/tacticalGrid.d.ts +11 -0
- package/dist/tactics/tacticalGrid.js +17 -0
- package/dist/time/simClock.d.ts +25 -1
- package/dist/time/simClock.js +28 -3
- package/dist/turn/intent.d.ts +14 -0
- package/dist/turn/intent.js +29 -0
- package/dist/turn/turnLoop.d.ts +2 -0
- package/dist/turn/turnLoop.js +8 -1
- package/dist/world/environmentSummary.d.ts +53 -0
- package/dist/world/environmentSummary.js +129 -0
- package/dist/world/features.d.ts +87 -6
- package/dist/world/features.js +52 -2
- package/dist/world/gridInstances.d.ts +13 -0
- package/dist/world/gridInstances.js +22 -0
- package/dist/world/placementController.d.ts +22 -1
- package/dist/world/placementController.js +56 -0
- package/dist/world/terrain.d.ts +23 -1
- package/dist/world/terrain.js +64 -1
- package/dist/world/voxelField.d.ts +54 -0
- package/dist/world/voxelField.js +241 -0
- package/llms.txt +4102 -0
- package/package.json +3 -2
|
@@ -62,6 +62,26 @@ export declare function drainPendingLeaderboardIncrements(session: Record<string
|
|
|
62
62
|
increments: LeaderboardIncrement[];
|
|
63
63
|
session: Record<string, unknown>;
|
|
64
64
|
};
|
|
65
|
+
export declare const LEADERBOARD_TOP_LIMIT = 100;
|
|
66
|
+
export type LeaderboardRow = {
|
|
67
|
+
gameId: string;
|
|
68
|
+
stat: string;
|
|
69
|
+
scope: LeaderboardScope;
|
|
70
|
+
serverId?: string;
|
|
71
|
+
userId: string;
|
|
72
|
+
value: number;
|
|
73
|
+
updatedAt: number;
|
|
74
|
+
};
|
|
75
|
+
export declare function leaderboardRowKey(row: Omit<LeaderboardRow, "value" | "updatedAt">): string;
|
|
76
|
+
export declare function applyLeaderboardRows(rows: Map<string, LeaderboardRow>, gameId: string, entries: LeaderboardIncrement[], now: number): void;
|
|
77
|
+
export declare function topLeaderboardRows(rows: Iterable<LeaderboardRow>, args: {
|
|
78
|
+
gameId: string;
|
|
79
|
+
stat: string;
|
|
80
|
+
scope: LeaderboardScope;
|
|
81
|
+
serverId?: string;
|
|
82
|
+
limit?: number;
|
|
83
|
+
}): LeaderboardEntry[];
|
|
84
|
+
export declare function profileLeaderboardStats(rows: Iterable<LeaderboardRow>, gameId: string, userId: string): Record<string, number>;
|
|
65
85
|
export declare const FEED_RING_LIMIT = 20;
|
|
66
86
|
export declare function trimFeedEntries<T>(entries: T[], limit?: number): T[];
|
|
67
87
|
export declare function shouldAutoSave(save: SaveConfig, dirtyAt: number | undefined, lastSavedAt: number | undefined, now: number): boolean;
|
|
@@ -78,6 +98,7 @@ export type ServerListing = {
|
|
|
78
98
|
updatedAt: number;
|
|
79
99
|
};
|
|
80
100
|
export declare function toServerListing(record: GameServerRecord): ServerListing;
|
|
101
|
+
export declare function toOpenServerListings(listings: Iterable<ServerListing>, limit?: number): ServerListing[];
|
|
81
102
|
export type HostPersistence = {
|
|
82
103
|
savePlan?: (plan: ServerPersistPlan) => Promise<void>;
|
|
83
104
|
resetScenario?: (reset: NormalizedScenarioReset) => Promise<void>;
|
|
@@ -20,6 +20,50 @@ export function drainPendingLeaderboardIncrements(session) {
|
|
|
20
20
|
delete rest[LEADERBOARD_PENDING_KEY];
|
|
21
21
|
return { increments: raw.filter(isLeaderboardIncrement), session: rest };
|
|
22
22
|
}
|
|
23
|
+
export const LEADERBOARD_TOP_LIMIT = 100;
|
|
24
|
+
export function leaderboardRowKey(row) {
|
|
25
|
+
return [row.gameId, row.scope, row.stat, row.serverId ?? "", row.userId].join("|");
|
|
26
|
+
}
|
|
27
|
+
export function applyLeaderboardRows(rows, gameId, entries, now) {
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
const key = leaderboardRowKey({ ...entry, gameId });
|
|
30
|
+
const existing = rows.get(key);
|
|
31
|
+
if (existing) {
|
|
32
|
+
rows.set(key, { ...existing, value: existing.value + entry.by, updatedAt: now });
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
rows.set(key, {
|
|
36
|
+
gameId,
|
|
37
|
+
stat: entry.stat,
|
|
38
|
+
scope: entry.scope,
|
|
39
|
+
serverId: entry.serverId,
|
|
40
|
+
userId: entry.userId,
|
|
41
|
+
value: entry.by,
|
|
42
|
+
updatedAt: now,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function topLeaderboardRows(rows, args) {
|
|
48
|
+
const limit = Math.min(Math.max(args.limit ?? LEADERBOARD_TOP_LIMIT, 1), LEADERBOARD_TOP_LIMIT);
|
|
49
|
+
return [...rows]
|
|
50
|
+
.filter((row) => row.gameId === args.gameId &&
|
|
51
|
+
row.stat === args.stat &&
|
|
52
|
+
row.scope === args.scope &&
|
|
53
|
+
(args.serverId === undefined || row.serverId === args.serverId))
|
|
54
|
+
.sort((a, b) => b.value - a.value)
|
|
55
|
+
.slice(0, limit)
|
|
56
|
+
.map((row) => ({ userId: row.userId, value: row.value }));
|
|
57
|
+
}
|
|
58
|
+
export function profileLeaderboardStats(rows, gameId, userId) {
|
|
59
|
+
const profile = {};
|
|
60
|
+
for (const row of rows) {
|
|
61
|
+
if (row.gameId === gameId && row.userId === userId && row.scope === "profile") {
|
|
62
|
+
profile[row.stat] = row.value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return profile;
|
|
66
|
+
}
|
|
23
67
|
export const FEED_RING_LIMIT = 20;
|
|
24
68
|
export function trimFeedEntries(entries, limit = FEED_RING_LIMIT) {
|
|
25
69
|
if (entries.length <= limit)
|
|
@@ -49,6 +93,12 @@ export function toServerListing(record) {
|
|
|
49
93
|
updatedAt: record.updatedAt,
|
|
50
94
|
};
|
|
51
95
|
}
|
|
96
|
+
export function toOpenServerListings(listings, limit = 20) {
|
|
97
|
+
return [...listings]
|
|
98
|
+
.filter((listing) => listing.status === "running")
|
|
99
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
100
|
+
.slice(0, limit);
|
|
101
|
+
}
|
|
52
102
|
export function buildHydratePlayers(server, profiles) {
|
|
53
103
|
const players = {};
|
|
54
104
|
for (const userId of server.memberUserIds) {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface InputSnapshot {
|
|
2
|
+
/** Replaces the held-action set for this frame. Called by the shell before `onTick` each frame; does not bump `ctx.version()` or notify `ctx.subscribe` listeners — per-frame publishes would storm subscribers. */
|
|
3
|
+
publish(held: readonly string[]): void;
|
|
4
|
+
isDown(action: string): boolean;
|
|
5
|
+
held(): readonly string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function createInputSnapshot(): InputSnapshot;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seam for game code to reach the vertical motion the shell's FrameDriver
|
|
3
|
+
* otherwise owns privately (#162.4). Game code calls `impulse`,
|
|
4
|
+
* `setVerticalVelocity`, and/or `setY` from `onTick` or commands; the shell
|
|
5
|
+
* calls `takePending()` once per frame, before integrating gravity, to drain
|
|
6
|
+
* what accumulated. `setY` wins over physics for that frame; impulses add to
|
|
7
|
+
* the vertical velocity the driver is about to integrate; a later
|
|
8
|
+
* `setVerticalVelocity` replaces that velocity outright.
|
|
9
|
+
*
|
|
10
|
+
* Intentionally not `ctx.version()`/`ctx.subscribe` wired: these fire at
|
|
11
|
+
* most once per frame and would otherwise storm subscribers with per-frame
|
|
12
|
+
* churn (same rationale as `InputSnapshot.publish`).
|
|
13
|
+
*/
|
|
14
|
+
export interface MotionIntents {
|
|
15
|
+
impulse(velocityY: number): void;
|
|
16
|
+
setVerticalVelocity(velocityY: number): void;
|
|
17
|
+
setY(y: number): void;
|
|
18
|
+
takePending(): MotionIntentBatch | null;
|
|
19
|
+
}
|
|
20
|
+
export interface MotionIntentBatch {
|
|
21
|
+
impulses: readonly number[];
|
|
22
|
+
verticalVelocity: number | null;
|
|
23
|
+
y: number | null;
|
|
24
|
+
}
|
|
25
|
+
export declare function createMotionIntents(): MotionIntents;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function createMotionIntents() {
|
|
2
|
+
let impulses = [];
|
|
3
|
+
let verticalVelocity = null;
|
|
4
|
+
let y = null;
|
|
5
|
+
return {
|
|
6
|
+
impulse(velocityY) {
|
|
7
|
+
impulses.push(velocityY);
|
|
8
|
+
},
|
|
9
|
+
setVerticalVelocity(velocityY) {
|
|
10
|
+
verticalVelocity = velocityY;
|
|
11
|
+
},
|
|
12
|
+
setY(nextY) {
|
|
13
|
+
y = nextY;
|
|
14
|
+
},
|
|
15
|
+
takePending() {
|
|
16
|
+
if (impulses.length === 0 && verticalVelocity === null && y === null)
|
|
17
|
+
return null;
|
|
18
|
+
const batch = { impulses, verticalVelocity, y };
|
|
19
|
+
impulses = [];
|
|
20
|
+
verticalVelocity = null;
|
|
21
|
+
y = null;
|
|
22
|
+
return batch;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ChatSync } from "../multiplayer/chatContract.js";
|
|
2
|
+
import type { PlayerPose } from "../multiplayer/poseSyncGate.js";
|
|
1
3
|
import type { PresenceTransport } from "../multiplayer/presenceContract.js";
|
|
2
4
|
export type JoinServerResult = {
|
|
3
5
|
serverId: string;
|
|
@@ -58,3 +60,33 @@ export type GameBackend<TPresenceRow = unknown, TPresenceLocation = unknown, TGa
|
|
|
58
60
|
feeds?: GameRuntimeFeeds;
|
|
59
61
|
presence?: PresenceTransport<TPresenceRow, TPresenceLocation, TGameId>;
|
|
60
62
|
};
|
|
63
|
+
export type PresencePoseRow = {
|
|
64
|
+
userId: string;
|
|
65
|
+
position: {
|
|
66
|
+
x: number;
|
|
67
|
+
y: number;
|
|
68
|
+
z: number;
|
|
69
|
+
};
|
|
70
|
+
rotationY: number;
|
|
71
|
+
rotationPitch: number;
|
|
72
|
+
lastSeenAt: number;
|
|
73
|
+
};
|
|
74
|
+
export type PresenceSync = {
|
|
75
|
+
subscribe: (serverId: string, onChange: (rows: PresencePoseRow[]) => void) => FeedUnsubscribe;
|
|
76
|
+
syncPose: (serverId: string, pose: PlayerPose) => void;
|
|
77
|
+
};
|
|
78
|
+
export type LiveGameBackend<TPresenceRow = unknown, TPresenceLocation = unknown, TGameId extends string = string> = GameBackend<TPresenceRow, TPresenceLocation, TGameId> & {
|
|
79
|
+
presenceSync: PresenceSync;
|
|
80
|
+
pushFeedEntry: (args: {
|
|
81
|
+
serverId: string;
|
|
82
|
+
action: string;
|
|
83
|
+
entry: unknown;
|
|
84
|
+
}) => Promise<void>;
|
|
85
|
+
chatSyncFor?: (serverId: string) => ChatSync;
|
|
86
|
+
};
|
|
87
|
+
export type MultiplayerSession = {
|
|
88
|
+
gameId: string;
|
|
89
|
+
userId: string;
|
|
90
|
+
backend: LiveGameBackend;
|
|
91
|
+
feedActions: string[];
|
|
92
|
+
};
|
|
@@ -2,6 +2,7 @@ import type { BehaviorDescriptor } from "./behaviors.js";
|
|
|
2
2
|
export type EntityRole = "player" | "npc" | "prop";
|
|
3
3
|
export interface EntityMovement {
|
|
4
4
|
walkSpeed?: number;
|
|
5
|
+
frozen?: boolean;
|
|
5
6
|
}
|
|
6
7
|
export type EntityPosition = readonly [number, number, number];
|
|
7
8
|
export type SpawnPositionInput = EntityPosition | {
|
|
@@ -25,6 +26,7 @@ export interface SceneEntity<TMeta = undefined> {
|
|
|
25
26
|
}
|
|
26
27
|
/** Ground speed (horizontal magnitude of velocity) in world units per second. Scale to km/h or mph in game code. */
|
|
27
28
|
export declare function groundSpeed(entity: SceneEntity<unknown>): number;
|
|
29
|
+
export declare function movedWhileFrozen(entity: SceneEntity<unknown>, threshold?: number): boolean;
|
|
28
30
|
export interface SpawnOptions<TMeta = undefined> {
|
|
29
31
|
id?: string;
|
|
30
32
|
position?: SpawnPositionInput;
|
|
@@ -44,6 +46,10 @@ export interface EntityPose {
|
|
|
44
46
|
/** Seconds since the previous pose; when > 0 the store derives `velocity` from the position delta. Omit for teleports (velocity unchanged). */
|
|
45
47
|
dt?: number;
|
|
46
48
|
}
|
|
49
|
+
export interface SpawnPose {
|
|
50
|
+
position: EntityPosition;
|
|
51
|
+
rotationY: number;
|
|
52
|
+
}
|
|
47
53
|
export interface EntityStore<TMeta = undefined> {
|
|
48
54
|
spawn(name: string, options?: SpawnOptions<TMeta>): string;
|
|
49
55
|
despawn(id: string): boolean;
|
|
@@ -54,5 +60,7 @@ export interface EntityStore<TMeta = undefined> {
|
|
|
54
60
|
clear(): void;
|
|
55
61
|
subscribe(listener: () => void): () => void;
|
|
56
62
|
snapshot(): readonly SceneEntity<TMeta>[];
|
|
63
|
+
spawnPoseOf(id: string): SpawnPose | null;
|
|
64
|
+
resetToSpawn(id: string): boolean;
|
|
57
65
|
}
|
|
58
66
|
export declare function createEntityStore<TMeta = undefined>(): EntityStore<TMeta>;
|
|
@@ -3,6 +3,13 @@ import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
|
|
|
3
3
|
export function groundSpeed(entity) {
|
|
4
4
|
return Math.hypot(entity.velocity[0], entity.velocity[2]);
|
|
5
5
|
}
|
|
6
|
+
const DEFAULT_FROZEN_MOVE_THRESHOLD = 1e-6;
|
|
7
|
+
export function movedWhileFrozen(entity, threshold = DEFAULT_FROZEN_MOVE_THRESHOLD) {
|
|
8
|
+
if (entity.movement.frozen !== true)
|
|
9
|
+
return false;
|
|
10
|
+
const speed = Math.hypot(entity.velocity[0], entity.velocity[1], entity.velocity[2]);
|
|
11
|
+
return speed > threshold;
|
|
12
|
+
}
|
|
6
13
|
function toEntityPosition(position) {
|
|
7
14
|
if (position === undefined)
|
|
8
15
|
return [0, 0, 0];
|
|
@@ -12,6 +19,7 @@ function toEntityPosition(position) {
|
|
|
12
19
|
}
|
|
13
20
|
export function createEntityStore() {
|
|
14
21
|
const store = createObservableKeyedStore();
|
|
22
|
+
const spawnPoses = new Map();
|
|
15
23
|
let nextCounter = 1;
|
|
16
24
|
function generateId() {
|
|
17
25
|
let id = `entity-${nextCounter}`;
|
|
@@ -28,11 +36,13 @@ export function createEntityStore() {
|
|
|
28
36
|
throw new Error(`Scene entity id "${options.id}" is already spawned.`);
|
|
29
37
|
}
|
|
30
38
|
const id = options.id ?? generateId();
|
|
39
|
+
const position = toEntityPosition(options.position);
|
|
40
|
+
const rotationY = options.rotationY ?? 0;
|
|
31
41
|
store.set(id, {
|
|
32
42
|
id,
|
|
33
43
|
name,
|
|
34
|
-
position
|
|
35
|
-
rotationY
|
|
44
|
+
position,
|
|
45
|
+
rotationY,
|
|
36
46
|
rotationX: options.rotationX ?? 0,
|
|
37
47
|
rotationZ: options.rotationZ ?? 0,
|
|
38
48
|
velocity: [0, 0, 0],
|
|
@@ -41,11 +51,13 @@ export function createEntityStore() {
|
|
|
41
51
|
behaviors: options.behaviors ?? [],
|
|
42
52
|
meta: options.meta,
|
|
43
53
|
});
|
|
54
|
+
spawnPoses.set(id, { position, rotationY });
|
|
44
55
|
return id;
|
|
45
56
|
},
|
|
46
57
|
despawn(id) {
|
|
47
58
|
const existed = store.has(id);
|
|
48
59
|
store.delete(id);
|
|
60
|
+
spawnPoses.delete(id);
|
|
49
61
|
return existed;
|
|
50
62
|
},
|
|
51
63
|
update(id, patch) {
|
|
@@ -94,5 +106,16 @@ export function createEntityStore() {
|
|
|
94
106
|
snapshot() {
|
|
95
107
|
return store.arraySnapshot();
|
|
96
108
|
},
|
|
109
|
+
spawnPoseOf(id) {
|
|
110
|
+
return spawnPoses.get(id) ?? null;
|
|
111
|
+
},
|
|
112
|
+
resetToSpawn(id) {
|
|
113
|
+
const pose = spawnPoses.get(id);
|
|
114
|
+
const current = store.get(id);
|
|
115
|
+
if (pose === undefined || current === undefined)
|
|
116
|
+
return false;
|
|
117
|
+
store.set(id, { ...current, position: pose.position, rotationY: pose.rotationY, velocity: [0, 0, 0] });
|
|
118
|
+
return true;
|
|
119
|
+
},
|
|
97
120
|
};
|
|
98
121
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { StatValue } from "./entityStats.js";
|
|
2
|
+
export interface MovementSpeedModifiers {
|
|
3
|
+
multiplier?: number;
|
|
4
|
+
flatBonus?: number;
|
|
5
|
+
}
|
|
6
|
+
/** `base * (multiplier ?? 1) + (flatBonus ?? 0)`, clamped to never go negative. */
|
|
7
|
+
export declare function deriveWalkSpeed(base: number, modifiers: MovementSpeedModifiers): number;
|
|
8
|
+
export interface MovementSpeedDeps {
|
|
9
|
+
stats: {
|
|
10
|
+
get(instanceId: string, statId: string): StatValue | null;
|
|
11
|
+
};
|
|
12
|
+
entities: {
|
|
13
|
+
get(id: string): {
|
|
14
|
+
movement?: {
|
|
15
|
+
walkSpeed?: number;
|
|
16
|
+
};
|
|
17
|
+
} | null;
|
|
18
|
+
update(id: string, patch: {
|
|
19
|
+
movement: {
|
|
20
|
+
walkSpeed: number;
|
|
21
|
+
};
|
|
22
|
+
}): boolean;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export interface MovementSpeedConfig {
|
|
26
|
+
baseSpeed: number;
|
|
27
|
+
multiplierStat?: string;
|
|
28
|
+
flatBonusStat?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Recomputes `movement.walkSpeed` for an entity from its stats and writes it back via `deps.entities.update`.
|
|
32
|
+
* Either stat id is optional; an omitted or unresolved stat contributes its neutral value (multiplier 1,
|
|
33
|
+
* flat bonus 0) rather than failing. Returns false only when the entity itself doesn't exist.
|
|
34
|
+
*/
|
|
35
|
+
export declare function applyStatDrivenSpeed(deps: MovementSpeedDeps, instanceId: string, config: MovementSpeedConfig): boolean;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** `base * (multiplier ?? 1) + (flatBonus ?? 0)`, clamped to never go negative. */
|
|
2
|
+
export function deriveWalkSpeed(base, modifiers) {
|
|
3
|
+
const multiplier = modifiers.multiplier ?? 1;
|
|
4
|
+
const flatBonus = modifiers.flatBonus ?? 0;
|
|
5
|
+
return Math.max(0, base * multiplier + flatBonus);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Recomputes `movement.walkSpeed` for an entity from its stats and writes it back via `deps.entities.update`.
|
|
9
|
+
* Either stat id is optional; an omitted or unresolved stat contributes its neutral value (multiplier 1,
|
|
10
|
+
* flat bonus 0) rather than failing. Returns false only when the entity itself doesn't exist.
|
|
11
|
+
*/
|
|
12
|
+
export function applyStatDrivenSpeed(deps, instanceId, config) {
|
|
13
|
+
const entity = deps.entities.get(instanceId);
|
|
14
|
+
if (entity === null)
|
|
15
|
+
return false;
|
|
16
|
+
const multiplier = config.multiplierStat !== undefined ? deps.stats.get(instanceId, config.multiplierStat)?.current : undefined;
|
|
17
|
+
const flatBonus = config.flatBonusStat !== undefined ? deps.stats.get(instanceId, config.flatBonusStat)?.current : undefined;
|
|
18
|
+
const walkSpeed = deriveWalkSpeed(config.baseSpeed, { multiplier, flatBonus });
|
|
19
|
+
return deps.entities.update(instanceId, { movement: { ...entity.movement, walkSpeed } });
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EntityPosition } from "./entityStore.js";
|
|
2
|
+
import type { SceneObject } from "./objectStore.js";
|
|
3
|
+
export interface ObjectRaycastInput {
|
|
4
|
+
origin: EntityPosition;
|
|
5
|
+
direction: EntityPosition;
|
|
6
|
+
maxDistance: number;
|
|
7
|
+
halfExtents?: EntityPosition;
|
|
8
|
+
filter?: (object: SceneObject) => boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface ObjectRaycastHit {
|
|
11
|
+
instanceId: string;
|
|
12
|
+
catalogId: string;
|
|
13
|
+
distance: number;
|
|
14
|
+
point: EntityPosition;
|
|
15
|
+
normal: EntityPosition;
|
|
16
|
+
}
|
|
17
|
+
export declare function raycastObjects(objects: readonly SceneObject[], input: ObjectRaycastInput): ObjectRaycastHit | null;
|
|
18
|
+
export declare function raycastObjectsAll(objects: readonly SceneObject[], input: ObjectRaycastInput): ObjectRaycastHit[];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const DEFAULT_HALF_EXTENTS = [0.5, 0.5, 0.5];
|
|
2
|
+
function normalize(direction) {
|
|
3
|
+
const length = Math.hypot(direction[0], direction[1], direction[2]);
|
|
4
|
+
if (length === 0)
|
|
5
|
+
return [0, 0, 0];
|
|
6
|
+
return [direction[0] / length, direction[1] / length, direction[2] / length];
|
|
7
|
+
}
|
|
8
|
+
function intersectAabb(origin, direction, min, max, maxDistance) {
|
|
9
|
+
let tMin = 0;
|
|
10
|
+
let tMax = maxDistance;
|
|
11
|
+
let normalAxis = -1;
|
|
12
|
+
let normalSign = 0;
|
|
13
|
+
for (let axis = 0; axis < 3; axis += 1) {
|
|
14
|
+
const originAxis = origin[axis];
|
|
15
|
+
const dirAxis = direction[axis];
|
|
16
|
+
const minAxis = min[axis];
|
|
17
|
+
const maxAxis = max[axis];
|
|
18
|
+
if (Math.abs(dirAxis) < 1e-12) {
|
|
19
|
+
if (originAxis < minAxis || originAxis > maxAxis)
|
|
20
|
+
return null;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const invD = 1 / dirAxis;
|
|
24
|
+
const t1 = (minAxis - originAxis) * invD;
|
|
25
|
+
const t2 = (maxAxis - originAxis) * invD;
|
|
26
|
+
const minIsNear = t1 <= t2;
|
|
27
|
+
const tNearAxis = minIsNear ? t1 : t2;
|
|
28
|
+
const tFarAxis = minIsNear ? t2 : t1;
|
|
29
|
+
const nearFaceSign = minIsNear ? -1 : 1;
|
|
30
|
+
if (tNearAxis > tMin) {
|
|
31
|
+
tMin = tNearAxis;
|
|
32
|
+
normalAxis = axis;
|
|
33
|
+
normalSign = nearFaceSign;
|
|
34
|
+
}
|
|
35
|
+
tMax = Math.min(tMax, tFarAxis);
|
|
36
|
+
if (tMin > tMax)
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (tMin > maxDistance || tMin < 0)
|
|
40
|
+
return null;
|
|
41
|
+
const normal = normalAxis === 0
|
|
42
|
+
? [normalSign, 0, 0]
|
|
43
|
+
: normalAxis === 1
|
|
44
|
+
? [0, normalSign, 0]
|
|
45
|
+
: normalAxis === 2
|
|
46
|
+
? [0, 0, normalSign]
|
|
47
|
+
: [0, 1, 0];
|
|
48
|
+
return { distance: tMin, normal };
|
|
49
|
+
}
|
|
50
|
+
function hitFor(object, origin, direction, halfExtents, maxDistance) {
|
|
51
|
+
const min = [
|
|
52
|
+
object.position[0] - halfExtents[0],
|
|
53
|
+
object.position[1] - halfExtents[1],
|
|
54
|
+
object.position[2] - halfExtents[2],
|
|
55
|
+
];
|
|
56
|
+
const max = [
|
|
57
|
+
object.position[0] + halfExtents[0],
|
|
58
|
+
object.position[1] + halfExtents[1],
|
|
59
|
+
object.position[2] + halfExtents[2],
|
|
60
|
+
];
|
|
61
|
+
const hit = intersectAabb(origin, direction, min, max, maxDistance);
|
|
62
|
+
if (hit === null)
|
|
63
|
+
return null;
|
|
64
|
+
return {
|
|
65
|
+
instanceId: object.instanceId,
|
|
66
|
+
catalogId: object.catalogId,
|
|
67
|
+
distance: hit.distance,
|
|
68
|
+
point: [
|
|
69
|
+
origin[0] + direction[0] * hit.distance,
|
|
70
|
+
origin[1] + direction[1] * hit.distance,
|
|
71
|
+
origin[2] + direction[2] * hit.distance,
|
|
72
|
+
],
|
|
73
|
+
normal: hit.normal,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function raycastObjects(objects, input) {
|
|
77
|
+
const direction = normalize(input.direction);
|
|
78
|
+
if (direction[0] === 0 && direction[1] === 0 && direction[2] === 0)
|
|
79
|
+
return null;
|
|
80
|
+
const halfExtents = input.halfExtents ?? DEFAULT_HALF_EXTENTS;
|
|
81
|
+
let nearest = null;
|
|
82
|
+
for (const object of objects) {
|
|
83
|
+
if (input.filter !== undefined && !input.filter(object))
|
|
84
|
+
continue;
|
|
85
|
+
const hit = hitFor(object, input.origin, direction, halfExtents, input.maxDistance);
|
|
86
|
+
if (hit === null)
|
|
87
|
+
continue;
|
|
88
|
+
if (nearest === null || hit.distance < nearest.distance)
|
|
89
|
+
nearest = hit;
|
|
90
|
+
}
|
|
91
|
+
return nearest;
|
|
92
|
+
}
|
|
93
|
+
export function raycastObjectsAll(objects, input) {
|
|
94
|
+
const direction = normalize(input.direction);
|
|
95
|
+
if (direction[0] === 0 && direction[1] === 0 && direction[2] === 0)
|
|
96
|
+
return [];
|
|
97
|
+
const halfExtents = input.halfExtents ?? DEFAULT_HALF_EXTENTS;
|
|
98
|
+
const hits = [];
|
|
99
|
+
for (const object of objects) {
|
|
100
|
+
if (input.filter !== undefined && !input.filter(object))
|
|
101
|
+
continue;
|
|
102
|
+
const hit = hitFor(object, input.origin, direction, halfExtents, input.maxDistance);
|
|
103
|
+
if (hit !== null)
|
|
104
|
+
hits.push(hit);
|
|
105
|
+
}
|
|
106
|
+
hits.sort((a, b) => a.distance - b.distance);
|
|
107
|
+
return hits;
|
|
108
|
+
}
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import type { EntityPosition } from "./entityStore.js";
|
|
2
|
+
export interface ObjectVisual {
|
|
3
|
+
scale?: number | readonly [number, number, number];
|
|
4
|
+
color?: string;
|
|
5
|
+
opacity?: number;
|
|
6
|
+
}
|
|
2
7
|
export interface SceneObject {
|
|
3
8
|
instanceId: string;
|
|
4
9
|
catalogId: string;
|
|
5
10
|
position: EntityPosition;
|
|
6
11
|
rotationY: number;
|
|
7
12
|
parentSpace?: string;
|
|
13
|
+
visual?: ObjectVisual;
|
|
8
14
|
}
|
|
9
15
|
export interface PlaceOptions {
|
|
10
16
|
instanceId?: string;
|
|
11
17
|
parentSpace?: string;
|
|
12
18
|
rotation?: number;
|
|
19
|
+
visual?: ObjectVisual;
|
|
13
20
|
}
|
|
14
21
|
export interface ObjectListFilter {
|
|
15
22
|
parentSpace?: string;
|
|
@@ -19,10 +26,14 @@ export interface ObjectStore {
|
|
|
19
26
|
remove(instanceId: string): boolean;
|
|
20
27
|
move(instanceId: string, x: number, y: number, z: number): boolean;
|
|
21
28
|
rotate(instanceId: string, rotationY: number): boolean;
|
|
29
|
+
setVisual(instanceId: string, visual: ObjectVisual | undefined): boolean;
|
|
22
30
|
get(instanceId: string): SceneObject | null;
|
|
23
31
|
list(filter?: ObjectListFilter): readonly SceneObject[];
|
|
32
|
+
at(x: number, y: number, z: number, tolerance?: number): SceneObject[];
|
|
33
|
+
inBox(min: EntityPosition, max: EntityPosition): readonly SceneObject[];
|
|
24
34
|
clear(): void;
|
|
25
35
|
subscribe(listener: () => void): () => void;
|
|
26
36
|
snapshot(): readonly SceneObject[];
|
|
27
37
|
}
|
|
28
38
|
export declare function createObjectStore(): ObjectStore;
|
|
39
|
+
export declare function objectVisualScale(visual: ObjectVisual | undefined): readonly [number, number, number];
|