We had broken null weapons changing sprites when the element was changed, and the change detection was also broken. Some more stringent logic checks fixed both.
612 lines
16 KiB
TypeScript
612 lines
16 KiB
TypeScript
import React, { useEffect, useState } from 'react'
|
|
import { useRouter } from 'next/router'
|
|
import { Trans, useTranslation } from 'next-i18next'
|
|
import { AxiosResponse } from 'axios'
|
|
import classNames from 'classnames'
|
|
import clonedeep from 'lodash.clonedeep'
|
|
|
|
import api from '~utils/api'
|
|
import { appState } from '~utils/appState'
|
|
|
|
import Alert from '~components/common/Alert'
|
|
import SearchModal from '~components/search/SearchModal'
|
|
import WeaponModal from '~components/weapon/WeaponModal'
|
|
import {
|
|
ContextMenu,
|
|
ContextMenuTrigger,
|
|
ContextMenuContent,
|
|
} from '~components/common/ContextMenu'
|
|
import ContextMenuItem from '~components/common/ContextMenuItem'
|
|
import WeaponHovercard from '~components/weapon/WeaponHovercard'
|
|
import UncapIndicator from '~components/uncap/UncapIndicator'
|
|
import Button from '~components/common/Button'
|
|
|
|
import type { GridWeaponObject, SearchableObject } from '~types'
|
|
|
|
import ax from '~data/ax'
|
|
|
|
import PlusIcon from '~public/icons/Add.svg'
|
|
import SettingsIcon from '~public/icons/Settings.svg'
|
|
import styles from './index.module.scss'
|
|
|
|
interface Props {
|
|
gridWeapon: GridWeapon | undefined
|
|
unitType: 0 | 1
|
|
position: number
|
|
editable: boolean
|
|
removeWeapon: (id: string) => void
|
|
updateObject: (object: SearchableObject, position: number) => void
|
|
updateUncap: (id: string, position: number, uncap: number) => void
|
|
}
|
|
|
|
const WeaponUnit = ({
|
|
gridWeapon,
|
|
unitType,
|
|
position,
|
|
editable,
|
|
removeWeapon: sendWeaponToRemove,
|
|
updateObject,
|
|
updateUncap,
|
|
}: Props) => {
|
|
// Translations and locale
|
|
const { t } = useTranslation('common')
|
|
const router = useRouter()
|
|
const locale =
|
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
|
|
|
// State: UI
|
|
const [detailsModalOpen, setDetailsModalOpen] = useState(false)
|
|
const [searchModalOpen, setSearchModalOpen] = useState(false)
|
|
const [contextMenuOpen, setContextMenuOpen] = useState(false)
|
|
const [alertOpen, setAlertOpen] = useState(false)
|
|
|
|
// State: Other
|
|
const [imageUrl, setImageUrl] = useState('')
|
|
|
|
// Classes
|
|
const classes = classNames({
|
|
unit: true,
|
|
[styles.unit]: true,
|
|
[styles.extra]: position >= 9,
|
|
[styles.mainhand]: unitType == 0,
|
|
[styles.weapon]: unitType == 1,
|
|
[styles.editable]: editable,
|
|
[styles.filled]: gridWeapon !== undefined,
|
|
[styles.empty]: gridWeapon == undefined,
|
|
})
|
|
|
|
// Other
|
|
const weapon = gridWeapon?.object
|
|
|
|
// Hooks
|
|
useEffect(() => {
|
|
generateImageUrl()
|
|
})
|
|
|
|
// Methods: Convenience
|
|
function canBeModified(gridWeapon: GridWeapon) {
|
|
const weapon = gridWeapon.object
|
|
|
|
return (
|
|
weapon.ax ||
|
|
weapon.awakenings ||
|
|
(weapon.series && [2, 3, 17, 22, 24].includes(weapon.series))
|
|
)
|
|
}
|
|
|
|
// Methods: Open layer
|
|
function openWeaponModal(event: Event) {
|
|
setDetailsModalOpen(true)
|
|
}
|
|
|
|
function openSearchModal() {
|
|
if (editable) setSearchModalOpen(true)
|
|
}
|
|
|
|
function openRemoveWeaponAlert() {
|
|
setAlertOpen(true)
|
|
}
|
|
|
|
// Methods: Handle button clicked
|
|
function handleButtonClicked() {
|
|
setContextMenuOpen(!contextMenuOpen)
|
|
}
|
|
|
|
// Methods: Handle open change
|
|
function handleContextMenuOpenChange(open: boolean) {
|
|
if (!open) setContextMenuOpen(false)
|
|
}
|
|
|
|
function handleWeaponModalOpenChange(open: boolean) {
|
|
setDetailsModalOpen(open)
|
|
}
|
|
|
|
function handleSearchModalOpenChange(open: boolean) {
|
|
setSearchModalOpen(open)
|
|
}
|
|
|
|
// Methods: Mutate data
|
|
function passUncapData(index: number) {
|
|
if (gridWeapon) updateUncap(gridWeapon.id, position, index)
|
|
}
|
|
|
|
function removeWeapon() {
|
|
if (gridWeapon) sendWeaponToRemove(gridWeapon.id)
|
|
setAlertOpen(false)
|
|
}
|
|
|
|
// Methods: Data fetching and manipulation
|
|
|
|
async function updateWeapon(object: GridWeaponObject) {
|
|
if (gridWeapon) {
|
|
return await api.endpoints.grid_weapons
|
|
.update(gridWeapon.id, object)
|
|
.then((response) => processResult(response))
|
|
.catch((error) => processError(error))
|
|
}
|
|
}
|
|
|
|
function processResult(response: AxiosResponse) {
|
|
const gridWeapon: GridWeapon = response.data
|
|
|
|
if (gridWeapon.mainhand) {
|
|
appState.grid.weapons.mainWeapon = gridWeapon
|
|
appState.party.element = gridWeapon.object.element
|
|
} else if (!gridWeapon.mainhand && gridWeapon.position !== null) {
|
|
let weapon = clonedeep(gridWeapon)
|
|
if (weapon.object.element === 0 && weapon.element < 1)
|
|
weapon.element = gridWeapon.object.element
|
|
|
|
appState.grid.weapons.allWeapons[gridWeapon.position] = weapon
|
|
}
|
|
}
|
|
|
|
function processError(error: any) {
|
|
console.error(error)
|
|
}
|
|
|
|
// Methods: Data fetching and manipulation
|
|
function getCanonicalAxSkill(index: number) {
|
|
if (
|
|
gridWeapon &&
|
|
gridWeapon.object.ax &&
|
|
gridWeapon.object.ax_type > 0 &&
|
|
gridWeapon.ax
|
|
) {
|
|
const axOptions = ax[gridWeapon.object.ax_type - 1]
|
|
const weaponAxSkill: SimpleAxSkill = gridWeapon.ax[0]
|
|
|
|
let axSkill = axOptions.find((ax) => ax.id === weaponAxSkill.modifier)
|
|
|
|
if (index !== 0 && axSkill && axSkill.secondary) {
|
|
const weaponSubAxSkill: SimpleAxSkill = gridWeapon.ax[1]
|
|
axSkill = axSkill.secondary.find(
|
|
(ax) => ax.id === weaponSubAxSkill.modifier
|
|
)
|
|
}
|
|
|
|
return axSkill
|
|
} else return
|
|
}
|
|
|
|
// Methods: Image string generation
|
|
function generateImageUrl() {
|
|
let imgSrc = ''
|
|
if (gridWeapon) {
|
|
const weapon = gridWeapon.object!
|
|
|
|
if (unitType == 0) {
|
|
if (gridWeapon.object.element == 0 && gridWeapon.element)
|
|
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}_${gridWeapon.element}.jpg`
|
|
else
|
|
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}.jpg`
|
|
} else {
|
|
if (gridWeapon.object.element == 0 && gridWeapon.element)
|
|
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}_${gridWeapon.element}.jpg`
|
|
else
|
|
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`
|
|
}
|
|
}
|
|
|
|
setImageUrl(imgSrc)
|
|
}
|
|
|
|
function placeholderImageUrl() {
|
|
return unitType == 0
|
|
? '/images/placeholders/placeholder-weapon-main.png'
|
|
: '/images/placeholders/placeholder-weapon-grid.png'
|
|
}
|
|
|
|
// Methods: Image element rendering
|
|
function awakeningImage() {
|
|
if (
|
|
gridWeapon &&
|
|
gridWeapon.object.awakenings &&
|
|
gridWeapon.awakening &&
|
|
gridWeapon.awakening.type
|
|
) {
|
|
const awakening = gridWeapon.awakening
|
|
return (
|
|
<img
|
|
alt={`${awakening.type.name[locale]} Lv${gridWeapon.awakening.level}`}
|
|
className={styles.awakening}
|
|
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/awakening/${gridWeapon.awakening.type.slug}.png`}
|
|
/>
|
|
)
|
|
}
|
|
}
|
|
|
|
function telumaImage(index: number) {
|
|
const baseUrl = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-keys/`
|
|
let filename = ''
|
|
let altText = ''
|
|
|
|
// If there is a grid weapon, it is a Draconic Weapon and it has keys
|
|
if (
|
|
gridWeapon &&
|
|
gridWeapon.object.series === 3 &&
|
|
gridWeapon.weapon_keys
|
|
) {
|
|
if (index === 0 && gridWeapon.weapon_keys[0]) {
|
|
altText = `${gridWeapon.weapon_keys[0].name[locale]}`
|
|
filename = `${gridWeapon.weapon_keys[0].slug}.png`
|
|
} else if (index === 1 && gridWeapon.weapon_keys[1]) {
|
|
altText = `${gridWeapon.weapon_keys[1].name[locale]}`
|
|
|
|
const element = gridWeapon.object.element
|
|
filename = `${gridWeapon.weapon_keys[1].slug}-${element}.png`
|
|
}
|
|
|
|
return (
|
|
<img
|
|
alt={altText}
|
|
key={altText}
|
|
className={styles.skill}
|
|
src={`${baseUrl}${filename}`}
|
|
/>
|
|
)
|
|
}
|
|
}
|
|
|
|
function telumaImages() {
|
|
let images: JSX.Element[] = []
|
|
if (
|
|
gridWeapon &&
|
|
gridWeapon.object.series === 3 &&
|
|
gridWeapon.weapon_keys &&
|
|
gridWeapon.weapon_keys.length > 0
|
|
) {
|
|
for (let i = 0; i < gridWeapon.weapon_keys.length; i++) {
|
|
const image = telumaImage(i)
|
|
if (image) images.push(image)
|
|
}
|
|
}
|
|
|
|
return images
|
|
}
|
|
|
|
function ultimaImage(index: number) {
|
|
const baseUrl = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-keys/`
|
|
let filename = ''
|
|
let altText = ''
|
|
|
|
// If there is a grid weapon, it is a Dark Opus Weapon and it has keys
|
|
if (
|
|
gridWeapon &&
|
|
gridWeapon.object.series === 17 &&
|
|
gridWeapon.weapon_keys
|
|
) {
|
|
if (
|
|
gridWeapon.weapon_keys[index] &&
|
|
(gridWeapon.weapon_keys[index].slot === 1 ||
|
|
gridWeapon.weapon_keys[index].slot === 2)
|
|
) {
|
|
altText = `${gridWeapon.weapon_keys[index].name[locale]}`
|
|
filename = `${gridWeapon.weapon_keys[index].slug}.png`
|
|
} else if (
|
|
gridWeapon.weapon_keys[index] &&
|
|
gridWeapon.weapon_keys[index].slot === 0
|
|
) {
|
|
altText = `${gridWeapon.weapon_keys[index].name[locale]}`
|
|
|
|
const weapon = gridWeapon.object.proficiency
|
|
|
|
const suffix = `${weapon}`
|
|
filename = `${gridWeapon.weapon_keys[index].slug}-${suffix}.png`
|
|
}
|
|
}
|
|
|
|
return (
|
|
<img
|
|
alt={altText}
|
|
key={altText}
|
|
className={styles.skill}
|
|
src={`${baseUrl}${filename}`}
|
|
/>
|
|
)
|
|
}
|
|
|
|
function ultimaImages() {
|
|
let images: JSX.Element[] = []
|
|
if (
|
|
gridWeapon &&
|
|
gridWeapon.object.series === 17 &&
|
|
gridWeapon.weapon_keys &&
|
|
gridWeapon.weapon_keys.length > 0
|
|
) {
|
|
for (let i = 0; i < gridWeapon.weapon_keys.length; i++) {
|
|
const image = ultimaImage(i)
|
|
if (image) images.push(image)
|
|
}
|
|
}
|
|
|
|
return images
|
|
}
|
|
|
|
function opusImage(index: number) {
|
|
const baseUrl = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-keys/`
|
|
let filename = ''
|
|
let altText = ''
|
|
|
|
// If there is a grid weapon, it is a Dark Opus Weapon and it has keys
|
|
if (
|
|
gridWeapon &&
|
|
gridWeapon.object.series === 2 &&
|
|
gridWeapon.weapon_keys
|
|
) {
|
|
if (
|
|
gridWeapon.weapon_keys[index] &&
|
|
gridWeapon.weapon_keys[index].slot === 0
|
|
) {
|
|
altText = `${gridWeapon.weapon_keys[index].name[locale]}`
|
|
filename = `${gridWeapon.weapon_keys[index].slug}.png`
|
|
} else if (
|
|
gridWeapon.weapon_keys[index] &&
|
|
gridWeapon.weapon_keys[index].slot === 1
|
|
) {
|
|
altText = `${gridWeapon.weapon_keys[index].name[locale]}`
|
|
|
|
const element = gridWeapon.object.element
|
|
const mod = gridWeapon.object.name.en.includes('Repudiation')
|
|
? 'primal'
|
|
: 'magna'
|
|
|
|
const suffix = `${mod}-${element}`
|
|
const weaponKey = gridWeapon.weapon_keys[index]
|
|
|
|
if (
|
|
[
|
|
'pendulum-strength',
|
|
'pendulum-zeal',
|
|
'pendulum-strife',
|
|
'chain-temperament',
|
|
'chain-restoration',
|
|
'chain-glorification',
|
|
].includes(weaponKey.slug)
|
|
) {
|
|
filename = `${gridWeapon.weapon_keys[index].slug}-${suffix}.png`
|
|
} else {
|
|
filename = `${gridWeapon.weapon_keys[index].slug}.png`
|
|
}
|
|
}
|
|
|
|
return (
|
|
<img
|
|
alt={altText}
|
|
key={altText}
|
|
className={styles.skill}
|
|
src={`${baseUrl}${filename}`}
|
|
/>
|
|
)
|
|
}
|
|
}
|
|
|
|
function opusImages() {
|
|
let images: JSX.Element[] = []
|
|
if (
|
|
gridWeapon &&
|
|
gridWeapon.object.series === 2 &&
|
|
gridWeapon.weapon_keys &&
|
|
gridWeapon.weapon_keys.length > 0
|
|
) {
|
|
for (let i = 0; i < gridWeapon.weapon_keys.length; i++) {
|
|
const image = opusImage(i)
|
|
if (image) images.push(image)
|
|
}
|
|
}
|
|
|
|
return images
|
|
}
|
|
|
|
function axImage(index: number) {
|
|
const axSkill = getCanonicalAxSkill(index)
|
|
|
|
if (
|
|
gridWeapon &&
|
|
gridWeapon.object.ax &&
|
|
gridWeapon.object.ax_type > 0 &&
|
|
gridWeapon.ax &&
|
|
axSkill
|
|
) {
|
|
const altText = `${axSkill.name[locale]} Lv${gridWeapon.ax[index].strength}`
|
|
return (
|
|
<img
|
|
alt={altText}
|
|
key={altText}
|
|
className={styles.skill}
|
|
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/ax/${axSkill.slug}.png`}
|
|
/>
|
|
)
|
|
}
|
|
}
|
|
|
|
function axImages() {
|
|
let images: JSX.Element[] = []
|
|
if (
|
|
gridWeapon &&
|
|
gridWeapon.object.ax &&
|
|
gridWeapon.ax &&
|
|
gridWeapon.ax.length > 0
|
|
) {
|
|
for (let i = 0; i < gridWeapon.ax.length; i++) {
|
|
const image = axImage(i)
|
|
if (image) images.push(image)
|
|
}
|
|
}
|
|
|
|
return images
|
|
}
|
|
|
|
// Methods: Layer element rendering
|
|
const weaponModal = () => {
|
|
if (gridWeapon) {
|
|
return (
|
|
<WeaponModal
|
|
gridWeapon={gridWeapon}
|
|
open={detailsModalOpen}
|
|
onOpenChange={handleWeaponModalOpenChange}
|
|
updateWeapon={updateWeapon}
|
|
/>
|
|
)
|
|
}
|
|
}
|
|
|
|
const contextMenu = () => {
|
|
if (editable && gridWeapon && gridWeapon.id) {
|
|
return (
|
|
<>
|
|
<ContextMenu onOpenChange={handleContextMenuOpenChange}>
|
|
<ContextMenuTrigger asChild>
|
|
<Button
|
|
active={contextMenuOpen}
|
|
floating={true}
|
|
leftAccessoryIcon={<SettingsIcon />}
|
|
className="options"
|
|
onClick={handleButtonClicked}
|
|
/>
|
|
</ContextMenuTrigger>
|
|
<ContextMenuContent align="start">
|
|
{canBeModified(gridWeapon) ? (
|
|
<ContextMenuItem onSelect={openWeaponModal}>
|
|
{t('context.modify.weapon')}
|
|
</ContextMenuItem>
|
|
) : (
|
|
''
|
|
)}
|
|
<ContextMenuItem onSelect={openRemoveWeaponAlert}>
|
|
{t('context.remove')}
|
|
</ContextMenuItem>
|
|
</ContextMenuContent>
|
|
</ContextMenu>
|
|
{weaponModal()}
|
|
{removeAlert()}
|
|
</>
|
|
)
|
|
}
|
|
}
|
|
|
|
const removeAlert = () => {
|
|
return (
|
|
<Alert
|
|
open={alertOpen}
|
|
primaryAction={removeWeapon}
|
|
primaryActionText={t('modals.weapon.buttons.remove')}
|
|
cancelAction={() => setAlertOpen(false)}
|
|
cancelActionText={t('buttons.cancel')}
|
|
message={
|
|
<Trans i18nKey="modals.weapons.messages.remove">
|
|
Are you sure you want to remove{' '}
|
|
<strong>{{ weapon: gridWeapon?.object.name[locale] }}</strong> from
|
|
your team?
|
|
</Trans>
|
|
}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const searchModal = () => {
|
|
return (
|
|
<SearchModal
|
|
placeholderText={t('search.placeholders.weapon')}
|
|
fromPosition={position}
|
|
object="weapons"
|
|
open={searchModalOpen}
|
|
onOpenChange={handleSearchModalOpenChange}
|
|
send={updateObject}
|
|
/>
|
|
)
|
|
}
|
|
|
|
// Methods: Core element rendering
|
|
const image = () => {
|
|
const image = (
|
|
<img
|
|
alt={weapon?.name[locale]}
|
|
className={classNames({
|
|
// TODO: Look into this gridImage class
|
|
[styles.gridImage]: true,
|
|
[styles.placeholder]: imageUrl === '',
|
|
})}
|
|
src={imageUrl !== '' ? imageUrl : placeholderImageUrl()}
|
|
/>
|
|
)
|
|
|
|
const content = (
|
|
<div className={styles.image} onClick={openSearchModal}>
|
|
<div className={styles.modifiers}>
|
|
{awakeningImage()}
|
|
<div className={styles.skills}>
|
|
{axImages()}
|
|
{telumaImages()}
|
|
{opusImages()}
|
|
{ultimaImages()}
|
|
</div>
|
|
</div>
|
|
{image}
|
|
{editable ? (
|
|
<span className={styles.icon}>
|
|
<PlusIcon />
|
|
</span>
|
|
) : (
|
|
''
|
|
)}
|
|
</div>
|
|
)
|
|
|
|
return gridWeapon ? (
|
|
<WeaponHovercard gridWeapon={gridWeapon} onTriggerClick={openSearchModal}>
|
|
{content}
|
|
</WeaponHovercard>
|
|
) : (
|
|
content
|
|
)
|
|
}
|
|
|
|
const unitContent = (
|
|
<>
|
|
<div className={classes}>
|
|
{contextMenu()}
|
|
{image()}
|
|
{gridWeapon && weapon ? (
|
|
<UncapIndicator
|
|
type="weapon"
|
|
ulb={gridWeapon.object.uncap.ulb || false}
|
|
flb={gridWeapon.object.uncap.flb || false}
|
|
uncapLevel={gridWeapon.uncap_level}
|
|
position={gridWeapon.position}
|
|
updateUncap={passUncapData}
|
|
special={false}
|
|
/>
|
|
) : (
|
|
''
|
|
)}
|
|
<h3 className={styles.name}>{weapon?.name[locale]}</h3>
|
|
</div>
|
|
{searchModal()}
|
|
</>
|
|
)
|
|
|
|
return unitContent
|
|
}
|
|
|
|
export default WeaponUnit
|