UPDATE: Add showcase card
All checks were successful
Gitea Auto Deploy / Deploy-Container (push) Successful in 1m45s

This commit is contained in:
2025-08-06 00:34:16 +07:00
parent 9b6a061cba
commit b17ccdcc07
44 changed files with 6418 additions and 913 deletions

View File

@@ -0,0 +1,11 @@
"use client"
import ShowCaseInfo from "@/components/showcaseCard"
export default function ShowcaseCard() {
return (
<div>
<ShowCaseInfo />
</div>
)
}

View File

@@ -294,7 +294,7 @@ export default function ActionBar() {
<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 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 className="btn btn-success btn-sm" onClick={() => actionMove('showcase-card')}>{transI18n("showcaseCard")}</button>
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm"> {isConnectPS ? transI18n("sync") : transI18n("connectPs")}</button>
</div>
</div>

View File

@@ -13,15 +13,16 @@ import ParseText from '../parseText';
import useLocaleStore from '@/stores/localeStore';
import useModelStore from '@/stores/modelStore';
import useMazeStore from '@/stores/mazeStore';
import Image from 'next/image';
export default function AvatarInfo() {
const { avatarSelected, mapAvatarInfo } = useAvatarStore()
const { Technique } = useMazeStore()
const { avatars, setAvatars, setAvatar } = useUserDataStore()
const { isOpenLightcone, setIsOpenLightcone } = useModelStore()
const { listLightcone, mapLightconeInfo } = useLightconeStore()
const { listLightcone, mapLightconeInfo, setDefaultFilter } = useLightconeStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore();
const lightcone = useMemo(() => {
if (!avatarSelected) return null;
const avatar = avatars[avatarSelected.id];
@@ -68,7 +69,8 @@ export default function AvatarInfo() {
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenLightcone]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpenLightcone ]);
return (
<div className="bg-base-100 max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
@@ -387,7 +389,13 @@ export default function AvatarInfo() {
{/* Action Buttons */}
<div className="flex flex-wrap gap-3 mt-2">
<button
onClick={() => handleShow("action_detail_modal")}
onClick={() => {
if (avatarSelected) {
const newDefaultFilter = { path: [avatarSelected.baseType.toLowerCase()], rarity: [] }
setDefaultFilter(newDefaultFilter)
handleShow("action_detail_modal")
}
}}
className="btn btn-primary btn-lg flex-1 min-w-fit"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -414,8 +422,9 @@ export default function AvatarInfo() {
{/* Lightcone Image */}
<div className="lg:col-span-1">
<div className="">
<img
loading="lazy"
<Image
width={904}
height={1260}
src={`https://api.hakush.in/hsr/UI/lightconemaxfigures/${lightconeDetail.id}.webp`}
className="w-full h-full rounded-lg object-cover shadow-lg"
alt="Lightcone"
@@ -477,7 +486,13 @@ export default function AvatarInfo() {
<h3 className="text-xl font-semibold mb-2">{transI18n("noLightconeEquipped")}</h3>
<p className="text-base-content/60 mb-6">{transI18n("equipLightconeNote")}</p>
<button
onClick={() => handleShow("action_detail_modal")}
onClick={() => {
if (avatarSelected) {
const newDefaultFilter = { path: [avatarSelected.baseType.toLowerCase()], rarity: [] }
setDefaultFilter(newDefaultFilter)
handleShow("action_detail_modal")
}
}}
className="btn btn-success btn-lg"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">

View File

@@ -1,666 +0,0 @@
"use client";
import React, { useState, useRef} from 'react';
export default function ShowCaseInfo() {
const [imageUrl, setImageUrl] = useState<string>('https://api.hakush.in/hsr/UI/avatardrawcard/1001.webp');
const [position, setPosition] = useState<{ x: number; y: number }>({ x: 0, y: 100 });
const ref = useRef<HTMLDivElement>(null)
return (
<div className="flex flex-col justify-start m-2">
<div className="overflow-auto">
<div ref={ref} className="relative min-h-[650px] w-[1400px] rounded-3xl overflow-hidden">
<img
src="https://cdn.jsdelivr.net/gh/picklejason/hsr-showcase@main/public/blur-bg.png"
alt="Showcase Background"
className="absolute inset-0 w-full h-full object-cover"
/>
<div className="absolute bottom-2 left-4 z-10">
<span className="shadow-black [text-shadow:1px_1px_2px_var(--tw-shadow-color)]"></span>
</div>
<div className="flex flex-row items-center">
<div
className="relative min-h-[650px] w-[28%]"
>
<div
className="flex h-[650px] items-center"
style={{ cursor: 'grab' }}
>
<div className="overflow-hidden w-full h-full"
>
<img
src={imageUrl}
className="scale-[1.8] cursor-pointer object-cover"
alt="Character Preview"
style={{
position: 'relative',
top: `${position.y}px`,
left: `${position.x}px`,
}}
/>
</div>
</div>
<div className="absolute right-0 top-0 mr-[-15px] pt-3">
<div className="flex flex-col">
{[
"https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/eidolon/memory-of-you-eidolon_icon_small.webp",
"https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/eidolon/memory-of-it-eidolon_icon_small.webp",
"https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/eidolon/memory-of-everything-eidolon_icon_small.webp",
"https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/eidolon/never-forfeit-again-eidolon_icon_small.webp",
"https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/eidolon/never-forget-again-eidolon_icon_small.webp",
"https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/eidolon/just-like-this-always-eidolon_icon_small.webp"
].map((src, index) => (
<div key={index} className="relative my-1 flex rounded-full border-2 border-neutral-300 bg-neutral-800">
<img src={src} alt="Rank Icon" className="h-auto w-10" />
<div className="absolute flex h-full w-full items-center justify-center rounded-full bg-neutral-800/70">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" className="size-5">
<path fillRule="evenodd" d="M10 1a4.5 4.5 0 0 0-4.5 4.5V9H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2h-.5V5.5A4.5 4.5 0 0 0 10 1Zm3 8V5.5a3 3 0 1 0-6 0V9h6Z" clipRule="evenodd"></path>
</svg>
</div>
</div>
))}
</div>
</div>
</div>
<div className="relative flex min-h-[650px] w-[72%] flex-row items-center gap-3.5 rounded-r-3xl pl-10">
<img
src="https://cdn.jsdelivr.net/gh/picklejason/hsr-showcase@main/public/fade-bg.png"
alt="Background"
className="absolute inset-0 w-full h-full object-cover rounded-r-3xl"
style={{ zIndex: -1 }}
/>
<div className="flex h-[650px] w-1/3 flex-col justify-between py-3">
<div className="flex h-full flex-col justify-between">
<div>
<div className="flex flex-row items-center justify-between">
<span className="text-4xl">March 7th</span>
<img src="https://api.hakush.in/hsr/UI/element/ice.webp" alt="Element Icon" className="h-auto w-14" />
</div>
<div className="flex flex-row items-center gap-2">
<img src="https://api.hakush.in/hsr/UI/pathicon/knight.webp" alt="Path Icon" className="h-auto w-8" />
<span className="text-xl">Preservation</span>
</div>
<div>
<span className="text-2xl">Lv. 80</span>
<span className="text-xl"> / </span>
<span className="text-xl text-neutral-400">80</span>
</div>
</div>
<div className="relative mx-4 flex h-[225px] w-auto flex-row items-center justify-evenly">
<div className="absolute mb-5">
<img src="https://api.hakush.in/hsr/UI/pathicon/knight.webp" alt="Path Icon" className="h-40 w-40 opacity-20" />
</div>
<div className="flex h-full w-1/3 flex-col justify-center gap-8">
<div className="flex flex-col items-center">
<div className="relative flex flex-col items-center">
<img src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/skill/frigid-cold-arrow-skill_icon.webp" alt="Skill Icon" className="h-auto w-12 rounded-full border-2 border-neutral-500 bg-neutral-800" />
<span className="black-blur absolute bottom-4 text-sm">6 / 6</span>
<span className="z-10 mt-1.5 truncate text-sm">Basic ATK</span>
</div>
</div>
<div className="flex flex-col items-center">
<div className="relative flex flex-col items-center">
<img src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/skill/the-power-of-cuteness-skill_icon.webp" alt="Skill Icon" className="h-auto w-12 rounded-full border-2 border-neutral-500 bg-neutral-800" />
<span className="black-blur absolute bottom-4 text-sm">10 / 10</span>
<span className="z-10 mt-1.5 truncate text-sm">Skill</span>
</div>
</div>
</div>
<div className="flex w-1/3 justify-center">
<div className="relative flex flex-col items-center">
<img src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/skill/glacial-cascade-skill_icon.webp" alt="Skill Icon" className="h-auto w-12 rounded-full border-2 border-neutral-500 bg-neutral-800" />
<span className="black-blur absolute bottom-4 text-sm">10 / 10</span>
<span className="z-10 mt-1.5 truncate text-sm">Ultimate</span>
</div>
</div>
<div className="flex h-full w-1/3 flex-col justify-center gap-8">
<div className="flex flex-col items-center">
<div className="relative flex flex-col items-center">
<img src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/skill/girl-power-skill_icon.webp" alt="Skill Icon" className="h-auto w-12 rounded-full border-2 border-neutral-500 bg-neutral-800" />
<span className="black-blur absolute bottom-4 text-sm">8 / 8</span>
<span className="z-10 mt-1.5 truncate text-sm">Talent</span>
</div>
</div>
<div className="flex flex-col items-center">
<div className="relative flex flex-col items-center">
<img src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/skill/freezing-beauty-skill_icon.webp" alt="Skill Icon" className="h-auto w-12 rounded-full border-2 border-neutral-500 bg-neutral-800" />
<span className="black-blur absolute bottom-4 text-sm">5 / 5</span>
<span className="z-10 mt-1.5 truncate text-sm">Technique</span>
</div>
</div>
</div>
</div>
<div className="flex items-center justify-center">
<div className="flex w-full flex-row justify-evenly">
{/* First Column */}
<div className="flex flex-col justify-center items-center" style={{ gap: '0.25rem' }}>
<img
src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/trace/purify-trace_icon.webp"
alt="Icon 1001101"
className="bg-neutral-800 h-auto w-5 rounded-full"
/>
<div className="flex flex-row justify-center items-center" style={{ gap: '0.25rem' }}>
<img
src="https://api.hakush.in/hsr/UI/trace/IconDefence.webp"
alt="Icon 1001101"
className="bg-neutral-800 h-auto w-3 rounded-full"
/>
<img
src="https://api.hakush.in/hsr/UI/trace/IconIceAddedRatio.webp"
alt="Icon 1001101"
className="bg-neutral-800 h-auto w-3 rounded-full"
/>
</div>
</div>
{/* Second Column */}
<div className="flex flex-col justify-center items-center" style={{ gap: '0.25rem' }}>
<img
src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/trace/reinforce-trace_icon.webp"
alt="Icon 1001102"
className="bg-neutral-800 h-auto w-5 rounded-full"
/>
<div className="flex flex-row justify-center items-center" style={{ gap: '0.25rem' }}>
<img
src="https://api.hakush.in/hsr/UI/trace/IconIceAddedRatio.webp"
alt="Icon 1001102"
className="bg-neutral-800 h-auto w-3 rounded-full"
/>
<img
src="https://api.hakush.in/hsr/UI/trace/IconStatusResistance.webp"
alt="Icon 1001102"
className="bg-neutral-800 h-auto w-3 rounded-full"
/>
</div>
</div>
{/* Third Column */}
<div className="flex flex-col justify-center items-center" style={{ gap: '0.25rem' }}>
<img
src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/trace/ice-spell-trace_icon.webp"
alt="Icon 1001103"
className="bg-neutral-800 h-auto w-5 rounded-full"
/>
<div className="flex flex-row justify-center items-center" style={{ gap: '0.25rem' }}>
<img
src="https://api.hakush.in/hsr/UI/trace/IconIceAddedRatio.webp"
alt="Icon 1001103"
className="bg-neutral-800 h-auto w-3 rounded-full"
/>
<img
src="https://api.hakush.in/hsr/UI/trace/IconIceAddedRatio.webp"
alt="Icon 1001103"
className="bg-neutral-800 h-auto w-3 rounded-full"
/>
<img
src="https://api.hakush.in/hsr/UI/trace/IconDefence.webp"
alt="Icon 1001103"
className="bg-neutral-800 h-auto w-3 rounded-full"
/>
</div>
</div>
{/* Fourth Column */}
<div className="flex flex-col justify-center items-center" style={{ gap: '0.25rem' }}>
<div className="flex flex-row justify-center items-center" style={{ gap: '0.25rem' }}>
<img
src="https://api.hakush.in/hsr/UI/trace/IconIceAddedRatio.webp"
alt="Icon 1001201"
className="bg-neutral-800 h-auto w-3 rounded-full"
/>
<img
src="https://api.hakush.in/hsr/UI/trace/IconStatusResistance.webp"
alt="Icon 1001201"
className="bg-neutral-800 h-auto w-3 rounded-full"
/>
<img
src="https://api.hakush.in/hsr/UI/trace/IconDefence.webp"
alt="Icon 1001201"
className="bg-neutral-800 h-auto w-3 rounded-full"
/>
</div>
</div>
</div>
</div>
<div className="flex flex-row items-center justify-center">
<div className="relative flex flex-col items-center">
<img
src="https://api.hakush.in/hsr/UI/lightconemaxfigures/24000.webp"
alt="Light Cone Preview"
className="h-auto w-32"
/>
<img
src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png"
alt="Light Cone Rarity Icon"
className="absolute bottom-0 left-1 h-auto w-36"
/>
</div>
<div className="flex w-3/5 flex-col items-center gap-2 text-center">
<span className="text-xl">On the Fall of an Aeon</span>
<div className="flex items-center gap-2">
<div className="h-6 w-6 rounded-full font-normal bg-[#f6ce71] text-black">
V
</div>
<div>
<span className="text-lg">Lv. 80</span>
<span> / </span>
<span className="text-neutral-400">80</span>
</div>
</div>
<div className="flex flex-row gap-1.5">
<div className="black-blur flex flex-row items-center rounded pr-1">
<img
src="https://srtools.pages.dev/icon/property/IconAttack.png"
alt="Attribute Icon"
className="h-auto w-6 p-1"
/>
<span className="text-sm">529</span>
</div>
<div className="black-blur flex flex-row items-center rounded pr-1">
<img
src="https://srtools.pages.dev/icon/property/IconDefence.png"
alt="Attribute Icon"
className="h-auto w-6 p-1"
/>
<span className="text-sm">397</span>
</div>
<div className="black-blur flex flex-row items-center rounded pr-1">
<img
src="https://srtools.pages.dev/icon/property/IconMaxHP.png"
alt="Attribute Icon"
className="h-auto w-6 p-1"
/>
<span className="text-sm">1058</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div className="flex h-[650px] w-1/3 flex-col justify-between py-3">
<div className="flex w-full flex-col justify-between gap-y-0.5 text-base h-[500px]">
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconMaxHP.png" alt="Stat Icon" className="h-auto w-10 p-2" />
<span className="font-bold">HP</span>
</div>
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
<div className="flex cursor-default flex-col text-right font-bold">2942</div>
</div>
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Stat Icon" className="h-auto w-10 p-2" />
<span className="font-bold">ATK</span>
</div>
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
<div className="flex cursor-default flex-col text-right font-bold">3212</div>
</div>
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconDefence.png" alt="Stat Icon" className="h-auto w-10 p-2" />
<span className="font-bold">DEF</span>
</div>
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
<div className="flex cursor-default flex-col text-right font-bold">1255</div>
</div>
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconSpeed.png" alt="Stat Icon" className="h-auto w-10 p-2" />
<span className="font-bold">SPD</span>
</div>
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
<div className="flex cursor-default flex-col text-right font-bold">106</div>
</div>
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconCriticalChance.png" alt="Stat Icon" className="h-auto w-10 p-2" />
<span className="font-bold">CRIT Rate</span>
</div>
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
<div className="flex cursor-default flex-col text-right font-bold">16.7%</div>
</div>
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconCriticalDamage.png" alt="Stat Icon" className="h-auto w-10 p-2" />
<span className="font-bold">CRIT DMG</span>
</div>
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
<div className="flex cursor-default flex-col text-right font-bold">79.2%</div>
</div>
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconBreakUp.png" alt="Stat Icon" className="h-auto w-10 p-2" />
<span className="font-bold">Break Effect</span>
</div>
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
<div className="flex cursor-default flex-col text-right font-bold">186.9%</div>
</div>
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconStatusResistance.png" alt="Stat Icon" className="h-auto w-10 p-2" />
<span className="font-bold">Effect RES</span>
</div>
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
<div className="flex cursor-default flex-col text-right font-bold">4.0%</div>
</div>
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconEnergyRecovery.png" alt="Stat Icon" className="h-auto w-10 p-2" />
<span className="font-bold">Energy Regeneration Rate</span>
</div>
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
<div className="flex cursor-default flex-col text-right font-bold">0.0%</div>
</div>
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconStatusProbability.png" alt="Stat Icon" className="h-auto w-10 p-2" />
<span className="font-bold">Effect Hit Rate</span>
</div>
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
<div className="flex cursor-default flex-col text-right font-bold">20.3%</div>
</div>
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconHealRatio.png" alt="Stat Icon" className="h-auto w-10 p-2" />
<span className="font-bold">Outgoing Healing Boost</span>
</div>
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
<div className="flex cursor-default flex-col text-right font-bold">0.0%</div>
</div>
<div className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconIceAddedRatio.png" alt="Stat Icon" className="h-auto w-10 p-2" />
<span className="font-bold">Ice DMG Boost</span>
</div>
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
<div className="flex cursor-default flex-col text-right font-bold">6.4%</div>
</div>
</div>
<hr />
<div className="flex flex-col items-center gap-1">
<div className="flex w-full flex-row justify-between text-left">
<span>Prisoner in Deep Confinement</span>
<div>
<span className="black-blur bg-black flex w-5 justify-center rounded px-1.5 py-0.5">2</span>
</div>
</div>
<div className="flex w-full flex-row justify-between text-left">
<span>Watchmaker, Master of Dream Machinations</span>
<div>
<span className="black-blur bg-black flex w-5 justify-center rounded px-1.5 py-0.5">2</span>
</div>
</div>
<div className="flex w-full flex-row justify-between text-left">
<span>Talia: Kingdom of Banditry</span>
<div>
<span className="black-blur bg-black flex w-5 justify-center rounded px-1.5 py-0.5">2</span>
</div>
</div>
</div>
</div>
<div className="w-1/3">
<div className="flex h-[650px] flex-col justify-between py-3 text-lg">
<div className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
<div className="flex">
<img src="https://api.hakush.in/hsr/UI/relicfigures/IconRelic_116_1.webp" alt="Relic Icon" className="h-auto w-20" />
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" />
</div>
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
<img src="https://srtools.pages.dev/icon/property/IconMaxHP.png" alt="Main Affix Icon" className="h-auto w-9" />
<span className="text-base text-[#f1a23c]">705</span>
<span className="black-blur rounded px-1 text-xs">+15</span>
</div>
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+7.8%</span>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconCriticalChance.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+3.2%</span>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconCriticalDamage.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+17.5%</span>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconBreakUp.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+15.6%</span>
</div>
</div>
</div>
</div>
<div className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
<div className="flex">
<img src="https://api.hakush.in/hsr/UI/relicfigures/IconRelic_118_2.webp" alt="Relic Icon" className="h-auto w-20" />
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" />
</div>
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Main Affix Icon" className="h-auto w-9" />
<span className="text-base text-[#f1a23c]">352</span>
<span className="black-blur rounded px-1 text-xs">+15</span>
</div>
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+6.9%</span>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconCriticalDamage.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+11.7%</span>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconStatusProbability.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+3.9%</span>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconBreakUp.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+23.3%</span>
</div>
</div>
</div>
</div>
<div className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
<div className="flex">
<img src="https://api.hakush.in/hsr/UI/relicfigures/IconRelic_116_3.webp" alt="Relic Icon" className="h-auto w-20" />
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" />
</div>
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Main Affix Icon" className="h-auto w-9" />
<span className="text-base text-[#f1a23c]">43.2%</span>
<span className="black-blur rounded px-1 text-xs">+15</span>
</div>
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconDefence.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+57</span>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconMaxHP.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+3.9%</span>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconCriticalChance.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+2.6%</span>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconBreakUp.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+19.4%</span>
</div>
</div>
</div>
</div>
<div className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
<div className="flex">
<img src="https://api.hakush.in/hsr/UI/relicfigures/IconRelic_118_4.webp" alt="Relic Icon" className="h-auto w-20" />
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" />
</div>
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Main Affix Icon" className="h-auto w-9" />
<span className="text-base text-[#f1a23c]">18.9%</span>
<span className="black-blur rounded px-1 text-xs">+15</span>
</div>
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconMaxHP.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+5.2%</span>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconCriticalDamage.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+4.3%</span>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconCriticalChance.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+2.7%</span>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconDefence.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+34</span>
</div>
</div>
</div>
</div>
<div className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
<div className="flex">
<img src="https://api.hakush.in/hsr/UI/relicfigures/IconRelic_118_4.webp" alt="Relic Icon" className="h-auto w-20" />
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" />
</div>
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Main Affix Icon" className="h-auto w-9" />
<span className="text-base text-[#f1a23c]">18.9%</span>
<span className="black-blur rounded px-1 text-xs">+15</span>
</div>
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconMaxHP.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+5.2%</span>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconCriticalDamage.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+4.3%</span>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconCriticalChance.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+2.7%</span>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconDefence.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+34</span>
</div>
</div>
</div>
</div>
<div className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
<div className="flex">
<img src="https://api.hakush.in/hsr/UI/relicfigures/IconRelic_118_4.webp" alt="Relic Icon" className="h-auto w-20" />
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" />
</div>
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Main Affix Icon" className="h-auto w-9" />
<span className="text-base text-[#f1a23c]">18.9%</span>
<span className="black-blur rounded px-1 text-xs">+15</span>
</div>
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconMaxHP.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+5.2%</span>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconCriticalDamage.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+4.3%</span>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconCriticalChance.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+2.7%</span>
</div>
</div>
<div className="flex flex-col">
<div className="flex flex-row items-center">
<img src="https://srtools.pages.dev/icon/property/IconDefence.png" alt="Sub Affix Icon" className="h-auto w-7" />
<span className="text-sm">+34</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -14,11 +14,29 @@ export default function LightconeBar() {
const [listPath, setListPath] = useState<Record<string, boolean>>({ "knight": false, "mage": false, "priest": false, "rogue": false, "shaman": false, "warlock": false, "warrior": false, "memory": false })
const [listRank, setListRank] = useState<Record<string, boolean>>({ "3": false, "4": false, "5": false })
const { locale } = useLocaleStore()
const { listLightcone, filter, setFilter } = useLightconeStore()
const { listLightcone, filter, setFilter, defaultFilter } = useLightconeStore()
const { setAvatar, avatars } = useUserDataStore()
const { avatarSelected } = useAvatarStore()
const { setIsOpenLightcone } = useModelStore()
const transI18n = useTranslations("DataPage")
useEffect(() => {
const newListPath: Record<string, boolean> = { "knight": false, "mage": false, "priest": false, "rogue": false, "shaman": false, "warlock": false, "warrior": false, "memory": false }
const newListRank: Record<string, boolean> = { "3": false, "4": false, "5": false }
for (const path of defaultFilter.path) {
if (path in newListPath) {
newListPath[path] = true
}
}
for (const rarity of defaultFilter.rarity) {
if (rarity in newListRank) {
newListRank[rarity] = true
}
}
setListPath(newListPath)
setListRank(newListRank)
}, [defaultFilter])
useEffect(() => {
setFilter({
...filter,

View File

@@ -249,13 +249,13 @@ export default function AsBar() {
>
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
{listMonster.find((monster) => monster.child.includes(waveValue))?.icon && <Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(waveValue))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
/>}
</div>
<div className="flex flex-col">
@@ -299,13 +299,13 @@ export default function AsBar() {
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
{listMonster.find((monster) => monster.child.includes(waveValue))?.icon && <Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(waveValue))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
/>}
</div>
<div className="flex flex-col">

View File

@@ -220,13 +220,13 @@ export default function CeBar() {
<div className="flex justify-center">
<Image
{listMonster.find((monster2) => monster2.child.includes(member.monster_id))?.icon && <Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster2) => monster2.child.includes(member.monster_id))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className=" object-contain w-20 h-20 overflow-hidden"
/>
/>}
</div>
<div className="flex justify-center gap-1 mb-2">

View File

@@ -232,13 +232,13 @@ export default function MocBar() {
>
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
{listMonster.find((monster) => monster.child.includes(waveValue))?.icon && <Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(waveValue))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
/>}
</div>
<div className="flex flex-col">
@@ -282,13 +282,13 @@ export default function MocBar() {
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
{listMonster.find((monster) => monster.child.includes(waveValue))?.icon && <Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(waveValue))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
/>}
</div>
<div className="flex flex-col">

View File

@@ -241,13 +241,13 @@ export default function PfBar() {
>
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
{listMonster.find((monster) => monster.child.includes(monsterId))?.icon && <Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(monsterId))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
/>}
</div>
<div className="flex flex-col">
@@ -291,13 +291,13 @@ export default function PfBar() {
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
{listMonster.find((monster) => monster.child.includes(monsterId))?.icon && <Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(monsterId))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
/>}
</div>
<div className="flex flex-col">

View File

@@ -4,7 +4,7 @@ import useUserDataStore from '@/stores/userDataStore';
import { AffixDetail, RelicDetail } from '@/types';
import React, { useEffect, useMemo } from 'react';
import SelectCustomImage from '../select/customSelectImage';
import { calcAffixBonus, randomPartition, randomStep, replaceByParam } from '@/helper';
import { calcAffixBonus, calcMainAffixBonus, randomPartition, randomStep, replaceByParam } from '@/helper';
import useAffixStore from '@/stores/affixStore';
import { mappingStats } from '@/constant/constant';
import useAvatarStore from '@/stores/avatarStore';
@@ -118,16 +118,7 @@ export default function RelicMaker() {
const data = affixSet[selectedMainStat];
if (!data) return 0;
const stat = mappingStats?.[data.property];
if (!stat) return 0;
const value = data.base + data.step * selectedRelicLevel;
if (stat.unit === "%") {
return (value * 100).toFixed(1) + "%";
}
return value.toFixed(0) + stat.unit;
return calcMainAffixBonus(data, selectedRelicLevel);
}, [mapMainAffix, selectedRelicSlot, selectedMainStat, selectedRelicLevel]);
const handleSubStatChange = (key: string, index: number, rollCount: number, stepCount: number) => {

View File

@@ -117,7 +117,7 @@ export default function RelicsInfo() {
handleShow("action_detail_modal")
}
const getEffects = useMemo(() => {
const relicEffects = useMemo(() => {
const avatar = avatars[avatarSelected?.id || ""];
const relicCount: { [key: string]: number } = {};
if (avatar) {
@@ -215,7 +215,7 @@ export default function RelicsInfo() {
</h3>
<div className="space-y-6">
{getEffects.map((setEffect, index) => {
{relicEffects.map((setEffect, index) => {
const relicInfo = mapRelicInfo[setEffect.key];
if (!relicInfo) return null;
return (

View File

@@ -0,0 +1,927 @@
"use client";
import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react';
import useListAvatarStore from "@/stores/avatarStore";
import { FastAverageColor, FastAverageColorResult } from 'fast-average-color';
import NextImage from 'next/image';
import ParseText from '../parseText';
import useLocaleStore from '@/stores/localeStore';
import { calcAffixBonus, calcBaseStat, calcBaseStatRaw, calcBonusStatRaw, calcMainAffixBonus, calcMainAffixBonusRaw, calcPromotion, calcSubAffixBonusRaw, convertToRoman, getNameChar, replaceByParam } from '@/helper';
import useUserDataStore from '@/stores/userDataStore';
import { traceShowCaseMap } from '@/constant/traceConstant';
import { StatusAddType } from '@/types';
import { mappingStats } from '@/constant/constant';
import useLightconeStore from '@/stores/lightconeStore';
import { useTranslations } from 'next-intl';
import useAffixStore from '@/stores/affixStore';
import useRelicStore from '@/stores/relicStore';
import { toast } from 'react-toastify';
import html2canvas from 'html2canvas-pro';
export default function ShowCaseInfo() {
const { avatarSelected, mapAvatarInfo } = useListAvatarStore()
const { mapLightconeInfo } = useLightconeStore()
const { mapMainAffix, mapSubAffix } = useAffixStore()
const { avatars } = useUserDataStore()
const [avgColor, setAvgColor] = useState('#222');
const imgRef = useRef(null);
const cardRef = useRef(null)
const { locale } = useLocaleStore()
const transI18n = useTranslations("DataPage")
const { mapRelicInfo } = useRelicStore()
const handleSaveImage = useCallback(() => {
if (cardRef.current === null) {
toast.error("Avatar showcase not found!");
return;
}
html2canvas(cardRef.current, {scale: 2, backgroundColor: '#000000'})
.then(function (canvas: HTMLCanvasElement) {
const link = document.createElement('a');
link.download = 'showcase.png';
link.href = canvas.toDataURL('image/png');
link.click();
})
.catch(() => {
toast.error("Error generating showcase card!");
});
}, [cardRef])
useEffect(() => {
if (!avatarSelected?.id) return;
const fac = new FastAverageColor();
const img = new Image();
img.crossOrigin = 'anonymous';
img.src = `https://api.hakush.in/hsr/UI/avatardrawcard/${avatarSelected.id}.webp`;
img.onload = () => {
fac.getColorAsync(img).then((color: FastAverageColorResult) => {
setAvgColor(color.hex); // #RRGGBB
});
};
}, [avatarSelected]);
const avatarInfo = useMemo(() => {
if (!avatarSelected) return
return mapAvatarInfo[avatarSelected.id]
}, [avatarSelected, mapAvatarInfo])
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 avatarData = useMemo(() => {
if (!avatarSelected) return
return avatars[avatarSelected.id]
}, [avatarSelected, avatars])
const avatarProfile = useMemo(() => {
if (!avatarSelected || !avatarData) return
return avatarData?.profileList?.[avatarData?.profileSelect]
}, [avatarSelected, avatarData])
const lightconeStats = useMemo(() => {
if (!avatarSelected || !avatarProfile?.lightcone || !mapLightconeInfo[avatarProfile?.lightcone?.item_id]) return
const promotion = calcPromotion(avatarProfile?.lightcone?.level)
const atkStat = calcBaseStat(
mapLightconeInfo[avatarProfile?.lightcone?.item_id].Stats[promotion].BaseAttack,
mapLightconeInfo[avatarProfile?.lightcone?.item_id].Stats[promotion].BaseAttackAdd,
0,
avatarProfile?.lightcone?.level
)
const hpStat = calcBaseStat(
mapLightconeInfo[avatarProfile?.lightcone?.item_id].Stats[promotion].BaseHP,
mapLightconeInfo[avatarProfile?.lightcone?.item_id].Stats[promotion].BaseHPAdd,
0,
avatarProfile?.lightcone?.level
)
const defStat = calcBaseStat(
mapLightconeInfo[avatarProfile?.lightcone?.item_id].Stats[promotion].BaseDefence,
mapLightconeInfo[avatarProfile?.lightcone?.item_id].Stats[promotion].BaseDefenceAdd,
0,
avatarProfile?.lightcone?.level
)
return {
attack: atkStat,
hp: hpStat,
def: defStat,
}
}, [avatarSelected, mapLightconeInfo, avatarProfile])
const relicEffects = useMemo(() => {
const avatar = avatars[avatarSelected?.id || ""];
const relicCount: { [key: string]: number } = {};
if (avatar) {
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
if (relicCount[relic.relic_set_id]) {
relicCount[relic.relic_set_id]++;
} else {
relicCount[relic.relic_set_id] = 1;
}
}
}
const listEffects: { key: string, count: number }[] = [];
Object.entries(relicCount).forEach(([key, value]) => {
if (value >= 2) {
listEffects.push({ key: key, count: value });
}
});
return listEffects;
}, [avatars, avatarSelected]);
const relicStats = useMemo(() => {
if (!avatarSelected || !avatarProfile?.relics || !mapMainAffix || !mapSubAffix) return
return Object.entries(avatarProfile?.relics).map(([key, value]) => {
const mainAffixMap = mapMainAffix["5" + key]
const subAffixMap = mapSubAffix["5"]
if (!mainAffixMap || !subAffixMap) return
return {
img: `https://api.hakush.in/hsr/UI/relicfigures/IconRelic_${value.relic_set_id}_${key}.webp`,
mainAffix: {
property: mainAffixMap?.[value.main_affix_id]?.property,
level: value.level,
valueAffix: calcMainAffixBonus(mainAffixMap?.[value.main_affix_id], value.level),
detail: mappingStats[mainAffixMap?.[value.main_affix_id].property]
},
subAffix: value?.sub_affixes.map((subValue) => {
return {
property: subAffixMap?.[subValue.sub_affix_id]?.property,
valueAffix: calcAffixBonus(subAffixMap?.[subValue.sub_affix_id], subValue.step, subValue.count),
detail: mappingStats[subAffixMap?.[subValue.sub_affix_id].property]
}
})
}
})
}, [avatarSelected, avatarProfile, mapMainAffix, mapSubAffix])
const characterStats = useMemo(() => {
if (!avatarSelected || !avatarData) return
const charPromotion = calcPromotion(avatarData.level)
const statsData: Record<string, {
value: number,
base: number,
name: string,
icon: string,
unit: string,
round: number
}> = {
HP: {
value: calcBaseStatRaw(
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.HPBase,
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.HPAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.HPBase,
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.HPAdd,
avatarData.level
),
name: "HP",
icon: "/icon/hp.webp",
unit: "",
round: 0
},
ATK: {
value: calcBaseStatRaw(
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.AttackBase,
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.AttackAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.AttackBase,
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.AttackAdd,
avatarData.level
),
name: "ATK",
icon: "/icon/attack.webp",
unit: "",
round: 0
},
DEF: {
value: calcBaseStatRaw(
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.DefenceBase,
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.DefenceAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.DefenceBase,
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.DefenceAdd,
avatarData.level
),
name: "DEF",
icon: "/icon/defence.webp",
unit: "",
round: 0
},
SPD: {
value: mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.SpeedBase || 0,
base: mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.SpeedBase || 0,
name: "SPD",
icon: "/icon/speed.webp",
unit: "",
round: 1
},
CRITRate: {
value: mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.CriticalChance || 0,
base: mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.CriticalChance || 0,
name: "CRIT Rate",
icon: "/icon/crit-rate.webp",
unit: "%",
round: 1
},
CRITDmg: {
value: mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.CriticalDamage || 0,
base: mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.CriticalDamage || 0,
name: "CRIT DMG",
icon: "/icon/crit-damage.webp",
unit: "%",
round: 1
},
BreakEffect: {
value: 0,
base: 0,
name: "Break Effect",
icon: "/icon/break-effect.webp",
unit: "%",
round: 1
},
EffectRES: {
value: 0,
base: 0,
name: "Effect RES",
icon: "/icon/effect-res.webp",
unit: "%",
round: 1
},
EnergyRate: {
value: 0,
base: 0,
name: "Energy Rate",
icon: "/icon/energy-rate.webp",
unit: "%",
round: 1
},
EffectHitRate: {
value: 0,
base: 0,
name: "Effect Hit Rate",
icon: "/icon/effect-hit-rate.webp",
unit: "%",
round: 1
},
HealBoost: {
value: 0,
base: 0,
name: "Healing Boost",
icon: "/icon/healing-boost.webp",
unit: "%",
round: 1
},
PhysicalAdd: {
value: 0,
base: 0,
name: "Physical Boost",
icon: "/icon/physical-add.webp",
unit: "%",
round: 1
},
FireAdd: {
value: 0,
base: 0,
name: "Fire Boost",
icon: "/icon/fire-add.webp",
unit: "%",
round: 1
},
IceAdd: {
value: 0,
base: 0,
name: "Ice Boost",
icon: "/icon/ice-add.webp",
unit: "%",
round: 1
},
ThunderAdd: {
value: 0,
base: 0,
name: "Thunder Boost",
icon: "/icon/thunder-add.webp",
unit: "%",
round: 1
},
WindAdd: {
value: 0,
base: 0,
name: "Wind Boost",
icon: "/icon/wind-add.webp",
unit: "%",
round: 1
},
QuantumAdd: {
value: 0,
base: 0,
name: "Quantum Boost",
icon: "/icon/quantum-add.webp",
unit: "%",
round: 1
},
ImaginaryAdd: {
value: 0,
base: 0,
name: "Imaginary Boost",
icon: "/icon/imaginary-add.webp",
unit: "%",
round: 1
},
}
if (avatarProfile?.lightcone && mapLightconeInfo[avatarProfile?.lightcone?.item_id]) {
const lightconePromotion = calcPromotion(avatarProfile?.lightcone?.level)
statsData.HP.value += calcBaseStatRaw(
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHP,
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd,
avatarProfile?.lightcone?.level
)
statsData.HP.base += calcBaseStatRaw(
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHP,
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd,
avatarProfile?.lightcone?.level
)
statsData.ATK.value += calcBaseStatRaw(
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseAttack,
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseAttackAdd,
avatarProfile?.lightcone?.level
)
statsData.ATK.base += calcBaseStatRaw(
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseAttack,
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseAttackAdd,
avatarProfile?.lightcone?.level
)
statsData.DEF.value += calcBaseStatRaw(
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseDefence,
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseDefenceAdd,
avatarProfile?.lightcone?.level
)
statsData.DEF.base += calcBaseStatRaw(
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseDefence,
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseDefenceAdd,
avatarProfile?.lightcone?.level
)
const bonusData = mapLightconeInfo[avatarProfile?.lightcone?.item_id].Bonus?.[avatarProfile?.lightcone.rank - 1]
if (bonusData && bonusData.length > 0) {
const bonusSpd = bonusData.filter((bonus) => bonus.type === "BaseSpeed")
const bonusOther = bonusData.filter((bonus) => bonus.type !== "BaseSpeed")
bonusSpd.forEach((bonus) => {
statsData.SPD.value += bonus.value
statsData.SPD.base += bonus.value
})
bonusOther.forEach((bonus) => {
const statsBase = mappingStats?.[bonus.type]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(bonus.type, statsData[statsBase].base, bonus.value)
}
})
}
}
if (avatarSkillTree) {
Object.values(avatarSkillTree).forEach((value) => {
if (value?.["1"]
&& value?.["1"]?.PointID
&& typeof avatarData?.data?.skills?.[value?.["1"]?.PointID] === "number"
&& avatarData?.data?.skills?.[value?.["1"]?.PointID] !== 0
&& value?.["1"]?.StatusAddList
&& value?.["1"].StatusAddList.length > 0) {
value?.["1"]?.StatusAddList.forEach((status) => {
const statsBase = mappingStats?.[status?.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(status?.PropertyType, statsData[statsBase].base, status.Value)
}
})
}
})
}
if (avatarProfile?.relics && mapMainAffix && mapSubAffix) {
Object.entries(avatarProfile?.relics).forEach(([key, value]) => {
const mainAffixMap = mapMainAffix["5" + key]
const subAffixMap = mapSubAffix["5"]
if (!mainAffixMap || !subAffixMap) return
const mainStats = mappingStats?.[mainAffixMap?.[value.main_affix_id]?.property]?.baseStat
if (mainStats && statsData[mainStats]) {
statsData[mainStats].value += calcMainAffixBonusRaw(mainAffixMap?.[value.main_affix_id], value.level, statsData[mainStats].base)
}
value?.sub_affixes.forEach((subValue) => {
const subStats = mappingStats?.[subAffixMap?.[subValue.sub_affix_id]?.property]?.baseStat
if (subStats && statsData[subStats]) {
statsData[subStats].value += calcSubAffixBonusRaw(subAffixMap?.[subValue.sub_affix_id], subValue.step, subValue.count, statsData[subStats].base)
}
})
})
}
if (relicEffects && relicEffects.length > 0) {
relicEffects.forEach((relic) => {
const dataBonus = mapRelicInfo?.[relic.key]?.Bonus
if (!dataBonus || Object.keys(dataBonus).length === 0) return
Object.entries(dataBonus || {}).forEach(([key, value]) => {
if (relic.count < Number(key)) return
value.forEach((bonus) => {
const statsBase = mappingStats?.[bonus.type]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(bonus.type, statsData[statsBase].base, bonus.value)
}
})
})
})
}
return statsData
}, [
avatarSelected,
avatarData,
mapAvatarInfo,
avatarProfile?.lightcone,
avatarProfile?.relics,
mapLightconeInfo,
mapMainAffix,
mapSubAffix,
relicEffects,
mapRelicInfo,
avatarSkillTree
])
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")}`
}
return ""
}, [avatarSelected])
return (
<div className="flex flex-col justify-start m-2 text-white">
<div className="flex items-center justify-start mt-4 mb-4">
<button className="btn btn-success w-24 text-sm" onClick={handleSaveImage}>Save Img</button>
</div>
<div className="overflow-auto">
<div ref={cardRef} className=" relative min-h-[650px] w-[1400px] rounded-3xl">
<div
className="absolute inset-0 rounded-3xl w-full h-full transition-all duration-500 z-0"
style={{
backgroundColor: `${avgColor}60`,
backdropFilter: "blur(50px)",
WebkitBackdropFilter: "blur(50px)",
filter: "brightness(70%)",
}}
/>
<div className="absolute bottom-2 left-4 z-10">
<span className="shadow-black [text-shadow:1px_1px_2px_var(--tw-shadow-color)]"></span>
</div>
<div className="flex flex-row items-center">
<div
className="relative min-h-[650px] w-[24%]"
>
<div className="flex justify-center items-center w-full h-full overflow-hidden">
{avatarSelected && (
<NextImage
ref={imgRef}
src={`https://api.hakush.in/hsr/UI/avatardrawcard/${avatarSelected?.id}.webp`}
className="object-cover scale-[2]"
alt="Character Preview"
width={1024}
height={1024}
priority={true}
style={{
position: 'absolute',
top: `130px`,
left: `0px`,
}}
/>
)}
</div>
</div>
<div
className="relative flex min-h-[650px] w-[76%] flex-row items-center gap-3.5 rounded-3xl pl-10"
>
<div
className="absolute inset-0 rounded-3xl transition-all duration-500 z-0"
style={{
backgroundColor: `${avgColor}80`,
backdropFilter: "blur(50px)",
WebkitBackdropFilter: "blur(50px)",
filter: "brightness(50%)",
}}
/>
<div className="absolute top-4 left-4 z-10">
{avatarSelected && avatarInfo && avatarData?.data && typeof avatarData?.data?.rank === "number" && (
<div className="flex flex-col">
{avatarInfo?.RankIcon.map((src, index) => {
const isActive = avatarData?.data?.rank > index;
return (
<div key={index} className="relative my-2 flex rounded-full">
{isActive && (
<div className="absolute inset-0">
<div className="w-full h-full rounded-full shadow-lg shadow-yellow-400/50">
</div>
</div>
)}
<NextImage
src={src}
alt="Rank Icon"
width={50}
height={50}
className={`h-auto w-10 transition-all duration-300 ease-in-out
${isActive
? 'opacity-100 grayscale-0 brightness-110 drop-shadow-[0_0_8px_rgba(255,215,0,0.5)] scale-105'
: 'opacity-50 grayscale brightness-75'
}`}
/>
</div>
);
})}
</div>
)}
</div>
<div className="flex h-[650px] w-1/3 flex-col justify-between py-3 pl-8 z-10">
<div className="flex h-full flex-col justify-between">
<div>
<div className="flex flex-row items-center justify-between">
<ParseText className="text-3xl" text={getNameChar(locale, avatarSelected || undefined)} locale={locale} />
</div>
<div className="flex flex-row items-center gap-6">
<div className="text-2xl text-[#d8b46e]">Lv. <span className="text-white">{avatarData?.level}</span>/<span className="text-neutral-400">80</span></div>
{avatarSelected && (
<div className="flex gap-1">
<NextImage src={`/icon/${avatarSelected?.baseType.toLowerCase()}.webp`} alt="Path Icon" width={32} height={32} className="h-auto w-8" />
<NextImage src={`/icon/${avatarSelected?.damageType.toLowerCase()}.webp`} alt="Element Icon" width={32} height={32} className="h-auto w-8" />
</div>
)}
</div>
</div>
<div className="relative flex h-[225px] w-auto flex-row items-center">
{avatarSelected && (
<div className="absolute inset-0 flex items-center justify-center">
<NextImage src={`/icon/${avatarSelected?.baseType.toLowerCase()}.webp`} alt="Path Icon" width={160} height={160} className="h-40 w-40 opacity-20" />
</div>
)}
<div className="flex flex-col gap-4">
{avatarData && avatarInfo && avatarSkillTree && traceShowCaseMap[avatarSelected?.baseType || ""]
&& Object.values(traceShowCaseMap[avatarSelected?.baseType || ""] || []).map((item, index) => {
return (
<div key={`row-${index}`} className="flex flex-row items-center">
{item.map((btn, idx) => {
const size = btn.size || "small";
const isBig = size === "big";
const isMedium = size === "medium";
const isBigMemory = size === "big-memory";
const sizeClass = isBigMemory ? "w-12 h-12 mx-1" : isBig ? "w-12 h-12 mx-1" : isMedium ? "w-10 h-10" : "w-8 h-8";
const imageSize = isBigMemory ? "w-10" : isBig ? "w-10" : isMedium ? "w-8" : "w-6";
const bgColor = isBigMemory
? "bg-[#2a1a39]/80"
: isBig
? "bg-[#2b1d00]/80"
: isMedium
? "bg-[#2b1d00]/50"
: "bg-[#000000]/50";
const filterClass = isBigMemory
? "filter sepia brightness-80 hue-rotate-[280deg] saturate-400 contrast-130"
: isBig
? "filter sepia brightness-150 hue-rotate-15 saturate-200"
: "";
return (
<div key={`item-${idx}`} className="relative flex flex-row items-center">
<div
className={
`relative flex items-center justify-center ${sizeClass}
rounded-full ${bgColor} border-2 border-gray-500
${avatarData.data.skills[avatarSkillTree?.[btn.id]?.["1"]?.PointID] ? "" : "opacity-50"}
`}
>
<NextImage
src={
getImageSkill(
avatarInfo.SkillTrees?.[btn.id]?.["1"]?.Icon,
avatarSkillTree?.[btn.id]?.["1"]?.StatusAddList[0]
) || ""
}
alt={btn.id}
width={32}
height={32}
className={`h-auto ${imageSize} ${filterClass}`}
/>
{(isBig || isBigMemory) && (
<span className="absolute bottom-0 left-0 text-[12px] text-white bg-black/70 px-1 rounded-sm">
{10}
</span>
)}
</div>
{btn.isLink && idx < item.length - 1 && (
<div className="w-3 h-[3px] bg-white opacity-80 mx-1" />
)}
</div>
);
})}
</div>
)
})}
</div>
</div>
{avatarProfile && avatarProfile?.lightcone && lightconeStats ? (
<div className="flex flex-row items-center justify-center mb-2">
<div className="relative w-36 h-48">
{/* Background SVG Border (offset top-left) */}
<svg
xmlns="http://www.w3.org/2000/svg"
className="absolute w-full h-full z-10"
style={{
color: '#f59e0b',
opacity: 0.7,
top: '0px',
left: '-1px'
}}
viewBox="0 0 5 7"
>
<path
fill="none"
stroke="currentColor"
strokeOpacity="0.9"
strokeWidth="0.065"
d="m.301.032-.269.25v6.436l.303.25h4.364l.269-.25V.282l-.269-.25z"
/>
</svg>
{/* Card Image */}
<NextImage
className="absolute object-cover rounded-xl z-9 w-[95%]"
src={`https://api.hakush.in/hsr/UI/lightconemaxfigures/${avatarProfile?.lightcone.item_id}.webp`}
alt="Lightcone Image"
width={904}
height={1206}
style={{
top: '0px',
left: '6px',
}}
/>
{/* Top SVG Border (offset bottom-right) */}
<svg
xmlns="http://www.w3.org/2000/svg"
className="absolute w-full h-full z-8"
style={{
color: '#f59e0b',
opacity: 0.8,
bottom: '0px',
right: '-4px'
}}
viewBox="0 0 5 7"
>
<path
fill="none"
stroke="currentColor"
strokeOpacity="0.9"
strokeWidth="0.065"
d="m.301.032-.269.25v6.436l.303.25h4.364l.269-.25V.282l-.269-.25z"
/>
<path
d="M.34.004 0 .268v6.464l.33.233L4.71 7 5 6.732V.268L4.71 0Zm.01.06L4.693.03l.22.268.023 6.406-.25.233-4.34.02-.25-.253L.018.297z"
style={{
fill: 'currentColor',
opacity: 0.3
}}
/>
</svg>
{/* Stars */}
<div
className="absolute text-yellow-500 font-bold z-10"
style={{
writingMode: 'vertical-rl',
left: '-0.5rem',
top: '0.5rem',
fontSize: '1.1rem',
letterSpacing: '-0.1em',
textShadow: `
0 0 0.2em #f59e0b,
0 0 0.4em #f59e0b,
0 0 0.8em #f59e0b,
-0.05em -0.05em 0.05em rgba(0,0,0,0.7),
0.05em 0.05em 0.05em rgba(0,0,0,0.7)
`
}}
>
{[...Array(
Number(
mapLightconeInfo[avatarProfile?.lightcone?.item_id]?.Rarity?.[
mapLightconeInfo[avatarProfile?.lightcone?.item_id]?.Rarity.length - 1
] || 0
)
)].map((_, i) => (
<span key={i}></span>
))}
</div>
</div>
<div className="flex w-2/5 flex-col gap-2 text-white ml-2 h-full">
<div className="flex flex-col h-full">
<div className="flex items-center h-full">
<div className="w-1 h-[70%] bg-yellow-400 mr-2 rounded" />
<ParseText
className="text-lg font-semibold"
locale={locale}
text={mapLightconeInfo[avatarProfile?.lightcone?.item_id].Name}
/>
</div>
<div className="flex items-center gap-2 mt-1 text-sm text-[#d8b46e]">
<div className="h-6 w-6 flex items-center justify-center rounded-full bg-[#5c4022] border border-[#d8b46e] text-[#d8b46e] text-xs font-medium">
{convertToRoman(avatarProfile?.lightcone?.rank)}
</div>
<span>
Lv. <span className="text-white">{avatarProfile.lightcone.level}</span>/<span className="text-neutral-400">80</span>
</span>
</div>
</div>
{/* Chỉ số */}
<div className="flex justify-center items-center flex-col gap-1 mt-1 ">
<div className="flex gap-1 text-sm ">
<div className="flex items-center gap-1 rounded bg-black/30 px-1 w-fit py-1">
<NextImage src="/icon/hp.webp" alt="HP" width={16} height={16} className="w-4 h-4" />
<span>{
lightconeStats?.hp
}</span>
</div>
<div className="flex items-center gap-1 rounded bg-black/30 px-1 w-fit py-1">
<NextImage src="/icon/attack.webp" alt="ATK" width={16} height={16} className="w-4 h-4" />
<span>{lightconeStats?.attack}</span>
</div>
</div>
<div className="flex items-center gap-1 rounded bg-black/30 px-1 w-fit py-1">
<NextImage src="/icon/defence.webp" alt="DEF" width={16} height={16} className="w-4 h-4" />
<span>{lightconeStats?.def}</span>
</div>
</div>
</div>
</div>
) : (<div className="flex h-1/4 items-center text-lg">
{transI18n("noLightconeEquipped")}
</div>)}
</div>
</div>
<div className="flex h-[650px] w-1/3 flex-col justify-between py-3 z-10">
<div className="flex w-full flex-col justify-between gap-y-0.5 text-base h-[500px]">
{Object.entries(characterStats || {})?.map(([key, stat], index) => {
if (!stat || (key.includes("Add") && stat.value === 0)) return null
return (
<div key={index} className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<NextImage src={stat?.icon || ""} alt="Stat Icon" width={40} height={40} className="h-auto w-10 p-2" />
<span className="font-bold">{stat.name}</span>
</div>
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
<div className="flex cursor-default flex-col text-right font-bold">{
stat.value ? stat.unit === "%" ? (stat.value * 100).toFixed(stat.round) : stat.value.toFixed(stat.round) : 0
}{stat.unit}</div>
</div>
)
})}
</div>
<hr />
<div className="flex flex-col items-center gap-1 w-full my-2">
{relicEffects.map((setEffect, index) => {
const relicInfo = mapRelicInfo[setEffect.key];
if (!relicInfo) return null;
return (
<div key={index} className="flex w-full flex-row justify-between text-left">
<div
className="font-bold truncate max-w-full mr-1"
style={{
fontSize: 'clamp(0.5rem, 2vw, 1rem)'
}}
dangerouslySetInnerHTML={{
__html: replaceByParam(
relicInfo.Name,
[]
)
}}
/>
<div>
<span className="black-blur bg-black/50 flex w-5 justify-center rounded px-1.5 py-0.5">{setEffect.count}</span>
</div>
</div>
)
})}
</div>
</div>
<div className="w-1/3 z-10">
<div className="flex h-[650px] flex-col justify-between py-3 text-lg">
{relicStats?.map((relic, index) => {
if (!relic) return null
return (
<div key={index} className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
<div className="flex">
<NextImage src={relic?.img || ""} width={80} height={80} alt="Relic Icon" className="h-auto w-20" />
{/* <img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" /> */}
<div
className="absolute text-yellow-500 font-bold z-10"
style={{
left: '0.7rem',
bottom: '-0.5rem',
fontSize: '1.1rem',
letterSpacing: '-0.1em',
textShadow: `
0 0 0.2em #f59e0b,
0 0 0.4em #f59e0b,
0 0 0.8em #f59e0b,
-0.05em -0.05em 0.05em rgba(0,0,0,0.7),
0.05em 0.05em 0.05em rgba(0,0,0,0.7)
`
}}
>
</div>
</div>
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
<NextImage src={relic?.mainAffix?.detail?.icon || ""} width={36} height={36} alt="Main Affix Icon" className="h-auto w-9" />
<span className="text-base text-[#f1a23c]">{relic?.mainAffix?.valueAffix + relic?.mainAffix?.detail?.unit}</span>
<span className="black-blur rounded px-1 text-xs">+{relic?.mainAffix?.level}</span>
</div>
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
{relic?.subAffix?.map((subAffix, index) => {
if (!subAffix) return null
return (
<div key={index} className="flex flex-col">
<div className="flex flex-row items-center">
<NextImage src={subAffix?.detail?.icon || ""} width={36} height={36} alt="Sub Affix Icon" className="h-auto w-9" />
<span className="text-sm">+{subAffix?.valueAffix + subAffix?.detail?.unit}</span>
</div>
</div>
)
})}
</div>
</div>
)
})}
{(!relicStats || !relicStats?.length) && <div className="flex flex-col items-center justify-center ">
<span className="text-lg">{transI18n("noRelicEquipped")}</span>
</div>}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -29,8 +29,6 @@ export default function SkillsInfo() {
return mapAvatarInfo[avatarSelected.id]
}, [avatarSelected, mapAvatarInfo])
const avatarData = useMemo(() => {
if (!avatarSelected) return
return avatars[avatarSelected.id]

View File

@@ -14,117 +14,152 @@ export const listCurrentLanguageApi : Record<string, string> = {
zh: "cn"
};
export const mappingStats = <Record<string, {name: string, icon: string, unit: string}> > {
export const mappingStats = <Record<string, {name: string, icon: string, unit: string, baseStat: string}> > {
"HPDelta": {
name:"HP",
icon:"/icon/hp.webp",
unit: ""
unit: "",
baseStat: "HP"
},
"AttackDelta": {
name:"ATK",
icon:"/icon/attack.webp",
unit: ""
unit: "",
baseStat: "ATK"
},
"HPAddedRatio": {
name:"HP",
icon:"/icon/hp.webp",
unit: "%"
unit: "%",
baseStat: "HP"
},
"AttackAddedRatio": {
name:"ATK",
icon:"/icon/attack.webp",
unit: "%"
unit: "%",
baseStat: "ATK"
},
"DefenceDelta": {
name:"DEF",
icon:"/icon/defence.webp",
unit: ""
unit: "",
baseStat: "DEF"
},
"DefenceAddedRatio": {
name:"DEF",
icon:"/icon/defence.webp",
unit: "%"
unit: "%",
baseStat: "DEF"
},
"SPDDelta": {
"SpeedAddedRatio": {
name:"SPD",
icon:"/icon/spd.webp",
unit: ""
icon:"/icon/speed.webp",
unit: "%",
baseStat: "SPD"
},
"BaseSpeed": {
name:"SPD",
icon:"/icon/speed.webp",
unit: "",
baseStat: "SPD"
},
"CriticalChanceBase": {
name:"CRIT Rate",
icon:"/icon/crit-rate.webp",
unit: "%"
unit: "%",
baseStat: "CRITRate"
},
"CriticalDamageBase": {
name:"CRIT DMG",
icon:"/icon/crit-damage.webp",
unit: "%"
unit: "%",
baseStat: "CRITDmg"
},
"HealRatioBase": {
name:"Outgoing Healing Boost",
icon:"/icon/healing-boost.webp",
unit: "%"
unit: "%",
baseStat: "HealBoost"
},
"StatusProbabilityBase": {
name:"Effect Hit Rate",
icon:"/icon/effect-hit-rate.webp",
unit: "%"
unit: "%",
baseStat: "EffectHitRate"
},
"StatusResistanceBase": {
name:"Effect RES",
icon:"/icon/effect-res.webp",
unit: "%"
unit: "%",
baseStat: "EffectRES"
},
"BreakDamageAddedRatioBase": {
name:"Break Effect",
icon:"/icon/break-effect.webp",
unit: "%"
unit: "%",
baseStat: "BreakEffect"
},
"SpeedDelta": {
name:"SPD",
icon:"/icon/speed.webp",
unit: ""
unit: "",
baseStat: "SPD"
},
"PhysicalAddedRatio": {
name:"Physical DMG Boost",
icon:"/icon/physical.webp",
unit: "%"
icon:"/icon/physical-add.webp",
unit: "%",
baseStat: "PhysicalAdd"
},
"FireAddedRatio": {
name:"Fire DMG Boost",
icon:"/icon/fire.webp",
unit: "%"
icon:"/icon/fire-add.webp",
unit: "%",
baseStat: "FireAdd"
},
"IceAddedRatio": {
name:"Ice DMG Boost",
icon:"/icon/ice.webp",
unit: "%"
icon:"/icon/ice-add.webp",
unit: "%",
baseStat: "IceAdd"
},
"ThunderAddedRatio": {
name:"Thunder DMG Boost",
icon:"/icon/thunder.webp",
unit: "%"
icon:"/icon/thunder-add.webp",
unit: "%",
baseStat: "ThunderAdd"
},
"WindAddedRatio": {
name:"Wind DMG Boost",
icon:"/icon/wind.webp",
unit: "%"
icon:"/icon/wind-add.webp",
unit: "%",
baseStat: "WindAdd"
},
"QuantumAddedRatio": {
name:"Quantum DMG Boost",
icon:"/icon/quantum.webp",
unit: "%"
icon:"/icon/quantum-add.webp",
unit: "%",
baseStat: "QuantumAdd"
},
"ImaginaryAddedRatio": {
name:"Imaginary DMG Boost",
icon:"/icon/imaginary.webp",
unit: "%"
icon:"/icon/imaginary-add.webp",
unit: "%",
baseStat: "ImaginaryAdd"
},
"SPRatioBase": {
name:"Energy Regeneration Rate",
icon:"/icon/energy-rate.webp",
unit: "%"
unit: "%",
baseStat: "EnergyRate"
}
}
export const ratioStats = [
"HPAddedRatio",
"AttackAddedRatio",
"DefenceAddedRatio",
"SpeedAddedRatio",
]

View File

@@ -235,4 +235,779 @@ export const traceLink : Record<string, Record<string, string[]>> = {
Point07: ["Point14", "Point15"],
Point06: ["Point12", "Point13"],
}
}
export const traceShowCaseMap : Record<string, Record<string, { id: string, size: string, isLink: boolean }[]>> = {
Knight: {
"1": [
{
id: "Point01",
size: "big",
isLink: false
},
{
id: "Point15",
size: "small",
isLink: true
},
{
id: "Point12",
size: "small",
isLink: true
},
{
id: "Point09",
size: "small",
isLink: true
}
],
"2": [
{
id: "Point02",
size: "big",
isLink: false
},
{
id: "Point06",
size: "medium",
isLink: true
},
{
id: "Point10",
size: "small",
isLink: true
},
{
id: "Point11",
size: "small",
isLink: true
}
],
"3": [
{
id: "Point03",
size: "big",
isLink: false
},
{
id: "Point07",
size: "medium",
isLink: true
},
{
id: "Point13",
size: "small",
isLink: true
},
{
id: "Point14",
size: "small",
isLink: true
}
],
"4": [
{
id: "Point04",
size: "big",
isLink: false
},
{
id: "Point08",
size: "medium",
isLink: true
},
{
id: "Point16",
size: "small",
isLink: true
},
{
id: "Point18",
size: "small",
isLink: true
},
{
id: "Point17",
size: "small",
isLink: true
}
]
},
Mage: {
"1": [
{
id: "Point01",
size: "big",
isLink: false
},
{
id: "Point09",
size: "small",
isLink: true
},
{
id: "Point18",
size: "small",
isLink: true
}
],
"2": [
{
id: "Point02",
size: "big",
isLink: false
},
{
id: "Point06",
size: "medium",
isLink: true
},
{
id: "Point10",
size: "small",
isLink: true
},
{
id: "Point11",
size: "small",
isLink: true
},
{
id: "Point12",
size: "small",
isLink: true
}
],
"3": [
{
id: "Point03",
size: "big",
isLink: false
},
{
id: "Point07",
size: "medium",
isLink: true
},
{
id: "Point13",
size: "small",
isLink: true
},
{
id: "Point14",
size: "small",
isLink: true
},
{
id: "Point15",
size: "small",
isLink: true
}
],
"4": [
{
id: "Point04",
size: "big",
isLink: false
},
{
id: "Point08",
size: "medium",
isLink: true
},
{
id: "Point17",
size: "small",
isLink: true
},
{
id: "Point16",
size: "small",
isLink: true
}
]
},
Priest: {
"1": [
{
id: "Point01",
size: "big",
isLink: false
},
{
id: "Point09",
size: "small",
isLink: true
},
{
id: "Point18",
size: "small",
isLink: true
}
],
"2": [
{
id: "Point02",
size: "big",
isLink: false
},
{
id: "Point06",
size: "medium",
isLink: true
},
{
id: "Point10",
size: "small",
isLink: true
},
{
id: "Point11",
size: "small",
isLink: true
},
{
id: "Point12",
size: "small",
isLink: true
}
],
"3": [
{
id: "Point03",
size: "big",
isLink: false
},
{
id: "Point07",
size: "medium",
isLink: true
},
{
id: "Point13",
size: "small",
isLink: true
},
{
id: "Point14",
size: "small",
isLink: true
},
{
id: "Point15",
size: "small",
isLink: true
}
],
"4": [
{
id: "Point04",
size: "big",
isLink: false
},
{
id: "Point08",
size: "medium",
isLink: true
},
{
id: "Point17",
size: "small",
isLink: true
},
{
id: "Point16",
size: "small",
isLink: true
}
]
},
Rogue: {
"1": [
{
id: "Point01",
size: "big",
isLink: false
},
{
id: "Point15",
size: "small",
isLink: true
},
{
id: "Point12",
size: "small",
isLink: true
},
{
id: "Point09",
size: "small",
isLink: true
}
],
"2": [
{
id: "Point02",
size: "big",
isLink: false
},
{
id: "Point06",
size: "medium",
isLink: true
},
{
id: "Point10",
size: "small",
isLink: true
},
{
id: "Point11",
size: "small",
isLink: true
}
],
"3": [
{
id: "Point03",
size: "big",
isLink: false
},
{
id: "Point07",
size: "medium",
isLink: true
},
{
id: "Point13",
size: "small",
isLink: true
},
{
id: "Point14",
size: "small",
isLink: true
}
],
"4": [
{
id: "Point04",
size: "big",
isLink: false
},
{
id: "Point08",
size: "medium",
isLink: true
},
{
id: "Point16",
size: "small",
isLink: true
},
{
id: "Point18",
size: "small",
isLink: true
},
{
id: "Point17",
size: "small",
isLink: true
}
]
},
Shaman: {
"1": [
{
id: "Point01",
size: "big",
isLink: false
},
{
id: "Point15",
size: "small",
isLink: true
},
{
id: "Point12",
size: "small",
isLink: true
},
{
id: "Point09",
size: "small",
isLink: true
}
],
"2": [
{
id: "Point02",
size: "big",
isLink: false
},
{
id: "Point06",
size: "medium",
isLink: true
},
{
id: "Point10",
size: "small",
isLink: true
},
{
id: "Point11",
size: "small",
isLink: true
}
],
"3": [
{
id: "Point03",
size: "big",
isLink: false
},
{
id: "Point07",
size: "medium",
isLink: true
},
{
id: "Point13",
size: "small",
isLink: true
},
{
id: "Point14",
size: "small",
isLink: true
}
],
"4": [
{
id: "Point04",
size: "big",
isLink: false
},
{
id: "Point08",
size: "medium",
isLink: true
},
{
id: "Point16",
size: "small",
isLink: true
},
{
id: "Point18",
size: "small",
isLink: true
},
{
id: "Point17",
size: "small",
isLink: true
}
]
},
Warlock: {
"1": [
{
id: "Point01",
size: "big",
isLink: false
},
{
id: "Point09",
size: "small",
isLink: true
},
{
id: "Point18",
size: "small",
isLink: true
}
],
"2": [
{
id: "Point02",
size: "big",
isLink: false
},
{
id: "Point06",
size: "medium",
isLink: true
},
{
id: "Point10",
size: "small",
isLink: true
},
{
id: "Point11",
size: "small",
isLink: true
},
{
id: "Point12",
size: "small",
isLink: true
}
],
"3": [
{
id: "Point03",
size: "big",
isLink: false
},
{
id: "Point07",
size: "medium",
isLink: true
},
{
id: "Point13",
size: "small",
isLink: true
},
{
id: "Point14",
size: "small",
isLink: true
},
{
id: "Point15",
size: "small",
isLink: true
}
],
"4": [
{
id: "Point04",
size: "big",
isLink: false
},
{
id: "Point08",
size: "medium",
isLink: true
},
{
id: "Point17",
size: "small",
isLink: true
},
{
id: "Point16",
size: "small",
isLink: true
}
]
},
Warrior: {
"1": [
{
id: "Point01",
size: "big",
isLink: false
},
{
id: "Point09",
size: "small",
isLink: true
}
],
"2": [
{
id: "Point02",
size: "big",
isLink: false
},
{
id: "Point06",
size: "medium",
isLink: true
},
{
id: "Point10",
size: "small",
isLink: true
},
{
id: "Point11",
size: "small",
isLink: true
},
{
id: "Point12",
size: "small",
isLink: true
}
],
"3": [
{
id: "Point03",
size: "big",
isLink: false
},
{
id: "Point07",
size: "medium",
isLink: true
},
{
id: "Point13",
size: "small",
isLink: true
},
{
id: "Point14",
size: "small",
isLink: true
},
{
id: "Point15",
size: "small",
isLink: true
}
],
"4": [
{
id: "Point04",
size: "big",
isLink: false
},
{
id: "Point08",
size: "medium",
isLink: true
},
{
id: "Point16",
size: "small",
isLink: true
},
{
id: "Point18",
size: "small",
isLink: true
},
{
id: "Point17",
size: "small",
isLink: true
}
]
},
Memory: {
"1": [
{
id: "Point01",
size: "big",
isLink: false
},
{
id: "Point09",
size: "small",
isLink: true
},
{
id: "Point10",
size: "small",
isLink: true
},
{
id: "Point11",
size: "small",
isLink: true
}
],
"2": [
{
id: "Point02",
size: "big",
isLink: false
},
{
id: "Point06",
size: "medium",
isLink: true
},
{
id: "Point13",
size: "small",
isLink: true
},
{
id: "Point12",
size: "small",
isLink: true
}
],
"3": [
{
id: "Point03",
size: "big",
isLink: false
},
{
id: "Point07",
size: "medium",
isLink: true
},
{
id: "Point14",
size: "small",
isLink: true
},
{
id: "Point15",
size: "small",
isLink: true
}
],
"4": [
{
id: "Point04",
size: "big",
isLink: false
},
{
id: "Point08",
size: "medium",
isLink: true
},
{
id: "Point16",
size: "small",
isLink: true
},
{
id: "Point17",
size: "small",
isLink: true
},
{
id: "Point18",
size: "small",
isLink: true
}
],
"5": [
{
id: "Point19",
size: "big-memory",
isLink: false
},
{
id: "Point20",
size: "big-memory",
isLink: false
}
]
}
}

View File

@@ -1,4 +1,4 @@
import { mappingStats } from "@/constant/constant"
import { mappingStats, ratioStats } from "@/constant/constant"
import { AffixDetail } from "@/types"
export function calcPromotion(level: number) {
@@ -37,14 +37,64 @@ export function calcRarity(rarity: string) {
return 1
}
export const calcAffixBonus = (affix: AffixDetail, stepCount: number, rollCount: number) => {
const data = affix;
if (!data) return 0;
if (mappingStats?.[data.property].unit === "%") {
return ((data.base * rollCount + data.step * stepCount) * 100).toFixed(1);
export function calcMainAffixBonus(affix?: AffixDetail, level?: number) {
if (!affix || typeof level !== "number") return "0"
const value = affix.base + affix.step * level;
if (mappingStats?.[affix.property].unit === "%") {
return (value * 100).toFixed(1);
}
if (mappingStats?.[data.property].name === "SPD") {
return (data.base * rollCount + data.step * stepCount).toFixed(1);
if (mappingStats?.[affix.property].name === "SPD") {
return value.toFixed(1);
}
return (data.base * rollCount + data.step * stepCount).toFixed(0);
return value.toFixed(0);
}
export const calcAffixBonus = (affix?: AffixDetail, stepCount?: number, rollCount?: number) => {
if (!affix || typeof stepCount !== "number" || typeof rollCount !== "number") return "0"
if (mappingStats?.[affix.property].unit === "%") {
return ((affix.base * rollCount + affix.step * stepCount) * 100).toFixed(1);
}
if (mappingStats?.[affix.property].name === "SPD") {
return (affix.base * rollCount + affix.step * stepCount).toFixed(1);
}
return (affix.base * rollCount + affix.step * stepCount).toFixed(0);
}
export const calcBaseStat = (baseStat: number, stepStat: number, roundFixed: number, level: number) => {
const promotionStat = baseStat + stepStat * (level-1);
return promotionStat.toFixed(roundFixed);
}
export const calcBaseStatRaw = (baseStat?: number, stepStat?: number, level?: number) => {
if (typeof baseStat !== "number" || typeof stepStat !== "number" || typeof level !== "number") return 0
return baseStat + stepStat * (level-1);
}
export const calcSubAffixBonusRaw = (affix?: AffixDetail, stepCount?: number, rollCount?: number, baseStat?: number) => {
if (!affix || typeof stepCount !== "number" || typeof rollCount !== "number" || typeof baseStat !== "number") return 0
if (ratioStats.includes(affix.property)) {
return (affix.base * rollCount + affix.step * stepCount) * baseStat;
}
return affix.base * rollCount + affix.step * stepCount;
}
export const calcMainAffixBonusRaw = (affix?: AffixDetail, level?: number, baseStat?: number) => {
if (!affix || typeof level !== "number" || typeof baseStat !== "number") return 0
const value = affix.base + affix.step * level;
if (ratioStats.includes(affix.property)) {
return baseStat * value
}
return value
}
export const calcBonusStatRaw = (affix?: string, baseStat?: number, bonusValue?: number) => {
if (!affix || typeof baseStat !== "number" || typeof bonusValue !== "number") return 0
if (ratioStats.includes(affix)) {
return baseStat * bonusValue
}
return bonusValue
}

View File

@@ -123,4 +123,20 @@ export function convertMonster(id: string, item: MonsterBasicRaw): MonsterBasic
};
return result;
}
}
export function convertToRoman(num: number): string {
const roman: [number, string][] = [
[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'],
[100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'],
[10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']
];
let result = '';
for (const [val, sym] of roman) {
while (num >= val) {
result += sym;
num -= val;
}
}
return result;
}

View File

@@ -6,7 +6,7 @@ import { getCharacterInfoApi } from '../api';
const DATA_DIR = path.join(process.cwd(), 'data');
const characterFileCache: Record<string, Record<string, CharacterDetail>> = {};
export let characterMap: Record<string, CharacterDetail> = {};
export let rankIconMap: Record<string, string[]> = {};
function getJsonFilePath(locale: string): string {
return path.join(DATA_DIR, `characters.${locale}.json`);
}
@@ -15,11 +15,20 @@ function loadFromFileIfExists(locale: string): Record<string, CharacterDetail> |
if (characterFileCache[locale]) return characterFileCache[locale];
const filePath = getJsonFilePath(locale);
const fileRankIconPath = path.join(DATA_DIR, `rank_icon.json`);
if (fs.existsSync(fileRankIconPath)) {
const data = JSON.parse(fs.readFileSync(fileRankIconPath, 'utf-8')) as Record<string, string[]>;
rankIconMap = data;
}
if (fs.existsSync(filePath)) {
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, CharacterDetail>;
Object.keys(data).forEach((key) => {
data[key].RankIcon = rankIconMap[key] || [];
});
characterFileCache[locale] = data;
return data;
}
return null;
}
@@ -37,7 +46,10 @@ export async function loadCharacters(charIds: string[], locale: string): Promise
await Promise.all(
charIds.map(async id => {
const info = await getCharacterInfoApi(Number(id), locale);
if (info) result[id] = info;
if (info){
info.RankIcon = rankIconMap[id] || [];
result[id] = info;
}
})
);

View File

@@ -6,6 +6,7 @@ import { LightConeDetail } from '@/types';
const DATA_DIR = path.join(process.cwd(), 'data');
const lightconeFileCache: Record<string, Record<string, LightConeDetail>> = {};
export let lightconeMap: Record<string, LightConeDetail> = {};
export let lightconeBonusMap: Record<string, Record<string, { type: string, value: number }[]>> = {};
function getJsonFilePath(locale: string): string {
return path.join(DATA_DIR, `lightcones.${locale}.json`);
@@ -15,9 +16,18 @@ function loadLightconeFromFileIfExists(locale: string): Record<string, LightCone
if (lightconeFileCache[locale]) return lightconeFileCache[locale];
const filePath = getJsonFilePath(locale);
const fileBonusPath = path.join(DATA_DIR, `lightcone_bonus.json`);
if (fs.existsSync(fileBonusPath)) {
const data = JSON.parse(fs.readFileSync(fileBonusPath, 'utf-8')) as Record<string, Record<string, { type: string, value: number }[]>>;
lightconeBonusMap = data;
}
if (fs.existsSync(filePath)) {
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, LightConeDetail>;
Object.keys(data).forEach((key) => {
data[key].Bonus = lightconeBonusMap[key] || {};
});
lightconeFileCache[locale] = data;
return data;
}
return null;
@@ -37,7 +47,10 @@ export async function loadLightcones(charIds: string[], locale: string): Promise
await Promise.all(
charIds.map(async id => {
const info = await getLightconeInfoApi(Number(id), locale);
if (info) result[id] = info;
if (info) {
info.Bonus = lightconeBonusMap[id] || {};
result[id] = info;
}
})
);

View File

@@ -6,7 +6,7 @@ import { getRelicInfoApi } from '../api';
const DATA_DIR = path.join(process.cwd(), 'data');
const relicFileCache: Record<string, Record<string, RelicDetail>> = {};
export let relicMap: Record<string, RelicDetail> = {};
export let relicBonusMap: Record<string, Record<string, { type: string, value: number }[]>> = {};
function getJsonFilePath(locale: string): string {
return path.join(DATA_DIR, `relics.${locale}.json`);
}
@@ -15,8 +15,16 @@ function loadRelicFromFileIfExists(locale: string): Record<string, RelicDetail>
if (relicFileCache[locale]) return relicFileCache[locale];
const filePath = getJsonFilePath(locale);
const fileBonusPath = path.join(DATA_DIR, `relic_bonus.json`);
if (fs.existsSync(fileBonusPath)) {
const data = JSON.parse(fs.readFileSync(fileBonusPath, 'utf-8')) as Record<string, Record<string, { type: string, value: number }[]>>;
relicBonusMap = data;
}
if (fs.existsSync(filePath)) {
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, RelicDetail>;
Object.keys(data).forEach((key) => {
data[key].Bonus = relicBonusMap[key] || {};
});
relicFileCache[locale] = data;
return data;
}
@@ -37,7 +45,10 @@ export async function loadRelics(charIds: string[], locale: string): Promise<Rec
await Promise.all(
charIds.map(async id => {
const info = await getRelicInfoApi(Number(id), locale);
if (info) result[id] = info;
if (info) {
info.Bonus = relicBonusMap[id] || {};
result[id] = info;
}
})
);

View File

@@ -5,7 +5,9 @@ interface LightconeState {
listLightcone: LightConeBasic[];
listRawLightcone: LightConeBasic[];
filter: FilterLightconeType;
defaultFilter: { path: string[], rarity: string[] };
mapLightconeInfo: Record<string, LightConeDetail>;
setDefaultFilter: (newDefaultFilter: { path: string[], rarity: string[] }) => void;
setListLightcone: (newListLightcone: LightConeBasic[]) => void;
setFilter: (newFilter: FilterLightconeType) => void;
setMapLightconeInfo: (lightconeId: string, newLightcone: LightConeDetail) => void;
@@ -22,6 +24,8 @@ const useLightconeStore = create<LightconeState>((set, get) => ({
locale: "",
rarity: [],
},
defaultFilter: { path: [], rarity: [] },
setDefaultFilter: (newDefaultFilter: { path: string[], rarity: string[] }) => set({ defaultFilter: newDefaultFilter }),
setListLightcone: (newListLightcone: LightConeBasic[]) => set({ listLightcone: newListLightcone, listRawLightcone: newListLightcone }),
setFilter: (newFilter: FilterLightconeType) => {
set({ filter: newFilter })

View File

@@ -16,6 +16,7 @@ export interface CharacterDetail {
Stats: Record<string, Stat>;
Relics: Relics;
Enhanced: Record<string, EnhancedType>;
RankIcon: string[];
}
export interface EnhancedType {

View File

@@ -5,6 +5,7 @@ export interface LightConeDetail {
BaseType: string;
Refinements: RefinementDetail;
Stats: StatEntryDetail[];
Bonus: Record<string, { type: string, value: number }[]>
}
interface RefinementDetail {

View File

@@ -14,5 +14,6 @@ export interface RelicDetail {
Icon: string;
Parts: Record<string, PartData>;
RequireNum: Record<string, RequireBonus>;
Bonus: Record<string, { type: string, value: number }[]>
}