Add Battlesuit page
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
import { Image, ThemeUIStyleObject } from 'theme-ui'
|
||||||
|
import { assetsBucketBaseUrl } from '../../lib/consts'
|
||||||
|
import { AttributeType } from '../../lib/v2-pre/data/types'
|
||||||
|
|
||||||
|
interface AttributeIconProps {
|
||||||
|
attributeType: AttributeType
|
||||||
|
size?: number
|
||||||
|
className?: string
|
||||||
|
sx?: ThemeUIStyleObject
|
||||||
|
}
|
||||||
|
|
||||||
|
const AttributeIcon = ({ size, attributeType, className, sx }: AttributeIconProps) => {
|
||||||
|
return (
|
||||||
|
<Image src={getAttributeIconSrc(attributeType)} width={size} className={className} sx={sx} alt={attributeType} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AttributeIcon
|
||||||
|
|
||||||
|
function getAttributeIconSrc(attributeType: AttributeType) {
|
||||||
|
switch (attributeType) {
|
||||||
|
case 'bio':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarattricons/AvatarShengWu.png`
|
||||||
|
case 'psy':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarattricons/AvatarYiNeng.png`
|
||||||
|
case 'mech':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarattricons/AvatarJiXie.png`
|
||||||
|
case 'qua':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarattricons/AvatarLiangZi.png`
|
||||||
|
case 'img':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarattricons/AvatarXuShu.png`
|
||||||
|
case 'none':
|
||||||
|
default:
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarattricons/AvatarNone.png`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Image, ThemeUICSSObject } from 'theme-ui'
|
||||||
|
import { assetsBucketBaseUrl } from '../../lib/consts'
|
||||||
|
|
||||||
|
interface AvatarFigureImageProps {
|
||||||
|
battlesuitId: string
|
||||||
|
className?: string
|
||||||
|
sx?: ThemeUICSSObject
|
||||||
|
}
|
||||||
|
|
||||||
|
const AvatarFigureImage = ({ battlesuitId, className, sx }: AvatarFigureImageProps) => {
|
||||||
|
return <Image src={getAvatarFigureImageSrc(battlesuitId)} className={className} alt={battlesuitId} sx={sx} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AvatarFigureImage
|
||||||
|
|
||||||
|
function getAvatarFigureImageSrc(battlesuitId: string) {
|
||||||
|
if (battlesuitId.length < 4) {
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarcardfigures/60${battlesuitId}.png`
|
||||||
|
}
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarcardfigures/6${battlesuitId}.png`
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Box } from 'theme-ui'
|
||||||
|
import { assetsBucketBaseUrl } from '../../lib/consts'
|
||||||
|
|
||||||
|
interface AvatarSkillIconProps {
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const AvatarSkillIcon = ({ icon: fileName }: AvatarSkillIconProps) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
background: `no-repeat 50%/80% url(${assetsBucketBaseUrl}/raw/skillicons/${fileName}.png)`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AvatarSkillIcon
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Box } from 'theme-ui'
|
||||||
|
import { assetsBucketBaseUrl } from '../../lib/consts'
|
||||||
|
|
||||||
|
interface AvatarSubSkillIconProps {
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const AvatarSubSkillIcon = ({ icon: fileName }: AvatarSubSkillIconProps) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
background: `no-repeat 50%/80% url(${assetsBucketBaseUrl}/raw/subskillicons/${fileName}.png)`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AvatarSubSkillIcon
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Box } from 'theme-ui'
|
||||||
|
import { assetsBucketBaseUrl } from '../../lib/consts'
|
||||||
|
import { AttributeType, BattlesuitCatalogItem } from '../../lib/v2-pre/data/types'
|
||||||
|
|
||||||
|
interface BattlesuitAvatarIconProps {
|
||||||
|
battlesuit: BattlesuitCatalogItem
|
||||||
|
}
|
||||||
|
|
||||||
|
const BattlesuitAvatarIcon = ({ battlesuit }: BattlesuitAvatarIconProps) => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
borderRadius: 5,
|
||||||
|
background: `no-repeat 50% / 130px url(${assetsBucketBaseUrl}/raw/avataricon/${
|
||||||
|
battlesuit.id
|
||||||
|
}.png), no-repeat 50% / 130px url(${getAttributeBgImageSrc(battlesuit.attributeType)})`,
|
||||||
|
width: 130,
|
||||||
|
height: 120
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BattlesuitAvatarIcon
|
||||||
|
|
||||||
|
function getAttributeBgImageSrc(attrId: AttributeType, square = false) {
|
||||||
|
if (square) {
|
||||||
|
switch (attrId) {
|
||||||
|
case 'bio':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avataricon/AttrShengWu1.png`
|
||||||
|
case 'psy':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avataricon/AttrYiNeng1.png`
|
||||||
|
case 'mech':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avataricon/AttrJiXie1.png`
|
||||||
|
case 'qua':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avataricon/AttrLiangZi1.png`
|
||||||
|
case 'img':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avataricon/AttrXuShu1.png`
|
||||||
|
case 'none':
|
||||||
|
default:
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avataricon/AttrDefault.png`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (attrId) {
|
||||||
|
case 'bio':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avataricon/AttrShengWu.png`
|
||||||
|
case 'psy':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avataricon/AttrYiNeng.png`
|
||||||
|
case 'mech':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avataricon/AttrJiXie.png`
|
||||||
|
case 'qua':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avataricon/AttrLiangZi.png`
|
||||||
|
case 'img':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avataricon/AttrXuShu.png`
|
||||||
|
case 'none':
|
||||||
|
default:
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avataricon/AttrDefault.png`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Box } from 'theme-ui'
|
||||||
|
import { BattlesuitCatalogItem } from '../../lib/v2-pre/data/types'
|
||||||
|
import BattlesuitAvatarIcon from './BattlesuitAvatarIcon'
|
||||||
|
|
||||||
|
interface BattlesuitCatalogItemCardProps {
|
||||||
|
battlesuit: BattlesuitCatalogItem
|
||||||
|
label?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const BattlesuitCatalogItemCard = ({ battlesuit, label = false }: BattlesuitCatalogItemCardProps) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
transform: 'scale(1)',
|
||||||
|
transition: 'transform 200ms ease-in-out',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'scale(1.05)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BattlesuitAvatarIcon battlesuit={battlesuit} />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
textAlign: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
width: 110,
|
||||||
|
marginLeft: -10,
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{battlesuit.fullName}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BattlesuitCatalogItemCard
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { Box } from 'theme-ui'
|
||||||
|
import { assetsBucketBaseUrl } from '../../lib/consts'
|
||||||
|
import { CharacterType } from '../../lib/v2-pre/data/types'
|
||||||
|
|
||||||
|
interface ChibiIconProps {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChibiIcon = ({ id }: ChibiIconProps) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
background: `no-repeat 50% 50% / 30px url(${assetsBucketBaseUrl}/raw/avatarchibiicons/${getChibiIconName(
|
||||||
|
id
|
||||||
|
)}.png)`
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChibiIcon
|
||||||
|
|
||||||
|
export function getChibiIconName(idOrType: string | CharacterType) {
|
||||||
|
switch (idOrType) {
|
||||||
|
case 'kiana':
|
||||||
|
return '106'
|
||||||
|
case 'mei':
|
||||||
|
return '206'
|
||||||
|
case 'bronya':
|
||||||
|
return '317'
|
||||||
|
case 'himeko':
|
||||||
|
return '412'
|
||||||
|
case 'theresa':
|
||||||
|
return '502_05_01'
|
||||||
|
case 'fuhua':
|
||||||
|
return '612'
|
||||||
|
case 'rita':
|
||||||
|
return '706'
|
||||||
|
case 'sakura':
|
||||||
|
return '212'
|
||||||
|
case 'kallen':
|
||||||
|
return '111'
|
||||||
|
case 'olenyevas':
|
||||||
|
return '422_05_01'
|
||||||
|
case 'seele':
|
||||||
|
return '713'
|
||||||
|
case 'durandal':
|
||||||
|
return '804'
|
||||||
|
case 'fischl':
|
||||||
|
return '2101'
|
||||||
|
case 'elysia':
|
||||||
|
return '2202'
|
||||||
|
case 'mobius':
|
||||||
|
return '2301'
|
||||||
|
case 'raven':
|
||||||
|
return '2401'
|
||||||
|
case 'carole':
|
||||||
|
return '2501'
|
||||||
|
case 'pardofelis':
|
||||||
|
return '2601'
|
||||||
|
case 'aponia':
|
||||||
|
return '2701'
|
||||||
|
case 'eden':
|
||||||
|
return '2801'
|
||||||
|
case 'griseo':
|
||||||
|
return '2901'
|
||||||
|
case 'vill-v':
|
||||||
|
return '3001'
|
||||||
|
case 'sushang':
|
||||||
|
return '3101'
|
||||||
|
case 'ai':
|
||||||
|
return '3201'
|
||||||
|
case 'susannah':
|
||||||
|
return '3301'
|
||||||
|
case 'hare':
|
||||||
|
return '3401'
|
||||||
|
case 'prometheus':
|
||||||
|
return '3501'
|
||||||
|
case 'kira':
|
||||||
|
return '3601'
|
||||||
|
case 'asuka':
|
||||||
|
return '901'
|
||||||
|
case 'keqing':
|
||||||
|
return '2001'
|
||||||
|
default:
|
||||||
|
return 'IconAvatarQuestion'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { Text } from 'theme-ui'
|
||||||
|
import { FormattedTextToken, tokenize } from '../../lib/v2-pre/formattedText/tokenize'
|
||||||
|
|
||||||
|
interface FormattedTextProps {
|
||||||
|
children: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const FormattedText = ({ children }: FormattedTextProps) => {
|
||||||
|
const tokens = tokenize(children)
|
||||||
|
|
||||||
|
return <>{renderTokens(tokens)}</>
|
||||||
|
|
||||||
|
function renderTokens(tokens: FormattedTextToken[]) {
|
||||||
|
return tokens.map((token, index) => {
|
||||||
|
if (token.type === 'element') {
|
||||||
|
return (
|
||||||
|
<Text key={index} style={{ color: adjustColor(token.attributes.color) }}>
|
||||||
|
{renderTokens(token.children)}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return token.text
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormattedText
|
||||||
|
|
||||||
|
function adjustColor(color: unknown): string | undefined {
|
||||||
|
if (typeof color !== 'string') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const normalizedColor = color.toUpperCase()
|
||||||
|
|
||||||
|
if (normalizedColor.startsWith('#FEDF4C')) {
|
||||||
|
return '#e6c010'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedColor.startsWith('#FFC741')) {
|
||||||
|
return '#f0a030'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
normalizedColor.startsWith('#23B2E') ||
|
||||||
|
normalizedColor.startsWith('#23B2E') ||
|
||||||
|
normalizedColor.startsWith('#23B2E')
|
||||||
|
) {
|
||||||
|
return '#1d9ecc'
|
||||||
|
}
|
||||||
|
|
||||||
|
return color
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Box } from 'theme-ui'
|
||||||
|
import { assetsBucketBaseUrl } from '../../lib/consts'
|
||||||
|
import { StarRank } from '../../lib/v2-pre/data/types'
|
||||||
|
|
||||||
|
interface StarIconProps {
|
||||||
|
star: StarRank
|
||||||
|
}
|
||||||
|
|
||||||
|
const StarIcon = ({ star }: StarIconProps) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
background: `no-repeat 50% / 30px url(${getStarIconPath(star)})`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StarIcon
|
||||||
|
|
||||||
|
function getStarIconPath(star: StarRank) {
|
||||||
|
switch (star) {
|
||||||
|
case 'b':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarstar/Star_Avatar_1.png`
|
||||||
|
case 'a':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarstar/Star_Avatar_2.png`
|
||||||
|
case 's':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarstar/Star_Avatar_3.png`
|
||||||
|
case 'ss':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarstar/Star_Avatar_4.png`
|
||||||
|
case 'sss':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarstar/Star_Avatar_5.png`
|
||||||
|
case 's1':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarstarskill/S1.png`
|
||||||
|
case 's2':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarstarskill/S2.png`
|
||||||
|
case 's3':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarstarskill/S3.png`
|
||||||
|
case 'ss1':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarstarskill/SS1.png`
|
||||||
|
case 'ss2':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarstarskill/SS2.png`
|
||||||
|
case 'ss3':
|
||||||
|
return `${assetsBucketBaseUrl}/raw/avatarstarskill/SS3.png`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { Box } from 'theme-ui'
|
||||||
|
import { assetsBucketBaseUrl } from '../../lib/consts'
|
||||||
|
import { TagType } from '../../lib/v2-pre/data/types'
|
||||||
|
|
||||||
|
interface TagIconProps {
|
||||||
|
type: TagType
|
||||||
|
strength?: 1 | 2 | 3 | 4
|
||||||
|
size?: 'sm' | 'md'
|
||||||
|
comment?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const TagIcon = ({ type, strength, size = 'md', comment }: TagIconProps) => {
|
||||||
|
const boxSizePx = size === 'md' ? '30px' : '20px'
|
||||||
|
const iconSizePx = size === 'md' ? '24px' : '16px'
|
||||||
|
return (
|
||||||
|
<Box sx={{ position: 'relative' }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: '2px',
|
||||||
|
top: '-2px',
|
||||||
|
fontSize: 0,
|
||||||
|
color: 'white',
|
||||||
|
textShadow:
|
||||||
|
'#555 0px 0px 1px, #555 0px 0px 1px, #555 0px 0px 1px,#555 0px 0px 1px, #555 0px 0px 1px, #555 0px 0px 1px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{strength}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
title={comment}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 5,
|
||||||
|
width: boxSizePx,
|
||||||
|
height: boxSizePx,
|
||||||
|
background: `no-repeat 50% / ${iconSizePx} ${iconSizePx} url(${assetsBucketBaseUrl}/raw/newstateicon/${getTagIconFileName(
|
||||||
|
type
|
||||||
|
)}.png), #5a8cb0`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagIcon
|
||||||
|
|
||||||
|
function getTagIconFileName(type: TagType) {
|
||||||
|
switch (type) {
|
||||||
|
case 'branch':
|
||||||
|
return 'State_Branch'
|
||||||
|
case 'charge':
|
||||||
|
return 'State_Charge'
|
||||||
|
case 'physical-dmg':
|
||||||
|
return 'State_Physical'
|
||||||
|
case 'fire-dmg':
|
||||||
|
return 'State_Fire'
|
||||||
|
case 'ice-dmg':
|
||||||
|
return 'State_Ice'
|
||||||
|
case 'lightning-dmg':
|
||||||
|
return 'State_Lightning'
|
||||||
|
case 'freeze':
|
||||||
|
return 'State_Frozen'
|
||||||
|
case 'paralyze':
|
||||||
|
return 'State_Paralysis'
|
||||||
|
case 'stun':
|
||||||
|
return 'State_Stun'
|
||||||
|
case 'ignite':
|
||||||
|
return 'State_Burn'
|
||||||
|
case 'bleed':
|
||||||
|
return 'State_Bleading'
|
||||||
|
case 'heavy-atk':
|
||||||
|
return 'State_Smash'
|
||||||
|
case 'weaken':
|
||||||
|
return 'State_Weak'
|
||||||
|
case 'impair':
|
||||||
|
return 'State_Fragile'
|
||||||
|
case 'float':
|
||||||
|
return 'State_Float'
|
||||||
|
case 'time-mastery':
|
||||||
|
return 'State_WitchTime'
|
||||||
|
case 'gather':
|
||||||
|
return 'State_SlowDown'
|
||||||
|
case 'heal':
|
||||||
|
return 'State_Heal'
|
||||||
|
case 'fast-atk':
|
||||||
|
return 'State_HighFreq'
|
||||||
|
case 'burst':
|
||||||
|
return 'State_Burst'
|
||||||
|
case 'shield':
|
||||||
|
return 'State_Shield'
|
||||||
|
case 'aerial':
|
||||||
|
return 'State_AntiAir'
|
||||||
|
case 'ranged':
|
||||||
|
return 'State_Remote'
|
||||||
|
case 'meele':
|
||||||
|
return 'State_Meele'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Box } from 'theme-ui'
|
||||||
|
import { assetsBucketBaseUrl } from '../../lib/consts'
|
||||||
|
import { WeaponType } from '../../lib/v2-pre/data/types'
|
||||||
|
|
||||||
|
interface WeaponTypeIconProps {
|
||||||
|
type: WeaponType
|
||||||
|
}
|
||||||
|
|
||||||
|
const WeaponTypeIcon = ({ type }: WeaponTypeIconProps) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
borderRadius: 5,
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
background: `no-repeat 50% 50% / 24px url(${assetsBucketBaseUrl}/raw/weapontypeicons/${getWeaponIconPath(
|
||||||
|
type
|
||||||
|
)}.png), #5a8cb0`
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default WeaponTypeIcon
|
||||||
|
|
||||||
|
function getWeaponIconPath(type: WeaponType) {
|
||||||
|
switch (type) {
|
||||||
|
case 'pistols':
|
||||||
|
return 'DoublePistolType'
|
||||||
|
case 'cannon':
|
||||||
|
return 'MissileType'
|
||||||
|
case 'katana':
|
||||||
|
return 'SwordType'
|
||||||
|
case 'cross':
|
||||||
|
return 'CrossType'
|
||||||
|
case '2-handed':
|
||||||
|
return 'ClaymoreType'
|
||||||
|
case 'scythe':
|
||||||
|
return 'ScytheType'
|
||||||
|
case 'lance':
|
||||||
|
return 'LanceType'
|
||||||
|
case 'fists':
|
||||||
|
return 'FistType'
|
||||||
|
case 'bow':
|
||||||
|
return 'BowType'
|
||||||
|
case 'chakram':
|
||||||
|
return 'BladeRingType'
|
||||||
|
case 'javelin':
|
||||||
|
return 'JavelinType'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { formatSubSkillInfo } from './formatText'
|
||||||
|
|
||||||
|
describe('formatSubSkillInfo', () => {
|
||||||
|
it('prepends 0 if the calculated value below 0', () => {
|
||||||
|
const params = {
|
||||||
|
info: '필살기 꿈 세계 창조는 추가로 SP를 소모해 물리 대미지가 추가 소모 SP*#1[f2]%만큼 증가하고, 크리티컬률이 추가 소모 SP*#2[f2]%만큼 증가한다(최대 추가 소모 SP: 50pt). 해당 효과는 피니쉬가 종료될 때까지 지속되며, 캐릭터가 사망하거나 퇴장 시 해제된다.',
|
||||||
|
maxLv: 11,
|
||||||
|
paramBase1: 0.004,
|
||||||
|
paramBase2: 0.0025,
|
||||||
|
paramBase3: 0,
|
||||||
|
paramAdd1: 0.0003,
|
||||||
|
paramAdd2: 0.00025,
|
||||||
|
paramAdd3: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = formatSubSkillInfo(params)
|
||||||
|
|
||||||
|
expect(result).toBe(
|
||||||
|
'필살기 꿈 세계 창조는 추가로 SP를 소모해 물리 대미지가 추가 소모 SP*<color=#23B2E3FF>0.70%</color>만큼 증가하고, 크리티컬률이 추가 소모 SP*<color=#23B2E3FF>0.50%</color>만큼 증가한다(최대 추가 소모 SP: 50pt). 해당 효과는 피니쉬가 종료될 때까지 지속되며, 캐릭터가 사망하거나 퇴장 시 해제된다.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('appends 0, based on precision, if the calculated value does not have decimals', () => {
|
||||||
|
const params = {
|
||||||
|
info: '전투 중 초기 SP가 #1[f1] 증가하며, 오픈월드에서는 10분에 1회 발동한다.',
|
||||||
|
|
||||||
|
maxLv: 11,
|
||||||
|
paramBase1: 20,
|
||||||
|
paramBase2: 0,
|
||||||
|
paramBase3: 0,
|
||||||
|
paramAdd1: 2,
|
||||||
|
paramAdd2: 0,
|
||||||
|
paramAdd3: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = formatSubSkillInfo(params)
|
||||||
|
|
||||||
|
expect(result).toBe('전투 중 초기 SP가 <color=#23B2E3FF>40.0</color> 증가하며, 오픈월드에서는 10분에 1회 발동한다.')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { BattlesuitSkill } from './types'
|
||||||
|
|
||||||
|
export function formatSkillInfo(skill: BattlesuitSkill) {
|
||||||
|
let param1 = 0
|
||||||
|
let param2 = 0
|
||||||
|
let param3 = 0
|
||||||
|
if (skill.paramSubId1 !== 0) {
|
||||||
|
const subSkill = skill.subSkills.find(subSkill => subSkill.id === skill.paramSubId1.toString())
|
||||||
|
if (subSkill != null) {
|
||||||
|
param1 = calculateParams(subSkill).param1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (skill.paramSubId2 !== 0) {
|
||||||
|
const subSkill = skill.subSkills.find(subSkill => subSkill.id === skill.paramSubId2.toString())
|
||||||
|
if (subSkill != null) {
|
||||||
|
param2 = calculateParams(subSkill).param2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (skill.paramSubId3 !== 0) {
|
||||||
|
const subSkill = skill.subSkills.find(subSkill => subSkill.id === skill.paramSubId1.toString())
|
||||||
|
if (subSkill != null) {
|
||||||
|
param1 = calculateParams(subSkill).param3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const params = {
|
||||||
|
param1,
|
||||||
|
param2,
|
||||||
|
param3
|
||||||
|
}
|
||||||
|
|
||||||
|
return replaceParams(replaceNewLine(skill.info), params)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSubSkillInfo({
|
||||||
|
info,
|
||||||
|
maxLv,
|
||||||
|
paramBase1,
|
||||||
|
paramBase2,
|
||||||
|
paramBase3,
|
||||||
|
paramAdd1,
|
||||||
|
paramAdd2,
|
||||||
|
paramAdd3
|
||||||
|
}: {
|
||||||
|
info: string
|
||||||
|
maxLv: number
|
||||||
|
paramBase1: number
|
||||||
|
paramBase2: number
|
||||||
|
paramBase3: number
|
||||||
|
paramAdd1: number
|
||||||
|
paramAdd2: number
|
||||||
|
paramAdd3: number
|
||||||
|
}) {
|
||||||
|
return replaceParams(
|
||||||
|
replaceNewLine(info),
|
||||||
|
calculateParams({
|
||||||
|
maxLv,
|
||||||
|
paramBase1,
|
||||||
|
paramBase2,
|
||||||
|
paramBase3,
|
||||||
|
paramAdd1,
|
||||||
|
paramAdd2,
|
||||||
|
paramAdd3
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceNewLine(value: string = '') {
|
||||||
|
return value.replace(/\\n/g, '\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToPercentage(value: number) {
|
||||||
|
return value * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimValue(value: number, precision: number = 1): string {
|
||||||
|
if (precision === 0) {
|
||||||
|
return Math.round(value).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const rounded = Math.round(value * Math.pow(10, precision)).toString()
|
||||||
|
|
||||||
|
return `${rounded.slice(0, -1 * precision) || '0'}.${rounded.slice(-1 * precision)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateParams(params: {
|
||||||
|
maxLv: number
|
||||||
|
paramBase1: number
|
||||||
|
paramBase2: number
|
||||||
|
paramBase3: number
|
||||||
|
paramAdd1: number
|
||||||
|
paramAdd2: number
|
||||||
|
paramAdd3: number
|
||||||
|
}): {
|
||||||
|
param1: number
|
||||||
|
param2: number
|
||||||
|
param3: number
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
param1: params.paramBase1 + (params.maxLv - 1) * params.paramAdd1,
|
||||||
|
param2: params.paramBase2 + (params.maxLv - 1) * params.paramAdd2,
|
||||||
|
param3: params.paramBase3 + (params.maxLv - 1) * params.paramAdd3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const paramRegex = /#([123])\[f([0-9])\](%)?/
|
||||||
|
function replaceParams(
|
||||||
|
text: string,
|
||||||
|
params: {
|
||||||
|
param1: number
|
||||||
|
param2: number
|
||||||
|
param3: number
|
||||||
|
}
|
||||||
|
): string {
|
||||||
|
const result = paramRegex.exec(text)
|
||||||
|
if (result == null) {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
const paramNumber = result[1]
|
||||||
|
const precision = parseInt(result[2], 10)
|
||||||
|
const asPercentage = result[3]!!
|
||||||
|
|
||||||
|
const value = paramNumber === '1' ? params.param1 : paramNumber === '2' ? params.param2 : params.param3
|
||||||
|
|
||||||
|
const valueString = asPercentage
|
||||||
|
? trimValue(convertToPercentage(value), precision) + '%'
|
||||||
|
: trimValue(value, precision)
|
||||||
|
|
||||||
|
const replacedText = text.replace(paramRegex, '<color=#23B2E3FF>' + valueString + '</color>')
|
||||||
|
|
||||||
|
return replaceParams(replacedText, params)
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import { AttributeType, CharacterType, SkillType, TagType, WeaponType } from './types'
|
||||||
|
|
||||||
|
export function getAttributeLabel(type: AttributeType) {
|
||||||
|
switch (type) {
|
||||||
|
case 'bio':
|
||||||
|
return '생물'
|
||||||
|
case 'psy':
|
||||||
|
return '이능'
|
||||||
|
case 'mech':
|
||||||
|
return '기계'
|
||||||
|
case 'qua':
|
||||||
|
return '양자'
|
||||||
|
case 'img':
|
||||||
|
return '허수'
|
||||||
|
case 'none':
|
||||||
|
return '무속성'
|
||||||
|
default:
|
||||||
|
return `Unknown Attribute Type (${type})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWeaponTypeLabel(type: WeaponType) {
|
||||||
|
switch (type) {
|
||||||
|
case 'pistols':
|
||||||
|
return '쌍권총'
|
||||||
|
case 'katana':
|
||||||
|
return '태도'
|
||||||
|
case 'cannon':
|
||||||
|
return '대포'
|
||||||
|
case 'cross':
|
||||||
|
return '십자가'
|
||||||
|
case '2-handed':
|
||||||
|
return '대검'
|
||||||
|
case 'scythe':
|
||||||
|
return '낫'
|
||||||
|
case 'lance':
|
||||||
|
return '랜스'
|
||||||
|
case 'fists':
|
||||||
|
return '건틀릿'
|
||||||
|
case 'bow':
|
||||||
|
return '활'
|
||||||
|
case 'chakram':
|
||||||
|
return '차크람'
|
||||||
|
case 'javelin':
|
||||||
|
return '재블린'
|
||||||
|
default:
|
||||||
|
return `Unknown Weapon Type (${type})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSkillTypeLabel(type: SkillType) {
|
||||||
|
switch (type) {
|
||||||
|
case 'leader':
|
||||||
|
return '리더 스킬'
|
||||||
|
case 'passive':
|
||||||
|
return '패시브 스킬'
|
||||||
|
case 'evasion':
|
||||||
|
return '회피'
|
||||||
|
case 'special':
|
||||||
|
return '기본 공격'
|
||||||
|
case 'ultimate':
|
||||||
|
return '필살기'
|
||||||
|
case 'basic':
|
||||||
|
return '기본 공격'
|
||||||
|
case 'sp':
|
||||||
|
return 'SP 스킬'
|
||||||
|
case 'none':
|
||||||
|
return '미분류'
|
||||||
|
default:
|
||||||
|
return `Unknown Skill Type (${type})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCharacterTypeLabel(type: CharacterType) {
|
||||||
|
switch (type) {
|
||||||
|
case 'kiana':
|
||||||
|
return '키아나'
|
||||||
|
case 'mei':
|
||||||
|
return '메이'
|
||||||
|
case 'bronya':
|
||||||
|
return '브로냐'
|
||||||
|
case 'himeko':
|
||||||
|
return '히메코'
|
||||||
|
case 'theresa':
|
||||||
|
return '테레사'
|
||||||
|
case 'fuhua':
|
||||||
|
return '후카'
|
||||||
|
case 'rita':
|
||||||
|
return '리타'
|
||||||
|
case 'sakura':
|
||||||
|
return '사쿠라'
|
||||||
|
case 'kallen':
|
||||||
|
return '카렌'
|
||||||
|
case 'olenyevas':
|
||||||
|
return '아린 자매'
|
||||||
|
case 'seele':
|
||||||
|
return '제레'
|
||||||
|
case 'durandal':
|
||||||
|
return '듀란달'
|
||||||
|
case 'fischl':
|
||||||
|
return '피슬'
|
||||||
|
case 'elysia':
|
||||||
|
return '엘리시아'
|
||||||
|
case 'mobius':
|
||||||
|
return '뫼비우스'
|
||||||
|
case 'raven':
|
||||||
|
return '레이븐'
|
||||||
|
case 'carole':
|
||||||
|
return '캐롤'
|
||||||
|
case 'pardofelis':
|
||||||
|
return '파르도펠리스'
|
||||||
|
case 'aponia':
|
||||||
|
return '아포니아'
|
||||||
|
case 'eden':
|
||||||
|
return '에덴'
|
||||||
|
case 'griseo':
|
||||||
|
return '그리세오'
|
||||||
|
case 'vill-v':
|
||||||
|
return '빌브이'
|
||||||
|
case 'sushang':
|
||||||
|
return '소상'
|
||||||
|
case 'ai':
|
||||||
|
return '아이'
|
||||||
|
case 'susannah':
|
||||||
|
return '수잔나'
|
||||||
|
case 'hare':
|
||||||
|
return '래빗'
|
||||||
|
case 'prometheus':
|
||||||
|
return '프로메테우스'
|
||||||
|
case 'kira':
|
||||||
|
return '키라'
|
||||||
|
case 'asuka':
|
||||||
|
return '아스카'
|
||||||
|
|
||||||
|
// Event Only
|
||||||
|
case 'keqing':
|
||||||
|
return '각청'
|
||||||
|
|
||||||
|
// APHO
|
||||||
|
|
||||||
|
case 'apho-mei':
|
||||||
|
return '후서 메이'
|
||||||
|
case 'apho-adam':
|
||||||
|
return '후서 아담'
|
||||||
|
case 'apho-carol':
|
||||||
|
return '후서 캐롤'
|
||||||
|
case 'apho-bronya':
|
||||||
|
return '후서 브로냐'
|
||||||
|
case 'apho-timido':
|
||||||
|
return '후서 티미도'
|
||||||
|
|
||||||
|
default:
|
||||||
|
return `Unknown Character Type (${type})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTagTypeLabel(type: TagType) {
|
||||||
|
switch (type) {
|
||||||
|
case 'branch':
|
||||||
|
return '분기'
|
||||||
|
case 'charge':
|
||||||
|
return '차지'
|
||||||
|
case 'physical-dmg':
|
||||||
|
return '물리'
|
||||||
|
case 'fire-dmg':
|
||||||
|
return '화염 대미지'
|
||||||
|
case 'ice-dmg':
|
||||||
|
return '빙결 대미지'
|
||||||
|
case 'lightning-dmg':
|
||||||
|
return '뇌전 대미지'
|
||||||
|
case 'freeze':
|
||||||
|
return '빙결'
|
||||||
|
case 'paralyze':
|
||||||
|
return '마비'
|
||||||
|
case 'stun':
|
||||||
|
return '기절'
|
||||||
|
case 'ignite':
|
||||||
|
return '점화'
|
||||||
|
case 'bleed':
|
||||||
|
return '출혈'
|
||||||
|
case 'heavy-atk':
|
||||||
|
return '강타'
|
||||||
|
case 'weaken':
|
||||||
|
return '허약'
|
||||||
|
case 'impair':
|
||||||
|
return '취약'
|
||||||
|
case 'float':
|
||||||
|
return '띄움'
|
||||||
|
case 'slow-down':
|
||||||
|
return '감속'
|
||||||
|
case 'time-mastery':
|
||||||
|
return '시공'
|
||||||
|
case 'gather':
|
||||||
|
return '흡인'
|
||||||
|
case 'heal':
|
||||||
|
return '치료'
|
||||||
|
case 'fast-atk':
|
||||||
|
return '높은 빈도'
|
||||||
|
case 'aerial':
|
||||||
|
return '버스트'
|
||||||
|
case 'burst':
|
||||||
|
return '실드'
|
||||||
|
case 'shield':
|
||||||
|
return '대공'
|
||||||
|
case 'meele':
|
||||||
|
return '근접'
|
||||||
|
case 'ranged':
|
||||||
|
return '원거리'
|
||||||
|
default:
|
||||||
|
return `Unknown Tag Type (${type})`
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
export type StarRank = 'b' | 'a' | 's' | 's1' | 's2' | 's3' | 'ss' | 'ss1' | 'ss2' | 'ss3' | 'sss'
|
||||||
|
export type AttributeType = 'bio' | 'mech' | 'psy' | 'qua' | 'img' | 'none'
|
||||||
|
export type WeaponType =
|
||||||
|
| 'pistols'
|
||||||
|
| 'cannon'
|
||||||
|
| 'katana'
|
||||||
|
| 'cross'
|
||||||
|
| '2-handed'
|
||||||
|
| 'scythe'
|
||||||
|
| 'lance'
|
||||||
|
| 'fists'
|
||||||
|
| 'bow'
|
||||||
|
| 'chakram'
|
||||||
|
| 'javelin'
|
||||||
|
|
||||||
|
export type CharacterType =
|
||||||
|
| 'kiana'
|
||||||
|
| 'mei'
|
||||||
|
| 'bronya'
|
||||||
|
| 'himeko'
|
||||||
|
| 'theresa'
|
||||||
|
| 'fuhua'
|
||||||
|
| 'rita'
|
||||||
|
| 'sakura'
|
||||||
|
| 'kallen'
|
||||||
|
| 'olenyevas'
|
||||||
|
| 'seele'
|
||||||
|
| 'durandal'
|
||||||
|
| 'fischl'
|
||||||
|
| 'elysia'
|
||||||
|
| 'mobius'
|
||||||
|
| 'raven'
|
||||||
|
| 'carole'
|
||||||
|
| 'pardofelis'
|
||||||
|
| 'aponia'
|
||||||
|
| 'eden'
|
||||||
|
| 'griseo'
|
||||||
|
| 'vill-v'
|
||||||
|
| 'sushang'
|
||||||
|
| 'ai'
|
||||||
|
| 'susannah'
|
||||||
|
| 'hare'
|
||||||
|
| 'prometheus'
|
||||||
|
| 'kira'
|
||||||
|
| 'asuka'
|
||||||
|
|
||||||
|
// Event Only
|
||||||
|
| 'keqing'
|
||||||
|
|
||||||
|
// APHO
|
||||||
|
| 'apho-mei'
|
||||||
|
| 'apho-adam'
|
||||||
|
| 'apho-carol'
|
||||||
|
| 'apho-bronya'
|
||||||
|
| 'apho-timido'
|
||||||
|
|
||||||
|
export type SkillType = 'leader' | 'passive' | 'evasion' | 'special' | 'ultimate' | 'basic' | 'sp' | 'none'
|
||||||
|
|
||||||
|
export type TagType =
|
||||||
|
| 'branch'
|
||||||
|
| 'charge'
|
||||||
|
| 'physical-dmg'
|
||||||
|
| 'fire-dmg'
|
||||||
|
| 'ice-dmg'
|
||||||
|
| 'lightning-dmg'
|
||||||
|
| 'freeze'
|
||||||
|
| 'paralyze'
|
||||||
|
| 'stun'
|
||||||
|
| 'ignite'
|
||||||
|
| 'bleed'
|
||||||
|
| 'heavy-atk'
|
||||||
|
| 'weaken'
|
||||||
|
| 'impair'
|
||||||
|
| 'float'
|
||||||
|
| 'slow-down'
|
||||||
|
| 'time-mastery'
|
||||||
|
| 'gather'
|
||||||
|
| 'heal'
|
||||||
|
| 'fast-atk'
|
||||||
|
| 'aerial'
|
||||||
|
| 'burst'
|
||||||
|
| 'shield'
|
||||||
|
| 'meele'
|
||||||
|
| 'ranged'
|
||||||
|
|
||||||
|
export interface Battlesuit {
|
||||||
|
id: string
|
||||||
|
fullName: string
|
||||||
|
shortName: string
|
||||||
|
desc: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
enFirstName: string
|
||||||
|
enLastName: string
|
||||||
|
isEasterner: boolean
|
||||||
|
initialStar: StarRank
|
||||||
|
attributeType: AttributeType
|
||||||
|
weapon: WeaponType
|
||||||
|
character: CharacterType
|
||||||
|
skills: BattlesuitSkill[]
|
||||||
|
tags: TagType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BattlesuitCatalogItem = Pick<
|
||||||
|
Battlesuit,
|
||||||
|
'id' | 'fullName' | 'attributeType' | 'weapon' | 'character' | 'initialStar'
|
||||||
|
>
|
||||||
|
|
||||||
|
export interface SkillTagItem {
|
||||||
|
type: TagType
|
||||||
|
strength: 1 | 2 | 3 | 4
|
||||||
|
comment: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BattlesuitSkill {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
info: string
|
||||||
|
icon: string
|
||||||
|
skillType: SkillType
|
||||||
|
tags: SkillTagItem[]
|
||||||
|
paramSubId1: number
|
||||||
|
paramSubId2: number
|
||||||
|
paramSubId3: number
|
||||||
|
subSkills: BattlesuitSubSkill[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BattlesuitSubSkill {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
info: string
|
||||||
|
brief: string
|
||||||
|
icon: string
|
||||||
|
maxLv: number
|
||||||
|
tags: SkillTagItem[]
|
||||||
|
paramBase1: number
|
||||||
|
paramBase2: number
|
||||||
|
paramBase3: number
|
||||||
|
paramAdd1: number
|
||||||
|
paramAdd2: number
|
||||||
|
paramAdd3: number
|
||||||
|
unlockStar: StarRank
|
||||||
|
toggle: boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { BattlesuitSkill } from './types'
|
||||||
|
|
||||||
|
export function sortBattlesuitSkill(a: BattlesuitSkill, b: BattlesuitSkill) {
|
||||||
|
return reversedSkillTypeOrder.indexOf(b.skillType) - reversedSkillTypeOrder.indexOf(a.skillType)
|
||||||
|
}
|
||||||
|
|
||||||
|
const reversedSkillTypeOrder = ['leader', 'passive', 'evasion', 'special', 'ultimate', 'basic', 'sp', 'none'].reverse()
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { tokenize } from './tokenize'
|
||||||
|
|
||||||
|
describe('tokenize', () => {
|
||||||
|
it('tokenize', () => {
|
||||||
|
const rawString =
|
||||||
|
'기본 공격 또는 분기 공격이 적에게 연소 게이지를 <color=#FEDF4CFF><color=#23B2E3FF>4.0</color>pt 누적한다.</color> 발동 간격: <color=#23B2E3FF>6.0</color>초. 기본 공격과 분기 공격이 가하는 화염 원소 대미지가 <color=#23B2E3FF>30.0%</color> 증가한다.'
|
||||||
|
|
||||||
|
const result = tokenize(rawString)
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: '기본 공격 또는 분기 공격이 적에게 연소 게이지를 '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
tagName: 'color',
|
||||||
|
attributes: {
|
||||||
|
color: '#FEDF4CFF'
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
tagName: 'color',
|
||||||
|
attributes: { color: '#23B2E3FF' },
|
||||||
|
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: '4.0'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: 'pt 누적한다.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: ' 발동 간격: '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
tagName: 'color',
|
||||||
|
attributes: { color: '#23B2E3FF' },
|
||||||
|
children: [{ type: 'text', text: '6.0' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: '초. 기본 공격과 분기 공격이 가하는 화염 원소 대미지가 '
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'element',
|
||||||
|
tagName: 'color',
|
||||||
|
attributes: { color: '#23B2E3FF' },
|
||||||
|
children: [{ type: 'text', text: '30.0%' }]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: ' 증가한다.'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
export type TextToken = {
|
||||||
|
type: 'text'
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
export type ElementToken = {
|
||||||
|
type: 'element'
|
||||||
|
tagName: string
|
||||||
|
children: FormattedTextToken[]
|
||||||
|
attributes: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FormattedTextToken = TextToken | ElementToken
|
||||||
|
|
||||||
|
export function tokenize(value: string) {
|
||||||
|
let tokens: FormattedTextToken[] = []
|
||||||
|
let currentValue = value.replace(/{{/g, '').replace(/}}/g, '')
|
||||||
|
|
||||||
|
while (currentValue.length > 0) {
|
||||||
|
const openingBracketStartIndex = currentValue.indexOf('<')
|
||||||
|
if (openingBracketStartIndex < 0) {
|
||||||
|
tokens.push({ type: 'text', text: currentValue })
|
||||||
|
eat(openingBracketStartIndex)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openingBracketStartIndex > 0) {
|
||||||
|
const beforeBracket = currentValue.slice(0, openingBracketStartIndex)
|
||||||
|
tokens.push({ type: 'text', text: beforeBracket })
|
||||||
|
eat(openingBracketStartIndex)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const openingBracketEndIndex = currentValue.indexOf('>')
|
||||||
|
if (openingBracketEndIndex < 0) {
|
||||||
|
tokens.push({ type: 'text', text: currentValue })
|
||||||
|
eat(currentValue.length)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const [tagName, attr] = currentValue.slice(openingBracketStartIndex + 1, openingBracketEndIndex).split('=')
|
||||||
|
|
||||||
|
// Child exists try to find end
|
||||||
|
const nextClosingBracketStartIndex = findClosingBracketStartIndex(currentValue)
|
||||||
|
const children = tokenize(currentValue.slice(openingBracketEndIndex + 1, nextClosingBracketStartIndex))
|
||||||
|
tokens.push({
|
||||||
|
type: 'element',
|
||||||
|
tagName,
|
||||||
|
attributes: {
|
||||||
|
color: attr
|
||||||
|
},
|
||||||
|
children
|
||||||
|
})
|
||||||
|
const nextClosingBracketEndIndex = currentValue.indexOf('>', nextClosingBracketStartIndex)
|
||||||
|
eat(nextClosingBracketEndIndex + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
function eat(length: number) {
|
||||||
|
currentValue = currentValue.slice(length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findClosingBracketStartIndex(value: string): number {
|
||||||
|
let currentValue = value
|
||||||
|
let cursor = 0
|
||||||
|
let depth = 0
|
||||||
|
let nextOpeningBracketStartIndex = -1
|
||||||
|
let nextClosingBracketStartIndex = -1
|
||||||
|
|
||||||
|
while (cursor < value.length) {
|
||||||
|
nextOpeningBracketStartIndex = currentValue.indexOf('<', cursor)
|
||||||
|
nextClosingBracketStartIndex = currentValue.indexOf('</', cursor)
|
||||||
|
cursor = nextOpeningBracketStartIndex
|
||||||
|
if (nextClosingBracketStartIndex === -1) {
|
||||||
|
return value.length
|
||||||
|
}
|
||||||
|
if (nextOpeningBracketStartIndex === nextClosingBracketStartIndex) {
|
||||||
|
depth -= 1
|
||||||
|
|
||||||
|
if (depth === 0) {
|
||||||
|
return nextClosingBracketStartIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextClosingBracketEndIndex = currentValue.indexOf('>', cursor)
|
||||||
|
cursor = nextClosingBracketEndIndex
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextOpeningBracketCloseIndex = currentValue.indexOf('>', nextOpeningBracketStartIndex)
|
||||||
|
if (nextOpeningBracketCloseIndex === -1) {
|
||||||
|
return value.length
|
||||||
|
}
|
||||||
|
cursor = nextOpeningBracketCloseIndex
|
||||||
|
depth += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Failed to find closing bracket. The value might be malformed?')
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import { NextPageContext } from 'next'
|
||||||
|
import { Box, Card, Flex, Heading } from 'theme-ui'
|
||||||
|
import AttributeIcon from '../../../components/v2-pre/AttributeIcon'
|
||||||
|
import AvatarFigureImage from '../../../components/v2-pre/AvatarFigureImage'
|
||||||
|
import AvatarSkillIcon from '../../../components/v2-pre/AvatarSkillIcon'
|
||||||
|
import AvatarSubSkillIcon from '../../../components/v2-pre/AvatarSubSkillIcon'
|
||||||
|
import BattlesuitAvatarIcon from '../../../components/v2-pre/BattlesuitAvatarIcon'
|
||||||
|
import FormattedText from '../../../components/v2-pre/FormattedText'
|
||||||
|
import { formatSkillInfo, formatSubSkillInfo } from '../../../lib/v2-pre/data/formatText'
|
||||||
|
import { loadBattlesuitCatalog, loadBattlesuitData } from '../../../lib/v2-pre/server/loadData'
|
||||||
|
import { Battlesuit, SkillTagItem } from '../../../lib/v2-pre/data/types'
|
||||||
|
import { Fragment } from 'react'
|
||||||
|
import TagIcon from '../../../components/v2-pre/TagIcon'
|
||||||
|
import {
|
||||||
|
getAttributeLabel,
|
||||||
|
getCharacterTypeLabel,
|
||||||
|
getSkillTypeLabel,
|
||||||
|
getTagTypeLabel,
|
||||||
|
getWeaponTypeLabel
|
||||||
|
} from '../../../lib/v2-pre/data/text'
|
||||||
|
import StarIcon from '../../../components/v2-pre/StarIcon'
|
||||||
|
import ChibiIcon from '../../../components/v2-pre/ChibiIcon'
|
||||||
|
import WeaponTypeIcon from '../../../components/v2-pre/WeaponTypeIcon'
|
||||||
|
import { sortBattlesuitSkill } from '../../../lib/v2-pre/data/utils'
|
||||||
|
|
||||||
|
interface BattlesuitShowPageProps {
|
||||||
|
battlesuit: Battlesuit
|
||||||
|
}
|
||||||
|
|
||||||
|
const BattlesuitShowPage = ({ battlesuit }: BattlesuitShowPageProps) => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Heading as="h1">{battlesuit.fullName}</Heading>
|
||||||
|
|
||||||
|
<AvatarFigureImage battlesuitId={battlesuit.id} sx={{ height: 400 }} />
|
||||||
|
|
||||||
|
<Box mb={3}>
|
||||||
|
<BattlesuitAvatarIcon battlesuit={battlesuit} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Card mb={3}>
|
||||||
|
<Flex sx={{ borderBottom: 'default', p: 1 }}>
|
||||||
|
<Flex sx={{ alignItems: 'center' }}>
|
||||||
|
<Box mr={2}>
|
||||||
|
<StarIcon star={battlesuit.initialStar} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<ChibiIcon id={battlesuit.character} />
|
||||||
|
|
||||||
|
<Box ml={1} mr={2}>
|
||||||
|
{getCharacterTypeLabel(battlesuit.character)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<AttributeIcon attributeType={battlesuit.attributeType} size={30} />
|
||||||
|
<Box ml={1} mr={2}>
|
||||||
|
{getAttributeLabel(battlesuit.attributeType)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<WeaponTypeIcon type={battlesuit.weapon} />
|
||||||
|
<Box ml={1}>{getWeaponTypeLabel(battlesuit.weapon)}</Box>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex sx={{ p: 1 }}>
|
||||||
|
{battlesuit.tags.map(tag => {
|
||||||
|
return (
|
||||||
|
<Flex key={tag} sx={{ mr: 2, alignItems: 'center' }}>
|
||||||
|
<TagIcon type={tag} />
|
||||||
|
<Box ml={1}>{getTagTypeLabel(tag)}</Box>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Heading as="h2">Skills</Heading>
|
||||||
|
{battlesuit.skills.sort(sortBattlesuitSkill).map(skill => {
|
||||||
|
return (
|
||||||
|
<Card key={skill.id} mb={3}>
|
||||||
|
<Box sx={{ borderBottom: 'default', p: 1 }}>
|
||||||
|
<Flex sx={{ alignItems: 'center' }}>
|
||||||
|
<AvatarSkillIcon icon={skill.icon} />
|
||||||
|
<Heading as="h3" sx={{ ml: 1, mb: 0 }}>
|
||||||
|
<FormattedText>{skill.name}</FormattedText>
|
||||||
|
</Heading>
|
||||||
|
{skill.tags.length > 0 && (
|
||||||
|
<Flex sx={{ ml: 1 }}>
|
||||||
|
{skill.tags.map((tag, index) => {
|
||||||
|
return <SkillTagItem key={index} tag={tag} />
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Box>{getSkillTypeLabel(skill.skillType)}</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ whiteSpace: 'pre-wrap', borderBottom: 'default', p: 1 }}>
|
||||||
|
<FormattedText>{formatSkillInfo(skill)}</FormattedText>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{skill.subSkills.map(subSkill => {
|
||||||
|
return (
|
||||||
|
<Fragment key={subSkill.id}>
|
||||||
|
<Box sx={{ borderBottom: 'default', p: 1 }}>
|
||||||
|
<Flex sx={{ alignItems: 'center' }}>
|
||||||
|
<AvatarSubSkillIcon icon={subSkill.icon} />
|
||||||
|
<Heading as="h4" sx={{ ml: 1, mb: 0 }}>
|
||||||
|
<FormattedText>{subSkill.name}</FormattedText>
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
{subSkill.unlockStar.localeCompare(battlesuit.initialStar) > 0 && (
|
||||||
|
<Box ml={2}>
|
||||||
|
<StarIcon star={subSkill.unlockStar} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{subSkill.tags.length > 0 && (
|
||||||
|
<Flex sx={{ ml: 2 }}>
|
||||||
|
{subSkill.tags.map((tag, index) => {
|
||||||
|
return <SkillTagItem key={index} tag={tag} />
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
borderBottom: 'default',
|
||||||
|
'&:last-child': { borderBottom: 'none' },
|
||||||
|
p: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedText>{formatSubSkillInfo(subSkill)}</FormattedText>
|
||||||
|
</Box>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<pre>{JSON.stringify(battlesuit, null, 2)}</pre>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BattlesuitShowPage
|
||||||
|
|
||||||
|
export async function getStaticProps({ locale, params }: NextPageContext & { params: { battlesuitId: string } }) {
|
||||||
|
const battlesuit = loadBattlesuitData(params.battlesuitId)
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: { battlesuit }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const battlesuitCatalog = loadBattlesuitCatalog()
|
||||||
|
|
||||||
|
return {
|
||||||
|
paths: battlesuitCatalog.map(catalogItem => {
|
||||||
|
return {
|
||||||
|
params: { battlesuitId: catalogItem.id }
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
fallback: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SkillTagBoxProps {
|
||||||
|
tag: SkillTagItem
|
||||||
|
}
|
||||||
|
|
||||||
|
const SkillTagItem = ({ tag }: SkillTagBoxProps) => {
|
||||||
|
return <TagIcon type={tag.type} strength={tag.strength} comment={tag.comment} size="sm" />
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/** @jsxImportSource theme-ui */
|
||||||
|
import { Box } from '@theme-ui/components'
|
||||||
|
import { NextPageContext } from 'next'
|
||||||
|
import { Flex, Link } from 'theme-ui'
|
||||||
|
import { loadBattlesuitCatalog } from '../../../lib/v2-pre/server/loadData'
|
||||||
|
import { BattlesuitCatalogItem } from '../../../lib/v2-pre/data/types'
|
||||||
|
import BattlesuitCatalogItemCard from '../../../components/v2-pre/BattlesuitCatalogItemCard'
|
||||||
|
|
||||||
|
interface BattlesuitListPageProps {
|
||||||
|
battlesuitCatalog: BattlesuitCatalogItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const BattlesuitListPage = ({ battlesuitCatalog }: BattlesuitListPageProps) => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<h1>Battlesuits</h1>
|
||||||
|
<Flex sx={{ flexWrap: 'wrap' }}>
|
||||||
|
{battlesuitCatalog.map(battlesuit => {
|
||||||
|
return (
|
||||||
|
<Box key={battlesuit.id}>
|
||||||
|
<Link href={`/v2-pre/battlesuits/${battlesuit.id}`}>
|
||||||
|
<BattlesuitCatalogItemCard battlesuit={battlesuit} />
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BattlesuitListPage
|
||||||
|
|
||||||
|
export async function getStaticProps({ locale }: NextPageContext) {
|
||||||
|
const battlesuitCatalog = loadBattlesuitCatalog()
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: { battlesuitCatalog }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const HomePage = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>V2 Pre</h1>
|
||||||
|
<a href="/v2-pre/battlesuits">Battlesuits</a>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HomePage
|
||||||
Reference in New Issue
Block a user