UPDATE: monster bar
Some checks failed
Gitea Auto Deploy / Deploy-Container (push) Failing after 1m36s

This commit is contained in:
2025-07-25 09:20:39 +07:00
parent 604cf1ceec
commit 487c29def1
133 changed files with 841207 additions and 16695 deletions

View File

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

View File

@@ -0,0 +1,20 @@
import { loadAS } 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 asIds = body.asIds as string[];
const { locale } = await params;
if (!Array.isArray(asIds) || asIds.some(id => typeof id !== 'string')) {
return NextResponse.json({ error: 'Invalid asIds' }, { status: 400 });
}
const asData = await loadAS(asIds, locale);
return NextResponse.json(asData);
} catch {
return NextResponse.json({ error: 'Failed to load as data' }, { status: 500 });
}
}

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'
import { loadCharacters } from '@/lib/characterLoader'
import { loadCharacters } from '@/lib/loader'
export async function GET(
req: NextRequest,

View File

@@ -1,4 +1,4 @@
import { loadCharacters } from "@/lib/characterLoader";
import { loadCharacters } from "@/lib/loader";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest, { params }: { params: Promise<{ locale: string }> }) {

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'
import { loadLightcones } from '@/lib/lighconeLoader'
import { loadLightcones } from '@/lib/loader'
export async function GET(
req: NextRequest,

View File

@@ -1,4 +1,4 @@
import { loadLightcones } from "@/lib/lighconeLoader";
import { loadLightcones } from "@/lib/loader";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest, { params }: { params: Promise<{ locale: string }> }) {

View File

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

View File

@@ -0,0 +1,20 @@
import { loadMOC } 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 mocIds = body.mocIds as string[];
const { locale } = await params;
if (!Array.isArray(mocIds) || mocIds.some(id => typeof id !== 'string')) {
return NextResponse.json({ error: 'Invalid mocIds' }, { status: 400 });
}
const mocData = await loadMOC(mocIds, locale);
return NextResponse.json(mocData);
} catch {
return NextResponse.json({ error: 'Failed to load moc data' }, { status: 500 });
}
}

View File

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

View File

@@ -0,0 +1,20 @@
import { loadPF } 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 pfIds = body.pfIds as string[];
const { locale } = await params;
if (!Array.isArray(pfIds) || pfIds.some(id => typeof id !== 'string')) {
return NextResponse.json({ error: 'Invalid pfIds' }, { status: 400 });
}
const pfData = await loadPF(pfIds, locale);
return NextResponse.json(pfData);
} catch {
return NextResponse.json({ error: 'Failed to load pf data' }, { status: 500 });
}
}

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'
import { loadRelics } from '@/lib/relicLoader'
import { loadRelics } from '@/lib/loader'
export async function GET(
req: NextRequest,

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { loadRelics } from "@/lib/relicLoader";
import { loadRelics } from "@/lib/loader";
export async function POST(request: NextRequest, { params }: { params: Promise<{ locale: string }> }) {
try {

View File

@@ -1,4 +1,4 @@
import { loadConfigMaze } from "@/lib/configMazeLoader";
import { loadConfigMaze } from "@/lib/loader";
import { NextResponse } from "next/server";
export async function GET() {

View File

@@ -1,4 +1,4 @@
import { loadMainAffix } from "@/lib/affixLoader";
import { loadMainAffix } from "@/lib/loader";
import { NextResponse } from "next/server";
export async function GET() {

View File

@@ -1,4 +1,4 @@
import { loadSubAffix } from "@/lib/affixLoader";
import { loadSubAffix } from "@/lib/loader";
import { NextResponse } from "next/server";
export async function GET() {

View File

@@ -9,6 +9,7 @@ import { getLocale, getMessages } from "next-intl/server";
import { ToastContainer } from 'react-toastify';
import AvatarBar from "@/components/avatarBar";
import ActionBar from "@/components/actionBar";
import QueryProviderWrapper from "@/components/queryProvider";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -39,25 +40,27 @@ export default async function RootLayout({
suppressHydrationWarning
>
<NextIntlClientProvider messages={messages}>
<ThemeProvider>
<ClientThemeWrapper>
<div className="min-h-screen w-full">
<Header />
<div className="grid grid-cols-12 w-full">
<div className="col-span-3 sticky top-0 self-start h-fit">
<AvatarBar />
</div>
<div className="col-span-9">
<ActionBar />
{children}
<QueryProviderWrapper>
<ThemeProvider>
<ClientThemeWrapper>
<div className="min-h-screen w-full">
<Header />
<div className="grid grid-cols-12 w-full">
<div className="col-span-3 sticky top-0 self-start h-fit">
<AvatarBar />
</div>
<div className="col-span-9">
<ActionBar />
{children}
</div>
</div>
<Footer />
</div>
<Footer />
</div>
</ClientThemeWrapper>
</ThemeProvider>
</ClientThemeWrapper>
</ThemeProvider>
</QueryProviderWrapper>
</NextIntlClientProvider>
<ToastContainer />
</body>

View File

@@ -24,7 +24,7 @@ export default function ActionBar() {
const { avatarSelected, listRawAvatar } = useListAvatarStore()
const { setListCopyAvatar } = useCopyProfileStore()
const transI18n = useTranslations("DataPage")
const { locale, setLocale } = useLocaleStore()
const { locale } = useLocaleStore()
const { isOpenCreateProfile, setIsOpenCreateProfile, isOpenCopy, setIsOpenCopy } = useModelStore()
const { avatars, setAvatar } = useUserDataStore()
const [profileName, setProfileName] = useState("");
@@ -106,7 +106,7 @@ export default function ActionBar() {
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenCreateProfile]);
}, [isOpenCopy, isOpenCreateProfile]);
const actionMove = (path: string) => {
router.push(`/${path}`)
@@ -162,7 +162,7 @@ export default function ActionBar() {
{avatarSelected && (
<>
<Image
src={"https://api.hakush.in/hsr/UI/element/" + avatarSelected.damageType.toLowerCase() + ".webp"}
src={ `/icon/${avatarSelected.damageType.toLowerCase()}.webp`}
alt={'fire'}
className="h-[40px] w-[40px] object-contain"
width={100}
@@ -209,7 +209,7 @@ export default function ActionBar() {
<li key={index} className="grid grid-cols-12">
<button
className={`col-span-8 btn btn-ghost`}
onClick={(e) => handleProfileSelect(index)}
onClick={() => handleProfileSelect(index)}
>
<span className="flex-1 truncate text-left">
{profile.profile_name}

View File

@@ -1,91 +1,31 @@
"use client"
import { getCharacterListApi, fetchCharactersByIdsNative, getConfigMazeApi, getLightconeListApi, fetchLightconesByIdsNative, fetchRelicsByIdsNative, getRelicSetListApi, getMainAffixApi, getSubAffixApi } from "@/lib/api"
import Image from "next/image"
import { useEffect, useState } from "react"
import CharacterCard from "../card/characterCard"
import useLocaleStore from "@/stores/localeStore"
import { listCurrentLanguageApi } from "@/lib/constant"
import useAvatarStore from "@/stores/avatarStore"
import useUserDataStore from "@/stores/userDataStore"
import { converterToAvatarStore, getAvatarNotExist } from "@/helper"
import useLightconeStore from "@/stores/lightconeStore"
import useRelicStore from "@/stores/relicStore"
import useAffixStore from "@/stores/affixStore"
import useMazeStore from "@/stores/mazeStore"
import { useTranslations } from "next-intl"
import { useFetchASData, useFetchAvatarData, useFetchConfigData, useFetchLightconeData, useFetchMOCData, useFetchMonsterData, useFetchPFData, useFetchRelicData } from "@/hooks"
export default function AvatarBar() {
const [listElement, setListElement] = useState<Record<string, boolean>>({ "fire": false, "ice": false, "imaginary": false, "physical": false, "quantum": false, "thunder": false, "wind": false })
const [listPath, setListPath] = useState<Record<string, boolean>>({ "knight": false, "mage": false, "priest": false, "rogue": false, "shaman": false, "warlock": false, "warrior": false, "memory": false })
const { listAvatar, setListAvatar, setAvatarSelected, setFilter, filter, setAllMapAvatarInfo, avatarSelected } = useAvatarStore()
const { setAvatars, avatars } = useUserDataStore()
const { listAvatar, setAvatarSelected, setFilter, filter } = useAvatarStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore()
const { setListLightcone, setAllMapLightconeInfo } = useLightconeStore()
const { setListRelic, setAllMapRelicInfo } = useRelicStore()
const { setMapMainAffix, setMapSubAffix } = useAffixStore()
const { setAllData } = useMazeStore()
useEffect(() => {
const fetchData = async () => {
// Maze
const maze = await getConfigMazeApi()
setAllData(maze)
// Affix
const mapMainAffix = await getMainAffixApi()
setMapMainAffix(mapMainAffix)
const mapSubAffix = await getSubAffixApi()
setMapSubAffix(mapSubAffix)
}
fetchData()
}, [])
useEffect(() => {
const fetchData = async () => {
// Avatar
const listAvatar = await getCharacterListApi()
listAvatar.sort((a, b) => {
const aHasRelease = typeof a.release === 'number';
const bHasRelease = typeof b.release === 'number';
if (!aHasRelease && !bHasRelease) return 0;
if (!aHasRelease) return -1;
if (!bHasRelease) return 1;
return b.release! - a.release!;
});
const mapAvatar = await fetchCharactersByIdsNative(listAvatar.map((item) => item.id), listCurrentLanguageApi[locale.toLowerCase()])
setListAvatar(listAvatar)
setAllMapAvatarInfo(mapAvatar)
const avatarStore = converterToAvatarStore(getAvatarNotExist())
if (Object.keys(avatarStore).length > 0) {
setAvatars({ ...avatars, ...avatarStore })
}
if (!avatarSelected) {
setAvatarSelected(listAvatar[0])
}
// Lightcone
const listLightcone = await getLightconeListApi()
listLightcone.sort((a, b) => Number(b.id) - Number(a.id))
setListLightcone(listLightcone)
const mapLightcone = await fetchLightconesByIdsNative(listLightcone.map((item) => item.id), listCurrentLanguageApi[locale.toLowerCase()])
setAllMapLightconeInfo(mapLightcone)
// Relic
const listRelic = await getRelicSetListApi()
setListRelic(listRelic)
const mapRelic = await fetchRelicsByIdsNative(listRelic.map((item) => item.id), listCurrentLanguageApi[locale.toLowerCase()])
setAllMapRelicInfo(mapRelic)
}
fetchData()
}, [locale])
useFetchConfigData()
useFetchAvatarData()
useFetchLightconeData()
useFetchRelicData()
useFetchMonsterData()
useFetchPFData()
useFetchMOCData()
useFetchASData()
useEffect(() => {
setFilter({ ...filter, locale: locale, element: Object.keys(listElement).filter((key) => listElement[key]), path: Object.keys(listPath).filter((key) => listPath[key]) })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [locale, listElement, listPath])
return (
@@ -103,7 +43,7 @@ export default function AvatarBar() {
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 lg:grid-cols-7 mb-1 mx-1 gap-2 w-full max-h-[17vh] min-h-[5vh] overflow-y-auto">
{Object.entries(listElement).map(([key, value], index) => (
{Object.keys(listElement).map((key, index) => (
<div
key={index}
onClick={() => {
@@ -113,7 +53,7 @@ export default function AvatarBar() {
style={{
backgroundColor: listElement[key] ? "#374151" : "#6B7280"
}}>
<Image src={"https://api.hakush.in/hsr/UI/element/" + key + ".webp"}
<Image src={ `/icon/${key}.webp`}
alt={key}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md"
width={200}
@@ -123,7 +63,7 @@ export default function AvatarBar() {
</div>
<div className="grid grid-cols-1 md:grid-cols-4 lg:grid-cols-8 mb-1 mx-1 gap-2 overflow-y-auto w-full max-h-[17vh] min-h-[5vh]">
{Object.entries(listPath).map(([key, value], index) => (
{Object.keys(listPath).map((key, index) => (
<div
key={index}
onClick={() => {
@@ -135,7 +75,7 @@ export default function AvatarBar() {
}}
>
<Image src={"https://api.hakush.in/hsr/UI/pathicon/" + key + ".webp"}
<Image src={`/icon/${key}.webp`}
alt={key}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md"
width={200}

View File

@@ -1,9 +1,8 @@
"use client"
import { useRouter } from 'next/navigation'
import useAvatarStore from "@/stores/avatarStore"
import useUserDataStore from "@/stores/userDataStore";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo } from "react";
import { motion } from "framer-motion";
import LightconeBar from '../lightconeBar'
import useLightconeStore from '@/stores/lightconeStore'
@@ -16,7 +15,6 @@ import useModelStore from '@/stores/modelStore';
import useMazeStore from '@/stores/mazeStore';
export default function AvatarInfo() {
const router = useRouter()
const { avatarSelected, mapAvatarInfo } = useAvatarStore()
const { Technique } = useMazeStore()
const { avatars, setAvatars, setAvatar } = useUserDataStore()
@@ -252,7 +250,7 @@ export default function AvatarInfo() {
className="select select-bordered select-secondary"
>
<option value="">{transI18n("origin")}</option>
{Object.entries(mapAvatarInfo[avatarSelected?.id || ""]?.Enhanced || {}).map(([key, value]) => (
{Object.keys(mapAvatarInfo[avatarSelected?.id || ""]?.Enhanced || {}).map((key) => (
<option key={key} value={key}>
{key}
</option>

View File

@@ -32,6 +32,7 @@ export default function CharacterCard({ data }: CharacterCardProps) {
width={376}
height={512}
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${data.id}.webp`}
priority={true}
className="w-full h-full rounded-md object-cover"
alt="ALT"
/>
@@ -39,14 +40,14 @@ export default function CharacterCard({ data }: CharacterCardProps) {
width={32}
height={32}
src={`https://api.hakush.in/hsr/UI/element/${data.damageType.toLowerCase()}.webp`}
src={`/icon/${data.damageType.toLowerCase()}.webp`}
className="absolute top-0 left-0 w-6 h-6 rounded-full"
alt={data.damageType.toLowerCase()}
/>
<Image
width={32}
height={32}
src={`https://api.hakush.in/hsr/UI/pathicon/${data.baseType.toLowerCase()}.webp`}
src={`/icon/${data.baseType.toLowerCase()}.webp`}
className="absolute top-0 right-0 w-6 h-6 rounded-full"
alt={data.baseType.toLowerCase()}
style={{

View File

@@ -1,11 +1,10 @@
"use client";
import React, { useState, useMemo } from 'react';
import React from 'react';
import { CharacterInfoCardType } from '@/types';
import { getNameChar, replaceByParam } from '@/helper';
import useLocaleStore from '@/stores/localeStore';
import useAvatarStore from '@/stores/avatarStore';
import useLightconeStore from '@/stores/lightconeStore';
import useRelicStore from '@/stores/relicStore';
import Image from 'next/image';
import ParseText from '../parseText';
@@ -14,7 +13,6 @@ export default function CharacterInfoCard({ character, selectedCharacters, onCha
const isSelected = selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id);
const { mapAvatarInfo } = useAvatarStore();
const { mapLightconeInfo } = useLightconeStore();
const { mapRelicInfo } = useRelicStore();
const { locale } = useLocaleStore();
return (
@@ -40,14 +38,14 @@ export default function CharacterInfoCard({ character, selectedCharacters, onCha
<Image
width={48}
height={48}
src={`https://api.hakush.in/hsr/UI/element/${mapAvatarInfo[character.avatar_id.toString()]?.DamageType.toLowerCase()}.webp`}
src={`/icon/${mapAvatarInfo[character.avatar_id.toString()]?.DamageType.toLowerCase()}.webp`}
className="absolute top-0 left-0 w-10 h-10 rounded-full"
alt={mapAvatarInfo[character.avatar_id.toString()]?.DamageType.toLowerCase()}
/>
<Image
width={48}
height={48}
src={`https://api.hakush.in/hsr/UI/pathicon/${mapAvatarInfo[character.avatar_id.toString()]?.BaseType.toLowerCase()}.webp`}
src={`/icon/${mapAvatarInfo[character.avatar_id.toString()]?.BaseType.toLowerCase()}.webp`}
className="absolute top-0 right-0 w-10 h-10 rounded-full"
alt={mapAvatarInfo[character.avatar_id.toString()]?.BaseType.toLowerCase()}
style={{

View File

@@ -1,6 +1,6 @@
"use client";
import { getNameLightcone } from '@/helper';
import { getLocaleName } from '@/helper';
import useLocaleStore from '@/stores/localeStore';
import { LightConeBasic } from '@/types';
import ParseText from '../parseText';
@@ -12,7 +12,7 @@ interface LightconeCardProps {
export default function LightconeCard({ data }: LightconeCardProps) {
const { locale } = useLocaleStore();
const text = getNameLightcone(locale, data)
const text = getLocaleName(locale, data)
return (
<li className="z-10 flex flex-col items-center rounded-md shadow-lg
bg-gradient-to-b from-customStart to-customEnd transform transition-transform duration-300

View File

@@ -1,10 +1,9 @@
"use client";
import React from 'react';
import { AvatarProfileCardType } from '@/types';
import useLocaleStore from '@/stores/localeStore';
import useAvatarStore from '@/stores/avatarStore';
import useLightconeStore from '@/stores/lightconeStore';
import useRelicStore from '@/stores/relicStore';
import Image from 'next/image';
import ParseText from '../parseText';
@@ -50,7 +49,7 @@ export default function ProfileCard({ profile, selectedProfile, onProfileToggle
{Object.keys(profile.relics).length > 0 && (
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
{Object.entries(profile.relics).map(([key, relic], index) => (
{Object.values(profile.relics).map((relic, index) => (
<div key={index} className="relative">
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50">
<Image

View File

@@ -1,4 +1,5 @@
import useLocaleStore from "@/stores/localeStore";
"use client";
import useUserDataStore from "@/stores/userDataStore";
import Image from "next/image";
import { useMemo } from "react";
@@ -26,31 +27,31 @@ const getRarityName = (slot: string) => {
);
case '2': return (
<div className="flex items-center gap-1">
<Image src="/relics/HAND.png" alt="Head" width={20} height={20} />
<Image src="/relics/HAND.png" alt="Hand" width={20} height={20} />
<h2>Hands</h2>
</div>
);
case '3': return (
<div className="flex items-center gap-1">
<Image src="/relics/BODY.png" alt="Head" width={20} height={20} />
<Image src="/relics/BODY.png" alt="Body" width={20} height={20} />
<h2>Body</h2>
</div>
);
case '4': return (
<div className="flex items-center gap-1">
<Image src="/relics/FOOT.png" alt="Head" width={20} height={20} />
<Image src="/relics/FOOT.png" alt="Foot" width={20} height={20} />
<h2>Feet</h2>
</div>
);
case '5': return (
<div className="flex items-center gap-1">
<Image src="/relics/OBJECT.png" alt="Head" width={20} height={20} />
<Image src="/relics/NECK.png" alt="Neck" width={20} height={20} />
<h2>Planar sphere</h2>
</div>
);
case '6': return (
<div className="flex items-center gap-1">
<Image src="/relics/NECK.png" alt="Head" width={20} height={20} />
<Image src="/relics/OBJECT.png" alt="Object" width={20} height={20} />
<h2>Link rope</h2>
</div>
);
@@ -58,7 +59,6 @@ const getRarityName = (slot: string) => {
}
};
export default function RelicCard({ slot, avatarId }: RelicCardProps) {
const { locale } = useLocaleStore();
const { avatars } = useUserDataStore()
const relicDetail = useMemo(() => {

View File

@@ -1,4 +1,5 @@
"use client";
import React, { useState, useRef} from 'react';
export default function ShowCaseInfo() {

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client"
import { replaceByParam } from "@/helper";
import useListAvatarStore from "@/stores/avatarStore";
@@ -30,8 +31,8 @@ export default function EidolonsInfo() {
className="flex flex-col items-center cursor-pointer hover:scale-105"
onClick={() => {
let newRank = Number(key)
if (Number(key) == 1 && avatars[avatarSelected?.id || ""]?.data?.rank == 1) {
newRank = 0
if (avatars[avatarSelected?.id || ""]?.data?.rank == Number(key)) {
newRank = Number(key) - 1
}
setAvatars({ ...avatars, [avatarSelected?.id || ""]: { ...avatars[avatarSelected?.id || ""], data: { ...avatars[avatarSelected?.id || ""].data, rank: newRank } } })
}}

View File

@@ -2,7 +2,6 @@
import { connectToPS, downloadJson, syncDataToPS } from "@/helper";
import { converterToFreeSRJson } from "@/helper/converterToFreeSRJson";
import { useChangeTheme } from "@/hooks/useChangeTheme";
import { listCurrentLanguage } from "@/lib/constant";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
@@ -18,6 +17,7 @@ import { toast } from "react-toastify";
import { micsSchema } from "@/zod";
import useConnectStore from "@/stores/connectStore";
import useGlobalStore from "@/stores/globalStore";
import MonsterBar from "../monsterBar";
const themes = [
{ label: "Winter" },
@@ -29,15 +29,34 @@ const themes = [
export default function Header() {
const { changeTheme } = useChangeTheme()
const { locale, setLocale } = useLocaleStore()
const { avatars, battle_config, setAvatars, setBattleConfig } = useUserDataStore()
const {
avatars,
battle_type,
setAvatars,
setBattleType,
moc_config,
pf_config,
as_config,
ce_config,
setMocConfig,
setPfConfig,
setAsConfig,
setCeConfig,
} = useUserDataStore()
const router = useRouter()
const transI18n = useTranslations("DataPage")
const { setIsOpenImport, isOpenImport } = useModelStore()
const {
setIsOpenImport,
isOpenImport,
setIsOpenMonster,
isOpenMonster,
setIsOpenConnect,
isOpenConnect
} = useModelStore()
const [message, setMessage] = useState({ text: '', type: '' });
const [importModal, setImportModal] = useState("enka");
const [isModalOpen, setIsModalOpen] = useState(false);
const {
connectionType,
privateType,
@@ -84,17 +103,14 @@ export default function Header() {
}
const handleShow = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
setIsModalOpen(true);
modal.showModal();
}
};
// Close modal handler
const handleCloseModal = (modalId: string) => {
setIsModalOpen(false);
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.close()
@@ -107,16 +123,25 @@ export default function Header() {
handleCloseModal("import_modal");
return;
}
if (!isOpenMonster) {
handleCloseModal("monster_modal");
return;
}
if (!isOpenConnect) {
handleCloseModal("connect_modal");
return;
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
handleCloseModal("connect_modal");
handleCloseModal("import_modal");
handleCloseModal("monster_modal");
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isModalOpen, isOpenImport]);
}, [isOpenImport, isOpenMonster, isOpenConnect]);
const handleImportDatabase = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -139,9 +164,13 @@ export default function Header() {
const data = JSON.parse(e.target?.result as string);
const parsed = micsSchema.parse(data)
setAvatars(parsed.avatars)
setBattleConfig(parsed.battle_config)
setBattleType(parsed.battle_type)
setMocConfig(parsed.moc_config)
setPfConfig(parsed.pf_config)
setAsConfig(parsed.as_config)
setCeConfig(parsed.ce_config)
toast.success(transI18n("importDatabaseSuccess"))
} catch (error) {
} catch {
toast.error(transI18n("fileMustBeAValidJsonFile"))
}
};
@@ -201,25 +230,31 @@ 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_config))}>{transI18n("freeSr")}</a></li>
<li><a onClick={() => downloadJson("database-data", { avatars: avatars, battle_config: battle_config })}>{transI18n("database")}</a></li>
<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>
</ul>
</details>
</li>
<li>
<button
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => handleShow("connection_modal")}
onClick={() => {
setIsOpenConnect(true)
handleShow("connect_modal")
}}
>
{transI18n("connectSetting")}
</button>
</li>
<li>
<button
disabled
onClick={() => {
setIsOpenMonster(true)
handleShow("monster_modal")
}}
className="disabled px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
>
{transI18n("monsterSetting")} ({transI18n("comingSoon")})
{transI18n("monsterSetting")}
</button>
</li>
</ul>
@@ -279,25 +314,31 @@ 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_config))}>{transI18n("freeSr")}</a></li>
<li><a onClick={() => downloadJson("database-data", { avatars: avatars, battle_config: battle_config })}>{transI18n("database")}</a></li>
<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>
</ul>
</details>
</li>
<li>
<button
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => handleShow("connection_modal")}
onClick={() => {
setIsOpenConnect(true)
handleShow("connect_modal")
}}
>
{transI18n("connectSetting")}
</button>
</li>
<li>
<button
disabled
className="disabled:opacity-50 px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => {
setIsOpenMonster(true)
handleShow("monster_modal")
}}
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
>
{transI18n("monsterSetting")} ({transI18n("comingSoon")})
{transI18n("monsterSetting")}
</button>
</li>
</ul>
@@ -409,14 +450,17 @@ export default function Header() {
</Link>
</div>
<dialog id="connection_modal" className="modal sm:modal-middle backdrop-blur-sm">
<dialog id="connect_modal" className="modal sm:modal-middle backdrop-blur-sm">
<div className="modal-box w-11/12 max-w-7xl bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={() => handleCloseModal("connection_modal")}
onClick={() => {
setIsOpenConnect(false)
handleCloseModal("connect_modal")
}}
>
</motion.button>
@@ -597,6 +641,32 @@ export default function Header() {
{importModal === "freesr" && <FreeSRImport />}
</div>
</dialog>
<dialog id="monster_modal" className="modal sm:backdrop-blur-sm md:backdrop-blur-none">
<div className="modal-box w-11/12 max-w-max bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={() => {
handleCloseModal("monster_modal")
setIsOpenMonster(false)
}}
>
</motion.button>
</div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
{transI18n("monsterSetting")}
</h3>
</div>
<MonsterBar />
</div>
</dialog>
</div>
)
}

View File

@@ -1,16 +1,15 @@
"use client"
import useModelStore from "@/stores/modelStore";
import useUserDataStore from "@/stores/userDataStore";
import { useEffect, useState } from "react";
import useCopyProfileStore from "@/stores/copyProfile";
import ProfileCard from "../card/profileCard";
import { AvatarProfileCardType, AvatarProfileStore } from "@/types";
import Image from "next/image";
import SelectCustom from "../select";
import useListAvatarStore from "@/stores/avatarStore";
import { getNameChar } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import { useTranslations } from "next-intl";
import SelectCustomImage from "../select/customSelectImage";
export default function CopyImport() {
const { avatars, setAvatar } = useUserDataStore();
@@ -50,6 +49,7 @@ export default function CopyImport() {
element: Object.keys(listElement).filter((key) => listElement[key]),
rarity: Object.keys(listRank).filter((key) => listRank[key])
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [listPath, listRank, locale])
const clearSelection = () => {
@@ -101,7 +101,7 @@ export default function CopyImport() {
return;
}
const newListProfile = avatars[avatarCopySelected.id.toString()].profileList.map((profile, index) => {
const newListProfile = avatars[avatarCopySelected.id.toString()].profileList.map((profile) => {
if (!profile.lightcone?.item_id && Object.keys(profile.relics).length == 0) {
return null;
}
@@ -149,7 +149,7 @@ export default function CopyImport() {
backgroundColor: listPath[key] ? "#374151" : "#6B7280"
}}>
<Image
src={`https://api.hakush.in/hsr/UI/pathicon/${key}.webp`}
src={`/icon/${key}.webp`}
alt={key}
className="h-[32px] w-[32px] object-contain rounded-md"
width={200}
@@ -174,7 +174,7 @@ export default function CopyImport() {
backgroundColor: listElement[key] ? "#374151" : "#6B7280"
}}>
<Image
src={`https://api.hakush.in/hsr/UI/element/${key}.webp`}
src={`/icon/${key}.webp`}
alt={key}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md"
width={200}
@@ -210,7 +210,7 @@ export default function CopyImport() {
<div>
<div>{transI18n("characterName")}</div>
{listCopyAvatar.length > 0 && (
<SelectCustom
<SelectCustomImage
customSet={listCopyAvatar.map((avatar) => ({
value: avatar.id.toString(),
label: getNameChar(locale, avatar),

View File

@@ -2,7 +2,7 @@
import { useState } from "react";
import CharacterInfoCard from "../card/characterInfoCard";
import useEnkaStore from "@/stores/enkaStore";
import { SendDataThroughProxy } from "@/lib/api";
import { SendDataThroughProxy } from "@/lib/api/api";
import { CharacterInfoCardType, EnkaResponse } from "@/types";
import useUserDataStore from "@/stores/userDataStore";
import { converterOneEnkaDataToAvatarStore } from "@/helper";
@@ -21,7 +21,7 @@ export default function EnkaImport() {
} = useEnkaStore();
const transI18n = useTranslations("DataPage")
const { avatars, setAvatar } = useUserDataStore();
const { isOpenImport, setIsOpenImport } = useModelStore()
const { setIsOpenImport } = useModelStore()
const [isLoading, setIsLoading] = useState(false)
const [Error, setError] = useState("")
@@ -191,7 +191,7 @@ export default function EnkaImport() {
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{enkaData?.detailInfo.avatarDetailList.map((character, index) => (
{enkaData?.detailInfo.avatarDetailList.map((character) => (
<CharacterInfoCard
key={character.avatarId}
character={{

View File

@@ -12,8 +12,8 @@ import { converterOneFreeSRDataToAvatarStore } from "@/helper";
import { useTranslations } from "next-intl";
export default function FreeSRImport() {
const { avatars, setAvatar, setBattleConfig } = useUserDataStore();
const { isOpenImport, setIsOpenImport } = useModelStore()
const { avatars, setAvatar } = useUserDataStore();
const { setIsOpenImport } = useModelStore()
const [isLoading, setIsLoading] = useState(false)
const [Error, setError] = useState("")
const { freeSRData, setFreeSRData, selectedCharacters, setSelectedCharacters } = useFreeSRStore()
@@ -33,7 +33,7 @@ export default function FreeSRImport() {
const selectAll = () => {
if (freeSRData) {
setSelectedCharacters(Object.entries(freeSRData?.avatars).map(([key, character]) => {
setSelectedCharacters(Object.values(freeSRData?.avatars).map((character) => {
const lightcone = freeSRData.lightcones.find((lightcone) => lightcone.equip_avatar === character.avatar_id)
const relics = freeSRData.relics.filter((relic) => relic.equip_avatar === character.avatar_id)
return {
@@ -84,7 +84,7 @@ export default function FreeSRImport() {
setFreeSRData(parsed)
setError("")
setSelectedCharacters(Object.entries(parsed?.avatars || {}).map(([key, character]) => {
setSelectedCharacters(Object.values(parsed?.avatars || {}).map((character) => {
const lightcone = parsed?.lightcones.find((lightcone) => lightcone.equip_avatar === character.avatar_id)
const relics = parsed?.relics.filter((relic) => relic.equip_avatar === character.avatar_id)
return {
@@ -104,7 +104,7 @@ export default function FreeSRImport() {
})) ?? [],
} as CharacterInfoCardType
}));
} catch (error) {
} catch {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("fileIsNotAValidFreeSRJsonFile"))
@@ -128,8 +128,8 @@ export default function FreeSRImport() {
setError("");
const listAvatars = { ...avatars }
const filterData = Object.entries(freeSRData?.avatars || {}).filter(([key, character]) => selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id))
filterData.forEach(([key, character]) => {
const filterData = Object.values(freeSRData?.avatars || {}).filter((character) => selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id))
filterData.forEach((character) => {
const newAvatar = { ...listAvatars[character.avatar_id] }
if (Object.keys(newAvatar).length !== 0) {
newAvatar.level = character.level
@@ -147,7 +147,6 @@ export default function FreeSRImport() {
}
})
setBattleConfig(freeSRData?.battle_config)
setIsOpenImport(false)
toast.success(transI18n("importFreeSRDataSuccess"))
}
@@ -200,7 +199,7 @@ export default function FreeSRImport() {
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{Object.entries(freeSRData?.avatars || {}).map(([key, character]) => {
{Object.values(freeSRData?.avatars || {}).map((character) => {
const lightcone = freeSRData?.lightcones.find((lightcone) => lightcone.equip_avatar === character.avatar_id)
const relics = freeSRData?.relics.filter((relic) => relic.equip_avatar === character.avatar_id)
return (

View File

@@ -26,6 +26,7 @@ export default function LightconeBar() {
path: Object.keys(listPath).filter((key) => listPath[key]),
rarity: Object.keys(listRank).filter((key) => listRank[key])
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [listPath, listRank, locale])
return (
@@ -49,7 +50,7 @@ export default function LightconeBar() {
<div className="grid grid-cols-12 mt-1 w-full">
<div className="grid justify-items-start col-span-8">
<div className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-8 mb-1 mx-1 gap-2">
{Object.entries(listPath).map(([key, value], index) => (
{Object.keys(listPath).map((key, index) => (
<div
key={index}
onClick={() => {
@@ -59,8 +60,8 @@ export default function LightconeBar() {
style={{
backgroundColor: listPath[key] ? "#374151" : "#6B7280"
}}>
<Image src={"https://api.hakush.in/hsr/UI/pathicon/" + key + ".webp"}
alt={key}
<Image src={`/icon/${key}.webp`}
alt={key}
className="h-[32px] w-[32px] object-contain rounded-md "
width={200}
height={200} />
@@ -71,7 +72,7 @@ export default function LightconeBar() {
<div className="grid justify-items-end col-span-4 w-full">
<div className="grid grid-cols-1 sm:grid-cols-3 mb-1 mx-1 gap-2">
{Object.entries(listRank).map(([key, value], index) => (
{Object.keys(listRank).map((key, index) => (
<div
key={index}
onClick={() => {

View File

@@ -0,0 +1,338 @@
"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 { MonsterStore } from "@/types";
import cloneDeep from 'lodash/cloneDeep'
import useMazeStore from "@/stores/mazeStore";
import { useTranslations } from "next-intl";
export default function AsBar() {
const { ASEvent, mapASInfo } = useEventStore()
const { listMonster } = useMonsterStore()
const { locale } = useLocaleStore()
const {
as_config,
setAsConfig
} = useUserDataStore()
const { AS } = useMazeStore()
const transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => {
return mapASInfo[as_config.event_id.toString()]?.Level.find((as) => as.Id === as_config.challenge_id)
}, [as_config, mapASInfo])
const eventSelected = useMemo(() => {
return mapASInfo[as_config.event_id.toString()]
}, [as_config, mapASInfo])
const buffList = useMemo(() => {
const challenge = AS[as_config.event_id.toString()];
if (!challenge) return { buffList: [], buffId: [] };
if (as_config.floor_side === "Upper" || as_config.floor_side === "Upper -> Lower") {
return {
buffList: eventSelected?.BuffList1 ?? [],
buffId: challenge.buff_1 ?? [],
};
}
if (as_config.floor_side === "Lower" || as_config.floor_side === "Lower -> Upper") {
return {
buffList: eventSelected?.BuffList2 ?? [],
buffId: challenge.buff_2 ?? [],
};
}
return { buffList: [], buffId: [] };
}, [AS, as_config.event_id, as_config.floor_side, eventSelected?.BuffList1, eventSelected?.BuffList2]);
useEffect(() => {
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.blessings = []
if (as_config.buff_id !== 0) {
newBattleConfig.blessings.push({
id: as_config.buff_id,
level: 1
})
}
if (AS[as_config.challenge_id.toString()]) {
newBattleConfig.blessings.push({
id: Number(AS[as_config.challenge_id.toString()].maze_buff),
level: 1
})
}
newBattleConfig.monsters = []
newBattleConfig.stage_id = 0
if ((as_config.floor_side === "Upper" || as_config.floor_side === "Upper -> Lower") && challenge.EventIDList1.length > 0) {
newBattleConfig.stage_id = challenge.EventIDList1[0].StageID
for (const wave of challenge.EventIDList1[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList1[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
if ((as_config.floor_side === "Lower" || as_config.floor_side === "Lower -> Upper") && challenge.EventIDList2.length > 0) {
newBattleConfig.stage_id = challenge.EventIDList2[0].StageID
for (const wave of challenge.EventIDList2[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList2[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
if (as_config.floor_side === "Lower -> Upper" && challenge.EventIDList1.length > 0) {
for (const wave of challenge.EventIDList1[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList1[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
} else if (as_config.floor_side === "Upper -> Lower" && challenge.EventIDList2.length > 0) {
for (const wave of challenge.EventIDList2[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList2[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
setAsConfig(newBattleConfig)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
as_config.event_id,
as_config.challenge_id,
as_config.floor_side,
as_config.buff_id,
mapASInfo,
AS,
])
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={ASEvent.filter(as => as.lang.get(locale)).map((as) => ({
id: as.id,
name: getLocaleName(locale, as),
time: `${as.begin} - ${as.end}`,
}))}
excludeSet={[]}
selectedCustomSet={as_config.event_id.toString()}
placeholder={transI18n("selectASEvent")}
setSelectedCustomSet={(id) => setAsConfig({ ...as_config, event_id: Number(id), challenge_id: 0, buff_id: 0 })}
/>
</div>
{/* Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
</label>
<select
value={as_config.challenge_id}
className="select select-success"
onChange={(e) => setAsConfig({ ...as_config, challenge_id: Number(e.target.value) })}
>
<option value={0} disabled={true}>{transI18n("selectFloor")}</option>
{mapASInfo[as_config.event_id.toString()]?.Level.map((as) => (
<option key={as.Id} value={as.Id}>{as.Id % 10}</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("side")}:{" "}</span>
</label>
<select
value={as_config.floor_side}
className="select select-success"
onChange={(e) => setAsConfig({ ...as_config, floor_side: e.target.value })}
>
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
<option value="Upper">{transI18n("upper")}</option>
<option value="Lower">{transI18n("lower")}</option>
<option value="Upper -> Lower">{transI18n("upperToLower")}</option>
<option value="Lower -> Upper">{transI18n("lowerToUpper")}</option>
</select>
</div>
</div>
{eventSelected && (
<div className="mb-4 w-full">
<SelectCustomText
customSet={
Array.isArray(buffList?.buffList) && Array.isArray(buffList?.buffId)
? buffList.buffList.map((buff, index) => ({
id: buffList.buffId?.[index]?.toString() || "",
name: buff?.Name || "",
description: replaceByParam(buff?.Desc || "", buff?.Param || []),
}))
: []
}
excludeSet={[]}
selectedCustomSet={as_config?.buff_id?.toString()}
placeholder={transI18n("selectBuff")}
setSelectedCustomSet={(id) => setAsConfig({ ...as_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>
{eventSelected && eventSelected.Buff?.Name ? (
<div
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
eventSelected.Buff?.Desc || "",
eventSelected.Buff?.Param || []
)
}}
/>
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* First Half */}
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{transI18n("firstHalfEnemies")}</h2>
{challengeSelected && challengeSelected?.EventIDList1?.length > 0 && challengeSelected?.EventIDList1[0].MonsterList.map((wave, 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">
{Object.values(wave).map((waveValue, 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">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(waveValue))?.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?.EventIDList1[0].Level}</div>
<div className="flex items-center space-x-1 mt-1">
{listMonster
.find((monster) => monster.child.includes(waveValue))
?.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>
{/* Second Half */}
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{transI18n("secondHalfEnemies")}</h2>
{challengeSelected && challengeSelected?.EventIDList2?.length > 0 && challengeSelected?.EventIDList2[0].MonsterList.map((wave, 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">
{Object.values(wave).map((waveValue, 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">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(waveValue))?.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?.EventIDList1[0].Level}</div>
<div className="flex items-center space-x-1 mt-1">
{listMonster
.find((monster) => monster.child.includes(waveValue))
?.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

@@ -0,0 +1,364 @@
"use client";
import React, { useEffect, useMemo, useState } from "react";
import {
Plus,
Trash2,
ChevronUp,
ChevronDown,
Search,
} from "lucide-react";
import useMazeStore from "@/stores/mazeStore";
import useUserDataStore from "@/stores/userDataStore";
import useMonsterStore from "@/stores/monsterStore";
import useLocaleStore from "@/stores/localeStore";
import { getLocaleName } from "@/helper";
import Image from "next/image";
import { MonsterBasic } from "@/types";
import { cloneDeep } from "lodash";
import { useTranslations } from "next-intl";
export default function CeBar() {
const [searchTerm, setSearchTerm] = useState("");
const [showSearchWaveId, setShowSearchWaveId] = useState<number | null>(null);
const { Stage } = useMazeStore()
const { ce_config, setCeConfig } = useUserDataStore()
const { listMonster } = useMonsterStore()
const { locale } = useLocaleStore()
const filteredMonsters = useMemo(() => {
const newlistMonster = new Set<MonsterBasic>()
for (const monster of listMonster) {
if (getLocaleName(locale, monster).toLowerCase().includes(searchTerm.toLowerCase())) {
newlistMonster.add(monster)
}
if (monster.id.toLowerCase().includes(searchTerm.toLowerCase())) {
newlistMonster.add(monster)
}
}
return Array.from(newlistMonster)
}, [listMonster, locale, searchTerm]);
const transI18n = useTranslations("DataPage")
const [showSearchStage, setShowSearchStage] = useState(false)
const [stageSearchTerm, setStageSearchTerm] = useState("")
const [stagePage, setStagePage] = useState(1)
const pageSize = 30
const stageList = useMemo(() => Object.values(Stage).map((stage) => ({
id: stage.stage_id.toString(),
name: `${stage.stage_type} (${stage.stage_id})`,
})), [Stage])
const filteredStages = useMemo(() => stageList.filter((s) =>
s.name.toLowerCase().includes(stageSearchTerm.toLowerCase())
), [stageList, stageSearchTerm])
const paginatedStages = useMemo(() => filteredStages.slice(
(stagePage - 1) * pageSize,
stagePage * pageSize
), [filteredStages, stagePage, pageSize])
const hasMorePages = useMemo(() => stagePage * pageSize < filteredStages.length, [stagePage, filteredStages])
useEffect(() => {
setStagePage(1)
}, [stageSearchTerm])
return (
<div className="container mx-auto p-6 z-4" onClick={() => {
setShowSearchWaveId(null)
setShowSearchStage(false)
}}>
<div className="mb-4 w-full relative">
<div className="flex items-center justify-center gap-2">
<Search className="w-6 h-6" />
<button
className="btn btn-outline w-[95%] text-left"
onClick={(e) => {
e.stopPropagation()
setShowSearchStage(true)
}}
>
<span className="text-left"> {transI18n("stage")}: {stageList.find((s) => s.id === ce_config.stage_id.toString())?.name || transI18n("selectStage")}</span>
</button>
</div>
{showSearchStage && (
<div className="absolute top-full mt-2 w-full z-50 border bg-base-200 border-slate-600 rounded-lg p-4 shadow-lg">
<div className="flex items-center gap-2 mb-2">
<Search className="w-4 h-4 text-slate-400" />
<input
type="text"
placeholder={transI18n("searchStage")}
className="input input-sm w-full placeholder-slate-400"
value={stageSearchTerm}
onChange={(e) => setStageSearchTerm(e.target.value)}
autoFocus
/>
</div>
<div className="max-h-60 overflow-y-auto space-y-1">
{paginatedStages.length > 0 ? (
<>
{paginatedStages.map((stage) => (
<div
key={stage.id}
className="p-2 hover:bg-primary/20 rounded cursor-pointer"
onClick={() => {
setCeConfig({ ...ce_config, stage_id: Number(stage.id), cycle_count: 30 })
setShowSearchStage(false)
setStageSearchTerm("")
}}
>
{stage.name}
</div>
))}
</>
) : (
<div className="text-sm text-center py-4">{transI18n("noStageFound")}</div>
)}
</div>
{paginatedStages.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<button
disabled={stagePage === 1}
className="btn btn-sm btn-outline btn-success mt-2"
onClick={(e) => {
e.stopPropagation()
setStagePage(stagePage - 1)
}}
>
{transI18n("previous")}
</button>
<button
disabled={!hasMorePages}
className="btn btn-sm btn-outline btn-success mt-2"
onClick={(e) => {
e.stopPropagation()
setStagePage(stagePage + 1)
}}
>
{transI18n("next")}
</button>
</div>
)}
</div>
)}
</div>
<div className="relative max-w-7xl mx-auto space-y-6">
{ce_config.monsters.map((wave, waveIndex) => (
<div key={waveIndex} className="card border border-slate-700/50 ">
<div className="card-body p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-white">{transI18n("wave")} {waveIndex + 1}</h2>
<div className="flex gap-2">
<button
disabled={waveIndex === 0}
onClick={(e) => {
e.stopPropagation()
const newCeConfig = cloneDeep(ce_config)
const waves = newCeConfig.monsters
const temp = waves[waveIndex - 1]
waves[waveIndex - 1] = waves[waveIndex]
waves[waveIndex] = temp
setCeConfig(newCeConfig)
}}
className="btn btn-sm btn-success"
>
<ChevronUp className="w-4 h-4" />
</button>
<button
disabled={waveIndex === ce_config.monsters.length - 1}
onClick={(e) => {
e.stopPropagation()
const newCeConfig = cloneDeep(ce_config)
const waves = newCeConfig.monsters
const temp = waves[waveIndex + 1]
waves[waveIndex + 1] = waves[waveIndex]
waves[waveIndex] = temp
setCeConfig(newCeConfig)
}}
className="btn btn-sm btn-success">
<ChevronDown className="w-4 h-4" />
</button>
<button
onClick={(e) => {
e.stopPropagation()
const newCeConfig = cloneDeep(ce_config)
newCeConfig.monsters.splice(waveIndex, 1)
setCeConfig(newCeConfig)
}}
className="btn btn-sm btn-error">
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 w-full h-full">
{wave.map((member, memberIndex) => (
<div key={memberIndex} className="relative group z-7 w-full h-full">
<div className="card border hover:border-slate-500 transition-colors w-full h-full">
<div className="card-body p-4">
<button
className="btn btn-xs btn-error absolute -top-2 -right-2 opacity-50 group-hover:opacity-100 transition-opacity"
onClick={() => {
const newCeConfig = cloneDeep(ce_config)
newCeConfig.monsters[waveIndex].splice(memberIndex, 1)
setCeConfig(newCeConfig)
}}
>
<Trash2 className="w-3 h-3" />
</button>
<div className="flex justify-center">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster2) => monster2.child.includes(member.monster_id))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className=" object-contain w-20 h-20 overflow-hidden"
/>
</div>
<div className="flex justify-center gap-1 mb-2">
{listMonster
.find((monster) => monster.child.includes(member.monster_id))
?.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 className="text-center flex flex-col items-center justify-center">
<div className="text-sm font-medium">
{getLocaleName(locale, listMonster.find((monster) => monster.child.includes(member.monster_id)))}
</div>
<div className="flex items-center gap-1 mt-1">
<span className="text-sm">Lv.</span>
<input
type="number"
className="w-16 text-center input input-sm"
value={member.level}
onChange={(e) => {
try {
Number(e.target.value)
} catch {
return;
}
if (Number(e.target.value) < 1 || Number(e.target.value) > 95) return;
const newCeConfig = cloneDeep(ce_config)
newCeConfig.monsters[waveIndex][memberIndex].level = Number(e.target.value)
setCeConfig(newCeConfig)
}}
/>
</div>
</div>
</div>
</div>
</div>
))}
{/* Add Member Button + Search */}
<div className="relative flex items-start justify-center w-full h-full">
<button
className="btn btn-outline btn-primary w-full h-full border-dashed"
onClick={(e) => {
e.stopPropagation();
setShowSearchWaveId(waveIndex)
}}
>
<Plus className="w-8 h-8" />
</button>
{showSearchWaveId === waveIndex && (
<div className="absolute top-full mt-2 w-72 z-50 border bg-base-200 border-slate-600 rounded-lg p-4 shadow-lg">
<div className="flex items-center gap-2 mb-2">
<Search className="w-4 h-4" />
<input
type="text"
placeholder={transI18n("searchMonster")}
className="input input-sm w-full placeholder-slate-400"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
autoFocus
/>
</div>
<div className="max-h-60 overflow-y-auto space-y-1">
{filteredMonsters.length > 0 ? (
filteredMonsters.map((monster) => (
<div
key={monster.id}
className="flex items-center gap-2 p-2 hover:bg-success/40 rounded cursor-pointer"
onClick={() => {
const newCeConfig = cloneDeep(ce_config)
newCeConfig.monsters[waveIndex].push({
monster_id: Number(monster.id),
level: 95,
amount: 1,
})
setCeConfig(newCeConfig)
setShowSearchWaveId(null);
}}
>
<div className="relative w-8 h-8 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster2) => monster2.child.includes(Number(monster.id)))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
</div>
<span className="">{getLocaleName(locale, monster)} {`(${monster.id})`}</span>
</div>
))
) : (
<div className="text-sm text-center py-4">
{transI18n("noMonstersFound")}
</div>
)}
</div>
</div>
)}
</div>
</div>
</div>
</div>
))}
{/* Add New Wave Button */}
<div className="card backdrop-blur-sm border border-slate-700/50 border-dashed">
<div className="card-body p-8">
<button
onClick={() => {
const newCeConfig = cloneDeep(ce_config)
newCeConfig.monsters.push([])
setCeConfig(newCeConfig)
}}
className="btn btn-primary btn-lg w-full">
<Plus className="w-6 h-6 mr-2" />
{transI18n("addNewWave")}
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,87 @@
import MocBar from "./moc";
import useUserDataStore from "@/stores/userDataStore";
import PfBar from "./pf";
import Image from "next/image";
import AsBar from "./as";
import { useTranslations } from "next-intl";
import CeBar from "./ce";
export default function MonsterBar() {
const { battle_type, setBattleType } = useUserDataStore()
const transI18n = useTranslations("DataPage")
const navItems = [
{ name: transI18n("memoryOfChaos"), icon: 'AbyssIcon01', value: 'MOC' },
{ name: transI18n("pureFiction"), icon: 'ChallengeStory', value: 'PF' },
{ name: transI18n("apocalypticShadow"), icon: 'ChallengeBoss', value: 'AS' },
{ name: transI18n("customEnemy"), icon: 'MonsterIcon', value: 'CE' },
{ name: transI18n("simulatedUniverse"), icon: 'SimulatedUniverse', value: 'SU' },
];
return (
<div className="min-h-screen">
{/* 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">
{/* Navigation Tabs */}
<div className="flex space-x-1">
{navItems.map((item) => (
<button
key={item.name}
onClick={() => setBattleType(item.value.toUpperCase())}
className={`px-4 py-2 rounded-lg transition-all cursor-pointer duration-300 flex items-center space-x-2 ${battle_type.toUpperCase() === item.value.toUpperCase()
? 'bg-success/30 shadow-lg'
: 'bg-base-200/20 hover:bg-base-200/40 '
}`}
>
<span
style={
battle_type.toUpperCase() === item.value.toUpperCase()
? {
filter:
'brightness(0) saturate(100%) invert(63%) sepia(78%) saturate(643%) hue-rotate(1deg) brightness(93%) contrast(89%)',
}
: undefined
}
>
<Image src={`/icon/${item.icon}.webp`} alt={item.name} width={24} height={24} />
</span>
<span
style={
battle_type.toUpperCase() === item.value.toUpperCase()
? {
filter:
'brightness(0) saturate(100%) invert(63%) sepia(78%) saturate(643%) hue-rotate(1deg) brightness(93%) contrast(89%)',
}
: undefined
}
>
<span className="font-medium">{item.name}</span>
</span>
</button>
))}
</div>
</div>
</div>
</nav>
{(battle_type.toUpperCase() === "DEFAULT" || battle_type.toUpperCase() === "") && (
<div className="container mx-auto px-4 py-8 text-center font-bold text-3xl">
{transI18n("noEventSelected")}
</div>
)}
{battle_type.toUpperCase() === 'MOC' && <MocBar />}
{battle_type.toUpperCase() === 'PF' && <PfBar />}
{battle_type.toUpperCase() === 'AS' && <AsBar />}
{battle_type.toUpperCase() === 'CE' && <CeBar />}
{battle_type.toUpperCase() === 'SU' && (
<div className="container mx-auto px-4 py-8 text-center font-bold text-3xl">
{transI18n("comingSoon")}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,321 @@
"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 { MonsterStore } from "@/types";
import cloneDeep from 'lodash/cloneDeep'
import useMazeStore from "@/stores/mazeStore";
import { useTranslations } from "next-intl";
export default function MocBar() {
const { MOCEvent, mapMOCInfo } = useEventStore()
const { listMonster } = useMonsterStore()
const { locale } = useLocaleStore()
const {
moc_config,
setMocConfig
} = useUserDataStore()
const { MOC } = useMazeStore()
const transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => {
return mapMOCInfo[moc_config.event_id.toString()]?.find((moc) => moc.Id === moc_config.challenge_id)
}, [moc_config, mapMOCInfo])
useEffect(() => {
const challenge = mapMOCInfo[moc_config.event_id.toString()]?.find((moc) => moc.Id === moc_config.challenge_id)
if (moc_config.event_id !== 0 && moc_config.challenge_id !== 0 && challenge) {
const newBattleConfig = cloneDeep(moc_config)
newBattleConfig.cycle_count = 0
if (moc_config.use_cycle_count) {
newBattleConfig.cycle_count = challenge.Countdown
}
newBattleConfig.blessings = []
if (moc_config.use_turbulence_buff && MOC[moc_config.challenge_id.toString()]) {
newBattleConfig.blessings.push({
id: Number(MOC[moc_config.challenge_id.toString()].maze_buff),
level: 1
})
}
newBattleConfig.monsters = []
newBattleConfig.stage_id = 0
if ((moc_config.floor_side === "Upper" || moc_config.floor_side === "Upper -> Lower") && challenge.EventIDList1.length > 0) {
newBattleConfig.stage_id = challenge.EventIDList1[0].StageID
for (const wave of challenge.EventIDList1[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList1[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
if ((moc_config.floor_side === "Lower" || moc_config.floor_side === "Lower -> Upper") && challenge.EventIDList2.length > 0) {
newBattleConfig.stage_id = challenge.EventIDList2[0].StageID
for (const wave of challenge.EventIDList2[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList2[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
if (moc_config.floor_side === "Lower -> Upper" && challenge.EventIDList1.length > 0) {
for (const wave of challenge.EventIDList1[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList1[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
} else if (moc_config.floor_side === "Upper -> Lower" && challenge.EventIDList2.length > 0) {
for (const wave of challenge.EventIDList2[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList2[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
setMocConfig(newBattleConfig)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
moc_config.event_id,
moc_config.challenge_id,
moc_config.floor_side,
mapMOCInfo,
MOC,
moc_config.use_cycle_count,
moc_config.use_turbulence_buff,
])
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={MOCEvent.map((moc) => ({
id: moc.id,
name: getLocaleName(locale, moc),
time: `${moc.begin} - ${moc.end}`,
}))}
excludeSet={[]}
selectedCustomSet={moc_config.event_id.toString()}
placeholder={transI18n("selectMOCEvent")}
setSelectedCustomSet={(id) => setMocConfig({ ...moc_config, event_id: Number(id), challenge_id: 0 })}
/>
</div>
{/* Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
</label>
<select
value={moc_config.challenge_id}
className="select select-success"
onChange={(e) => setMocConfig({ ...moc_config, challenge_id: Number(e.target.value) })}
>
<option value={0} disabled={true}>Select a Floor</option>
{mapMOCInfo[moc_config.event_id.toString()]?.map((moc) => (
<option key={moc.Id} value={moc.Id}>{moc.Id % 100}</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("side")}:{" "}</span>
</label>
<select
value={moc_config.floor_side}
className="select select-success"
onChange={(e) => setMocConfig({ ...moc_config, floor_side: e.target.value })}
>
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
<option value="Upper">{transI18n("upper")}</option>
<option value="Lower">{transI18n("lower")}</option>
<option value="Upper -> Lower">{transI18n("upperToLower")}</option>
<option value="Lower -> Upper">{transI18n("lowerToUpper")}</option>
</select>
</div>
<div className="flex items-center gap-2">
<label className="label cursor-pointer">
<span
onClick={() => setMocConfig({ ...moc_config, use_cycle_count: !moc_config.use_cycle_count })}
className="label-text font-bold text-success cursor-pointer"
>
{transI18n("useCycleCount")} {" "}
</span>
<input
type="checkbox"
checked={moc_config.use_cycle_count}
onChange={(e) => setMocConfig({ ...moc_config, use_cycle_count: e.target.checked })}
className="checkbox checkbox-primary"
/>
</label>
</div>
</div>
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<div className="flex items-center space-x-2 mb-2">
<input
type="checkbox"
checked={moc_config.use_turbulence_buff}
onChange={(e) => setMocConfig({ ...moc_config, use_turbulence_buff: e.target.checked })}
className="checkbox checkbox-primary"
/>
<span
onClick={() => setMocConfig({ ...moc_config, use_turbulence_buff: !moc_config.use_turbulence_buff })}
className="font-bold text-success cursor-pointer">
{transI18n("useTurbulenceBuff")}
</span>
</div>
{challengeSelected && challengeSelected?.Desc ? (
<div
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
challengeSelected?.Desc || "",
challengeSelected?.Param || []
)
}}
/>
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* First Half */}
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{transI18n("firstHalfEnemies")}</h2>
{challengeSelected && challengeSelected?.EventIDList1?.length > 0 && challengeSelected?.EventIDList1[0].MonsterList.map((wave, 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">
{Object.values(wave).map((waveValue, 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">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(waveValue))?.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?.EventIDList1[0].Level}</div>
<div className="flex items-center space-x-1 mt-1">
{listMonster
.find((monster) => monster.child.includes(waveValue))
?.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>
{/* Second Half */}
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{transI18n("secondHalfEnemies")}</h2>
{challengeSelected && challengeSelected?.EventIDList2?.length > 0 && challengeSelected?.EventIDList2[0].MonsterList.map((wave, 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">
{Object.values(wave).map((waveValue, 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">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(waveValue))?.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?.EventIDList1[0].Level}</div>
<div className="flex items-center space-x-1 mt-1">
{listMonster
.find((monster) => monster.child.includes(waveValue))
?.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

@@ -0,0 +1,330 @@
"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 { MonsterStore } from "@/types";
import cloneDeep from 'lodash/cloneDeep'
import useMazeStore from "@/stores/mazeStore";
import { useTranslations } from "next-intl";
export default function PfBar() {
const { PFEvent, mapPFInfo } = useEventStore()
const { listMonster } = useMonsterStore()
const { locale } = useLocaleStore()
const {
pf_config,
setPfConfig
} = useUserDataStore()
const { PF } = useMazeStore()
const transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => {
return mapPFInfo[pf_config.event_id.toString()]?.Level.find((pf) => pf.Id === pf_config.challenge_id)
}, [pf_config, mapPFInfo])
const eventSelected = useMemo(() => {
return mapPFInfo[pf_config.event_id.toString()]
}, [pf_config, mapPFInfo])
useEffect(() => {
const challenge = mapPFInfo[pf_config.event_id.toString()]?.Level.find((pf) => pf.Id === pf_config.challenge_id)
if (pf_config.event_id !== 0 && pf_config.challenge_id !== 0 && challenge) {
const newBattleConfig = cloneDeep(pf_config)
newBattleConfig.cycle_count = 0
newBattleConfig.blessings = []
if (pf_config.buff_id !== 0) {
newBattleConfig.blessings.push({
id: pf_config.buff_id,
level: 1
})
}
if (PF[pf_config.challenge_id.toString()]) {
newBattleConfig.blessings.push({
id: Number(PF[pf_config.challenge_id.toString()].maze_buff),
level: 1
})
}
newBattleConfig.monsters = []
newBattleConfig.stage_id = 0
if ((pf_config.floor_side === "Upper" || pf_config.floor_side === "Upper -> Lower" ) && challenge.EventIDList1.length > 0 ) {
newBattleConfig.stage_id = challenge.EventIDList1[0].StageID
for (const wave of challenge.EventIDList1[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList1[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
if ((pf_config.floor_side === "Lower" || pf_config.floor_side === "Lower -> Upper") && challenge.EventIDList2.length > 0) {
newBattleConfig.stage_id = challenge.EventIDList2[0].StageID
for (const wave of challenge.EventIDList2[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList2[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
if (pf_config.floor_side === "Lower -> Upper" && challenge.EventIDList1.length > 0) {
for (const wave of challenge.EventIDList1[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList1[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
} else if (pf_config.floor_side === "Upper -> Lower" && challenge.EventIDList2.length > 0) {
for (const wave of challenge.EventIDList2[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList2[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
setPfConfig(newBattleConfig)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
pf_config.event_id,
pf_config.challenge_id,
pf_config.floor_side,
pf_config.buff_id,
mapPFInfo,
PF,
])
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={PFEvent.map((pf) => ({
id: pf.id,
name: getLocaleName(locale, pf),
time: `${pf.begin} - ${pf.end}`,
}))}
excludeSet={[]}
selectedCustomSet={pf_config.event_id.toString()}
placeholder={transI18n("selectPFEvent")}
setSelectedCustomSet={(id) => setPfConfig({ ...pf_config, event_id: Number(id), challenge_id: 0, buff_id: 0 })}
/>
</div>
{/* Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
</label>
<select
value={pf_config.challenge_id}
className="select select-success"
onChange={(e) => setPfConfig({ ...pf_config, challenge_id: Number(e.target.value) })}
>
<option value={0} disabled={true}>{transI18n("selectFloor")}</option>
{mapPFInfo[pf_config.event_id.toString()]?.Level.map((pf) => (
<option key={pf.Id} value={pf.Id}>{pf.Id % 10}</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("side")}:{" "}</span>
</label>
<select
value={pf_config.floor_side}
className="select select-success"
onChange={(e) => setPfConfig({ ...pf_config, floor_side: e.target.value })}
>
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
<option value="Upper">{transI18n("upper")}</option>
<option value="Lower">{transI18n("lower")}</option>
<option value="Upper -> Lower">{transI18n("upperToLower")}</option>
<option value="Lower -> Upper">{transI18n("lowerToUpper")}</option>
</select>
</div>
</div>
{eventSelected && (
<div className="mb-4 w-full">
<SelectCustomText
customSet={eventSelected.Option.map((buff, index) => ({
id: PF[eventSelected.Id.toString()]?.buff[index]?.toString(),
name: buff.Name,
description: replaceByParam(buff.Desc, buff.Param || []),
}))}
excludeSet={[]}
selectedCustomSet={pf_config?.buff_id?.toString()}
placeholder={transI18n("selectBuff")}
setSelectedCustomSet={(id) => setPfConfig({ ...pf_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>
{eventSelected && eventSelected.SubOption.length > 0 ? (
eventSelected.SubOption.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>
))
) : eventSelected && eventSelected.SubOption.length === 0 ? (
<div
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
eventSelected.Buff?.Desc || "",
eventSelected.Buff?.Param || []
)
}}
/>
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* First Half */}
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{transI18n("firstHalfEnemies")}</h2>
{challengeSelected && Object.values(challengeSelected.InfiniteList1).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">
<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?.EventIDList1[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>
{/* Second Half */}
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{transI18n("secondHalfEnemies")}</h2>
{challengeSelected && Object.values(challengeSelected?.InfiniteList2).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">
<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?.EventIDList1[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

@@ -0,0 +1,12 @@
"use client";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import React from 'react';
export default function QueryProviderWrapper({ children }: { children: React.ReactNode }) {
const [queryClient] = React.useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}

View File

@@ -3,7 +3,7 @@ import useRelicStore from '@/stores/relicStore';
import useUserDataStore from '@/stores/userDataStore';
import { AffixDetail, RelicDetail } from '@/types';
import React, { useEffect, useMemo } from 'react';
import SelectCustom from '../select';
import SelectCustomImage from '../select/customSelectImage';
import { calcAffixBonus, randomPartition, randomStep, replaceByParam } from '@/helper';
import useAffixStore from '@/stores/affixStore';
import { mapingStats } from '@/lib/constant';
@@ -12,6 +12,7 @@ import useModelStore from '@/stores/modelStore';
import useRelicMakerStore from '@/stores/relicMakerStore';
import { toast } from 'react-toastify';
import { useTranslations } from 'next-intl';
import cloneDeep from 'lodash/cloneDeep'
export default function RelicMaker() {
const { avatars, setAvatars } = useUserDataStore()
@@ -77,7 +78,7 @@ export default function RelicMaker() {
const mainProp = mainAffixMap[selectedMainStat]?.property;
if (!mainProp) return;
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
const newSubAffixes = cloneDeep(listSelectedSubStats);
let updated = false;
for (let i = 0; i < newSubAffixes.length; i++) {
@@ -91,6 +92,7 @@ export default function RelicMaker() {
}
if (updated) setListSelectedSubStats(newSubAffixes);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedMainStat, mapSubAffix, mapMainAffix, selectedRelicSlot]);
const exSubAffixOptions = useMemo(() => {
@@ -129,7 +131,7 @@ export default function RelicMaker() {
}, [mapMainAffix, selectedRelicSlot, selectedMainStat, selectedRelicLevel]);
const handleSubStatChange = (key: string, index: number, rollCount: number, stepCount: number) => {
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
const newSubAffixes = cloneDeep(listSelectedSubStats);
if (!subAffixOptions[key]) {
newSubAffixes[index].affixId = "";
newSubAffixes[index].property = "";
@@ -153,8 +155,8 @@ export default function RelicMaker() {
const keys = Object.keys(preSelectedSubStats[index]);
if (keys.length <= 1) return;
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
const listHistory = preSelectedSubStats[index].map(item => ({ ...item }));
const newSubAffixes = cloneDeep(listSelectedSubStats);
const listHistory = cloneDeep(preSelectedSubStats[index]);
const secondLastKey = listHistory.length - 2;
const preSubAffixes = { ...listHistory[secondLastKey] };
newSubAffixes[index].rollCount = preSubAffixes.rollCount;
@@ -164,7 +166,7 @@ export default function RelicMaker() {
};
const resetSubStat = (index: number) => {
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
const newSubAffixes = cloneDeep(listSelectedSubStats);
resetHistory(index);
newSubAffixes[index].affixId = "";
newSubAffixes[index].property = "";
@@ -174,7 +176,7 @@ export default function RelicMaker() {
};
const randomizeStats = () => {
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
const newSubAffixes = cloneDeep(listSelectedSubStats);
const exKeys = Object.keys(exSubAffixOptions);
for (let i = 0; i < newSubAffixes.length; i++) {
const keys = Object.keys(subAffixOptions).filter((key) => !exKeys.includes(key));
@@ -194,7 +196,7 @@ export default function RelicMaker() {
};
const randomizeRolls = () => {
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
const newSubAffixes = cloneDeep(listSelectedSubStats);
const randomRolls = randomPartition(9, listSelectedSubStats.length);
for (let i = 0; i < listSelectedSubStats.length; i++) {
newSubAffixes[i].rollCount = randomRolls[i];
@@ -252,7 +254,7 @@ export default function RelicMaker() {
{/* Main Stat */}
<div>
<label className="block text-lg font-medium mb-2">{transI18n("mainStat")}</label>
<SelectCustom
<SelectCustomImage
customSet={Object.entries(mapMainAffix["5" + selectedRelicSlot] || {}).map(([key, value]) => ({
value: key,
label: mapingStats[value.property].name + " " + mapingStats[value.property].unit,
@@ -267,7 +269,7 @@ export default function RelicMaker() {
{/* Relic Set Selection */}
<div>
<label className="block text-lg font-medium mb-2">{transI18n("set")}</label>
<SelectCustom
<SelectCustomImage
customSet={Object.entries(relicSets).map(([key, value]) => ({
value: key,
label: value.Name,
@@ -356,7 +358,7 @@ export default function RelicMaker() {
{/* Stat Selection */}
<div className="col-span-8">
<SelectCustom
<SelectCustomImage
customSet={Object.entries(subAffixOptions).map(([key, value]) => ({
value: key,
label: mapingStats[value.property].name + " " + mapingStats[value.property].unit,

View File

@@ -1,11 +1,8 @@
"use client"
;
import { useRouter } from 'next/navigation'
import { useCallback, useEffect, useMemo, useState } from "react";
"use client";
import { useCallback, useEffect, useMemo } from "react";
import RelicMaker from "../relicBar";
import { motion } from "framer-motion";
import useUserDataStore from "@/stores/userDataStore";
import useLocaleStore from "@/stores/localeStore";
import { useTranslations } from "next-intl";
import RelicCard from "../card/relicCard";
import useAvatarStore from "@/stores/avatarStore";
@@ -16,7 +13,6 @@ import useRelicMakerStore from '@/stores/relicMakerStore';
import useAffixStore from '@/stores/affixStore';
export default function RelicsInfo() {
const router = useRouter()
const { avatars, setAvatars } = useUserDataStore()
const { avatarSelected } = useAvatarStore()
const {
@@ -32,7 +28,7 @@ export default function RelicsInfo() {
const { mapSubAffix } = useAffixStore()
const { isOpenRelic, setIsOpenRelic } = useModelStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore();
const { mapRelicInfo } = useRelicStore()
const handleShow = (modalId: string) => {
@@ -67,6 +63,7 @@ export default function RelicsInfo() {
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpenRelic]);
const getRelic = useCallback((slot: string) => {
@@ -124,7 +121,7 @@ export default function RelicsInfo() {
const avatar = avatars[avatarSelected?.id || ""];
const relicCount: { [key: string]: number } = {};
if (avatar) {
for (const [slot, relic] of Object.entries(avatar.profileList[avatar.profileSelect].relics)) {
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
if (relicCount[relic.relic_set_id]) {
relicCount[relic.relic_set_id]++;
} else {
@@ -152,7 +149,7 @@ export default function RelicsInfo() {
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-gradient-to-b from-primary to-primary/50 rounded-full"></div>
Equipment
{transI18n("relics")}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-2xl mx-auto">
@@ -170,16 +167,14 @@ export default function RelicsInfo() {
/>
</div>
{/* Action buttons - chỉ hiện khi hover */}
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
{/* Nút chuyển relic */}
<button
onClick={(e) => {
e.stopPropagation()
handlerChangeRelic(item)
}}
className="btn btn-info p-1.5 rounded-full shadow-lg transition-colors duration-200"
title="Chuyển relic"
title={transI18n("changeRelic")}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
@@ -190,12 +185,12 @@ export default function RelicsInfo() {
<button
onClick={(e) => {
e.stopPropagation()
if (window.confirm(`Bạn có chắc muốn xóa relic ở slot ${item}?`)) {
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${item}?`)) {
handlerDeleteRelic(item)
}
}}
className="btn btn-error p-1.5 rounded-full shadow-lg transition-colors duration-200"
title="Xóa relic"
title={transI18n("deleteRelic")}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
@@ -216,7 +211,7 @@ export default function RelicsInfo() {
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h3 className="flex items-center gap-2 text-xl font-bold mb-4 text-base-content">
<div className="w-2 h-6 bg-gradient-to-b from-primary to-primary/50 rounded-full"></div>
Set Effects
{transI18n("setEffects")}
</h3>
<div className="space-y-6">

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import Select, { SingleValue } from 'react-select'
import Image from 'next/image'
@@ -18,7 +19,7 @@ type SelectCustomProp = {
setSelectedCustomSet: (value: string) => void
}
export default function SelectCustom({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
export default function SelectCustomImage({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
const options: SelectOption[] = customSet
const { locale } = useLocaleStore()
const customStyles = {
@@ -27,22 +28,24 @@ export default function SelectCustom({ customSet, excludeSet, selectedCustomSet,
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px'
padding: '8px',
backgroundColor: 'transparent',
}),
singleValue: (provided: any) => ({
...provided,
display: 'flex',
alignItems: 'center',
gap: '8px'
gap: '8px',
backgroundColor: 'transparent',
}),
menuPortal: (provided: any) => ({ ...provided, zIndex: 9999 }),
menu: (provided: any) => ({ ...provided, zIndex: 9999 })
}
const formatOptionLabel = (option: SelectOption) => (
<div className="flex items-center gap-1 bg-slate-400 w-full h-full z-50">
<Image src={option.imageUrl} alt="" width={125} height={125} className="w-8 h-8 object-contain" />
<ParseText className='text-black font-bold' text={option.label} locale={locale} />
<div className="flex items-center gap-1 w-full h-full z-50">
<Image src={option.imageUrl} alt="" width={125} height={125} className="w-8 h-8 object-contain bg-warning-content rounded-full" />
<ParseText className='font-bold text-warning-content' text={option.label} locale={locale} />
</div>
)
@@ -52,7 +55,6 @@ export default function SelectCustom({ customSet, excludeSet, selectedCustomSet,
value={options.find(opt => {
return opt.value === selectedCustomSet}) || null}
onChange={(selected: SingleValue<SelectOption>) => {
console.log(selected);
setSelectedCustomSet(selected?.value || '')
}}
formatOptionLabel={formatOptionLabel}

View File

@@ -0,0 +1,83 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import Select, { SingleValue } from 'react-select'
import { replaceByParam } from '@/helper'
export type SelectOption = {
id: string
name: string
time?: string
description?: string
}
type SelectCustomProp = {
customSet: SelectOption[]
excludeSet: SelectOption[]
selectedCustomSet: string
placeholder: string
setSelectedCustomSet: (value: string) => void
}
export default function SelectCustomText({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
const options: SelectOption[] = customSet
const customStyles = {
option: (provided: any) => ({
...provided,
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px',
backgroundColor: 'transparent',
}),
singleValue: (provided: any) => ({
...provided,
display: 'flex',
alignItems: 'center',
gap: '8px'
}),
menuPortal: (provided: any) => ({ ...provided, zIndex: 9999 }),
menu: (provided: any) => ({ ...provided, zIndex: 9999 })
}
const formatOptionLabel = (option: SelectOption) => (
<div className="flex flex-col gap-1 w-full h-full z-50 text-warning-content ">
<div
className="font-bold text-lg"
dangerouslySetInnerHTML={{
__html: replaceByParam(
option.name,
[]
)
}}
/>
{option.time && <div className='text-base'>{option.time}</div>}
{option.description && <div
className="text-base"
dangerouslySetInnerHTML={{
__html: option.description
}}
/>}
</div>
)
return (
<Select
options={options.filter(opt => !excludeSet.some(ex => ex.id === opt.id))}
value={options.find(opt => {
return opt.id === selectedCustomSet
}) || null}
onChange={(selected: SingleValue<SelectOption>) => {
setSelectedCustomSet(selected?.id || '')
}}
formatOptionLabel={formatOptionLabel}
styles={customStyles}
placeholder={placeholder}
className="my-react-select-container"
classNamePrefix="my-react-select"
isSearchable
isClearable
/>
)
}

View File

@@ -1,9 +1,9 @@
"use client"
import { SendDataThroughProxy, SendDataToServer } from "@/lib/api"
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 { pSResponseSchema } from "@/zod"
export const connectToPS = async (): Promise<{ success: boolean, message: string }> => {
const {
@@ -27,7 +27,7 @@ export const connectToPS = async (): Promise<{ success: boolean, message: string
} else if (response.error) {
return { success: false, message: response.error }
} else {
const parsed = psResponseSchema.safeParse(response.data)
const parsed = pSResponseSchema.safeParse(response.data)
if (!parsed.success) {
return { success: false, message: "Invalid response schema" }
}
@@ -53,8 +53,8 @@ export const syncDataToPS = async (): Promise<{ success: boolean, message: strin
password
} = useConnectStore.getState()
const {avatars, battle_config} = useUserDataStore.getState()
const data = converterToFreeSRJson(avatars, battle_config)
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)
let urlQuery = serverUrl
if (!urlQuery.startsWith("http://") && !urlQuery.startsWith("https://")) {
@@ -69,7 +69,7 @@ export const syncDataToPS = async (): Promise<{ success: boolean, message: strin
} else if (response.error) {
return { success: false, message: response.error }
} else {
const parsed = psResponseSchema.safeParse(response.data)
const parsed = pSResponseSchema.safeParse(response.data)
if (!parsed.success) {
return { success: false, message: "Invalid response schema" }
}

View File

@@ -1,4 +1,4 @@
import { CharacterBasic, CharacterBasicRaw, LightConeBasic, LightConeBasicRaw, RelicBasic, RelicBasicEffect, RelicBasicRaw } from "@/types";
import { CharacterBasic, CharacterBasicRaw, EventBasic, EventBasicRaw, LightConeBasic, LightConeBasicRaw, MonsterBasic, MonsterBasicRaw, RelicBasic, RelicBasicEffect, RelicBasicRaw } from "@/types";
export function convertRelicSet(id: string, item: RelicBasicRaw): RelicBasic {
let lang = new Map<string, string>([
@@ -86,4 +86,43 @@ export function convertAvatar(id: string, item: CharacterBasicRaw): CharacterBas
return result;
}
export function convertEvent(id: string, item: EventBasicRaw): EventBasic {
let 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 {
let 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;
}

View File

@@ -1,18 +1,69 @@
import { AvatarJson, AvatarStore, BattleConfigJson, BattleConfigStore, FreeSRJson, LightconeJson, RelicJson } from "@/types";
import useUserDataStore from "@/stores/userDataStore";
import { ASConfigStore, AvatarJson, AvatarStore, BattleConfigJson, CEConfigStore, FreeSRJson, LightconeJson, MOCConfigStore, PFConfigStore, RelicJson } from "@/types";
export function converterToFreeSRJson(avatars: Record<string, AvatarStore>, battle_config: BattleConfigStore): FreeSRJson {
export function converterToFreeSRJson(
avatars: Record<string, AvatarStore>,
battle_type: string,
moc_config: MOCConfigStore,
pf_config: PFConfigStore,
as_config: ASConfigStore,
ce_config: CEConfigStore,
): FreeSRJson {
const lightcones: LightconeJson[] = []
const relics: RelicJson[] = []
const battleJson: BattleConfigJson = {
battle_type: battle_config.battle_type,
blessings: battle_config.blessings,
custom_stats: battle_config.custom_stats,
cycle_count: battle_config.cycle_count,
stage_id: battle_config.stage_id,
path_resonance_id: battle_config.path_resonance_id,
monsters: battle_config.monsters,
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 {
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

View File

@@ -1,5 +1,5 @@
import { listCurrentLanguage } from "@/lib/constant";
import { CharacterBasic, LightConeBasic } from "@/types";
import { CharacterBasic, EventBasic, LightConeBasic, MonsterBasic } from "@/types";
export function getNameChar(locale: string, data: CharacterBasic | undefined): string {
@@ -22,7 +22,7 @@ export function getNameChar(locale: string, data: CharacterBasic | undefined): s
return text
}
export function getNameLightcone(locale: string, data: LightConeBasic | undefined): string {
export function getLocaleName(locale: string, data: LightConeBasic | EventBasic | MonsterBasic | undefined): string {
if (!data) {
return ""
}

9
src/hooks/index.ts Normal file
View File

@@ -0,0 +1,9 @@
export * from "./useFetchConfigData";
export * from "./useChangeTheme";
export * from "./useFetchAvatarData";
export * from "./useFetchLightconeData";
export * from "./useFetchRelicData";
export * from "./useFetchMonsterData";
export * from "./useFetchPFData";
export * from "./useFetchMOCData";
export * from "./useFetchASData";

View File

@@ -0,0 +1,66 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { fetchASByIdsNative, getASEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import { listCurrentLanguageApi } from '@/lib/constant'
import useLocaleStore from '@/stores/localeStore'
import { toast } from 'react-toastify'
import useEventStore from '@/stores/eventStore'
import { EventStageDetail } from '@/types'
export const useFetchASData = () => {
const { setASEvent, setMapASInfo } = useEventStore()
const { locale } = useLocaleStore()
const { data: dataAS, error: errorAS } = useQuery({
queryKey: ['asData'],
queryFn: getASEventListApi,
select: (data) => data.sort((a, b) => Number(b.id) - Number(a.id)),
staleTime: 1000 * 60 * 5,
})
const { data: dataASInfo, error: errorASInfo } = useQuery({
queryKey: ['asInfoData', locale],
queryFn: () =>
fetchASByIdsNative(
dataAS!.map((item) => item.id),
listCurrentLanguageApi[locale.toLowerCase()]
),
staleTime: 1000 * 60 * 5,
enabled: !!dataAS,
select: (data) => {
const newData = { ...data }
for (const key in newData) {
for (const item of newData[key].Level) {
item.EventIDList1 = item.EventIDList1.map((event: EventStageDetail) => ({
...event,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
MonsterList: event.MonsterList.map(({ $type, ...rest }) => rest)
}))
item.EventIDList2 = item.EventIDList2.map((event: EventStageDetail) => ({
...event,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
MonsterList: event.MonsterList.map(({ $type, ...rest }) => rest)
}))
}
}
return newData
},
});
useEffect(() => {
if (dataAS && !errorAS) {
setASEvent(dataAS)
} else if (errorAS) {
toast.error("Failed to load AS data")
}
}, [dataAS, errorAS, setASEvent])
useEffect(() => {
if (dataASInfo && !errorASInfo) {
setMapASInfo(dataASInfo)
} else if (errorASInfo) {
toast.error("Failed to load AS info data")
}
}, [dataASInfo, errorASInfo, setMapASInfo])
}

View File

@@ -0,0 +1,61 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { fetchCharactersByIdsNative, getCharacterListApi } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useAvatarStore from '@/stores/avatarStore'
import { listCurrentLanguageApi } from '@/lib/constant'
import useLocaleStore from '@/stores/localeStore'
import { converterToAvatarStore, getAvatarNotExist } from '@/helper'
import useUserDataStore from '@/stores/userDataStore'
export const useFetchAvatarData = () => {
const { setAvatars, avatars } = useUserDataStore()
const { setListAvatar, setAllMapAvatarInfo, setAvatarSelected } = useAvatarStore()
const { locale } = useLocaleStore()
const { data: dataAvatar, error: errorAvatar } = useQuery({
queryKey: ['avatarData'],
queryFn: getCharacterListApi,
select: (data) => data.sort((a, b) => {
const aHasRelease = typeof a.release === 'number';
const bHasRelease = typeof b.release === 'number';
if (!aHasRelease && !bHasRelease) return 0;
if (!aHasRelease) return -1;
if (!bHasRelease) return 1;
return b.release! - a.release!;
}),
staleTime: 1000 * 60 * 5,
})
const { data: dataAvatarInfo, error: errorAvatarInfo } = useQuery({
queryKey: ['avatarInfoData', locale],
queryFn: () =>
fetchCharactersByIdsNative(
dataAvatar!.map((item) => item.id),
listCurrentLanguageApi[locale.toLowerCase()]
),
staleTime: 1000 * 60 * 5,
enabled: !!dataAvatar,
});
useEffect(() => {
if (dataAvatar && !errorAvatar) {
setListAvatar(dataAvatar)
setAvatarSelected(dataAvatar[0])
const avatarStore = converterToAvatarStore(getAvatarNotExist())
if (Object.keys(avatarStore).length > 0) {
setAvatars({ ...avatars, ...avatarStore })
}
} else if (errorAvatar) {
toast.error("Failed to load avatar data")
}
}, [dataAvatar, errorAvatar, setAvatarSelected, setAvatars, setListAvatar, avatars])
useEffect(() => {
if (dataAvatarInfo && !errorAvatarInfo) {
setAllMapAvatarInfo(dataAvatarInfo)
} else if (errorAvatarInfo) {
toast.error("Failed to load avatar info data")
}
}, [dataAvatarInfo, errorAvatarInfo, setAllMapAvatarInfo])
}

View File

@@ -0,0 +1,37 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getConfigMazeApi, getMainAffixApi, getSubAffixApi } from '@/lib/api'
import useAffixStore from '@/stores/affixStore'
import useMazeStore from '@/stores/mazeStore'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
export const useFetchConfigData = () => {
const { setMapMainAffix, setMapSubAffix } = useAffixStore()
const { setAllMazeData } = useMazeStore()
const { data, error } = useQuery({
queryKey: ['initialConfigData'],
queryFn: async () => {
const [maze, main, sub] = await Promise.all([
getConfigMazeApi(),
getMainAffixApi(),
getSubAffixApi(),
])
return { maze, main, sub }
},
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (data && !error) {
setAllMazeData(data.maze)
setMapMainAffix(data.main)
setMapSubAffix(data.sub)
}
else if (error) {
toast.error("Failed to load initial config data")
}
}, [data, error, setAllMazeData, setMapMainAffix, setMapSubAffix])
}

View File

@@ -0,0 +1,47 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { fetchLightconesByIdsNative, getLightconeListApi } from '@/lib/api'
import { useEffect } from 'react'
import useLightconeStore from '@/stores/lightconeStore'
import { listCurrentLanguageApi } from '@/lib/constant'
import useLocaleStore from '@/stores/localeStore'
import { toast } from 'react-toastify'
export const useFetchLightconeData = () => {
const { setListLightcone, setAllMapLightconeInfo } = useLightconeStore()
const { locale } = useLocaleStore()
const { data: dataLightcone, error: errorLightcone } = useQuery({
queryKey: ['lightconeData'],
queryFn: getLightconeListApi,
select: (data) => data.sort((a, b) => Number(b.id) - Number(a.id)),
staleTime: 1000 * 60 * 5,
})
const { data: dataLightconeInfo, error: errorLightconeInfo } = useQuery({
queryKey: ['lightconeInfoData', locale],
queryFn: () =>
fetchLightconesByIdsNative(
dataLightcone!.map((item) => item.id),
listCurrentLanguageApi[locale.toLowerCase()]
),
staleTime: 1000 * 60 * 5,
enabled: !!dataLightcone,
});
useEffect(() => {
if (dataLightcone && !errorLightcone) {
setListLightcone(dataLightcone)
} else if (errorLightcone) {
toast.error("Failed to load lightcone data")
}
}, [dataLightcone, errorLightcone, setListLightcone])
useEffect(() => {
if (dataLightconeInfo && !errorLightconeInfo) {
setAllMapLightconeInfo(dataLightconeInfo)
} else if (errorLightconeInfo) {
toast.error("Failed to load lightcone info data")
}
}, [dataLightconeInfo, errorLightconeInfo, setAllMapLightconeInfo])
}

View File

@@ -0,0 +1,68 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { fetchMOCByIdsNative, getMOCEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import { listCurrentLanguageApi } from '@/lib/constant'
import useLocaleStore from '@/stores/localeStore'
import { toast } from 'react-toastify'
import useEventStore from '@/stores/eventStore'
import { EventStageDetail, MocDetail } from '@/types'
export const useFetchMOCData = () => {
const { setMOCEvent, setMapMOCInfo } = useEventStore()
const { locale } = useLocaleStore()
const { data: dataMOC, error: errorMOC } = useQuery({
queryKey: ['mocData'],
queryFn: getMOCEventListApi,
select: (data) => data.sort((a, b) => Number(b.id) - Number(a.id)),
staleTime: 1000 * 60 * 5,
})
const { data: dataMOCInfo, error: errorMOCInfo } = useQuery({
queryKey: ['mocInfoData', locale],
queryFn: async () => {
const result = await fetchMOCByIdsNative(
dataMOC!.map((item) => item.id),
listCurrentLanguageApi[locale.toLowerCase()]
);
return result;
},
staleTime: 1000 * 60 * 5,
select: (data) => {
const newData = { ...data }
for (const key in newData) {
for (const item of newData[key]) {
item.EventIDList1 = item.EventIDList1.map((event: EventStageDetail) => ({
...event,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
MonsterList: event.MonsterList.map(({ $type, ...rest }) => rest)
}))
item.EventIDList2 = item.EventIDList2.map((event: EventStageDetail) => ({
...event,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
MonsterList: event.MonsterList.map(({ $type, ...rest }) => rest)
}))
}
}
return newData
},
enabled: !!dataMOC,
});
useEffect(() => {
if (dataMOC && !errorMOC) {
setMOCEvent(dataMOC)
} else if (errorMOC) {
toast.error("Failed to load MOC data")
}
}, [dataMOC, errorMOC, setMOCEvent])
useEffect(() => {
if (dataMOCInfo && !errorMOCInfo) {
setMapMOCInfo(dataMOCInfo as Record<string, MocDetail[]>)
} else if (errorMOCInfo) {
toast.error("Failed to load MOC info data")
}
}, [dataMOCInfo, errorMOCInfo, setMapMOCInfo])
}

View File

@@ -0,0 +1,37 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getMonsterDetailApi, getMonsterListApi } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useMonsterStore from '@/stores/monsterStore'
export const useFetchMonsterData = () => {
const { setAllMapMonster, setListMonster, setAllMapMonsterInfo } = useMonsterStore()
const { data: dataMonster, error: errorMonster } = useQuery({
queryKey: ['monsterData'],
queryFn: getMonsterListApi,
staleTime: 1000 * 60 * 5,
})
const { data: dataMonsterInfo, error: errorMonsterInfo } = useQuery({
queryKey: ['monsterInfoData'],
queryFn: getMonsterDetailApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (dataMonster && !errorMonster) {
setListMonster(dataMonster.list.sort((a, b) => Number(b.id) - Number(a.id)))
setAllMapMonster(dataMonster.map)
} else if (errorMonster) {
toast.error("Failed to load monster data")
}
}, [dataMonster, errorMonster, setAllMapMonster, setListMonster])
useEffect(() => {
if (dataMonsterInfo && !errorMonsterInfo) {
setAllMapMonsterInfo(dataMonsterInfo)
} else if (errorMonsterInfo) {
toast.error("Failed to load monster info data")
}
}, [dataMonsterInfo, errorMonsterInfo, setAllMapMonsterInfo])
}

View File

@@ -0,0 +1,66 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { fetchPFByIdsNative, getPFEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import { listCurrentLanguageApi } from '@/lib/constant'
import useLocaleStore from '@/stores/localeStore'
import { toast } from 'react-toastify'
import useEventStore from '@/stores/eventStore'
import { EventStageDetail } from '@/types'
export const useFetchPFData = () => {
const { setPFEvent, setMapPFInfo } = useEventStore()
const { locale } = useLocaleStore()
const { data: dataPF, error: errorPF } = useQuery({
queryKey: ['pfData'],
queryFn: getPFEventListApi,
select: (data) => data.sort((a, b) => Number(b.id) - Number(a.id)),
staleTime: 1000 * 60 * 5,
})
const { data: dataPFInfo, error: errorPFInfo } = useQuery({
queryKey: ['pfInfoData', locale],
queryFn: () =>
fetchPFByIdsNative(
dataPF!.map((item) => item.id),
listCurrentLanguageApi[locale.toLowerCase()]
),
staleTime: 1000 * 60 * 5,
enabled: !!dataPF,
select: (data) => {
const newData = { ...data }
for (const key in newData) {
for (const item of newData[key].Level) {
item.EventIDList1 = item.EventIDList1.map((event: EventStageDetail) => ({
...event,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
MonsterList: event.MonsterList.map(({ $type, ...rest }) => rest)
}))
item.EventIDList2 = item.EventIDList2.map((event: EventStageDetail) => ({
...event,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
MonsterList: event.MonsterList.map(({ $type, ...rest }) => rest)
}))
}
}
return newData
},
});
useEffect(() => {
if (dataPF && !errorPF) {
setPFEvent(dataPF)
} else if (errorPF) {
toast.error("Failed to load PF data")
}
}, [dataPF, errorPF, setPFEvent])
useEffect(() => {
if (dataPFInfo && !errorPFInfo) {
setMapPFInfo(dataPFInfo)
} else if (errorPFInfo) {
toast.error("Failed to load PF info data")
}
}, [dataPFInfo, errorPFInfo, setMapPFInfo])
}

View File

@@ -0,0 +1,46 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { fetchRelicsByIdsNative, getRelicSetListApi } from '@/lib/api'
import { useEffect } from 'react'
import useRelicStore from '@/stores/relicStore'
import { listCurrentLanguageApi } from '@/lib/constant'
import useLocaleStore from '@/stores/localeStore'
import { toast } from 'react-toastify'
export const useFetchRelicData = () => {
const { setListRelic, setAllMapRelicInfo } = useRelicStore()
const { locale } = useLocaleStore()
const { data: dataRelic, error: errorRelic } = useQuery({
queryKey: ['relicData'],
queryFn: getRelicSetListApi,
staleTime: 1000 * 60 * 5,
})
const { data: dataRelicInfo, error: errorRelicInfo } = useQuery({
queryKey: ['relicInfoData', locale],
queryFn: () =>
fetchRelicsByIdsNative(
dataRelic!.map((item) => item.id),
listCurrentLanguageApi[locale.toLowerCase()]
),
staleTime: 1000 * 60 * 5,
enabled: !!dataRelic,
});
useEffect(() => {
if (dataRelic && !errorRelic) {
setListRelic(dataRelic)
} else if (errorRelic) {
toast.error("Failed to load relic data")
}
}, [dataRelic, errorRelic, setListRelic])
useEffect(() => {
if (dataRelicInfo && !errorRelicInfo) {
setAllMapRelicInfo(dataRelicInfo)
} else if (errorRelicInfo) {
toast.error("Failed to load relic info data")
}
}, [dataRelicInfo, errorRelicInfo, setAllMapRelicInfo])
}

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

@@ -0,0 +1,218 @@
import { AffixDetail, ASDetail, CharacterDetail, ConfigMaze, FreeSRJson, LightConeDetail, MocDetail, PFDetail, PSResponse, RelicDetail } from "@/types";
import axios from 'axios';
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 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 fetchASByIdsNative(ids: string[], locale: string): Promise<Record<string, ASDetail> | null> {
try {
const res = await axios.post<Record<string, ASDetail>>(`/api/${locale}/as`, { asIds: ids });
return res.data;
} catch (error) {
console.error('Failed to fetch AS:', error);
return null;
}
}
export async function fetchASByIdNative(ids: string, locale: string): Promise<ASDetail | null> {
try {
const res = await axios.get<ASDetail>(`/api/${locale}/as/${ids}`);
return res.data;
} catch (error) {
console.error('Failed to fetch AS:', error);
return null;
}
}
export async function fetchPFByIdsNative(ids: string[], locale: string): Promise<Record<string, PFDetail> | null> {
try {
const res = await axios.post<Record<string, PFDetail>>(`/api/${locale}/pf`, { pfIds: ids });
return res.data;
} catch (error) {
console.error('Failed to fetch PF:', error);
return null;
}
}
export async function fetchPFByIdNative(ids: string, locale: string): Promise<PFDetail | null> {
try {
const res = await axios.get<PFDetail>(`/api/${locale}/pf/${ids}`);
return res.data;
} catch (error) {
console.error('Failed to fetch PF:', error);
return null;
}
}
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 });
return res.data;
} catch (error) {
console.error('Failed to fetch MOC:', error);
return null;
}
}
export async function fetchMOCByIdNative(ids: string, locale: string): Promise<MocDetail[] | null> {
try {
const res = await axios.get<MocDetail[]>(`/api/${locale}/moc/${ids}`);
return res.data;
} catch (error) {
console.error('Failed to fetch MOC:', 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 })
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

@@ -1,102 +1,6 @@
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;
}
}
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 axios from "axios";
export async function getLightconeInfoApi(lightconeId: number, locale: string): Promise<LightConeDetail | null> {
try {
@@ -142,63 +46,91 @@ export async function getRelicInfoApi(relicId: number, locale: string): Promise<
}
}
export async function fetchCharacterByIdNative(id: string, locale: string): Promise<CharacterDetail | null> {
export async function getCharacterInfoApi(avatarId: number, 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);
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 fetchCharactersByIdsNative(ids: string[], locale: string): Promise<Record<string, CharacterDetail>> {
export async function getMOCEventInfoApi(eventId: number, locale: string): Promise<MocDetail[] | null> {
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 {};
}
}
const res = await axios.get<MocDetail[]>(
`https://api.hakush.in/hsr/data/${locale}/maze/${eventId}.json`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
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 res.data as MocDetail[];
} 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 fetchLightconesByIdsNative(ids: string[], locale: string): Promise<Record<string, LightConeDetail>> {
export async function getASEventInfoApi(eventId: number, locale: string): Promise<ASDetail | null> {
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 {};
}
}
const res = await axios.get<ASDetail>(
`https://api.hakush.in/hsr/data/${locale}/boss/${eventId}.json`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
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 res.data as ASDetail;
} 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 fetchRelicsByIdsNative(ids: string[], locale: string): Promise<Record<string, RelicDetail>> {
export async function getPFEventInfoApi(eventId: number, locale: string): Promise<PFDetail | null> {
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 {};
const res = await axios.get<PFDetail>(
`https://api.hakush.in/hsr/data/${locale}/story/${eventId}.json`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data as PFDetail;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return null;
}
}
@@ -274,24 +206,124 @@ export async function getRelicSetListApi(): Promise<RelicBasic[]> {
}
}
export async function SendDataToServer(username: string, password: string, serverUrl: string, data: FreeSRJson | null): Promise<PSResponse | string> {
export async function getMOCEventListApi(): Promise<EventBasic[]> {
try {
const response = await axios.post(`${serverUrl}`, { username, password, data })
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return "Invalid response schema";
const res = await axios.get<Record<string, EventBasicRaw>>(
'https://api.hakush.in/hsr/data/maze.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 parsed.data;
} catch (error: any) {
return error?.message || "Unknown error";
return [];
}
}
export async function SendDataThroughProxy({data}: {data: any}) {
export async function getASEventListApi(): Promise<EventBasic[]> {
try {
const response = await axios.post(`/api/proxy`, { ...data })
return response.data;
} catch (error: any) {
return error;
const res = await axios.get<Record<string, EventBasicRaw>>(
'https://api.hakush.in/hsr/data/maze_boss.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 getPFEventListApi(): Promise<EventBasic[]> {
try {
const res = await axios.get<Record<string, EventBasicRaw>>(
'https://api.hakush.in/hsr/data/maze_extra.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>>(
'https://api.hakush.in/hsr/data/monster.json',
{
headers: {
'Content-Type': 'application/json',
},
}
);
const dataArr = Array.from(Object.entries(res.data)).map(([id, it]) => convertMonster(id, it));
const dataMap = Object.fromEntries(Object.entries(res.data).map(([id, it]) => [id, convertMonster(id, it)]));
return {
list: dataArr,
map: dataMap
};
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return {list: [], map: {}};
}
}
export async function getMonsterDetailApi(): Promise<Record<string, MonsterDetail> | null> {
try {
const res = await axios.get<Record<string, MonsterDetail>>(
`https://api.hakush.in/hsr/data/monstervalue.json`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return null;
}
}

2
src/lib/api/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from "./api";
export * from "./hakushi";

View File

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

View File

@@ -1,7 +1,7 @@
import fs from 'fs';
import path from 'path';
import { getCharacterInfoApi } from './api';
import { CharacterDetail } from '@/types/characterDetail';
import { getCharacterInfoApi } from '../api';
const DATA_DIR = path.join(process.cwd(), 'data');
const characterFileCache: Record<string, Record<string, CharacterDetail>> = {};

8
src/lib/loader/index.ts Normal file
View File

@@ -0,0 +1,8 @@
export * from "./characterLoader";
export * from "./lighconeLoader";
export * from "./affixLoader";
export * from "./configMazeLoader";
export * from "./relicLoader";
export * from "./asLoader";
export * from "./pfLoader";
export * from "./mocLoader";

View File

@@ -1,6 +1,6 @@
import fs from 'fs';
import path from 'path';
import { getLightconeInfoApi } from './api';
import { getLightconeInfoApi } from '../api';
import { LightConeDetail } from '@/types';
const DATA_DIR = path.join(process.cwd(), 'data');

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import fs from 'fs';
import path from 'path';
import { getRelicInfoApi } from './api';
import { RelicDetail } from '@/types';
import { getRelicInfoApi } from '../api';
const DATA_DIR = path.join(process.cwd(), 'data');
const relicFileCache: Record<string, Record<string, RelicDetail>> = {};

35
src/stores/eventStore.ts Normal file
View File

@@ -0,0 +1,35 @@
import { MocDetail, EventBasic, PFDetail, ASDetail } from '@/types';
import { create } from 'zustand'
interface EventState {
MOCEvent: EventBasic[];
mapMOCInfo: Record<string, MocDetail[]>;
PFEvent: EventBasic[];
mapPFInfo: Record<string, PFDetail>;
ASEvent: EventBasic[];
mapASInfo: Record<string, ASDetail>;
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;
}
const useEventStore = create<EventState>((set) => ({
MOCEvent: [],
mapMOCInfo: {},
PFEvent: [],
mapPFInfo: {},
ASEvent: [],
mapASInfo: {},
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 }),
}));
export default useEventStore;

View File

@@ -1,4 +1,4 @@
import { AffixDetail, ASConfigMaze, AvatarConfigMaze, ConfigMaze, MOCConfigMaze, PFConfigMaze } from '@/types';
import { AffixDetail, ASConfigMaze, AvatarConfigMaze, ConfigMaze, MOCConfigMaze, PFConfigMaze, StageConfigMaze } from '@/types';
import { create } from 'zustand'
interface MazeState {
@@ -6,11 +6,13 @@ interface MazeState {
MOC: Record<string, MOCConfigMaze>;
AS: Record<string, ASConfigMaze>;
PF: Record<string, PFConfigMaze>;
Stage: Record<string, StageConfigMaze>;
setTechnique: (newTechnique: Record<string, AvatarConfigMaze>) => void;
setMOC: (newMOC: Record<string, MOCConfigMaze>) => void;
setAS: (newAS: Record<string, ASConfigMaze>) => void;
setPF: (newPF: Record<string, PFConfigMaze>) => void;
setAllData: (newData: ConfigMaze) => void;
setStage: (newStage: Record<string, StageConfigMaze>) => void;
setAllMazeData: (newData: ConfigMaze) => void;
}
const useMazeStore = create<MazeState>((set) => ({
@@ -18,11 +20,13 @@ const useMazeStore = create<MazeState>((set) => ({
MOC: {},
AS: {},
PF: {},
Stage: {},
setTechnique: (newTechnique: Record<string, AvatarConfigMaze>) => set({ Technique: newTechnique }),
setMOC: (newMOC: Record<string, MOCConfigMaze>) => set({ MOC: newMOC }),
setAS: (newAS: Record<string, ASConfigMaze>) => set({ AS: newAS }),
setPF: (newPF: Record<string, PFConfigMaze>) => set({ PF: newPF }),
setAllData: (newData: ConfigMaze) => set({ Technique: newData.Avatar, MOC: newData.MOC, AS: newData.AS, PF: newData.PF }),
setStage: (newStage: Record<string, StageConfigMaze>) => set({ Stage: newStage }),
setAllMazeData: (newData: ConfigMaze) => set({ Technique: newData.Avatar, MOC: newData.MOC, AS: newData.AS, PF: newData.PF, Stage: newData.Stage }),
}));
export default useMazeStore;

View File

@@ -8,6 +8,10 @@ interface ModelState {
isOpenCreateProfile: boolean;
isOpenImport: boolean;
isOpenCopy: boolean;
isOpenMonster: boolean;
isOpenConnect: boolean;
setIsOpenConnect: (newIsOpenConnect: boolean) => void;
setIsOpenMonster: (newIsOpenMonster: boolean) => void;
setIsOpenLightcone: (newIsOpenLightcone: boolean) => void;
setIsOpenRelic: (newIsOpenRelic: boolean) => void;
setIsOpenAvatar: (newIsOpenAvatar: boolean) => void;
@@ -16,13 +20,17 @@ interface ModelState {
setIsOpenCopy: (newIsOpenCopy: boolean) => void;
}
const useModelStore = create<ModelState>((set, get) => ({
const useModelStore = create<ModelState>((set) => ({
isOpenLightcone: false,
isOpenRelic: false,
isOpenAvatar: false,
isOpenCreateProfile: false,
isOpenImport: false,
isOpenCopy: false,
isOpenMonster: false,
isOpenConnect: false,
setIsOpenConnect: (newIsOpenConnect: boolean) => set({ isOpenConnect: newIsOpenConnect }),
setIsOpenMonster: (newIsOpenMonster: boolean) => set({ isOpenMonster: newIsOpenMonster }),
setIsOpenLightcone: (newIsOpenLightcone: boolean) => set({ isOpenLightcone: newIsOpenLightcone }),
setIsOpenRelic: (newIsOpenRelic: boolean) => set({ isOpenRelic: newIsOpenRelic }),
setIsOpenAvatar: (newIsOpenAvatar: boolean) => set({ isOpenAvatar: newIsOpenAvatar }),

View File

@@ -0,0 +1,27 @@
import { MonsterBasic, MonsterDetail } from '@/types';
import { create } from 'zustand'
interface MonsterState {
listMonster: MonsterBasic[];
mapMonster: Record<string, MonsterBasic>;
mapMonsterInfo: Record<string, MonsterDetail>;
setListMonster: (newListMonster: MonsterBasic[]) => void;
setMapMonsterInfo: (monsterId: string, newMonster: MonsterDetail) => void;
setAllMapMonsterInfo: (newMonster: Record<string, MonsterDetail>) => void;
setMapMonster: (monsterId: string, newMonster: MonsterBasic) => void;
setAllMapMonster: (newMonster: Record<string, MonsterBasic>) => void;
}
const useMonsterStore = create<MonsterState>((set) => ({
listMonster: [],
mapMonster: {},
mapMonsterInfo: {},
setListMonster: (newListMonster: MonsterBasic[]) => set({ listMonster: newListMonster }),
setMapMonster: (monsterId: string, newMonster: MonsterBasic) => set((state) => ({ mapMonster: { ...state.mapMonster, [monsterId]: newMonster } })),
setAllMapMonster: (newMonster: Record<string, MonsterBasic>) => set({ mapMonster: newMonster }),
setMapMonsterInfo: (monsterId: string, newMonster: MonsterDetail) => set((state) => ({ mapMonsterInfo: { ...state.mapMonsterInfo, [monsterId]: newMonster } })),
setAllMapMonsterInfo: (newMonster: Record<string, MonsterDetail>) => set({ mapMonsterInfo: newMonster }),
}));
export default useMonsterStore;

View File

@@ -1,4 +1,5 @@
import { create } from 'zustand'
import cloneDeep from 'lodash/cloneDeep'
interface RelicMakerState {
selectedRelicSlot: string;
@@ -29,7 +30,7 @@ interface RelicMakerState {
}
const useRelicMakerStore = create<RelicMakerState>((set, get) => ({
const useRelicMakerStore = create<RelicMakerState>((set) => ({
selectedRelicSlot: "",
selectedMainStat: "",
selectedRelicSet: "",
@@ -184,14 +185,14 @@ const useRelicMakerStore = create<RelicMakerState>((set, get) => ({
return {}
}),
popHistory: (index: number) => set((state) => {
const newPreSelectedSubStats = { ...state.preSelectedSubStats };
const newPreSelectedSubStats = cloneDeep(state.preSelectedSubStats);
const copied = state.preSelectedSubStats[index].map(item => ({ ...item }));
copied.pop();
newPreSelectedSubStats[index] = copied;
return { preSelectedSubStats: newPreSelectedSubStats };
}),
addHistory: (index, affix) => set((state) => {
const newPreSelectedSubStats = { ...state.preSelectedSubStats };
const newPreSelectedSubStats = cloneDeep(state.preSelectedSubStats);
const currentList = state.preSelectedSubStats[index];
const copied = currentList ? currentList.map(item => ({ ...item })) : [
@@ -202,7 +203,7 @@ const useRelicMakerStore = create<RelicMakerState>((set, get) => ({
];
if (copied) {
const newAffix = { ...affix };
const newAffix = cloneDeep(affix);
copied.push(newAffix);
newPreSelectedSubStats[index] = copied;
return { preSelectedSubStats: newPreSelectedSubStats };

View File

@@ -1,32 +1,73 @@
import { AvatarStore, BattleConfigStore } from '@/types';
import { ASConfigStore, AvatarStore, CEConfigStore, MOCConfigStore, PFConfigStore } from '@/types';
import { create } from 'zustand'
import { persist } from 'zustand/middleware';
interface UserDataState {
avatars: { [key: string]: AvatarStore };
battle_config: BattleConfigStore;
battle_type: string;
moc_config: MOCConfigStore;
pf_config: PFConfigStore;
as_config: ASConfigStore;
ce_config: CEConfigStore;
setAvatars: (newAvatars: { [key: string]: AvatarStore }) => void;
setAvatar: (newAvatar: AvatarStore) => void;
setBattleConfig: (newBattleConfig: BattleConfigStore) => void;
setBattleType: (newBattleType: string) => void;
setMocConfig: (newMocConfig: MOCConfigStore) => void;
setPfConfig: (newPfConfig: PFConfigStore) => void;
setAsConfig: (newAsConfig: ASConfigStore) => void;
setCeConfig: (newCeConfig: CEConfigStore) => void;
}
const useUserDataStore = create<UserDataState>()(
persist(
(set) => ({
avatars: {},
battle_config: {
battle_type: "",
battle_type: "",
moc_config: {
event_id: 0,
challenge_id: 0,
floor_side: "Upper",
use_turbulence_buff: true,
use_cycle_count: true,
blessings: [],
custom_stats: [],
cycle_count: 0,
stage_id: 0,
path_resonance_id: 0,
monsters: [],
},
pf_config: {
event_id: 0,
challenge_id: 0,
buff_id: 0,
floor_side: "Upper",
blessings: [],
cycle_count: 0,
stage_id: 0,
monsters: [],
},
as_config: {
event_id: 0,
challenge_id: 0,
buff_id: 0,
floor_side: "Upper",
blessings: [],
cycle_count: 0,
stage_id: 0,
monsters: [],
},
ce_config: {
blessings: [],
cycle_count: 30,
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 } })),
setBattleConfig: (newBattleConfig: BattleConfigStore) => set({ battle_config: newBattleConfig }),
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 }),
setCeConfig: (newCeConfig: CEConfigStore) => set({ ce_config: newCeConfig }),
}),
{
name: 'user-data-storage',

70
src/types/asDetail.ts Normal file
View File

@@ -0,0 +1,70 @@
import { ChallengeDetail, EventStageDetail } from "./mocDetail"
import { BuffDetail, OptionDetail } from "./pfDetail"
export interface ASDetail {
Id: number
Name: string
Buff: BuffDetail
BuffList1: OptionDetail[]
BuffList2: OptionDetail[]
BeginTime: string
EndTime: string
Level: ASLevel[]
}
export interface ASLevel {
Id: number
Name: string
Challenge: ChallengeDetail[]
DamageType1: string[]
DamageType2: string[]
MazeGroupID1: number
MazeGroupID2: number
BossMonsterID1: number
BossMonsterID2: number
BossMonsterID1SkillList: number[]
BossMonsterID2SkillList: number[]
BossMonsterConfig1: BossMonsterConfig
BossMonsterConfig2: BossMonsterConfig
EventIDList1: EventStageDetail[]
EventIDList2: EventStageDetail[]
}
export interface BossMonsterConfig {
Difficulty: number
DifficultyList: number[]
TagList: BossTag[]
DifficultyGuideList: BossDifficultyGuide[]
TextGuideList: string[]
PhaseList: BossPhase[]
}
export interface BossTag {
Name: string
Desc: string
Param: number[]
SkillID: number | null
ParamFix: number[]
Child: BossChildTag[]
}
export interface BossChildTag {
Name: string
Desc: string
Param: number[]
}
export interface BossDifficultyGuide {
Desc: string
Param: number[]
SkillID: number | null
ParamFix: number[]
}
export interface BossPhase {
Name: string
Desc: string
Answer: string
Difficulty: number
SkillList: number[]
}

View File

@@ -17,10 +17,18 @@ export type MOCConfigMaze = {
export type AvatarConfigMaze = {
maze_buff: number[];
}
export type StageConfigMaze = {
stage_id: number;
stage_type: string;
level: number;
monster_list: Array<Record<string, number>>;
};
export type ConfigMaze = {
Avatar: Record<string, AvatarConfigMaze>;
MOC: Record<string, MOCConfigMaze>;
AS: Record<string, ASConfigMaze>;
PF: Record<string, PFConfigMaze>;
Stage: Record<string, StageConfigMaze>;
};

23
src/types/eventBasic.ts Normal file
View File

@@ -0,0 +1,23 @@
export interface EventBasicRaw {
param?: number[];
en: string;
id: string;
begin: string;
end: string;
live_begin: string;
live_end: string;
kr: string;
cn: string;
jp: string;
}
export interface EventBasic {
param?: number[];
id: string;
begin: string;
end: string;
live_begin: string;
live_end: string;
lang: Map<string, string>;
}

View File

@@ -10,4 +10,10 @@ export * from "./relicBasic"
export * from "./relicDetail"
export * from "./affix"
export * from "./enka"
export * from "./card"
export * from "./card"
export * from "./eventBasic"
export * from "./monsterBasic"
export * from "./pfDetail"
export * from "./asDetail"
export * from "./mocDetail"
export * from "./monsterDetail"

View File

@@ -59,13 +59,53 @@ export type BattleBuffStore = {
id: number;
dynamic_key?: DynamicKeyStore;
}
export type BattleConfigStore = {
battle_type: string;
export type MOCConfigStore = {
event_id: number;
challenge_id: number;
floor_side: string;
use_turbulence_buff: boolean;
use_cycle_count: boolean;
blessings: BattleBuffStore[]
custom_stats: SubAffixStore[];
cycle_count: number;
stage_id: number;
path_resonance_id: number;
monsters: MonsterStore[][];
}
export type PFConfigStore = {
event_id: number;
challenge_id: number;
buff_id: number;
floor_side: string;
blessings: BattleBuffStore[]
cycle_count: number;
stage_id: number;
monsters: MonsterStore[][];
}
export type ASConfigStore = {
event_id: number;
challenge_id: number;
buff_id: number;
floor_side: string;
blessings: BattleBuffStore[]
cycle_count: number;
stage_id: number;
monsters: MonsterStore[][];
}
export type CEConfigStore = {
blessings: BattleBuffStore[]
cycle_count: number;
stage_id: number;
monsters: MonsterStore[][];
}
export interface Mics {
avatars: Record<string, AvatarStore>,
battle_type: string;
moc_config: MOCConfigStore;
pf_config: PFConfigStore;
as_config: ASConfigStore;
ce_config: CEConfigStore;
}

51
src/types/mocDetail.ts Normal file
View File

@@ -0,0 +1,51 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export interface MocDetail {
Id: number
Name: string
GroupName: string
Desc: string
Param: number[]
Challenge: ChallengeDetail[]
Countdown: number
DamageType1: string[]
DamageType2: string[]
MazeGroupID1: number
MazeGroupID2: number
NpcMonsterIDList1: number[]
NpcMonsterIDList2: number[]
EventIDList1: EventStageDetail[]
EventIDList2: EventStageDetail[]
BeginTime: string
EndTime: string
}
export interface ChallengeDetail {
Name: string
Param?: number
}
export interface EventStageDetail {
StageID: number
StageType: string
StageName: number
HardLevelGroup: number
Level: number
EliteGroup?: number
LevelGraphPath: string
StageAbilityConfig: any[]
BattleScoringGroup?: number
SubLevelGraphs: any[]
StageConfigData: StageConfig[]
MonsterList: Record<string, number>[]
LevelLoseCondition: string[]
LevelWinCondition: string[]
Release: boolean
ForbidExitBattle: boolean
MonsterWarningRatio?: number
TrialAvatarList: any[]
}
export interface StageConfig {
$type: string
[key: string]: string
}

25
src/types/monsterBasic.ts Normal file
View File

@@ -0,0 +1,25 @@
export interface MonsterBasicRaw {
rank: string;
camp: string | null;
icon: string;
child: number[];
weak: string[];
en: string;
desc: string;
kr: string;
cn: string;
jp: string;
}
export interface MonsterBasic {
id: string;
rank: string;
camp: string | null;
icon: string;
child: number[];
weak: string[];
desc: string;
lang: Map<string, string>;
}

View File

@@ -0,0 +1,23 @@
export interface MonsterChild {
Id: number
AttackModifyRatio: number
DefenceModifyRatio: number
EliteGroup: number
HPModifyRatio: number
SpeedModifyRatio: number
SpeedModifyValue: number | null
StanceModifyRatio: number
HardLevelGroup: number
StanceWeakList: string[]
}
export interface MonsterDetail {
Rank: string
AttackBase: number
DefenceBase: number
HPBase: number
SpeedBase: number
StanceBase: number
StatusResistanceBase: number
child: MonsterChild[]
}

51
src/types/pfDetail.ts Normal file
View File

@@ -0,0 +1,51 @@
import { ChallengeDetail, EventStageDetail } from "./mocDetail"
export interface PFDetail {
Id: number
Name: string
Buff: BuffDetail
Option: OptionDetail[]
SubOption: OptionDetail[]
BeginTime: string
EndTime: string
Level: PFLevel[]
}
export interface BuffDetail {
Name: string | null
Desc: string | null
Param: number[]
}
export interface OptionDetail {
Name: string
Desc: string
Param: number[]
}
export interface PFLevel {
Id: number
Name: string
Challenge: ChallengeDetail[]
DamageType1: string[]
DamageType2: string[]
MazeGroupID1: number
MazeGroupID2: number
NpcMonsterIDList1: number[]
NpcMonsterIDList2: number[]
EventIDList1: EventStageDetail[]
EventIDList2: EventStageDetail[]
InfiniteList1: Record<string, InfiniteWave>
InfiniteList2: Record<string, InfiniteWave>
}
export interface InfiniteWave {
InfiniteWaveID: number
MonsterGroupIDList: number[]
MaxMonsterCount: number
MaxTeammateCount: number
Ability: string
ParamList: number[]
ClearPreviousAbility: boolean
EliteGroup: number
}

78
src/zod/asDetail.zod.ts Normal file
View File

@@ -0,0 +1,78 @@
// Generated by ts-to-zod
import { z } from "zod";
export const bossDifficultyGuideSchema = z.object({
Desc: z.string(),
Param: z.array(z.number()),
SkillID: z.number().nullable(),
ParamFix: z.array(z.number()),
});
export const bossPhaseSchema = z.object({
Name: z.string(),
Desc: z.string(),
Answer: z.string(),
Difficulty: z.number(),
SkillList: z.array(z.number()),
});
export const bossChildTagSchema = z.object({
Name: z.string(),
Desc: z.string(),
Param: z.array(z.number()),
});
const buffDetailSchema = z.any();
const optionDetailSchema = z.any();
const challengeDetailSchema = z.any();
const eventStageDetailSchema = z.any();
export const bossTagSchema = z.object({
Name: z.string(),
Desc: z.string(),
Param: z.array(z.number()),
SkillID: z.number().nullable(),
ParamFix: z.array(z.number()),
Child: z.array(bossChildTagSchema),
});
export const bossMonsterConfigSchema = z.object({
Difficulty: z.number(),
DifficultyList: z.array(z.number()),
TagList: z.array(bossTagSchema),
DifficultyGuideList: z.array(bossDifficultyGuideSchema),
TextGuideList: z.array(z.string()),
PhaseList: z.array(bossPhaseSchema),
});
export const aSLevelSchema = z.object({
Id: z.number(),
Name: z.string(),
Challenge: z.array(challengeDetailSchema),
DamageType1: z.array(z.string()),
DamageType2: z.array(z.string()),
MazeGroupID1: z.number(),
MazeGroupID2: z.number(),
BossMonsterID1: z.number(),
BossMonsterID2: z.number(),
BossMonsterID1SkillList: z.array(z.number()),
BossMonsterID2SkillList: z.array(z.number()),
BossMonsterConfig1: bossMonsterConfigSchema,
BossMonsterConfig2: bossMonsterConfigSchema,
EventIDList1: z.array(eventStageDetailSchema),
EventIDList2: z.array(eventStageDetailSchema),
});
export const aSDetailSchema = z.object({
Id: z.number(),
Name: z.string(),
Buff: buffDetailSchema,
BuffList1: z.array(optionDetailSchema),
BuffList2: z.array(optionDetailSchema),
BeginTime: z.string(),
EndTime: z.string(),
Level: z.array(aSLevelSchema),
});

View File

@@ -19,3 +19,14 @@ export const characterInfoCardTypeSchema = z.object({
}),
),
});
const lightconeStoreSchema = z.any();
const relicStoreSchema = z.any();
export const avatarProfileCardTypeSchema = z.object({
key: z.number(),
profile_name: z.string(),
lightcone: lightconeStoreSchema.nullable(),
relics: z.record(relicStoreSchema),
});

15
src/zod/eventBasic.zod.ts Normal file
View File

@@ -0,0 +1,15 @@
// Generated by ts-to-zod
import { z } from "zod";
export const eventBasicRawSchema = z.object({
param: z.array(z.number()).optional(),
en: z.string(),
id: z.string(),
begin: z.string(),
end: z.string(),
live_begin: z.string(),
live_end: z.string(),
kr: z.string(),
cn: z.string(),
jp: z.string(),
});

View File

@@ -5,7 +5,7 @@ export const filterAvatarTypeSchema = z.object({
name: z.string(),
path: z.array(z.string()),
element: z.array(z.string()),
rarity: z.string(),
rarity: z.array(z.string()),
locale: z.string(),
});
@@ -20,3 +20,11 @@ export const filterRelicTypeSchema = z.object({
locale: z.string(),
name: z.string(),
});
export const filterMonsterTypeSchema = z.object({
name: z.string(),
id: z.string(),
rarity: z.array(z.string()),
locale: z.string(),
weak: z.array(z.string()),
});

View File

@@ -10,4 +10,4 @@ export * from "./lightconeDetail.zod";
export * from "./mics.zod";
export * from "./relicBasic.zod";
export * from "./relicDetail.zod";
export * from "./srtools.zod";
export * from "./srtools.zod";

2
src/zod/index.zod.ts Normal file
View File

@@ -0,0 +1,2 @@
// Generated by ts-to-zod
import { z } from "zod";

View File

@@ -65,18 +65,52 @@ export const battleBuffStoreSchema = z.object({
dynamic_key: dynamicKeyStoreSchema.optional(),
});
export const battleConfigStoreSchema = z.object({
battle_type: z.string(),
export const mOCConfigStoreSchema = z.object({
event_id: z.number(),
challenge_id: z.number(),
floor_side: z.string(),
use_turbulence_buff: z.boolean(),
use_cycle_count: z.boolean(),
blessings: z.array(battleBuffStoreSchema),
custom_stats: z.array(subAffixStoreSchema),
cycle_count: z.number(),
stage_id: z.number(),
path_resonance_id: z.number(),
monsters: z.array(z.array(monsterStoreSchema)),
});
export const pFConfigStoreSchema = z.object({
event_id: z.number(),
challenge_id: z.number(),
floor_side: z.string(),
buff_id: z.number(),
blessings: z.array(battleBuffStoreSchema),
cycle_count: z.number(),
stage_id: z.number(),
monsters: z.array(z.array(monsterStoreSchema)),
});
export const aSConfigStoreSchema = z.object({
event_id: z.number(),
challenge_id: z.number(),
buff_id: z.number(),
floor_side: z.string(),
blessings: z.array(battleBuffStoreSchema),
cycle_count: z.number(),
stage_id: z.number(),
monsters: z.array(z.array(monsterStoreSchema)),
});
export const cEConfigStoreSchema = z.object({
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(z.string(), avatarStoreSchema),
battle_config: battleConfigStoreSchema,
})
avatars: z.record(avatarStoreSchema),
battle_type: z.string(),
moc_config: mOCConfigStoreSchema,
pf_config: pFConfigStoreSchema,
as_config: aSConfigStoreSchema,
ce_config: cEConfigStoreSchema,
});

54
src/zod/mocDetail.zod.ts Normal file
View File

@@ -0,0 +1,54 @@
// Generated by ts-to-zod
import { z } from "zod";
export const challengeDetailSchema = z.object({
Name: z.string(),
Param: z.number().optional(),
});
export const stageConfigSchema = z.record(z.string()).and(
z.object({
$type: z.string(),
}),
);
export const eventStageDetailSchema = z.object({
StageID: z.number(),
StageType: z.string(),
StageName: z.number(),
HardLevelGroup: z.number(),
Level: z.number(),
EliteGroup: z.number().optional(),
LevelGraphPath: z.string(),
StageAbilityConfig: z.array(z.any()),
BattleScoringGroup: z.number().optional(),
SubLevelGraphs: z.array(z.any()),
StageConfigData: z.array(stageConfigSchema),
MonsterList: z.array(z.record(z.number())),
LevelLoseCondition: z.array(z.string()),
LevelWinCondition: z.array(z.string()),
Release: z.boolean(),
ForbidExitBattle: z.boolean(),
MonsterWarningRatio: z.number().optional(),
TrialAvatarList: z.array(z.any()),
});
export const mocDetailSchema = z.object({
Id: z.number(),
Name: z.string(),
GroupName: z.string(),
Desc: z.string(),
Param: z.array(z.number()),
Challenge: z.array(challengeDetailSchema),
Countdown: z.number(),
DamageType1: z.array(z.string()),
DamageType2: z.array(z.string()),
MazeGroupID1: z.number(),
MazeGroupID2: z.number(),
NpcMonsterIDList1: z.array(z.number()),
NpcMonsterIDList2: z.array(z.number()),
EventIDList1: z.array(eventStageDetailSchema),
EventIDList2: z.array(eventStageDetailSchema),
BeginTime: z.string(),
EndTime: z.string(),
});

View File

@@ -0,0 +1,15 @@
// Generated by ts-to-zod
import { z } from "zod";
export const monsterBasicRawSchema = z.object({
rank: z.string(),
camp: z.string().nullable(),
icon: z.string(),
child: z.array(z.number()),
weak: z.array(z.string()),
en: z.string(),
desc: z.string(),
kr: z.string(),
cn: z.string(),
jp: z.string(),
});

56
src/zod/pfDetail.zod.ts Normal file
View File

@@ -0,0 +1,56 @@
// Generated by ts-to-zod
import { z } from "zod";
export const buffDetailSchema = z.object({
Name: z.string().nullable(),
Desc: z.string().nullable(),
Param: z.array(z.number()),
});
export const optionDetailSchema = z.object({
Name: z.string(),
Desc: z.string(),
Param: z.array(z.number()),
});
export const infiniteWaveSchema = z.object({
InfiniteWaveID: z.number(),
MonsterGroupIDList: z.array(z.number()),
MaxMonsterCount: z.number(),
MaxTeammateCount: z.number(),
Ability: z.string(),
ParamList: z.array(z.number()),
ClearPreviousAbility: z.boolean(),
EliteGroup: z.number(),
});
const challengeDetailSchema = z.any();
const eventStageDetailSchema = z.any();
export const pFLevelSchema = z.object({
Id: z.number(),
Name: z.string(),
Challenge: z.array(challengeDetailSchema),
DamageType1: z.array(z.string()),
DamageType2: z.array(z.string()),
MazeGroupID1: z.number(),
MazeGroupID2: z.number(),
NpcMonsterIDList1: z.array(z.number()),
NpcMonsterIDList2: z.array(z.number()),
EventIDList1: z.array(eventStageDetailSchema),
EventIDList2: z.array(eventStageDetailSchema),
InfiniteList1: z.record(infiniteWaveSchema),
InfiniteList2: z.record(infiniteWaveSchema),
});
export const pFDetailSchema = z.object({
Id: z.number(),
Name: z.string(),
Buff: buffDetailSchema,
Option: z.array(optionDetailSchema),
SubOption: z.array(optionDetailSchema),
BeginTime: z.string(),
EndTime: z.string(),
Level: z.array(pFLevelSchema),
});

View File

@@ -76,7 +76,7 @@ export const freeSRJsonSchema = z.object({
battle_config: battleConfigJsonSchema,
});
export const psResponseSchema = z.object({
export const pSResponseSchema = z.object({
status: z.number(),
message: z.string(),
});