Merge pull request #50 from jedmund/grid-mechanics
Implement grid mechanics
This commit is contained in:
commit
98710539e6
14 changed files with 544 additions and 108 deletions
|
|
@ -3,7 +3,6 @@ import * as AlertDialog from '@radix-ui/react-alert-dialog'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
import Button from '~components/Button'
|
import Button from '~components/Button'
|
||||||
import { ButtonType } from '~utils/enums'
|
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
.Conflict.Dialog {
|
|
||||||
& > p {
|
|
||||||
line-height: 1.2;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow {
|
|
||||||
color: $grey-55;
|
|
||||||
font-size: 4rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.character {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: $unit;
|
|
||||||
text-align: center;
|
|
||||||
width: 12rem;
|
|
||||||
font-weight: $medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diagram {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
ul {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: $unit * 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: $unit;
|
|
||||||
|
|
||||||
.Button {
|
|
||||||
font-size: $font-regular;
|
|
||||||
padding: ($unit * 1.5) ($unit * 2);
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&.btn-disabled {
|
|
||||||
background: $grey-90;
|
|
||||||
color: $grey-70;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.btn-disabled) {
|
|
||||||
background: $grey-90;
|
|
||||||
color: $grey-50;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $grey-80;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { setCookie } from 'cookies-next'
|
import { useRouter } from 'next/router'
|
||||||
import Router, { useRouter } from 'next/router'
|
import { Trans, useTranslation } from 'next-i18next'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { AxiosResponse } from 'axios'
|
|
||||||
|
|
||||||
import * as Dialog from '@radix-ui/react-dialog'
|
import * as Dialog from '@radix-ui/react-dialog'
|
||||||
|
|
||||||
import api from '~utils/api'
|
|
||||||
import { appState } from '~utils/appState'
|
import { appState } from '~utils/appState'
|
||||||
import { accountState } from '~utils/accountState'
|
|
||||||
|
|
||||||
import Button from '~components/Button'
|
import Button from '~components/Button'
|
||||||
|
|
||||||
|
|
@ -24,7 +20,11 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const CharacterConflictModal = (props: Props) => {
|
const CharacterConflictModal = (props: Props) => {
|
||||||
|
// Localization
|
||||||
|
const router = useRouter()
|
||||||
const { t } = useTranslation('common')
|
const { t } = useTranslation('common')
|
||||||
|
const locale =
|
||||||
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||||
|
|
||||||
// States
|
// States
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
@ -60,11 +60,11 @@ const CharacterConflictModal = (props: Props) => {
|
||||||
|
|
||||||
function openChange(open: boolean) {
|
function openChange(open: boolean) {
|
||||||
setOpen(open)
|
setOpen(open)
|
||||||
|
props.resetConflict()
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
props.resetConflict()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -75,33 +75,37 @@ const CharacterConflictModal = (props: Props) => {
|
||||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
Only one version of a character can be included in each party. Do
|
<Trans i18nKey="modals.conflict.character"></Trans>
|
||||||
you want to change your party members?
|
|
||||||
</p>
|
</p>
|
||||||
<div className="diagram">
|
<div className="CharacterDiagram Diagram">
|
||||||
<ul>
|
<ul>
|
||||||
{props.conflictingCharacters?.map((character, i) => (
|
{props.conflictingCharacters?.map((character, i) => (
|
||||||
<li className="character" key={`conflict-${i}`}>
|
<li className="character" key={`conflict-${i}`}>
|
||||||
<img
|
<img
|
||||||
alt={character.object.name.en}
|
alt={character.object.name[locale]}
|
||||||
src={imageUrl(character.object, character.uncap_level)}
|
src={imageUrl(character.object, character.uncap_level)}
|
||||||
/>
|
/>
|
||||||
<span>{character.object.name.en}</span>
|
<span>{character.object.name[locale]}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<span className="arrow">→</span>
|
<span className="arrow">→</span>
|
||||||
<div className="character">
|
<div className="wrapper">
|
||||||
<img
|
<div className="character">
|
||||||
alt={props.incomingCharacter?.name.en}
|
<img
|
||||||
src={imageUrl(props.incomingCharacter)}
|
alt={props.incomingCharacter?.name[locale]}
|
||||||
/>
|
src={imageUrl(props.incomingCharacter)}
|
||||||
{props.incomingCharacter?.name.en}
|
/>
|
||||||
|
<span>{props.incomingCharacter?.name[locale]}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
<Button onClick={close} text="Nevermind" />
|
<Button onClick={close} text={t('buttons.cancel')} />
|
||||||
<Button onClick={props.resolveConflict} text="Confirm" />
|
<Button
|
||||||
|
onClick={props.resolveConflict}
|
||||||
|
text={t('modals.conflict.buttons.confirm')}
|
||||||
|
/>
|
||||||
</footer>
|
</footer>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
<Dialog.Overlay className="Overlay" />
|
<Dialog.Overlay className="Overlay" />
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,8 @@ const CharacterGrid = (props: Props) => {
|
||||||
async function resolveConflict() {
|
async function resolveConflict() {
|
||||||
if (incoming && conflicts.length > 0) {
|
if (incoming && conflicts.length > 0) {
|
||||||
await api
|
await api
|
||||||
.resolveCharacterConflict({
|
.resolveConflict({
|
||||||
|
object: 'characters',
|
||||||
incoming: incoming.id,
|
incoming: incoming.id,
|
||||||
conflicting: conflicts.map((c) => c.id),
|
conflicting: conflicts.map((c) => c.id),
|
||||||
position: position,
|
position: position,
|
||||||
|
|
|
||||||
|
|
@ -92,4 +92,101 @@
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.Conflict.Dialog {
|
||||||
|
$weapon-diameter: 14rem;
|
||||||
|
|
||||||
|
& > p {
|
||||||
|
line-height: 1.2;
|
||||||
|
max-width: 400px;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: $bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:lang(ja) {
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.weapon,
|
||||||
|
.character {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $unit;
|
||||||
|
text-align: center;
|
||||||
|
width: $weapon-diameter;
|
||||||
|
font-weight: $medium;
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: 1rem;
|
||||||
|
width: $weapon-diameter;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Diagram {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
&.CharacterDiagram {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $unit * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
align-items: center;
|
||||||
|
color: $grey-55;
|
||||||
|
display: flex;
|
||||||
|
font-size: 4rem;
|
||||||
|
text-align: center;
|
||||||
|
height: $weapon-diameter;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: $unit;
|
||||||
|
|
||||||
|
.Button {
|
||||||
|
font-size: $font-regular;
|
||||||
|
padding: ($unit * 1.5) ($unit * 2);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&.btn-disabled {
|
||||||
|
background: $grey-90;
|
||||||
|
color: $grey-70;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.btn-disabled) {
|
||||||
|
background: $grey-90;
|
||||||
|
color: $grey-50;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $grey-80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,9 +151,22 @@ const SearchModal = (props: Props) => {
|
||||||
openChange()
|
openChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extraPositions = () => {
|
||||||
|
if (props.object === 'weapons') return [9, 10, 11]
|
||||||
|
else if (props.object === 'summons') return [4, 5]
|
||||||
|
else return []
|
||||||
|
}
|
||||||
|
|
||||||
function receiveFilters(filters: { [key: string]: any }) {
|
function receiveFilters(filters: { [key: string]: any }) {
|
||||||
setCurrentPage(1)
|
setCurrentPage(1)
|
||||||
setResults([])
|
setResults([])
|
||||||
|
|
||||||
|
// Only show extra or subaura objects if invoked from those positions
|
||||||
|
if (extraPositions().includes(props.fromPosition)) {
|
||||||
|
if (props.object === 'weapons') filters.extra = true
|
||||||
|
else if (props.object === 'summons') filters.subaura = true
|
||||||
|
}
|
||||||
|
|
||||||
setFilters(filters)
|
setFilters(filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -175,7 +188,12 @@ const SearchModal = (props: Props) => {
|
||||||
: []
|
: []
|
||||||
|
|
||||||
if (open) {
|
if (open) {
|
||||||
if (firstLoad && cookieObj && cookieObj.length > 0) {
|
if (
|
||||||
|
firstLoad &&
|
||||||
|
cookieObj &&
|
||||||
|
cookieObj.length > 0 &&
|
||||||
|
!extraPositions().includes(props.fromPosition)
|
||||||
|
) {
|
||||||
setResults(cookieObj)
|
setResults(cookieObj)
|
||||||
setRecordCount(cookieObj.length)
|
setRecordCount(cookieObj.length)
|
||||||
setFirstLoad(false)
|
setFirstLoad(false)
|
||||||
|
|
|
||||||
0
components/WeaponConflictModal/index.scss
Normal file
0
components/WeaponConflictModal/index.scss
Normal file
108
components/WeaponConflictModal/index.tsx
Normal file
108
components/WeaponConflictModal/index.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import * as Dialog from '@radix-ui/react-dialog'
|
||||||
|
import Button from '~components/Button'
|
||||||
|
|
||||||
|
import mapWeaponSeries from '~utils/mapWeaponSeries'
|
||||||
|
|
||||||
|
import './index.scss'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean
|
||||||
|
incomingWeapon: Weapon
|
||||||
|
conflictingWeapons: GridWeapon[]
|
||||||
|
desiredPosition: number
|
||||||
|
resolveConflict: () => void
|
||||||
|
resetConflict: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const WeaponConflictModal = (props: Props) => {
|
||||||
|
// Localization
|
||||||
|
const router = useRouter()
|
||||||
|
const { t } = useTranslation('common')
|
||||||
|
const locale =
|
||||||
|
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||||
|
|
||||||
|
// States
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(props.open)
|
||||||
|
}, [setOpen, props.open])
|
||||||
|
|
||||||
|
function imageUrl(weapon?: Weapon) {
|
||||||
|
return `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-square/${weapon?.granblue_id}.jpg`
|
||||||
|
}
|
||||||
|
|
||||||
|
function openChange(open: boolean) {
|
||||||
|
setOpen(open)
|
||||||
|
props.resetConflict()
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const infoString = () => {
|
||||||
|
const series = props.incomingWeapon.series
|
||||||
|
const seriesSlug = t(`series.${mapWeaponSeries(series)}`)
|
||||||
|
|
||||||
|
return [2, 3].includes(series) ? (
|
||||||
|
<Trans i18nKey="modals.conflict.weapon.opus-draconic"></Trans>
|
||||||
|
) : (
|
||||||
|
<Trans i18nKey="modals.conflict.weapon.generic">
|
||||||
|
Only one weapon from the
|
||||||
|
<strong>{{ series: seriesSlug }} Series</strong> can be included in each
|
||||||
|
party. Do you want to change your weapons?
|
||||||
|
</Trans>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root open={open} onOpenChange={openChange}>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Content
|
||||||
|
className="Conflict Dialog"
|
||||||
|
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
|
<p>{infoString()}</p>
|
||||||
|
<div className="WeaponDiagram Diagram">
|
||||||
|
<ul>
|
||||||
|
{props.conflictingWeapons?.map((weapon, i) => (
|
||||||
|
<li className="weapon" key={`conflict-${i}`}>
|
||||||
|
<img
|
||||||
|
alt={weapon.object.name[locale]}
|
||||||
|
src={imageUrl(weapon.object)}
|
||||||
|
/>
|
||||||
|
<span>{weapon.object.name[locale]}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<span className="arrow">→</span>
|
||||||
|
<div className="wrapper">
|
||||||
|
<div className="weapon">
|
||||||
|
<img
|
||||||
|
alt={props.incomingWeapon?.name[locale]}
|
||||||
|
src={imageUrl(props.incomingWeapon)}
|
||||||
|
/>
|
||||||
|
{props.incomingWeapon?.name[locale]}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<Button onClick={close} text={t('buttons.cancel')} />
|
||||||
|
<Button
|
||||||
|
onClick={props.resolveConflict}
|
||||||
|
text={t('modals.conflict.buttons.confirm')}
|
||||||
|
/>
|
||||||
|
</footer>
|
||||||
|
</Dialog.Content>
|
||||||
|
<Dialog.Overlay className="Overlay" />
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WeaponConflictModal
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { getCookie } from 'cookies-next'
|
import { getCookie } from 'cookies-next'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
|
import { useTranslation } from 'next-i18next'
|
||||||
|
|
||||||
import { AxiosResponse } from 'axios'
|
import { AxiosResponse } from 'axios'
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
|
|
@ -15,6 +16,8 @@ import { appState } from '~utils/appState'
|
||||||
import type { SearchableObject } from '~types'
|
import type { SearchableObject } from '~types'
|
||||||
|
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
import WeaponConflictModal from '~components/WeaponConflictModal'
|
||||||
|
import Alert from '~components/Alert'
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -28,18 +31,25 @@ const WeaponGrid = (props: Props) => {
|
||||||
// Constants
|
// Constants
|
||||||
const numWeapons: number = 9
|
const numWeapons: number = 9
|
||||||
|
|
||||||
|
// Localization
|
||||||
|
const { t } = useTranslation('common')
|
||||||
|
|
||||||
// Cookies
|
// Cookies
|
||||||
const cookie = getCookie('account')
|
const cookie = getCookie('account')
|
||||||
const accountData: AccountCookie = cookie
|
const accountData: AccountCookie = cookie
|
||||||
? JSON.parse(cookie as string)
|
? JSON.parse(cookie as string)
|
||||||
: null
|
: null
|
||||||
const headers = accountData
|
|
||||||
? { headers: { Authorization: `Bearer ${accountData.token}` } }
|
|
||||||
: {}
|
|
||||||
|
|
||||||
// Set up state for view management
|
// Set up state for view management
|
||||||
const { party, grid } = useSnapshot(appState)
|
const { party, grid } = useSnapshot(appState)
|
||||||
const [slug, setSlug] = useState()
|
const [slug, setSlug] = useState()
|
||||||
|
const [modalOpen, setModalOpen] = useState(false)
|
||||||
|
const [showIncompatibleAlert, setShowIncompatibleAlert] = useState(false)
|
||||||
|
|
||||||
|
// Set up state for conflict management
|
||||||
|
const [incoming, setIncoming] = useState<Weapon>()
|
||||||
|
const [conflicts, setConflicts] = useState<GridWeapon[]>([])
|
||||||
|
const [position, setPosition] = useState(0)
|
||||||
|
|
||||||
// Create a temporary state to store previous weapon uncap values
|
// Create a temporary state to store previous weapon uncap values
|
||||||
const [previousUncapValues, setPreviousUncapValues] = useState<{
|
const [previousUncapValues, setPreviousUncapValues] = useState<{
|
||||||
|
|
@ -90,9 +100,45 @@ const WeaponGrid = (props: Props) => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
saveWeapon(party.id, weapon, position).then((response) =>
|
if (party.editable)
|
||||||
storeGridWeapon(response.data)
|
saveWeapon(party.id, weapon, position)
|
||||||
)
|
.then((response) => handleWeaponResponse(response.data))
|
||||||
|
.catch((error) => {
|
||||||
|
const code = error.response.status
|
||||||
|
const data = error.response.data
|
||||||
|
console.log(error.response)
|
||||||
|
|
||||||
|
console.log(data, code)
|
||||||
|
if (code === 422) {
|
||||||
|
if (data.code === 'incompatible_weapon_for_position') {
|
||||||
|
console.log('Here')
|
||||||
|
setShowIncompatibleAlert(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleWeaponResponse(data: any) {
|
||||||
|
if (data.hasOwnProperty('conflicts')) {
|
||||||
|
if (data.incoming) setIncoming(data.incoming)
|
||||||
|
if (data.conflicts) setConflicts(data.conflicts)
|
||||||
|
if (data.position) setPosition(data.position)
|
||||||
|
setModalOpen(true)
|
||||||
|
} else {
|
||||||
|
storeGridWeapon(data.grid_weapon)
|
||||||
|
|
||||||
|
// If we replaced an existing weapon, remove it from the grid
|
||||||
|
if (data.hasOwnProperty('meta') && data.meta['replaced'] !== undefined) {
|
||||||
|
const position = data.meta['replaced']
|
||||||
|
|
||||||
|
if (position == -1) {
|
||||||
|
appState.grid.weapons.mainWeapon = undefined
|
||||||
|
appState.party.element = 0
|
||||||
|
} else {
|
||||||
|
appState.grid.weapons.allWeapons[position] = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,18 +147,15 @@ const WeaponGrid = (props: Props) => {
|
||||||
if (weapon.uncap.ulb) uncapLevel = 5
|
if (weapon.uncap.ulb) uncapLevel = 5
|
||||||
else if (weapon.uncap.flb) uncapLevel = 4
|
else if (weapon.uncap.flb) uncapLevel = 4
|
||||||
|
|
||||||
return await api.endpoints.weapons.create(
|
return await api.endpoints.weapons.create({
|
||||||
{
|
weapon: {
|
||||||
weapon: {
|
party_id: partyId,
|
||||||
party_id: partyId,
|
weapon_id: weapon.id,
|
||||||
weapon_id: weapon.id,
|
position: position,
|
||||||
position: position,
|
mainhand: position == -1,
|
||||||
mainhand: position == -1,
|
uncap_level: uncapLevel,
|
||||||
uncap_level: uncapLevel,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
headers
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function storeGridWeapon(gridWeapon: GridWeapon) {
|
function storeGridWeapon(gridWeapon: GridWeapon) {
|
||||||
|
|
@ -125,6 +168,44 @@ const WeaponGrid = (props: Props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveConflict() {
|
||||||
|
if (incoming && conflicts.length > 0) {
|
||||||
|
await api
|
||||||
|
.resolveConflict({
|
||||||
|
object: 'weapons',
|
||||||
|
incoming: incoming.id,
|
||||||
|
conflicting: conflicts.map((c) => c.id),
|
||||||
|
position: position,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
// Store new character in state
|
||||||
|
storeGridWeapon(response.data)
|
||||||
|
|
||||||
|
// Remove conflicting characters from state
|
||||||
|
conflicts.forEach((c) => {
|
||||||
|
if (appState.grid.weapons.mainWeapon?.object.id === c.id) {
|
||||||
|
appState.grid.weapons.mainWeapon = undefined
|
||||||
|
appState.party.element = 0
|
||||||
|
} else {
|
||||||
|
appState.grid.weapons.allWeapons[c.position] = undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reset conflict
|
||||||
|
resetConflict()
|
||||||
|
|
||||||
|
// Close modal
|
||||||
|
setModalOpen(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetConflict() {
|
||||||
|
setPosition(-1)
|
||||||
|
setConflicts([])
|
||||||
|
setIncoming(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
// Methods: Updating uncap level
|
// Methods: Updating uncap level
|
||||||
// Note: Saves, but debouncing is not working properly
|
// Note: Saves, but debouncing is not working properly
|
||||||
async function saveUncap(id: string, position: number, uncapLevel: number) {
|
async function saveUncap(id: string, position: number, uncapLevel: number) {
|
||||||
|
|
@ -242,8 +323,39 @@ const WeaponGrid = (props: Props) => {
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const conflictModal = () => {
|
||||||
|
return incoming && conflicts ? (
|
||||||
|
<WeaponConflictModal
|
||||||
|
open={modalOpen}
|
||||||
|
incomingWeapon={incoming}
|
||||||
|
conflictingWeapons={conflicts}
|
||||||
|
desiredPosition={position}
|
||||||
|
resolveConflict={resolveConflict}
|
||||||
|
resetConflict={resetConflict}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const incompatibleAlert = () => {
|
||||||
|
console.log(t('alert.incompatible_weapon'))
|
||||||
|
return showIncompatibleAlert ? (
|
||||||
|
<Alert
|
||||||
|
open={showIncompatibleAlert}
|
||||||
|
cancelAction={() => setShowIncompatibleAlert(!showIncompatibleAlert)}
|
||||||
|
cancelActionText={t('buttons.confirm')}
|
||||||
|
message={t('alert.incompatible_weapon')}
|
||||||
|
></Alert>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="WeaponGrid">
|
<div id="WeaponGrid">
|
||||||
|
{conflicts ? conflictModal() : ''}
|
||||||
|
{incompatibleAlert()}
|
||||||
<div id="MainGrid">
|
<div id="MainGrid">
|
||||||
{mainhandElement}
|
{mainhandElement}
|
||||||
<ul className="grid_weapons">{weaponGridElement}</ul>
|
<ul className="grid_weapons">{weaponGridElement}</ul>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
{
|
{
|
||||||
|
"alert": {
|
||||||
|
"incompatible_weapon": "You've selected a weapon that can't be added to the Additional Weapon slots."
|
||||||
|
},
|
||||||
"ax": {
|
"ax": {
|
||||||
"no_skill": "No AX Skill",
|
"no_skill": "No AX Skill",
|
||||||
"errors": {
|
"errors": {
|
||||||
|
|
@ -20,6 +23,7 @@
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"copy": "Copy link",
|
"copy": "Copy link",
|
||||||
|
"confirm": "Got it",
|
||||||
"delete": "Delete team",
|
"delete": "Delete team",
|
||||||
"show_info": "Edit info",
|
"show_info": "Edit info",
|
||||||
"hide_info": "Hide info",
|
"hide_info": "Hide info",
|
||||||
|
|
@ -86,6 +90,7 @@
|
||||||
"olden_primal": "Olden Primal",
|
"olden_primal": "Olden Primal",
|
||||||
"beast": "Beast",
|
"beast": "Beast",
|
||||||
"omega": "Omega",
|
"omega": "Omega",
|
||||||
|
"regalia": "Regalia",
|
||||||
"militis": "Militis",
|
"militis": "Militis",
|
||||||
"xeno": "Xeno",
|
"xeno": "Xeno",
|
||||||
"astral": "Astral",
|
"astral": "Astral",
|
||||||
|
|
@ -101,7 +106,10 @@
|
||||||
"vintage": "Vintage",
|
"vintage": "Vintage",
|
||||||
"class_champion": "Class Champion",
|
"class_champion": "Class Champion",
|
||||||
"sephira": "Sephira",
|
"sephira": "Sephira",
|
||||||
"new_world": "New World Foundation"
|
"new_world": "New World Foundation",
|
||||||
|
"revenant": "Revenant",
|
||||||
|
"proving": "Proving Grounds",
|
||||||
|
"disaster": "Disaster"
|
||||||
},
|
},
|
||||||
"recency": {
|
"recency": {
|
||||||
"all_time": "All time",
|
"all_time": "All time",
|
||||||
|
|
@ -122,6 +130,16 @@
|
||||||
"about": {
|
"about": {
|
||||||
"title": "About"
|
"title": "About"
|
||||||
},
|
},
|
||||||
|
"conflict": {
|
||||||
|
"character": "Only one <strong>version of a character</strong> can be included in each party. Do you want to change your party members?",
|
||||||
|
"weapon": {
|
||||||
|
"generic": "Only one weapon from the <strong>{{series}} Series</strong> can be included in each party. Do you want to change your weapons?",
|
||||||
|
"opus-draconic": "Only one <strong>Dark Opus</strong> or <strong>Draconic Weapon</strong> can be included in each party. Do you want to change your weapons?"
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"confirm": "Confirm"
|
||||||
|
}
|
||||||
|
},
|
||||||
"delete_team": {
|
"delete_team": {
|
||||||
"title": "Delete team",
|
"title": "Delete team",
|
||||||
"description": "Are you sure you want to permanently delete this team?",
|
"description": "Are you sure you want to permanently delete this team?",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
{
|
{
|
||||||
|
"alert": {
|
||||||
|
"incompatible_weapon": "Additional Weaponsに装備できない武器を入れました。"
|
||||||
|
},
|
||||||
"ax": {
|
"ax": {
|
||||||
"no_skill": "EXスキルなし",
|
"no_skill": "EXスキルなし",
|
||||||
"errors": {
|
"errors": {
|
||||||
|
|
@ -18,8 +21,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"cancel": "キャンセルs",
|
"cancel": "キャンセル",
|
||||||
"copy": "リンクをコピー",
|
"copy": "リンクをコピー",
|
||||||
|
"confirm": "OK",
|
||||||
"delete": "編成を削除",
|
"delete": "編成を削除",
|
||||||
"show_info": "詳細を編集",
|
"show_info": "詳細を編集",
|
||||||
"save_info": "詳細を保存",
|
"save_info": "詳細を保存",
|
||||||
|
|
@ -86,6 +90,7 @@
|
||||||
"olden_primal": "オールド・プライマルシリーズ",
|
"olden_primal": "オールド・プライマルシリーズ",
|
||||||
"beast": "四象武器",
|
"beast": "四象武器",
|
||||||
"omega": "マグナシリーズ",
|
"omega": "マグナシリーズ",
|
||||||
|
"regalia": "レガリアシリーズ",
|
||||||
"militis": "ミーレスシリーズ",
|
"militis": "ミーレスシリーズ",
|
||||||
"xeno": "六道武器",
|
"xeno": "六道武器",
|
||||||
"astral": "アストラルウェポン",
|
"astral": "アストラルウェポン",
|
||||||
|
|
@ -101,7 +106,10 @@
|
||||||
"vintage": "ヴィンテージシリーズ",
|
"vintage": "ヴィンテージシリーズ",
|
||||||
"class_champion": "英雄武器",
|
"class_champion": "英雄武器",
|
||||||
"sephira": "セフィラン・オールドウェポン",
|
"sephira": "セフィラン・オールドウェポン",
|
||||||
"new_world": "新世界の礎"
|
"new_world": "新世界の礎",
|
||||||
|
"revenant": "天星器",
|
||||||
|
"proving": "ブレイブグラウンド",
|
||||||
|
"disaster": "災害シリーズ"
|
||||||
},
|
},
|
||||||
"recency": {
|
"recency": {
|
||||||
"all_time": "全ての期間",
|
"all_time": "全ての期間",
|
||||||
|
|
@ -122,6 +130,16 @@
|
||||||
"about": {
|
"about": {
|
||||||
"title": "このサイトについて"
|
"title": "このサイトについて"
|
||||||
},
|
},
|
||||||
|
"conflict": {
|
||||||
|
"character": "<strong>同じ名前のキャラクター</strong>がパーティに編成されています。<br />以下のキャラクターを入れ替えますか?",
|
||||||
|
"weapon": {
|
||||||
|
"generic": "<strong>{{series}}</strong>の武器に装備制限があり、武器が既に装備されています。<br /><br />以下の武器を入れ替えますか?",
|
||||||
|
"opus-draconic": "<strong>終末の神器シリーズ</strong>と<strong>ドラコニックシリーズ</strong>から1本しか装備できない制限があり、武器が既に装備されています。<br /><br />以下の武器を入れ替えますか?"
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"confirm": "入れ替える"
|
||||||
|
}
|
||||||
|
},
|
||||||
"delete_team": {
|
"delete_team": {
|
||||||
"title": "編成を削除しますか",
|
"title": "編成を削除しますか",
|
||||||
"description": "編成を削除する操作は取り消せません。",
|
"description": "編成を削除する操作は取り消せません。",
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,8 @@ class Api {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveCharacterConflict({ incoming, conflicting, position, params }: {
|
resolveConflict({ object, incoming, conflicting, position, params }: {
|
||||||
|
object: 'characters' | 'weapons'
|
||||||
incoming: string
|
incoming: string
|
||||||
conflicting: string[]
|
conflicting: string[]
|
||||||
position: number,
|
position: number,
|
||||||
|
|
@ -90,7 +91,7 @@ class Api {
|
||||||
position: position,
|
position: position,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const resourceUrl = `${this.url}/characters/resolve`
|
const resourceUrl = `${this.url}/${object}/resolve`
|
||||||
return axios.post(resourceUrl, body, { headers: params })
|
return axios.post(resourceUrl, body, { headers: params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
4
utils/mapWeaponSeries.tsx
Normal file
4
utils/mapWeaponSeries.tsx
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { weaponSeries } from '~utils/weaponSeries'
|
||||||
|
|
||||||
|
export default (id: number) =>
|
||||||
|
weaponSeries.find((series) => series.id === id)?.slug
|
||||||
119
utils/weaponSeries.tsx
Normal file
119
utils/weaponSeries.tsx
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
export interface WeaponSeries {
|
||||||
|
id: number
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const weaponSeries: WeaponSeries[] = [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
slug: 'seraphic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
slug: 'grand',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
slug: 'opus',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
slug: 'draconic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
slug: 'revenant',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
slug: 'primal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
slug: 'beast',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
slug: 'regalia',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
slug: 'omega',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
slug: 'olden_primal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
slug: 'militis',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
slug: 'hollowsky',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
slug: 'xeno',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 14,
|
||||||
|
slug: 'astral',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 15,
|
||||||
|
slug: 'rose',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 16,
|
||||||
|
slug: 'bahamut',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 17,
|
||||||
|
slug: 'ultima',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 18,
|
||||||
|
slug: 'epic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 19,
|
||||||
|
slug: 'ennead',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 20,
|
||||||
|
slug: 'cosmic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 21,
|
||||||
|
slug: 'ancestral',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 22,
|
||||||
|
slug: 'superlative',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 23,
|
||||||
|
slug: 'vintage',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 24,
|
||||||
|
slug: 'class_champion',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 25,
|
||||||
|
slug: 'proving',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 28,
|
||||||
|
slug: 'sephira',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 29,
|
||||||
|
slug: 'new_world',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 30,
|
||||||
|
slug: 'disaster',
|
||||||
|
},
|
||||||
|
]
|
||||||
Loading…
Reference in a new issue