hensei-web/components/weapon/WeaponModal/index.tsx
Justin Edmund a54f1b4c46 Fix weapon element logic
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.
2023-07-03 21:25:28 -07:00

510 lines
15 KiB
TypeScript

import React, { PropsWithChildren, useEffect, useState } from 'react'
import { getCookie } from 'cookies-next'
import { useRouter } from 'next/router'
import { Trans, useTranslation } from 'next-i18next'
import { isEqual } from 'lodash'
import { GridWeaponObject } from '~types'
import Alert from '~components/common/Alert'
import { Dialog, DialogTrigger } from '~components/common/Dialog'
import DialogHeader from '~components/common/DialogHeader'
import DialogFooter from '~components/common/DialogFooter'
import DialogContent from '~components/common/DialogContent'
import AwakeningSelectWithInput from '~components/mastery/AwakeningSelectWithInput'
import AXSelect from '~components/mastery/AxSelect'
import ElementToggle from '~components/ElementToggle'
import WeaponKeySelect from '~components/weapon/WeaponKeySelect'
import Button from '~components/common/Button'
import { NO_AWAKENING } from '~data/awakening'
import styles from './index.module.scss'
interface Props {
gridWeapon: GridWeapon
open: boolean
onOpenChange: (open: boolean) => void
updateWeapon: (object: GridWeaponObject) => Promise<any>
}
const WeaponModal = ({
gridWeapon,
open: modalOpen,
children,
onOpenChange,
updateWeapon,
}: PropsWithChildren<Props>) => {
const router = useRouter()
const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const { t } = useTranslation('common')
// Cookies
const cookie = getCookie('account')
const accountData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null
const headers = accountData
? { Authorization: `Bearer ${accountData.token}` }
: {}
// State: Component
const [alertOpen, setAlertOpen] = useState(false)
const [formValid, setFormValid] = useState(false)
// State: Selects
const [weaponKey1Open, setWeaponKey1Open] = useState(false)
const [weaponKey2Open, setWeaponKey2Open] = useState(false)
const [weaponKey3Open, setWeaponKey3Open] = useState(false)
const [weaponKey4Open, setWeaponKey4Open] = useState(false)
const [ax1Open, setAx1Open] = useState(false)
const [ax2Open, setAx2Open] = useState(false)
const [awakeningOpen, setAwakeningOpen] = useState(false)
// State: Data
const [element, setElement] = useState<number>(0)
const [awakening, setAwakening] = useState<Awakening>()
const [awakeningLevel, setAwakeningLevel] = useState(1)
const [primaryAxModifier, setPrimaryAxModifier] = useState(-1)
const [secondaryAxModifier, setSecondaryAxModifier] = useState(-1)
const [primaryAxValue, setPrimaryAxValue] = useState(0.0)
const [secondaryAxValue, setSecondaryAxValue] = useState(0.0)
const [weaponKey1, setWeaponKey1] = useState<WeaponKey | undefined>()
const [weaponKey2, setWeaponKey2] = useState<WeaponKey | undefined>()
const [weaponKey3, setWeaponKey3] = useState<WeaponKey | undefined>()
// Refs
const headerRef = React.createRef<HTMLDivElement>()
const footerRef = React.createRef<HTMLDivElement>()
// Hooks
// Set up modal data state when the gridWeapon changes
useEffect(() => {
setElement(gridWeapon.element)
if (gridWeapon.weapon_keys) {
gridWeapon.weapon_keys.forEach((key) => {
if (key.slot + 1 === 1) {
setWeaponKey1(key)
} else if (key.slot + 1 === 2) {
setWeaponKey2(key)
} else if (key.slot + 1 === 3) {
setWeaponKey3(key)
}
})
}
if (gridWeapon.awakening) {
setAwakening(gridWeapon.awakening.type)
setAwakeningLevel(gridWeapon.awakening.level)
}
if (gridWeapon.object.ax || gridWeapon.object.awakenings) {
setFormValid(false)
} else {
setFormValid(true)
}
}, [gridWeapon])
// Methods: Data retrieval
// Receive values from ElementToggle
function receiveElementValue(elementId: number) {
setElement(elementId)
}
// Receive values from AXSelect
function receiveAxValues(
primaryAxModifier: number,
primaryAxValue: number,
secondaryAxModifier: number,
secondaryAxValue: number
) {
setPrimaryAxModifier(primaryAxModifier)
setSecondaryAxModifier(secondaryAxModifier)
setPrimaryAxValue(primaryAxValue)
setSecondaryAxValue(secondaryAxValue)
}
// Receive values from AwakeningSelectWithInput
function receiveAwakeningValues(id: string, level: number) {
setAwakening(gridWeapon.object.awakenings.find((a) => a.id === id))
console.log(level)
setAwakeningLevel(level)
setFormValid(true)
}
// Receive values from WeaponKeySelect
function receiveWeaponKey(value: WeaponKey, slot: number) {
if (slot === 0) setWeaponKey1(value)
if (slot === 1) setWeaponKey2(value)
if (slot === 2) setWeaponKey3(value)
}
// Receive form validity from child components
function receiveValidity(isValid: boolean) {
setFormValid(isValid)
}
// Methods: Data submission
function prepareObject() {
let object: GridWeaponObject = { weapon: {} }
if (gridWeapon.object.element == 0) object.weapon.element = element
if ([2, 3, 17, 24].includes(gridWeapon.object.series) && weaponKey1) {
object.weapon.weapon_key1_id = weaponKey1.id
}
if ([2, 3, 17].includes(gridWeapon.object.series) && weaponKey2)
object.weapon.weapon_key2_id = weaponKey2.id
if (gridWeapon.object.series == 17 && weaponKey3)
object.weapon.weapon_key3_id = weaponKey3.id
if (gridWeapon.object.ax && gridWeapon.object.ax_type > 0) {
object.weapon.ax_modifier1 = primaryAxModifier
object.weapon.ax_modifier2 = secondaryAxModifier
object.weapon.ax_strength1 = primaryAxValue
object.weapon.ax_strength2 = secondaryAxValue
}
if (gridWeapon.object.awakenings) {
object.weapon.awakening_id = awakening?.id
object.weapon.awakening_level = awakeningLevel
}
return object
}
async function handleUpdateWeapon() {
await updateWeapon(prepareObject())
if (onOpenChange) onOpenChange(false)
}
// Methods: Event handlers
const anySelectOpen =
weaponKey1Open ||
weaponKey2Open ||
weaponKey3Open ||
weaponKey4Open ||
ax1Open ||
ax2Open ||
awakeningOpen
function openSelect(index: 1 | 2 | 3 | 4) {
setWeaponKey1Open(index === 1 ? !weaponKey1Open : false)
setWeaponKey2Open(index === 2 ? !weaponKey2Open : false)
setWeaponKey3Open(index === 3 ? !weaponKey3Open : false)
setWeaponKey4Open(index === 4 ? !weaponKey4Open : false)
}
function receiveAxOpen(index: 1 | 2, isOpen: boolean) {
if (index === 1) setAx1Open(isOpen)
if (index === 2) setAx2Open(isOpen)
}
function receiveAwakeningOpen(isOpen: boolean) {
setAwakeningOpen(isOpen)
}
function close() {
// Reset values
setElement(gridWeapon.element)
setWeaponKey1(
gridWeapon.weapon_keys && gridWeapon.weapon_keys[0]
? gridWeapon.weapon_keys[0]
: undefined
)
setWeaponKey2(
gridWeapon.weapon_keys && gridWeapon.weapon_keys[1]
? gridWeapon.weapon_keys[1]
: undefined
)
setWeaponKey3(
gridWeapon.weapon_keys && gridWeapon.weapon_keys[2]
? gridWeapon.weapon_keys[2]
: undefined
)
setAwakening(gridWeapon.awakening?.type)
setAwakeningLevel(gridWeapon.awakening?.level || 1)
setAlertOpen(false)
onOpenChange(false)
}
function handleOpenChange(open: boolean) {
if (modalOpen && hasBeenModified()) {
setAlertOpen(true)
} else {
onOpenChange(open)
}
}
function onEscapeKeyDown(event: KeyboardEvent) {
if (anySelectOpen) {
return event.preventDefault()
} else if (hasBeenModified()) {
setAlertOpen(true)
} else {
close()
}
}
// Methods: Modification checking
function hasBeenModified() {
return (
elementChanged() ||
weaponKeysChanged() ||
axChanged() ||
awakeningChanged()
)
}
function elementChanged() {
return element !== gridWeapon.element
}
function weaponKeyChanged(index: number) {
// Get the correct key ID from the given index and
// reset it to an empty string if it's 'no-key'
let weaponKey =
index === 0 ? weaponKey1 : index === 1 ? weaponKey2 : weaponKey3
if (weaponKey && weaponKey.id === 'no-key') weaponKey = undefined
// If the key is empty and the gridWeapon has no keys, nothing has changed
if (weaponKey === undefined && !gridWeapon.weapon_keys) return false
// If the key is not empty but the gridWeapon has no keys, the key has changed
if (
weaponKey !== undefined &&
gridWeapon.weapon_keys &&
gridWeapon.weapon_keys.length === 0
)
return true
// If gridWeapon has a key at the current index, but it doesn't match the key ID,
// then the key has changed
const weaponKeyChanged =
weaponKey &&
gridWeapon.weapon_keys &&
gridWeapon.weapon_keys[index] &&
weaponKey.id !== gridWeapon.weapon_keys[index].id
return weaponKeyChanged
}
function weaponKeysChanged() {
if (!gridWeapon.weapon_keys) return false
const weaponKey1Changed = weaponKeyChanged(0)
const weaponKey2Changed = weaponKeyChanged(1)
const weaponKey3Changed = weaponKeyChanged(2)
return weaponKey1Changed || weaponKey2Changed || weaponKey3Changed
}
function axChanged() {
if (!gridWeapon.ax) return false
const ax1Changed =
gridWeapon.ax[0].modifier !== primaryAxModifier ||
gridWeapon.ax[0].strength !== primaryAxValue
const ax2Changed =
gridWeapon.ax[1].modifier !== secondaryAxModifier ||
gridWeapon.ax[1].strength !== secondaryAxValue
return ax1Changed || ax2Changed
}
function awakeningChanged() {
if (!gridWeapon.awakening) return false
// Check if the awakening in local state is different from the one on the current GridCharacter
const awakeningChanged =
!isEqual(gridWeapon.awakening.type, awakening) ||
gridWeapon.awakening.level !== awakeningLevel
console.log(
gridWeapon.awakening.type,
awakening,
gridWeapon.awakening.level,
awakeningLevel
)
// Return true if the awakening has been modified and is not empty
return awakeningChanged
}
// Methods: Rendering
const elementSelect = (
<section>
<h3>{t('modals.weapon.subtitles.element')}</h3>
<ElementToggle
currentElement={gridWeapon.element}
sendValue={receiveElementValue}
/>
</section>
)
const keySelect = (
<section>
<h3>{t('modals.weapon.subtitles.weapon_keys')}</h3>
{[2, 3, 17, 22].includes(gridWeapon.object.series) ? (
<WeaponKeySelect
open={weaponKey1Open}
weaponKey={weaponKey1}
series={gridWeapon.object.series}
slot={0}
onOpenChange={() => openSelect(1)}
onChange={receiveWeaponKey}
onClose={() => setWeaponKey1Open(false)}
/>
) : (
''
)}
{[2, 3, 17].includes(gridWeapon.object.series) ? (
<WeaponKeySelect
open={weaponKey2Open}
weaponKey={weaponKey2}
series={gridWeapon.object.series}
slot={1}
onOpenChange={() => openSelect(2)}
onChange={receiveWeaponKey}
onClose={() => setWeaponKey2Open(false)}
/>
) : (
''
)}
{gridWeapon.object.series == 17 ? (
<WeaponKeySelect
open={weaponKey3Open}
weaponKey={weaponKey3}
series={gridWeapon.object.series}
slot={2}
onOpenChange={() => openSelect(3)}
onChange={receiveWeaponKey}
onClose={() => setWeaponKey3Open(false)}
/>
) : (
''
)}
{gridWeapon.object.series == 24 && gridWeapon.object.uncap.ulb ? (
<WeaponKeySelect
open={weaponKey4Open}
weaponKey={weaponKey1}
series={gridWeapon.object.series}
slot={0}
onOpenChange={() => openSelect(4)}
onChange={receiveWeaponKey}
onClose={() => setWeaponKey4Open(false)}
/>
) : (
''
)}
</section>
)
const axSelect = (
<section>
<h3>{t('modals.weapon.subtitles.ax_skills')}</h3>
<AXSelect
axType={gridWeapon.object.ax_type}
currentSkills={gridWeapon.ax}
onOpenChange={receiveAxOpen}
sendValidity={receiveValidity}
sendValues={receiveAxValues}
/>
</section>
)
const awakeningSelect = (
<section>
<h3>{t('modals.weapon.subtitles.awakening')}</h3>
<AwakeningSelectWithInput
dataSet={gridWeapon.object.awakenings}
awakening={gridWeapon.awakening?.type}
level={gridWeapon.awakening?.level}
defaultAwakening={NO_AWAKENING}
maxLevel={gridWeapon.object.max_awakening_level}
onOpenChange={receiveAwakeningOpen}
sendValidity={receiveValidity}
sendValues={receiveAwakeningValues}
/>
</section>
)
const confirmationAlert = (
<Alert
message={
<span>
<Trans i18nKey="alerts.unsaved_changes.object">
You will lose all changes to{' '}
<strong>{{ objectName: gridWeapon.object.name[locale] }}</strong> if
you continue.
<br />
<br />
Are you sure you want to continue without saving?
</Trans>
</span>
}
open={alertOpen}
primaryActionText="Close"
primaryAction={close}
cancelActionText="Nevermind"
cancelAction={() => setAlertOpen(false)}
/>
)
return (
<>
{confirmationAlert}
<Dialog open={modalOpen} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent
className="Weapon"
headerref={headerRef}
footerref={footerRef}
onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={onEscapeKeyDown}
>
<DialogHeader
ref={headerRef}
title={gridWeapon.object.name[locale]}
subtitle={t('modals.characters.title')}
image={{
src: `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-square/${gridWeapon.object.granblue_id}.jpg`,
alt: gridWeapon.object.name[locale],
}}
/>
<section className={styles.mods}>
{gridWeapon.object.element == 0 && elementSelect}
{[2, 3, 17, 24].includes(gridWeapon.object.series) && keySelect}
{gridWeapon.object.ax && axSelect}
{gridWeapon.object.awakenings && awakeningSelect}
</section>
<DialogFooter
ref={footerRef}
rightElements={[
<Button
bound={true}
onClick={handleUpdateWeapon}
key="confirm"
disabled={!formValid}
text={t('modals.weapon.buttons.confirm')}
/>,
]}
/>
</DialogContent>
</Dialog>
</>
)
}
export default WeaponModal