UPDATE: Skilltree
All checks were successful
Gitea Auto Deploy / Deploy-Container (push) Successful in 1m46s

This commit is contained in:
2025-07-30 21:10:09 +07:00
parent 31724fa88c
commit 9b6a061cba
50 changed files with 15044 additions and 6214 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -212,6 +212,28 @@
"changeRelic": "更换遗物",
"deleteRelic": "删除遗物",
"deleteRelicConfirm": "确定要删除插槽中的遗物吗",
"setEffects": "设置效果"
"setEffects": "设置效果",
"details": "详情",
"normal": "普通攻击",
"bpskill": "技能",
"maze": "技巧",
"ultra": "终结技",
"servantskill": "记灵技能",
"severaltalent": "记灵天赋",
"singleattack": "单体攻击",
"enhance": "强化",
"summon": "召唤",
"blast": "爆裂",
"restore": "恢复",
"support": "支援",
"aoeattack": "范围攻击",
"mazeattack": "迷宫秘技",
"impair": "削弱",
"active": "活跃",
"inactive": "不活跃",
"maxAll": "全部最大化",
"defence": "防御",
"maxAllSuccess": "技能等级已成功设置为最大。",
"maxAllFailed": "设置技能等级为最大失败。"
}
}

View File

@@ -212,6 +212,29 @@
"changeRelic": "Change relic",
"deleteRelic": "Delete relic",
"deleteRelicConfirm": "Are you sure you want to delete relic in slot",
"setEffects": "Set Effects"
"setEffects": "Set Effects",
"details": "Details",
"normal": "Basic ATK",
"bpskill": "Skill",
"maze": "Technique",
"ultra": "Ultimate",
"servantskill": "Memosprite Skill",
"severaltalent": "Memosprite Talent",
"singleattack": "Single Attack",
"enhance": "Enhance",
"summon": "Summon",
"mazeattack": "Technique Attack",
"blast": "Blast",
"restore": "Restore",
"support": "Support",
"aoeattack": "AoE Attack",
"impair": "Impair",
"bounce": "Bounce",
"active": "Active",
"defence": "Defence",
"inactive": "Inactive",
"maxAll": "Max All",
"maxAllSuccess": "Successfully set skill level to max.",
"maxAllFailed": "Failed to set skill level to max."
}
}

View File

@@ -212,6 +212,30 @@
"changeRelic": "遺物を変更",
"deleteRelic": "遺物を削除",
"deleteRelicConfirm": "スロットの遺物を削除してもよろしいですか?",
"setEffects": "効果を設定"
"setEffects": "効果を設定",
"details": "詳細",
"normal": "通常攻撃",
"bpskill": "スキル",
"maze": "技術",
"ultra": "アルティメット",
"servantskill": "メモスプライトスキル",
"severaltalent": "メモスプライトの才能",
"singleattack": "単体攻撃",
"enhance": "強化",
"summon": "召喚",
"blast": "爆発",
"restore": "回復",
"support": "支援",
"aoeattack": "範囲攻撃",
"mazeattack": "迷宮秘技",
"impair": "弱体化",
"bounce": "弾跳",
"active": "アクティブ",
"inactive": "非アクティブ",
"defence": "防御",
"maxAll": "すべて最大化",
"maxAllSuccess": "スキルレベルを最大に設定しました。",
"maxAllFailed": "スキルレベルの最大設定に失敗しました。"
}
}

View File

@@ -212,6 +212,29 @@
"changeRelic": "유물 변경",
"deleteRelic": "유물 삭제",
"deleteRelicConfirm": "이 슬롯의 유물을 삭제하시겠습니까?",
"setEffects": "효과 설정"
"setEffects": "효과 설정",
"details": "상세",
"normal": "기본 공격",
"bpskill": "스킬",
"maze": "전술",
"ultra": "필살기",
"servantskill": "메모스프라이트 스킬",
"severaltalent": "메모스프라이트 재능",
"singleattack": "단일 공격",
"enhance": "강화",
"summon": "소환",
"blast": "광역 공격",
"restore": "회복",
"support": "지원",
"aoeattack": "광역 공격",
"mazeattack": "미궁 비기",
"impair": "약화",
"bounce": "튕김",
"active": "활성",
"inactive": "비활성",
"defence": "방어",
"maxAll": "모두 최대치",
"maxAllSuccess": "스킬 레벨이 최대치로 설정되었습니다.",
"maxAllFailed": "스킬 레벨을 최대치로 설정하지 못했습니다."
}
}

View File

@@ -118,7 +118,7 @@
"techniqueNote": "Bật hiệu ứng chiến pháp trước trận",
"enhancement": "Cường hóa",
"enhancementLevel": "Cấp độ cường hóa",
"origin": "Ngun gốc",
"origin": "Ngun gốc",
"enhancedNote": "Cường hóa cao mở thêm kỹ năng",
"lightconeEquipment": "Trang bị nón ánh sáng",
"lightconeSettings": "Cài đặt nón ánh sáng",
@@ -212,6 +212,29 @@
"changeRelic": "Thay đổi di vật",
"deleteRelic": "Xóa di vật",
"deleteRelicConfirm": "Bạn có chắc chắn muốn xóa di vật trong ô này không?",
"setEffects": "Thiết lập hiệu ứng"
}
}
"setEffects": "Thiết lập hiệu ứng",
"details": "Chi tiết",
"normal": "Đánh thường",
"bpskill": "Chiến kỹ",
"maze": "Bí kỹ",
"ultra": "Tuyệt kỹ",
"servantskill": "Kỹ năng vật triệu hồi",
"severaltalent": "Thiên phú vật triệu hồi",
"singleattack": "Tấn công đơn",
"enhance": "Cường hóa",
"summon": "Triệu hồi",
"blast": "Tấn công 3 mục tiêu",
"restore": "Hồi phục",
"support": "Hỗ trợ",
"aoeattack": "Tấn công đa mục tiêu",
"mazeattack": "Bí kỹ tấn công",
"impair": "Suy yếu",
"bounce": "Nảy bật",
"active": "Kích hoạt",
"inactive": "Không kích hoạt",
"defence": "Phòng thủ",
"maxAll": "Tối đa tất cả",
"maxAllSuccess": "Đã thiết lập cấp độ kỹ năng tối đa thành công.",
"maxAllFailed": "Thiết lập cấp độ kỹ năng tối đa thất bại."
}
}

View File

@@ -212,6 +212,29 @@
"changeRelic": "更换遗物",
"deleteRelic": "删除遗物",
"deleteRelicConfirm": "确定要删除插槽中的遗物吗",
"setEffects": "设置效果"
"setEffects": "设置效果",
"details": "详情",
"normal": "普通攻击",
"bpskill": "技能",
"maze": "技巧",
"ultra": "终结技",
"servantskill": "记灵技能",
"severaltalent": "记灵天赋",
"singleattack": "单体攻击",
"enhance": "强化",
"summon": "召唤",
"blast": "爆裂",
"restore": "恢复",
"support": "支援",
"aoeattack": "范围攻击",
"mazeattack": "迷宫秘技",
"impair": "削弱",
"bounce": "弹跳",
"active": "活跃",
"defence": "防御",
"inactive": "不活跃",
"maxAll": "全部最大化",
"maxAllSuccess": "技能等级已成功设置为最大。",
"maxAllFailed": "设置技能等级为最大失败。"
}
}

View File

@@ -24,19 +24,16 @@ const nextConfig: NextConfig = {
hostname: 'api.hakush.in',
pathname: '**',
},
{
protocol: 'https',
hostname: 'homdgcat.wiki',
pathname: '**',
},
],
},
eslint: {
ignoreDuringBuilds: true,
},
async rewrites() {
return [
{
source: '/api/character/:id',
destination: 'https://api.hakush.in/hsr/data/en/character/:id.json',
},
];
},
};
export default withNextIntl(nextConfig);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 914 KiB

After

Width:  |  Height:  |  Size: 846 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
public/skilltree/MAGE.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

BIN
public/skilltree/ROGUE.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -1,9 +1,6 @@
"use client"
import Image from "next/image";
import EidolonsInfo from "@/components/eidolonsInfo";
import { useRouter } from 'next/navigation'
export default function EidolonsInfoPage() {
const router = useRouter()
return (
<div className="w-full">
<EidolonsInfo/>

View File

@@ -7,6 +7,12 @@
nocompatible: true;
}
:root {
--size-big: 4vw;
--size-medium: 3vw;
--size-small: 2vw;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;

View File

@@ -1,10 +1,8 @@
"use client"
import Image from "next/image";
"use client";
import RelicsInfo from "@/components/relicsInfo";
import { useRouter } from 'next/navigation'
export default function RelicsInfoPage() {
const router = useRouter()
return (
<div className="w-full">
<RelicsInfo></RelicsInfo>

View File

@@ -0,0 +1,11 @@
"use client";
import SkillsInfo from "@/components/skillsInfo";
export default function SkillsInfoPage() {
return (
<div className="w-full">
<SkillsInfo></SkillsInfo>
</div>
);
}

View File

@@ -293,7 +293,7 @@ export default function ActionBar() {
<button className="btn btn-success btn-sm" onClick={() => actionMove('')}>{transI18n("characterInformation")}</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove('relics-info')}>{transI18n("relics")}</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove('eidolons-info')}>{transI18n("eidolons")}</button>
<button disabled className="btn btn-success btn-sm cursor-not-allowed italic">{transI18n("skills")} ({transI18n("comingSoon")})</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove('skills-info')}>{transI18n("skills")}</button>
<button disabled className="btn btn-success btn-sm cursor-not-allowed italic">{transI18n("showcaseCard")} {transI18n("comingSoon")}</button>
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm"> {isConnectPS ? transI18n("sync") : transI18n("connectPs")}</button>
</div>

View File

@@ -11,7 +11,7 @@ import { useFetchASData, useFetchAvatarData, useFetchConfigData, useFetchLightco
export default function AvatarBar() {
const [listElement, setListElement] = useState<Record<string, boolean>>({ "fire": false, "ice": false, "imaginary": false, "physical": false, "quantum": false, "thunder": false, "wind": false })
const [listPath, setListPath] = useState<Record<string, boolean>>({ "knight": false, "mage": false, "priest": false, "rogue": false, "shaman": false, "warlock": false, "warrior": false, "memory": false })
const { listAvatar, setAvatarSelected, setFilter, filter } = useAvatarStore()
const { listAvatar, setAvatarSelected, setSkillSelected, setFilter, filter } = useAvatarStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore()
@@ -91,7 +91,7 @@ export default function AvatarBar() {
<div className="flex items-start h-full">
<ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 w-full h-[65vh] overflow-y-scroll overflow-x-hidden">
{listAvatar.map((item, index) => (
<div key={index} onClick={() => setAvatarSelected(item)}>
<div key={index} onClick={() => {setAvatarSelected(item); setSkillSelected(null)}}>
<CharacterCard data={item} />
</div>
))}

View File

@@ -21,37 +21,73 @@ const getRarityName = (slot: string) => {
switch (slot) {
case '1': return (
<div className="flex items-center gap-1">
<Image src="/relics/HEAD.png" alt="Head" width={20} height={20} />
<Image
src="/relics/HEAD.png"
alt="Head"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Head</h2>
</div>
);
case '2': return (
<div className="flex items-center gap-1">
<Image src="/relics/HAND.png" alt="Hand" width={20} height={20} />
<Image
src="/relics/HAND.png"
alt="Hand"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Hands</h2>
</div>
);
case '3': return (
<div className="flex items-center gap-1">
<Image src="/relics/BODY.png" alt="Body" width={20} height={20} />
<Image
src="/relics/BODY.png"
alt="Body"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Body</h2>
</div>
);
case '4': return (
<div className="flex items-center gap-1">
<Image src="/relics/FOOT.png" alt="Foot" width={20} height={20} />
<Image
src="/relics/FOOT.png"
alt="Foot"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Feet</h2>
</div>
);
case '5': return (
<div className="flex items-center gap-1">
<Image src="/relics/NECK.png" alt="Neck" width={20} height={20} />
<Image
src="/relics/NECK.png"
alt="Neck"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Planar sphere</h2>
</div>
);
case '6': return (
<div className="flex items-center gap-1">
<Image src="/relics/OBJECT.png" alt="Object" width={20} height={20} />
<Image
src="/relics/OBJECT.png"
alt="Object"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Link rope</h2>
</div>
);

View File

@@ -2,7 +2,7 @@
import { connectToPS, downloadJson, syncDataToPS } from "@/helper";
import { converterToFreeSRJson } from "@/helper/converterToFreeSRJson";
import { useChangeTheme } from "@/hooks/useChangeTheme";
import { listCurrentLanguage } from "@/lib/constant";
import { listCurrentLanguage } from "@/constant/constant";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import { motion } from "framer-motion";
@@ -264,7 +264,7 @@ export default function Header() {
{/* Logo */}
<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">
<div className="flex items-center justify-center gap-2">
<Image src="/ff-srtool.png" alt="Logo" width={50} height={50} />
<div className="flex flex-col justify-center items-start">
<h1 className="text-xl font-bold">

View File

@@ -120,7 +120,7 @@ export default function EnkaImport() {
}
const newProfile = converterOneEnkaDataToAvatarStore(character, newAvatar.profileList.length)
if (newProfile) {
newAvatar.profileList.push()
newAvatar.profileList.push(newProfile)
newAvatar.profileSelect = newAvatar.profileList.length - 1
}
setAvatar(newAvatar)

View File

@@ -6,7 +6,7 @@ import React, { useEffect, useMemo } from 'react';
import SelectCustomImage from '../select/customSelectImage';
import { calcAffixBonus, randomPartition, randomStep, replaceByParam } from '@/helper';
import useAffixStore from '@/stores/affixStore';
import { mapingStats } from '@/lib/constant';
import { mappingStats } from '@/constant/constant';
import useAvatarStore from '@/stores/avatarStore';
import useModelStore from '@/stores/modelStore';
import useRelicMakerStore from '@/stores/relicMakerStore';
@@ -118,7 +118,7 @@ export default function RelicMaker() {
const data = affixSet[selectedMainStat];
if (!data) return 0;
const stat = mapingStats?.[data.property];
const stat = mappingStats?.[data.property];
if (!stat) return 0;
const value = data.base + data.step * selectedRelicLevel;
@@ -257,8 +257,8 @@ export default function RelicMaker() {
<SelectCustomImage
customSet={Object.entries(mapMainAffix["5" + selectedRelicSlot] || {}).map(([key, value]) => ({
value: key,
label: mapingStats[value.property].name + " " + mapingStats[value.property].unit,
imageUrl: mapingStats[value.property].icon
label: mappingStats[value.property].name + " " + mappingStats[value.property].unit,
imageUrl: mappingStats[value.property].icon
}))}
excludeSet={[]}
selectedCustomSet={selectedMainStat}
@@ -361,13 +361,13 @@ export default function RelicMaker() {
<SelectCustomImage
customSet={Object.entries(subAffixOptions).map(([key, value]) => ({
value: key,
label: mapingStats[value.property].name + " " + mapingStats[value.property].unit,
imageUrl: mapingStats[value.property].icon
label: mappingStats[value.property].name + " " + mappingStats[value.property].unit,
imageUrl: mappingStats[value.property].icon
}))}
excludeSet={Object.entries(exSubAffixOptions).map(([key, value]) => ({
value: key,
label: mapingStats[value.property].name + " " + mapingStats[value.property].unit,
imageUrl: mapingStats[value.property].icon
label: mappingStats[value.property].name + " " + mappingStats[value.property].unit,
imageUrl: mappingStats[value.property].icon
}))}
selectedCustomSet={v.affixId}
placeholder={transI18n("selectASubStat")}
@@ -378,7 +378,7 @@ export default function RelicMaker() {
{/* Current Value */}
<div className="col-span-4 text-center flex items-center justify-center gap-2">
<span className="text-2xl font-mono">+{ }</span>
<div className="text-xl font-bold text-info">{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount, v.rollCount)}{mapingStats?.[subAffixOptions[v.affixId]?.property]?.unit || ""}</div>
<div className="text-xl font-bold text-info">{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount, v.rollCount)}{mappingStats?.[subAffixOptions[v.affixId]?.property]?.unit || ""}</div>
</div>
{/* Roll Values */}
@@ -387,19 +387,19 @@ export default function RelicMaker() {
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 0)}
className="btn btn-sm bg-white text-slate-800 hover:bg-gray-200 border-0"
>
{calcAffixBonus(subAffixOptions[v.affixId], 0 , v.rollCount + 1)}{mapingStats?.[subAffixOptions[v.affixId]?.property]?.unit || ""}
{calcAffixBonus(subAffixOptions[v.affixId], 0 , v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.property]?.unit || ""}
</button>
<button
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 1)}
className="btn btn-sm bg-white text-slate-800 hover:bg-gray-200 border-0"
>
{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount + 1, v.rollCount + 1)}{mapingStats?.[subAffixOptions[v.affixId]?.property]?.unit || ""}
{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount + 1, v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.property]?.unit || ""}
</button>
<button
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 2)}
className="btn btn-sm bg-white text-slate-800 hover:bg-gray-200 border-0"
>
{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount + 2, v.rollCount + 1)}{mapingStats?.[subAffixOptions[v.affixId]?.property]?.unit || ""}
{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount + 2, v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.property]?.unit || ""}
</button>
</div>

View File

@@ -0,0 +1,326 @@
"use client"
import { useTranslations } from "next-intl";
import useAvatarStore from "@/stores/avatarStore";
import { useCallback, useMemo } from "react";
import { traceButtonsInfo, traceLink } from "@/constant/traceConstant";
import useUserDataStore from "@/stores/userDataStore";
import useLocaleStore from "@/stores/localeStore";
import Image from "next/image";
import { replaceByParam } from "@/helper";
import { mappingStats } from "@/constant/constant";
import { StatusAddType } from "@/types";
import cloneDeep from "lodash/cloneDeep";
import { toast } from "react-toastify";
export default function SkillsInfo() {
const transI18n = useTranslations("DataPage")
const { theme } = useLocaleStore()
const { avatarSelected, mapAvatarInfo, skillSelected, setSkillSelected } = useAvatarStore()
const { avatars, setAvatar } = useUserDataStore()
const traceButtons = useMemo(() => {
if (!avatarSelected) return
return traceButtonsInfo[avatarSelected.baseType]
}, [avatarSelected])
const avatarInfo = useMemo(() => {
if (!avatarSelected) return
return mapAvatarInfo[avatarSelected.id]
}, [avatarSelected, mapAvatarInfo])
const avatarData = useMemo(() => {
if (!avatarSelected) return
return avatars[avatarSelected.id]
}, [avatarSelected, avatars])
const avatarSkillTree = useMemo(() => {
if (!avatarSelected || !avatars[avatarSelected.id]) return {}
if (avatars[avatarSelected.id].enhanced) {
return avatarInfo?.Enhanced[avatars[avatarSelected.id].enhanced.toString()].SkillTrees || {}
}
return avatarInfo?.SkillTrees || {}
}, [avatarSelected, avatarInfo, avatars])
const skillInfo = useMemo(() => {
if (!avatarSelected || !skillSelected) return
return avatarSkillTree?.[skillSelected || ""]?.["1"]
}, [avatarSelected, avatarSkillTree, skillSelected])
const getImageSkill = useCallback((icon: string | undefined, status: StatusAddType | undefined) => {
if (!icon) return
if (icon.startsWith("SkillIcon")) {
if (Number(avatarSelected?.id) > 8000 && Number(avatarSelected?.id) % 2 === 0) {
return `https://homdgcat.wiki/images/skillicons/avatar/${Number(avatarSelected?.id) - 1}/${icon.replaceAll(avatarSelected?.id || "", (Number(avatarSelected?.id) - 1).toString())}`
}
return `https://homdgcat.wiki/images/skillicons/avatar/${avatarSelected?.id}/${icon}`
} else if (status && mappingStats[status.PropertyType]) {
return mappingStats[status.PropertyType].icon
}
else if (icon.startsWith("Icon")) {
return `https://api.hakush.in/hsr/UI/trace/${icon.replace(".png", ".webp")}`
}
}, [avatarSelected])
const getTraceBuffDisplay = useCallback((status: StatusAddType) => {
const dataDisplay = mappingStats[status.PropertyType]
if (!dataDisplay) return ""
if (dataDisplay.unit === "%") {
return `${(status.Value * 100).toFixed(1)}${dataDisplay.unit}`
}
if (dataDisplay.name === "SPD") {
return `${status.Value.toFixed(1)}${dataDisplay.unit}`
}
return `${status.Value.toFixed(0)}${dataDisplay.unit}`
}, [])
const dataLevelUpSkill = useMemo(() => {
const skillIds: number[] = skillInfo?.LevelUpSkillID || []
if (!avatarSelected || !avatarInfo || !avatarData) return
let result = Object.values(avatarInfo.Skills || {})?.filter((skill) => skillIds.includes(skill.Id))
if (avatarData.enhanced) {
result = Object.values(avatarInfo.Enhanced[avatarData.enhanced.toString()].Skills || {})?.filter((skill) => skillIds.includes(skill.Id))
}
if (result && result.length > 0) {
return {
isServant: false,
data: result,
servantData: null,
}
}
const resultServant = Object.entries(avatarInfo.Memosprite?.Skills || {})
?.filter(([skillId]) => skillIds.includes(Number(skillId)))
?.map(([skillId, skillData]) => ({
Id: Number(skillId),
...skillData,
}))
if (resultServant && resultServant.length > 0) {
return {
isServant: true,
data: resultServant,
servantData: avatarInfo.Memosprite,
}
}
return undefined
}, [skillInfo?.LevelUpSkillID, avatarSelected, avatarInfo, avatarData])
const handlerMaxAll = () => {
if (!avatarInfo || !avatarData || !avatarSkillTree) {
toast.error(transI18n("maxAllFailed"))
return
}
const newData = cloneDeep(avatarData)
newData.data.skills = Object.values(avatarSkillTree).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>)
toast.success(transI18n("maxAllSuccess"))
setAvatar(newData)
}
const handlerChangeStatusTrace = (status: boolean) => {
if (!avatarData || !skillInfo) return
const newData = cloneDeep(avatarData)
newData.data.skills[skillInfo?.PointID] = status ? 1 : 0
if (!status && traceLink?.[avatarSelected?.baseType || ""]?.[skillSelected || ""]) {
traceLink[avatarSelected?.baseType || ""][skillSelected || ""].forEach((pointId) => {
if (avatarSkillTree?.[pointId]?.["1"]) {
console.log(avatarSkillTree?.[pointId]?.["1"].PointID)
newData.data.skills[avatarSkillTree?.[pointId]?.["1"].PointID] = 0
}
})
}
setAvatar(newData)
}
return (
<div className="w-full">
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-gradient-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("skills")}
</h2>
<div className="flex flex-col items-center">
<button className="btn btn-success" onClick={handlerMaxAll}>{transI18n("maxAll")}</button>
{traceButtons && avatarInfo && (
<div className="grid col-span-4 relative w-full aspect-square">
<Image
src={`/skilltree/${avatarSelected?.baseType?.toUpperCase()}.webp`}
alt=""
width={612}
priority={true}
height={612}
style={{
filter: (theme === "winter" || theme === "cupcake") ? "invert(1)" : "none"
}}
className={`w-full h-full object-cover rounded-xl`}
/>
{traceButtons.map((btn, index) => (
<div
key={`${btn.id} + ${index}`}
id={btn.id}
className={`
absolute rounded-full border border-black
bg-no-repeat bg-contain
cursor-pointer transition-all duration-200 ease-in-out
shadow-[0_0_5px_white] flex justify-center items-center
hover:scale-110
${btn.size === "small" ? "w-[2vw] h-[2vw] bg-white" : ""}
${btn.size === "medium" ? "w-[3vw] h-[3vw] bg-white" : ""}
${btn.size === "big" ? "w-[3.5vw] h-[3.5vw] bg-black" : ""}
${skillSelected === btn.id ? "border-4 border-primary" : ""}
${avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID] === 0 ? "opacity-50 cursor-not-allowed" : ""}
`}
onClick={() => {
setSkillSelected(btn.id === skillSelected ? null : btn.id)
}}
style={{
left: `calc(${btn.left} - var(--size-${btn.size}) / 2)`,
top: `calc(${btn.top} - var(--size-${btn.size}) / 2)`,
}}
>
<Image
src={getImageSkill(avatarInfo?.SkillTrees?.[btn.id]?.["1"]?.Icon, avatarSkillTree?.[btn.id]?.["1"]?.StatusAddList[0]) || ""}
alt={btn.id.replaceAll("Point", "")}
priority={true}
width={124}
height={124}
style={{
filter: btn.size !== "big"
? 'brightness(0%)'
: 'brightness(200%)'
}}
/>
{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>
)}
</div>
))}
</div>
)}
</div>
</div>
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-gradient-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("details")}
</h2>
{skillSelected && avatarInfo?.SkillTrees && avatarData && (
<div>
{skillInfo?.MaxLevel && skillInfo?.MaxLevel > 1 ? (
<div>
<div className="font-bold text-success">{transI18n("level")}</div>
<div className="w-full max-w-xs">
<input type="range"
min={1}
max={skillInfo?.MaxLevel || 1}
value={avatarData?.data.skills?.[skillInfo?.PointID] || 1}
onChange={(e) => {
const newData = cloneDeep(avatarData)
newData.data.skills[skillInfo?.PointID] = parseInt(e.target.value)
setAvatar(newData)
}}
className="range range-success"
step="1" />
<div className="flex justify-between px-2.5 mt-2 text-xs">
{Array.from({ length: skillInfo?.MaxLevel }, (_, index) => index + 1).map((index) => (
<span key={index}>{index}</span>
))}
</div>
</div>
</div>
) : skillInfo?.MaxLevel && skillInfo?.MaxLevel === 1 && traceButtons?.find((btn) => btn.id === skillSelected)?.size !== "big" ? (
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={avatarData?.data.skills?.[skillInfo?.PointID] === 1}
className="toggle toggle-success"
onChange={(e) => {
handlerChangeStatusTrace(e.target.checked)
}}
/>
<div className="font-bold text-success">
{avatarData?.data.skills?.[skillInfo?.PointID] === 1 ? transI18n("active") : transI18n("inactive")}
</div>
</div>
) : (
null
)}
{((skillInfo?.PointName && skillInfo?.PointDesc) ||
(skillInfo?.PointName && skillInfo?.StatusAddList.length > 0))
&& (
<div className="text-xl font-bold flex items-center gap-2 mt-2">
{skillInfo.PointName}
{skillInfo.StatusAddList.length > 0 && (
<div>
{skillInfo.StatusAddList.map((status, index) => (
<div key={index}>
<div className="text-xl font-bold">{getTraceBuffDisplay(status)}</div>
</div>
))}
</div>
)}
</div>
)}
{skillInfo?.PointDesc && (
<div
dangerouslySetInnerHTML={{
__html: replaceByParam(
skillInfo?.PointDesc || "",
skillInfo?.ParamList || []
)
}}
/>
)}
{skillInfo?.LevelUpSkillID
&& skillInfo?.LevelUpSkillID.length > 0
&& dataLevelUpSkill
&& (
<div className="mt-2 flex flex-col gap-2">
{dataLevelUpSkill?.data?.map((skill, index) => (
<div key={index}>
<div className="text-xl font-bold text-primary">
{transI18n(dataLevelUpSkill.isServant ? `${skill?.Type ? "severaltalent" : "servantskill"}` : `${skill?.Type ? skill?.Type.toLowerCase() : "talent"}`)}
{` (${transI18n(skill.Tag.toLowerCase())})`}
</div>
<div className="text-lg font-bold" dangerouslySetInnerHTML={{ __html: replaceByParam(skill.Name, []) }}>
</div>
<div
dangerouslySetInnerHTML={{
__html: replaceByParam(
skill.Desc || skill.SimpleDesc,
skill.Level[avatarData?.data.skills?.[skillInfo?.PointID]?.toString() || ""]?.ParamList || []
)
}}
/>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,5 +1,6 @@
"use client";
import { createContext, PropsWithChildren, useEffect, useState } from "react";
import { createContext, PropsWithChildren, useEffect } from "react";
import useLocaleStore from "@/stores/localeStore";
interface ThemeContextType {
theme?: string;
@@ -8,7 +9,8 @@ interface ThemeContextType {
export const ThemeContext = createContext<ThemeContextType>({});
export const ThemeProvider = ({ children }: PropsWithChildren) => {
const [theme, setTheme] = useState<string>("light");
const { theme, setTheme } = useLocaleStore()
useEffect(() => {
if (typeof window !== "undefined") {
@@ -19,14 +21,13 @@ export const ThemeProvider = ({ children }: PropsWithChildren) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const changeTheme = (nextTheme: string | null) => {
console.log(nextTheme)
if (nextTheme) {
setTheme(nextTheme);
if (typeof window !== "undefined") {
localStorage.setItem("theme", nextTheme);
}
} else {
setTheme((prev) => (prev === "light" ? "night" : "light"));
setTheme(theme === "winter" ? "night" : "winter");
if (typeof window !== "undefined") {
localStorage.setItem("theme", theme);
}

View File

@@ -14,7 +14,7 @@ export const listCurrentLanguageApi : Record<string, string> = {
zh: "cn"
};
export const mapingStats = <Record<string, {name: string, icon: string, unit: string}> > {
export const mappingStats = <Record<string, {name: string, icon: string, unit: string}> > {
"HPDelta": {
name:"HP",
icon:"/icon/hp.webp",
@@ -125,4 +125,6 @@ export const mapingStats = <Record<string, {name: string, icon: string, unit: st
icon:"/icon/energy-rate.webp",
unit: "%"
}
}
}

View File

@@ -0,0 +1,238 @@
export const traceButtonsInfo: Record<string, { id: string, size: string, left: string, top: string }[]> = {
Knight: [
{ id: 'Point03', size: 'big', left: '51%', top: '52%' },
{ id: 'Point04', size: 'big', left: '51%', top: '35%' },
{ id: 'Point02', size: 'big', left: '67%', top: '55%' },
{ id: 'Point05', size: 'big', left: '51%', top: '69.5%' },
{ id: 'Point01', size: 'big', left: '33%', top: '55%' },
{ id: 'Point08', size: 'medium', left: '50%', top: '21.5%' },
{ id: 'Point07', size: 'medium', left: '71%', top: '86%' },
{ id: 'Point06', size: 'medium', left: '29%', top: '86%' },
{ id: 'Point16', size: 'small', left: '50%', top: '9%' },
{ id: 'Point18', size: 'small', left: '66%', top: '13%' },
{ id: 'Point17', size: 'small', left: '34%', top: '13%' },
{ id: 'Point15', size: 'small', left: '79.5%', top: '44.5%' },
{ id: 'Point12', size: 'small', left: '20.5%', top: '44.5%' },
{ id: 'Point09', size: 'small', left: '50%', top: '82.5%' },
{ id: 'Point13', size: 'small', left: '81%', top: '78%' },
{ id: 'Point10', size: 'small', left: '19.5%', top: '78.5%' },
{ id: 'Point14', size: 'small', left: '89%', top: '70%' },
{ id: 'Point11', size: 'small', left: '11%', top: '70%' }
],
Mage: [
{ id: 'Point03', size: 'big', left: '50.5%', top: '53.5%' },
{ id: 'Point04', size: 'big', left: '51%', top: '32%' },
{ id: 'Point02', size: 'big', left: '68.5%', top: '53.5%' },
{ id: 'Point05', size: 'big', left: '51%', top: '84%' },
{ id: 'Point01', size: 'big', left: '33.5%', top: '53.5%' },
{ id: 'Point08', size: 'medium', left: '50%', top: '14.5%' },
{ id: 'Point07', size: 'medium', left: '80.5%', top: '53%' },
{ id: 'Point06', size: 'medium', left: '19.5%', top: '53%' },
{ id: 'Point17', size: 'small', left: '66%', top: '17.5%' },
{ id: 'Point16', size: 'small', left: '34%', top: '17.5%' },
{ id: 'Point18', size: 'small', left: '66%', top: '79%' },
{ id: 'Point09', size: 'small', left: '34%', top: '79%' },
{ id: 'Point13', size: 'small', left: '93%', top: '52%' },
{ id: 'Point14', size: 'small', left: '89%', top: '63%' },
{ id: 'Point15', size: 'small', left: '89%', top: '41.5%' },
{ id: 'Point10', size: 'small', left: '7%', top: '52%' },
{ id: 'Point11', size: 'small', left: '11.5%', top: '63%' },
{ id: 'Point12', size: 'small', left: '12%', top: '41.5%' }
],
Priest: [
{ id: 'Point03', size: 'big', left: '51%', top: '49%' },
{ id: 'Point04', size: 'big', left: '51%', top: '31%' },
{ id: 'Point02', size: 'big', left: '68.5%', top: '46%' },
{ id: 'Point05', size: 'big', left: '51%', top: '66%' },
{ id: 'Point01', size: 'big', left: '33.5%', top: '46%' },
{ id: 'Point08', size: 'medium', left: '50%', top: '13%' },
{ id: 'Point07', size: 'medium', left: '68%', top: '79%' },
{ id: 'Point06', size: 'medium', left: '33%', top: '79%' },
{ id: 'Point16', size: 'small', left: '65%', top: '18%' },
{ id: 'Point17', size: 'small', left: '35%', top: '18%' },
{ id: 'Point09', size: 'small', left: '57%', top: '89%' },
{ id: 'Point18', size: 'small', left: '43%', top: '89%' },
{ id: 'Point10', size: 'small', left: '80.5%', top: '66%' },
{ id: 'Point11', size: 'small', left: '93%', top: '52%' },
{ id: 'Point12', size: 'small', left: '81%', top: '39%' },
{ id: 'Point13', size: 'small', left: '20.5%', top: '66%' },
{ id: 'Point14', size: 'small', left: '7%', top: '52%' },
{ id: 'Point15', size: 'small', left: '20%', top: '39%' }
],
Rogue: [
{ id: 'Point03', size: 'big', left: '51%', top: '53%' },
{ id: 'Point04', size: 'big', left: '51%', top: '36%' },
{ id: 'Point02', size: 'big', left: '68.5%', top: '46%' },
{ id: 'Point05', size: 'big', left: '51%', top: '70%' },
{ id: 'Point01', size: 'big', left: '33.5%', top: '46%' },
{ id: 'Point08', size: 'medium', left: '50%', top: '22%' },
{ id: 'Point07', size: 'medium', left: '68%', top: '71%' },
{ id: 'Point06', size: 'medium', left: '32%', top: '71%' },
{ id: 'Point16', size: 'small', left: '50%', top: '9%' },
{ id: 'Point18', size: 'small', left: '66%', top: '14%' },
{ id: 'Point17', size: 'small', left: '34%', top: '14%' },
{ id: 'Point09', size: 'small', left: '50%', top: '87%' },
{ id: 'Point15', size: 'small', left: '81%', top: '35%' },
{ id: 'Point12', size: 'small', left: '19%', top: '35%' },
{ id: 'Point13', size: 'small', left: '81%', top: '57%' },
{ id: 'Point10', size: 'small', left: '20%', top: '57%' },
{ id: 'Point14', size: 'small', left: '93%', top: '44%' },
{ id: 'Point11', size: 'small', left: '7%', top: '44%' }
],
Shaman: [
{ id: 'Point03', size: 'big', left: '51%', top: '57%' },
{ id: 'Point04', size: 'big', left: '51%', top: '36%' },
{ id: 'Point02', size: 'big', left: '68.5%', top: '41%' },
{ id: 'Point05', size: 'big', left: '51%', top: '75%' },
{ id: 'Point01', size: 'big', left: '33.5%', top: '41%' },
{ id: 'Point08', size: 'medium', left: '50%', top: '22%' },
{ id: 'Point07', size: 'medium', left: '89.5%', top: '56.5%' },
{ id: 'Point06', size: 'medium', left: '17.5%', top: '56.5%' },
{ id: 'Point16', size: 'small', left: '50%', top: '10%' },
{ id: 'Point18', size: 'small', left: '66%', top: '14%' },
{ id: 'Point17', size: 'small', left: '34%', top: '14%' },
{ id: 'Point09', size: 'small', left: '50%', top: '87%' },
{ id: 'Point15', size: 'small', left: '62%', top: '83%' },
{ id: 'Point12', size: 'small', left: '38%', top: '83%' },
{ id: 'Point13', size: 'small', left: '77%', top: '70%' },
{ id: 'Point14', size: 'small', left: '67%', top: '61%' },
{ id: 'Point10', size: 'small', left: '7%', top: '44%' },
{ id: 'Point11', size: 'small', left: '20%', top: '31%' }
],
Warlock: [
{ id: 'Point03', size: 'big', left: '51%', top: '44%' },
{ id: 'Point04', size: 'big', left: '51%', top: '25%' },
{ id: 'Point02', size: 'big', left: '68.5%', top: '47%' },
{ id: 'Point05', size: 'big', left: '51%', top: '62%' },
{ id: 'Point01', size: 'big', left: '33.5%', top: '47%' },
{ id: 'Point08', size: 'medium', left: '50%', top: '8%' },
{ id: 'Point07', size: 'medium', left: '81.5%', top: '37%' },
{ id: 'Point06', size: 'medium', left: '20.5%', top: '37%' },
{ id: 'Point17', size: 'small', left: '66%', top: '14%' },
{ id: 'Point16', size: 'small', left: '34%', top: '14%' },
{ id: 'Point09', size: 'small', left: '50%', top: '74%' },
{ id: 'Point18', size: 'small', left: '50%', top: '87%' },
{ id: 'Point13', size: 'small', left: '94%', top: '48%' },
{ id: 'Point14', size: 'small', left: '81%', top: '61%' },
{ id: 'Point15', size: 'small', left: '68%', top: '74%' },
{ id: 'Point10', size: 'small', left: '6%', top: '48%' },
{ id: 'Point11', size: 'small', left: '20%', top: '61%' },
{ id: 'Point12', size: 'small', left: '33%', top: '74%' }
],
Warrior: [
{ id: 'Point03', size: 'big', left: '51%', top: '53%' },
{ id: 'Point04', size: 'big', left: '51%', top: '36%' },
{ id: 'Point02', size: 'big', left: '69%', top: '48%' },
{ id: 'Point05', size: 'big', left: '51%', top: '70.5%' },
{ id: 'Point01', size: 'big', left: '33%', top: '48%' },
{ id: 'Point08', size: 'medium', left: '50%', top: '22%' },
{ id: 'Point07', size: 'medium', left: '67%', top: '83%' },
{ id: 'Point06', size: 'medium', left: '33%', top: '83%' },
{ id: 'Point16', size: 'small', left: '50%', top: '9%' },
{ id: 'Point18', size: 'small', left: '66%', top: '14%' },
{ id: 'Point17', size: 'small', left: '34%', top: '14%' },
{ id: 'Point09', size: 'small', left: '50%', top: '87%' },
{ id: 'Point15', size: 'small', left: '81%', top: '43.5%' },
{ id: 'Point12', size: 'small', left: '19%', top: '43.5%' },
{ id: 'Point13', size: 'small', left: '81%', top: '70%' },
{ id: 'Point10', size: 'small', left: '19%', top: '70%' },
{ id: 'Point14', size: 'small', left: '93%', top: '56.5%' },
{ id: 'Point11', size: 'small', left: '7%', top: '56.5%' }
],
Memory: [
{ id: 'Point03', size: 'big', left: '51%', top: '72%' },
{ id: 'Point04', size: 'big', left: '75%', top: '53%' },
{ id: 'Point02', size: 'big', left: '67%', top: '67%' },
{ id: 'Point05', size: 'big', left: '27%', top: '53%' },
{ id: 'Point01', size: 'big', left: '35%', top: '67%' },
{ id: 'Point08', size: 'medium', left: '34%', top: '34%' },
{ id: 'Point07', size: 'medium', left: '50%', top: '87%' },
{ id: 'Point06', size: 'medium', left: '91%', top: '51%' },
{ id: 'Point16', size: 'small', left: '27.5%', top: '22%' },
{ id: 'Point17', size: 'small', left: '43%', top: '14%' },
{ id: 'Point18', size: 'small', left: '59%', top: '14%' },
{ id: 'Point19', size: 'big', left: '51%', top: '50%' },
{ id: 'Point20', size: 'big', left: '51%', top: '28%' },
{ id: 'Point12', size: 'small', left: '86%', top: '40%' },
{ id: 'Point13', size: 'small', left: '86%', top: '63%' },
{ id: 'Point14', size: 'small', left: '35%', top: '82%' },
{ 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%' }
]
}
export const traceLink : Record<string, Record<string, string[]>> = {
Knight: {
Point06: ["Point10", "Point11"],
Point07: ["Point13", "Point14"],
Point08: ["Point16", "Point17", "Point18"],
Point09: ["Point07", "Point06", "Point10", "Point11", "Point13", "Point14"],
Point10: ["Point11"],
Point16: ["Point17", "Point18"],
Point13: ["Point14"],
},
Mage: {
Point08: ["Point16", "Point17"],
Point07: ["Point13", "Point14", "Point15"],
Point13: ["Point14", "Point15"],
Point06: ["Point10", "Point11", "Point12"],
Point10: ["Point11", "Point12"],
},
Priest: {
Point08: ["Point16", "Point17"],
Point07: ["Point10", "Point11", "Point12"],
Point10: ["Point11", "Point12"],
Point11: ["Point12"],
Point06: ["Point13", "Point14", "Point15"],
Point13: ["Point14", "Point15"],
Point14: ["Point15"],
},
Rogue: {
Point08: ["Point16", "Point17", "Point18"],
Point16: ["Point17", "Point18"],
Point07: ["Point13", "Point14"],
Point13: ["Point14"],
Point06: ["Point10", "Point11"],
Point10: ["Point11"],
},
Shaman: {
Point08: ["Point16", "Point17", "Point18"],
Point16: ["Point17", "Point18"],
Point07: ["Point13", "Point14"],
Point13: ["Point14"],
Point06: ["Point10", "Point11"],
Point10: ["Point11"],
Point09: ["Point15", "Point12"],
},
Warlock: {
Point08: ["Point16", "Point17"],
Point07: ["Point13", "Point14", "Point15"],
Point13: ["Point14", "Point15"],
Point14: ["Point15"],
Point06: ["Point10", "Point11", "Point12"],
Point10: ["Point11", "Point12"],
Point11: ["Point12"],
Point09: ["Point18"],
},
Warrior: {
Point08: ["Point16", "Point17", "Point18"],
Point16: ["Point17", "Point18"],
Point07: ["Point13", "Point14", "Point15"],
Point13: ["Point14", "Point15"],
Point14: ["Point15"],
Point06: ["Point10", "Point11", "Point12"],
Point10: ["Point11", "Point12"],
Point11: ["Point12"],
},
Memory: {
Point16: ["Point17", "Point18"],
Point17: ["Point18"],
Point09: ["Point10", "Point11"],
Point07: ["Point14", "Point15"],
Point06: ["Point12", "Point13"],
}
}

View File

@@ -1,4 +1,4 @@
import { mapingStats } from "@/lib/constant"
import { mappingStats } from "@/constant/constant"
import { AffixDetail } from "@/types"
export function calcPromotion(level: number) {
@@ -40,10 +40,10 @@ export function calcRarity(rarity: string) {
export const calcAffixBonus = (affix: AffixDetail, stepCount: number, rollCount: number) => {
const data = affix;
if (!data) return 0;
if (mapingStats?.[data.property].unit === "%") {
if (mappingStats?.[data.property].unit === "%") {
return ((data.base * rollCount + data.step * stepCount) * 100).toFixed(1);
}
if (mapingStats?.[data.property].name === "SPD") {
if (mappingStats?.[data.property].name === "SPD") {
return (data.base * rollCount + data.step * stepCount).toFixed(1);
}
return (data.base * rollCount + data.step * stepCount).toFixed(0);

View File

@@ -1,4 +1,4 @@
import { listCurrentLanguage } from "@/lib/constant";
import { listCurrentLanguage } from "@/constant/constant";
import { CharacterBasic, EventBasic, LightConeBasic, MonsterBasic } from "@/types";

View File

@@ -1,4 +1,4 @@
import useAvatarStore from "@/stores/avatarStore"
import useAvatarStore from "@/stores/avatarStore";
export function getSkillTree(enhanced: string) {
const { avatarSelected, mapAvatarInfo } = useAvatarStore.getState()
@@ -20,3 +20,4 @@ export function getSkillTree(enhanced: string) {
return acc;
}, {} as Record<string, number>);
}

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function downloadJson(fileName: string, data: any) {
const json = JSON.stringify(data, null, 2)
const blob = new Blob([json], { type: 'application/json' })

View File

@@ -1,26 +1,52 @@
export function replaceByParam(desc: string, params: number[]): string {
desc = desc.replace(/<color=#[0-9a-fA-F]{8}>(.*?)<\/color>/g, (match, inner) => {
const colorCode = match.match(/#[0-9a-fA-F]{8}/)?.[0] ?? "#ffffff";
const processed = inner.replace(/#(\d+)\[i\](%)?/g, (_: string, index: string, percent: string | undefined) => {
const i = parseInt(index, 10) - 1;
const value = params[i];
if (value === undefined) return "";
return percent ? `${(value * 100).toFixed(0)}%` : `${value}`;
}).replace(/<unbreak>(.*?)<\/unbreak>/g, "$1");
function formatParam(
indexStr: string,
format: string,
floatDigits: string | undefined,
percent: string | undefined
): string {
const i: number = parseInt(indexStr, 10) - 1;
const value: number | undefined = params[i];
if (value === undefined) return "";
if (format.startsWith("f")) {
const digits: number = parseInt(floatDigits || "1", 10);
const num: number = percent ? value * 100 : value;
return `${num.toFixed(digits)}${percent ? "%" : ""}`;
}
if (format === "i") {
return percent ? `${(value * 100).toFixed(0)}%` : `${Math.round(value)}`;
}
return `${value}`;
}
const desc1 = desc.replace(/<color=#[0-9a-fA-F]{8}>(.*?)<\/color>/g, (match: string, inner: string): string => {
const colorCode: string = match.match(/#[0-9a-fA-F]{8}/)?.[0] ?? "#ffffff";
const processed: string = inner
.replace(/#(\d+)\[(f(\d+)|i)\](%)?/g, (
_: string,
index: string,
format: string,
floatDigits: string | undefined,
percent: string | undefined
): string => formatParam(index, format, floatDigits, percent))
.replace(/<unbreak>(.*?)<\/unbreak>/g, "$1");
return `<span style="color:${colorCode}">${processed}</span>`;
});
desc = desc.replace(/<unbreak>#(\d+)\[i\](%)?<\/unbreak>/g, (_: string, index: string, percent: string | undefined) => {
const i = parseInt(index, 10) - 1;
const value = params[i];
if (value === undefined) return "";
return percent ? `${(value * 100).toFixed(0)}%` : `${value}`;
});
const desc2 = desc1.replace(/<unbreak>#(\d+)\[(f(\d+)|i)\](%)?<\/unbreak>/g, (
_: string,
index: string,
format: string,
floatDigits: string | undefined,
percent: string | undefined
): string => formatParam(index, format, floatDigits, percent));
desc = desc.replace(/<unbreak>(\d+)<\/unbreak>/g, (_, number) => number);
const desc3 = desc2.replace(/<unbreak>(\d+)<\/unbreak>/g, (_: string, number: string): string => number);
desc = desc.replaceAll("\\n", "<br></br>");
return desc;
const desc4 = desc3.replaceAll("\\n", "<br></br>");
return desc4;
}

View File

@@ -2,7 +2,7 @@
import { useQuery } from '@tanstack/react-query'
import { fetchASByIdsNative, getASEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import { listCurrentLanguageApi } from '@/lib/constant'
import { listCurrentLanguageApi } from '@/constant/constant'
import useLocaleStore from '@/stores/localeStore'
import { toast } from 'react-toastify'
import useEventStore from '@/stores/eventStore'

View File

@@ -4,7 +4,7 @@ import { fetchCharactersByIdsNative, getCharacterListApi } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useAvatarStore from '@/stores/avatarStore'
import { listCurrentLanguageApi } from '@/lib/constant'
import { listCurrentLanguageApi } from '@/constant/constant'
import useLocaleStore from '@/stores/localeStore'
import useUserDataStore from '@/stores/userDataStore'
import { converterToAvatarStore } from '@/helper'
@@ -12,7 +12,7 @@ import { CharacterDetail } from '@/types'
export const useFetchAvatarData = () => {
const { setAvatars, avatars } = useUserDataStore()
const { setListAvatar, setAllMapAvatarInfo, mapAvatarInfo, setAvatarSelected } = useAvatarStore()
const { setListAvatar, setAllMapAvatarInfo, mapAvatarInfo, setAvatarSelected, avatarSelected } = useAvatarStore()
const { locale } = useLocaleStore()
const { data: dataAvatar, error: errorAvatar } = useQuery({
queryKey: ['avatarData'],
@@ -57,12 +57,15 @@ export const useFetchAvatarData = () => {
useEffect(() => {
if (dataAvatar && !errorAvatar) {
setListAvatar(dataAvatar)
setAvatarSelected(dataAvatar[0])
if (!avatarSelected) {
setAvatarSelected(dataAvatar[0])
}
} else if (errorAvatar) {
toast.error("Failed to load avatar data")
}
}, [dataAvatar, errorAvatar, setAvatarSelected, setAvatars, setListAvatar, avatars])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataAvatar, errorAvatar])
useEffect(() => {
if (dataAvatarInfo && !errorAvatarInfo) {
@@ -70,5 +73,6 @@ export const useFetchAvatarData = () => {
} else if (errorAvatarInfo) {
toast.error("Failed to load avatar info data")
}
}, [dataAvatarInfo, errorAvatarInfo, setAllMapAvatarInfo, setAvatars])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataAvatarInfo, errorAvatarInfo])
}

View File

@@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'
import { fetchLightconesByIdsNative, getLightconeListApi } from '@/lib/api'
import { useEffect } from 'react'
import useLightconeStore from '@/stores/lightconeStore'
import { listCurrentLanguageApi } from '@/lib/constant'
import { listCurrentLanguageApi } from '@/constant/constant'
import useLocaleStore from '@/stores/localeStore'
import { toast } from 'react-toastify'

View File

@@ -2,7 +2,7 @@
import { useQuery } from '@tanstack/react-query'
import { fetchMOCByIdsNative, getMOCEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import { listCurrentLanguageApi } from '@/lib/constant'
import { listCurrentLanguageApi } from '@/constant/constant'
import useLocaleStore from '@/stores/localeStore'
import { toast } from 'react-toastify'
import useEventStore from '@/stores/eventStore'

View File

@@ -2,7 +2,7 @@
import { useQuery } from '@tanstack/react-query'
import { fetchPFByIdsNative, getPFEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import { listCurrentLanguageApi } from '@/lib/constant'
import { listCurrentLanguageApi } from '@/constant/constant'
import useLocaleStore from '@/stores/localeStore'
import { toast } from 'react-toastify'
import useEventStore from '@/stores/eventStore'

View File

@@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'
import { fetchRelicsByIdsNative, getRelicSetListApi } from '@/lib/api'
import { useEffect } from 'react'
import useRelicStore from '@/stores/relicStore'
import { listCurrentLanguageApi } from '@/lib/constant'
import { listCurrentLanguageApi } from '@/constant/constant'
import useLocaleStore from '@/stores/localeStore'
import { toast } from 'react-toastify'

View File

@@ -8,11 +8,13 @@ interface AvatarState {
filter: FilterAvatarType;
avatarSelected: CharacterBasic | null;
mapAvatarInfo: Record<string, CharacterDetail>;
skillSelected: string | null;
setListAvatar: (newListAvatar: CharacterBasic[]) => void;
setAvatarSelected: (newAvatarSelected: CharacterBasic) => void;
setFilter: (newFilter: FilterAvatarType) => void;
setMapAvatarInfo: (avatarId: string, newCharacter: CharacterDetail) => void;
setAllMapAvatarInfo: (newCharacter: Record<string, CharacterDetail>) => void;
setSkillSelected: (newSkillSelected: string | null) => void;
}
const useAvatarStore = create<AvatarState>((set, get) => ({
@@ -26,7 +28,9 @@ const useAvatarStore = create<AvatarState>((set, get) => ({
locale: "",
},
avatarSelected: null,
skillSelected: null,
mapAvatarInfo: {},
setSkillSelected: (newSkillSelected: string | null) => set({ skillSelected: newSkillSelected }),
setListAvatar: (newListAvatar: CharacterBasic[]) => set({ listAvatar: newListAvatar, listRawAvatar: newListAvatar }),
setAvatarSelected: (newAvatarSelected: CharacterBasic) => set({ avatarSelected: newAvatarSelected }),
setFilter: (newFilter: FilterAvatarType) => {

View File

@@ -4,6 +4,8 @@ import { createJSONStorage, persist } from 'zustand/middleware';
interface LocaleState {
locale: string;
theme: string;
setTheme: (newTheme: string) => void;
setLocale: (newLocale: string) => void;
}
@@ -11,6 +13,8 @@ const useLocaleStore = create<LocaleState>()(
persist(
(set) => ({
locale: "en",
theme: "night",
setTheme: (newTheme: string) => set({ theme: newTheme }),
setLocale: (newLocale: string) => set({ locale: newLocale }),
}),
{

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export interface CharacterDetail {
Name: string;
Desc: string;
@@ -50,7 +51,8 @@ export interface RankType {
export interface SkillType {
Id: number;
Name: string;
Desc: string;
Desc: string | null;
SimpleDesc: string;
Type: string;
Tag: string;
SPBase: number | null;
@@ -66,6 +68,13 @@ export interface LevelParams {
ParamList: number[];
}
export type StatusAddType = {
$type: string;
PropertyType: string;
Value: number;
Name: string;
};
export interface SkillTreePoint {
Anchor: string;
AvatarPromotionLimit: number | null;
@@ -75,14 +84,14 @@ export interface SkillTreePoint {
LevelUpSkillID: number[];
MaterialList: ItemConfigRow[];
MaxLevel: number;
ParamList: any[];
ParamList: number[];
PointID: number;
PointName: string | null;
PointDesc: string | null;
PointTriggerKey: number;
PointType: number;
PrePoint: string[];
StatusAddList: any[];
StatusAddList: StatusAddType[];
}
export interface ItemConfigRow {
@@ -109,6 +118,7 @@ export interface Memosprite {
export interface SpriteSkill {
Name: string;
Desc: string | null;
SimpleDesc: string;
Type: string | null;
Tag: string;
SPBase: number | null;
@@ -116,6 +126,7 @@ export interface SpriteSkill {
BPAdd: number | null;
ShowStanceList: number[];
SkillComboValueDelta: number | null;
Extra: Record<string, Extra>;
Level: Record<string, LevelParams>;
}
@@ -124,6 +135,13 @@ export interface UniqueAbility {
Name: string;
Desc: string;
Param: number[];
Extra: Record<string, Extra>;
}
export interface Extra {
name: string;
desc: string;
param: number[];
}
export interface Stat {

View File

@@ -10,6 +10,7 @@
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"baseUrl": ".",
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,