Skip to content

Commit 36708af

Browse files
committed
Add more character information
1 parent ff37717 commit 36708af

File tree

5 files changed

+173
-50
lines changed

5 files changed

+173
-50
lines changed

pages/characters/[name].tsx

Lines changed: 134 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@ import { useState } from "react"
55
import FormattedLink from "../../components/FormattedLink"
66
import Main from "../../components/Main"
77
import YouTube from "../../components/YouTube"
8-
import { getCharacterCurves, getCharacters, urlify } from "../../utils/data-cache"
9-
import { elements, ElementType, getCharStatsAt, isFullCharacter, stat, weapons } from "../../utils/utils"
10-
import { Character, CharacterFull, CostTemplate, CurveEnum, Skills } from "../../utils/types"
8+
import { CharacterCurves, CostTemplates, getCharacterCurves, getCharacters, getCostTemplates, urlify } from "../../utils/data-cache"
9+
import { elements, ElementType, getCharStatsAt, getCostsFromTemplate, image, isFullCharacter, stat, weapons } from "../../utils/utils"
10+
import { Character, CharacterFull, CostTemplate, CurveEnum, Meta, Skills } from "../../utils/types"
11+
import styles from "../style.module.css"
1112

1213
interface Props {
1314
char: Character,
14-
characterCurves: Record<CurveEnum, number[]> | null
15+
characterCurves: CharacterCurves | null
16+
costTemplates: CostTemplates | null
1517
}
1618

17-
export default function CharacterWebpage({ char, location, characterCurves }: Props & { location: string }) {
19+
export default function CharacterWebpage({ char, location, characterCurves, costTemplates }: Props & { location: string }) {
1820
const charElems = char.skills?.map(skill => skill.ult?.type).filter(x => x) as ElementType[] ?? [char.meta.element]
1921
return (
2022
<Main>
@@ -34,12 +36,15 @@ export default function CharacterWebpage({ char, location, characterCurves }: Pr
3436
{char.name}
3537
</h1>
3638

37-
<div className="float-right border-2 border-slate-500 dark:bg-slate-600 bg-slate-200 m-2">
38-
Table of Content
39+
<div className="float-right border-2 border-slate-500 dark:bg-slate-600 bg-slate-200 m-2 px-2">
40+
Table of Contents
41+
{isFullCharacter(char) && characterCurves && <TOC href="#stats" title="Stats / Ascensions" />}
42+
{char.meta && <TOC href="#meta" title="Meta" />}
43+
{char.media.videos && <TOC href="#videos" title={Object.keys(char.media.videos).length > 1 ? "Videos" : "Video"} />}
3944
</div>
4045

41-
<div>
42-
<blockquote className="pl-5 border-l-2">
46+
<div id="description">
47+
<blockquote className="pl-5 mb-2 border-l-2">
4348
{char.desc}
4449
</blockquote>
4550

@@ -61,29 +66,69 @@ export default function CharacterWebpage({ char, location, characterCurves }: Pr
6166

6267
{char.ascensionCosts && <AscensionCosts costs={char.ascensionCosts} />}
6368
{char.skills && <TalentCosts skills={char.skills} />}
64-
{isFullCharacter(char) && characterCurves && <QuickStats char={char} curves={characterCurves}/>}
69+
{isFullCharacter(char) && characterCurves && <Stats char={char} curves={characterCurves} />}
70+
{char.ascensionCosts && costTemplates && <FullAscensionCosts template={char.ascensionCosts} costTemplates={costTemplates} />}
71+
{char.meta && <Meta meta={char.meta} />}
6572
{char.media.videos && <Videos videos={char.media.videos} />}
6673
</div>
67-
6874
</Main>
6975
)
7076
}
7177

78+
function TOC({ href, title }: { href: string, title: string }) {
79+
return <div>
80+
<FormattedLink href={href} size="base">{title}</FormattedLink>
81+
</div>
82+
}
83+
7284
function AscensionCosts({ costs }: { costs: CostTemplate }) {
7385
const ascensionCosts = [
7486
costs.mapping.Gem4,
7587
costs.mapping.BossMat,
7688
costs.mapping.Specialty,
7789
costs.mapping.EnemyDropTier3,
7890
].filter(x => x)
79-
return <div>
80-
<h4 className="text-base font-semibold pt-1 inline-block pr-1">Ascension materials:</h4>
81-
{ascensionCosts.map(e => <div className="inline-block pr-1" key={e}>
82-
{e}
91+
return <div className="flex flex-row items-center">
92+
<div className="text-base font-semibold pt-1 inline-block pr-1 h-9">Ascension materials:</div>
93+
{ascensionCosts.map(e => <div className="inline-block pr-1 w-6 h-6 md:h-8 md:w-8" key={e}>
94+
<Image src={image("material", e)} alt={e} width={256} height={256} />
8395
</div>)}
8496
</div>
8597
}
8698

99+
function FullAscensionCosts({ template, costTemplates }: { template: CostTemplate, costTemplates: CostTemplates }) {
100+
const costs = getCostsFromTemplate(template, costTemplates)
101+
102+
return <>
103+
<table className={`table-auto w-full ${styles.table} mb-2`}>
104+
<thead className="font-semibold divide-x divide-gray-200 dark:divide-gray-500">
105+
<td>Asc.</td>
106+
<td>Mora</td>
107+
<td>Items</td>
108+
</thead>
109+
<tbody className="divide-y divide-gray-200 dark:divide-gray-500">
110+
{costs.slice(1).map(({ mora, items }, ind) => {
111+
let newItems = items
112+
if (ind == 0 && template.mapping.BossMat)
113+
newItems = [items[0], { name: "", count: 0 }, ...items.slice(1)]
114+
return <tr className="pr-1 divide-x divide-gray-200 dark:divide-gray-500" key={ind}>
115+
<td>A{ind + 1}</td>
116+
<td className="text-right">{mora}</td>
117+
{newItems.map(({ count, name }) => <td key={name}>
118+
{count > 0 &&
119+
<div className="flex flex-row align-middle items-center">
120+
<div>{count}&times;</div>
121+
<div className="pr-1 w-6 h-6 md:h-8 md:w-8">
122+
<Image src={image("material", name)} alt={name} width={256} height={256} />
123+
</div>
124+
<div>{name}</div></div>}
125+
</td>)}
126+
</tr>})}
127+
</tbody>
128+
</table>
129+
</>
130+
}
131+
87132
function TalentCosts({ skills }: { skills: Skills[] }) {
88133
const talents = skills
89134
.flatMap(s => [...(s.talents ?? []), s.ult])
@@ -107,48 +152,84 @@ function TalentCosts({ skills }: { skills: Skills[] }) {
107152
.map(s => s?.costs?.mapping?.EnemyDropTier3)
108153
.filter((x, i, a) => x && a.indexOf(x) == i)
109154

110-
const all = [...books, ...mats, ...drops]
111-
return <div>
112-
<h4 className="text-base font-semibold pt-1 inline-block pr-1">Talent materials:</h4>
113-
{all.map(e => <div className="inline-block pr-1" key={e}>
114-
{e}
155+
const all = [...books, ...mats, ...drops] as string[]
156+
return <div className="flex flex-row items-center">
157+
<div className="text-base font-semibold pt-1 inline-block pr-1 h-9">Talent materials:</div>
158+
{all.map(e => <div className="inline-block pr-1 w-6 h-6 md:h-8 md:w-8" key={e}>
159+
<Image src={image("material", e)} alt={e} width={256} height={256} />
115160
</div>)}
116161
</div>
117162
}
118163

119-
function QuickStats({ char, curves }: { char: CharacterFull, curves: Record<CurveEnum, number[]> }) {
164+
function Stats({ char, curves }: { char: CharacterFull, curves: Record<CurveEnum, number[]> }) {
120165
const maxAscension = char.ascensions[char.ascensions.length - 1]
121166

122-
const base = getCharStatsAt(char, 1, 0, curves)
167+
const levels: { a: number, lv: number }[] = []
168+
169+
let prev = 1
170+
for (const asc of char.ascensions) {
171+
levels.push({ a: asc.level, lv: prev })
172+
levels.push({ a: asc.level, lv: asc.maxLevel })
173+
prev = asc.maxLevel
174+
}
123175
const max = getCharStatsAt(char, maxAscension.maxLevel, maxAscension.level, curves)
124176

125-
// TODO
177+
return <>
178+
<h3 className="text-lg font-bold pt-1" id="stats">Stats / Ascensions:</h3>
179+
<table className={`table-auto w-full ${styles.table} ${styles.stattable} mb-2`}>
180+
<thead className="font-semibold divide-x divide-gray-200 dark:divide-gray-500">
181+
<td>Asc.</td>
182+
<td>Lv.</td>
183+
{Object.keys(max).map((name) => <td key={name}>{name}</td>)}
184+
</thead>
185+
<tbody className="divide-y divide-gray-200 dark:divide-gray-500">
186+
{levels.map(({ a, lv }) => <tr className="pr-1 divide-x divide-gray-200 dark:divide-gray-500" key={a + "," + lv}>
187+
<td>A{a}</td>
188+
<td>{lv}</td>
189+
{Object.entries(getCharStatsAt(char, lv, a, curves)).map(([name, value]) => <td key={name}>{stat(name, value)}</td>)}
190+
</tr>)}
191+
</tbody>
192+
</table>
193+
</>
194+
}
126195

127-
return <div className="flex flex-row justify-evenly">
128-
<div>
129-
<h4 className="text-base font-semibold pt-1 inline-block pr-1">Base stats:</h4>
130-
{Object.entries(base)
131-
.map(([name, value]) => <div className="pr-1" key={name}>
132-
{name}: {stat(name, value)}
133-
</div>)}
134-
</div>
135-
<div>
136-
<h4 className="text-base font-semibold pt-1 inline-block pr-1">Max stats:</h4>
137-
{Object.entries(max)
138-
.map(([name, value]) => <div className="pr-1" key={name}>
139-
{name}: {stat(name, value)}
140-
</div>)}
141-
</div>
142-
</div>
196+
function Meta({ meta }: { meta: Meta }) {
197+
return <>
198+
<h3 className="text-lg font-bold pt-1" id="meta">Meta:</h3>
199+
<table className={`table-auto ${styles.table} mb-2`}>
200+
<tbody className="divide-y divide-gray-200 dark:divide-gray-500">
201+
{meta.title && <tr><td>Title</td><td>{meta.title}</td></tr>}
202+
{meta.birthDay && meta.birthMonth && <tr><td>Title</td><td>{
203+
new Date(Date.UTC(2020, meta.birthMonth - 1, meta.birthDay))
204+
.toLocaleString("en-UK", {
205+
timeZone: "UTC",
206+
month: "long",
207+
day: "numeric",
208+
})}</td></tr>}
209+
{meta.association && <tr><td>Association</td><td>{meta.association}</td></tr>}
210+
{meta.affiliation && <tr><td>Affiliation</td><td>{meta.affiliation}</td></tr>}
211+
{meta.constellation && <tr><td>Constellation</td><td>{meta.constellation}</td></tr>}
212+
{meta.element && <tr><td>Element</td><td>{Object.keys(elements).includes(meta.element) ? <>
213+
<div className="w-5 inline-block pr-1">
214+
<Image src={elements[meta.element as ElementType]} alt={`${meta.element} Element`} />
215+
</div>
216+
{meta.element}</> : meta.element}</td></tr>}
217+
{meta.cvChinese && <tr><td>Chinese voice actor</td><td>{meta.cvChinese}</td></tr>}
218+
{meta.cvJapanese && <tr><td>Japanese voice actor</td><td>{meta.cvJapanese}</td></tr>}
219+
{meta.cvEnglish && <tr><td>English voice actor</td><td>{meta.cvEnglish}</td></tr>}
220+
{meta.cvKorean && <tr><td>Korean voice actor</td><td>{meta.cvKorean}</td></tr>}
221+
</tbody>
222+
</table>
223+
</>
143224
}
144225

145226
function Videos({ videos }: { videos: Record<string, string> }) {
146227
const multiple = Object.keys(videos).length > 1
147228

148-
return <div>
149-
<h3 className="text-lg font-bold pt-1">{multiple ? "Videos" : "Video"}:</h3>
229+
return <>
230+
<h3 className="text-lg font-bold pt-1" id="videos">{multiple ? "Videos" : "Video"}:</h3>
150231
{Object.entries(videos).map(([name, link]) => <Video key={name} name={name} link={link} />)}
151-
</div>
232+
</>
152233
}
153234

154235
function Video({ name, link }: { name: string, link: string }) {
@@ -176,13 +257,22 @@ export async function getStaticProps(context: GetStaticPropsContext): Promise<Ge
176257
if (isFullCharacter(char)) {
177258
const curves = await getCharacterCurves()
178259
if (curves)
179-
characterCurves = Object.fromEntries(char.curves.map(c => c.curve).filter((v, i, arr) => arr.indexOf(v) == i).map(c => [c, curves[c]])) as Record<CurveEnum, number[]>
260+
characterCurves = Object.fromEntries(char.curves.map(c => c.curve).filter((v, i, arr) => arr.indexOf(v) == i).map(c => [c, curves[c]])) as CharacterCurves
261+
}
262+
263+
let costTemplates = null
264+
if (char.ascensionCosts) {
265+
const templates = await getCostTemplates()
266+
if (templates)
267+
costTemplates = Object.fromEntries([char.ascensionCosts.template].filter((v, i, arr) => arr.indexOf(v) == i).map(c => [c, templates[c]])) as CostTemplates
268+
180269
}
181270

182271
return {
183272
props: {
184273
char,
185-
characterCurves
274+
characterCurves,
275+
costTemplates
186276
},
187277
revalidate: 60 * 60
188278
}

pages/characters/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,14 @@ export default function Characters(props: Props & { location: string }) {
127127
<span className="absolute block p-0.5 top-0 w-full">
128128
<div className="flex flex-col">
129129
{char.element && char.element.map(e => <div key={e} className="w-6 md:w-8">
130-
<Image src={elements[e]} alt={`${e} Element`} />
130+
<Image src={elements[e]} alt={`${e} Element`} loading="eager" />
131131
</div>)}
132132
</div>
133133
</span>
134134
<span className="absolute block p-0.5 top-0 w-full">
135135
<div className="flex flex-col float-right">
136136
{char.weapon && <div className="w-6 md:w-8">
137-
<Image src={weapons[char.weapon]} alt={char.weapon} />
137+
<Image src={weapons[char.weapon]} alt={char.weapon} loading="eager" />
138138
</div>}
139139
</div>
140140
</span>
@@ -189,7 +189,7 @@ function Icon({ char, className }: { char: SmallChar, className?: string }) {
189189

190190
if (src.startsWith("img")) src = "/" + src
191191

192-
return <Image alt={char.name} src={src} className={className} width={256} height={256} onError={(e) => (e.target as HTMLImageElement).src = "/img/unknown.png"} />
192+
return <Image alt={char.name} src={src} className={className} width={256} height={256} onError={(e) => (e.target as HTMLImageElement).src = "/img/unknown.png"} loading="eager" />
193193
}
194194

195195
export async function getStaticProps(context: GetStaticPropsContext): Promise<GetStaticPropsResult<Props>> {

pages/style.module.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,15 @@
1616
padding-bottom: "56.2;%";
1717
padding-top: 25;
1818
height: 0;
19+
}
20+
21+
.table tr:nth-child(odd) {
22+
background-color: rgba(71, 85, 105, 0.7);
23+
}
24+
.table td {
25+
padding-right: 0.5rem;
26+
padding-left: 0.25rem;
27+
}
28+
.stattable tbody td {
29+
text-align: right;
1930
}

utils/data-cache.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
import { Character, CurveEnum, Guide } from "./types"
1+
import { Character, Cost, CurveEnum, Guide } from "./types"
22

33
const baseURL = "https://raw.githubusercontent.com/Tibowl/HuTao/master/src/data"
44

55
type Guides = Guide[]
66
type Characters = Record<string, Character>
7-
type CharacterCurves = Record<CurveEnum, number[]>
7+
export type CharacterCurves = Record<CurveEnum, number[]>
88
type CharacterLevels = number[]
9+
export type CostTemplates = Record<string, Cost[]>
910

1011
type Cache = {
1112
guides: Cacher<Guides>
1213
characters: Cacher<Characters>
1314
characterCurves: Cacher<CharacterCurves>
1415
characterLevels: Cacher<CharacterLevels>
16+
costTemplates: Cacher<CostTemplates>
1517
}
1618

1719
interface Cacher<T> {
@@ -31,6 +33,9 @@ const cached: Cache = {
3133
},
3234
characterLevels: {
3335
time: 0
36+
},
37+
costTemplates: {
38+
time: 0
3439
}
3540
}
3641
export function urlify(input: string, shouldYeetBrackets: boolean): string {
@@ -47,6 +52,7 @@ export const getGuides: (() => Promise<Guides | undefined>) = createGetCacheable
4752
export const getCharacters: (() => Promise<Characters | undefined>) = createGetCacheable("characters", "gamedata/characters")
4853
export const getCharacterCurves: (() => Promise<CharacterCurves | undefined>) = createGetCacheable("characterCurves", "gamedata/character_curves")
4954
export const getCharacterLevels: (() => Promise<CharacterLevels | undefined>) = createGetCacheable("characterLevels", "gamedata/character_levels")
55+
export const getCostTemplates: (() => Promise<CostTemplates | undefined>) = createGetCacheable("costTemplates", "gamedata/cost_templates")
5056

5157

5258
function createGetCacheable(name: keyof Cache, path: string = name): () => Promise<any> {

utils/utils.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Catalyst from "../public/img/weapon_types/Catalyst.png"
1010
import Claymore from "../public/img/weapon_types/Claymore.png"
1111
import Polearm from "../public/img/weapon_types/Polearm.png"
1212
import Sword from "../public/img/weapon_types/Sword.png"
13-
import { Character, CharacterFull, CurveEnum, WeaponType } from "./types"
13+
import { Character, CharacterFull, Cost, CostTemplate, CurveEnum, WeaponType } from "./types"
1414

1515
export const elements = {
1616
Pyro, Electro, Cryo, Hydro, Anemo, Geo, Dendro
@@ -81,3 +81,19 @@ export function stat(name: string, value: number): string {
8181
return value < 2 ? ((value * 100).toFixed(1) + "%") : value.toFixed(0)
8282
}
8383
}
84+
85+
export function image(type: string, name: string, ext="png"): string {
86+
return `/img/${type}/${name.replace(/[:\-,'"]/g, "").replace(/ +/g, "_")}.${ext}`
87+
}
88+
89+
export function getCostsFromTemplate(costTemplate: CostTemplate, costTemplates: Record<string, Cost[]>): Cost[] {
90+
const template = costTemplates[costTemplate.template]
91+
92+
return template.map(c => ({
93+
mora: c.mora,
94+
items: c.items.map(i => ({
95+
count: i.count,
96+
name: i.name.replace(/<(.*?)>/g, (_, x) => costTemplate.mapping[x])
97+
}))
98+
}))
99+
}

0 commit comments

Comments
 (0)