From f6f2a0866db5982746754ff62667308e06ea29ed Mon Sep 17 00:00:00 2001 From: Sakura Knoll Date: Sun, 9 Jul 2023 07:31:28 +0900 Subject: [PATCH] Add Battlesuit page --- src/components/v2-pre/AttributeIcon.tsx | 36 +++ src/components/v2-pre/AvatarFigureImage.tsx | 21 ++ src/components/v2-pre/AvatarSkillIcon.tsx | 20 ++ src/components/v2-pre/AvatarSubSkillIcon.tsx | 20 ++ .../v2-pre/BattlesuitAvatarIcon.tsx | 61 +++++ .../v2-pre/BattlesuitCatalogItemCard.tsx | 39 ++++ src/components/v2-pre/ChibiIcon.tsx | 90 ++++++++ src/components/v2-pre/FormattedText.tsx | 53 +++++ src/components/v2-pre/StarIcon.tsx | 48 ++++ src/components/v2-pre/TagIcon.tsx | 98 ++++++++ src/components/v2-pre/WeaponTypeIcon.tsx | 50 +++++ src/lib/v2-pre/data/formatText.spec.ts | 40 ++++ src/lib/v2-pre/data/formatText.ts | 131 +++++++++++ src/lib/v2-pre/data/text.ts | 212 ++++++++++++++++++ src/lib/v2-pre/data/types.ts | 144 ++++++++++++ src/lib/v2-pre/data/utils.ts | 7 + src/lib/v2-pre/formattedText/tokenize.spec.ts | 66 ++++++ src/lib/v2-pre/formattedText/tokenize.ts | 101 +++++++++ .../v2-pre/battlesuits/[battlesuitId].tsx | 176 +++++++++++++++ src/pages/v2-pre/battlesuits/index.tsx | 40 ++++ src/pages/v2-pre/index.tsx | 10 + 21 files changed, 1463 insertions(+) create mode 100644 src/components/v2-pre/AttributeIcon.tsx create mode 100644 src/components/v2-pre/AvatarFigureImage.tsx create mode 100644 src/components/v2-pre/AvatarSkillIcon.tsx create mode 100644 src/components/v2-pre/AvatarSubSkillIcon.tsx create mode 100644 src/components/v2-pre/BattlesuitAvatarIcon.tsx create mode 100644 src/components/v2-pre/BattlesuitCatalogItemCard.tsx create mode 100644 src/components/v2-pre/ChibiIcon.tsx create mode 100644 src/components/v2-pre/FormattedText.tsx create mode 100644 src/components/v2-pre/StarIcon.tsx create mode 100644 src/components/v2-pre/TagIcon.tsx create mode 100644 src/components/v2-pre/WeaponTypeIcon.tsx create mode 100644 src/lib/v2-pre/data/formatText.spec.ts create mode 100644 src/lib/v2-pre/data/formatText.ts create mode 100644 src/lib/v2-pre/data/text.ts create mode 100644 src/lib/v2-pre/data/types.ts create mode 100644 src/lib/v2-pre/data/utils.ts create mode 100644 src/lib/v2-pre/formattedText/tokenize.spec.ts create mode 100644 src/lib/v2-pre/formattedText/tokenize.ts create mode 100644 src/pages/v2-pre/battlesuits/[battlesuitId].tsx create mode 100644 src/pages/v2-pre/battlesuits/index.tsx create mode 100644 src/pages/v2-pre/index.tsx diff --git a/src/components/v2-pre/AttributeIcon.tsx b/src/components/v2-pre/AttributeIcon.tsx new file mode 100644 index 00000000..62202bd1 --- /dev/null +++ b/src/components/v2-pre/AttributeIcon.tsx @@ -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 ( + {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` + } +} diff --git a/src/components/v2-pre/AvatarFigureImage.tsx b/src/components/v2-pre/AvatarFigureImage.tsx new file mode 100644 index 00000000..43042eca --- /dev/null +++ b/src/components/v2-pre/AvatarFigureImage.tsx @@ -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 {battlesuitId} +} + +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` +} diff --git a/src/components/v2-pre/AvatarSkillIcon.tsx b/src/components/v2-pre/AvatarSkillIcon.tsx new file mode 100644 index 00000000..0d0d9804 --- /dev/null +++ b/src/components/v2-pre/AvatarSkillIcon.tsx @@ -0,0 +1,20 @@ +import { Box } from 'theme-ui' +import { assetsBucketBaseUrl } from '../../lib/consts' + +interface AvatarSkillIconProps { + icon: string +} + +const AvatarSkillIcon = ({ icon: fileName }: AvatarSkillIconProps) => { + return ( + + ) +} + +export default AvatarSkillIcon diff --git a/src/components/v2-pre/AvatarSubSkillIcon.tsx b/src/components/v2-pre/AvatarSubSkillIcon.tsx new file mode 100644 index 00000000..4f6a474d --- /dev/null +++ b/src/components/v2-pre/AvatarSubSkillIcon.tsx @@ -0,0 +1,20 @@ +import { Box } from 'theme-ui' +import { assetsBucketBaseUrl } from '../../lib/consts' + +interface AvatarSubSkillIconProps { + icon: string +} + +const AvatarSubSkillIcon = ({ icon: fileName }: AvatarSubSkillIconProps) => { + return ( + + ) +} + +export default AvatarSubSkillIcon diff --git a/src/components/v2-pre/BattlesuitAvatarIcon.tsx b/src/components/v2-pre/BattlesuitAvatarIcon.tsx new file mode 100644 index 00000000..84f97349 --- /dev/null +++ b/src/components/v2-pre/BattlesuitAvatarIcon.tsx @@ -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 ( + + + + ) +} + +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` + } +} diff --git a/src/components/v2-pre/BattlesuitCatalogItemCard.tsx b/src/components/v2-pre/BattlesuitCatalogItemCard.tsx new file mode 100644 index 00000000..446d1b6f --- /dev/null +++ b/src/components/v2-pre/BattlesuitCatalogItemCard.tsx @@ -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 ( + + + + {battlesuit.fullName} + + + ) +} + +export default BattlesuitCatalogItemCard diff --git a/src/components/v2-pre/ChibiIcon.tsx b/src/components/v2-pre/ChibiIcon.tsx new file mode 100644 index 00000000..68e7330b --- /dev/null +++ b/src/components/v2-pre/ChibiIcon.tsx @@ -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 ( + + ) +} + +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' + } +} diff --git a/src/components/v2-pre/FormattedText.tsx b/src/components/v2-pre/FormattedText.tsx new file mode 100644 index 00000000..b16326d9 --- /dev/null +++ b/src/components/v2-pre/FormattedText.tsx @@ -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 ( + + {renderTokens(token.children)} + + ) + } + + 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 +} diff --git a/src/components/v2-pre/StarIcon.tsx b/src/components/v2-pre/StarIcon.tsx new file mode 100644 index 00000000..27226240 --- /dev/null +++ b/src/components/v2-pre/StarIcon.tsx @@ -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 ( + + ) +} + +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` + } +} diff --git a/src/components/v2-pre/TagIcon.tsx b/src/components/v2-pre/TagIcon.tsx new file mode 100644 index 00000000..1cee10b9 --- /dev/null +++ b/src/components/v2-pre/TagIcon.tsx @@ -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 ( + + + {strength} + + + + ) +} + +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' + } +} diff --git a/src/components/v2-pre/WeaponTypeIcon.tsx b/src/components/v2-pre/WeaponTypeIcon.tsx new file mode 100644 index 00000000..b6feb820 --- /dev/null +++ b/src/components/v2-pre/WeaponTypeIcon.tsx @@ -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 ( + + ) +} +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' + } +} diff --git a/src/lib/v2-pre/data/formatText.spec.ts b/src/lib/v2-pre/data/formatText.spec.ts new file mode 100644 index 00000000..93627482 --- /dev/null +++ b/src/lib/v2-pre/data/formatText.spec.ts @@ -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*0.70%만큼 증가하고, 크리티컬률이 추가 소모 SP*0.50%만큼 증가한다(최대 추가 소모 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가 40.0 증가하며, 오픈월드에서는 10분에 1회 발동한다.') + }) +}) diff --git a/src/lib/v2-pre/data/formatText.ts b/src/lib/v2-pre/data/formatText.ts new file mode 100644 index 00000000..4a73780e --- /dev/null +++ b/src/lib/v2-pre/data/formatText.ts @@ -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, '' + valueString + '') + + return replaceParams(replacedText, params) +} diff --git a/src/lib/v2-pre/data/text.ts b/src/lib/v2-pre/data/text.ts new file mode 100644 index 00000000..57d117a8 --- /dev/null +++ b/src/lib/v2-pre/data/text.ts @@ -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})` + } +} diff --git a/src/lib/v2-pre/data/types.ts b/src/lib/v2-pre/data/types.ts new file mode 100644 index 00000000..10423dc4 --- /dev/null +++ b/src/lib/v2-pre/data/types.ts @@ -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 +} diff --git a/src/lib/v2-pre/data/utils.ts b/src/lib/v2-pre/data/utils.ts new file mode 100644 index 00000000..ecaad68f --- /dev/null +++ b/src/lib/v2-pre/data/utils.ts @@ -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() diff --git a/src/lib/v2-pre/formattedText/tokenize.spec.ts b/src/lib/v2-pre/formattedText/tokenize.spec.ts new file mode 100644 index 00000000..30dc38b8 --- /dev/null +++ b/src/lib/v2-pre/formattedText/tokenize.spec.ts @@ -0,0 +1,66 @@ +import { tokenize } from './tokenize' + +describe('tokenize', () => { + it('tokenize', () => { + const rawString = + '기본 공격 또는 분기 공격이 적에게 연소 게이지를 4.0pt 누적한다. 발동 간격: 6.0초. 기본 공격과 분기 공격이 가하는 화염 원소 대미지가 30.0% 증가한다.' + + 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: ' 증가한다.' + } + ]) + }) +}) diff --git a/src/lib/v2-pre/formattedText/tokenize.ts b/src/lib/v2-pre/formattedText/tokenize.ts new file mode 100644 index 00000000..d805d91a --- /dev/null +++ b/src/lib/v2-pre/formattedText/tokenize.ts @@ -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 = 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?') +} diff --git a/src/pages/v2-pre/battlesuits/[battlesuitId].tsx b/src/pages/v2-pre/battlesuits/[battlesuitId].tsx new file mode 100644 index 00000000..6170b23c --- /dev/null +++ b/src/pages/v2-pre/battlesuits/[battlesuitId].tsx @@ -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 ( + + {battlesuit.fullName} + + + + + + + + + + + + + + + + + + {getCharacterTypeLabel(battlesuit.character)} + + + + + {getAttributeLabel(battlesuit.attributeType)} + + + + {getWeaponTypeLabel(battlesuit.weapon)} + + + + {battlesuit.tags.map(tag => { + return ( + + + {getTagTypeLabel(tag)} + + ) + })} + + + + Skills + {battlesuit.skills.sort(sortBattlesuitSkill).map(skill => { + return ( + + + + + + {skill.name} + + {skill.tags.length > 0 && ( + + {skill.tags.map((tag, index) => { + return + })} + + )} + + + {getSkillTypeLabel(skill.skillType)} + + + {formatSkillInfo(skill)} + + + {skill.subSkills.map(subSkill => { + return ( + + + + + + {subSkill.name} + + + {subSkill.unlockStar.localeCompare(battlesuit.initialStar) > 0 && ( + + + + )} + + {subSkill.tags.length > 0 && ( + + {subSkill.tags.map((tag, index) => { + return + })} + + )} + + + + + {formatSubSkillInfo(subSkill)} + + + ) + })} + + ) + })} +
{JSON.stringify(battlesuit, null, 2)}
+
+ ) +} + +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 +} diff --git a/src/pages/v2-pre/battlesuits/index.tsx b/src/pages/v2-pre/battlesuits/index.tsx new file mode 100644 index 00000000..ec8dc60a --- /dev/null +++ b/src/pages/v2-pre/battlesuits/index.tsx @@ -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 ( + +

Battlesuits

+ + {battlesuitCatalog.map(battlesuit => { + return ( + + + + + + ) + })} + +
+ ) +} + +export default BattlesuitListPage + +export async function getStaticProps({ locale }: NextPageContext) { + const battlesuitCatalog = loadBattlesuitCatalog() + + return { + props: { battlesuitCatalog } + } +} diff --git a/src/pages/v2-pre/index.tsx b/src/pages/v2-pre/index.tsx new file mode 100644 index 00000000..7cd1f3ad --- /dev/null +++ b/src/pages/v2-pre/index.tsx @@ -0,0 +1,10 @@ +const HomePage = () => { + return ( +
+

V2 Pre

+ Battlesuits +
+ ) +} + +export default HomePage