diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..44aeb406 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules\\typescript\\lib" +} diff --git a/components/Header/Header.tsx b/components/Header/Header.tsx index 60e6d090..9bfd4722 100644 --- a/components/Header/Header.tsx +++ b/components/Header/Header.tsx @@ -16,6 +16,7 @@ import { ActionIcon, Tooltip, Anchor, + Flex, } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; import Image from "next/image"; @@ -26,6 +27,8 @@ import { SearchButton } from "../search-button/search-button"; import { OnlinePlayers } from "../online-players"; import { raceType } from "../../src/coh3/coh3-types"; import { localizedNames } from "../../src/coh3/coh3-data"; +import FactionIcon from "../faction-icon"; +import config from "../../config"; export interface HeaderProps { // children?: React.ReactNode; @@ -114,26 +117,30 @@ const useStyles = createStyles((theme) => ({ }, })); +const FactionLink = ({ faction, href }: { faction: raceType; href: string }) => ( + + + {localizedNames[faction]} + + +); + export const Header: React.FC = () => { const { classes, cx } = useStyles(); const [opened, { toggle, close }] = useDisclosure(false); - const factionLink = (faction: raceType, gamemode: string) => ( - - - {factionLink("american", gamemode)} - {factionLink("german", gamemode)} - {factionLink("dak", gamemode)} - {factionLink("british", gamemode)} + {factionLeaderboardLink("american", gamemode)} + {factionLeaderboardLink("german", gamemode)} + {factionLeaderboardLink("dak", gamemode)} + {factionLeaderboardLink("british", gamemode)} ); return ( @@ -176,18 +183,54 @@ export const Header: React.FC = () => { - - - Statistics{" "} - - - - - + + {config.isDevEnv() ? ( + + +
+ + + Statistics + + + +
+
+ + + + + + + + + + + + + + + + + + + + +
+ ) : ( + + + Statistics{" "} + + + + + + )} +module.exports = async () => withEdgio( withServiceWorker({ // Output sourcemaps so that stack traces have original source filenames and line numbers when tailing diff --git a/package.json b/package.json index 432b6e9f..5655e7c5 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,6 @@ "jest-environment-jsdom": "^29.4.3", "lint-staged": "^13.1.1", "prettier": "^2.8.4", - "typescript": "4.8.4" + "typescript": "4.9.5" } } diff --git a/pages/statistics.tsx b/pages/statistics.tsx new file mode 100644 index 00000000..9ce66475 --- /dev/null +++ b/pages/statistics.tsx @@ -0,0 +1,39 @@ +import { NextPage } from "next"; +import Head from "next/head"; + +import { Container, Title, Group, Anchor, Flex } from "@mantine/core"; +import { localizedNames } from "../src/coh3/coh3-data"; +import FactionIcon from "../components/faction-icon"; +import Link from "next/link"; + +const Explorer: NextPage = () => { + const factions = Object.keys(localizedNames) as Array; + + return ( +
+ + Explorer + + + <> + + + Statistics + + + {factions.map((link) => ( + + + + {localizedNames[link]} + + + ))} + + + +
+ ); +}; + +export default Explorer; diff --git a/pages/statistics/[faction].tsx b/pages/statistics/[faction].tsx new file mode 100644 index 00000000..147f4003 --- /dev/null +++ b/pages/statistics/[faction].tsx @@ -0,0 +1,160 @@ +import { Container, Flex, Table, Title, TextInput } from "@mantine/core"; +import Head from "next/head"; +import { GetStaticPaths, GetStaticProps } from "next"; +import { localizedNames } from "../../src/coh3/coh3-data"; +import { normalizeWeapons } from "../../src/lib/weapon-normalizer"; +import FactionIcon from "../../components/faction-icon"; +import { useState } from "react"; +import { IconSearch } from "@tabler/icons"; +import { keys } from "lodash"; +import { raceType } from "../../src/coh3/coh3-types"; +import { NullableNormalizedWeapon } from "../../src/lib/types"; + +function filterData(data: NullableNormalizedWeapon[], search: string) { + const query = search.toLowerCase().trim(); + + return data.filter((item) => + keys(data[0]).some((key) => { + const value = item[key as keyof NullableNormalizedWeapon]; + return value && typeof value === "string" && value.toLowerCase().includes(query); + }), + ); +} + +export interface FactionProps { + faction?: raceType; + weapons?: NullableNormalizedWeapon[]; +} + +const Faction = ({ faction, weapons }: FactionProps) => { + console.log(faction); + const [search, setSearch] = useState(""); + const wehrmachtWeapons = weapons?.filter((weapon) => + // compensating for a little mismatch between our raceType and the game's name for DAK + faction === "dak" ? weapon.owner === "afrika_korps" : weapon.owner === faction, + ); + + const [sortedData, setSortedData] = useState(wehrmachtWeapons); + + if (!weapons || !faction) return null; + + const name = (w: NullableNormalizedWeapon) => + w.displayName ? `${w.displayName} - ${w.referenceName}` : w.referenceName; + + const handleSearchChange = (event: React.ChangeEvent) => { + if (wehrmachtWeapons === undefined) return; + + const { value } = event.currentTarget; + setSearch(value); + setSortedData(filterData(wehrmachtWeapons, value)); + }; + + const ths = ( + + Name + Category + Type + Subtype + Accuracy Far + Accuracy Mid + Accuracy Near + + ); + + const rows = sortedData?.map((element) => ( + + {name(element)} + {element.category} + {element.type} + {element.subtype} + {element.accuracy?.far} + {element.accuracy?.mid} + {element.accuracy?.near} + + )); + + return ( +
+ + Explorer + + + <> + + + + + {faction && localizedNames[faction]} Weapons + + + } + value={search} + onChange={handleSearchChange} + /> + + + {ths} + {rows} +
{localizedNames[faction]} Weapons
+
+ +
+ ); +}; + +export const getStaticPaths: GetStaticPaths<{ faction: raceType }> = async () => { + return { + paths: [ + { + params: { faction: "american" }, + }, + { + params: { faction: "german" }, + }, + { + params: { faction: "british" }, + }, + { + params: { faction: "dak" }, + }, + ], + fallback: false, + }; +}; + +export const getStaticProps: GetStaticProps = async ({ params }) => { + const faction = params?.faction as raceType | undefined; + + const weaponsReq = await fetch( + "https://github.com/cohstats/coh3-data/raw/xml-data/scripts/xml-to-json/exported/weapon.json", + ); + + const locstringReq = await fetch( + "https://github.com/cohstats/coh3-data/raw/xml-data/scripts/xml-to-json/exported/locstring.json", + ); + + const weaponData = await weaponsReq.json(); + const locstringData = await locstringReq.json(); + + const flattenedWeapons = normalizeWeapons(weaponData, locstringData); + + return { + props: { + weapons: flattenedWeapons, + faction, + key: faction, + }, + }; +}; + +export default Faction; diff --git a/src/coh3/coh3-types.ts b/src/coh3/coh3-types.ts index 55d4481e..057804f3 100644 --- a/src/coh3/coh3-types.ts +++ b/src/coh3/coh3-types.ts @@ -79,3 +79,320 @@ export type PlayerCardDataType = { standings: InternalStandings; info: { country: string; level: number; name: string; xp: number | undefined }; }; + +export interface WeaponsData { + afrika_korps: Partial; + american: Partial; + british: Partial; + british_africa: Partial; + common: Partial; + dev: Partial; + german: Partial; +} + +export interface WeaponTypes { + ballistic_weapon: BallisticWeapon; + explosive_weapon: ExplosiveWeapon; + campaign: Record; + flame_throwers: Record; + small_arms: Record; + special: Record; +} + +export interface ExplosiveWeapon { + grenade: WeaponData; + heavy_artillery: WeaponData; + light_artillery: WeaponData; + mine: WeaponData; + mortar: WeaponData; +} + +export interface BallisticWeapon { + anti_tank_gun: WeaponData; + infantry_anti_tank_weapon: WeaponData; + tank_gun: WeaponData; +} + +export interface WeaponData { + weapon_bag: WeaponBag; + pbgid: number; +} + +export interface WeaponBag { + accuracy: Accuracy; + aim: Aim; + anim_table: AnimTable; + area_effect: AreaEffect; + behaviour: Behaviour; + damage: Damage; + deflection: Deflection; + fire: Fire; + fx_always_visible: string; + fx_munition_name: string; + fx_use_building_panel_normal: string; + moving: Moving; + priority: Priority; + projectile: Projectile; + range: Range; + reload: Reload; + scatter: Scatter; + setup: SetupOrTeardown; + suppressed: Suppressed; + suppression: Suppression; + teardown: SetupOrTeardown; + tracking: Tracking; + ui_name: UiNameOrHelpText; + ui_setfacing: ProjectileOrAnimationRoleOrUiSetfacingOrParentPbgOrUiRangeOrBuildingCharringSettings; + penetration: Penetration; + weapon_class: string; + target_type_table?: TargetTypeTableEntity[] | null; + cover_table: CoverTable; + default_attack_type: string; + animation_role_option: AnimationRoleOption; + height_bonuses: HeightBonuses; + icon_name?: string; +} + +export interface Accuracy { + near: number; + far: number; + mid: number; +} + +export interface Aim { + fire_aim_time: FireAimTime; + post_firing_aim_time_seconds: number; + aim_time_multiplier: AimTimeMultiplier; +} + +export interface FireAimTime { + max: number; + min: number; +} + +export interface AimTimeMultiplier { + far: number; + mid: number; +} + +export interface AnimTable { + state_name: string; + track_horizontal: string; + track_vertical: string; +} +export interface AreaEffect { + accuracy: Penetration; + area_info: AreaInfo; + damage: Damage; + damage_friendly: Penetration; + distance: Penetration; + has_friendly_fire: string; + aoe_penetration: AoePenetration; +} +export interface AreaInfo { + template_reference: TemplateReferenceOrLocstring; + outer_radius: number; + inner_radius: number; + is_two_dimensional: string; + dynamic_radius_type: string; +} +export interface TemplateReferenceOrLocstring { + name: string; + value: string; +} +export interface AoePenetration { + template_reference: TemplateReferenceOrLocstring; + far: number; + mid: number; + near: number; +} +export interface Behaviour { + combat_slot_offset: number; + enable_auto_target_search: string; + ground_hit_rate: number; + non_moving_setup: string; + reaction_radius: number; + attack_ground_type: string; + non_moving_setup_requires_facing: string; + can_damage_allies: string; +} +export interface Damage { + max: number; + min: number; + damage_type: string; +} +export interface Deflection { + deflection_damage_multiplier: number; + has_deflection_damage: string; +} +export interface Fire { + wind_down: number; +} +export interface Moving { + accuracy_multiplier: number; + moving_end_time: number; + moving_start_time: number; +} +export interface Priority { + current_target: number; + distance: DistanceOrAimTimeMultiplierOrDamageOrAccuracy; + rotation: number; + penetration: number; + suggested_target: number; + vs_armor_type: number; +} +export interface DistanceOrAimTimeMultiplierOrDamageOrAccuracy { + near: number; + mid: number; +} +export interface Projectile { + projectile: ProjectileOrAnimationRoleOrUiSetfacingOrParentPbgOrUiRangeOrBuildingCharringSettings; +} +export interface ProjectileOrAnimationRoleOrUiSetfacingOrParentPbgOrUiRangeOrBuildingCharringSettings { + instance_reference: string; +} +export interface Range { + max: number; + distance: Penetration; +} +export interface Reload { + duration: Duration; + require_reload_after_switch: string; +} +export interface Scatter { + angle_scatter: number; + distance_scatter_max: number; + distance_scatter_offset: number; + distance_scatter_ratio: number; + fow_angle_multiplier: number; + fow_distance_multiplier: number; +} +export interface SetupOrTeardown { + duration: number; +} +export interface Suppressed { + pinned_burst_multiplier: number; + pinned_cooldown_multiplier: number; + pinned_reload_multiplier: number; + suppressed_burst_multiplier: number; + suppressed_cooldown_multiplier: number; + suppressed_reload_multiplier: number; +} +export interface Suppression { + target_pinned_multipliers: TargetPinnedMultipliersOrTargetSuppressedMultipliersOrWeaponMultipliersOrMultipliers; + target_suppressed_multipliers: TargetPinnedMultipliersOrTargetSuppressedMultipliersOrWeaponMultipliersOrMultipliers; +} +export interface TargetPinnedMultipliersOrTargetSuppressedMultipliersOrWeaponMultipliersOrMultipliers { + accuracy_multiplier: number; + damage_multiplier: number; + penetration_multiplier: number; + suppression_multiplier: number; +} +export interface Tracking { + fire_cone_angle: number; + normal: Normal; +} +export interface Normal { + max_left: number; + max_right: number; + max_up: number; + speed_horizontal: number; + speed_vertical: number; +} +export interface UiNameOrHelpText { + locstring: TemplateReferenceOrLocstring; +} +export interface TargetTypeTableEntity { + target_unit_type_multipliers: TargetUnitTypeMultipliers; +} +export interface TargetUnitTypeMultipliers { + unit_type: string; + weapon_multipliers: TargetPinnedMultipliersOrTargetSuppressedMultipliersOrWeaponMultipliersOrMultipliers; + base_damage_modifier: number; +} +export interface CoverTable { + tp_heavy_cover: TpHeavyCoverOrTpNegativeCoverOrTpGarrisonCover; + tp_light_cover: TpLightCoverOrTpSmokeCover; + tp_negative_cover: TpHeavyCoverOrTpNegativeCoverOrTpGarrisonCover; + tp_smoke_cover: TpSmokeCoverOrTpLightCoverOrTpNegativeCoverOrTpHeavyCover; + tp_garrison_cover: TpHeavyCoverOrTpNegativeCoverOrTpGarrisonCover; +} +export interface TpHeavyCoverOrTpNegativeCoverOrTpGarrisonCover { + template_reference: TemplateReferenceOrLocstring; + accuracy_multiplier: number; + damage_multiplier: number; + suppression_multiplier: number; +} +export interface TpLightCoverOrTpSmokeCover { + template_reference: TemplateReferenceOrLocstring; + accuracy_multiplier: number; + suppression_multiplier: number; +} +export interface TpSmokeCoverOrTpLightCoverOrTpNegativeCoverOrTpHeavyCover { + template_reference: TemplateReferenceOrLocstring; + accuracy_multiplier: number; +} +export interface AnimationRoleOption { + template_reference: TemplateReferenceOrLocstring; + squad_animation_roles?: SquadAnimationRolesEntity[] | null; +} +export interface SquadAnimationRolesEntity { + flavour: Flavour; +} +export interface Flavour { + squad_contents?: SquadContentsEntity[] | null; + key: string; +} +export interface SquadContentsEntity { + squad_member: SquadMember; +} +export interface SquadMember { + animation_variants?: AnimationVariantsEntity[] | null; +} +export interface AnimationVariantsEntity { + animation_role: ProjectileOrAnimationRoleOrUiSetfacingOrParentPbgOrUiRangeOrBuildingCharringSettings; +} +export interface HeightBonuses { + height_advantage: HeightAdvantageOrHeightDisadvantage; +} +export interface HeightAdvantageOrHeightDisadvantage { + height_threshold: number; +} + +export interface ReloadOrCooldown { + duration: Duration; +} + +export interface Setup { + has_instant_setup: string; + can_interrupt_setup: string; +} + +export interface HeightAdvantage { + height_threshold: number; + multipliers: MultipliersOrMoving; + negates_cover: string; +} +export interface MultipliersOrMoving { + accuracy_multiplier: number; +} + +export interface Range { + max: number; +} + +export interface Duration { + max: number; + min: number; +} + +export interface Penetration { + near: number; + far: number; + mid: number; +} + +export interface Damage { + far: number; + mid: number; +} diff --git a/src/data-normalizers/common.test.ts b/src/data-normalizers/common.test.ts new file mode 100644 index 00000000..02365e3a --- /dev/null +++ b/src/data-normalizers/common.test.ts @@ -0,0 +1,93 @@ +import { flattenByProperty, CommonProperties } from "./common"; +import { Owner } from "./types"; + +describe("flattenByProperty", () => { + const targetProperty = "weapon_bag"; + const testData = { + afrika_korps: { + ballistic_weapon: { + anti_tank_gun: { + "75mm_at_gun_ak": { + weapon_bag: { + accuracy: { + near: 0.07, + far: 0.04, + mid: 0.045, + }, + aim: { + fire_aim_time: { + max: 0.5, + min: 0.5, + }, + }, + }, + }, + }, + }, + }, + }; + + it("should flatten the data when target property is found", () => { + const flattenedData = flattenByProperty(targetProperty, testData); + const expectedData: CommonProperties<"id">[] = [ + { + path: "/afrika_korps/ballistic_weapon/anti_tank_gun/75mm_at_gun_ak", + owner: "afrika_korps", + weapon_bag: { + accuracy: { + near: 0.07, + far: 0.04, + mid: 0.045, + }, + aim: { + fire_aim_time: { + max: 0.5, + min: 0.5, + }, + }, + }, + }, + ]; + + expect(flattenedData).toEqual(expectedData); + }); + + // it('should flatten the data when target property is found in nested object', () => { + // const flattenedData = flattenByProperty(targetProperty, testData.address); + // const expectedData = [ + // { + // path: 'address', + // owner: 'address', + // id: '2', + // street: '123 Main St', + // city: 'Anytown', + // state: 'CA', + // zip: '12345', + // }, + // ]; + + // expect(flattenedData).toEqual(expectedData); + // }); + + // it('should flatten the data when target property is found in nested array of objects', () => { + // const flattenedData = flattenByProperty(targetProperty, testData.pets[0]); + // const expectedData= [ + // { + // path: 'pets.0', + // owner: 'pets', + // id: '3', + // name: 'Fluffy', + // species: 'cat', + // }, + // ]; + + // expect(flattenedData).toEqual(expectedData); + // }); + + // it("should return an empty array when target property is not found", () => { + // const flattenedData = flattenByProperty("unknownProperty", testData); + // const expectedData = []; + + // expect(flattenedData).toEqual(expectedData); + // }); +}); diff --git a/src/data-normalizers/common.ts b/src/data-normalizers/common.ts new file mode 100644 index 00000000..32eb9d98 --- /dev/null +++ b/src/data-normalizers/common.ts @@ -0,0 +1,48 @@ +import { Owner } from "./types"; +import nodePath from "path"; + +export type CommonProperties = Record & { + path: string; + owner: Owner; +} & { [key in T]?: unknown }; + +/** + * Unwraps a nested object into a flat array of objects, where each object has a property + * with the name of the targetProperty + * @param targetProperty the property to look for in the nested object + * @param data the nested object to flatten + * @param path the path to the current node + */ +export function flattenByProperty( + targetProperty: T, + data: Record, + path = "/", +) { + const flattenedData: CommonProperties[] = []; + + return (function flattenNode(targetProperty: T, data: Record, path = "/") { + if (data && typeof data === "object") { + if (data.hasOwnProperty(targetProperty)) { + console.log(data); + const rootNode = { + path, + owner: path.split("/")[0] as Owner, + [targetProperty]: data[targetProperty], + ...data, + } satisfies CommonProperties; + + flattenedData.push(rootNode); + + return flattenedData; + } else { + Object.entries(data).forEach(([key, value]) => { + flattenByProperty( + targetProperty, + value as Record, + nodePath.join(path, key), + ); + }); + } + } + })(targetProperty, data, path); +} diff --git a/src/data-normalizers/types.ts b/src/data-normalizers/types.ts new file mode 100644 index 00000000..998bd703 --- /dev/null +++ b/src/data-normalizers/types.ts @@ -0,0 +1,67 @@ +import { WeaponBag } from "../coh3/coh3-types"; + +export type Owner = + | "afrika_korps" + | "american" + | "british" + | "british_africa" + | "common" + | "dev" + | "german"; + +export type WeaponCategory = + | "ballistic_weapon" + | "explosive_weapon" + | "campaign" + | "flame_throwers" + | "small_arms" + | "special"; + +export interface Accuracy { + near: number; + far: number; + mid: number; +} + +export interface NormalizedWeapon { + owner: Owner; + category: WeaponCategory | string | null; + type: string; + subtype: string; + pbgid: number; + accuracy: Accuracy; + displayName: string; + referenceName: string; + iconName: string; + rawWeaponBag: WeaponBag | Record | null; +} + +export type Nullable = { + [K in keyof T]: T[K] | null; +}; + +// ensure we can serialize the normalized data +export type NullableNormalizedWeapon = Nullable; + +export function isWeaponOwner(value: string): value is Owner { + return ( + value === "afrika_korps" || + value === "american" || + value === "british" || + value === "british_africa" || + value === "common" || + value === "dev" || + value === "german" + ); +} + +export function isWeaponCategory(value: string): value is WeaponCategory { + return ( + value === "ballistic_weapon" || + value === "explosive_weapon" || + value === "campaign" || + value === "flame_throwers" || + value === "small_arms" || + value === "special" + ); +} diff --git a/src/data-normalizers/weapon-normalizer.test.ts b/src/data-normalizers/weapon-normalizer.test.ts new file mode 100644 index 00000000..09603625 --- /dev/null +++ b/src/data-normalizers/weapon-normalizer.test.ts @@ -0,0 +1,146 @@ +import { WeaponBag, WeaponsData } from "../coh3/coh3-types"; +import { NullableNormalizedWeapon } from "./types"; +import { getWeaponProperties, normalizeWeapons } from "./weapon-normalizer"; + +describe("normalizeWeapons", () => { + it("should return an empty array when weaponData is empty", () => { + expect(normalizeWeapons({})).toEqual([]); + }); + + it("should correctly normalize weapon data", () => { + const weaponData = { + american: { + small_arms: { + american_bar_rifle: { + weapon_bag: { + accuracy: { + near: 0.2, + far: 0.5, + mid: 0.3, + }, + ui_name: { + locstring: { + value: "bar-rifle", + name: "american_bar_rifle", + }, + }, + icon_name: "american_bar_rifle", + } as WeaponBag, + pbgid: 111, + }, + }, + }, + } as Partial; + + const locstrings = { + "bar-rifle": "Bar Rifle", + }; + + const expectedOutput: NullableNormalizedWeapon[] = [ + { + owner: "american", + category: "small_arms", + type: null, + subtype: null, + referenceName: "american_bar_rifle", + pbgid: 111, + accuracy: { + near: 0.2, + far: 0.5, + mid: 0.3, + }, + rawWeaponBag: { + accuracy: { + near: 0.2, + far: 0.5, + mid: 0.3, + }, + ui_name: { + locstring: { + value: "bar-rifle", + name: "american_bar_rifle", + }, + }, + icon_name: "american_bar_rifle", + }, + displayName: "Bar Rifle", + iconName: "american_bar_rifle", + }, + ]; + + const output = normalizeWeapons(weaponData, locstrings); + expect(output).toEqual(expectedOutput); + }); + + it("should correctly handle null or missing values", () => { + const weaponData = { + american: { + small_arms: { + american_bar_rifle: { + weapon_bag: null, + pbgid: null, + }, + }, + }, + } as unknown as WeaponsData; + + const expectedOutput: NullableNormalizedWeapon[] = []; + + const output = normalizeWeapons(weaponData); + expect(output).toEqual(expectedOutput); + }); +}); + +describe("getWeaponProperties", () => { + it("returns null when given an empty array", () => { + const result = getWeaponProperties([]); + expect(result).toEqual(null); + }); + + it("returns the correct properties for a path without subtype", () => { + const result = getWeaponProperties([ + "afrika_korps", + "ballistic_weapon", + "anti_tank_gun", + "75_mm_leig_direct_shot_ak", + ]); + + expect(result).toEqual({ + owner: "afrika_korps", + category: "ballistic_weapon", + type: "anti_tank_gun", + subtype: null, + referenceName: "75_mm_leig_direct_shot_ak", + }); + }); + + it("returns the correct properties for a path with subtype", () => { + const result = getWeaponProperties([ + "american", + "small_arms", + "single_fire", + "rifle", + "m1_garand", + ]); + + expect(result).toEqual({ + owner: "american", + category: "small_arms", + type: "single_fire", + subtype: "rifle", + referenceName: "m1_garand", + }); + }); + + it("returns the correct properties for a short path", () => { + const result = getWeaponProperties(["afrika_korps", "special", "propaganda_war_ak"]); + + expect(result).toEqual({ + owner: "afrika_korps", + category: "special", + type: null, + subtype: null, + referenceName: "propaganda_war_ak", + }); + }); +}); diff --git a/src/data-normalizers/weapon-normalizer.ts b/src/data-normalizers/weapon-normalizer.ts new file mode 100644 index 00000000..2938a735 --- /dev/null +++ b/src/data-normalizers/weapon-normalizer.ts @@ -0,0 +1,100 @@ +import { WeaponBag, WeaponData, WeaponsData } from "../coh3/coh3-types"; +import { NullableNormalizedWeapon, isWeaponOwner, Nullable } from "./types"; + +export function getWeaponProperties(path: string[]): { + owner: NullableNormalizedWeapon["owner"]; + category: NullableNormalizedWeapon["category"]; + type: NullableNormalizedWeapon["type"]; + subtype: NullableNormalizedWeapon["subtype"]; + referenceName: NullableNormalizedWeapon["referenceName"]; +} | null { + let category: NullableNormalizedWeapon["category"] = null; + let type: NullableNormalizedWeapon["type"] = null; + let subtype: NullableNormalizedWeapon["subtype"] = null; + let referenceName: NullableNormalizedWeapon["referenceName"] = null; + + if (!isWeaponOwner(path[0])) { + console.log(`Provided weapon owner was not valid. Provided weapon owner: "${path[0]}"`); + return null; + } + + // TODO: Find a less flimsy and terrible way to do this + if (path.length === 5) { + // we have a subtype + category = path[1]; + type = path[2]; + subtype = path[3]; + referenceName = path[4]; + } else if (path.length === 4) { + // we have a subtype + category = path[1]; + type = path[2]; + referenceName = path[3]; + } else if (path.length === 3) { + category = path[1]; + referenceName = path[2]; + } else { + referenceName = path[1]; + } + + return { owner: path[0], category, type, subtype, referenceName }; +} + +/** + * + * @param weaponData Raw weapon data from the game + * @param locstrings a map of locstring keys to their values, which are usually human-readable names for the weapons + * @returns a flattened array of weapons + */ +export function normalizeWeapons( + weaponData: Nullable>, + locstrings?: Record, +): NullableNormalizedWeapon[] { + const weapons: NullableNormalizedWeapon[] = []; + // if we find this property in a node, we will stop flattening and return the data + const targetProperty = "weapon_bag"; + + function flattenNode(node: Record, path: string[] = []) { + if (node && typeof node === "object") { + if (node.hasOwnProperty(targetProperty)) { + const weaponRoot = node as Partial | undefined; + const weaponBag = weaponRoot?.[targetProperty]; + + if (!weaponRoot || !weaponBag) return null; + + const pbgid = weaponRoot?.pbgid; + const accuracy = weaponBag?.["accuracy"]; + const displayNameLocstring = weaponBag?.ui_name?.locstring.value; + const displayName = locstrings?.[displayNameLocstring ?? ""] ?? null; + const iconName = weaponBag?.icon_name ?? null; + + const basicProperties = getWeaponProperties(path); + + if (!basicProperties) return null; + + const normalizedWeapon: NullableNormalizedWeapon = { + owner: basicProperties?.owner, + category: basicProperties?.category, + type: basicProperties?.type, + subtype: basicProperties?.subtype, + referenceName: basicProperties?.referenceName, + pbgid: pbgid ?? null, + accuracy: accuracy ?? null, + rawWeaponBag: weaponBag ?? null, + displayName, + iconName, + }; + + weapons.push(normalizedWeapon); + } else { + Object.entries(node).forEach(([key, value]) => { + flattenNode(value as Record, [...path, key]); + }); + } + } + } + + flattenNode(weaponData); + + return weapons; +} diff --git a/yarn.lock b/yarn.lock index bc75678c..33983548 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3495,7 +3495,7 @@ aproba@^1.0.3, aproba@^1.1.1: "aproba@^1.0.3 || ^2.0.0": version "2.0.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + resolved "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz" integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== are-we-there-yet@^2.0.0: @@ -3980,7 +3980,7 @@ brace-expansion@^1.1.7: brace-expansion@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz" integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== dependencies: balanced-match "^1.0.0" @@ -4580,7 +4580,7 @@ commander@^2.20.0: commander@^9.4.1: version "9.5.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" + resolved "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz" integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== common-tags@^1.8.0: @@ -4962,7 +4962,7 @@ dedent@^0.7.0: deeks@2.6.1: version "2.6.1" - resolved "https://registry.yarnpkg.com/deeks/-/deeks-2.6.1.tgz#ee852ab76d5b4ca4bcfda34ba55660c40b92f505" + resolved "https://registry.npmjs.org/deeks/-/deeks-2.6.1.tgz" integrity sha512-PZrpz5xLo2JPZa3L+kqMMMdZU5pRwMysTM1xd6pLhNtgQw4Iq3wbF2QWaQTVh+HRq9Yg4rcjDIJ+scfGLxmsjQ== deep-equal@^2.0.5: @@ -5128,7 +5128,7 @@ dir-glob@^3.0.1: doc-path@3.0.7: version "3.0.7" - resolved "https://registry.yarnpkg.com/doc-path/-/doc-path-3.0.7.tgz#68e4432172c084ccb5e78403de291a0b95086848" + resolved "https://registry.npmjs.org/doc-path/-/doc-path-3.0.7.tgz" integrity sha512-w+3BuczDBuq7JtIs+ZrGaoCAlMYTeV15+gBqx8JPvBd30pT8OX/wluSRwoGUOsUFwLPB/GeDVf9ZquI69bHv4w== doctrine@^2.1.0: @@ -6068,7 +6068,7 @@ execa@^3.2.0: execa@^5.0.0: version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== dependencies: cross-spawn "^7.0.3" @@ -6083,7 +6083,7 @@ execa@^5.0.0: execa@^6.1.0: version "6.1.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-6.1.0.tgz#cea16dee211ff011246556388effa0818394fb20" + resolved "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz" integrity sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA== dependencies: cross-spawn "^7.0.3" @@ -6661,7 +6661,7 @@ get-stream@^5.0.0: get-stream@^6.0.0, get-stream@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== get-symbol-description@^1.0.0: @@ -6975,7 +6975,7 @@ html-encoding-sniffer@^3.0.0: html-escaper@^2.0.0: version "2.0.2" - resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== html-react-parser@1.4.12: @@ -7081,7 +7081,7 @@ human-signals@^1.1.1: human-signals@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== human-signals@^3.0.1: @@ -7206,7 +7206,7 @@ ini@^1.3.4, ini@~1.3.0: inline-style-parser@0.1.1: version "0.1.1" - resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" + resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz" integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== internal-slot@^1.0.3, internal-slot@^1.0.4: @@ -7429,14 +7429,14 @@ is-generator-fn@^2.0.0: is-glob@^3.1.0: version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz" integrity sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw== dependencies: is-extglob "^2.1.0" is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== dependencies: is-extglob "^2.1.1" @@ -7669,12 +7669,12 @@ isobject@^3.0.0, isobject@^3.0.1: istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz" integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: version "5.2.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz" integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== dependencies: "@babel/core" "^7.12.3" @@ -7685,7 +7685,7 @@ istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: istanbul-lib-report@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" + resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== dependencies: istanbul-lib-coverage "^3.0.0" @@ -7694,7 +7694,7 @@ istanbul-lib-report@^3.0.0: istanbul-lib-source-maps@^4.0.0: version "4.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + resolved "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz" integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== dependencies: debug "^4.1.1" @@ -7703,7 +7703,7 @@ istanbul-lib-source-maps@^4.0.0: istanbul-reports@^3.1.3: version "3.1.5" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.5.tgz#cc9a6ab25cb25659810e4785ed9d9fb742578bae" + resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz" integrity sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w== dependencies: html-escaper "^2.0.0" @@ -7721,7 +7721,7 @@ jake@^10.8.5: jest-changed-files@^29.4.3: version "29.4.3" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.4.3.tgz#7961fe32536b9b6d5c28dfa0abcfab31abcf50a7" + resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.4.3.tgz" integrity sha512-Vn5cLuWuwmi2GNNbokPOEcvrXGSGrqVnPEZV7rC6P7ck07Dyw9RFnvWglnupSh+hGys0ajGtw/bc2ZgweljQoQ== dependencies: execa "^5.0.0" @@ -7930,7 +7930,7 @@ jest-regex-util@^29.4.3: jest-resolve-dependencies@^29.4.3: version "29.4.3" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.4.3.tgz#9ad7f23839a6d88cef91416bda9393a6e9fd1da5" + resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.4.3.tgz" integrity sha512-uvKMZAQ3nmXLH7O8WAOhS5l0iWyT3WmnJBdmIHiV5tBbdaDZ1wqtNX04FONGoaFvSOSHBJxnwAVnSn1WHdGVaw== dependencies: jest-regex-util "^29.4.3" @@ -8195,7 +8195,7 @@ jsdom@^20.0.0: jsesc@^2.5.1: version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== jsesc@^3.0.2: @@ -8210,7 +8210,7 @@ jsesc@~0.5.0: json-2-csv@^3.19.0: version "3.19.0" - resolved "https://registry.yarnpkg.com/json-2-csv/-/json-2-csv-3.19.0.tgz#43e4c89b24f47d011f88b9695c04f154b669498d" + resolved "https://registry.npmjs.org/json-2-csv/-/json-2-csv-3.19.0.tgz" integrity sha512-l8keHM/wfEgYnO9rLFHoWNfq4sAI9r5OJXzMxblLb1Pn/dmMcaNK2M0kJ7DjC9ZJKHkkTj8Bbj6aBsqJX/EUFA== dependencies: deeks "2.6.1" @@ -8988,7 +8988,7 @@ neo-async@^2.5.0, neo-async@^2.6.1: next-offline@^5.0.3: version "5.0.5" - resolved "https://registry.yarnpkg.com/next-offline/-/next-offline-5.0.5.tgz#91b2792f92a65e33eee00c9ab0dd1af9b9f71caa" + resolved "https://registry.npmjs.org/next-offline/-/next-offline-5.0.5.tgz" integrity sha512-GOpq+mD7ecrgW+A8+Y31BLNca3b8EeyRMhW5C9PsCc/C2RO1AzrssgIgQIrRulFuXDvXuPE5hWI4/DTHeOUp6Q== dependencies: copy-webpack-plugin "~5.1.2" @@ -11736,10 +11736,10 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript@4.8.4: - version "4.8.4" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz" - integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== +typescript@4.9.5: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== unbox-primitive@^1.0.2: version "1.0.2"