@jgengine/core 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/dist/ai/populationDirector.d.ts +103 -0
- package/dist/ai/populationDirector.js +190 -0
- package/dist/ai/pursuit.d.ts +56 -0
- package/dist/ai/pursuit.js +53 -0
- package/dist/authoring.d.ts +1 -1
- package/dist/authoring.js +1 -1
- package/dist/combat/effects.d.ts +11 -4
- package/dist/combat/effects.js +24 -17
- package/dist/combat/magazine.d.ts +13 -0
- package/dist/combat/magazine.js +27 -0
- package/dist/combat/vfxPresets.d.ts +176 -0
- package/dist/combat/vfxPresets.js +74 -0
- package/dist/combat/weaponFire.d.ts +114 -0
- package/dist/combat/weaponFire.js +96 -0
- package/dist/combat.d.ts +5 -2
- package/dist/combat.js +4 -1
- package/dist/devtools/controls.d.ts +38 -0
- package/dist/devtools/controls.js +181 -0
- package/dist/devtools/devtools.d.ts +5 -197
- package/dist/devtools/devtools.js +38 -774
- package/dist/devtools/discover.d.ts +32 -0
- package/dist/devtools/discover.js +240 -0
- package/dist/devtools/fallbackSeams.d.ts +50 -0
- package/dist/devtools/fallbackSeams.js +111 -0
- package/dist/devtools/frame.d.ts +15 -0
- package/dist/devtools/frame.js +247 -0
- package/dist/devtools/logs.d.ts +15 -0
- package/dist/devtools/logs.js +86 -0
- package/dist/devtools/overrides.d.ts +16 -0
- package/dist/devtools/overrides.js +66 -0
- package/dist/devtools/types.d.ts +212 -0
- package/dist/devtools/types.js +1 -0
- package/dist/devtools/urlFlags.d.ts +22 -0
- package/dist/devtools/urlFlags.js +62 -0
- package/dist/economy/auctionBook.d.ts +122 -0
- package/dist/economy/auctionBook.js +213 -0
- package/dist/economy/priceHistory.d.ts +44 -0
- package/dist/economy/priceHistory.js +89 -0
- package/dist/economy/unlockPoints.d.ts +92 -0
- package/dist/economy/unlockPoints.js +103 -0
- package/dist/editor/commandApply.d.ts +5 -0
- package/dist/editor/commandApply.js +755 -0
- package/dist/editor/commands.d.ts +48 -3
- package/dist/editor/commands.js +40 -754
- package/dist/editor/directives.d.ts +94 -0
- package/dist/editor/directives.js +206 -0
- package/dist/editor/document.d.ts +37 -2
- package/dist/editor/document.js +422 -6
- package/dist/editor/entityCatalog.d.ts +42 -0
- package/dist/editor/entityCatalog.js +101 -0
- package/dist/editor/environment.d.ts +74 -0
- package/dist/editor/environment.js +169 -0
- package/dist/editor/index.d.ts +26 -1
- package/dist/editor/index.js +8 -1
- package/dist/editor/minimap.d.ts +31 -0
- package/dist/editor/minimap.js +73 -0
- package/dist/editor/streamer.d.ts +66 -0
- package/dist/editor/streamer.js +68 -0
- package/dist/editor/types.d.ts +143 -0
- package/dist/editor/world.d.ts +136 -0
- package/dist/editor/world.js +303 -0
- package/dist/game/breeding.d.ts +161 -0
- package/dist/game/breeding.js +181 -0
- package/dist/game/cameraConfig.d.ts +451 -0
- package/dist/game/cameraConfig.js +1 -0
- package/dist/game/clipRoles.d.ts +51 -0
- package/dist/game/clipRoles.js +142 -0
- package/dist/game/defineGame.d.ts +16 -9
- package/dist/game/defineGame.js +14 -6
- package/dist/game/defineSystem.d.ts +2 -0
- package/dist/game/defineSystem.js +2 -0
- package/dist/game/events.d.ts +28 -0
- package/dist/game/partAnimation.d.ts +78 -0
- package/dist/game/partAnimation.js +147 -0
- package/dist/game/playableGame.d.ts +72 -440
- package/dist/game/playableGame.js +1 -1
- package/dist/game/progression.d.ts +35 -0
- package/dist/game/progression.js +22 -1
- package/dist/game/quest.js +1 -3
- package/dist/game/tribe.d.ts +184 -0
- package/dist/game/tribe.js +432 -0
- package/dist/gameplay.d.ts +3 -1
- package/dist/gameplay.js +3 -1
- package/dist/inventory/encumbrance.d.ts +69 -0
- package/dist/inventory/encumbrance.js +68 -0
- package/dist/inventory/inventoryModel.d.ts +24 -8
- package/dist/inventory/inventoryModel.js +47 -8
- package/dist/item/durability.js +1 -3
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +48 -1
- package/dist/movement/movementModel.d.ts +15 -1
- package/dist/movement/movementModel.js +33 -15
- package/dist/movement/playerMovement.js +102 -2
- package/dist/multiplayer/combatSnapshot.js +13 -8
- package/dist/multiplayer/presenceModel.d.ts +2 -0
- package/dist/multiplayer/presenceModel.js +8 -0
- package/dist/physics/drivableVehicle.d.ts +2 -0
- package/dist/physics/drivableVehicle.js +1 -1
- package/dist/physics/flightDynamics.d.ts +82 -0
- package/dist/physics/flightDynamics.js +165 -0
- package/dist/physics/gravityField.d.ts +25 -0
- package/dist/physics/gravityField.js +41 -0
- package/dist/physics/kinematicVehicle.d.ts +87 -0
- package/dist/physics/kinematicVehicle.js +193 -11
- package/dist/physics/physicsWorld.d.ts +26 -0
- package/dist/physics/physicsWorld.js +109 -117
- package/dist/physics/vehicleObstacles.d.ts +44 -0
- package/dist/physics/vehicleObstacles.js +115 -0
- package/dist/random/rng.d.ts +4 -1
- package/dist/random/rng.js +4 -1
- package/dist/runtime/adapter.d.ts +13 -0
- package/dist/runtime/adapter.js +13 -0
- package/dist/runtime/context/combat.d.ts +51 -0
- package/dist/runtime/context/combat.js +131 -0
- package/dist/runtime/context/combatFx.js +15 -5
- package/dist/runtime/context/deathLoot.d.ts +30 -0
- package/dist/runtime/context/deathLoot.js +39 -0
- package/dist/runtime/context/player.d.ts +48 -0
- package/dist/runtime/context/player.js +125 -0
- package/dist/runtime/context/scene.d.ts +60 -0
- package/dist/runtime/context/scene.js +329 -0
- package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
- package/dist/runtime/context/snapshotCodecs.js +26 -0
- package/dist/runtime/descriptors/baseline.js +26 -3
- package/dist/runtime/descriptors/features.d.ts +5 -0
- package/dist/runtime/descriptors/features.js +20 -2
- package/dist/runtime/gameContext.d.ts +2 -428
- package/dist/runtime/gameContext.js +133 -500
- package/dist/runtime/gameContextTypes.d.ts +500 -0
- package/dist/runtime/gameContextTypes.js +1 -0
- package/dist/runtime/headlessRunner.d.ts +17 -2
- package/dist/runtime/headlessRunner.js +15 -0
- package/dist/runtime/hostPolicy.d.ts +51 -0
- package/dist/runtime/hostPolicy.js +68 -0
- package/dist/runtime/hostedGameRunner.d.ts +3 -1
- package/dist/runtime/hostedGameRunner.js +1 -0
- package/dist/runtime/hostedWorldSession.d.ts +3 -1
- package/dist/runtime/hostedWorldSession.js +1 -0
- package/dist/runtime/worldSnapshot.d.ts +13 -1
- package/dist/runtime/worldSnapshot.js +15 -3
- package/dist/scene/assetCatalog.d.ts +7 -0
- package/dist/scene/assetSpace.js +1 -3
- package/dist/scene/authoredTriggers.d.ts +50 -0
- package/dist/scene/authoredTriggers.js +114 -0
- package/dist/scene/builtinSceneKinds.js +8 -3
- package/dist/scene/colliders.d.ts +79 -0
- package/dist/scene/colliders.js +170 -0
- package/dist/scene/collisionMesh.d.ts +94 -0
- package/dist/scene/collisionMesh.js +699 -0
- package/dist/scene/companion.d.ts +147 -0
- package/dist/scene/companion.js +193 -0
- package/dist/scene/entityStats.d.ts +7 -9
- package/dist/scene/entityStats.js +19 -15
- package/dist/scene/sceneKinds.d.ts +16 -0
- package/dist/scene/sceneKinds.js +4 -0
- package/dist/scene/sceneRaycast.js +15 -0
- package/dist/stats/spawnLevelStats.d.ts +110 -0
- package/dist/stats/spawnLevelStats.js +140 -0
- package/dist/stats/statModifiers.d.ts +16 -0
- package/dist/stats/statModifiers.js +30 -1
- package/dist/stats/statPool.d.ts +70 -0
- package/dist/stats/statPool.js +62 -0
- package/dist/store/changeSignal.d.ts +3 -0
- package/dist/store/changeSignal.js +2 -0
- package/dist/store/observableKeyedStore.d.ts +2 -0
- package/dist/store/observableKeyedStore.js +1 -0
- package/dist/survival/decayMeter.js +1 -3
- package/dist/survival/regionHealth.js +1 -3
- package/dist/ui/hudLayout.d.ts +5 -0
- package/dist/ui/hudLayout.js +17 -1
- package/dist/ui/panelModel.d.ts +120 -0
- package/dist/ui/panelModel.js +168 -0
- package/dist/ui.d.ts +1 -0
- package/dist/ui.js +1 -0
- package/dist/world/authoredEntities.d.ts +29 -0
- package/dist/world/authoredEntities.js +29 -0
- package/dist/world/authoredObjects.d.ts +15 -1
- package/dist/world/authoredObjects.js +13 -1
- package/dist/world/authoredSpawn.d.ts +39 -0
- package/dist/world/authoredSpawn.js +37 -0
- package/dist/world/buildSockets.d.ts +152 -0
- package/dist/world/buildSockets.js +188 -0
- package/dist/world/buildingLots.d.ts +84 -0
- package/dist/world/buildingLots.js +119 -0
- package/dist/world/buildings.d.ts +6 -0
- package/dist/world/cityBlocks.d.ts +152 -0
- package/dist/world/cityBlocks.js +670 -0
- package/dist/world/cityContent.d.ts +77 -0
- package/dist/world/cityContent.js +261 -0
- package/dist/world/cityGenerator.d.ts +41 -0
- package/dist/world/cityGenerator.js +58 -0
- package/dist/world/cityGeometry.d.ts +114 -0
- package/dist/world/cityGeometry.js +573 -0
- package/dist/world/cityKind.d.ts +314 -0
- package/dist/world/cityKind.js +1578 -0
- package/dist/world/environmentSummary.js +57 -1
- package/dist/world/features.d.ts +53 -5
- package/dist/world/features.js +15 -3
- package/dist/world/grassKind.js +2 -2
- package/dist/world/markers.d.ts +50 -4
- package/dist/world/markers.js +49 -0
- package/dist/world/massing.js +1 -1
- package/dist/world/minimapBake.d.ts +78 -0
- package/dist/world/minimapBake.js +208 -0
- package/dist/world/minimapTrack.d.ts +17 -0
- package/dist/world/minimapTrack.js +22 -0
- package/dist/world/place.d.ts +166 -0
- package/dist/world/place.js +96 -0
- package/dist/world/placeAsset.js +3 -1
- package/dist/world/poleLineKind.d.ts +40 -0
- package/dist/world/poleLineKind.js +97 -0
- package/dist/world/polyline.js +9 -2
- package/dist/world/resourceNode.d.ts +109 -0
- package/dist/world/resourceNode.js +140 -0
- package/dist/world/roads.d.ts +19 -2
- package/dist/world/roads.js +60 -3
- package/dist/world/scatter.js +2 -20
- package/dist/world/streetGenerator.d.ts +159 -0
- package/dist/world/streetGenerator.js +684 -0
- package/dist/world/streets.d.ts +103 -1
- package/dist/world/streets.js +273 -0
- package/dist/world/terraform.js +58 -88
- package/dist/world/terrain.d.ts +65 -1
- package/dist/world/terrain.js +119 -2
- package/dist/world.d.ts +14 -5
- package/dist/world.js +12 -3
- package/package.json +1 -1
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +8 -4
- package/skills/jgengine/api.md +182 -81
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-combat/SKILL.md +19 -1
- package/skills/jgengine-combat/api.md +75 -2
- package/skills/jgengine-combat/capabilities.md +61 -1
- package/skills/jgengine-combat/recipes/portable-damage-effects.md +197 -0
- package/skills/jgengine-combat/recipes/portable-runtime-state.md +154 -0
- package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
- package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +156 -0
- package/skills/jgengine-combat/recipes/visible-attacks.md +81 -0
- package/skills/jgengine-combat/reference.md +33 -4
- package/skills/jgengine-gameplay/SKILL.md +8 -0
- package/skills/jgengine-gameplay/api.md +182 -26
- package/skills/jgengine-gameplay/capabilities.md +96 -2
- package/skills/jgengine-gameplay/recipes/portable-xp-leveling.md +168 -0
- package/skills/jgengine-gameplay/reference-systems.md +10 -5
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +453 -21
- package/skills/jgengine-world/capabilities.md +112 -2
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +43 -4
- package/skills/jgengine/reference-cartridge.md +0 -72
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL-backed debug flags — the single seam hosts use to mirror an engine debug surface (the scene
|
|
3
|
+
* editor, the devtools overlay) into the page query string. Mirroring means the URL is the honest
|
|
4
|
+
* record of what is open: share it to reproduce the state, and strip the param (then reload) to get
|
|
5
|
+
* a clean game with the surface gone. All functions no-op safely off the main browser thread.
|
|
6
|
+
*/
|
|
7
|
+
/** Values that read as "off" when a param is present but disabled — `?debug=0` stays closed. */
|
|
8
|
+
const FALSY = new Set(["", "0", "false", "off", "no"]);
|
|
9
|
+
/** The live browser window, or null when there is no DOM (SSR, tests, worker). */
|
|
10
|
+
function browserWindow() {
|
|
11
|
+
const candidate = globalThis.window;
|
|
12
|
+
if (candidate === undefined || candidate.location === undefined || candidate.history === undefined) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return candidate;
|
|
16
|
+
}
|
|
17
|
+
/** The current query value of `param`, or null when absent (or no DOM). */
|
|
18
|
+
export function readUrlParam(param) {
|
|
19
|
+
const win = browserWindow();
|
|
20
|
+
if (win === null)
|
|
21
|
+
return null;
|
|
22
|
+
return new URLSearchParams(win.location.search).get(param);
|
|
23
|
+
}
|
|
24
|
+
/** True when `param` is present and not an explicit off value (`0`/`false`/`off`/`no`). */
|
|
25
|
+
export function readUrlFlag(param) {
|
|
26
|
+
const value = readUrlParam(param);
|
|
27
|
+
return value !== null && !FALSY.has(value.toLowerCase());
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Sets `param` to `value`, or removes it when `value` is null, rewriting the URL in place with
|
|
31
|
+
* `history.replaceState` so the rest of the query and the hash survive and no history entry is
|
|
32
|
+
* pushed. No-ops without a DOM, and skips the write when the URL already matches.
|
|
33
|
+
*/
|
|
34
|
+
export function writeUrlParam(param, value) {
|
|
35
|
+
const win = browserWindow();
|
|
36
|
+
if (win === null)
|
|
37
|
+
return;
|
|
38
|
+
const url = new URL(win.location.href);
|
|
39
|
+
if (value === null) {
|
|
40
|
+
if (!url.searchParams.has(param))
|
|
41
|
+
return;
|
|
42
|
+
url.searchParams.delete(param);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
if (url.searchParams.get(param) === value)
|
|
46
|
+
return;
|
|
47
|
+
url.searchParams.set(param, value);
|
|
48
|
+
}
|
|
49
|
+
win.history.replaceState(win.history.state, "", url.toString());
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Fires `listener` whenever the query string may have changed out from under a flag — browser
|
|
53
|
+
* back/forward (`popstate`). Manual address-bar edits reload the page, so initial reads cover
|
|
54
|
+
* those; this keeps in-app state honest across history navigation. Returns an unsubscribe.
|
|
55
|
+
*/
|
|
56
|
+
export function subscribeUrlChange(listener) {
|
|
57
|
+
const win = browserWindow();
|
|
58
|
+
if (win === null)
|
|
59
|
+
return () => { };
|
|
60
|
+
win.addEventListener("popstate", listener);
|
|
61
|
+
return () => win.removeEventListener("popstate", listener);
|
|
62
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { CollectionBoxSnapshot } from "./listingBook.js";
|
|
2
|
+
/** One live timed auction in an {@link AuctionBook}: an item stack under open bidding until it closes. */
|
|
3
|
+
export interface Auction {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly sellerId: string;
|
|
6
|
+
readonly itemId: string;
|
|
7
|
+
readonly count: number;
|
|
8
|
+
readonly currency: string;
|
|
9
|
+
readonly startPrice: number;
|
|
10
|
+
readonly minIncrement: number;
|
|
11
|
+
readonly buyoutPrice: number | null;
|
|
12
|
+
readonly listedAt: number;
|
|
13
|
+
readonly endsAt: number;
|
|
14
|
+
readonly highBid: HighBid | null;
|
|
15
|
+
}
|
|
16
|
+
/** The current leading bid on an {@link Auction}. */
|
|
17
|
+
export interface HighBid {
|
|
18
|
+
readonly bidderId: string;
|
|
19
|
+
readonly amount: number;
|
|
20
|
+
readonly at: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Tunables for {@link createAuctionBook} — per-seller auction cap, run length, house cut, and the
|
|
24
|
+
* anti-snipe window: a bid landing within `antiSnipeWindowSeconds` of the close pushes the close
|
|
25
|
+
* out so it ends at least `antiSnipeExtensionSeconds` after that bid.
|
|
26
|
+
*/
|
|
27
|
+
export interface AuctionBookConfig {
|
|
28
|
+
maxAuctionsPerSeller: number;
|
|
29
|
+
durationSeconds: number;
|
|
30
|
+
cutRate: number;
|
|
31
|
+
antiSnipeWindowSeconds?: number;
|
|
32
|
+
antiSnipeExtensionSeconds?: number;
|
|
33
|
+
}
|
|
34
|
+
/** Input to {@link AuctionBook.post} — the goods, opening terms, and current game-time. */
|
|
35
|
+
export interface PostAuctionInput {
|
|
36
|
+
id?: string;
|
|
37
|
+
sellerId: string;
|
|
38
|
+
itemId: string;
|
|
39
|
+
count: number;
|
|
40
|
+
currency: string;
|
|
41
|
+
startPrice: number;
|
|
42
|
+
minIncrement: number;
|
|
43
|
+
buyoutPrice?: number;
|
|
44
|
+
now: number;
|
|
45
|
+
}
|
|
46
|
+
/** Why {@link AuctionBook.post} refused an auction. */
|
|
47
|
+
export type PostAuctionReason = "invalid-count" | "invalid-price" | "invalid-increment" | "invalid-buyout" | "auction-cap-reached";
|
|
48
|
+
/** Result of {@link AuctionBook.post}. */
|
|
49
|
+
export type PostAuctionResult = {
|
|
50
|
+
status: "ok";
|
|
51
|
+
auction: Auction;
|
|
52
|
+
} | {
|
|
53
|
+
status: "rejected";
|
|
54
|
+
reason: PostAuctionReason;
|
|
55
|
+
};
|
|
56
|
+
/** Why {@link AuctionBook.bid} refused a bid. */
|
|
57
|
+
export type BidReason = "not-found" | "ended" | "own-auction" | "already-leading" | "bid-too-low" | "invalid-amount" | "no-buyout";
|
|
58
|
+
/**
|
|
59
|
+
* Result of {@link AuctionBook.bid}. On `ok` the caller must escrow `escrowed` from the bidder's
|
|
60
|
+
* wallet; any previously escrowed leading bid has already been refunded to that bidder's
|
|
61
|
+
* collection box. `won` is true when the bid met the buyout price and settled the auction
|
|
62
|
+
* immediately.
|
|
63
|
+
*/
|
|
64
|
+
export type BidResult = {
|
|
65
|
+
status: "ok";
|
|
66
|
+
auction: Auction;
|
|
67
|
+
escrowed: number;
|
|
68
|
+
won: boolean;
|
|
69
|
+
} | {
|
|
70
|
+
status: "rejected";
|
|
71
|
+
reason: BidReason;
|
|
72
|
+
};
|
|
73
|
+
/** Result of {@link AuctionBook.cancel}: a bid-on auction can no longer be withdrawn. */
|
|
74
|
+
export type CancelAuctionResult = {
|
|
75
|
+
status: "ok";
|
|
76
|
+
auction: Auction;
|
|
77
|
+
} | {
|
|
78
|
+
status: "rejected";
|
|
79
|
+
reason: "not-found" | "not-owner" | "has-bids";
|
|
80
|
+
};
|
|
81
|
+
/** How one closed auction resolved during {@link AuctionBook.settleExpired}. */
|
|
82
|
+
export type AuctionSettlement = {
|
|
83
|
+
status: "sold";
|
|
84
|
+
auction: Auction;
|
|
85
|
+
winnerId: string;
|
|
86
|
+
price: number;
|
|
87
|
+
houseCut: number;
|
|
88
|
+
sellerProceeds: number;
|
|
89
|
+
} | {
|
|
90
|
+
status: "returned";
|
|
91
|
+
auction: Auction;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* A timed-bid auction marketplace plus the per-player collection boxes behind it. See
|
|
95
|
+
* {@link createAuctionBook}.
|
|
96
|
+
*/
|
|
97
|
+
export interface AuctionBook {
|
|
98
|
+
post(input: PostAuctionInput): PostAuctionResult;
|
|
99
|
+
bid(auctionId: string, bidderId: string, amount: number, now: number): BidResult;
|
|
100
|
+
buyout(auctionId: string, buyerId: string, now: number): BidResult;
|
|
101
|
+
cancel(auctionId: string, sellerId: string): CancelAuctionResult;
|
|
102
|
+
settleExpired(now: number): readonly AuctionSettlement[];
|
|
103
|
+
get(auctionId: string): Auction | null;
|
|
104
|
+
active(): readonly Auction[];
|
|
105
|
+
auctionsOf(sellerId: string): readonly Auction[];
|
|
106
|
+
countOf(sellerId: string): number;
|
|
107
|
+
collectionOf(playerId: string): CollectionBoxSnapshot;
|
|
108
|
+
claimCurrency(playerId: string): Readonly<Record<string, number>>;
|
|
109
|
+
claimItem(playerId: string, itemId: string, count: number): boolean;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Timed-bid auctions in the WoW/BDO auction-house mold: post an item with a start price, minimum
|
|
113
|
+
* increment, and optional buyout; bids escrow currency, outbid players are refunded into their
|
|
114
|
+
* collection box, bids near the close extend it (anti-snipe), and settlement pays the seller minus
|
|
115
|
+
* the house cut while the item lands in the winner's collection box. Unsold auctions return the
|
|
116
|
+
* goods to the seller's box. Wallet and inventory movement is the caller's job (mirrors
|
|
117
|
+
* `economy/listingBook`) — this primitive owns the auction lifecycle and the escrowed
|
|
118
|
+
* collection-box bookkeeping behind it.
|
|
119
|
+
*
|
|
120
|
+
* @capability auction-book timed bid auctions with escrowed bids, buyout, anti-snipe close extension, and collection-box settlement
|
|
121
|
+
*/
|
|
122
|
+
export declare function createAuctionBook(config: AuctionBookConfig): AuctionBook;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timed-bid auctions in the WoW/BDO auction-house mold: post an item with a start price, minimum
|
|
3
|
+
* increment, and optional buyout; bids escrow currency, outbid players are refunded into their
|
|
4
|
+
* collection box, bids near the close extend it (anti-snipe), and settlement pays the seller minus
|
|
5
|
+
* the house cut while the item lands in the winner's collection box. Unsold auctions return the
|
|
6
|
+
* goods to the seller's box. Wallet and inventory movement is the caller's job (mirrors
|
|
7
|
+
* `economy/listingBook`) — this primitive owns the auction lifecycle and the escrowed
|
|
8
|
+
* collection-box bookkeeping behind it.
|
|
9
|
+
*
|
|
10
|
+
* @capability auction-book timed bid auctions with escrowed bids, buyout, anti-snipe close extension, and collection-box settlement
|
|
11
|
+
*/
|
|
12
|
+
export function createAuctionBook(config) {
|
|
13
|
+
const auctions = new Map();
|
|
14
|
+
const boxes = new Map();
|
|
15
|
+
let counter = 0;
|
|
16
|
+
function boxOf(playerId) {
|
|
17
|
+
let box = boxes.get(playerId);
|
|
18
|
+
if (box === undefined) {
|
|
19
|
+
box = { currency: {}, items: [] };
|
|
20
|
+
boxes.set(playerId, box);
|
|
21
|
+
}
|
|
22
|
+
return box;
|
|
23
|
+
}
|
|
24
|
+
function creditCurrency(playerId, currency, amount) {
|
|
25
|
+
const box = boxOf(playerId);
|
|
26
|
+
box.currency[currency] = (box.currency[currency] ?? 0) + amount;
|
|
27
|
+
}
|
|
28
|
+
function creditItem(playerId, itemId, count) {
|
|
29
|
+
const box = boxOf(playerId);
|
|
30
|
+
const existing = box.items.find((stack) => stack.itemId === itemId);
|
|
31
|
+
if (existing !== undefined)
|
|
32
|
+
existing.count += count;
|
|
33
|
+
else
|
|
34
|
+
box.items.push({ itemId, count });
|
|
35
|
+
}
|
|
36
|
+
function countOf(sellerId) {
|
|
37
|
+
let total = 0;
|
|
38
|
+
for (const auction of auctions.values())
|
|
39
|
+
if (auction.sellerId === sellerId)
|
|
40
|
+
total += 1;
|
|
41
|
+
return total;
|
|
42
|
+
}
|
|
43
|
+
function snapshot(state) {
|
|
44
|
+
return { ...state, highBid: state.highBid };
|
|
45
|
+
}
|
|
46
|
+
function minimumBid(state) {
|
|
47
|
+
return state.highBid === null ? state.startPrice : state.highBid.amount + state.minIncrement;
|
|
48
|
+
}
|
|
49
|
+
function settle(state, winnerId, price) {
|
|
50
|
+
auctions.delete(state.id);
|
|
51
|
+
const houseCut = Math.floor(price * config.cutRate);
|
|
52
|
+
const sellerProceeds = price - houseCut;
|
|
53
|
+
creditCurrency(state.sellerId, state.currency, sellerProceeds);
|
|
54
|
+
creditItem(winnerId, state.itemId, state.count);
|
|
55
|
+
return { status: "sold", auction: snapshot(state), winnerId, price, houseCut, sellerProceeds };
|
|
56
|
+
}
|
|
57
|
+
function acceptBid(state, bidderId, amount, now) {
|
|
58
|
+
if (state.highBid !== null) {
|
|
59
|
+
creditCurrency(state.highBid.bidderId, state.currency, state.highBid.amount);
|
|
60
|
+
}
|
|
61
|
+
if (state.buyoutPrice !== null && amount >= state.buyoutPrice) {
|
|
62
|
+
state.highBid = { bidderId, amount: state.buyoutPrice, at: now };
|
|
63
|
+
const settlement = settle(state, bidderId, state.buyoutPrice);
|
|
64
|
+
if (settlement.status !== "sold")
|
|
65
|
+
throw new Error("unreachable");
|
|
66
|
+
return { status: "ok", auction: settlement.auction, escrowed: state.buyoutPrice, won: true };
|
|
67
|
+
}
|
|
68
|
+
state.highBid = { bidderId, amount, at: now };
|
|
69
|
+
const window = config.antiSnipeWindowSeconds ?? 0;
|
|
70
|
+
const extension = config.antiSnipeExtensionSeconds ?? 0;
|
|
71
|
+
if (window > 0 && extension > 0 && state.endsAt - now < window) {
|
|
72
|
+
state.endsAt = Math.max(state.endsAt, now + extension);
|
|
73
|
+
}
|
|
74
|
+
return { status: "ok", auction: snapshot(state), escrowed: amount, won: false };
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
post(input) {
|
|
78
|
+
if (!Number.isInteger(input.count) || input.count < 1) {
|
|
79
|
+
return { status: "rejected", reason: "invalid-count" };
|
|
80
|
+
}
|
|
81
|
+
if (!Number.isFinite(input.startPrice) || input.startPrice <= 0) {
|
|
82
|
+
return { status: "rejected", reason: "invalid-price" };
|
|
83
|
+
}
|
|
84
|
+
if (!Number.isFinite(input.minIncrement) || input.minIncrement <= 0) {
|
|
85
|
+
return { status: "rejected", reason: "invalid-increment" };
|
|
86
|
+
}
|
|
87
|
+
if (input.buyoutPrice !== undefined) {
|
|
88
|
+
if (!Number.isFinite(input.buyoutPrice) || input.buyoutPrice < input.startPrice) {
|
|
89
|
+
return { status: "rejected", reason: "invalid-buyout" };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (countOf(input.sellerId) >= config.maxAuctionsPerSeller) {
|
|
93
|
+
return { status: "rejected", reason: "auction-cap-reached" };
|
|
94
|
+
}
|
|
95
|
+
const id = input.id ?? `auction_${(counter += 1)}`;
|
|
96
|
+
const state = {
|
|
97
|
+
id,
|
|
98
|
+
sellerId: input.sellerId,
|
|
99
|
+
itemId: input.itemId,
|
|
100
|
+
count: input.count,
|
|
101
|
+
currency: input.currency,
|
|
102
|
+
startPrice: input.startPrice,
|
|
103
|
+
minIncrement: input.minIncrement,
|
|
104
|
+
buyoutPrice: input.buyoutPrice ?? null,
|
|
105
|
+
listedAt: input.now,
|
|
106
|
+
endsAt: input.now + config.durationSeconds,
|
|
107
|
+
highBid: null,
|
|
108
|
+
};
|
|
109
|
+
auctions.set(id, state);
|
|
110
|
+
return { status: "ok", auction: snapshot(state) };
|
|
111
|
+
},
|
|
112
|
+
bid(auctionId, bidderId, amount, now) {
|
|
113
|
+
const state = auctions.get(auctionId);
|
|
114
|
+
if (state === undefined)
|
|
115
|
+
return { status: "rejected", reason: "not-found" };
|
|
116
|
+
if (state.endsAt <= now)
|
|
117
|
+
return { status: "rejected", reason: "ended" };
|
|
118
|
+
if (state.sellerId === bidderId)
|
|
119
|
+
return { status: "rejected", reason: "own-auction" };
|
|
120
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
121
|
+
return { status: "rejected", reason: "invalid-amount" };
|
|
122
|
+
}
|
|
123
|
+
if (state.highBid?.bidderId === bidderId) {
|
|
124
|
+
return { status: "rejected", reason: "already-leading" };
|
|
125
|
+
}
|
|
126
|
+
if (amount < minimumBid(state))
|
|
127
|
+
return { status: "rejected", reason: "bid-too-low" };
|
|
128
|
+
return acceptBid(state, bidderId, amount, now);
|
|
129
|
+
},
|
|
130
|
+
buyout(auctionId, buyerId, now) {
|
|
131
|
+
const state = auctions.get(auctionId);
|
|
132
|
+
if (state === undefined)
|
|
133
|
+
return { status: "rejected", reason: "not-found" };
|
|
134
|
+
if (state.endsAt <= now)
|
|
135
|
+
return { status: "rejected", reason: "ended" };
|
|
136
|
+
if (state.sellerId === buyerId)
|
|
137
|
+
return { status: "rejected", reason: "own-auction" };
|
|
138
|
+
if (state.buyoutPrice === null)
|
|
139
|
+
return { status: "rejected", reason: "no-buyout" };
|
|
140
|
+
if (state.highBid?.bidderId === buyerId) {
|
|
141
|
+
creditCurrency(buyerId, state.currency, state.highBid.amount);
|
|
142
|
+
state.highBid = null;
|
|
143
|
+
}
|
|
144
|
+
return acceptBid(state, buyerId, state.buyoutPrice, now);
|
|
145
|
+
},
|
|
146
|
+
cancel(auctionId, sellerId) {
|
|
147
|
+
const state = auctions.get(auctionId);
|
|
148
|
+
if (state === undefined)
|
|
149
|
+
return { status: "rejected", reason: "not-found" };
|
|
150
|
+
if (state.sellerId !== sellerId)
|
|
151
|
+
return { status: "rejected", reason: "not-owner" };
|
|
152
|
+
if (state.highBid !== null)
|
|
153
|
+
return { status: "rejected", reason: "has-bids" };
|
|
154
|
+
auctions.delete(auctionId);
|
|
155
|
+
return { status: "ok", auction: snapshot(state) };
|
|
156
|
+
},
|
|
157
|
+
settleExpired(now) {
|
|
158
|
+
const ended = [];
|
|
159
|
+
for (const state of auctions.values())
|
|
160
|
+
if (state.endsAt <= now)
|
|
161
|
+
ended.push(state);
|
|
162
|
+
const settlements = [];
|
|
163
|
+
for (const state of ended) {
|
|
164
|
+
if (state.highBid === null) {
|
|
165
|
+
auctions.delete(state.id);
|
|
166
|
+
creditItem(state.sellerId, state.itemId, state.count);
|
|
167
|
+
settlements.push({ status: "returned", auction: snapshot(state) });
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
settlements.push(settle(state, state.highBid.bidderId, state.highBid.amount));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return settlements;
|
|
174
|
+
},
|
|
175
|
+
get(auctionId) {
|
|
176
|
+
const state = auctions.get(auctionId);
|
|
177
|
+
return state === undefined ? null : snapshot(state);
|
|
178
|
+
},
|
|
179
|
+
active() {
|
|
180
|
+
return [...auctions.values()].map(snapshot);
|
|
181
|
+
},
|
|
182
|
+
auctionsOf(sellerId) {
|
|
183
|
+
return [...auctions.values()].filter((a) => a.sellerId === sellerId).map(snapshot);
|
|
184
|
+
},
|
|
185
|
+
countOf,
|
|
186
|
+
collectionOf(playerId) {
|
|
187
|
+
const box = boxes.get(playerId);
|
|
188
|
+
if (box === undefined)
|
|
189
|
+
return { currency: {}, items: [] };
|
|
190
|
+
return { currency: { ...box.currency }, items: box.items.map((stack) => ({ ...stack })) };
|
|
191
|
+
},
|
|
192
|
+
claimCurrency(playerId) {
|
|
193
|
+
const box = boxOf(playerId);
|
|
194
|
+
const claimed = box.currency;
|
|
195
|
+
box.currency = {};
|
|
196
|
+
return claimed;
|
|
197
|
+
},
|
|
198
|
+
claimItem(playerId, itemId, count) {
|
|
199
|
+
const box = boxOf(playerId);
|
|
200
|
+
const index = box.items.findIndex((stack) => stack.itemId === itemId);
|
|
201
|
+
if (index === -1)
|
|
202
|
+
return false;
|
|
203
|
+
const stack = box.items[index];
|
|
204
|
+
if (stack.count < count)
|
|
205
|
+
return false;
|
|
206
|
+
if (stack.count === count)
|
|
207
|
+
box.items.splice(index, 1);
|
|
208
|
+
else
|
|
209
|
+
box.items[index] = { ...stack, count: stack.count - count };
|
|
210
|
+
return true;
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** One completed sale recorded into a {@link PriceHistory}. */
|
|
2
|
+
export interface SaleRecord {
|
|
3
|
+
readonly itemId: string;
|
|
4
|
+
readonly count: number;
|
|
5
|
+
readonly unitPrice: number;
|
|
6
|
+
readonly currency: string;
|
|
7
|
+
readonly at: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Tunables for {@link createPriceHistory} — the per-item sample cap bounds memory; the optional
|
|
11
|
+
* window drops samples older than `windowSeconds` whenever the history is written or read with a
|
|
12
|
+
* newer timestamp.
|
|
13
|
+
*/
|
|
14
|
+
export interface PriceHistoryConfig {
|
|
15
|
+
maxSamplesPerItem: number;
|
|
16
|
+
windowSeconds?: number;
|
|
17
|
+
}
|
|
18
|
+
/** Aggregated market stats for one item over the retained samples. */
|
|
19
|
+
export interface PriceStats {
|
|
20
|
+
readonly sampleCount: number;
|
|
21
|
+
readonly volume: number;
|
|
22
|
+
readonly minUnitPrice: number;
|
|
23
|
+
readonly maxUnitPrice: number;
|
|
24
|
+
readonly averageUnitPrice: number;
|
|
25
|
+
readonly latestUnitPrice: number;
|
|
26
|
+
}
|
|
27
|
+
/** A bounded rolling record of completed sales per item. See {@link createPriceHistory}. */
|
|
28
|
+
export interface PriceHistory {
|
|
29
|
+
record(sale: SaleRecord): void;
|
|
30
|
+
statsOf(itemId: string, now?: number): PriceStats | null;
|
|
31
|
+
samplesOf(itemId: string, now?: number): readonly SaleRecord[];
|
|
32
|
+
itemIds(): readonly string[];
|
|
33
|
+
clear(itemId?: string): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The "market price" readout behind every auction house: a bounded rolling record of completed
|
|
37
|
+
* sales per item, aggregated into min/max/volume-weighted-average/latest unit-price stats so games
|
|
38
|
+
* can show current value and recent trends. Memory is bounded by `maxSamplesPerItem` (oldest
|
|
39
|
+
* samples drop first) and optionally by a sliding time window; timestamps are injected by the
|
|
40
|
+
* caller so the history stays deterministic.
|
|
41
|
+
*
|
|
42
|
+
* @capability price-history rolling per-item sale records aggregated into market price stats
|
|
43
|
+
*/
|
|
44
|
+
export declare function createPriceHistory(config: PriceHistoryConfig): PriceHistory;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "market price" readout behind every auction house: a bounded rolling record of completed
|
|
3
|
+
* sales per item, aggregated into min/max/volume-weighted-average/latest unit-price stats so games
|
|
4
|
+
* can show current value and recent trends. Memory is bounded by `maxSamplesPerItem` (oldest
|
|
5
|
+
* samples drop first) and optionally by a sliding time window; timestamps are injected by the
|
|
6
|
+
* caller so the history stays deterministic.
|
|
7
|
+
*
|
|
8
|
+
* @capability price-history rolling per-item sale records aggregated into market price stats
|
|
9
|
+
*/
|
|
10
|
+
export function createPriceHistory(config) {
|
|
11
|
+
const samples = new Map();
|
|
12
|
+
function trim(list, now) {
|
|
13
|
+
if (config.windowSeconds !== undefined && now !== undefined) {
|
|
14
|
+
const cutoff = now - config.windowSeconds;
|
|
15
|
+
let drop = 0;
|
|
16
|
+
while (drop < list.length && list[drop].at < cutoff)
|
|
17
|
+
drop += 1;
|
|
18
|
+
if (drop > 0)
|
|
19
|
+
list.splice(0, drop);
|
|
20
|
+
}
|
|
21
|
+
if (list.length > config.maxSamplesPerItem) {
|
|
22
|
+
list.splice(0, list.length - config.maxSamplesPerItem);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function retained(itemId, now) {
|
|
26
|
+
const list = samples.get(itemId);
|
|
27
|
+
if (list === undefined)
|
|
28
|
+
return null;
|
|
29
|
+
trim(list, now);
|
|
30
|
+
if (list.length === 0) {
|
|
31
|
+
samples.delete(itemId);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return list;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
record(sale) {
|
|
38
|
+
if (!Number.isFinite(sale.unitPrice) || sale.unitPrice < 0)
|
|
39
|
+
return;
|
|
40
|
+
if (!Number.isInteger(sale.count) || sale.count < 1)
|
|
41
|
+
return;
|
|
42
|
+
let list = samples.get(sale.itemId);
|
|
43
|
+
if (list === undefined) {
|
|
44
|
+
list = [];
|
|
45
|
+
samples.set(sale.itemId, list);
|
|
46
|
+
}
|
|
47
|
+
list.push(sale);
|
|
48
|
+
trim(list, sale.at);
|
|
49
|
+
},
|
|
50
|
+
statsOf(itemId, now) {
|
|
51
|
+
const list = retained(itemId, now);
|
|
52
|
+
if (list === null)
|
|
53
|
+
return null;
|
|
54
|
+
let volume = 0;
|
|
55
|
+
let spend = 0;
|
|
56
|
+
let min = Number.POSITIVE_INFINITY;
|
|
57
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
58
|
+
for (const sale of list) {
|
|
59
|
+
volume += sale.count;
|
|
60
|
+
spend += sale.unitPrice * sale.count;
|
|
61
|
+
if (sale.unitPrice < min)
|
|
62
|
+
min = sale.unitPrice;
|
|
63
|
+
if (sale.unitPrice > max)
|
|
64
|
+
max = sale.unitPrice;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
sampleCount: list.length,
|
|
68
|
+
volume,
|
|
69
|
+
minUnitPrice: min,
|
|
70
|
+
maxUnitPrice: max,
|
|
71
|
+
averageUnitPrice: spend / volume,
|
|
72
|
+
latestUnitPrice: list[list.length - 1].unitPrice,
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
samplesOf(itemId, now) {
|
|
76
|
+
const list = retained(itemId, now);
|
|
77
|
+
return list === null ? [] : [...list];
|
|
78
|
+
},
|
|
79
|
+
itemIds() {
|
|
80
|
+
return [...samples.keys()];
|
|
81
|
+
},
|
|
82
|
+
clear(itemId) {
|
|
83
|
+
if (itemId === undefined)
|
|
84
|
+
samples.clear();
|
|
85
|
+
else
|
|
86
|
+
samples.delete(itemId);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { type Curve } from "../game/progression.js";
|
|
2
|
+
/**
|
|
3
|
+
* Plain, JSON-serializable snapshot of a point pool. `earned` is the lifetime total ever
|
|
4
|
+
* banked; every committed unlock lives in `unlocked` as `id → cost paid`. `spent` and
|
|
5
|
+
* `available` are always derived (`spent = Σ unlocked`, `available = earned − spent`) so the
|
|
6
|
+
* two can never drift out of sync across a serialize/hydrate round-trip.
|
|
7
|
+
*/
|
|
8
|
+
export interface UnlockPointsState {
|
|
9
|
+
/** Total points ever granted; unaffected by spending, refunds, or respec. */
|
|
10
|
+
earned: number;
|
|
11
|
+
/** Committed unlocks as `unlockId → cost paid`. Sums to the currently spent total. */
|
|
12
|
+
unlocked: Readonly<Record<string, number>>;
|
|
13
|
+
}
|
|
14
|
+
/** Options for {@link UnlockPoints.spend}. */
|
|
15
|
+
export interface SpendOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Injected prerequisite gate, checked before any points are deducted. Return `false` to
|
|
18
|
+
* reject with `"missing-prerequisites"`. Compose with `economy/techTree` without coupling —
|
|
19
|
+
* e.g. `{ requires: () => techTree.canUnlock(userId, id).ok }` — or any game-defined rule.
|
|
20
|
+
*/
|
|
21
|
+
requires?: () => boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Why a {@link UnlockPoints.spend} was refused; the pool is left untouched on any rejection. */
|
|
24
|
+
export type SpendRejection = "invalid-cost" | "already-unlocked" | "missing-prerequisites" | "insufficient-points";
|
|
25
|
+
/** Result of {@link UnlockPoints.spend}: the new `available` balance on success, else a reason. */
|
|
26
|
+
export type SpendResult = {
|
|
27
|
+
ok: true;
|
|
28
|
+
available: number;
|
|
29
|
+
} | {
|
|
30
|
+
ok: false;
|
|
31
|
+
reason: SpendRejection;
|
|
32
|
+
};
|
|
33
|
+
/** Configuration for {@link createUnlockPoints}. */
|
|
34
|
+
export interface UnlockPointsConfig {
|
|
35
|
+
/**
|
|
36
|
+
* Points granted for *reaching* a level, evaluated by {@link UnlockPoints.grantForLevel} and
|
|
37
|
+
* summed across gained levels by {@link UnlockPoints.grantOnLevelUp}. Non-uniform curves are
|
|
38
|
+
* fine (front-loaded, milestone `steps`, etc.). Defaults to one point per level.
|
|
39
|
+
*/
|
|
40
|
+
perLevel?: Curve;
|
|
41
|
+
/** Points banked before any leveling — a starting unlock budget. Default `0`. */
|
|
42
|
+
start?: number;
|
|
43
|
+
/** Rehydrate from a prior {@link UnlockPoints.snapshot}. */
|
|
44
|
+
state?: UnlockPointsState;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A spendable unlock-point economy: a pool you earn (per level or by direct grant) and spend to
|
|
48
|
+
* unlock nodes, with per-unlock refunds and a full respec.
|
|
49
|
+
*/
|
|
50
|
+
export interface UnlockPoints {
|
|
51
|
+
/** Unspent points on hand (`earned − spent`). */
|
|
52
|
+
available(): number;
|
|
53
|
+
/** Lifetime points ever granted; never reduced by spending or respec. */
|
|
54
|
+
earned(): number;
|
|
55
|
+
/** Points currently committed to unlocks (`Σ` of every unlock cost). */
|
|
56
|
+
spent(): number;
|
|
57
|
+
/** Bank an arbitrary point grant (quest reward, difficulty bonus). Non-positive/non-finite is ignored. */
|
|
58
|
+
grant(amount: number): void;
|
|
59
|
+
/** Bank the `perLevel` grant for reaching `level`; returns the amount actually added. */
|
|
60
|
+
grantForLevel(level: number): number;
|
|
61
|
+
/** Bank grants for every level in `(fromLevel, toLevel]`; returns the total added. */
|
|
62
|
+
grantOnLevelUp(fromLevel: number, toLevel: number): number;
|
|
63
|
+
/** True when `available() >= cost`. */
|
|
64
|
+
canAfford(cost: number): boolean;
|
|
65
|
+
/** Deduct `cost` and record `unlockId`, unless rejected (see {@link SpendResult}). */
|
|
66
|
+
spend(unlockId: string, cost: number, opts?: SpendOptions): SpendResult;
|
|
67
|
+
/** True once `unlockId` has been spent on and not refunded. */
|
|
68
|
+
isUnlocked(id: string): boolean;
|
|
69
|
+
/** Points paid for `id`, or `0` if it is not unlocked. */
|
|
70
|
+
costOf(id: string): number;
|
|
71
|
+
/** Every currently unlocked id, in insertion order. */
|
|
72
|
+
unlockedIds(): string[];
|
|
73
|
+
/** Refund a single unlock, returning its cost to `available`. Returns `false` if not unlocked. */
|
|
74
|
+
refund(unlockId: string): boolean;
|
|
75
|
+
/** Mindwipe: refund every unlock and clear the recorded set, keeping `earned`. Returns points refunded. */
|
|
76
|
+
respec(): number;
|
|
77
|
+
/** Plain serializable snapshot; safe to `JSON.stringify` and later {@link hydrate}. */
|
|
78
|
+
snapshot(): UnlockPointsState;
|
|
79
|
+
/** Replace all state from a prior {@link snapshot}, discarding invalid entries. */
|
|
80
|
+
hydrate(state: UnlockPointsState): void;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Spendable unlock-point economy — the seam between leveling and a tech tree. Points are earned
|
|
84
|
+
* per level (via a configurable, possibly non-uniform grant curve) or granted directly, then
|
|
85
|
+
* spent to unlock nodes; spending can defer prerequisite checks to an injected predicate so it
|
|
86
|
+
* composes with `economy/techTree` (or any gate) without depending on it. Individual unlocks
|
|
87
|
+
* refund, and `respec` fully refunds a build while preserving lifetime earned points. All state
|
|
88
|
+
* is plain and JSON round-trips.
|
|
89
|
+
*
|
|
90
|
+
* @capability unlock-points spendable point pool earned per level and spent to unlock nodes, with refund and respec
|
|
91
|
+
*/
|
|
92
|
+
export declare function createUnlockPoints(config?: UnlockPointsConfig): UnlockPoints;
|