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:
@@ -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'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, '')
|
||||
}
|
||||
@@ -113,6 +113,26 @@ export const remembranceSigilIds = [
|
||||
'key-to-the-deep',
|
||||
]
|
||||
|
||||
export function isGeneralSigil(sigilId: string) {
|
||||
switch (sigilId) {
|
||||
case 'it-will-be-written':
|
||||
case 'dreamful-gold':
|
||||
case 'an-old-pal-s-legacy':
|
||||
case 'empty-like-shala':
|
||||
case 'tsukimi-himiko':
|
||||
case 'boundless-logos':
|
||||
case 'hometown':
|
||||
case 'because-of-you':
|
||||
case 'boundless-feeling':
|
||||
case 'falling-in-past-light':
|
||||
case 'out-of-reach':
|
||||
case 'the-lonely-moon':
|
||||
case 'awakening':
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export interface SignetData {
|
||||
id: string
|
||||
name: string
|
||||
@@ -127,6 +147,8 @@ export interface SignetGroup {
|
||||
id: string
|
||||
name: string
|
||||
krName: string
|
||||
altName: string
|
||||
krAltName: string
|
||||
setIds: string[]
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,11 @@ export const theme: Theme = {
|
||||
lineHeight: 'heading',
|
||||
},
|
||||
},
|
||||
forms: {
|
||||
checkbox: {
|
||||
color: 'border',
|
||||
},
|
||||
},
|
||||
buttons: {
|
||||
primary: {
|
||||
color: 'primary',
|
||||
@@ -144,6 +149,9 @@ export const theme: Theme = {
|
||||
'&.hidden': {
|
||||
display: 'none',
|
||||
},
|
||||
'&:disabled': {
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
secondary: {
|
||||
color: 'background',
|
||||
|
||||
@@ -4,7 +4,6 @@ import Breadcrumb from '../../../components/organisms/Breadcrumb'
|
||||
import { getBattlesuitMapByIds } from '../../../server/data/honkai3rd/battlesuits'
|
||||
import { BattlesuitData } from '../../../lib/honkai3rd/battlesuits'
|
||||
import BattlesuitCard from '../../../components/molecules/BattlesuitCard'
|
||||
|
||||
import { NextPageContext } from 'next'
|
||||
import { getI18NProps } from '../../../server/i18n'
|
||||
import { useTranslation } from '../../../lib/i18n'
|
||||
@@ -19,6 +18,7 @@ import {
|
||||
getRemembranceSigils as getRemembranceSigils,
|
||||
getSupportBattlesuits,
|
||||
} from '../../../server/data/honkai3rd/elysianRealm'
|
||||
import Link from 'next/link'
|
||||
|
||||
type BattlesuitListItemData = Pick<
|
||||
BattlesuitData,
|
||||
@@ -152,6 +152,11 @@ const ElysianRealmIndexPage = ({
|
||||
})}
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box>
|
||||
<Link href='/honkai3rd/elysian-realm/utils/gen-guide'>
|
||||
Guide Generator (KR Only)
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
</Honkai3rdLayout>
|
||||
)
|
||||
@@ -180,6 +185,7 @@ export async function getStaticProps({ locale }: NextPageContext) {
|
||||
name,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
props: {
|
||||
battlesuitMap: getBattlesuitMapByIds(erBattlesuitIds, locale),
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/* eslint-disable @next/next/no-page-custom-font */
|
||||
import { NextPageContext } from 'next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { pick } from 'ramda'
|
||||
import { Box, Button, Flex, Heading } from 'theme-ui'
|
||||
import ERGuideGenerator from '../../../../components/organisms/ERGuideGenerator/ERGuideGenerator'
|
||||
import { BattlesuitData } from '../../../../lib/honkai3rd/battlesuits'
|
||||
import {
|
||||
PopulatedSignetGroup,
|
||||
RemembranceSigil,
|
||||
} from '../../../../lib/honkai3rd/elysianRealm'
|
||||
import { WeaponData } from '../../../../lib/honkai3rd/weapons'
|
||||
import { StigmataData } from '../../../../lib/honkai3rd/stigmata'
|
||||
import { listBattlesuits } from '../../../../server/data/honkai3rd/battlesuits'
|
||||
import {
|
||||
getRemembranceSigils,
|
||||
getSignetGroupById,
|
||||
} from '../../../../server/data/honkai3rd/elysianRealm'
|
||||
import { listStigmata } from '../../../../server/data/honkai3rd/stigmata'
|
||||
import { listWeapons } from '../../../../server/data/honkai3rd/weapons'
|
||||
import { getI18NProps } from '../../../../server/i18n'
|
||||
|
||||
interface GenerateGuidePageProps {
|
||||
weapons: WeaponData[]
|
||||
stigmata: StigmataData[]
|
||||
battlesuits: BattlesuitData[]
|
||||
exSignetGroup: PopulatedSignetGroup
|
||||
sigils: RemembranceSigil[]
|
||||
}
|
||||
|
||||
const GenerateGuidePage = ({
|
||||
weapons,
|
||||
stigmata,
|
||||
battlesuits,
|
||||
exSignetGroup,
|
||||
sigils,
|
||||
}: GenerateGuidePageProps) => {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Flex m={2} sx={{ alignItems: 'center' }}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
router.push('/honkai3rd')
|
||||
}}
|
||||
mr={2}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Heading>ER Guide Generator</Heading>
|
||||
</Flex>
|
||||
<ERGuideGenerator
|
||||
weapons={weapons}
|
||||
stigmata={stigmata}
|
||||
battlesuits={battlesuits}
|
||||
exSignetGroup={exSignetGroup}
|
||||
sigils={sigils}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default GenerateGuidePage
|
||||
|
||||
export async function getStaticProps({ locale }: NextPageContext) {
|
||||
return {
|
||||
props: {
|
||||
weapons: listWeapons('ko-KR').map((weapon) => {
|
||||
return pick(['name', 'id', 'rarity', 'category'], weapon)
|
||||
}),
|
||||
stigmata: listStigmata('ko-KR').map((stigmata) => {
|
||||
return pick(['name', 'id', 'type'], stigmata)
|
||||
}),
|
||||
battlesuits: listBattlesuits('ko-KR').map((battlesuit) => {
|
||||
return {
|
||||
id: battlesuit.id,
|
||||
name: battlesuit.name,
|
||||
type: battlesuit.type,
|
||||
valkyrie: battlesuit.valkyrie,
|
||||
}
|
||||
}),
|
||||
exSignetGroup: getSignetGroupById('elysia', 'ko-KR'),
|
||||
sigils: getRemembranceSigils('ko-KR'),
|
||||
...(await getI18NProps('ko-KR')),
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user