@jgengine/core 0.6.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/CHANGELOG.md +167 -2
- package/dist/ai/crowd.d.ts +36 -0
- package/dist/ai/crowd.js +213 -0
- package/dist/ai/jobBoard.d.ts +49 -0
- package/dist/ai/jobBoard.js +155 -0
- package/dist/ai/spawnDirector.d.ts +56 -0
- package/dist/ai/spawnDirector.js +150 -0
- package/dist/ai/threat.d.ts +25 -0
- package/dist/ai/threat.js +67 -0
- package/dist/audio/audioFalloff.d.ts +38 -0
- package/dist/audio/audioFalloff.js +32 -0
- package/dist/board/laneBoard.d.ts +63 -0
- package/dist/board/laneBoard.js +114 -0
- package/dist/board/timelineBoard.d.ts +36 -0
- package/dist/board/timelineBoard.js +70 -0
- package/dist/cards/cardPile.d.ts +63 -0
- package/dist/cards/cardPile.js +166 -0
- package/dist/cards/modifierPipeline.d.ts +33 -0
- package/dist/cards/modifierPipeline.js +48 -0
- package/dist/combat/abilityKit.d.ts +42 -0
- package/dist/combat/abilityKit.js +129 -0
- package/dist/combat/animationState.d.ts +47 -0
- package/dist/combat/animationState.js +101 -0
- package/dist/combat/attackTags.d.ts +14 -0
- package/dist/combat/attackTags.js +26 -0
- package/dist/combat/breakMeters.d.ts +32 -0
- package/dist/combat/breakMeters.js +51 -0
- package/dist/combat/comboString.d.ts +35 -0
- package/dist/combat/comboString.js +59 -0
- package/dist/combat/death.d.ts +8 -0
- package/dist/combat/death.js +5 -2
- package/dist/combat/defensiveWindow.d.ts +34 -0
- package/dist/combat/defensiveWindow.js +66 -0
- package/dist/combat/downed.d.ts +35 -0
- package/dist/combat/downed.js +104 -0
- package/dist/combat/effects.d.ts +11 -0
- package/dist/combat/effects.js +22 -13
- package/dist/combat/hitReaction.d.ts +23 -0
- package/dist/combat/hitReaction.js +20 -0
- package/dist/combat/resistance.d.ts +13 -0
- package/dist/combat/resistance.js +46 -0
- package/dist/combat/telegraph.d.ts +27 -0
- package/dist/combat/telegraph.js +37 -0
- package/dist/crafting/crop.d.ts +59 -0
- package/dist/crafting/crop.js +181 -0
- package/dist/crafting/production.d.ts +64 -0
- package/dist/crafting/production.js +130 -0
- package/dist/crafting/recipe.d.ts +62 -0
- package/dist/crafting/recipe.js +101 -0
- package/dist/economy/sharedWallet.d.ts +28 -0
- package/dist/economy/sharedWallet.js +62 -0
- package/dist/economy/techTree.d.ts +40 -0
- package/dist/economy/techTree.js +66 -0
- package/dist/game/cosmetics.d.ts +25 -0
- package/dist/game/cosmetics.js +51 -0
- package/dist/game/defineGame.d.ts +2 -0
- package/dist/game/events.d.ts +62 -0
- package/dist/game/lootFilter.d.ts +33 -0
- package/dist/game/lootFilter.js +48 -0
- package/dist/game/ping.d.ts +67 -0
- package/dist/game/ping.js +86 -0
- package/dist/game/playableGame.d.ts +281 -4
- package/dist/game/race.d.ts +95 -0
- package/dist/game/race.js +216 -0
- package/dist/game/runDraft.d.ts +34 -0
- package/dist/game/runDraft.js +97 -0
- package/dist/game/social.d.ts +27 -0
- package/dist/game/social.js +16 -0
- package/dist/game/worldItem.d.ts +92 -0
- package/dist/game/worldItem.js +132 -0
- package/dist/input/axisInput.d.ts +43 -0
- package/dist/input/axisInput.js +70 -0
- package/dist/input/pointer.d.ts +24 -0
- package/dist/input/pointer.js +17 -0
- package/dist/interaction/contextMenu.d.ts +30 -0
- package/dist/interaction/contextMenu.js +22 -0
- package/dist/interaction/qte.d.ts +20 -0
- package/dist/interaction/qte.js +24 -0
- package/dist/interaction/skillCheck.d.ts +20 -0
- package/dist/interaction/skillCheck.js +26 -0
- package/dist/inventory/shapedGrid.d.ts +56 -0
- package/dist/inventory/shapedGrid.js +176 -0
- package/dist/inventory/storageTier.d.ts +52 -0
- package/dist/inventory/storageTier.js +77 -0
- package/dist/item/affix.d.ts +54 -0
- package/dist/item/affix.js +133 -0
- package/dist/item/durability.d.ts +51 -0
- package/dist/item/durability.js +78 -0
- package/dist/item/modularItem.d.ts +46 -0
- package/dist/item/modularItem.js +75 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +35 -1
- package/dist/movement/dash.d.ts +37 -0
- package/dist/movement/dash.js +65 -0
- package/dist/multiplayer/combatSnapshot.d.ts +45 -0
- package/dist/multiplayer/combatSnapshot.js +89 -0
- package/dist/multiplayer/lagCompensation.d.ts +43 -0
- package/dist/multiplayer/lagCompensation.js +154 -0
- package/dist/multiplayer/matchmaking.d.ts +33 -0
- package/dist/multiplayer/matchmaking.js +60 -0
- package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
- package/dist/multiplayer/simultaneousCommit.js +53 -0
- package/dist/nav/navGrid.d.ts +46 -0
- package/dist/nav/navGrid.js +248 -0
- package/dist/nav/pathFollow.d.ts +29 -0
- package/dist/nav/pathFollow.js +67 -0
- package/dist/physics/buoyancy.d.ts +49 -0
- package/dist/physics/buoyancy.js +112 -0
- package/dist/physics/carryable.d.ts +46 -0
- package/dist/physics/carryable.js +110 -0
- package/dist/physics/damageZones.d.ts +56 -0
- package/dist/physics/damageZones.js +107 -0
- package/dist/physics/forceVolume.d.ts +48 -0
- package/dist/physics/forceVolume.js +119 -0
- package/dist/physics/physicsWorld.d.ts +83 -0
- package/dist/physics/physicsWorld.js +261 -1
- package/dist/physics/ragdoll.d.ts +49 -0
- package/dist/physics/ragdoll.js +103 -0
- package/dist/physics/spatialGrid.d.ts +41 -0
- package/dist/physics/spatialGrid.js +138 -0
- package/dist/physics/structure.d.ts +83 -0
- package/dist/physics/structure.js +0 -0
- package/dist/physics/traversal.d.ts +75 -0
- package/dist/physics/traversal.js +138 -0
- package/dist/physics/vehicleBody.d.ts +93 -0
- package/dist/physics/vehicleBody.js +234 -0
- package/dist/runtime/gameContext.d.ts +64 -2
- package/dist/runtime/gameContext.js +154 -4
- package/dist/runtime/hostPersistence.d.ts +18 -0
- package/dist/runtime/hostPersistence.js +4 -0
- package/dist/runtime/persistenceScope.d.ts +32 -0
- package/dist/runtime/persistenceScope.js +64 -0
- package/dist/scene/autoTarget.d.ts +15 -0
- package/dist/scene/autoTarget.js +54 -0
- package/dist/scene/behaviors.d.ts +13 -1
- package/dist/scene/behaviors.js +3 -0
- package/dist/scene/captureCheck.d.ts +7 -0
- package/dist/scene/captureCheck.js +11 -0
- package/dist/scene/entityStore.d.ts +1 -1
- package/dist/scene/form.d.ts +40 -0
- package/dist/scene/form.js +56 -0
- package/dist/scene/mount.d.ts +62 -0
- package/dist/scene/mount.js +107 -0
- package/dist/scene/possession.d.ts +30 -0
- package/dist/scene/possession.js +51 -0
- package/dist/scene/roster.d.ts +25 -0
- package/dist/scene/roster.js +57 -0
- package/dist/scene/selection.d.ts +30 -0
- package/dist/scene/selection.js +38 -0
- package/dist/scene/stationClaim.d.ts +43 -0
- package/dist/scene/stationClaim.js +109 -0
- package/dist/sensor/frustumSensor.d.ts +60 -0
- package/dist/sensor/frustumSensor.js +104 -0
- package/dist/sensor/hiddenStateProbe.d.ts +31 -0
- package/dist/sensor/hiddenStateProbe.js +40 -0
- package/dist/sensor/recordingBuffer.d.ts +29 -0
- package/dist/sensor/recordingBuffer.js +61 -0
- package/dist/sensor/revealQuery.d.ts +22 -0
- package/dist/sensor/revealQuery.js +36 -0
- package/dist/session/contestedChannel.d.ts +36 -0
- package/dist/session/contestedChannel.js +100 -0
- package/dist/session/extraction.d.ts +54 -0
- package/dist/session/extraction.js +108 -0
- package/dist/session/ring.d.ts +36 -0
- package/dist/session/ring.js +88 -0
- package/dist/session/roundState.d.ts +55 -0
- package/dist/session/roundState.js +129 -0
- package/dist/stats/accumulatorMeter.d.ts +31 -0
- package/dist/stats/accumulatorMeter.js +81 -0
- package/dist/stats/eventMeter.d.ts +34 -0
- package/dist/stats/eventMeter.js +76 -0
- package/dist/stats/rollCheck.d.ts +15 -0
- package/dist/stats/rollCheck.js +15 -0
- package/dist/survival/decayMeter.d.ts +59 -0
- package/dist/survival/decayMeter.js +95 -0
- package/dist/survival/moodle.d.ts +48 -0
- package/dist/survival/moodle.js +91 -0
- package/dist/survival/regionHealth.d.ts +72 -0
- package/dist/survival/regionHealth.js +129 -0
- package/dist/tactics/predictiveQuery.d.ts +34 -0
- package/dist/tactics/predictiveQuery.js +44 -0
- package/dist/tactics/snapshot.d.ts +18 -0
- package/dist/tactics/snapshot.js +65 -0
- package/dist/tactics/surface.d.ts +52 -0
- package/dist/tactics/surface.js +135 -0
- package/dist/tactics/tacticalGrid.d.ts +57 -0
- package/dist/tactics/tacticalGrid.js +244 -0
- package/dist/time/beatClock.d.ts +40 -0
- package/dist/time/beatClock.js +73 -0
- package/dist/turn/commit.d.ts +27 -0
- package/dist/turn/commit.js +72 -0
- package/dist/turn/turnLoop.d.ts +58 -0
- package/dist/turn/turnLoop.js +216 -0
- package/dist/world/buildPermissions.d.ts +47 -0
- package/dist/world/buildPermissions.js +134 -0
- package/dist/world/carve.d.ts +105 -0
- package/dist/world/carve.js +175 -0
- package/dist/world/connectors.d.ts +40 -0
- package/dist/world/connectors.js +67 -0
- package/dist/world/envField.d.ts +71 -0
- package/dist/world/envField.js +96 -0
- package/dist/world/fog.d.ts +40 -0
- package/dist/world/fog.js +137 -0
- package/dist/world/markers.d.ts +52 -0
- package/dist/world/markers.js +96 -0
- package/dist/world/minimap.d.ts +42 -0
- package/dist/world/minimap.js +72 -0
- package/dist/world/placedStructureStore.d.ts +39 -0
- package/dist/world/placedStructureStore.js +95 -0
- package/dist/world/placementController.d.ts +48 -0
- package/dist/world/placementController.js +97 -0
- package/dist/world/realm.d.ts +61 -0
- package/dist/world/realm.js +66 -0
- package/dist/world/support.d.ts +27 -0
- package/dist/world/support.js +75 -0
- package/dist/world/terraform.d.ts +54 -0
- package/dist/world/terraform.js +186 -0
- package/dist/world/walls.d.ts +61 -0
- package/dist/world/walls.js +221 -0
- package/dist/world/weather.d.ts +92 -0
- package/dist/world/weather.js +169 -0
- package/package.json +1 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
|
|
2
|
+
export const DEFAULT_MARKER_KINDS = {
|
|
3
|
+
player: { id: "player", color: "#4ade80", glyph: "◆", priority: 90 },
|
|
4
|
+
ally: { id: "ally", color: "#38bdf8", glyph: "▲", priority: 70 },
|
|
5
|
+
objective: { id: "objective", color: "#facc15", glyph: "★", priority: 100 },
|
|
6
|
+
location: { id: "location", color: "#e2e8f0", glyph: "⬡", priority: 40 },
|
|
7
|
+
loot: { id: "loot", color: "#c084fc", glyph: "◈", priority: 60 },
|
|
8
|
+
enemy: { id: "enemy", color: "#f87171", glyph: "✖", priority: 80 },
|
|
9
|
+
danger: { id: "danger", color: "#fb923c", glyph: "⚠", priority: 85 },
|
|
10
|
+
ping: { id: "ping", color: "#22d3ee", glyph: "◎", priority: 65 },
|
|
11
|
+
};
|
|
12
|
+
export function markerKindStyle(kind, styles = DEFAULT_MARKER_KINDS) {
|
|
13
|
+
return styles[kind] ?? { id: kind, color: "#e2e8f0", glyph: "•", priority: 30 };
|
|
14
|
+
}
|
|
15
|
+
function distanceXZ(a, b) {
|
|
16
|
+
return Math.hypot(a[0] - b[0], a[2] - b[2]);
|
|
17
|
+
}
|
|
18
|
+
export function createMarkerSet(now = Date.now) {
|
|
19
|
+
const store = createObservableKeyedStore();
|
|
20
|
+
let counter = 0;
|
|
21
|
+
function generateId() {
|
|
22
|
+
let id = `marker-${counter}`;
|
|
23
|
+
while (store.has(id)) {
|
|
24
|
+
counter += 1;
|
|
25
|
+
id = `marker-${counter}`;
|
|
26
|
+
}
|
|
27
|
+
counter += 1;
|
|
28
|
+
return id;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
add(input) {
|
|
32
|
+
const id = input.id ?? generateId();
|
|
33
|
+
const marker = {
|
|
34
|
+
id,
|
|
35
|
+
kind: input.kind,
|
|
36
|
+
position: input.position,
|
|
37
|
+
createdAt: input.createdAt ?? now(),
|
|
38
|
+
};
|
|
39
|
+
if (input.label !== undefined)
|
|
40
|
+
marker.label = input.label;
|
|
41
|
+
if (input.owner !== undefined)
|
|
42
|
+
marker.owner = input.owner;
|
|
43
|
+
if (input.expiresAt !== undefined)
|
|
44
|
+
marker.expiresAt = input.expiresAt;
|
|
45
|
+
if (input.meta !== undefined)
|
|
46
|
+
marker.meta = input.meta;
|
|
47
|
+
store.set(id, marker);
|
|
48
|
+
return id;
|
|
49
|
+
},
|
|
50
|
+
remove(id) {
|
|
51
|
+
const existed = store.has(id);
|
|
52
|
+
store.delete(id);
|
|
53
|
+
return existed;
|
|
54
|
+
},
|
|
55
|
+
get(id) {
|
|
56
|
+
return store.get(id) ?? null;
|
|
57
|
+
},
|
|
58
|
+
list() {
|
|
59
|
+
return store.arraySnapshot();
|
|
60
|
+
},
|
|
61
|
+
query(query) {
|
|
62
|
+
return store.arraySnapshot().filter((marker) => {
|
|
63
|
+
if (query.kind !== undefined && marker.kind !== query.kind)
|
|
64
|
+
return false;
|
|
65
|
+
if (query.owner !== undefined && marker.owner !== query.owner)
|
|
66
|
+
return false;
|
|
67
|
+
if (query.near !== undefined) {
|
|
68
|
+
const radius = query.radius ?? Number.POSITIVE_INFINITY;
|
|
69
|
+
if (distanceXZ(marker.position, query.near) > radius)
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
prune(at) {
|
|
76
|
+
let removed = 0;
|
|
77
|
+
for (const marker of store.arraySnapshot()) {
|
|
78
|
+
if (marker.expiresAt !== undefined && marker.expiresAt <= at) {
|
|
79
|
+
store.delete(marker.id);
|
|
80
|
+
removed += 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return removed;
|
|
84
|
+
},
|
|
85
|
+
clear() {
|
|
86
|
+
for (const marker of store.arraySnapshot())
|
|
87
|
+
store.delete(marker.id);
|
|
88
|
+
},
|
|
89
|
+
subscribe(listener) {
|
|
90
|
+
return store.subscribe(listener);
|
|
91
|
+
},
|
|
92
|
+
snapshot() {
|
|
93
|
+
return store.arraySnapshot();
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type WorldXZ = readonly [number, number];
|
|
2
|
+
/**
|
|
3
|
+
* Compass bearing (radians, 0 = map north = −Z, increasing clockwise toward +X
|
|
4
|
+
* = east) from one world XZ point to another. Feeds both the minimap direction
|
|
5
|
+
* and the compass strip.
|
|
6
|
+
*/
|
|
7
|
+
export declare function compassBearing(from: WorldXZ, to: WorldXZ): number;
|
|
8
|
+
/** Bearing of an entity facing direction given its `rotationY` (yaw) in radians. */
|
|
9
|
+
export declare function headingToBearing(yaw: number): number;
|
|
10
|
+
export type Cardinal = "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW";
|
|
11
|
+
export declare function bearingToCardinal(bearing: number): Cardinal;
|
|
12
|
+
/** Signed offset of `bearing` from `reference`, wrapped into (−π, π]. */
|
|
13
|
+
export declare function relativeBearing(bearing: number, reference: number): number;
|
|
14
|
+
export interface MinimapView {
|
|
15
|
+
/** World XZ at the minimap center (usually the player position). */
|
|
16
|
+
center: WorldXZ;
|
|
17
|
+
/** World-unit distance from center to the minimap edge. */
|
|
18
|
+
worldRadius: number;
|
|
19
|
+
/** Pixel diameter of the minimap. */
|
|
20
|
+
size: number;
|
|
21
|
+
/** Rotate the projection so this facing bearing points up (rotating minimap). Omit for north-up. */
|
|
22
|
+
rotate?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface MinimapPoint {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
/** Radial distance in world units. */
|
|
28
|
+
distance: number;
|
|
29
|
+
/** True when within `worldRadius` of center. */
|
|
30
|
+
inside: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Project a world XZ (or XYZ) point into minimap pixel space. Origin is the
|
|
34
|
+
* top-left of the `size×size` box; north (−Z) maps to −Y (up). Pass
|
|
35
|
+
* `view.rotate` to spin the map under a fixed north-up player arrow.
|
|
36
|
+
*/
|
|
37
|
+
export declare function projectToMinimap(world: WorldXZ | readonly [number, number, number], view: MinimapView): MinimapPoint;
|
|
38
|
+
/** Clamp a projected point to the minimap edge, preserving direction (edge markers). */
|
|
39
|
+
export declare function clampToMinimapEdge(point: MinimapPoint, size: number): {
|
|
40
|
+
x: number;
|
|
41
|
+
y: number;
|
|
42
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const TWO_PI = Math.PI * 2;
|
|
2
|
+
function normalizeAngle(angle) {
|
|
3
|
+
let value = angle % TWO_PI;
|
|
4
|
+
if (value < 0)
|
|
5
|
+
value += TWO_PI;
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Compass bearing (radians, 0 = map north = −Z, increasing clockwise toward +X
|
|
10
|
+
* = east) from one world XZ point to another. Feeds both the minimap direction
|
|
11
|
+
* and the compass strip.
|
|
12
|
+
*/
|
|
13
|
+
export function compassBearing(from, to) {
|
|
14
|
+
return normalizeAngle(Math.atan2(to[0] - from[0], -(to[1] - from[1])));
|
|
15
|
+
}
|
|
16
|
+
/** Bearing of an entity facing direction given its `rotationY` (yaw) in radians. */
|
|
17
|
+
export function headingToBearing(yaw) {
|
|
18
|
+
return normalizeAngle(Math.atan2(Math.sin(yaw), -Math.cos(yaw)));
|
|
19
|
+
}
|
|
20
|
+
const CARDINALS = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
|
|
21
|
+
export function bearingToCardinal(bearing) {
|
|
22
|
+
const index = Math.round(normalizeAngle(bearing) / (Math.PI / 4)) % 8;
|
|
23
|
+
return CARDINALS[index];
|
|
24
|
+
}
|
|
25
|
+
/** Signed offset of `bearing` from `reference`, wrapped into (−π, π]. */
|
|
26
|
+
export function relativeBearing(bearing, reference) {
|
|
27
|
+
let delta = normalizeAngle(bearing) - normalizeAngle(reference);
|
|
28
|
+
if (delta > Math.PI)
|
|
29
|
+
delta -= TWO_PI;
|
|
30
|
+
if (delta <= -Math.PI)
|
|
31
|
+
delta += TWO_PI;
|
|
32
|
+
return delta;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Project a world XZ (or XYZ) point into minimap pixel space. Origin is the
|
|
36
|
+
* top-left of the `size×size` box; north (−Z) maps to −Y (up). Pass
|
|
37
|
+
* `view.rotate` to spin the map under a fixed north-up player arrow.
|
|
38
|
+
*/
|
|
39
|
+
export function projectToMinimap(world, view) {
|
|
40
|
+
const worldX = world[0];
|
|
41
|
+
const worldZ = world.length === 3 ? world[2] : world[1];
|
|
42
|
+
let dx = worldX - view.center[0];
|
|
43
|
+
let dz = worldZ - view.center[1];
|
|
44
|
+
if (view.rotate !== undefined && view.rotate !== 0) {
|
|
45
|
+
const cos = Math.cos(view.rotate);
|
|
46
|
+
const sin = Math.sin(view.rotate);
|
|
47
|
+
const rx = dx * cos - dz * sin;
|
|
48
|
+
const rz = dx * sin + dz * cos;
|
|
49
|
+
dx = rx;
|
|
50
|
+
dz = rz;
|
|
51
|
+
}
|
|
52
|
+
const distance = Math.hypot(dx, dz);
|
|
53
|
+
const half = view.size / 2;
|
|
54
|
+
const scale = view.worldRadius === 0 ? 0 : half / view.worldRadius;
|
|
55
|
+
return {
|
|
56
|
+
x: half + dx * scale,
|
|
57
|
+
y: half + dz * scale,
|
|
58
|
+
distance,
|
|
59
|
+
inside: distance <= view.worldRadius,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Clamp a projected point to the minimap edge, preserving direction (edge markers). */
|
|
63
|
+
export function clampToMinimapEdge(point, size) {
|
|
64
|
+
const half = size / 2;
|
|
65
|
+
const dx = point.x - half;
|
|
66
|
+
const dy = point.y - half;
|
|
67
|
+
const length = Math.hypot(dx, dy);
|
|
68
|
+
if (length <= half || length === 0)
|
|
69
|
+
return { x: point.x, y: point.y };
|
|
70
|
+
const scale = half / length;
|
|
71
|
+
return { x: half + dx * scale, y: half + dy * scale };
|
|
72
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type StructureVec3 = readonly [number, number, number];
|
|
2
|
+
export interface PlacedStructure {
|
|
3
|
+
id: string;
|
|
4
|
+
catalogId: string;
|
|
5
|
+
position: StructureVec3;
|
|
6
|
+
rotationY: number;
|
|
7
|
+
plotId?: string;
|
|
8
|
+
data?: Readonly<Record<string, unknown>>;
|
|
9
|
+
}
|
|
10
|
+
export interface AddStructureInput {
|
|
11
|
+
id?: string;
|
|
12
|
+
catalogId: string;
|
|
13
|
+
position: StructureVec3;
|
|
14
|
+
rotationY?: number;
|
|
15
|
+
plotId?: string;
|
|
16
|
+
data?: Readonly<Record<string, unknown>>;
|
|
17
|
+
}
|
|
18
|
+
export interface StructureFilter {
|
|
19
|
+
plotId?: string;
|
|
20
|
+
catalogId?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface PlacedStructureSnapshot {
|
|
23
|
+
structures: readonly PlacedStructure[];
|
|
24
|
+
}
|
|
25
|
+
export interface PlacedStructureStore {
|
|
26
|
+
add(input: AddStructureInput): PlacedStructure;
|
|
27
|
+
move(id: string, position: StructureVec3): boolean;
|
|
28
|
+
rotate(id: string, rotationY: number): boolean;
|
|
29
|
+
remove(id: string): boolean;
|
|
30
|
+
get(id: string): PlacedStructure | null;
|
|
31
|
+
list(filter?: StructureFilter): readonly PlacedStructure[];
|
|
32
|
+
clear(): void;
|
|
33
|
+
select(id: string | null): boolean;
|
|
34
|
+
selected(): PlacedStructure | null;
|
|
35
|
+
snapshot(): PlacedStructureSnapshot;
|
|
36
|
+
load(snapshot: PlacedStructureSnapshot): void;
|
|
37
|
+
subscribe(listener: () => void): () => void;
|
|
38
|
+
}
|
|
39
|
+
export declare function createPlacedStructureStore(): PlacedStructureStore;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
|
|
2
|
+
export function createPlacedStructureStore() {
|
|
3
|
+
const store = createObservableKeyedStore();
|
|
4
|
+
let counter = 1;
|
|
5
|
+
let selectedId = null;
|
|
6
|
+
function generateId() {
|
|
7
|
+
let id = `structure-${counter}`;
|
|
8
|
+
while (store.has(id)) {
|
|
9
|
+
counter += 1;
|
|
10
|
+
id = `structure-${counter}`;
|
|
11
|
+
}
|
|
12
|
+
counter += 1;
|
|
13
|
+
return id;
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
add(input) {
|
|
17
|
+
if (input.id !== undefined && store.has(input.id)) {
|
|
18
|
+
throw new Error(`Structure id "${input.id}" already exists.`);
|
|
19
|
+
}
|
|
20
|
+
const id = input.id ?? generateId();
|
|
21
|
+
const structure = {
|
|
22
|
+
id,
|
|
23
|
+
catalogId: input.catalogId,
|
|
24
|
+
position: input.position,
|
|
25
|
+
rotationY: input.rotationY ?? 0,
|
|
26
|
+
...(input.plotId === undefined ? {} : { plotId: input.plotId }),
|
|
27
|
+
...(input.data === undefined ? {} : { data: input.data }),
|
|
28
|
+
};
|
|
29
|
+
store.set(id, structure);
|
|
30
|
+
return structure;
|
|
31
|
+
},
|
|
32
|
+
move(id, position) {
|
|
33
|
+
const current = store.get(id);
|
|
34
|
+
if (current === undefined)
|
|
35
|
+
return false;
|
|
36
|
+
store.set(id, { ...current, position });
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
rotate(id, rotationY) {
|
|
40
|
+
const current = store.get(id);
|
|
41
|
+
if (current === undefined)
|
|
42
|
+
return false;
|
|
43
|
+
store.set(id, { ...current, rotationY });
|
|
44
|
+
return true;
|
|
45
|
+
},
|
|
46
|
+
remove(id) {
|
|
47
|
+
const existed = store.has(id);
|
|
48
|
+
store.delete(id);
|
|
49
|
+
if (selectedId === id)
|
|
50
|
+
selectedId = null;
|
|
51
|
+
return existed;
|
|
52
|
+
},
|
|
53
|
+
get(id) {
|
|
54
|
+
return store.get(id) ?? null;
|
|
55
|
+
},
|
|
56
|
+
list(filter) {
|
|
57
|
+
const all = store.arraySnapshot();
|
|
58
|
+
if (filter === undefined)
|
|
59
|
+
return all;
|
|
60
|
+
return all.filter((structure) => (filter.plotId === undefined || structure.plotId === filter.plotId) &&
|
|
61
|
+
(filter.catalogId === undefined || structure.catalogId === filter.catalogId));
|
|
62
|
+
},
|
|
63
|
+
clear() {
|
|
64
|
+
for (const structure of store.arraySnapshot())
|
|
65
|
+
store.delete(structure.id);
|
|
66
|
+
selectedId = null;
|
|
67
|
+
},
|
|
68
|
+
select(id) {
|
|
69
|
+
if (id === null) {
|
|
70
|
+
selectedId = null;
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (!store.has(id))
|
|
74
|
+
return false;
|
|
75
|
+
selectedId = id;
|
|
76
|
+
return true;
|
|
77
|
+
},
|
|
78
|
+
selected() {
|
|
79
|
+
return selectedId === null ? null : store.get(selectedId) ?? null;
|
|
80
|
+
},
|
|
81
|
+
snapshot() {
|
|
82
|
+
return { structures: store.arraySnapshot() };
|
|
83
|
+
},
|
|
84
|
+
load(snapshot) {
|
|
85
|
+
for (const structure of store.arraySnapshot())
|
|
86
|
+
store.delete(structure.id);
|
|
87
|
+
selectedId = null;
|
|
88
|
+
for (const structure of snapshot.structures)
|
|
89
|
+
store.set(structure.id, structure);
|
|
90
|
+
},
|
|
91
|
+
subscribe(listener) {
|
|
92
|
+
return store.subscribe(listener);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type Aabb, type Footprint, type Vec2 } from "./geometry.js";
|
|
2
|
+
import { type PlacementRules } from "./placement.js";
|
|
3
|
+
export type SnapMode = "grid" | "free" | "surface";
|
|
4
|
+
export type PlacementVec3 = readonly [number, number, number];
|
|
5
|
+
export interface PlacementHit {
|
|
6
|
+
point: PlacementVec3;
|
|
7
|
+
normal: PlacementVec3;
|
|
8
|
+
}
|
|
9
|
+
export interface PlacementControllerConfig {
|
|
10
|
+
footprint: Footprint;
|
|
11
|
+
rules?: PlacementRules;
|
|
12
|
+
snapMode?: SnapMode;
|
|
13
|
+
grid?: number;
|
|
14
|
+
quarterTurns?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface PlacementPreview {
|
|
17
|
+
center: Vec2;
|
|
18
|
+
y: number;
|
|
19
|
+
quarterTurns: number;
|
|
20
|
+
footprint: Footprint;
|
|
21
|
+
aabb: Aabb;
|
|
22
|
+
valid: boolean;
|
|
23
|
+
reason?: "out-of-bounds" | "overlap";
|
|
24
|
+
snapMode: SnapMode;
|
|
25
|
+
normal: PlacementVec3;
|
|
26
|
+
}
|
|
27
|
+
export interface PlacementCommit {
|
|
28
|
+
center: Vec2;
|
|
29
|
+
y: number;
|
|
30
|
+
rotationY: number;
|
|
31
|
+
quarterTurns: number;
|
|
32
|
+
footprint: Footprint;
|
|
33
|
+
aabb: Aabb;
|
|
34
|
+
}
|
|
35
|
+
export interface PlacementController {
|
|
36
|
+
hover(hit: PlacementHit): PlacementPreview;
|
|
37
|
+
current(): PlacementPreview | null;
|
|
38
|
+
rotate(steps?: number): PlacementPreview | null;
|
|
39
|
+
setSnapMode(mode: SnapMode): PlacementPreview | null;
|
|
40
|
+
cycleSnapMode(): SnapMode;
|
|
41
|
+
setFootprint(footprint: Footprint): void;
|
|
42
|
+
setRules(rules: PlacementRules): void;
|
|
43
|
+
setGrid(grid: number): void;
|
|
44
|
+
commit(): PlacementCommit | null;
|
|
45
|
+
reset(): void;
|
|
46
|
+
}
|
|
47
|
+
export declare function quarterTurnsToRotationY(quarterTurns: number): number;
|
|
48
|
+
export declare function createPlacementController(config: PlacementControllerConfig): PlacementController;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { footprintAabb, snapToGrid } from "./geometry.js";
|
|
2
|
+
import { validatePlacement } from "./placement.js";
|
|
3
|
+
const SNAP_ORDER = ["grid", "free", "surface"];
|
|
4
|
+
export function quarterTurnsToRotationY(quarterTurns) {
|
|
5
|
+
const turns = ((quarterTurns % 4) + 4) % 4;
|
|
6
|
+
return turns === 0 ? 0 : -turns * (Math.PI / 2);
|
|
7
|
+
}
|
|
8
|
+
export function createPlacementController(config) {
|
|
9
|
+
let footprint = config.footprint;
|
|
10
|
+
let rules = config.rules ?? {};
|
|
11
|
+
let snapMode = config.snapMode ?? "grid";
|
|
12
|
+
let grid = config.grid ?? rules.snap ?? 1;
|
|
13
|
+
let quarterTurns = ((config.quarterTurns ?? 0) % 4 + 4) % 4;
|
|
14
|
+
let lastHit = null;
|
|
15
|
+
let preview = null;
|
|
16
|
+
function snapCenter(raw) {
|
|
17
|
+
if (snapMode === "grid")
|
|
18
|
+
return snapToGrid(raw, grid);
|
|
19
|
+
return raw;
|
|
20
|
+
}
|
|
21
|
+
function compute(hit) {
|
|
22
|
+
const raw = [hit.point[0], hit.point[2]];
|
|
23
|
+
const center = snapCenter(raw);
|
|
24
|
+
const rulesWithoutSnap = snapMode === "grid" ? { ...rules, snap: undefined } : rules;
|
|
25
|
+
const result = validatePlacement({ center, footprint, quarterTurns }, rulesWithoutSnap);
|
|
26
|
+
const aabb = result.status === "ok" ? result.aabb : footprintAabb(center, footprint, quarterTurns);
|
|
27
|
+
const next = {
|
|
28
|
+
center: result.status === "ok" ? result.center : center,
|
|
29
|
+
y: hit.point[1],
|
|
30
|
+
quarterTurns,
|
|
31
|
+
footprint,
|
|
32
|
+
aabb,
|
|
33
|
+
valid: result.status === "ok",
|
|
34
|
+
snapMode,
|
|
35
|
+
normal: hit.normal,
|
|
36
|
+
...(result.status === "rejected" ? { reason: result.reason } : {}),
|
|
37
|
+
};
|
|
38
|
+
preview = next;
|
|
39
|
+
return next;
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
hover(hit) {
|
|
43
|
+
lastHit = hit;
|
|
44
|
+
return compute(hit);
|
|
45
|
+
},
|
|
46
|
+
current() {
|
|
47
|
+
return preview;
|
|
48
|
+
},
|
|
49
|
+
rotate(steps = 1) {
|
|
50
|
+
quarterTurns = ((quarterTurns + steps) % 4 + 4) % 4;
|
|
51
|
+
return lastHit === null ? null : compute(lastHit);
|
|
52
|
+
},
|
|
53
|
+
setSnapMode(mode) {
|
|
54
|
+
snapMode = mode;
|
|
55
|
+
return lastHit === null ? null : compute(lastHit);
|
|
56
|
+
},
|
|
57
|
+
cycleSnapMode() {
|
|
58
|
+
const index = SNAP_ORDER.indexOf(snapMode);
|
|
59
|
+
snapMode = SNAP_ORDER[(index + 1) % SNAP_ORDER.length];
|
|
60
|
+
if (lastHit !== null)
|
|
61
|
+
compute(lastHit);
|
|
62
|
+
return snapMode;
|
|
63
|
+
},
|
|
64
|
+
setFootprint(next) {
|
|
65
|
+
footprint = next;
|
|
66
|
+
if (lastHit !== null)
|
|
67
|
+
compute(lastHit);
|
|
68
|
+
},
|
|
69
|
+
setRules(next) {
|
|
70
|
+
rules = next;
|
|
71
|
+
if (lastHit !== null)
|
|
72
|
+
compute(lastHit);
|
|
73
|
+
},
|
|
74
|
+
setGrid(next) {
|
|
75
|
+
grid = next;
|
|
76
|
+
if (lastHit !== null)
|
|
77
|
+
compute(lastHit);
|
|
78
|
+
},
|
|
79
|
+
commit() {
|
|
80
|
+
if (preview === null || !preview.valid)
|
|
81
|
+
return null;
|
|
82
|
+
return {
|
|
83
|
+
center: preview.center,
|
|
84
|
+
y: preview.y,
|
|
85
|
+
rotationY: quarterTurnsToRotationY(quarterTurns),
|
|
86
|
+
quarterTurns,
|
|
87
|
+
footprint: preview.footprint,
|
|
88
|
+
aabb: preview.aabb,
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
reset() {
|
|
92
|
+
lastHit = null;
|
|
93
|
+
preview = null;
|
|
94
|
+
quarterTurns = ((config.quarterTurns ?? 0) % 4 + 4) % 4;
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type EnvironmentField, type EnvironmentFieldConfig } from "./envField.js";
|
|
2
|
+
import type { WeatherState } from "./weather.js";
|
|
3
|
+
/** The recomposable environment parameters a realm card can override. */
|
|
4
|
+
export interface RealmEnvironmentParams {
|
|
5
|
+
baseTemperature: number;
|
|
6
|
+
nightDrop: number;
|
|
7
|
+
altitudeLapse: number;
|
|
8
|
+
/** Game-seconds per day (a "long night" minor card shortens the day cycle). */
|
|
9
|
+
dayLength: number;
|
|
10
|
+
/** Ambient rain intensity 0..1. */
|
|
11
|
+
rain: number;
|
|
12
|
+
/** Ambient light floor 0..1 (moonlight). */
|
|
13
|
+
ambientFloor: number;
|
|
14
|
+
}
|
|
15
|
+
export declare const DEFAULT_REALM_ENVIRONMENT: RealmEnvironmentParams;
|
|
16
|
+
/** Spawn-table edits a card layers on: replace weights, add/scale, or remove ids. */
|
|
17
|
+
export interface SpawnTableOverride {
|
|
18
|
+
set?: Record<string, number>;
|
|
19
|
+
add?: Record<string, number>;
|
|
20
|
+
scale?: Record<string, number>;
|
|
21
|
+
remove?: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
export interface RealmCard {
|
|
24
|
+
id: string;
|
|
25
|
+
/** A major card sets the biome base; minors layer weather/day-length/aesthetic on top. */
|
|
26
|
+
kind: "major" | "minor";
|
|
27
|
+
/** Environment param overrides (last write wins across the ordered deck). */
|
|
28
|
+
environment?: Partial<RealmEnvironmentParams>;
|
|
29
|
+
/** Weather state overrides (kind/intensity/wind). */
|
|
30
|
+
weather?: Partial<WeatherState>;
|
|
31
|
+
/** Spawn-table edits applied in deck order. */
|
|
32
|
+
spawn?: SpawnTableOverride;
|
|
33
|
+
/** Opaque aesthetic tag (skybox, palette) the renderer maps — engine never inspects it. */
|
|
34
|
+
aesthetic?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface RealmBase {
|
|
37
|
+
environment?: Partial<RealmEnvironmentParams>;
|
|
38
|
+
weather?: WeatherState;
|
|
39
|
+
spawn?: Record<string, number>;
|
|
40
|
+
}
|
|
41
|
+
export interface ComposedRealm {
|
|
42
|
+
params: RealmEnvironmentParams;
|
|
43
|
+
weather: WeatherState;
|
|
44
|
+
spawnTable: Record<string, number>;
|
|
45
|
+
aesthetics: readonly string[];
|
|
46
|
+
cards: readonly RealmCard[];
|
|
47
|
+
/**
|
|
48
|
+
* Build an environment field (#91) from the composed params, wired to the composed
|
|
49
|
+
* weather's rain intensity. Extra config (terrain, occluders, heat sources) merges on top.
|
|
50
|
+
*/
|
|
51
|
+
environmentField(extra?: Omit<EnvironmentFieldConfig, "baseTemperature" | "nightDrop" | "altitudeLapse" | "dayLength" | "rain" | "ambientFloor">): EnvironmentField;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Assemble a played realm instance at runtime from a deck of modifier cards — the
|
|
55
|
+
* Nightingale "realm card" model. A major card is the biome base; minor cards layer
|
|
56
|
+
* weather, day length, and spawn edits. The result recomposes both the environment
|
|
57
|
+
* (into a sampleable field via `environmentField()`) and the spawn table, and it depends
|
|
58
|
+
* on the weather hooks in this group (#92) to turn its `weather` into gameplay modifiers.
|
|
59
|
+
* Cards apply in array order; sort your deck (majors first) before composing.
|
|
60
|
+
*/
|
|
61
|
+
export declare function composeRealm(base: RealmBase, cards: readonly RealmCard[]): ComposedRealm;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createEnvironmentField } from "./envField.js";
|
|
2
|
+
export const DEFAULT_REALM_ENVIRONMENT = {
|
|
3
|
+
baseTemperature: 20,
|
|
4
|
+
nightDrop: 12,
|
|
5
|
+
altitudeLapse: 0,
|
|
6
|
+
dayLength: 24 * 3600,
|
|
7
|
+
rain: 0,
|
|
8
|
+
ambientFloor: 0.05,
|
|
9
|
+
};
|
|
10
|
+
function applySpawn(table, override) {
|
|
11
|
+
if (override === undefined)
|
|
12
|
+
return;
|
|
13
|
+
if (override.set !== undefined)
|
|
14
|
+
for (const [id, weight] of Object.entries(override.set))
|
|
15
|
+
table[id] = weight;
|
|
16
|
+
if (override.add !== undefined)
|
|
17
|
+
for (const [id, weight] of Object.entries(override.add))
|
|
18
|
+
table[id] = (table[id] ?? 0) + weight;
|
|
19
|
+
if (override.scale !== undefined)
|
|
20
|
+
for (const [id, factor] of Object.entries(override.scale))
|
|
21
|
+
table[id] = (table[id] ?? 0) * factor;
|
|
22
|
+
if (override.remove !== undefined)
|
|
23
|
+
for (const id of override.remove)
|
|
24
|
+
delete table[id];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Assemble a played realm instance at runtime from a deck of modifier cards — the
|
|
28
|
+
* Nightingale "realm card" model. A major card is the biome base; minor cards layer
|
|
29
|
+
* weather, day length, and spawn edits. The result recomposes both the environment
|
|
30
|
+
* (into a sampleable field via `environmentField()`) and the spawn table, and it depends
|
|
31
|
+
* on the weather hooks in this group (#92) to turn its `weather` into gameplay modifiers.
|
|
32
|
+
* Cards apply in array order; sort your deck (majors first) before composing.
|
|
33
|
+
*/
|
|
34
|
+
export function composeRealm(base, cards) {
|
|
35
|
+
const params = { ...DEFAULT_REALM_ENVIRONMENT, ...base.environment };
|
|
36
|
+
let weather = base.weather ?? { kind: "clear", intensity: 0 };
|
|
37
|
+
const spawnTable = { ...base.spawn };
|
|
38
|
+
const aesthetics = [];
|
|
39
|
+
for (const card of cards) {
|
|
40
|
+
if (card.environment !== undefined)
|
|
41
|
+
Object.assign(params, card.environment);
|
|
42
|
+
if (card.weather !== undefined)
|
|
43
|
+
weather = { ...weather, ...card.weather };
|
|
44
|
+
applySpawn(spawnTable, card.spawn);
|
|
45
|
+
if (card.aesthetic !== undefined)
|
|
46
|
+
aesthetics.push(card.aesthetic);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
params,
|
|
50
|
+
weather,
|
|
51
|
+
spawnTable,
|
|
52
|
+
aesthetics,
|
|
53
|
+
cards,
|
|
54
|
+
environmentField(extra) {
|
|
55
|
+
return createEnvironmentField({
|
|
56
|
+
...extra,
|
|
57
|
+
baseTemperature: params.baseTemperature,
|
|
58
|
+
nightDrop: params.nightDrop,
|
|
59
|
+
altitudeLapse: params.altitudeLapse,
|
|
60
|
+
dayLength: params.dayLength,
|
|
61
|
+
rain: weather.kind === "clear" ? 0 : params.rain,
|
|
62
|
+
ambientFloor: params.ambientFloor,
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AddBodyOptions } from "../physics/physicsWorld.js";
|
|
2
|
+
import type { ConnectorVec3 } from "./connectors.js";
|
|
3
|
+
export interface SupportPiece {
|
|
4
|
+
id: string;
|
|
5
|
+
grounded?: boolean;
|
|
6
|
+
position?: ConnectorVec3;
|
|
7
|
+
halfExtents?: ConnectorVec3;
|
|
8
|
+
mass?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface SupportLink {
|
|
11
|
+
a: string;
|
|
12
|
+
b: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SupportConfig {
|
|
15
|
+
maxDistance?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface SupportResult {
|
|
18
|
+
supported: readonly string[];
|
|
19
|
+
unsupported: readonly string[];
|
|
20
|
+
distance: Readonly<Record<string, number>>;
|
|
21
|
+
}
|
|
22
|
+
export declare function solveSupport(pieces: readonly SupportPiece[], links: readonly SupportLink[], config?: SupportConfig): SupportResult;
|
|
23
|
+
export interface DebrisOptions {
|
|
24
|
+
scatter?: number;
|
|
25
|
+
seed?: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function toDebrisBodies(pieces: readonly SupportPiece[], collapsedIds: readonly string[], options?: DebrisOptions): AddBodyOptions[];
|