189 lines
5.8 KiB
TypeScript
189 lines
5.8 KiB
TypeScript
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<number, Set<string>>();
|
|
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<string>();
|
|
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;
|
|
}
|