This commit is contained in:
1 parent
8eb519023a
commit
1593581729
18 files changed
+2757
-98
No files matched your search
+1
-1
@@ -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
@@ -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,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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -1,4 +1,4 @@
|
||||
import { getCurrentLanguage } from './i18n.js?v=2.23';
|
||||
import { getCurrentLanguage } from './i18n.js?v=2.26';
|
||||
|
||||
const strings = {
|
||||
en: {
|
||||
|
||||
Reference in new issue
Block a user