22import { GetStaticPathsResult , GetStaticPropsContext , GetStaticPropsResult } from "next"
33import Head from "next/head"
44import Image from "next/image"
5- import { useState } from "react"
5+ import { ReactElement , useState } from "react"
6+ import ReactMarkdown from "react-markdown"
67import FormattedLink from "../../components/FormattedLink"
8+ import Icon from "../../components/Icon"
79import Main from "../../components/Main"
810import YouTube from "../../components/YouTube"
911import { 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"
1214import styles from "../style.module.css"
1315
1416interface Props {
@@ -20,6 +22,11 @@ interface Props {
2022
2123export 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 } →{ 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 } ×</ 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+
274428export 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 ) ] )
0 commit comments