Add Battlesuit page

This commit is contained in:
Sakura Knoll
2023-07-09 07:31:28 +09:00 Unverified
parent 9ce20302a3
commit f6f2a0866d
21 changed files with 1463 additions and 0 deletions
+40
View File
@@ -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회 발동한다.')
})
})
+131
View File
@@ -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)
}
+212
View File
@@ -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})`
}
}
+144
View File
@@ -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
}
+7
View File
@@ -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: ' 증가한다.'
}
])
})
})
+101
View File
@@ -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?')
}