UPDATE: Support skills_by_anchor_type
Some checks failed
Gitea Auto Deploy / Deploy-Container (push) Failing after 1m4s

This commit is contained in:
2026-01-11 23:12:45 +07:00
parent 81023dc006
commit 6a9d99b30f
37 changed files with 7122 additions and 214 deletions

View File

@@ -275,5 +275,6 @@
"detailHiddenUi": "Enabling this feature will hide the game UI.", "detailHiddenUi": "Enabling this feature will hide the game UI.",
"detailDisableCensorship": "Enabling this feature will disable in-game censorship.", "detailDisableCensorship": "Enabling this feature will disable in-game censorship.",
"detailMultipathCharacter": "Allows changing the Path of certain characters." "detailMultipathCharacter": "Allows changing the Path of certain characters."
} }
} }

View File

@@ -0,0 +1,10 @@
[
{
"version": "3.8.5",
"date": "11/1/2026",
"type": "improvement",
"items": [
"Support skills_by_anchor_type"
]
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client"; "use client";
import useListAvatarStore from "@/stores/avatarStore"; import useListAvatarStore from "@/stores/avatarStore";
@@ -11,7 +12,7 @@ import { useEffect, useMemo, useState } from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import useModelStore from "@/stores/modelStore"; import useModelStore from "@/stores/modelStore";
import useUserDataStore from "@/stores/userDataStore"; import useUserDataStore from "@/stores/userDataStore";
import { RelicStore } from "@/types"; import { ModalConfig, RelicStore } from "@/types";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import useGlobalStore from "@/stores/globalStore"; import useGlobalStore from "@/stores/globalStore";
import { connectToPS, syncDataToPS } from "@/helper"; import { connectToPS, syncDataToPS } from "@/helper";
@@ -91,40 +92,6 @@ export default function ActionBar() {
} }
}; };
// Handle ESC key to close modal
useEffect(() => {
if (!isOpenCreateProfile) {
handleCloseModal("update_profile_modal");
return;
}
if (!isOpenCopy) {
handleCloseModal("copy_profile_modal");
return;
}
console.log(isOpenAvatars)
if (!isOpenAvatars) {
handleCloseModal("avatars_modal");
return;
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpenCreateProfile) {
handleCloseModal("update_profile_modal");
}
if (event.key === 'Escape' && isOpenCopy) {
handleCloseModal("copy_profile_modal");
}
if (event.key === 'Escape' && isOpenAvatars) {
handleCloseModal("avatars_modal");
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenCopy, isOpenCreateProfile, isOpenAvatars]);
const actionMove = (path: string) => { const actionMove = (path: string) => {
router.push(`/${path}`) router.push(`/${path}`)
@@ -168,11 +135,11 @@ export default function ActionBar() {
} }
} }
const modalConfigs: ModalConfig[] = [
const modalConfigs = [
{ {
id: "update_profile_modal", id: "update_profile_modal",
title: formState === "CREATE" ? transI18n("createNewProfile") : transI18n("editProfile"), title: formState === "CREATE" ? transI18n("createNewProfile") : transI18n("editProfile"),
isOpen: isOpenCreateProfile,
onClose: () => { onClose: () => {
setIsOpenCreateProfile(false) setIsOpenCreateProfile(false)
handleCloseModal("update_profile_modal") handleCloseModal("update_profile_modal")
@@ -204,6 +171,7 @@ export default function ActionBar() {
{ {
id: "copy_profile_modal", id: "copy_profile_modal",
title: transI18n("copyProfiles").toUpperCase(), title: transI18n("copyProfiles").toUpperCase(),
isOpen: isOpenCopy,
onClose: () => { onClose: () => {
setIsOpenCopy(false) setIsOpenCopy(false)
handleCloseModal("copy_profile_modal") handleCloseModal("copy_profile_modal")
@@ -213,6 +181,7 @@ export default function ActionBar() {
{ {
id: "avatars_modal", id: "avatars_modal",
title: transI18n("avatars").toUpperCase(), title: transI18n("avatars").toUpperCase(),
isOpen: isOpenAvatars,
onClose: () => { onClose: () => {
setIsOpenAvatars(false) setIsOpenAvatars(false)
handleCloseModal("avatars_modal") handleCloseModal("avatars_modal")
@@ -221,6 +190,26 @@ export default function ActionBar() {
} }
] ]
// Handle ESC key to close modal
useEffect(() => {
for (const item of modalConfigs) {
if (!item?.isOpen) {
handleCloseModal(item?.id || "")
}
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
for (const item of modalConfigs) {
handleCloseModal(item?.id || "")
}
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenCopy, isOpenCreateProfile, isOpenAvatars]);
return ( return (
<div className="w-full px-4 pb-4 bg-base-200"> <div className="w-full px-4 pb-4 bg-base-200">

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client" "use client"
import useAvatarStore from "@/stores/avatarStore" import useAvatarStore from "@/stores/avatarStore"
@@ -69,7 +70,6 @@ export default function AvatarInfo() {
window.addEventListener('keydown', handleEscKey); window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey); return () => window.removeEventListener('keydown', handleEscKey);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpenLightcone]); }, [isOpenLightcone]);
return ( return (

View File

@@ -0,0 +1,94 @@
import useLocaleStore from '@/stores/localeStore';
import { Check, AlertCircle, Sparkles, Bug, Zap, Package, Calendar } from 'lucide-react';
export default function ChangelogBar() {
const { changelog } = useLocaleStore()
const getIcon = (type: string) => {
switch (type) {
case 'feature':
return <Sparkles className="w-3 h-3 md:w-4 md:h-4" />;
case 'fix':
return <Bug className="w-3 h-3 md:w-4 md:h-4" />;
case 'improvement':
return <Zap className="w-3 h-3 md:w-4 md:h-4" />;
default:
return <Package className="w-3 h-3 md:w-4 md:h-4" />;
}
};
const getBadgeClass = (type: string) => {
switch (type) {
case 'feature':
return 'badge-success';
case 'fix':
return 'badge-error';
case 'improvement':
return 'badge-warning';
default:
return 'badge-info';
}
};
const getTypeLabel = (type: string) => {
switch (type) {
case 'feature':
return 'Feature';
case 'fix':
return 'Fix';
case 'improvement':
return 'Improvement';
default:
return 'Update';
}
};
return (
<div className="md:p-4">
{/* Alert */}
<div className="alert alert-info mb-4 md:p-2 p-1">
<AlertCircle className="w-6 h-6" />
<div>
<h3 className="font-bold text-sm">If you have any suggestions or problems, please contact me! @kain0304</h3>
</div>
</div>
{/* Timeline */}
<div className="flex flex-col gap-4">
{changelog.map((change, index) => (
<div key={index} className="bg-base-100 shadow-sm">
<div className="flex flex-col gap-2">
{/* Version Header */}
<div className="flex flex-row items-center gap-2 md:gap-3">
<div className={`badge ${getBadgeClass(change.type)} gap-1 md:gap-2 p-1`}>
{getIcon(change.type)}
{getTypeLabel(change.type)}
</div>
<h2 className="card-title text-sm md:text-lg">
Version {change.version}
</h2>
<div className="flex items-center gap-1 md:gap-2 text-base-content/60 ml-auto">
<Calendar className="w-3 h-3 md:w-4 md:h-4" />
<span className="text-sm">{change.date}</span>
</div>
</div>
{/* Changes List */}
<ul className="space-y-3">
{change.items.map((item, itemIndex) => (
<li key={itemIndex} className="flex items-start gap-3">
<div className="mt-1">
<Check className="w-5 h-5 text-success" />
</div>
<span className="text-base-content">{item}</span>
</li>
))}
</ul>
</div>
</div>
))}
</div>
</div>
);
}

View File

@@ -3,6 +3,7 @@
import { import {
useFetchASData, useFetchASData,
useFetchAvatarData, useFetchAvatarData,
useFetchChangelog,
useFetchConfigData, useFetchConfigData,
useFetchLightconeData, useFetchLightconeData,
useFetchMOCData, useFetchMOCData,
@@ -10,7 +11,7 @@ import {
useFetchPEAKData, useFetchPEAKData,
useFetchPFData, useFetchPFData,
useFetchRelicData useFetchRelicData
} from "@/hooks"; } from "@/lib/hooks";
export default function ClientDataFetcher() { export default function ClientDataFetcher() {
useFetchConfigData(); useFetchConfigData();
@@ -22,6 +23,7 @@ export default function ClientDataFetcher() {
useFetchMOCData(); useFetchMOCData();
useFetchASData(); useFetchASData();
useFetchPEAKData(); useFetchPEAKData();
useFetchChangelog();
return null; return null;
} }

View File

@@ -22,7 +22,7 @@ export default function ExtraSettingBar() {
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-bold flex items-center gap-2"> <h3 className="text-lg font-bold flex items-center gap-2">
{transI18n("theoryCraft")} {transI18n("theoryCraft")}
<div className="tooltip tooltip-info" data-tip={transI18n("detailTheoryCraft")}> <div className="tooltip tooltip-info tooltip-bottom" data-tip={transI18n("detailTheoryCraft")}>
<Info className="text-primary" size={20} /> <Info className="text-primary" size={20} />
</div> </div>
</h3> </h3>
@@ -78,13 +78,11 @@ export default function ExtraSettingBar() {
</div> </div>
)} )}
{/*MULTIPATH CHAR */} {/*MULTIPATH CHAR */}
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-lg font-bold flex items-center gap-2"> <h3 className="text-lg font-bold flex items-center gap-2">
{transI18n("multipathCharacter")} {transI18n("multipathCharacter")}
<div className="tooltip tooltip-info" data-tip={transI18n("detailMultipathCharacter")}> <div className="tooltip tooltip-info tooltip-bottom" data-tip={transI18n("detailMultipathCharacter")}>
<Info className="text-primary" size={20} /> <Info className="text-primary" size={20} />
</div> </div>
</h3> </h3>
@@ -157,7 +155,7 @@ export default function ExtraSettingBar() {
<label className="flex items-center gap-3"> <label className="flex items-center gap-3">
<Swords className="text-error" size={20} /> <Swords className="text-error" size={20} />
<span className="label-text font-semibold">{transI18n("anomalyArbitration")}</span> <span className="label-text font-semibold">{transI18n("anomalyArbitration")}</span>
<div className="tooltip tooltip-info" data-tip={transI18n("detailChallengePeak")}> <div className="tooltip tooltip-info tooltip-bottom" data-tip={transI18n("detailChallengePeak")}>
<Info className="text-primary" size={20} /> <Info className="text-primary" size={20} />
</div> </div>
<select <select
@@ -187,7 +185,7 @@ export default function ExtraSettingBar() {
<label className="flex items-center gap-3"> <label className="flex items-center gap-3">
<SkipForward className="text-warning" size={20} /> <SkipForward className="text-warning" size={20} />
<span className="label-text font-semibold">{transI18n("skipNode")}</span> <span className="label-text font-semibold">{transI18n("skipNode")}</span>
<div className="tooltip tooltip-info" data-tip={transI18n("detailSkipNode")}> <div className="tooltip tooltip-info tooltip-bottom" data-tip={transI18n("detailSkipNode")}>
<Info className="text-primary" size={20} /> <Info className="text-primary" size={20} />
</div> </div>
<select <select
@@ -241,7 +239,7 @@ export default function ExtraSettingBar() {
} }
/> />
<span className="label-text font-semibold">{transI18n("hideUI")}</span> <span className="label-text font-semibold">{transI18n("hideUI")}</span>
<div className="tooltip tooltip-info" data-tip={transI18n("detailHiddenUi")}> <div className="tooltip tooltip-info tooltip-bottom" data-tip={transI18n("detailHiddenUi")}>
<Info className="text-primary" size={20} /> <Info className="text-primary" size={20} />
</div> </div>
</label> </label>
@@ -269,7 +267,7 @@ export default function ExtraSettingBar() {
} }
/> />
<span className="label-text font-semibold">{transI18n("disableCensorship")}</span> <span className="label-text font-semibold">{transI18n("disableCensorship")}</span>
<div className="tooltip tooltip-info" data-tip={transI18n("detailDisableCensorship")}> <div className="tooltip tooltip-info tooltip-bottom" data-tip={transI18n("detailDisableCensorship")}>
<Info className="text-primary" size={20} /> <Info className="text-primary" size={20} />
</div> </div>
</label> </label>
@@ -277,8 +275,6 @@ export default function ExtraSettingBar() {
)} )}
</div> </div>
</div> </div>
) )
} }

View File

@@ -1,8 +1,6 @@
export default function Footer() { export default function Footer() {
return ( return (
<footer className="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10"> <footer className="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10">
<aside> <aside>
<p>Copyright © {new Date().getFullYear()} - Kain (Powered by Nextjs & DaisyUi)</p> <p>Copyright © {new Date().getFullYear()} - Kain (Powered by Nextjs & DaisyUi)</p>
</aside> </aside>

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client" "use client"
import { downloadJson } from "@/helper"; import { downloadJson } from "@/helper";
import { converterToFreeSRJson } from "@/helper/converterToFreeSRJson"; import { converterToFreeSRJson } from "@/helper/converterToFreeSRJson";
@@ -20,6 +21,9 @@ import MonsterBar from "../monsterBar";
import Image from "next/image"; import Image from "next/image";
import ConnectBar from "../connectBar"; import ConnectBar from "../connectBar";
import ExtraSettingBar from "../extraSettingBar"; import ExtraSettingBar from "../extraSettingBar";
import ChangelogBar from "../changelog";
import { ModalConfig } from "@/types";
import { ScrollText } from "lucide-react";
const themes = [ const themes = [
{ label: "Winter" }, { label: "Winter" },
@@ -58,7 +62,9 @@ export default function Header() {
setIsOpenConnect, setIsOpenConnect,
isOpenConnect, isOpenConnect,
setIsOpenExtra, setIsOpenExtra,
isOpenExtra isOpenExtra,
setIsChangelog,
isChangelog
} = useModelStore() } = useModelStore()
const [importModal, setImportModal] = useState("enka"); const [importModal, setImportModal] = useState("enka");
@@ -111,76 +117,7 @@ export default function Header() {
} }
}; };
// Handle ESC key to close modal const modalConfigs: ModalConfig[] = [
useEffect(() => {
if (!isOpenImport) {
handleCloseModal("import_modal");
return;
}
if (!isOpenMonster) {
handleCloseModal("monster_modal");
return;
}
if (!isOpenConnect) {
handleCloseModal("connect_modal");
return;
}
if (!isOpenExtra) {
handleCloseModal("extra_modal");
return;
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
handleCloseModal("connect_modal");
handleCloseModal("import_modal");
handleCloseModal("monster_modal");
handleCloseModal("extra_modal");
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenImport, isOpenMonster, isOpenConnect, isOpenExtra]);
const handleImportDatabase = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) {
toast.error(transI18n("pleaseSelectAFile"))
return
}
if (!file.name.endsWith(".json") || file.type !== "application/json") {
toast.error(transI18n("fileMustBeAValidJsonFile"))
return
}
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target?.result as string);
const parsed = micsSchema.parse(data)
setAvatars(parsed.avatars)
setBattleType(parsed.battle_type)
setMocConfig(parsed.moc_config)
setPfConfig(parsed.pf_config)
setAsConfig(parsed.as_config)
setCeConfig(parsed.ce_config)
setPeakConfig(parsed.peak_config)
toast.success(transI18n("importDatabaseSuccess"))
} catch {
toast.error(transI18n("fileMustBeAValidJsonFile"))
}
};
reader.readAsText(file);
}
};
const modalConfigs = [
{ {
id: "connect_modal", id: "connect_modal",
title: transI18n("psConnection"), title: transI18n("psConnection"),
@@ -225,9 +162,75 @@ export default function Header() {
handleCloseModal("extra_modal") handleCloseModal("extra_modal")
}, },
content: <ExtraSettingBar /> content: <ExtraSettingBar />
},
{
id: "changelog_modal",
title: "Changelog",
isOpen: isChangelog,
onClose: () => {
setIsChangelog(false)
handleCloseModal("changelog_modal")
},
content: <ChangelogBar />
} }
] ]
// Handle ESC key to close modal
useEffect(() => {
for (const item of modalConfigs) {
if (!item?.isOpen) {
handleCloseModal(item?.id || "")
} else {
handleShow(item?.id || "")
}
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
for (const item of modalConfigs) {
handleCloseModal(item?.id || "")
}
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenImport, isOpenMonster, isOpenConnect, isOpenExtra, isChangelog]);
const handleImportDatabase = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) {
toast.error(transI18n("pleaseSelectAFile"))
return
}
if (!file.name.endsWith(".json") || file.type !== "application/json") {
toast.error(transI18n("fileMustBeAValidJsonFile"))
return
}
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target?.result as string);
const parsed = micsSchema.parse(data)
setAvatars(parsed.avatars)
setBattleType(parsed.battle_type)
setMocConfig(parsed.moc_config)
setPfConfig(parsed.pf_config)
setAsConfig(parsed.as_config)
setCeConfig(parsed.ce_config)
setPeakConfig(parsed.peak_config)
toast.success(transI18n("importDatabaseSuccess"))
} catch {
toast.error(transI18n("fileMustBeAValidJsonFile"))
}
};
reader.readAsText(file);
}
};
return ( return (
<div className="navbar bg-base-100 shadow-md sticky top-0 z-50 px-3 py-1"> <div className="navbar bg-base-100 shadow-md sticky top-0 z-50 px-3 py-1">
@@ -250,7 +253,6 @@ export default function Header() {
<li><a onClick={() => { <li><a onClick={() => {
setImportModal("freesr") setImportModal("freesr")
setIsOpenImport(true) setIsOpenImport(true)
handleShow("import_modal")
}}>{transI18n("freeSr")}</a></li> }}>{transI18n("freeSr")}</a></li>
<li> <li>
<> <>
@@ -271,7 +273,6 @@ export default function Header() {
<li><a onClick={() => { <li><a onClick={() => {
setImportModal("enka") setImportModal("enka")
setIsOpenImport(true) setIsOpenImport(true)
handleShow("import_modal")
}}>{transI18n("enka")}</a></li> }}>{transI18n("enka")}</a></li>
</ul> </ul>
</details> </details>
@@ -308,7 +309,6 @@ export default function Header() {
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium" className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => { onClick={() => {
setIsOpenConnect(true) setIsOpenConnect(true)
handleShow("connect_modal")
}} }}
> >
{transI18n("connectSetting")} {transI18n("connectSetting")}
@@ -318,7 +318,6 @@ export default function Header() {
<button <button
onClick={() => { onClick={() => {
setIsOpenMonster(true) setIsOpenMonster(true)
handleShow("monster_modal")
}} }}
className="disabled px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium" className="disabled px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
> >
@@ -331,7 +330,6 @@ export default function Header() {
<button <button
onClick={() => { onClick={() => {
setIsOpenExtra(true) setIsOpenExtra(true)
handleShow("extra_modal")
}} }}
className="disabled px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium" className="disabled px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
> >
@@ -346,9 +344,9 @@ export default function Header() {
<a className="hidden sm:grid sm:grid-cols-1 items-start justify-items-center text-left gap-0 hover:scale-105 px-2"> <a className="hidden sm:grid sm:grid-cols-1 items-start justify-items-center text-left gap-0 hover:scale-105 px-2">
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<Image src="/ff-srtool.png" alt="Logo" width={50} height={50} /> <Image src="/ff-srtool.png" alt="Logo" width={250} height={250} className="w-10 h-10 xl:w-12 xl:h-12 object-contain" />
<div className="flex flex-col justify-center items-start"> <div className="flex flex-col justify-center items-start">
<h1 className="text-xl font-bold"> <h1 className="text-lg xl:text-xl font-bold leading-tight">
<span className="text-emerald-500">Firefly Sr</span> <span className="text-emerald-500">Firefly Sr</span>
<span className="bg-clip-text text-transparent bg-linear-to-r from-emerald-400 via-orange-500 to-red-500"> <span className="bg-clip-text text-transparent bg-linear-to-r from-emerald-400 via-orange-500 to-red-500">
Tools Tools
@@ -371,7 +369,6 @@ export default function Header() {
<li><a onClick={() => { <li><a onClick={() => {
setImportModal("freesr") setImportModal("freesr")
setIsOpenImport(true) setIsOpenImport(true)
handleShow("import_modal")
}}>{transI18n("freeSr")}</a></li> }}>{transI18n("freeSr")}</a></li>
<li> <li>
<> <>
@@ -392,7 +389,6 @@ export default function Header() {
<li><a onClick={() => { <li><a onClick={() => {
setImportModal("enka") setImportModal("enka")
setIsOpenImport(true) setIsOpenImport(true)
handleShow("import_modal")
}}>{transI18n("enka")}</a></li> }}>{transI18n("enka")}</a></li>
</ul> </ul>
</details> </details>
@@ -429,7 +425,6 @@ export default function Header() {
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium" className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => { onClick={() => {
setIsOpenConnect(true) setIsOpenConnect(true)
handleShow("connect_modal")
}} }}
> >
{transI18n("connectSetting")} {transI18n("connectSetting")}
@@ -439,7 +434,6 @@ export default function Header() {
<button <button
onClick={() => { onClick={() => {
setIsOpenMonster(true) setIsOpenMonster(true)
handleShow("monster_modal")
}} }}
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium" className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
> >
@@ -451,7 +445,6 @@ export default function Header() {
<button <button
onClick={() => { onClick={() => {
setIsOpenExtra(true) setIsOpenExtra(true)
handleShow("extra_modal")
}} }}
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium" className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
> >
@@ -462,18 +455,27 @@ export default function Header() {
</ul> </ul>
</div> </div>
{/* Right side items */} {/* Right side items */}
<div className="navbar-end gap-2"> <div className="navbar-end gap-2 pr-1 ">
<div className="px-2"> <div data-tip="Connection Status" className="tooltip tooltip-bottom">
<div className="flex items-center space-x-2 p-1.5 rounded-full shadow-md"> <div className="flex items-center space-x-2 p-1.5 rounded-full shadow-md">
<div className={`hidden lg:block text-sm italic ${isConnectPS ? 'text-green-500' : 'text-red-500'}`}> <div className={`hidden xl:block text-sm italic ${isConnectPS ? 'text-green-500' : 'text-red-500'}`}>
{isConnectPS ? transI18n("connected") : transI18n("unconnected")} {isConnectPS ? transI18n("connected") : transI18n("unconnected")}
</div> </div>
<div className={`w-3 h-3 rounded-full ${isConnectPS ? 'bg-green-500' : 'bg-red-500'}`}></div> <div className={`w-3 h-3 rounded-full ${isConnectPS ? 'bg-green-500' : 'bg-red-500'}`}></div>
</div> </div>
</div> </div>
<div
data-tip="View Logs"
className="btn btn-ghost btn-sm btn-circle items-center justify-center w-fit tooltip tooltip-bottom"
onClick={() => {
setIsChangelog(true)
}}
>
<ScrollText className="w-5 h-5 text-warning" />
</div>
{/* Language selector - REFINED */} {/* Language selector - REFINED */}
<div className="dropdown dropdown-end"> <div className="dropdown dropdown-end">
<div className="flex items-center gap-1 border border-base-300 rounded text-sm px-1.5 py-0.5 hover:bg-base-200 cursor-pointer transition-all duration-200"> <div className="flex items-center gap-1 border border-base-300 rounded text-sm px-1.5 py-0.5 hover:bg-base-200 cursor-pointer transition-all duration-200">
@@ -493,7 +495,7 @@ export default function Header() {
</div> </div>
</div> </div>
<div title="Change Theme" className="dropdown dropdown-end"> <div className="dropdown dropdown-end">
<div tabIndex={0} role="button" className="btn btn-ghost btn-sm hover:bg-base-200 transition-all duration-200 px-2"> <div tabIndex={0} role="button" className="btn btn-ghost btn-sm hover:bg-base-200 transition-all duration-200 px-2">
<svg <svg
width={16} width={16}
@@ -557,10 +559,11 @@ export default function Header() {
{/* GitHub Link */} {/* GitHub Link */}
<Link <Link
className='hidden sm:flex btn btn-ghost btn-sm btn-circle bg-white/20 hover:bg-white transition-all duration-200 items-center justify-center' className='flex btn btn-ghost btn-sm btn-circle bg-white/20 hover:bg-white transition-all duration-200 items-center justify-center tooltip tooltip-bottom'
href={"https://github.com/AzenKain/Firefly-Srtools"} href={"https://github.com/AzenKain/Firefly-Srtools"}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
data-tip="Github"
> >
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"> <svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512">
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" /> <path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
@@ -568,7 +571,7 @@ export default function Header() {
</Link> </Link>
</div> </div>
{modalConfigs.map(({ id, title, onClose, content }) => ( {modalConfigs?.map(({ id, title, onClose, content }) => (
<dialog key={id} id={id} className="modal"> <dialog key={id} id={id} className="modal">
<div className="modal-box w-11/12 max-w-7xl max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20"> <div className="modal-box w-11/12 max-w-7xl max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10"> <div className="sticky top-0 z-10">

View File

@@ -270,6 +270,14 @@ export default function QuickView() {
unit: "%", unit: "%",
round: 1 round: 1
}, },
ElationAdd: {
value: 0,
base: 0,
name: "Elation Boost",
icon: "/icon/IconJoy.webp",
unit: "%",
round: 1
}
} }
if (avatarProfile?.lightcone && mapLightconeInfo[avatarProfile?.lightcone?.item_id]) { if (avatarProfile?.lightcone && mapLightconeInfo[avatarProfile?.lightcone?.item_id]) {

View File

@@ -238,7 +238,7 @@ export default function RelicMaker() {
} }
return ( return (
<div className=""> <div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4"> <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-linear-to-r from-pink-400 to-cyan-400"> <h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{transI18n("relicMaker")} {transI18n("relicMaker")}
@@ -357,7 +357,7 @@ export default function RelicMaker() {
{/* Right Panel - Sub Stats */} {/* Right Panel - Sub Stats */}
<div className="space-y-4"> <div className="space-y-4">
{/* Total Roll */} {/* Total Roll */}
<div className="bg-base-100 rounded-xl p-4 border border-slate-700 z-[1]"> <div className="bg-base-100 rounded-xl p-4 border border-slate-700 z-1">
<h3 className="text-lg font-bold mb-4">{transI18n("totalRoll")} {listSelectedSubStats.reduce((a, b) => a + b.rollCount, 0)}</h3> <h3 className="text-lg font-bold mb-4">{transI18n("totalRoll")} {listSelectedSubStats.reduce((a, b) => a + b.rollCount, 0)}</h3>
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client"; "use client";
import { useCallback, useEffect, useMemo } from "react"; import { useCallback, useEffect, useMemo } from "react";
import RelicMaker from "../relicBar"; import RelicMaker from "../relicBar";
@@ -12,6 +13,7 @@ import { replaceByParam } from '@/helper';
import useRelicMakerStore from '@/stores/relicMakerStore'; import useRelicMakerStore from '@/stores/relicMakerStore';
import useAffixStore from '@/stores/affixStore'; import useAffixStore from '@/stores/affixStore';
import QuickView from "../quickView"; import QuickView from "../quickView";
import { ModalConfig } from "@/types";
export default function RelicsInfo() { export default function RelicsInfo() {
const { avatars, setAvatars } = useUserDataStore() const { avatars, setAvatars } = useUserDataStore()
@@ -53,30 +55,6 @@ export default function RelicsInfo() {
} }
}; };
// Handle ESC key to close modal
useEffect(() => {
if (!isOpenRelic) {
handleCloseModal("action_detail_modal");
return;
};
if (!isOpenQuickView) {
handleCloseModal("quick_view_modal");
return;
};
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpenRelic) {
handleCloseModal("action_detail_modal");
}
if (event.key === 'Escape' && isOpenQuickView) {
handleCloseModal("quick_view_modal");
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpenRelic]);
const getRelic = useCallback((slot: string) => { const getRelic = useCallback((slot: string) => {
const avatar = avatars[avatarSelected?.id || ""]; const avatar = avatars[avatarSelected?.id || ""];
@@ -150,10 +128,11 @@ export default function RelicsInfo() {
return listEffects; return listEffects;
}, [avatars, avatarSelected]); }, [avatars, avatarSelected]);
const modalConfigs = [ const modalConfigs: ModalConfig[] = [
{ {
id: "action_detail_modal", id: "action_detail_modal",
title: null, // không có title title: "",
isOpen: isOpenRelic,
onClose: () => { onClose: () => {
setIsOpenRelic(false) setIsOpenRelic(false)
handleCloseModal("action_detail_modal") handleCloseModal("action_detail_modal")
@@ -163,6 +142,7 @@ export default function RelicsInfo() {
{ {
id: "quick_view_modal", id: "quick_view_modal",
title: transI18n("quickView").toUpperCase(), title: transI18n("quickView").toUpperCase(),
isOpen: isOpenQuickView,
onClose: () => { onClose: () => {
setIsOpenQuickView(false) setIsOpenQuickView(false)
handleCloseModal("quick_view_modal") handleCloseModal("quick_view_modal")
@@ -171,6 +151,24 @@ export default function RelicsInfo() {
} }
] ]
// Handle ESC key to close modal
useEffect(() => {
for (const item of modalConfigs) {
if (!item?.isOpen) {
handleCloseModal(item?.id || "")
}
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
for (const item of modalConfigs) {
handleCloseModal(item?.id || "")
}
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenRelic]);
return ( return (
<div className="max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden"> <div className="max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">

View File

@@ -1,3 +1,4 @@
import useMazeStore from "@/stores/mazeStore";
import { ASConfigStore, AvatarJson, AvatarStore, BattleConfigJson, CEConfigStore, FreeSRJson, LightconeJson, MOCConfigStore, PEAKConfigStore, PFConfigStore, RelicJson } from "@/types"; import { ASConfigStore, AvatarJson, AvatarStore, BattleConfigJson, CEConfigStore, FreeSRJson, LightconeJson, MOCConfigStore, PEAKConfigStore, PFConfigStore, RelicJson } from "@/types";
@@ -10,6 +11,7 @@ export function converterToFreeSRJson(
ce_config: CEConfigStore, ce_config: CEConfigStore,
peak_config: PEAKConfigStore, peak_config: PEAKConfigStore,
): FreeSRJson { ): FreeSRJson {
const { SkillTree } = useMazeStore.getState()
const lightcones: LightconeJson[] = [] const lightcones: LightconeJson[] = []
const relics: RelicJson[] = [] const relics: RelicJson[] = []
let battleJson: BattleConfigJson let battleJson: BattleConfigJson
@@ -78,11 +80,22 @@ export function converterToFreeSRJson(
const avatarsJson: { [key: string]: AvatarJson } = {} const avatarsJson: { [key: string]: AvatarJson } = {}
let internalUidLightcone = 0 let internalUidLightcone = 0
let internalUidRelic = 0 let internalUidRelic = 0
Object.entries(avatars).forEach(([avatarId, avatar]) => { Object.entries(avatars).forEach(([avatarId, avatar]) => {
const skillsByAnchorType: Record<string, number> = {}
for (const [skillId, level] of Object.entries(avatar.data.skills)) {
if (SkillTree[skillId]) {
skillsByAnchorType[SkillTree[skillId].index_slot] = level > SkillTree[skillId].max_level ? SkillTree[skillId].max_level : level
}
}
avatarsJson[avatarId] = { avatarsJson[avatarId] = {
owner_uid: Number(avatar.owner_uid || 0), owner_uid: Number(avatar.owner_uid || 0),
avatar_id: Number(avatar.avatar_id || 0), avatar_id: Number(avatar.avatar_id || 0),
data: avatar.data, data: {
rank: Number(avatar.data.rank || 0),
skills: avatar.data.skills,
skills_by_anchor_type: Object.keys(skillsByAnchorType).length > 0 ? skillsByAnchorType : undefined,
},
level: Number(avatar.level || 0), level: Number(avatar.level || 0),
promotion: Number(avatar.promotion || 0), promotion: Number(avatar.promotion || 0),
techniques: avatar.techniques, techniques: avatar.techniques,

View File

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

View File

@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { AffixDetail, ASDetail, CharacterDetail, ConfigMaze, FreeSRJson, LightConeDetail, MocDetail, MonsterDetail, PeakDetail, PFDetail, PSResponse, RelicDetail } from "@/types"; import { AffixDetail, ASDetail, ChangelogItemType, CharacterDetail, ConfigMaze, FreeSRJson, LightConeDetail, MocDetail, MonsterDetail, PeakDetail, PFDetail, PSResponse, RelicDetail } from "@/types";
import axios from 'axios'; import axios from 'axios';
import { psResponseSchema } from "@/zod"; import { psResponseSchema } from "@/zod";
import { ExtraData } from "@/types"; import { ExtraData } from "@/types";
@@ -151,6 +151,16 @@ export async function fetchMonstersApi(locale: string): Promise<Record<string, M
} }
} }
export async function fetchChangelog(): Promise<ChangelogItemType[] | null> {
try {
const res = await axios.get<ChangelogItemType[]>(`/data/changelog.json`);
return res.data;
} catch (error) {
console.error('Failed to fetch monster:', error);
return null;
}
}
export async function SendDataToServer( export async function SendDataToServer(
username: string, username: string,
password: string, password: string,

10
src/lib/hooks/index.ts Normal file
View File

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

View File

@@ -0,0 +1,31 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { fetchChangelog } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useModelStore from '@/stores/modelStore'
import useLocaleStore from '@/stores/localeStore'
export const useFetchChangelog = () => {
const { currentVersion, setChangelog, setCurrentVersion } = useLocaleStore()
const { setIsChangelog } = useModelStore()
const { data: dataChangelog, error: errorChangelog } = useQuery({
queryKey: ['changelog'],
queryFn: fetchChangelog,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (dataChangelog && !errorChangelog) {
setChangelog(dataChangelog)
if (dataChangelog?.[0] && dataChangelog[0].version != currentVersion) {
setIsChangelog(true)
setCurrentVersion(dataChangelog[0].version)
}
} else if (errorChangelog) {
toast.error("Failed to load changelog data")
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataChangelog, errorChangelog, setChangelog, setCurrentVersion, setIsChangelog])
}

View File

@@ -1,3 +1,4 @@
import { ChangelogItemType } from '@/types';
import { create } from 'zustand' import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware'; import { createJSONStorage, persist } from 'zustand/middleware';
@@ -5,6 +6,10 @@ import { createJSONStorage, persist } from 'zustand/middleware';
interface LocaleState { interface LocaleState {
locale: string; locale: string;
theme: string; theme: string;
currentVersion: string
changelog: ChangelogItemType[]
setCurrentVersion: (currentVersion: string) => void;
setChangelog: (newChangelog: ChangelogItemType[]) => void;
setTheme: (newTheme: string) => void; setTheme: (newTheme: string) => void;
setLocale: (newLocale: string) => void; setLocale: (newLocale: string) => void;
} }
@@ -14,6 +19,10 @@ const useLocaleStore = create<LocaleState>()(
(set) => ({ (set) => ({
locale: "en", locale: "en",
theme: "night", theme: "night",
currentVersion: "",
changelog: [],
setCurrentVersion: (currentVersion: string) => set({ currentVersion }),
setChangelog: (newChangelog: ChangelogItemType[]) => set({ changelog: newChangelog }),
setTheme: (newTheme: string) => set({ theme: newTheme }), setTheme: (newTheme: string) => set({ theme: newTheme }),
setLocale: (newLocale: string) => set({ locale: newLocale }), setLocale: (newLocale: string) => set({ locale: newLocale }),
}), }),

View File

@@ -1,4 +1,4 @@
import { ASConfigMaze, AvatarConfigMaze, ConfigMaze, MOCConfigMaze, PFConfigMaze, StageConfigMaze } from '@/types'; import { ASConfigMaze, AvatarConfigMaze, ConfigMaze, MOCConfigMaze, PFConfigMaze, SkillConfigMaze, StageConfigMaze } from '@/types';
import { create } from 'zustand' import { create } from 'zustand'
interface MazeState { interface MazeState {
@@ -7,11 +7,13 @@ interface MazeState {
AS: Record<string, ASConfigMaze>; AS: Record<string, ASConfigMaze>;
PF: Record<string, PFConfigMaze>; PF: Record<string, PFConfigMaze>;
Stage: Record<string, StageConfigMaze>; Stage: Record<string, StageConfigMaze>;
SkillTree: Record<string, SkillConfigMaze>;
setTechnique: (newTechnique: Record<string, AvatarConfigMaze>) => void; setTechnique: (newTechnique: Record<string, AvatarConfigMaze>) => void;
setMOC: (newMOC: Record<string, MOCConfigMaze>) => void; setMOC: (newMOC: Record<string, MOCConfigMaze>) => void;
setAS: (newAS: Record<string, ASConfigMaze>) => void; setAS: (newAS: Record<string, ASConfigMaze>) => void;
setPF: (newPF: Record<string, PFConfigMaze>) => void; setPF: (newPF: Record<string, PFConfigMaze>) => void;
setStage: (newStage: Record<string, StageConfigMaze>) => void; setStage: (newStage: Record<string, StageConfigMaze>) => void;
setSkillTree: (newSkillTree: Record<string, SkillConfigMaze>) => void;
setAllMazeData: (newData: ConfigMaze) => void; setAllMazeData: (newData: ConfigMaze) => void;
} }
@@ -21,12 +23,21 @@ const useMazeStore = create<MazeState>((set) => ({
AS: {}, AS: {},
PF: {}, PF: {},
Stage: {}, Stage: {},
SkillTree: {},
setSkillTree: (newSkillTree: Record<string, SkillConfigMaze>) => set({ SkillTree: newSkillTree }),
setTechnique: (newTechnique: Record<string, AvatarConfigMaze>) => set({ Technique: newTechnique }), setTechnique: (newTechnique: Record<string, AvatarConfigMaze>) => set({ Technique: newTechnique }),
setMOC: (newMOC: Record<string, MOCConfigMaze>) => set({ MOC: newMOC }), setMOC: (newMOC: Record<string, MOCConfigMaze>) => set({ MOC: newMOC }),
setAS: (newAS: Record<string, ASConfigMaze>) => set({ AS: newAS }), setAS: (newAS: Record<string, ASConfigMaze>) => set({ AS: newAS }),
setPF: (newPF: Record<string, PFConfigMaze>) => set({ PF: newPF }), setPF: (newPF: Record<string, PFConfigMaze>) => set({ PF: newPF }),
setStage: (newStage: Record<string, StageConfigMaze>) => set({ Stage: newStage }), 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 }), setAllMazeData: (newData: ConfigMaze) => set({
Technique: newData.Avatar,
MOC: newData.MOC,
AS: newData.AS,
PF: newData.PF,
Stage: newData.Stage,
SkillTree: newData.Skill
}),
})); }));
export default useMazeStore; export default useMazeStore;

View File

@@ -13,6 +13,7 @@ interface ModelState {
isOpenAvatars: boolean; isOpenAvatars: boolean;
isOpenQuickView: boolean; isOpenQuickView: boolean;
isOpenExtra: boolean; isOpenExtra: boolean;
isChangelog: boolean;
setIsOpenExtra: (newIsOpenExtra: boolean) => void; setIsOpenExtra: (newIsOpenExtra: boolean) => void;
setIsOpenQuickView: (newIsOpenQuickView: boolean) => void; setIsOpenQuickView: (newIsOpenQuickView: boolean) => void;
setIsOpenAvatars: (newIsOpenAvatars: boolean) => void; setIsOpenAvatars: (newIsOpenAvatars: boolean) => void;
@@ -24,6 +25,7 @@ interface ModelState {
setIsOpenCreateProfile: (newIsOpenCreateProfile: boolean) => void; setIsOpenCreateProfile: (newIsOpenCreateProfile: boolean) => void;
setIsOpenImport: (newIsOpenImport: boolean) => void; setIsOpenImport: (newIsOpenImport: boolean) => void;
setIsOpenCopy: (newIsOpenCopy: boolean) => void; setIsOpenCopy: (newIsOpenCopy: boolean) => void;
setIsChangelog: (newChangelog: boolean) => void;
} }
const useModelStore = create<ModelState>((set) => ({ const useModelStore = create<ModelState>((set) => ({
@@ -38,6 +40,7 @@ const useModelStore = create<ModelState>((set) => ({
isOpenAvatars: false, isOpenAvatars: false,
isOpenQuickView: false, isOpenQuickView: false,
isOpenExtra: false, isOpenExtra: false,
isChangelog: false,
setIsOpenExtra: (newIsOpenExtra: boolean) => set({ isOpenExtra: newIsOpenExtra }), setIsOpenExtra: (newIsOpenExtra: boolean) => set({ isOpenExtra: newIsOpenExtra }),
setIsOpenQuickView: (newIsOpenQuickView: boolean) => set({ isOpenQuickView: newIsOpenQuickView }), setIsOpenQuickView: (newIsOpenQuickView: boolean) => set({ isOpenQuickView: newIsOpenQuickView }),
setIsOpenAvatars: (newIsOpenAvatars: boolean) => set({ isOpenAvatars: newIsOpenAvatars }), setIsOpenAvatars: (newIsOpenAvatars: boolean) => set({ isOpenAvatars: newIsOpenAvatars }),
@@ -49,6 +52,7 @@ const useModelStore = create<ModelState>((set) => ({
setIsOpenCreateProfile: (newIsOpenCreateProfile: boolean) => set({ isOpenCreateProfile: newIsOpenCreateProfile }), setIsOpenCreateProfile: (newIsOpenCreateProfile: boolean) => set({ isOpenCreateProfile: newIsOpenCreateProfile }),
setIsOpenImport: (newIsOpenImport: boolean) => set({ isOpenImport: newIsOpenImport }), setIsOpenImport: (newIsOpenImport: boolean) => set({ isOpenImport: newIsOpenImport }),
setIsOpenCopy: (newIsOpenCopy: boolean) => set({ isOpenCopy: newIsOpenCopy }), setIsOpenCopy: (newIsOpenCopy: boolean) => set({ isOpenCopy: newIsOpenCopy }),
setIsChangelog: (newChangelog: boolean) => set({ isChangelog: newChangelog }),
})); }));
export default useModelStore; export default useModelStore;

7
src/types/changelog.ts Normal file
View File

@@ -0,0 +1,7 @@
export interface ChangelogItemType {
version: string,
date: string,
type: string,
items: string[]
}

View File

@@ -17,6 +17,7 @@ export type MOCConfigMaze = {
export type AvatarConfigMaze = { export type AvatarConfigMaze = {
maze_buff: number[]; maze_buff: number[];
} }
export type StageConfigMaze = { export type StageConfigMaze = {
stage_id: number; stage_id: number;
stage_type: string; stage_type: string;
@@ -24,11 +25,18 @@ export type StageConfigMaze = {
monster_list: Array<Record<string, number>>; monster_list: Array<Record<string, number>>;
}; };
export type SkillConfigMaze = {
max_level: number;
index_slot: number;
}
export type ConfigMaze = { export type ConfigMaze = {
Avatar: Record<string, AvatarConfigMaze>; Avatar: Record<string, AvatarConfigMaze>;
MOC: Record<string, MOCConfigMaze>; MOC: Record<string, MOCConfigMaze>;
AS: Record<string, ASConfigMaze>; AS: Record<string, ASConfigMaze>;
PF: Record<string, PFConfigMaze>; PF: Record<string, PFConfigMaze>;
Stage: Record<string, StageConfigMaze>; Stage: Record<string, StageConfigMaze>;
Skill: Record<string, SkillConfigMaze>;
}; };

View File

@@ -22,3 +22,5 @@ export * from "./monsterDetail"
export * from "./extraData" export * from "./extraData"
export * from "./showcase" export * from "./showcase"
export * from "./srtools" export * from "./srtools"
export * from "./changelog"
export * from "./modelConfig"

7
src/types/modelConfig.ts Normal file
View File

@@ -0,0 +1,7 @@
export type ModalConfig = {
id: string
title: string
isOpen: boolean
onClose: () => void
content: React.ReactNode
}

View File

@@ -27,6 +27,7 @@ export interface LightconeJson {
export interface AvatarData { export interface AvatarData {
rank: number, rank: number,
skills: Record<string, number> skills: Record<string, number>
skills_by_anchor_type?: Record<string,number>
} }
export interface AvatarJson { export interface AvatarJson {

View File

@@ -27,10 +27,16 @@ export const stageConfigMazeSchema = z.object({
monster_list: z.array(z.record(z.string(), z.number())) monster_list: z.array(z.record(z.string(), z.number()))
}); });
export const skillConfigMazeSchema = z.object({
max_level: z.number(),
index_slot: z.number()
});
export const configMazeSchema = z.object({ export const configMazeSchema = z.object({
Avatar: z.record(z.string(), avatarConfigMazeSchema), Avatar: z.record(z.string(), avatarConfigMazeSchema),
MOC: z.record(z.string(), mocConfigMazeSchema), MOC: z.record(z.string(), mocConfigMazeSchema),
AS: z.record(z.string(), asConfigMazeSchema), AS: z.record(z.string(), asConfigMazeSchema),
PF: z.record(z.string(), pfConfigMazeSchema), PF: z.record(z.string(), pfConfigMazeSchema),
Stage: z.record(z.string(), stageConfigMazeSchema) Stage: z.record(z.string(), stageConfigMazeSchema),
Skill: z.record(z.string(), skillConfigMazeSchema)
}); });

View File

@@ -28,7 +28,8 @@ export const lightconeJsonSchema = z.object({
export const avatarDataSchema = z.object({ export const avatarDataSchema = z.object({
rank: z.number(), rank: z.number(),
skills: z.record(z.string(), z.number()) skills: z.record(z.string(), z.number()),
skills_by_anchor_type: z.record(z.string(), z.number()).optional()
}); });
export const avatarJsonSchema = z.object({ export const avatarJsonSchema = z.object({