mirror of
https://github.com/djdevin/recflare.git
synced 2026-09-08 22:51:30 -07:00
[econ] friendly name for weekly
This commit is contained in:
@@ -325,6 +325,12 @@ export interface WeeklyChallengeRotation {
|
|||||||
Challenges: RotationChallenge[]
|
Challenges: RotationChallenge[]
|
||||||
Gift: ChallengeGiftBlock
|
Gift: ChallengeGiftBlock
|
||||||
FallbackGiftName: string
|
FallbackGiftName: string
|
||||||
|
/**
|
||||||
|
* What the week is themed on — the FriendlyName of the item its `Gift` hands over, set
|
||||||
|
* by {@link withWeeklyGift} once the catalog has named the roll. The static file's value
|
||||||
|
* is only a placeholder: a generated week's reward isn't known until it is rolled. A
|
||||||
|
* PINNED rotation keeps whatever string it ships.
|
||||||
|
*/
|
||||||
ChallengeThemeString: string
|
ChallengeThemeString: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,6 +340,8 @@ export interface EquipmentGift {
|
|||||||
EquipmentPrefabName: string
|
EquipmentPrefabName: string
|
||||||
EquipmentModificationGuid: string
|
EquipmentModificationGuid: string
|
||||||
Rarity: number
|
Rarity: number
|
||||||
|
/** The catalog's display name for the item — what the week is themed on. */
|
||||||
|
FriendlyName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether the shipped file pins the week, in which case nothing here is generated. */
|
/** Whether the shipped file pins the week, in which case nothing here is generated. */
|
||||||
@@ -564,11 +572,19 @@ function pickChallenges(random: () => number): RotationChallenge[] {
|
|||||||
* Null when the pool is empty (the catalog didn't load), and the caller keeps the static
|
* Null when the pool is empty (the catalog didn't load), and the caller keeps the static
|
||||||
* file's block so the reward preview is still something rather than nothing.
|
* file's block so the reward preview is still something rather than nothing.
|
||||||
*/
|
*/
|
||||||
function pickWeeklyGift(mapId: number, pool: EquipmentGift[]): ChallengeGiftBlock | null {
|
function pickWeeklyGift(
|
||||||
|
mapId: number,
|
||||||
|
pool: EquipmentGift[]
|
||||||
|
): { gift: ChallengeGiftBlock; friendlyName: string } | null {
|
||||||
if (pool.length === 0) return null
|
if (pool.length === 0) return null
|
||||||
const random = mulberry32(seedFor(mapId, 0x9e3779b9))
|
const random = mulberry32(seedFor(mapId, 0x9e3779b9))
|
||||||
const gift = pool[Math.floor(random() * pool.length)] as EquipmentGift
|
const gift = pool[Math.floor(random() * pool.length)] as EquipmentGift
|
||||||
|
// The name comes back alongside rather than on the block: the block is the wire shape,
|
||||||
|
// whose display strings are optional and left unset here so `toChallengeGiftDrop` keeps
|
||||||
|
// resolving them from the catalog entry that sells the item.
|
||||||
return {
|
return {
|
||||||
|
friendlyName: gift.FriendlyName,
|
||||||
|
gift: {
|
||||||
GiftDropId: gift.GiftDropId,
|
GiftDropId: gift.GiftDropId,
|
||||||
AvatarItemDesc: '',
|
AvatarItemDesc: '',
|
||||||
AvatarItemType: 0,
|
AvatarItemType: 0,
|
||||||
@@ -580,6 +596,7 @@ function pickWeeklyGift(mapId: number, pool: EquipmentGift[]): ChallengeGiftBloc
|
|||||||
Level: 0,
|
Level: 0,
|
||||||
GiftContext: 0,
|
GiftContext: 0,
|
||||||
GiftRarity: gift.Rarity,
|
GiftRarity: gift.Rarity,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,6 +648,11 @@ export function withWeeklyGift(
|
|||||||
pool: EquipmentGift[]
|
pool: EquipmentGift[]
|
||||||
): WeeklyChallengeRotation {
|
): WeeklyChallengeRotation {
|
||||||
if (pinnedRotation() !== null) return rotation
|
if (pinnedRotation() !== null) return rotation
|
||||||
const gift = pickWeeklyGift(rotation.ChallengeMapId, pool)
|
const picked = pickWeeklyGift(rotation.ChallengeMapId, pool)
|
||||||
return gift === null ? rotation : { ...rotation, Gift: gift }
|
if (picked === null) return rotation
|
||||||
|
// The week is themed on its reward: `ChallengeThemeString` is the item's catalog name,
|
||||||
|
// which is the same string `toChallengeGiftDrop` resolves for the grant, so the heading
|
||||||
|
// and the thing handed over read as one. The static file's value is a placeholder — it
|
||||||
|
// can't name an item that is rolled per week.
|
||||||
|
return { ...rotation, Gift: picked.gift, ChallengeThemeString: picked.friendlyName }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -701,6 +701,9 @@ function toEquipmentGiftPool(catalog: StoreItem[]): EquipmentGift[] {
|
|||||||
EquipmentPrefabName: item.GiftDrop.EquipmentPrefabName,
|
EquipmentPrefabName: item.GiftDrop.EquipmentPrefabName,
|
||||||
EquipmentModificationGuid: item.GiftDrop.EquipmentModificationGuid,
|
EquipmentModificationGuid: item.GiftDrop.EquipmentModificationGuid,
|
||||||
Rarity: item.GiftDrop.Rarity,
|
Rarity: item.GiftDrop.Rarity,
|
||||||
|
// Carried so the rotation can theme the week on the item it rolled; the grant path
|
||||||
|
// resolves the same name from this entry when it hands the item over.
|
||||||
|
FriendlyName: item.GiftDrop.FriendlyName,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { CHALLENGE_GIFT_SCHEMA_DDL, CHALLENGE_STATUS_SCHEMA_DDL } from '../../ch
|
|||||||
// The live weekly rotation, generated the same way the worker generates it, so the challenge
|
// The live weekly rotation, generated the same way the worker generates it, so the challenge
|
||||||
// tests exercise whatever this week actually holds instead of ids from a rotation that has
|
// tests exercise whatever this week actually holds instead of ids from a rotation that has
|
||||||
// since rolled over.
|
// since rolled over.
|
||||||
import { buildRotation, rotationIndex } from '../../challenge-rotation'
|
import { buildRotation, rotationIndex, withWeeklyGift } from '../../challenge-rotation'
|
||||||
import { CONSUMABLE_SCHEMA_DDL, grantConsumable } from '../../consumables-db'
|
import { CONSUMABLE_SCHEMA_DDL, grantConsumable } from '../../consumables-db'
|
||||||
import { EQUIPMENT_SCHEMA_DDL, grantEquipment } from '../../equipment-db'
|
import { EQUIPMENT_SCHEMA_DDL, grantEquipment } from '../../equipment-db'
|
||||||
import { INVENTORY_SCHEMA_DDL } from '../../inventory-db'
|
import { INVENTORY_SCHEMA_DDL } from '../../inventory-db'
|
||||||
@@ -1912,6 +1912,45 @@ describe('econ endpoints', () => {
|
|||||||
expect(buildRotation(nextWeek).StartAt).toBe(buildRotation(at).EndAt)
|
expect(buildRotation(nextWeek).StartAt).toBe(buildRotation(at).EndAt)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('the week is themed on the name of the item it rolls', async () => {
|
||||||
|
// `ChallengeThemeString` is the reward's catalog FriendlyName. The static file ships it
|
||||||
|
// empty on purpose — a generated week's gift isn't known until it is rolled — so the
|
||||||
|
// theming happens where the pick does.
|
||||||
|
const pool = [
|
||||||
|
{
|
||||||
|
GiftDropId: 11,
|
||||||
|
EquipmentPrefabName: '[ShareCamera]',
|
||||||
|
EquipmentModificationGuid: 'guid-a',
|
||||||
|
Rarity: 30,
|
||||||
|
FriendlyName: 'Camera Skin (Comic)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
GiftDropId: 12,
|
||||||
|
EquipmentPrefabName: '[Boombox]',
|
||||||
|
EquipmentModificationGuid: 'guid-b',
|
||||||
|
Rarity: 20,
|
||||||
|
FriendlyName: 'Boombox (Neon)',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const at = new Date('2026-08-25T12:00:00Z')
|
||||||
|
const themed = withWeeklyGift(buildRotation(at), pool)
|
||||||
|
const rolled = pool.find((p) => p.GiftDropId === themed.Gift.GiftDropId)
|
||||||
|
expect(rolled).toBeDefined()
|
||||||
|
expect(themed.ChallengeThemeString).toBe(rolled!.FriendlyName)
|
||||||
|
|
||||||
|
// An empty pool (the catalog didn't load) leaves the rotation as it was rather than
|
||||||
|
// theming the week on nothing.
|
||||||
|
expect(withWeeklyGift(buildRotation(at), []).ChallengeThemeString).toBe(
|
||||||
|
buildRotation(at).ChallengeThemeString
|
||||||
|
)
|
||||||
|
|
||||||
|
// And over the live catalog the route serves a real name, not the placeholder.
|
||||||
|
const served = (await (
|
||||||
|
await exports.default.fetch(`${ORIGIN}/api/challenge/v2/getCurrent`)
|
||||||
|
).json()) as { ChallengeThemeString: string }
|
||||||
|
expect(served.ChallengeThemeString).not.toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
test('every generated week is five valid, distinct challenges', async () => {
|
test('every generated week is five valid, distinct challenges', async () => {
|
||||||
// Walk two years of rotations: the pool, the constraints and the tree builders all have
|
// Walk two years of rotations: the pool, the constraints and the tree builders all have
|
||||||
// to hold for every week, not just this one.
|
// to hold for every week, not just this one.
|
||||||
|
|||||||
Reference in New Issue
Block a user