@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.
Files changed (222) hide show
  1. package/CHANGELOG.md +167 -2
  2. package/dist/ai/crowd.d.ts +36 -0
  3. package/dist/ai/crowd.js +213 -0
  4. package/dist/ai/jobBoard.d.ts +49 -0
  5. package/dist/ai/jobBoard.js +155 -0
  6. package/dist/ai/spawnDirector.d.ts +56 -0
  7. package/dist/ai/spawnDirector.js +150 -0
  8. package/dist/ai/threat.d.ts +25 -0
  9. package/dist/ai/threat.js +67 -0
  10. package/dist/audio/audioFalloff.d.ts +38 -0
  11. package/dist/audio/audioFalloff.js +32 -0
  12. package/dist/board/laneBoard.d.ts +63 -0
  13. package/dist/board/laneBoard.js +114 -0
  14. package/dist/board/timelineBoard.d.ts +36 -0
  15. package/dist/board/timelineBoard.js +70 -0
  16. package/dist/cards/cardPile.d.ts +63 -0
  17. package/dist/cards/cardPile.js +166 -0
  18. package/dist/cards/modifierPipeline.d.ts +33 -0
  19. package/dist/cards/modifierPipeline.js +48 -0
  20. package/dist/combat/abilityKit.d.ts +42 -0
  21. package/dist/combat/abilityKit.js +129 -0
  22. package/dist/combat/animationState.d.ts +47 -0
  23. package/dist/combat/animationState.js +101 -0
  24. package/dist/combat/attackTags.d.ts +14 -0
  25. package/dist/combat/attackTags.js +26 -0
  26. package/dist/combat/breakMeters.d.ts +32 -0
  27. package/dist/combat/breakMeters.js +51 -0
  28. package/dist/combat/comboString.d.ts +35 -0
  29. package/dist/combat/comboString.js +59 -0
  30. package/dist/combat/death.d.ts +8 -0
  31. package/dist/combat/death.js +5 -2
  32. package/dist/combat/defensiveWindow.d.ts +34 -0
  33. package/dist/combat/defensiveWindow.js +66 -0
  34. package/dist/combat/downed.d.ts +35 -0
  35. package/dist/combat/downed.js +104 -0
  36. package/dist/combat/effects.d.ts +11 -0
  37. package/dist/combat/effects.js +22 -13
  38. package/dist/combat/hitReaction.d.ts +23 -0
  39. package/dist/combat/hitReaction.js +20 -0
  40. package/dist/combat/resistance.d.ts +13 -0
  41. package/dist/combat/resistance.js +46 -0
  42. package/dist/combat/telegraph.d.ts +27 -0
  43. package/dist/combat/telegraph.js +37 -0
  44. package/dist/crafting/crop.d.ts +59 -0
  45. package/dist/crafting/crop.js +181 -0
  46. package/dist/crafting/production.d.ts +64 -0
  47. package/dist/crafting/production.js +130 -0
  48. package/dist/crafting/recipe.d.ts +62 -0
  49. package/dist/crafting/recipe.js +101 -0
  50. package/dist/economy/sharedWallet.d.ts +28 -0
  51. package/dist/economy/sharedWallet.js +62 -0
  52. package/dist/economy/techTree.d.ts +40 -0
  53. package/dist/economy/techTree.js +66 -0
  54. package/dist/game/cosmetics.d.ts +25 -0
  55. package/dist/game/cosmetics.js +51 -0
  56. package/dist/game/defineGame.d.ts +2 -0
  57. package/dist/game/events.d.ts +62 -0
  58. package/dist/game/lootFilter.d.ts +33 -0
  59. package/dist/game/lootFilter.js +48 -0
  60. package/dist/game/ping.d.ts +67 -0
  61. package/dist/game/ping.js +86 -0
  62. package/dist/game/playableGame.d.ts +281 -4
  63. package/dist/game/race.d.ts +95 -0
  64. package/dist/game/race.js +216 -0
  65. package/dist/game/runDraft.d.ts +34 -0
  66. package/dist/game/runDraft.js +97 -0
  67. package/dist/game/social.d.ts +27 -0
  68. package/dist/game/social.js +16 -0
  69. package/dist/game/worldItem.d.ts +92 -0
  70. package/dist/game/worldItem.js +132 -0
  71. package/dist/input/axisInput.d.ts +43 -0
  72. package/dist/input/axisInput.js +70 -0
  73. package/dist/input/pointer.d.ts +24 -0
  74. package/dist/input/pointer.js +17 -0
  75. package/dist/interaction/contextMenu.d.ts +30 -0
  76. package/dist/interaction/contextMenu.js +22 -0
  77. package/dist/interaction/qte.d.ts +20 -0
  78. package/dist/interaction/qte.js +24 -0
  79. package/dist/interaction/skillCheck.d.ts +20 -0
  80. package/dist/interaction/skillCheck.js +26 -0
  81. package/dist/inventory/shapedGrid.d.ts +56 -0
  82. package/dist/inventory/shapedGrid.js +176 -0
  83. package/dist/inventory/storageTier.d.ts +52 -0
  84. package/dist/inventory/storageTier.js +77 -0
  85. package/dist/item/affix.d.ts +54 -0
  86. package/dist/item/affix.js +133 -0
  87. package/dist/item/durability.d.ts +51 -0
  88. package/dist/item/durability.js +78 -0
  89. package/dist/item/modularItem.d.ts +46 -0
  90. package/dist/item/modularItem.js +75 -0
  91. package/dist/meta/changelog.d.ts +1 -1
  92. package/dist/meta/changelog.js +35 -1
  93. package/dist/movement/dash.d.ts +37 -0
  94. package/dist/movement/dash.js +65 -0
  95. package/dist/multiplayer/combatSnapshot.d.ts +45 -0
  96. package/dist/multiplayer/combatSnapshot.js +89 -0
  97. package/dist/multiplayer/lagCompensation.d.ts +43 -0
  98. package/dist/multiplayer/lagCompensation.js +154 -0
  99. package/dist/multiplayer/matchmaking.d.ts +33 -0
  100. package/dist/multiplayer/matchmaking.js +60 -0
  101. package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
  102. package/dist/multiplayer/simultaneousCommit.js +53 -0
  103. package/dist/nav/navGrid.d.ts +46 -0
  104. package/dist/nav/navGrid.js +248 -0
  105. package/dist/nav/pathFollow.d.ts +29 -0
  106. package/dist/nav/pathFollow.js +67 -0
  107. package/dist/physics/buoyancy.d.ts +49 -0
  108. package/dist/physics/buoyancy.js +112 -0
  109. package/dist/physics/carryable.d.ts +46 -0
  110. package/dist/physics/carryable.js +110 -0
  111. package/dist/physics/damageZones.d.ts +56 -0
  112. package/dist/physics/damageZones.js +107 -0
  113. package/dist/physics/forceVolume.d.ts +48 -0
  114. package/dist/physics/forceVolume.js +119 -0
  115. package/dist/physics/physicsWorld.d.ts +83 -0
  116. package/dist/physics/physicsWorld.js +261 -1
  117. package/dist/physics/ragdoll.d.ts +49 -0
  118. package/dist/physics/ragdoll.js +103 -0
  119. package/dist/physics/spatialGrid.d.ts +41 -0
  120. package/dist/physics/spatialGrid.js +138 -0
  121. package/dist/physics/structure.d.ts +83 -0
  122. package/dist/physics/structure.js +0 -0
  123. package/dist/physics/traversal.d.ts +75 -0
  124. package/dist/physics/traversal.js +138 -0
  125. package/dist/physics/vehicleBody.d.ts +93 -0
  126. package/dist/physics/vehicleBody.js +234 -0
  127. package/dist/runtime/gameContext.d.ts +64 -2
  128. package/dist/runtime/gameContext.js +154 -4
  129. package/dist/runtime/hostPersistence.d.ts +18 -0
  130. package/dist/runtime/hostPersistence.js +4 -0
  131. package/dist/runtime/persistenceScope.d.ts +32 -0
  132. package/dist/runtime/persistenceScope.js +64 -0
  133. package/dist/scene/autoTarget.d.ts +15 -0
  134. package/dist/scene/autoTarget.js +54 -0
  135. package/dist/scene/behaviors.d.ts +13 -1
  136. package/dist/scene/behaviors.js +3 -0
  137. package/dist/scene/captureCheck.d.ts +7 -0
  138. package/dist/scene/captureCheck.js +11 -0
  139. package/dist/scene/entityStore.d.ts +1 -1
  140. package/dist/scene/form.d.ts +40 -0
  141. package/dist/scene/form.js +56 -0
  142. package/dist/scene/mount.d.ts +62 -0
  143. package/dist/scene/mount.js +107 -0
  144. package/dist/scene/possession.d.ts +30 -0
  145. package/dist/scene/possession.js +51 -0
  146. package/dist/scene/roster.d.ts +25 -0
  147. package/dist/scene/roster.js +57 -0
  148. package/dist/scene/selection.d.ts +30 -0
  149. package/dist/scene/selection.js +38 -0
  150. package/dist/scene/stationClaim.d.ts +43 -0
  151. package/dist/scene/stationClaim.js +109 -0
  152. package/dist/sensor/frustumSensor.d.ts +60 -0
  153. package/dist/sensor/frustumSensor.js +104 -0
  154. package/dist/sensor/hiddenStateProbe.d.ts +31 -0
  155. package/dist/sensor/hiddenStateProbe.js +40 -0
  156. package/dist/sensor/recordingBuffer.d.ts +29 -0
  157. package/dist/sensor/recordingBuffer.js +61 -0
  158. package/dist/sensor/revealQuery.d.ts +22 -0
  159. package/dist/sensor/revealQuery.js +36 -0
  160. package/dist/session/contestedChannel.d.ts +36 -0
  161. package/dist/session/contestedChannel.js +100 -0
  162. package/dist/session/extraction.d.ts +54 -0
  163. package/dist/session/extraction.js +108 -0
  164. package/dist/session/ring.d.ts +36 -0
  165. package/dist/session/ring.js +88 -0
  166. package/dist/session/roundState.d.ts +55 -0
  167. package/dist/session/roundState.js +129 -0
  168. package/dist/stats/accumulatorMeter.d.ts +31 -0
  169. package/dist/stats/accumulatorMeter.js +81 -0
  170. package/dist/stats/eventMeter.d.ts +34 -0
  171. package/dist/stats/eventMeter.js +76 -0
  172. package/dist/stats/rollCheck.d.ts +15 -0
  173. package/dist/stats/rollCheck.js +15 -0
  174. package/dist/survival/decayMeter.d.ts +59 -0
  175. package/dist/survival/decayMeter.js +95 -0
  176. package/dist/survival/moodle.d.ts +48 -0
  177. package/dist/survival/moodle.js +91 -0
  178. package/dist/survival/regionHealth.d.ts +72 -0
  179. package/dist/survival/regionHealth.js +129 -0
  180. package/dist/tactics/predictiveQuery.d.ts +34 -0
  181. package/dist/tactics/predictiveQuery.js +44 -0
  182. package/dist/tactics/snapshot.d.ts +18 -0
  183. package/dist/tactics/snapshot.js +65 -0
  184. package/dist/tactics/surface.d.ts +52 -0
  185. package/dist/tactics/surface.js +135 -0
  186. package/dist/tactics/tacticalGrid.d.ts +57 -0
  187. package/dist/tactics/tacticalGrid.js +244 -0
  188. package/dist/time/beatClock.d.ts +40 -0
  189. package/dist/time/beatClock.js +73 -0
  190. package/dist/turn/commit.d.ts +27 -0
  191. package/dist/turn/commit.js +72 -0
  192. package/dist/turn/turnLoop.d.ts +58 -0
  193. package/dist/turn/turnLoop.js +216 -0
  194. package/dist/world/buildPermissions.d.ts +47 -0
  195. package/dist/world/buildPermissions.js +134 -0
  196. package/dist/world/carve.d.ts +105 -0
  197. package/dist/world/carve.js +175 -0
  198. package/dist/world/connectors.d.ts +40 -0
  199. package/dist/world/connectors.js +67 -0
  200. package/dist/world/envField.d.ts +71 -0
  201. package/dist/world/envField.js +96 -0
  202. package/dist/world/fog.d.ts +40 -0
  203. package/dist/world/fog.js +137 -0
  204. package/dist/world/markers.d.ts +52 -0
  205. package/dist/world/markers.js +96 -0
  206. package/dist/world/minimap.d.ts +42 -0
  207. package/dist/world/minimap.js +72 -0
  208. package/dist/world/placedStructureStore.d.ts +39 -0
  209. package/dist/world/placedStructureStore.js +95 -0
  210. package/dist/world/placementController.d.ts +48 -0
  211. package/dist/world/placementController.js +97 -0
  212. package/dist/world/realm.d.ts +61 -0
  213. package/dist/world/realm.js +66 -0
  214. package/dist/world/support.d.ts +27 -0
  215. package/dist/world/support.js +75 -0
  216. package/dist/world/terraform.d.ts +54 -0
  217. package/dist/world/terraform.js +186 -0
  218. package/dist/world/walls.d.ts +61 -0
  219. package/dist/world/walls.js +221 -0
  220. package/dist/world/weather.d.ts +92 -0
  221. package/dist/world/weather.js +169 -0
  222. package/package.json +1 -1
@@ -0,0 +1,33 @@
1
+ export interface LootFilterItem {
2
+ rarity: string;
3
+ baseType: string;
4
+ affixTier?: number;
5
+ }
6
+ export interface LootFilterCondition {
7
+ rarity?: string | readonly string[];
8
+ baseType?: string | readonly string[];
9
+ minAffixTier?: number;
10
+ maxAffixTier?: number;
11
+ }
12
+ export interface LootFilterRule {
13
+ id: string;
14
+ when: LootFilterCondition;
15
+ hide?: boolean;
16
+ color?: string;
17
+ beam?: boolean;
18
+ label?: string;
19
+ }
20
+ export interface LootFilterOverride {
21
+ hidden?: boolean;
22
+ color?: string;
23
+ beam?: boolean;
24
+ label?: string;
25
+ }
26
+ /**
27
+ * First matching rule wins (PoE/Last Epoch block semantics) — later rules
28
+ * never override an earlier match. Returns overrides only; fields the rule
29
+ * doesn't set are left for the caller's baseline (rarity style) to fill in.
30
+ */
31
+ export declare function evaluateLootFilter(rules: readonly LootFilterRule[], item: LootFilterItem): LootFilterOverride;
32
+ /** Validating factory — rule ids must be unique so authoring mistakes fail loudly. */
33
+ export declare function lootFilter(rules: readonly LootFilterRule[]): readonly LootFilterRule[];
@@ -0,0 +1,48 @@
1
+ function matchesOneOf(value, expected) {
2
+ if (expected === undefined)
3
+ return true;
4
+ return typeof expected === "string" ? expected === value : expected.includes(value);
5
+ }
6
+ function matchesCondition(when, item) {
7
+ if (!matchesOneOf(item.rarity, when.rarity))
8
+ return false;
9
+ if (!matchesOneOf(item.baseType, when.baseType))
10
+ return false;
11
+ if (when.minAffixTier !== undefined && (item.affixTier ?? 0) < when.minAffixTier)
12
+ return false;
13
+ if (when.maxAffixTier !== undefined && (item.affixTier ?? 0) > when.maxAffixTier)
14
+ return false;
15
+ return true;
16
+ }
17
+ /**
18
+ * First matching rule wins (PoE/Last Epoch block semantics) — later rules
19
+ * never override an earlier match. Returns overrides only; fields the rule
20
+ * doesn't set are left for the caller's baseline (rarity style) to fill in.
21
+ */
22
+ export function evaluateLootFilter(rules, item) {
23
+ for (const rule of rules) {
24
+ if (!matchesCondition(rule.when, item))
25
+ continue;
26
+ const override = {};
27
+ if (rule.hide !== undefined)
28
+ override.hidden = rule.hide;
29
+ if (rule.color !== undefined)
30
+ override.color = rule.color;
31
+ if (rule.beam !== undefined)
32
+ override.beam = rule.beam;
33
+ if (rule.label !== undefined)
34
+ override.label = rule.label;
35
+ return override;
36
+ }
37
+ return {};
38
+ }
39
+ /** Validating factory — rule ids must be unique so authoring mistakes fail loudly. */
40
+ export function lootFilter(rules) {
41
+ const seen = new Set();
42
+ for (const rule of rules) {
43
+ if (seen.has(rule.id))
44
+ throw new Error(`loot filter rule id "${rule.id}" is duplicated`);
45
+ seen.add(rule.id);
46
+ }
47
+ return rules;
48
+ }
@@ -0,0 +1,67 @@
1
+ import type { PointerHit } from "../input/pointer.js";
2
+ import type { MapMarker, MarkerPosition, MarkerSet } from "../world/markers.js";
3
+ export type PingCategory = string;
4
+ export interface PingCategoryDef {
5
+ id: PingCategory;
6
+ markerKind: string;
7
+ label: string;
8
+ callout: string;
9
+ }
10
+ /** Content-agnostic default ping wheel: enemy / loot / location / danger. */
11
+ export declare const DEFAULT_PING_CATEGORIES: Record<PingCategory, PingCategoryDef>;
12
+ export declare const PING_FEED_ACTION = "party.ping";
13
+ export interface PingClassifyDeps {
14
+ roleOf?(entityId: string): string | null | undefined;
15
+ categoryOf?(objectId: string): PingCategory | null | undefined;
16
+ }
17
+ export interface PingClassifyOptions {
18
+ hostileRoles?: readonly string[];
19
+ enemyCategory?: PingCategory;
20
+ lootCategory?: PingCategory;
21
+ locationCategory?: PingCategory;
22
+ dangerCategory?: PingCategory;
23
+ }
24
+ /**
25
+ * Classify what a pointer/aim ray hit into a ping category. Entity hits resolve
26
+ * by catalog role (hostile → enemy, else location); object hits by an
27
+ * optional catalog category tag; open ground is a location ping.
28
+ */
29
+ export declare function classifyPing(hit: PointerHit, deps?: PingClassifyDeps, options?: PingClassifyOptions): PingCategory;
30
+ export interface PingPayload {
31
+ id: string;
32
+ from: string;
33
+ category: PingCategory;
34
+ position: MarkerPosition;
35
+ entityId: string | null;
36
+ objectId: string | null;
37
+ at: number;
38
+ callout: string;
39
+ /** Party members the broadcast targets; empty when solo or party unconfigured. */
40
+ recipients: readonly string[];
41
+ }
42
+ export interface PingFeedSink {
43
+ push(action: string, entry: unknown): void;
44
+ }
45
+ export interface PingParty {
46
+ membersOf(userId: string): string[];
47
+ }
48
+ export interface PingSystemDeps {
49
+ markers: MarkerSet;
50
+ feed: PingFeedSink;
51
+ party?: PingParty;
52
+ categories?: Record<PingCategory, PingCategoryDef>;
53
+ now?(): number;
54
+ /** Marker lifetime in ms; omit for permanent markers. */
55
+ ttlMs?: number;
56
+ feedAction?: string;
57
+ classify?: PingClassifyDeps;
58
+ classifyOptions?: PingClassifyOptions;
59
+ }
60
+ export interface PingSystem {
61
+ classify(hit: PointerHit): PingCategory;
62
+ buildPayload(from: string, hit: PointerHit, category?: PingCategory): PingPayload;
63
+ broadcast(payload: PingPayload): MapMarker;
64
+ /** Classify → build → broadcast in one call; the ping verb. */
65
+ ping(from: string, hit: PointerHit, category?: PingCategory): PingPayload;
66
+ }
67
+ export declare function createPingSystem(deps: PingSystemDeps): PingSystem;
@@ -0,0 +1,86 @@
1
+ /** Content-agnostic default ping wheel: enemy / loot / location / danger. */
2
+ export const DEFAULT_PING_CATEGORIES = {
3
+ enemy: { id: "enemy", markerKind: "enemy", label: "Enemy", callout: "Enemy spotted" },
4
+ loot: { id: "loot", markerKind: "loot", label: "Loot", callout: "Loot here" },
5
+ location: { id: "location", markerKind: "location", label: "Location", callout: "Going here" },
6
+ danger: { id: "danger", markerKind: "danger", label: "Danger", callout: "Watch out" },
7
+ };
8
+ export const PING_FEED_ACTION = "party.ping";
9
+ const DEFAULT_HOSTILE_ROLES = ["enemy", "hostile"];
10
+ /**
11
+ * Classify what a pointer/aim ray hit into a ping category. Entity hits resolve
12
+ * by catalog role (hostile → enemy, else location); object hits by an
13
+ * optional catalog category tag; open ground is a location ping.
14
+ */
15
+ export function classifyPing(hit, deps = {}, options = {}) {
16
+ const hostileRoles = options.hostileRoles ?? DEFAULT_HOSTILE_ROLES;
17
+ const enemyCategory = options.enemyCategory ?? "enemy";
18
+ const locationCategory = options.locationCategory ?? "location";
19
+ if (hit.entity !== null) {
20
+ const role = deps.roleOf?.(hit.entity);
21
+ if (role !== null && role !== undefined && hostileRoles.includes(role))
22
+ return enemyCategory;
23
+ return locationCategory;
24
+ }
25
+ if (hit.object !== null) {
26
+ const category = deps.categoryOf?.(hit.object);
27
+ if (category !== null && category !== undefined)
28
+ return category;
29
+ return locationCategory;
30
+ }
31
+ return locationCategory;
32
+ }
33
+ function categoryDef(categories, category) {
34
+ return categories[category] ?? { id: category, markerKind: category, label: category, callout: category };
35
+ }
36
+ export function createPingSystem(deps) {
37
+ const now = deps.now ?? Date.now;
38
+ const categories = deps.categories ?? DEFAULT_PING_CATEGORIES;
39
+ const feedAction = deps.feedAction ?? PING_FEED_ACTION;
40
+ let counter = 0;
41
+ const system = {
42
+ classify(hit) {
43
+ return classifyPing(hit, deps.classify, deps.classifyOptions);
44
+ },
45
+ buildPayload(from, hit, category) {
46
+ const resolved = category ?? system.classify(hit);
47
+ const at = now();
48
+ counter += 1;
49
+ const def = categoryDef(categories, resolved);
50
+ const recipients = deps.party?.membersOf(from).filter((member) => member !== from) ?? [];
51
+ return {
52
+ id: `ping-${from}-${counter}`,
53
+ from,
54
+ category: resolved,
55
+ position: hit.point,
56
+ entityId: hit.entity,
57
+ objectId: hit.object,
58
+ at,
59
+ callout: def.callout,
60
+ recipients,
61
+ };
62
+ },
63
+ broadcast(payload) {
64
+ const def = categoryDef(categories, payload.category);
65
+ const markerInput = {
66
+ id: payload.id,
67
+ kind: def.markerKind,
68
+ position: payload.position,
69
+ label: def.label,
70
+ owner: payload.from,
71
+ createdAt: payload.at,
72
+ ...(deps.ttlMs !== undefined ? { expiresAt: payload.at + deps.ttlMs } : {}),
73
+ meta: { ping: true, category: payload.category, callout: payload.callout },
74
+ };
75
+ deps.markers.add(markerInput);
76
+ deps.feed.push(feedAction, payload);
77
+ return deps.markers.get(payload.id);
78
+ },
79
+ ping(from, hit, category) {
80
+ const payload = system.buildPayload(from, hit, category);
81
+ system.broadcast(payload);
82
+ return payload;
83
+ },
84
+ };
85
+ return system;
86
+ }
@@ -1,7 +1,31 @@
1
+ import type { AudioBusDef, SoundDef } from "../audio/audioFalloff.js";
1
2
  import type { PositionedPrompt } from "../interaction/proximityPrompt.js";
2
3
  import type { GameContext, GameContextContent } from "../runtime/gameContext.js";
3
4
  import type { ModelDims } from "../scene/assetCatalog.js";
4
5
  import type { GameDefinition, GameLoop } from "./defineGame.js";
6
+ import type { LootFilterRule } from "./lootFilter.js";
7
+ import type { RarityStyle } from "./worldItem.js";
8
+ export interface PointerConfig {
9
+ /**
10
+ * Left-click on open ground runs this command with `{ point, entity, object }`
11
+ * (click-to-move / ground-target). Suppresses the default left-click hotbar fire.
12
+ */
13
+ moveCommand?: string;
14
+ /** Enable left-drag marquee + single-click box-select of entities (RTS unit command). */
15
+ select?: boolean;
16
+ /** Only entities matching this pass are selectable/orderable; default all non-local entities. */
17
+ selectFilter?: (entityId: string) => boolean;
18
+ /** Right-click on ground runs this command with `{ selection, point }` — order the selection. */
19
+ orderCommand?: string;
20
+ /** Right-click on an entity/object opens the target's catalog verb menu (#31). */
21
+ contextMenu?: boolean;
22
+ /** Route the primary ability's aim to the cursor world point instead of camera yaw/pitch (#22). */
23
+ aim?: boolean;
24
+ /** Left-click a `worldItem` within pickup radius grants it to the local player and despawns it (#32). */
25
+ grabWorldItems?: boolean;
26
+ /** Press the bound `ping` action → `worldHit()` → run this command with `{ point, entity, object, normal }` (contextual ping, #94). */
27
+ pingCommand?: string;
28
+ }
5
29
  export interface CameraFollowState {
6
30
  entityId: string;
7
31
  target: {
@@ -28,11 +52,241 @@ export interface FirstPersonCameraConfig {
28
52
  /** Render a simple first-person weapon in view. Default true. */
29
53
  viewmodel?: boolean;
30
54
  }
55
+ /**
56
+ * Which camera rig the shell mounts. Every rig accepts `followEntityId: null`
57
+ * (avatar-less games — city-builders, card games, auto-battlers — still get a
58
+ * camera). Rigs are tuned through their config block below, never by writing
59
+ * camera positions from `onTick`.
60
+ * - `orbit` — third-person chase (the historical default; `perspective: "third"`).
61
+ * - `first` — pointer-lock mouse-look (`perspective: "first"`).
62
+ * - `topDown` — fixed height/pitch/yaw with decoupled follow (ARPG iso, top-down).
63
+ * - `rts` — free-pan / edge-scroll / rotate / zoom, optional follow.
64
+ * - `shoulder` — over-the-shoulder with ADS transition + shoulder swap.
65
+ * - `lockOn` — yaw bound to the player→target vector; move axis becomes strafe.
66
+ * - `chase` — speed-reactive vehicle chase (speed→FOV, spring arm, shake) + cockpit/hood/rear views.
67
+ * - `observer` — detached spectator/photo cam bound to any entity or fixed point; never reads player input.
68
+ */
69
+ export type CameraRigKind = "orbit" | "first" | "topDown" | "rts" | "shoulder" | "lockOn" | "chase" | "observer";
70
+ /** Fixed top-down / isometric rig (#23) — height/pitch/yaw + decoupled follow. */
71
+ export interface TopDownCameraConfig {
72
+ /** Camera height above the follow point. Default 18. */
73
+ height?: number;
74
+ /** Look-down angle in radians (0 = straight down, PI/2 = level). Default ~1.02 (~58°, ARPG iso). */
75
+ pitch?: number;
76
+ /** Fixed azimuth in radians; ~PI/4 reads as isometric, 0 as straight top-down. Default PI/4. */
77
+ yaw?: number;
78
+ /** Horizontal look offset applied to the follow point. */
79
+ targetOffset?: {
80
+ x?: number;
81
+ y?: number;
82
+ z?: number;
83
+ };
84
+ /** Exponential follow smoothing (higher = snappier, decoupled from any orbit drag). Default 8. */
85
+ followSmoothing?: number;
86
+ /** Scroll zoom bounds as a height multiplier. */
87
+ zoom?: {
88
+ min?: number;
89
+ max?: number;
90
+ speed?: number;
91
+ };
92
+ }
93
+ /** Free-pan / edge-scroll RTS rig (#24) — pan/rotate/zoom independent of any avatar. */
94
+ export interface RtsCameraConfig extends TopDownCameraConfig {
95
+ /** WASD / arrow pan speed in world units per second. Default 24. */
96
+ panSpeed?: number;
97
+ /** Edge-scroll when the pointer nears a screen edge; number sets the margin fraction. Default true. */
98
+ edgeScroll?: boolean | {
99
+ margin?: number;
100
+ speed?: number;
101
+ };
102
+ /** Yaw rotate speed (radians/sec) for rotate keys / middle-drag. Default 1.4. */
103
+ rotateSpeed?: number;
104
+ /** World-space clamp for the pan center. */
105
+ bounds?: {
106
+ minX?: number;
107
+ maxX?: number;
108
+ minZ?: number;
109
+ maxZ?: number;
110
+ };
111
+ /** Start centered on this world point when there is no follow target. */
112
+ start?: {
113
+ x?: number;
114
+ z?: number;
115
+ };
116
+ }
117
+ /** Over-the-shoulder combat rig (#25) — offset, ADS, shoulder swap, decoupled reticle. */
118
+ export interface ShoulderCameraConfig {
119
+ /** Lateral shoulder offset (world units, sign flips on swap). Default 0.6. */
120
+ shoulderOffset?: number;
121
+ /** Vertical offset above the follow point. Default 1.6. */
122
+ heightOffset?: number;
123
+ /** Boom distance behind the shoulder. Default 3.2. */
124
+ distance?: number;
125
+ /** Starting shoulder side. Default "right". */
126
+ side?: "left" | "right";
127
+ /** Aim-down-sights framing, blended in while `aim` is held. */
128
+ ads?: {
129
+ distance?: number;
130
+ shoulderOffset?: number;
131
+ fov?: number;
132
+ heightOffset?: number;
133
+ };
134
+ /** ADS blend speed (higher = snappier). Default 10. */
135
+ adsTransitionSpeed?: number;
136
+ /** Screen-space reticle offset (fraction of viewport), decoupled from camera center. */
137
+ reticleOffset?: {
138
+ x?: number;
139
+ y?: number;
140
+ };
141
+ /** Mouse-look sensitivity (radians per pixel). Default 0.0025. */
142
+ sensitivity?: number;
143
+ /** Base field of view. Default 55. */
144
+ fov?: number;
145
+ }
146
+ /** Lock-on / strafe rig (#26) — yaw bound to player→target, move axis becomes strafe. */
147
+ export interface LockOnCameraConfig {
148
+ /** Target instance id; when omitted the rig reads the follower's current `getTarget`. */
149
+ targetEntityId?: string;
150
+ /** Boom distance behind the player, away from the target. Default 5. */
151
+ distance?: number;
152
+ /** Camera height above the follow point. Default 2.4. */
153
+ height?: number;
154
+ /** Height of the framed look point above the midpoint. Default 1.2. */
155
+ lookHeight?: number;
156
+ /** 0 frames the player, 1 frames the target, 0.5 the midpoint. Default 0.5. */
157
+ framingBias?: number;
158
+ /** Exponential yaw smoothing toward the player→target vector. Default 9. */
159
+ yawSmoothing?: number;
160
+ }
161
+ /** Fixed interior view for the chase rig (#27). */
162
+ export type ChaseView = "chase" | "cockpit" | "hood" | "rear";
163
+ /** Speed-reactive vehicle chase rig (#27) — speed→FOV, spring arm, procedural shake, interior views. */
164
+ export interface ChaseCameraConfig {
165
+ /** Boom distance behind the vehicle. Default 6. */
166
+ distance?: number;
167
+ /** Camera height above the follow point. Default 2.6. */
168
+ height?: number;
169
+ /** Height of the look point above the follow point. Default 1.2. */
170
+ lookHeight?: number;
171
+ /** Spring-arm damping (higher = stiffer, less lag). Default 6. */
172
+ springDamping?: number;
173
+ /** Speed→FOV curve: FOV lerps from `base` to `max` as speed climbs to `speedForMax`. */
174
+ fov?: {
175
+ base?: number;
176
+ max?: number;
177
+ speedForMax?: number;
178
+ };
179
+ /** Procedural shake amplitude per unit of speed (adds to the trauma channel). Default 0.0. */
180
+ shakePerSpeed?: number;
181
+ /** Which view to mount. Default "chase". */
182
+ view?: ChaseView;
183
+ /** Local offset for cockpit/hood/rear seats (relative to the vehicle, +z forward). */
184
+ seatOffsets?: {
185
+ cockpit?: {
186
+ x?: number;
187
+ y?: number;
188
+ z?: number;
189
+ };
190
+ hood?: {
191
+ x?: number;
192
+ y?: number;
193
+ z?: number;
194
+ };
195
+ rear?: {
196
+ x?: number;
197
+ y?: number;
198
+ z?: number;
199
+ };
200
+ };
201
+ }
202
+ /** Detached spectator/photo cam (#120) — binds to any entity or fixed point, never reads player input. */
203
+ export interface ObserverCameraConfig {
204
+ /** What the observer looks at. Omit to stay on a fixed point at the origin (or `point`, if given). */
205
+ bind?: {
206
+ kind: "entity";
207
+ entityId: string;
208
+ } | {
209
+ kind: "point";
210
+ position: {
211
+ x: number;
212
+ y: number;
213
+ z: number;
214
+ };
215
+ };
216
+ /** Orbit distance from the bound subject. Default 8. */
217
+ distance?: number;
218
+ /** Camera height above the bound subject. Default 3. */
219
+ height?: number;
220
+ /** Height of the look point above the subject. Default 1.2. */
221
+ lookHeight?: number;
222
+ /** Radians/second of automatic orbit around the subject (CCTV-style sweep); 0 holds a fixed angle. Default 0.2. */
223
+ orbitSpeed?: number;
224
+ /** Starting orbit angle in radians. Default 0. */
225
+ startAngle?: number;
226
+ fov?: number;
227
+ }
228
+ /** One stop on a scripted camera path (#29). */
229
+ export interface CameraKeyframe {
230
+ position: {
231
+ x: number;
232
+ y: number;
233
+ z: number;
234
+ };
235
+ lookAt: {
236
+ x: number;
237
+ y: number;
238
+ z: number;
239
+ };
240
+ fov?: number;
241
+ /** Seconds to travel from the previous keyframe to this one. Default 1. */
242
+ duration?: number;
243
+ /** Interpolation into this keyframe. Default "smooth" (smoothstep ease). */
244
+ ease?: "linear" | "smooth";
245
+ }
246
+ /** Scripted keyframe / path player (#29). When set it overrides the active rig. */
247
+ export interface CinematicCameraConfig {
248
+ keyframes: readonly CameraKeyframe[];
249
+ /** Restart from the first keyframe after the last. Default false. */
250
+ loop?: boolean;
251
+ }
252
+ /** Camera-shake / trauma defaults (#28). Any rig reads the trauma channel; game systems feed it via `cameraShake()`. */
253
+ export interface CameraShakeConfig {
254
+ /** Positional shake amplitude at full trauma (world units). Default 0.5. */
255
+ maxOffset?: number;
256
+ /** Rotational shake amplitude at full trauma (radians). Default 0.06. */
257
+ maxRoll?: number;
258
+ /** Trauma decay per second (fraction). Default 1.6. */
259
+ decayPerSecond?: number;
260
+ /** Trauma→shake exponent; 2 = quadratic falloff so small hits are subtle. Default 2. */
261
+ exponent?: number;
262
+ /** Noise frequency (Hz) of the shake oscillation. Default 24. */
263
+ frequency?: number;
264
+ }
31
265
  export interface GameCameraConfig {
32
- /** "third" mounts the orbit camera (default); "first" mounts pointer-lock mouse-look. */
266
+ /** Selects the rig. Overrides `perspective`; leave unset to fall back to `perspective`. */
267
+ rig?: CameraRigKind;
268
+ /** "third" mounts the orbit camera (default); "first" mounts pointer-lock mouse-look. Shorthand for `rig: "orbit" | "first"`. */
33
269
  perspective?: "third" | "first";
34
- /** First-person tuning; only read when perspective is "first". */
270
+ /** First-person tuning; only read when the rig resolves to "first". */
35
271
  firstPerson?: FirstPersonCameraConfig;
272
+ /** Top-down / isometric tuning (#23); read when `rig: "topDown"`. */
273
+ topDown?: TopDownCameraConfig;
274
+ /** Free-pan / edge-scroll RTS tuning (#24); read when `rig: "rts"`. */
275
+ rts?: RtsCameraConfig;
276
+ /** Over-the-shoulder tuning (#25); read when `rig: "shoulder"`. */
277
+ shoulder?: ShoulderCameraConfig;
278
+ /** Lock-on / strafe tuning (#26); read when `rig: "lockOn"`. */
279
+ lockOn?: LockOnCameraConfig;
280
+ /** Vehicle chase tuning (#27); read when `rig: "chase"`. */
281
+ chase?: ChaseCameraConfig;
282
+ /** Detached spectator/photo cam tuning (#120); read when `rig: "observer"`. */
283
+ observer?: ObserverCameraConfig;
284
+ /** Camera-shake / trauma channel defaults (#28); read by every rig. */
285
+ shake?: CameraShakeConfig;
286
+ /** Scripted keyframe path (#29); when set, plays over the active rig. */
287
+ cinematic?: CinematicCameraConfig;
288
+ /** Seconds to cross-fade the camera when the rig changes (#29). 0 hard-cuts. Default 0.6. */
289
+ transitionSeconds?: number;
36
290
  minDistance?: number;
37
291
  maxDistance?: number;
38
292
  targetHeight?: number;
@@ -48,8 +302,8 @@ export interface GameCameraConfig {
48
302
  followLock?: boolean;
49
303
  /** When false, orbit target stays fixed (cinematic / debug). Default true. */
50
304
  followEnabled?: boolean;
51
- /** Instance id to follow; defaults to local player in the dev shell. */
52
- followEntityId?: string;
305
+ /** Instance id to follow, or null for an avatar-less free camera; defaults to local player in the dev shell. */
306
+ followEntityId?: string | null;
53
307
  /** Fired each frame after cameraFollow lock is applied. */
54
308
  onCameraFollow?: (state: CameraFollowState) => void;
55
309
  rotateSpeed?: number;
@@ -68,6 +322,16 @@ export interface EntitySpriteConfig {
68
322
  height: number;
69
323
  y: number;
70
324
  }
325
+ export interface WorldItemRenderConfig {
326
+ /** Baseline rarity→color/beam/label render binding (#32); the game's rarity palette. */
327
+ rarityStyle?: Record<string, RarityStyle>;
328
+ /** Loot-filter rules layered over the rarity baseline (#33) — data the game supplies, first match wins. */
329
+ filter?: readonly LootFilterRule[];
330
+ /** World units within which a ground item is grabbable/highlighted. Default 2. */
331
+ pickupRadius?: number;
332
+ /** Beam height above the item's ground position. Default 2.5. */
333
+ beamHeight?: number;
334
+ }
71
335
  export interface ModelConfig {
72
336
  url: string;
73
337
  scale?: number;
@@ -100,8 +364,21 @@ export interface PlayableGame<TUi = unknown, TWorldOverlay = unknown, TRenderEnt
100
364
  prompts?: (ctx: GameContext) => readonly PositionedPrompt[];
101
365
  /** Camera tuning (perspective, orbit, first-person) for the dev game player shell. */
102
366
  camera?: GameCameraConfig;
367
+ /** Pointer-driven input: click-to-move, box-select, right-click verbs, cursor aim (#22/#30/#31). */
368
+ pointer?: PointerConfig;
103
369
  /** Opt in to world-space health bars floating over non-local entities that carry the stat. */
104
370
  worldHealthBars?: boolean | {
105
371
  statId?: string;
106
372
  };
373
+ /** Sound catalog + mix buses (music/sfx/ambient/…) the shell's Web Audio glue plays from. Catalog-first — no per-game audio wiring. */
374
+ audio?: {
375
+ sounds: Record<string, SoundDef>;
376
+ buses?: Record<string, AudioBusDef>;
377
+ };
378
+ /** Continuous positional emitter keyed by entity kind name: while a matching entity exists, the shell plays and repositions the mapped `audio.sounds` id (looping engine hum, campfire crackle, footstep loop) with listener-distance falloff. */
379
+ entitySounds?: Record<string, string>;
380
+ /** Same as `entitySounds` but keyed by placed-object catalog id (torches, machinery). */
381
+ objectSounds?: Record<string, string>;
382
+ /** Rarity render binding + loot filter for dropped-item ground presentation (#32/#33). */
383
+ worldItem?: WorldItemRenderConfig;
107
384
  }
@@ -0,0 +1,95 @@
1
+ export interface Checkpoint {
2
+ id: string;
3
+ center: readonly [number, number, number];
4
+ half: readonly [number, number, number];
5
+ }
6
+ export interface RaceTrackConfig {
7
+ checkpoints: readonly Checkpoint[];
8
+ laps?: number;
9
+ }
10
+ export interface RaceTrack {
11
+ readonly checkpoints: readonly Checkpoint[];
12
+ readonly laps: number;
13
+ }
14
+ /**
15
+ * A race track is an ordered ring of checkpoint trigger volumes plus a lap count. The final checkpoint
16
+ * is the lap/finish line: a racer completes a lap by passing all checkpoints in order and hitting the
17
+ * last one.
18
+ */
19
+ export declare function raceTrack(config: RaceTrackConfig): RaceTrack;
20
+ export interface RacerProgress {
21
+ racerId: string;
22
+ lap: number;
23
+ nextCheckpoint: number;
24
+ lastCheckpoint: number;
25
+ progress: number;
26
+ position: number;
27
+ finished: boolean;
28
+ finishTime: number | null;
29
+ eliminated: boolean;
30
+ splits: readonly number[];
31
+ }
32
+ export type RaceEvent = {
33
+ type: "checkpoint.hit";
34
+ racerId: string;
35
+ checkpoint: number;
36
+ lap: number;
37
+ time: number;
38
+ } | {
39
+ type: "lap.completed";
40
+ racerId: string;
41
+ lap: number;
42
+ time: number;
43
+ } | {
44
+ type: "position.changed";
45
+ racerId: string;
46
+ position: number;
47
+ previous: number;
48
+ } | {
49
+ type: "race.finished";
50
+ ranking: readonly string[];
51
+ time: number;
52
+ };
53
+ export type RaceWinCondition = (standings: readonly RacerProgress[], track: RaceTrack) => readonly string[] | null;
54
+ /** Race ends when `count` racers have crossed the finish; ranking is the current standings order. */
55
+ export declare function firstPastPost(count?: number): RaceWinCondition;
56
+ /** Round-cut (Fall Guys): the first `k` finishers qualify; race resolves the moment `k` cross. */
57
+ export declare function topK(k: number): RaceWinCondition;
58
+ /** Every non-eliminated racer must finish. */
59
+ export declare function everyoneFinishes(): RaceWinCondition;
60
+ /** Destruction-derby last-man-standing: ends when at most one racer is left un-eliminated. */
61
+ export declare function lastStanding(): RaceWinCondition;
62
+ export interface RaceStateConfig {
63
+ track: RaceTrack;
64
+ win?: RaceWinCondition;
65
+ }
66
+ /**
67
+ * Race state machine (issue #87). Drive it each tick with `update(now, positions)` — `now` is game time
68
+ * (`ctx.time`), `positions` maps each racer to a world point tested against the ordered checkpoint
69
+ * volumes. It emits `checkpoint.hit` / `lap.completed` / `position.changed` / `race.finished`, keeps
70
+ * cumulative split times for PB deltas, resolves a pluggable win condition (first-past-post, round-cut,
71
+ * derby last-standing), and `resetToCheckpoint` hands back a respawn pose at the racer's last checkpoint.
72
+ */
73
+ export declare class RaceState {
74
+ private readonly track;
75
+ private readonly win;
76
+ private readonly racers;
77
+ private readonly order;
78
+ private raceFinished;
79
+ private finalRanking;
80
+ constructor(config: RaceStateConfig);
81
+ addRacer(racerId: string, startTime?: number): void;
82
+ eliminate(racerId: string): void;
83
+ get finished(): boolean;
84
+ get ranking(): readonly string[];
85
+ progressOf(racerId: string): RacerProgress | null;
86
+ private snapshot;
87
+ standings(): readonly RacerProgress[];
88
+ resetToCheckpoint(racerId: string): {
89
+ position: [number, number, number];
90
+ heading: number;
91
+ } | null;
92
+ private compare;
93
+ update(now: number, positions: Record<string, readonly [number, number, number]> | Map<string, readonly [number, number, number]>): readonly RaceEvent[];
94
+ }
95
+ export declare function createRaceState(config: RaceStateConfig): RaceState;