add new content end game
All checks were successful
Gitea Auto Deploy / Deploy-Container (push) Successful in 2m2s

This commit is contained in:
2025-08-15 21:29:41 +07:00
parent f40a403ea2
commit 55f1415e2f
51 changed files with 67356 additions and 2765 deletions

View File

@@ -0,0 +1,18 @@
import { NextRequest, NextResponse } from 'next/server'
import { loadPeak } from '@/lib/loader'
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string, locale: string }> }
) {
const { id, locale } = await params
const peakData = await loadPeak([id], locale)
const peak = peakData[id]
if (!peak) {
return NextResponse.json({ error: 'Peak info not found' }, { status: 404 })
}
return NextResponse.json(peak)
}

View File

@@ -0,0 +1,20 @@
import { loadPeak } from "@/lib/loader";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest, { params }: { params: Promise<{ locale: string }> }) {
try {
const body = await request.json();
const peakIds = body.peakIds as string[];
const { locale } = await params;
if (!Array.isArray(peakIds) || peakIds.some(id => typeof id !== 'string')) {
return NextResponse.json({ error: 'Invalid peakIds' }, { status: 400 });
}
const peakData = await loadPeak(peakIds, locale);
return NextResponse.json(peakData);
} catch {
return NextResponse.json({ error: 'Failed to load peak data' }, { status: 500 });
}
}

View File

@@ -5,7 +5,17 @@ import CharacterCard from "../card/characterCard"
import useLocaleStore from "@/stores/localeStore"
import useAvatarStore from "@/stores/avatarStore"
import { useTranslations } from "next-intl"
import { useFetchASData, useFetchAvatarData, useFetchConfigData, useFetchLightconeData, useFetchMOCData, useFetchMonsterData, useFetchPFData, useFetchRelicData } from "@/hooks"
import {
useFetchASData,
useFetchAvatarData,
useFetchConfigData,
useFetchLightconeData,
useFetchMOCData,
useFetchMonsterData,
useFetchPEAKData,
useFetchPFData,
useFetchRelicData
} from "@/hooks"
export default function AvatarBar() {
@@ -23,6 +33,7 @@ export default function AvatarBar() {
useFetchPFData()
useFetchMOCData()
useFetchASData()
useFetchPEAKData()
useEffect(() => {
setFilter({ ...filter, locale: locale, element: Object.keys(listElement).filter((key) => listElement[key]), path: Object.keys(listPath).filter((key) => listPath[key]) })

View File

@@ -174,6 +174,7 @@ export default function AvatarInfo() {
<button
className="btn btn-sm btn-outline btn-warning"
onClick={() => {
if (!avatars[avatarSelected?.id || ""]?.can_change_sp) return
const newSpValue = Math.ceil(avatars[avatarSelected?.id || ""]?.sp_max / 2);
const newAvatar = { ...avatars[avatarSelected?.id || ""], sp_value: newSpValue }
setAvatar(newAvatar)

View File

@@ -39,10 +39,12 @@ export default function Header() {
pf_config,
as_config,
ce_config,
peak_config,
setMocConfig,
setPfConfig,
setAsConfig,
setCeConfig,
setPeakConfig,
} = useUserDataStore()
const router = useRouter()
@@ -170,8 +172,10 @@ export default function Header() {
setPfConfig(parsed.pf_config)
setAsConfig(parsed.as_config)
setCeConfig(parsed.ce_config)
setPeakConfig(parsed.peak_config)
toast.success(transI18n("importDatabaseSuccess"))
} catch {
} catch (e) {
console.log(e)
toast.error(transI18n("fileMustBeAValidJsonFile"))
}
};
@@ -231,8 +235,26 @@ export default function Header() {
<details>
<summary className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium">{transI18n("exportData")}</summary>
<ul className="p-2">
<li><a onClick={() => downloadJson("freesr-data", converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config))}>{transI18n("freeSr")}</a></li>
<li><a onClick={() => downloadJson("database-data", { avatars: avatars, battle_type: battle_type, moc_config: moc_config, pf_config: pf_config, as_config: as_config, ce_config: ce_config })}>{transI18n("database")}</a></li>
<li><a onClick={() => downloadJson("freesr-data",
converterToFreeSRJson(
avatars,
battle_type,
moc_config,
pf_config,
as_config,
ce_config,
peak_config
)
)}>{transI18n("freeSr")}</a></li>
<li><a onClick={() => downloadJson("database-data", {
avatars: avatars,
battle_type: battle_type,
moc_config: moc_config,
pf_config: pf_config,
as_config: as_config,
ce_config: ce_config,
peak_config: peak_config
})}>{transI18n("database")}</a></li>
</ul>
</details>
</li>
@@ -320,8 +342,26 @@ export default function Header() {
<details>
<summary className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium">{transI18n("exportData")}</summary>
<ul className="p-2">
<li><a onClick={() => downloadJson("freesr-data", converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config))}>{transI18n("freeSr")}</a></li>
<li><a onClick={() => downloadJson("database-data", { avatars: avatars, battle_type: battle_type, moc_config: moc_config, pf_config: pf_config, as_config: as_config, ce_config: ce_config })}>{transI18n("database")}</a></li>
<li><a onClick={() => downloadJson("freesr-data",
converterToFreeSRJson(
avatars,
battle_type,
moc_config,
pf_config,
as_config,
ce_config,
peak_config
)
)}>{transI18n("freeSr")}</a></li>
<li><a onClick={() => downloadJson("database-data", {
avatars: avatars,
battle_type: battle_type,
moc_config: moc_config,
pf_config: pf_config,
as_config: as_config,
ce_config: ce_config,
peak_config: peak_config
})}>{transI18n("database")}</a></li>
</ul>
</details>
</li>

View File

@@ -56,7 +56,7 @@ export default function AsBar() {
const challenge = mapASInfo[as_config.event_id.toString()]?.Level.find((as) => as.Id === as_config.challenge_id)
if (as_config.event_id !== 0 && as_config.challenge_id !== 0 && challenge) {
const newBattleConfig = cloneDeep(as_config)
newBattleConfig.cycle_count = 4
newBattleConfig.cycle_count = 0
newBattleConfig.blessings = []
if (as_config.buff_id !== 0) {

View File

@@ -5,6 +5,7 @@ import Image from "next/image";
import AsBar from "./as";
import { useTranslations } from "next-intl";
import CeBar from "./ce";
import PeakBar from "./peak";
export default function MonsterBar() {
const { battle_type, setBattleType } = useUserDataStore()
@@ -14,6 +15,7 @@ export default function MonsterBar() {
{ name: transI18n("memoryOfChaos"), icon: 'AbyssIcon01', value: 'MOC' },
{ name: transI18n("pureFiction"), icon: 'ChallengeStory', value: 'PF' },
{ name: transI18n("apocalypticShadow"), icon: 'ChallengeBoss', value: 'AS' },
{ name: transI18n("anomalyArbitration"), icon: 'AbyssIcon02', value: 'PEAK' },
{ name: transI18n("customEnemy"), icon: 'MonsterIcon', value: 'CE' },
{ name: transI18n("simulatedUniverse"), icon: 'SimulatedUniverse', value: 'SU' },
];
@@ -24,9 +26,9 @@ export default function MonsterBar() {
{/* Header Navigation */}
<nav className="border-b border-warning/30 relative">
<div className="container mx-auto px-4">
<div className="flex items-center justify-center h-16">
<div className="flex items-center justify-center">
{/* Navigation Tabs */}
<div className="flex space-x-1">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-1">
{navItems.map((item) => (
<button
key={item.name}
@@ -76,6 +78,7 @@ export default function MonsterBar() {
{battle_type.toUpperCase() === 'PF' && <PfBar />}
{battle_type.toUpperCase() === 'AS' && <AsBar />}
{battle_type.toUpperCase() === 'CE' && <CeBar />}
{battle_type.toUpperCase() === 'PEAK' && <PeakBar />}
{battle_type.toUpperCase() === 'SU' && (
<div className="container mx-auto px-4 py-8 text-center font-bold text-3xl">
{transI18n("comingSoon")}

View File

@@ -0,0 +1,272 @@
"use client"
import { useEffect, useMemo } from "react";
import SelectCustomText from "../select/customSelectText";
import useEventStore from "@/stores/eventStore";
import { getLocaleName, replaceByParam } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import useMonsterStore from "@/stores/monsterStore";
import Image from "next/image";
import cloneDeep from 'lodash/cloneDeep'
import { useTranslations } from "next-intl";
import { MonsterStore } from "@/types";
export default function PeakBar() {
const { PEAKEvent, mapPEAKInfo } = useEventStore()
const { listMonster } = useMonsterStore()
const { locale } = useLocaleStore()
const {
peak_config,
setPeakConfig
} = useUserDataStore()
const transI18n = useTranslations("DataPage")
const listFloor = useMemo(() => {
if (!mapPEAKInfo?.[peak_config?.event_id?.toString()]) return []
return [
...mapPEAKInfo[peak_config?.event_id?.toString()]?.PreLevel,
mapPEAKInfo[peak_config?.event_id?.toString()]?.BossLevel,
]
}, [peak_config, mapPEAKInfo])
const eventSelected = useMemo(() => {
return mapPEAKInfo?.[peak_config?.event_id?.toString()]
}, [peak_config, mapPEAKInfo])
const bossConfig = useMemo(() => {
return mapPEAKInfo?.[peak_config?.event_id?.toString()]?.BossConfig;
}, [peak_config, mapPEAKInfo])
const challengeSelected = useMemo(() => {
const challenge = cloneDeep(listFloor.find((peak) => peak.Id === peak_config.challenge_id))
if (
challenge
&& challenge.Id === mapPEAKInfo?.[peak_config?.event_id?.toString()]?.BossLevel?.Id
&& bossConfig
&& peak_config?.boss_mode === "Hard"
) {
challenge.Name = bossConfig.HardName
challenge.EventIDList = bossConfig.EventIDList
challenge.InfiniteList = bossConfig.InfiniteList
challenge.TagList = bossConfig.TagList
}
return challenge
}, [peak_config, listFloor, mapPEAKInfo, bossConfig])
useEffect(() => {
if (!challengeSelected) return
if (peak_config.event_id !== 0 && peak_config.challenge_id !== 0 && challengeSelected) {
const newBattleConfig = cloneDeep(peak_config)
newBattleConfig.cycle_count = 6
newBattleConfig.blessings = []
for (const value of challengeSelected.TagList) {
newBattleConfig.blessings.push({
id: Number(value.Id),
level: 1
})
}
if (peak_config.buff_id !== 0) {
newBattleConfig.blessings.push({
id: peak_config.buff_id,
level: 1
})
}
newBattleConfig.monsters = []
newBattleConfig.stage_id = challengeSelected.EventIDList[0].StageID
for (const wave of challengeSelected.EventIDList[0].MonsterList) {
if (!wave) continue
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
if (!value) continue
newWave.push({
monster_id: Number(value),
level: challengeSelected.EventIDList[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
setPeakConfig(newBattleConfig)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
peak_config.event_id,
peak_config.challenge_id,
peak_config.buff_id,
mapPEAKInfo,
])
return (
<div className="container mx-auto px-4 py-8 relative">
{/* Title Card */}
<div className="rounded-xl p-4 mb-2 border border-warning">
<div className="mb-4 w-full">
<SelectCustomText
customSet={PEAKEvent.filter(peak => peak.lang.get(locale)).map((peak) => ({
id: peak.id,
name: `${getLocaleName(locale, peak)} (${peak.id}) `,
}))}
excludeSet={[]}
selectedCustomSet={peak_config.event_id.toString()}
placeholder={transI18n("selectASEvent")}
setSelectedCustomSet={(id) => setPeakConfig({ ...peak_config, event_id: Number(id), challenge_id: 0, buff_id: 0 })}
/>
</div>
{/* Settings */}
<div className={
`grid grid-cols-1
${eventSelected && eventSelected.BossLevel.Id === peak_config.challenge_id ? "md:grid-cols-2" : ""}
gap-4 mb-4 justify-items-center items-center w-full`}
>
<div className="flex items-center gap-2 w-full">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
</label>
<select
value={peak_config.challenge_id}
className="select select-success w-full"
onChange={(e) => setPeakConfig({ ...peak_config, challenge_id: Number(e.target.value) })}
>
<option value={0} disabled={true}>{transI18n("selectFloor")}</option>
{listFloor.map((peak) => (
<option key={peak.Id} value={peak.Id}>{peak.Name}</option>
))}
</select>
</div>
{eventSelected && eventSelected.BossLevel.Id === peak_config.challenge_id && (
<div className="flex items-center gap-2 w-full">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("mode")}:{" "}</span>
</label>
<select
value={peak_config.boss_mode}
className="select select-success w-full"
onChange={(e) => setPeakConfig({ ...peak_config, boss_mode: e.target.value })}
>
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
<option value="Normal">{transI18n("normalMode")}</option>
<option value="Hard">{transI18n("hardMode")}</option>
</select>
</div>
)}
</div>
{
eventSelected
&& eventSelected.BossLevel.Id === peak_config.challenge_id
&& bossConfig
&& bossConfig.BuffList
&& (
<div className="mb-4 w-full">
<SelectCustomText
customSet={
Array.isArray(bossConfig.BuffList)
? bossConfig.BuffList.map((buff) => ({
id: buff.Id.toString(),
name: buff?.Name || "",
description: replaceByParam(buff?.Desc || "", buff?.Param || []),
}))
: []
}
excludeSet={[]}
selectedCustomSet={peak_config?.buff_id?.toString()}
placeholder={transI18n("selectBuff")}
setSelectedCustomSet={(id) => setPeakConfig({ ...peak_config, buff_id: Number(id) })}
/>
</div>
)
}
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<h2 className="text-2xl font-bold mb-2 text-info">
{transI18n("turbulenceBuff")}
</h2>
{challengeSelected && challengeSelected?.TagList?.length > 0 ? (
challengeSelected.TagList.map((subOption, index) => (
<div key={index}>
<label className="label">
<span className="label-text font-bold text-success">
{index + 1}. {subOption.Name}
</span>
</label>
<div
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
subOption.Desc,
subOption.Param || []
)
}}
/>
</div>
))
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
<div className="grid grid-cols-1 gap-4">
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{challengeSelected?.Name}</h2>
{challengeSelected && Object.values(challengeSelected.InfiniteList).map((waveValue, waveIndex) => (
<div key={waveIndex} className="mb-6">
<h3 className="text-lg font-semibold mb-t">{transI18n("wave")} {waveIndex + 1}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Array.from(new Set(waveValue.MonsterGroupIDList)).map((monsterId, enemyIndex) => (
<div
key={enemyIndex}
className="rounded-xl p-2 border border-white/10 shadow-md hover:border-white/20 hover:shadow-lg transition"
>
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
{listMonster.find((monster) => monster.child.includes(monsterId))?.icon && <Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(monsterId))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>}
</div>
<div className="flex flex-col">
<div className="text-sm font-semibold">Lv. {challengeSelected?.EventIDList[0].Level}</div>
<div className="flex items-center space-x-1 mt-1">
{listMonster
.find((monster) => monster.child.includes(monsterId))
?.weak?.map((icon, iconIndex) => (
<Image
src={`/icon/${icon.toLowerCase()}.webp`}
alt={icon}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md border border-white/20 shadow-sm"
width={200}
height={200}
key={iconIndex}
/>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -53,8 +53,8 @@ export const syncDataToPS = async (): Promise<{ success: boolean, message: strin
password
} = useConnectStore.getState()
const {avatars, battle_type, moc_config, pf_config, as_config, ce_config} = useUserDataStore.getState()
const data = converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config)
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://")) {

View File

@@ -1,4 +1,4 @@
import { ASConfigStore, AvatarJson, AvatarStore, BattleConfigJson, CEConfigStore, FreeSRJson, LightconeJson, MOCConfigStore, PFConfigStore, RelicJson } from "@/types";
import { ASConfigStore, AvatarJson, AvatarStore, BattleConfigJson, CEConfigStore, FreeSRJson, LightconeJson, MOCConfigStore, PEAKConfigStore, PFConfigStore, RelicJson } from "@/types";
export function converterToFreeSRJson(
@@ -8,6 +8,7 @@ export function converterToFreeSRJson(
pf_config: PFConfigStore,
as_config: ASConfigStore,
ce_config: CEConfigStore,
peak_config: PEAKConfigStore,
): FreeSRJson {
const lightcones: LightconeJson[] = []
const relics: RelicJson[] = []
@@ -52,6 +53,16 @@ export function converterToFreeSRJson(
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,

View File

@@ -6,4 +6,5 @@ export * from "./useFetchRelicData";
export * from "./useFetchMonsterData";
export * from "./useFetchPFData";
export * from "./useFetchMOCData";
export * from "./useFetchASData";
export * from "./useFetchASData";
export * from "./useFetchPEAKData";

View File

@@ -0,0 +1,71 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { fetchPeakByIdsNative, getPEAKEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import { listCurrentLanguageApi } from '@/constant/constant'
import useLocaleStore from '@/stores/localeStore'
import { toast } from 'react-toastify'
import useEventStore from '@/stores/eventStore'
import { EventStageDetail, PeakDetail } from '@/types'
export const useFetchPEAKData = () => {
const { setPEAKEvent, setMapPEAKInfo } = useEventStore()
const { locale } = useLocaleStore()
const { data: dataPEAK, error: errorPEAK } = useQuery({
queryKey: ['peakData'],
queryFn: getPEAKEventListApi,
select: (data) => data.sort((a, b) => Number(b.id) - Number(a.id)),
staleTime: 1000 * 60 * 5,
})
const { data: dataPEAKInfo, error: errorPEAKInfo } = useQuery({
queryKey: ['peakInfoData', locale],
queryFn: () =>
fetchPeakByIdsNative(
dataPEAK!.map((item) => item.id),
listCurrentLanguageApi[locale.toLowerCase()]
),
staleTime: 1000 * 60 * 5,
select: (data) => {
const newData = { ...data }
for (const key in newData) {
for (const item of newData[key].PreLevel) {
item.EventIDList = item.EventIDList.map((event: EventStageDetail) => ({
...event,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
MonsterList: event.MonsterList.map(({ $type, ...rest }) => rest)
}))
}
newData[key].BossLevel.EventIDList = newData[key].BossLevel.EventIDList.map((event: EventStageDetail) => ({
...event,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
MonsterList: event.MonsterList.map(({ $type, ...rest }) => rest)
}))
newData[key].BossConfig.EventIDList = newData[key].BossConfig.EventIDList.map((event: EventStageDetail) => ({
...event,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
MonsterList: event.MonsterList.map(({ $type, ...rest }) => rest)
}))
}
return newData
},
enabled: !!dataPEAK,
});
useEffect(() => {
if (dataPEAK && !errorPEAK) {
setPEAKEvent(dataPEAK)
} else if (errorPEAK) {
toast.error("Failed to load PEAK data")
}
}, [dataPEAK, errorPEAK, setPEAKEvent])
useEffect(() => {
if (dataPEAKInfo && !errorPEAKInfo) {
setMapPEAKInfo(dataPEAKInfo as Record<string, PeakDetail>)
} else if (errorPEAKInfo) {
toast.error("Failed to load PEAK info data")
}
}, [dataPEAKInfo, errorPEAKInfo, setMapPEAKInfo])
}

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { AffixDetail, ASDetail, CharacterDetail, ConfigMaze, FreeSRJson, LightConeDetail, MocDetail, PFDetail, PSResponse, RelicDetail } from "@/types";
import { AffixDetail, ASDetail, CharacterDetail, ConfigMaze, FreeSRJson, LightConeDetail, MocDetail, PeakDetail, PFDetail, PSResponse, RelicDetail } from "@/types";
import axios from 'axios';
import { pSResponseSchema } from "@/zod";
@@ -176,7 +176,6 @@ export async function fetchPFByIdNative(ids: string, locale: string): Promise<PF
}
}
export async function fetchMOCByIdsNative(ids: string[], locale: string): Promise<Record<string, MocDetail[]> | null> {
try {
const res = await axios.post<Record<string, MocDetail[]>>(`/api/${locale}/moc`, { mocIds: ids });
@@ -197,6 +196,27 @@ export async function fetchMOCByIdNative(ids: string, locale: string): Promise<M
}
}
export async function fetchPeakByIdsNative(ids: string[], locale: string): Promise<Record<string, PeakDetail> | null> {
try {
const res = await axios.post<Record<string, PeakDetail>>(`/api/${locale}/peak`, { peakIds: ids });
return res.data;
} catch (error) {
console.error('Failed to fetch peak:', error);
return null;
}
}
export async function fetchPeakByIdNative(ids: string, locale: string): Promise<PeakDetail | null> {
try {
const res = await axios.get<PeakDetail>(`/api/${locale}/peak/${ids}`);
return res.data;
} catch (error) {
console.error('Failed to fetch peak:', error);
return null;
}
}
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 })

View File

@@ -1,5 +1,5 @@
import { convertAvatar, convertEvent, convertLightcone, convertMonster, convertRelicSet } from "@/helper";
import { ASDetail, CharacterBasic, CharacterBasicRaw, CharacterDetail, EventBasic, EventBasicRaw, LightConeBasic, LightConeBasicRaw, LightConeDetail, MocDetail, MonsterBasic, MonsterBasicRaw, MonsterDetail, PFDetail, RelicBasic, RelicBasicRaw, RelicDetail } from "@/types";
import { ASDetail, CharacterBasic, CharacterBasicRaw, CharacterDetail, EventBasic, EventBasicRaw, LightConeBasic, LightConeBasicRaw, LightConeDetail, MocDetail, MonsterBasic, MonsterBasicRaw, MonsterDetail, PeakDetail, PFDetail, RelicBasic, RelicBasicRaw, RelicDetail } from "@/types";
import axios from "axios";
export async function getLightconeInfoApi(lightconeId: number, locale: string): Promise<LightConeDetail | null> {
@@ -134,6 +134,28 @@ export async function getPFEventInfoApi(eventId: number, locale: string): Promis
}
}
export async function getPeakEventInfoApi(eventId: number, locale: string): Promise<PeakDetail | null> {
try {
const res = await axios.get<PeakDetail>(
`https://api.hakush.in/hsr/data/${locale}/peak/${eventId}.json`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data as PeakDetail;
} 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 getCharacterListApi(): Promise<CharacterBasic[]> {
try {
const res = await axios.get<Record<string, CharacterBasicRaw>>(
@@ -278,6 +300,30 @@ export async function getPFEventListApi(): Promise<EventBasic[]> {
}
}
export async function getPEAKEventListApi(): Promise<EventBasic[]> {
try {
const res = await axios.get<Record<string, EventBasicRaw>>(
'https://api.hakush.in/hsr/data/maze_peak.json',
{
headers: {
'Content-Type': 'application/json',
},
}
);
const data = new Map(Object.entries(res.data));
return Array.from(data.entries()).map(([id, it]) => convertEvent(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 getMonsterListApi(): Promise<{list: MonsterBasic[], map: Record<string, MonsterBasic>}> {
try {
const res = await axios.get<Record<string, MonsterBasicRaw>>(

View File

@@ -6,3 +6,5 @@ export * from "./relicLoader";
export * from "./asLoader";
export * from "./pfLoader";
export * from "./mocLoader";
export * from "./peakLoader";

View File

@@ -0,0 +1,51 @@
import fs from 'fs';
import path from 'path';
import { PeakDetail } from '@/types';
import { getPeakEventInfoApi } from '../api';
const DATA_DIR = path.join(process.cwd(), 'data');
const peakFileCache: Record<string, Record<string, PeakDetail>> = {};
export let peakMap: Record<string, PeakDetail> = {};
function getJsonFilePath(locale: string): string {
return path.join(DATA_DIR, `peak.${locale}.json`);
}
function loadFromFileIfExists(locale: string): Record<string, PeakDetail> | null {
if (peakFileCache[locale]) return peakFileCache[locale];
const filePath = getJsonFilePath(locale);
if (fs.existsSync(filePath)) {
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, PeakDetail>;
peakFileCache[locale] = data;
return data;
}
return null;
}
export async function loadPeak(charIds: string[], locale: string): Promise<Record<string, PeakDetail>> {
const fileData = loadFromFileIfExists(locale);
const fileIds = fileData ? Object.keys(fileData) : [];
if (fileData && charIds.every(id => fileIds.includes(id))) {
peakMap = fileData;
return peakMap;
}
const result: Record<string, PeakDetail> = {};
await Promise.all(
charIds.map(async id => {
const info = await getPeakEventInfoApi(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');
peakFileCache[locale] = result;
peakMap = result;
return result;
}

View File

@@ -1,4 +1,4 @@
import { MocDetail, EventBasic, PFDetail, ASDetail } from '@/types';
import { MocDetail, EventBasic, PFDetail, ASDetail, PeakDetail } from '@/types';
import { create } from 'zustand'
@@ -9,12 +9,16 @@ interface EventState {
mapPFInfo: Record<string, PFDetail>;
ASEvent: EventBasic[];
mapASInfo: Record<string, ASDetail>;
PEAKEvent: EventBasic[];
mapPEAKInfo: Record<string, PeakDetail>;
setMOCEvent: (newListEvent: EventBasic[]) => void;
setMapMOCInfo: (newMapMOCInfo: Record<string, MocDetail[]>) => void;
setPFEvent: (newListEvent: EventBasic[]) => void;
setMapPFInfo: (newMapPFInfo: Record<string, PFDetail>) => void;
setASEvent: (newListEvent: EventBasic[]) => void;
setMapASInfo: (newMapASInfo: Record<string, ASDetail>) => void;
setPEAKEvent: (newListEvent: EventBasic[]) => void;
setMapPEAKInfo: (newMapPEAKInfo: Record<string, PeakDetail>) => void;
}
const useEventStore = create<EventState>((set) => ({
@@ -24,12 +28,16 @@ const useEventStore = create<EventState>((set) => ({
mapPFInfo: {},
ASEvent: [],
mapASInfo: {},
PEAKEvent: [],
mapPEAKInfo: {},
setMOCEvent: (newListEvent: EventBasic[]) => set({ MOCEvent: newListEvent }),
setMapMOCInfo: (newMapMOCInfo: Record<string, MocDetail[]>) => set({ mapMOCInfo: newMapMOCInfo }),
setPFEvent: (newListEvent: EventBasic[]) => set({ PFEvent: newListEvent }),
setMapPFInfo: (newMapPFInfo: Record<string, PFDetail>) => set({ mapPFInfo: newMapPFInfo }),
setASEvent: (newListEvent: EventBasic[]) => set({ ASEvent: newListEvent }),
setMapASInfo: (newMapASInfo: Record<string, ASDetail>) => set({ mapASInfo: newMapASInfo }),
setPEAKEvent: (newListEvent: EventBasic[]) => set({ PEAKEvent: newListEvent }),
setMapPEAKInfo: (newMapPEAKInfo: Record<string, PeakDetail>) => set({ mapPEAKInfo: newMapPEAKInfo }),
}));
export default useEventStore;

View File

@@ -1,4 +1,4 @@
import { ASConfigStore, AvatarStore, CEConfigStore, MOCConfigStore, PFConfigStore } from '@/types';
import { ASConfigStore, AvatarStore, CEConfigStore, MOCConfigStore, PEAKConfigStore, PFConfigStore } from '@/types';
import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware';
@@ -9,6 +9,7 @@ interface UserDataState {
moc_config: MOCConfigStore;
pf_config: PFConfigStore;
as_config: ASConfigStore;
peak_config: PEAKConfigStore;
ce_config: CEConfigStore;
setAvatars: (newAvatars: { [key: string]: AvatarStore }) => void;
setAvatar: (newAvatar: AvatarStore) => void;
@@ -16,6 +17,7 @@ interface UserDataState {
setMocConfig: (newMocConfig: MOCConfigStore) => void;
setPfConfig: (newPfConfig: PFConfigStore) => void;
setAsConfig: (newAsConfig: ASConfigStore) => void;
setPeakConfig: (newPeakConfig: PEAKConfigStore) => void;
setCeConfig: (newCeConfig: CEConfigStore) => void;
}
@@ -61,12 +63,23 @@ const useUserDataStore = create<UserDataState>()(
stage_id: 0,
monsters: [],
},
peak_config: {
event_id: 0,
challenge_id: 0,
buff_id: 0,
boss_mode: "Normal",
blessings: [],
cycle_count: 0,
stage_id: 0,
monsters: [],
},
setAvatars: (newAvatars: { [key: string]: AvatarStore }) => set({ avatars: newAvatars }),
setAvatar: (newAvatar: AvatarStore) => set((state) => ({ avatars: { ...state.avatars, [newAvatar.avatar_id.toString()]: newAvatar } })),
setBattleType: (newBattleType: string) => set({ battle_type: newBattleType }),
setMocConfig: (newMocConfig: MOCConfigStore) => set({ moc_config: newMocConfig }),
setPfConfig: (newPfConfig: PFConfigStore) => set({ pf_config: newPfConfig }),
setAsConfig: (newAsConfig: ASConfigStore) => set({ as_config: newAsConfig }),
setPeakConfig: (newPeakConfig: PEAKConfigStore) => set({ peak_config: newPeakConfig }),
setCeConfig: (newCeConfig: CEConfigStore) => set({ ce_config: newCeConfig }),
}),
{

View File

@@ -17,3 +17,4 @@ export * from "./pfDetail"
export * from "./asDetail"
export * from "./mocDetail"
export * from "./monsterDetail"
export * from "./peakDetail"

View File

@@ -93,8 +93,18 @@ export type ASConfigStore = {
monsters: MonsterStore[][];
}
export type CEConfigStore = {
export type PEAKConfigStore = {
event_id: number;
challenge_id: number;
buff_id: number;
boss_mode: string;
blessings: BattleBuffStore[]
cycle_count: number;
stage_id: number;
monsters: MonsterStore[][];
}
export type CEConfigStore = {
blessings: BattleBuffStore[]
cycle_count: number;
stage_id: number;

37
src/types/peakDetail.ts Normal file
View File

@@ -0,0 +1,37 @@
import { EventStageDetail } from "./mocDetail";
import { InfiniteWave } from "./pfDetail";
export interface PeakDetail {
Id: number;
Name: string;
PreLevel: PeakLevel[];
BossLevel: PeakLevel;
BossConfig: BossConfig;
}
export interface PeakLevel {
Id: number;
Name: string;
DamageType: string[];
MazeGroupID: number;
NpcMonsterIDList: number[];
EventIDList: EventStageDetail[];
TagList: ChallengeTag[];
InfiniteList: Record<string, InfiniteWave>;
}
export interface BossConfig {
HardName: string;
BuffList: ChallengeTag[];
EventIDList: EventStageDetail[];
TagList: ChallengeTag[];
InfiniteList: Record<string, InfiniteWave>;
}
export interface ChallengeTag {
Id: number;
Name: string;
Desc: string;
Param: number[];
}

View File

@@ -106,6 +106,17 @@ export const cEConfigStoreSchema = z.object({
monsters: z.array(z.array(monsterStoreSchema)),
});
export const pEAKConfigStoreSchema = z.object({
event_id: z.number(),
challenge_id: z.number(),
buff_id: z.number(),
boss_mode: z.string(),
blessings: z.array(battleBuffStoreSchema),
cycle_count: z.number(),
stage_id: z.number(),
monsters: z.array(z.array(monsterStoreSchema)),
});
export const micsSchema = z.object({
avatars: z.record(avatarStoreSchema),
battle_type: z.string(),
@@ -113,4 +124,5 @@ export const micsSchema = z.object({
pf_config: pFConfigStoreSchema,
as_config: aSConfigStoreSchema,
ce_config: cEConfigStoreSchema,
peak_config: pEAKConfigStoreSchema,
});