Files
shd.build/src/lib/weapons/mod-slots.ts
T
jb 57068b61cd
Validate and deploy production / validate (push) Canceled after 0s
Validate and deploy production / deploy (push) Canceled after 0s
Validate and deploy staging / validate (push) Successful in 28s
Validate and deploy staging / deploy (push) Successful in 1m3s
feat: expand ISAC content modeling
2026-08-03 01:23:20 -04:00

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;
}