export const WEAPON_MOD_SLOT_DEFINITIONS = [ { id: '28ffc3b1-5d98-4273-a9af-67dbff3e5c76', name: 'Optics' }, { id: 'c4598f51-2cb0-4a20-a3a7-6d8983a26598', name: 'Muzzle' }, { id: '85ca3493-1871-4d22-950a-ed718fd547e0', name: 'Underbarrel' }, { id: '4d809a35-8285-4bff-be98-a0effe1d257e', name: 'Magazine' }, { id: '0e157cfe-975c-441d-8f49-72983161e7b1', name: 'Barrel' }, { id: '551fd7c8-3e5b-4c89-8437-9b901e89e8aa', name: 'Upper Receiver' }, { id: '72573367-6e91-4312-9cc3-710ede4f078e', name: 'Lower Receiver' }, { id: '8dda5edb-6079-4a23-a556-cd9be4521c70', name: 'Trigger' }, ] as const; export type WeaponModSlotType = (typeof WEAPON_MOD_SLOT_DEFINITIONS)[number]['name']; export const WEAPON_MOD_SLOT_TYPES = WEAPON_MOD_SLOT_DEFINITIONS.map( (definition) => definition.name, ) as WeaponModSlotType[]; export function weaponModSlotDefinitionById(id: string) { return WEAPON_MOD_SLOT_DEFINITIONS.find((definition) => definition.id === id); } export function weaponModSlotDefinitionByName(name: string) { return WEAPON_MOD_SLOT_DEFINITIONS.find( (definition) => definition.name === name, ); } export type WeaponModSlotConfig = { tier: number; slots: string[]; }; export type PublicWeaponModSlot = { slot: string; unlockedAtTierOne: boolean; iconUrl?: string; }; export function normalizeWeaponModSlotPayload( payload: unknown, ): WeaponModSlotConfig[] { if (!Array.isArray(payload)) return []; const byTier = new Map>(); for (const row of payload) { if (!row || typeof row !== 'object') continue; const tier = Number('tier' in row ? row.tier : Number.NaN); if (!Number.isInteger(tier) || tier < 0 || tier > 2) continue; const slots = 'slots' in row && Array.isArray(row.slots) ? row.slots.map((slot: unknown) => String(slot)) : []; const collected = byTier.get(tier) ?? new Set(); for (const slot of slots) collected.add(slot); byTier.set(tier, collected); } return [...byTier.entries()] .sort(([left], [right]) => left - right) .map(([tier, slots]) => ({ tier, slots: [...slots] })); } export function requiredWeaponModSlotCount(tier: number): number { return tier === 0 ? 4 : 5; } export function sharedWeaponModSlots( configurations: WeaponModSlotConfig[], ): string[] { return [ ...new Set( configurations .filter((row) => Number(row.tier) === 1 || Number(row.tier) === 2) .flatMap((row) => row.slots), ), ]; } export function publicWeaponModSlots( nativeTier: number, configurations: WeaponModSlotConfig[], ): PublicWeaponModSlot[] { const nativeSlots = configurations.find((row) => Number(row.tier) === nativeTier)?.slots ?? []; const displayedSlots = nativeTier === 0 ? sharedWeaponModSlots(configurations).length ? sharedWeaponModSlots(configurations) : nativeSlots : nativeSlots; const ordered = [...new Set(displayedSlots)].sort( (left, right) => WEAPON_MOD_SLOT_TYPES.indexOf( left as (typeof WEAPON_MOD_SLOT_TYPES)[number], ) - WEAPON_MOD_SLOT_TYPES.indexOf( right as (typeof WEAPON_MOD_SLOT_TYPES)[number], ), ); return ordered.map((slot) => ({ slot, unlockedAtTierOne: nativeTier === 0 && !nativeSlots.includes(slot), })); } export function excludedTierZeroWeaponModSlot( sharedSlots: string[], configurations: WeaponModSlotConfig[], ): string { const tierZeroSlots = configurations.find((row) => Number(row.tier) === 0)?.slots ?? []; const excluded = sharedSlots.filter((slot) => !tierZeroSlots.includes(slot)); return excluded.length === 1 && tierZeroSlots.every((slot) => sharedSlots.includes(slot)) ? excluded[0] : ''; } export function expandSharedWeaponModSlots( sharedSlots: string[], tierZeroExcludedSlot: string, ): WeaponModSlotConfig[] { const uniqueSharedSlots = [...new Set(sharedSlots)]; return [ { tier: 0, slots: tierZeroExcludedSlot ? uniqueSharedSlots.filter((slot) => slot !== tierZeroExcludedSlot) : [], }, { tier: 1, slots: [...uniqueSharedSlots] }, { tier: 2, slots: [...uniqueSharedSlots] }, ]; } export function weaponModSlotsComplete( nativeTier: number, configurations: WeaponModSlotConfig[], ): boolean { for (let tier = nativeTier; tier <= 2; tier += 1) { const configured = configurations.find((row) => Number(row.tier) === tier); if (configured?.slots.length !== requiredWeaponModSlotCount(tier)) return false; } if (nativeTier <= 1) { const tierOne = configurations.find((row) => Number(row.tier) === 1)?.slots ?? []; const tierTwo = configurations.find((row) => Number(row.tier) === 2)?.slots ?? []; if ( tierOne.some((slot) => !tierTwo.includes(slot)) || tierTwo.some((slot) => !tierOne.includes(slot)) ) return false; } if (nativeTier === 0) { const tierZero = configurations.find((row) => Number(row.tier) === 0)?.slots ?? []; const tierOne = configurations.find((row) => Number(row.tier) === 1)?.slots ?? []; if (tierZero.some((slot) => !tierOne.includes(slot))) return false; } return true; } export function validateWeaponModSlots( nativeTier: number, configurations: WeaponModSlotConfig[], ): string | null { for (let tier = nativeTier; tier <= 2; tier += 1) { const configured = configurations.find((row) => Number(row.tier) === tier); const slots = configured?.slots ?? []; const required = requiredWeaponModSlotCount(tier); if ( slots.some( (slot) => !WEAPON_MOD_SLOT_TYPES.includes( slot as (typeof WEAPON_MOD_SLOT_TYPES)[number], ), ) ) { return `Tier ${tier} includes an unavailable Weapon Mod slot.`; } if (slots.length > required) { return `Tier ${tier} supports at most ${required} unique Weapon Mod slots.`; } } return null; }