UPDATE: New cdn, assets
All checks were successful
Gitea Auto Deploy / Deploy-Container (push) Successful in 39s
@@ -105,6 +105,7 @@
|
||||
"level": "等级",
|
||||
"relics": "遗器",
|
||||
"eidolons": "星魂",
|
||||
"lightcones": "光锥"
|
||||
"lightcones": "光锥",
|
||||
"trailblazer": "开拓者"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,15 +55,15 @@
|
||||
"noCharactersInLineup": "No characters in lineup",
|
||||
"noTurns": "No turns yet",
|
||||
"type": "Type",
|
||||
"warrior": "Destruction",
|
||||
"knight": "Preservation",
|
||||
"mage": "Erudition",
|
||||
"priest": "Abundance",
|
||||
"warrior": "The Destruction",
|
||||
"knight": "The Preservation",
|
||||
"mage": "The Erudition",
|
||||
"priest": "The Abundance",
|
||||
"rogue": "The Hunt",
|
||||
"shaman": "Harmony",
|
||||
"warlock": "Nihility",
|
||||
"memory": "Remembrance",
|
||||
"elation": "Elation",
|
||||
"shaman": "The Harmony",
|
||||
"warlock": "The Nihility",
|
||||
"memory": "The Remembrance",
|
||||
"elation": "The Elation",
|
||||
"fire": "Fire",
|
||||
"ice": "Ice",
|
||||
"imaginary": "Imaginary",
|
||||
@@ -105,6 +105,7 @@
|
||||
"level": "Level",
|
||||
"relics": "Relics",
|
||||
"eidolons": "Eidolons",
|
||||
"lightcones": "Lightcones"
|
||||
"lightcones": "Lightcones",
|
||||
"trailblazer": "Trailblazer"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
"level": "レベル",
|
||||
"relics": "遺物",
|
||||
"eidolons": "星魂",
|
||||
"lightcones": "光円錐"
|
||||
"lightcones": "光円錐",
|
||||
"trailblazer": "開拓者"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
"level": "레벨",
|
||||
"relics": "유물",
|
||||
"eidolons": "에이돌론",
|
||||
"lightcones": "광추"
|
||||
"lightcones": "광추",
|
||||
"trailblazer": "개척자"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
"level": "Cấp độ",
|
||||
"relics": "Di vật",
|
||||
"eidolons": "Tinh hồn",
|
||||
"lightcones": "Nón Ánh sáng"
|
||||
"lightcones": "Nón Ánh sáng",
|
||||
"trailblazer": "Nhà khai phá"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
"level": "等级",
|
||||
"relics": "遗器",
|
||||
"eidolons": "星魂",
|
||||
"lightcones": "光锥"
|
||||
"lightcones": "光锥",
|
||||
"trailblazer": "开拓者"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,24 +12,35 @@ const nextConfig: NextConfig = {
|
||||
reactStrictMode: false,
|
||||
output: 'standalone',
|
||||
images: {
|
||||
unoptimized: true,
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'localhost',
|
||||
pathname: '**',
|
||||
},
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: 'localhost',
|
||||
pathname: '**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'api.hakush.in',
|
||||
pathname: '**',
|
||||
},
|
||||
],
|
||||
unoptimized: true,
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'localhost',
|
||||
pathname: '**',
|
||||
},
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: 'localhost',
|
||||
pathname: '**',
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "cdn.kain.id.vn",
|
||||
pathname: "**",
|
||||
},
|
||||
{
|
||||
protocol: "http",
|
||||
hostname: "cdn.kain.id.vn",
|
||||
pathname: "**",
|
||||
}
|
||||
],
|
||||
},
|
||||
compiler: {
|
||||
styledComponents: true,
|
||||
},
|
||||
env: {
|
||||
CDN_URL: "https://cdn.kain.id.vn/firefly/assets/asbres",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
1133
public/data/character.json
Normal file
55232
public/data/monster.json
Normal file
BIN
public/icon/AbyssIcon01.webp
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/icon/AbyssIcon02.webp
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
public/icon/ChallengeBoss.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/icon/ChallengePeakIcon.webp
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
public/icon/ChallengeStory.webp
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/icon/IconJoy.webp
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/icon/MonsterIcon.webp
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/icon/SimulatedUniverse.webp
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/icon/attack.webp
Normal file
|
After Width: | Height: | Size: 958 B |
BIN
public/icon/break-effect.webp
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/icon/crit-damage.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/icon/crit-rate.webp
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/icon/defence.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/icon/effect-hit-rate.webp
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/icon/effect-res.webp
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/icon/elation.webp
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
public/icon/energy-rate.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/icon/fire-add.webp
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/icon/fire.webp
Normal file
|
After Width: | Height: | Size: 744 B |
BIN
public/icon/healing-boost.webp
Normal file
|
After Width: | Height: | Size: 372 B |
BIN
public/icon/hp.webp
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/icon/ice-add.webp
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/icon/ice.webp
Normal file
|
After Width: | Height: | Size: 918 B |
BIN
public/icon/imaginary-add.webp
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/icon/imaginary.webp
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/icon/knight.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/icon/mage.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
public/icon/memory.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/icon/physical-add.webp
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
public/icon/physical.webp
Normal file
|
After Width: | Height: | Size: 1020 B |
BIN
public/icon/priest.webp
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
public/icon/quantum-add.webp
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
public/icon/quantum.webp
Normal file
|
After Width: | Height: | Size: 710 B |
BIN
public/icon/rogue.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/icon/shaman.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/icon/speed.webp
Normal file
|
After Width: | Height: | Size: 984 B |
BIN
public/icon/thunder-add.webp
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/icon/thunder.webp
Normal file
|
After Width: | Height: | Size: 872 B |
BIN
public/icon/warlock.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/icon/warrior.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/icon/wind-add.webp
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/icon/wind.webp
Normal file
|
After Width: | Height: | Size: 860 B |
@@ -11,10 +11,11 @@ import MultiCharLineChart from "@/components/chart/damageLineForAll";
|
||||
import DamagePerCycleForAll from "@/components/chart/damagePerCycleForAll";
|
||||
import DamagePercentChartForAll from "@/components/chart/damagePercentForAll";
|
||||
import EnemyBar from "@/components/enemybar";
|
||||
import { CharacterBasic, MonsterBasic } from "@/types";
|
||||
|
||||
export default function Home() {
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
const { setListAvatar, setListEnemy } = useAvatarDataStore();
|
||||
const { setListAvatar, setMapAvatar, setListEnemy, setMapEnemy } = useAvatarDataStore();
|
||||
const {
|
||||
totalAV,
|
||||
totalDamage,
|
||||
@@ -34,10 +35,20 @@ export default function Home() {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const data = await getCharacterListApi();
|
||||
setListAvatar(data);
|
||||
const avatarData = await getCharacterListApi();
|
||||
setListAvatar(avatarData);
|
||||
const avatarMap = avatarData.reduce<Record<string, CharacterBasic>>((acc, m) => {
|
||||
acc[m.id] = m
|
||||
return acc
|
||||
}, {})
|
||||
setMapAvatar(avatarMap)
|
||||
const enemyData = await getEnemyListApi();
|
||||
setListEnemy(enemyData);
|
||||
const monsterMap = enemyData.reduce<Record<string, MonsterBasic>>((acc, m) => {
|
||||
acc[m.id] = m
|
||||
return acc
|
||||
}, {})
|
||||
setMapEnemy(monsterMap)
|
||||
};
|
||||
fetchData();
|
||||
}, [setListAvatar, setListEnemy]);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import useAvatarDataStore from "@/stores/avatarDataStore";
|
||||
import useBattleDataStore from "@/stores/battleDataStore";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import {attackTypeToString, AvatarHakushiType} from "@/types";
|
||||
import { attackTypeToString, CharacterBasic } from "@/types";
|
||||
import { SkillBattleInfo } from "@/types/mics";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
@@ -13,7 +13,7 @@ import NameAvatar from "../nameAvatar";
|
||||
|
||||
export default function ActionBar() {
|
||||
const [selectTurn, setSelectTurn] = useState<SkillBattleInfo | null>(null);
|
||||
const [selectAvatar, setSelectAvatar] = useState<AvatarHakushiType | null>(null);
|
||||
const [selectAvatar, setSelectAvatar] = useState<CharacterBasic | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const { skillHistory, turnHistory, cycleIndex, waveIndex, maxWave } = useBattleDataStore();
|
||||
const { listAvatar } = useAvatarDataStore();
|
||||
@@ -29,7 +29,7 @@ export default function ActionBar() {
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
const handleShow = (modalId: string, avatar: AvatarHakushiType, turn: SkillBattleInfo) => {
|
||||
const handleShow = (modalId: string, avatar: CharacterBasic, turn: SkillBattleInfo) => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
setSelectAvatar(avatar);
|
||||
@@ -105,13 +105,13 @@ export default function ActionBar() {
|
||||
skillHistory.map((turn, index) => {
|
||||
const data = listAvatar.find(it => it.id === turn.avatarId.toString());
|
||||
if (!data) return null;
|
||||
const text = getNameChar(locale, data);
|
||||
const text = getNameChar(locale, transI18n, data);
|
||||
|
||||
return (
|
||||
<div key={index} className="h-full md:w-full">
|
||||
<div
|
||||
onClick={() => handleShow("action_detail_modal", data, turn)}
|
||||
className="h-full grid grid-cols-2 gap-2 border bg-base-100 w-full hover:bg-base-200 transition-colors duration-200 border-cyan-400 border-l-4 cursor-pointer min-w-[200px] sm:min-w-[250px] md:min-w-0"
|
||||
className="h-full grid grid-cols-2 gap-2 border bg-base-100 w-full hover:bg-base-200 transition-colors duration-200 border-cyan-400 border-l-4 cursor-pointer min-w-50 sm:min-w-62.5 md:min-w-0"
|
||||
>
|
||||
<div
|
||||
style={contentStyle}
|
||||
@@ -121,17 +121,19 @@ export default function ActionBar() {
|
||||
<div className="w-12 h-12 rounded-full border-2 flex items-center justify-center bg-base-300 border-cyan-400 border-l-4">
|
||||
|
||||
<Image
|
||||
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${data.id}.webp`}
|
||||
src={`${process.env.CDN_URL}/${data.icon}`}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
alt={text}
|
||||
width={32}
|
||||
height={32}
|
||||
width={125}
|
||||
height={125}
|
||||
className="w-8 h-8 object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NameAvatar
|
||||
locale={locale}
|
||||
text={getNameChar(locale, data)}
|
||||
text={getNameChar(locale, transI18n, data)}
|
||||
className="text-base-content text-center text-sm mt-1 font-medium"
|
||||
/>
|
||||
</div>
|
||||
@@ -195,15 +197,17 @@ export default function ActionBar() {
|
||||
<span className="font-medium text-base-content/70">{transI18n("character")}:</span>
|
||||
<NameAvatar
|
||||
locale={locale}
|
||||
text={getNameChar(locale, selectAvatar)}
|
||||
text={getNameChar(locale, transI18n, selectAvatar)}
|
||||
className="font-bold"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center items-center">
|
||||
<Image
|
||||
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${selectAvatar.id}.webp`}
|
||||
alt={getNameChar(locale, selectAvatar)}
|
||||
src={`${process.env.CDN_URL}/${selectAvatar?.icon}`}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
alt={getNameChar(locale, transI18n, selectAvatar)}
|
||||
width={80}
|
||||
height={80}
|
||||
className="h-20 w-20 object-cover rounded-full border-2 border-purple-500 shadow-lg shadow-purple-500/20"
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
|
||||
import { getNameChar } from '@/helper';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import { AvatarHakushiType } from '@/types';
|
||||
import { CharacterBasic } from '@/types';
|
||||
import NameAvatar from '../nameAvatar';
|
||||
import useBattleDataStore from '@/stores/battleDataStore';
|
||||
import Image from 'next/image';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface CharacterCardProps {
|
||||
data: AvatarHakushiType
|
||||
data: CharacterBasic
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default function CharacterCard({ data }: CharacterCardProps) {
|
||||
const { locale } = useLocaleStore();
|
||||
const text = getNameChar(locale, data)
|
||||
const { avatarDetail } = useBattleDataStore()
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
const text = getNameChar(locale, transI18n, data)
|
||||
|
||||
return (
|
||||
<li className="z-10 flex flex-col w-28 items-center p-1 rounded-md shadow-lg bg-linear-to-b from-customStart to-customEnd transform transition-transform duration-300 hover:scale-105 m-1">
|
||||
@@ -31,21 +31,27 @@ export default function CharacterCard({ data }: CharacterCardProps) {
|
||||
<Image
|
||||
width={376}
|
||||
height={512}
|
||||
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${data.id}.webp`}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${data.icon}`}
|
||||
className="w-full h-full rounded-md object-cover"
|
||||
alt="ALT"
|
||||
/>
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
src={`https://api.hakush.in/hsr/UI/element/${data.damageType.toLowerCase()}.webp`}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`/icon/${data.damageType.toLowerCase()}.webp`}
|
||||
className="absolute top-0 left-0 w-6 h-6"
|
||||
alt={data.damageType.toLowerCase()}
|
||||
/>
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
src={`https://api.hakush.in/hsr/UI/pathicon/${data.baseType.toLowerCase()}.webp`}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`/icon/${data.baseType.toLowerCase()}.webp`}
|
||||
className="absolute top-0 right-0 w-6 h-6"
|
||||
alt={data.baseType.toLowerCase()}
|
||||
/>
|
||||
|
||||
@@ -24,14 +24,13 @@ export default function MultiCharLineChart() {
|
||||
const [mode, setMode] = useState<1 | 2>(2);
|
||||
const dataByAvatar = useDamageLinesForAll(mode);
|
||||
const avatarIds = Object.keys(dataByAvatar).map(Number);
|
||||
const { listAvatar } = useAvatarDataStore()
|
||||
const { mapAvatar } = useAvatarDataStore()
|
||||
const { locale } = useLocaleStore();
|
||||
const transI18n = useTranslations("DataAnalysisPage")
|
||||
|
||||
|
||||
const data = {
|
||||
datasets: avatarIds.map((id, idx) => ({
|
||||
label: getNameChar(locale, listAvatar.find(it => it.id == id.toString())),
|
||||
label: getNameChar(locale, transI18n, mapAvatar?.[id.toString()]),
|
||||
data: dataByAvatar[id].map(({ x, y }: { x: number; y: number }) => {
|
||||
return {
|
||||
x: x.toFixed(2),
|
||||
|
||||
@@ -48,17 +48,16 @@ const borderPalette = colorPalette.map((c) => c.replace('0.6', '1'));
|
||||
export default function DamagePerAvatarForAll() {
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
const { lineup, skillHistory } = useBattleDataStore();
|
||||
const { listAvatar } = useAvatarDataStore();
|
||||
const { mapAvatar } = useAvatarDataStore();
|
||||
const { locale } = useLocaleStore();
|
||||
|
||||
const [mode, setMode] = useState<number>(2);
|
||||
|
||||
const avatarMap = lineup.map((avatar) => {
|
||||
const char = listAvatar.find(it => it.id === avatar.avatarId.toString());
|
||||
if (!char) return undefined;
|
||||
if (!mapAvatar?.[avatar.avatarId.toString()]) return undefined;
|
||||
return {
|
||||
avatarId: avatar.avatarId,
|
||||
avatarName: getNameChar(locale, char)
|
||||
avatarName: getNameChar(locale, transI18n, mapAvatar?.[avatar.avatarId.toString()])
|
||||
};
|
||||
}).filter(Boolean) as { avatarId: number, avatarName: string }[];
|
||||
|
||||
|
||||
@@ -27,14 +27,14 @@ export default function DamagePercentChartForAll() {
|
||||
const [mode, setMode] = useState<1 | 2>(1);
|
||||
const damageByAvatar = useDamagePercentPerAvatar();
|
||||
const damageByType = useDamagePercentByType();
|
||||
const { listAvatar } = useAvatarDataStore();
|
||||
const { mapAvatar } = useAvatarDataStore();
|
||||
const { locale } = useLocaleStore();
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
|
||||
const chartData = {
|
||||
labels: (mode === 1
|
||||
? damageByAvatar.map(d =>
|
||||
getNameChar(locale, listAvatar.find(it => it.id == d.avatarId.toString()))
|
||||
getNameChar(locale, transI18n, mapAvatar?.[d.avatarId.toString()])
|
||||
)
|
||||
: damageByType.map(d => transI18n(d.type.toLowerCase()))
|
||||
),
|
||||
|
||||
@@ -10,7 +10,7 @@ import NameAvatar from "../nameAvatar";
|
||||
|
||||
export default function EnemyBar() {
|
||||
const { enemyDetail } = useBattleDataStore()
|
||||
const { listEnemy } = useAvatarDataStore()
|
||||
const { mapEnemy } = useAvatarDataStore()
|
||||
const { locale } = useLocaleStore()
|
||||
|
||||
return (
|
||||
@@ -21,10 +21,12 @@ export default function EnemyBar() {
|
||||
<div key={uid} className="bg-base-200 rounded-lg p-3 border border-gray-700 w-52 shrink-0">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{listEnemy.find((monster) => monster.child.includes(enemy.id))?.icon?.split("/")?.pop()?.replace(".png", "") && (
|
||||
{mapEnemy?.[enemy.id.toString()]?.icon && (
|
||||
<Image
|
||||
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listEnemy.find((monster) => monster.child.includes(enemy.id))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
|
||||
src={`${process.env.CDN_URL}/${mapEnemy?.[enemy.id.toString()]?.icon}`}
|
||||
alt={enemy.name}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
width={40}
|
||||
height={40}
|
||||
className="object-cover w-10 h-10 rounded-lg"
|
||||
@@ -33,7 +35,7 @@ export default function EnemyBar() {
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<NameAvatar
|
||||
text={getNameEnemy(locale, listEnemy.find((monster) => monster.child.includes(enemy.id)))}
|
||||
text={getNameEnemy(locale, mapEnemy?.[enemy.id.toString()])}
|
||||
locale={locale}
|
||||
className="text-base font-semibold leading-tight truncate overflow-hidden"
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,7 @@ import useBattleDataStore from "@/stores/battleDataStore";
|
||||
import CharacterCard from "../card/characterCard";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState, useEffect } from "react";
|
||||
import { AvatarHakushiType } from "@/types";
|
||||
import { CharacterBasic } from "@/types";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import { getNameChar } from '@/helper/getNameChar';
|
||||
import SkillBarChart from "../chart/skillBarChart";
|
||||
@@ -18,12 +18,12 @@ import NameAvatar from "../nameAvatar";
|
||||
// import ShowCaseInfo from "../card/showCaseCard";
|
||||
|
||||
export default function LineupBar() {
|
||||
const [selectedCharacter, setSelectedCharacter] = useState<AvatarHakushiType | undefined>(undefined);
|
||||
const [selectedCharacter, setSelectedCharacter] = useState<CharacterBasic | undefined>(undefined);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
const { lineup, turnHistory, dataAvatar } = useBattleDataStore();
|
||||
const { listAvatar } = useAvatarDataStore();
|
||||
const { listAvatar, mapAvatar } = useAvatarDataStore();
|
||||
const { locale } = useLocaleStore();
|
||||
const totalDamage = useCalcTotalDmgAvatar(selectedCharacter ? Number(selectedCharacter.id) : 0);
|
||||
const totalTurn = useCalcTotalTurnAvatar(selectedCharacter ? Number(selectedCharacter.id) : 0)
|
||||
@@ -33,7 +33,7 @@ export default function LineupBar() {
|
||||
lineup.some(av => av.avatarId.toString() === item.id)
|
||||
);
|
||||
|
||||
const handleShow = (modalId: string, item: AvatarHakushiType) => {
|
||||
const handleShow = (modalId: string, item: CharacterBasic) => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
setSelectedCharacter(item);
|
||||
@@ -81,16 +81,16 @@ export default function LineupBar() {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
|
||||
<span className="text-sm truncate flex flex-row">
|
||||
<div>
|
||||
{transI18n("lastTurn")}:
|
||||
</div>
|
||||
<span className="text-sm truncate flex flex-row">
|
||||
<div>
|
||||
{transI18n("lastTurn")}:
|
||||
</div>
|
||||
|
||||
<NameAvatar
|
||||
locale={locale}
|
||||
text={getNameChar(locale, listAvatar.find(it => it.id === turnHistory.findLast(i => i?.avatarId)?.avatarId?.toString()))}
|
||||
/>
|
||||
</span>
|
||||
<NameAvatar
|
||||
locale={locale}
|
||||
text={getNameChar(locale, transI18n, mapAvatar?.[turnHistory.findLast(i => i?.avatarId)?.avatarId?.toString() || ""])}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -145,7 +145,7 @@ export default function LineupBar() {
|
||||
|
||||
<NameAvatar
|
||||
locale={locale}
|
||||
text={getNameChar(locale, selectedCharacter).toUpperCase()}
|
||||
text={getNameChar(locale, transI18n, selectedCharacter).toUpperCase()}
|
||||
className={"font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400"}
|
||||
/>
|
||||
</div>
|
||||
@@ -170,8 +170,9 @@ export default function LineupBar() {
|
||||
<span className="font-bold">{transI18n(selectedCharacter.baseType.toLowerCase())}</span>
|
||||
{selectedCharacter.baseType && (
|
||||
<Image
|
||||
|
||||
src={`https://api.hakush.in/hsr/UI/pathicon/${selectedCharacter.baseType.toLowerCase()}.webp`}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`/icon/${selectedCharacter.baseType.toLowerCase()}.webp`}
|
||||
className="w-6 h-6"
|
||||
alt={selectedCharacter.baseType.toLowerCase()}
|
||||
width={24}
|
||||
@@ -185,7 +186,9 @@ export default function LineupBar() {
|
||||
<p className="flex items-center space-x-2">
|
||||
<span>{transI18n("element")}:</span>
|
||||
<Image
|
||||
src={`https://api.hakush.in/hsr/UI/element/${selectedCharacter.damageType.toLowerCase()}.webp`}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`/icon/${selectedCharacter.damageType.toLowerCase()}.webp`}
|
||||
className="w-6 h-6"
|
||||
alt={selectedCharacter.damageType.toLowerCase()}
|
||||
width={24}
|
||||
@@ -212,7 +215,9 @@ export default function LineupBar() {
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>{transI18n("lightcones")}:</span>
|
||||
<Image
|
||||
src={`https://api.hakush.in/hsr/UI/lightconemediumicon/${avatar?.Lightcone?.item_id}.webp`}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/spriteoutput/lightconemediumicon/${avatar?.Lightcone?.item_id}.png`}
|
||||
className="w-12 h-12"
|
||||
alt={avatar?.Lightcone?.item_id?.toString() || ""}
|
||||
width={200}
|
||||
@@ -224,8 +229,10 @@ export default function LineupBar() {
|
||||
<div className="grid grid-cols-3 md:flex md:flex-row w-full">
|
||||
{relicIds.map(it => (
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
key={it}
|
||||
src={`https://api.hakush.in/hsr/UI/relicfigures/IconRelic_${it}.webp`}
|
||||
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${it}.png`}
|
||||
className="w-12 h-12"
|
||||
alt={avatar?.Lightcone?.item_id?.toString() || ""}
|
||||
width={200}
|
||||
@@ -243,8 +250,10 @@ export default function LineupBar() {
|
||||
|
||||
</div>
|
||||
<Image
|
||||
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${selectedCharacter.id}.webp`}
|
||||
alt={getNameChar(locale, selectedCharacter)}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${selectedCharacter.icon}`}
|
||||
alt={getNameChar(locale, transI18n, selectedCharacter)}
|
||||
className="h-32 w-32 object-cover rounded-full border-2 border-purple-500"
|
||||
width={128}
|
||||
height={128}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
import { createContext, PropsWithChildren, useEffect, useState } from "react";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import { createContext, PropsWithChildren, useEffect } from "react";
|
||||
|
||||
interface ThemeContextType {
|
||||
theme?: string;
|
||||
@@ -8,7 +9,7 @@ interface ThemeContextType {
|
||||
export const ThemeContext = createContext<ThemeContextType>({});
|
||||
|
||||
export const ThemeProvider = ({ children }: PropsWithChildren) => {
|
||||
const [theme, setTheme] = useState<string>("light");
|
||||
const { theme, setTheme } = useLocaleStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
@@ -19,14 +20,13 @@ export const ThemeProvider = ({ children }: PropsWithChildren) => {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const changeTheme = (nextTheme: string | null) => {
|
||||
console.log(nextTheme)
|
||||
if (nextTheme) {
|
||||
setTheme(nextTheme);
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("theme", nextTheme);
|
||||
}
|
||||
} else {
|
||||
setTheme((prev) => (prev === "light" ? "night" : "light"));
|
||||
setTheme(theme === "winter" ? "night" : "winter");
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("theme", theme);
|
||||
}
|
||||
|
||||
@@ -1,38 +1,51 @@
|
||||
import { listCurrentLanguage } from "@/lib/constant";
|
||||
import { AvatarHakushiType, EnemyHakushiType } from "@/types";
|
||||
import { CharacterBasic, MonsterBasic } from "@/types";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
|
||||
export function getNameChar(locale: string, data: AvatarHakushiType | undefined): string {
|
||||
if (!data) {
|
||||
return ""
|
||||
}
|
||||
if (!listCurrentLanguage.hasOwnProperty(locale)) {
|
||||
return ""
|
||||
}
|
||||
type TFunc = ReturnType<typeof useTranslations>
|
||||
|
||||
let text = data.lang.get(listCurrentLanguage[locale as keyof typeof listCurrentLanguage].toLowerCase()) ?? "";
|
||||
if (!text) {
|
||||
text = data.lang.get("en") ?? "";
|
||||
}
|
||||
if (Number(data.id) % 2 === 0 && Number(data.id) > 8000) {
|
||||
text = `Female ${data.damageType} MC`
|
||||
} else if (Number(data.id) > 8000) {
|
||||
text = `Male ${data.damageType} MC`
|
||||
}
|
||||
return text
|
||||
export function getNameChar(
|
||||
locale: string,
|
||||
t: TFunc,
|
||||
data: CharacterBasic | undefined
|
||||
): string {
|
||||
if (!data) return "";
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguage, locale)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const langKey = listCurrentLanguage[locale as keyof typeof listCurrentLanguage].toLowerCase();
|
||||
|
||||
let text = data.lang[langKey] ?? "";
|
||||
|
||||
if (!text) {
|
||||
text = data.lang["en"] ?? "";
|
||||
}
|
||||
|
||||
if (Number(data.id) > 8000) {
|
||||
text = `${t("trailblazer")} • ${t(data?.baseType?.toLowerCase() ?? "")}`;
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
export function getNameEnemy(locale: string, data: EnemyHakushiType | undefined): string {
|
||||
export function getNameEnemy(locale: string, data: MonsterBasic | undefined): string {
|
||||
if (!data) {
|
||||
return ""
|
||||
}
|
||||
if (!listCurrentLanguage.hasOwnProperty(locale)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguage, locale)) {
|
||||
return ""
|
||||
}
|
||||
|
||||
let text = data.lang.get(listCurrentLanguage[locale as keyof typeof listCurrentLanguage].toLowerCase()) ?? "";
|
||||
const langKey = listCurrentLanguage[locale as keyof typeof listCurrentLanguage].toLowerCase();
|
||||
|
||||
|
||||
let text = data.lang[langKey] ?? "";
|
||||
|
||||
if (!text) {
|
||||
text = data.lang.get("en") ?? "";
|
||||
text = data.lang["en"] ?? "";
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import useSocketStore from "@/stores/socketSettingStore";
|
||||
import { EnemyHakushiRawType, EnemyHakushiType } from "@/types";
|
||||
import { AvatarHakushiType, AvatarHakushiRawType } from "@/types/avatar";
|
||||
import { CharacterBasic, MonsterBasic } from "@/types";
|
||||
import axios from 'axios';
|
||||
|
||||
export async function checkConnectTcpApi(): Promise<boolean> {
|
||||
@@ -23,20 +22,10 @@ export async function checkConnectTcpApi(): Promise<boolean> {
|
||||
return false
|
||||
}
|
||||
|
||||
export async function getCharacterListApi(): Promise<AvatarHakushiType[]> {
|
||||
export async function getCharacterListApi(): Promise<CharacterBasic[]> {
|
||||
try {
|
||||
const res = await axios.get<Record<string, AvatarHakushiRawType>>(
|
||||
'https://api.hakush.in/hsr/data/character.json',
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const data = new Map(Object.entries(res.data));
|
||||
|
||||
return Array.from(data.entries()).map(([id, it]) => convertAvatar(id, it));
|
||||
const res = await axios.get<CharacterBasic[]>("/data/character.json");
|
||||
return res.data
|
||||
} catch (error: unknown) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.log(`Error: ${error.response?.status} - ${error.message}`);
|
||||
@@ -47,20 +36,10 @@ export async function getCharacterListApi(): Promise<AvatarHakushiType[]> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getEnemyListApi(): Promise<EnemyHakushiType[]> {
|
||||
export async function getEnemyListApi(): Promise<MonsterBasic[]> {
|
||||
try {
|
||||
const res = await axios.get<Record<string, EnemyHakushiRawType>>(
|
||||
'https://api.hakush.in/hsr/data/monster.json',
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const data = new Map(Object.entries(res.data));
|
||||
|
||||
return Array.from(data.entries()).map(([id, it]) => convertMonster(id, it));
|
||||
const res = await axios.get<MonsterBasic[]>("/data/monster.json");
|
||||
return res.data
|
||||
} catch (error: unknown) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.log(`Error: ${error.response?.status} - ${error.message}`);
|
||||
@@ -70,46 +49,3 @@ export async function getEnemyListApi(): Promise<EnemyHakushiType[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function convertAvatar(id: string, item: AvatarHakushiRawType): AvatarHakushiType {
|
||||
const lang = new Map<string, string>([
|
||||
['en', item.en],
|
||||
['kr', item.kr],
|
||||
['cn', item.cn],
|
||||
['jp', item.jp]
|
||||
]);
|
||||
|
||||
const result: AvatarHakushiType = {
|
||||
release: item.release,
|
||||
icon: item.icon,
|
||||
rank: item.rank,
|
||||
baseType: item.baseType,
|
||||
damageType: item.damageType,
|
||||
desc: item.desc,
|
||||
lang: lang,
|
||||
id: id
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function convertMonster(id: string, item: EnemyHakushiRawType): EnemyHakushiType {
|
||||
const lang = new Map<string, string>([
|
||||
['en', item.en],
|
||||
['kr', item.kr],
|
||||
['cn', item.cn],
|
||||
['jp', item.jp]
|
||||
]);
|
||||
const result: EnemyHakushiType = {
|
||||
id: id,
|
||||
rank: item.rank,
|
||||
camp: item.camp,
|
||||
icon: item.icon,
|
||||
child: item.child,
|
||||
weak: item.weak,
|
||||
desc: item.desc,
|
||||
lang: lang
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,19 +1,30 @@
|
||||
import { AvatarHakushiType, EnemyHakushiType } from '@/types';
|
||||
import { CharacterBasic, MonsterBasic } from '@/types';
|
||||
import { create } from 'zustand'
|
||||
|
||||
|
||||
interface AvatarDataState {
|
||||
listAvatar: AvatarHakushiType[];
|
||||
listEnemy: EnemyHakushiType[];
|
||||
setListAvatar: (list: AvatarHakushiType[]) => void;
|
||||
setListEnemy: (list: EnemyHakushiType[]) => void;
|
||||
listAvatar: CharacterBasic[];
|
||||
mapAvatar: Record<string, CharacterBasic>;
|
||||
listEnemy: MonsterBasic[];
|
||||
mapEnemy: Record<string, MonsterBasic>;
|
||||
setListAvatar: (list: CharacterBasic[]) => void;
|
||||
setMapAvatar: (data: Record<string, CharacterBasic>) => void;
|
||||
setListEnemy: (list: MonsterBasic[]) => void;
|
||||
setMapEnemy: (data: Record<string, MonsterBasic>) => void;
|
||||
|
||||
|
||||
}
|
||||
|
||||
const useAvatarDataStore = create<AvatarDataState>((set) => ({
|
||||
listAvatar: [],
|
||||
listEnemy: [],
|
||||
setListAvatar: (list: AvatarHakushiType[]) => set({ listAvatar: list }),
|
||||
setListEnemy: (list: EnemyHakushiType[]) => set({ listEnemy: list }),
|
||||
mapAvatar: {},
|
||||
mapEnemy: {},
|
||||
setListAvatar: (list: CharacterBasic[]) => set({ listAvatar: list }),
|
||||
setMapAvatar: (data: Record<string, CharacterBasic>) => set({ mapAvatar: data }),
|
||||
setListEnemy: (list: MonsterBasic[]) => set({ listEnemy: list }),
|
||||
setMapEnemy: (data: Record<string, MonsterBasic>) => set({ mapEnemy: data })
|
||||
|
||||
}));
|
||||
|
||||
export default useAvatarDataStore;
|
||||
@@ -1,15 +1,25 @@
|
||||
import { create } from 'zustand'
|
||||
import { createJSONStorage, persist } from 'zustand/middleware';
|
||||
|
||||
|
||||
interface LocaleState {
|
||||
locale: string;
|
||||
theme: string
|
||||
setTheme: (newTheme: string) => void;
|
||||
setLocale: (newLocale: string) => void;
|
||||
}
|
||||
|
||||
const useLocaleStore = create<LocaleState>((set) => ({
|
||||
locale: "en",
|
||||
setLocale: (newLocale: string) => set({ locale: newLocale }),
|
||||
|
||||
}));
|
||||
const useLocaleStore = create<LocaleState>()(persist(
|
||||
(set) => ({
|
||||
locale: "en",
|
||||
theme: "night",
|
||||
setTheme: (newTheme: string) => set({ theme: newTheme }),
|
||||
setLocale: (newLocale: string) => set({ locale: newLocale }),
|
||||
}),
|
||||
{
|
||||
name: 'locale-storage',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
}
|
||||
));
|
||||
|
||||
export default useLocaleStore;
|
||||
@@ -1,23 +1,8 @@
|
||||
export interface AvatarHakushiRawType {
|
||||
release: number;
|
||||
icon: string;
|
||||
rank: string;
|
||||
baseType: string;
|
||||
damageType: string;
|
||||
en: string;
|
||||
desc: string;
|
||||
kr: string;
|
||||
cn: string;
|
||||
jp: string;
|
||||
}
|
||||
|
||||
export interface AvatarHakushiType {
|
||||
export interface CharacterBasic {
|
||||
id: string;
|
||||
release: number;
|
||||
icon: string;
|
||||
rank: string;
|
||||
baseType: string;
|
||||
damageType: string;
|
||||
desc: string;
|
||||
lang: Map<string, string>;
|
||||
lang: Record<string, string>;
|
||||
}
|
||||
|
||||
@@ -11,26 +11,12 @@ export interface InitializeEnemyType {
|
||||
enemy: EnemyType
|
||||
}
|
||||
|
||||
export interface EnemyHakushiRawType {
|
||||
rank: string;
|
||||
camp: string | null;
|
||||
icon: string;
|
||||
child: number[];
|
||||
weak: string[];
|
||||
en: string;
|
||||
desc: string;
|
||||
kr: string;
|
||||
cn: string;
|
||||
jp: string;
|
||||
}
|
||||
|
||||
export interface EnemyHakushiType {
|
||||
export interface MonsterBasic {
|
||||
id: string;
|
||||
rank: string;
|
||||
camp: string | null;
|
||||
icon: string;
|
||||
child: number[];
|
||||
image: string;
|
||||
weak: string[];
|
||||
desc: string;
|
||||
lang: Map<string, string>;
|
||||
desc: Record<string, string>;
|
||||
lang: Record<string, string>;
|
||||
}
|
||||
|
||||