This commit is contained in:
1 parent
8eb519023a
commit
1593581729
18 files changed
+2757
-98
No files matched your search
@@ -0,0 +1,243 @@
|
||||
// Builds the compact endgame catalog used by the FireflyGo monster settings.
|
||||
//
|
||||
// Firefly SrTools serves the raw client tables from
|
||||
// https://srtools.punklorde.org/api/data/<name> (moc, pf, as, peak, monster).
|
||||
// Those files are 2-14 MB each, so this script reduces them to the small subset
|
||||
// the UI needs (season -> floor -> side -> waves + monster names/icons) and
|
||||
// writes assets/firefly-endgame.json.
|
||||
//
|
||||
// Usage: node scripts/build-firefly-endgame-data.cjs [cacheDir]
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const cacheDir = process.argv[2] || path.join(require('node:os').tmpdir(), 'ff-endgame-cache');
|
||||
const ORIGIN = 'https://srtools.punklorde.org/api/data';
|
||||
const LOCALES = ['en', 'th'];
|
||||
|
||||
function clean(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/\{RUBY_B#[^}]*\}/g, '')
|
||||
.replace(/\{RUBY_E#\}/g, '')
|
||||
.replace(/<[^>]*>/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function pickName(name) {
|
||||
if (!name || typeof name !== 'object') return {};
|
||||
const out = {};
|
||||
for (const locale of LOCALES) {
|
||||
const text = clean(name[locale]);
|
||||
if (text) out[locale] = text;
|
||||
}
|
||||
if (!out.en) {
|
||||
const fallback = clean(name.cn || name.cht || '');
|
||||
if (fallback) out.en = fallback;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function buffList(list) {
|
||||
if (!Array.isArray(list)) return [];
|
||||
return list
|
||||
.map((buff) => {
|
||||
const id = Number(buff?.ID);
|
||||
if (!Number.isFinite(id) || id <= 0) return null;
|
||||
return { id, name: pickName(buff?.Name) };
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function wavesOf(stage) {
|
||||
if (!Array.isArray(stage?.MonsterList)) return [];
|
||||
return stage.MonsterList
|
||||
.map((wave) => (Array.isArray(wave) ? wave : Object.values(wave || {}))
|
||||
.map((id) => Number(id))
|
||||
.filter((id) => Number.isFinite(id) && id > 0))
|
||||
.filter((wave) => wave.length > 0);
|
||||
}
|
||||
|
||||
function sideOf(side, stage) {
|
||||
if (!stage) return null;
|
||||
return {
|
||||
side,
|
||||
stageId: Number(stage.ID) || 0,
|
||||
level: Number(stage.Level) || 0,
|
||||
name: pickName(stage.Name),
|
||||
waves: wavesOf(stage),
|
||||
...(stage.Invasion?.MazeBuff?.ID
|
||||
? { invasionBuffId: Number(stage.Invasion.MazeBuff.ID) }
|
||||
: {})
|
||||
};
|
||||
}
|
||||
|
||||
// MOC / PF / AS all expose EventList1 (upper) + EventList2 (lower); MOC also has
|
||||
// a third node on the Tierce (hard) config.
|
||||
function sidesOf(level, tierce) {
|
||||
return [
|
||||
sideOf('firstNode', level?.EventList1?.[0]),
|
||||
sideOf('secondNode', level?.EventList2?.[0]),
|
||||
sideOf('thirdNode', tierce?.EventList?.[0])
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
function levelEntry(level, tierce) {
|
||||
const sides = sidesOf(level, tierce);
|
||||
if (!sides.length) return null;
|
||||
return {
|
||||
id: Number(level.ID) || 0,
|
||||
floor: Number(level.Floor) || 0,
|
||||
name: pickName(level.Name),
|
||||
turnLimit: Number(level.TurnLimit) || 0,
|
||||
buffs: buffList(level.MazeBuff),
|
||||
sides
|
||||
};
|
||||
}
|
||||
|
||||
function groupEntry(id, group, mode) {
|
||||
const levels = (Array.isArray(group.Level) ? group.Level : [])
|
||||
.map((level) => levelEntry(level, group.Tierce))
|
||||
.filter(Boolean);
|
||||
if (!levels.length) return null;
|
||||
return {
|
||||
id: Number(id) || 0,
|
||||
name: pickName(group.Name),
|
||||
begin: group.BeginTime || '',
|
||||
end: group.EndTime || '',
|
||||
levels
|
||||
};
|
||||
}
|
||||
|
||||
function buildMocLike(data, mode) {
|
||||
return Object.entries(data)
|
||||
.map(([id, group]) => groupEntry(id, group, mode))
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.id - b.id);
|
||||
}
|
||||
|
||||
function peakStage(level, extra = {}) {
|
||||
const stage = level?.EventList?.[0];
|
||||
if (!stage) return null;
|
||||
return {
|
||||
id: Number(level.ID) || 0,
|
||||
stageId: Number(stage.ID) || 0,
|
||||
level: Number(stage.Level) || 0,
|
||||
name: pickName(stage.Name),
|
||||
turnLimit: Number(level.TurnLimit) || 0,
|
||||
buffs: buffList(level.MazeBuff),
|
||||
waves: wavesOf(stage),
|
||||
...(stage.Invasion?.MazeBuff?.ID ? { invasionBuffId: Number(stage.Invasion.MazeBuff.ID) } : {}),
|
||||
...extra
|
||||
};
|
||||
}
|
||||
|
||||
function buildPeak(data) {
|
||||
return Object.entries(data)
|
||||
.map(([id, group]) => {
|
||||
const stages = [];
|
||||
for (const level of group.PreLevel || []) {
|
||||
const entry = peakStage(level, { kind: 'pre' });
|
||||
if (entry) stages.push(entry);
|
||||
}
|
||||
const boss = peakStage(group.BossLevel, { kind: 'boss' });
|
||||
if (boss) stages.push(boss);
|
||||
const hard = peakStage(group.BossConfig, { kind: 'hard' });
|
||||
if (hard) stages.push(hard);
|
||||
if (!stages.length) return null;
|
||||
return {
|
||||
id: Number(id) || 0,
|
||||
name: pickName(group.Name),
|
||||
stages
|
||||
};
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.id - b.id);
|
||||
}
|
||||
|
||||
function collectMonsterIds(catalog) {
|
||||
const ids = new Set();
|
||||
const add = (waves) => waves.forEach((wave) => wave.forEach((id) => ids.add(id)));
|
||||
for (const group of catalog) {
|
||||
if (group.levels) {
|
||||
for (const level of group.levels) {
|
||||
for (const side of level.sides) add(side.waves);
|
||||
}
|
||||
}
|
||||
if (group.stages) {
|
||||
for (const stage of group.stages) add(stage.waves);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
async function loadTable(name) {
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
const file = path.join(cacheDir, `${name}.json`);
|
||||
if (fs.existsSync(file)) {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
}
|
||||
const res = await fetch(`${ORIGIN}/${name}`);
|
||||
if (!res.ok) throw new Error(`${name}: HTTP ${res.status}`);
|
||||
const text = await res.text();
|
||||
fs.writeFileSync(file, text);
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [moc, pf, as, peak, monsters] = await Promise.all([
|
||||
loadTable('moc'),
|
||||
loadTable('pf'),
|
||||
loadTable('as'),
|
||||
loadTable('peak'),
|
||||
loadTable('monster')
|
||||
]);
|
||||
|
||||
const modes = {
|
||||
moc: { groups: buildMocLike(moc, 'moc') },
|
||||
pf: { groups: buildMocLike(pf, 'pf') },
|
||||
as: { groups: buildMocLike(as, 'as') },
|
||||
peak: { groups: buildPeak(peak) }
|
||||
};
|
||||
|
||||
const monsterIds = new Set();
|
||||
for (const mode of Object.values(modes)) {
|
||||
for (const id of collectMonsterIds(mode.groups)) monsterIds.add(id);
|
||||
}
|
||||
|
||||
const monsterIndex = {};
|
||||
const missing = [];
|
||||
for (const id of [...monsterIds].sort((a, b) => a - b)) {
|
||||
const entry = monsters[String(id)];
|
||||
if (!entry) {
|
||||
missing.push(id);
|
||||
continue;
|
||||
}
|
||||
monsterIndex[id] = {
|
||||
name: pickName(entry.Name),
|
||||
rank: Number(entry.Rank) || 0,
|
||||
stance: Number(entry.StanceCount) || 0,
|
||||
weak: Array.isArray(entry.StanceWeakList) ? entry.StanceWeakList : [],
|
||||
icon: entry.Image?.IconPath ? `https://cdn.punklorde.org/asbres/${entry.Image.IconPath}` : ''
|
||||
};
|
||||
}
|
||||
|
||||
const output = {
|
||||
version: new Date().toISOString().slice(0, 10),
|
||||
source: 'https://srtools.punklorde.org/api/data',
|
||||
modes,
|
||||
monsters: monsterIndex
|
||||
};
|
||||
|
||||
const target = path.join(root, 'assets', 'firefly-endgame.json');
|
||||
fs.writeFileSync(target, JSON.stringify(output));
|
||||
const bytes = fs.statSync(target).size;
|
||||
console.log(`Wrote ${target} (${(bytes / 1024).toFixed(1)} KB)`);
|
||||
console.log(`MOC ${modes.moc.groups.length} groups, PF ${modes.pf.groups.length}, AS ${modes.as.groups.length}, PEAK ${modes.peak.groups.length}`);
|
||||
console.log(`Monsters ${Object.keys(monsterIndex).length} (missing ${missing.length}${missing.length ? ': ' + missing.slice(0, 20).join(',') : ''})`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in new issue
Block a user