Refactor object grids to handle business logic instead of Party

This commit is contained in:
Justin Edmund 2022-02-02 16:54:14 -08:00
parent 44966fe8fe
commit 827473ee5a
16 changed files with 726 additions and 477 deletions

View file

@ -1,11 +1,18 @@
import React, { useState } from 'react' /* eslint-disable react-hooks/exhaustive-deps */
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useCookies } from 'react-cookie'
import { useModal as useModal } from '~utils/useModal' import { useModal as useModal } from '~utils/useModal'
import { AxiosResponse } from 'axios'
import debounce from 'lodash.debounce'
import CharacterUnit from '~components/CharacterUnit' import CharacterUnit from '~components/CharacterUnit'
import SearchModal from '~components/SearchModal' import SearchModal from '~components/SearchModal'
import api from '~utils/api'
import './index.scss' import './index.scss'
// GridType
export enum GridType { export enum GridType {
Class, Class,
Character, Character,
@ -13,67 +20,185 @@ export enum GridType {
Summon Summon
} }
// Props
interface Props { interface Props {
userId?: string partyId?: string
grid: GridArray<Character> characters: GridArray<GridCharacter>
editable: boolean editable: boolean
exists: boolean createParty: () => Promise<AxiosResponse<any, any>>
onSelect: (type: GridType, character: Character, position: number) => void pushHistory?: (path: string) => void
} }
const CharacterGrid = (props: Props) => { const CharacterGrid = (props: Props) => {
const { open, openModal, closeModal } = useModal() // Constants
const [searchPosition, setSearchPosition] = useState(0)
const numCharacters: number = 5 const numCharacters: number = 5
function isCharacter(object: Character | Weapon | Summon): object is Character { // Cookies
// There aren't really any unique fields here const [cookies, _] = useCookies(['user'])
return (object as Character).gender !== undefined const headers = (cookies.user != null) ? {
} headers: {
'Authorization': `Bearer ${cookies.user.access_token}`
}
} : {}
// Set up states for Grid data
const [characters, setCharacters] = useState<GridArray<GridCharacter>>({})
// Set up states for Search
const { open, openModal, closeModal } = useModal()
const [itemPositionForSearch, setItemPositionForSearch] = useState(0)
// Create a temporary state to store previous character uncap values
const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({})
// Create a state dictionary to store pure objects for Search
const [searchGrid, setSearchGrid] = useState<GridArray<Character>>({})
// Set states from props
useEffect(() => {
setCharacters(props.characters || {})
}, [props])
// Update search grid whenever characters are updated
useEffect(() => {
let newSearchGrid = Object.values(characters).map((o) => o.character)
setSearchGrid(newSearchGrid)
}, [characters])
// Methods: Adding an object from search
function openSearchModal(position: number) { function openSearchModal(position: number) {
setSearchPosition(position) setItemPositionForSearch(position)
openModal() openModal()
} }
function receiveCharacter(character: Character, position: number) { function receiveCharacterFromSearch(object: Character | Weapon | Summon, position: number) {
props.onSelect(GridType.Character, character, position) const character = object as Character
}
function sendData(object: Character | Weapon | Summon, position: number) { if (!props.partyId) {
if (isCharacter(object)) { props.createParty()
receiveCharacter(object, position) .then(response => {
const party = response.data.party
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
saveCharacter(party.id, character, position)
.then(response => storeGridCharacter(response.data.grid_character))
})
} else {
saveCharacter(props.partyId, character, position)
.then(response => storeGridCharacter(response.data.grid_character))
} }
} }
async function saveCharacter(partyId: string, character: Character, position: number) {
return await api.endpoints.characters.create({
'character': {
'party_id': partyId,
'character_id': character.id,
'position': position,
'mainhand': (position == -1),
'uncap_level': characterUncapLevel(character)
}
}, headers)
}
function storeGridCharacter(gridCharacter: GridCharacter) {
// Store the grid unit at the correct position
let newCharacters = Object.assign({}, characters)
newCharacters[gridCharacter.position] = gridCharacter
setCharacters(newCharacters)
}
// Methods: Helpers
function characterUncapLevel(character: Character) {
let uncapLevel
if (character.special) {
uncapLevel = 3
if (character.uncap.ulb) uncapLevel = 5
else if (character.uncap.flb) uncapLevel = 4
} else {
uncapLevel = 4
if (character.uncap.ulb) uncapLevel = 6
else if (character.uncap.flb) uncapLevel = 5
}
return uncapLevel
}
// Methods: Updating uncap level
// Note: Saves, but debouncing is not working properly
async function saveUncap(id: string, position: number, uncapLevel: number) {
try {
await api.updateUncap('weapon', id, uncapLevel)
.then(response => {
storeGridCharacter(response.data.grid_character)
})
} catch (error) {
console.error(error)
// Revert optimistic UI
updateUncapLevel(position, previousUncapValues[position])
// Remove optimistic key
let newPreviousValues = {...previousUncapValues}
delete newPreviousValues[position]
setPreviousUncapValues(newPreviousValues)
}
}
const initiateUncapUpdate = useCallback(
(id: string, position: number, uncapLevel: number) => {
debouncedAction(id, position, uncapLevel)
// Save the current value in case of an unexpected result
let newPreviousValues = {...previousUncapValues}
newPreviousValues[position] = characters[position].uncap_level
setPreviousUncapValues(newPreviousValues)
// Optimistically update UI
updateUncapLevel(position, uncapLevel)
}, [previousUncapValues, characters]
)
const debouncedAction = useMemo(() =>
debounce((id, position, number) => {
saveUncap(id, position, number)
}, 1000), [saveUncap]
)
const updateUncapLevel = (position: number, uncapLevel: number) => {
let newCharacters = Object.assign({}, characters)
newCharacters[position].uncap_level = uncapLevel
setCharacters(newCharacters)
}
// Render: JSX components
return ( return (
<div className="CharacterGrid"> <div className="CharacterGrid">
<ul id="grid_characters"> <ul id="grid_characters">
{ {Array.from(Array(numCharacters)).map((x, i) => {
Array.from(Array(numCharacters)).map((x, i) => { return (
return ( <li key={`grid_unit_${i}`} >
<li key={`grid_unit_${i}`} > <CharacterUnit
<CharacterUnit gridCharacter={props.characters[i]}
onClick={() => { openSearchModal(i) }} editable={props.editable}
editable={props.editable} position={i}
position={i} onClick={() => { openSearchModal(i) }}
character={props.grid[i]} updateUncap={initiateUncapUpdate}
/> />
</li> </li>
) )
}) })}
}
{open ? ( {open ? (
<SearchModal <SearchModal
grid={props.grid} grid={searchGrid}
close={closeModal} close={closeModal}
send={sendData} send={receiveCharacterFromSearch}
fromPosition={searchPosition} fromPosition={itemPositionForSearch}
object="characters" object="characters"
placeholderText="Search for a character..." placeholderText="Search for a character..."
/> />
) : null} ) : null}
</ul> </ul>
</div> </div>
) )

View file

@ -2,45 +2,53 @@ import React, { useEffect, useState } from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import UncapIndicator from '~components/UncapIndicator' import UncapIndicator from '~components/UncapIndicator'
import PlusIcon from '~public/icons/plus.svg' import PlusIcon from '~public/icons/plus.svg'
import './index.scss' import './index.scss'
interface Props { interface Props {
onClick: () => void gridCharacter: GridCharacter | undefined
character: Character | undefined
position: number position: number
editable: boolean editable: boolean
onClick: () => void
updateUncap: (id: string, position: number, uncap: number) => void
} }
const CharacterUnit = (props: Props) => { const CharacterUnit = (props: Props) => {
console.log(props.gridCharacter?.character.name.en, props.gridCharacter?.uncap_level)
const [imageUrl, setImageUrl] = useState('') const [imageUrl, setImageUrl] = useState('')
const classes = classnames({ const classes = classnames({
CharacterUnit: true, CharacterUnit: true,
'editable': props.editable, 'editable': props.editable,
'filled': (props.character !== undefined) 'filled': (props.gridCharacter !== undefined)
}) })
const character = props.character const gridCharacter = props.gridCharacter
const character = gridCharacter?.character
useEffect(() => { useEffect(() => {
generateImageUrl() generateImageUrl()
}) })
function generateImageUrl() { function generateImageUrl() {
let imgSrc = "" let imgSrc = ""
if (props.character) { if (props.gridCharacter) {
const character = props.character! const character = props.gridCharacter.character!
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-main/${character.granblue_id}_01.jpg` imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-main/${character.granblue_id}_01.jpg`
} }
setImageUrl(imgSrc) setImageUrl(imgSrc)
} }
function passUncapData(uncap: number) {
console.log(`passuncapdata ${uncap}`)
if (props.gridCharacter)
props.updateUncap(props.gridCharacter.id, props.position, uncap)
}
return ( return (
<div> <div>
<div className={classes}> <div className={classes}>
@ -48,11 +56,15 @@ const CharacterUnit = (props: Props) => {
<img alt={character?.name.en} className="grid_image" src={imageUrl} /> <img alt={character?.name.en} className="grid_image" src={imageUrl} />
{ (props.editable) ? <span className='icon'><PlusIcon /></span> : '' } { (props.editable) ? <span className='icon'><PlusIcon /></span> : '' }
</div> </div>
<UncapIndicator { (gridCharacter && character) ?
type="character" <UncapIndicator
flb={character?.uncap.flb || false} type="character"
uncapLevel={(character?.rarity == 2) ? 3 : 4} flb={character.uncap.flb || false}
/> ulb={character.uncap.ulb || false}
uncapLevel={gridCharacter.uncap_level}
updateUncap={passUncapData}
special={character.special}
/> : '' }
<h3 className="CharacterName">{character?.name.en}</h3> <h3 className="CharacterName">{character?.name.en}</h3>
</div> </div>
</div> </div>

View file

@ -18,7 +18,7 @@ interface Props {
found?: boolean found?: boolean
offset: number offset: number
onClick: (position: number) => void onClick: (position: number) => void
updateUncap: (id: string, uncap: number) => void updateUncap: (id: string, position: number, uncap: number) => void
} }
const ExtraSummons = (props: Props) => { const ExtraSummons = (props: Props) => {

View file

@ -15,11 +15,10 @@ export enum GridType {
interface Props { interface Props {
grid: GridArray<GridWeapon> grid: GridArray<GridWeapon>
editable: boolean editable: boolean
exists: boolean
found?: boolean found?: boolean
offset: number offset: number
onClick: (position: number) => void onClick: (position: number) => void
updateUncap: (id: string, uncap: number) => void updateUncap: (id: string, position: number, uncap: number) => void
} }
const ExtraWeapons = (props: Props) => { const ExtraWeapons = (props: Props) => {

View file

@ -1,15 +1,14 @@
import React, { ChangeEvent, useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useCookies } from 'react-cookie' import { useCookies } from 'react-cookie'
import api from '~utils/api'
// UI Elements
import PartySegmentedControl from '~components/PartySegmentedControl' import PartySegmentedControl from '~components/PartySegmentedControl'
// Grids
import WeaponGrid from '~components/WeaponGrid' import WeaponGrid from '~components/WeaponGrid'
import SummonGrid from '~components/SummonGrid' import SummonGrid from '~components/SummonGrid'
import CharacterGrid from '~components/CharacterGrid' import CharacterGrid from '~components/CharacterGrid'
import api from '~utils/api'
import './index.scss'
// GridType // GridType
enum GridType { enum GridType {
Class, Class,
@ -17,96 +16,58 @@ enum GridType {
Weapon, Weapon,
Summon Summon
} }
export { GridType }
import './index.scss'
// Props
interface Props { interface Props {
partyId?: string partyId?: string
mainWeapon?: GridWeapon mainWeapon?: GridWeapon
mainSummon?: GridSummon mainSummon?: GridSummon
friendSummon?: GridSummon friendSummon?: GridSummon
characters?: GridArray<Character> characters?: GridArray<GridCharacter>
weapons?: GridArray<GridWeapon> weapons?: GridArray<GridWeapon>
summons?: GridArray<GridSummon> summons?: GridArray<GridSummon>
extra: boolean extra: boolean
editable: boolean editable: boolean
exists: boolean
pushHistory?: (path: string) => void pushHistory?: (path: string) => void
} }
const Party = (props: Props) => { const Party = (props: Props) => {
// Cookies
const [cookies, _] = useCookies(['user']) const [cookies, _] = useCookies(['user'])
const headers = (cookies.user != null) ? { const headers = (cookies.user != null) ? {
headers: { headers: {
'Authorization': `Bearer ${cookies.user.access_token}` 'Authorization': `Bearer ${cookies.user.access_token}`
} }
} : {} } : {}
// Grid data // Set up states
const [characters, setCharacters] = useState<GridArray<Character>>({})
const [weapons, setWeapons] = useState<GridArray<GridWeapon>>({})
const [summons, setSummons] = useState<GridArray<GridSummon>>({})
const [mainWeapon, setMainWeapon] = useState<GridWeapon>()
const [mainSummon, setMainSummon] = useState<GridSummon>()
const [friendSummon, setFriendSummon] = useState<GridSummon>()
const [extra, setExtra] = useState<boolean>(false) const [extra, setExtra] = useState<boolean>(false)
useEffect(() => {
setPartyId(props.partyId || '')
setMainWeapon(props.mainWeapon)
setMainSummon(props.mainSummon)
setFriendSummon(props.friendSummon)
setCharacters(props.characters || {})
setWeapons(props.weapons || {})
setSummons(props.summons || {})
setExtra(props.extra || false)
}, [props.partyId, props.mainWeapon, props.mainSummon, props.friendSummon, props.characters, props.weapons, props.summons, props.extra])
const weaponGrid = (
<WeaponGrid
userId={cookies.user ? cookies.user.userId : ''}
mainhand={mainWeapon}
grid={weapons}
editable={props.editable}
exists={props.exists}
extra={extra}
onSelect={itemSelected}
/>
)
const summonGrid = (
<SummonGrid
userId={cookies.user ? cookies.user.userId : ''}
main={mainSummon}
friend={friendSummon}
grid={summons}
editable={props.editable}
exists={props.exists}
onSelect={itemSelected}
/>
)
const characterGrid = (
<CharacterGrid
userId={cookies.user ? cookies.user.userId : ''}
grid={characters}
editable={props.editable}
exists={props.exists}
onSelect={itemSelected}
/>
)
const [currentTab, setCurrentTab] = useState<GridType>(GridType.Weapon) const [currentTab, setCurrentTab] = useState<GridType>(GridType.Weapon)
const [partyId, setPartyId] = useState('')
// Set states from props
useEffect(() => {
setExtra(props.extra || false)
}, [props])
// Methods: Creating a new party
async function createParty() {
let body = {
party: {
...(cookies.user) && { user_id: cookies.user.user_id },
is_extra: extra
}
}
return await api.endpoints.parties.create(body, headers)
}
// Methods: Updating the party's extra flag
// Note: This doesn't save to the server yet.
function checkboxChanged(event: React.ChangeEvent<HTMLInputElement>) { function checkboxChanged(event: React.ChangeEvent<HTMLInputElement>) {
setExtra(event.target.checked) setExtra(event.target.checked)
} }
// Methods: Navigating with segmented control
function segmentClicked(event: React.ChangeEvent<HTMLInputElement>) { function segmentClicked(event: React.ChangeEvent<HTMLInputElement>) {
switch(event.target.value) { switch(event.target.value) {
case 'class': case 'class':
@ -126,174 +87,66 @@ const Party = (props: Props) => {
} }
} }
function itemSelected(type: GridType, item: Character | Weapon | Summon, position: number) { // Render: JSX components
if (!partyId) { const navigation = (
createParty() <PartySegmentedControl
.then(response => { extra={props.extra}
return response.data.party editable={props.editable}
}) selectedTab={currentTab}
.then(party => { onClick={segmentClicked}
if (props.pushHistory) { onCheckboxChange={checkboxChanged}
props.pushHistory(`/p/${party.shortcode}`) />
} )
return party.id const weaponGrid = (
}) <WeaponGrid
.then(partyId => { partyId={props.partyId}
setPartyId(partyId) mainhand={props.mainWeapon}
saveItem(partyId, type, item, position) weapons={props.weapons || {}}
}) extra={props.extra}
} else { editable={props.editable}
saveItem(partyId, type, item, position) createParty={createParty}
} pushHistory={props.pushHistory}
} />
)
async function createParty() { const summonGrid = (
const body = (!cookies.user) ? { <SummonGrid
party: { partyId={props.partyId}
is_extra: extra mainSummon={props.mainSummon}
} friendSummon={props.friendSummon}
} : { summons={props.summons || {}}
party: { editable={props.editable}
user_id: cookies.user.userId, createParty={createParty}
is_extra: extra pushHistory={props.pushHistory}
} />
} )
return await api.endpoints.parties.create(body, headers) const characterGrid = (
} <CharacterGrid
partyId={props.partyId}
characters={props.characters || {}}
editable={props.editable}
createParty={createParty}
pushHistory={props.pushHistory}
/>
)
function saveItem(partyId: string, type: GridType, item: Character | Weapon | Summon, position: number) { const currentGrid = () => {
switch(type) { switch(currentTab) {
case GridType.Class:
saveClass()
break
case GridType.Character: case GridType.Character:
const character = item as Character return characterGrid
saveCharacter(character, position, partyId)
.then(() => {
storeCharacter(character, position)
})
break
case GridType.Weapon: case GridType.Weapon:
const weapon = item as Weapon return weaponGrid
saveWeapon(weapon, position, partyId)
.then((response) => {
storeWeapon(response.data.grid_weapon)
})
break
case GridType.Summon: case GridType.Summon:
const summon = item as Summon return summonGrid
saveSummon(summon, position, partyId)
.then((response) => {
storeSummon(response.data.grid_summon, position)
})
break
} }
} }
// Weapons
function storeWeapon(weapon: GridWeapon) {
if (weapon.position == -1) {
setMainWeapon(weapon)
} else {
// Store the grid unit weapon at the correct position
let newWeapons = Object.assign({}, weapons)
newWeapons[weapon.position!] = weapon
setWeapons(newWeapons)
}
}
async function saveWeapon(weapon: Weapon, position: number, party: string) {
let uncapLevel = 3
if (weapon.uncap.ulb)
uncapLevel = 5
else if (weapon.uncap.flb)
uncapLevel = 4
return await api.endpoints.weapons.create({
'weapon': {
'party_id': party,
'weapon_id': weapon.id,
'position': position,
'mainhand': (position == -1),
'uncap_level': uncapLevel
}
}, headers)
}
// Summons
function storeSummon(summon: GridSummon, position: number) {
if (position == -1) {
setMainSummon(summon)
} else if (position == 6) {
setFriendSummon(summon)
} else {
// Store the grid unit summon at the correct position
let newSummons = Object.assign({}, summons)
newSummons[position] = summon
setSummons(newSummons)
}
}
async function saveSummon(summon: Summon, position: number, party: string) {
return await api.endpoints.summons.create({
'summon': {
'party_id': party,
'summon_id': summon.id,
'position': position,
'main': (position == -1),
'friend': (position == 6)
}
}, headers)
}
// Character
function storeCharacter(character: Character, position: number) {
// Store the grid unit character at the correct position
let newCharacters = Object.assign({}, characters)
newCharacters[position] = character
setCharacters(newCharacters)
}
async function saveCharacter(character: Character, position: number, party: string) {
await api.endpoints.characters.create({
'character': {
'party_id': party,
'character_id': character.id,
'position': position
}
}, headers)
}
// Class
function saveClass() {
// TODO: Implement this
}
return ( return (
<div> <div>
<PartySegmentedControl { navigation }
extra={extra} { currentGrid() }
editable={props.editable}
selectedTab={currentTab}
onClick={segmentClicked}
onCheckboxChange={checkboxChanged}
/>
{
(() => {
switch(currentTab) {
case GridType.Character:
return characterGrid
case GridType.Weapon:
return weaponGrid
case GridType.Summon:
return summonGrid
}
})()
}
</div> </div>
) )
} }

View file

@ -1,6 +1,9 @@
import React, { useCallback, useState } from 'react' /* eslint-disable react-hooks/exhaustive-deps */
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useCookies } from 'react-cookie'
import { useModal as useModal } from '~utils/useModal' import { useModal as useModal } from '~utils/useModal'
import { AxiosResponse } from 'axios'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import SearchModal from '~components/SearchModal' import SearchModal from '~components/SearchModal'
@ -20,128 +23,237 @@ export enum GridType {
// Props // Props
interface Props { interface Props {
userId?: string
partyId?: string partyId?: string
main?: GridSummon | undefined mainSummon: GridSummon | undefined
friend?: GridSummon | undefined friendSummon: GridSummon | undefined
grid: GridArray<GridSummon> summons: GridArray<GridSummon>
editable: boolean editable: boolean
exists: boolean createParty: () => Promise<AxiosResponse<any, any>>
found?: boolean pushHistory?: (path: string) => void
onSelect: (type: GridType, summon: Summon, position: number) => void
} }
const SummonGrid = (props: Props) => { const SummonGrid = (props: Props) => {
const { open, openModal, closeModal } = useModal() // Constants
const [searchPosition, setSearchPosition] = useState(0)
const numSummons: number = 4 const numSummons: number = 4
const searchGrid: GridArray<Summon> = Object.values(props.grid).map((o) => o.summon)
function receiveSummon(summon: Summon, position: number) { // Cookies
props.onSelect(GridType.Summon, summon, position) const [cookies, _] = useCookies(['user'])
} const headers = (cookies.user != null) ? {
headers: {
function sendData(object: Character | Weapon | Summon, position: number) { 'Authorization': `Bearer ${cookies.user.access_token}`
if (isSummon(object)) {
receiveSummon(object, position)
} }
} } : {}
function isSummon(object: Character | Weapon | Summon): object is Summon { // Set up states for Grid data
// There aren't really any unique fields here const [summons, setSummons] = useState<GridArray<GridSummon>>({})
return (object as Summon).granblue_id !== undefined const [mainSummon, setMainSummon] = useState<GridSummon>()
} const [friendSummon, setFriendSummon] = useState<GridSummon>()
// Set up states for Search
const { open, openModal, closeModal } = useModal()
const [itemPositionForSearch, setItemPositionForSearch] = useState(0)
// Create a temporary state to store previous weapon uncap value
const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({})
// Create a state dictionary to store pure objects for Search
const [searchGrid, setSearchGrid] = useState<GridArray<Summon>>({})
// Set states from props
useEffect(() => {
setSummons(props.summons || {})
setMainSummon(props.mainSummon)
setFriendSummon(props.friendSummon)
}, [props])
// Update search grid whenever any summon is updated
useEffect(() => {
let newSearchGrid = Object.values(summons).map((o) => o.summon)
if (mainSummon)
newSearchGrid.unshift(mainSummon.summon)
if (friendSummon)
newSearchGrid.unshift(friendSummon.summon)
setSearchGrid(newSearchGrid)
}, [summons, mainSummon, friendSummon])
// Methods: Adding an object from search
function openSearchModal(position: number) { function openSearchModal(position: number) {
setSearchPosition(position) setItemPositionForSearch(position)
openModal() openModal()
} }
async function updateUncap(id: string, level: number) { function receiveSummonFromSearch(object: Character | Weapon | Summon, position: number) {
await api.updateUncap('summon', id, level) const summon = object as Summon
.catch(error => {
console.error(error) if (!props.partyId) {
}) props.createParty()
.then(response => {
const party = response.data.party
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
saveSummon(party.id, summon, position)
.then(response => storeGridSummon(response.data.grid_summon))
})
} else {
saveSummon(props.partyId, summon, position)
.then(response => storeGridSummon(response.data.grid_summon))
}
} }
const initiateUncapUpdate = (id: string, uncapLevel: number) => { async function saveSummon(partyId: string, summon: Summon, position: number) {
debouncedAction(id, uncapLevel) let uncapLevel = 3
if (summon.uncap.ulb) uncapLevel = 5
else if (summon.uncap.flb) uncapLevel = 4
return await api.endpoints.summons.create({
'summon': {
'party_id': partyId,
'summon_id': summon.id,
'position': position,
'main': (position == -1),
'friend': (position == 6),
'uncap_level': uncapLevel
}
}, headers)
} }
const debouncedAction = useCallback( function storeGridSummon(gridSummon: GridSummon) {
() => debounce((id, number) => { if (gridSummon.position == -1) {
updateUncap(id, number) setMainSummon(gridSummon)
}, 1000), [] } else if (gridSummon.position == 6) {
)() setFriendSummon(gridSummon)
} else {
// Store the grid unit at the correct position
let newSummons = Object.assign({}, summons)
newSummons[gridSummon.position] = gridSummon
setSummons(newSummons)
}
}
// Methods: Updating uncap level
// Note: Saves, but debouncing is not working properly
async function saveUncap(id: string, position: number, uncapLevel: number) {
try {
await api.updateUncap('summon', id, uncapLevel)
.then(response => {
storeGridSummon(response.data.grid_summon)
})
} catch (error) {
console.error(error)
// Revert optimistic UI
updateUncapLevel(position, previousUncapValues[position])
// Remove optimistic key
let newPreviousValues = {...previousUncapValues}
delete newPreviousValues[position]
setPreviousUncapValues(newPreviousValues)
}
}
const initiateUncapUpdate = useCallback(
(id: string, position: number, uncapLevel: number) => {
debouncedAction(id, position, uncapLevel)
// Save the current value in case of an unexpected result
let newPreviousValues = {...previousUncapValues}
newPreviousValues[position] = summons[position].uncap_level
setPreviousUncapValues(newPreviousValues)
// Optimistically update UI
updateUncapLevel(position, uncapLevel)
}, [previousUncapValues, summons]
)
const debouncedAction = useMemo(() =>
debounce((id, position, number) => {
saveUncap(id, position, number)
}, 1000), [saveUncap]
)
const updateUncapLevel = (position: number, uncapLevel: number) => {
let newSummons = Object.assign({}, summons)
newSummons[position].uncap_level = uncapLevel
setSummons(newSummons)
}
// Render: JSX components
const mainSummonElement = (
<div className="LabeledUnit">
<div className="Label">Main Summon</div>
<SummonUnit
gridSummon={props.mainSummon}
editable={props.editable}
key="grid_main_summon"
position={-1}
unitType={0}
onClick={() => { openSearchModal(-1) }}
updateUncap={initiateUncapUpdate}
/>
</div>
)
const friendSummonElement = (
<div className="LabeledUnit">
<div className="Label">Friend Summon</div>
<SummonUnit
gridSummon={props.friendSummon}
editable={props.editable}
key="grid_friend_summon"
position={6}
unitType={2}
onClick={() => { openSearchModal(6) }}
updateUncap={initiateUncapUpdate}
/>
</div>
)
const summonGridElement = (
<div id="LabeledGrid">
<div className="Label">Summons</div>
<ul id="grid_summons">
{Array.from(Array(numSummons)).map((x, i) => {
return (<li key={`grid_unit_${i}`} >
<SummonUnit
gridSummon={props.summons[i]}
editable={props.editable}
position={i}
unitType={1}
onClick={() => { openSearchModal(i) }}
updateUncap={initiateUncapUpdate}
/>
</li>)
})}
</ul>
</div>
)
const subAuraSummonElement = (
<ExtraSummons
grid={props.summons}
editable={props.editable}
exists={false}
offset={numSummons}
onClick={openSearchModal}
updateUncap={initiateUncapUpdate}
/>
)
return ( return (
<div> <div>
<div className="SummonGrid"> <div className="SummonGrid">
<div className="LabeledUnit"> { mainSummonElement }
<div className="Label">Main Summon</div> { friendSummonElement }
<SummonUnit { summonGridElement }
editable={props.editable}
key="grid_main_summon"
position={-1}
unitType={0}
gridSummon={props.main}
onClick={() => { openSearchModal(-1) }}
updateUncap={initiateUncapUpdate}
/>
</div>
<div className="LabeledUnit">
<div className="Label">Friend Summon</div>
<SummonUnit
editable={props.editable}
key="grid_friend_summon"
position={6}
unitType={2}
gridSummon={props.friend}
onClick={() => { openSearchModal(6) }}
updateUncap={initiateUncapUpdate}
/>
</div>
<div id="LabeledGrid">
<div className="Label">Summons</div>
<ul id="grid_summons">
{
Array.from(Array(numSummons)).map((x, i) => {
return (
<li key={`grid_unit_${i}`} >
<SummonUnit
editable={props.editable}
position={i}
unitType={1}
gridSummon={props.grid[i]}
onClick={() => { openSearchModal(i) }}
updateUncap={initiateUncapUpdate}
/>
</li>
)
})
}
</ul>
</div>
</div> </div>
<ExtraSummons { subAuraSummonElement }
grid={props.grid}
editable={props.editable}
exists={false}
offset={numSummons}
onClick={openSearchModal}
updateUncap={initiateUncapUpdate}
/>
{open ? ( {open ? (
<SearchModal <SearchModal
grid={searchGrid} grid={searchGrid}
close={closeModal} close={closeModal}
send={sendData} send={receiveSummonFromSearch}
fromPosition={searchPosition} fromPosition={itemPositionForSearch}
object="summons" object="summons"
placeholderText="Search for a summon..." placeholderText="Search for a summon..."
/> />

View file

@ -8,7 +8,7 @@ import './index.scss'
interface Props { interface Props {
onClick: () => void onClick: () => void
updateUncap: (id: string, uncap: number) => void updateUncap: (id: string, position: number, uncap: number) => void
gridSummon: GridSummon | undefined gridSummon: GridSummon | undefined
position: number position: number
editable: boolean editable: boolean
@ -68,6 +68,7 @@ const SummonUnit = (props: Props) => {
flb={summon?.uncap.flb || false} flb={summon?.uncap.flb || false}
uncapLevel={gridSummon?.uncap_level} uncapLevel={gridSummon?.uncap_level}
updateUncap={passUncapData} updateUncap={passUncapData}
special={false}
/> : '' } /> : '' }
<h3 className="SummonName">{summon?.name.en}</h3> <h3 className="SummonName">{summon?.name.en}</h3>
</div> </div>

View file

@ -8,26 +8,35 @@ interface Props {
rarity?: number rarity?: number
uncapLevel: number uncapLevel: number
flb: boolean flb: boolean
ulb?: boolean ulb: boolean
special: boolean
updateUncap: (uncap: number) => void updateUncap: (uncap: number) => void
} }
const UncapIndicator = (props: Props) => { const UncapIndicator = (props: Props) => {
const [uncap, setUncap] = useState(props.uncapLevel) const [uncap, setUncap] = useState(props.uncapLevel)
useEffect(() => {
props.updateUncap(uncap)
}, [uncap])
const numStars = setNumStars() const numStars = setNumStars()
function setNumStars() { function setNumStars() {
let numStars let numStars
if (props.type === 'character') { if (props.type === 'character') {
if (props.flb) { if (props.special) {
numStars = 5 if (props.ulb) {
numStars = 5
} else if (props.flb) {
numStars = 4
} else {
numStars = 3
}
} else { } else {
numStars = 4 if (props.ulb) {
numStars = 6
} else if (props.flb) {
numStars = 5
} else {
numStars = 4
}
} }
} else { } else {
if (props.ulb) { if (props.ulb) {
@ -43,31 +52,38 @@ const UncapIndicator = (props: Props) => {
} }
function toggleStar(index: number, empty: boolean) { function toggleStar(index: number, empty: boolean) {
if (empty) setUncap(index + 1) if (empty) props.updateUncap(index + 1)
else setUncap(index) else props.updateUncap(index)
} }
const transcendence = (i: number) => { const transcendence = (i: number) => {
return <UncapStar ulb={true} empty={i >= uncap} key={`star_${i}`} index={i} onClick={toggleStar} /> return <UncapStar ulb={true} empty={i >= props.uncapLevel} key={`star_${i}`} index={i} onClick={toggleStar} />
}
const ulb = (i: number) => {
return <UncapStar ulb={true} empty={i >= props.uncapLevel} key={`star_${i}`} index={i} onClick={toggleStar} />
} }
const flb = (i: number) => { const flb = (i: number) => {
return <UncapStar flb={true} empty={i >= uncap} key={`star_${i}`} index={i} onClick={toggleStar} /> return <UncapStar flb={true} empty={i >= props.uncapLevel} key={`star_${i}`} index={i} onClick={toggleStar} />
} }
const mlb = (i: number) => { const mlb = (i: number) => {
return <UncapStar empty={i >= uncap} key={`star_${i}`} index={i} onClick={toggleStar} /> // console.log("MLB; Number of stars:", props.uncapLevel)
return <UncapStar empty={i >= props.uncapLevel} key={`star_${i}`} index={i} onClick={toggleStar} />
} }
return ( return (
<ul className="UncapIndicator"> <ul className="UncapIndicator">
{ {
Array.from(Array(numStars)).map((x, i) => { Array.from(Array(numStars)).map((x, i) => {
if (props.type === 'character' && i > 4) { if (props.type === 'character' && i > 4) {
return transcendence(i) if (props.special)
return ulb(i)
else
return transcendence(i)
} else if ( } else if (
props.special && props.type === 'character' && i == 3 ||
props.type === 'character' && i == 4 || props.type === 'character' && i == 4 ||
props.type !== 'character' && i > 2) { props.type !== 'character' && i > 2) {
return flb(i) return flb(i)

View file

@ -1,6 +1,9 @@
import React, { useCallback, useState } from 'react' /* eslint-disable react-hooks/exhaustive-deps */
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useCookies } from 'react-cookie'
import { useModal as useModal } from '~utils/useModal' import { useModal as useModal } from '~utils/useModal'
import { AxiosResponse } from 'axios'
import debounce from 'lodash.debounce' import debounce from 'lodash.debounce'
import SearchModal from '~components/SearchModal' import SearchModal from '~components/SearchModal'
@ -20,65 +23,199 @@ export enum GridType {
// Props // Props
interface Props { interface Props {
userId?: string
partyId?: string partyId?: string
mainhand?: GridWeapon | undefined mainhand: GridWeapon | undefined
grid: GridArray<GridWeapon> weapons: GridArray<GridWeapon>
extra: boolean extra: boolean
editable: boolean editable: boolean
exists: boolean createParty: () => Promise<AxiosResponse<any, any>>
found?: boolean pushHistory?: (path: string) => void
onSelect: (type: GridType, weapon: Weapon, position: number) => void
} }
const WeaponGrid = (props: Props) => { const WeaponGrid = (props: Props) => {
const { open, openModal, closeModal } = useModal() // Constants
const [searchPosition, setSearchPosition] = useState(0)
const numWeapons: number = 9 const numWeapons: number = 9
const searchGrid: GridArray<Weapon> = Object.values(props.grid).map((o) => o.weapon)
function receiveWeapon(weapon: Weapon, position: number) { // Cookies
props.onSelect(GridType.Weapon, weapon, position) const [cookies, _] = useCookies(['user'])
} const headers = (cookies.user != null) ? {
headers: {
function sendData(object: Character | Weapon | Summon, position: number) { 'Authorization': `Bearer ${cookies.user.access_token}`
if (isWeapon(object)) {
receiveWeapon(object, position)
} }
} } : {}
function isWeapon(object: Character | Weapon | Summon): object is Weapon { // Set up states for Grid data
return (object as Weapon).proficiency !== undefined const [weapons, setWeapons] = useState<GridArray<GridWeapon>>({})
} const [mainWeapon, setMainWeapon] = useState<GridWeapon>()
// Set up states for Search
const { open, openModal, closeModal } = useModal()
const [itemPositionForSearch, setItemPositionForSearch] = useState(0)
// Create a temporary state to store previous weapon uncap values
const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({})
// Create a state dictionary to store pure objects for Search
const [searchGrid, setSearchGrid] = useState<GridArray<Weapon>>({})
// Set states from props
useEffect(() => {
setWeapons(props.weapons || {})
setMainWeapon(props.mainhand)
}, [props])
// Update search grid whenever weapons or the mainhand are updated
useEffect(() => {
let newSearchGrid = Object.values(weapons).map((o) => o.weapon)
if (mainWeapon)
newSearchGrid.unshift(mainWeapon.weapon)
setSearchGrid(newSearchGrid)
}, [weapons, mainWeapon])
// Methods: Adding an object from search
function openSearchModal(position: number) { function openSearchModal(position: number) {
setSearchPosition(position) setItemPositionForSearch(position)
openModal() openModal()
} }
async function updateUncap(id: string, level: number) { function receiveWeaponFromSearch(object: Character | Weapon | Summon, position: number) {
await api.updateUncap('weapon', id, level) const weapon = object as Weapon
.catch(error => {
console.error(error) if (!props.partyId) {
}) props.createParty()
.then(response => {
const party = response.data.party
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
saveWeapon(party.id, weapon, position)
.then(response => storeGridWeapon(response.data.grid_weapon))
})
} else {
saveWeapon(props.partyId, weapon, position)
.then(response => storeGridWeapon(response.data.grid_weapon))
}
} }
const initiateUncapUpdate = (id: string, uncapLevel: number) => { async function saveWeapon(partyId: string, weapon: Weapon, position: number) {
debouncedAction(id, uncapLevel) let uncapLevel = 3
if (weapon.uncap.ulb) uncapLevel = 5
else if (weapon.uncap.flb) uncapLevel = 4
return await api.endpoints.weapons.create({
'weapon': {
'party_id': partyId,
'weapon_id': weapon.id,
'position': position,
'mainhand': (position == -1),
'uncap_level': uncapLevel
}
}, headers)
} }
const debouncedAction = useCallback( function storeGridWeapon(gridWeapon: GridWeapon) {
() => debounce((id, number) => { if (gridWeapon.position == -1) {
updateUncap(id, number) setMainWeapon(gridWeapon)
}, 1000), [] } else {
)() // Store the grid unit at the correct position
let newWeapons = Object.assign({}, props.weapons)
newWeapons[gridWeapon.position] = gridWeapon
setWeapons(newWeapons)
}
}
const extraGrid = ( // Methods: Updating uncap level
<ExtraWeapons // Note: Saves, but debouncing is not working properly
grid={props.grid} async function saveUncap(id: string, position: number, uncapLevel: number) {
// TODO: Don't make an API call if the new uncapLevel is the same as the current uncapLevel
try {
await api.updateUncap('weapon', id, uncapLevel)
.then(response => {
storeGridWeapon(response.data.grid_weapon)
})
} catch (error) {
console.error(error)
// Revert optimistic UI
updateUncapLevel(position, previousUncapValues[position])
// Remove optimistic key
let newPreviousValues = {...previousUncapValues}
delete newPreviousValues[position]
setPreviousUncapValues(newPreviousValues)
}
}
const memoizeAction = useCallback(
(id: string, position: number, uncapLevel: number) => {
debouncedAction(id, position, uncapLevel)
}, []
)
function initiateUncapUpdate(id: string, position: number, uncapLevel: number) {
memoizeAction(id, position, uncapLevel)
// Save the current value in case of an unexpected result
let newPreviousValues = {...previousUncapValues}
newPreviousValues[position] = (mainWeapon && position == -1) ? mainWeapon.uncap_level : weapons[position].uncap_level
setPreviousUncapValues(newPreviousValues)
// Optimistically update UI
updateUncapLevel(position, uncapLevel)
}
const debouncedAction = useMemo(() =>
debounce((id, position, number) => {
saveUncap(id, position, number)
}, 1000), [saveUncap]
)
const updateUncapLevel = (position: number, uncapLevel: number) => {
if (mainWeapon && position == -1) {
mainWeapon.uncap_level = uncapLevel
setMainWeapon(mainWeapon)
} else {
let newWeapons = Object.assign({}, weapons)
newWeapons[position].uncap_level = uncapLevel
setWeapons(newWeapons)
}
}
// Render: JSX components
const mainhandElement = (
<WeaponUnit
gridWeapon={mainWeapon}
editable={props.editable}
key="grid_mainhand"
position={-1}
unitType={0}
onClick={() => { openSearchModal(-1) }}
updateUncap={initiateUncapUpdate}
/>
)
const weaponGridElement = (
Array.from(Array(numWeapons)).map((x, i) => {
return (
<li key={`grid_unit_${i}`} >
<WeaponUnit
gridWeapon={weapons[i]}
editable={props.editable}
position={i}
unitType={1}
onClick={() => { openSearchModal(i) }}
updateUncap={initiateUncapUpdate}
/>
</li>
)
})
)
const extraGridElement = (
<ExtraWeapons
grid={weapons}
editable={props.editable} editable={props.editable}
exists={false}
offset={numWeapons} offset={numWeapons}
onClick={openSearchModal} onClick={openSearchModal}
updateUncap={initiateUncapUpdate} updateUncap={initiateUncapUpdate}
@ -88,48 +225,18 @@ const WeaponGrid = (props: Props) => {
return ( return (
<div id="weapon_grids"> <div id="weapon_grids">
<div id="WeaponGrid"> <div id="WeaponGrid">
<WeaponUnit { mainhandElement }
editable={props.editable} <ul id="grid_weapons">{ weaponGridElement }</ul>
key="grid_mainhand"
position={-1}
unitType={0}
gridWeapon={props.mainhand}
onClick={() => { openSearchModal(-1) }}
updateUncap={initiateUncapUpdate}
/>
<ul id="grid_weapons">
{
Array.from(Array(numWeapons)).map((x, i) => {
return (
<li key={`grid_unit_${i}`} >
<WeaponUnit
editable={props.editable}
position={i}
unitType={1}
gridWeapon={props.grid[i]}
onClick={() => { openSearchModal(i) }}
updateUncap={initiateUncapUpdate}
/>
</li>
)
})
}
</ul>
</div> </div>
{ (() => { { (() => { return (props.extra) ? extraGridElement : '' })() }
if(props.extra) {
return extraGrid
}
})() }
{open ? ( {open ? (
<SearchModal <SearchModal
grid={searchGrid} grid={searchGrid}
close={closeModal} close={closeModal}
send={sendData} send={receiveWeaponFromSearch}
fromPosition={searchPosition} fromPosition={itemPositionForSearch}
object="weapons" object="weapons"
placeholderText="Search for a weapon..." placeholderText="Search for a weapon..."
/> />

View file

@ -7,12 +7,12 @@ import PlusIcon from '~public/icons/plus.svg'
import './index.scss' import './index.scss'
interface Props { interface Props {
onClick: () => void
updateUncap: (id: string, uncap: number) => void
gridWeapon: GridWeapon | undefined gridWeapon: GridWeapon | undefined
unitType: 0 | 1
position: number position: number
editable: boolean editable: boolean
unitType: 0 | 1 onClick: () => void
updateUncap: (id: string, position: number, uncap: number) => void
} }
const WeaponUnit = (props: Props) => { const WeaponUnit = (props: Props) => {
@ -48,8 +48,9 @@ const WeaponUnit = (props: Props) => {
} }
function passUncapData(uncap: number) { function passUncapData(uncap: number) {
console.log("Passing uncap data to updateUncap callback...")
if (props.gridWeapon) if (props.gridWeapon)
props.updateUncap(props.gridWeapon.id, uncap) props.updateUncap(props.gridWeapon.id, props.position, uncap)
} }
return ( return (
@ -67,6 +68,7 @@ const WeaponUnit = (props: Props) => {
flb={gridWeapon.weapon.uncap.flb || false} flb={gridWeapon.weapon.uncap.flb || false}
uncapLevel={gridWeapon.uncap_level} uncapLevel={gridWeapon.uncap_level}
updateUncap={passUncapData} updateUncap={passUncapData}
special={false}
/> : '' /> : ''
} }
</div> </div>

18
package-lock.json generated
View file

@ -12,6 +12,7 @@
"@radix-ui/react-label": "^0.1.4", "@radix-ui/react-label": "^0.1.4",
"@radix-ui/react-switch": "^0.1.4", "@radix-ui/react-switch": "^0.1.4",
"@svgr/webpack": "^6.2.0", "@svgr/webpack": "^6.2.0",
"@types/axios": "^0.14.0",
"axios": "^0.25.0", "axios": "^0.25.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
@ -2982,6 +2983,15 @@
"node": ">=10.13.0" "node": ">=10.13.0"
} }
}, },
"node_modules/@types/axios": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz",
"integrity": "sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=",
"deprecated": "This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed!",
"dependencies": {
"axios": "*"
}
},
"node_modules/@types/cookie": { "node_modules/@types/cookie": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz",
@ -8754,6 +8764,14 @@
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==" "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="
}, },
"@types/axios": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz",
"integrity": "sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=",
"requires": {
"axios": "*"
}
},
"@types/cookie": { "@types/cookie": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz",

View file

@ -17,6 +17,7 @@
"@radix-ui/react-label": "^0.1.4", "@radix-ui/react-label": "^0.1.4",
"@radix-ui/react-switch": "^0.1.4", "@radix-ui/react-switch": "^0.1.4",
"@svgr/webpack": "^6.2.0", "@svgr/webpack": "^6.2.0",
"@types/axios": "^0.14.0",
"axios": "^0.25.0", "axios": "^0.25.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",

View file

@ -22,7 +22,7 @@ const PartyRoute: React.FC = () => {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [editable, setEditable] = useState(false) const [editable, setEditable] = useState(false)
const [characters, setCharacters] = useState<GridArray<Character>>({}) const [characters, setCharacters] = useState<GridArray<GridCharacter>>({})
const [weapons, setWeapons] = useState<GridArray<GridWeapon>>({}) const [weapons, setWeapons] = useState<GridArray<GridWeapon>>({})
const [summons, setSummons] = useState<GridArray<GridSummon>>({}) const [summons, setSummons] = useState<GridArray<GridSummon>>({})
@ -73,11 +73,11 @@ const PartyRoute: React.FC = () => {
} }
function populateCharacters(list: [GridCharacter]) { function populateCharacters(list: [GridCharacter]) {
let characters: GridArray<Character> = {} let characters: GridArray<GridCharacter> = {}
list.forEach((object: GridCharacter) => { list.forEach((object: GridCharacter) => {
if (object.position != null) if (object.position != null)
characters[object.position] = object.character characters[object.position] = object
}) })
return characters return characters

View file

@ -21,6 +21,7 @@ interface Character {
} }
uncap: { uncap: {
flb: boolean flb: boolean
ulb: boolean
} }
race: { race: {
race1: number race1: number
@ -31,4 +32,5 @@ interface Character {
proficiency2: number proficiency2: number
} }
position?: number position?: number
special: boolean
} }

View file

@ -1,5 +1,6 @@
interface GridCharacter { interface GridCharacter {
id: string id: string
position: number | null position: number
character: Character character: Character
uncap_level: number
} }

View file

@ -2,7 +2,7 @@ interface GridSummon {
id: string id: string
main: boolean main: boolean
friend: boolean friend: boolean
position: number | null position: number
summon: Summon summon: Summon
uncap_level: number uncap_level: number
} }