FIX: All bug in Custom Enemy, UPDATE: Add specical main skill, optimaze ui in custom enemy
All checks were successful
Gitea Auto Deploy / Deploy-Container (push) Successful in 1m39s
All checks were successful
Gitea Auto Deploy / Deploy-Container (push) Successful in 1m39s
This commit is contained in:
333726
data/config_maze.json
333726
data/config_maze.json
File diff suppressed because it is too large
Load Diff
229269
data/monster.cn.json
Normal file
229269
data/monster.cn.json
Normal file
File diff suppressed because it is too large
Load Diff
229269
data/monster.en.json
Normal file
229269
data/monster.en.json
Normal file
File diff suppressed because it is too large
Load Diff
229269
data/monster.jp.json
Normal file
229269
data/monster.jp.json
Normal file
File diff suppressed because it is too large
Load Diff
229269
data/monster.kr.json
Normal file
229269
data/monster.kr.json
Normal file
File diff suppressed because it is too large
Load Diff
18
src/app/api/[locale]/monster/[id]/route.ts
Normal file
18
src/app/api/[locale]/monster/[id]/route.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { loadMonster } from '@/lib/loader'
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string, locale: string }> }
|
||||
) {
|
||||
|
||||
const { id, locale } = await params
|
||||
const monsterData = await loadMonster([id], locale)
|
||||
const monster = monsterData[id]
|
||||
|
||||
if (!monster) {
|
||||
return NextResponse.json({ error: 'Monster info not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json(monster)
|
||||
}
|
||||
20
src/app/api/[locale]/monster/route.ts
Normal file
20
src/app/api/[locale]/monster/route.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { loadMonster } 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 monsterIds = body.monsterIds as string[];
|
||||
const { locale } = await params;
|
||||
|
||||
if (!Array.isArray(monsterIds) || monsterIds.some(id => typeof id !== 'string')) {
|
||||
return NextResponse.json({ error: 'Invalid monsterIds' }, { status: 400 });
|
||||
}
|
||||
|
||||
const monsterData = await loadMonster(monsterIds, locale);
|
||||
|
||||
return NextResponse.json(monsterData);
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Failed to load monster data' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ChevronUp,
|
||||
ChevronDown,
|
||||
Search,
|
||||
CopyPlus,
|
||||
} from "lucide-react";
|
||||
|
||||
import useMazeStore from "@/stores/mazeStore";
|
||||
@@ -18,6 +19,7 @@ import Image from "next/image";
|
||||
import { MonsterBasic } from "@/types";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { listCurrentLanguageApi } from "@/constant/constant";
|
||||
|
||||
|
||||
export default function CeBar() {
|
||||
@@ -25,7 +27,7 @@ export default function CeBar() {
|
||||
const [showSearchWaveId, setShowSearchWaveId] = useState<number | null>(null);
|
||||
const { Stage } = useMazeStore()
|
||||
const { ce_config, setCeConfig } = useUserDataStore()
|
||||
const { listMonster } = useMonsterStore()
|
||||
const { mapMonsterInfo } = useMonsterStore()
|
||||
const { locale } = useLocaleStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const [showSearchStage, setShowSearchStage] = useState(false)
|
||||
@@ -34,9 +36,36 @@ export default function CeBar() {
|
||||
|
||||
const pageSize = 30
|
||||
|
||||
const pageSizeMonsters = 30
|
||||
const [monsterPage, setMonsterPage] = useState(1)
|
||||
|
||||
const listMonsterDetail = useMemo(() => {
|
||||
const result: MonsterBasic[] = []
|
||||
|
||||
for (const monster of Object.values(mapMonsterInfo)) {
|
||||
for (const monsterChild of monster.Child) {
|
||||
result.push({
|
||||
id: monsterChild.Id.toString(),
|
||||
rank: monster.Rank,
|
||||
camp: null,
|
||||
icon: monster.ImagePath,
|
||||
weak: monsterChild.StanceWeakList,
|
||||
desc: monster.Desc,
|
||||
child: [],
|
||||
lang: new Map<string, string>([
|
||||
[listCurrentLanguageApi[locale], monster.Name],
|
||||
]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}, [mapMonsterInfo, locale])
|
||||
|
||||
|
||||
const filteredMonsters = useMemo(() => {
|
||||
const newlistMonster = new Set<MonsterBasic>()
|
||||
for (const monster of listMonster) {
|
||||
for (const monster of listMonsterDetail) {
|
||||
if (getLocaleName(locale, monster).toLowerCase().includes(searchTerm.toLowerCase())) {
|
||||
newlistMonster.add(monster)
|
||||
}
|
||||
@@ -45,7 +74,21 @@ export default function CeBar() {
|
||||
}
|
||||
}
|
||||
return Array.from(newlistMonster)
|
||||
}, [listMonster, locale, searchTerm]);
|
||||
}, [listMonsterDetail, locale, searchTerm]);
|
||||
|
||||
const paginatedMonsters = useMemo(() =>
|
||||
filteredMonsters.slice((monsterPage - 1) * pageSizeMonsters, monsterPage * pageSizeMonsters),
|
||||
[filteredMonsters, monsterPage]
|
||||
)
|
||||
|
||||
const hasMoreMonsterPages = useMemo(
|
||||
() => monsterPage * pageSizeMonsters < filteredMonsters.length,
|
||||
[monsterPage, filteredMonsters]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setMonsterPage(1) // reset về trang 1 khi searchTerm thay đổi
|
||||
}, [searchTerm])
|
||||
|
||||
const stageList = useMemo(() => Object.values(Stage).map((stage) => ({
|
||||
id: stage.stage_id.toString(),
|
||||
@@ -68,35 +111,52 @@ export default function CeBar() {
|
||||
}, [stageSearchTerm])
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6 z-4" onClick={() => {
|
||||
<div className="p-6 z-4 h-full w-full" 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"
|
||||
className="btn btn-outline w-full text-left flex items-center gap-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setShowSearchStage(true)
|
||||
}}
|
||||
>
|
||||
<Search className="w-6 h-6" />
|
||||
<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 onClick={(e) => e.stopPropagation()} 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" />
|
||||
|
||||
<label className="input w-full">
|
||||
<svg className="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="2.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.3-4.3"></path>
|
||||
</g>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
type="search" className="grow"
|
||||
placeholder={transI18n("searchStage")}
|
||||
className="input input-sm w-full placeholder-slate-400"
|
||||
value={stageSearchTerm}
|
||||
onChange={(e) => setStageSearchTerm(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="max-h-60 overflow-y-auto space-y-1">
|
||||
@@ -107,7 +167,9 @@ export default function CeBar() {
|
||||
key={stage.id}
|
||||
className="p-2 hover:bg-primary/20 rounded cursor-pointer"
|
||||
onClick={() => {
|
||||
if (ce_config.stage_id !== Number(stage.id)) {
|
||||
setCeConfig({ ...ce_config, stage_id: Number(stage.id), cycle_count: 30 })
|
||||
}
|
||||
setShowSearchStage(false)
|
||||
setStageSearchTerm("")
|
||||
}}
|
||||
@@ -191,6 +253,20 @@ export default function CeBar() {
|
||||
className="btn btn-sm btn-success">
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
const newCeConfig = cloneDeep(ce_config)
|
||||
const waves = newCeConfig.monsters
|
||||
const temp = waves[waveIndex]
|
||||
newCeConfig.monsters.push([...temp])
|
||||
setCeConfig(newCeConfig)
|
||||
}}
|
||||
className="btn btn-sm btn-success">
|
||||
<CopyPlus className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
@@ -210,7 +286,22 @@ export default function CeBar() {
|
||||
<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"
|
||||
className="btn btn-xs btn-success absolute -top-2 right-5 opacity-50 group-hover:opacity-100 transition-opacity"
|
||||
onClick={() => {
|
||||
const newCeConfig = cloneDeep(ce_config)
|
||||
|
||||
newCeConfig.monsters[waveIndex].push({
|
||||
monster_id: Number(member.monster_id),
|
||||
level: member.level,
|
||||
amount: member.amount,
|
||||
})
|
||||
setCeConfig(newCeConfig)
|
||||
}}
|
||||
>
|
||||
<CopyPlus className="w-3 h-3" />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-xs btn-error absolute -top-2 -right-4 opacity-50 group-hover:opacity-100 transition-opacity"
|
||||
onClick={() => {
|
||||
const newCeConfig = cloneDeep(ce_config)
|
||||
newCeConfig.monsters[waveIndex].splice(memberIndex, 1)
|
||||
@@ -220,10 +311,9 @@ export default function CeBar() {
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
|
||||
|
||||
<div className="flex justify-center">
|
||||
{listMonster.find((monster2) => monster2.child.includes(member.monster_id))?.icon && <Image
|
||||
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster2) => monster2.child.includes(member.monster_id))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
|
||||
{listMonsterDetail.find((monster2) => monster2.id === member.monster_id.toString())?.icon && <Image
|
||||
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonsterDetail.find((monster2) => monster2.id === member.monster_id.toString())?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
|
||||
alt="Enemy Icon"
|
||||
width={376}
|
||||
height={512}
|
||||
@@ -231,9 +321,9 @@ export default function CeBar() {
|
||||
/>}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center gap-1 mb-2">
|
||||
{listMonster
|
||||
.find((monster) => monster.child.includes(member.monster_id))
|
||||
<div className="flex flex-wrap justify-center gap-1 mb-2">
|
||||
{listMonsterDetail
|
||||
.find((monster) => monster.id === member.monster_id.toString())
|
||||
?.weak?.map((icon, iconIndex) => (
|
||||
<Image
|
||||
src={`/icon/${icon.toLowerCase()}.webp`}
|
||||
@@ -247,7 +337,7 @@ export default function CeBar() {
|
||||
</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)))}
|
||||
{getLocaleName(locale, listMonsterDetail.find((monster) => monster.id === member.monster_id.toString())) } {`(${member.monster_id})`}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 mt-1">
|
||||
<span className="text-sm">Lv.</span>
|
||||
@@ -257,14 +347,12 @@ export default function CeBar() {
|
||||
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 val = Number(e.target.value)
|
||||
if (isNaN(val) || val < 1 || val > 95) return
|
||||
if (ce_config.monsters[waveIndex][memberIndex].level === val) return
|
||||
|
||||
const newCeConfig = cloneDeep(ce_config)
|
||||
newCeConfig.monsters[waveIndex][memberIndex].level = Number(e.target.value)
|
||||
newCeConfig.monsters[waveIndex][memberIndex].level = val
|
||||
setCeConfig(newCeConfig)
|
||||
}}
|
||||
/>
|
||||
@@ -277,11 +365,16 @@ export default function CeBar() {
|
||||
))}
|
||||
|
||||
{/* Add Member Button + Search */}
|
||||
<div className="relative flex items-start justify-center w-full h-full">
|
||||
<div className="relative flex items-start justify-center w-full h-full z-39" onClick={(e) => e.stopPropagation()}>
|
||||
<button
|
||||
className="btn btn-outline btn-primary w-full h-full border-dashed"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (showSearchWaveId === waveIndex) {
|
||||
setShowSearchWaveId(null)
|
||||
return
|
||||
}
|
||||
|
||||
setShowSearchWaveId(waveIndex)
|
||||
}}
|
||||
>
|
||||
@@ -289,22 +382,37 @@ export default function CeBar() {
|
||||
</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="absolute top-full mt-2 w-72 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" />
|
||||
|
||||
<label className="input w-full">
|
||||
<svg className="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
strokeWidth="2.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.3-4.3"></path>
|
||||
</g>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
type="search" className="grow"
|
||||
placeholder={transI18n("searchMonster")}
|
||||
className="input input-sm w-full placeholder-slate-400"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="max-h-60 overflow-y-auto space-y-1">
|
||||
{filteredMonsters.length > 0 ? (
|
||||
filteredMonsters.map((monster) => (
|
||||
{paginatedMonsters.length > 0 ? (
|
||||
paginatedMonsters.map((monster) => (
|
||||
<div
|
||||
key={monster.id}
|
||||
className="flex items-center gap-2 p-2 hover:bg-success/40 rounded cursor-pointer"
|
||||
@@ -316,13 +424,13 @@ export default function CeBar() {
|
||||
amount: 1,
|
||||
})
|
||||
setCeConfig(newCeConfig)
|
||||
setShowSearchWaveId(null);
|
||||
setShowSearchWaveId(null)
|
||||
}}
|
||||
>
|
||||
<div className="relative w-8 h-8 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
|
||||
{listMonster.find((monster2) => monster2.child.includes(Number(monster.id)))?.icon?.split("/")?.pop()?.replace(".png", "") && (
|
||||
{listMonsterDetail.find((monster2) => monster2.id === monster.id)?.icon?.split("/")?.pop()?.replace(".png", "") && (
|
||||
<Image
|
||||
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster2) => monster2.child.includes(Number(monster.id)))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
|
||||
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonsterDetail.find((monster2) => monster2.id ===monster.id)?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
|
||||
alt="Enemy Icon"
|
||||
width={376}
|
||||
height={512}
|
||||
@@ -330,7 +438,7 @@ export default function CeBar() {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span className="">{getLocaleName(locale, monster)} {`(${monster.id})`}</span>
|
||||
<span>{getLocaleName(locale, monster)} {`(${monster.id})`}</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
@@ -339,6 +447,33 @@ export default function CeBar() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{filteredMonsters.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mt-2">
|
||||
<button
|
||||
disabled={monsterPage === 1}
|
||||
className="btn btn-sm btn-outline btn-success"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setMonsterPage(monsterPage - 1)
|
||||
}}
|
||||
>
|
||||
{transI18n("previous")}
|
||||
</button>
|
||||
|
||||
<button
|
||||
disabled={!hasMoreMonsterPages}
|
||||
className="btn btn-sm btn-outline btn-success"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setMonsterPage(monsterPage + 1)
|
||||
}}
|
||||
>
|
||||
{transI18n("next")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -348,7 +483,7 @@ export default function CeBar() {
|
||||
))}
|
||||
|
||||
{/* Add New Wave Button */}
|
||||
<div className="card backdrop-blur-sm border border-slate-700/50 border-dashed">
|
||||
<div className="card border border-slate-700/50 border-dashed">
|
||||
<div className="card-body p-8">
|
||||
<button
|
||||
onClick={() => {
|
||||
|
||||
@@ -165,7 +165,11 @@ export default function SkillsInfo() {
|
||||
}}
|
||||
className={`w-full h-full object-cover rounded-xl`}
|
||||
/>
|
||||
{traceButtons.map((btn, index) => (
|
||||
{traceButtons.map((btn, index) => {
|
||||
if (!avatarInfo?.SkillTrees?.[btn.id]) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={`${btn.id} + ${index}`}
|
||||
id={btn.id}
|
||||
@@ -206,12 +210,12 @@ export default function SkillsInfo() {
|
||||
}}
|
||||
/>
|
||||
{btn.size === "big" && (
|
||||
<p className="text-xs md:text-base font-bold text-center rounded-full absolute bottom-[-1.4vw] left-1/2 transform -translate-x-1/2">{`${avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID]}/${avatarSkillTree?.[btn.id]?.["1"]?.MaxLevel}`}</p>
|
||||
<p className="text-xs md:text-base font-bold text-center rounded-full absolute bottom-[-1.4vw] left-1/2 transform -translate-x-1/2">{`${avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID] || 0}/${avatarSkillTree?.[btn.id]?.["1"]?.MaxLevel}`}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
))}
|
||||
)})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -161,7 +161,8 @@ export const traceButtonsInfo: Record<string, { id: string, size: string, left:
|
||||
{ id: 'Point15', size: 'small', left: '65%', top: '82%' },
|
||||
{ id: 'Point09', size: 'small', left: '9%', top: '51%' },
|
||||
{ id: 'Point10', size: 'small', left: '13%', top: '40%' },
|
||||
{ id: 'Point11', size: 'small', left: '13%', top: '63%' }
|
||||
{ id: 'Point11', size: 'small', left: '13%', top: '63%' },
|
||||
{ id: 'Point21', size: 'big', left: '70%', top: '34%'}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1008,6 +1009,14 @@ export const traceShowCaseMap : Record<string, Record<string, { id: string, size
|
||||
size: "big-memory",
|
||||
isLink: false
|
||||
}
|
||||
],
|
||||
|
||||
"6": [
|
||||
{
|
||||
id: "Point21",
|
||||
size: "big",
|
||||
isLink: false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,38 @@
|
||||
"use client"
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { getMonsterDetailApi, getMonsterListApi } from '@/lib/api'
|
||||
import { getMonsterValueApi, getMonsterListApi, fetchMonsterByIdsNative } from '@/lib/api'
|
||||
import { useEffect } from 'react'
|
||||
import { toast } from 'react-toastify'
|
||||
import useMonsterStore from '@/stores/monsterStore'
|
||||
import { listCurrentLanguageApi } from '@/constant/constant'
|
||||
import useLocaleStore from '@/stores/localeStore'
|
||||
|
||||
export const useFetchMonsterData = () => {
|
||||
const { setAllMapMonster, setListMonster, setAllMapMonsterInfo } = useMonsterStore()
|
||||
const { setAllMapMonster, setListMonster, setAllMapMonsterValue, setAllMapMonsterInfo } = useMonsterStore()
|
||||
const { locale } = useLocaleStore()
|
||||
const { data: dataMonster, error: errorMonster } = useQuery({
|
||||
queryKey: ['monsterData'],
|
||||
queryFn: getMonsterListApi,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
})
|
||||
const { data: dataMonsterInfo, error: errorMonsterInfo } = useQuery({
|
||||
queryKey: ['monsterInfoData'],
|
||||
queryFn: getMonsterDetailApi,
|
||||
|
||||
const { data: dataMonsterValue, error: errorMonsterValue } = useQuery({
|
||||
queryKey: ['monsterValueData'],
|
||||
queryFn: getMonsterValueApi,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
})
|
||||
|
||||
const { data: dataMonsterDetail, error: errorMonsterDetail } = useQuery({
|
||||
queryKey: ['monsterDetailData', locale],
|
||||
queryFn: () =>
|
||||
fetchMonsterByIdsNative(
|
||||
dataMonster!.list.map((item) => item.id),
|
||||
listCurrentLanguageApi[locale.toLowerCase()]
|
||||
),
|
||||
staleTime: 1000 * 60 * 5,
|
||||
enabled: !!dataMonster,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (dataMonster && !errorMonster) {
|
||||
setListMonster(dataMonster.list.sort((a, b) => Number(b.id) - Number(a.id)))
|
||||
@@ -28,10 +43,18 @@ export const useFetchMonsterData = () => {
|
||||
}, [dataMonster, errorMonster, setAllMapMonster, setListMonster])
|
||||
|
||||
useEffect(() => {
|
||||
if (dataMonsterInfo && !errorMonsterInfo) {
|
||||
setAllMapMonsterInfo(dataMonsterInfo)
|
||||
} else if (errorMonsterInfo) {
|
||||
toast.error("Failed to load monster info data")
|
||||
if (dataMonsterValue && !errorMonsterValue) {
|
||||
setAllMapMonsterValue(dataMonsterValue)
|
||||
} else if (errorMonsterValue) {
|
||||
toast.error("Failed to load monster value data")
|
||||
}
|
||||
}, [dataMonsterInfo, errorMonsterInfo, setAllMapMonsterInfo])
|
||||
}, [dataMonsterValue, errorMonsterValue, setAllMapMonsterValue])
|
||||
|
||||
useEffect(() => {
|
||||
if (dataMonsterDetail && !errorMonsterDetail) {
|
||||
setAllMapMonsterInfo(dataMonsterDetail)
|
||||
} else if (errorMonsterDetail) {
|
||||
toast.error("Failed to load monster detail data")
|
||||
}
|
||||
}, [dataMonsterDetail, errorMonsterDetail, setAllMapMonsterInfo])
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { AffixDetail, ASDetail, CharacterDetail, ConfigMaze, FreeSRJson, LightConeDetail, MocDetail, PeakDetail, PFDetail, PSResponse, RelicDetail } from "@/types";
|
||||
import { AffixDetail, ASDetail, CharacterDetail, ConfigMaze, FreeSRJson, LightConeDetail, MocDetail, MonsterDetail, PeakDetail, PFDetail, PSResponse, RelicDetail } from "@/types";
|
||||
import axios from 'axios';
|
||||
import { pSResponseSchema } from "@/zod";
|
||||
|
||||
@@ -196,7 +196,6 @@ export async function fetchMOCByIdNative(ids: string, locale: string): Promise<M
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function fetchPeakByIdsNative(ids: string[], locale: string): Promise<Record<string, PeakDetail> | null> {
|
||||
try {
|
||||
const res = await axios.post<Record<string, PeakDetail>>(`/api/${locale}/peak`, { peakIds: ids });
|
||||
@@ -217,6 +216,28 @@ export async function fetchPeakByIdNative(ids: string, locale: string): Promise<
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchMonsterByIdsNative(ids: string[], locale: string): Promise<Record<string, MonsterDetail> | null> {
|
||||
try {
|
||||
const res = await axios.post<Record<string, MonsterDetail>>(`/api/${locale}/monster`, { monsterIds: ids });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch monster:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchMonsterByIdNative(ids: string, locale: string): Promise<MonsterDetail | null> {
|
||||
try {
|
||||
const res = await axios.get<MonsterDetail>(`/api/${locale}/monster/${ids}`);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch monster:', 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 })
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { convertAvatar, convertEvent, convertLightcone, convertMonster, convertRelicSet } from "@/helper";
|
||||
import { ASDetail, CharacterBasic, CharacterBasicRaw, CharacterDetail, EventBasic, EventBasicRaw, LightConeBasic, LightConeBasicRaw, LightConeDetail, MocDetail, MonsterBasic, MonsterBasicRaw, MonsterDetail, PeakDetail, PFDetail, RelicBasic, RelicBasicRaw, RelicDetail } from "@/types";
|
||||
import { ASDetail, CharacterBasic, CharacterBasicRaw, CharacterDetail, EventBasic, EventBasicRaw, LightConeBasic, LightConeBasicRaw, LightConeDetail, MocDetail, MonsterBasic, MonsterBasicRaw, MonsterDetail, MonsterValue, PeakDetail, PFDetail, RelicBasic, RelicBasicRaw, RelicDetail } from "@/types";
|
||||
import axios from "axios";
|
||||
|
||||
export async function getLightconeInfoApi(lightconeId: number, locale: string): Promise<LightConeDetail | null> {
|
||||
@@ -352,9 +352,9 @@ export async function getMonsterListApi(): Promise<{list: MonsterBasic[], map: R
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMonsterDetailApi(): Promise<Record<string, MonsterDetail> | null> {
|
||||
export async function getMonsterValueApi(): Promise<Record<string, MonsterValue> | null> {
|
||||
try {
|
||||
const res = await axios.get<Record<string, MonsterDetail>>(
|
||||
const res = await axios.get<Record<string, MonsterValue>>(
|
||||
`https://api.hakush.in/hsr/data/monstervalue.json`,
|
||||
{
|
||||
headers: {
|
||||
@@ -373,3 +373,27 @@ export async function getMonsterDetailApi(): Promise<Record<string, MonsterDetai
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function getMonsterDetailApi(monsterId: number, locale: string): Promise<MonsterDetail | null> {
|
||||
try {
|
||||
const res = await axios.get<MonsterDetail>(
|
||||
`https://api.hakush.in/hsr/data/${locale}/monster/${monsterId}.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;
|
||||
}
|
||||
}
|
||||
@@ -7,4 +7,4 @@ export * from "./asLoader";
|
||||
export * from "./pfLoader";
|
||||
export * from "./mocLoader";
|
||||
export * from "./peakLoader";
|
||||
|
||||
export * from "./monsterLoader";
|
||||
|
||||
51
src/lib/loader/monsterLoader.ts
Normal file
51
src/lib/loader/monsterLoader.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { MonsterDetail } from '@/types';
|
||||
import { getMonsterDetailApi } from '../api';
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'data');
|
||||
const monsterFileCache: Record<string, Record<string, MonsterDetail>> = {};
|
||||
export let monsterMap: Record<string, MonsterDetail> = {};
|
||||
|
||||
function getJsonFilePath(locale: string): string {
|
||||
return path.join(DATA_DIR, `monster.${locale}.json`);
|
||||
}
|
||||
|
||||
function loadFromFileIfExists(locale: string): Record<string, MonsterDetail> | null {
|
||||
if (monsterFileCache[locale]) return monsterFileCache[locale];
|
||||
|
||||
const filePath = getJsonFilePath(locale);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, MonsterDetail>;
|
||||
monsterFileCache[locale] = data;
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function loadMonster(charIds: string[], locale: string): Promise<Record<string, MonsterDetail>> {
|
||||
const fileData = loadFromFileIfExists(locale);
|
||||
const fileIds = fileData ? Object.keys(fileData) : [];
|
||||
|
||||
if (fileData && charIds.every(id => fileIds.includes(id))) {
|
||||
monsterMap = fileData;
|
||||
return monsterMap;
|
||||
}
|
||||
|
||||
const result: Record<string, MonsterDetail> = {};
|
||||
|
||||
await Promise.all(
|
||||
charIds.map(async id => {
|
||||
const info = await getMonsterDetailApi(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');
|
||||
|
||||
monsterFileCache[locale] = result;
|
||||
monsterMap = result;
|
||||
return result;
|
||||
}
|
||||
@@ -1,27 +1,52 @@
|
||||
import { MonsterBasic, MonsterDetail } from '@/types';
|
||||
import { MonsterBasic, MonsterDetail, MonsterValue } 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;
|
||||
listMonster: MonsterBasic[]
|
||||
mapMonster: Record<string, MonsterBasic>
|
||||
mapMonsterInfo: Record<string, MonsterDetail>
|
||||
mapMonsterValue: Record<string, MonsterValue>
|
||||
setListMonster: (newListMonster: MonsterBasic[]) => void
|
||||
setMapMonsterInfo: (monsterId: string, newMonster: MonsterDetail) => void
|
||||
setAllMapMonsterInfo: (newMonster: Record<string, MonsterDetail>) => void
|
||||
setMapMonsterValue: (monsterId: string, newMonster: MonsterValue) => void
|
||||
setAllMapMonsterValue: (newMonster: Record<string, MonsterValue>) => 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 }),
|
||||
}));
|
||||
mapMonsterValue: {},
|
||||
|
||||
export default useMonsterStore;
|
||||
setListMonster: (newListMonster) =>
|
||||
set({ listMonster: newListMonster }),
|
||||
|
||||
setMapMonster: (monsterId, newMonster) =>
|
||||
set((state) => ({
|
||||
mapMonster: { ...state.mapMonster, [monsterId]: newMonster },
|
||||
})),
|
||||
|
||||
setAllMapMonster: (newMonster) =>
|
||||
set({ mapMonster: newMonster }),
|
||||
|
||||
setMapMonsterInfo: (monsterId, newMonster) =>
|
||||
set((state) => ({
|
||||
mapMonsterInfo: { ...state.mapMonsterInfo, [monsterId]: newMonster },
|
||||
})),
|
||||
|
||||
setAllMapMonsterInfo: (newMonster) =>
|
||||
set({ mapMonsterInfo: newMonster }),
|
||||
|
||||
setMapMonsterValue: (monsterId, newMonster) =>
|
||||
set((state) => ({
|
||||
mapMonsterValue: { ...state.mapMonsterValue, [monsterId]: newMonster },
|
||||
})),
|
||||
|
||||
setAllMapMonsterValue: (newMonster) =>
|
||||
set({ mapMonsterValue: newMonster }),
|
||||
}))
|
||||
|
||||
export default useMonsterStore
|
||||
|
||||
@@ -16,5 +16,6 @@ export * from "./monsterBasic"
|
||||
export * from "./pfDetail"
|
||||
export * from "./asDetail"
|
||||
export * from "./mocDetail"
|
||||
export * from "./monsterDetail"
|
||||
export * from "./monsterValue"
|
||||
export * from "./peakDetail"
|
||||
export * from "./monsterDetail"
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
export interface MonsterChild {
|
||||
export interface MonsterDetail {
|
||||
Id: number
|
||||
Name: string
|
||||
Desc: string
|
||||
MonsterCampID: number | null
|
||||
AttackBase: number
|
||||
CriticalDamageBase: number
|
||||
DefenceBase: number
|
||||
HPBase: number
|
||||
InitialDelayRatio: number
|
||||
ImagePath: string
|
||||
MinimumFatigueRatio: number
|
||||
Rank: string
|
||||
SpeedBase: number
|
||||
StanceBase: number
|
||||
StanceCount: number
|
||||
StatusResistanceBase: number
|
||||
Child: MonsterDetailChild[]
|
||||
Drop: MonsterDetailDrop[]
|
||||
}
|
||||
|
||||
export interface MonsterDetailChild {
|
||||
Id: number
|
||||
AttackModifyRatio: number
|
||||
DefenceModifyRatio: number
|
||||
@@ -7,17 +28,34 @@ export interface MonsterChild {
|
||||
SpeedModifyRatio: number
|
||||
SpeedModifyValue: number | null
|
||||
StanceModifyRatio: number
|
||||
HardLevelGroup: number
|
||||
StanceWeakList: string[]
|
||||
HardLevelGroup: number
|
||||
DamageTypeResistance: MonsterDetailElementResistance[]
|
||||
SkillList: MonsterDetailSkill[]
|
||||
}
|
||||
|
||||
export interface MonsterDetail {
|
||||
Rank: string
|
||||
AttackBase: number
|
||||
DefenceBase: number
|
||||
HPBase: number
|
||||
SpeedBase: number
|
||||
StanceBase: number
|
||||
StatusResistanceBase: number
|
||||
child: MonsterChild[]
|
||||
export interface MonsterDetailElementResistance {
|
||||
$type: string
|
||||
DamageType: string
|
||||
Value: number
|
||||
}
|
||||
|
||||
export interface MonsterDetailSkill {
|
||||
Id: number
|
||||
SkillName: string | null
|
||||
SkillDesc: string | null
|
||||
DamageType: string
|
||||
SPHitBase: number | string
|
||||
}
|
||||
|
||||
export interface MonsterDetailDrop {
|
||||
MonsterTemplateID: number
|
||||
WorldLevel?: number
|
||||
AvatarExpReward: number
|
||||
DisplayItemList: MonsterDetailDropItem[]
|
||||
}
|
||||
|
||||
export interface MonsterDetailDropItem {
|
||||
$type: string
|
||||
ID: number
|
||||
}
|
||||
|
||||
23
src/types/monsterValue.ts
Normal file
23
src/types/monsterValue.ts
Normal 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 MonsterValue {
|
||||
Rank: string
|
||||
AttackBase: number
|
||||
DefenceBase: number
|
||||
HPBase: number
|
||||
SpeedBase: number
|
||||
StanceBase: number
|
||||
StatusResistanceBase: number
|
||||
child: MonsterChild[]
|
||||
}
|
||||
Reference in New Issue
Block a user