Update awakening (#315)

* Add Awakening type and remove old defs

We remove the flat list of awakening data, as we will be pulling data from the database

* Update types to use new Awakening type

* Update WeaponUnit for Grand weapon awakenings

* Update object modals

We needed to update CharacterModal and WeaponModal to display awakenings from the new data format. However, the component used (`SelectWithInput`) was tied to AX Skills in a way that would take exponentially more time to resolve.

Instead, we forked `SelectWithInput` into `AwakeningSelectWithInput` and did our work there.

`AwakeningSelect` was found to be redundant, so it was removed.

* Update hovercards
This commit is contained in:
Justin Edmund 2023-06-19 00:46:03 -07:00 committed by GitHub
parent f86a199098
commit 0ebd1a6c66
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 316 additions and 306 deletions

View file

@ -16,7 +16,6 @@ import {
aetherialMastery,
permanentMastery,
} from '~data/overMastery'
import { characterAwakening } from '~data/awakening'
import { ExtendedMastery } from '~types'
import './index.scss'
@ -27,13 +26,6 @@ interface Props {
onTriggerClick: () => void
}
interface KeyNames {
[key: string]: {
en: string
jp: string
}
}
const CharacterHovercard = (props: Props) => {
const router = useRouter()
const { t } = useTranslation('common')
@ -181,27 +173,20 @@ const CharacterHovercard = (props: Props) => {
const awakeningSection = () => {
const gridAwakening = props.gridCharacter.awakening
const awakening = characterAwakening.find(
(awakening) => awakening.id === gridAwakening?.type
)
if (gridAwakening && awakening) {
if (gridAwakening) {
return (
<section className="Awakening">
<h5 className={tintElement}>
{t('modals.characters.subtitles.awakening')}
</h5>
<div>
{gridAwakening.type > 1 ? (
<img
alt={awakening.name[locale]}
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/awakening/character_${gridAwakening.type}.jpg`}
/>
) : (
''
)}
<img
alt={gridAwakening.type.name[locale]}
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/awakening/${gridAwakening.type.slug}.jpg`}
/>
<span>
<strong>{`${awakening.name[locale]}`}</strong>&nbsp;
<strong>{`${gridAwakening.type.name[locale]}`}</strong>&nbsp;
{`Lv${gridAwakening.level}`}
</span>
</div>

View file

@ -1,13 +1,7 @@
// Core dependencies
import React, {
PropsWithChildren,
useCallback,
useEffect,
useState,
} from 'react'
import React, { PropsWithChildren, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import { AxiosResponse } from 'axios'
import classNames from 'classnames'
// UI dependencies
@ -20,14 +14,10 @@ import {
import DialogContent from '~components/common/DialogContent'
import Button from '~components/common/Button'
import SelectWithInput from '~components/common/SelectWithInput'
import AwakeningSelect from '~components/mastery/AwakeningSelect'
import RingSelect from '~components/mastery/RingSelect'
import Switch from '~components/common/Switch'
// Utilities
import api from '~utils/api'
import { appState } from '~utils/appState'
import { retrieveCookies } from '~utils/retrieveCookies'
import elementalizeAetherialMastery from '~utils/elementalizeAetherialMastery'
// Data
@ -36,6 +26,8 @@ const emptyExtendedMastery: ExtendedMastery = {
strength: 0,
}
const MAX_AWAKENING_LEVEL = 9
// Styles and icons
import CrossIcon from '~public/icons/Cross.svg'
import './index.scss'
@ -46,6 +38,7 @@ import {
ExtendedMastery,
GridCharacterObject,
} from '~types'
import AwakeningSelectWithInput from '~components/mastery/AwakeningSelectWithInput'
interface Props {
gridCharacter: GridCharacter
@ -66,9 +59,6 @@ const CharacterModal = ({
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const { t } = useTranslation('common')
// Cookies
const cookies = retrieveCookies()
// UI state
const [open, setOpen] = useState(false)
const [formValid, setFormValid] = useState(false)
@ -103,8 +93,8 @@ const CharacterModal = ({
const [earring, setEarring] = useState<ExtendedMastery>(emptyExtendedMastery)
// Character properties: Awakening
const [awakeningType, setAwakeningType] = useState(0)
const [awakeningLevel, setAwakeningLevel] = useState(0)
const [awakening, setAwakening] = useState<Awakening>()
const [awakeningLevel, setAwakeningLevel] = useState(1)
// Character properties: Transcendence
const [transcendenceStep, setTranscendenceStep] = useState(0)
@ -118,7 +108,7 @@ const CharacterModal = ({
})
}
setAwakeningType(gridCharacter.awakening.type)
setAwakening(gridCharacter.awakening.type)
setAwakeningLevel(gridCharacter.awakening.level)
setPerpetuity(gridCharacter.perpetuity)
}, [gridCharacter])
@ -147,15 +137,16 @@ const CharacterModal = ({
modifier: earring.modifier,
strength: earring.strength,
},
awakening: {
type: awakeningType,
level: awakeningLevel,
},
transcendence_step: transcendenceStep,
perpetuity: perpetuity,
},
}
if (awakening) {
object.character.awakening_id = awakening.id
object.character.awakening_level = awakeningLevel
}
return object
}
@ -191,8 +182,8 @@ const CharacterModal = ({
if (onOpenChange) onOpenChange(false)
}
function receiveAwakeningValues(type: number, level: number) {
setAwakeningType(type)
function receiveAwakeningValues(id: string, level: number) {
setAwakening(gridCharacter.object.awakenings.find((a) => a.id === id))
setAwakeningLevel(level)
}
@ -234,10 +225,16 @@ const CharacterModal = ({
return (
<section>
<h3>{t('modals.characters.subtitles.awakening')}</h3>
<AwakeningSelect
object="character"
type={awakeningType}
level={awakeningLevel}
<AwakeningSelectWithInput
dataSet={gridCharacter.object.awakenings}
awakening={gridCharacter.awakening.type}
level={gridCharacter.awakening.level}
defaultAwakening={
gridCharacter.object.awakenings.find(
(a) => a.slug === 'character-balanced'
)!
}
maxLevel={MAX_AWAKENING_LEVEL}
sendValidity={receiveValidity}
sendValues={receiveAwakeningValues}
/>

View file

@ -1,97 +0,0 @@
import React, { useEffect, useState } from 'react'
import cloneDeep from 'lodash.clonedeep'
import SelectWithInput from '~components/common/SelectWithInput'
import { weaponAwakening, characterAwakening } from '~data/awakening'
import './index.scss'
interface Props {
object: 'character' | 'weapon'
type?: number
level?: number
onOpenChange?: (open: boolean) => void
sendValidity: (isValid: boolean) => void
sendValues: (type: number, level: number) => void
}
const AwakeningSelect = (props: Props) => {
// Data states
const [awakeningType, setAwakeningType] = useState(
props.object === 'weapon' ? 0 : 1
)
const [awakeningLevel, setAwakeningLevel] = useState(1)
// Data
const chooseDataset = () => {
let list: ItemSkill[] = []
switch (props.object) {
case 'character':
list = characterAwakening
break
case 'weapon':
// WARNING: Clonedeep is masking a deeper error
// which is running this method every time this component is rerendered
// causing multiple "No awakening" items to be added
const awakening = cloneDeep(weaponAwakening)
awakening.unshift({
id: 0,
name: {
en: 'No awakening',
ja: '覚醒なし',
},
granblue_id: '',
slug: 'no-awakening',
minValue: 0,
maxValue: 0,
fractional: false,
})
list = awakening
break
}
return list
}
// Set default awakening and level based on object type
useEffect(() => {
const defaultAwakening = props.object === 'weapon' ? 0 : 1
const type = props.type != undefined ? props.type : defaultAwakening
setAwakeningType(type)
setAwakeningLevel(props.level ? props.level : 1)
}, [props.object, props.type, props.level])
// Send validity of form when awakening level changes
useEffect(() => {
props.sendValidity(awakeningLevel > 0)
}, [props.sendValidity, awakeningLevel])
// Classes
function changeOpen(open: boolean) {
if (props.onOpenChange) props.onOpenChange(open)
}
function handleValueChange(type: number, level: number) {
setAwakeningType(type)
setAwakeningLevel(level)
props.sendValues(type, level)
}
return (
<div className="Awakening">
<SelectWithInput
object={`${props.object}_awakening`}
dataSet={chooseDataset()}
selectValue={awakeningType}
inputValue={awakeningLevel}
onOpenChange={changeOpen}
sendValidity={props.sendValidity}
sendValues={handleValueChange}
/>
</div>
)
}
export default AwakeningSelect

View file

@ -1,4 +1,22 @@
.AwakeningSelect .AwakeningSet {
.SelectWithItem {
.InputSet {
display: flex;
flex-direction: row;
gap: $unit;
width: 100%;
.SelectTrigger {
flex-grow: 1;
width: 100%;
}
.Input {
flex-grow: 0;
text-align: right;
width: 13rem;
}
}
.errors {
color: $error;
display: none;
@ -8,30 +26,4 @@
display: block;
}
}
.fields {
display: flex;
flex-direction: row;
gap: $unit;
width: 100%;
.SelectTrigger {
flex-grow: 1;
}
.Label {
display: none;
flex-grow: 0;
&.Visible {
display: block;
width: auto;
}
.Input {
min-width: $unit * 12;
width: inherit;
}
}
}
}

View file

@ -0,0 +1,215 @@
// Core dependencies
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next'
import classNames from 'classnames'
// UI Dependencies
import Input from '~components/common/Input'
import Select from '~components/common/Select'
import SelectItem from '~components/common/SelectItem'
// Styles and icons
import './index.scss'
// Types
interface Props {
dataSet: Awakening[]
defaultAwakening: Awakening
awakening?: Awakening
level?: number
maxLevel: number
selectDisabled: boolean
onOpenChange?: (open: boolean) => void
sendValidity: (isValid: boolean) => void
sendValues: (type: string, level: number) => void
}
const defaultProps = {
selectDisabled: false,
}
const AwakeningSelectWithInput = ({
dataSet,
defaultAwakening,
awakening,
level,
maxLevel,
selectDisabled,
onOpenChange,
sendValidity,
sendValues,
}: Props) => {
// Set up translations
const router = useRouter()
const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const { t } = useTranslation('common')
// State: Component
const [open, setOpen] = useState(false)
const [error, setError] = useState('')
// State: Data
const [currentAwakening, setCurrentAwakening] = useState<Awakening>()
const [currentLevel, setCurrentLevel] = useState(1)
// Refs
const inputRef = React.createRef<HTMLInputElement>()
// Classes
const inputClasses = classNames({
Bound: true,
Hidden: currentAwakening === undefined || currentAwakening.id === '0',
})
const errorClasses = classNames({
errors: true,
visible: error !== '',
})
// Hooks
useEffect(() => {
setCurrentAwakening(awakening)
setCurrentLevel(level ? level : 1)
if (awakening) sendValidity(true)
}, [])
// Methods: UI state management
function changeOpen() {
if (!selectDisabled) {
setOpen(!open)
if (onOpenChange) onOpenChange(!open)
}
}
function onClose() {
if (onOpenChange) onOpenChange(false)
}
// Methods: Rendering
function generateOptions() {
const sortedDataSet = [...dataSet].sort((a, b) => {
return a.order - b.order
})
let options: React.ReactNode[] = sortedDataSet.map((awakening, i) => {
return generateItem(awakening)
})
if (!dataSet.includes(defaultAwakening))
options.unshift(generateItem(defaultAwakening))
return options
}
function generateItem(awakening: Awakening) {
return (
<SelectItem key={awakening.slug} value={awakening.id}>
{awakening.name[locale]}
</SelectItem>
)
}
// Methods: User input detection
function handleSelectChange(id: string) {
const input = inputRef.current
if (input && !handleInputError(parseFloat(input.value))) return
setCurrentAwakening(dataSet.find((awakening) => awakening.id === id))
sendValues(id, currentLevel)
}
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
const input = inputRef.current
if (input && !handleInputError(parseFloat(input.value))) return
setCurrentLevel(parseInt(event.target.value))
sendValues(
currentAwakening ? currentAwakening.id : '0',
parseInt(event.target.value)
)
}
// Methods: Handle error
function handleInputError(value: number) {
let error = ''
if (currentAwakening) {
if (value < 1) {
error = t(`awakening.errors.value_too_low`, {
minValue: 1,
})
} else if (value > maxLevel) {
error = t(`awakening.errors.value_too_high`, {
maxValue: maxLevel,
})
} else if (value % 1 != 0) {
error = t(`awakening.errors.value_not_whole`)
} else if (!value || value <= 0) {
error = t(`awakening.errors.value_empty`)
} else {
error = ''
}
}
setError(error)
if (error.length > 0) {
sendValidity(false)
return false
} else return true
}
const rangeString = () => {
let placeholder = ''
if (awakening) {
const minValue = 1
const maxValue = maxLevel
placeholder = `${minValue}~${maxValue}`
}
return placeholder
}
return (
<div className="SelectWithItem">
<div className="InputSet">
<Select
key="awakening-type"
value={`${awakening ? awakening.id : defaultAwakening.id}`}
open={open}
disabled={selectDisabled}
onValueChange={handleSelectChange}
onOpenChange={changeOpen}
onClose={onClose}
triggerClass="modal"
overlayVisible={false}
>
{generateOptions()}
</Select>
<Input
value={level ? level : 1}
className={inputClasses}
type="number"
placeholder={rangeString()}
min={1}
max={maxLevel}
step="1"
onChange={handleInputChange}
visible={awakening ? 'true' : 'false'}
ref={inputRef}
/>
</div>
<p className={errorClasses}>{error}</p>
</div>
)
}
AwakeningSelectWithInput.defaultProps = defaultProps
export default AwakeningSelectWithInput

View file

@ -12,7 +12,6 @@ import WeaponLabelIcon from '~components/weapon/WeaponLabelIcon'
import UncapIndicator from '~components/uncap/UncapIndicator'
import ax from '~data/ax'
import { weaponAwakening } from '~data/awakening'
import './index.scss'
@ -146,11 +145,8 @@ const WeaponHovercard = (props: Props) => {
const awakeningSection = () => {
const gridAwakening = props.gridWeapon.awakening
const awakening = weaponAwakening.find(
(awakening) => awakening.id === gridAwakening?.type
)
if (gridAwakening && awakening) {
if (gridAwakening) {
return (
<section className="awakening">
<h5 className={tintElement}>
@ -158,11 +154,11 @@ const WeaponHovercard = (props: Props) => {
</h5>
<div>
<img
alt={awakening.name[locale]}
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/awakening/weapon_${gridAwakening.type}.png`}
alt={gridAwakening.type.name[locale]}
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/awakening/${gridAwakening.type.slug}.png`}
/>
<span>
<strong>{`${awakening.name[locale]}`}</strong>&nbsp;
<strong>{`${gridAwakening.type.name[locale]}`}</strong>&nbsp;
{`Lv${gridAwakening.level}`}
</span>
</div>

View file

@ -11,14 +11,15 @@ import {
DialogTrigger,
} from '~components/common/Dialog'
import DialogContent from '~components/common/DialogContent'
import AwakeningSelectWithInput from '~components/mastery/AwakeningSelectWithInput'
import AXSelect from '~components/mastery/AxSelect'
import AwakeningSelect from '~components/mastery/AwakeningSelect'
import ElementToggle from '~components/ElementToggle'
import WeaponKeySelect from '~components/weapon/WeaponKeySelect'
import Button from '~components/common/Button'
import api from '~utils/api'
import { appState } from '~utils/appState'
import { NO_AWAKENING } from '~data/awakening'
import CrossIcon from '~public/icons/Cross.svg'
import './index.scss'
@ -33,7 +34,7 @@ interface GridWeaponObject {
ax_modifier2?: number
ax_strength1?: number
ax_strength2?: number
awakening_type?: number
awakening_id?: string
awakening_level?: Number
}
}
@ -70,7 +71,7 @@ const WeaponModal = ({
const [element, setElement] = useState(-1)
const [awakeningType, setAwakeningType] = useState(0)
const [awakening, setAwakening] = useState<Awakening>()
const [awakeningLevel, setAwakeningLevel] = useState(1)
const [primaryAxModifier, setPrimaryAxModifier] = useState(-1)
@ -136,9 +137,10 @@ const WeaponModal = ({
setFormValid(isValid)
}
function receiveAwakeningValues(type: number, level: number) {
setAwakeningType(type)
function receiveAwakeningValues(id: string, level: number) {
setAwakening(gridWeapon.object.awakenings.find((a) => a.id === id))
setAwakeningLevel(level)
setFormValid(true)
}
function receiveElementValue(element: string) {
@ -167,8 +169,8 @@ const WeaponModal = ({
object.weapon.ax_strength2 = secondaryAxValue
}
if (gridWeapon.object.awakening) {
object.weapon.awakening_type = awakeningType
if (gridWeapon.object.awakenings) {
object.weapon.awakening_id = awakening?.id
object.weapon.awakening_level = awakeningLevel
}
@ -313,10 +315,12 @@ const WeaponModal = ({
return (
<section>
<h3>{t('modals.weapon.subtitles.awakening')}</h3>
<AwakeningSelect
object="weapon"
type={gridWeapon.awakening?.type}
<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}
@ -326,7 +330,7 @@ const WeaponModal = ({
}
function handleOpenChange(open: boolean) {
if (gridWeapon.object.ax || gridWeapon.object.awakening) {
if (gridWeapon.object.ax || gridWeapon.object.awakenings) {
setFormValid(false)
} else {
setFormValid(true)
@ -387,7 +391,7 @@ const WeaponModal = ({
{gridWeapon.object.element == 0 ? elementSelect() : ''}
{[2, 3, 17, 24].includes(gridWeapon.object.series) ? keySelect() : ''}
{gridWeapon.object.ax ? axSelect() : ''}
{gridWeapon.awakening ? awakeningSelect() : ''}
{gridWeapon.object.awakenings ? awakeningSelect() : ''}
</div>
<div className="DialogFooter" ref={footerRef}>
<Button

View file

@ -19,7 +19,6 @@ import Button from '~components/common/Button'
import type { SearchableObject } from '~types'
import ax from '~data/ax'
import { weaponAwakening } from '~data/awakening'
import PlusIcon from '~public/icons/Add.svg'
import SettingsIcon from '~public/icons/Settings.svg'
@ -88,7 +87,7 @@ const WeaponUnit = ({
return (
weapon.ax ||
weapon.awakening ||
weapon.awakenings ||
(weapon.series && [2, 3, 17, 22, 24].includes(weapon.series))
)
}
@ -190,21 +189,16 @@ const WeaponUnit = ({
function awakeningImage() {
if (
gridWeapon &&
gridWeapon.object.awakening &&
gridWeapon.object.awakenings &&
gridWeapon.awakening &&
gridWeapon.awakening.type > 0 &&
gridWeapon.awakening.type != null
gridWeapon.awakening.type
) {
const awakening = weaponAwakening.find(
(awakening) => awakening.id === gridWeapon?.awakening?.type
)
const name = awakening?.name[locale]
const awakening = gridWeapon.awakening
return (
<img
alt={`${name} Lv${gridWeapon.awakening.level}`}
alt={`${awakening.type.name[locale]} Lv${gridWeapon.awakening.level}`}
className="Awakening"
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/awakening/weapon_${gridWeapon.awakening.type}.png`}
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/awakening/${gridWeapon.awakening.type.slug}.png`}
/>
)
}

View file

@ -1,97 +1,10 @@
export type Awakening = {
id: number
name: {
[key: string]: string
en: string
ja: string
}
}
export const characterAwakening: ItemSkill[] = [
{
id: 1,
granblue_id: '',
name: {
en: 'Balanced',
ja: 'バランス',
},
slug: 'balanced',
minValue: 1,
maxValue: 9,
fractional: false,
},
{
id: 2,
granblue_id: '',
name: {
en: 'Attack',
ja: '攻撃',
},
slug: 'attack',
minValue: 1,
maxValue: 9,
fractional: false,
},
{
id: 3,
granblue_id: '',
name: {
en: 'Defense',
ja: '防御',
},
slug: 'defense',
minValue: 1,
maxValue: 9,
fractional: false,
},
{
id: 4,
granblue_id: '',
name: {
en: 'Multiattack',
ja: '連続攻撃',
},
slug: 'multiattack',
minValue: 1,
maxValue: 9,
fractional: false,
},
]
export const MAX_CHARACTER_AWAKENING_LEVEL = 9
export const weaponAwakening: ItemSkill[] = [
{
id: 1,
granblue_id: '',
name: {
en: 'Attack',
ja: '攻撃',
},
slug: 'attack',
minValue: 1,
maxValue: 15,
fractional: false,
export const NO_AWAKENING: Awakening = {
id: '0',
name: {
en: 'No awakening',
jp: '覚醒なし',
},
{
id: 2,
granblue_id: '',
name: {
en: 'Defense',
ja: '防御',
},
slug: 'defense',
minValue: 1,
maxValue: 15,
fractional: false,
},
{
id: 3,
granblue_id: '',
name: {
en: 'Special',
ja: '特殊',
},
slug: 'special',
minValue: 1,
maxValue: 15,
fractional: false,
},
]
slug: 'no-awakening',
}

11
types/Awakening.d.ts vendored Normal file
View file

@ -0,0 +1,11 @@
interface Awakening {
id: string
name: {
[key: string]: string
en: string
jp: string
}
slug: string
object_type?: string
order: number
}

View file

@ -35,6 +35,7 @@ interface Character {
proficiency1: number
proficiency2: number
}
awakenings: Awakening[]
position?: number
special: boolean
}

View file

@ -7,7 +7,7 @@ interface GridCharacter {
over_mastery: CharacterOverMastery
aetherial_mastery: ExtendedMastery
awakening: {
type: number
type: Awakening
level: number
}
perpetuity: boolean

View file

@ -8,7 +8,7 @@ interface GridWeapon {
weapon_keys?: Array<WeaponKey>
ax?: Array<SimpleAxSkill>
awakening?: {
type: number
type: Awakening
level: number
}
}

3
types/Weapon.d.ts vendored
View file

@ -7,10 +7,11 @@ interface Weapon {
proficiency: number
max_level: number
max_skill_level: number
max_awakening_level: number
series: number
ax: boolean
ax_type: number
awakening: boolean
awakenings: Awakening[]
name: {
[key: string]: string
en: string

6
types/index.d.ts vendored
View file

@ -63,10 +63,8 @@ interface GridCharacterObject {
ring3: ExtendedMastery
ring4: ExtendedMastery
earring: ExtendedMastery
awakening: {
type?: number
level?: number
}
awakening_id?: string
awakening_level?: number
transcendence_step: number
perpetuity: boolean
}