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

This commit is contained in:
2025-09-26 18:29:33 +07:00
parent 95c700fd9f
commit af4451fd97
19 changed files with 1251278 additions and 128 deletions

File diff suppressed because it is too large Load Diff

229269
data/monster.cn.json Normal file

File diff suppressed because it is too large Load Diff

229269
data/monster.en.json Normal file

File diff suppressed because it is too large Load Diff

229269
data/monster.jp.json Normal file

File diff suppressed because it is too large Load Diff

229269
data/monster.kr.json Normal file

File diff suppressed because it is too large Load Diff

View 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)
}

View 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 });
}
}

View File

@@ -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={() => {

View File

@@ -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>

View File

@@ -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
}
]
}
}

View File

@@ -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])
}

View File

@@ -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 })

View File

@@ -1,5 +1,5 @@
import { convertAvatar, convertEvent, convertLightcone, convertMonster, convertRelicSet } from "@/helper";
import { ASDetail, CharacterBasic, CharacterBasicRaw, CharacterDetail, EventBasic, EventBasicRaw, LightConeBasic, LightConeBasicRaw, LightConeDetail, MocDetail, MonsterBasic, MonsterBasicRaw, MonsterDetail, 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;
}
}

View File

@@ -7,4 +7,4 @@ export * from "./asLoader";
export * from "./pfLoader";
export * from "./mocLoader";
export * from "./peakLoader";
export * from "./monsterLoader";

View 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;
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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
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 MonsterValue {
Rank: string
AttackBase: number
DefenceBase: number
HPBase: number
SpeedBase: number
StanceBase: number
StatusResistanceBase: number
child: MonsterChild[]
}