UPDATE: Update readme and Next 16.07 (CVE-2025-66478)

This commit is contained in:
2025-12-04 23:27:41 +07:00
commit 6b079db470
280 changed files with 364214 additions and 0 deletions

100
src/helper/calcData.ts Normal file
View File

@@ -0,0 +1,100 @@
import { mappingStats, ratioStats } from "@/constant/constant"
import { AffixDetail } from "@/types"
export function calcPromotion(level: number) {
if (level < 20) {
return 0
}
if (level < 30) {
return 1
}
if (level < 40) {
return 2
}
if (level < 50) {
return 3
}
if (level < 60) {
return 4
}
if (level < 70) {
return 5
}
return 6
}
export function calcRarity(rarity: string) {
if (rarity.includes("5")) {
return 5
}
if (rarity.includes("4")) {
return 4
}
if (rarity.includes("3")) {
return 3
}
return 1
}
export function calcMainAffixBonus(affix?: AffixDetail, level?: number) {
if (!affix || typeof level !== "number") return "0"
const value = affix.base + affix.step * level;
if (mappingStats?.[affix.property].unit === "%") {
return (value * 100).toFixed(1);
}
if (mappingStats?.[affix.property].name === "SPD") {
return value.toFixed(1);
}
return value.toFixed(0);
}
export const calcAffixBonus = (affix?: AffixDetail, stepCount?: number, rollCount?: number) => {
if (!affix || typeof stepCount !== "number" || typeof rollCount !== "number") return "0"
if (mappingStats?.[affix.property].unit === "%") {
return ((affix.base * rollCount + affix.step * stepCount) * 100).toFixed(1);
}
if (mappingStats?.[affix.property].name === "SPD") {
return (affix.base * rollCount + affix.step * stepCount).toFixed(1);
}
return (affix.base * rollCount + affix.step * stepCount).toFixed(0);
}
export const calcBaseStat = (baseStat: number, stepStat: number, roundFixed: number, level: number) => {
const promotionStat = baseStat + stepStat * (level-1);
return promotionStat.toFixed(roundFixed);
}
export const calcBaseStatRaw = (baseStat?: number, stepStat?: number, level?: number) => {
if (typeof baseStat !== "number" || typeof stepStat !== "number" || typeof level !== "number") return 0
return baseStat + stepStat * (level-1);
}
export const calcSubAffixBonusRaw = (affix?: AffixDetail, stepCount?: number, rollCount?: number, baseStat?: number) => {
if (!affix || typeof stepCount !== "number" || typeof rollCount !== "number" || typeof baseStat !== "number") return 0
if (ratioStats.includes(affix.property)) {
return (affix.base * rollCount + affix.step * stepCount) * baseStat;
}
return affix.base * rollCount + affix.step * stepCount;
}
export const calcMainAffixBonusRaw = (affix?: AffixDetail, level?: number, baseStat?: number) => {
if (!affix || typeof level !== "number" || typeof baseStat !== "number") return 0
const value = affix.base + affix.step * level;
if (ratioStats.includes(affix.property)) {
return baseStat * value
}
return value
}
export const calcBonusStatRaw = (affix?: string, baseStat?: number, bonusValue?: number) => {
if (!affix || typeof baseStat !== "number" || typeof bonusValue !== "number") return 0
if (ratioStats.includes(affix)) {
return baseStat * bonusValue
}
return bonusValue
}

100
src/helper/connect.ts Normal file
View File

@@ -0,0 +1,100 @@
"use client"
import { SendDataThroughProxy, SendDataToServer } from "@/lib/api/api"
import useConnectStore from "@/stores/connectStore"
import useUserDataStore from "@/stores/userDataStore"
import { converterToFreeSRJson } from "./converterToFreeSRJson"
import { psResponseSchema } from "@/zod"
import useGlobalStore from "@/stores/globalStore"
export const connectToPS = async (): Promise<{ success: boolean, message: string }> => {
const {
connectionType,
privateType,
serverUrl,
username,
password
} = useConnectStore.getState()
const {setExtraData, setIsConnectPS} = useGlobalStore.getState()
let urlQuery = serverUrl
if (!urlQuery.startsWith("http://") && !urlQuery.startsWith("https://")) {
urlQuery = `http://${urlQuery}`
}
if (connectionType === "FireflyGo") {
urlQuery = "http://localhost:21000/sync"
} else if (connectionType === "Other" && privateType === "Server") {
const response = await SendDataThroughProxy({data: {username, password, serverUrl, data: null, method: "POST"}})
if (response instanceof Error) {
return { success: false, message: response.message }
} else if (response.error) {
return { success: false, message: response.error }
} else {
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return { success: false, message: "Invalid response schema" }
}
return { success: true, message: "" }
}
}
const response = await SendDataToServer(username, password, urlQuery, null)
if (typeof response === "string") {
setIsConnectPS(false)
return { success: false, message: response }
} else if (response.status != 200) {
setIsConnectPS(false)
return { success: false, message: response.message }
} else {
setIsConnectPS(true)
setExtraData(response?.extra_data)
return { success: true, message: "" }
}
}
export const syncDataToPS = async (): Promise<{ success: boolean, message: string }> => {
const {
connectionType,
privateType,
serverUrl,
username,
password
} = useConnectStore.getState()
const {extraData, setIsConnectPS, setExtraData} = useGlobalStore.getState()
const {avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config} = useUserDataStore.getState()
const data = converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config)
let urlQuery = serverUrl
if (!urlQuery.startsWith("http://") && !urlQuery.startsWith("https://")) {
urlQuery = `http://${urlQuery}`
}
if (connectionType === "FireflyGo") {
urlQuery = "http://localhost:21000/sync"
} else if (connectionType === "Other" && privateType === "Server") {
const response = await SendDataThroughProxy({data: {username, password, serverUrl, data, method: "POST"}})
if (response instanceof Error) {
return { success: false, message: response.message }
} else if (response.error) {
return { success: false, message: response.error }
} else {
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return { success: false, message: "Invalid response schema" }
}
return { success: true, message: "" }
}
}
const response = await SendDataToServer(username, password, urlQuery, data, extraData)
if (typeof response === "string") {
setIsConnectPS(false)
return { success: false, message: response }
} else if (response.status != 200) {
setIsConnectPS(false)
return { success: false, message: response.message }
} else {
setIsConnectPS(true)
setExtraData(response?.extra_data)
return { success: true, message: "" }
}
}

142
src/helper/convertData.ts Normal file
View File

@@ -0,0 +1,142 @@
import { CharacterBasic, CharacterBasicRaw, EventBasic, EventBasicRaw, LightConeBasic, LightConeBasicRaw, MonsterBasic, MonsterBasicRaw, RelicBasic, RelicBasicEffect, RelicBasicRaw } from "@/types";
export function convertRelicSet(id: string, item: RelicBasicRaw): RelicBasic {
const lang = new Map<string, string>([
['en', item.en],
['kr', item.kr],
['cn', item.cn],
['jp', item.jp]
]);
const setRelic = new Map<string, RelicBasicEffect>();
Object.entries(item.set).forEach(([key, value]) => {
setRelic.set(key, {
ParamList: value.ParamList,
lang: new Map<string, string>([
['en', value.en],
['kr', value.kr],
['cn', value.cn],
['jp', value.jp]
])
});
});
const result: RelicBasic = {
icon: item.icon,
lang: lang,
id: id,
set: setRelic
};
return result;
}
export function convertLightcone(id: string, item: LightConeBasicRaw): LightConeBasic {
const lang = new Map<string, string>([
['en', item.en],
['kr', item.kr],
['cn', item.cn],
['jp', item.jp]
]);
const result: LightConeBasic = {
rank: item.rank,
baseType: item.baseType,
desc: item.desc,
lang: lang,
id: id
};
return result;
}
export function convertAvatar(id: string, item: CharacterBasicRaw): CharacterBasic {
const lang = new Map<string, string>([
['en', item.en],
['kr', item.kr],
['cn', item.cn],
['jp', item.jp]
]);
let text = ""
if (Number(id) % 2 === 0 && Number(id) > 8000) {
text = `Female ${item.damageType} MC`
} else if (Number(id) > 8000) {
text = `Male ${item.damageType} MC`
}
if (text !== "") {
lang.set("en", text)
lang.set("kr", text)
lang.set("cn", text)
lang.set("jp", text)
}
const result: CharacterBasic = {
release: item.release,
icon: item.icon,
rank: item.rank,
baseType: item.baseType,
damageType: item.damageType,
desc: item.desc,
lang: lang,
id: id
};
return result;
}
export function convertEvent(id: string, item: EventBasicRaw): EventBasic {
const lang = new Map<string, string>([
['en', item.en],
['kr', item.kr],
['cn', item.cn],
['jp', item.jp]
]);
const result: EventBasic = {
lang: lang,
id: id,
begin: item.begin,
end: item.end,
live_begin: item.live_begin,
live_end: item.live_end,
param: item.param,
};
return result;
}
export function convertMonster(id: string, item: MonsterBasicRaw): MonsterBasic {
const lang = new Map<string, string>([
['en', item.en],
['kr', item.kr],
['cn', item.cn],
['jp', item.jp]
]);
const result: MonsterBasic = {
id: id,
rank: item.rank,
camp: item.camp,
icon: item.icon,
child: item.child,
weak: item.weak,
desc: item.desc,
lang: lang
};
return result;
}
export function convertToRoman(num: number): string {
const roman: [number, string][] = [
[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'],
[100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'],
[10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']
];
let result = '';
for (const [val, sym] of roman) {
while (num >= val) {
result += sym;
num -= val;
}
}
return result;
}

View File

@@ -0,0 +1,101 @@
import { AvatarEnkaDetail, AvatarProfileStore, AvatarStore, CharacterDetail, FreeSRJson, RelicStore } from "@/types";
function safeNumber(val: string | number | null, fallback = 0): number {
if (!val) return fallback;
const num = Number(val);
return Number.isFinite(num) && num !== 0 ? num : fallback;
}
export function converterToAvatarStore(data: Record<string, CharacterDetail>): { [key: string]: AvatarStore } {
return Object.fromEntries(
Object.entries(data).map(([key, value]) => [
key,
{
owner_uid: 0,
avatar_id: Number(key),
data: {
rank: 0,
skills: Object.values(value.SkillTrees).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>)
},
level: 80,
promotion: 6,
techniques: [],
sp_max: safeNumber(value.SPNeed, 100),
can_change_sp: safeNumber(value.SPNeed) !== 0,
sp_value: Math.ceil(safeNumber(value.SPNeed) / 2),
profileSelect: 0,
enhanced: "",
profileList: [{
profile_name: "Default",
lightcone: null,
relics: {} as Record<string, RelicStore>
} as AvatarProfileStore]
}
])
) as { [key: string]: AvatarStore }
}
export function converterOneEnkaDataToAvatarStore(data: AvatarEnkaDetail, count: number): AvatarProfileStore | null {
if (!data.equipment && (!data.relicList || data.relicList.length === 0)) return null
const profile: AvatarProfileStore = {
profile_name: `Enka Profile ${count}`,
lightcone: (data.equipment && data.equipment.tid) ? {
level: data.equipment?.level ?? 0,
item_id: data.equipment?.tid ?? 0,
rank: data.equipment?.rank ?? 0,
promotion: data.equipment?.promotion ?? 0,
} : null,
relics: Object.fromEntries(data.relicList.map((relic) => [relic.tid.toString()[relic.tid.toString().length - 1], {
level: relic.level ?? 0,
relic_id: relic.tid,
relic_set_id: parseInt(relic.tid.toString().slice(1, -1), 10),
main_affix_id: relic.mainAffixId,
sub_affixes: relic.subAffixList.map((subAffix) => ({
sub_affix_id: subAffix.affixId,
count: subAffix.cnt,
step: subAffix.step ?? 0
}))
}]))
}
return profile
}
export function converterOneFreeSRDataToAvatarStore(data: FreeSRJson, count: number , avatar_id: number): AvatarProfileStore | null {
const lightcone = data.lightcones.find((lightcone) => lightcone.equip_avatar === avatar_id)
const relics = data.relics.filter((relic) => relic.equip_avatar === avatar_id)
if (!lightcone && (!relics || relics.length === 0)) return null
const relicsMap = {} as Record<string, RelicStore>
relics.forEach((relic) => {
relicsMap[relic.relic_id.toString()[relic.relic_id.toString().length - 1]] = {
level: relic.level,
relic_id: relic.relic_id,
relic_set_id: relic.relic_set_id,
main_affix_id: relic.main_affix_id,
sub_affixes: relic.sub_affixes.map((subAffix) => ({
sub_affix_id: subAffix.sub_affix_id,
count: subAffix.count,
step: subAffix.step ?? 0
}))
}
})
const profile: AvatarProfileStore = {
profile_name: `FreeSR Profile ${count}`,
lightcone: (lightcone && lightcone.item_id) ? {
level: lightcone?.level ?? 0,
item_id: lightcone?.item_id ?? 0,
rank: lightcone?.rank ?? 0,
promotion: lightcone?.promotion ?? 0,
} : null,
relics: relicsMap
}
return profile
}

View File

@@ -0,0 +1,133 @@
import { ASConfigStore, AvatarJson, AvatarStore, BattleConfigJson, CEConfigStore, FreeSRJson, LightconeJson, MOCConfigStore, PEAKConfigStore, PFConfigStore, RelicJson } from "@/types";
export function converterToFreeSRJson(
avatars: Record<string, AvatarStore>,
battle_type: string,
moc_config: MOCConfigStore,
pf_config: PFConfigStore,
as_config: ASConfigStore,
ce_config: CEConfigStore,
peak_config: PEAKConfigStore,
): FreeSRJson {
const lightcones: LightconeJson[] = []
const relics: RelicJson[] = []
let battleJson: BattleConfigJson
if (battle_type === "MOC") {
battleJson = {
battle_type: battle_type,
blessings: moc_config.blessings,
custom_stats: [],
cycle_count: moc_config.cycle_count,
stage_id: moc_config.stage_id,
path_resonance_id: 0,
monsters: moc_config.monsters,
}
} else if (battle_type === "PF") {
battleJson = {
battle_type: battle_type,
blessings: pf_config.blessings,
custom_stats: [],
cycle_count: pf_config.cycle_count,
stage_id: pf_config.stage_id,
path_resonance_id: 0,
monsters: pf_config.monsters,
}
} else if (battle_type === "AS") {
battleJson = {
battle_type: battle_type,
blessings: as_config.blessings,
custom_stats: [],
cycle_count: as_config.cycle_count,
stage_id: as_config.stage_id,
path_resonance_id: 0,
monsters: as_config.monsters,
}
} else if (battle_type === "CE") {
battleJson = {
battle_type: battle_type,
blessings: ce_config.blessings,
custom_stats: [],
cycle_count: ce_config.cycle_count,
stage_id: ce_config.stage_id,
path_resonance_id: 0,
monsters: ce_config.monsters,
}
} else if (battle_type === "PEAK") {
battleJson = {
battle_type: battle_type,
blessings: peak_config.blessings,
custom_stats: [],
cycle_count: peak_config.cycle_count,
stage_id: peak_config.stage_id,
path_resonance_id: 0,
monsters: peak_config.monsters,
}
} else {
battleJson = {
battle_type: battle_type,
blessings: [],
custom_stats: [],
cycle_count: 0,
stage_id: 0,
path_resonance_id: 0,
monsters: [],
}
}
const avatarsJson: { [key: string]: AvatarJson } = {}
let internalUidLightcone = 0
let internalUidRelic = 0
Object.entries(avatars).forEach(([avatarId, avatar]) => {
avatarsJson[avatarId] = {
owner_uid: Number(avatar.owner_uid || 0),
avatar_id: Number(avatar.avatar_id || 0),
data: avatar.data,
level: Number(avatar.level || 0),
promotion: Number(avatar.promotion || 0),
techniques: avatar.techniques,
sp_value: Number(avatar.sp_value || 0),
sp_max: Number(avatar.sp_max || 0),
}
const currentProfile = avatar.profileList[avatar.profileSelect]
if (currentProfile.lightcone && currentProfile.lightcone.item_id !== 0) {
const newLightcone: LightconeJson = {
level: Number(currentProfile.lightcone.level || 0),
item_id: Number(currentProfile.lightcone.item_id || 0),
rank: Number(currentProfile.lightcone.rank || 0),
promotion: Number(currentProfile.lightcone.promotion || 0),
internal_uid: internalUidLightcone,
equip_avatar: Number(avatar.avatar_id || 0),
}
internalUidLightcone++
lightcones.push(newLightcone)
}
if (currentProfile.relics) {
["1", "2", "3", "4", "5", "6"].forEach(slot => {
const relic = currentProfile.relics[slot]
if (relic && relic.relic_id !== 0) {
const newRelic: RelicJson = {
level: Number(relic.level || 0),
relic_id: Number(relic.relic_id || 0),
relic_set_id: Number(relic.relic_set_id || 0),
main_affix_id: Number(relic.main_affix_id || 0),
sub_affixes: relic.sub_affixes,
internal_uid: internalUidRelic,
equip_avatar: Number(avatar.avatar_id || 0),
}
internalUidRelic++
relics.push(newRelic)
}
})
}
})
return {
lightcones,
relics,
avatars: avatarsJson,
battle_config: battleJson,
}
}

45
src/helper/getName.ts Normal file
View File

@@ -0,0 +1,45 @@
import { listCurrentLanguage } from "@/constant/constant";
import { CharacterBasic, EventBasic, LightConeBasic, MonsterBasic } from "@/types";
export function getNameChar(locale: string, data: CharacterBasic | undefined): string {
if (!data) {
return ""
}
if (!listCurrentLanguage.hasOwnProperty(locale)) {
return ""
}
let text = data.lang.get(listCurrentLanguage[locale as keyof typeof listCurrentLanguage].toLowerCase()) ?? "";
if (!text) {
text = data.lang.get("en") ?? "";
}
if (Number(data.id) % 2 === 0 && Number(data.id) > 8000) {
text = `Female ${data.damageType} MC`
} else if (Number(data.id) > 8000) {
text = `Male ${data.damageType} MC`
}
return text
}
export function getLocaleName(locale: string, data: LightConeBasic | EventBasic | MonsterBasic | undefined): string {
if (!data) {
return ""
}
if (!listCurrentLanguage.hasOwnProperty(locale)) {
return ""
}
let text = data.lang.get(listCurrentLanguage[locale as keyof typeof listCurrentLanguage].toLowerCase()) ?? "";
if (!text) {
text = data.lang.get("en") ?? "";
}
return text
}
export function parseRuby(text: string): string {
const rubyRegex = /\{RUBY_B#(.*?)\}(.*?)\{RUBY_E#\}/gs;
return text.replace(rubyRegex, (_match, furigana, kanji) => {
return `<ruby>${kanji}<rt>${furigana}</rt></ruby>`;
});
}

View File

@@ -0,0 +1,23 @@
import useAvatarStore from "@/stores/avatarStore";
export function getSkillTree(enhanced: string) {
const { avatarSelected, mapAvatarInfo } = useAvatarStore.getState()
if (!avatarSelected) return null;
if (enhanced != "") return Object.values(mapAvatarInfo[avatarSelected.id || ""]?.Enhanced[enhanced].SkillTrees || {}).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>)
return Object.values(mapAvatarInfo[avatarSelected.id || ""]?.SkillTrees).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>);
}

8
src/helper/index.ts Normal file
View File

@@ -0,0 +1,8 @@
export * from "./getName"
export * from "./replaceByParam"
export * from "./converterToAvatarStore"
export * from "./calcData"
export * from "./random"
export * from "./json"
export * from "./convertData"
export * from "./connect"

15
src/helper/json.ts Normal file
View File

@@ -0,0 +1,15 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function downloadJson(fileName: string, data: any) {
const json = JSON.stringify(data, null, 2)
const blob = new Blob([json], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `${fileName}.json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}

27
src/helper/random.ts Normal file
View File

@@ -0,0 +1,27 @@
export function randomPartition(sum: number, parts: number): number[] {
const raw = Array.from({ length: parts }, () => Math.random());
const total = raw.reduce((a, b) => a + b, 0);
const result = raw.map(r => Math.floor((r / total) * (sum - parts)) + 1);
let diff = sum - result.reduce((a, b) => a + b, 0);
while (diff !== 0) {
for (let i = 0; i < result.length && diff !== 0; i++) {
if (diff > 0) {
result[i]++;
diff--;
} else if (result[i] > 1) {
result[i]--;
diff++;
}
}
}
return result;
}
export function randomStep(x: number): number {
let total = 0;
for (let i = 0; i < x; i++) {
total += Math.floor(Math.random() * 3);
}
return total;
}

View File

@@ -0,0 +1,52 @@
export function replaceByParam(desc: string, params: number[]): string {
function formatParam(
indexStr: string,
format: string,
floatDigits: string | undefined,
percent: string | undefined
): string {
const i: number = parseInt(indexStr, 10) - 1;
const value: number | undefined = params[i];
if (value === undefined) return "";
if (format.startsWith("f")) {
const digits: number = parseInt(floatDigits || "1", 10);
const num: number = percent ? value * 100 : value;
return `${num.toFixed(digits)}${percent ? "%" : ""}`;
}
if (format === "i") {
return percent ? `${(value * 100).toFixed(0)}%` : `${Math.round(value)}`;
}
return `${value}`;
}
const desc1 = desc.replace(/<color=#[0-9a-fA-F]{8}>(.*?)<\/color>/g, (match: string, inner: string): string => {
const colorCode: string = match.match(/#[0-9a-fA-F]{8}/)?.[0] ?? "#ffffff";
const processed: string = inner
.replace(/#(\d+)\[(f(\d+)|i)\](%)?/g, (
_: string,
index: string,
format: string,
floatDigits: string | undefined,
percent: string | undefined
): string => formatParam(index, format, floatDigits, percent))
.replace(/<unbreak>(.*?)<\/unbreak>/g, "$1");
return `<span style="color:${colorCode}">${processed}</span>`;
});
const desc2 = desc1.replace(/<unbreak>#(\d+)\[(f(\d+)|i)\](%)?<\/unbreak>/g, (
_: string,
index: string,
format: string,
floatDigits: string | undefined,
percent: string | undefined
): string => formatParam(index, format, floatDigits, percent));
const desc3 = desc2.replace(/<unbreak>(\d+)<\/unbreak>/g, (_: string, number: string): string => number);
const desc4 = desc3.replaceAll("\\n", "<br></br>");
return desc4;
}