Skip to content

Commit

Permalink
Add more character information
Browse files Browse the repository at this point in the history
  • Loading branch information
Tibowl committed Jan 8, 2022
1 parent ff37717 commit 36708af
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 50 deletions.
178 changes: 134 additions & 44 deletions pages/characters/[name].tsx
Expand Up @@ -5,16 +5,18 @@ import { useState } from "react"
import FormattedLink from "../../components/FormattedLink"
import Main from "../../components/Main"
import YouTube from "../../components/YouTube"
import { getCharacterCurves, getCharacters, urlify } from "../../utils/data-cache"
import { elements, ElementType, getCharStatsAt, isFullCharacter, stat, weapons } from "../../utils/utils"
import { Character, CharacterFull, CostTemplate, CurveEnum, Skills } from "../../utils/types"
import { CharacterCurves, CostTemplates, getCharacterCurves, getCharacters, getCostTemplates, urlify } from "../../utils/data-cache"
import { elements, ElementType, getCharStatsAt, getCostsFromTemplate, image, isFullCharacter, stat, weapons } from "../../utils/utils"
import { Character, CharacterFull, CostTemplate, CurveEnum, Meta, Skills } from "../../utils/types"
import styles from "../style.module.css"

interface Props {
char: Character,
characterCurves: Record<CurveEnum, number[]> | null
characterCurves: CharacterCurves | null
costTemplates: CostTemplates | null
}

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

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

<div>
<blockquote className="pl-5 border-l-2">
<div id="description">
<blockquote className="pl-5 mb-2 border-l-2">
{char.desc}
</blockquote>

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

{char.ascensionCosts && <AscensionCosts costs={char.ascensionCosts} />}
{char.skills && <TalentCosts skills={char.skills} />}
{isFullCharacter(char) && characterCurves && <QuickStats char={char} curves={characterCurves}/>}
{isFullCharacter(char) && characterCurves && <Stats char={char} curves={characterCurves} />}
{char.ascensionCosts && costTemplates && <FullAscensionCosts template={char.ascensionCosts} costTemplates={costTemplates} />}
{char.meta && <Meta meta={char.meta} />}
{char.media.videos && <Videos videos={char.media.videos} />}
</div>

</Main>
)
}

function TOC({ href, title }: { href: string, title: string }) {
return <div>
<FormattedLink href={href} size="base">{title}</FormattedLink>
</div>
}

function AscensionCosts({ costs }: { costs: CostTemplate }) {
const ascensionCosts = [
costs.mapping.Gem4,
costs.mapping.BossMat,
costs.mapping.Specialty,
costs.mapping.EnemyDropTier3,
].filter(x => x)
return <div>
<h4 className="text-base font-semibold pt-1 inline-block pr-1">Ascension materials:</h4>
{ascensionCosts.map(e => <div className="inline-block pr-1" key={e}>
{e}
return <div className="flex flex-row items-center">
<div className="text-base font-semibold pt-1 inline-block pr-1 h-9">Ascension materials:</div>
{ascensionCosts.map(e => <div className="inline-block pr-1 w-6 h-6 md:h-8 md:w-8" key={e}>
<Image src={image("material", e)} alt={e} width={256} height={256} />
</div>)}
</div>
}

function FullAscensionCosts({ template, costTemplates }: { template: CostTemplate, costTemplates: CostTemplates }) {
const costs = getCostsFromTemplate(template, costTemplates)

return <>
<table className={`table-auto w-full ${styles.table} mb-2`}>
<thead className="font-semibold divide-x divide-gray-200 dark:divide-gray-500">
<td>Asc.</td>
<td>Mora</td>
<td>Items</td>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-500">
{costs.slice(1).map(({ mora, items }, ind) => {
let newItems = items
if (ind == 0 && template.mapping.BossMat)
newItems = [items[0], { name: "", count: 0 }, ...items.slice(1)]
return <tr className="pr-1 divide-x divide-gray-200 dark:divide-gray-500" key={ind}>
<td>A{ind + 1}</td>
<td className="text-right">{mora}</td>
{newItems.map(({ count, name }) => <td key={name}>
{count > 0 &&
<div className="flex flex-row align-middle items-center">
<div>{count}&times;</div>
<div className="pr-1 w-6 h-6 md:h-8 md:w-8">
<Image src={image("material", name)} alt={name} width={256} height={256} />
</div>
<div>{name}</div></div>}
</td>)}
</tr>})}
</tbody>
</table>
</>
}

function TalentCosts({ skills }: { skills: Skills[] }) {
const talents = skills
.flatMap(s => [...(s.talents ?? []), s.ult])
Expand All @@ -107,48 +152,84 @@ function TalentCosts({ skills }: { skills: Skills[] }) {
.map(s => s?.costs?.mapping?.EnemyDropTier3)
.filter((x, i, a) => x && a.indexOf(x) == i)

const all = [...books, ...mats, ...drops]
return <div>
<h4 className="text-base font-semibold pt-1 inline-block pr-1">Talent materials:</h4>
{all.map(e => <div className="inline-block pr-1" key={e}>
{e}
const all = [...books, ...mats, ...drops] as string[]
return <div className="flex flex-row items-center">
<div className="text-base font-semibold pt-1 inline-block pr-1 h-9">Talent materials:</div>
{all.map(e => <div className="inline-block pr-1 w-6 h-6 md:h-8 md:w-8" key={e}>
<Image src={image("material", e)} alt={e} width={256} height={256} />
</div>)}
</div>
}

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

const base = getCharStatsAt(char, 1, 0, curves)
const levels: { a: number, lv: number }[] = []

let prev = 1
for (const asc of char.ascensions) {
levels.push({ a: asc.level, lv: prev })
levels.push({ a: asc.level, lv: asc.maxLevel })
prev = asc.maxLevel
}
const max = getCharStatsAt(char, maxAscension.maxLevel, maxAscension.level, curves)

// TODO
return <>
<h3 className="text-lg font-bold pt-1" id="stats">Stats / Ascensions:</h3>
<table className={`table-auto w-full ${styles.table} ${styles.stattable} mb-2`}>
<thead className="font-semibold divide-x divide-gray-200 dark:divide-gray-500">
<td>Asc.</td>
<td>Lv.</td>
{Object.keys(max).map((name) => <td key={name}>{name}</td>)}
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-500">
{levels.map(({ a, lv }) => <tr className="pr-1 divide-x divide-gray-200 dark:divide-gray-500" key={a + "," + lv}>
<td>A{a}</td>
<td>{lv}</td>
{Object.entries(getCharStatsAt(char, lv, a, curves)).map(([name, value]) => <td key={name}>{stat(name, value)}</td>)}
</tr>)}
</tbody>
</table>
</>
}

return <div className="flex flex-row justify-evenly">
<div>
<h4 className="text-base font-semibold pt-1 inline-block pr-1">Base stats:</h4>
{Object.entries(base)
.map(([name, value]) => <div className="pr-1" key={name}>
{name}: {stat(name, value)}
</div>)}
</div>
<div>
<h4 className="text-base font-semibold pt-1 inline-block pr-1">Max stats:</h4>
{Object.entries(max)
.map(([name, value]) => <div className="pr-1" key={name}>
{name}: {stat(name, value)}
</div>)}
</div>
</div>
function Meta({ meta }: { meta: Meta }) {
return <>
<h3 className="text-lg font-bold pt-1" id="meta">Meta:</h3>
<table className={`table-auto ${styles.table} mb-2`}>
<tbody className="divide-y divide-gray-200 dark:divide-gray-500">
{meta.title && <tr><td>Title</td><td>{meta.title}</td></tr>}
{meta.birthDay && meta.birthMonth && <tr><td>Title</td><td>{
new Date(Date.UTC(2020, meta.birthMonth - 1, meta.birthDay))
.toLocaleString("en-UK", {
timeZone: "UTC",
month: "long",
day: "numeric",
})}</td></tr>}
{meta.association && <tr><td>Association</td><td>{meta.association}</td></tr>}
{meta.affiliation && <tr><td>Affiliation</td><td>{meta.affiliation}</td></tr>}
{meta.constellation && <tr><td>Constellation</td><td>{meta.constellation}</td></tr>}
{meta.element && <tr><td>Element</td><td>{Object.keys(elements).includes(meta.element) ? <>
<div className="w-5 inline-block pr-1">
<Image src={elements[meta.element as ElementType]} alt={`${meta.element} Element`} />
</div>
{meta.element}</> : meta.element}</td></tr>}
{meta.cvChinese && <tr><td>Chinese voice actor</td><td>{meta.cvChinese}</td></tr>}
{meta.cvJapanese && <tr><td>Japanese voice actor</td><td>{meta.cvJapanese}</td></tr>}
{meta.cvEnglish && <tr><td>English voice actor</td><td>{meta.cvEnglish}</td></tr>}
{meta.cvKorean && <tr><td>Korean voice actor</td><td>{meta.cvKorean}</td></tr>}
</tbody>
</table>
</>
}

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

return <div>
<h3 className="text-lg font-bold pt-1">{multiple ? "Videos" : "Video"}:</h3>
return <>
<h3 className="text-lg font-bold pt-1" id="videos">{multiple ? "Videos" : "Video"}:</h3>
{Object.entries(videos).map(([name, link]) => <Video key={name} name={name} link={link} />)}
</div>
</>
}

function Video({ name, link }: { name: string, link: string }) {
Expand Down Expand Up @@ -176,13 +257,22 @@ export async function getStaticProps(context: GetStaticPropsContext): Promise<Ge
if (isFullCharacter(char)) {
const curves = await getCharacterCurves()
if (curves)
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[]>
characterCurves = Object.fromEntries(char.curves.map(c => c.curve).filter((v, i, arr) => arr.indexOf(v) == i).map(c => [c, curves[c]])) as CharacterCurves
}

let costTemplates = null
if (char.ascensionCosts) {
const templates = await getCostTemplates()
if (templates)
costTemplates = Object.fromEntries([char.ascensionCosts.template].filter((v, i, arr) => arr.indexOf(v) == i).map(c => [c, templates[c]])) as CostTemplates

}

return {
props: {
char,
characterCurves
characterCurves,
costTemplates
},
revalidate: 60 * 60
}
Expand Down
6 changes: 3 additions & 3 deletions pages/characters/index.tsx
Expand Up @@ -127,14 +127,14 @@ export default function Characters(props: Props & { location: string }) {
<span className="absolute block p-0.5 top-0 w-full">
<div className="flex flex-col">
{char.element && char.element.map(e => <div key={e} className="w-6 md:w-8">
<Image src={elements[e]} alt={`${e} Element`} />
<Image src={elements[e]} alt={`${e} Element`} loading="eager" />
</div>)}
</div>
</span>
<span className="absolute block p-0.5 top-0 w-full">
<div className="flex flex-col float-right">
{char.weapon && <div className="w-6 md:w-8">
<Image src={weapons[char.weapon]} alt={char.weapon} />
<Image src={weapons[char.weapon]} alt={char.weapon} loading="eager" />
</div>}
</div>
</span>
Expand Down Expand Up @@ -189,7 +189,7 @@ function Icon({ char, className }: { char: SmallChar, className?: string }) {

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

return <Image alt={char.name} src={src} className={className} width={256} height={256} onError={(e) => (e.target as HTMLImageElement).src = "/img/unknown.png"} />
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" />
}

export async function getStaticProps(context: GetStaticPropsContext): Promise<GetStaticPropsResult<Props>> {
Expand Down
11 changes: 11 additions & 0 deletions pages/style.module.css
Expand Up @@ -16,4 +16,15 @@
padding-bottom: "56.2;%";
padding-top: 25;
height: 0;
}

.table tr:nth-child(odd) {
background-color: rgba(71, 85, 105, 0.7);
}
.table td {
padding-right: 0.5rem;
padding-left: 0.25rem;
}
.stattable tbody td {
text-align: right;
}
10 changes: 8 additions & 2 deletions utils/data-cache.ts
@@ -1,17 +1,19 @@
import { Character, CurveEnum, Guide } from "./types"
import { Character, Cost, CurveEnum, Guide } from "./types"

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

type Guides = Guide[]
type Characters = Record<string, Character>
type CharacterCurves = Record<CurveEnum, number[]>
export type CharacterCurves = Record<CurveEnum, number[]>
type CharacterLevels = number[]
export type CostTemplates = Record<string, Cost[]>

type Cache = {
guides: Cacher<Guides>
characters: Cacher<Characters>
characterCurves: Cacher<CharacterCurves>
characterLevels: Cacher<CharacterLevels>
costTemplates: Cacher<CostTemplates>
}

interface Cacher<T> {
Expand All @@ -31,6 +33,9 @@ const cached: Cache = {
},
characterLevels: {
time: 0
},
costTemplates: {
time: 0
}
}
export function urlify(input: string, shouldYeetBrackets: boolean): string {
Expand All @@ -47,6 +52,7 @@ export const getGuides: (() => Promise<Guides | undefined>) = createGetCacheable
export const getCharacters: (() => Promise<Characters | undefined>) = createGetCacheable("characters", "gamedata/characters")
export const getCharacterCurves: (() => Promise<CharacterCurves | undefined>) = createGetCacheable("characterCurves", "gamedata/character_curves")
export const getCharacterLevels: (() => Promise<CharacterLevels | undefined>) = createGetCacheable("characterLevels", "gamedata/character_levels")
export const getCostTemplates: (() => Promise<CostTemplates | undefined>) = createGetCacheable("costTemplates", "gamedata/cost_templates")


function createGetCacheable(name: keyof Cache, path: string = name): () => Promise<any> {
Expand Down
18 changes: 17 additions & 1 deletion utils/utils.ts
Expand Up @@ -10,7 +10,7 @@ import Catalyst from "../public/img/weapon_types/Catalyst.png"
import Claymore from "../public/img/weapon_types/Claymore.png"
import Polearm from "../public/img/weapon_types/Polearm.png"
import Sword from "../public/img/weapon_types/Sword.png"
import { Character, CharacterFull, CurveEnum, WeaponType } from "./types"
import { Character, CharacterFull, Cost, CostTemplate, CurveEnum, WeaponType } from "./types"

export const elements = {
Pyro, Electro, Cryo, Hydro, Anemo, Geo, Dendro
Expand Down Expand Up @@ -81,3 +81,19 @@ export function stat(name: string, value: number): string {
return value < 2 ? ((value * 100).toFixed(1) + "%") : value.toFixed(0)
}
}

export function image(type: string, name: string, ext="png"): string {
return `/img/${type}/${name.replace(/[:\-,'"]/g, "").replace(/ +/g, "_")}.${ext}`
}

export function getCostsFromTemplate(costTemplate: CostTemplate, costTemplates: Record<string, Cost[]>): Cost[] {
const template = costTemplates[costTemplate.template]

return template.map(c => ({
mora: c.mora,
items: c.items.map(i => ({
count: i.count,
name: i.name.replace(/<(.*?)>/g, (_, x) => costTemplate.mapping[x])
}))
}))
}

0 comments on commit 36708af

Please sign in to comment.