feat: model specialization combat effects
This commit is contained in:
@@ -3,14 +3,30 @@ export type GearSelection = { itemId: string; level: number; brandId: string; ta
|
||||
export type WeaponSelection = { itemId: string; level: number; tier: number; damage: number; firepower: number; toughness: number; engineering: number; talent1Id: string; talent2Id: string; rolls: [Roll, Roll] };
|
||||
export type ChipSelection = { setId: string; quality: 'Superior' | 'High-end'; primary: Roll; secondary: [Roll, Roll] };
|
||||
export type ProtocolSelection = { itemId: string; level: number; talentUpgrades: number };
|
||||
export type CombatProfile = { encounterSeconds: number; hitRate: number; headshotRate: number; firingActivity: number };
|
||||
export type EffectMode = 'off' | 'peak' | 'encounter';
|
||||
export type ConditionalEffectSelection = { mode: EffectMode; overrides?: Partial<CombatProfile> };
|
||||
export type SpecializationSelection = {
|
||||
focusId: string;
|
||||
nodeRanks: Record<string, number>;
|
||||
effectStates: Record<string, ConditionalEffectSelection>;
|
||||
combatProfile: CombatProfile;
|
||||
unassignedChips: ChipSelection[];
|
||||
};
|
||||
export type LoadoutDraft = {
|
||||
version: 1;
|
||||
version: 2;
|
||||
buildName: string;
|
||||
gear: Record<string, GearSelection>;
|
||||
weapons: [WeaponSelection, WeaponSelection];
|
||||
chips: [ChipSelection, ChipSelection, ChipSelection];
|
||||
chips: [ChipSelection, ChipSelection, ChipSelection, ChipSelection, ChipSelection, ChipSelection, ChipSelection, ChipSelection, ChipSelection];
|
||||
protocol: ProtocolSelection;
|
||||
activeTalents: string[];
|
||||
specialization: SpecializationSelection;
|
||||
};
|
||||
|
||||
type LegacyLoadoutDraft = Omit<LoadoutDraft, 'version' | 'chips' | 'specialization'> & {
|
||||
version: 1;
|
||||
chips: [ChipSelection, ChipSelection, ChipSelection];
|
||||
};
|
||||
|
||||
export const gearSlots = ['Holster', 'Gloves', 'Backpack', 'Knee Pads', 'Body Armor', 'Mask'] as const;
|
||||
@@ -18,16 +34,81 @@ export const blankRoll = (): Roll => ({ statId: '', value: 0 });
|
||||
export const blankGear = (): GearSelection => ({ itemId: '', level: 50, brandId: '', talentId: '', health: 0, armor: 0, firepower: 0, toughness: 0, engineering: 0, rolls: [blankRoll(), blankRoll()], augmentSlots: [] });
|
||||
export const blankWeapon = (): WeaponSelection => ({ itemId: '', level: 50, tier: 0, damage: 0, firepower: 0, toughness: 0, engineering: 0, talent1Id: '', talent2Id: '', rolls: [blankRoll(), blankRoll()] });
|
||||
export const blankChip = (): ChipSelection => ({ setId: '', quality: 'Superior', primary: blankRoll(), secondary: [blankRoll(), blankRoll()] });
|
||||
export const defaultCombatProfile = (): CombatProfile => ({ encounterSeconds: 60, hitRate: 75, headshotRate: 20, firingActivity: 80 });
|
||||
export const createDraft = (): LoadoutDraft => ({
|
||||
version: 1,
|
||||
version: 2,
|
||||
buildName: 'Untitled loadout',
|
||||
gear: Object.fromEntries(gearSlots.map((slot) => [slot, blankGear()])),
|
||||
weapons: [blankWeapon(), blankWeapon()],
|
||||
chips: [blankChip(), blankChip(), blankChip()],
|
||||
chips: [blankChip(), blankChip(), blankChip(), blankChip(), blankChip(), blankChip(), blankChip(), blankChip(), blankChip()],
|
||||
protocol: { itemId: '', level: 1, talentUpgrades: 0 },
|
||||
activeTalents: []
|
||||
activeTalents: [],
|
||||
specialization: {
|
||||
focusId: '',
|
||||
nodeRanks: {},
|
||||
effectStates: {},
|
||||
combatProfile: defaultCombatProfile(),
|
||||
unassignedChips: []
|
||||
}
|
||||
});
|
||||
|
||||
export function migrateLoadoutDraft(value: unknown): LoadoutDraft | null {
|
||||
if (!value || typeof value !== 'object') return null;
|
||||
const draft = value as {
|
||||
version?: number;
|
||||
buildName?: string;
|
||||
gear?: Record<string, GearSelection>;
|
||||
weapons?: WeaponSelection[];
|
||||
chips?: ChipSelection[];
|
||||
protocol?: ProtocolSelection;
|
||||
activeTalents?: string[];
|
||||
specialization?: SpecializationSelection;
|
||||
};
|
||||
if (typeof draft.buildName !== 'string' || !draft.gear || !Array.isArray(draft.weapons)
|
||||
|| draft.weapons.length !== 2 || !draft.protocol || !Array.isArray(draft.chips)) return null;
|
||||
if (draft.version === 1 && draft.chips.length === 3) {
|
||||
const migrated = createDraft();
|
||||
return {
|
||||
...migrated,
|
||||
...draft,
|
||||
version: 2,
|
||||
gear: draft.gear,
|
||||
weapons: draft.weapons as [WeaponSelection, WeaponSelection],
|
||||
chips: migrated.chips,
|
||||
protocol: draft.protocol,
|
||||
activeTalents: draft.activeTalents ?? [],
|
||||
specialization: {
|
||||
...migrated.specialization,
|
||||
unassignedChips: draft.chips.map((chip) => structuredClone(chip))
|
||||
}
|
||||
};
|
||||
}
|
||||
if (draft.version !== 2) return null;
|
||||
const fresh = createDraft();
|
||||
const chips = [...draft.chips.slice(0, 9)];
|
||||
while (chips.length < 9) chips.push(blankChip());
|
||||
return {
|
||||
...fresh,
|
||||
...draft,
|
||||
version: 2,
|
||||
gear: draft.gear,
|
||||
weapons: draft.weapons as [WeaponSelection, WeaponSelection],
|
||||
chips: chips as LoadoutDraft['chips'],
|
||||
protocol: draft.protocol,
|
||||
activeTalents: draft.activeTalents ?? [],
|
||||
specialization: {
|
||||
...fresh.specialization,
|
||||
...(draft.specialization ?? {}),
|
||||
nodeRanks: { ...(draft.specialization?.nodeRanks ?? {}) },
|
||||
effectStates: { ...(draft.specialization?.effectStates ?? {}) },
|
||||
combatProfile: { ...fresh.specialization.combatProfile, ...(draft.specialization?.combatProfile ?? {}) },
|
||||
unassignedChips: Array.isArray(draft.specialization?.unassignedChips)
|
||||
? draft.specialization.unassignedChips
|
||||
: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const clamp = (value: unknown, min: unknown, max: unknown) => {
|
||||
const low = Number(min), high = Number(max), numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) return Number.isFinite(low) ? low : 0;
|
||||
|
||||
Reference in New Issue
Block a user