@jgengine/core 0.26.0 → 0.28.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 (143) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/dist/ai/policeResponse.d.ts +253 -0
  3. package/dist/ai/policeResponse.js +421 -0
  4. package/dist/anim/actionLayer.d.ts +47 -0
  5. package/dist/anim/actionLayer.js +63 -0
  6. package/dist/anim/ikSolver.d.ts +14 -0
  7. package/dist/anim/ikSolver.js +37 -0
  8. package/dist/anim/locomotionGraph.d.ts +58 -3
  9. package/dist/anim/locomotionGraph.js +142 -15
  10. package/dist/anim/ragdollRig.d.ts +53 -0
  11. package/dist/anim/ragdollRig.js +132 -0
  12. package/dist/anim/skeletonStandard.d.ts +140 -0
  13. package/dist/anim/skeletonStandard.js +326 -0
  14. package/dist/combat/damageRuntime.d.ts +14 -1
  15. package/dist/combat/damageRuntime.js +62 -4
  16. package/dist/combat/death.js +7 -3
  17. package/dist/combat/deathReason.d.ts +7 -1
  18. package/dist/combat/effects.d.ts +25 -0
  19. package/dist/combat/effects.js +16 -0
  20. package/dist/combat/statusRuntime.d.ts +2 -0
  21. package/dist/combat/statusRuntime.js +1 -0
  22. package/dist/economy/loanBook.d.ts +182 -0
  23. package/dist/economy/loanBook.js +303 -0
  24. package/dist/economy/market.d.ts +161 -0
  25. package/dist/economy/market.js +248 -0
  26. package/dist/economy/transactionLedger.d.ts +162 -0
  27. package/dist/economy/transactionLedger.js +301 -0
  28. package/dist/editor/creatorStorage.js +3 -1
  29. package/dist/editor/simulation.d.ts +5 -2
  30. package/dist/editor/simulation.js +29 -19
  31. package/dist/game/cameraConfig.d.ts +14 -0
  32. package/dist/game/clipRoles.d.ts +5 -4
  33. package/dist/game/clipRoles.js +34 -4
  34. package/dist/game/events.d.ts +27 -0
  35. package/dist/game/gameSave.d.ts +142 -0
  36. package/dist/game/gameSave.js +174 -0
  37. package/dist/game/playableGame.d.ts +74 -6
  38. package/dist/game/saveSlotIndex.d.ts +54 -0
  39. package/dist/game/saveSlotIndex.js +71 -0
  40. package/dist/game/saveSlots.d.ts +2 -0
  41. package/dist/game/saveSlots.js +1 -1
  42. package/dist/game/saveStore.d.ts +8 -0
  43. package/dist/game/saveStore.js +16 -0
  44. package/dist/gameplay.d.ts +17 -2
  45. package/dist/gameplay.js +15 -0
  46. package/dist/inventory/carryLoad.d.ts +48 -0
  47. package/dist/inventory/carryLoad.js +44 -0
  48. package/dist/inventory/equipment.d.ts +95 -0
  49. package/dist/inventory/equipment.js +169 -0
  50. package/dist/inventory/inventoryModel.d.ts +15 -6
  51. package/dist/inventory/inventoryModel.js +54 -5
  52. package/dist/inventory/shapedGrid.d.ts +26 -0
  53. package/dist/inventory/shapedGrid.js +47 -0
  54. package/dist/loot/lootGen.d.ts +186 -0
  55. package/dist/loot/lootGen.js +437 -0
  56. package/dist/loot/lootWeapon.d.ts +35 -0
  57. package/dist/loot/lootWeapon.js +70 -0
  58. package/dist/meta/changelog.d.ts +1 -1
  59. package/dist/meta/changelog.js +156 -1
  60. package/dist/mission/compositions.d.ts +85 -0
  61. package/dist/mission/compositions.js +130 -0
  62. package/dist/mission/mission.d.ts +435 -0
  63. package/dist/mission/mission.js +770 -0
  64. package/dist/mission/triggers.d.ts +61 -0
  65. package/dist/mission/triggers.js +108 -0
  66. package/dist/nav/laneGraph.d.ts +178 -0
  67. package/dist/nav/laneGraph.js +671 -0
  68. package/dist/nav/traffic.d.ts +222 -0
  69. package/dist/nav/traffic.js +918 -0
  70. package/dist/physics/kinematicVehicle.d.ts +36 -0
  71. package/dist/physics/kinematicVehicle.js +15 -1
  72. package/dist/physics/vehicleDamage.d.ts +136 -0
  73. package/dist/physics/vehicleDamage.js +214 -0
  74. package/dist/physics/vehicleParts.d.ts +62 -0
  75. package/dist/physics/vehicleParts.js +65 -0
  76. package/dist/progression/gearScore.d.ts +56 -0
  77. package/dist/progression/gearScore.js +59 -0
  78. package/dist/progression/skillTree.d.ts +129 -0
  79. package/dist/progression/skillTree.js +265 -0
  80. package/dist/progression/xpCurve.d.ts +60 -0
  81. package/dist/progression/xpCurve.js +141 -0
  82. package/dist/runtime/context/combat.js +11 -0
  83. package/dist/scene/builtinSceneKinds.js +2 -0
  84. package/dist/settings/accessibilitySettings.d.ts +81 -0
  85. package/dist/settings/accessibilitySettings.js +152 -0
  86. package/dist/settings/graphicsProfile.d.ts +3 -0
  87. package/dist/settings/graphicsProfile.js +4 -0
  88. package/dist/settings/settingsModel.d.ts +1 -1
  89. package/dist/settings/settingsModel.js +1 -0
  90. package/dist/sim/nearAgents.d.ts +8 -1
  91. package/dist/sim/nearAgents.js +34 -17
  92. package/dist/sim/offScreen.d.ts +2 -1
  93. package/dist/sim/offScreen.js +14 -3
  94. package/dist/sim/population.d.ts +147 -0
  95. package/dist/sim/population.js +182 -0
  96. package/dist/sim/queue.d.ts +90 -0
  97. package/dist/sim/queue.js +171 -0
  98. package/dist/sim/rides.d.ts +287 -0
  99. package/dist/sim/rides.js +471 -0
  100. package/dist/sim/routine.js +19 -11
  101. package/dist/ui/loadingTips.d.ts +41 -0
  102. package/dist/ui/loadingTips.js +64 -0
  103. package/dist/ui/menuNav.d.ts +79 -0
  104. package/dist/ui/menuNav.js +147 -0
  105. package/dist/ui/menuTransition.d.ts +50 -0
  106. package/dist/ui/menuTransition.js +67 -0
  107. package/dist/ui.d.ts +2 -0
  108. package/dist/ui.js +2 -0
  109. package/dist/vfx/effectLibrary.d.ts +98 -0
  110. package/dist/vfx/effectLibrary.js +181 -0
  111. package/dist/vfx/particleBudget.d.ts +40 -0
  112. package/dist/vfx/particleBudget.js +39 -0
  113. package/dist/vfx/particleDirector.d.ts +16 -0
  114. package/dist/vfx/particleDirector.js +18 -0
  115. package/dist/world/authoredSimulation.js +7 -1
  116. package/dist/world/captureCamera.d.ts +30 -0
  117. package/dist/world/captureCamera.js +19 -0
  118. package/dist/world/decalKind.d.ts +48 -0
  119. package/dist/world/decalKind.js +86 -0
  120. package/dist/world/features.d.ts +18 -5
  121. package/dist/world/features.js +1 -0
  122. package/dist/world/grassKind.d.ts +5 -0
  123. package/dist/world/grassKind.js +3 -0
  124. package/dist/world/terraform.d.ts +8 -0
  125. package/dist/world/terrain.d.ts +1 -0
  126. package/dist/world/terrain.js +2 -0
  127. package/dist/world.d.ts +7 -2
  128. package/dist/world.js +5 -0
  129. package/package.json +1 -1
  130. package/skills/jgengine/api.md +2 -2
  131. package/skills/jgengine-combat/SKILL.md +9 -1
  132. package/skills/jgengine-combat/api.md +5 -4
  133. package/skills/jgengine-combat/reference.md +4 -0
  134. package/skills/jgengine-gameplay/SKILL.md +86 -0
  135. package/skills/jgengine-gameplay/api.md +485 -16
  136. package/skills/jgengine-gameplay/capabilities.md +131 -4
  137. package/skills/jgengine-gameplay/reference-systems.md +118 -0
  138. package/skills/jgengine-world/SKILL.md +8 -0
  139. package/skills/jgengine-world/api.md +243 -11
  140. package/skills/jgengine-world/capabilities.md +61 -1
  141. package/skills/jgengine-world/recipes/character-animation.md +25 -2
  142. package/skills/jgengine-world/recipes/vehicle-feel.md +5 -0
  143. package/skills/jgengine-world/reference.md +1 -1
@@ -7,10 +7,13 @@ export { advanceTransport, createProductionState, drainOutput, feedProduction, p
7
7
  export { canCraft, craft, craftSeconds, createRecipeGraph, stationSatisfied, type RecipeDef, type RecipeItem, } from "./crafting/recipe.js";
8
8
  export { createAuctionBook, type Auction, type AuctionSettlement } from "./economy/auctionBook.js";
9
9
  export { createListingBook, type Listing } from "./economy/listingBook.js";
10
+ export { buildLoanSchedule, createLoanBook, type LoanAdvanceResult, type LoanBook, type LoanBookOptions, type LoanBookState, type LoanBookStats, type LoanPayment, type LoanRecord, type LoanRepayment, type LoanSchedule, type LoanState, type LoanTerms, } from "./economy/loanBook.js";
11
+ export { createMarket, type Market, type MarketConfig, type MarketGoodDef, type MarketGoodState, type MarketPriceInput, type MarketQuote, type MarketSale, type MarketSaleRejection, type MarketSaleResult, type MarketSnapshot, type MarketStats, } from "./economy/market.js";
10
12
  export { createPriceHistory, type PriceStats, type SaleRecord } from "./economy/priceHistory.js";
11
13
  export { addScheduledRule, advanceLedger, annotate, balanceOf, cancelRule, capAmount, createResourceLedger, curveScale, pauseRule, redirect, rejectWhen, resumeRule, taxFraction, thresholdScale, type AdvanceOptions, type AdvanceResult, type AppliedTransaction, type CatchUpPolicy, type LedgerEvent, type LedgerEventKind, type PolicyContext, type PolicyRead, type Precision, type ResourceLedger, type ResourcePolicy, type ResourceTransaction, type RuleCursor, type ScheduledRule, type ThresholdBand, } from "./economy/resourceLedger.js";
12
14
  export { createShopStock, type ShopBuyRejection, type ShopBuyResult, type ShopPrice, type ShopSellRejection, type ShopSellResult, type ShopStock, type ShopStockConfig, type ShopStockEntry, type ShopStockSnapshot, } from "./economy/shopStock.js";
13
15
  export { type TechNodeDef } from "./economy/techTree.js";
16
+ export { createTransactionLedger, type ChargeCursor, type LedgerAdvanceResult, type LedgerEntry, type LedgerEntryFilter, type LedgerPostInput, type LedgerPostResult, type LedgerState, type LedgerStats, type LedgerTotals, type RecurringCharge, type TransactionLedger, type TransactionLedgerOptions, } from "./economy/transactionLedger.js";
14
17
  export { balance, canAfford, charge, chargeAll, createEmptyWallet, grant, isOverdrawn, type ChargeOptions, type ChargeResult, type Overdraft, } from "./economy/wallet.js";
15
18
  export { createAchievementTracker, type AchievementDef, type AchievementSnapshot, type AchievementTracker, type AchievementTrackerOptions, type AchievementUnlock, type AchievementView, } from "./game/achievements.js";
16
19
  export { DEFAULT_CHAT_BODY_LENGTH, DEFAULT_CHAT_HISTORY_LIMIT, DEFAULT_CHAT_RATE_LIMIT, createChatRateLimiter, type Chat, type ChatMessage, type ChatRateLimit, type ChatSendResult, } from "./game/chat.js";
@@ -25,6 +28,7 @@ export { createEventTicker, type EventTicker, type EventTickerEntry, type EventT
25
28
  export { createGameEvents, type CombatTelegraphEvent, type CombatVfxEvent, type EntityDiedEvent, type EntityFloatTextEvent, type GameEventMap, type GameEvents, type RetainedVfxKind, type StatLevelUpEvent, type VfxKind, type VfxRef, } from "./game/events.js";
26
29
  export { appendFeed, createGameFeed, pruneFeed, type FeedEntry, type FeedWindow, type TimedFeedEntry, } from "./game/feed.js";
27
30
  export { gamePhase, setGamePhase, type GamePhase } from "./game/gamePhase.js";
31
+ export { GAME_SAVE_VERSION, createGameSave, gameSaveDigest, type GameSave, type GameSaveConfig, type GameSaveDocument, type GameSaveRng, type GameSaveRngState, type GameSaveSystem, } from "./game/gameSave.js";
28
32
  export { createKeyValueStore, type KeyValueStorage } from "./game/keyValueStore.js";
29
33
  export { createLeaderboard, type LeaderboardRow, type LeaderboardScope } from "./game/leaderboard.js";
30
34
  export { medalFor, rankLeaderboard, type RankLeaderboardOptions, type RankableRow, type RankedEntry, type TieMode, } from "./game/leaderboardRank.js";
@@ -39,7 +43,7 @@ export { createNotificationCenter, type NotificationCenterOptions, type Notifica
39
43
  export { evaluateObjective } from "./game/objectives.js";
40
44
  export { createLayerRegistry, diffParams, orderLayers, resolveParams, resolveSelection, validateLayers, type LayerConflict, type LayerOps, type LayerRegistry, type LayerSelection, type ParamContribution, type ParamDelta, type ParamLayer, type ParamOp, type ParamSnapshot, } from "./game/paramLayers.js";
41
45
  export { DEFAULT_PING_CATEGORIES, PING_FEED_ACTION, createPingSystem, type PingCategory, type PingSystem, } from "./game/ping.js";
42
- export { CAMERA_FRUSTUM_DEFAULTS, worldHealthBarAllowsRole, type BackdropConfig, type CameraKeyframe, type CameraRigKind, type ChaseCameraConfig, type CinematicCameraConfig, type DirectionalLightingConfig, type EntitySpriteConfig, type FirstPersonCameraConfig, type GameCameraConfig, type InspectionCameraConfig, type InspectionZoomAnchor, type LightingConfig, type LockOnCameraConfig, type ModelConfig, type ModelMaterialMaps, type ModelMaterialOverride, type ObjectStyle, type ObserverCameraConfig, type PlayableGame, type PointerConfig, type RtsCameraConfig, type ShoulderCameraConfig, type SideScrollCameraConfig, type TopDownCameraConfig, type WorldItemRenderConfig, type WorldOverlayProps, } from "./game/playableGame.js";
46
+ export { CAMERA_FRUSTUM_DEFAULTS, worldHealthBarAllowsRole, type BackdropConfig, type CameraKeyframe, type CameraRigKind, type ChaseCameraConfig, type CinematicCameraConfig, type DirectionalLightingConfig, type EntitySpriteConfig, type FirstPersonCameraConfig, type FirstPersonCollisionConfig, type GameCameraConfig, type InspectionCameraConfig, type InspectionZoomAnchor, type LightingConfig, type LockOnCameraConfig, type ModelConfig, type ModelMaterialMaps, type ModelMaterialOverride, type ObjectStyle, type ObserverCameraConfig, type PlayableGame, type PointerConfig, type RtsCameraConfig, type ShoulderCameraConfig, type SideScrollCameraConfig, type TopDownCameraConfig, type WorldItemRenderConfig, type WorldOverlayProps, } from "./game/playableGame.js";
43
47
  export { curve, evalCurve, leveling, type Curve, type LevelProgress, type LevelingConfig, type LevelingTrack, } from "./game/progression.js";
44
48
  export { createQuestJournal, defaultObjectiveLabel, describeTrackedQuest, type QuestDef, type QuestInstance, type QuestRewards, type TrackedObjectiveView, type TrackedQuestView, } from "./game/quest.js";
45
49
  export { RaceState, createLapTimer, createRaceState, finishRaceSession, firstPastPost, idleRaceSession, lapDurations, parDelta, placementOf, raceOutcomeOf, racePlacements, raceTrack, splitSegments, startRaceCountdown, tickRaceSession, } from "./game/race.js";
@@ -47,7 +51,7 @@ export { createRecordBook } from "./game/recordBook.js";
47
51
  export { createRuleRegistry, rerollRules, selectRules, type RuleDef, type RuleRegistry, type RuleSelection, type RuleSelectionConfig, } from "./game/ruleSelection.js";
48
52
  export { createRunDraft, type RunDraft, type RunModifierOffer } from "./game/runDraft.js";
49
53
  export { createSaveSlots, type SaveSlotMeta, type SaveSlotWrite, type SaveSlots, type SaveSlotsConfig, type SaveSlotsSnapshot, } from "./game/saveSlots.js";
50
- export { createSaveStore, localSaveBackend, memorySaveBackend, remoteSaveBackend, type SaveBackend, type SaveStatus, type SaveStore, } from "./game/saveStore.js";
54
+ export { createSaveStore, localSaveBackend, memorySaveBackend, remoteSaveBackend, type SaveAutoConfig, type SaveBackend, type SaveStatus, type SaveStore, type SaveTimers, } from "./game/saveStore.js";
51
55
  export { createSocial, type FriendEntry, type FriendRequestEntry, type Friends, type Party, type PartyInviteEntry, type PartyMemberEntry, type PresenceInfo, type Social, type SocialDeps, type WorldInvite, type WorldInviteTarget, } from "./game/social.js";
52
56
  export { createSpawnPoints } from "./game/spawnPoints.js";
53
57
  export { composeGameLoop } from "./game/systemRuntime.js";
@@ -81,7 +85,15 @@ export { computeEffectiveStats, createModularItem, install, isComplete, missingR
81
85
  export { createItemUse, type ItemUseHandler, type ItemUseInput } from "./item/use.js";
82
86
  export { createUseBehaviorRegistry, type BehaviorConfig, type BehaviorState, type ComposedUse, type CompositionResult, type SerializedBehaviorState, type UseBehaviorContext, type UseBehaviorDef, type UseBehaviorOutcome, type UseBehaviorRef, type UseBehaviorRegistry, type UseBehaviorRejection, } from "./item/useBehavior.js";
83
87
  export { createWeaponStats } from "./item/weapon.js";
88
+ export { MAX_LOOT_AFFIXES, assertLootCatalog, compareLootItems, createLootRoller, lootLevelScale, lootStatContributions, rollLoot, scoreLootStats, validateLootItem, type LootBaseDef, type LootCatalog, type LootComparison, type LootComparisonOptions, type LootInstalledPart, type LootIssueCode, type LootItem, type LootManufacturerDef, type LootPartDef, type LootRollOptions, type LootRoller, type LootRollerOptions, type LootRollerState, type LootStatDelta, type LootValidationIssue, } from "./loot/lootGen.js";
89
+ export { lootWeaponDefinition, type LootWeaponProjection, type LootWeaponStatMap } from "./loot/lootWeapon.js";
90
+ export { chaseMission, defendMission, escortMission, type ChaseMissionOptions, type ComposedMissionOptions, type DefendMissionOptions, type EscortMissionOptions, } from "./mission/compositions.js";
91
+ export { DEFAULT_ESCAPE_MULTIPLE, DEFAULT_PURSUE_DISTANCE, DEFAULT_REACH_RADIUS, MISSION_COUNTER_KINDS, applyMissionRewards, createMissionRuntime, isCounterObjective, validateMissionCatalog, type MissionActorFact, type MissionBranch, type MissionDef, type MissionEvent, type MissionEventFilter, type MissionFactCondition, type MissionFailCondition, type MissionFailDef, type MissionIssue, type MissionObjectiveDef, type MissionObjectiveKind, type MissionRewardAppliers, type MissionRewards, type MissionRuntime, type MissionRuntimeConfig, type MissionRuntimeSnapshot, type MissionState, type MissionStats, type MissionStatus, type MissionTickInput, type MissionValidationOptions, type MissionView, type MissionWorldEvent, type MissionWorldEventKind, } from "./mission/mission.js";
92
+ export { MISSION_TRIGGER_ACTIONS, createMissionTriggerBridge, missionMarkers, registerMissionTriggerActions, type MissionTriggerBridge, } from "./mission/triggers.js";
93
+ export { compareGearSets, gearScore, type GearScore, type GearScoreOptions, type GearSlotItem, } from "./progression/gearScore.js";
94
+ export { assertSkillTreeConfig, createSkillTree, type SkillActiveDef, type SkillNodeDef, type SkillRequirement, type SkillResolution, type SkillSpendReason, type SkillSpendResult, type SkillSynergyDef, type SkillTree, type SkillTreeConfig, type SkillTreeState, } from "./progression/skillTree.js";
84
95
  export { createStatGraph, statModifierContributions, type StatContribution, type StatContributionStep, type StatDeriveContext, type StatDerivedDef, type StatExplanation, type StatGraph, type StatGraphDef, type StatInputDef, type StatModEntry, type StatOp, type StatSheet, type StatSheetState, } from "./progression/statGraph.js";
96
+ export { assertXpCurve, defineXpCurve, type XpCurve, type XpCurveDef, type XpGrowthRule, type XpLevelStep, } from "./progression/xpCurve.js";
85
97
  export { type CellGrid } from "./puzzle/cellGrid.js";
86
98
  export { type ShapeTable } from "./puzzle/fallingPiece.js";
87
99
  export { createNameGenerator } from "./random/nameGen.js";
@@ -98,6 +110,9 @@ export { type RoundConfig, type RoundSnapshot } from "./session/roundState.js";
98
110
  export { createNearAgents, type NearAgentSnapshot, type NearAgentView, type NearAgentsOptions, type NearCrowdAgents, } from "./sim/nearAgents.js";
99
111
  export { createNeedSet, type NeedDefinition, type NeedReading, type NeedSet, type NeedSnapshot } from "./sim/needs.js";
100
112
  export { createOffScreenWorld, type NearAgents, type NearHandoff, type NearSpawn, type OffScreenAgentView, type OffScreenMode, type OffScreenOptions, type OffScreenPolicy, type OffScreenPolicyPatch, type OffScreenResidentSpec, type OffScreenSnapshot, type OffScreenStats, type OffScreenWorld, } from "./sim/offScreen.js";
113
+ export { createVisitorPopulation, type Visitor, type VisitorFlow, type VisitorPolicy, type VisitorPolicyPatch, type VisitorPopulation, type VisitorPopulationOptions, type VisitorPopulationSnapshot, type VisitorPopulationStats, type VisitorWorld, } from "./sim/population.js";
114
+ export { DEFAULT_QUEUE_SEED_SERVICE_SECONDS, DEFAULT_QUEUE_WINDOW, createWaitQueue, type QueueEntry, type QueueWait, type WaitQueue, type WaitQueueOptions, type WaitQueueSnapshot, } from "./sim/queue.js";
115
+ export { DEFAULT_RIDE_BREAKDOWN, DEFAULT_RIDE_FUN_CURVE, DEFAULT_RIDE_RATING_RANGE, createRideChooser, createRideSystem, rideAppeal, rideChoiceCandidates, rideChoiceOptions, rideQueueCostWeight, type RideBreakdown, type RideChoicePolicy, type RideChooser, type RideChooserConfig, type RideChooserSnapshot, type RideDefinition, type RidePhase, type RideQueueView, type RideRatingRange, type RideRatings, type RideRetune, type RideServiceResult, type RideStats, type RideSystem, type RideSystemConfig, type RideSystemSnapshot, type RideTaste, type RideTickSink, type RideView, type RideWaitSource, } from "./sim/rides.js";
101
116
  export { resolveRoutine, type RoutineConfig, type RoutinePhase, type RoutineStop, type RoutineStopSpec, type RoutineTimeline, } from "./sim/routine.js";
102
117
  export { createDecayMeterSet, decayMeterMoodles, decayMeterSnapshot, decayMeterState, decayMeters, initDecayMeters, refillMeter, type DecayMeterSet, type DecayMeterValues, type DecayModifier, } from "./survival/decayMeter.js";
103
118
  export { createMoodleStack, stackMoodles, type Moodle, type MoodleStack } from "./survival/moodle.js";
package/dist/gameplay.js CHANGED
@@ -7,10 +7,13 @@ export { advanceTransport, createProductionState, drainOutput, feedProduction, p
7
7
  export { canCraft, craft, craftSeconds, createRecipeGraph, stationSatisfied, } from "./crafting/recipe.js";
8
8
  export { createAuctionBook } from "./economy/auctionBook.js";
9
9
  export { createListingBook } from "./economy/listingBook.js";
10
+ export { buildLoanSchedule, createLoanBook, } from "./economy/loanBook.js";
11
+ export { createMarket, } from "./economy/market.js";
10
12
  export { createPriceHistory } from "./economy/priceHistory.js";
11
13
  export { addScheduledRule, advanceLedger, annotate, balanceOf, cancelRule, capAmount, createResourceLedger, curveScale, pauseRule, redirect, rejectWhen, resumeRule, taxFraction, thresholdScale, } from "./economy/resourceLedger.js";
12
14
  export { createShopStock, } from "./economy/shopStock.js";
13
15
  export {} from "./economy/techTree.js";
16
+ export { createTransactionLedger, } from "./economy/transactionLedger.js";
14
17
  export { balance, canAfford, charge, chargeAll, createEmptyWallet, grant, isOverdrawn, } from "./economy/wallet.js";
15
18
  export { createAchievementTracker, } from "./game/achievements.js";
16
19
  export { DEFAULT_CHAT_BODY_LENGTH, DEFAULT_CHAT_HISTORY_LIMIT, DEFAULT_CHAT_RATE_LIMIT, createChatRateLimiter, } from "./game/chat.js";
@@ -25,6 +28,7 @@ export { createEventTicker, } from "./game/eventTicker.js";
25
28
  export { createGameEvents, } from "./game/events.js";
26
29
  export { appendFeed, createGameFeed, pruneFeed, } from "./game/feed.js";
27
30
  export { gamePhase, setGamePhase } from "./game/gamePhase.js";
31
+ export { GAME_SAVE_VERSION, createGameSave, gameSaveDigest, } from "./game/gameSave.js";
28
32
  export { createKeyValueStore } from "./game/keyValueStore.js";
29
33
  export { createLeaderboard } from "./game/leaderboard.js";
30
34
  export { medalFor, rankLeaderboard, } from "./game/leaderboardRank.js";
@@ -81,7 +85,15 @@ export { computeEffectiveStats, createModularItem, install, isComplete, missingR
81
85
  export { createItemUse } from "./item/use.js";
82
86
  export { createUseBehaviorRegistry, } from "./item/useBehavior.js";
83
87
  export { createWeaponStats } from "./item/weapon.js";
88
+ export { MAX_LOOT_AFFIXES, assertLootCatalog, compareLootItems, createLootRoller, lootLevelScale, lootStatContributions, rollLoot, scoreLootStats, validateLootItem, } from "./loot/lootGen.js";
89
+ export { lootWeaponDefinition } from "./loot/lootWeapon.js";
90
+ export { chaseMission, defendMission, escortMission, } from "./mission/compositions.js";
91
+ export { DEFAULT_ESCAPE_MULTIPLE, DEFAULT_PURSUE_DISTANCE, DEFAULT_REACH_RADIUS, MISSION_COUNTER_KINDS, applyMissionRewards, createMissionRuntime, isCounterObjective, validateMissionCatalog, } from "./mission/mission.js";
92
+ export { MISSION_TRIGGER_ACTIONS, createMissionTriggerBridge, missionMarkers, registerMissionTriggerActions, } from "./mission/triggers.js";
93
+ export { compareGearSets, gearScore, } from "./progression/gearScore.js";
94
+ export { assertSkillTreeConfig, createSkillTree, } from "./progression/skillTree.js";
84
95
  export { createStatGraph, statModifierContributions, } from "./progression/statGraph.js";
96
+ export { assertXpCurve, defineXpCurve, } from "./progression/xpCurve.js";
85
97
  export {} from "./puzzle/cellGrid.js";
86
98
  export {} from "./puzzle/fallingPiece.js";
87
99
  export { createNameGenerator } from "./random/nameGen.js";
@@ -98,6 +110,9 @@ export {} from "./session/roundState.js";
98
110
  export { createNearAgents, } from "./sim/nearAgents.js";
99
111
  export { createNeedSet } from "./sim/needs.js";
100
112
  export { createOffScreenWorld, } from "./sim/offScreen.js";
113
+ export { createVisitorPopulation, } from "./sim/population.js";
114
+ export { DEFAULT_QUEUE_SEED_SERVICE_SECONDS, DEFAULT_QUEUE_WINDOW, createWaitQueue, } from "./sim/queue.js";
115
+ export { DEFAULT_RIDE_BREAKDOWN, DEFAULT_RIDE_FUN_CURVE, DEFAULT_RIDE_RATING_RANGE, createRideChooser, createRideSystem, rideAppeal, rideChoiceCandidates, rideChoiceOptions, rideQueueCostWeight, } from "./sim/rides.js";
101
116
  export { resolveRoutine, } from "./sim/routine.js";
102
117
  export { createDecayMeterSet, decayMeterMoodles, decayMeterSnapshot, decayMeterState, decayMeters, initDecayMeters, refillMeter, } from "./survival/decayMeter.js";
103
118
  export { createMoodleStack, stackMoodles } from "./survival/moodle.js";
@@ -0,0 +1,48 @@
1
+ import { type EncumbranceConfig, type EncumbranceState, type LoadEntry } from "./encumbrance.js";
2
+ import type { InventoryState } from "./inventoryModel.js";
3
+ /** Per-item mass in the caller's weight unit; an unknown item weighs 0. */
4
+ export type WeightLookup = (itemId: string) => number;
5
+ /**
6
+ * Every filled stack in the given inventories as load entries, in slot order. Only the inventories
7
+ * the caller passes count, so a stash left out of the list adds no weight.
8
+ *
9
+ * @capability carry-load read carried stacks out of one or more inventories for a weight query
10
+ */
11
+ export declare function carriedEntries(states: readonly InventoryState[]): LoadEntry[];
12
+ /** Everything a {@link characterLoad} reading takes: carried inventories, worn gear and the capacity they are measured against. */
13
+ export interface CharacterLoadInput {
14
+ /** Carried inventories (bag, rig, pockets); a stash left out does not weigh anything. */
15
+ carried?: readonly InventoryState[];
16
+ /** Worn item ids, one unit each — `Equipment.worn()` mapped to `itemId`. */
17
+ worn?: readonly string[];
18
+ weightOf: WeightLookup;
19
+ /** Carrying capacity in the same unit as `weightOf`; the caller adds level and gear bonuses. */
20
+ capacity: number;
21
+ /** Fraction of worn gear weight that counts against capacity (default `1`). */
22
+ wornFactor?: number;
23
+ encumbrance?: EncumbranceConfig;
24
+ }
25
+ /** A carrier's load: the encumbrance state, how it splits into carried and worn, and capacity left. */
26
+ export interface CharacterLoad extends EncumbranceState {
27
+ /** Mass of the carried inventories alone. */
28
+ carriedMass: number;
29
+ /** Mass worn gear adds after `wornFactor`. */
30
+ wornMass: number;
31
+ /** Capacity left before the carrier is immobile; negative when overloaded. */
32
+ remaining: number;
33
+ }
34
+ /**
35
+ * Weigh a character: carried stacks plus worn gear against a capacity, resolved into the shared
36
+ * encumbrance bands. Movement reads `moveMultiplier`, `canSprint`, `canJump` and `immobile`; pickup
37
+ * gates read `remaining` or {@link canCarry}. The stash is not part of `carried`, so it adds nothing.
38
+ *
39
+ * @capability carry-encumbrance weigh carried inventories plus worn gear against a capacity and hand back movement penalties
40
+ */
41
+ export declare function characterLoad(input: CharacterLoadInput): CharacterLoad;
42
+ /**
43
+ * Whether `addedMass` more weight still fits under `capacity`. Pure: the caller decides what to do
44
+ * with a refusal, so an overload policy stays with the game.
45
+ *
46
+ * @capability carry-encumbrance gate a pickup against the carrier's remaining capacity
47
+ */
48
+ export declare function canCarry(load: Pick<CharacterLoad, "mass" | "capacity">, addedMass: number): boolean;
@@ -0,0 +1,44 @@
1
+ import { resolveEncumbrance, totalLoad, } from "./encumbrance.js";
2
+ /**
3
+ * Every filled stack in the given inventories as load entries, in slot order. Only the inventories
4
+ * the caller passes count, so a stash left out of the list adds no weight.
5
+ *
6
+ * @capability carry-load read carried stacks out of one or more inventories for a weight query
7
+ */
8
+ export function carriedEntries(states) {
9
+ const entries = [];
10
+ for (const state of states) {
11
+ for (const slot of state.slots) {
12
+ if (slot !== null)
13
+ entries.push({ itemId: slot.itemId, quantity: slot.count });
14
+ }
15
+ }
16
+ return entries;
17
+ }
18
+ /**
19
+ * Weigh a character: carried stacks plus worn gear against a capacity, resolved into the shared
20
+ * encumbrance bands. Movement reads `moveMultiplier`, `canSprint`, `canJump` and `immobile`; pickup
21
+ * gates read `remaining` or {@link canCarry}. The stash is not part of `carried`, so it adds nothing.
22
+ *
23
+ * @capability carry-encumbrance weigh carried inventories plus worn gear against a capacity and hand back movement penalties
24
+ */
25
+ export function characterLoad(input) {
26
+ const carriedMass = totalLoad(carriedEntries(input.carried ?? []), input.weightOf);
27
+ const factor = input.wornFactor !== undefined && Number.isFinite(input.wornFactor) && input.wornFactor >= 0
28
+ ? input.wornFactor
29
+ : 1;
30
+ const wornEntries = (input.worn ?? []).map((itemId) => ({ itemId, quantity: 1 }));
31
+ const wornMass = totalLoad(wornEntries, input.weightOf) * factor;
32
+ const base = resolveEncumbrance(carriedMass + wornMass, input.capacity, input.encumbrance);
33
+ return { ...base, carriedMass, wornMass, remaining: input.capacity - base.mass };
34
+ }
35
+ /**
36
+ * Whether `addedMass` more weight still fits under `capacity`. Pure: the caller decides what to do
37
+ * with a refusal, so an overload policy stays with the game.
38
+ *
39
+ * @capability carry-encumbrance gate a pickup against the carrier's remaining capacity
40
+ */
41
+ export function canCarry(load, addedMass) {
42
+ const added = Number.isFinite(addedMass) && addedMass > 0 ? addedMass : 0;
43
+ return load.mass + added <= load.capacity;
44
+ }
@@ -0,0 +1,95 @@
1
+ /** One body slot: the item kinds it accepts and the slots it occupies while filled. */
2
+ export interface EquipSlotDef {
3
+ /** Stable slot id, in the caller's nouns ("weapon", "rig", "boots"). */
4
+ id: string;
5
+ /** Item kinds this slot accepts; omitted accepts any kind. */
6
+ accepts?: readonly string[];
7
+ /** Slot ids this slot occupies while filled, e.g. a two-handed weapon blocking "offhand". */
8
+ blocks?: readonly string[];
9
+ }
10
+ /**
11
+ * The gear facts the equip rules read from an item. A rolled item resolves its runtime id through
12
+ * the instance registry to its base and answers from that base's data — this is not a second item type.
13
+ */
14
+ export interface EquipItemDef {
15
+ /** Slot ids this item may occupy, most preferred first. */
16
+ slots: readonly string[];
17
+ /** Kind tag checked against {@link EquipSlotDef.accepts}. */
18
+ kind?: string;
19
+ }
20
+ /** Item lookup the rules read; returns nothing for an unknown id. */
21
+ export type EquipItemLookup = (itemId: string) => EquipItemDef | null | undefined;
22
+ /** Serializable equip state: the filled slots as `[slotId, itemId]` pairs. */
23
+ export interface EquipmentState {
24
+ slots: readonly (readonly [string, string])[];
25
+ }
26
+ /** Why an equip attempt was refused; `detail` names the item and slots involved. */
27
+ export type EquipRejection = "unknown-slot" | "not-equippable" | "wrong-slot" | "already-equipped" | "blocked";
28
+ /** An equip attempt: the new worn set plus the replaced item, or the refusal with its reason and detail. */
29
+ export type EquipResult = {
30
+ status: "ok";
31
+ state: EquipmentState;
32
+ slotId: string;
33
+ replaced: string | null;
34
+ } | {
35
+ status: "rejected";
36
+ reason: EquipRejection;
37
+ detail: string;
38
+ };
39
+ /** An unequip attempt: the new worn set plus the item taken off, or the refusal with its detail. */
40
+ export type UnequipResult = {
41
+ status: "ok";
42
+ state: EquipmentState;
43
+ itemId: string;
44
+ } | {
45
+ status: "rejected";
46
+ reason: "unknown-slot" | "empty-slot";
47
+ detail: string;
48
+ };
49
+ /** One worn item and the slot holding it. */
50
+ export interface WornEntry {
51
+ slotId: string;
52
+ itemId: string;
53
+ }
54
+ /** What the debug overlay reads about a worn set. */
55
+ export interface EquipmentProbeReading {
56
+ filled: number;
57
+ slots: Readonly<Record<string, string>>;
58
+ }
59
+ /** A worn-gear set: one item per slot, rules from slot and item data, state out and back in. */
60
+ export interface Equipment {
61
+ /** Wear `itemId` in `slotId`; a filled slot is replaced and its item returned as `replaced`. */
62
+ equip(slotId: string, itemId: string): EquipResult;
63
+ /** Wear `itemId` in the first of its preferred slots that is empty, else the first slot that accepts it. */
64
+ equipBest(itemId: string): EquipResult;
65
+ /** Clear `slotId` and hand back the item it held. */
66
+ unequip(slotId: string): UnequipResult;
67
+ /** Worn items in declared slot order. */
68
+ worn(): readonly WornEntry[];
69
+ /** The slot holding `itemId`, or null when that item is not worn. */
70
+ slotOf(itemId: string): string | null;
71
+ /** Declared slot ids, in order. */
72
+ slotIds(): readonly string[];
73
+ state(): EquipmentState;
74
+ /** Replace the worn set; returns false without mutation when an entry breaks the rules. */
75
+ restore(next: EquipmentState): boolean;
76
+ reset(): void;
77
+ /** Drop the devtools probe registered by `devtoolsName`. Safe to call twice. */
78
+ dispose(): void;
79
+ }
80
+ /** Options for {@link createEquipment}. */
81
+ export interface EquipmentOptions {
82
+ /** Register a devtools probe under this name reporting which slots are filled. */
83
+ devtoolsName?: string;
84
+ }
85
+ /**
86
+ * A worn-gear set over caller slot and item data.
87
+ *
88
+ * Slot defs own what a slot accepts and which slots it blocks; item defs own which slots an item may
89
+ * occupy and its kind. Equipping into a filled slot swaps and returns the replaced item, so gear is
90
+ * never silently lost: the caller puts `replaced` back. A two-handed item and the slot it blocks
91
+ * cannot both be filled, and the rejection names the occupied slot.
92
+ *
93
+ * @capability equipment-slot-inventory wear gear by slot with item-kind rules, two-handed blocking and replace-on-equip
94
+ */
95
+ export declare function createEquipment(slotDefs: readonly EquipSlotDef[], lookup: EquipItemLookup, options?: EquipmentOptions): Equipment;
@@ -0,0 +1,169 @@
1
+ import { devtools } from "../devtools/devtools.js";
2
+ function slotAcceptsKind(slot, kind) {
3
+ if (slot.accepts === undefined)
4
+ return true;
5
+ return kind !== undefined && slot.accepts.includes(kind);
6
+ }
7
+ /**
8
+ * A worn-gear set over caller slot and item data.
9
+ *
10
+ * Slot defs own what a slot accepts and which slots it blocks; item defs own which slots an item may
11
+ * occupy and its kind. Equipping into a filled slot swaps and returns the replaced item, so gear is
12
+ * never silently lost: the caller puts `replaced` back. A two-handed item and the slot it blocks
13
+ * cannot both be filled, and the rejection names the occupied slot.
14
+ *
15
+ * @capability equipment-slot-inventory wear gear by slot with item-kind rules, two-handed blocking and replace-on-equip
16
+ */
17
+ export function createEquipment(slotDefs, lookup, options = {}) {
18
+ const byId = new Map();
19
+ for (const def of slotDefs) {
20
+ if (def.id.trim() === "")
21
+ throw new RangeError('Equipment slot field "id" must not be empty');
22
+ if (byId.has(def.id))
23
+ throw new RangeError(`Equipment slot "${def.id}" is declared twice`);
24
+ byId.set(def.id, def);
25
+ }
26
+ const order = slotDefs.map((def) => def.id);
27
+ const filled = new Map();
28
+ function readState() {
29
+ const slots = [];
30
+ for (const slotId of order) {
31
+ const itemId = filled.get(slotId);
32
+ if (itemId !== undefined)
33
+ slots.push([slotId, itemId]);
34
+ }
35
+ return { slots };
36
+ }
37
+ function conflicts(slotId, occupied = filled) {
38
+ const blockedByOwn = byId.get(slotId)?.blocks ?? [];
39
+ const blocking = order.filter((other) => other !== slotId && (byId.get(other)?.blocks ?? []).includes(slotId));
40
+ return [...new Set([...blockedByOwn, ...blocking])].filter((other) => occupied.has(other));
41
+ }
42
+ function check(slotId, itemId, occupied) {
43
+ const slot = byId.get(slotId);
44
+ if (slot === undefined) {
45
+ return { status: "rejected", reason: "unknown-slot", detail: `"${slotId}" is not a declared slot (${order.join(", ")})` };
46
+ }
47
+ const item = lookup(itemId);
48
+ if (item === null || item === undefined || item.slots.length === 0) {
49
+ return { status: "rejected", reason: "not-equippable", detail: `item "${itemId}" declares no equip slot` };
50
+ }
51
+ if (!item.slots.includes(slotId)) {
52
+ return {
53
+ status: "rejected",
54
+ reason: "wrong-slot",
55
+ detail: `item "${itemId}" belongs in ${item.slots.join(", ")}; "${slotId}" is not one of them`,
56
+ };
57
+ }
58
+ if (!slotAcceptsKind(slot, item.kind)) {
59
+ return {
60
+ status: "rejected",
61
+ reason: "wrong-slot",
62
+ detail: `slot "${slotId}" accepts ${(slot.accepts ?? []).join(", ")}; item "${itemId}" is ${item.kind ?? "untyped"}`,
63
+ };
64
+ }
65
+ const wornAt = [...occupied].find(([, worn]) => worn === itemId)?.[0];
66
+ if (wornAt !== undefined && wornAt !== slotId) {
67
+ return { status: "rejected", reason: "already-equipped", detail: `item "${itemId}" is already worn in "${wornAt}"` };
68
+ }
69
+ const conflict = conflicts(slotId, occupied).filter((other) => other !== slotId)[0];
70
+ if (conflict !== undefined) {
71
+ return {
72
+ status: "rejected",
73
+ reason: "blocked",
74
+ detail: `slot "${conflict}" is occupied and cannot be worn with "${slotId}"`,
75
+ };
76
+ }
77
+ return null;
78
+ }
79
+ function equipInto(slotId, itemId) {
80
+ const refusal = check(slotId, itemId, filled);
81
+ if (refusal !== null)
82
+ return refusal;
83
+ const replaced = filled.get(slotId) ?? null;
84
+ filled.set(slotId, itemId);
85
+ return { status: "ok", state: readState(), slotId, replaced };
86
+ }
87
+ const probe = options.devtoolsName === undefined
88
+ ? null
89
+ : devtools.probes.register(options.devtoolsName, () => ({
90
+ filled: filled.size,
91
+ slots: Object.fromEntries(filled),
92
+ }));
93
+ return {
94
+ equip: equipInto,
95
+ equipBest(itemId) {
96
+ const item = lookup(itemId);
97
+ if (item === null || item === undefined || item.slots.length === 0) {
98
+ return { status: "rejected", reason: "not-equippable", detail: `item "${itemId}" declares no equip slot` };
99
+ }
100
+ const allowed = item.slots.filter((slotId) => byId.has(slotId));
101
+ if (allowed.length === 0) {
102
+ return {
103
+ status: "rejected",
104
+ reason: "unknown-slot",
105
+ detail: `item "${itemId}" names no declared slot (${item.slots.join(", ")})`,
106
+ };
107
+ }
108
+ const preferred = allowed.filter((slotId) => check(slotId, itemId, filled) === null);
109
+ if (preferred.length === 0)
110
+ return check(allowed[0], itemId, filled);
111
+ return equipInto(preferred.find((slotId) => !filled.has(slotId)) ?? preferred[0], itemId);
112
+ },
113
+ unequip(slotId) {
114
+ if (!byId.has(slotId)) {
115
+ return { status: "rejected", reason: "unknown-slot", detail: `"${slotId}" is not a declared slot (${order.join(", ")})` };
116
+ }
117
+ const itemId = filled.get(slotId);
118
+ if (itemId === undefined) {
119
+ return { status: "rejected", reason: "empty-slot", detail: `slot "${slotId}" holds nothing` };
120
+ }
121
+ filled.delete(slotId);
122
+ return { status: "ok", state: readState(), itemId };
123
+ },
124
+ worn() {
125
+ const out = [];
126
+ for (const slotId of order) {
127
+ const itemId = filled.get(slotId);
128
+ if (itemId !== undefined)
129
+ out.push({ slotId, itemId });
130
+ }
131
+ return out;
132
+ },
133
+ slotOf(itemId) {
134
+ for (const [slotId, worn] of filled)
135
+ if (worn === itemId)
136
+ return slotId;
137
+ return null;
138
+ },
139
+ slotIds: () => [...order],
140
+ state: readState,
141
+ restore(next) {
142
+ if (next === null || typeof next !== "object" || !Array.isArray(next.slots))
143
+ return false;
144
+ const staged = new Map();
145
+ for (const entry of next.slots) {
146
+ if (!Array.isArray(entry) || entry.length !== 2)
147
+ return false;
148
+ const [slotId, itemId] = entry;
149
+ if (typeof slotId !== "string" || typeof itemId !== "string" || itemId.trim() === "")
150
+ return false;
151
+ if (staged.has(slotId))
152
+ return false;
153
+ if (check(slotId, itemId, staged) !== null)
154
+ return false;
155
+ staged.set(slotId, itemId);
156
+ }
157
+ filled.clear();
158
+ for (const [slotId, itemId] of staged)
159
+ filled.set(slotId, itemId);
160
+ return true;
161
+ },
162
+ reset() {
163
+ filled.clear();
164
+ },
165
+ dispose() {
166
+ probe?.();
167
+ },
168
+ };
169
+ }
@@ -41,7 +41,7 @@ export type SplitResult = {
41
41
  state: InventoryState;
42
42
  } | {
43
43
  status: "rejected";
44
- reason: "invalid-slot" | "empty-slot" | "invalid-amount" | "no-space" | "slot-occupied";
44
+ reason: "invalid-slot" | "empty-slot" | "invalid-amount" | "no-space" | "slot-occupied" | "over-stack";
45
45
  };
46
46
  /**
47
47
  * A bag of stackable items supporting add, remove, count, and move — the base inventory model.
@@ -63,13 +63,14 @@ export declare function hasItem(state: InventoryState, itemId: string, count: nu
63
63
  export declare function moveItem(from: InventoryState, fromSlot: number, to: InventoryState, toLayout: InventoryLayout, traits: ItemTraits, toSlot?: number): MoveResult;
64
64
  /**
65
65
  * Splits `amount` off the stack in `slot` into `toSlot` (or the first empty slot when `toSlot` is
66
- * omitted). Limit-agnostic: it does not consult {@link ItemTraits}, so a same-item target merges by
67
- * simple addition. Rejects an empty source, `amount <= 0`, `amount >= stack count` (a split must
68
- * leave both sides non-empty), no free slot, or a target holding a different item.
66
+ * omitted). Without `limit` it is limit-agnostic: a same-item target merges by simple addition.
67
+ * Pass the item's stack limit to keep the merge legal, which rejects `over-stack`. Rejects an empty
68
+ * source, `amount <= 0`, `amount >= stack count` (a split must leave both sides non-empty), no free
69
+ * slot, or a target holding a different item.
69
70
  *
70
- * @capability inventory-split split a stack into a target or first-empty slot
71
+ * @capability inventory-split split a stack into a target or first-empty slot, honoring a stack limit
71
72
  */
72
- export declare function splitStack(state: InventoryState, slot: number, amount: number, toSlot?: number): SplitResult;
73
+ export declare function splitStack(state: InventoryState, slot: number, amount: number, toSlot?: number, limit?: number): SplitResult;
73
74
  export interface InventorySet<TId extends string> {
74
75
  put(id: TId, itemId: string, count: number, options?: {
75
76
  slot?: number;
@@ -80,6 +81,14 @@ export interface InventorySet<TId extends string> {
80
81
  has(id: TId, itemId: string, count: number): boolean;
81
82
  state(id: TId): InventoryState;
82
83
  replaceState(id: TId, state: InventoryState): void;
84
+ /** Detached copy of every declared inventory — a save, never a live alias. */
85
+ snapshot(): InventorySetState<TId>;
86
+ /** Replace every declared inventory from a snapshot; returns false without mutation when an entry is malformed or incomplete. */
87
+ restore(next: InventorySetState<TId>): boolean;
88
+ /** Empty every declared inventory. */
89
+ reset(): void;
83
90
  }
91
+ /** Every inventory in a set, keyed by id, as plain slot data. */
92
+ export type InventorySetState<TId extends string> = Record<TId, InventoryState>;
84
93
  /** Build a keyed set of named inventories from their `layouts`, sharing one `traits` table — the stateful façade over put/take/move/count. */
85
94
  export declare function createInventorySet<TId extends string>(layouts: Record<TId, InventoryLayout>, traits: ItemTraits): InventorySet<TId>;
@@ -175,13 +175,14 @@ export function moveItem(from, fromSlot, to, toLayout, traits, toSlot) {
175
175
  }
176
176
  /**
177
177
  * Splits `amount` off the stack in `slot` into `toSlot` (or the first empty slot when `toSlot` is
178
- * omitted). Limit-agnostic: it does not consult {@link ItemTraits}, so a same-item target merges by
179
- * simple addition. Rejects an empty source, `amount <= 0`, `amount >= stack count` (a split must
180
- * leave both sides non-empty), no free slot, or a target holding a different item.
178
+ * omitted). Without `limit` it is limit-agnostic: a same-item target merges by simple addition.
179
+ * Pass the item's stack limit to keep the merge legal, which rejects `over-stack`. Rejects an empty
180
+ * source, `amount <= 0`, `amount >= stack count` (a split must leave both sides non-empty), no free
181
+ * slot, or a target holding a different item.
181
182
  *
182
- * @capability inventory-split split a stack into a target or first-empty slot
183
+ * @capability inventory-split split a stack into a target or first-empty slot, honoring a stack limit
183
184
  */
184
- export function splitStack(state, slot, amount, toSlot) {
185
+ export function splitStack(state, slot, amount, toSlot, limit) {
185
186
  if (!validSlot(state, slot))
186
187
  return { status: "rejected", reason: "invalid-slot" };
187
188
  const source = state.slots[slot];
@@ -206,6 +207,9 @@ export function splitStack(state, slot, amount, toSlot) {
206
207
  }
207
208
  const slots = state.slots.slice();
208
209
  const dest = slots[target];
210
+ if (limit !== undefined && dest !== null && dest !== undefined && dest.count + amount > limit) {
211
+ return { status: "rejected", reason: "over-stack" };
212
+ }
209
213
  slots[slot] = { itemId: source.itemId, count: source.count - amount };
210
214
  slots[target] =
211
215
  dest === null || dest === undefined
@@ -213,6 +217,26 @@ export function splitStack(state, slot, amount, toSlot) {
213
217
  : { itemId: source.itemId, count: dest.count + amount };
214
218
  return { status: "ok", state: { slots } };
215
219
  }
220
+ function cloneSlots(slots) {
221
+ return slots.map((slot) => (slot === null ? null : { itemId: slot.itemId, count: slot.count }));
222
+ }
223
+ function validInventoryState(state, slots) {
224
+ if (state === null || typeof state !== "object" || !Array.isArray(state.slots))
225
+ return false;
226
+ if (state.slots.length !== slots)
227
+ return false;
228
+ for (const slot of state.slots) {
229
+ if (slot === null)
230
+ continue;
231
+ if (typeof slot !== "object")
232
+ return false;
233
+ if (typeof slot.itemId !== "string")
234
+ return false;
235
+ if (!Number.isFinite(slot.count) || slot.count <= 0)
236
+ return false;
237
+ }
238
+ return true;
239
+ }
216
240
  /** Build a keyed set of named inventories from their `layouts`, sharing one `traits` table — the stateful façade over put/take/move/count. */
217
241
  export function createInventorySet(layouts, traits) {
218
242
  const states = new Map();
@@ -258,5 +282,30 @@ export function createInventorySet(layouts, traits) {
258
282
  replaceState(id, state) {
259
283
  states.set(id, state);
260
284
  },
285
+ snapshot() {
286
+ const out = {};
287
+ for (const id of Object.keys(layouts)) {
288
+ out[id] = { slots: cloneSlots(requireState(id).slots) };
289
+ }
290
+ return out;
291
+ },
292
+ restore(next) {
293
+ if (next === null || typeof next !== "object")
294
+ return false;
295
+ const staged = new Map();
296
+ for (const id of Object.keys(layouts)) {
297
+ const state = next[id];
298
+ if (state === undefined || !validInventoryState(state, layouts[id].slots))
299
+ return false;
300
+ staged.set(id, { slots: cloneSlots(state.slots) });
301
+ }
302
+ for (const [id, state] of staged)
303
+ states.set(id, state);
304
+ return true;
305
+ },
306
+ reset() {
307
+ for (const id of Object.keys(layouts))
308
+ states.set(id, createEmptyInventory(layouts[id]));
309
+ },
261
310
  };
262
311
  }