@jgengine/core 0.3.0 → 0.4.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.
|
@@ -13,12 +13,20 @@ export interface RaycastHit {
|
|
|
13
13
|
at: EntityPosition;
|
|
14
14
|
}
|
|
15
15
|
export type Raycast = (from: string, aim: Aim, range: number) => RaycastHit[];
|
|
16
|
+
export interface ProjectileSettleReport {
|
|
17
|
+
from: string;
|
|
18
|
+
origin: EntityPosition;
|
|
19
|
+
at: EntityPosition;
|
|
20
|
+
effect: string;
|
|
21
|
+
hit: boolean;
|
|
22
|
+
}
|
|
16
23
|
export interface ProjectileSystemDeps {
|
|
17
24
|
effects: EffectSystem;
|
|
18
25
|
spatial: CombatSpatialDeps;
|
|
19
26
|
getStat(itemId: string, stat: string): number | null;
|
|
20
27
|
raycast?: Raycast;
|
|
21
28
|
now?: () => number;
|
|
29
|
+
onSettle?(report: ProjectileSettleReport): void;
|
|
22
30
|
}
|
|
23
31
|
export interface ProjectilePrediction {
|
|
24
32
|
hits: {
|
|
@@ -132,8 +132,11 @@ export function createProjectileSystem(deps) {
|
|
|
132
132
|
return { status: "rejected", shotId, reason: "already-settled" };
|
|
133
133
|
shot.settled = true;
|
|
134
134
|
const { input } = shot;
|
|
135
|
+
const origin = shotOrigin(input.from, input.aim) ?? [0, 0, 0];
|
|
135
136
|
if (isBallistic(input.via)) {
|
|
136
|
-
|
|
137
|
+
const at = ballisticSettlePoint(input);
|
|
138
|
+
deps.onSettle?.({ from: input.from, origin, at, effect: input.effect, hit: false });
|
|
139
|
+
return { status: "settled", shotId, at, hits: [] };
|
|
137
140
|
}
|
|
138
141
|
const { visible } = predictHits(input);
|
|
139
142
|
const receivable = visible.filter((hit) => deps.effects.canReceive(hit.instanceId, input.effect) === null);
|
|
@@ -150,7 +153,9 @@ export function createProjectileSystem(deps) {
|
|
|
150
153
|
}));
|
|
151
154
|
}
|
|
152
155
|
}
|
|
153
|
-
|
|
156
|
+
const at = settledAt(input, receivable);
|
|
157
|
+
deps.onSettle?.({ from: input.from, origin, at, effect: input.effect, hit: receivable.length > 0 });
|
|
158
|
+
return { status: "settled", shotId, at, hits };
|
|
154
159
|
},
|
|
155
160
|
};
|
|
156
161
|
}
|
package/dist/game/events.d.ts
CHANGED
|
@@ -66,8 +66,23 @@ export interface StatLevelUpEvent {
|
|
|
66
66
|
stat: string;
|
|
67
67
|
level: number;
|
|
68
68
|
}
|
|
69
|
+
export interface EntityFloatTextEvent {
|
|
70
|
+
instanceId?: string;
|
|
71
|
+
position: [number, number, number];
|
|
72
|
+
text: string;
|
|
73
|
+
kind: string;
|
|
74
|
+
amount?: number;
|
|
75
|
+
}
|
|
76
|
+
export interface ProjectileSettledEvent {
|
|
77
|
+
from: string;
|
|
78
|
+
origin: [number, number, number];
|
|
79
|
+
at: [number, number, number];
|
|
80
|
+
effect: string;
|
|
81
|
+
hit: boolean;
|
|
82
|
+
}
|
|
69
83
|
export interface GameEventMap {
|
|
70
84
|
"entity.died": EntityDiedEvent;
|
|
85
|
+
"entity.floatText": EntityFloatTextEvent;
|
|
71
86
|
"loot.granted": LootGrantedEvent;
|
|
72
87
|
"inventory.added": InventoryAddedEvent;
|
|
73
88
|
"quest.accepted": QuestAcceptedEvent;
|
|
@@ -77,6 +92,7 @@ export interface GameEventMap {
|
|
|
77
92
|
"social.party.joined": SocialPartyJoinedEvent;
|
|
78
93
|
"social.party.left": SocialPartyLeftEvent;
|
|
79
94
|
"stat.levelUp": StatLevelUpEvent;
|
|
95
|
+
"projectile.settled": ProjectileSettledEvent;
|
|
80
96
|
}
|
|
81
97
|
export type GameEventHandler<TPayload> = (payload: TPayload) => void;
|
|
82
98
|
export interface GameEvents<TMap extends GameEventMap = GameEventMap> {
|
|
@@ -15,7 +15,23 @@ export interface CameraFollowState {
|
|
|
15
15
|
};
|
|
16
16
|
distance: number;
|
|
17
17
|
}
|
|
18
|
+
export interface FirstPersonCameraConfig {
|
|
19
|
+
/** Camera height above the followed entity's origin. Default 1.6. */
|
|
20
|
+
eyeHeight?: number;
|
|
21
|
+
/** Mouse-look radians per pixel of pointer movement. Default 0.0025. */
|
|
22
|
+
sensitivity?: number;
|
|
23
|
+
/** Clamp on look pitch in radians (± from horizon). Default 1.45. */
|
|
24
|
+
maxPitch?: number;
|
|
25
|
+
/** Show the centered crosshair overlay. Default true. */
|
|
26
|
+
reticle?: boolean;
|
|
27
|
+
/** Render a simple first-person weapon in view. Default true. */
|
|
28
|
+
viewmodel?: boolean;
|
|
29
|
+
}
|
|
18
30
|
export interface GameCameraConfig {
|
|
31
|
+
/** "third" mounts the orbit camera (default); "first" mounts pointer-lock mouse-look. */
|
|
32
|
+
perspective?: "third" | "first";
|
|
33
|
+
/** First-person tuning; only read when perspective is "first". */
|
|
34
|
+
firstPerson?: FirstPersonCameraConfig;
|
|
19
35
|
minDistance?: number;
|
|
20
36
|
maxDistance?: number;
|
|
21
37
|
targetHeight?: number;
|
|
@@ -41,6 +57,9 @@ export interface GameCameraConfig {
|
|
|
41
57
|
targetSmoothing?: number;
|
|
42
58
|
dragTargetSmoothing?: number;
|
|
43
59
|
distanceSmoothing?: number;
|
|
60
|
+
/** Vertical look clamp (three.js polar angle, radians): 0 = top-down over the head, PI/2 = level, >PI/2 = look up from below. Widen for top-down or vertical aim; unset keeps the standard chase feel. */
|
|
61
|
+
minPolarAngle?: number;
|
|
62
|
+
maxPolarAngle?: number;
|
|
44
63
|
}
|
|
45
64
|
export interface EntitySpriteConfig {
|
|
46
65
|
url: string;
|
|
@@ -48,6 +67,11 @@ export interface EntitySpriteConfig {
|
|
|
48
67
|
height: number;
|
|
49
68
|
y: number;
|
|
50
69
|
}
|
|
70
|
+
export interface ModelConfig {
|
|
71
|
+
url: string;
|
|
72
|
+
scale?: number;
|
|
73
|
+
y?: number;
|
|
74
|
+
}
|
|
51
75
|
export interface PlayableGame<TUi = unknown, TWorldOverlay = unknown> {
|
|
52
76
|
game: GameDefinition;
|
|
53
77
|
content: GameContextContent;
|
|
@@ -57,10 +81,18 @@ export interface PlayableGame<TUi = unknown, TWorldOverlay = unknown> {
|
|
|
57
81
|
WorldOverlay?: TWorldOverlay;
|
|
58
82
|
/** Billboard sprites keyed by entity kind name; unmatched entities get primitive markers. */
|
|
59
83
|
entitySprites?: Record<string, EntitySpriteConfig>;
|
|
84
|
+
/** GLB models keyed by entity kind name; a string resolves as an asset-catalog key via game.assets, a ModelConfig renders directly. Takes priority over sprites, then primitives. */
|
|
85
|
+
entityModels?: Record<string, string | ModelConfig>;
|
|
86
|
+
/** GLB models keyed by object catalog id; a string resolves via game.assets, a ModelConfig renders directly. Replaces the colored box when present. */
|
|
87
|
+
objectModels?: Record<string, string | ModelConfig>;
|
|
60
88
|
/** Optional scroll-selected hotbar index for primary ability (mouse0). */
|
|
61
89
|
hotbarSelection?: () => number;
|
|
62
90
|
/** Positioned proximity prompts for the interact key + HUD; single source shared with useActivePrompt. */
|
|
63
91
|
prompts?: (ctx: GameContext) => readonly PositionedPrompt[];
|
|
64
|
-
/**
|
|
92
|
+
/** Camera tuning (perspective, orbit, first-person) for the dev game player shell. */
|
|
65
93
|
camera?: GameCameraConfig;
|
|
94
|
+
/** Opt in to world-space health bars floating over non-local entities that carry the stat. */
|
|
95
|
+
worldHealthBars?: boolean | {
|
|
96
|
+
statId?: string;
|
|
97
|
+
};
|
|
66
98
|
}
|
|
@@ -63,6 +63,13 @@ export interface GameContextOptions<TAssetRef extends ModelAssetRef = ModelAsset
|
|
|
63
63
|
export interface SceneObjectContext extends ObjectStore {
|
|
64
64
|
catalog(instanceId: string): GameContextObjectEntry | null;
|
|
65
65
|
}
|
|
66
|
+
export interface FloatTextInput {
|
|
67
|
+
instanceId?: string;
|
|
68
|
+
position?: [number, number, number];
|
|
69
|
+
text?: string;
|
|
70
|
+
kind?: string;
|
|
71
|
+
amount?: number;
|
|
72
|
+
}
|
|
66
73
|
export interface SceneEntityContext {
|
|
67
74
|
spawn(name: string, options?: SpawnOptions): string;
|
|
68
75
|
despawn(instanceId: string): boolean;
|
|
@@ -70,6 +77,7 @@ export interface SceneEntityContext {
|
|
|
70
77
|
get(instanceId: string): SceneEntity | null;
|
|
71
78
|
list(): readonly SceneEntity[];
|
|
72
79
|
stats: EntityStatsApi;
|
|
80
|
+
floatText(input: FloatTextInput): void;
|
|
73
81
|
setTarget(fromId: string, toId: string | null): void;
|
|
74
82
|
getTarget(fromId: string): string | null;
|
|
75
83
|
cycleTarget(fromId: string, options?: CycleTargetOptions): string | null;
|
|
@@ -280,11 +280,67 @@ export function createGameContext(options) {
|
|
|
280
280
|
}
|
|
281
281
|
},
|
|
282
282
|
}), ["applyEffect"], signal.notify);
|
|
283
|
+
function emitFloatText(input) {
|
|
284
|
+
const position = input.position ??
|
|
285
|
+
(input.instanceId === undefined ? undefined : entities.get(input.instanceId)?.position);
|
|
286
|
+
if (position === undefined)
|
|
287
|
+
return;
|
|
288
|
+
const text = input.text ?? (input.amount === undefined ? "" : String(Math.round(input.amount)));
|
|
289
|
+
const event = {
|
|
290
|
+
position: [position[0], position[1], position[2]],
|
|
291
|
+
text,
|
|
292
|
+
kind: input.kind ?? "info",
|
|
293
|
+
};
|
|
294
|
+
if (input.instanceId !== undefined)
|
|
295
|
+
event.instanceId = input.instanceId;
|
|
296
|
+
if (input.amount !== undefined)
|
|
297
|
+
event.amount = input.amount;
|
|
298
|
+
events.emit("entity.floatText", event);
|
|
299
|
+
}
|
|
300
|
+
function applyEffectAndFloat(input) {
|
|
301
|
+
const positionsBefore = new Map();
|
|
302
|
+
for (const entity of entities.list())
|
|
303
|
+
positionsBefore.set(entity.id, entity.position);
|
|
304
|
+
const results = effects.applyEffect(input);
|
|
305
|
+
for (const result of results) {
|
|
306
|
+
let total = 0;
|
|
307
|
+
for (const delta of result.applied)
|
|
308
|
+
total += delta.delta;
|
|
309
|
+
if (total === 0)
|
|
310
|
+
continue;
|
|
311
|
+
const position = entities.get(result.instanceId)?.position ?? positionsBefore.get(result.instanceId);
|
|
312
|
+
if (position === undefined)
|
|
313
|
+
continue;
|
|
314
|
+
const magnitude = Math.abs(total);
|
|
315
|
+
emitFloatText({
|
|
316
|
+
instanceId: result.instanceId,
|
|
317
|
+
position: [position[0], position[1], position[2]],
|
|
318
|
+
text: String(Math.round(magnitude)),
|
|
319
|
+
kind: total < 0 ? "damage" : "heal",
|
|
320
|
+
amount: magnitude,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return results;
|
|
324
|
+
}
|
|
325
|
+
const floatingEffects = {
|
|
326
|
+
canReceive: effects.canReceive,
|
|
327
|
+
preview: effects.preview,
|
|
328
|
+
applyEffect: applyEffectAndFloat,
|
|
329
|
+
};
|
|
283
330
|
const projectiles = notifyAfter(createProjectileSystem({
|
|
284
|
-
effects,
|
|
331
|
+
effects: floatingEffects,
|
|
285
332
|
spatial: combatSpatial,
|
|
286
333
|
getStat: weapon.getStat,
|
|
287
334
|
now,
|
|
335
|
+
onSettle(report) {
|
|
336
|
+
events.emit("projectile.settled", {
|
|
337
|
+
from: report.from,
|
|
338
|
+
origin: [report.origin[0], report.origin[1], report.origin[2]],
|
|
339
|
+
at: [report.at[0], report.at[1], report.at[2]],
|
|
340
|
+
effect: report.effect,
|
|
341
|
+
hit: report.hit,
|
|
342
|
+
});
|
|
343
|
+
},
|
|
288
344
|
}), ["fireProjectile", "settleProjectile"], signal.notify);
|
|
289
345
|
const sceneObjects = {
|
|
290
346
|
...objects,
|
|
@@ -300,12 +356,13 @@ export function createGameContext(options) {
|
|
|
300
356
|
get: entities.get,
|
|
301
357
|
list: entities.list,
|
|
302
358
|
stats: entityStats,
|
|
359
|
+
floatText: emitFloatText,
|
|
303
360
|
setTarget: targeting.setTarget,
|
|
304
361
|
getTarget: targeting.getTarget,
|
|
305
362
|
cycleTarget: targeting.cycleTarget,
|
|
306
363
|
canReceive: effects.canReceive,
|
|
307
364
|
preview: effects.preview,
|
|
308
|
-
effect:
|
|
365
|
+
effect: applyEffectAndFloat,
|
|
309
366
|
willHitProjectile: projectiles.willHitProjectile,
|
|
310
367
|
fireProjectile: projectiles.fireProjectile,
|
|
311
368
|
settleProjectile: projectiles.settleProjectile,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
|
-
"build": "
|
|
26
|
-
"check-types": "
|
|
25
|
+
"build": "tsgo -p tsconfig.build.json && bun ../../scripts/fix-extensions.ts dist",
|
|
26
|
+
"check-types": "tsgo --noEmit -p tsconfig.json",
|
|
27
27
|
"test": "bun test src"
|
|
28
28
|
},
|
|
29
29
|
"publishConfig": {
|