"use client"; import { useEffect, useState, useRef, useMemo, useCallback } from 'react'; 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 { mappingStats } from '@/constant/constant'; import { useTranslations } from 'next-intl'; import { toast } from 'react-toastify'; import RelicShowcase from './relicShowcase'; import useDetailDataStore from '@/stores/detailDataStore'; import useCurrentDataStore from '@/stores/currentDataStore'; import { getLocaleName } from '@/helper'; export default function ShowCaseInfo() { const { avatarSelected } = useCurrentDataStore() const { mainAffix, subAffix, mapRelicSet, mapAvatar, mapLightCone, baseType, damageType } = useDetailDataStore() const { avatars } = useUserDataStore() const [avgColor, setAvgColor] = useState('#222'); const imgRef = useRef(null); const cardRef = useRef(null) const { locale } = useLocaleStore() const transI18n = useTranslations("DataPage") 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(() => { toast.error("Error generating showcase card!"); }); }, [avatarSelected, locale, transI18n]); useEffect(() => { if (!avatarSelected) return; const fac = new FastAverageColor(); const img = new Image(); img.crossOrigin = 'anonymous'; img.src = `${process.env.CDN_URL}/${avatarSelected?.Image?.AvatarCutinFrontImgPath}`; img.onload = () => { fac.getColorAsync(img) .then((color) => { setAvgColor(color.hex); }) .catch(e => console.error("Error:", e)); }; return () => { fac.destroy(); img.onload = null; }; }, [avatarSelected]); const avatarSkillTree = useMemo(() => { if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {} if (avatars[avatarSelected?.ID?.toString()].enhanced) { return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {} } return avatarSelected?.SkillTrees || {} }, [avatarSelected, avatars]) const avatarData = useMemo(() => { if (!avatarSelected) return return avatars[avatarSelected?.ID?.toString()] }, [avatarSelected, avatars]) const avatarProfile = useMemo(() => { if (!avatarSelected || !avatarData) return return avatarData?.profileList?.[avatarData?.profileSelect] }, [avatarSelected, avatarData]) const lightconeStats = useMemo(() => { if (!avatarSelected || !avatarProfile?.lightcone || !mapLightCone[avatarProfile?.lightcone?.item_id]) return const promotion = calcPromotion(avatarProfile?.lightcone?.level) const atkStat = calcBaseStat( mapLightCone[avatarProfile?.lightcone?.item_id]?.Stats?.[promotion]?.BaseAttack, mapLightCone[avatarProfile?.lightcone?.item_id]?.Stats?.[promotion]?.BaseAttackAdd, 0, avatarProfile?.lightcone?.level ) const hpStat = calcBaseStat( mapLightCone[avatarProfile?.lightcone?.item_id]?.Stats?.[promotion]?.BaseHP, mapLightCone[avatarProfile?.lightcone?.item_id]?.Stats?.[promotion]?.BaseHPAdd, 0, avatarProfile?.lightcone?.level ) const defStat = calcBaseStat( mapLightCone[avatarProfile?.lightcone?.item_id]?.Stats?.[promotion]?.BaseDefence, mapLightCone[avatarProfile?.lightcone?.item_id]?.Stats?.[promotion]?.BaseDefenceAdd, 0, avatarProfile?.lightcone?.level ) return { attack: atkStat, hp: hpStat, def: defStat, } }, [avatarSelected, mapLightCone, avatarProfile]) const relicEffects = useMemo(() => { const avatar = avatars[avatarSelected?.ID?.toString() || ""]; 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 || !mainAffix || !subAffix) return return Object.entries(avatarProfile?.relics).map(([key, value]) => { const mainAffixMap = mainAffix["5" + key] const subAffixMap = subAffix["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, mainAffix, subAffix]) const totalSubStats = useMemo(() => { if (!relicStats?.length) return 0 return (relicStats ?? []).reduce((acc, relic) => { const subAffixList = relic?.subAffix ?? [] return acc + subAffixList.reduce((subAcc, subAffix) => { if (avatarSelected?.Relics?.SubAffixPropertyList.findIndex(it => it === subAffix.property) !== -1) { return subAcc + (subAffix?.count ?? 0) } return subAcc }, 0) }, 0) }, [relicStats, avatarSelected]) const characterStats = useMemo(() => { if (!avatarSelected || !avatarData) return const charPromotion = calcPromotion(avatarData.level) const statsData: Record = { HP: { value: calcBaseStatRaw( mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase, mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd, avatarData.level ), base: calcBaseStatRaw( mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase, mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd, avatarData.level ), name: "HP", icon: "spriteoutput/ui/avatar/icon/IconMaxHP.png", unit: "", round: 0 }, ATK: { value: calcBaseStatRaw( mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase, mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd, avatarData.level ), base: calcBaseStatRaw( mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase, mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd, avatarData.level ), name: "ATK", icon: "spriteoutput/ui/avatar/icon/IconAttack.png", unit: "", round: 0 }, DEF: { value: calcBaseStatRaw( mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase, mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd, avatarData.level ), base: calcBaseStatRaw( mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase, mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd, avatarData.level ), name: "DEF", icon: "spriteoutput/ui/avatar/icon/IconDefence.png", unit: "", round: 0 }, SPD: { value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0, base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0, name: "SPD", icon: "spriteoutput/ui/avatar/icon/IconSpeed.png", unit: "", round: 1 }, CRITRate: { value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0, base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0, name: "CRIT Rate", icon: "spriteoutput/ui/avatar/icon/IconCriticalChance.png", unit: "%", round: 1 }, CRITDmg: { value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0, base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0, name: "CRIT DMG", icon: "spriteoutput/ui/avatar/icon/IconCriticalDamage.png", unit: "%", round: 1 }, BreakEffect: { value: 0, base: 0, name: "Break Effect", icon: "spriteoutput/ui/avatar/icon/IconBreakUp.png", unit: "%", round: 1 }, EffectRES: { value: 0, base: 0, name: "Effect RES", icon: "spriteoutput/ui/avatar/icon/IconStatusResistance.png", unit: "%", round: 1 }, EnergyRate: { value: 0, base: 0, name: "Energy Rate", icon: "spriteoutput/ui/avatar/icon/IconEnergyRecovery.png", unit: "%", round: 1 }, EffectHitRate: { value: 0, base: 0, name: "Effect Hit Rate", icon: "spriteoutput/ui/avatar/icon/IconStatusProbability.png", unit: "%", round: 1 }, HealBoost: { value: 0, base: 0, name: "Healing Boost", icon: "spriteoutput/ui/avatar/icon/IconHealRatio.png", unit: "%", round: 1 }, PhysicalAdd: { value: 0, base: 0, name: "Physical Boost", icon: "spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png", unit: "%", round: 1 }, FireAdd: { value: 0, base: 0, name: "Fire Boost", icon: "spriteoutput/ui/avatar/icon/IconFireAddedRatio.png", unit: "%", round: 1 }, IceAdd: { value: 0, base: 0, name: "Ice Boost", icon: "spriteoutput/ui/avatar/icon/IconIceAddedRatio.png", unit: "%", round: 1 }, ThunderAdd: { value: 0, base: 0, name: "Thunder Boost", icon: "spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png", unit: "%", round: 1 }, WindAdd: { value: 0, base: 0, name: "Wind Boost", icon: "spriteoutput/ui/avatar/icon/IconWindAddedRatio.png", unit: "%", round: 1 }, QuantumAdd: { value: 0, base: 0, name: "Quantum Boost", icon: "spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png", unit: "%", round: 1 }, ImaginaryAdd: { value: 0, base: 0, name: "Imaginary Boost", icon: "spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png", unit: "%", round: 1 }, ElationAdd: { value: 0, base: 0, name: "Elation Boost", icon: "spriteoutput/ui/avatar/icon/IconJoy.png", unit: "%", round: 1 } } if (avatarProfile?.lightcone && mapLightCone[avatarProfile?.lightcone?.item_id]) { const lightconePromotion = calcPromotion(avatarProfile?.lightcone?.level) statsData.HP.value += calcBaseStatRaw( mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHP, mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd, avatarProfile?.lightcone?.level ) statsData.HP.base += calcBaseStatRaw( mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHP, mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd, avatarProfile?.lightcone?.level ) statsData.ATK.value += calcBaseStatRaw( mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseAttack, mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseAttackAdd, avatarProfile?.lightcone?.level ) statsData.ATK.base += calcBaseStatRaw( mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseAttack, mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseAttackAdd, avatarProfile?.lightcone?.level ) statsData.DEF.value += calcBaseStatRaw( mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseDefence, mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseDefenceAdd, avatarProfile?.lightcone?.level ) statsData.DEF.base += calcBaseStatRaw( mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseDefence, mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseDefenceAdd, avatarProfile?.lightcone?.level ) const bonusData = mapLightCone[avatarProfile?.lightcone?.item_id]?.Skills?.Level?.[avatarProfile?.lightcone.rank]?.Bonus if (bonusData && bonusData.length > 0) { const bonusSpd = bonusData.filter((bonus) => bonus.PropertyType === "BaseSpeed") const bonusOther = bonusData.filter((bonus) => bonus.PropertyType !== "BaseSpeed") bonusSpd.forEach((bonus) => { statsData.SPD.value += bonus.Value statsData.SPD.base += bonus.Value }) bonusOther.forEach((bonus) => { const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat if (statsBase && statsData[statsBase]) { statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, 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 && mainAffix && subAffix) { Object.entries(avatarProfile?.relics).forEach(([key, value]) => { const mainAffixMap = mainAffix["5" + key] const subAffixMap = subAffix["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 = mapRelicSet?.[relic.key]?.Skills if (!dataBonus || Object.keys(dataBonus).length === 0) return Object.entries(dataBonus || {}).forEach(([key, value]) => { if (relic.count < Number(key)) return value.Bonus.forEach((bonus) => { const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat if (statsBase && statsData[statsBase]) { statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value) } }) }) }) } return statsData }, [ avatarSelected, avatarData, mapAvatar, avatarProfile?.lightcone, avatarProfile?.relics, mapLightCone, mainAffix, subAffix, relicEffects, mapRelicSet, 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")}`; }, []) return (
{avatarSelected && ( )}
{avatarSelected && avatarSelected && avatarData?.data && typeof avatarData?.data?.rank === "number" && (
{Object.values(avatarSelected?.Ranks || {})?.map((rank, index) => { const isActive = avatarData?.data?.rank > index; return (
{isActive && (
)}
); })}
)}
Lv. {avatarData?.level}/80
{totalSubStats} {avatarSelected && (
)}
{avatarSelected && (
)}
{avatarData && avatarSelected && 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 (
{(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( mapLightCone[avatarProfile?.lightcone?.item_id]?.Rarity?.[ mapLightCone[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 = mapRelicSet[setEffect.key]; if (!relicInfo) return null; return (
{setEffect.count}
) })}
{relicStats?.map((relic, index) => { if (!relic || !avatarSelected) return null return ( ) })} {(!relicStats || !relicStats?.length) && (
{transI18n("noRelicEquipped")}
)}
); }