// To run this script you need from https://github.com/Leanny/leanny.github.io // 1) WeaponInfoMain.json inside dicts // 2) WeaponInfoSub.json inside dicts // 3) WeaponInfoSpecial.json inside dicts // 4) params (weapon folder) inside dicts // xxx: internal name can be deleted when to prod // xxx: finish merging with create-weapon-json import type { SpecialWeaponId } from "~/modules/in-game-lists"; import { type SubWeaponId, subWeaponIds } from "~/modules/in-game-lists"; import weapons from "./dicts/WeaponInfoMain.json"; // xxx: for example suction missing ink consume level, ink saver lvl... we are not considering default? import subWeapons from "./dicts/WeaponInfoSub.json"; import specialWeapons from "./dicts/WeaponInfoSpecial.json"; import fs from "node:fs"; import path from "node:path"; import invariant from "tiny-invariant"; import type { MainWeaponParams, SubWeaponParams } from "~/modules/analyzer"; import type { ParamsJson } from "~/modules/analyzer/types"; import { z } from "zod"; import { LANG_JSONS_TO_CREATE, loadLangDicts } from "./utils"; const CURRENT_SEASON = 1; type MainWeapon = typeof weapons[number]; type SubWeapon = typeof subWeapons[number]; type SpecialWeapon = typeof specialWeapons[number]; type TranslationArray = Array<{ language: string; key: string; value: string }>; async function main() { const mainWeaponsResult: Record = {}; const subWeaponsResult: Record = {}; const translations: TranslationArray = []; const langDicts = await loadLangDicts(); for (const weapon of weapons) { if (mainWeaponShouldBeSkipped(weapon)) continue; const rawParams = loadWeaponParamsObject(weapon); const params = parametersToMainWeaponResult(weapon, rawParams); translationsToArray({ arr: translations, internalName: params.internalName, weaponId: weapon.Id, type: "Main", translations: langDicts, }); mainWeaponsResult[weapon.Id] = params; } for (const subWeapon of subWeapons) { if (subWeaponShouldBeSkipped(subWeapon)) continue; const rawParams = loadWeaponParamsObject(subWeapon); const params = parametersToSubWeaponResult(subWeapon, rawParams); translationsToArray({ arr: translations, internalName: params.internalName, weaponId: subWeapon.Id, type: "Sub", translations: langDicts, }); subWeaponsResult[subWeapon.Id] = params; } for (const specialWeapon of specialWeapons) { if (specialWeaponShouldBeSkipped(specialWeapon)) continue; translationsToArray({ arr: translations, internalName: specialWeapon.__RowId, weaponId: specialWeapon.Id, type: "Special", translations: langDicts, }); } const toFile: ParamsJson = { mainWeapons: mainWeaponsResult, subWeapons: subWeaponsResult, }; fs.writeFileSync( path.join(__dirname, "output", `params.json`), JSON.stringify(toFile, null, 2) + "\n" ); writeTranslationsJsons(translations); logWeaponIds(mainWeaponsResult); } function parametersToMainWeaponResult( weapon: MainWeapon, params: any ): MainWeaponParams { return { SpecialPoint: weapon.SpecialPoint, subWeaponId: resolveSubWeaponId(weapon), specialWeaponId: resolveSpecialWeaponId(weapon), overwrites: resolveOverwrites(params), internalName: weapon.__RowId, InkConsume: params["WeaponParam"]?.["InkConsume"], InkConsumeFullCharge: params["WeaponParam"]?.["InkConsumeFullCharge"], InkConsumeMinCharge: params["WeaponParam"]?.["InkConsumeMinCharge"], InkConsume_WeaponSwingParam: params["WeaponSwingParam"]?.["InkConsume"], InkConsumeMaxPerFrame_WeaponRollParam: params["WeaponRollParam"]?.["InkConsumeMaxPerFrame"], InkConsumeMinPerFrame_WeaponRollParam: params["WeaponRollParam"]?.["InkConsumeMinPerFrame"], InkConsume_WeaponVerticalSwingParam: params["WeaponVerticalSwingParam"]?.["InkConsume"], InkConsume_WeaponWideSwingParam: params["WeaponWideSwingParam"]?.["InkConsume"], InkConsumeUmbrella_WeaponShelterCanopyParam: params["spl__WeaponShelterCanopyParam"]?.["InkConsumeUmbrella"], InkConsume_WeaponShelterShotgunParam: params["spl__WeaponShelterShotgunParam"]?.["InkConsume"], InkConsume_SideStepParam: params["SideStepParam"]?.["InkConsume"], InkConsume_SwingParam: params["spl__WeaponSaberParam"]?.["SwingParam"]?.["InkConsume"], InkConsumeFullCharge_ChargeParam: params["spl__WeaponSaberParam"]?.["ChargeParam"]?.[ "InkConsumeFullCharge" ] ?? params["spl__WeaponStringerParam"]?.["ChargeParam"]?.[ "InkConsumeFullCharge" ], InkConsumeMinCharge_ChargeParam: params["spl__WeaponSaberParam"]?.["ChargeParam"]?.[ "InkConsumeMinCharge" ] ?? params["spl__WeaponStringerParam"]?.["ChargeParam"]?.[ "InkConsumeMinCharge" ], }; } // const LEGAL_SUB_INK_SAVE_LV = [0, 1, 2, 3]; function parametersToSubWeaponResult( subWeapon: SubWeapon, params: any ): SubWeaponParams { const SubInkSaveLv = params["SubWeaponSetting"]?.["SubInkSaveLv"]; // xxx: enable when all sub weapons have SubInkSaveLv's // invariant( // LEGAL_SUB_INK_SAVE_LV.includes(SubInkSaveLv), // `Unknown SubInkSaveLv ${SubInkSaveLv} for ${subWeapon.__RowId}` // ); return { internalName: subWeapon.__RowId, // xxx: not every sub has this, why? e.g. Splash Wall SubInkSaveLv, InkConsume: params["WeaponParam"]["InkConsume"], InkRecoverStop: params["WeaponParam"]["InkRecoverStop"], DistanceDamage: params["BlastParam"]?.["DistanceDamage"], DistanceDamage_BlastParamMaxCharge: params["BlastParamMaxCharge"]?.["DistanceDamage"], DistanceDamage_BlastParamMinCharge: params["BlastParamMinCharge"]?.["DistanceDamage"], DirectDamage: params["MoveParam"]?.["DirectDamage"] ?? params["MoveParam"]?.["DamageDirectHit"], DistanceDamage_BlastParamArray: params["MoveParam"]?.[ "BlastParamArray" ]?.map((b: any) => b.DistanceDamage), DistanceDamage_BlastParamChase: params["BlastParamChase"]?.["DistanceDamage"], DistanceDamage_SplashBlastParam: params["BlastParamChase"]?.["SplashBlastParam"]?.["DistanceDamage"], }; } function resolveSubWeaponId(weapon: MainWeapon) { const codeName = weapon.SubWeapon.replace("Work/Gyml/", "").replace( ".spl__WeaponInfoSub.gyml", "" ); const subWeaponObj = subWeapons.find((wpn) => codeName === wpn.__RowId); invariant(subWeaponObj, `Could not find sub weapon for '${weapon.__RowId}'`); invariant( subWeaponIds.includes(subWeaponObj.Id as any), `Invalid sub weapon id` ); return subWeaponObj.Id as SubWeaponId; } function resolveSpecialWeaponId(weapon: MainWeapon) { const codeName = weapon.SpecialWeapon.replace("Work/Gyml/", "").replace( ".spl__WeaponInfoSpecial.gyml", "" ); const specialWeaponObj = specialWeapons.find( (wpn) => codeName === wpn.__RowId ); invariant( specialWeaponObj, `Could not find special weapon for '${codeName}'` ); return specialWeaponObj.Id as SpecialWeaponId; } const overwriteSchema = z.object({ High: z.number().optional(), Mid: z.number().optional(), Low: z.number().optional(), }); function resolveOverwrites(params: any) { const result: MainWeaponParams["overwrites"] = {}; for (const [key, value] of Object.entries(params)) { const parsed = overwriteSchema.safeParse(value); // each object has a $type property which we ignore if ( key.includes("PlayerGearSkillParam") && parsed.success && Object.keys(parsed).length > 1 ) { const abilityKey = key.split("_").at(-1); invariant(abilityKey, `Could not find ability key for '${key}'`); if (!parsed.data.High && !parsed.data.Mid && !parsed.data.Low) { continue; } result[abilityKey] = { High: parsed.data.High, Mid: parsed.data.Mid, Low: parsed.data.Low, }; } } if (Object.keys(result).length === 0) return; return result; } const WEAPON_TYPES_TO_IGNORE = [ "Mission", "Coop", "Hero", "Rival", "SalmonBuddy", ]; const INTERNAL_WEAPON_NAMES_TO_IGNORE: readonly string[] = ["Free"] as const; function mainWeaponShouldBeSkipped(mainWeapon: MainWeapon) { if ( WEAPON_TYPES_TO_IGNORE.includes(mainWeapon.Type) || INTERNAL_WEAPON_NAMES_TO_IGNORE.includes(mainWeapon.__RowId) || mainWeapon.Season > CURRENT_SEASON ) { return true; } return false; } function subWeaponShouldBeSkipped(subWeapon: SubWeapon) { if (subWeapon.Id === 10000) return true; if (WEAPON_TYPES_TO_IGNORE.some((val) => subWeapon.__RowId.includes(val))) { return true; } return false; } function specialWeaponShouldBeSkipped(specialWeapon: SpecialWeapon) { if (WEAPON_TYPES_TO_IGNORE.some((val) => specialWeapon.Type.includes(val))) { return true; } if (specialWeapon.__RowId === "SpGachihoko") return true; return false; } function loadWeaponParamsObject(weapon: MainWeapon | SubWeapon) { return JSON.parse( fs.readFileSync( path.join(__dirname, "dicts", "weapon", weaponRowIdToFileName(weapon)), "utf8" ) )["GameParameters"]; } function weaponRowIdToFileName(weapon: MainWeapon | SubWeapon) { const [category, codeName] = weapon.__RowId.split("_"); invariant(category); return `Weapon${category}${codeName ?? ""}.game__GameParameterTable.json`; } function translationsToArray({ arr, internalName, weaponId, type, translations, }: { arr: TranslationArray; internalName: string; weaponId: number; type: "Main" | "Sub" | "Special"; translations: [ langCode: string, translations: Record> ][]; }) { for (const langCode of LANG_JSONS_TO_CREATE) { const translationOfLanguage = translations.find((t) => t[0] === langCode); invariant( translationOfLanguage, `Could not find translation for '${langCode}'` ); const value = translationOfLanguage[1][`CommonMsg/Weapon/WeaponName_${type}`]?.[ internalName ]; invariant(value, `Could not find translation for '${internalName}'`); arr.push({ key: `${type.toUpperCase()}_${weaponId}`, language: langCode, value, }); } } function writeTranslationsJsons(arr: TranslationArray) { for (const langCode of LANG_JSONS_TO_CREATE) { fs.writeFileSync( path.join( __dirname, "..", "public", "locales", langCode.slice(2), `weapons.json` ), JSON.stringify( Object.fromEntries( arr .filter((val) => val.language === langCode) .map(({ key, value }) => [key, value]) ), null, 2 ) + "\n" ); } } function logWeaponIds(weapons: Record) { // eslint-disable-next-line no-console console.log(JSON.stringify(Object.keys(weapons).map(Number))); } void main();