FireflyGO api support
Deploy GaC-Srtool to GitHub Pages / deploy (push) Canceled after 0s

This commit is contained in:
CAtlas0784 committed 2026-11-04 17:14:50 +07:00
1 parent 8eb519023a
commit 1593581729
18 files changed
+2757 -98

No files matched your search

+219
View File
@@ -0,0 +1,219 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const { pathToFileURL } = require('node:url');
const moduleUrl = pathToFileURL(path.join(__dirname, '../js/endgame.js'));
async function load() {
return import(moduleUrl.href);
}
function sampleCatalog() {
return {
modes: {
moc: {
groups: [
{
id: 1000,
name: { en: 'Old season' },
levels: [
{
id: 5001,
floor: 10,
turnLimit: 24,
buffs: [{ id: 900 }],
sides: [{ side: 'firstNode', stageId: 9001, level: 80, waves: [[11]] }]
}
]
},
{
id: 1037,
name: { en: 'Echo of Darkness' },
levels: [
{
id: 5612,
floor: 12,
turnLimit: 30,
buffs: [{ id: 1001 }],
sides: [
{
side: 'firstNode',
stageId: 30127111,
level: 92,
waves: [[4033010, 4033020], [4044010]],
invasionBuffId: 3034002
},
{ side: 'secondNode', stageId: 30127112, level: 95, waves: [[1003010]] }
]
}
]
}
]
},
pf: { groups: [] },
as: { groups: [] },
peak: {
groups: [
{
id: 9,
name: { en: 'Return of the Legion' },
stages: [
{ kind: 'pre', stageId: 4001, level: 80, waves: [[11]], buffs: [] },
{ kind: 'boss', stageId: 4002, level: 90, waves: [[22]], buffs: [{ id: 2001 }] },
{ kind: 'hard', stageId: 4003, level: 95, waves: [[33]], buffs: [{ id: 2002 }] }
]
}
]
}
},
monsters: {
11: { name: { en: 'Monster 11' }, icon: 'https://cdn.test/11.webp' },
4033010: { name: { en: 'Monster 4033010' }, icon: 'https://cdn.test/4033010.webp' }
}
};
}
test('defaultEndgameSelection picks the newest season, highest floor and first side', async () => {
const { defaultEndgameSelection } = await load();
const selection = defaultEndgameSelection(sampleCatalog(), 'moc');
assert.equal(selection.mode, 'moc');
assert.equal(selection.groupId, 1037);
assert.equal(selection.levelId, 5612);
assert.equal(selection.side, 'firstNode');
assert.equal(selection.useCycle, true);
assert.equal(selection.useBuff, true);
});
test('defaultEndgameSelection defaults peak to the boss stage', async () => {
const { defaultEndgameSelection } = await load();
const selection = defaultEndgameSelection(sampleCatalog(), 'peak');
assert.equal(selection.mode, 'peak');
assert.equal(selection.groupId, 9);
assert.equal(selection.stageIndex, 1);
});
test('buildFireflyBattleConfig builds the FireflyGo battle_config shape', async () => {
const { buildFireflyBattleConfig, defaultEndgameSelection } = await load();
const catalog = sampleCatalog();
const config = buildFireflyBattleConfig(catalog, defaultEndgameSelection(catalog, 'moc'));
assert.equal(config.battle_type, 'MOC');
assert.equal(config.stage_id, 30127111);
assert.equal(config.cycle_count, 30);
assert.equal(config.path_resonance_id, 0);
assert.deepEqual(config.custom_stats, []);
assert.deepEqual(config.blessings, [
{ id: 1001, level: 1 },
{ id: 3034002, level: 1 }
]);
assert.deepEqual(config.monsters, [
[
{ monster_id: 4033010, level: 92, amount: 1 },
{ monster_id: 4033020, level: 92, amount: 1 }
],
[{ monster_id: 4044010, level: 92, amount: 1 }]
]);
});
test('cycle and buff toggles remove the corresponding values', async () => {
const { buildFireflyBattleConfig } = await load();
const config = buildFireflyBattleConfig(sampleCatalog(), {
mode: 'moc',
groupId: 1037,
levelId: 5612,
side: 'secondNode',
useCycle: false,
useBuff: false
});
assert.equal(config.battle_type, 'MOC');
assert.equal(config.stage_id, 30127112);
assert.equal(config.cycle_count, 0);
assert.deepEqual(config.blessings, []);
assert.deepEqual(config.monsters, [[{ monster_id: 1003010, level: 95, amount: 1 }]]);
});
test('peak uses the selected stage index and reports PEAK', async () => {
const { buildFireflyBattleConfig, buildFireflyChallenge } = await load();
const catalog = sampleCatalog();
const config = buildFireflyBattleConfig(catalog, {
mode: 'peak',
groupId: 9,
stageIndex: 2,
useCycle: true,
useBuff: true
});
assert.equal(config.battle_type, 'PEAK');
assert.equal(config.stage_id, 4003);
assert.deepEqual(config.blessings, [{ id: 2002, level: 1 }]);
assert.deepEqual(buildFireflyChallenge({ mode: 'peak', groupId: 9 }, { skip_node: 3, challenge_peak_group_id: 1 }), {
skip_node: 3,
challenge_peak_group_id: 9,
challenge_peak_group_id_list: [9]
});
// Non-peak modes keep the existing challenge untouched.
assert.deepEqual(buildFireflyChallenge({ mode: 'moc', groupId: 1037 }, { skip_node: 1, challenge_peak_group_id: 4 }), {
skip_node: 1,
challenge_peak_group_id: 4,
challenge_peak_group_id_list: []
});
});
test('findSelectionByStageId reopens the saved room', async () => {
const { findSelectionByStageId } = await load();
const catalog = sampleCatalog();
assert.deepEqual(findSelectionByStageId(catalog, 'moc', 30127112), {
mode: 'moc',
groupId: 1037,
levelId: 5612,
side: 'secondNode',
useCycle: true,
useBuff: true
});
assert.deepEqual(findSelectionByStageId(catalog, 'peak', 4003, 9), {
mode: 'peak',
groupId: 9,
stageIndex: 2,
useCycle: true,
useBuff: true
});
assert.equal(findSelectionByStageId(catalog, 'peak', 9999), null);
assert.equal(findSelectionByStageId(catalog, 'custom', 4003), null);
});
test('custom mode keeps arbitrary waves and rejects empty input', async () => {
const { buildFireflyBattleConfig, summarizeEndgameSelection } = await load();
const catalog = sampleCatalog();
const config = buildFireflyBattleConfig(catalog, {
mode: 'custom',
customCycle: 4,
customWaves: [[{ monster_id: '4033010', level: '90', amount: '2' }], [{ monster_id: 22 }]]
});
assert.equal(config.battle_type, 'CE');
assert.equal(config.stage_id, 0);
assert.equal(config.cycle_count, 4);
assert.deepEqual(config.monsters, [
[{ monster_id: 4033010, level: 90, amount: 2 }],
[{ monster_id: 22, level: 80, amount: 1 }]
]);
assert.equal(buildFireflyBattleConfig(catalog, { mode: 'custom', customWaves: [[{ monster_id: 0 }]] }), null);
assert.equal(buildFireflyBattleConfig(catalog, { mode: 'custom', customWaves: [] }), null);
const summary = summarizeEndgameSelection(catalog, {
mode: 'custom',
customCycle: 4,
customWaves: [[{ monster_id: '4033010' }], [{ monster_id: 22 }]]
});
assert.equal(summary.waveCount, 2);
assert.equal(summary.monsterCount, 2);
assert.equal(summary.cycleCount, 4);
});
test('unknown modes and empty catalogs resolve to null', async () => {
const { buildFireflyBattleConfig, resolveEndgameSelection } = await load();
assert.equal(resolveEndgameSelection({ modes: {} }, { mode: 'moc' }), null);
assert.equal(buildFireflyBattleConfig(sampleCatalog(), { mode: 'su' }), null);
assert.equal(buildFireflyBattleConfig(sampleCatalog(), { mode: 'pf' }), null);
});
+43 -3
View File
@@ -152,18 +152,58 @@ test('buildSyncPayload emits the FreeSR shape FireflyGo expects', async () => {
assert.equal(payload.relics.length, 1);
assert.deepEqual(payload.relics[0].sub_affixes, [{ sub_affix_id: 2, count: 3, step: 0 }]);
// Unknown battle_config keys such as custom_battle_lineup are stripped.
// Unknown battle_config keys such as custom_battle_lineup are stripped and
// the blessing/monster arrays are coerced into FireflyGo's zod shape.
assert.deepEqual(payload.battle_config, {
battle_type: 'MOC',
blessings: [1, 2],
blessings: [{ id: 1, level: 1 }, { id: 2, level: 1 }],
custom_stats: [],
cycle_count: 3,
stage_id: 12,
path_resonance_id: 0,
monsters: [[1002010, 1]]
monsters: [[{ monster_id: 1002010, level: 1, amount: 1 }, { monster_id: 1, level: 1, amount: 1 }]]
});
});
test('buildSyncPayload normalises detailed blessings and monster waves', async () => {
const { buildSyncPayload } = await load();
const payload = buildSyncPayload({
battle_config: {
battle_type: 'PEAK',
blessings: [
{ id: 3034002, level: '2', dynamic_key: { key: 'abc', value: '4' } },
{ id: 0, level: 1 },
{ id: '3001' }
],
cycle_count: '6',
stage_id: '30127123',
monsters: [
[
{ monster_id: 4033010, level: 92, amount: 2 },
{ monster_id: 0, level: 1 },
{ monster_id: '4033020', level: '95' }
],
[],
[4044010]
]
}
});
assert.deepEqual(payload.battle_config.blessings, [
{ id: 3034002, level: 2, dynamic_key: { key: 'abc', value: 4 } },
{ id: 3001, level: 1 }
]);
assert.deepEqual(payload.battle_config.monsters, [
[
{ monster_id: 4033010, level: 92, amount: 2 },
{ monster_id: 4033020, level: 95, amount: 1 }
],
[{ monster_id: 4044010, level: 1, amount: 1 }]
]);
assert.equal(payload.battle_config.cycle_count, 6);
assert.equal(payload.battle_config.stage_id, 30127123);
});
test('buildSyncPayload derives the anchor map when the save has none', async () => {
const { buildSyncPayload } = await load();
const payload = buildSyncPayload({
+24 -3
View File
@@ -19,6 +19,7 @@ test('GaCy GO keeps every tool feature', async () => {
]) {
assert.equal(caps[feature], true, `${feature} must be available on GaCy GO`);
}
assert.equal(caps.fireflyEndgame, false);
assert.equal(caps.extraData, false);
assert.equal(caps.ownedAvatarsOnly, false);
assert.deepEqual(caps.jsonModes, ['freesr', 'persistent']);
@@ -34,10 +35,14 @@ test('FireflyGo hides every GaCy-only feature and exposes extra_data', async ()
assert.equal(caps[feature], false, `${feature} must be hidden on FireflyGo`);
}
assert.equal(caps.extraData, true);
assert.equal(caps.fireflyEndgame, true);
assert.equal(caps.ownedAvatarsOnly, true);
assert.deepEqual(caps.jsonModes, ['freesr']);
assert.equal(modeSupports('extraData', 'firefly'), true);
assert.equal(modeSupports('saveManager', 'firefly'), false);
assert.equal(modeSupports('fireflyEndgame', 'firefly'), true);
assert.equal(modeSupports('monsterSettings', 'firefly'), false);
assert.equal(modeSupports('fireflyEndgame', 'gac'), false);
assert.equal(modeSupports('fireflyEndgame', 'robin'), false);
});
test('unknown modes fall back to GaCy GO', async () => {
@@ -73,7 +78,20 @@ test('mode UI strings exist in Thai and English', async () => {
'btnFireflyProbe', 'btnFireflySaveExtra', 'btnFireflySync',
'fireflyProbing', 'fireflySyncing', 'fireflyProbeSuccess', 'fireflySyncSuccess',
'fireflyRequestFailed', 'fireflyRequestFailedDetail', 'fireflyTimeout',
'fireflyNoData', 'fireflyExtraSaved', 'fireflyHpInvalid'
'fireflyNoData', 'fireflyExtraSaved', 'fireflyHpInvalid',
'ffEndgameTitle', 'ffEndgameTablistLabel', 'ffEndgameDescription', 'ffEndgameRule', 'ffEndgameModePeak',
'ffEndgameLoading', 'ffEndgameReady', 'ffEndgameCatalogError', 'ffEndgameSeason',
'ffEndgameFloor', 'ffEndgameFloorValue', 'ffEndgameSide', 'ffEndgameSideFirst',
'ffEndgameSideSecond', 'ffEndgameSideThird', 'ffEndgameStage', 'ffEndgameStageValue',
'ffEndgameStageKindPre', 'ffEndgameStageKindBoss', 'ffEndgameStageKindHard',
'ffEndgameStageId', 'ffEndgamePreview', 'ffEndgameNoStages', 'ffEndgameUseCycle',
'ffEndgameUseBuff', 'ffEndgameCycles', 'ffEndgameBlessings', 'ffEndgameMonsterCount',
'ffEndgameApply', 'ffEndgameApplySync', 'ffEndgameReset', 'ffEndgameResetDone',
'ffEndgameSaving', 'ffEndgameSaved', 'ffEndgameSynced', 'ffEndgameApplyError',
'ffEndgameNoData', 'ffEndgameSyncError', 'ffEndgameCustomDescription',
'ffEndgameAddWave', 'ffEndgameRemoveWave', 'ffEndgameAddMonster',
'ffEndgameRemoveMonster', 'ffEndgameMonsterId', 'ffEndgameMonsterLevel',
'ffEndgameMonsterAmount', 'ffEndgameCycleCount'
];
for (const lang of ['th', 'en']) {
for (const key of keys) {
@@ -103,7 +121,10 @@ test('mode-gated DOM hooks exist in index.html', () => {
'ff-challenge-skip-node', 'ff-challenge-peak-group', 'ff-challenge-peak-list',
'ff-theory-mode', 'ff-theory-cycle-count', 'ff-theory-stage-id', 'ff-theory-lineup',
'ff-theory-hp', 'ff-lua', 'btn-firefly-probe', 'btn-firefly-save-extra',
'btn-firefly-sync', 'firefly-extra-status'
'btn-firefly-sync', 'firefly-extra-status',
'monster-settings-gac', 'monster-settings-firefly', 'ff-endgame-tabs',
'ff-endgame-status', 'ff-endgame-view', 'ff-endgame-reset',
'ff-endgame-apply', 'ff-endgame-apply-sync'
]) {
assert.ok(html.includes(`id="${id}"`), `index.html must contain #${id}`);
}