mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 06:31:27 -07:00
[econ] tweak progression slope
This commit is contained in:
@@ -311,8 +311,9 @@ describe('public endpoints', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('progression reads back the XP game rewards banked, levelled up', async () => {
|
test('progression reads back the XP game rewards banked, levelled up', async () => {
|
||||||
// What `econ` writes when a game reward is claimed — the two workers share the table.
|
// The two workers share this table; `econ` writes it when a game reward is claimed (5 XP
|
||||||
// 25 XP from level 1 pays the 10 to reach 2 and the 10 to reach 3, leaving 5.
|
// at a time). Granted in one lump here to exercise a multi-level climb: 25 XP from level
|
||||||
|
// 1 pays the 10 to reach 2 and the 10 to reach 3, leaving 5.
|
||||||
expect(await addXp(env.DB, 4242, 25)).toEqual({
|
expect(await addXp(env.DB, 4242, 25)).toEqual({
|
||||||
progression: { PlayerId: 4242, Level: 3, XP: 5 },
|
progression: { PlayerId: 4242, Level: 3, XP: 5 },
|
||||||
levelsGained: 2,
|
levelsGained: 2,
|
||||||
|
|||||||
+16
-13
@@ -46,7 +46,7 @@ missing/invalid). `~` = optional auth: served to anyone, personalised for a vali
|
|||||||
| GET | `/api/challenge/v2/getCurrent` | ~ | Weekly rotation + the caller's progress |
|
| GET | `/api/challenge/v2/getCurrent` | ~ | Weekly rotation + the caller's progress |
|
||||||
| POST | `/api/challenge/v2/updateProgress` | ✓ | Report challenge progress |
|
| POST | `/api/challenge/v2/updateProgress` | ✓ | Report challenge progress |
|
||||||
| GET | `/api/gamerewards/v1/pending` | | Pending game rewards (stub `[]`) |
|
| GET | `/api/gamerewards/v1/pending` | | Pending game rewards (stub `[]`) |
|
||||||
| POST | `/api/gamerewards/v1/request` | ✓ | Claim a game reward → 25 XP + gift box |
|
| POST | `/api/gamerewards/v1/request` | ✓ | Claim a game reward → 5 XP + gift box |
|
||||||
| GET | `/api/roomkeys/v1/mine` | | The player's room keys (stub `[]`) |
|
| GET | `/api/roomkeys/v1/mine` | | The player's room keys (stub `[]`) |
|
||||||
| GET | `/api/roomkeys/v1/room` | | Room keys for a room (stub `[]`) |
|
| GET | `/api/roomkeys/v1/room` | | Room keys for a room (stub `[]`) |
|
||||||
| POST | `/api/CampusCard/v1/UpdateAndGetSubscription` | | Subscription lookup (both null) |
|
| POST | `/api/CampusCard/v1/UpdateAndGetSubscription` | | Subscription lookup (both null) |
|
||||||
@@ -381,7 +381,7 @@ keyed by type.
|
|||||||
- **`giftContext` (the activity, e.g. `Soccer`) is accepted and ignored** — the cooldown is
|
- **`giftContext` (the activity, e.g. `Soccer`) is accepted and ignored** — the cooldown is
|
||||||
per type, shared across activities, so it is not part of the key.
|
per type, shared across activities, so it is not part of the key.
|
||||||
|
|
||||||
**What a claim pays: 25 XP, in a gift box.** The XP (`GAME_REWARD_XP`) is banked in
|
**What a claim pays: 5 XP, in a gift box.** The XP (`GAME_REWARD_XP`) is banked in
|
||||||
`progression` and the box is the wrapper the client shows for it — no item, every item field
|
`progression` and the box is the wrapper the client shows for it — no item, every item field
|
||||||
empty, `GiftContext` 50 (`GameRewards`). The box wears the `Message` the client posted
|
empty, `GiftContext` 50 (`GameRewards`). The box wears the `Message` the client posted
|
||||||
(`First Game of the Day`), and a `GiftPackageReceivedImmediate` frame goes out with it, the
|
(`First Game of the Day`), and a `GiftPackageReceivedImmediate` frame goes out with it, the
|
||||||
@@ -391,6 +391,9 @@ failure can't leave a box promising XP nobody was credited.
|
|||||||
- **One flat amount for every reward type**, matching the one flat cooldown they share.
|
- **One flat amount for every reward type**, matching the one flat cooldown they share.
|
||||||
Pricing `FirstActivityOfDay` differently from `PostGameActivity` is a map keyed by type,
|
Pricing `FirstActivityOfDay` differently from `PostGameActivity` is a map keyed by type,
|
||||||
the same shape the per-type cooldown would take.
|
the same shape the per-type cooldown would take.
|
||||||
|
- **Deliberately smaller than a level.** The first level costs 10 XP, so a single action
|
||||||
|
can't be a level-up — it takes two rewards to reach level 2, and the early levels are paced
|
||||||
|
by the hourly cooldown rather than cleared in one match.
|
||||||
- **The response stays `[]`.** It's what the client already accepts, and the reward is
|
- **The response stays `[]`.** It's what the client already accepts, and the reward is
|
||||||
delivered as a box, so there's nothing to put in the body. The reference answers its own
|
delivered as a box, so there's nothing to put in the body. The reference answers its own
|
||||||
(different) flow with `{ error, success, value: null }`, not a list of rewards.
|
(different) flow with `{ error, success, value: null }`, not a list of rewards.
|
||||||
@@ -404,9 +407,9 @@ inserts.
|
|||||||
|
|
||||||
**Levelling spends the XP.** `xp` is progress into the current level, not a lifetime total:
|
**Levelling spends the XP.** `xp` is progress into the current level, not a lifetime total:
|
||||||
`addXp` adds the grant, then walks the ladder in `LEVEL_REQUIRED_XP`, subtracting each
|
`addXp` adds the grant, then walks the ladder in `LEVEL_REQUIRED_XP`, subtracting each
|
||||||
level's cost while it's covered. One 25 XP reward takes a fresh player from level 1 to level
|
level's cost while it's covered — so a big enough grant can cross several levels at once.
|
||||||
**3** with 5 XP over (10 + 10 spent), because the early tiers are cheap — the ladder steps
|
The ladder steps 10 → 20 → 45 → 115 → 360 → 1080 every ten levels and stops at 50, so the
|
||||||
10 → 20 → 45 → 115 → 360 → 1080 every ten levels and stops at 50.
|
first level costs 10 XP and the last costs a hundred times that.
|
||||||
|
|
||||||
That table is copied from the `LevelProgressionMaps` the client is served in
|
That table is copied from the `LevelProgressionMaps` the client is served in
|
||||||
`apps/api/static/api-config-v2.json`, and **both sides have to agree** or the bar fills to a
|
`apps/api/static/api-config-v2.json`, and **both sides have to agree** or the bar fills to a
|
||||||
@@ -429,9 +432,10 @@ costs are easy to edit one at a time and hard to eyeball as a curve.
|
|||||||
| 40 – 49 | 4-Star Clothing (rarity 30) |
|
| 40 – 49 | 4-Star Clothing (rarity 30) |
|
||||||
| 50 | 5-Star Clothing (rarity 50) — the only one |
|
| 50 | 5-Star Clothing (rarity 50) — the only one |
|
||||||
|
|
||||||
**One reward per level crossed**, so the 25 XP that takes a fresh player from 1 to 3 hands
|
**One reward per level crossed** — a grant spanning several levels pays each of them. In
|
||||||
over three boxes: the XP reward itself, 2-Star Clothing for level 2, and a consumable for
|
practice a 5 XP game reward crosses at most one, so the second reward a fresh player claims
|
||||||
level 3. Each arrives as a gift box announced like any other (`Level 3!`).
|
hands over two boxes: the XP reward itself and the 2-Star Clothing for reaching level 2. Each
|
||||||
|
arrives as a gift box announced like any other (`Level 2!`).
|
||||||
|
|
||||||
- **"Clothing" is why the roll passes `avatarItemsOnly`** — the prize has to be something the
|
- **"Clothing" is why the roll passes `avatarItemsOnly`** — the prize has to be something the
|
||||||
player can wear and be seen in, never an equipment skin for a weapon they may not own.
|
player can wear and be seen in, never an equipment skin for a weapon they may not own.
|
||||||
@@ -487,9 +491,8 @@ Add a storefront by dropping a new `sfN.json` in `static/storefronts` — no cod
|
|||||||
- Consumables are granted and listed but never spent by gameplay, so `Count` only grows.
|
- Consumables are granted and listed but never spent by gameplay, so `Count` only grows.
|
||||||
- Several routes (room keys, wishlist, equipment, room consumables/currencies) are
|
- Several routes (room keys, wishlist, equipment, room consumables/currencies) are
|
||||||
empty-list stubs pending their own stores.
|
empty-list stubs pending their own stores.
|
||||||
- Game rewards pay a flat 25 XP; there is no daily XP cap beyond the hourly cooldown (the
|
- Game rewards pay a flat 5 XP; there is no daily XP cap beyond the hourly cooldown (the
|
||||||
reference caps activity XP per day in `daily_xp_ledgers`).
|
reference caps activity XP per day in `daily_xp_ledgers`).
|
||||||
- The weekly-challenge gift is granted but not announced: the box appears in the gifts list
|
- The level-reward table and the served config's `GiftRarity` disagree in places (see the
|
||||||
with no `GiftPackageReceived` notification, so the player sees it the next time the client
|
level section); we grant from the table and leave the config as captured, so a client that
|
||||||
reads that list rather than the moment they finish the set. Same gap as gifting to another
|
previews an upcoming reward would preview the config's answer, not ours.
|
||||||
player, and the same reason — the frame's payload shape hasn't been captured.
|
|
||||||
|
|||||||
@@ -708,8 +708,12 @@ async function grantGiftDrop(
|
|||||||
* XP paid for a claimed game reward. One flat amount for every reward type, matching the
|
* XP paid for a claimed game reward. One flat amount for every reward type, matching the
|
||||||
* one flat cooldown they share — "First Game of the Day" and "Activity completed!" are the
|
* one flat cooldown they share — "First Game of the Day" and "Activity completed!" are the
|
||||||
* same size of pat on the back until there's reason to price them apart.
|
* same size of pat on the back until there's reason to price them apart.
|
||||||
|
*
|
||||||
|
* Deliberately smaller than the 10 XP the first level costs: a single action shouldn't be a
|
||||||
|
* level-up, let alone two of them. At 5 it takes two rewards to reach level 2, and the early
|
||||||
|
* levels are paced by the hourly cooldown rather than cleared in one match.
|
||||||
*/
|
*/
|
||||||
const GAME_REWARD_XP = 25
|
const GAME_REWARD_XP = 5
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `GiftContext.GameRewards` — what the box says it came from, so the client files it under
|
* `GiftContext.GameRewards` — what the box says it came from, so the client files it under
|
||||||
@@ -769,9 +773,9 @@ function toLevelUpDrop(rarity: number): StoreGiftDrop {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Hand over the rewards a run of level-ups earned — ONE PER LEVEL crossed, since the
|
* Hand over the rewards a run of level-ups earned — ONE PER LEVEL crossed, since the
|
||||||
* published table names a reward for every level and a single grant can cross several (25 XP
|
* published table names a reward for every level and a single grant can cross several (a
|
||||||
* takes a fresh player from 1 to 3, so two rewards). Each arrives as a gift box, announced
|
* large enough grant could clear the first three levels at 10 XP each). Each arrives as a
|
||||||
* like any other unasked-for gift.
|
* gift box, announced like any other unasked-for gift.
|
||||||
*
|
*
|
||||||
* Which reward is per level, not per tier: the early levels pay CONSUMABLES and the rest pay
|
* Which reward is per level, not per tier: the early levels pay CONSUMABLES and the rest pay
|
||||||
* clothing at a rising star rating. The catalog is read once and shared across the boxes.
|
* clothing at a rising star rating. The catalog is read once and shared across the boxes.
|
||||||
|
|||||||
@@ -1765,6 +1765,13 @@ describe('econ endpoints', () => {
|
|||||||
},
|
},
|
||||||
body,
|
body,
|
||||||
})
|
})
|
||||||
|
/** Age the cooldown so the next ask is eligible again. */
|
||||||
|
const passAnHour = () =>
|
||||||
|
env.DB.prepare(
|
||||||
|
"UPDATE reward_status SET granted_at = ?1 WHERE account_id = 82 AND reward_type = 'FirstActivityOfDay'"
|
||||||
|
)
|
||||||
|
.bind(new Date(Date.now() - 61 * 60 * 1000).toISOString())
|
||||||
|
.run()
|
||||||
|
|
||||||
await drainFrames()
|
await drainFrames()
|
||||||
expect((await getProgression(env.DB, 82)).XP).toBe(0)
|
expect((await getProgression(env.DB, 82)).XP).toBe(0)
|
||||||
@@ -1772,83 +1779,93 @@ describe('econ endpoints', () => {
|
|||||||
expect(res.status).toBe(200)
|
expect(res.status).toBe(200)
|
||||||
expect(await res.json()).toEqual([])
|
expect(await res.json()).toEqual([])
|
||||||
|
|
||||||
// The XP is banked and spent on levels: 25 pays the 10 to reach level 2 and the 10 to
|
// 5 XP is deliberately less than the 10 the first level costs, so one action moves the
|
||||||
// reach 3, leaving 5 as progress into the next.
|
// bar without levelling anyone up.
|
||||||
expect(await getProgression(env.DB, 82)).toEqual({ PlayerId: 82, Level: 3, XP: 5 })
|
expect(await getProgression(env.DB, 82)).toEqual({ PlayerId: 82, Level: 1, XP: 5 })
|
||||||
|
|
||||||
// Three boxes: the XP reward itself, then one per level it crossed.
|
// One box: the XP reward itself, carrying the message the client asked to show and no
|
||||||
const boxes = await giftBoxes('82')
|
// item — a game reward is not an item.
|
||||||
expect(boxes).toHaveLength(3)
|
const first = await giftBoxes('82')
|
||||||
|
expect(first).toHaveLength(1)
|
||||||
// The reward box carries the XP and the message the client asked to show, and nothing
|
expect(first[0]).toMatchObject({
|
||||||
// else — a game reward is not an item.
|
Xp: 5,
|
||||||
expect(boxes[0]).toMatchObject({
|
|
||||||
Xp: 25,
|
|
||||||
Message: 'First Game of the Day',
|
Message: 'First Game of the Day',
|
||||||
AvatarItemDesc: '',
|
AvatarItemDesc: '',
|
||||||
EquipmentModificationGuid: '',
|
EquipmentModificationGuid: '',
|
||||||
ConsumableItemDesc: '',
|
ConsumableItemDesc: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
// The published table pays 2-Star Clothing for level 2 and a Consumable for level 3.
|
// The box, then the bar — no level-up box, since no level was crossed.
|
||||||
const [clothingBox, consumableBox] = boxes.slice(1)
|
const frames = await drainFrames()
|
||||||
expect(boxes.slice(1).map((b) => b.Message)).toEqual(['Level 2!', 'Level 3!'])
|
expect(frames.map((f) => f.notificationType)).toEqual([
|
||||||
|
NotificationType.GiftPackageReceivedImmediate,
|
||||||
|
NotificationType.PlayerProgressionLevelUpdate,
|
||||||
|
])
|
||||||
|
expect(frames[0]?.accountId).toBe(82)
|
||||||
|
expect(frames[0]?.payload).toMatchObject({
|
||||||
|
Id: first[0]?.Id,
|
||||||
|
FromPlayerId: 1,
|
||||||
|
Xp: 5,
|
||||||
|
// GiftContext.GameRewards — the box came from gameplay, not a purchase.
|
||||||
|
GiftContext: 50,
|
||||||
|
Message: 'First Game of the Day',
|
||||||
|
})
|
||||||
|
expect(frames[1]?.payload).toEqual({ PlayerId: 82, Level: 1, XP: 5 })
|
||||||
|
|
||||||
// Clothing is an AVATAR ITEM — never an equipment skin, which is what the avatar-only
|
// An on-cooldown ask pays nothing: no more boxes, no frames, no more XP.
|
||||||
// roll is for — at the star tier the table names (2-Star = rarity 10).
|
expect((await request('rewardType=FirstActivityOfDay&Message=again')).status).toBe(200)
|
||||||
|
expect(await getProgression(env.DB, 82)).toEqual({ PlayerId: 82, Level: 1, XP: 5 })
|
||||||
|
expect(await giftBoxes('82')).toHaveLength(1)
|
||||||
|
expect(await drainFrames()).toEqual([])
|
||||||
|
|
||||||
|
// A SECOND reward completes the 10 XP level 1 costs — two actions per early level, which
|
||||||
|
// is the pacing the smaller grant buys.
|
||||||
|
await passAnHour()
|
||||||
|
expect((await request('rewardType=FirstActivityOfDay&Message=Second')).status).toBe(200)
|
||||||
|
expect(await getProgression(env.DB, 82)).toEqual({ PlayerId: 82, Level: 2, XP: 0 })
|
||||||
|
|
||||||
|
// …and level 2 pays 2-Star Clothing per the published table: an AVATAR ITEM, never an
|
||||||
|
// equipment skin, which is what the avatar-only roll is for.
|
||||||
|
const afterLevel2 = await giftBoxes('82')
|
||||||
|
expect(afterLevel2).toHaveLength(3)
|
||||||
|
const clothingBox = afterLevel2[2]
|
||||||
|
expect(clothingBox?.Message).toBe('Level 2!')
|
||||||
expect(clothingBox?.AvatarItemDesc).not.toBe('')
|
expect(clothingBox?.AvatarItemDesc).not.toBe('')
|
||||||
expect(clothingBox?.EquipmentModificationGuid).toBe('')
|
expect(clothingBox?.EquipmentModificationGuid).toBe('')
|
||||||
expect(clothingBox?.ConsumableItemDesc).toBe('')
|
expect(clothingBox?.ConsumableItemDesc).toBe('')
|
||||||
expect(clothingBox?.GiftRarity).toBe(10)
|
expect(clothingBox?.GiftRarity).toBe(10)
|
||||||
|
|
||||||
// The consumable level rolls a consumable instead, at no particular rarity.
|
|
||||||
expect(consumableBox?.ConsumableItemDesc).not.toBe('')
|
|
||||||
expect(consumableBox?.AvatarItemDesc).toBe('')
|
|
||||||
expect(consumableBox?.EquipmentModificationGuid).toBe('')
|
|
||||||
|
|
||||||
// …and both are owned, not just pictured on an unopened box.
|
|
||||||
const items = await exports.default.fetch(`${ORIGIN}/api/avatar/v4/items`, {
|
const items = await exports.default.fetch(`${ORIGIN}/api/avatar/v4/items`, {
|
||||||
headers: await bearer('82'),
|
headers: await bearer('82'),
|
||||||
})
|
})
|
||||||
const owned = (await items.json()) as Array<{ AvatarItemDesc: string }>
|
const owned = (await items.json()) as Array<{ AvatarItemDesc: string }>
|
||||||
expect(owned.map((i) => i.AvatarItemDesc)).toContain(clothingBox?.AvatarItemDesc)
|
expect(owned.map((i) => i.AvatarItemDesc)).toContain(clothingBox?.AvatarItemDesc)
|
||||||
|
expect((await drainFrames()).map((f) => f.notificationType)).toEqual([
|
||||||
|
NotificationType.GiftPackageReceivedImmediate,
|
||||||
|
NotificationType.PlayerProgressionLevelUpdate,
|
||||||
|
NotificationType.GiftPackageReceivedImmediate,
|
||||||
|
])
|
||||||
|
|
||||||
|
// Two more rewards reach level 3, which the table pays as a CONSUMABLE rather than
|
||||||
|
// clothing — rolled without a rarity, since the table names none for them.
|
||||||
|
for (const message of ['Third', 'Fourth']) {
|
||||||
|
await passAnHour()
|
||||||
|
expect((await request(`rewardType=FirstActivityOfDay&Message=${message}`)).status).toBe(200)
|
||||||
|
}
|
||||||
|
expect(await getProgression(env.DB, 82)).toEqual({ PlayerId: 82, Level: 3, XP: 0 })
|
||||||
|
|
||||||
|
const afterLevel3 = await giftBoxes('82')
|
||||||
|
const consumableBox = afterLevel3[afterLevel3.length - 1]
|
||||||
|
expect(consumableBox?.Message).toBe('Level 3!')
|
||||||
|
expect(consumableBox?.ConsumableItemDesc).not.toBe('')
|
||||||
|
expect(consumableBox?.AvatarItemDesc).toBe('')
|
||||||
|
expect(consumableBox?.EquipmentModificationGuid).toBe('')
|
||||||
|
|
||||||
const consumables = await exports.default.fetch(`${ORIGIN}/api/consumables/v2/getUnlocked`, {
|
const consumables = await exports.default.fetch(`${ORIGIN}/api/consumables/v2/getUnlocked`, {
|
||||||
headers: await bearer('82'),
|
headers: await bearer('82'),
|
||||||
})
|
})
|
||||||
const held = (await consumables.json()) as Array<{ ConsumableItemDesc: string }>
|
const held = (await consumables.json()) as Array<{ ConsumableItemDesc: string }>
|
||||||
expect(held.map((cons) => cons.ConsumableItemDesc)).toContain(consumableBox?.ConsumableItemDesc)
|
expect(held.map((cons) => cons.ConsumableItemDesc)).toContain(consumableBox?.ConsumableItemDesc)
|
||||||
|
|
||||||
// One frame per box, plus the progression update between them.
|
|
||||||
const frames = await drainFrames()
|
|
||||||
expect(frames.map((f) => f.notificationType)).toEqual([
|
|
||||||
NotificationType.GiftPackageReceivedImmediate,
|
|
||||||
NotificationType.PlayerProgressionLevelUpdate,
|
|
||||||
NotificationType.GiftPackageReceivedImmediate,
|
|
||||||
NotificationType.GiftPackageReceivedImmediate,
|
|
||||||
])
|
|
||||||
expect(frames[0]?.accountId).toBe(82)
|
|
||||||
expect(frames[0]?.payload).toMatchObject({
|
|
||||||
Id: boxes[0]?.Id,
|
|
||||||
FromPlayerId: 1,
|
|
||||||
Xp: 25,
|
|
||||||
// GiftContext.GameRewards — the box came from gameplay, not a purchase.
|
|
||||||
GiftContext: 50,
|
|
||||||
Message: 'First Game of the Day',
|
|
||||||
})
|
|
||||||
// The bar moves, which is the only thing that tells the client it levelled.
|
|
||||||
expect(frames[1]?.payload).toEqual({ PlayerId: 82, Level: 3, XP: 5 })
|
|
||||||
expect(frames[3]?.payload).toMatchObject({
|
|
||||||
Id: consumableBox?.Id,
|
|
||||||
ConsumableItemDesc: consumableBox?.ConsumableItemDesc,
|
|
||||||
Message: 'Level 3!',
|
|
||||||
})
|
|
||||||
|
|
||||||
// An on-cooldown ask pays nothing: no more boxes, no frames, no more XP.
|
|
||||||
expect((await request('rewardType=FirstActivityOfDay&Message=again')).status).toBe(200)
|
|
||||||
expect(await getProgression(env.DB, 82)).toEqual({ PlayerId: 82, Level: 3, XP: 5 })
|
|
||||||
expect(await giftBoxes('82')).toHaveLength(3)
|
|
||||||
expect(await drainFrames()).toEqual([])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('POST /api/gamerewards/v1/request is 401 without a token, and ignores a typeless ask', async () => {
|
test('POST /api/gamerewards/v1/request is 401 without a token, and ignores a typeless ask', async () => {
|
||||||
|
|||||||
@@ -64,64 +64,22 @@ export const CONSUMABLE_REWARD = -1
|
|||||||
* 20 there — and this table is the one we grant from, being per-level and explicit. See the
|
* 20 there — and this table is the one we grant from, being per-level and explicit. See the
|
||||||
* econ README.
|
* econ README.
|
||||||
*/
|
*/
|
||||||
|
// prettier-ignore — the row-per-decade layout is the table; packing it hides the shape.
|
||||||
|
// prettier-ignore
|
||||||
export const LEVEL_REWARDS: readonly number[] = [
|
export const LEVEL_REWARDS: readonly number[] = [
|
||||||
// Level 0 is not a level anyone reaches; 0 is "no reward" rather than a rarity.
|
// Level 0 is not a level anyone reaches; 0 is "no reward" rather than a rarity.
|
||||||
0,
|
0,
|
||||||
// 1–10: consumables interleaved with the first clothing drops.
|
// 1–10: consumables interleaved with the first clothing drops.
|
||||||
CONSUMABLE_REWARD,
|
CONSUMABLE_REWARD, 10, CONSUMABLE_REWARD, 10, CONSUMABLE_REWARD,
|
||||||
10,
|
CONSUMABLE_REWARD, CONSUMABLE_REWARD, 10, CONSUMABLE_REWARD, 10,
|
||||||
CONSUMABLE_REWARD,
|
|
||||||
10,
|
|
||||||
CONSUMABLE_REWARD,
|
|
||||||
CONSUMABLE_REWARD,
|
|
||||||
CONSUMABLE_REWARD,
|
|
||||||
10,
|
|
||||||
CONSUMABLE_REWARD,
|
|
||||||
10,
|
|
||||||
// 11–20: 2-Star clothing all the way.
|
// 11–20: 2-Star clothing all the way.
|
||||||
10,
|
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
|
||||||
10,
|
|
||||||
10,
|
|
||||||
10,
|
|
||||||
10,
|
|
||||||
10,
|
|
||||||
10,
|
|
||||||
10,
|
|
||||||
10,
|
|
||||||
10,
|
|
||||||
// 21–30: 2-Star alternating with 3-Star.
|
// 21–30: 2-Star alternating with 3-Star.
|
||||||
10,
|
10, 20, 10, 20, 10, 20, 10, 20, 10, 20,
|
||||||
20,
|
|
||||||
10,
|
|
||||||
20,
|
|
||||||
10,
|
|
||||||
20,
|
|
||||||
10,
|
|
||||||
20,
|
|
||||||
10,
|
|
||||||
20,
|
|
||||||
// 31–40: 3-Star with a 4-Star every few levels.
|
// 31–40: 3-Star with a 4-Star every few levels.
|
||||||
30,
|
30, 20, 20, 20, 30, 20, 20, 20, 20, 30,
|
||||||
20,
|
|
||||||
20,
|
|
||||||
20,
|
|
||||||
30,
|
|
||||||
20,
|
|
||||||
20,
|
|
||||||
20,
|
|
||||||
20,
|
|
||||||
30,
|
|
||||||
// 41–50: 4-Star to the top, then the game's only 5-Star.
|
// 41–50: 4-Star to the top, then the game's only 5-Star.
|
||||||
30,
|
30, 30, 30, 30, 30, 30, 30, 30, 30, 50,
|
||||||
30,
|
|
||||||
30,
|
|
||||||
30,
|
|
||||||
30,
|
|
||||||
30,
|
|
||||||
30,
|
|
||||||
30,
|
|
||||||
30,
|
|
||||||
50,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
/** What reaching a level pays out, or null when it pays nothing. */
|
/** What reaching a level pays out, or null when it pays nothing. */
|
||||||
@@ -145,7 +103,8 @@ export const MAX_LEVEL = LEVEL_REQUIRED_XP.length - 1
|
|||||||
/**
|
/**
|
||||||
* Spend XP on levels: while the current level's cost is met, subtract it and step up. The
|
* Spend XP on levels: while the current level's cost is met, subtract it and step up. The
|
||||||
* remainder stays as progress into the next level, and a big enough grant can cross several
|
* remainder stays as progress into the next level, and a big enough grant can cross several
|
||||||
* at once (25 XP takes a fresh player from level 1 to level 3).
|
* at once (a 25 XP grant would take a fresh player from level 1 to level 3, the first two
|
||||||
|
* levels costing 10 each).
|
||||||
*
|
*
|
||||||
* A cost of 0 or less stops the loop rather than looping forever — the level-0 entry is 0,
|
* A cost of 0 or less stops the loop rather than looping forever — the level-0 entry is 0,
|
||||||
* and a future config could zero one by mistake.
|
* and a future config could zero one by mistake.
|
||||||
@@ -181,9 +140,10 @@ export interface XpGrant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The levels a grant took the player THROUGH, in order — `[2, 3]` for the 25 XP that lifts a
|
* The levels a grant took the player THROUGH, in order — `[2, 3]` for a grant that lifts a
|
||||||
* fresh player from level 1 to level 3. One entry per level reached, which is one reward
|
* fresh player from level 1 to level 3. One entry per level reached, which is one reward
|
||||||
* each; an empty list when the grant only moved the bar.
|
* each; an empty list when the grant only moved the bar, which is the common case for a
|
||||||
|
* game reward.
|
||||||
*/
|
*/
|
||||||
export function levelsReached(grant: XpGrant): number[] {
|
export function levelsReached(grant: XpGrant): number[] {
|
||||||
const from = grant.progression.Level - grant.levelsGained
|
const from = grant.progression.Level - grant.levelsGained
|
||||||
|
|||||||
Reference in New Issue
Block a user