Skip to content

Commit d59da8f

Browse files
committed
Add more char info
1 parent 7ecb429 commit d59da8f

File tree

5 files changed

+214
-40
lines changed

5 files changed

+214
-40
lines changed

components/Icon.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Image from "next/image"
2+
3+
export default function Icon({ icon, className }: { icon: {name: string, icon?: string}, className?: string }) {
4+
const src = icon.icon ?? "img/unknown.png"
5+
6+
if (src.startsWith("img"))
7+
// eslint-disable-next-line @next/next/no-img-element
8+
return <img alt={icon.name} src={"/" + src} className={className} width={256} height={256} onError={(e) => (e.target as HTMLImageElement).src = "/img/unknown.png"} loading="eager" />
9+
10+
return <Image alt={icon.name} src={src} className={className} width={256} height={256} onError={(e) => (e.target as HTMLImageElement).src = "/img/unknown.png"} loading="eager" />
11+
}

pages/characters/[name].tsx

Lines changed: 192 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
import { GetStaticPathsResult, GetStaticPropsContext, GetStaticPropsResult } from "next"
33
import Head from "next/head"
44
import Image from "next/image"
5-
import { useState } from "react"
5+
import { ReactElement, useState } from "react"
6+
import ReactMarkdown from "react-markdown"
67
import FormattedLink from "../../components/FormattedLink"
8+
import Icon from "../../components/Icon"
79
import Main from "../../components/Main"
810
import YouTube from "../../components/YouTube"
911
import { CharacterCurves, CostTemplates, getCharacterCurves, getCharacters, getCostTemplates } from "../../utils/data-cache"
10-
import { Character, CharacterFull, CostTemplate, CurveEnum, Meta, Skills } from "../../utils/types"
11-
import { elements, ElementType, getCharStatsAt, getCostsFromTemplate, getGuidesFor, getLinkToGuide, image, isFullCharacter, stat, urlify, weapons } from "../../utils/utils"
12+
import { Character, CharacterFull, Constellation, CostTemplate, CurveEnum, Meta, Passive, Skill, Skills, TalentTable, TalentValue } from "../../utils/types"
13+
import { elements, ElementType, getCharStatsAt, getCostsFromTemplate, getGuidesFor, getLinkToGuide, image, isFullCharacter, isValueTable, stat, urlify, weapons } from "../../utils/utils"
1214
import styles from "../style.module.css"
1315

1416
interface Props {
@@ -20,6 +22,11 @@ interface Props {
2022

2123
export default function CharacterWebpage({ char, location, characterCurves, costTemplates, guides }: Props & { location: string }) {
2224
const charElems = char.skills?.map(skill => skill.ult?.type).filter(x => x) as ElementType[] ?? [char.meta.element]
25+
const multiskill = (char.skills?.length ?? 0) > 1
26+
let color = ""
27+
if (char.star == 5) color = "bg-amber-600 dark:bg-amber-700"
28+
if (char.star == 4) color = "bg-purple-700 dark:bg-purple-800"
29+
2330
return (
2431
<Main>
2532
<Head>
@@ -34,54 +41,75 @@ export default function CharacterWebpage({ char, location, characterCurves, cost
3441
</FormattedLink>
3542
</h2>
3643

37-
<h1 className="text-4xl font-bold pb-2">
44+
<h1 className="text-4xl font-bold pb-2 sm:sr-only not-sr-only">
45+
<Icon icon={char} className={`${color} rounded-xl sm:w-0 mr-2 w-12 inline-block`} />
3846
{char.name}
3947
</h1>
4048

41-
<div className="float-right border-2 border-slate-500 dark:bg-slate-600 bg-slate-200 m-2 px-2">
49+
<div className="sm:float-right border-2 border-slate-500 dark:bg-slate-600 bg-slate-200 m-2 px-2">
4250
Table of Contents
4351
{isFullCharacter(char) && characterCurves && <TOC href="#stats" title="Stats" />}
4452
{char.ascensionCosts && costTemplates && <TOC href="#ascensions" title="Ascensions" />}
4553
{char.media.videos && <TOC href="#videos" title={Object.keys(char.media.videos).length > 1 ? "Videos" : "Video"} />}
4654
{char.meta && <TOC href="#meta" title="Meta" />}
55+
{char.skills && char.skills.map((s, i) => (<>
56+
{multiskill && <div>{s.ult?.type ?? `Skillset #${i}`}</div>}
57+
{s.talents && <TOC depth={multiskill ? 1 : 0} href={`#talents${i > 0 ? `-${i}` : ""}`} title="Talents" />}
58+
{s.passive && <TOC depth={multiskill ? 1 : 0} href={`#passive${i > 0 ? `-${i}` : ""}`} title="Passive" />}
59+
{s.constellations && <TOC depth={multiskill ? 1 : 0} href={`#const${i > 0 ? `-${i}` : ""}`} title="Constellations" />}
60+
</>))}
4761
</div>
4862

49-
<div id="description">
50-
<blockquote className="pl-5 mb-2 border-l-2">
51-
{char.desc}
52-
</blockquote>
63+
<div className="grid grid-flow-col">
64+
<div className="sm:w-36 mr-2 w-0 ">
65+
<Icon icon={char} className={`${color} rounded-xl`} />
66+
</div>
5367

54-
{charElems.map(e => <div key={e} className="w-5 inline-block pr-1">
55-
<Image src={elements[e]} alt={`${e} Element`} />
56-
</div>)}
68+
<div id="description" className="w-full">
69+
<h1 className="text-4xl font-bold pb-2 sm:not-sr-only sr-only">
70+
{char.name}
71+
</h1>
5772

58-
{char.star && <div className="inline-block pr-2">
59-
{char.star}
60-
</div>}
73+
<blockquote className="pl-5 mb-2 border-l-2">
74+
<ReactMarkdown>{(char.desc?.replace(/ ?\$\{.*?\}/g, "") ?? "")}</ReactMarkdown>
75+
</blockquote>
6176

62-
{char.weaponType ? <div className="inline-block">
63-
<div className="w-5 inline-block pr-1">
64-
<Image src={weapons[char.weaponType]} alt={`${char.weaponType} Element`} />
65-
</div>
77+
{charElems.map(e => <div key={e} className="w-5 inline-block pr-1">
78+
<Image src={elements[e]} alt={`${e} Element`} />
79+
</div>)}
6680

67-
{char.weaponType} user
68-
</div> : <div className="inline-block">Character (unreleased)</div>}
81+
{char.star && <div className="inline-block pr-2">
82+
{char.star}
83+
</div>}
84+
85+
{char.weaponType ? <div className="inline-block">
86+
<div className="w-5 inline-block pr-1">
87+
<Image src={weapons[char.weaponType]} alt={`${char.weaponType} Element`} />
88+
</div>
89+
90+
{char.weaponType} user
91+
</div> : <div className="inline-block">Character (unreleased)</div>}
92+
</div>
93+
</div>
6994

95+
<div id="details">
7096
{char.ascensionCosts && <AscensionCosts costs={char.ascensionCosts} />}
7197
{char.skills && <TalentCosts skills={char.skills} />}
7298
{guides && guides.length > 0 && <Guides guides={guides} />}
7399
{isFullCharacter(char) && characterCurves && <Stats char={char} curves={characterCurves} />}
74100
{char.ascensionCosts && costTemplates && <FullAscensionCosts template={char.ascensionCosts} costTemplates={costTemplates} />}
75101
{char.media.videos && <Videos videos={char.media.videos} />}
76102
{char.meta && <Meta meta={char.meta} />}
103+
{char.skills && costTemplates && <CharacterSkills skills={char.skills} costTemplates={costTemplates} />}
77104
</div>
78105
</Main>
79106
)
80107
}
81108

82-
function TOC({ href, title }: { href: string, title: string }) {
109+
function TOC({ href, title, depth = 0 }: { href: string, title: string, depth?: number }) {
110+
const size = depth > 0 ? "sm" : "base"
83111
return <div>
84-
<FormattedLink href={href} size="base">{title}</FormattedLink>
112+
<FormattedLink href={href} size={size} className={`ml-${depth}`}>{title}</FormattedLink>
85113
</div>
86114
}
87115

@@ -92,7 +120,7 @@ function AscensionCosts({ costs }: { costs: CostTemplate }) {
92120
costs.mapping.Specialty,
93121
costs.mapping.EnemyDropTier3,
94122
].filter(x => x)
95-
return <div className="flex flex-row items-center">
123+
return <div className="flex flex-wrap items-center">
96124
<div className="text-base font-semibold pt-1 inline-block pr-1 h-9">Ascension materials:</div>
97125
{ascensionCosts.map(e => <div className="inline-block pr-1 w-6 h-6 md:h-8 md:w-8" key={e}>
98126
<img src={image("material", e)} alt={e} width={256} height={256} />
@@ -167,7 +195,7 @@ function TalentCosts({ skills }: { skills: Skills[] }) {
167195
.filter((x, i, a) => x && a.indexOf(x) == i)
168196

169197
const all = [...books, ...mats, ...drops] as string[]
170-
return <div className="flex flex-row items-center">
198+
return <div className="flex flex-wrap items-center">
171199
<div className="text-base font-semibold pt-1 inline-block pr-1 h-9">Talent materials:</div>
172200
{all.map(e => <div className="inline-block pr-1 w-6 h-6 md:h-8 md:w-8" key={e}>
173201
<img src={image("material", e)} alt={e} width={256} height={256} />
@@ -271,6 +299,132 @@ function Guides({ guides }: { guides: string[][] }) {
271299
</>
272300
}
273301

302+
function CharacterSkills({ skills, costTemplates }: { skills: Skills[], costTemplates: CostTemplates }) {
303+
return <>
304+
{skills.map((skill, i) => {
305+
return <>
306+
{(skill.talents || skill.ult) && <>
307+
<h3 className="text-lg font-bold pt-1" id={`talents${i > 0 ? `-${i}` : ""}`}>Talents:</h3>
308+
{[...(skill.talents ?? []), skill.ult].map(s => s && <Talent costTemplates={costTemplates} talent={s} key={s.name} />)}
309+
</>}
310+
{skill.passive && <>
311+
<h3 className="text-lg font-bold pt-1" id={`passive${i > 0 ? `-${i}` : ""}`}>Passive:</h3>
312+
{skill.passive.map(p => p && <Passive passive={p} key={p.name} />)}
313+
</>}
314+
{skill.constellations && <>
315+
<h3 className="text-lg font-bold pt-1" id={`const${i > 0 ? `-${i}` : ""}`}>Constellations:</h3>
316+
{skill.constellations.map(c => c && <Constellation c={c} key={c.name} />)}
317+
</>}
318+
</>
319+
})}
320+
</>
321+
}
322+
323+
function Talent({ talent, costTemplates }: { talent: Skill, costTemplates: CostTemplates }) {
324+
325+
return <div>
326+
<div className="font-semibold">{talent.name}</div>
327+
{talent.icon && <Icon icon={talent} className="rounded-xl w-12 inline-block" />}
328+
<ReactMarkdown>{(talent.desc?.replace(/ ?\$\{.*?\}/g, "") ?? "")}</ReactMarkdown>
329+
{talent.talentTable && <TalentTable table={talent.talentTable} />}
330+
{talent.costs && <TalentCost template={talent.costs} costTemplates={costTemplates} />}
331+
{talent.video}
332+
</div>
333+
}
334+
335+
function TalentCost({ template, costTemplates }: { template: CostTemplate, costTemplates: CostTemplates }) {
336+
const costs = getCostsFromTemplate(template, costTemplates)
337+
const maxCostWidth = costs?.reduce((p, c) => Math.max(p, c.items.length), 1) ?? 1
338+
const [expanded, setExpanded] = useState(false)
339+
340+
return <table className={`table-auto w-full ${styles.table} mb-2 ${expanded ? "" : "cursor-pointer"} sm:text-sm md:text-base text-xs`} onClick={() => setExpanded(true)}>
341+
<thead className="font-semibold divide-x divide-gray-200 dark:divide-gray-500">
342+
<td>Lv.</td>
343+
<td>Mora</td>
344+
<td>Items</td>
345+
</thead>
346+
<tbody className="divide-y divide-gray-200 dark:divide-gray-500">
347+
{costs
348+
.map(({ mora, items }, ind) => <tr className="pr-1 divide-x divide-gray-200 dark:divide-gray-500" key={ind}>
349+
<td>{ind + 1}&rarr;{ind + 2}</td>
350+
<td className="text-right">{mora}</td>
351+
{items.map(({ count, name }, i, arr) => <td key={name} colSpan={i == arr.length - 1 ? maxCostWidth - i : 1}>
352+
{count > 0 &&
353+
<div className="flex flex-row align-middle items-center">
354+
<div>{count}&times;</div>
355+
<div className="pr-1 w-8 h-8 sm:h-6 sm:w-6 md:h-8 md:w-8">
356+
<img src={image("material", name)} alt={name} width={256} height={256} />
357+
</div>
358+
<div className="md:text-sm lg:text-base sm:not-sr-only sr-only text-xs">{name}</div>
359+
</div>}
360+
</td>)}
361+
</tr>
362+
)
363+
.filter((_, i, arr) => expanded ? true : (i == arr.length - 1))}
364+
{!expanded && <tr className="pr-1 cursor-pointer text-blue-700 dark:text-blue-300 hover:text-blue-400 dark:hover:text-blue-400 no-underline transition-all duration-200 font-semibold">
365+
<td colSpan={maxCostWidth + 2} style={({ textAlign: "center" })}>Click to expand...</td>
366+
</tr>
367+
}
368+
</tbody>
369+
</table>
370+
}
371+
372+
function TalentTable({ table }: { table: (TalentTable | TalentValue)[] }) {
373+
const maxLevel = table.reduce((p, c) => Math.max(p, isValueTable(c) ? c.values.length : 1), 1)
374+
const levels = []
375+
for (let i = 0; i < maxLevel; i++)
376+
levels.push(i)
377+
378+
function hint(input: string): ReactElement {
379+
return <>
380+
{input.split("").map(x => <>{x}{x.match(/[+/%]/) && <wbr />}</>)}
381+
</>
382+
}
383+
function countUp<T>(arr: T[], v: T, i: number): number {
384+
let j = 1
385+
while (i < arr.length) {
386+
if (arr[++i] == v)
387+
j++
388+
else
389+
break
390+
}
391+
return j
392+
}
393+
394+
return <div className="overflow-x-auto">
395+
<table className={`table-auto w-full ${styles.table} mb-2 sm:text-sm md:text-base text-xs`}>
396+
<thead className="font-semibold divide-x divide-gray-200 dark:divide-gray-500">
397+
<td>Name</td>
398+
{levels.map((i) => <td key={i}>Lv. {i + 1}</td>)}
399+
</thead>
400+
<tbody className="divide-y divide-gray-200 dark:divide-gray-500">
401+
{table
402+
.map(row => <tr className="pr-1 divide-x divide-gray-200 dark:divide-gray-500" key={row.name}>
403+
<td>{row.name}</td>
404+
{isValueTable(row) ? row.values.map((v, i, arr) => arr[i - 1] != v && <td key={i} colSpan={countUp(arr, v, i)} className="text-center">{hint(v)}</td>) : <td colSpan={maxLevel} style={({ textAlign: "center" })}>{hint(row.value)}</td>}
405+
</tr>)}
406+
</tbody>
407+
</table>
408+
</div>
409+
}
410+
411+
function Passive({ passive }: { passive: Passive }) {
412+
return <div>
413+
<div className="font-semibold">{passive.name}</div>
414+
{passive.icon && <Icon icon={passive} className="rounded-xl w-12 inline-block" />}
415+
<ReactMarkdown>{(passive.desc?.replace(/ ?\$\{.*?\}/g, "") ?? "")}</ReactMarkdown>
416+
{passive.minAscension != undefined && <div>Unlocks at ascension {passive.minAscension}</div>}
417+
</div>
418+
}
419+
420+
function Constellation({ c }: { c: Constellation }) {
421+
return <div>
422+
<div className="font-semibold">{c.name}</div>
423+
{c.icon && <Icon icon={c} className="rounded-xl w-12 inline-block" />}
424+
<ReactMarkdown>{(c.desc?.replace(/ ?\$\{.*?\}/g, "") ?? "")}</ReactMarkdown>
425+
</div>
426+
}
427+
274428
export async function getStaticProps(context: GetStaticPropsContext): Promise<GetStaticPropsResult<Props>> {
275429
const charName = context.params?.name
276430
const data = await getCharacters()
@@ -283,19 +437,30 @@ export async function getStaticProps(context: GetStaticPropsContext): Promise<Ge
283437
}
284438
}
285439

440+
const neededTemplates: string[] = []
286441
let characterCurves = null
287442
if (isFullCharacter(char)) {
288443
const curves = await getCharacterCurves()
289444
if (curves)
290445
characterCurves = Object.fromEntries(char.curves.map(c => c.curve).filter((v, i, arr) => arr.indexOf(v) == i).map(c => [c, curves[c]])) as CharacterCurves
446+
447+
neededTemplates.push(...char.skills.flatMap(s => {
448+
const templates = s.talents?.flatMap(t => t.costs?.template ?? []) ?? []
449+
if (s.ult?.costs?.template)
450+
templates.push(s.ult.costs.template)
451+
return templates
452+
}))
291453
}
292454

293455
let costTemplates = null
294456
if (char.ascensionCosts) {
457+
neededTemplates.push(char.ascensionCosts.template)
458+
}
459+
460+
if (neededTemplates.length > 0) {
295461
const templates = await getCostTemplates()
296462
if (templates)
297-
costTemplates = Object.fromEntries([char.ascensionCosts.template].filter((v, i, arr) => arr.indexOf(v) == i).map(c => [c, templates[c]])) as CostTemplates
298-
463+
costTemplates = Object.fromEntries(neededTemplates.filter((v, i, arr) => arr.indexOf(v) == i).map(c => [c, templates[c]])) as CostTemplates
299464
}
300465

301466
const guides = (await getGuidesFor("character", char.name))?.map(({ guide, page }) => [page.name, getLinkToGuide(guide, page)])

pages/characters/index.tsx

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Head from "next/head"
33
import Image from "next/image"
44
import { Dispatch, SetStateAction, useState } from "react"
55
import FormattedLink from "../../components/FormattedLink"
6+
import Icon from "../../components/Icon"
67
import Main from "../../components/Main"
78
import { getCharacters } from "../../utils/data-cache"
89
import { WeaponType } from "../../utils/types"
@@ -123,7 +124,7 @@ export default function Characters(props: Props & { location: string }) {
123124

124125
return <FormattedLink key={char.name} font="bold" size="sm" location={props.location} href={`/characters/${urlify(char.name, false)}`} className="bg-slate-300 dark:bg-slate-800 w-24 sm:w-28 lg:w-32 m-1 relative rounded-xl transition-all duration-100 hover:outline outline-slate-800 dark:outline-slate-300" >
125126
<div className={`${color} rounded-t-xl h-24 sm:h-28 lg:h-32`}>
126-
<Icon char={char} className="rounded-t-xl m-0 p-0" />
127+
<Icon icon={char} className="rounded-t-xl m-0 p-0" />
127128
<span className="absolute block p-0.5 top-0 w-full">
128129
<div className="flex flex-col">
129130
{char.element && char.element.map(e => <div key={e} className="w-6 md:w-8">
@@ -183,17 +184,6 @@ function ToggleButton<T>({ type, value, setter, children }: { type: T[], value:
183184
</div>
184185
}
185186

186-
187-
function Icon({ char, className }: { char: SmallChar, className?: string }) {
188-
const src = char.icon ?? "img/unknown.png"
189-
190-
if (src.startsWith("img"))
191-
// eslint-disable-next-line @next/next/no-img-element
192-
return <img alt={char.name} src={"/" + src} className={className} width={256} height={256} onError={(e) => (e.target as HTMLImageElement).src = "/img/unknown.png"} loading="eager" />
193-
194-
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" />
195-
}
196-
197187
export async function getStaticProps(context: GetStaticPropsContext): Promise<GetStaticPropsResult<Props>> {
198188
const data = await getCharacters()
199189

@@ -220,6 +210,7 @@ export async function getStaticProps(context: GetStaticPropsContext): Promise<Ge
220210
.map(c => {
221211
const char: SmallChar = { name: c.name }
222212
if (c.star) char.stars = c.star
213+
if (c.meta.element) char.element = [c.meta.element as ElementType]
223214
if (c.skills) char.element = c.skills.map(skill => skill.ult?.type).filter(x => x) as ElementType[]
224215
if (c.weaponType) char.weapon = c.weaponType
225216
if (c.icon) char.icon = c.icon

utils/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ export interface Passive {
152152
name: string
153153
desc: string
154154
minAscension?: number
155+
icon?: string
155156
}
156157

157158
export interface Skill {
@@ -162,6 +163,7 @@ export interface Skill {
162163
costs?: CostTemplate
163164
type?: string
164165
video?: string
166+
icon?: string
165167
}
166168

167169
export interface TalentTable {

0 commit comments

Comments
 (0)