Guide generator (#231)

* Replace sw, sns battlesuit images

* Add gen guide page

* Make supports configurable

* Extract DifficultySelect & Improve Ex Signet Ordering style

* Extract BattlesuitSelect

* Use BattlesuitSelect for support valk

* Swap support if duplicated

* Add sigil selector

* Set initial ex signets

* Change layout

* Add sigil icon to single value

* Add equipment select

* Fix difficulty box label

* Implement SignetBox

* Add Signet form control

* Improve rendering perf of stigma and weapon select

* Implement image converter

* Fix signet select

* Remove FontHead

* Remove wrong new line

* Fix warnings

* Add custom style configurator

* Make labels of DiffucltyBox and ValkBox configurable

* Add background and improve layout

* Improve ex signet fontsize

* Style boundaries of left bottom boxes

* Apply fonts directly

* Style signet box

* Add Signet group label

* Style ValkBox

* Add support valk label

* Style layout of SignetBox

* Add shadow to difficulty box

* Add sigil label

* Adjust signet group label

* Add equipment label

* Configure colors of left bottom boxes

* Add tag, rank, signature

* Add icon to current value of battlesuit select

* Adjust form styles

* Add signet group select

* Implement import/export data

* Fix dashed border of difficulty box

* Add gen link to elysian realm home

* Prefix types
This commit is contained in:
Sakura Knoll
2022-08-29 20:08:19 +09:00
committed by GitHub
Unverified
parent 2dd48df340
commit 5c8ed4f603
28 changed files with 3064 additions and 180 deletions
@@ -0,0 +1,104 @@
import { useMemo } from 'react'
import ReactSelect, { components } from 'react-select'
import { Flex, Image } from 'theme-ui'
import { assetsBucketBaseUrl } from '../../../lib/consts'
import { BattlesuitData } from '../../../lib/honkai3rd/battlesuits'
interface BattlesuitSelectProps {
instanceId: string
value: string
optionIds: string[]
battlesuits: BattlesuitData[]
onChange: (newValue: string) => void
}
const BattlesuitSelect = ({
instanceId,
optionIds,
value,
battlesuits,
onChange,
}: BattlesuitSelectProps) => {
const battlesuitOptions = useMemo(() => {
return optionIds.map((battlesuitId) => {
const battlesuit = battlesuits.find((aBattlesuit) => {
return aBattlesuit.id === battlesuitId
})
if (battlesuit == null) {
return {
value: 'unknown',
label: 'unknown',
}
}
return {
value: battlesuit.id,
label: battlesuit.name,
}
})
}, [battlesuits, optionIds])
const battlesuit = battlesuits.find((battlesuit) => {
return battlesuit.id === value
})
return (
<ReactSelect
instanceId={instanceId}
value={
battlesuit != null
? {
label: battlesuit.name,
value: battlesuit.id,
}
: null
}
onChange={(option) => {
if (option == null) {
return
}
onChange(option.value)
}}
options={battlesuitOptions}
components={{
SingleValue: (props) => {
return (
<>
<components.SingleValue {...props}>
<Flex sx={{ alignItems: 'center', color: 'black' }}>
<Image
width={20}
height={20}
alt={props.data.label}
src={`${assetsBucketBaseUrl}/honkai3rd/battlesuits/portrait-${props.data.value}.png`}
sx={{ flexShrink: 0, mr: 2 }}
/>
{props.children}
</Flex>
</components.SingleValue>
</>
)
},
Option: (props) => {
return (
<>
<components.Option {...props}>
<Flex sx={{ alignItems: 'center', color: 'black' }}>
<Image
width={20}
height={20}
alt={props.data.label}
src={`${assetsBucketBaseUrl}/honkai3rd/battlesuits/portrait-${props.data.value}.png`}
sx={{ mr: 2, flexShrink: 0 }}
/>
{props.children}
</Flex>
</components.Option>
</>
)
},
}}
/>
)
}
export default BattlesuitSelect
@@ -0,0 +1,509 @@
import { Box, Button, Flex, Input, Label, Select, Textarea } from 'theme-ui'
import { ERGGData, ERGGDataUpdater, ERGGExSignetType } from './types'
import {
erVersions,
isGeneralSigil,
PopulatedSignetGroup,
RemembranceSigil,
remembranceSigilIds,
supportBattlesuitIds,
} from '../../../lib/honkai3rd/elysianRealm'
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core'
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { BattlesuitData } from '../../../lib/honkai3rd/battlesuits'
import DifficultySelect from './DifficultySelect'
import { getExSignetLabel } from './utils'
import BattlesuitSelect from './BattlesuitSelect'
import SigilSelect from './SigilSelect'
import { WeaponData } from '../../../lib/honkai3rd/weapons'
import { StigmataData } from '../../../lib/honkai3rd/stigmata'
import { useMemo } from 'react'
import EquipmentSetControl from './EquipmentSetControl'
import SignetGroupSelect from './SignetGroupSelect'
interface DataFormProps {
updateData: ERGGDataUpdater
data: ERGGData
battlesuits: BattlesuitData[]
exSignetGroup: PopulatedSignetGroup
sigils: RemembranceSigil[]
weapons: WeaponData[]
stigmata: StigmataData[]
}
const DataForm = ({
updateData,
data,
battlesuits,
exSignetGroup,
sigils,
weapons,
stigmata,
}: DataFormProps) => {
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
)
const { topStigmaIds, midStigmaIds, botStigmaIds } = useMemo(() => {
return stigmata.reduce(
(map, stigma) => {
switch (stigma.type) {
case 'top':
map.topStigmaIds.push(stigma.id)
break
case 'mid':
map.midStigmaIds.push(stigma.id)
break
case 'bot':
map.botStigmaIds.push(stigma.id)
break
}
return map
},
{
topStigmaIds: [] as string[],
midStigmaIds: [] as string[],
botStigmaIds: [] as string[],
}
)
}, [stigmata])
return (
<Box sx={{ maxWidth: 960 }}>
<Flex>
<Box sx={{ flex: 1, p: 2 }}>
<Box>
<Label></Label>
<Textarea
value={data.signature}
placeholder='서명...'
onChange={(event) => {
updateData('signature', event.target.value)
}}
sx={{ resize: 'vertical', minHeight: 100 }}
/>
</Box>
<Box>
<Label></Label>
<Select
value={data.rank || 'na'}
onChange={(event) => {
switch (event.target.value) {
case 'na':
updateData('rank', undefined)
break
default:
updateData('rank', event.target.value)
}
}}
>
<option value='na'>N/A</option>
<option value='s'>s</option>
<option value='s1'>s1</option>
<option value='s2'>s2</option>
<option value='s3'>s3</option>
<option value='ss'>ss</option>
<option value='ss1'>ss1</option>
<option value='ss2'>ss2</option>
<option value='ss3'>ss3</option>
<option value='sss'>sss</option>
<option value='a'>a</option>
</Select>
</Box>
<Box>
<Label></Label>
<Input
value={data.tag}
placeholder='태그...(필살기, 평타)'
onChange={(event) => {
updateData('tag', event.target.value)
}}
/>
</Box>
<Box>
<Label></Label>
<DifficultySelect
onChange={(newValue) => updateData('difficulty', newValue)}
value={data.difficulty}
/>
</Box>
<Box>
<Label></Label>
<BattlesuitSelect
instanceId='battlesuit-select'
battlesuits={battlesuits}
optionIds={erVersions.reduce<string[]>(
(battlesuitIds, version) => {
return [...battlesuitIds, ...version.battlesuits]
},
[]
)}
value={data.battlesuitId}
onChange={(newValue) => {
updateData('battlesuitId', newValue)
const exSignetSet = exSignetGroup.sets.find((signetSet) => {
return signetSet.id === `elysia-${newValue}`
})
if (exSignetSet != null) {
updateData(
'exSignets',
exSignetSet.signets.map((signet, index) => {
return {
type:
data.exSignets[index] != null
? data.exSignets[index].type
: 'na',
name: signet.name,
}
})
)
}
}}
/>
</Box>
<Box>
<Label> </Label>
<Flex>
<Box>
{data.exSignets.map((signet, index) => {
return (
<Flex
key={index}
sx={{ p: 1, height: 40, alignItems: 'center', mb: 2 }}
>
<Select
value={signet.type}
sx={{ height: 40, width: 80, mr: 1, p: 2 }}
onChange={(event) => {
const newExSignets = data.exSignets.slice()
newExSignets[index] = {
...data.exSignets[index],
type: event.target.value as ERGGExSignetType,
}
updateData('exSignets', newExSignets)
}}
>
<option value='start'></option>
<option value='1st'></option>
<option value='2nd'></option>
<option value='backup'></option>
<option value='na'></option>
</Select>
</Flex>
)
})}
</Box>
<Box sx={{ flex: 1 }}>
<DndContext
id='exSignetOrdering'
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={(event) => {
const { active, over } = event
if (over == null) {
return
}
if (active.id !== over.id) {
const nameArray = data.exSignets.map(
(signet) => signet.name
)
const oldIndex = nameArray.indexOf(active.id as string)
const newIndex = nameArray.indexOf(over.id as string)
const newNameArray = arrayMove(
nameArray,
oldIndex,
newIndex
)
const newExSignets = data.exSignets.map(
(signet, index) => {
return {
...signet,
name: newNameArray[index],
}
}
)
updateData('exSignets', newExSignets)
}
}}
>
<SortableContext
items={data.exSignets.map((signet) => ({
id: signet.name,
}))}
strategy={verticalListSortingStrategy}
>
{data.exSignets.map((signet) => (
<SortableItem key={signet.name} name={signet.name} />
))}
</SortableContext>
</DndContext>
</Box>
</Flex>
</Box>
</Box>
<Box sx={{ flex: 1, p: 2 }}>
<Box>
{data.signets.map((signet, index) => {
return (
<Box key={index}>
<Label>{index + 1} </Label>
<Flex sx={{ alignItems: 'center', p: 1 }}>
<Box sx={{ flex: 1, flexShrink: 0, mr: 1 }}>
<SignetGroupSelect
value={signet.group}
onChange={(newValue) => {
const newSignets = data.signets.slice()
newSignets[index] = {
...signet,
group: newValue as any,
}
updateData('signets', newSignets)
}}
instanceId={`signet-select-${index}`}
/>
</Box>
<Box>
<Button
onClick={() => {
const newSignets = data.signets.slice()
newSignets[index] = {
...signet,
nexus: signet.nexus === 1 ? 2 : 1,
}
updateData('signets', newSignets)
}}
sx={{ mr: 1, width: 40 }}
>
{signet.nexus === 1 ? 'I' : 'II'}
</Button>
</Box>
<Box sx={{ width: 60 }}>
<Select
value={signet.type}
onChange={(event) => {
const newSignets = data.signets.slice()
newSignets[index] = {
...signet,
type: event.target.value as any,
}
updateData('signets', newSignets)
}}
>
<option value='start'></option>
<option value='core'></option>
<option value='sub'></option>
</Select>
</Box>
</Flex>
<Box sx={{ p: 1 }}>
<Textarea
value={signet.description}
onChange={(event) => {
const newSignets = data.signets.slice()
newSignets[index] = {
...signet,
description: event.target.value,
}
updateData('signets', newSignets)
}}
placeholder='각인 설명...'
sx={{ resize: 'vertical' }}
/>
</Box>
</Box>
)
})}
</Box>
</Box>
<Box sx={{ flex: 1, p: 2 }}>
<Box>
<Label> </Label>
{data.supportSets.map((supportSet, index) => {
return (
<Box key={index}>
<Box>{supportSet.type === 'util' ? '유틸' : '딜링'}</Box>
<Flex>
<Box sx={{ flex: 1, mr: 1 }}>
<BattlesuitSelect
instanceId={`battlesuit-support-select-${index}-1`}
value={supportSet.battlesuitIds[0]}
optionIds={supportBattlesuitIds}
battlesuits={battlesuits}
onChange={(newValue) => {
const newSupportSets = data.supportSets.slice()
newSupportSets[index] = {
...newSupportSets[index],
battlesuitIds: [
...newSupportSets[index].battlesuitIds,
],
}
newSupportSets[index].battlesuitIds[0] = newValue
if (
data.supportSets[index].battlesuitIds[1] ===
newValue
) {
newSupportSets[index].battlesuitIds[1] =
data.supportSets[index].battlesuitIds[0]
}
updateData('supportSets', newSupportSets)
}}
/>
</Box>
<Box sx={{ flex: 1 }}>
<BattlesuitSelect
instanceId={`battlesuit-support-select-${index}-2`}
value={supportSet.battlesuitIds[1]}
optionIds={supportBattlesuitIds}
battlesuits={battlesuits}
onChange={(newValue) => {
const newSupportSets = data.supportSets.slice()
newSupportSets[index] = {
...newSupportSets[index],
battlesuitIds: [
...newSupportSets[index].battlesuitIds,
],
}
newSupportSets[index].battlesuitIds[1] = newValue
if (
data.supportSets[index].battlesuitIds[0] ===
newValue
) {
newSupportSets[index].battlesuitIds[0] =
data.supportSets[index].battlesuitIds[1]
}
updateData('supportSets', newSupportSets)
}}
/>
</Box>
</Flex>
</Box>
)
})}
</Box>
<Box sx={{ flex: 1 }}>
{data.sigilSets.map((sigilSet, index) => {
return (
<Box key={index}>
<Box>
{sigilSet.type === 'start'
? '초반'
: sigilSet.type === 'mid'
? '중반'
: '후반'}{' '}
</Box>
<Flex>
<Box sx={{ flex: 1, mr: 1 }}>
<SigilSelect
instanceId={`sigil-select-${index}-general`}
value={sigilSet.sigilIds[0]}
optionIds={remembranceSigilIds.filter(isGeneralSigil)}
sigils={sigils}
onChange={(newValue) => {
const newSigilSets = data.sigilSets.slice()
newSigilSets[index] = {
...newSigilSets[index],
sigilIds: [
newValue,
newSigilSets[index].sigilIds[1],
],
}
updateData('sigilSets', newSigilSets)
}}
/>
</Box>
<Box>
<SigilSelect
instanceId={`sigil-select-${index}-support`}
value={sigilSet.sigilIds[1]}
optionIds={remembranceSigilIds.filter(
(sigilId) => !isGeneralSigil(sigilId)
)}
sigils={sigils}
onChange={(newValue) => {
const newSigilSets = data.sigilSets.slice()
newSigilSets[index] = {
...newSigilSets[index],
sigilIds: [
newSigilSets[index].sigilIds[0],
newValue,
],
}
updateData('sigilSets', newSigilSets)
}}
/>
</Box>
</Flex>
</Box>
)
})}
</Box>
<Box>
{data.equipmentSets.map((equipmentSet, index) => {
return (
<EquipmentSetControl
key={index}
equipmentSet={equipmentSet}
weapons={weapons}
stigmata={stigmata}
topStigmaIds={topStigmaIds}
midStigmaIds={midStigmaIds}
botStigmaIds={botStigmaIds}
index={index}
updateData={updateData}
/>
)
})}
</Box>
</Box>
</Flex>
</Box>
)
}
export default DataForm
export function SortableItem(props: { name: string }) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: props.name })
const style = {
transform: CSS.Transform.toString(transform),
transition,
}
return (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
<Flex
sx={{
height: 40,
mb: 2,
cursor: 'ns-resize',
alignItems: 'center',
p: 1,
}}
>
{getExSignetLabel(props.name)}
</Flex>
</div>
)
}
@@ -0,0 +1,83 @@
import { Box } from 'theme-ui'
import { colors } from './styles'
interface DifficultyBoxProps {
difficulty: 'abstinence' | 'corruption' | 'inferno'
}
const DifficultyBox = ({ difficulty }: DifficultyBoxProps) => {
return (
<Box
sx={{
position: 'absolute',
left: 15,
width: 60,
height: 105,
backgroundColor:
difficulty === 'corruption'
? colors.corruptionDifficultyColor
: difficulty === 'abstinence'
? colors.abstinenceDifficultyColor
: colors.infernoDifficultyColor,
padding: '5px',
borderBottomLeftRadius: 10,
borderBottomRightRadius: 10,
boxShadow: '5px 5px 10px rgba(0,0,0,0.5)',
}}
className='difficultyLabel'
>
<Box
sx={{
color: colors.backgroundColor,
fontWeight: 'bold',
textAlign: 'center',
fontSize: 16,
}}
>
</Box>
<Box
sx={{
borderColor: colors.backgroundColor,
borderWidth: '1px 0 1px',
borderStyle: 'dashed',
width: '100%',
}}
/>
<Box
sx={{
color: colors.backgroundColor,
fontWeight: 'bold',
fontSize: 25,
textAlign: 'center',
}}
>
{difficulty === 'corruption'
? '침식'
: difficulty === 'abstinence'
? '제약'
: '겁화'}
</Box>
<Box
sx={{
borderColor: colors.backgroundColor,
borderWidth: '1px 0 1px',
borderStyle: 'dashed',
width: '100%',
}}
/>
<Box
sx={{
color: colors.backgroundColor,
fontWeight: 'bold',
textAlign: 'center',
fontSize: 16,
}}
>
</Box>
</Box>
)
}
export default DifficultyBox
@@ -0,0 +1,30 @@
import { Select } from 'theme-ui'
import { ERGGDifficulty } from './types'
interface DifficultySelectProps {
onChange: (newValue: ERGGDifficulty) => void
value: ERGGDifficulty
}
const DifficultySelect = ({ onChange, value }: DifficultySelectProps) => {
return (
<Select
onChange={(event) => {
switch (event.target.value) {
case 'abstinence':
case 'corruption':
case 'inferno':
onChange(event.target.value)
break
}
}}
value={value}
>
<option value='corruption'></option>
<option value='abstinence'></option>
<option value='inferno'></option>
</Select>
)
}
export default DifficultySelect
@@ -0,0 +1,444 @@
/* eslint-disable @next/next/no-page-custom-font */
import { useCallback, useEffect, useRef, useState } from 'react'
import {
Box,
Button,
Flex,
Heading,
Image,
Input,
Label,
Text,
Textarea,
} from 'theme-ui'
import { BattlesuitData } from '../../../lib/honkai3rd/battlesuits'
import { WeaponData } from '../../../lib/honkai3rd/weapons'
import DifficultyBox from './DifficultyBox'
import SignetBox from './SignetBox'
import { colors } from './styles'
import ValkBox from './ValkBox'
import {
PopulatedSignetGroup,
RemembranceSigil,
} from '../../../lib/honkai3rd/elysianRealm'
import SigilBox from './SigilBox'
import SupportBox from './SupportBox'
import DataForm from './DataForm'
import EquipmentBox from './EquipmentBox'
import { ERGGData, ERGGDataUpdater, ERGGExSignetType } from './types'
import { StigmataData } from '../../../lib/honkai3rd/stigmata'
import { saveAs } from 'file-saver'
import { toBlob } from 'html-to-image'
import { assetsBucketBaseUrl } from '../../../lib/consts'
interface ERGuideGeneratorProps {
weapons: WeaponData[]
stigmata: StigmataData[]
battlesuits: BattlesuitData[]
exSignetGroup: PopulatedSignetGroup
sigils: RemembranceSigil[]
}
const exSignetTypes: ERGGExSignetType[] = [
'start',
'1st',
'2nd',
'backup',
'na',
]
const ERGuideGenerator = ({
weapons,
stigmata,
battlesuits,
exSignetGroup,
sigils,
}: ERGuideGeneratorProps) => {
const [data, setData] = useState<ERGGData>({
tag: '',
signature:
'작성자 : XXX\nAbyss Lab에서 생성됨\nhttps://abyss-lab.app/honkai3rd',
battlesuitId: 'vill-v',
difficulty: 'corruption',
exSignets:
exSignetGroup.sets
.find((set) => set.id === 'elysia-vill-v')
?.signets.map((signet, index) => {
return {
type: exSignetTypes[index],
name: signet.name,
}
}) || [],
supportSets: [
{ type: 'util', battlesuitIds: ['le', 'ae'] },
{ type: 'dps', battlesuitIds: ['br', 'ae'] },
],
sigilSets: [
{
type: 'start',
sigilIds: ['burden', 'it-will-be-written'],
},
{
type: 'mid',
sigilIds: ['burden', 'it-will-be-written'],
},
{
type: 'end',
sigilIds: ['burden', 'it-will-be-written'],
},
],
equipmentSets: [
{
type: 'best',
weapon: 'vill-v-pri-weapon',
top: 'carlo-collodi-top',
mid: 'carlo-collodi-mid',
bot: 'carlo-collodi-bot',
},
{
type: 'alt',
weapon: 'vill-v-pri-weapon',
top: 'carlo-collodi-top',
mid: 'carlo-collodi-mid',
bot: 'carlo-collodi-bot',
},
],
signets: [
{
type: 'core',
group: 'vill-v',
nexus: 2,
description: '',
},
{
type: 'core',
group: 'vill-v',
nexus: 2,
description: '',
},
{
type: 'sub',
group: 'vill-v',
nexus: 2,
description: '',
},
{
type: 'sub',
group: 'vill-v',
nexus: 2,
description: '',
},
{
type: 'sub',
group: 'vill-v',
nexus: 2,
description: '',
},
],
})
const [fileName, setFileName] = useState('guide')
const guideRef = useRef(null)
const updateData = useCallback<ERGGDataUpdater>(
(key, value) => {
setData((previousData) => {
const newData = {
...previousData,
[key]: typeof value === 'function' ? value(previousData[key]) : value,
}
return newData
})
},
[setData]
)
const [customStyle, setCustomStyle] = useState('')
const loadedRef = useRef(false)
useEffect(() => {
if (loadedRef.current) {
localStorage.setItem('ergg:customStyle', customStyle)
} else {
setCustomStyle(localStorage.getItem('ergg:customStyle') || '')
loadedRef.current = true
}
}, [customStyle])
return (
<Box
onDragOver={(event) => {
event.preventDefault()
}}
onDrop={async (event) => {
event.preventDefault()
const result = await new Promise<string>((resolve, reject) => {
const file = event.dataTransfer.files[0]
const reader = new FileReader()
reader.addEventListener(
'load',
() => {
resolve(reader.result as string)
},
{ once: true }
)
reader.addEventListener(
'error',
(error) => {
reject(error)
},
{ once: true }
)
reader.readAsText(file)
})
const parsedResult = JSON.parse(result)
setData(parsedResult)
}}
>
<Box sx={{ width: 960 }}>
<Box
ref={guideRef}
sx={{
width: 960,
height: 660,
position: 'relative',
backgroundColor: colors.backgroundColor,
color: '#FFF',
fontFamily: `'NanumSquare', sans-serif`,
}}
>
<style
dangerouslySetInnerHTML={{
__html: `@font-face {
font-family: 'GyeonggiTitleM';
src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_one@1.0/GyeonggiTitleM.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'YdestreetB';
src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_2110@1.0/YdestreetB.woff2') format('woff2');
font-weight: normal;
font-style: normal;
}
@import url('https://cdn.rawgit.com/moonspam/NanumSquare/master/nanumsquare.css');
@font-face {
font-family: 'ghanachoco';
src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_20-04@1.0/ghanachoco.woff') format('woff');
font-weight: normal;
font-style: normal;
}
.exSignetLabel {
font-family: 'ghanachoco';
}
.difficultyLabel {
font-family: 'YdestreetB';
}
.signetTypeLabel {
font-family: 'YdestreetB';
}
.signetDescription {
font-family: 'GyeonggiTitleM';
}
.signetGroupLabel {
font-family: 'YdestreetB';
}
.tagLabel {
font-family: 'YdestreetB';
}
`,
}}
/>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
width: 960,
height: 660,
opacity: 0.15,
backgroundImage: `url('/assets/erbg.png')`,
backgroundSize: 1300,
backgroundPositionY: -55,
backgroundPositionX: -30,
}}
></Box>
<style
dangerouslySetInnerHTML={{
__html: customStyle,
}}
/>
<Flex
sx={{
position: 'absolute',
top: 15,
left: 85,
}}
>
{data.rank && (
<Image
alt={data.rank}
width={40}
height={40}
src={`${assetsBucketBaseUrl}/honkai3rd/rank-icons/${data.rank}-rank.png`}
sx={{ mr: '5px' }}
/>
)}
{data.tag && (
<Box
sx={{
padding: '5px',
border: 'solid 1px gray',
boxSizing: 'border-box',
backgroundColor: '#181614',
boxShadow: '5px 5px 10px rgba(0,0,0,0.5)',
}}
className='tagLabel'
>
{data.tag}
</Box>
)}
</Flex>
<Flex
sx={{
position: 'absolute',
right: 10,
padding: '5px 10px',
boxSizing: 'border-box',
bottom: 15,
maxWidth: 460,
alignItems: 'center',
whiteSpace: 'pre-wrap',
color: '#A59A9B',
fontStyle: 'italic',
textAlign: 'right',
}}
>
<Text>{data.signature}</Text>
</Flex>
<SignetBox signets={data.signets} />
<DifficultyBox difficulty={data.difficulty} />
<ValkBox
battlesuitId={data.battlesuitId}
exSignets={data.exSignets}
/>
<Box
sx={{
position: 'absolute',
bottom: 15 + (87 + 10) * 2,
left: 15,
}}
>
<SupportBox supportSets={data.supportSets} />
</Box>
<Box
sx={{
position: 'absolute',
bottom: 15 + 87 + 10,
left: 15,
}}
>
<SigilBox sigilSets={data.sigilSets} sigils={sigils} />
</Box>
<Box
sx={{
position: 'absolute',
bottom: '15px',
left: 15,
}}
>
<EquipmentBox
equipmentSets={data.equipmentSets}
weapons={weapons}
stigmata={stigmata}
/>
</Box>
</Box>
</Box>
<Box sx={{ m: 2 }}>
<Flex sx={{ alignItems: 'center' }}>
<Box sx={{ mr: 1 }}>
<Label>File Name</Label>
</Box>
<Box sx={{ mr: 1 }}>
<Input
value={fileName}
onChange={(event) => {
setFileName(event.target.value)
}}
/>
</Box>
<Button
sx={{ mr: 1 }}
onClick={async () => {
if (guideRef.current == null) {
return
}
const blob = await toBlob(guideRef.current)
if (blob == null) {
return
}
saveAs(blob, `${fileName}.png`)
}}
>
Download Image ({fileName}.png)
</Button>
<Button
onClick={() => {
console.log(JSON.stringify(data))
saveAs(new Blob([JSON.stringify(data)]), `${fileName}.json`)
}}
>
Download JSON Data ({fileName}.json)
</Button>
</Flex>
</Box>
<DataForm
exSignetGroup={exSignetGroup}
battlesuits={battlesuits}
updateData={updateData}
data={data}
sigils={sigils}
weapons={weapons}
stigmata={stigmata}
/>
<hr />
<Box sx={{ p: 2 }}>
<Heading as='h3'> </Heading>
<Box as='ul'>
<Box as='li'>
Y이드스트릿체 https://www.yspotlight.co.kr/brand/font?tabNo=1
</Box>
<Box as='li'> https://hangeul.naver.com/2017/nanum</Box>
<Box as='li'>
https://www.gg.go.kr/contents/contents.do?ciIdx=679&menuId=2457
</Box>
<Box as='li'>
릿 hhttps://www.lotteconf.co.kr/prcenter/gana
</Box>
</Box>
<Box sx={{ display: 'none' }}>
<Label>Custom Style(Don&apos;t paste any suspicious scripts!!)</Label>
<Textarea
value={customStyle}
onChange={(event) => {
setCustomStyle(event.target.value)
}}
/>
</Box>
</Box>
</Box>
)
}
export default ERGuideGenerator
@@ -0,0 +1,136 @@
import { Box, Flex, Image } from 'theme-ui'
import { assetsBucketBaseUrl } from '../../../lib/consts'
import { StigmataData } from '../../../lib/honkai3rd/stigmata'
import { WeaponData } from '../../../lib/honkai3rd/weapons'
import { ERGGEquipmentSet } from './types'
interface EquipmentBoxProps {
equipmentSets: ERGGEquipmentSet[]
weapons: WeaponData[]
stigmata: StigmataData[]
}
const EquipmentBox = ({
equipmentSets,
weapons,
stigmata,
}: EquipmentBoxProps) => {
return (
<Box
sx={{
padding: '5px',
border: 'solid 1px gray',
boxSizing: 'border-box',
backgroundColor: '#181614',
boxShadow: '5px 5px 10px rgba(0,0,0,0.5)',
}}
>
<Flex>
<Box
sx={{
backgroundColor: '#8E5B45',
p: '5px',
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
mr: '5px',
width: 55,
}}
>
<Box></Box>
<Box></Box>
</Box>
{equipmentSets.map((equipmentSet, index) => {
return (
<Box
key={index}
sx={{
backgroundColor: '#615559',
p: '15px 5px 0',
borderRadius: '5px',
position: 'relative',
mr: '5px',
'&:last-child': {
mr: 0,
},
}}
>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
backgroundColor:
equipmentSet.type === 'best' ? '#EE4E2C' : '#9082BD',
borderTopLeftRadius: '5px',
borderBottomRightRadius: '5px',
p: '0 7px',
fontSize: 12,
}}
>
{equipmentSet.type === 'best' ? '베스트' : '대체'}
</Box>
<Box sx={{ height: 40 }}>
<Image
alt=''
src={`${assetsBucketBaseUrl}/honkai3rd/weapons/${equipmentSet.weapon}.png`}
width={40}
height={40}
sx={{ borderRadius: '5px', mr: '5px' }}
/>
<Image
alt=''
src={`${assetsBucketBaseUrl}/honkai3rd/stigmata/icon-${equipmentSet.top}.png`}
width={40}
height={40}
sx={{ borderRadius: '5px', mr: '5px' }}
/>
<Image
alt=''
src={`${assetsBucketBaseUrl}/honkai3rd/stigmata/icon-${equipmentSet.mid}.png`}
width={40}
height={40}
sx={{ borderRadius: '5px', mr: '5px' }}
/>
<Image
alt=''
src={`${assetsBucketBaseUrl}/honkai3rd/stigmata/icon-${equipmentSet.bot}.png`}
width={40}
height={40}
sx={{ borderRadius: '5px' }}
/>
</Box>
<Flex
sx={{
height: 20,
fontSize: 12,
alignItems: 'center',
justifyContent: 'center',
}}
>
{getWeaponName(equipmentSet.weapon)}+
{getStigmataName(equipmentSet.top)}/
{getStigmataName(equipmentSet.mid)}/
{getStigmataName(equipmentSet.bot)}
</Flex>
</Box>
)
})}
</Flex>
</Box>
)
function getWeaponName(id: string) {
return weapons
.find((weapon) => weapon.id === id)
?.name.split(' ')[0]
.slice(0, 4)
.trim()
}
function getStigmataName(id: string) {
return stigmata.find((stigma) => stigma.id === id)?.name.slice(0, 3)
}
}
export default EquipmentBox
@@ -0,0 +1,150 @@
import { useCallback, useMemo } from 'react'
import { Box, Flex, Label } from 'theme-ui'
import { StigmataData } from '../../../lib/honkai3rd/stigmata'
import { WeaponData } from '../../../lib/honkai3rd/weapons'
import StigmaSelect from './StigmaSelect'
import { ERGGDataUpdater, ERGGEquipmentSet } from './types'
import WeaponSelect from './WeaponSelect'
interface EquipmentSetControlProps {
index: number
equipmentSet: ERGGEquipmentSet
updateData: ERGGDataUpdater
weapons: WeaponData[]
stigmata: StigmataData[]
topStigmaIds: string[]
midStigmaIds: string[]
botStigmaIds: string[]
}
const EquipmentSetControl = ({
index,
equipmentSet,
updateData,
weapons,
stigmata,
topStigmaIds,
midStigmaIds,
botStigmaIds,
}: EquipmentSetControlProps) => {
const handleWeaponChange = useCallback(
(newValue: string) => {
updateData('equipmentSets', (previousEquipmentSets) => {
const newEquipmentSets = previousEquipmentSets.slice()
newEquipmentSets[index] = {
...newEquipmentSets[index],
weapon: newValue,
}
return newEquipmentSets
})
},
[index, updateData]
)
const weaponIds = useMemo(() => weapons.map((weapon) => weapon.id), [weapons])
const handleTopStigmaChange = useCallback(
(newValue: string) => {
updateData('equipmentSets', (previousEquipmentSets) => {
const newEquipmentSets = previousEquipmentSets.slice()
newEquipmentSets[index] = {
...newEquipmentSets[index],
top: newValue,
}
return newEquipmentSets
})
},
[index, updateData]
)
const handleMidStigmaChange = useCallback(
(newValue: string) => {
updateData('equipmentSets', (previousEquipmentSets) => {
const newEquipmentSets = previousEquipmentSets.slice()
newEquipmentSets[index] = {
...newEquipmentSets[index],
mid: newValue,
}
return newEquipmentSets
})
},
[index, updateData]
)
const handleBotStigmaChange = useCallback(
(newValue: string) => {
updateData('equipmentSets', (previousEquipmentSets) => {
const newEquipmentSets = previousEquipmentSets.slice()
newEquipmentSets[index] = {
...newEquipmentSets[index],
bot: newValue,
}
return newEquipmentSets
})
},
[index, updateData]
)
return (
<Box>
<Box>{equipmentSet.type === 'best' ? '베스트' : '대체'}</Box>
<Flex sx={{ alignItems: 'center' }}>
<Box sx={{ mr: 2 }}>
<Label></Label>
</Box>
<Box sx={{ flexShrink: 0, flex: 1 }}>
<WeaponSelect
instanceId={`equipment-${index}-weapon`}
value={equipmentSet.weapon}
onChange={handleWeaponChange}
weapons={weapons}
optionIds={weaponIds}
/>
</Box>
</Flex>
<Flex sx={{ alignItems: 'center' }}>
<Box sx={{ mr: 2 }}>
<Label></Label>
</Box>
<Box sx={{ flexShrink: 0, flex: 1 }}>
<StigmaSelect
instanceId={`equipment-${index}-top`}
value={equipmentSet.top}
onChange={handleTopStigmaChange}
stigmata={stigmata}
optionIds={topStigmaIds}
/>
</Box>
</Flex>
<Flex sx={{ alignItems: 'center' }}>
<Box sx={{ mr: 2 }}>
<Label></Label>
</Box>
<Box sx={{ flexShrink: 0, flex: 1 }}>
<StigmaSelect
instanceId={`equipment-${index}-mid`}
value={equipmentSet.mid}
onChange={handleMidStigmaChange}
stigmata={stigmata}
optionIds={midStigmaIds}
/>
</Box>
</Flex>
<Flex sx={{ alignItems: 'center' }}>
<Box sx={{ mr: 2 }}>
<Label></Label>
</Box>
<Box sx={{ flexShrink: 0, flex: 1 }}>
<StigmaSelect
instanceId={`equipment-${index}-bot`}
value={equipmentSet.bot}
onChange={handleBotStigmaChange}
stigmata={stigmata}
optionIds={botStigmaIds}
/>
</Box>
</Flex>
</Box>
)
}
export default EquipmentSetControl
@@ -0,0 +1,116 @@
import { memo } from 'react'
import { Box, Flex, Image } from 'theme-ui'
import { assetsBucketBaseUrl } from '../../../lib/consts'
import { RemembranceSigil } from '../../../lib/honkai3rd/elysianRealm'
import { ERGGSigilSet } from './types'
interface SigilBoxProps {
sigilSets: ERGGSigilSet[]
sigils: RemembranceSigil[]
}
const SigilBox = ({ sigilSets, sigils }: SigilBoxProps) => {
return (
<Box
sx={{
padding: '5px',
border: 'solid 1px gray',
boxSizing: 'border-box',
backgroundColor: 'rgba(0,0,0,0.5)',
boxShadow: '5px 5px 10px rgba(0,0,0,0.5)',
}}
>
<Flex>
<Box
sx={{
backgroundColor: '#8E5B45',
p: '5px',
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
mr: '5px',
width: 55,
}}
>
<Box></Box>
<Box></Box>
</Box>
{sigilSets.map((sigilSet, index) => {
return (
<Box
key={index}
sx={{
backgroundColor: '#615559',
p: '15px 5px 0',
borderRadius: '5px',
position: 'relative',
mr: '5px',
'&:last-child': {
mr: 0,
},
}}
>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
backgroundColor:
sigilSet.type === 'start'
? '#666480'
: sigilSet.type === 'mid'
? '#8A6A5C'
: '#C06848',
borderTopLeftRadius: '5px',
borderBottomRightRadius: '5px',
p: '0 7px',
fontSize: 12,
}}
>
{sigilSet.type === 'start'
? '초반'
: sigilSet.type === 'mid'
? '중반'
: '후반'}
</Box>
<Box sx={{ height: 40 }}>
<Image
alt=''
src={`${assetsBucketBaseUrl}/honkai3rd/elysian-realm/remembrance-sigils/${sigilSet.sigilIds[0]}.png`}
width={40}
height={40}
sx={{ mr: '5px', borderRadius: 5 }}
/>
<Image
alt=''
src={`${assetsBucketBaseUrl}/honkai3rd/elysian-realm/remembrance-sigils/${sigilSet.sigilIds[1]}.png`}
width={40}
height={40}
sx={{ borderRadius: 5 }}
/>
</Box>
<Flex
sx={{
height: 20,
fontSize: 12,
alignItems: 'center',
justifyContent: 'center',
}}
>
{getSigilName(sigilSet.sigilIds[0])}+
{getSigilName(sigilSet.sigilIds[1])}
</Flex>
</Box>
)
})}
</Flex>
</Box>
)
function getSigilName(id: string) {
return sigils.find((sigil) => sigil.id === id)?.name.slice(0, 3)
}
}
export default memo(SigilBox)
@@ -0,0 +1,108 @@
import { RemembranceSigil } from '../../../lib/honkai3rd/elysianRealm'
import ReactSelect, { components } from 'react-select'
import { Flex, Image } from 'theme-ui'
import { memo, useMemo } from 'react'
import { assetsBucketBaseUrl } from '../../../lib/consts'
interface SigilSelectProps {
instanceId: string
value: string
optionIds: string[]
sigils: RemembranceSigil[]
onChange: (newValue: string) => void
}
const SigilSelect = ({
instanceId,
optionIds,
sigils,
onChange,
value,
}: SigilSelectProps) => {
const sigilOptions = useMemo(() => {
return optionIds.map((sigilId) => {
const sigil = sigils.find((aSigil) => {
return aSigil.id === sigilId
})
if (sigil == null) {
return {
value: 'unknown',
label: 'unknown',
}
}
return {
value: sigil.id,
label: sigil.name,
}
})
}, [optionIds, sigils])
const sigil = useMemo(
() =>
sigils.find((sigil) => {
return sigil.id === value
}),
[sigils, value]
)
return (
<ReactSelect
instanceId={instanceId}
value={
sigil != null
? {
label: sigil.name,
value: sigil.id,
}
: null
}
onChange={(option) => {
if (option == null) {
return
}
onChange(option.value)
}}
options={sigilOptions}
components={{
SingleValue: (props) => {
return (
<>
<components.SingleValue {...props}>
<Flex sx={{ alignItems: 'center', color: 'black' }}>
<Image
width={20}
height={20}
alt={props.data.label}
src={`${assetsBucketBaseUrl}/honkai3rd/elysian-realm/remembrance-sigils/${props.data.value}.png`}
mr={2}
/>
{props.children}
</Flex>
</components.SingleValue>
</>
)
},
Option: (props) => {
return (
<>
<components.Option {...props}>
<Flex sx={{ alignItems: 'center', color: 'black' }}>
<Image
width={20}
height={20}
alt={props.data.label}
src={`${assetsBucketBaseUrl}/honkai3rd/elysian-realm/remembrance-sigils/${props.data.value}.png`}
mr={2}
/>
{props.children}
</Flex>
</components.Option>
</>
)
},
}}
/>
)
}
export default memo(SigilSelect)
@@ -0,0 +1,156 @@
import React from 'react'
import { Box, Flex, Image, Paragraph } from 'theme-ui'
import { assetsBucketBaseUrl } from '../../../lib/consts'
import { signetGroupMap } from '../../../lib/honkai3rd/elysianRealm'
import { ERGGSignet } from './types'
interface SignetBoxProps {
signets: ERGGSignet[]
}
const SignetBox = ({ signets }: SignetBoxProps) => {
return (
<>
{signets.map((signet, index) => {
const dia = 460
const radius = dia / 2
const margin = 30
const descriptionTop = 40 + margin + ((dia - margin * 2) / 4) * index
const top =
40 + radius + radius * Math.sin((30 / 180) * Math.PI * (index - 2))
const angle = Math.asin((radius - (top - 40)) / radius)
const left = Math.cos(angle) * radius + 100 + radius
const signetRadius = 45
return (
<React.Fragment key={index}>
<Flex
sx={{
position: 'absolute',
zIndex: 1,
top: `${top - signetRadius}px`,
left: `${left - signetRadius}px`,
backgroundColor: '#10131C',
height: signetRadius * 2,
width: signetRadius * 2,
justifyContent: 'center',
alignItems: 'center',
borderRadius: signetRadius,
border: '1px solid gray',
boxShadow: '5px 5px 10px rgba(0,0,0,0.5)',
flexDirection: 'column',
}}
>
<Image
alt=''
src={`${assetsBucketBaseUrl}/honkai3rd/elysian-realm/signets/${signet.group}.png`}
width={45}
height={45}
/>
<Box
className='signetGroupLabel'
sx={{
transform: 'translateX(-3px)',
fontSize: 18,
}}
>
{getSignetAltName(signet.group)}
</Box>
<Box
sx={{
backgroundColor: 'black',
border: '1px solid gray',
width: 20,
height: 20,
textAlign: 'center',
borderRadius: 4,
position: 'absolute',
bottom: '12px',
fontSize: 16,
lineHeight: '20px',
right: `${5}px`,
}}
>
{signet.nexus === 1 ? 'I' : 'II'}
</Box>
</Flex>
<Box
sx={{
position: 'absolute',
borderBottom: '2px dashed gray',
height: 0,
top: `${top}px`,
left: `${left}px`,
right: 340,
}}
/>
<Flex
sx={{
top: `${descriptionTop - 40}px`,
height: 80,
border: 'solid 1px gray',
boxSizing: 'border-box',
backgroundColor: 'rgba(0,0,0,0.5)',
boxShadow: '5px 5px 10px rgba(0,0,0,0.5)',
width: 330,
position: 'absolute',
right: 10,
alignItems: 'center',
}}
>
<Flex
sx={{
width: 80,
height: 60,
borderRight: 'solid 1px gray',
justifyContent: 'center',
alignItems: 'center',
mr: '5px',
flexShrink: 0,
}}
>
<Box
sx={{
fontSize: 4,
lineHeight: 1.3,
fontWeight: 'bold',
color:
signet.type === 'core'
? '#E39070'
: signet.type === 'start'
? '#FEDEC2'
: '#A59A9B',
}}
className='signetTypeLabel'
>
<Box>
<Box>
{signet.type === 'core'
? '핵심'
: signet.type === 'start'
? '과도'
: '보조'}
</Box>
<Box></Box>
</Box>
</Box>
</Flex>
<Paragraph
className='signetDescription'
sx={{ padding: '5px', whiteSpace: 'pre-wrap' }}
>
{signet.description}
</Paragraph>
</Flex>
</React.Fragment>
)
})}
</>
)
}
export default SignetBox
function getSignetAltName(id: string) {
return signetGroupMap.get(id)?.krAltName
}
@@ -0,0 +1,82 @@
import { useMemo } from 'react'
import ReactSelect, { components } from 'react-select'
import { Flex, Image } from 'theme-ui'
import { assetsBucketBaseUrl } from '../../../lib/consts'
import { signetGroups } from '../../../lib/honkai3rd/elysianRealm'
interface SignetGroupSelectProps {
instanceId: string
value: string
onChange: (newValue: string) => void
}
const SignetGroupSelect = ({
instanceId,
value,
onChange,
}: SignetGroupSelectProps) => {
const options = useMemo(() => {
return signetGroups.slice(1).map((signetGroup) => {
return {
value: signetGroup.id,
label: signetGroup.krAltName,
}
})
}, [])
const currentOption = options.find((option) => option.value === value)
return (
<ReactSelect
instanceId={instanceId}
value={currentOption}
onChange={(option) => {
if (option == null) {
return
}
onChange(option.value)
}}
options={options}
components={{
SingleValue: (props) => {
return (
<>
<components.SingleValue {...props}>
<Flex sx={{ alignItems: 'center', color: 'black' }}>
<Image
width={20}
height={20}
alt={props.data.label}
src={`${assetsBucketBaseUrl}/honkai3rd/elysian-realm/signets/${props.data.value}.png`}
sx={{ flexShrink: 0, mr: 2 }}
/>
{props.children}
</Flex>
</components.SingleValue>
</>
)
},
Option: (props) => {
return (
<>
<components.Option {...props}>
<Flex sx={{ alignItems: 'center', color: 'black' }}>
<Image
width={20}
height={20}
alt={props.data.label}
src={`${assetsBucketBaseUrl}/honkai3rd/elysian-realm/signets/${props.data.value}.png`}
sx={{ mr: 2, flexShrink: 0 }}
/>
{props.children}
</Flex>
</components.Option>
</>
)
},
}}
/>
)
}
export default SignetGroupSelect
@@ -0,0 +1,108 @@
import ReactSelect, { components } from 'react-select'
import { Flex, Image } from 'theme-ui'
import { memo, useMemo } from 'react'
import { assetsBucketBaseUrl } from '../../../lib/consts'
import { StigmataData } from '../../../lib/honkai3rd/stigmata'
interface StigmaSelectProps {
instanceId: string
value: string
optionIds: string[]
stigmata: StigmataData[]
onChange: (newValue: string) => void
}
const StigmaSelect = ({
instanceId,
optionIds,
stigmata,
onChange,
value,
}: StigmaSelectProps) => {
const stigmaOptions = useMemo(() => {
return optionIds.map((stigmaId) => {
const stigma = stigmata.find((aStigma) => {
return aStigma.id === stigmaId
})
if (stigma == null) {
return {
value: 'unknown',
label: 'unknown',
}
}
return {
value: stigma.id,
label: stigma.name,
}
})
}, [optionIds, stigmata])
const stigma = useMemo(
() =>
stigmata.find((aStigma) => {
return aStigma.id === value
}),
[value, stigmata]
)
return (
<ReactSelect
instanceId={instanceId}
value={
stigma != null
? {
label: stigma.name,
value: stigma.id,
}
: null
}
onChange={(option) => {
if (option == null) {
return
}
onChange(option.value)
}}
options={stigmaOptions}
components={{
SingleValue: (props) => {
return (
<>
<components.SingleValue {...props}>
<Flex sx={{ alignItems: 'center', color: 'black' }}>
<Image
width={20}
height={20}
alt={props.data.label}
src={`${assetsBucketBaseUrl}/honkai3rd/stigmata/icon-${props.data.value}.png`}
sx={{ mr: 2, flexShrink: 0 }}
/>
{props.children}
</Flex>
</components.SingleValue>
</>
)
},
Option: (props) => {
return (
<>
<components.Option {...props}>
<Flex sx={{ alignItems: 'center', color: 'black' }}>
<Image
width={20}
height={20}
alt={props.data.label}
src={`${assetsBucketBaseUrl}/honkai3rd/stigmata/icon-${props.data.value}.png`}
sx={{ mr: 2, flexShrink: 0 }}
/>
{props.children}
</Flex>
</components.Option>
</>
)
},
}}
/>
)
}
export default memo(StigmaSelect)
@@ -0,0 +1,132 @@
import { Box, Flex, Image } from 'theme-ui'
import { assetsBucketBaseUrl } from '../../../lib/consts'
import { ERGGSupportSet } from './types'
interface SupportBoxProps {
supportSets: ERGGSupportSet[]
}
const SupportBox = ({ supportSets }: SupportBoxProps) => {
return (
<Box
sx={{
padding: '5px',
border: 'solid 1px gray',
boxSizing: 'border-box',
backgroundColor: 'rgba(0,0,0,0.5)',
boxShadow: '5px 5px 10px rgba(0,0,0,0.5)',
}}
>
<Flex>
<Box
sx={{
backgroundColor: '#8E5B45',
p: '5px',
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
width: 55,
mr: '5px',
}}
>
<Box></Box>
<Box></Box>
</Box>
{supportSets.map((supportSet, index) => {
return (
<Box
key={index}
sx={{
backgroundColor: '#615559',
p: '15px 5px 0',
borderRadius: '5px',
position: 'relative',
mr: '5px',
'&:last-child': {
mr: 0,
},
}}
>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
backgroundColor:
supportSet.type === 'util' ? '#356D7E' : '#EE4D2A',
borderTopLeftRadius: '5px',
borderBottomRightRadius: '5px',
p: '0 7px',
fontSize: 12,
}}
>
{supportSet.type === 'util' ? '유틸' : '딜링'}
</Box>
<Box sx={{ height: 40 }}>
{supportSet.battlesuitIds.map((battlesuitId) => {
return (
<Image
key={battlesuitId}
alt=''
src={`${assetsBucketBaseUrl}/honkai3rd/battlesuits/portrait-${battlesuitId}.png`}
width={40}
height={40}
sx={{
borderRadius: '5px',
mr: '5px',
'&:last-child': { mr: 0 },
}}
/>
)
})}
</Box>
<Flex
sx={{
height: 20,
fontSize: 12,
alignItems: 'center',
justifyContent: 'center',
}}
>
{getSupportName(supportSet.battlesuitIds[0])}+
{getSupportName(supportSet.battlesuitIds[1])}
</Flex>
</Box>
)
})}
</Flex>
</Box>
)
}
export default SupportBox
function getSupportName(id: string) {
switch (id) {
case 'le':
return '귀메'
case 'vc':
return '삐로냐'
case 'ss':
return '빙로냐'
case 'sn':
return '쩨레'
case 'br':
return '스메코'
case 'ae':
return '선인'
case 'hb':
return '브로니'
case 'ma':
return '레이븐'
case 'dp':
return '성녀'
case 'bke':
return '월백'
case 'rc':
return '파르도'
case 'sa':
return '테갈'
}
}
@@ -0,0 +1,149 @@
import { Box, Flex, Text } from 'theme-ui'
import { assetsBucketBaseUrl } from '../../../lib/consts'
import { colors } from './styles'
import { ERGGExSignet, ERGGExSignetType } from './types'
import { getExSignetLabel } from './utils'
interface ValkBoxProps {
battlesuitId: string
exSignets: ERGGExSignet[]
}
const ValkBox = ({ battlesuitId, exSignets }: ValkBoxProps) => {
return (
<Box
sx={{
position: 'absolute',
left: 100,
top: 40,
width: 460,
height: 460,
}}
>
{exSignets.map(({ type, name }, index) => {
return (
<ValkExSignetBox key={index} index={index} type={type} name={name} />
)
})}
<Box
sx={{
borderRadius: '50%',
border: 'solid 1px gray',
boxShadow: '5px 5px 10px rgba(0,0,0,0.5)',
width: 460,
height: 460,
backgroundImage: `url('${assetsBucketBaseUrl}/honkai3rd/battlesuits/${battlesuitId}.png')`,
backgroundSize: 'cover',
backgroundPosition: getBattlesuitImagePosition(battlesuitId),
backgroundRepeat: 'no-repeat',
backgroundColor: 'rgba(0,0,0,0.2)',
}}
></Box>
</Box>
)
}
function getBattlesuitImagePosition(battlesuitId: string) {
switch (battlesuitId) {
case 'meme':
return '0 10px'
default:
return 'center'
}
}
const ValkExSignetBox = ({
index,
type,
name,
}: {
index: number
type: ERGGExSignetType
name: string
}) => {
const label = getExSignetLabel(name)
const intervalAngle = 15
const angle = -intervalAngle * (index + 1) - 24
return (
<Box
sx={{
position: 'absolute',
top: 230 - 230 * Math.cos((angle / 180) * Math.PI) - 25,
left: 230 + 230 * Math.sin((angle / 180) * Math.PI) - 25,
width: 50,
height: 50,
}}
>
<Box
sx={{
position: 'absolute',
backgroundColor:
type === 'start'
? '#C52CC5'
: type === '1st'
? '#EE4D2A'
: type === '2nd'
? '#9082BD'
: type === 'backup'
? '#C4C4C4'
: 'transparent',
borderWidth: 1,
borderStyle: 'solid',
color: type === 'backup' ? 'black' : '#fff',
borderColor: type === 'na' ? '#AA9FA3' : 'transparent',
top: '5px',
right: 25,
whiteSpace: 'nowrap',
paddingRight: '30px',
paddingLeft: '10px',
textAlign: 'right',
lineHeight: 1.3,
zIndex: 1,
borderTopLeftRadius: '4px',
borderBottomLeftRadius: '4px',
}}
>
{type === 'start'
? '시작'
: type === '1st'
? '우선'
: type === '2nd'
? '차선'
: type === 'backup'
? '땜빵'
: '미선택'}
</Box>
<Flex
sx={{
position: 'absolute',
zIndex: 2,
border: 'solid 1px gray',
borderRadius: '50%',
backgroundColor: colors.backgroundColor,
boxShadow: '5px 5px 10px rgba(0,0,0,0.5)',
alignItems: 'center',
justifyContent: 'center',
fontSize:
label.length > 3
? label.indexOf('\n') >= 0
? '14px'
: '11px'
: label.length > 2
? 16
: 20,
width: 50,
height: 50,
lineHeight: 1.2,
textAlign: 'center',
whiteSpace: 'pre',
}}
className='exSignetLabel'
>
<Text>{label}</Text>
</Flex>
</Box>
)
}
export default ValkBox
@@ -0,0 +1,105 @@
import ReactSelect, { components } from 'react-select'
import { Flex, Image } from 'theme-ui'
import { memo, useMemo } from 'react'
import { assetsBucketBaseUrl } from '../../../lib/consts'
import { WeaponData } from '../../../lib/honkai3rd/weapons'
interface WeaponSelectProps {
instanceId: string
value: string
optionIds: string[]
weapons: WeaponData[]
onChange: (newValue: string) => void
}
const WeaponSelect = ({
instanceId,
optionIds,
weapons,
onChange,
value,
}: WeaponSelectProps) => {
const weaponOptions = useMemo(() => {
return optionIds.map((weaponId) => {
const weapon = weapons.find((aWeapon) => {
return aWeapon.id === weaponId
})
if (weapon == null) {
return {
value: 'unknown',
label: 'unknown',
}
}
return {
value: weapon.id,
label: weapon.name,
}
})
}, [optionIds, weapons])
const weaponValue = useMemo(() => {
const weapon = weapons.find((aWeapon) => {
return aWeapon.id === value
})
return weapon != null
? {
label: weapon.name,
value: weapon.id,
}
: null
}, [value, weapons])
return (
<ReactSelect
instanceId={instanceId}
value={weaponValue}
onChange={(option) => {
if (option == null) {
return
}
onChange(option.value)
}}
options={weaponOptions}
components={{
SingleValue: (props) => {
return (
<>
<components.SingleValue {...props}>
<Flex sx={{ alignItems: 'center', color: 'black' }}>
<Image
width={20}
height={20}
alt={props.data.label}
src={`${assetsBucketBaseUrl}/honkai3rd/weapons/${props.data.value}.png`}
sx={{ mr: 2, flexShrink: 0 }}
/>
{props.children}
</Flex>
</components.SingleValue>
</>
)
},
Option: (props) => {
return (
<>
<components.Option {...props}>
<Flex sx={{ alignItems: 'center', color: 'black' }}>
<Image
width={20}
height={20}
alt={props.data.label}
src={`${assetsBucketBaseUrl}/honkai3rd/weapons/${props.data.value}.png`}
sx={{ mr: 2, flexShrink: 0 }}
/>
{props.children}
</Flex>
</components.Option>
</>
)
},
}}
/>
)
}
export default memo(WeaponSelect)
@@ -0,0 +1,6 @@
export const colors = {
backgroundColor: '#311E22',
abstinenceDifficultyColor: '#F09F85',
corruptionDifficultyColor: '#EE4E2C',
infernoDifficultyColor: '#9DA3D3',
}
@@ -0,0 +1,51 @@
export type ERGGExSignetType = 'start' | '1st' | '2nd' | 'backup' | 'na'
export interface ERGGExSignet {
type: ERGGExSignetType
name: string
}
export type ERGGDataUpdater = <T extends keyof ERGGData>(
key: T,
value: ERGGData[T] | ((previousData: ERGGData[T]) => ERGGData[T])
) => void
export interface ERGGSupportSet {
type: 'util' | 'dps'
battlesuitIds: [string, string]
}
export interface ERGGSigilSet {
type: 'start' | 'mid' | 'end'
sigilIds: [string, string]
}
export type ERGGDifficulty = 'abstinence' | 'corruption' | 'inferno'
export interface ERGGEquipmentSet {
type: 'best' | 'alt'
weapon: string
top: string
mid: string
bot: string
}
export interface ERGGSignet {
group: string
type: 'core' | 'start' | 'sub'
nexus: 1 | 2
description: string
}
export type ERGGData = {
rank?: string
tag: string
signature: string
battlesuitId: string
difficulty: ERGGDifficulty
exSignets: ERGGExSignet[]
supportSets: ERGGSupportSet[]
sigilSets: ERGGSigilSet[]
equipmentSets: ERGGEquipmentSet[]
signets: ERGGSignet[]
}
@@ -0,0 +1,6 @@
export function getExSignetLabel(name: string) {
return name
.replace('의 축복', '')
.replace(/\s/g, '\n')
.replace(/[\[\]]/g, '')
}