@jgengine/core 0.1.1 → 0.3.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.
@@ -1,5 +1,5 @@
1
1
  import type { EntityPosition } from "../scene/entityStore.js";
2
- import { type PoolStatMap } from "../scene/entityStats.js";
2
+ import { type StatValueMap } from "../scene/entityStats.js";
3
3
  export interface ReceiveRule {
4
4
  order: string[];
5
5
  modifiers?: Record<string, number>;
@@ -47,7 +47,7 @@ export interface CombatSpatialDeps {
47
47
  }
48
48
  export interface EffectSystemDeps {
49
49
  resolveReceive(instanceId: string): ReceiveMap | null | undefined;
50
- resolveStats(instanceId: string): PoolStatMap | undefined;
50
+ resolveStats(instanceId: string): StatValueMap | undefined;
51
51
  getStat(itemId: string, stat: string): number | null;
52
52
  spatial: CombatSpatialDeps;
53
53
  drainStatByEffect?: Record<string, string>;
@@ -1,3 +1,4 @@
1
+ import type { PositionedPrompt } from "../interaction/proximityPrompt.js";
1
2
  import type { GameContext, GameContextContent } from "../runtime/gameContext.js";
2
3
  import type { GameDefinition, GameLoop } from "./defineGame.js";
3
4
  export interface CameraFollowState {
@@ -58,6 +59,8 @@ export interface PlayableGame<TUi = unknown, TWorldOverlay = unknown> {
58
59
  entitySprites?: Record<string, EntitySpriteConfig>;
59
60
  /** Optional scroll-selected hotbar index for primary ability (mouse0). */
60
61
  hotbarSelection?: () => number;
62
+ /** Positioned proximity prompts for the interact key + HUD; single source shared with useActivePrompt. */
63
+ prompts?: (ctx: GameContext) => readonly PositionedPrompt[];
61
64
  /** Third-person orbit camera tuning for the dev game player shell. */
62
65
  camera?: GameCameraConfig;
63
66
  }
@@ -30,6 +30,22 @@ export type ActionCodes<TCode extends string = string> = readonly TCode[] | {
30
30
  };
31
31
  export type ActionCodesMap<TAction extends string = string, TCode extends string = string> = Record<TAction, ActionCodes<TCode>>;
32
32
  export declare function toActionStateBindingMap<TAction extends string, TCode extends string>(map: ActionCodesMap<TAction, TCode>): ActionStateBindingMap<TAction, TCode>;
33
+ export declare function hotbarSlotBindings(count: number, options?: {
34
+ action?: (slot: number) => string;
35
+ }): ActionCodesMap;
36
+ /** 0-based slot index for a hotbar-slot action name (`slot1`, `hotbarSlot1`), or null. */
37
+ export declare function hotbarSlotActionIndex(action: string): number | null;
38
+ export declare function isHotbarSlotAction(action: string): boolean;
39
+ /**
40
+ * Convention mapping a bound input action to the command it fires: the action's
41
+ * own name when a command by that name exists, otherwise a `ui.<action>`
42
+ * fallback. Reserved actions (consumed natively by the shell) and hotbar slots
43
+ * yield null so the shell can handle them itself.
44
+ */
45
+ export declare function resolveActionCommand(action: string, has: (command: string) => boolean, reserved: ReadonlySet<string>): string | null;
46
+ export declare function bindingLabel(code: string): string;
47
+ /** Short display label for an action's first bound code, or null when unbound. */
48
+ export declare function actionLabel(map: ActionCodesMap, action: string): string | null;
33
49
  export interface ActionStateTracker<TAction extends string> {
34
50
  handleDown(code: string): TAction | null;
35
51
  handleUp(code: string): TAction | null;
@@ -44,6 +44,101 @@ export function toActionStateBindingMap(map) {
44
44
  }
45
45
  return result;
46
46
  }
47
+ const HOTBAR_DIGIT_CODES = [
48
+ "Digit1",
49
+ "Digit2",
50
+ "Digit3",
51
+ "Digit4",
52
+ "Digit5",
53
+ "Digit6",
54
+ "Digit7",
55
+ "Digit8",
56
+ "Digit9",
57
+ "Digit0",
58
+ ];
59
+ export function hotbarSlotBindings(count, options) {
60
+ if (!Number.isInteger(count) || count < 1 || count > HOTBAR_DIGIT_CODES.length) {
61
+ throw new Error(`hotbarSlotBindings: count must be an integer in [1, ${HOTBAR_DIGIT_CODES.length}]`);
62
+ }
63
+ const action = options?.action ?? ((slot) => `hotbarSlot${slot}`);
64
+ const map = {};
65
+ for (let slot = 1; slot <= count; slot += 1) {
66
+ map[action(slot)] = [HOTBAR_DIGIT_CODES[slot - 1]];
67
+ }
68
+ return map;
69
+ }
70
+ const HOTBAR_SLOT_ACTION = /^(?:hotbar)?slot(\d+)$/i;
71
+ /** 0-based slot index for a hotbar-slot action name (`slot1`, `hotbarSlot1`), or null. */
72
+ export function hotbarSlotActionIndex(action) {
73
+ const match = HOTBAR_SLOT_ACTION.exec(action);
74
+ return match === null ? null : Number(match[1]) - 1;
75
+ }
76
+ export function isHotbarSlotAction(action) {
77
+ return HOTBAR_SLOT_ACTION.test(action);
78
+ }
79
+ /**
80
+ * Convention mapping a bound input action to the command it fires: the action's
81
+ * own name when a command by that name exists, otherwise a `ui.<action>`
82
+ * fallback. Reserved actions (consumed natively by the shell) and hotbar slots
83
+ * yield null so the shell can handle them itself.
84
+ */
85
+ export function resolveActionCommand(action, has, reserved) {
86
+ if (reserved.has(action) || isHotbarSlotAction(action))
87
+ return null;
88
+ if (has(action))
89
+ return action;
90
+ const uiCommand = `ui.${action}`;
91
+ return has(uiCommand) ? uiCommand : null;
92
+ }
93
+ const CODE_LABELS = {
94
+ mouse0: "LMB",
95
+ mouse1: "MMB",
96
+ mouse2: "RMB",
97
+ Space: "Space",
98
+ space: "Space",
99
+ Escape: "Esc",
100
+ Tab: "Tab",
101
+ Enter: "Enter",
102
+ Backspace: "Bksp",
103
+ Shift: "Shift",
104
+ ShiftLeft: "Shift",
105
+ ShiftRight: "Shift",
106
+ Control: "Ctrl",
107
+ ControlLeft: "Ctrl",
108
+ ControlRight: "Ctrl",
109
+ AltLeft: "Alt",
110
+ AltRight: "Alt",
111
+ ArrowUp: "↑",
112
+ ArrowDown: "↓",
113
+ ArrowLeft: "←",
114
+ ArrowRight: "→",
115
+ };
116
+ export function bindingLabel(code) {
117
+ const direct = CODE_LABELS[code];
118
+ if (direct !== undefined)
119
+ return direct;
120
+ if (code.startsWith("Key") && code.length === 4)
121
+ return code.slice(3);
122
+ if (code.startsWith("Digit") && code.length === 6)
123
+ return code.slice(5);
124
+ return code.length === 1 ? code.toUpperCase() : code;
125
+ }
126
+ /** Short display label for an action's first bound code, or null when unbound. */
127
+ export function actionLabel(map, action) {
128
+ const codes = map[action];
129
+ if (codes === undefined)
130
+ return null;
131
+ let list;
132
+ if (Array.isArray(codes)) {
133
+ list = codes;
134
+ }
135
+ else {
136
+ const modes = codes;
137
+ list = modes.hold ?? modes.toggle ?? [];
138
+ }
139
+ const first = list[0];
140
+ return first === undefined ? null : bindingLabel(first);
141
+ }
47
142
  function resolveActionBindingModes(config) {
48
143
  if (Array.isArray(config))
49
144
  return { hold: config, toggle: [] };
@@ -12,12 +12,13 @@ import { type QuestJournal } from "../game/quest.js";
12
12
  import { type Social } from "../game/social.js";
13
13
  import { type TradeField, type TradeSystem } from "../game/trade.js";
14
14
  import { type Unlocks } from "../game/unlocks.js";
15
- import { type InventorySet } from "../inventory/inventoryModel.js";
15
+ import { type InventoryLayout, type InventorySet } from "../inventory/inventoryModel.js";
16
+ import type { ProximityPrompt } from "../interaction/proximityPrompt.js";
16
17
  import { type ItemUseHandler, type ItemUseInput, type ItemUseRejection, type ItemUseResult } from "../item/use.js";
17
18
  import { type WeaponStats } from "../item/weapon.js";
18
19
  import { type PoseAllowedStates, type PoseState } from "../movement/poseState.js";
19
20
  import type { ModelAssetRef } from "../scene/assetCatalog.js";
20
- import { type EntityStatsApi, type PoolStatCatalog } from "../scene/entityStats.js";
21
+ import { type EntityStatsApi, type StatCatalog } from "../scene/entityStats.js";
21
22
  import type { EntityPose, SceneEntity, SpawnOptions } from "../scene/entityStore.js";
22
23
  import { type ObjectStore } from "../scene/objectStore.js";
23
24
  import { type SpatialApi } from "../scene/spatial.js";
@@ -28,18 +29,27 @@ export interface GameContextItemEntry {
28
29
  weapon?: Record<string, unknown>;
29
30
  trade?: TradeField;
30
31
  }
32
+ export type CatalogEntityRole = "player" | "enemy" | "hostile" | "npc" | "vehicle";
31
33
  export interface GameContextEntityEntry {
32
- stats?: PoolStatCatalog;
34
+ stats?: StatCatalog;
33
35
  receive?: ReceiveMap;
34
36
  onDeath?: OnDeathSpec;
35
37
  movement?: PoseAllowedStates & {
36
38
  walkSpeed?: number;
37
39
  };
38
- role?: string;
40
+ role?: CatalogEntityRole;
41
+ }
42
+ export interface GameContextObjectEntry {
43
+ proximityPrompt?: ProximityPrompt;
44
+ breakable?: false | {
45
+ baseBreakTime: number;
46
+ };
47
+ slotInventory?: InventoryLayout;
39
48
  }
40
49
  export interface GameContextContent {
41
50
  itemById?(itemId: string): GameContextItemEntry | null | undefined;
42
51
  entityById?(catalogId: string): GameContextEntityEntry | null | undefined;
52
+ objectById?(catalogId: string): GameContextObjectEntry | null | undefined;
43
53
  }
44
54
  export interface GameContextOptions<TAssetRef extends ModelAssetRef = ModelAssetRef, TMultiplayer = unknown> {
45
55
  definition: GameDefinition<TAssetRef, TMultiplayer>;
@@ -50,6 +60,9 @@ export interface GameContextOptions<TAssetRef extends ModelAssetRef = ModelAsset
50
60
  };
51
61
  now?: () => number;
52
62
  }
63
+ export interface SceneObjectContext extends ObjectStore {
64
+ catalog(instanceId: string): GameContextObjectEntry | null;
65
+ }
53
66
  export interface SceneEntityContext {
54
67
  spawn(name: string, options?: SpawnOptions): string;
55
68
  despawn(instanceId: string): boolean;
@@ -102,7 +115,7 @@ export interface GameContextItemUse {
102
115
  }
103
116
  export interface GameContext {
104
117
  scene: {
105
- object: ObjectStore;
118
+ object: SceneObjectContext;
106
119
  entity: SceneEntityContext;
107
120
  };
108
121
  game: {
@@ -16,7 +16,7 @@ import { createInventorySet, putItem, } from "../inventory/inventoryModel.js";
16
16
  import { createItemUse, } from "../item/use.js";
17
17
  import { createWeaponStats } from "../item/weapon.js";
18
18
  import { createPoseState } from "../movement/poseState.js";
19
- import { createEntityStatsApi, seedPoolStats, setPoolStat, } from "../scene/entityStats.js";
19
+ import { createEntityStatsApi, seedStatValues, setStatValue, } from "../scene/entityStats.js";
20
20
  import { createObjectStore } from "../scene/objectStore.js";
21
21
  import { createSpatialApi } from "../scene/spatial.js";
22
22
  import { createTargeting } from "../scene/targeting.js";
@@ -44,6 +44,10 @@ export function createGameContext(options) {
44
44
  const entity = entities.get(instanceId);
45
45
  return entity === null ? undefined : content.entityById?.(entity.name);
46
46
  }
47
+ function catalogObject(instanceId) {
48
+ const object = objects.get(instanceId);
49
+ return object === null ? undefined : content.objectById?.(object.catalogId);
50
+ }
47
51
  const spatial = createSpatialApi({
48
52
  resolvePosition: (instanceId) => entities.get(instanceId)?.position,
49
53
  candidates: () => entities.list().map((entity) => entity.id),
@@ -166,7 +170,7 @@ export function createGameContext(options) {
166
170
  });
167
171
  function seedUserPool(userId, statId, pool) {
168
172
  const map = ensureInstanceStats(userId);
169
- const next = setPoolStat(map, statId, pool);
173
+ const next = setStatValue(map, statId, pool);
170
174
  map[statId] = next[statId];
171
175
  }
172
176
  const rawQuest = createQuestJournal({
@@ -226,7 +230,7 @@ export function createGameContext(options) {
226
230
  : { ...spawnOptions, movement: { ...spawnOptions?.movement, walkSpeed } };
227
231
  const instanceId = entities.spawn(name, options);
228
232
  death.revive(instanceId);
229
- statsByInstance.set(instanceId, entry?.stats === undefined ? {} : seedPoolStats(entry.stats));
233
+ statsByInstance.set(instanceId, entry?.stats === undefined ? {} : seedStatValues(entry.stats));
230
234
  return instanceId;
231
235
  }
232
236
  function despawnEntity(instanceId) {
@@ -282,9 +286,13 @@ export function createGameContext(options) {
282
286
  getStat: weapon.getStat,
283
287
  now,
284
288
  }), ["fireProjectile", "settleProjectile"], signal.notify);
289
+ const sceneObjects = {
290
+ ...objects,
291
+ catalog: (instanceId) => catalogObject(instanceId) ?? null,
292
+ };
285
293
  const ctx = {
286
294
  scene: {
287
- object: objects,
295
+ object: sceneObjects,
288
296
  entity: {
289
297
  spawn: spawnEntity,
290
298
  despawn: despawnEntity,
@@ -1,38 +1,38 @@
1
- export interface PoolStat {
1
+ export interface StatValue {
2
2
  current: number;
3
3
  max: number;
4
4
  min: number;
5
5
  }
6
- export interface PoolStatPatch {
6
+ export interface StatValuePatch {
7
7
  current?: number;
8
8
  max?: number;
9
9
  min?: number;
10
10
  }
11
- export type PoolStatMap = Record<string, PoolStat>;
12
- export type PoolStatCatalog = Record<string, {
11
+ export type StatValueMap = Record<string, StatValue>;
12
+ export type StatCatalog = Record<string, {
13
13
  max: number;
14
14
  min?: number;
15
15
  current?: number;
16
16
  }>;
17
17
  export type PoolDeltaResult = {
18
18
  status: "ok";
19
- map: PoolStatMap;
20
- stat: PoolStat;
19
+ map: StatValueMap;
20
+ stat: StatValue;
21
21
  hitMin: boolean;
22
22
  hitMax: boolean;
23
23
  } | {
24
24
  status: "rejected";
25
25
  reason: string;
26
26
  };
27
- export declare function getPoolStat(map: PoolStatMap, statId: string): PoolStat | null;
28
- export declare function setPoolStat(map: PoolStatMap, statId: string, patch: PoolStatPatch): PoolStatMap;
29
- export declare function applyPoolDelta(map: PoolStatMap, statId: string, amount: number): PoolDeltaResult;
30
- export declare function seedPoolStats(catalogStats: PoolStatCatalog): PoolStatMap;
27
+ export declare function getStatValue(map: StatValueMap, statId: string): StatValue | null;
28
+ export declare function setStatValue(map: StatValueMap, statId: string, patch: StatValuePatch): StatValueMap;
29
+ export declare function applyPoolDelta(map: StatValueMap, statId: string, amount: number): PoolDeltaResult;
30
+ export declare function seedStatValues(catalogStats: StatCatalog): StatValueMap;
31
31
  export interface EntityStatsApi {
32
- get(instanceId: string, statId: string): PoolStat | null;
33
- set(instanceId: string, statId: string, patch: PoolStatPatch): boolean;
32
+ get(instanceId: string, statId: string): StatValue | null;
33
+ set(instanceId: string, statId: string, patch: StatValuePatch): boolean;
34
34
  delta(instanceId: string, statId: string, amount: number): null | {
35
35
  reason: string;
36
36
  };
37
37
  }
38
- export declare function createEntityStatsApi(resolve: (instanceId: string) => PoolStatMap | undefined): EntityStatsApi;
38
+ export declare function createEntityStatsApi(resolve: (instanceId: string) => StatValueMap | undefined): EntityStatsApi;
@@ -1,10 +1,10 @@
1
1
  function clamp(value, min, max) {
2
2
  return Math.min(max, Math.max(min, value));
3
3
  }
4
- export function getPoolStat(map, statId) {
4
+ export function getStatValue(map, statId) {
5
5
  return map[statId] ?? null;
6
6
  }
7
- export function setPoolStat(map, statId, patch) {
7
+ export function setStatValue(map, statId, patch) {
8
8
  const existing = map[statId];
9
9
  const max = patch.max ?? existing?.max ?? 0;
10
10
  const min = patch.min ?? existing?.min ?? 0;
@@ -26,7 +26,7 @@ export function applyPoolDelta(map, statId, amount) {
26
26
  hitMax: current === existing.max,
27
27
  };
28
28
  }
29
- export function seedPoolStats(catalogStats) {
29
+ export function seedStatValues(catalogStats) {
30
30
  const map = {};
31
31
  for (const [statId, declaration] of Object.entries(catalogStats)) {
32
32
  const min = declaration.min ?? 0;
@@ -41,13 +41,13 @@ export function createEntityStatsApi(resolve) {
41
41
  const map = resolve(instanceId);
42
42
  if (map === undefined)
43
43
  return null;
44
- return getPoolStat(map, statId);
44
+ return getStatValue(map, statId);
45
45
  },
46
46
  set(instanceId, statId, patch) {
47
47
  const map = resolve(instanceId);
48
48
  if (map === undefined)
49
49
  return false;
50
- const next = setPoolStat(map, statId, patch);
50
+ const next = setStatValue(map, statId, patch);
51
51
  map[statId] = next[statId];
52
52
  return true;
53
53
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jgengine/core",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Genre-agnostic TypeScript game engine SDK: game runtime, state store, entity scene, combat, movement, inventory, economy, quests, multiplayer primitives.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",