@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.
- package/dist/combat/effects.d.ts +2 -2
- package/dist/game/playableGame.d.ts +3 -0
- package/dist/input/actionBindings.d.ts +16 -0
- package/dist/input/actionBindings.js +95 -0
- package/dist/runtime/gameContext.d.ts +18 -5
- package/dist/runtime/gameContext.js +12 -4
- package/dist/scene/entityStats.d.ts +13 -13
- package/dist/scene/entityStats.js +5 -5
- package/package.json +1 -1
package/dist/combat/effects.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EntityPosition } from "../scene/entityStore.js";
|
|
2
|
-
import { type
|
|
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):
|
|
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
|
|
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?:
|
|
34
|
+
stats?: StatCatalog;
|
|
33
35
|
receive?: ReceiveMap;
|
|
34
36
|
onDeath?: OnDeathSpec;
|
|
35
37
|
movement?: PoseAllowedStates & {
|
|
36
38
|
walkSpeed?: number;
|
|
37
39
|
};
|
|
38
|
-
role?:
|
|
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:
|
|
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,
|
|
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 =
|
|
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 ? {} :
|
|
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:
|
|
295
|
+
object: sceneObjects,
|
|
288
296
|
entity: {
|
|
289
297
|
spawn: spawnEntity,
|
|
290
298
|
despawn: despawnEntity,
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface StatValue {
|
|
2
2
|
current: number;
|
|
3
3
|
max: number;
|
|
4
4
|
min: number;
|
|
5
5
|
}
|
|
6
|
-
export interface
|
|
6
|
+
export interface StatValuePatch {
|
|
7
7
|
current?: number;
|
|
8
8
|
max?: number;
|
|
9
9
|
min?: number;
|
|
10
10
|
}
|
|
11
|
-
export type
|
|
12
|
-
export type
|
|
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:
|
|
20
|
-
stat:
|
|
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
|
|
28
|
-
export declare function
|
|
29
|
-
export declare function applyPoolDelta(map:
|
|
30
|
-
export declare function
|
|
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):
|
|
33
|
-
set(instanceId: string, statId: string, patch:
|
|
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) =>
|
|
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
|
|
4
|
+
export function getStatValue(map, statId) {
|
|
5
5
|
return map[statId] ?? null;
|
|
6
6
|
}
|
|
7
|
-
export function
|
|
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
|
|
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
|
|
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 =
|
|
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.
|
|
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",
|