init
All checks were successful
Gitea Auto Deploy / Deploy-Container (push) Successful in 1m31s

This commit is contained in:
2025-07-01 09:33:43 +07:00
parent 3a26f09a01
commit 331aba4489
156 changed files with 1206219 additions and 146 deletions

32
src/lib/affixLoader.ts Normal file
View File

@@ -0,0 +1,32 @@
import { AffixDetail } from '@/types';
import fs from 'fs';
import path from 'path';
const DATA_DIR = path.join(process.cwd(), 'data');
let mainAffixCache: Record<string, Record<string, AffixDetail>> = {};
let subAffixCache: Record<string, Record<string, AffixDetail>> = {};
const mainAffixPath = path.join(DATA_DIR, `main_affixes.json`)
const subAffixPath = path.join(DATA_DIR, `sub_affixes.json`)
function loadFromFileIfExists(filePath: string): Record<string, Record<string, AffixDetail>> {
if (fs.existsSync(filePath)) {
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, Record<string, AffixDetail>>;
return data;
}
return {};
}
export async function loadMainAffix(): Promise<Record<string, Record<string, AffixDetail>>> {
if (Object.keys(mainAffixCache).length > 0) return mainAffixCache;
const fileData = loadFromFileIfExists(mainAffixPath);
mainAffixCache = fileData;
return fileData;
}
export async function loadSubAffix(): Promise<Record<string, Record<string, AffixDetail>>> {
if (Object.keys(subAffixCache).length > 0) return subAffixCache;
const fileData = loadFromFileIfExists(subAffixPath);
subAffixCache = fileData;
return fileData;
}

297
src/lib/api.ts Normal file
View File

@@ -0,0 +1,297 @@
import { CharacterBasic, CharacterBasicRaw } from "@/types/characterBasic";
import { AffixDetail, CharacterDetail, ConfigMaze, EnkaResponse, FreeSRJson, LightConeBasic, LightConeBasicRaw, LightConeDetail, PSResponse, RelicBasic, RelicBasicEffect, RelicBasicRaw, RelicDetail } from "@/types";
import axios from 'axios';
import { convertAvatar, convertLightcone, convertRelicSet } from "@/helper";
import { psResponseSchema } from "@/zod";
export async function getConfigMazeApi(): Promise<ConfigMaze> {
try {
const res = await axios.get<ConfigMaze>(
`/api/config-maze`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data as ConfigMaze;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return {
Avatar: {},
MOC: {},
AS: {},
PF: {},
};
}
}
export async function getMainAffixApi(): Promise<Record<string, Record<string, AffixDetail>>> {
try {
const res = await axios.get<Record<string, Record<string, AffixDetail>>>(
`/api/main-affixes`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data as Record<string, Record<string, AffixDetail>>;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return {};
}
}
export async function getSubAffixApi(): Promise<Record<string, Record<string, AffixDetail>>> {
try {
const res = await axios.get<Record<string, Record<string, AffixDetail>>>(
`/api/sub-affixes`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data as Record<string, Record<string, AffixDetail>>;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return {};
}
}
export async function getCharacterInfoApi(avatarId: number, locale: string): Promise<CharacterDetail | null> {
try {
const res = await axios.get<CharacterDetail>(
`https://api.hakush.in/hsr/data/${locale}/character/${avatarId}.json`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data as CharacterDetail;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return null;
}
}
export async function getLightconeInfoApi(lightconeId: number, locale: string): Promise<LightConeDetail | null> {
try {
const res = await axios.get<LightConeDetail>(
`https://api.hakush.in/hsr/data/${locale}/lightcone/${lightconeId}.json`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data as LightConeDetail;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return null;
}
}
export async function getRelicInfoApi(relicId: number, locale: string): Promise<RelicDetail | null> {
try {
const res = await axios.get<RelicDetail>(
`https://api.hakush.in/hsr/data/${locale}/relicset/${relicId}.json`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data as RelicDetail;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return null;
}
}
export async function fetchCharacterByIdNative(id: string, locale: string): Promise<CharacterDetail | null> {
try {
const res = await axios.get<CharacterDetail>(`/api/${locale}/characters/${id}`);
return res.data;
} catch (error) {
console.error('Failed to fetch character:', error);
return null;
}
}
export async function fetchCharactersByIdsNative(ids: string[], locale: string): Promise<Record<string, CharacterDetail>> {
try {
const res = await axios.post<Record<string, CharacterDetail>>(`/api/${locale}/characters`, { charIds: ids });
return res.data;
} catch (error) {
console.error('Failed to fetch characters:', error);
return {};
}
}
export async function fetchLightconeByIdNative(id: string, locale: string): Promise<LightConeDetail | null> {
try {
const res = await axios.get<LightConeDetail>(`/api/${locale}/lightcones/${id}`);
return res.data;
} catch (error) {
console.error('Failed to fetch lightcone:', error);
return null;
}
}
export async function fetchLightconesByIdsNative(ids: string[], locale: string): Promise<Record<string, LightConeDetail>> {
try {
const res = await axios.post<Record<string, LightConeDetail>>(`/api/${locale}/lightcones`, { lightconeIds: ids });
return res.data;
} catch (error) {
console.error('Failed to fetch lightcones:', error);
return {};
}
}
export async function fetchRelicByIdNative(id: string, locale: string): Promise<RelicDetail | null> {
try {
const res = await axios.get<RelicDetail>(`/api/${locale}/relics/${id}`);
return res.data;
} catch (error) {
console.error('Failed to fetch relic:', error);
return null;
}
}
export async function fetchRelicsByIdsNative(ids: string[], locale: string): Promise<Record<string, RelicDetail>> {
try {
const res = await axios.post<Record<string, RelicDetail>>(`/api/${locale}/relics`, { relicIds: ids });
return res.data;
} catch (error) {
console.error('Failed to fetch relics:', error);
return {};
}
}
export async function getCharacterListApi(): Promise<CharacterBasic[]> {
try {
const res = await axios.get<Record<string, CharacterBasicRaw>>(
'https://api.hakush.in/hsr/data/character.json',
{
headers: {
'Content-Type': 'application/json',
},
}
);
const data = new Map(Object.entries(res.data));
return Array.from(data.entries()).map(([id, it]) => convertAvatar(id, it));
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return [];
}
}
export async function getLightconeListApi(): Promise<LightConeBasic[]> {
try {
const res = await axios.get<Record<string, LightConeBasicRaw>>(
'https://api.hakush.in/hsr/data/lightcone.json',
{
headers: {
'Content-Type': 'application/json',
},
}
);
const data = new Map(Object.entries(res.data));
return Array.from(data.entries()).map(([id, it]) => convertLightcone(id, it));
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return [];
}
}
export async function getRelicSetListApi(): Promise<RelicBasic[]> {
try {
const res = await axios.get<Record<string, RelicBasicRaw>>(
'https://api.hakush.in/hsr/data/relicset.json',
{
headers: {
'Content-Type': 'application/json',
},
}
);
const data = new Map(Object.entries(res.data));
return Array.from(data.entries()).map(([id, it]) => convertRelicSet(id, it));
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return [];
}
}
export async function SendDataToServer(username: string, password: string, serverUrl: string, data: FreeSRJson | null): Promise<PSResponse | string> {
try {
const response = await axios.post(`${serverUrl}`, { username, password, data })
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return "Invalid response schema";
}
return parsed.data;
} catch (error: any) {
return error?.message || "Unknown error";
}
}
export async function SendDataThroughProxy({data}: {data: any}) {
try {
const response = await axios.post(`/api/proxy`, { ...data })
return response.data;
} catch (error: any) {
return error;
}
}

View File

@@ -0,0 +1,51 @@
import fs from 'fs';
import path from 'path';
import { getCharacterInfoApi } from './api';
import { CharacterDetail } from '@/types/characterDetail';
const DATA_DIR = path.join(process.cwd(), 'data');
const characterFileCache: Record<string, Record<string, CharacterDetail>> = {};
export let characterMap: Record<string, CharacterDetail> = {};
function getJsonFilePath(locale: string): string {
return path.join(DATA_DIR, `characters.${locale}.json`);
}
function loadFromFileIfExists(locale: string): Record<string, CharacterDetail> | null {
if (characterFileCache[locale]) return characterFileCache[locale];
const filePath = getJsonFilePath(locale);
if (fs.existsSync(filePath)) {
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, CharacterDetail>;
characterFileCache[locale] = data;
return data;
}
return null;
}
export async function loadCharacters(charIds: string[], locale: string): Promise<Record<string, CharacterDetail>> {
const fileData = loadFromFileIfExists(locale);
const fileIds = fileData ? Object.keys(fileData) : [];
if (fileData && charIds.every(id => fileIds.includes(id))) {
characterMap = fileData;
return characterMap;
}
const result: Record<string, CharacterDetail> = {};
await Promise.all(
charIds.map(async id => {
const info = await getCharacterInfoApi(Number(id), locale);
if (info) result[id] = info;
})
);
fs.mkdirSync(DATA_DIR, { recursive: true });
const filePath = getJsonFilePath(locale);
fs.writeFileSync(filePath, JSON.stringify(result, null, 2), 'utf-8');
characterFileCache[locale] = result;
characterMap = result;
return result;
}

View File

@@ -0,0 +1,23 @@
import { ConfigMaze } from "@/types";
import fs from 'fs';
import path from "path";
const DATA_DIR = path.join(process.cwd(), 'data');
export let configMazeMap: Record<string, ConfigMaze> = {};
function loadFromFileIfExists(): Record<string, ConfigMaze> | null {
const filePath = path.join(DATA_DIR, 'config_maze.json');
if (fs.existsSync(filePath)) {
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, ConfigMaze>;
configMazeMap = data;
return data;
}
return null;
}
export async function loadConfigMaze(): Promise<Record<string, ConfigMaze>> {
if (Object.keys(configMazeMap).length > 0) return configMazeMap;
const fileData = loadFromFileIfExists();
return fileData || {};
}

128
src/lib/constant.ts Normal file
View File

@@ -0,0 +1,128 @@
export const listCurrentLanguage = {
ja: "JP",
ko: "KR",
en: "US",
vi: "VN",
zh: "CN"
};
export const listCurrentLanguageApi : Record<string, string> = {
ja: "jp",
ko: "kr",
en: "en",
vi: "en",
zh: "cn"
};
export const mapingStats = <Record<string, {name: string, icon: string, unit: string}> > {
"HPDelta": {
name:"HP",
icon:"/icon/hp.webp",
unit: ""
},
"AttackDelta": {
name:"ATK",
icon:"/icon/attack.webp",
unit: ""
},
"HPAddedRatio": {
name:"HP",
icon:"/icon/hp.webp",
unit: "%"
},
"AttackAddedRatio": {
name:"ATK",
icon:"/icon/attack.webp",
unit: "%"
},
"DefenceDelta": {
name:"DEF",
icon:"/icon/defence.webp",
unit: ""
},
"DefenceAddedRatio": {
name:"DEF",
icon:"/icon/defence.webp",
unit: "%"
},
"SPDDelta": {
name:"SPD",
icon:"/icon/spd.webp",
unit: ""
},
"CriticalChanceBase": {
name:"CRIT Rate",
icon:"/icon/crit-rate.webp",
unit: "%"
},
"CriticalDamageBase": {
name:"CRIT DMG",
icon:"/icon/crit-damage.webp",
unit: "%"
},
"HealRatioBase": {
name:"Outgoing Healing Boost",
icon:"/icon/healing-boost.webp",
unit: "%"
},
"StatusProbabilityBase": {
name:"Effect Hit Rate",
icon:"/icon/effect-hit-rate.webp",
unit: "%"
},
"StatusResistanceBase": {
name:"Effect RES",
icon:"/icon/effect-res.webp",
unit: "%"
},
"BreakDamageAddedRatioBase": {
name:"Break Effect",
icon:"/icon/break-effect.webp",
unit: "%"
},
"SpeedDelta": {
name:"SPD",
icon:"/icon/speed.webp",
unit: ""
},
"PhysicalAddedRatio": {
name:"Physical DMG Boost",
icon:"/icon/physical.webp",
unit: "%"
},
"FireAddedRatio": {
name:"Fire DMG Boost",
icon:"/icon/fire.webp",
unit: "%"
},
"IceAddedRatio": {
name:"Ice DMG Boost",
icon:"/icon/ice.webp",
unit: "%"
},
"ThunderAddedRatio": {
name:"Thunder DMG Boost",
icon:"/icon/thunder.webp",
unit: "%"
},
"WindAddedRatio": {
name:"Wind DMG Boost",
icon:"/icon/wind.webp",
unit: "%"
},
"QuantumAddedRatio": {
name:"Quantum DMG Boost",
icon:"/icon/quantum.webp",
unit: "%"
},
"ImaginaryAddedRatio": {
name:"Imaginary DMG Boost",
icon:"/icon/imaginary.webp",
unit: "%"
},
"SPRatioBase": {
name:"Energy Regeneration Rate",
icon:"/icon/energy-rate.webp",
unit: "%"
}
}

51
src/lib/lighconeLoader.ts Normal file
View File

@@ -0,0 +1,51 @@
import fs from 'fs';
import path from 'path';
import { getLightconeInfoApi } from './api';
import { LightConeDetail } from '@/types';
const DATA_DIR = path.join(process.cwd(), 'data');
const lightconeFileCache: Record<string, Record<string, LightConeDetail>> = {};
export let lightconeMap: Record<string, LightConeDetail> = {};
function getJsonFilePath(locale: string): string {
return path.join(DATA_DIR, `lightcones.${locale}.json`);
}
function loadLightconeFromFileIfExists(locale: string): Record<string, LightConeDetail> | null {
if (lightconeFileCache[locale]) return lightconeFileCache[locale];
const filePath = getJsonFilePath(locale);
if (fs.existsSync(filePath)) {
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, LightConeDetail>;
lightconeFileCache[locale] = data;
return data;
}
return null;
}
export async function loadLightcones(charIds: string[], locale: string): Promise<Record<string, LightConeDetail>> {
const fileData = loadLightconeFromFileIfExists(locale);
const fileIds = fileData ? Object.keys(fileData) : [];
if (fileData && charIds.every(id => fileIds.includes(id))) {
lightconeMap = fileData;
return lightconeMap;
}
const result: Record<string, LightConeDetail> = {};
await Promise.all(
charIds.map(async id => {
const info = await getLightconeInfoApi(Number(id), locale);
if (info) result[id] = info;
})
);
fs.mkdirSync(DATA_DIR, { recursive: true });
const filePath = getJsonFilePath(locale);
fs.writeFileSync(filePath, JSON.stringify(result, null, 2), 'utf-8');
lightconeFileCache[locale] = result;
lightconeMap = result;
return result;
}

51
src/lib/relicLoader.ts Normal file
View File

@@ -0,0 +1,51 @@
import fs from 'fs';
import path from 'path';
import { getRelicInfoApi } from './api';
import { RelicDetail } from '@/types';
const DATA_DIR = path.join(process.cwd(), 'data');
const relicFileCache: Record<string, Record<string, RelicDetail>> = {};
export let relicMap: Record<string, RelicDetail> = {};
function getJsonFilePath(locale: string): string {
return path.join(DATA_DIR, `relics.${locale}.json`);
}
function loadRelicFromFileIfExists(locale: string): Record<string, RelicDetail> | null {
if (relicFileCache[locale]) return relicFileCache[locale];
const filePath = getJsonFilePath(locale);
if (fs.existsSync(filePath)) {
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, RelicDetail>;
relicFileCache[locale] = data;
return data;
}
return null;
}
export async function loadRelics(charIds: string[], locale: string): Promise<Record<string, RelicDetail>> {
const fileData = loadRelicFromFileIfExists(locale);
const fileIds = fileData ? Object.keys(fileData) : [];
if (fileData && charIds.every(id => fileIds.includes(id))) {
relicMap = fileData;
return relicMap;
}
const result: Record<string, RelicDetail> = {};
await Promise.all(
charIds.map(async id => {
const info = await getRelicInfoApi(Number(id), locale);
if (info) result[id] = info;
})
);
fs.mkdirSync(DATA_DIR, { recursive: true });
const filePath = getJsonFilePath(locale);
fs.writeFileSync(filePath, JSON.stringify(result, null, 2), 'utf-8');
relicFileCache[locale] = result;
relicMap = result;
return result;
}