mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-24 12:06:54 -05:00
Weapon params page (#3170)
This commit is contained in:
@@ -1,9 +1,4 @@
|
||||
// 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) SplPlayer.game__GameParameterTable.json inside dicts
|
||||
// 5) params (weapon folder) inside dicts
|
||||
// To run this script drop the https://github.com/Leanny/splat3 repo into scripts/dicts/splat3
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
@@ -28,19 +23,25 @@ import {
|
||||
} from "~/modules/in-game-lists/weapon-ids";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { logger } from "~/utils/logger";
|
||||
import playersParams from "./dicts/SplPlayer.game__GameParameterTable.json";
|
||||
import weapons from "./dicts/WeaponInfoMain.json";
|
||||
import specialWeapons from "./dicts/WeaponInfoSpecial.json";
|
||||
import subWeapons from "./dicts/WeaponInfoSub.json";
|
||||
import {
|
||||
LANG_JSONS_TO_CREATE,
|
||||
loadLangDicts,
|
||||
loadSplPlayerParams,
|
||||
loadWeaponInfoMain,
|
||||
loadWeaponInfoSpecial,
|
||||
loadWeaponInfoSub,
|
||||
translationJsonFolderName,
|
||||
weaponParamsDir,
|
||||
} from "./utils";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const playersParams = loadSplPlayerParams();
|
||||
const weapons = loadWeaponInfoMain();
|
||||
const subWeapons = loadWeaponInfoSub();
|
||||
const specialWeapons = loadWeaponInfoSpecial();
|
||||
|
||||
const CURRENT_SEASON = 9;
|
||||
|
||||
type MainWeapon = (typeof weapons)[number];
|
||||
@@ -1093,7 +1094,7 @@ function loadWeaponParamsObject(
|
||||
) {
|
||||
return JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(__dirname, "dicts", "weapon", weaponRowIdToFileName(weapon)),
|
||||
path.join(weaponParamsDir(), weaponRowIdToFileName(weapon)),
|
||||
"utf8",
|
||||
),
|
||||
).GameParameters;
|
||||
|
||||
@@ -3,11 +3,11 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { brandIds } from "~/modules/in-game-lists/brand-ids";
|
||||
import invariant from "~/utils/invariant";
|
||||
import clothes from "./dicts/GearInfoClothes.json";
|
||||
import head from "./dicts/GearInfoHead.json";
|
||||
import shoes from "./dicts/GearInfoShoes.json";
|
||||
import {
|
||||
LANG_JSONS_TO_CREATE,
|
||||
loadGearInfoClothes,
|
||||
loadGearInfoHead,
|
||||
loadGearInfoShoes,
|
||||
loadLangDicts,
|
||||
translationJsonFolderName,
|
||||
} from "./utils";
|
||||
@@ -15,6 +15,10 @@ import {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const clothes = loadGearInfoClothes();
|
||||
const head = loadGearInfoHead();
|
||||
const shoes = loadGearInfoShoes();
|
||||
|
||||
const CURRENT_SEASON = 9;
|
||||
const OUTPUT_DIR_PATH = path.join(__dirname, "output");
|
||||
|
||||
|
||||
@@ -12,19 +12,35 @@ import {
|
||||
specialWeaponIds,
|
||||
subWeaponIds,
|
||||
} from "~/modules/in-game-lists/weapon-ids";
|
||||
// 1) WeaponInfoMain.json inside dicts
|
||||
// 2) WeaponInfoSub.json inside dicts
|
||||
// 3) WeaponInfoSpecial.json inside dicts
|
||||
// 4) misc/spl__DamageRateInfoConfig.pp__CombinationDataTableData.json
|
||||
import params from "./dicts/spl__DamageRateInfoConfig.pp__CombinationDataTableData.json";
|
||||
import weapons from "./dicts/WeaponInfoMain.json";
|
||||
import specialWeapons from "./dicts/WeaponInfoSpecial.json";
|
||||
import subWeapons from "./dicts/WeaponInfoSub.json";
|
||||
// To run this script drop the https://github.com/Leanny/splat3 repo into scripts/dicts/splat3
|
||||
import {
|
||||
loadDamageRateInfo,
|
||||
loadWeaponInfoMain,
|
||||
loadWeaponInfoSpecial,
|
||||
loadWeaponInfoSub,
|
||||
PARAMETER_DIR,
|
||||
} from "./utils";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const params = loadDamageRateInfo();
|
||||
const weapons = loadWeaponInfoMain();
|
||||
const subWeapons = loadWeaponInfoSub();
|
||||
const specialWeapons = loadWeaponInfoSpecial();
|
||||
|
||||
const OUTPUT_DIR_PATH = path.join(__dirname, "output");
|
||||
const DAMAGE_RATE_CONFIG_FILE_NAME =
|
||||
"spl__DamageRateInfoConfig.pp__CombinationDataTableData.json";
|
||||
const HISTORY_OUTPUT_PATH = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"app",
|
||||
"features",
|
||||
"params",
|
||||
"data",
|
||||
"damage-rate-history.json",
|
||||
);
|
||||
|
||||
type DamageReceiver = (typeof DAMAGE_RECEIVERS)[number];
|
||||
|
||||
@@ -41,6 +57,21 @@ type DamageRateCell = {
|
||||
DamageRate?: number;
|
||||
};
|
||||
|
||||
type DamageRateConfig = { CellList: Record<string, DamageRateCell> };
|
||||
|
||||
type TargetHistory = {
|
||||
target: string;
|
||||
current: number;
|
||||
history: Array<{ version: string; value: number }>;
|
||||
};
|
||||
|
||||
type DamageRateHistoryRow = {
|
||||
mainWeaponIds: MainWeaponId[];
|
||||
subWeaponIds: SubWeaponId[];
|
||||
specialWeaponIds: SpecialWeaponId[];
|
||||
targets: TargetHistory[];
|
||||
};
|
||||
|
||||
const weaponParamsToWeaponIds = (
|
||||
params: typeof weapons | typeof subWeapons | typeof specialWeapons,
|
||||
key: string,
|
||||
@@ -60,75 +91,194 @@ const weaponParamsToWeaponIds = (
|
||||
const isDamageReceiver = (key: string): key is DamageReceiver =>
|
||||
(DAMAGE_RECEIVERS as readonly string[]).includes(key);
|
||||
|
||||
const result: Record<string, ResultEntry | undefined> = {};
|
||||
for (const cell of Object.values(params.CellList) as DamageRateCell[]) {
|
||||
if (!isDamageReceiver(cell.ColumnKey)) continue;
|
||||
if (!cell.DamageRate) continue;
|
||||
const mainIdsForRow = (rowKey: string) =>
|
||||
weaponParamsToWeaponIds(weapons, rowKey).filter((id): id is MainWeaponId =>
|
||||
(mainWeaponIds as readonly number[]).includes(id),
|
||||
);
|
||||
const subIdsForRow = (rowKey: string) =>
|
||||
weaponParamsToWeaponIds(subWeapons, rowKey).filter((id): id is SubWeaponId =>
|
||||
(subWeaponIds as readonly number[]).includes(id),
|
||||
);
|
||||
const specialIdsForRow = (rowKey: string) =>
|
||||
weaponParamsToWeaponIds(specialWeapons, rowKey).filter(
|
||||
(id): id is SpecialWeaponId =>
|
||||
(specialWeaponIds as readonly number[]).includes(id),
|
||||
);
|
||||
|
||||
if (!result[cell.RowKey]) {
|
||||
result[cell.RowKey] = {
|
||||
mainWeaponIds: weaponParamsToWeaponIds(weapons, cell.RowKey).filter(
|
||||
(id): id is MainWeaponId =>
|
||||
(mainWeaponIds as readonly number[]).includes(id),
|
||||
),
|
||||
subWeaponIds: weaponParamsToWeaponIds(subWeapons, cell.RowKey).filter(
|
||||
(id): id is SubWeaponId =>
|
||||
(subWeaponIds as readonly number[]).includes(id),
|
||||
),
|
||||
specialWeaponIds: weaponParamsToWeaponIds(
|
||||
specialWeapons,
|
||||
cell.RowKey,
|
||||
).filter((id): id is SpecialWeaponId =>
|
||||
(specialWeaponIds as readonly number[]).includes(id),
|
||||
),
|
||||
rates: [],
|
||||
};
|
||||
/**
|
||||
* Resolves the per-target damage rate of every damage rate info row in a single config dump.
|
||||
* Only the PvP-relevant receivers are kept and the synthetic launched/Recycled Brella canopy
|
||||
* targets are derived the same way the live object damage calculator expects them.
|
||||
*/
|
||||
const damageRatesByRow = (
|
||||
config: DamageRateConfig,
|
||||
): Map<string, Map<string, number>> => {
|
||||
const result = new Map<string, Map<string, number>>();
|
||||
|
||||
for (const cell of Object.values(config.CellList)) {
|
||||
if (!isDamageReceiver(cell.ColumnKey)) continue;
|
||||
if (!cell.DamageRate) continue;
|
||||
|
||||
let row = result.get(cell.RowKey);
|
||||
if (!row) {
|
||||
row = new Map();
|
||||
result.set(cell.RowKey, row);
|
||||
}
|
||||
|
||||
row.set(cell.ColumnKey, cell.DamageRate);
|
||||
|
||||
// launched versions have double health but share the same rate
|
||||
if (
|
||||
cell.ColumnKey.includes("BulletUmbrellaCanopyNormal") ||
|
||||
cell.ColumnKey.includes("BulletUmbrellaCanopyWide")
|
||||
) {
|
||||
row.set(`${cell.ColumnKey}_Launched`, cell.DamageRate);
|
||||
}
|
||||
|
||||
// Recycled Brella reuses Splat Brella's special damage rates
|
||||
if (cell.ColumnKey === "BulletUmbrellaCanopyNormal") {
|
||||
row.set("BulletShelterCanopyFocus", cell.DamageRate);
|
||||
row.set("BulletShelterCanopyFocus_Launched", cell.DamageRate);
|
||||
}
|
||||
}
|
||||
|
||||
const entry = result[cell.RowKey]!;
|
||||
return result;
|
||||
};
|
||||
|
||||
const result: Record<string, ResultEntry | undefined> = {};
|
||||
for (const [rowKey, rates] of damageRatesByRow(params)) {
|
||||
const mainWeaponIdsForRow = mainIdsForRow(rowKey);
|
||||
const subWeaponIdsForRow = subIdsForRow(rowKey);
|
||||
const specialWeaponIdsForRow = specialIdsForRow(rowKey);
|
||||
|
||||
// if it applies to no PvP weapons, we don't care about it
|
||||
if (
|
||||
entry.mainWeaponIds.length === 0 &&
|
||||
entry.subWeaponIds.length === 0 &&
|
||||
entry.specialWeaponIds.length === 0 &&
|
||||
cell.RowKey !== "ObjectEffect_Up"
|
||||
mainWeaponIdsForRow.length === 0 &&
|
||||
subWeaponIdsForRow.length === 0 &&
|
||||
specialWeaponIdsForRow.length === 0 &&
|
||||
rowKey !== "ObjectEffect_Up"
|
||||
) {
|
||||
result[cell.RowKey] = undefined;
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.rates.push({
|
||||
target: cell.ColumnKey,
|
||||
rate: cell.DamageRate,
|
||||
});
|
||||
|
||||
// add a second rate for launched versions, since they have double health
|
||||
if (
|
||||
cell.ColumnKey.includes("BulletUmbrellaCanopyNormal") ||
|
||||
cell.ColumnKey.includes("BulletUmbrellaCanopyWide")
|
||||
) {
|
||||
entry.rates.push({
|
||||
target: `${cell.ColumnKey}_Launched`,
|
||||
rate: cell.DamageRate,
|
||||
});
|
||||
}
|
||||
|
||||
// if it has special damage rates for Splat Brella, add the same value for Recycled Brella
|
||||
if (cell.ColumnKey === "BulletUmbrellaCanopyNormal") {
|
||||
entry.rates.push({
|
||||
target: "BulletShelterCanopyFocus",
|
||||
rate: cell.DamageRate,
|
||||
});
|
||||
|
||||
entry.rates.push({
|
||||
target: "BulletShelterCanopyFocus_Launched",
|
||||
rate: cell.DamageRate,
|
||||
});
|
||||
}
|
||||
result[rowKey] = {
|
||||
mainWeaponIds: mainWeaponIdsForRow,
|
||||
subWeaponIds: subWeaponIdsForRow,
|
||||
specialWeaponIds: specialWeaponIdsForRow,
|
||||
rates: [...rates].map(([target, rate]) => ({ target, rate })),
|
||||
};
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(OUTPUT_DIR_PATH, "object-dmg.json"),
|
||||
JSON.stringify(result, null, 2),
|
||||
);
|
||||
|
||||
writeDamageRateHistory();
|
||||
|
||||
function versionDirToDisplay(version: string): string {
|
||||
const num = Number.parseInt(version, 10);
|
||||
const major = Math.floor(num / 100);
|
||||
const minor = Math.floor((num % 100) / 10);
|
||||
const patch = num % 10;
|
||||
return `${major}.${minor}.${patch}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the per-row, per-target damage rate history across every versioned config dump and
|
||||
* writes it for the params page to surface in its patch history. Only PvP-relevant rows and
|
||||
* only targets whose rate actually changed at some point are kept, so the output stays small.
|
||||
*/
|
||||
function writeDamageRateHistory() {
|
||||
const versionDirs = fs
|
||||
.readdirSync(PARAMETER_DIR)
|
||||
.filter((dir) => /^\d+$/.test(dir))
|
||||
.sort((a, b) => Number.parseInt(a, 10) - Number.parseInt(b, 10));
|
||||
|
||||
const ratesByVersion = new Map<string, Map<string, Map<string, number>>>();
|
||||
for (const version of versionDirs) {
|
||||
const filePath = path.join(
|
||||
PARAMETER_DIR,
|
||||
version,
|
||||
"misc",
|
||||
DAMAGE_RATE_CONFIG_FILE_NAME,
|
||||
);
|
||||
if (!fs.existsSync(filePath)) continue;
|
||||
|
||||
const config = JSON.parse(
|
||||
fs.readFileSync(filePath, "utf8"),
|
||||
) as DamageRateConfig;
|
||||
ratesByVersion.set(version, damageRatesByRow(config));
|
||||
}
|
||||
|
||||
const presentVersions = versionDirs.filter((version) =>
|
||||
ratesByVersion.has(version),
|
||||
);
|
||||
const latestVersion = presentVersions[presentVersions.length - 1];
|
||||
|
||||
const rows: Record<string, DamageRateHistoryRow> = {};
|
||||
for (const [rowKey, latestRates] of ratesByVersion.get(latestVersion) ?? []) {
|
||||
const mainWeaponIdsForRow = mainIdsForRow(rowKey);
|
||||
const subWeaponIdsForRow = subIdsForRow(rowKey);
|
||||
const specialWeaponIdsForRow = specialIdsForRow(rowKey);
|
||||
|
||||
if (
|
||||
mainWeaponIdsForRow.length === 0 &&
|
||||
subWeaponIdsForRow.length === 0 &&
|
||||
specialWeaponIdsForRow.length === 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const targets: TargetHistory[] = [];
|
||||
for (const [target, current] of latestRates) {
|
||||
const presentForTarget = presentVersions.filter(
|
||||
(version) =>
|
||||
ratesByVersion.get(version)?.get(rowKey)?.get(target) !== undefined,
|
||||
);
|
||||
|
||||
const history: Array<{ version: string; value: number }> = [];
|
||||
for (let i = 0; i < presentForTarget.length - 1; i++) {
|
||||
const value = ratesByVersion
|
||||
.get(presentForTarget[i])!
|
||||
.get(rowKey)!
|
||||
.get(target)!;
|
||||
const nextValue = ratesByVersion
|
||||
.get(presentForTarget[i + 1])!
|
||||
.get(rowKey)!
|
||||
.get(target)!;
|
||||
if (value !== nextValue) {
|
||||
history.push({
|
||||
version: versionDirToDisplay(presentForTarget[i]),
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (history.length > 0) {
|
||||
targets.push({ target, current, history });
|
||||
}
|
||||
}
|
||||
|
||||
if (targets.length > 0) {
|
||||
rows[rowKey] = {
|
||||
mainWeaponIds: mainWeaponIdsForRow,
|
||||
subWeaponIds: subWeaponIdsForRow,
|
||||
specialWeaponIds: specialWeaponIdsForRow,
|
||||
targets,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
HISTORY_OUTPUT_PATH,
|
||||
JSON.stringify(
|
||||
{
|
||||
metadata: { versions: presentVersions.map(versionDirToDisplay) },
|
||||
rows,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { logger } from "~/utils/logger";
|
||||
import weapons from "./dicts/WeaponInfoMain.json";
|
||||
import { loadWeaponInfoMain } from "./utils";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const weapons = loadWeaponInfoMain();
|
||||
|
||||
const DIR_PATH_1 = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
|
||||
@@ -1,22 +1,41 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
mainWeaponIds,
|
||||
specialWeaponIds,
|
||||
subWeaponIds,
|
||||
} from "~/modules/in-game-lists/weapon-ids";
|
||||
import { logger } from "~/utils/logger";
|
||||
import weapons from "./dicts/WeaponInfoMain.json";
|
||||
import {
|
||||
loadWeaponInfoMain,
|
||||
loadWeaponInfoSpecial,
|
||||
loadWeaponInfoSub,
|
||||
MUSH_DIR,
|
||||
PARAMETER_DIR,
|
||||
} from "./utils";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const PARAMETER_DIR = path.join(__dirname, "dicts", "parameter");
|
||||
const weapons = loadWeaponInfoMain();
|
||||
const subWeapons = loadWeaponInfoSub();
|
||||
const specialWeapons = loadWeaponInfoSpecial();
|
||||
|
||||
const OUTPUT_DIR = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"app",
|
||||
"features",
|
||||
"weapons",
|
||||
"params",
|
||||
"data",
|
||||
);
|
||||
const OUTPUT_FILE = path.join(OUTPUT_DIR, "weapon-params.json");
|
||||
const OUTPUT_FILE = path.join(OUTPUT_DIR, "all-version-weapon-params.json");
|
||||
const SUB_OUTPUT_FILE = path.join(OUTPUT_DIR, "all-version-sub-params.json");
|
||||
const SPECIAL_OUTPUT_FILE = path.join(
|
||||
OUTPUT_DIR,
|
||||
"all-version-special-params.json",
|
||||
);
|
||||
|
||||
const WEAPON_TYPES_TO_IGNORE = [
|
||||
"Mission",
|
||||
@@ -72,6 +91,31 @@ function buildWeaponFileNameToIdMap(): Map<string, number> {
|
||||
return map;
|
||||
}
|
||||
|
||||
// Sub and special weapons share the per-version `weapon` GameParameterTable dump with main
|
||||
// weapons, so only the canonical "Versus" entry of each id is kept (Hero/Mission/etc. variants
|
||||
// of the same weapon are ignored).
|
||||
function buildSubOrSpecialFileNameToIdMap(
|
||||
entries: Array<{ Id: number; __RowId: string; Type: string }>,
|
||||
allowedIds: ReadonlySet<number>,
|
||||
): Map<string, number> {
|
||||
const map = new Map<string, number>();
|
||||
const seenFileNames = new Set<string>();
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.Type !== "Versus") continue;
|
||||
if (!allowedIds.has(entry.Id)) continue;
|
||||
|
||||
const fileName = weaponRowIdToFileName(entry.__RowId);
|
||||
|
||||
if (!seenFileNames.has(fileName)) {
|
||||
seenFileNames.add(fileName);
|
||||
map.set(fileName, entry.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function stripTypeFields(obj: unknown): unknown {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj;
|
||||
@@ -196,6 +240,133 @@ function mergeWithHistory(
|
||||
return result;
|
||||
}
|
||||
|
||||
// SpecialPoint lives in WeaponInfoMain (kit data), which is not part of the per-version weapon
|
||||
// GameParameterTable dump, so it is read per version from the local mush dir.
|
||||
function collectSpecialPointsByVersion(
|
||||
sortedVersions: string[],
|
||||
): Map<number, Map<string, number>> {
|
||||
const result = new Map<number, Map<string, number>>();
|
||||
const mainWeaponIdSet = new Set<number>(mainWeaponIds);
|
||||
|
||||
for (const version of sortedVersions) {
|
||||
const filePath = path.join(MUSH_DIR, version, "WeaponInfoMain.json");
|
||||
if (!fs.existsSync(filePath)) continue;
|
||||
|
||||
let entries: MainWeapon[];
|
||||
try {
|
||||
entries = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
} catch {
|
||||
logger.warn(`Failed to parse ${filePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const weapon of entries) {
|
||||
if (mainWeaponShouldBeSkipped(weapon)) continue;
|
||||
if (!mainWeaponIdSet.has(weapon.Id)) continue;
|
||||
if (typeof weapon.SpecialPoint !== "number") continue;
|
||||
|
||||
if (!result.has(weapon.Id)) {
|
||||
result.set(weapon.Id, new Map());
|
||||
}
|
||||
result.get(weapon.Id)!.set(version, weapon.SpecialPoint);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildSpecialPointsHistory(
|
||||
specialPointsByVersion: Map<number, Map<string, number>>,
|
||||
sortedVersions: string[],
|
||||
): Record<
|
||||
string,
|
||||
{ current: number; history: Array<{ version: string; value: number }> }
|
||||
> {
|
||||
const result: Record<
|
||||
string,
|
||||
{ current: number; history: Array<{ version: string; value: number }> }
|
||||
> = {};
|
||||
|
||||
for (const [weaponId, byVersion] of specialPointsByVersion) {
|
||||
const presentVersions = sortedVersions.filter((v) => byVersion.has(v));
|
||||
if (presentVersions.length === 0) continue;
|
||||
|
||||
const current = byVersion.get(presentVersions[presentVersions.length - 1])!;
|
||||
const history: Array<{ version: string; value: number }> = [];
|
||||
|
||||
for (let i = 0; i < presentVersions.length - 1; i++) {
|
||||
const value = byVersion.get(presentVersions[i])!;
|
||||
const nextValue = byVersion.get(presentVersions[i + 1])!;
|
||||
if (value !== nextValue) {
|
||||
history.push({
|
||||
version: parseVersionToDisplay(presentVersions[i]),
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
result[String(weaponId)] = { current, history };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Reads every per-version `weapon` GameParameterTable dump for the given files and folds the
|
||||
// historical values of each weapon into its latest params using versioned (`Key@version`) keys.
|
||||
function buildParamsWithHistory(
|
||||
fileNameToId: Map<string, number>,
|
||||
sortedVersions: string[],
|
||||
): Record<string, Record<string, unknown>> {
|
||||
const allVersions = new Map<number, Map<string, Record<string, unknown>>>();
|
||||
|
||||
for (const version of sortedVersions) {
|
||||
const weaponDir = path.join(PARAMETER_DIR, version, "weapon");
|
||||
if (!fs.existsSync(weaponDir)) continue;
|
||||
|
||||
for (const file of fs.readdirSync(weaponDir)) {
|
||||
if (!fileNameToId.has(file)) continue;
|
||||
|
||||
const weaponId = fileNameToId.get(file)!;
|
||||
const filePath = path.join(weaponDir, file);
|
||||
|
||||
try {
|
||||
const content = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
const params = stripTypeFields(content.GameParameters) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
if (!allVersions.has(weaponId)) {
|
||||
allVersions.set(weaponId, new Map());
|
||||
}
|
||||
allVersions.get(weaponId)!.set(version, params);
|
||||
} catch {
|
||||
logger.warn(`Failed to parse ${filePath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const output: Record<string, Record<string, unknown>> = {};
|
||||
const latestVersion = sortedVersions[sortedVersions.length - 1];
|
||||
|
||||
for (const [weaponId, versionParams] of allVersions) {
|
||||
const latestParams = versionParams.get(latestVersion);
|
||||
if (!latestParams) continue;
|
||||
|
||||
const versionsWithWeapon = sortedVersions.filter((v) =>
|
||||
versionParams.has(v),
|
||||
);
|
||||
|
||||
output[String(weaponId)] = mergeWithHistory(
|
||||
latestParams,
|
||||
versionParams,
|
||||
versionsWithWeapon,
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
logger.info("Starting weapon params sync...");
|
||||
|
||||
@@ -208,78 +379,62 @@ async function main() {
|
||||
`Found ${sortedVersions.length} versions: ${sortedVersions.map(parseVersionToDisplay).join(", ")}`,
|
||||
);
|
||||
|
||||
const latestVersion = sortedVersions[sortedVersions.length - 1];
|
||||
const metadata = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
latestVersion: parseVersionToDisplay(latestVersion),
|
||||
versions: sortedVersions.map(parseVersionToDisplay),
|
||||
};
|
||||
|
||||
const weaponFileNameToId = buildWeaponFileNameToIdMap();
|
||||
logger.info(`Processing ${weaponFileNameToId.size} unique weapons`);
|
||||
const outputWeapons = buildParamsWithHistory(
|
||||
weaponFileNameToId,
|
||||
sortedVersions,
|
||||
);
|
||||
|
||||
const weaponParamsAllVersions = new Map<
|
||||
number,
|
||||
Map<string, Record<string, unknown>>
|
||||
>();
|
||||
const specialPoints = buildSpecialPointsHistory(
|
||||
collectSpecialPointsByVersion(sortedVersions),
|
||||
sortedVersions,
|
||||
);
|
||||
|
||||
for (const version of sortedVersions) {
|
||||
const weaponDir = path.join(PARAMETER_DIR, version, "weapon");
|
||||
if (!fs.existsSync(weaponDir)) continue;
|
||||
const outputSubWeapons = buildParamsWithHistory(
|
||||
buildSubOrSpecialFileNameToIdMap(subWeapons, new Set(subWeaponIds)),
|
||||
sortedVersions,
|
||||
);
|
||||
|
||||
const files = fs.readdirSync(weaponDir);
|
||||
|
||||
for (const file of files) {
|
||||
if (!weaponFileNameToId.has(file)) continue;
|
||||
|
||||
const weaponId = weaponFileNameToId.get(file)!;
|
||||
const filePath = path.join(weaponDir, file);
|
||||
|
||||
try {
|
||||
const content = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||
const params = stripTypeFields(content.GameParameters) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
if (!weaponParamsAllVersions.has(weaponId)) {
|
||||
weaponParamsAllVersions.set(weaponId, new Map());
|
||||
}
|
||||
weaponParamsAllVersions.get(weaponId)!.set(version, params);
|
||||
} catch {
|
||||
logger.warn(`Failed to parse ${filePath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const outputWeapons: Record<string, Record<string, unknown>> = {};
|
||||
const latestVersion = sortedVersions[sortedVersions.length - 1];
|
||||
|
||||
for (const [weaponId, versionParams] of weaponParamsAllVersions) {
|
||||
const latestParams = versionParams.get(latestVersion);
|
||||
if (!latestParams) continue;
|
||||
|
||||
const versionsWithWeapon = sortedVersions.filter((v) =>
|
||||
versionParams.has(v),
|
||||
);
|
||||
const paramsWithHistory = mergeWithHistory(
|
||||
latestParams,
|
||||
versionParams,
|
||||
versionsWithWeapon,
|
||||
);
|
||||
|
||||
outputWeapons[String(weaponId)] = paramsWithHistory;
|
||||
}
|
||||
|
||||
const output = {
|
||||
metadata: {
|
||||
generatedAt: new Date().toISOString(),
|
||||
latestVersion: parseVersionToDisplay(latestVersion),
|
||||
versions: sortedVersions.map(parseVersionToDisplay),
|
||||
},
|
||||
weapons: outputWeapons,
|
||||
};
|
||||
const outputSpecialWeapons = buildParamsWithHistory(
|
||||
buildSubOrSpecialFileNameToIdMap(specialWeapons, new Set(specialWeaponIds)),
|
||||
sortedVersions,
|
||||
);
|
||||
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(output, null, 2));
|
||||
fs.writeFileSync(
|
||||
OUTPUT_FILE,
|
||||
JSON.stringify(
|
||||
{ metadata, weapons: outputWeapons, specialPoints },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
SUB_OUTPUT_FILE,
|
||||
JSON.stringify({ metadata, weapons: outputSubWeapons }, null, 2),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
SPECIAL_OUTPUT_FILE,
|
||||
JSON.stringify({ metadata, weapons: outputSpecialWeapons }, null, 2),
|
||||
);
|
||||
|
||||
logger.info(`Written to ${OUTPUT_FILE}`);
|
||||
logger.info(`Total weapons: ${Object.keys(outputWeapons).length}`);
|
||||
logger.info(`Total main weapons: ${Object.keys(outputWeapons).length}`);
|
||||
logger.info(`Total sub weapons: ${Object.keys(outputSubWeapons).length}`);
|
||||
logger.info(
|
||||
`Total special weapons: ${Object.keys(outputSpecialWeapons).length}`,
|
||||
);
|
||||
}
|
||||
|
||||
main().catch((err) => logger.error(err));
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type euEn from "./dicts/langs/EUen.json";
|
||||
import type euEn from "./dicts/splat3/data/language/EUen.json";
|
||||
// The splat3 dump exposes a `latest` symlink to the newest version folder, so these loaders never
|
||||
// need to hard-code a version.
|
||||
import type gearInfoClothes from "./dicts/splat3/data/mush/latest/GearInfoClothes.json";
|
||||
import type gearInfoHead from "./dicts/splat3/data/mush/latest/GearInfoHead.json";
|
||||
import type gearInfoShoes from "./dicts/splat3/data/mush/latest/GearInfoShoes.json";
|
||||
import type weaponInfoMain from "./dicts/splat3/data/mush/latest/WeaponInfoMain.json";
|
||||
import type weaponInfoSpecial from "./dicts/splat3/data/mush/latest/WeaponInfoSpecial.json";
|
||||
import type weaponInfoSub from "./dicts/splat3/data/mush/latest/WeaponInfoSub.json";
|
||||
import type splPlayer from "./dicts/splat3/data/parameter/latest/misc/SplPlayer.game__GameParameterTable.json";
|
||||
import type damageRateInfo from "./dicts/splat3/data/parameter/latest/misc/spl__DamageRateInfoConfig.pp__CombinationDataTableData.json";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const LANG_DICTS_PATH = path.join(__dirname, "dicts", "langs");
|
||||
const SPLAT3_DATA_PATH = path.join(__dirname, "dicts", "splat3", "data");
|
||||
|
||||
/** Per-version weapon/sub/special `GameParameterTable` dumps, keyed by patch version folder. */
|
||||
export const PARAMETER_DIR = path.join(SPLAT3_DATA_PATH, "parameter");
|
||||
/** Per-version `WeaponInfo`/`GearInfo` dumps, keyed by patch version folder. */
|
||||
export const MUSH_DIR = path.join(SPLAT3_DATA_PATH, "mush");
|
||||
|
||||
const LANG_DICTS_PATH = path.join(SPLAT3_DATA_PATH, "language");
|
||||
|
||||
export const LANG_JSONS_TO_CREATE = [
|
||||
"EUen",
|
||||
@@ -47,3 +64,44 @@ export function translationJsonFolderName(langCode: string) {
|
||||
if (langCode === "USfr") return "fr-CA";
|
||||
return langCode.slice(2);
|
||||
}
|
||||
|
||||
/** Latest-version directory holding the per-weapon `GameParameterTable` dumps. */
|
||||
export function weaponParamsDir() {
|
||||
return path.join(PARAMETER_DIR, "latest", "weapon");
|
||||
}
|
||||
|
||||
export const loadWeaponInfoMain = () =>
|
||||
loadLatestMushJson<typeof weaponInfoMain>("WeaponInfoMain");
|
||||
export const loadWeaponInfoSub = () =>
|
||||
loadLatestMushJson<typeof weaponInfoSub>("WeaponInfoSub");
|
||||
export const loadWeaponInfoSpecial = () =>
|
||||
loadLatestMushJson<typeof weaponInfoSpecial>("WeaponInfoSpecial");
|
||||
export const loadGearInfoClothes = () =>
|
||||
loadLatestMushJson<typeof gearInfoClothes>("GearInfoClothes");
|
||||
export const loadGearInfoHead = () =>
|
||||
loadLatestMushJson<typeof gearInfoHead>("GearInfoHead");
|
||||
export const loadGearInfoShoes = () =>
|
||||
loadLatestMushJson<typeof gearInfoShoes>("GearInfoShoes");
|
||||
export const loadSplPlayerParams = () =>
|
||||
loadLatestParameterMiscJson<typeof splPlayer>(
|
||||
"SplPlayer.game__GameParameterTable",
|
||||
);
|
||||
export const loadDamageRateInfo = () =>
|
||||
loadLatestParameterMiscJson<typeof damageRateInfo>(
|
||||
"spl__DamageRateInfoConfig.pp__CombinationDataTableData",
|
||||
);
|
||||
|
||||
function loadLatestMushJson<T>(fileName: string): T {
|
||||
return JSON.parse(
|
||||
fs.readFileSync(path.join(MUSH_DIR, "latest", `${fileName}.json`), "utf8"),
|
||||
);
|
||||
}
|
||||
|
||||
function loadLatestParameterMiscJson<T>(fileName: string): T {
|
||||
return JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(PARAMETER_DIR, "latest", "misc", `${fileName}.json`),
|
||||
"utf8",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user