mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 22:51:30 -07:00
[econ] update challenge to only require 3
This commit is contained in:
@@ -16,8 +16,8 @@
|
||||
* unique within a rotation, so a challenge that returns in a later week would otherwise
|
||||
* start out already complete on the old week's row.
|
||||
*
|
||||
* Finishing every challenge in a rotation earns the rotation's `Gift`, which is handed out
|
||||
* from the same `updateProgress` call that completes the set. That payout is gated by a
|
||||
* Finishing enough of a rotation's challenges earns its `Gift`, which is handed out from the
|
||||
* same `updateProgress` call that reaches the threshold. That payout is gated by a
|
||||
* second table here, `challenge_gift` — one row per (account, rotation), claimed once.
|
||||
*
|
||||
* The `econ` worker owns both tables and their migrations
|
||||
@@ -88,8 +88,9 @@ export async function recordChallengeProgress(
|
||||
* so a stale row from an earlier week — same challenge id, different `challenge_map_id` —
|
||||
* doesn't show up pre-completed before the client has reported anything against it.
|
||||
*
|
||||
* Also what "the whole set is finished" is decided from: the rotation's `Gift` is due once
|
||||
* every challenge in static/weekly-challenge.json appears here.
|
||||
* Also what earning the rotation's `Gift` is decided from: it is due once ENOUGH of the
|
||||
* challenges in static/weekly-challenge.json appear here — three of the five a week
|
||||
* publishes, not all of them (see `CHALLENGES_REQUIRED_FOR_GIFT` in econ.app.ts).
|
||||
*/
|
||||
export async function getCompletedChallengeIds(
|
||||
db: D1Database,
|
||||
|
||||
+33
-11
@@ -739,15 +739,35 @@ function toChallengeFallbackDrop(): StoreGiftDrop {
|
||||
}
|
||||
|
||||
/**
|
||||
* Award the rotation's `Gift` if this player has just finished the whole set, doing nothing
|
||||
* otherwise. Called after each completing progress report, since `updateProgress` is the
|
||||
* only place a challenge is ever finished — there is no separate claim endpoint, and the
|
||||
* client never asks for this reward.
|
||||
* How many of a rotation's challenges earn its gift. A week presents five and asks for
|
||||
* three: the reward is for playing most of the week's set, not for clearing all of it, so
|
||||
* the two a player can't reach (a quest they don't own, a mode they don't like) don't sink
|
||||
* the whole week.
|
||||
*/
|
||||
const CHALLENGES_REQUIRED_FOR_GIFT = 3
|
||||
|
||||
/**
|
||||
* How many completions this rotation's gift needs. `CompletedRequired` makes the set
|
||||
* all-or-nothing when it's true — the reading its name and the partial default suggest —
|
||||
* and a rotation shorter than the threshold can only ever ask for what it publishes.
|
||||
*/
|
||||
function challengesRequiredForGift(): number {
|
||||
const published = weeklyChallenge.Challenges.length
|
||||
return weeklyChallenge.CompletedRequired
|
||||
? published
|
||||
: Math.min(CHALLENGES_REQUIRED_FOR_GIFT, published)
|
||||
}
|
||||
|
||||
/**
|
||||
* Award the rotation's `Gift` if this player has just earned it, doing nothing otherwise.
|
||||
* Called after each completing progress report, since `updateProgress` is the only place a
|
||||
* challenge is ever finished — there is no separate claim endpoint, and the client never
|
||||
* asks for this reward.
|
||||
*
|
||||
* "The whole set" is every challenge in the current rotation, read back from
|
||||
* `challenge_status`. The rotation's `CompletedRequired` flag is NOT consulted: what it
|
||||
* means is inferred, and the only reading under which the gift is due before the set is
|
||||
* finished would pay out on the first challenge, which no rotation can have intended.
|
||||
* Earning it takes {@link challengesRequiredForGift} of the rotation's challenges, counted
|
||||
* from `challenge_status`. Only challenges the rotation still publishes count: a report can
|
||||
* carry an id this week's set no longer lists (an edited rotation under a live client), and
|
||||
* three of those shouldn't buy a gift the player never worked for.
|
||||
*
|
||||
* What lands is the `Gift` block's item — or, if the player already owns it, the box named
|
||||
* by `FallbackGiftName`, which rolls something they don't have at that tier. Finishing the
|
||||
@@ -757,8 +777,8 @@ function toChallengeFallbackDrop(): StoreGiftDrop {
|
||||
* A grant that throws is swallowed: the client is reporting gameplay progress, and failing
|
||||
* that report (which it would then retry with the same completion) is worse than missing
|
||||
* the reward — the claim row is already taken, so the miss is permanent but visible in the
|
||||
* logs. An empty rotation is not "all complete"; without the guard, `every` on it is
|
||||
* vacuously true and every report would win a gift.
|
||||
* logs. An empty rotation earns nothing: its threshold clamps to zero, which every player
|
||||
* would otherwise meet without playing.
|
||||
*/
|
||||
async function awardChallengeGift(c: Context<App>, accountId: number): Promise<void> {
|
||||
try {
|
||||
@@ -768,7 +788,8 @@ async function awardChallengeGift(c: Context<App>, accountId: number): Promise<v
|
||||
accountId,
|
||||
weeklyChallenge.ChallengeMapId
|
||||
)
|
||||
if (!weeklyChallenge.Challenges.every((ch) => complete.has(ch.ChallengeId))) return
|
||||
const done = weeklyChallenge.Challenges.filter((ch) => complete.has(ch.ChallengeId)).length
|
||||
if (done < challengesRequiredForGift()) return
|
||||
// Claim first: this is what stops the next report paying out a second time.
|
||||
const claimed = await claimChallengeGift(c.env.DB, accountId, weeklyChallenge.ChallengeMapId)
|
||||
if (!claimed) return
|
||||
@@ -792,6 +813,7 @@ async function awardChallengeGift(c: Context<App>, accountId: number): Promise<v
|
||||
challengeMapId: weeklyChallenge.ChallengeMapId,
|
||||
giftId: granted.id,
|
||||
fallbackRoll: duplicate,
|
||||
challengesComplete: done,
|
||||
})
|
||||
} catch (err) {
|
||||
logger.error('failed to grant weekly challenge gift', {
|
||||
|
||||
@@ -1443,7 +1443,15 @@ describe('econ endpoints', () => {
|
||||
expect(await completeOf(await post('18', 'True'))).toBe(true)
|
||||
})
|
||||
|
||||
/** Report every challenge of the live rotation complete, for one player. */
|
||||
/**
|
||||
* How many of the rotation's challenges earn the gift — three, unless the rotation
|
||||
* publishes fewer or declares itself all-or-nothing (`CHALLENGES_REQUIRED_FOR_GIFT`).
|
||||
*/
|
||||
const REQUIRED_FOR_GIFT = weeklyChallenge.CompletedRequired
|
||||
? weeklyChallenge.Challenges.length
|
||||
: Math.min(3, weeklyChallenge.Challenges.length)
|
||||
|
||||
/** Report the live rotation's challenges complete, for one player. */
|
||||
async function finishTheRotation(sub: string) {
|
||||
const headers = { ...(await bearer(sub)), 'Content-Type': 'application/json' }
|
||||
const ids = weeklyChallenge.Challenges.map((challenge) => challenge.ChallengeId)
|
||||
@@ -1474,15 +1482,21 @@ describe('econ endpoints', () => {
|
||||
}>
|
||||
}
|
||||
|
||||
test('finishing every challenge in the rotation grants its gift, once', async () => {
|
||||
// The whole live rotation, so this follows whatever static/weekly-challenge.json holds.
|
||||
test('completing enough of the rotation grants its gift, once', async () => {
|
||||
// The live rotation, so this follows whatever static/weekly-challenge.json holds.
|
||||
const { ids, report } = await finishTheRotation('74')
|
||||
for (const id of ids.slice(0, -1)) expect((await report(id)).status).toBe(200)
|
||||
// One challenge short of the set — the gift isn't due yet.
|
||||
// The whole point of the threshold: the gift lands before the set is finished (the
|
||||
// published week is five challenges for three).
|
||||
expect(REQUIRED_FOR_GIFT).toBeLessThan(ids.length)
|
||||
for (const id of ids.slice(0, REQUIRED_FOR_GIFT - 1)) {
|
||||
expect((await report(id)).status).toBe(200)
|
||||
}
|
||||
// One short of the threshold — the gift isn't due yet, even though challenges remain
|
||||
// unfinished either way.
|
||||
expect(await giftBoxes('74')).toEqual([])
|
||||
await drainFrames()
|
||||
|
||||
expect((await report(ids[ids.length - 1] ?? 0)).status).toBe(200)
|
||||
expect((await report(ids[REQUIRED_FOR_GIFT - 1] ?? 0)).status).toBe(200)
|
||||
const won = await giftBoxes('74')
|
||||
expect(won).toHaveLength(1)
|
||||
expect(won[0]?.Message).toBe('Weekly challenge complete!')
|
||||
@@ -1525,8 +1539,8 @@ describe('econ endpoints', () => {
|
||||
weeklyChallenge.Gift.EquipmentModificationGuid
|
||||
)
|
||||
|
||||
// The client keeps reporting progress after the set is finished; a second pass over
|
||||
// the same completions must not mint a second reward.
|
||||
// Finishing the REST of the set, and re-reporting what's already done (which the client
|
||||
// keeps doing), must not mint a second reward.
|
||||
for (const id of ids) expect((await report(id)).status).toBe(200)
|
||||
expect(await giftBoxes('74')).toHaveLength(1)
|
||||
})
|
||||
@@ -1544,9 +1558,11 @@ describe('econ endpoints', () => {
|
||||
})
|
||||
|
||||
const { ids, report } = await finishTheRotation('75')
|
||||
for (const id of ids.slice(0, -1)) expect((await report(id)).status).toBe(200)
|
||||
for (const id of ids.slice(0, REQUIRED_FOR_GIFT - 1)) {
|
||||
expect((await report(id)).status).toBe(200)
|
||||
}
|
||||
await drainFrames()
|
||||
expect((await report(ids[ids.length - 1] ?? 0)).status).toBe(200)
|
||||
expect((await report(ids[REQUIRED_FOR_GIFT - 1] ?? 0)).status).toBe(200)
|
||||
|
||||
const won = await giftBoxes('75')
|
||||
expect(won).toHaveLength(1)
|
||||
|
||||
Reference in New Issue
Block a user