@idosgames/core 0.5.1 → 0.7.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 CHANGED
@@ -14,6 +14,154 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.7.0] - 2026-08-10
18
+
19
+ Real-money payment support. Every configured price becomes a **`PriceOptions` dictionary**, a
20
+ store SKU becomes one of the ways to pay, and the client now tells the server which platform it
21
+ runs on. Breaking — see below.
22
+
23
+ ### Added
24
+
25
+ - **`PriceOption` / `PriceOptions` (`zPriceOption`, `zPriceOptions`)** — the platform-wide price
26
+ standard. A `ResourceConsume` is charged as a whole ("and"); a price across platforms is a
27
+ choice ("or") — a store SKU on mobile, a token on web. The dictionary key is the `OptionID`,
28
+ and the server substitutes it when the field is empty.
29
+ - **`ResourceEntryType.Purchase` + `ResourceEntry.ProductID`** — an entry that means "paid in a
30
+ store". `ProductID` addresses `Purchase.Products`, our stable id, not a per-store SKU.
31
+ - **`PaymentProof` (`zPaymentProof`)** — the receipt attached to a request. Required exactly when
32
+ the chosen option holds a `Purchase` entry: the server never takes money itself, it verifies
33
+ what the store issued to this client.
34
+ - **`client.purchase` (`PurchaseService`)** — `getDefinitions`, `getUserState`,
35
+ `validatePurchase`, `validatePurchasesBatch`. `Resources` is applied only when `Granted` is
36
+ true: `AlreadyProcessed` and `Restored` carry the same block, and crediting it would pay the
37
+ reward twice for one payment — which is exactly what a store's re-delivered receipt looks like.
38
+ - **`client.checkout` (`CheckoutService`)** — `availableOptions`, `isAvailable`, `requirementOf`,
39
+ `storeProductOf`, `cryptoShortfallOf`. It deliberately buys nothing: the purchase belongs to the
40
+ module that owns the entity (`client.store.purchase`, `client.lootbox.open`, …), which is the
41
+ only place that knows count, target ids and the idempotency key.
42
+ - **`ClientPlatform`** (`Unknown | Web | Android | Ios`) and `IDosGamesClientConfig.clientPlatform`.
43
+ Auto-detected by `detectClientPlatform()` and sent as **`X-IG-Platform`** on every request —
44
+ once, in the transport, because a service that forgot to pass it would silently get a different
45
+ price list. Detection is conservative: a browser is `Web` even on a phone, only a real native
46
+ wrapper (Capacitor / Cordova) counts as `Android`/`Ios`, and no `navigator` at all is `Unknown`
47
+ rather than `Web`.
48
+ - `SegmentGate.Platforms` — gate any definition on the client platform.
49
+ - Optional **`selectedOptionID`** on every call that charges a configured price —
50
+ `store.purchase`, `lootbox.open`, `craft.craft`, `premium.purchaseItemOrCurrency`,
51
+ `character.unlockCharacter`, `collection.openPack`, `dealOffer.executeNode`,
52
+ `timedBoost.activate`, `item.upgradeLevel`, `coopEvent.spin`, `gameLoop.boardSpecialChoose`,
53
+ `match.createMatch`, `marketplace.createListing` / `createAuction`. Omitting it keeps
54
+ single-price entities working with no client change: the server takes the first option
55
+ available on this platform.
56
+ - Optional **`payment`** (a `PaymentProof`) on the calls the server can settle against a store
57
+ receipt: `store.purchase`, `lootbox.open`, `character.unlockCharacter`, `collection.openPack`,
58
+ `dealOffer.executeNode`, `timedBoost.activate`. The rest deliberately have none — an upgrade
59
+ priced by a level formula, a per-fact coop spin, a refundable listing fee and a P2P match stake
60
+ cannot be one store SKU, and the server rejects a `Purchase` entry in those places.
61
+
62
+ ### Changed
63
+
64
+ - **Every price field is now `PriceOptions`.** `StoreOfferDefinition.Cost`,
65
+ `CraftDefinition.RequiredResources`, `LootboxDefinition.PriceOptions` (was a list of
66
+ `LootboxPriceOption`), `PremiumDefinition.PriceOptions`, `CollectionPackDefinition.Cost`,
67
+ `CharacterDefinition.Unlock.Cost` / stat & level `BaseCostResource` / `UpgradeCost`,
68
+ `ItemUpgrade.BaseCostResource`, `TimedBoostDefinition.ActivationCost`,
69
+ `CoopEventBuildObjects.SpinCost`, `GameLoop` special-mode choice `EntryCost`,
70
+ `DealOfferPurchaseAction.DirectCost`, `MatchCreationSettings.Cost`,
71
+ `MarketplaceListingSettings.ListingFee` — all replaced by a `PriceOptions` dictionary.
72
+ - `lootbox.open`'s `selectedOptionID` is a **string** (was `number`) and is now optional.
73
+ - Prices come back **already filtered by the request's platform**, and the charge re-checks it.
74
+ The client renders what the server gave; a hidden option cannot be paid anyway.
75
+
76
+ ### Removed
77
+
78
+ - **`ResourceEntryType.UsdCent`** — it was declared but never worked (any operation with it
79
+ failed inside the inventory layer), so a price in real money looked configurable while the
80
+ entity was impossible to buy. `Purchase` replaces it.
81
+
82
+ ### Migration
83
+
84
+ Read a price through `client.checkout.availableOptions(entity.PriceOptions)` instead of the old
85
+ single cost field, and pass the chosen `OptionID` to the module's buying call. A price paid in a
86
+ store additionally needs a `PaymentProof` — buy in the store first, then hand the receipt to the
87
+ same call. Nothing else changes for entities that keep a single price.
88
+
89
+ ## [0.6.0] - 2026-08-09
90
+
91
+ ### Added
92
+
93
+ - **`client.tutorial` (`TutorialService`)** — onboarding flows: `getTutorialDefinitions`,
94
+ `getUserTutorialState`, `startFlow`, `reportStepShown`, `completeStep`,
95
+ `completeStepsBatch`, `skipStep`, `skipFlow`, `claimFlowReward`, `resetFlow`. Definitions
96
+ are cached like the rest of the title config.
97
+ - **Tutorial progress rides on every response.** A tutorial step is usually closed by an
98
+ ordinary game action (a purchase, a dice roll), not by a tutorial call — so the backend puts
99
+ an optional `TutorialProgress` block into the envelope of **any** response, the transport
100
+ lifts it out, and `UserData` merges it into the state. Subscribe to `tutorial:systemProgress`
101
+ instead of polling `getUserTutorialState`: the state is already fresh after any request, and
102
+ polling also skews the `ReportStepShown` funnel.
103
+ - `SegmentGate.Tutorial` (`TutorialGateCondition` / `TutorialGateMode`) — gate any module's
104
+ definitions on the player's onboarding progress.
105
+ - **`client.localization` (`LocalizationService`)** — `t(key, params)`, `has`, `locale`,
106
+ `fallbackLocale`, `locales`, `setLocale`, `getManifest`. Resolution is
107
+ **own table → fallback table → the key itself**, so a title whose config holds literal
108
+ display names ("Iron Sword") keeps working without migration: the literal is a key with no
109
+ translation. Plural forms come from `Intl.PluralRules` (suffixes `.one/.few/.many/.other`).
110
+ - Translation tables arrive **with the player state** — no separate load. `UserService` sends
111
+ the locale and the versions of the tables it already has and resolves what comes back
112
+ through the same source chain as the title config (storage → CDN → endpoint → stale copy),
113
+ with one deliberate difference: a failure here does **not** fail the login. No config means
114
+ nothing to play; no translations means the UI shows keys.
115
+ - `settings.locale` (`SettingsInput.locale`) — the starting locale, defaulting to the device
116
+ language. It is a **wish**, not a fact: the server folds it to what the title is actually
117
+ translated into (`pt-BR` → `pt` → `en`), and the answer is `client.localization.locale`.
118
+ - `TitlePublicConfigurationModel.Localization` — locale settings only. The tables are
119
+ delivered separately, one file per locale.
120
+ - `localization:changed` event (login, `setLocale`, new tables). `t()` is synchronous, so
121
+ already-rendered labels need this signal to repaint.
122
+ - `PostOptions.throttleKey` — override the throttle / in-flight-dedup key when one route
123
+ serves genuinely different requests back to back. Added for the localization tables: own and
124
+ fallback both go through `Localization/GetLocalizationTable`, and under the default key
125
+ (the endpoint) the second call would be dropped as a duplicate — the fallback table would
126
+ never arrive, silently, and only for players whose language is partially translated. The
127
+ throttle window still applies, just per key.
128
+ - **Batch claims** — `season.claimTierRewardsBatch(seasonChainID, tierNumbers)` and
129
+ `referral.claimInviteRewardsBatch(inviteRewardIDs)`. Season tiers in particular are reached
130
+ in batches: one token grant can raise a player through several at once. The merged
131
+ `Resources` is applied **once** from the top level; per-item `Data.Resources` is null by
132
+ contract so summing both cannot double count.
133
+ - **Multi-open ceilings** — `collection.openPack(collectionID, packTypeID, count?)`,
134
+ `collection.openCollectionChest(collectionID, chestID, count?)` and
135
+ `coopEvent.spin(chainID, groupID, count?)` open/spin `count` times in one atomic operation
136
+ (`count` defaults to 1, so existing calls are unaffected). Each roll sees the state left by
137
+ the previous one, so a repeat drop is correctly counted as a duplicate. The count is clamped
138
+ server-side; responses report what actually happened (`OpenedCount` + per-item `Packs` /
139
+ `Chests`, `RequestedSpins` / `SpinsUsed` / `Sectors`). Only spins that happen are charged —
140
+ a run stops at the spin that completes the object.
141
+ - Config for those ceilings: `LootboxDefinitions.Settings` (`LootboxGlobalSettings`) and
142
+ `LootboxDefinition.MaxOpenCount`, `CollectionDefinitions.Settings`
143
+ (`CollectionGlobalSettings`), `CollectionPackTypeDefinition.MaxOpenCount`,
144
+ `CollectionChestDefinition.MaxOpenCount`, `CoopBuildObjectsDefinition.MaxSpinsPerCall`.
145
+ - `FriendActionResponse.Target` (the other side's public profile) and `.Counters`
146
+ (`SocialCounters`) — a new friend can be shown without a second `getFriendsList()`.
147
+ - New events: `tutorial:definitionsLoaded` / `:userStateLoaded` / `:flowStarted` /
148
+ `:flowSkipped` / `:flowReset` / `:stepChanged` / `:stepsCompletedBatch` / `:rewardClaimed` /
149
+ `:systemProgress`, `localization:changed`, `season:tierRewardsBatchClaimed`,
150
+ `referral:inviteRewardsBatchClaimed`, `user:tutorialUpdated`.
151
+
152
+ ### Notes
153
+
154
+ - Everything here is additive: the new `count` parameters default to `1`, the new response
155
+ fields are optional, and no existing signature changed.
156
+ - `TutorialStepPolicy.TimeoutSeconds` is carried in the schema but **not applied** — neither
157
+ the backend (it has no "N seconds passed" moment) nor `core` (it renders no UI) can enforce
158
+ it. Whoever draws the tutorial implements it, and only for `ClientAck` steps; on a step
159
+ waiting for a game event the backend rejects `completeStep`.
160
+
161
+ ## [0.5.1] - 2026-08-05
162
+
163
+ Version-only republish of 0.5.0 — no source changes.
164
+
17
165
  ## [0.5.0] - 2026-08-04
18
166
 
19
167
  ### Added