"use client"; import { useEffect, useState, useRef, useMemo, useCallback } from 'react'; import useAvatarStore from "@/stores/avatarStore"; import { FastAverageColor } 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 RelicShowcase from './relicShowcase'; export default function ShowCaseInfo() { const { avatarSelected, mapAvatarInfo } = useAvatarStore() 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 || !avatarSelected) { toast.error("Avatar showcase not found!"); return; } import("html2canvas-pro") .then(({ default: html2canvas }) => html2canvas(cardRef.current!, { scale: 2, backgroundColor: "#000000", allowTaint: false, useCORS: true }) ) .then((canvas: HTMLCanvasElement) => { const link = document.createElement("a"); link.download = `${getNameChar(locale, transI18n, avatarSelected)}_showcase.png`; link.href = canvas.toDataURL("image/png"); link.click(); }) .catch((e) => { console.log(e) toast.error("Error generating showcase card!"); }); }, [avatarSelected, locale, transI18n]); useEffect(() => { if (!avatarSelected?.id) return; const fac = new FastAverageColor(); const img = new Image(); img.crossOrigin = 'anonymous'; img.src = `${process.env.CDN_URL}/spriteoutput/avatardrawcard/${avatarSelected?.id}.png`; img.onload = () => { fac.getColorAsync(img) .then((color) => { setAvgColor(color.hex); }) .catch(e => console.error("Vẫn lỗi CORS:", e)); }; return () => { fac.destroy(); img.onload = null; }; }, [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: `${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${value.relic_set_id}_${key}.png`, 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], step: subValue?.step, count: subValue?.count } }) } }) }, [avatarSelected, avatarProfile, mapMainAffix, mapSubAffix]) const totalSubStats = useMemo(() => { if (!relicStats?.length) return 0 return (relicStats ?? []).reduce((acc, relic) => { const subAffixList = relic?.subAffix ?? [] return acc + subAffixList.reduce((subAcc, subAffix) => { if (avatarInfo?.Relics?.SubAffixPropertyList.findIndex(it => it === subAffix.property) !== -1) { return subAcc + (subAffix?.count ?? 0) } return subAcc }, 0) }, 0) }, [relicStats, avatarInfo]) const characterStats = useMemo(() => { if (!avatarSelected || !avatarData) return const charPromotion = calcPromotion(avatarData.level) const statsData: Record = { 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 }, ElationAdd: { value: 0, base: 0, name: "Elation Boost", icon: "/icon/IconJoy.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 applyBrightness = useCallback((hex: string, brightness: number): string => { const r = Math.round(parseInt(hex.slice(1, 3), 16) * brightness); const g = Math.round(parseInt(hex.slice(3, 5), 16) * brightness); const b = Math.round(parseInt(hex.slice(5, 7), 16) * brightness); return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`; }, []) const getImageSkill = useCallback((icon: string | undefined, status: StatusAddType | undefined) => { if (!icon) return if (icon.startsWith("SkillIcon")) { return `${process.env.CDN_URL}/spriteoutput/skillicons/avatar/${avatarSelected?.id}/${icon}` } else if (status && mappingStats[status.PropertyType]) { return mappingStats[status.PropertyType].icon } else if (icon.startsWith("Icon")) { return `${process.env.CDN_URL}/spriteoutput/trace/${icon}` } return "" }, [avatarSelected?.id]) return (
{avatarSelected && ( )}
{avatarSelected && avatarInfo && avatarData?.data && typeof avatarData?.data?.rank === "number" && (
{avatarInfo?.RankIcon?.map((src, index) => { const isActive = avatarData?.data?.rank > index; return (
{isActive && (
)}
); })}
)}
Lv. {avatarData?.level}/80
{totalSubStats} {avatarSelected && (
)}
{avatarSelected && (
)}
{avatarData && avatarInfo && avatarSkillTree && traceShowCaseMap[avatarSelected?.baseType || ""] && Object.values(traceShowCaseMap[avatarSelected?.baseType || ""] || []).map((item, index) => { return (
{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" : ""; if (!avatarSkillTree?.[btn.id]) { return null; } return (
{ (() => { const skillImg = getImageSkill( avatarInfo.SkillTrees?.[btn.id]?.["1"]?.Icon, avatarSkillTree?.[btn.id]?.["1"]?.StatusAddList[0] ); return skillImg ? ( ) : null; })() } {(isBig || isBigMemory) && ( {avatarData?.data?.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID] ? avatarData?.data?.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID] : 1} )}
{btn.isLink && idx < item.length - 1 && (
)}
); })}
) })}
{avatarProfile && avatarProfile?.lightcone && lightconeStats ? (
{/* Background SVG Border (offset top-left) */} {/* Card Image */} {/* Top SVG Border (offset bottom-right) */} {/* Stars */}
{[...Array( Number( mapLightconeInfo[avatarProfile?.lightcone?.item_id]?.Rarity?.[ mapLightconeInfo[avatarProfile?.lightcone?.item_id]?.Rarity.length - 1 ] || 0 ) )].map((_, i) => ( ))}
{convertToRoman(avatarProfile?.lightcone?.rank)}
Lv. {avatarProfile.lightcone.level}/80
{ lightconeStats?.hp }
{lightconeStats?.attack}
{lightconeStats?.def}
) : (
{transI18n("noLightconeEquipped")}
)}
{Object.entries(characterStats || {})?.map(([key, stat], index) => { if (!stat || (key.includes("Add") && stat.value === 0)) return null return (
{stat.name}
{ stat.value ? stat.unit === "%" ? (stat.value * 100).toFixed(stat.round) : stat.value.toFixed(stat.round) : 0 }{stat.unit}
) })}
{relicEffects.map((setEffect, index) => { const relicInfo = mapRelicInfo[setEffect.key]; if (!relicInfo) return null; return (
{setEffect.count}
) })}
{relicStats?.map((relic, index) => { if (!relic || !avatarInfo) return null return ( ) })} {(!relicStats || !relicStats?.length) && (
{transI18n("noRelicEquipped")}
)}
); }