Some checks failed
Test and deploy staging / staging (push) Failing after 5m29s
157 lines
5.2 KiB
TypeScript
157 lines
5.2 KiB
TypeScript
// Camo mastery for checklist games. Users tick per-weapon milestones (max level,
|
|
// Gold, …); class tiers (Diamond) and overall tiers (Dark Matter) are derived.
|
|
// Post-launch weapons never gate a tier. They earn an achieved tier once their
|
|
// own base milestone is done, as DLC weapons do in Black Ops III and WWII.
|
|
|
|
export type Milestone = {
|
|
key: string;
|
|
label: string;
|
|
description?: string;
|
|
// limit to these weapon classes; every class when omitted
|
|
classes?: string[];
|
|
exceptWeapons?: string[];
|
|
postLaunchOnly?: boolean;
|
|
};
|
|
|
|
export type Tier = {
|
|
key: string;
|
|
label: string;
|
|
scope: 'class' | 'all';
|
|
// a milestone key, or for `all` tiers also a class tier key
|
|
requires: string;
|
|
description?: string;
|
|
classes?: string[];
|
|
// class tiers only: the classes whose weapons a class depends on
|
|
classRequirements?: Record<string, string[]>;
|
|
};
|
|
|
|
export type MasteryRules = {
|
|
milestones: Milestone[];
|
|
tiers: Tier[];
|
|
classes: string[];
|
|
steamApps: { id: number; label: string }[];
|
|
rankCaps?: {
|
|
player: { level: number; prestige: number };
|
|
weaponPrestige: number;
|
|
weaponLevelsByClass: Record<string, number>;
|
|
weaponLevelOverrides?: Record<string, number>;
|
|
sourceUrl?: string;
|
|
};
|
|
};
|
|
|
|
export type Weapon = { id: number; name: string; category: string; postLaunch: boolean };
|
|
export type Count = { value: number; total: number; achieved: boolean };
|
|
|
|
export function appliesTo(milestone: Milestone, weapon: Weapon): boolean {
|
|
if (milestone.postLaunchOnly && !weapon.postLaunch) return false;
|
|
if (milestone.classes && !milestone.classes.includes(weapon.category)) return false;
|
|
return !milestone.exceptWeapons?.includes(weapon.name);
|
|
}
|
|
|
|
function count(value: number, total: number): Count {
|
|
return { value, total, achieved: total > 0 && value === total };
|
|
}
|
|
|
|
export function computeMastery(
|
|
rules: MasteryRules,
|
|
weapons: readonly Weapon[],
|
|
isDone: (weaponId: number, milestone: string) => boolean
|
|
) {
|
|
const milestones = new Map(rules.milestones.map((milestone) => [milestone.key, milestone]));
|
|
const tiers = new Map(rules.tiers.map((tier) => [tier.key, tier]));
|
|
const counted = (key: string, classes: readonly string[]) => {
|
|
const milestone = milestones.get(key);
|
|
return weapons.filter(
|
|
(weapon) =>
|
|
milestone &&
|
|
!weapon.postLaunch &&
|
|
classes.includes(weapon.category) &&
|
|
appliesTo(milestone, weapon)
|
|
);
|
|
};
|
|
const done = (list: readonly Weapon[], key: string) =>
|
|
list.filter((weapon) => isDone(weapon.id, key)).length;
|
|
|
|
const classTiers = new Map<string, Map<string, Count>>();
|
|
for (const tier of rules.tiers) {
|
|
if (tier.scope !== 'class') continue;
|
|
const perClass = new Map<string, Count>();
|
|
for (const name of tier.classes ?? rules.classes) {
|
|
const members = counted(tier.requires, tier.classRequirements?.[name] ?? [name]);
|
|
if (members.length) perClass.set(name, count(done(members, tier.requires), members.length));
|
|
}
|
|
classTiers.set(tier.key, perClass);
|
|
}
|
|
|
|
const allTiers = new Map<string, Count>();
|
|
for (const tier of rules.tiers) {
|
|
if (tier.scope !== 'all') continue;
|
|
const perClass = classTiers.get(tier.requires);
|
|
if (perClass) {
|
|
const classes = [...perClass].filter(
|
|
([name]) => !tier.classes || tier.classes.includes(name)
|
|
);
|
|
allTiers.set(
|
|
tier.key,
|
|
count(classes.filter(([, value]) => value.achieved).length, classes.length)
|
|
);
|
|
} else {
|
|
const members = counted(tier.requires, tier.classes ?? rules.classes);
|
|
allTiers.set(tier.key, count(done(members, tier.requires), members.length));
|
|
}
|
|
}
|
|
|
|
// the per-weapon milestone at the bottom of a tier chain, e.g. darkMatter → diamond → gold
|
|
const baseMilestone = (key: string): string => {
|
|
const tier = tiers.get(key);
|
|
return tier ? baseMilestone(tier.requires) : key;
|
|
};
|
|
|
|
function weaponTiers(weapon: Weapon): Tier[] {
|
|
return rules.tiers.filter((tier) => {
|
|
const base = baseMilestone(tier.key);
|
|
const milestone = milestones.get(base);
|
|
if (!milestone || !appliesTo(milestone, weapon)) return false;
|
|
const achieved =
|
|
tier.scope === 'class'
|
|
? classTiers.get(tier.key)?.get(weapon.category)?.achieved
|
|
: allTiers.get(tier.key)?.achieved &&
|
|
(!tier.classes || tier.classes.includes(weapon.category));
|
|
return !!achieved && (!weapon.postLaunch || isDone(weapon.id, base));
|
|
});
|
|
}
|
|
|
|
return { classTiers, allTiers, weaponTiers };
|
|
}
|
|
|
|
export function summarizeGame(
|
|
rules: MasteryRules,
|
|
weapons: readonly Weapon[],
|
|
achievementIds: readonly number[],
|
|
isDone: (itemId: number, milestone: string) => boolean
|
|
) {
|
|
const mastery = computeMastery(rules, weapons, isDone);
|
|
return {
|
|
achievements: count(
|
|
achievementIds.filter((id) => isDone(id, 'done')).length,
|
|
achievementIds.length
|
|
),
|
|
milestones: rules.milestones.map((milestone) => {
|
|
const list = weapons.filter((weapon) => appliesTo(milestone, weapon));
|
|
return {
|
|
...milestone,
|
|
...count(list.filter((weapon) => isDone(weapon.id, milestone.key)).length, list.length)
|
|
};
|
|
}),
|
|
tiers: rules.tiers.map((tier) => {
|
|
if (tier.scope === 'all') return { ...tier, ...mastery.allTiers.get(tier.key)! };
|
|
const perClass = [...(mastery.classTiers.get(tier.key)?.values() ?? [])];
|
|
return {
|
|
...tier,
|
|
...count(perClass.filter((value) => value.achieved).length, perClass.length)
|
|
};
|
|
}),
|
|
mastery
|
|
};
|
|
}
|