@jgengine/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +11 -0
  3. package/dist/assets/spriteSheet.d.ts +46 -0
  4. package/dist/assets/spriteSheet.js +86 -0
  5. package/dist/combat/death.d.ts +61 -0
  6. package/dist/combat/death.js +61 -0
  7. package/dist/combat/effects.d.ts +61 -0
  8. package/dist/combat/effects.js +108 -0
  9. package/dist/combat/projectiles.d.ts +45 -0
  10. package/dist/combat/projectiles.js +156 -0
  11. package/dist/commands/commandRegistry.d.ts +23 -0
  12. package/dist/commands/commandRegistry.js +26 -0
  13. package/dist/economy/currency.d.ts +26 -0
  14. package/dist/economy/currency.js +28 -0
  15. package/dist/economy/wallet.d.ts +16 -0
  16. package/dist/economy/wallet.js +49 -0
  17. package/dist/game/defineGame.d.ts +41 -0
  18. package/dist/game/defineGame.js +7 -0
  19. package/dist/game/events.d.ts +87 -0
  20. package/dist/game/events.js +24 -0
  21. package/dist/game/feed.d.ts +19 -0
  22. package/dist/game/feed.js +47 -0
  23. package/dist/game/leaderboard.d.ts +46 -0
  24. package/dist/game/leaderboard.js +60 -0
  25. package/dist/game/loadout.d.ts +47 -0
  26. package/dist/game/loadout.js +36 -0
  27. package/dist/game/lootTable.d.ts +27 -0
  28. package/dist/game/lootTable.js +78 -0
  29. package/dist/game/playableGame.d.ts +55 -0
  30. package/dist/game/playableGame.js +1 -0
  31. package/dist/game/quest.d.ts +93 -0
  32. package/dist/game/quest.js +228 -0
  33. package/dist/game/social.d.ts +78 -0
  34. package/dist/game/social.js +227 -0
  35. package/dist/game/trade.d.ts +35 -0
  36. package/dist/game/trade.js +53 -0
  37. package/dist/game/unlocks.d.ts +13 -0
  38. package/dist/game/unlocks.js +34 -0
  39. package/dist/index.d.ts +6 -0
  40. package/dist/index.js +6 -0
  41. package/dist/input/actionBindings.d.ts +41 -0
  42. package/dist/input/actionBindings.js +115 -0
  43. package/dist/input/lookChannel.d.ts +29 -0
  44. package/dist/input/lookChannel.js +44 -0
  45. package/dist/input/touchGestures.d.ts +31 -0
  46. package/dist/input/touchGestures.js +72 -0
  47. package/dist/interaction/proximityPrompt.d.ts +51 -0
  48. package/dist/interaction/proximityPrompt.js +76 -0
  49. package/dist/inventory/inventoryModel.d.ts +57 -0
  50. package/dist/inventory/inventoryModel.js +208 -0
  51. package/dist/item/use.d.ts +25 -0
  52. package/dist/item/use.js +40 -0
  53. package/dist/item/weapon.d.ts +8 -0
  54. package/dist/item/weapon.js +18 -0
  55. package/dist/movement/avatarGait.d.ts +14 -0
  56. package/dist/movement/avatarGait.js +20 -0
  57. package/dist/movement/cameraRig.d.ts +15 -0
  58. package/dist/movement/cameraRig.js +43 -0
  59. package/dist/movement/movementModel.d.ts +96 -0
  60. package/dist/movement/movementModel.js +171 -0
  61. package/dist/movement/poseState.d.ts +23 -0
  62. package/dist/movement/poseState.js +61 -0
  63. package/dist/multiplayer/poseSyncGate.d.ts +18 -0
  64. package/dist/multiplayer/poseSyncGate.js +30 -0
  65. package/dist/multiplayer/presenceContract.d.ts +60 -0
  66. package/dist/multiplayer/presenceContract.js +19 -0
  67. package/dist/multiplayer/presenceModel.d.ts +60 -0
  68. package/dist/multiplayer/presenceModel.js +51 -0
  69. package/dist/runtime/adapter.d.ts +24 -0
  70. package/dist/runtime/adapter.js +12 -0
  71. package/dist/runtime/commandRunner.d.ts +15 -0
  72. package/dist/runtime/commandRunner.js +25 -0
  73. package/dist/runtime/gameContext.d.ts +138 -0
  74. package/dist/runtime/gameContext.js +371 -0
  75. package/dist/runtime/gameRuntime.d.ts +42 -0
  76. package/dist/runtime/gameRuntime.js +95 -0
  77. package/dist/runtime/hostPersistence.d.ts +105 -0
  78. package/dist/runtime/hostPersistence.js +117 -0
  79. package/dist/runtime/save.d.ts +9 -0
  80. package/dist/runtime/save.js +29 -0
  81. package/dist/runtime/snapshot.d.ts +87 -0
  82. package/dist/runtime/snapshot.js +75 -0
  83. package/dist/runtime/transport.d.ts +60 -0
  84. package/dist/runtime/transport.js +1 -0
  85. package/dist/scene/assetCatalog.d.ts +10 -0
  86. package/dist/scene/assetCatalog.js +17 -0
  87. package/dist/scene/assetPreload.d.ts +7 -0
  88. package/dist/scene/assetPreload.js +22 -0
  89. package/dist/scene/behaviors.d.ts +19 -0
  90. package/dist/scene/behaviors.js +18 -0
  91. package/dist/scene/entityStats.d.ts +38 -0
  92. package/dist/scene/entityStats.js +65 -0
  93. package/dist/scene/entityStore.d.ts +52 -0
  94. package/dist/scene/entityStore.js +84 -0
  95. package/dist/scene/objectStore.d.ts +28 -0
  96. package/dist/scene/objectStore.js +69 -0
  97. package/dist/scene/spatial.d.ts +34 -0
  98. package/dist/scene/spatial.js +96 -0
  99. package/dist/scene/targeting.d.ts +20 -0
  100. package/dist/scene/targeting.js +56 -0
  101. package/dist/stats/statModifiers.d.ts +18 -0
  102. package/dist/stats/statModifiers.js +56 -0
  103. package/dist/store/changeSignal.d.ts +7 -0
  104. package/dist/store/changeSignal.js +32 -0
  105. package/dist/store/engineStore.d.ts +22 -0
  106. package/dist/store/engineStore.js +50 -0
  107. package/dist/store/features.d.ts +10 -0
  108. package/dist/store/features.js +23 -0
  109. package/dist/store/observableKeyedStore.d.ts +10 -0
  110. package/dist/store/observableKeyedStore.js +42 -0
  111. package/dist/time/calendarClock.d.ts +17 -0
  112. package/dist/time/calendarClock.js +42 -0
  113. package/dist/time/gameClock.d.ts +9 -0
  114. package/dist/time/gameClock.js +20 -0
  115. package/dist/time/serverTick.d.ts +16 -0
  116. package/dist/time/serverTick.js +18 -0
  117. package/dist/world/features.d.ts +39 -0
  118. package/dist/world/features.js +15 -0
  119. package/package.json +32 -0
@@ -0,0 +1,35 @@
1
+ export interface TradeField {
2
+ buy?: Record<string, number>;
3
+ sell?: Record<string, number>;
4
+ shops?: string[];
5
+ }
6
+ export interface TradeRejection {
7
+ reason: string;
8
+ }
9
+ export interface TradeWallet {
10
+ canAfford(costs: Record<string, number>): string | null;
11
+ charge(costs: Record<string, number>): void;
12
+ grant(gains: Record<string, number>): void;
13
+ }
14
+ export interface TradeInventory {
15
+ put(inventoryId: string, itemId: string, count: number): TradeRejection | null;
16
+ take(inventoryId: string, itemId: string, count: number): TradeRejection | null;
17
+ count(inventoryId: string, itemId: string): number;
18
+ }
19
+ export interface TradeSystemDeps {
20
+ resolveTrade(itemId: string): TradeField | null | undefined;
21
+ wallet: TradeWallet;
22
+ inventory: TradeInventory;
23
+ }
24
+ export interface TradeParty {
25
+ shop: string;
26
+ inventoryId: string;
27
+ }
28
+ export interface TradeSystem {
29
+ canBuy(itemId: string, shopId: string, count?: number): string | null;
30
+ canSell(itemId: string, count?: number): string | null;
31
+ buy(itemId: string, count: number | undefined, party: TradeParty): TradeRejection | null;
32
+ sell(itemId: string, count: number | undefined, party: TradeParty): TradeRejection | null;
33
+ tradableAt(shopId: string, allItemIds: string[]): string[];
34
+ }
35
+ export declare function createTradeSystem(deps: TradeSystemDeps): TradeSystem;
@@ -0,0 +1,53 @@
1
+ function scale(amounts, count) {
2
+ const scaled = {};
3
+ for (const [currency, amount] of Object.entries(amounts))
4
+ scaled[currency] = amount * count;
5
+ return scaled;
6
+ }
7
+ export function createTradeSystem(deps) {
8
+ const { resolveTrade, wallet, inventory } = deps;
9
+ function canBuy(itemId, shopId, count = 1) {
10
+ const trade = resolveTrade(itemId);
11
+ if (!trade?.buy)
12
+ return "not-purchasable";
13
+ if (!trade.shops?.includes(shopId))
14
+ return "not-stocked";
15
+ return wallet.canAfford(scale(trade.buy, count));
16
+ }
17
+ function canSell(itemId, _count = 1) {
18
+ const trade = resolveTrade(itemId);
19
+ if (!trade?.sell)
20
+ return "not-sellable";
21
+ return null;
22
+ }
23
+ return {
24
+ canBuy,
25
+ canSell,
26
+ buy(itemId, count = 1, { shop, inventoryId }) {
27
+ const rejection = canBuy(itemId, shop, count);
28
+ if (rejection)
29
+ return { reason: rejection };
30
+ const costs = scale(resolveTrade(itemId).buy, count);
31
+ wallet.charge(costs);
32
+ const putResult = inventory.put(inventoryId, itemId, count);
33
+ if (putResult) {
34
+ wallet.grant(costs);
35
+ return putResult;
36
+ }
37
+ return null;
38
+ },
39
+ sell(itemId, count = 1, { inventoryId }) {
40
+ const rejection = canSell(itemId, count);
41
+ if (rejection)
42
+ return { reason: rejection };
43
+ const takeResult = inventory.take(inventoryId, itemId, count);
44
+ if (takeResult)
45
+ return takeResult;
46
+ wallet.grant(scale(resolveTrade(itemId).sell, count));
47
+ return null;
48
+ },
49
+ tradableAt(shopId, allItemIds) {
50
+ return allItemIds.filter((itemId) => resolveTrade(itemId)?.shops?.includes(shopId) ?? false);
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,13 @@
1
+ export interface UnlockDef {
2
+ id: string;
3
+ category?: string;
4
+ }
5
+ export interface Unlocks {
6
+ has(userId: string, unlockId: string): boolean;
7
+ grant(userId: string, unlockId: string): void;
8
+ list(userId: string): string[];
9
+ tree(categoryId: string): UnlockDef[];
10
+ snapshot(userId: string): string[];
11
+ hydrate(userId: string, ids: string[]): void;
12
+ }
13
+ export declare function createUnlocks(defs?: UnlockDef[]): Unlocks;
@@ -0,0 +1,34 @@
1
+ export function createUnlocks(defs = []) {
2
+ const definitions = new Map();
3
+ for (const def of defs)
4
+ definitions.set(def.id, def);
5
+ const granted = new Map();
6
+ function requireGrantedSet(userId) {
7
+ let set = granted.get(userId);
8
+ if (!set) {
9
+ set = new Set();
10
+ granted.set(userId, set);
11
+ }
12
+ return set;
13
+ }
14
+ return {
15
+ has(userId, unlockId) {
16
+ return granted.get(userId)?.has(unlockId) ?? false;
17
+ },
18
+ grant(userId, unlockId) {
19
+ requireGrantedSet(userId).add(unlockId);
20
+ },
21
+ list(userId) {
22
+ return Array.from(granted.get(userId) ?? []);
23
+ },
24
+ tree(categoryId) {
25
+ return Array.from(definitions.values()).filter((def) => def.category === categoryId);
26
+ },
27
+ snapshot(userId) {
28
+ return Array.from(granted.get(userId) ?? []);
29
+ },
30
+ hydrate(userId, ids) {
31
+ granted.set(userId, new Set(ids));
32
+ },
33
+ };
34
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./runtime/save.js";
2
+ export * from "./runtime/snapshot.js";
3
+ export * from "./runtime/commandRunner.js";
4
+ export * from "./runtime/gameRuntime.js";
5
+ export * from "./runtime/adapter.js";
6
+ export * from "./runtime/transport.js";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./runtime/save.js";
2
+ export * from "./runtime/snapshot.js";
3
+ export * from "./runtime/commandRunner.js";
4
+ export * from "./runtime/gameRuntime.js";
5
+ export * from "./runtime/adapter.js";
6
+ export * from "./runtime/transport.js";
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Action-binding model: games bind semantic actions ("jump", "interact") to
3
+ * physical control codes; capture layers resolve raw events through this map
4
+ * so gameplay code never sees keycodes. Control codes are plain strings, so
5
+ * the same model serves keyboard codes, mouse buttons, touch controls, or
6
+ * gamepad inputs.
7
+ */
8
+ export interface ActionBinding<TCode extends string = string> {
9
+ primary: TCode;
10
+ secondary: TCode | null;
11
+ }
12
+ export type ActionBindingMap<TAction extends string, TCode extends string = string> = Record<TAction, ActionBinding<TCode>>;
13
+ export declare function bindingMatches<TCode extends string>(code: TCode, binding: ActionBinding<TCode>): boolean;
14
+ /** First action (in the map's key order) bound to the given control code, or null. */
15
+ export declare function resolveBoundAction<TAction extends string, TCode extends string>(code: TCode, bindings: ActionBindingMap<TAction, TCode>): TAction | null;
16
+ /**
17
+ * Collapse left/right modifier variants of a KeyboardEvent.code so bindings
18
+ * store one logical key.
19
+ */
20
+ export declare function normalizeKeyCode(code: string): string;
21
+ export interface ActionBindingModes<TCode extends string = string> {
22
+ hold?: ActionBinding<TCode>[];
23
+ toggle?: ActionBinding<TCode>[];
24
+ }
25
+ export type ActionBindingConfig<TCode extends string = string> = ActionBinding<TCode>[] | ActionBindingModes<TCode>;
26
+ export type ActionStateBindingMap<TAction extends string, TCode extends string = string> = Record<TAction, ActionBindingConfig<TCode>>;
27
+ export type ActionCodes<TCode extends string = string> = readonly TCode[] | {
28
+ hold?: readonly TCode[];
29
+ toggle?: readonly TCode[];
30
+ };
31
+ export type ActionCodesMap<TAction extends string = string, TCode extends string = string> = Record<TAction, ActionCodes<TCode>>;
32
+ export declare function toActionStateBindingMap<TAction extends string, TCode extends string>(map: ActionCodesMap<TAction, TCode>): ActionStateBindingMap<TAction, TCode>;
33
+ export interface ActionStateTracker<TAction extends string> {
34
+ handleDown(code: string): TAction | null;
35
+ handleUp(code: string): TAction | null;
36
+ isDown(action: TAction): boolean;
37
+ wasPressed(action: TAction): boolean;
38
+ endFrame(): void;
39
+ reset(): void;
40
+ }
41
+ export declare function createActionStateTracker<TAction extends string, TCode extends string = string>(map: ActionStateBindingMap<TAction, TCode>): ActionStateTracker<TAction>;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Action-binding model: games bind semantic actions ("jump", "interact") to
3
+ * physical control codes; capture layers resolve raw events through this map
4
+ * so gameplay code never sees keycodes. Control codes are plain strings, so
5
+ * the same model serves keyboard codes, mouse buttons, touch controls, or
6
+ * gamepad inputs.
7
+ */
8
+ export function bindingMatches(code, binding) {
9
+ return code === binding.primary || code === binding.secondary;
10
+ }
11
+ /** First action (in the map's key order) bound to the given control code, or null. */
12
+ export function resolveBoundAction(code, bindings) {
13
+ for (const action of Object.keys(bindings)) {
14
+ if (bindingMatches(code, bindings[action]))
15
+ return action;
16
+ }
17
+ return null;
18
+ }
19
+ /**
20
+ * Collapse left/right modifier variants of a KeyboardEvent.code so bindings
21
+ * store one logical key.
22
+ */
23
+ export function normalizeKeyCode(code) {
24
+ if (code === "ShiftLeft" || code === "ShiftRight")
25
+ return "Shift";
26
+ if (code === "ControlLeft" || code === "ControlRight")
27
+ return "Control";
28
+ return code;
29
+ }
30
+ function toBindings(codes) {
31
+ return codes.map((code) => ({ primary: code, secondary: null }));
32
+ }
33
+ export function toActionStateBindingMap(map) {
34
+ const result = {};
35
+ for (const action of Object.keys(map)) {
36
+ const codes = map[action];
37
+ if (Array.isArray(codes)) {
38
+ result[action] = toBindings(codes);
39
+ }
40
+ else {
41
+ const modes = codes;
42
+ result[action] = { hold: toBindings(modes.hold ?? []), toggle: toBindings(modes.toggle ?? []) };
43
+ }
44
+ }
45
+ return result;
46
+ }
47
+ function resolveActionBindingModes(config) {
48
+ if (Array.isArray(config))
49
+ return { hold: config, toggle: [] };
50
+ return { hold: config.hold ?? [], toggle: config.toggle ?? [] };
51
+ }
52
+ export function createActionStateTracker(map) {
53
+ const actions = Object.keys(map);
54
+ const modesByAction = new Map(actions.map((action) => [action, resolveActionBindingModes(map[action])]));
55
+ const heldCodesByAction = new Map(actions.map((action) => [action, new Set()]));
56
+ const toggledActions = new Set();
57
+ const pressedThisFrame = new Set();
58
+ const activeCodes = new Set();
59
+ function findAction(code, pick) {
60
+ for (const action of actions) {
61
+ if (pick(modesByAction.get(action)).some((binding) => bindingMatches(code, binding)))
62
+ return action;
63
+ }
64
+ return null;
65
+ }
66
+ return {
67
+ handleDown(code) {
68
+ const normalized = normalizeKeyCode(code);
69
+ if (activeCodes.has(normalized))
70
+ return null;
71
+ activeCodes.add(normalized);
72
+ let matched = null;
73
+ const holdAction = findAction(normalized, (modes) => modes.hold);
74
+ if (holdAction !== null) {
75
+ heldCodesByAction.get(holdAction).add(normalized);
76
+ pressedThisFrame.add(holdAction);
77
+ matched = holdAction;
78
+ }
79
+ const toggleAction = findAction(normalized, (modes) => modes.toggle);
80
+ if (toggleAction !== null) {
81
+ if (toggledActions.has(toggleAction))
82
+ toggledActions.delete(toggleAction);
83
+ else
84
+ toggledActions.add(toggleAction);
85
+ pressedThisFrame.add(toggleAction);
86
+ matched = matched ?? toggleAction;
87
+ }
88
+ return matched;
89
+ },
90
+ handleUp(code) {
91
+ const normalized = normalizeKeyCode(code);
92
+ activeCodes.delete(normalized);
93
+ const holdAction = findAction(normalized, (modes) => modes.hold);
94
+ if (holdAction !== null)
95
+ heldCodesByAction.get(holdAction).delete(normalized);
96
+ return holdAction;
97
+ },
98
+ isDown(action) {
99
+ return (heldCodesByAction.get(action)?.size ?? 0) > 0 || toggledActions.has(action);
100
+ },
101
+ wasPressed(action) {
102
+ return pressedThisFrame.has(action);
103
+ },
104
+ endFrame() {
105
+ pressedThisFrame.clear();
106
+ },
107
+ reset() {
108
+ for (const codes of heldCodesByAction.values())
109
+ codes.clear();
110
+ toggledActions.clear();
111
+ pressedThisFrame.clear();
112
+ activeCodes.clear();
113
+ },
114
+ };
115
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Per-frame look channel shared between an event-driven capture layer (writer
3
+ * of raw pointer deltas) and a frame-driven controller (consumer), plus the
4
+ * latest committed pose for same-frame readers such as presence sync. Kept as
5
+ * plain mutable state on purpose: routing per-frame deltas through a reactive
6
+ * store would notify subscribers every frame for state no UI reads.
7
+ */
8
+ export interface LookDeltas {
9
+ yaw: number;
10
+ pitch: number;
11
+ }
12
+ export interface LookChannel {
13
+ accumulate(dx: number, dy: number): void;
14
+ /** Yaw/pitch deltas (radians) for this frame; clears the accumulator. */
15
+ consume(): LookDeltas;
16
+ setYaw(yaw: number): void;
17
+ readYaw(): number;
18
+ setPitch(pitch: number): void;
19
+ readPitch(): number;
20
+ setVerticalOffset(offset: number): void;
21
+ readVerticalOffset(): number;
22
+ }
23
+ export interface LookChannelOptions {
24
+ /** Radians of rotation per pixel of pointer travel. */
25
+ sensitivity: number;
26
+ /** Clamp for the vertical offset register (e.g. peak jump height). */
27
+ maxVerticalOffset?: number;
28
+ }
29
+ export declare function createLookChannel({ sensitivity, maxVerticalOffset }: LookChannelOptions): LookChannel;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Per-frame look channel shared between an event-driven capture layer (writer
3
+ * of raw pointer deltas) and a frame-driven controller (consumer), plus the
4
+ * latest committed pose for same-frame readers such as presence sync. Kept as
5
+ * plain mutable state on purpose: routing per-frame deltas through a reactive
6
+ * store would notify subscribers every frame for state no UI reads.
7
+ */
8
+ export function createLookChannel({ sensitivity, maxVerticalOffset = Infinity }) {
9
+ let deltaX = 0;
10
+ let deltaY = 0;
11
+ let yaw = 0;
12
+ let pitch = 0;
13
+ let verticalOffset = 0;
14
+ return {
15
+ accumulate(dx, dy) {
16
+ deltaX += dx;
17
+ deltaY += dy;
18
+ },
19
+ consume() {
20
+ const result = { yaw: -deltaX * sensitivity, pitch: -deltaY * sensitivity };
21
+ deltaX = 0;
22
+ deltaY = 0;
23
+ return result;
24
+ },
25
+ setYaw(value) {
26
+ yaw = value;
27
+ },
28
+ readYaw() {
29
+ return yaw;
30
+ },
31
+ setPitch(value) {
32
+ pitch = value;
33
+ },
34
+ readPitch() {
35
+ return pitch;
36
+ },
37
+ setVerticalOffset(value) {
38
+ verticalOffset = Math.max(0, Math.min(maxVerticalOffset, value));
39
+ },
40
+ readVerticalOffset() {
41
+ return verticalOffset;
42
+ },
43
+ };
44
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Single-finger gesture disambiguation for touch look controls: one touch
3
+ * stream drives look-drag, tap, and long-press-hold without fighting.
4
+ * Movement past the tap threshold commits the gesture to "look" and cancels
5
+ * tap/hold; a still finger that lifts quickly is a tap; a still finger held
6
+ * past the long-press delay is a hold. Timestamps are injected so the state
7
+ * machine stays pure; the capture layer owns timers and touch identifiers.
8
+ */
9
+ export interface TouchGestureTuning {
10
+ /** A finger that moves less than this (px) and lifts quickly is a tap. */
11
+ tapMoveThresholdPx: number;
12
+ tapMaxMs: number;
13
+ /** Hold this long without moving to begin a hold gesture. */
14
+ longPressMs: number;
15
+ }
16
+ export type TouchGestureEnd = "tap" | "hold-end" | null;
17
+ export interface TouchGestureTracker {
18
+ begin(x: number, y: number, nowMs: number): void;
19
+ /** Look-drag delta since the last move, or null when no gesture is active. */
20
+ move(x: number, y: number): {
21
+ dx: number;
22
+ dy: number;
23
+ } | null;
24
+ /** Commit to a hold if the finger is still down and hasn't moved. */
25
+ beginHold(): boolean;
26
+ end(nowMs: number): TouchGestureEnd;
27
+ cancel(): TouchGestureEnd;
28
+ isActive(): boolean;
29
+ isHolding(): boolean;
30
+ }
31
+ export declare function createTouchGestureTracker(tuning: TouchGestureTuning): TouchGestureTracker;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Single-finger gesture disambiguation for touch look controls: one touch
3
+ * stream drives look-drag, tap, and long-press-hold without fighting.
4
+ * Movement past the tap threshold commits the gesture to "look" and cancels
5
+ * tap/hold; a still finger that lifts quickly is a tap; a still finger held
6
+ * past the long-press delay is a hold. Timestamps are injected so the state
7
+ * machine stays pure; the capture layer owns timers and touch identifiers.
8
+ */
9
+ export function createTouchGestureTracker(tuning) {
10
+ let active = false;
11
+ let moved = false;
12
+ let holding = false;
13
+ let startX = 0;
14
+ let startY = 0;
15
+ let lastX = 0;
16
+ let lastY = 0;
17
+ let startTime = 0;
18
+ return {
19
+ begin(x, y, nowMs) {
20
+ active = true;
21
+ moved = false;
22
+ holding = false;
23
+ startX = lastX = x;
24
+ startY = lastY = y;
25
+ startTime = nowMs;
26
+ },
27
+ move(x, y) {
28
+ if (!active)
29
+ return null;
30
+ if (!moved && Math.hypot(x - startX, y - startY) > tuning.tapMoveThresholdPx) {
31
+ moved = true;
32
+ }
33
+ const dx = x - lastX;
34
+ const dy = y - lastY;
35
+ lastX = x;
36
+ lastY = y;
37
+ return { dx, dy };
38
+ },
39
+ beginHold() {
40
+ if (!active || moved || holding)
41
+ return false;
42
+ holding = true;
43
+ return true;
44
+ },
45
+ end(nowMs) {
46
+ if (!active)
47
+ return null;
48
+ active = false;
49
+ if (holding) {
50
+ holding = false;
51
+ return "hold-end";
52
+ }
53
+ if (!moved && nowMs - startTime <= tuning.tapMaxMs)
54
+ return "tap";
55
+ return null;
56
+ },
57
+ cancel() {
58
+ active = false;
59
+ if (holding) {
60
+ holding = false;
61
+ return "hold-end";
62
+ }
63
+ return null;
64
+ },
65
+ isActive() {
66
+ return active;
67
+ },
68
+ isHolding() {
69
+ return holding;
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,51 @@
1
+ export interface PromptPoint {
2
+ x: number;
3
+ z: number;
4
+ }
5
+ export interface KeybindPromptDisplay {
6
+ kind: "keybind";
7
+ actionId: string;
8
+ }
9
+ export interface GaugePromptDisplay {
10
+ kind: "gauge";
11
+ gaugeId: string;
12
+ }
13
+ export interface LabelPromptDisplay {
14
+ kind: "label";
15
+ text: string;
16
+ }
17
+ export type ProximityPromptDisplay = KeybindPromptDisplay | GaugePromptDisplay | LabelPromptDisplay;
18
+ export declare function keybind(actionId: string): KeybindPromptDisplay;
19
+ export declare function gauge(gaugeId: string): GaugePromptDisplay;
20
+ export declare function label(text: string): LabelPromptDisplay;
21
+ export interface PromptCommand {
22
+ name: string;
23
+ input: unknown;
24
+ }
25
+ export declare function command(name: string, input?: unknown): PromptCommand;
26
+ export interface ProximityPrompt {
27
+ radius: number;
28
+ display: ProximityPromptDisplay;
29
+ invoke: PromptCommand | null;
30
+ }
31
+ export interface ProximityPromptConfig {
32
+ radius: number;
33
+ display: ProximityPromptDisplay;
34
+ invoke?: PromptCommand | null;
35
+ }
36
+ export declare function proximityPrompt({ radius, display, invoke }: ProximityPromptConfig): ProximityPrompt;
37
+ export interface PositionedPrompt {
38
+ id: string;
39
+ position: PromptPoint;
40
+ priority?: number;
41
+ prompt: ProximityPrompt;
42
+ }
43
+ /**
44
+ * Nearest prompt strictly within its radius wins; a higher-priority prompt in
45
+ * range beats any lower-priority one regardless of distance; equal priority
46
+ * and distance keep the earliest prompt in the list.
47
+ */
48
+ export declare function resolveActivePrompt<T extends PositionedPrompt>(playerPosition: PromptPoint, prompts: readonly T[]): T | null;
49
+ export declare function promptDisplaysEqual(a: ProximityPromptDisplay, b: ProximityPromptDisplay): boolean;
50
+ export declare function promptCommandsEqual(a: PromptCommand | null, b: PromptCommand | null): boolean;
51
+ export declare function positionedPromptsEqual(a: PositionedPrompt, b: PositionedPrompt): boolean;
@@ -0,0 +1,76 @@
1
+ export function keybind(actionId) {
2
+ return { kind: "keybind", actionId };
3
+ }
4
+ export function gauge(gaugeId) {
5
+ return { kind: "gauge", gaugeId };
6
+ }
7
+ export function label(text) {
8
+ return { kind: "label", text };
9
+ }
10
+ export function command(name, input) {
11
+ return { name, input };
12
+ }
13
+ export function proximityPrompt({ radius, display, invoke = null }) {
14
+ return { radius, display, invoke };
15
+ }
16
+ /**
17
+ * Nearest prompt strictly within its radius wins; a higher-priority prompt in
18
+ * range beats any lower-priority one regardless of distance; equal priority
19
+ * and distance keep the earliest prompt in the list.
20
+ */
21
+ export function resolveActivePrompt(playerPosition, prompts) {
22
+ let active = null;
23
+ let activePriority = Number.NEGATIVE_INFINITY;
24
+ let activeDistance = Number.POSITIVE_INFINITY;
25
+ for (const candidate of prompts) {
26
+ const nextDistance = Math.hypot(candidate.position.x - playerPosition.x, candidate.position.z - playerPosition.z);
27
+ if (nextDistance >= candidate.prompt.radius)
28
+ continue;
29
+ const priority = candidate.priority ?? 0;
30
+ if (priority > activePriority || (priority === activePriority && nextDistance < activeDistance)) {
31
+ active = candidate;
32
+ activePriority = priority;
33
+ activeDistance = nextDistance;
34
+ }
35
+ }
36
+ return active;
37
+ }
38
+ export function promptDisplaysEqual(a, b) {
39
+ if (a === b)
40
+ return true;
41
+ if (a.kind === "keybind")
42
+ return b.kind === "keybind" && a.actionId === b.actionId;
43
+ if (a.kind === "gauge")
44
+ return b.kind === "gauge" && a.gaugeId === b.gaugeId;
45
+ return b.kind === "label" && a.text === b.text;
46
+ }
47
+ function commandInputsEqual(a, b) {
48
+ if (Object.is(a, b))
49
+ return true;
50
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
51
+ return false;
52
+ const aRecord = a;
53
+ const bRecord = b;
54
+ const keys = Object.keys(aRecord);
55
+ if (keys.length !== Object.keys(bRecord).length)
56
+ return false;
57
+ return keys.every((key) => Object.is(aRecord[key], bRecord[key]));
58
+ }
59
+ export function promptCommandsEqual(a, b) {
60
+ if (a === b)
61
+ return true;
62
+ if (a === null || b === null)
63
+ return false;
64
+ return a.name === b.name && commandInputsEqual(a.input, b.input);
65
+ }
66
+ export function positionedPromptsEqual(a, b) {
67
+ if (a === b)
68
+ return true;
69
+ return a.id === b.id
70
+ && a.position.x === b.position.x
71
+ && a.position.z === b.position.z
72
+ && (a.priority ?? 0) === (b.priority ?? 0)
73
+ && a.prompt.radius === b.prompt.radius
74
+ && promptDisplaysEqual(a.prompt.display, b.prompt.display)
75
+ && promptCommandsEqual(a.prompt.invoke, b.prompt.invoke);
76
+ }
@@ -0,0 +1,57 @@
1
+ export interface ItemTraits {
2
+ stackLimit(itemId: string): number;
3
+ kind?(itemId: string): string | null;
4
+ }
5
+ export interface InventoryLayout {
6
+ slots: number;
7
+ accepts?: string | readonly string[];
8
+ }
9
+ export type InventorySlot = {
10
+ itemId: string;
11
+ count: number;
12
+ } | null;
13
+ export interface InventoryState {
14
+ slots: InventorySlot[];
15
+ }
16
+ export type PutResult = {
17
+ status: "ok";
18
+ state: InventoryState;
19
+ } | {
20
+ status: "rejected";
21
+ reason: "no-space" | "wrong-kind" | "slot-occupied" | "invalid-slot";
22
+ };
23
+ export type TakeResult = {
24
+ status: "ok";
25
+ state: InventoryState;
26
+ } | {
27
+ status: "rejected";
28
+ reason: "insufficient";
29
+ };
30
+ export type MoveResult = {
31
+ status: "ok";
32
+ from: InventoryState;
33
+ to: InventoryState;
34
+ } | {
35
+ status: "rejected";
36
+ reason: "invalid-slot" | "empty-slot" | "wrong-kind" | "no-space";
37
+ };
38
+ export declare function createEmptyInventory(layout: InventoryLayout): InventoryState;
39
+ export declare function putItem(state: InventoryState, layout: InventoryLayout, traits: ItemTraits, itemId: string, count: number, options?: {
40
+ slot?: number;
41
+ }): PutResult;
42
+ export declare function takeItem(state: InventoryState, itemId: string, count: number): TakeResult;
43
+ export declare function countItem(state: InventoryState, itemId: string): number;
44
+ export declare function hasItem(state: InventoryState, itemId: string, count: number): boolean;
45
+ export declare function moveItem(from: InventoryState, fromSlot: number, to: InventoryState, toLayout: InventoryLayout, traits: ItemTraits, toSlot?: number): MoveResult;
46
+ export interface InventorySet<TId extends string> {
47
+ put(id: TId, itemId: string, count: number, options?: {
48
+ slot?: number;
49
+ }): PutResult;
50
+ take(id: TId, itemId: string, count: number): TakeResult;
51
+ move(fromId: TId, fromSlot: number, toId: TId, toSlot?: number): MoveResult;
52
+ count(id: TId, itemId: string): number;
53
+ has(id: TId, itemId: string, count: number): boolean;
54
+ state(id: TId): InventoryState;
55
+ replaceState(id: TId, state: InventoryState): void;
56
+ }
57
+ export declare function createInventorySet<TId extends string>(layouts: Record<TId, InventoryLayout>, traits: ItemTraits): InventorySet<TId>;