@jgengine/core 0.25.0 → 0.26.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 (95) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/ai/rvo.d.ts +106 -0
  3. package/dist/ai/rvo.js +581 -0
  4. package/dist/ai/utilityBrain.d.ts +173 -0
  5. package/dist/ai/utilityBrain.js +277 -0
  6. package/dist/combat/cover.d.ts +169 -0
  7. package/dist/combat/cover.js +264 -0
  8. package/dist/combat/damageRuntime.d.ts +150 -0
  9. package/dist/combat/damageRuntime.js +208 -0
  10. package/dist/combat/damageTable.d.ts +116 -0
  11. package/dist/combat/damageTable.js +165 -0
  12. package/dist/combat/hitZones.d.ts +70 -0
  13. package/dist/combat/hitZones.js +77 -0
  14. package/dist/combat/magazine.d.ts +8 -0
  15. package/dist/combat/magazine.js +20 -2
  16. package/dist/combat/squadCombat.d.ts +167 -0
  17. package/dist/combat/squadCombat.js +557 -0
  18. package/dist/combat/squadTactics.d.ts +73 -0
  19. package/dist/combat/squadTactics.js +49 -0
  20. package/dist/combat/statusRuntime.d.ts +142 -0
  21. package/dist/combat/statusRuntime.js +236 -0
  22. package/dist/combat/weaponDefinition.d.ts +106 -0
  23. package/dist/combat/weaponDefinition.js +108 -0
  24. package/dist/combat/weaponSystem.d.ts +189 -0
  25. package/dist/combat/weaponSystem.js +469 -0
  26. package/dist/combat.d.ts +9 -0
  27. package/dist/combat.js +9 -0
  28. package/dist/devtools/devtools.d.ts +4 -2
  29. package/dist/devtools/devtools.js +2 -1
  30. package/dist/devtools/discover.d.ts +2 -1
  31. package/dist/devtools/discover.js +38 -0
  32. package/dist/devtools/frame.d.ts +0 -2
  33. package/dist/devtools/frame.js +1 -2
  34. package/dist/devtools/logBus.d.ts +58 -0
  35. package/dist/devtools/logBus.js +79 -0
  36. package/dist/devtools/logs.d.ts +6 -3
  37. package/dist/devtools/logs.js +17 -13
  38. package/dist/devtools/report.d.ts +120 -0
  39. package/dist/devtools/report.js +49 -0
  40. package/dist/devtools/reportEndpoint.d.ts +17 -0
  41. package/dist/devtools/reportEndpoint.js +7 -0
  42. package/dist/devtools/types.d.ts +48 -11
  43. package/dist/editor/document.js +210 -0
  44. package/dist/editor/environment.d.ts +48 -1
  45. package/dist/editor/environment.js +161 -0
  46. package/dist/editor/index.d.ts +6 -2
  47. package/dist/editor/index.js +1 -1
  48. package/dist/editor/types.d.ts +54 -0
  49. package/dist/gameplay.d.ts +4 -0
  50. package/dist/gameplay.js +4 -0
  51. package/dist/meta/changelog.d.ts +1 -1
  52. package/dist/meta/changelog.js +56 -1
  53. package/dist/nav/navCrowd.d.ts +138 -0
  54. package/dist/nav/navCrowd.js +412 -0
  55. package/dist/nav/navRuntime.d.ts +108 -0
  56. package/dist/nav/navRuntime.js +256 -0
  57. package/dist/render/colorPipeline.d.ts +53 -0
  58. package/dist/render/colorPipeline.js +79 -0
  59. package/dist/render/lightProbes.d.ts +97 -0
  60. package/dist/render/lightProbes.js +221 -0
  61. package/dist/render/lookPreset.js +4 -4
  62. package/dist/render/physicalSky.d.ts +39 -0
  63. package/dist/render/physicalSky.js +89 -0
  64. package/dist/render/postProcessing.d.ts +40 -8
  65. package/dist/render/postProcessing.js +1 -1
  66. package/dist/render/skyOwnedLighting.d.ts +31 -0
  67. package/dist/render/skyOwnedLighting.js +46 -0
  68. package/dist/sensor/perception.d.ts +66 -5
  69. package/dist/sensor/perception.js +167 -42
  70. package/dist/sim/nearAgents.d.ts +68 -0
  71. package/dist/sim/nearAgents.js +152 -0
  72. package/dist/sim/needs.d.ts +60 -0
  73. package/dist/sim/needs.js +108 -0
  74. package/dist/sim/offScreen.d.ts +181 -0
  75. package/dist/sim/offScreen.js +450 -0
  76. package/dist/sim/routine.d.ts +79 -0
  77. package/dist/sim/routine.js +137 -0
  78. package/dist/visibility/visibilitySystem.js +21 -0
  79. package/dist/world/features.d.ts +8 -0
  80. package/dist/world.d.ts +5 -0
  81. package/dist/world.js +5 -0
  82. package/package.json +1 -1
  83. package/skills/jgengine/api.md +59 -6
  84. package/skills/jgengine/capabilities.md +4 -0
  85. package/skills/jgengine-combat/SKILL.md +17 -0
  86. package/skills/jgengine-combat/api.md +246 -0
  87. package/skills/jgengine-combat/capabilities.md +112 -0
  88. package/skills/jgengine-combat/reference.md +32 -0
  89. package/skills/jgengine-gameplay/SKILL.md +8 -0
  90. package/skills/jgengine-gameplay/api.md +71 -1
  91. package/skills/jgengine-gameplay/capabilities.md +16 -0
  92. package/skills/jgengine-world/SKILL.md +3 -1
  93. package/skills/jgengine-world/api.md +107 -3
  94. package/skills/jgengine-world/capabilities.md +22 -2
  95. package/skills/jgengine-world/recipes/npc-senses.md +8 -2
package/dist/combat.d.ts CHANGED
@@ -6,9 +6,12 @@ export { createCastRunner } from "./combat/castRunner.js";
6
6
  export { createComboMeter, type ComboMeter, type ComboMeterConfig, type ComboMeterSnapshot, type ComboMeterView, type ComboTier, } from "./combat/comboMeter.js";
7
7
  export { createComboPoints } from "./combat/comboPoints.js";
8
8
  export { advanceCombo, createComboRunner, type ComboStep } from "./combat/comboString.js";
9
+ export { coverPointsFromSolids, coverPointsNear, createCoverField, scoreCoverPoint, selectCover, type CoverChoice, type CoverField, type CoverFieldOptions, type CoverFieldSnapshot, type CoverPoint, type CoverPointOptions, type CoverQuery, type CoverSolid, type CoverSource, type CoverThreat, type CoverWeights, } from "./combat/cover.js";
9
10
  export { MAX_INTERCEPT_STEPS, createAntiOneShotPolicy, createDamageClamp, createDamagePipeline, createImmunityWindow, resolveDamage, type AntiOneShotConfig, type AntiOneShotPolicy, type DamageClampConfig, type DamageInterceptor, type DamagePipeline, type DamageResolution, type ImmunityWindow, type InterceptContext, type InterceptDecision, type InterceptRecord, type PendingDamage, } from "./combat/damageInterceptors.js";
10
11
  export { resolveMatchup, type DamageChannelId, type DamageMatchup, type MatchupOutputs, type ResolvedMatchup, type TargetTraitId, } from "./combat/damageMatchup.js";
11
12
  export { resolveDamageHit, type DamageHitInput, type DamageHitResolution } from "./combat/damageResolution.js";
13
+ export { UnknownTargetProfileError, createDamageRuntime, type DamageRuntimeConfig, type DamageRuntimeHitInput, type DamageRuntimeResolution, type DamageRuntimeRetune, type DamageRuntimeState, type DamageStatusRequest, type TargetProfile, type TimeToKill, } from "./combat/damageRuntime.js";
14
+ export { DEFAULT_ARMOR_HALF_VALUE, DEFAULT_MAX_MITIGATION, DuplicateDamageTypeError, UnknownDamageTypeError, armorMitigation, assertDamageTable, formatDamageTable, readDamageTable, resolveTableDamage, typeBypassesArmor, typeMultiplier, type ArmorConfig, type DamageTableDef, type DamageTableRow, type DamageTypeDef, } from "./combat/damageTable.js";
12
15
  export { createDeathSystem, type OnDeathSpec } from "./combat/death.js";
13
16
  export { type DeathReason } from "./combat/deathReason.js";
14
17
  export { createDefensiveWindow, resolveDefense, type DefenseResolution } from "./combat/defensiveWindow.js";
@@ -17,6 +20,7 @@ export { createDownedState } from "./combat/downed.js";
17
20
  export { createEffectSystem, type ReceiveMap } from "./combat/effects.js";
18
21
  export { BUILTIN_COMPLETION_PREDICATES, BUILTIN_SPAWN_PROVIDERS, activePhaseId, createEncounterState, encounterProgress, forceCompletePhase, injectPhase, phaseStatus, startEncounter, updateEncounter, type CompletionPredicate, type EncounterConfig, type EncounterContext, type EncounterEvent, type EncounterPhase, type EncounterSignals, type EncounterSpawnRequest, type EncounterState, type EncounterStep, type PhaseCompletionRef, type PhaseInjectAt, type PhaseRuntime, type PhaseSpawnRef, type PhaseStatus, type PredicateContext, type SpawnProvider, type SpawnProviderContext, } from "./combat/encounterSequence.js";
19
22
  export { impactPresets, resolveHitReaction, type HitReactionConfig } from "./combat/hitReaction.js";
23
+ export { DEFAULT_CRIT_MULTIPLIER, UnknownHitZoneError, assertHitZoneTable, hitZoneMultiplier, resolveHitZone, type HitZoneDef, type HitZoneResolution, type HitZoneTable, } from "./combat/hitZones.js";
20
24
  export { createMagazine, type Magazine, type MagazineConfig, type MagazineReserve, type MagazineSnapshot, } from "./combat/magazine.js";
21
25
  export { createProjectileSystem, type ObjectRaycastHit, type ProjectileSystemDeps, type RaycastHit, } from "./combat/projectiles.js";
22
26
  export { resolveReceivedDamage, type DamageContext, type DamagePredicate, type ReceivedDamageResult, type ReceivedModifier, type ReceivedPolicy, } from "./combat/receivedDamage.js";
@@ -26,12 +30,17 @@ export { resistanceScale, resolveResistance } from "./combat/resistance.js";
26
30
  export { createResourcePool, type ResourcePool } from "./combat/resourcePool.js";
27
31
  export { allocateRewards, filterOutcomeFor, resolveClaim, type AllocationOutcome, type AllocationRequest, type ClaimOutcome, type ClaimablePool, type RewardClaimSpec, type RewardGrant, type RewardPolicyKind, type RewardRecipient, type RewardResult, type RewardShare, } from "./combat/rewardAllocation.js";
28
32
  export { DEFAULT_EYE_HEIGHT, convergeShot, eyeHeightFromColliders, resolveShot, type ResolvedShot, type ShotOriginPolicy, } from "./combat/shotOrigin.js";
33
+ export { createSquadCombat, type SquadCombat, type SquadCombatOptions, type SquadCombatSnapshot, type SquadCombatStats, type SquadCombatTunables, type SquadMemberSpec, type SquadMemberView, type SquadMode, } from "./combat/squadCombat.js";
34
+ export { assignSquadRoles, threatsFromMemory, type FireDiscipline, type SquadComposition, type SquadRole, type ThreatReadOptions, type ThreatReading, } from "./combat/squadTactics.js";
29
35
  export { resolveStatusApplication, type StatusApplicationOutcome, type StatusApplicationSpec, type StatusImmunity, type StatusInstance, type StatusStackPolicy, } from "./combat/statusApplication.js";
30
36
  export { statusEffectRemainingFraction, toStatusEffectView, toStatusEffectViews, type StatusEffectView, type StatusEffectViewOptions, } from "./combat/statusEffectView.js";
37
+ export { UnknownStatusError, createStatusRuntime, type StatusApplyOptions, type StatusDamageTick, type StatusDef, type StatusEffect, type StatusExpiredTick, type StatusRuntime, type StatusRuntimeEntry, type StatusRuntimeOptions, type StatusRuntimeState, type StatusStatTick, type StatusTickEvent, } from "./combat/statusRuntime.js";
31
38
  export { type TelegraphConfig, type TelegraphShape } from "./combat/telegraph.js";
32
39
  export { createThresholdTracker, type CrossingDirection, type CrossingPolicy, type CrossingTrigger, type ThresholdCrossing, type ThresholdCrossingConfig, type ThresholdMark, type ThresholdTracker, type ThresholdTrackerSnapshot, } from "./combat/thresholdCrossings.js";
33
40
  export { DEFAULT_VFX_PRESET, resolveVfxPreset, vfxPresets, type VfxPreset, type VfxPresetName, } from "./combat/vfxPresets.js";
41
+ export { DEFAULT_WEAPON_PELLETS_IN_FLIGHT, MAX_WEAPON_BURST_COUNT, MAX_WEAPON_PELLETS_IN_FLIGHT, MAX_WEAPON_PELLETS_PER_SHOT, assertWeaponDefinition, weaponDamageAt, weaponIntervalMs, weaponMaxInFlight, type WeaponBurstDef, type WeaponDamageDef, type WeaponDefinition, type WeaponFalloffDef, type WeaponFireMode, type WeaponHitscanDef, type WeaponMagazineDef, type WeaponProjectileDef, type WeaponVec3, } from "./combat/weaponDefinition.js";
34
42
  export { createFireCadence, createWeaponRuntime, type FireCadence, type FireCadenceConfig, type WeaponFireResult, type WeaponFireStatus, type WeaponRuntime, type WeaponRuntimeConfig, } from "./combat/weaponFire.js";
43
+ export { createWeaponSystem, type WeaponAim, type WeaponAmmo, type WeaponContact, type WeaponImpact, type WeaponImpactContext, type WeaponProjectileView, type WeaponReloadState, type WeaponShotRecord, type WeaponStats, type WeaponSystem, type WeaponSystemDeps, type WeaponSystemState, type WeaponTickResult, } from "./combat/weaponSystem.js";
35
44
  export { createAccumulatorMeter, tierAt, type MeterAddResult } from "./stats/accumulatorMeter.js";
36
45
  export { createEventMeter, type EventMeter, type EventMeterFeedResult } from "./stats/eventMeter.js";
37
46
  export { rollCheck, type CheckAdvantage, type CheckResult } from "./stats/rollCheck.js";
package/dist/combat.js CHANGED
@@ -6,9 +6,12 @@ export { createCastRunner } from "./combat/castRunner.js";
6
6
  export { createComboMeter, } from "./combat/comboMeter.js";
7
7
  export { createComboPoints } from "./combat/comboPoints.js";
8
8
  export { advanceCombo, createComboRunner } from "./combat/comboString.js";
9
+ export { coverPointsFromSolids, coverPointsNear, createCoverField, scoreCoverPoint, selectCover, } from "./combat/cover.js";
9
10
  export { MAX_INTERCEPT_STEPS, createAntiOneShotPolicy, createDamageClamp, createDamagePipeline, createImmunityWindow, resolveDamage, } from "./combat/damageInterceptors.js";
10
11
  export { resolveMatchup, } from "./combat/damageMatchup.js";
11
12
  export { resolveDamageHit } from "./combat/damageResolution.js";
13
+ export { UnknownTargetProfileError, createDamageRuntime, } from "./combat/damageRuntime.js";
14
+ export { DEFAULT_ARMOR_HALF_VALUE, DEFAULT_MAX_MITIGATION, DuplicateDamageTypeError, UnknownDamageTypeError, armorMitigation, assertDamageTable, formatDamageTable, readDamageTable, resolveTableDamage, typeBypassesArmor, typeMultiplier, } from "./combat/damageTable.js";
12
15
  export { createDeathSystem } from "./combat/death.js";
13
16
  export {} from "./combat/deathReason.js";
14
17
  export { createDefensiveWindow, resolveDefense } from "./combat/defensiveWindow.js";
@@ -17,6 +20,7 @@ export { createDownedState } from "./combat/downed.js";
17
20
  export { createEffectSystem } from "./combat/effects.js";
18
21
  export { BUILTIN_COMPLETION_PREDICATES, BUILTIN_SPAWN_PROVIDERS, activePhaseId, createEncounterState, encounterProgress, forceCompletePhase, injectPhase, phaseStatus, startEncounter, updateEncounter, } from "./combat/encounterSequence.js";
19
22
  export { impactPresets, resolveHitReaction } from "./combat/hitReaction.js";
23
+ export { DEFAULT_CRIT_MULTIPLIER, UnknownHitZoneError, assertHitZoneTable, hitZoneMultiplier, resolveHitZone, } from "./combat/hitZones.js";
20
24
  export { createMagazine, } from "./combat/magazine.js";
21
25
  export { createProjectileSystem, } from "./combat/projectiles.js";
22
26
  export { resolveReceivedDamage, } from "./combat/receivedDamage.js";
@@ -26,12 +30,17 @@ export { resistanceScale, resolveResistance } from "./combat/resistance.js";
26
30
  export { createResourcePool } from "./combat/resourcePool.js";
27
31
  export { allocateRewards, filterOutcomeFor, resolveClaim, } from "./combat/rewardAllocation.js";
28
32
  export { DEFAULT_EYE_HEIGHT, convergeShot, eyeHeightFromColliders, resolveShot, } from "./combat/shotOrigin.js";
33
+ export { createSquadCombat, } from "./combat/squadCombat.js";
34
+ export { assignSquadRoles, threatsFromMemory, } from "./combat/squadTactics.js";
29
35
  export { resolveStatusApplication, } from "./combat/statusApplication.js";
30
36
  export { statusEffectRemainingFraction, toStatusEffectView, toStatusEffectViews, } from "./combat/statusEffectView.js";
37
+ export { UnknownStatusError, createStatusRuntime, } from "./combat/statusRuntime.js";
31
38
  export {} from "./combat/telegraph.js";
32
39
  export { createThresholdTracker, } from "./combat/thresholdCrossings.js";
33
40
  export { DEFAULT_VFX_PRESET, resolveVfxPreset, vfxPresets, } from "./combat/vfxPresets.js";
41
+ export { DEFAULT_WEAPON_PELLETS_IN_FLIGHT, MAX_WEAPON_BURST_COUNT, MAX_WEAPON_PELLETS_IN_FLIGHT, MAX_WEAPON_PELLETS_PER_SHOT, assertWeaponDefinition, weaponDamageAt, weaponIntervalMs, weaponMaxInFlight, } from "./combat/weaponDefinition.js";
34
42
  export { createFireCadence, createWeaponRuntime, } from "./combat/weaponFire.js";
43
+ export { createWeaponSystem, } from "./combat/weaponSystem.js";
35
44
  export { createAccumulatorMeter, tierAt } from "./stats/accumulatorMeter.js";
36
45
  export { createEventMeter } from "./stats/eventMeter.js";
37
46
  export { rollCheck } from "./stats/rollCheck.js";
@@ -2,9 +2,11 @@ import type { TunableOptions } from "./tunableSchema.js";
2
2
  import type { Devtools, DevtoolsSnapshot, Tunable } from "./types.js";
3
3
  export type { AngleUnit, DevtoolsControlKind, DevtoolsOverrides, DiscoverySkip, OverrideApplyDiagnostic, TunableChoice, TunableInterval, TunableOptions, TunableVec2, TunableVec3, TunableVec4, ScanMeta, ScanFieldMeta, NormalizedColor, } from "./tunableSchema.js";
4
4
  export { CONTROL_SCHEMA_VERSION, OVERRIDES_FORMAT_VERSION, convertAngle, escapePathSegment, formatColor, joinTunablePath, normalizeAngle, normalizeColorValue, parseColor, parseOverridesPayload, splitTunablePath, unescapePathSegment, validateControlValue, } from "./tunableSchema.js";
5
- export type { Devtools, DevtoolsControl, DevtoolsLogEntry, DevtoolsLogLevel, DevtoolsSnapshot, DiscoveredEntry, FrameRecordSample, FrameStats, LatencyStats, LongFrameEvent, OverrideApplyResult, PhaseStats, RenderSample, Tunable, TunableAccessor, } from "./types.js";
6
- export { LONG_FRAME_MS } from "./frame.js";
5
+ export type { Devtools, DevtoolsControl, DevtoolsLogEntry, DevtoolsLogLevel, DevtoolsLogs, DevtoolsSnapshot, DiscoveredEntry, FrameRecordSample, FrameStats, LatencyStats, LongFrameEvent, OverrideApplyResult, PhaseStats, RenderSample, SystemProbe, SystemProbeReading, Tunable, TunableAccessor, } from "./types.js";
6
+ export { LONG_FRAME_MS } from "./report.js";
7
7
  export { formatLogMessage } from "./logs.js";
8
+ export type { FrameWindow, GlReport, PerfQuality, PerfReport, ProfileEntry, ProfileReport, RenderReport, ReportLogLine, ReportLogs, ReportTelemetry, SceneNodeSource, } from "./report.js";
9
+ export type { LogBusCounts, LogBusEntry, LogBusSnapshot } from "./logBus.js";
8
10
  export declare function createDevtools(): Devtools;
9
11
  export declare const devtools: Devtools;
10
12
  export declare function tunable<T>(name: string, initial: T, options?: TunableOptions<T>): Tunable<T>;
@@ -5,7 +5,7 @@ import { createControlsModule } from "./controls.js";
5
5
  import { createDiscoverModule } from "./discover.js";
6
6
  import { createOverridesModule } from "./overrides.js";
7
7
  export { CONTROL_SCHEMA_VERSION, OVERRIDES_FORMAT_VERSION, convertAngle, escapePathSegment, formatColor, joinTunablePath, normalizeAngle, normalizeColorValue, parseColor, parseOverridesPayload, splitTunablePath, unescapePathSegment, validateControlValue, } from "./tunableSchema.js";
8
- export { LONG_FRAME_MS } from "./frame.js";
8
+ export { LONG_FRAME_MS } from "./report.js";
9
9
  export { formatLogMessage } from "./logs.js";
10
10
  export function createDevtools() {
11
11
  const signal = createChangeSignal();
@@ -44,6 +44,7 @@ export function createDevtools() {
44
44
  longFrames: [...frame.frame.longFrames()],
45
45
  logs: [...logs.logs.list()],
46
46
  probes: discover.readProbes(),
47
+ systems: discover.readSystems(),
47
48
  controls: [...controls.controlRecords.values()].map((record) => ({
48
49
  name: record.name,
49
50
  kind: record.kind,
@@ -1,7 +1,7 @@
1
1
  import type { ChangeSignal } from "../store/changeSignal.js";
2
2
  import { type DevtoolsControlKind, type DiscoverySkip, type TunableOptions } from "./tunableSchema.js";
3
3
  import type { ControlRecord } from "./controls.js";
4
- import type { Devtools, Tunable } from "./types.js";
4
+ import type { Devtools, SystemProbeReading, Tunable } from "./types.js";
5
5
  /** Internal record for an auto-discovered field: identity, kind, initial value, source object, and get/set accessors. */
6
6
  export interface DiscoveredRecord {
7
7
  id: string;
@@ -20,6 +20,7 @@ export interface DiscoverModule {
20
20
  discover: Devtools["discover"];
21
21
  probes: Devtools["probes"];
22
22
  readProbes: () => Record<string, unknown>;
23
+ readSystems: () => Record<string, SystemProbeReading>;
23
24
  enableDiscovered: (id: string) => void;
24
25
  discoveredRecords: Map<string, DiscoveredRecord>;
25
26
  getDiscoverySkips: () => readonly DiscoverySkip[];
@@ -72,6 +72,7 @@ export const createDiscoverModule = (deps) => {
72
72
  const { signal, register, controlRecords } = deps;
73
73
  const discoveredRecords = new Map();
74
74
  const probeReaders = new Map();
75
+ const systemProbes = new Map();
75
76
  let boundProps = new WeakMap();
76
77
  let discoverySkips = [];
77
78
  const readProbes = () => {
@@ -86,6 +87,30 @@ export const createDiscoverModule = (deps) => {
86
87
  }
87
88
  return values;
88
89
  };
90
+ const finiteOnly = (values) => {
91
+ const out = {};
92
+ for (const [key, value] of Object.entries(values ?? {})) {
93
+ if (typeof value === "number" && Number.isFinite(value))
94
+ out[key] = value;
95
+ }
96
+ return out;
97
+ };
98
+ const readSystems = () => {
99
+ const out = {};
100
+ for (const [id, probe] of systemProbes) {
101
+ try {
102
+ out[id] = {
103
+ counters: finiteOnly(probe.counters?.()),
104
+ timers: finiteOnly(probe.timers?.()),
105
+ state: probe.state?.() ?? "",
106
+ };
107
+ }
108
+ catch (error) {
109
+ out[id] = { counters: {}, timers: {}, state: `probe failed: ${formatLogArg(error)}` };
110
+ }
111
+ }
112
+ return out;
113
+ };
89
114
  const bindDiscovered = (table, key, kind, accessor, source, options) => {
90
115
  const id = `${table}/${key}`;
91
116
  const existing = discoveredRecords.get(id);
@@ -215,6 +240,7 @@ export const createDiscoverModule = (deps) => {
215
240
  controlRecords.delete(record.id);
216
241
  }
217
242
  }
243
+ // A clear drops discovered bindings; system probes belong to their systems, not to discovery.
218
244
  discoveredRecords.clear();
219
245
  discoverySkips = [];
220
246
  boundProps = new WeakMap();
@@ -231,8 +257,20 @@ export const createDiscoverModule = (deps) => {
231
257
  };
232
258
  },
233
259
  read: readProbes,
260
+ registerSystem(id, probe) {
261
+ systemProbes.set(id, probe);
262
+ signal.notify();
263
+ return () => {
264
+ if (systemProbes.get(id) !== probe)
265
+ return;
266
+ systemProbes.delete(id);
267
+ signal.notify();
268
+ };
269
+ },
270
+ systems: readSystems,
234
271
  },
235
272
  readProbes,
273
+ readSystems,
236
274
  enableDiscovered,
237
275
  discoveredRecords,
238
276
  getDiscoverySkips: () => discoverySkips,
@@ -1,6 +1,4 @@
1
1
  import type { Devtools } from "./types.js";
2
- /** Frame duration in milliseconds above which a frame is recorded as a long-frame spike (~30fps budget). */
3
- export declare const LONG_FRAME_MS = 33.4;
4
2
  /** @internal */
5
3
  export declare function nowMs(): number;
6
4
  /** Frame subsystem exposing the frame-timing, profiling, and render-sample facades. */
@@ -1,7 +1,6 @@
1
+ import { LONG_FRAME_MS } from "./report.js";
1
2
  const FRAME_CAPACITY = 240;
2
3
  const RECENT_FRAMES = 60;
3
- /** Frame duration in milliseconds above which a frame is recorded as a long-frame spike (~30fps budget). */
4
- export const LONG_FRAME_MS = 33.4;
5
4
  const LONG_FRAME_CAPACITY = 40;
6
5
  const MAX_PHASES_PER_FRAME = 24;
7
6
  const MAX_LONG_FRAME_PHASES = 8;
@@ -0,0 +1,58 @@
1
+ import type { DevtoolsLogLevel } from "./types.js";
2
+ /** Default measured window for the log ring; older lines are evicted, never grown. */
3
+ export declare const LOG_BUS_CAPACITY = 200;
4
+ /** Warnings and errors also land in their own ring, so an error is never pushed out by info traffic. */
5
+ export declare const LOG_BUS_SEVERE_CAPACITY = 64;
6
+ /** Longest kept message; a longer one is cut once, at write time. */
7
+ export declare const LOG_BUS_MESSAGE_MAX = 400;
8
+ /** One captured line: when, how bad, which system said it, and what it said. */
9
+ export interface LogBusEntry {
10
+ at: number;
11
+ level: DevtoolsLogLevel;
12
+ /** Owning system tag, e.g. `needs`, `nav`, `render/culling`, `console`. */
13
+ system: string;
14
+ message: string;
15
+ }
16
+ /** Lifetime totals per level, plus how many lines the ring dropped. */
17
+ export interface LogBusCounts {
18
+ log: number;
19
+ info: number;
20
+ warn: number;
21
+ error: number;
22
+ }
23
+ /** Read-out of the log bus: newest-window entries, the warning/error ring, totals, and evictions. */
24
+ export interface LogBusSnapshot {
25
+ entries: readonly LogBusEntry[];
26
+ severe: readonly LogBusEntry[];
27
+ counts: LogBusCounts;
28
+ dropped: number;
29
+ capacity: number;
30
+ }
31
+ /** Tunables for the log ring: capacity, the severity ring's capacity, and the clock. */
32
+ export interface LogBusOptions {
33
+ capacity?: number;
34
+ severeCapacity?: number;
35
+ now?: () => number;
36
+ }
37
+ /**
38
+ * A fixed-capacity ring of log lines tagged by owning system. `write` reuses a
39
+ * preallocated slot — no array growth, no splice, no per-write object — so a hot
40
+ * path can call it and only the message string (already built by the caller)
41
+ * costs an allocation.
42
+ *
43
+ * @capability devtools-log-bus a bounded, per-system-tagged log ring a system writes to and a report reads
44
+ */
45
+ export interface LogBus {
46
+ write(level: DevtoolsLogLevel, system: string, message: string): void;
47
+ /** Oldest-to-newest entries currently held. Allocates once per read. */
48
+ list(): readonly LogBusEntry[];
49
+ /** Warnings and errors only, oldest first. */
50
+ severe(): readonly LogBusEntry[];
51
+ counts(): LogBusCounts;
52
+ dropped(): number;
53
+ snapshot(): LogBusSnapshot;
54
+ /** Empty every ring and counter, keeping the allocation. */
55
+ reset(): void;
56
+ }
57
+ /** Create the bounded log bus the devtools log facade writes through. */
58
+ export declare function createLogBus(options?: LogBusOptions): LogBus;
@@ -0,0 +1,79 @@
1
+ /** Default measured window for the log ring; older lines are evicted, never grown. */
2
+ export const LOG_BUS_CAPACITY = 200;
3
+ /** Warnings and errors also land in their own ring, so an error is never pushed out by info traffic. */
4
+ export const LOG_BUS_SEVERE_CAPACITY = 64;
5
+ /** Longest kept message; a longer one is cut once, at write time. */
6
+ export const LOG_BUS_MESSAGE_MAX = 400;
7
+ function blankEntry() {
8
+ return { at: 0, level: "log", system: "", message: "" };
9
+ }
10
+ function createRing(capacity) {
11
+ const slots = new Array(capacity);
12
+ for (let index = 0; index < capacity; index += 1)
13
+ slots[index] = blankEntry();
14
+ return { slots, head: 0, size: 0, total: 0 };
15
+ }
16
+ function ringPush(ring, at, level, system, message) {
17
+ const slot = ring.slots[ring.head];
18
+ slot.at = at;
19
+ slot.level = level;
20
+ slot.system = system;
21
+ slot.message = message.length > LOG_BUS_MESSAGE_MAX ? `${message.slice(0, LOG_BUS_MESSAGE_MAX)}…` : message;
22
+ ring.head = (ring.head + 1) % ring.slots.length;
23
+ if (ring.size < ring.slots.length)
24
+ ring.size += 1;
25
+ ring.total += 1;
26
+ }
27
+ function ringRead(ring) {
28
+ const out = new Array(ring.size);
29
+ const start = (ring.head - ring.size + ring.slots.length) % ring.slots.length;
30
+ for (let offset = 0; offset < ring.size; offset += 1) {
31
+ const slot = ring.slots[(start + offset) % ring.slots.length];
32
+ out[offset] = { at: slot.at, level: slot.level, system: slot.system, message: slot.message };
33
+ }
34
+ return out;
35
+ }
36
+ /** Create the bounded log bus the devtools log facade writes through. */
37
+ export function createLogBus(options = {}) {
38
+ const capacity = Math.max(1, Math.floor(options.capacity ?? LOG_BUS_CAPACITY));
39
+ const severeCapacity = Math.max(1, Math.floor(options.severeCapacity ?? LOG_BUS_SEVERE_CAPACITY));
40
+ const now = options.now ?? Date.now;
41
+ const ring = createRing(capacity);
42
+ const severeRing = createRing(Math.min(severeCapacity, capacity));
43
+ const totals = { log: 0, info: 0, warn: 0, error: 0 };
44
+ return {
45
+ write(level, system, message) {
46
+ const at = now();
47
+ const text = message.length > LOG_BUS_MESSAGE_MAX ? `${message.slice(0, LOG_BUS_MESSAGE_MAX)}…` : message;
48
+ ringPush(ring, at, level, system, text);
49
+ totals[level] += 1;
50
+ if (level === "warn" || level === "error")
51
+ ringPush(severeRing, at, level, system, text);
52
+ },
53
+ list: () => ringRead(ring),
54
+ severe: () => ringRead(severeRing),
55
+ counts: () => ({ ...totals }),
56
+ dropped: () => Math.max(0, ring.total - ring.size),
57
+ snapshot() {
58
+ return {
59
+ entries: ringRead(ring),
60
+ severe: ringRead(severeRing),
61
+ counts: { ...totals },
62
+ dropped: Math.max(0, ring.total - ring.size),
63
+ capacity: ring.slots.length,
64
+ };
65
+ },
66
+ reset() {
67
+ ring.head = 0;
68
+ ring.size = 0;
69
+ ring.total = 0;
70
+ severeRing.head = 0;
71
+ severeRing.size = 0;
72
+ severeRing.total = 0;
73
+ totals.log = 0;
74
+ totals.info = 0;
75
+ totals.warn = 0;
76
+ totals.error = 0;
77
+ },
78
+ };
79
+ }
@@ -1,13 +1,16 @@
1
1
  import type { ChangeSignal } from "../store/changeSignal.js";
2
- import type { Devtools } from "./types.js";
2
+ import type { DevtoolsLogs, DevtoolsLatency } from "./types.js";
3
+ import { type LogBus } from "./logBus.js";
3
4
  /** @internal */
4
5
  export declare function formatLogArg(arg: unknown): string;
5
6
  /** Format an array of console arguments into a single truncated log message string. */
6
7
  export declare function formatLogMessage(args: readonly unknown[]): string;
7
8
  /** Logs subsystem exposing the log-capture and latency-tracking facades. */
8
9
  export interface LogsModule {
9
- logs: Devtools["logs"];
10
- latency: Devtools["latency"];
10
+ logs: DevtoolsLogs;
11
+ latency: DevtoolsLatency;
12
+ /** The ring the facade writes through, for a reporter that needs the whole window at once. */
13
+ bus: LogBus;
11
14
  }
12
15
  /** Create the logs subsystem that buffers captured console output and records latency samples. */
13
16
  export declare const createLogsModule: (deps: {
@@ -1,6 +1,5 @@
1
+ import { createLogBus, LOG_BUS_MESSAGE_MAX } from "./logBus.js";
1
2
  const LATENCY_CAPACITY = 120;
2
- const LOG_CAPACITY = 200;
3
- const LOG_MESSAGE_MAX = 400;
4
3
  /** @internal */
5
4
  export function formatLogArg(arg) {
6
5
  if (typeof arg === "string")
@@ -17,19 +16,14 @@ export function formatLogArg(arg) {
17
16
  /** Format an array of console arguments into a single truncated log message string. */
18
17
  export function formatLogMessage(args) {
19
18
  const message = args.map(formatLogArg).join(" ");
20
- return message.length > LOG_MESSAGE_MAX ? `${message.slice(0, LOG_MESSAGE_MAX)}…` : message;
19
+ return message.length > LOG_BUS_MESSAGE_MAX ? `${message.slice(0, LOG_BUS_MESSAGE_MAX)}…` : message;
21
20
  }
22
21
  /** Create the logs subsystem that buffers captured console output and records latency samples. */
23
22
  export const createLogsModule = (deps) => {
24
23
  const { signal } = deps;
25
- const logEntries = [];
24
+ const bus = createLogBus();
26
25
  let consoleCaptured = false;
27
26
  const latencySamples = [];
28
- const pushLog = (level, message) => {
29
- logEntries.push({ at: Date.now(), level, message });
30
- if (logEntries.length > LOG_CAPACITY)
31
- logEntries.splice(0, logEntries.length - LOG_CAPACITY);
32
- };
33
27
  const latencyStats = () => {
34
28
  if (latencySamples.length === 0)
35
29
  return null;
@@ -51,7 +45,12 @@ export const createLogsModule = (deps) => {
51
45
  };
52
46
  return {
53
47
  logs: {
54
- push: pushLog,
48
+ write(level, system, message) {
49
+ bus.write(level, system, message);
50
+ },
51
+ push(level, message, system = "console") {
52
+ bus.write(level, system, message);
53
+ },
55
54
  captureConsole() {
56
55
  if (consoleCaptured)
57
56
  return;
@@ -63,14 +62,18 @@ export const createLogsModule = (deps) => {
63
62
  for (const level of levels) {
64
63
  const original = globalConsole[level].bind(globalConsole);
65
64
  globalConsole[level] = (...args) => {
66
- pushLog(level, formatLogMessage(args));
65
+ bus.write(level, "console", formatLogMessage(args));
67
66
  original(...args);
68
67
  };
69
68
  }
70
69
  },
71
- list: () => logEntries,
70
+ list: () => bus.list(),
71
+ severe: () => bus.severe(),
72
+ counts: () => bus.counts(),
73
+ dropped: () => bus.dropped(),
74
+ snapshot: () => bus.snapshot(),
72
75
  clear() {
73
- logEntries.length = 0;
76
+ bus.reset();
74
77
  signal.notify();
75
78
  },
76
79
  },
@@ -82,5 +85,6 @@ export const createLogsModule = (deps) => {
82
85
  },
83
86
  stats: latencyStats,
84
87
  },
88
+ bus,
85
89
  };
86
90
  };
@@ -0,0 +1,120 @@
1
+ import type { LogBusCounts, LogBusSnapshot } from "./logBus.js";
2
+ import type { DevtoolsLogLevel, SystemProbeReading } from "./types.js";
3
+ /**
4
+ * The one perf record shape. `bun run perf` (CDP, out of process) and the in-page
5
+ * telemetry report (`window.__JG.report()`, the dev-server route, `bun run debug`)
6
+ * both produce exactly this object, so a budget check and a diff read either one.
7
+ */
8
+ /** Frame duration above which a frame counts as a spike (~30 fps). */
9
+ export declare const LONG_FRAME_MS = 33.4;
10
+ /** Graphics tier the record was measured at. */
11
+ export type PerfQuality = "full" | "draft";
12
+ /** Where the scene-node count came from: the live scene graph, a heap walk, or nowhere. */
13
+ export type SceneNodeSource = "scene-graph" | "heap-object3d" | "unavailable";
14
+ /** Frame timings over the measured window, as the player sees them. */
15
+ export interface FrameWindow {
16
+ fps: number;
17
+ avgMs: number;
18
+ p95Ms: number;
19
+ maxMs: number;
20
+ samples: number;
21
+ longFrames: number;
22
+ }
23
+ /** Renderer-side counts for the measured window; `null` where the source could not read them. */
24
+ export interface RenderReport {
25
+ draws: number | null;
26
+ triangles: number | null;
27
+ geometries: number | null;
28
+ textures: number | null;
29
+ nodes: number | null;
30
+ nodesSource: SceneNodeSource;
31
+ scenes: number;
32
+ }
33
+ /** GPU-object creation counts. `total` is since the page loaded, the named fields since warmup. */
34
+ export interface GlReport {
35
+ programsLinked: number;
36
+ framebuffers: number;
37
+ buffers: number;
38
+ textures: number;
39
+ vertexArrays: number;
40
+ allocations: number;
41
+ residentPrograms: number | null;
42
+ total: Record<string, number>;
43
+ /** True once the counters provably wrapped the WebGL calls; false means a zero is not evidence. */
44
+ attached: boolean;
45
+ }
46
+ /** One sampled stack frame reduced to a readable key and its self time. */
47
+ export interface ProfileEntry {
48
+ fn: string;
49
+ at: string;
50
+ selfMs: number;
51
+ selfPct: number;
52
+ }
53
+ /** CPU profile summary; only the CDP harness can produce one. */
54
+ export interface ProfileReport {
55
+ samplingUs: number;
56
+ seconds: number;
57
+ top: readonly ProfileEntry[];
58
+ }
59
+ /** One log line as it appears in the report. */
60
+ export interface ReportLogLine {
61
+ at: number;
62
+ level: DevtoolsLogLevel;
63
+ system: string;
64
+ message: string;
65
+ }
66
+ /** Bounded log read-out carried by the report: warn/error lines survive even a flood of info logs. */
67
+ export interface ReportLogs {
68
+ counts: LogBusCounts;
69
+ dropped: number;
70
+ severe: readonly ReportLogLine[];
71
+ }
72
+ /** Who produced the report, and how much producing it cost. */
73
+ export interface ReportTelemetry {
74
+ source: "in-page" | "cdp";
75
+ /** True once the counters have a post-warmup baseline, which the link/framebuffer fields need. */
76
+ warmedUp: boolean;
77
+ /** Milliseconds since the sampler started. */
78
+ ageMs: number;
79
+ /** Milliseconds the report itself took to gather. */
80
+ buildMs: number;
81
+ heapBytes: number | null;
82
+ }
83
+ /** One measured window of one game: the record `bun run perf`, `window.__JG.report()` and `bun run debug` print. */
84
+ export interface PerfReport {
85
+ game: string;
86
+ gpu: string;
87
+ quality: PerfQuality;
88
+ viewport: string;
89
+ /** Length of the measured frame window in seconds. */
90
+ seconds: number;
91
+ /** Wall-clock timestamp when the record was taken. */
92
+ at: number;
93
+ frame: FrameWindow;
94
+ render: RenderReport;
95
+ gl: GlReport;
96
+ sim: {
97
+ avgMs: number;
98
+ outsideMs: number;
99
+ } | null;
100
+ profile: ProfileReport | null;
101
+ /** Per-system probe readings, keyed by system id (`render/culling`, `needs`, …). */
102
+ systems: Record<string, SystemProbeReading>;
103
+ logs: ReportLogs | null;
104
+ telemetry: ReportTelemetry | null;
105
+ }
106
+ /** Round to `digits` decimals so two runs of different lengths print comparably. @internal */
107
+ export declare function roundReport(value: number, digits?: number): number;
108
+ /**
109
+ * Reduce raw frame deltas to {@link FrameWindow}. One implementation, so the CDP
110
+ * harness and the in-page sampler cannot disagree about fps or p95.
111
+ * @internal
112
+ */
113
+ export declare function summarizeFrameWindow(samples: ArrayLike<number>): FrameWindow;
114
+ /**
115
+ * Project a log-bus read-out onto the report's compact shape: totals plus the last
116
+ * twelve warning/error lines. A thrown error or a lost GL context therefore survives
117
+ * in the report even when hundreds of info lines followed it.
118
+ * @internal
119
+ */
120
+ export declare function toReportLogs(snapshot: LogBusSnapshot, severeLimit?: number): ReportLogs;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The one perf record shape. `bun run perf` (CDP, out of process) and the in-page
3
+ * telemetry report (`window.__JG.report()`, the dev-server route, `bun run debug`)
4
+ * both produce exactly this object, so a budget check and a diff read either one.
5
+ */
6
+ /** Frame duration above which a frame counts as a spike (~30 fps). */
7
+ export const LONG_FRAME_MS = 33.4;
8
+ /** Round to `digits` decimals so two runs of different lengths print comparably. @internal */
9
+ export function roundReport(value, digits = 2) {
10
+ const scale = 10 ** digits;
11
+ return Math.round(value * scale) / scale;
12
+ }
13
+ /**
14
+ * Reduce raw frame deltas to {@link FrameWindow}. One implementation, so the CDP
15
+ * harness and the in-page sampler cannot disagree about fps or p95.
16
+ * @internal
17
+ */
18
+ export function summarizeFrameWindow(samples) {
19
+ const kept = [];
20
+ for (let index = 0; index < samples.length; index += 1) {
21
+ const ms = samples[index];
22
+ if (Number.isFinite(ms) && ms > 0)
23
+ kept.push(ms);
24
+ }
25
+ if (kept.length === 0)
26
+ return { fps: 0, avgMs: 0, p95Ms: 0, maxMs: 0, samples: 0, longFrames: 0 };
27
+ const sorted = [...kept].sort((a, b) => a - b);
28
+ const avgMs = kept.reduce((sum, ms) => sum + ms, 0) / kept.length;
29
+ return {
30
+ fps: roundReport(1000 / avgMs, 1),
31
+ avgMs: roundReport(avgMs),
32
+ p95Ms: roundReport(sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))]),
33
+ maxMs: roundReport(sorted[sorted.length - 1]),
34
+ samples: kept.length,
35
+ longFrames: kept.filter((ms) => ms > LONG_FRAME_MS).length,
36
+ };
37
+ }
38
+ /**
39
+ * Project a log-bus read-out onto the report's compact shape: totals plus the last
40
+ * twelve warning/error lines. A thrown error or a lost GL context therefore survives
41
+ * in the report even when hundreds of info lines followed it.
42
+ * @internal
43
+ */
44
+ export function toReportLogs(snapshot, severeLimit = 12) {
45
+ const severe = snapshot.severe
46
+ .slice(-severeLimit)
47
+ .map((entry) => ({ at: entry.at, level: entry.level, system: entry.system, message: entry.message }));
48
+ return { counts: snapshot.counts, dropped: snapshot.dropped, severe };
49
+ }
@@ -0,0 +1,17 @@
1
+ import type { PerfReport } from "./report.js";
2
+ /**
3
+ * The dev-only route a running game pushes its in-page report to, and that
4
+ * `bun run debug <game>` reads back. One process: the page gathers, the dev server
5
+ * holds the newest record per game, the CLI fetches it. Nothing here ships in a
6
+ * production build — the Vite plugin mounts it under `apply: "serve"`.
7
+ */
8
+ export declare const REPORT_ENDPOINT_PATH = "/__jgengine/report";
9
+ /** Body the page POSTs: the game it measured and the record itself. */
10
+ export interface ReportPushBody {
11
+ gameId: string;
12
+ report: PerfReport;
13
+ }
14
+ /** Body sent when there is nothing to return yet. */
15
+ export interface ReportRouteError {
16
+ error: string;
17
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The dev-only route a running game pushes its in-page report to, and that
3
+ * `bun run debug <game>` reads back. One process: the page gathers, the dev server
4
+ * holds the newest record per game, the CLI fetches it. Nothing here ships in a
5
+ * production build — the Vite plugin mounts it under `apply: "serve"`.
6
+ */
7
+ export const REPORT_ENDPOINT_PATH = "/__jgengine/report";