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

+817 -40
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1,4 +1,4 @@
import { getCurrentLanguage, translateElement, translatePath } from "./i18n.js?v=2.23";
import { getCurrentLanguage, translateElement, translatePath } from "./i18n.js?v=2.26";
export const AVATAR_METADATA = {
"1001": {
+305
View File
@@ -0,0 +1,305 @@
// FireflyGo endgame (Monster Setting) helpers.
//
// The catalog is a compact extract of the Firefly SrTools client tables:
// modes.<moc|pf|as>.groups[] -> levels[] (floors) -> sides[]
// modes.peak.groups[] -> stages[]
// monsters[id] -> { name, rank, stance, weak, icon }
//
// Every helper here is pure so it can be unit tested with node:test.
export const FIREFLY_ENDGAME_CATALOG_URL = './assets/firefly-endgame.json';
export const ENDGAME_MODES = ['moc', 'pf', 'as', 'peak', 'custom'];
export const ENDGAME_BATTLE_TYPES = {
moc: 'MOC',
pf: 'PF',
as: 'AS',
peak: 'PEAK',
custom: 'CE'
};
export function battleTypeForMode(mode) {
return ENDGAME_BATTLE_TYPES[mode] || 'DEFAULT';
}
function cloneJson(value) {
return JSON.parse(JSON.stringify(value));
}
function num(value, fallback = 0) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
}
export function groupList(catalog, mode) {
return Array.isArray(catalog?.modes?.[mode]?.groups) ? catalog.modes[mode].groups : [];
}
export function findGroup(catalog, mode, groupId) {
const groups = groupList(catalog, mode);
return groups.find((group) => Number(group.id) === Number(groupId)) || null;
}
export function findLevel(group, levelId) {
const levels = Array.isArray(group?.levels) ? group.levels : [];
return levels.find((level) => Number(level.id) === Number(levelId)) || null;
}
function newestGroup(catalog, mode) {
const groups = groupList(catalog, mode);
return groups.slice().sort((a, b) => Number(b.id) - Number(a.id))[0] || null;
}
function highestLevel(group) {
const levels = Array.isArray(group?.levels) ? group.levels : [];
return levels.slice().sort((a, b) => Number(b.floor) - Number(a.floor))[0] || null;
}
/**
* Picks the sensible default for a mode: newest season, highest floor and the
* first side. Peak defaults to the boss stage because that is the room players
* usually want to test.
*/
export function defaultEndgameSelection(catalog, mode) {
if (mode === 'custom') {
return {
mode,
customWaves: [[{ monster_id: 0, level: 80 }]],
customCycle: 0
};
}
const group = newestGroup(catalog, mode);
if (!group) {
return { mode, groupId: null, levelId: null, stageIndex: 0, side: 'firstNode', useCycle: true, useBuff: true };
}
if (mode === 'peak') {
const stages = Array.isArray(group.stages) ? group.stages : [];
const bossIndex = stages.findIndex((stage) => stage.kind === 'boss');
return {
mode,
groupId: Number(group.id),
stageIndex: bossIndex >= 0 ? bossIndex : Math.max(0, stages.length - 1),
useCycle: true,
useBuff: true
};
}
const level = highestLevel(group);
const sides = Array.isArray(level?.sides) ? level.sides : [];
const side = sides.some((entry) => entry.side === 'firstNode') ? 'firstNode' : (sides[0]?.side || 'firstNode');
return {
mode,
groupId: Number(group.id),
levelId: level ? Number(level.id) : null,
side,
useCycle: true,
useBuff: true
};
}
/**
* Resolves a selection against the catalog. Returns null when the catalog has
* no group/stage for the requested mode.
*/
export function resolveEndgameSelection(catalog, selection) {
const mode = selection?.mode;
if (!ENDGAME_MODES.includes(mode)) return null;
if (mode === 'custom') return { mode: 'custom', custom: true };
const groups = groupList(catalog, mode);
if (!groups.length) return null;
const group = findGroup(catalog, mode, selection.groupId) || newestGroup(catalog, mode);
if (!group) return null;
if (mode === 'peak') {
const stages = Array.isArray(group.stages) ? group.stages : [];
if (!stages.length) return null;
const rawIndex = num(selection.stageIndex, 0);
const index = Math.min(Math.max(rawIndex, 0), stages.length - 1);
return { mode, group, stage: stages[index], stageIndex: index };
}
const level = findLevel(group, selection.levelId) || highestLevel(group);
if (!level) return null;
const sides = Array.isArray(level.sides) ? level.sides : [];
if (!sides.length) return null;
const stage = sides.find((entry) => entry.side === selection.side) || sides[0];
return { mode, group, level, sides, stage };
}
export function monsterInfo(catalog, monsterId) {
return catalog?.monsters?.[String(monsterId)] || null;
}
/**
* Rebuilds a selection from an existing battle_config.stage_id so the picker
* opens on the room the save file is already using. Peak seasons are looked up
* first in the group referenced by extra_data.challenge when given.
*/
export function findSelectionByStageId(catalog, mode, stageId, groupId = 0) {
if (!ENDGAME_MODES.includes(mode) || mode === 'custom') return null;
const groups = groupList(catalog, mode);
if (!groups.length) return null;
const preferred = Number(groupId) > 0 ? findGroup(catalog, mode, groupId) : null;
const ordered = preferred ? [preferred, ...groups.filter((group) => group !== preferred)] : groups;
const target = num(stageId);
for (const group of ordered) {
if (mode === 'peak') {
const stages = Array.isArray(group.stages) ? group.stages : [];
const index = stages.findIndex((stage) => Number(stage.stageId) === target);
if (index >= 0) {
return { mode, groupId: Number(group.id), stageIndex: index, useCycle: true, useBuff: true };
}
continue;
}
const levels = Array.isArray(group.levels) ? group.levels : [];
for (const level of levels) {
const sides = Array.isArray(level.sides) ? level.sides : [];
const side = sides.find((entry) => Number(entry.stageId) === target);
if (side) {
return {
mode,
groupId: Number(group.id),
levelId: Number(level.id),
side: side.side,
useCycle: true,
useBuff: true
};
}
}
}
return null;
}
function normalizeCustomWaves(waves) {
if (!Array.isArray(waves)) return [];
return waves
.map((wave) => (Array.isArray(wave) ? wave : [])
.map((entry) => {
const id = num(entry?.monster_id ?? entry?.id ?? entry);
if (!id) return null;
return {
monster_id: id,
level: num(entry?.level, 80) || 80,
amount: num(entry?.amount, 1) || 1
};
})
.filter(Boolean))
.filter((wave) => wave.length > 0);
}
/**
* Builds the exact battle_config FireflyGo validates:
* blessings: [{ id, level }]
* monsters: [[{ monster_id, level, amount }]]
*/
export function buildFireflyBattleConfig(catalog, selection) {
const resolved = resolveEndgameSelection(catalog, selection);
if (!resolved) return null;
const type = battleTypeForMode(selection.mode);
if (resolved.custom) {
const waves = normalizeCustomWaves(selection.customWaves);
if (!waves.length) return null;
return {
battle_type: type,
blessings: [],
custom_stats: [],
cycle_count: num(selection.customCycle),
stage_id: 0,
path_resonance_id: 0,
monsters: waves
};
}
const { stage } = resolved;
const waves = (Array.isArray(stage.waves) ? stage.waves : [])
.map((wave) => wave
.map((id) => Number(id))
.filter((id) => id > 0)
.map((id) => ({ monster_id: id, level: Number(stage.level) || 1, amount: 1 })))
.filter((wave) => wave.length > 0);
const blessings = [];
if (selection.useBuff !== false) {
const buffs = selection.mode === 'peak' ? stage.buffs : resolved.level?.buffs;
for (const buff of Array.isArray(buffs) ? buffs : []) {
const id = num(buff?.id);
if (id > 0) blessings.push({ id, level: 1 });
}
}
if (stage.invasionBuffId) {
blessings.push({ id: Number(stage.invasionBuffId), level: 1 });
}
return {
battle_type: type,
blessings,
custom_stats: [],
cycle_count: selection.useCycle ? (num(stage.turnLimit) || num(resolved.level?.turnLimit)) : 0,
stage_id: num(stage.stageId),
path_resonance_id: 0,
monsters: waves
};
}
/**
* Anomaly Arbitration is selected through extra_data.challenge, so the chosen
* Peak season has to travel next to battle_config.
*/
export function buildFireflyChallenge(selection, current) {
const challenge = current && typeof current === 'object' ? cloneJson(current) : {};
challenge.skip_node = num(challenge.skip_node);
challenge.challenge_peak_group_id = num(challenge.challenge_peak_group_id);
challenge.challenge_peak_group_id_list = Array.isArray(challenge.challenge_peak_group_id_list)
? challenge.challenge_peak_group_id_list.map((id) => num(id)).filter((id) => id > 0)
: [];
if (selection?.mode === 'peak') {
const groupId = num(selection.groupId);
if (groupId > 0) {
challenge.challenge_peak_group_id = groupId;
challenge.challenge_peak_group_id_list = [groupId];
}
}
return challenge;
}
export function summarizeEndgameSelection(catalog, selection) {
const resolved = resolveEndgameSelection(catalog, selection);
if (!resolved) return null;
if (resolved.custom) {
const waves = normalizeCustomWaves(selection.customWaves);
return {
mode: 'custom',
battleType: battleTypeForMode('custom'),
stageId: 0,
waveCount: waves.length,
monsterCount: waves.reduce((sum, wave) => sum + wave.length, 0),
blessings: 0,
cycleCount: num(selection.customCycle)
};
}
const { stage } = resolved;
const waves = Array.isArray(stage.waves) ? stage.waves : [];
const buffs = (selection.useBuff !== false ? (selection.mode === 'peak' ? stage.buffs : resolved.level?.buffs) : []) || [];
return {
mode: selection.mode,
battleType: battleTypeForMode(selection.mode),
stageId: num(stage.stageId),
stageLevel: num(stage.level),
waveCount: waves.length,
monsterCount: waves.reduce((sum, wave) => sum + (wave?.length || 0), 0),
blessings: (Array.isArray(buffs) ? buffs.length : 0) + (stage.invasionBuffId ? 1 : 0),
cycleCount: selection.useCycle ? (num(stage.turnLimit) || num(resolved.level?.turnLimit)) : 0,
groupId: num(resolved.group?.id),
groupName: resolved.group?.name || null,
floor: resolved.level ? num(resolved.level.floor) : 0,
side: stage.side || ''
};
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { getCurrentLanguage, translatePath } from "./i18n.js?v=2.23";
import { getCurrentLanguage, translatePath } from "./i18n.js?v=2.26";
export const LIGHTCONE_METADATA = {
"20000": {
+49 -2
View File
@@ -148,16 +148,63 @@ function normalizeAnchorMap(skillsByAnchorType, skills) {
return Object.keys(derived).length > 0 ? derived : undefined;
}
// FireflyGo validates `blessings: [{ id, level, dynamic_key? }]` and rejects
// the raw id arrays some FreeSR exports still contain.
function normalizeBlessings(value) {
if (!Array.isArray(value)) return [];
return value
.map((entry) => {
const raw = entry && typeof entry === 'object' ? entry : { id: entry };
const id = num(raw.id ?? raw.ID);
if (id <= 0) return null;
const blessing = { id, level: Math.max(1, num(raw.level, 1) || 1) };
const dynamicKey = raw.dynamic_key;
if (dynamicKey && typeof dynamicKey === 'object' && dynamicKey.key != null && dynamicKey.key !== '') {
blessing.dynamic_key = {
key: typeof dynamicKey.key === 'number' ? num(dynamicKey.key) : String(dynamicKey.key),
value: num(dynamicKey.value)
};
}
return blessing;
})
.filter(Boolean);
}
// `monsters` is a list of waves; every wave is a list of
// `{ monster_id, level, amount }` objects.
function normalizeMonsterWaves(value) {
if (!Array.isArray(value)) return [];
return value
.map((wave) => {
const entries = Array.isArray(wave)
? wave
: Object.values(wave && typeof wave === 'object' ? wave : {});
return entries
.map((entry) => {
const raw = entry && typeof entry === 'object' ? entry : { monster_id: entry };
const monsterId = num(raw.monster_id ?? raw.id);
if (monsterId <= 0) return null;
return {
monster_id: monsterId,
level: Math.max(1, num(raw.level, 1) || 1),
amount: Math.max(1, num(raw.amount, 1) || 1)
};
})
.filter(Boolean);
})
.filter((wave) => wave.length > 0);
}
function normalizeBattleConfig(battleConfig) {
const source = battleConfig && typeof battleConfig === 'object' ? battleConfig : {};
return {
battle_type: typeof source.battle_type === 'string' && source.battle_type ? source.battle_type : 'DEFAULT',
blessings: Array.isArray(source.blessings) ? source.blessings : [],
blessings: normalizeBlessings(source.blessings),
custom_stats: Array.isArray(source.custom_stats) ? source.custom_stats : [],
cycle_count: num(source.cycle_count),
stage_id: num(source.stage_id),
path_resonance_id: num(source.path_resonance_id),
monsters: Array.isArray(source.monsters) ? source.monsters : []
monsters: normalizeMonsterWaves(source.monsters)
};
}
+105 -4
View File
@@ -7099,7 +7099,7 @@ const MODAL_AND_TOOL_TRANSLATIONS = {
fireflyChallengeGroupId: 'Peak group id',
fireflyChallengeGroupList: 'รายการ Peak group id',
fireflyTheoryTitle: '🧪 Theory Craft',
fireflyTheoryMode: 'เปิดโหมด Theory Craft',
fireflyTheoryMode: 'เปิดโหมด Theory Craft (= โหมดสร้างสรรค์)',
fireflyTheoryCycleCount: 'จำนวนรอบ',
fireflyTheoryStageId: 'Stage id',
fireflyTheoryLineup: 'ทีมที่กำหนดเอง',
@@ -7118,6 +7118,8 @@ const MODAL_AND_TOOL_TRANSLATIONS = {
fireflyRequestFailedDetail: 'ไม่สำเร็จ: {message}',
fireflyTimeout: 'FireflyGo ไม่ตอบสนอง (หมดเวลา)',
fireflyNoData: 'ยังไม่มีข้อมูลตัวละครให้ซิงค์',
fireflyPlayerNotLoaded: 'ข้อมูลผู้เล่นยังไม่ได้โหลด กรุณาล็อกอินตัวละครเข้าเกมก่อน',
fireflyMixedContentHint: 'เบราว์เซอร์บล็อกการเชื่อมต่อ HTTP จากเว็บ HTTPS กรุณาเปิดเว็บผ่าน localhost หรืออนุญาต Insecure content ในการตั้งค่าไซต์',
fireflyExtraSaved: 'บันทึกตัวควบคุม FireflyGo แล้ว',
fireflyHpInvalid: 'ค่า HP ต้องเป็น JSON object เช่น {"1001": [12000, 9000]}',
@@ -7253,7 +7255,55 @@ const MODAL_AND_TOOL_TRANSLATIONS = {
relicSetSearchLabel: 'ค้นหาชุดรีลิกส์',
relicSetSearchPlaceholder: '🔍 ค้นหาชุดรีลิกส์...',
relicSetGridLabel: 'ชุดรีลิกส์',
analystDamageAvPlaceholder: "เฉลี่ยต่อรอบ: -", analystCyclesPlaceholder: "0 รอบ", analystTurnsPlaceholder: "0 เทิร์น", analystAutoTurnsPlaceholder: "เทิร์นอัตโนมัติ: 0", analystDamageAvPlaceholder: "Avg per cycle: -", analystCyclesPlaceholder: "0 Cycles", analystTurnsPlaceholder: "0 Turns", analystAutoTurnsPlaceholder: "Auto Turns: 0", analystDamageAvPlaceholder: "ラウンド平均: -", analystCyclesPlaceholder: "0 ラウンド", analystTurnsPlaceholder: "0 ターン", analystAutoTurnsPlaceholder: "オートターン: 0", analystDamageAvPlaceholder: "라운드당 평균: -", analystCyclesPlaceholder: "0 라운드", analystTurnsPlaceholder: "0 턴", analystAutoTurnsPlaceholder: "자동 턴: 0", analystDamageAvPlaceholder: "Trung bình mỗi vòng: -", analystCyclesPlaceholder: "0 vòng", analystTurnsPlaceholder: "0 lượt", analystAutoTurnsPlaceholder: "Lượt tự động: 0", analystDamageAvPlaceholder: "Rata-rata per siklus: -", analystCyclesPlaceholder: "0 Siklus", analystTurnsPlaceholder: "0 Giliran", analystAutoTurnsPlaceholder: "Giliran otomatis: 0", analystDamageAvPlaceholder: "Promedio por ciclo: -", analystCyclesPlaceholder: "0 ciclos", analystTurnsPlaceholder: "0 turnos", analystAutoTurnsPlaceholder: "Turnos automáticos: 0", analystDamageAvPlaceholder: "Moyenne par cycle : -", analystCyclesPlaceholder: "0 cycle", analystTurnsPlaceholder: "0 tour", analystAutoTurnsPlaceholder: "Tours auto : 0", analystDamageAvPlaceholder: "Durchschnitt pro Zyklus: -", analystCyclesPlaceholder: "0 Zyklen", analystTurnsPlaceholder: "0 Züge", analystAutoTurnsPlaceholder: "Auto-Züge: 0", analystDamageAvPlaceholder: "В среднем за цикл: -", analystCyclesPlaceholder: "0 циклов", analystTurnsPlaceholder: "0 ходов", analystAutoTurnsPlaceholder: "Авто-ходы: 0", analystDamageAvPlaceholder: "Média por ciclo: -", analystCyclesPlaceholder: "0 ciclos", analystTurnsPlaceholder: "0 turnos", analystAutoTurnsPlaceholder: "Turnos automáticos: 0", miniViewFullAnalystTooltip: "เปิดหน้า Damage Analyst ตัวเต็ม", miniViewFullAnalystTooltip: "Open full Damage Analyst", miniViewFullAnalystTooltip: "完全なダメージ分析を開く", miniViewFullAnalystTooltip: "전체 데미지 분석 열기", miniViewFullAnalystTooltip: "Mở Trình phân tích sát thương đầy đủ", miniViewFullAnalystTooltip: "Buka Analis Kerusakan Lengkap", miniViewFullAnalystTooltip: "Abrir analizador de daño completo", miniViewFullAnalystTooltip: "Ouvrir l'analyseur de dégâts complet", miniViewFullAnalystTooltip: "Vollständigen Schadensanalysten öffnen", miniViewFullAnalystTooltip: "Открыть полный анализ урона", miniViewFullAnalystTooltip: "Abrir analisador de dano completo", copyCardGenerated: 'สร้างโดย GaCy-Srtool'
analystDamageAvPlaceholder: "เฉลี่ยต่อรอบ: -", analystCyclesPlaceholder: "0 รอบ", analystTurnsPlaceholder: "0 เทิร์น", analystAutoTurnsPlaceholder: "เทิร์นอัตโนมัติ: 0", analystDamageAvPlaceholder: "Avg per cycle: -", analystCyclesPlaceholder: "0 Cycles", analystTurnsPlaceholder: "0 Turns", analystAutoTurnsPlaceholder: "Auto Turns: 0", analystDamageAvPlaceholder: "ラウンド平均: -", analystCyclesPlaceholder: "0 ラウンド", analystTurnsPlaceholder: "0 ターン", analystAutoTurnsPlaceholder: "オートターン: 0", analystDamageAvPlaceholder: "라운드당 평균: -", analystCyclesPlaceholder: "0 라운드", analystTurnsPlaceholder: "0 턴", analystAutoTurnsPlaceholder: "자동 턴: 0", analystDamageAvPlaceholder: "Trung bình mỗi vòng: -", analystCyclesPlaceholder: "0 vòng", analystTurnsPlaceholder: "0 lượt", analystAutoTurnsPlaceholder: "Lượt tự động: 0", analystDamageAvPlaceholder: "Rata-rata per siklus: -", analystCyclesPlaceholder: "0 Siklus", analystTurnsPlaceholder: "0 Giliran", analystAutoTurnsPlaceholder: "Giliran otomatis: 0", analystDamageAvPlaceholder: "Promedio por ciclo: -", analystCyclesPlaceholder: "0 ciclos", analystTurnsPlaceholder: "0 turnos", analystAutoTurnsPlaceholder: "Turnos automáticos: 0", analystDamageAvPlaceholder: "Moyenne par cycle : -", analystCyclesPlaceholder: "0 cycle", analystTurnsPlaceholder: "0 tour", analystAutoTurnsPlaceholder: "Tours auto : 0", analystDamageAvPlaceholder: "Durchschnitt pro Zyklus: -", analystCyclesPlaceholder: "0 Zyklen", analystTurnsPlaceholder: "0 Züge", analystAutoTurnsPlaceholder: "Auto-Züge: 0", analystDamageAvPlaceholder: "В среднем за цикл: -", analystCyclesPlaceholder: "0 циклов", analystTurnsPlaceholder: "0 ходов", analystAutoTurnsPlaceholder: "Авто-ходы: 0", analystDamageAvPlaceholder: "Média por ciclo: -", analystCyclesPlaceholder: "0 ciclos", analystTurnsPlaceholder: "0 turnos", analystAutoTurnsPlaceholder: "Turnos automáticos: 0", miniViewFullAnalystTooltip: "เปิดหน้า Damage Analyst ตัวเต็ม", miniViewFullAnalystTooltip: "Open full Damage Analyst", miniViewFullAnalystTooltip: "完全なダメージ分析を開く", miniViewFullAnalystTooltip: "전체 데미지 분석 열기", miniViewFullAnalystTooltip: "Mở Trình phân tích sát thương đầy đủ", miniViewFullAnalystTooltip: "Buka Analis Kerusakan Lengkap", miniViewFullAnalystTooltip: "Abrir analizador de daño completo", miniViewFullAnalystTooltip: "Ouvrir l'analyseur de dégâts complet", miniViewFullAnalystTooltip: "Vollständigen Schadensanalysten öffnen", miniViewFullAnalystTooltip: "Открыть полный анализ урона", miniViewFullAnalystTooltip: "Abrir analisador de dano completo", copyCardGenerated: 'สร้างโดย GaCy-Srtool',
// FireflyGo endgame room picker
ffEndgameTitle: '🦋 ห้อง Endgame (FireflyGO)',
ffEndgameTablistLabel: 'โหมดด่าน endgame ของ FireflyGO',
ffEndgameDescription: 'เลือกด่าน endgame และคลื่นมอนที่ FireflyGO จะใช้ แล้วเขียนลง battle_config โดยตรง',
ffEndgameRule: 'ค่าเหล่านี้เขียนลง battle_config (และ extra_data.challenge สำหรับ Anomaly Arbitration) จากนั้นบันทึกในเครื่องหรือซิงค์ขึ้น FireflyGO',
ffEndgameModePeak: 'Anomaly Arbitration',
ffEndgameLoading: 'กำลังโหลดข้อมูลด่าน endgame…',
ffEndgameReady: 'พร้อมใช้งาน · เลือกห้องแล้วกดใช้กับ battle_config',
ffEndgameCatalogError: 'โหลดข้อมูลด่าน endgame ไม่ได้: {error}',
ffEndgameSeason: 'ซีซัน / กลุ่มด่าน',
ffEndgameFloor: 'ชั้น',
ffEndgameFloorValue: 'ชั้น {floor}',
ffEndgameSide: 'ฝั่ง',
ffEndgameSideFirst: 'ครึ่งแรก',
ffEndgameSideSecond: 'ครึ่งหลัง',
ffEndgameSideThird: 'โหนดที่สาม',
ffEndgameStage: 'ด่าน',
ffEndgameStageValue: 'ด่าน {id}',
ffEndgameStageKindPre: 'ด่านก่อนบอส',
ffEndgameStageKindBoss: 'บอส',
ffEndgameStageKindHard: 'โหมดยาก',
ffEndgameStageId: 'stage_id {id}',
ffEndgamePreview: 'ตัวอย่าง battle_config',
ffEndgameNoStages: 'ไม่พบข้อมูลด่านสำหรับโหมดนี้',
ffEndgameUseCycle: 'ใช้จำนวนรอบจากด่าน (TurnLimit)',
ffEndgameUseBuff: 'ใช้บัฟของด่าน',
ffEndgameCycles: 'รอบ: {count}',
ffEndgameBlessings: 'บัฟ: {count}',
ffEndgameMonsterCount: 'มอน: {count}',
ffEndgameApply: '💾 ใช้กับ battle_config',
ffEndgameApplySync: '🚀 ใช้และซิงค์',
ffEndgameReset: '↺ รีเซ็ตค่าเริ่มต้น',
ffEndgameResetDone: 'รีเซ็ตค่าเริ่มต้นของโหมดนี้แล้ว',
ffEndgameSaving: 'กำลังบันทึก battle_config…',
ffEndgameSaved: 'บันทึก battle_config ในเครื่องแล้ว',
ffEndgameSynced: 'บันทึกและซิงค์ขึ้น FireflyGO แล้ว',
ffEndgameApplyError: 'เลือกห้องให้ครบก่อนใช้ค่านี้',
ffEndgameNoData: 'ยังไม่มีข้อมูล FreeSR — โหลดหรือซิงค์ข้อมูลก่อน',
ffEndgameSyncError: 'ซิงค์ไม่สำเร็จ: {error}',
ffEndgameCustomDescription: 'กำหนดคลื่นมอนเองโดยใช้ monster_id จากเกม ค่าที่ใช้ได้ต้องตรงกับที่ FireflyGO รองรับ',
ffEndgameAddWave: 'เพิ่มคลื่น',
ffEndgameRemoveWave: 'ลบคลื่น',
ffEndgameAddMonster: 'เพิ่มมอน',
ffEndgameRemoveMonster: 'ลบมอน',
ffEndgameMonsterId: 'Monster ID',
ffEndgameMonsterLevel: 'เลเวล',
ffEndgameMonsterAmount: 'จำนวน',
ffEndgameCycleCount: 'จำนวนรอบ'
},
en: {
// Server modes
@@ -7306,7 +7356,7 @@ const MODAL_AND_TOOL_TRANSLATIONS = {
fireflyChallengeGroupId: 'Peak group id',
fireflyChallengeGroupList: 'Peak group id list',
fireflyTheoryTitle: '🧪 Theory Craft',
fireflyTheoryMode: 'Enable theory craft',
fireflyTheoryMode: 'Enable Theory Craft (= Creative Mode)',
fireflyTheoryCycleCount: 'Cycle count',
fireflyTheoryStageId: 'Stage id',
fireflyTheoryLineup: 'Custom lineup',
@@ -7325,6 +7375,8 @@ const MODAL_AND_TOOL_TRANSLATIONS = {
fireflyRequestFailedDetail: 'Failed: {message}',
fireflyTimeout: 'FireflyGo did not respond (timed out)',
fireflyNoData: 'There is no character data to sync yet',
fireflyPlayerNotLoaded: 'Player data is not loaded yet. Please log into the game with your character first.',
fireflyMixedContentHint: 'Browser blocked HTTP localhost from HTTPS page. Please open via localhost or allow Insecure content in site settings.',
fireflyExtraSaved: 'FireflyGo controls saved',
fireflyHpInvalid: 'HP overrides must be a JSON object such as {"1001": [12000, 9000]}',
@@ -7460,7 +7512,56 @@ const MODAL_AND_TOOL_TRANSLATIONS = {
relicSetSearchLabel: 'Search relic sets',
relicSetSearchPlaceholder: '🔍 Search relic sets...',
relicSetGridLabel: 'Relic sets',
copyCardGenerated: 'Generated via GaCy-Srtool'
copyCardGenerated: 'Generated via GaCy-Srtool',
// FireflyGo endgame room picker
ffEndgameTitle: '🦋 Endgame Rooms (FireflyGO)',
ffEndgameTablistLabel: 'FireflyGO endgame room modes',
ffEndgameDescription: 'Pick the endgame room and monster waves FireflyGO should load, then write them straight into battle_config.',
ffEndgameRule: 'These values are written to battle_config (plus extra_data.challenge for Anomaly Arbitration), then saved locally or synced to FireflyGO.',
ffEndgameModePeak: 'Anomaly Arbitration',
ffEndgameLoading: 'Loading endgame data…',
ffEndgameReady: 'Ready · pick a room, then apply it to battle_config',
ffEndgameCatalogError: 'Could not load endgame data: {error}',
ffEndgameSeason: 'Season / stage group',
ffEndgameFloor: 'Floor',
ffEndgameFloorValue: 'Floor {floor}',
ffEndgameSide: 'Side',
ffEndgameSideFirst: 'First half',
ffEndgameSideSecond: 'Second half',
ffEndgameSideThird: 'Third node',
ffEndgameStage: 'Stage',
ffEndgameStageValue: 'Stage {id}',
ffEndgameStageKindPre: 'Pre-boss',
ffEndgameStageKindBoss: 'Boss',
ffEndgameStageKindHard: 'Hard mode',
ffEndgameStageId: 'stage_id {id}',
ffEndgamePreview: 'battle_config preview',
ffEndgameNoStages: 'No stage data for this mode',
ffEndgameUseCycle: 'Use the stage turn limit',
ffEndgameUseBuff: 'Use the stage buffs',
ffEndgameCycles: 'Cycles: {count}',
ffEndgameBlessings: 'Buffs: {count}',
ffEndgameMonsterCount: 'Monsters: {count}',
ffEndgameApply: '💾 Apply to battle_config',
ffEndgameApplySync: '🚀 Apply & sync',
ffEndgameReset: '↺ Reset to defaults',
ffEndgameResetDone: 'This mode was reset to its defaults',
ffEndgameSaving: 'Saving battle_config…',
ffEndgameSaved: 'battle_config saved locally',
ffEndgameSynced: 'Saved and synced to FireflyGO',
ffEndgameApplyError: 'Finish selecting a room before applying',
ffEndgameNoData: 'No FreeSR data yet — load or sync a save first',
ffEndgameSyncError: 'Sync failed: {error}',
ffEndgameCustomDescription: 'Build custom monster waves with the in-game monster_id. Values must match what FireflyGO accepts.',
ffEndgameAddWave: 'Add wave',
ffEndgameRemoveWave: 'Remove wave',
ffEndgameAddMonster: 'Add monster',
ffEndgameRemoveMonster: 'Remove monster',
ffEndgameMonsterId: 'Monster ID',
ffEndgameMonsterLevel: 'Level',
ffEndgameMonsterAmount: 'Amount',
ffEndgameCycleCount: 'Cycle count'
}
};
for (const [language, additions] of Object.entries(MODAL_AND_TOOL_TRANSLATIONS)) {
+1 -1
View File
@@ -1,4 +1,4 @@
import { getCurrentLanguage, t, translatePath } from './i18n.js?v=2.23';
import { getCurrentLanguage, t, translatePath } from './i18n.js?v=2.26';
const CATALOG_URL = new URL('../assets/item-catalog.json?v=2', import.meta.url);
const PAGE_SIZE = 48;
+5
View File
@@ -20,6 +20,9 @@ export const MODE_CAPABILITIES = {
exranks: true,
inventory: true,
monsterSettings: true,
// FireflyGo ships its own endgame room picker; GaCy GO reads the Client
// catalog from /api/monster-settings instead.
fireflyEndgame: false,
damageAnalyst: true,
sceneCard: true,
gmConsole: true,
@@ -36,6 +39,7 @@ export const MODE_CAPABILITIES = {
exranks: false,
inventory: false,
monsterSettings: false,
fireflyEndgame: true,
damageAnalyst: false,
sceneCard: false,
gmConsole: false,
@@ -50,6 +54,7 @@ export const MODE_CAPABILITIES = {
exranks: false,
inventory: false,
monsterSettings: false,
fireflyEndgame: false,
damageAnalyst: false,
sceneCard: false,
gmConsole: false,
+1 -1
View File
@@ -1,4 +1,4 @@
import { getCurrentLanguage } from './i18n.js?v=2.23';
import { getCurrentLanguage } from './i18n.js?v=2.26';
const strings = {
en: {