Merge pull request #20 from jedmund/weapon-mods

Add the ability to add modifications to grid weapons
This commit is contained in:
Justin Edmund 2022-03-03 16:58:15 -08:00 committed by GitHub
commit 25bf58da2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1903 additions and 42 deletions

View file

@ -0,0 +1,48 @@
.AXSelect {
display: flex;
flex-direction: column;
gap: $unit;
.AXSet {
&.hidden {
display: none;
}
.errors {
color: $error;
display: none;
padding: $unit 0;
&.visible {
display: block;
}
}
.fields {
display: flex;
flex-direction: row;
gap: $unit;
select {
flex-grow: 1;
margin: 0;
}
.Input {
-webkit-font-smoothing: antialiased;
border: none;
background-color: $grey-90;
border-radius: 6px;
box-sizing: border-box;
color: $grey-00;
height: $unit * 6;
display: block;
font-size: $font-regular;
padding: $unit;
text-align: right;
min-width: 100px;
width: 100px;
}
}
}
}

View file

@ -0,0 +1,243 @@
import React, { useEffect, useState } from 'react'
import classNames from 'classnames'
import { axData } from '~utils/axData'
import './index.scss'
interface ErrorMap {
[index: string]: string
axValue1: string
axValue2: string
}
interface Props {
axType: number
currentSkills?: SimpleAxSkill[],
sendValidity: (isValid: boolean) => void
sendValues: (primaryAxModifier: number, primaryAxValue: number, secondaryAxModifier: number, secondaryAxValue: number) => void
}
const AXSelect = (props: Props) => {
// Set up form states and error handling
const [errors, setErrors] = useState<ErrorMap>({
axValue1: '',
axValue2: ''
})
const primaryErrorClasses = classNames({
'errors': true,
'visible': errors.axValue1.length > 0
})
const secondaryErrorClasses = classNames({
'errors': true,
'visible': errors.axValue2.length > 0
})
// Refs
const primaryAxModifierSelect = React.createRef<HTMLSelectElement>()
const primaryAxValueInput = React.createRef<HTMLInputElement>()
const secondaryAxModifierSelect = React.createRef<HTMLSelectElement>()
const secondaryAxValueInput = React.createRef<HTMLInputElement>()
// States
const [primaryAxModifier, setPrimaryAxModifier] = useState(-1)
const [secondaryAxModifier, setSecondaryAxModifier] = useState(-1)
const [primaryAxValue, setPrimaryAxValue] = useState(0.0)
const [secondaryAxValue, setSecondaryAxValue] = useState(0.0)
useEffect(() => {
if (props.currentSkills && props.currentSkills[0]) {
if (props.currentSkills[0].modifier != null)
setPrimaryAxModifier(props.currentSkills[0].modifier)
setPrimaryAxValue(props.currentSkills[0].strength)
}
if (props.currentSkills && props.currentSkills[1]) {
if (props.currentSkills[1].modifier != null)
setSecondaryAxModifier(props.currentSkills[1].modifier)
setSecondaryAxValue(props.currentSkills[1].strength)
}
}, [props.currentSkills])
useEffect(() => {
props.sendValues(primaryAxModifier, primaryAxValue, secondaryAxModifier, secondaryAxValue)
}, [props, primaryAxModifier, primaryAxValue, secondaryAxModifier, secondaryAxValue])
useEffect(() => {
props.sendValidity(primaryAxValue > 0 && errors.axValue1 === '' && errors.axValue2 === '')
}, [props, primaryAxValue, errors])
// Classes
const secondarySetClasses = classNames({
'AXSet': true,
'hidden': primaryAxModifier < 0
})
function generateOptions(modifierSet: number) {
const axOptions = axData[props.axType - 1]
let axOptionElements: React.ReactNode[] = []
if (modifierSet == 0) {
axOptionElements = axOptions.map((ax, i) => {
return (
<option key={i} value={ax.id}>{ax.name.en}</option>
)
})
} else {
// If we are loading data from the server, state doesn't set before render,
// so our defaultValue is undefined.
let modifier = -1;
if (primaryAxModifier >= 0)
modifier = primaryAxModifier
else if (props.currentSkills)
modifier = props.currentSkills[0].modifier
if (modifier >= 0 && axOptions[modifier]) {
const primarySkill = axOptions[modifier]
if (primarySkill.secondary) {
const secondaryAxOptions = primarySkill.secondary
axOptionElements = secondaryAxOptions.map((ax, i) => {
return (
<option key={i} value={ax.id}>{ax.name.en}</option>
)
})
}
}
}
axOptionElements?.unshift(<option key={-1} value={-1}>No AX Skill</option>)
return axOptionElements
}
function handleSelectChange(event: React.ChangeEvent<HTMLSelectElement>) {
const value = parseInt(event.target.value)
if (primaryAxModifierSelect.current == event.target) {
setPrimaryAxModifier(value)
if (primaryAxValueInput.current && secondaryAxModifierSelect.current && secondaryAxValueInput.current) {
setupInput(axData[props.axType - 1][value], primaryAxValueInput.current)
secondaryAxModifierSelect.current.value = "-1"
secondaryAxValueInput.current.value = ""
}
} else {
setSecondaryAxModifier(value)
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
const currentAxSkill = (primaryAxSkill.secondary) ?
primaryAxSkill.secondary.find(skill => skill.id == value) : undefined
if (secondaryAxValueInput.current)
setupInput(currentAxSkill, secondaryAxValueInput.current)
}
}
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = parseFloat(event.target.value)
let newErrors = {...errors}
if (primaryAxValueInput.current == event.target) {
if (handlePrimaryErrors(value))
setPrimaryAxValue(value)
} else {
if (handleSecondaryErrors(value))
setSecondaryAxValue(value)
}
}
function handlePrimaryErrors(value: number) {
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
let newErrors = {...errors}
if (value < primaryAxSkill.minValue) {
newErrors.axValue1 = `${primaryAxSkill.name.en} must be at least ${primaryAxSkill.minValue}${ (primaryAxSkill.suffix) ? primaryAxSkill.suffix : ''}`
} else if (value > primaryAxSkill.maxValue) {
newErrors.axValue1 = `${primaryAxSkill.name.en} cannot be greater than ${primaryAxSkill.maxValue}${ (primaryAxSkill.suffix) ? primaryAxSkill.suffix : ''}`
} else if (!value || value <= 0) {
newErrors.axValue1 = `${primaryAxSkill.name.en} must have a value`
} else {
newErrors.axValue1 = ''
}
setErrors(newErrors)
return newErrors.axValue1.length === 0
}
function handleSecondaryErrors(value: number) {
const primaryAxSkill = axData[props.axType - 1][primaryAxModifier]
let newErrors = {...errors}
if (primaryAxSkill.secondary) {
const secondaryAxSkill = primaryAxSkill.secondary.find(skill => skill.id == secondaryAxModifier)
if (secondaryAxSkill) {
if (value < secondaryAxSkill.minValue) {
newErrors.axValue2 = `${secondaryAxSkill.name.en} must be at least ${secondaryAxSkill.minValue}${ (secondaryAxSkill.suffix) ? secondaryAxSkill.suffix : ''}`
} else if (value > secondaryAxSkill.maxValue) {
newErrors.axValue2 = `${secondaryAxSkill.name.en} cannot be greater than ${secondaryAxSkill.maxValue}${ (secondaryAxSkill.suffix) ? secondaryAxSkill.suffix : ''}`
} else if (!secondaryAxSkill.suffix && value % 1 !== 0) {
newErrors.axValue2 = `${secondaryAxSkill.name.en} must be a whole number`
} else if (primaryAxValue <= 0) {
newErrors.axValue1 = `${primaryAxSkill.name.en} must have a value`
} else {
newErrors.axValue2 = ''
}
}
}
setErrors(newErrors)
return newErrors.axValue2.length === 0
}
function setupInput(ax: AxSkill | undefined, element: HTMLInputElement) {
if (ax) {
const rangeString = `${ax.minValue}~${ax.maxValue}${ax.suffix || ''}`
element.disabled = false
element.placeholder = rangeString
element.min = `${ax.minValue}`
element.max = `${ax.maxValue}`
element.step = (ax.suffix) ? "0.5" : "1"
} else {
if (primaryAxValueInput.current && secondaryAxValueInput.current) {
if (primaryAxValueInput.current == element) {
primaryAxValueInput.current.disabled = true
primaryAxValueInput.current.placeholder = ''
}
secondaryAxValueInput.current.disabled = true
secondaryAxValueInput.current.placeholder = ''
}
}
}
return (
<div className="AXSelect">
<div className="AXSet">
<div className="fields">
<select key="ax1" defaultValue={ (props.currentSkills && props.currentSkills[0]) ? props.currentSkills[0].modifier : -1 } onChange={handleSelectChange} ref={primaryAxModifierSelect}>{ generateOptions(0) }</select>
<input defaultValue={ (props.currentSkills && props.currentSkills[0]) ? props.currentSkills[0].strength : 0 } className="Input" type="number" onChange={handleInputChange} ref={primaryAxValueInput} disabled />
</div>
<p className={primaryErrorClasses}>{errors.axValue1}</p>
</div>
<div className={secondarySetClasses}>
<div className="fields">
<select key="ax2" defaultValue={ (props.currentSkills && props.currentSkills[1]) ? props.currentSkills[1].modifier : -1 } onChange={handleSelectChange} ref={secondaryAxModifierSelect}>{ generateOptions(1) }</select>
<input defaultValue={ (props.currentSkills && props.currentSkills[1]) ? props.currentSkills[1].strength : 0 } className="Input" type="number" onChange={handleInputChange} ref={secondaryAxValueInput} disabled />
</div>
<p className={secondaryErrorClasses}>{errors.axValue2}</p>
</div>
</div>
)
}
export default AXSelect

View file

@ -86,6 +86,11 @@
fill: none;
stroke: $grey-50;
}
&.settings svg {
height: 13px;
width: 13px;
}
}
&.Active {

View file

@ -9,6 +9,7 @@ import EditIcon from '~public/icons/Edit.svg'
import LinkIcon from '~public/icons/Link.svg'
import MenuIcon from '~public/icons/Menu.svg'
import SaveIcon from '~public/icons/Save.svg'
import SettingsIcon from '~public/icons/Settings.svg'
import './index.scss'
@ -68,6 +69,10 @@ class Button extends React.Component<Props, State> {
icon = <span className='icon stroke'>
<SaveIcon />
</span>
} else if (this.props.icon === 'settings') {
icon = <span className='icon settings'>
<SettingsIcon />
</span>
}
const classes = classNames({

View file

@ -0,0 +1,59 @@
.ToggleGroup {
$height: 36px;
border: 1px solid rgba(0, 0, 0, 0.14);
border-radius: $height;
display: flex;
height: $height;
gap: $unit / 4;
padding: $unit / 2;
.ToggleItem {
background: white;
border: none;
border-radius: 18px;
color: $grey-40;
flex-grow: 1;
font-size: $font-regular;
padding: ($unit) $unit * 2;
&:hover {
cursor: pointer;
}
&:hover, &[data-state="on"] {
background:$grey-80;
color: $grey-00;
&.fire {
background: $fire-bg-light;
color: $fire-text-dark;
}
&.water {
background: $water-bg-light;
color: $water-text-dark;
}
&.earth {
background: $earth-bg-light;
color: $earth-text-dark;
}
&.wind {
background: $wind-bg-light;
color: $wind-text-dark;
}
&.dark {
background: $dark-bg-light;
color: $dark-text-dark;
}
&.light {
background: $light-bg-light;
color: $light-text-dark;
}
}
}
}

View file

@ -0,0 +1,39 @@
import React from 'react'
import * as ToggleGroup from '@radix-ui/react-toggle-group'
import './index.scss'
interface Props {
currentElement: number
sendValue: (value: string) => void
}
const ElementToggle = (props: Props) => {
return (
<ToggleGroup.Root className="ToggleGroup" type="single" defaultValue={`${props.currentElement}`} aria-label="Element" onValueChange={props.sendValue}>
<ToggleGroup.Item className="ToggleItem" value="0" aria-label="null">
Null
</ToggleGroup.Item>
<ToggleGroup.Item className="ToggleItem wind" value="1" aria-label="wind">
Wind
</ToggleGroup.Item>
<ToggleGroup.Item className="ToggleItem fire" value="2" aria-label="fire">
Fire
</ToggleGroup.Item>
<ToggleGroup.Item className="ToggleItem water" value="3" aria-label="water">
Water
</ToggleGroup.Item>
<ToggleGroup.Item className="ToggleItem earth" value="4" aria-label="earth">
Earth
</ToggleGroup.Item>
<ToggleGroup.Item className="ToggleItem dark" value="5" aria-label="dark">
Dark
</ToggleGroup.Item>
<ToggleGroup.Item className="ToggleItem light" value="6" aria-label="light">
Light
</ToggleGroup.Item>
</ToggleGroup.Root>
)
}
export default ElementToggle

View file

@ -28,7 +28,7 @@
background: url('/icons/Arrow.svg'), $grey-90;
background-repeat: no-repeat;
background-position-y: center;
background-position-x: 98%;
background-position-x: 95%;
background-size: $unit * 1.5;
color: $grey-50;
font-size: $font-small;

View file

@ -17,10 +17,16 @@ interface Props {
}
const PartySegmentedControl = (props: Props) => {
const { party } = useSnapshot(appState)
const { party, grid } = useSnapshot(appState)
function getElement() {
switch(party.element) {
let element: number = 0
if (party.element == 0 && grid.weapons.mainWeapon)
element = grid.weapons.mainWeapon.element
else
element = party.element
switch(element) {
case 1: return "wind"; break
case 2: return "fire"; break
case 3: return "water"; break

View file

@ -1,5 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react'
import { useCookies } from 'react-cookie'
import { appState } from '~utils/appState'
import api from '~utils/api'
@ -15,7 +14,6 @@ interface Props {
}
const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFieldSet(props, ref) {
const [cookies] = useCookies(['user'])
const [raids, setRaids] = useState<Raid[][]>()
const raidGroups = [
@ -50,12 +48,8 @@ const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFiel
}, [props.allOption])
useEffect(() => {
const headers = (cookies.user != null) ? {
headers: { 'Authorization': `Bearer ${cookies.user.access_token}` }
} : {}
function fetchRaids() {
api.endpoints.raids.getAll(headers)
api.endpoints.raids.getAll()
.then((response) => {
const raids = response.data.map((r: any) => r.raid)
@ -65,7 +59,7 @@ const RaidDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFiel
}
fetchRaids()
}, [cookies.user, organizeRaids])
}, [organizeRaids])
function raidGroup(index: number) {
const options = raids && raids.length > 0 && raids[index].length > 0 &&

View file

@ -232,7 +232,7 @@ const WeaponGrid = (props: Props) => {
// Render: JSX components
const mainhandElement = (
<WeaponUnit
gridWeapon={grid.weapons.mainWeapon}
gridWeapon={appState.grid.weapons.mainWeapon}
editable={party.editable}
key="grid_mainhand"
position={-1}
@ -247,7 +247,7 @@ const WeaponGrid = (props: Props) => {
return (
<li key={`grid_unit_${i}`} >
<WeaponUnit
gridWeapon={grid.weapons.allWeapons[i]}
gridWeapon={appState.grid.weapons.allWeapons[i]}
editable={party.editable}
position={i}
unitType={1}
@ -261,7 +261,7 @@ const WeaponGrid = (props: Props) => {
const extraGridElement = (
<ExtraWeapons
grid={grid.weapons.allWeapons}
grid={appState.grid.weapons.allWeapons}
editable={party.editable}
offset={numWeapons}
updateObject={receiveWeaponFromSearch}

View file

View file

@ -0,0 +1,140 @@
import React, { useCallback, useEffect, useState } from 'react'
import api from '~utils/api'
import './index.scss'
// Props
interface Props {
currentValue?: WeaponKey
series: number
slot: number
onChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
}
const WeaponKeyDropdown = React.forwardRef<HTMLSelectElement, Props>(function useFieldSet(props, ref) {
const [keys, setKeys] = useState<WeaponKey[][]>([])
const pendulumNames = [
{ en: 'Pendulum', jp: '' },
{ en: 'Chain', jp: '' }
]
const telumaNames = [ { en: 'Teluma', jp: '' } ]
const emblemNames = [ { en: 'Emblem', jp: '' } ]
const gauphNames = [
{ en: 'Gauph Key', jp: '' },
{ en: 'Ultima Key', jp: '' },
{ en: 'Gate of Omnipotence', jp: '' }
]
const keyName = () => {
return {
en: '',
jp: ''
}
}
const emptyKey: WeaponKey = {
id: '0',
name: {
en: `No ${keyName().en}`,
jp: `${keyName().jp}なし`
},
series: props.series,
slot: props.slot,
group: -1,
order: 0
}
const organizeWeaponKeys = useCallback((weaponKeys: WeaponKey[]) => {
const numGroups = Math.max.apply(Math, weaponKeys.map(key => key.group))
let groupedKeys = []
for (let i = 0; i <= numGroups; i++) {
groupedKeys[i] = weaponKeys.filter(key => key.group == i)
}
setKeys(groupedKeys)
}, [])
useEffect(() => {
const filterParams = {
params: {
series: props.series,
slot: props.slot
}
}
function fetchWeaponKeys() {
api.endpoints.weapon_keys.getAll(filterParams)
.then((response) => {
const keys = response.data.map((k: any) => k.weapon_key)
organizeWeaponKeys(keys)
})
}
fetchWeaponKeys()
}, [props.series, props.slot, organizeWeaponKeys])
function weaponKeyGroup(index: number) {
['α','β','γ','Δ'].sort((a, b) => a.localeCompare(b, 'el'))
const sortByOrder = (a: WeaponKey, b: WeaponKey) => a.order > b.order ? 1 : -1
const options = keys && keys.length > 0 && keys[index].length > 0 &&
keys[index].sort(sortByOrder).map((item, i) => {
return (
<option key={i} value={item.id}>{item.name.en}</option>
)
})
let name: { [key: string]: string } = {}
if (props.series == 2 && index == 0)
name = pendulumNames[0]
else if (props.series == 2 && props.slot == 1 && index == 1)
name = pendulumNames[1]
else if (props.series == 3)
name = telumaNames[index]
else if (props.series == 17)
name = gauphNames[props.slot]
else if (props.series == 22)
name = emblemNames[index]
return (
<optgroup key={index} label={ (props.series == 17 && props.slot == 2) ? name.en : `${name.en}s`}>
{options}
</optgroup>
)
}
const emptyOption = () => {
let name = ''
if (props.series == 2)
name = pendulumNames[0].en
else if (props.series == 3)
name = telumaNames[0].en
else if (props.series == 17)
name = gauphNames[props.slot].en
else if (props.series == 22)
name = emblemNames[0].en
return `No ${name}`
}
return (
<select
key={ (props.currentValue) ? props.currentValue.id : ''}
defaultValue={ (props.currentValue) ? props.currentValue.id : '' }
onBlur={props.onBlur}
onChange={props.onChange}
ref={ref}>
<option key="-1" value="-1">{emptyOption()}</option>
{ Array.from(Array(keys?.length)).map((x, i) => {
return weaponKeyGroup(i)
})}
</select>
)
})
export default WeaponKeyDropdown

View file

@ -0,0 +1,44 @@
.Weapon.Dialog {
.mods {
display: flex;
flex-direction: column;
gap: $unit * 4;
section {
display: flex;
flex-direction: column;
gap: $unit / 2;
h3 {
color: $grey-50;
font-size: $font-small;
margin-bottom: $unit;
}
select {
background-color: $grey-90;
}
}
.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-40;
&:hover {
background: $grey-80;
}
}
}
}
}

View file

@ -0,0 +1,216 @@
import React, { useState } from 'react'
import { useCookies } from 'react-cookie'
import { AxiosResponse } from 'axios'
import * as Dialog from '@radix-ui/react-dialog'
import AXSelect from '~components/AxSelect'
import ElementToggle from '~components/ElementToggle'
import WeaponKeyDropdown from '~components/WeaponKeyDropdown'
import Button from '~components/Button'
import api from '~utils/api'
import { appState } from '~utils/appState'
import CrossIcon from '~public/icons/Cross.svg'
import './index.scss'
interface GridWeaponObject {
weapon: {
element?: number
weapon_key1_id?: string
weapon_key2_id?: string
weapon_key3_id?: string
ax_modifier1?: number
ax_modifier2?: number
ax_strength1?: number
ax_strength2?: number
}
}
interface Props {
gridWeapon: GridWeapon
children: React.ReactNode
}
const WeaponModal = (props: Props) => {
// Cookies
const [cookies, _] = useCookies(['user'])
const headers = (cookies.user != null) ? {
headers: {
'Authorization': `Bearer ${cookies.user.access_token}`
}
} : {}
// Refs
const weaponKey1Select = React.createRef<HTMLSelectElement>()
const weaponKey2Select = React.createRef<HTMLSelectElement>()
const weaponKey3Select = React.createRef<HTMLSelectElement>()
// State
const [open, setOpen] = useState(false)
const [formValid, setFormValid] = useState(false)
const [element, setElement] = useState(-1)
const [primaryAxModifier, setPrimaryAxModifier] = useState(-1)
const [secondaryAxModifier, setSecondaryAxModifier] = useState(-1)
const [primaryAxValue, setPrimaryAxValue] = useState(0.0)
const [secondaryAxValue, setSecondaryAxValue] = useState(0.0)
function receiveAxValues(primaryAxModifier: number, primaryAxValue: number, secondaryAxModifier: number, secondaryAxValue: number) {
setPrimaryAxModifier(primaryAxModifier)
setSecondaryAxModifier(secondaryAxModifier)
setPrimaryAxValue(primaryAxValue)
setSecondaryAxValue(secondaryAxValue)
}
function receiveAxValidity(isValid: boolean) {
setFormValid(isValid)
}
function receiveElementValue(element: string) {
setElement(parseInt(element))
}
function prepareObject() {
let object: GridWeaponObject = { weapon: {} }
if (props.gridWeapon.object.element == 0)
object.weapon.element = element
if ([2, 3, 17, 24].includes(props.gridWeapon.object.series))
object.weapon.weapon_key1_id = weaponKey1Select.current?.value
if ([2, 3, 17].includes(props.gridWeapon.object.series))
object.weapon.weapon_key2_id = weaponKey2Select.current?.value
if (props.gridWeapon.object.series == 17)
object.weapon.weapon_key3_id = weaponKey3Select.current?.value
if (props.gridWeapon.object.ax > 0) {
object.weapon.ax_modifier1 = primaryAxModifier
object.weapon.ax_modifier2 = secondaryAxModifier
object.weapon.ax_strength1 = primaryAxValue
object.weapon.ax_strength2 = secondaryAxValue
}
return object
}
async function updateWeapon() {
const updateObject = prepareObject()
return await api.endpoints.grid_weapons.update(props.gridWeapon.id, updateObject, headers)
.then(response => processResult(response))
.catch(error => processError(error))
}
function processResult(response: AxiosResponse) {
const gridWeapon: GridWeapon = response.data.grid_weapon
if (gridWeapon.mainhand)
appState.grid.weapons.mainWeapon = gridWeapon
else
appState.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon
setOpen(false)
}
function processError(error: any) {
console.error(error)
}
const elementSelect = () => {
return (
<section>
<h3>Element</h3>
<ElementToggle
currentElement={props.gridWeapon.element}
sendValue={receiveElementValue}
/>
</section>
)
}
const keySelect = () => {
return (
<section>
<h3>Weapon Keys</h3>
{ ([2, 3, 17, 22].includes(props.gridWeapon.object.series)) ?
<WeaponKeyDropdown
currentValue={ (props.gridWeapon.weapon_keys) ? props.gridWeapon.weapon_keys[0] : undefined }
series={props.gridWeapon.object.series}
slot={0}
ref={weaponKey1Select} />
: ''}
{ ([2, 3, 17].includes(props.gridWeapon.object.series)) ?
<WeaponKeyDropdown
currentValue={ (props.gridWeapon.weapon_keys) ? props.gridWeapon.weapon_keys[1] : undefined }
series={props.gridWeapon.object.series}
slot={1}
ref={weaponKey2Select} />
: ''}
{ (props.gridWeapon.object.series == 17) ?
<WeaponKeyDropdown
currentValue={ (props.gridWeapon.weapon_keys) ? props.gridWeapon.weapon_keys[2] : undefined }
series={props.gridWeapon.object.series}
slot={2}
ref={weaponKey3Select} />
: ''}
</section>
)
}
const axSelect = () => {
return (
<section>
<AXSelect
axType={props.gridWeapon.object.ax}
currentSkills={props.gridWeapon.ax}
sendValidity={receiveAxValidity}
sendValues={receiveAxValues}
/>
</section>
)
}
function openChange(open: boolean) {
setFormValid(false)
setOpen(open)
}
return (
<Dialog.Root open={open} onOpenChange={openChange}>
<Dialog.Trigger asChild>
{ props.children }
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Content className="Weapon Dialog" onOpenAutoFocus={ (event) => event.preventDefault() }>
<div className="DialogHeader">
<div className="DialogTop">
<Dialog.Title className="SubTitle">Modify Weapon</Dialog.Title>
<Dialog.Title className="DialogTitle">{props.gridWeapon.object.name.en}</Dialog.Title>
</div>
<Dialog.Close className="DialogClose" asChild>
<span>
<CrossIcon />
</span>
</Dialog.Close>
</div>
<div className="mods">
{ (props.gridWeapon.object.element == 0) ? elementSelect() : '' }
{ ([2, 3, 17, 24].includes(props.gridWeapon.object.series)) ? keySelect() : '' }
{ (props.gridWeapon.object.ax > 0) ? axSelect() : '' }
<Button click={updateWeapon} disabled={!formValid}>Save Weapon</Button>
</div>
</Dialog.Content>
<Dialog.Overlay className="Overlay" />
</Dialog.Portal>
</Dialog.Root>
)
}
export default WeaponModal

View file

@ -3,11 +3,16 @@
flex-direction: column;
gap: 4px;
min-height: 139px;
position: relative;
@media (max-width: $medium-screen) {
min-height: auto;
}
&:hover .Button {
display: block;
}
&.editable .WeaponImage:hover {
border: $hover-stroke;
box-shadow: $hover-shadow;
@ -66,6 +71,16 @@
display: none;
}
.Button {
background: white;
box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.14);
display: none;
position: absolute;
left: $unit;
top: $unit;
z-index: 3;
}
h3 {
color: $grey-00;
font-size: $font-button;

View file

@ -2,9 +2,13 @@ import React, { useEffect, useState } from 'react'
import classnames from 'classnames'
import SearchModal from '~components/SearchModal'
import WeaponModal from '~components/WeaponModal'
import UncapIndicator from '~components/UncapIndicator'
import PlusIcon from '~public/icons/Add.svg'
import Button from '~components/Button'
import { ButtonType } from '~utils/enums'
import PlusIcon from '~public/icons/Add.svg'
import './index.scss'
interface Props {
@ -39,10 +43,17 @@ const WeaponUnit = (props: Props) => {
if (props.gridWeapon) {
const weapon = props.gridWeapon.object!
if (props.unitType == 0)
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}.jpg`
else
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`
if (props.unitType == 0) {
if (props.gridWeapon.object.element == 0 && props.gridWeapon.element)
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}_${props.gridWeapon.element}.jpg`
else
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}.jpg`
} else {
if (props.gridWeapon.object.element == 0 && props.gridWeapon.element)
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}_${props.gridWeapon.element}.jpg`
else
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`
}
}
setImageUrl(imgSrc)
@ -53,6 +64,13 @@ const WeaponUnit = (props: Props) => {
props.updateUncap(props.gridWeapon.id, props.position, uncap)
}
function canBeModified(gridWeapon: GridWeapon) {
const weapon = gridWeapon.object
return weapon.ax > 0 ||
(weapon.series) && [2, 3, 17, 22].includes(weapon.series)
}
const image = (
<div className="WeaponImage">
<img alt={weapon?.name.en} className="grid_image" src={imageUrl} />
@ -61,18 +79,26 @@ const WeaponUnit = (props: Props) => {
)
const editableImage = (
<SearchModal
placeholderText="Search for a weapon..."
fromPosition={props.position}
object="weapons"
send={props.updateObject}>
{image}
</SearchModal>
<div className="WeaponImage">
<SearchModal
placeholderText="Search for a weapon..."
fromPosition={props.position}
object="weapons"
send={props.updateObject}>
{image}
</SearchModal>
</div>
)
return (
<div>
<div className={classes}>
{ (props.editable && gridWeapon && canBeModified(gridWeapon)) ?
<WeaponModal gridWeapon={gridWeapon}>
<div>
<Button icon="settings" type={ButtonType.IconOnly}/>
</div>
</WeaponModal>: '' }
{ (props.editable) ? editableImage : image }
{ (gridWeapon) ?
<UncapIndicator

186
package-lock.json generated
View file

@ -12,6 +12,7 @@
"@radix-ui/react-hover-card": "^0.1.3",
"@radix-ui/react-label": "^0.1.4",
"@radix-ui/react-switch": "^0.1.4",
"@radix-ui/react-toggle-group": "^0.1.5",
"@svgr/webpack": "^6.2.0",
"@types/axios": "^0.14.0",
"axios": "^0.25.0",
@ -2630,6 +2631,107 @@
"react": "^16.8 || ^17.0"
}
},
"node_modules/@radix-ui/react-toggle": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-0.1.4.tgz",
"integrity": "sha512-gxUq6NgMc4ChV8VJnwdYqueeoblspwXHAexYo+jM9N2hFLbI1C587jLjdTHzIcUa9q68Xaw4jtiImWDOokEhRw==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "0.1.0",
"@radix-ui/react-primitive": "0.1.4",
"@radix-ui/react-use-controllable-state": "0.1.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0"
}
},
"node_modules/@radix-ui/react-toggle-group": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-0.1.5.tgz",
"integrity": "sha512-Yp14wFiqe00azF+sG5CCJz4JGOP/f5Jj+CxLlZCmMpG5qhVTWeaeG4YH6pvX4KL41fS8x9FAaLb8wW9y01o67g==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "0.1.0",
"@radix-ui/react-context": "0.1.1",
"@radix-ui/react-primitive": "0.1.4",
"@radix-ui/react-roving-focus": "0.1.5",
"@radix-ui/react-toggle": "0.1.4",
"@radix-ui/react-use-controllable-state": "0.1.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0"
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-collection": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-0.1.4.tgz",
"integrity": "sha512-3muGI15IdgaDFjOcO7xX8a35HQRBRF6LH9pS6UCeZeRmbslkVeHyJRQr2rzICBUoX7zgIA0kXyMDbpQnJGyJTA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "0.1.0",
"@radix-ui/react-context": "0.1.1",
"@radix-ui/react-primitive": "0.1.4",
"@radix-ui/react-slot": "0.1.2"
},
"peerDependencies": {
"react": "^16.8 || ^17.0"
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-id": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-0.1.5.tgz",
"integrity": "sha512-IPc4H/63bes0IZ1GJJozSEkSWcDyhNGtKFWUpJ+XtaLyQ1X3x7Mf6fWwWhDcpqlYEP+5WtAvfqcyEsyjP+ZhBQ==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-layout-effect": "0.1.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0"
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-primitive": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.4.tgz",
"integrity": "sha512-6gSl2IidySupIMJFjYnDIkIWRyQdbu/AHK7rbICPani+LW4b0XdxBXc46og/iZvuwW8pjCS8I2SadIerv84xYA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "0.1.2"
},
"peerDependencies": {
"react": "^16.8 || ^17.0"
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-roving-focus": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-0.1.5.tgz",
"integrity": "sha512-ClwKPS5JZE+PaHCoW7eu1onvE61pDv4kO8W4t5Ra3qMFQiTJLZMdpBQUhksN//DaVygoLirz4Samdr5Y1x1FSA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "0.1.0",
"@radix-ui/react-collection": "0.1.4",
"@radix-ui/react-compose-refs": "0.1.0",
"@radix-ui/react-context": "0.1.1",
"@radix-ui/react-id": "0.1.5",
"@radix-ui/react-primitive": "0.1.4",
"@radix-ui/react-use-callback-ref": "0.1.0",
"@radix-ui/react-use-controllable-state": "0.1.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0"
}
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.4.tgz",
"integrity": "sha512-6gSl2IidySupIMJFjYnDIkIWRyQdbu/AHK7rbICPani+LW4b0XdxBXc46og/iZvuwW8pjCS8I2SadIerv84xYA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "0.1.2"
},
"peerDependencies": {
"react": "^16.8 || ^17.0"
}
},
"node_modules/@radix-ui/react-use-body-pointer-events": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-body-pointer-events/-/react-use-body-pointer-events-0.1.0.tgz",
@ -8660,6 +8762,90 @@
"@radix-ui/react-use-size": "0.1.0"
}
},
"@radix-ui/react-toggle": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-0.1.4.tgz",
"integrity": "sha512-gxUq6NgMc4ChV8VJnwdYqueeoblspwXHAexYo+jM9N2hFLbI1C587jLjdTHzIcUa9q68Xaw4jtiImWDOokEhRw==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "0.1.0",
"@radix-ui/react-primitive": "0.1.4",
"@radix-ui/react-use-controllable-state": "0.1.0"
},
"dependencies": {
"@radix-ui/react-primitive": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.4.tgz",
"integrity": "sha512-6gSl2IidySupIMJFjYnDIkIWRyQdbu/AHK7rbICPani+LW4b0XdxBXc46og/iZvuwW8pjCS8I2SadIerv84xYA==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "0.1.2"
}
}
}
},
"@radix-ui/react-toggle-group": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-0.1.5.tgz",
"integrity": "sha512-Yp14wFiqe00azF+sG5CCJz4JGOP/f5Jj+CxLlZCmMpG5qhVTWeaeG4YH6pvX4KL41fS8x9FAaLb8wW9y01o67g==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "0.1.0",
"@radix-ui/react-context": "0.1.1",
"@radix-ui/react-primitive": "0.1.4",
"@radix-ui/react-roving-focus": "0.1.5",
"@radix-ui/react-toggle": "0.1.4",
"@radix-ui/react-use-controllable-state": "0.1.0"
},
"dependencies": {
"@radix-ui/react-collection": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-0.1.4.tgz",
"integrity": "sha512-3muGI15IdgaDFjOcO7xX8a35HQRBRF6LH9pS6UCeZeRmbslkVeHyJRQr2rzICBUoX7zgIA0kXyMDbpQnJGyJTA==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-compose-refs": "0.1.0",
"@radix-ui/react-context": "0.1.1",
"@radix-ui/react-primitive": "0.1.4",
"@radix-ui/react-slot": "0.1.2"
}
},
"@radix-ui/react-id": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-0.1.5.tgz",
"integrity": "sha512-IPc4H/63bes0IZ1GJJozSEkSWcDyhNGtKFWUpJ+XtaLyQ1X3x7Mf6fWwWhDcpqlYEP+5WtAvfqcyEsyjP+ZhBQ==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-use-layout-effect": "0.1.0"
}
},
"@radix-ui/react-primitive": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-0.1.4.tgz",
"integrity": "sha512-6gSl2IidySupIMJFjYnDIkIWRyQdbu/AHK7rbICPani+LW4b0XdxBXc46og/iZvuwW8pjCS8I2SadIerv84xYA==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-slot": "0.1.2"
}
},
"@radix-ui/react-roving-focus": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-0.1.5.tgz",
"integrity": "sha512-ClwKPS5JZE+PaHCoW7eu1onvE61pDv4kO8W4t5Ra3qMFQiTJLZMdpBQUhksN//DaVygoLirz4Samdr5Y1x1FSA==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "0.1.0",
"@radix-ui/react-collection": "0.1.4",
"@radix-ui/react-compose-refs": "0.1.0",
"@radix-ui/react-context": "0.1.1",
"@radix-ui/react-id": "0.1.5",
"@radix-ui/react-primitive": "0.1.4",
"@radix-ui/react-use-callback-ref": "0.1.0",
"@radix-ui/react-use-controllable-state": "0.1.0"
}
}
}
},
"@radix-ui/react-use-body-pointer-events": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-body-pointer-events/-/react-use-body-pointer-events-0.1.0.tgz",

View file

@ -17,6 +17,7 @@
"@radix-ui/react-hover-card": "^0.1.3",
"@radix-ui/react-label": "^0.1.4",
"@radix-ui/react-switch": "^0.1.4",
"@radix-ui/react-toggle-group": "^0.1.5",
"@svgr/webpack": "^6.2.0",
"@types/axios": "^0.14.0",
"axios": "^0.25.0",

View file

@ -1,13 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
type Data = {
name: string
}
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
) {
res.status(200).json({ name: 'John Doe' })
}

View file

@ -1,3 +1,3 @@
<svg viewBox="0 0 14 14" fill="#444" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.3121 4.60937C2.09636 4.43701 1.78173 4.47219 1.60937 4.68793C1.43701 4.90368 1.47219 5.2183 1.68793 5.39066L6.68107 9.37966C6.75234 9.4366 6.8344 9.47089 6.91856 9.48351C7.05774 9.51055 7.20746 9.47854 7.32678 9.38296L12.311 5.39025C12.5265 5.21761 12.5613 4.90294 12.3886 4.68742C12.216 4.4719 11.9013 4.43714 11.6858 4.60979L7.00557 8.35897L2.3121 4.60937Z" />
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.93618 4.62991C2.73179 4.44423 2.41557 4.4594 2.22989 4.6638C2.04421 4.8682 2.05938 5.18441 2.26378 5.37009L6.65743 9.36145C6.72962 9.42702 6.81576 9.46755 6.90516 9.48353C7.05808 9.51688 7.2243 9.47812 7.34882 9.3647L11.7346 5.36964C11.9388 5.18368 11.9535 4.86745 11.7676 4.6633C11.5816 4.45916 11.2654 4.44441 11.0612 4.63037L7.00447 8.32569L2.93618 4.62991Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 495 B

After

Width:  |  Height:  |  Size: 532 B

View file

@ -0,0 +1,3 @@
<svg width="14" height="15" viewBox="0 0 14 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.9363 5.12991C2.73191 4.94423 2.41569 4.9594 2.23001 5.1638C2.04433 5.3682 2.0595 5.68441 2.2639 5.87009L6.65755 9.86145C6.72974 9.92702 6.81588 9.96755 6.90529 9.98353C7.05821 10.0169 7.22443 9.97812 7.34894 9.8647L11.7348 5.86964C11.9389 5.68368 11.9537 5.36745 11.7677 5.1633C11.5817 4.95916 11.2655 4.94441 11.0614 5.13037L7.00459 8.82569L2.9363 5.12991Z" fill="#888888"/>
</svg>

After

Width:  |  Height:  |  Size: 531 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -49,7 +49,7 @@ select {
background-image: url('/icons/Arrow.svg');
background-repeat: no-repeat;
background-position-y: center;
background-position-x: 98%;
background-position-x: 97%;
background-size: $unit * 1.5;
border: none;
border-radius: 6px;
@ -110,6 +110,7 @@ select {
.DialogHeader {
display: flex;
align-items: center;
gap: $unit;
.left {
@ -150,6 +151,19 @@ select {
flex-grow: 1;
}
.DialogTop {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: $unit / 2;
.SubTitle {
color: $grey-50;
font-size: $font-small;
font-weight: $medium;
}
}
.DialogDescription {
flex-grow: 1;
}

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

@ -0,0 +1,11 @@
interface AxSkill {
name: {
en: string,
jp: string
},
id: number,
minValue: number,
maxValue: number,
suffix?: string,
secondary?: AxSkill[]
}

View file

@ -4,4 +4,7 @@ interface GridWeapon {
position: number
object: Weapon
uncap_level: number
element: number
weapon_keys?: Array<WeaponKey>
ax?: Array<SimpleAxSkill>
}

4
types/SimpleAxSkill.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
interface SimpleAxSkill {
modifier: number
strength: number
}

2
types/Weapon.d.ts vendored
View file

@ -7,6 +7,8 @@ interface Weapon {
proficiency: number
max_level: number
max_skill_level: number
series: number
ax: number
name: {
en: string
jp: string

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

@ -0,0 +1,11 @@
interface WeaponKey {
id: string
name: {
en: string,
jp: string
}
series: integer
slot: integer
group: integer
order: integer
}

View file

@ -103,10 +103,12 @@ class Api {
const api: Api = new Api({ url: process.env.NEXT_PUBLIC_SIERO_API_URL || 'https://localhost:3000/api/v1'})
api.createEntity( { name: 'users' })
api.createEntity( { name: 'parties' })
api.createEntity( { name: 'grid_weapons' })
api.createEntity( { name: 'characters' })
api.createEntity( { name: 'weapons' })
api.createEntity( { name: 'summons' })
api.createEntity( { name: 'raids' })
api.createEntity( { name: 'weapon_keys' })
api.createEntity( { name: 'favorites' })
export default api

791
utils/axData.tsx Normal file
View file

@ -0,0 +1,791 @@
export const axData: AxSkill[][] = [
[
{
name: {
"en": "ATK",
"jp": "攻撃"
},
id: 0,
minValue: 1,
maxValue: 3.5,
suffix: '%',
secondary: [
{
name: {
"en": "C.A. DMG",
"jp": "奥義ダメ"
},
id: 3,
minValue: 2,
maxValue: 4,
suffix: '%'
},
{
name: {
"en": "Double Attack Rate",
"jp": "DA確率"
},
id: 5,
minValue: 1,
maxValue: 2,
suffix: '%'
},
{
name: {
"en": "Triple Attack Rate",
"jp": "TA確率"
},
id: 6,
minValue: 1,
maxValue: 2,
suffix: '%'
},
{
name: {
"en": "Skill DMG Cap",
"jp": "アビ上限"
},
id: 7,
minValue: 1,
maxValue: 2,
suffix: '%'
}
]
},
{
name: {
"en": "DEF",
"jp": "防御"
},
id: 1,
minValue: 1,
maxValue: 8,
suffix: '%',
secondary: [
{
name: {
"en": "HP",
"jp": "HP"
},
id: 2,
minValue: 1,
maxValue: 3,
suffix: '%'
},
{
name: {
"en": "Debuff Resistance",
"jp": "弱体耐性"
},
id: 9,
minValue: 1,
maxValue: 3,
suffix: '%'
},
{
name: {
"en": "Healing",
"jp": "回復性能"
},
id: 10,
minValue: 2,
maxValue: 5,
suffix: '%'
},
{
name: {
"en": "Enmity",
"jp": "背水"
},
id: 11,
minValue: 1,
maxValue: 3
}
]
},
{
name: {
"en": "HP",
"jp": "HP"
},
id: 2,
minValue: 1,
maxValue: 11,
suffix: '%',
secondary: [
{
name: {
"en": "DEF",
"jp": "防御"
},
id: 1,
minValue: 1,
maxValue: 3,
suffix: '%'
},
{
name: {
"en": "Debuff Resistance",
"jp": "弱体耐性"
},
id: 9,
minValue: 1,
maxValue: 3,
suffix: '%'
},
{
name: {
"en": "Healing",
"jp": "回復性能"
},
id: 10,
minValue: 2,
maxValue: 5,
suffix: '%'
},
{
name: {
"en": "Stamina",
"jp": "渾身"
},
id: 12,
minValue: 1,
maxValue: 3
}
]
},
{
name: {
"en": "C.A. DMG",
"jp": "奥義ダメ"
},
id: 3,
minValue: 2,
maxValue: 8.5,
suffix: '%',
secondary: [
{
name: {
"en": "ATK",
"jp": "攻撃"
},
id: 0,
minValue: 1,
maxValue: 1.5,
suffix: '%'
},
{
name: {
"en": "Elemental ATK",
"jp": "全属性攻撃力"
},
id: 13,
minValue: 1,
maxValue: 5,
suffix: '%'
},
{
name: {
"en": "C.A. DMG Cap",
"jp": "奥義上限"
},
id: 8,
minValue: 1,
maxValue: 2,
suffix: '%'
},
{
name: {
"en": "Stamina",
"jp": "渾身"
},
id: 12,
minValue: 1,
maxValue: 3
}
]
},
{
name: {
"en": "Multiattack Rate",
"jp": "連撃率"
},
id: 4,
minValue: 1,
maxValue: 4,
suffix: '%',
secondary: [
{
name: {
"en": "C.A. DMG",
"jp": "奥義ダメ"
},
id: 3,
minValue: 2,
maxValue: 4,
suffix: '%'
},
{
name: {
"en": "Elemental ATK",
"jp": "全属性攻撃力"
},
id: 13,
minValue: 1,
maxValue: 5,
suffix: '%'
},
{
name: {
"en": "Double Attack Rate",
"jp": "DA確率"
},
id: 5,
minValue: 1,
maxValue: 2,
suffix: '%'
},
{
name: {
"en": "Triple Attack Rate",
"jp": "TA確率"
},
id: 6,
minValue: 1,
maxValue: 2,
suffix: '%'
}
]
}
], [
{
name: {
"en": "ATK",
"jp": "攻撃"
},
id: 0,
minValue: 1,
maxValue: 3.5,
suffix: '%',
secondary: [
{
name: {
"en": "C.A. DMG",
"jp": "奥義ダメ"
},
id: 3,
minValue: 2,
maxValue: 8.5,
suffix: '%'
},
{
name: {
"en": "Multiattack Rate",
"jp": "連撃確率"
},
id: 4,
minValue: 1.5,
maxValue: 4,
suffix: '%'
},
{
name: {
"en": "Normal ATK DMG Cap",
"jp": "通常ダメ上限"
},
id: 14,
minValue: 0.5,
maxValue: 1.5,
suffix: '%'
},
{
name: {
"en": "Supplemental Skill DMG",
"jp": "アビ与ダメ上昇"
},
id: 15,
minValue: 1,
maxValue: 5
}
]
},
{
name: {
"en": "DEF",
"jp": "防御"
},
id: 1,
minValue: 1,
maxValue: 8,
suffix: '%',
secondary: [
{
name: {
"en": "Elemental DMG Reduction",
"jp": "属性ダメ軽減"
},
id: 17,
minValue: 1,
maxValue: 5,
suffix: '%'
},
{
name: {
"en": "Debuff Resistance",
"jp": "弱体耐性"
},
id: 9,
minValue: 1,
maxValue: 3,
suffix: '%'
},
{
name: {
"en": "Healing",
"jp": "回復性能"
},
id: 10,
minValue: 2,
maxValue: 5,
suffix: '%'
},
{
name: {
"en": "Enmity",
"jp": "背水"
},
id: 11,
minValue: 1,
maxValue: 3
}
]
},
{
name: {
"en": "HP",
"jp": "HP"
},
id: 2,
minValue: 1,
maxValue: 11,
suffix: '%',
secondary: [
{
name: {
"en": "Elemental DMG Reduction",
"jp": "属性ダメ軽減"
},
id: 17,
minValue: 1,
maxValue: 5,
suffix: '%'
},
{
name: {
"en": "Debuff Resistance",
"jp": "弱体耐性"
},
id: 9,
minValue: 1,
maxValue: 3,
suffix: '%'
},
{
name: {
"en": "Healing",
"jp": "回復性能"
},
id: 10,
minValue: 2,
maxValue: 5,
suffix: '%'
},
{
name: {
"en": "Stamina",
"jp": "渾身"
},
id: 12,
minValue: 1,
maxValue: 3
}
]
},
{
name: {
"en": "C.A. DMG",
"jp": "奥義ダメ"
},
id: 3,
minValue: 2,
maxValue: 8.5,
suffix: '%',
secondary: [
{
name: {
"en": "Multiattack Rate",
"jp": "連撃率"
},
id: 4,
minValue: 1.5,
maxValue: 4,
suffix: '%'
},
{
name: {
"en": "Supplemental Skill DMG",
"jp": "アビ与ダメ上昇"
},
id: 15,
minValue: 1,
maxValue: 5,
},
{
name: {
"en": "Supplemental C.A. DMG",
"jp": "奥義与ダメ上昇"
},
id: 16,
minValue: 1,
maxValue: 5,
},
{
name: {
"en": "Stamina",
"jp": "渾身"
},
id: 12,
minValue: 1,
maxValue: 3
}
]
},
{
name: {
"en": "Multiattack Rate",
"jp": "連撃率"
},
id: 4,
minValue: 1,
maxValue: 4,
suffix: '%',
secondary: [
{
name: {
"en": "Supplemental C.A. DMG",
"jp": "奥義与ダメ上昇"
},
id: 16,
minValue: 1,
maxValue: 5,
},
{
name: {
"en": "Normal ATK DMG Cap",
"jp": "通常ダメ上限"
},
id: 14,
minValue: 0.5,
maxValue: 1.5,
suffix: '%'
},
{
name: {
"en": "Stamina",
"jp": "渾身"
},
id: 12,
minValue: 1,
maxValue: 3,
},
{
name: {
"en": "Enmity",
"jp": "背水"
},
id: 11,
minValue: 1,
maxValue: 3,
}
]
}
], [
{
name: {
"en": "ATK",
"jp": "攻撃"
},
id: 0,
minValue: 1,
maxValue: 3.5,
suffix: '%',
secondary: [
{
name: {
"en": "C.A. DMG",
"jp": "奥義ダメ"
},
id: 3,
minValue: 2,
maxValue: 4,
suffix: '%'
},
{
name: {
"en": "Double Attack Rate",
"jp": "DA確率"
},
id: 5,
minValue: 1,
maxValue: 2,
suffix: '%'
},
{
name: {
"en": "Triple Attack Rate",
"jp": "TA確率"
},
id: 6,
minValue: 1,
maxValue: 2,
suffix: '%'
},
{
name: {
"en": "Skill DMG Cap",
"jp": "アビ上限"
},
id: 7,
minValue: 1,
maxValue: 2,
suffix: '%'
}
]
},
{
name: {
"en": "DEF",
"jp": "防御"
},
id: 1,
minValue: 1,
maxValue: 8,
suffix: '%',
secondary: [
{
name: {
"en": "HP",
"jp": "HP"
},
id: 2,
minValue: 1,
maxValue: 3,
suffix: '%'
},
{
name: {
"en": "Debuff Resistance",
"jp": "弱体耐性"
},
id: 9,
minValue: 1,
maxValue: 3,
suffix: '%'
},
{
name: {
"en": "Healing",
"jp": "回復性能"
},
id: 10,
minValue: 2,
maxValue: 5,
suffix: '%'
},
{
name: {
"en": "Enmity",
"jp": "背水"
},
id: 11,
minValue: 1,
maxValue: 3
}
]
},
{
name: {
"en": "HP",
"jp": "HP"
},
id: 2,
minValue: 1,
maxValue: 11,
suffix: '%',
secondary: [
{
name: {
"en": "DEF",
"jp": "防御"
},
id: 1,
minValue: 1,
maxValue: 3,
suffix: '%'
},
{
name: {
"en": "Debuff Resistance",
"jp": "弱体耐性"
},
id: 9,
minValue: 1,
maxValue: 3,
suffix: '%'
},
{
name: {
"en": "Healing",
"jp": "回復性能"
},
id: 10,
minValue: 2,
maxValue: 5,
suffix: '%'
},
{
name: {
"en": "Stamina",
"jp": "渾身"
},
id: 12,
minValue: 1,
maxValue: 3
}
]
},
{
name: {
"en": "C.A. DMG",
"jp": "奥義ダメ"
},
id: 3,
minValue: 2,
maxValue: 8.5,
suffix: '%',
secondary: [
{
name: {
"en": "ATK",
"jp": "攻撃"
},
id: 0,
minValue: 1,
maxValue: 1.5,
suffix: '%'
},
{
name: {
"en": "Elemental ATK",
"jp": "全属性攻撃力"
},
id: 13,
minValue: 1,
maxValue: 5,
suffix: '%'
},
{
name: {
"en": "C.A. DMG Cap",
"jp": "奥義上限"
},
id: 8,
minValue: 1,
maxValue: 2,
suffix: '%'
},
{
name: {
"en": "Stamina",
"jp": "渾身"
},
id: 12,
minValue: 1,
maxValue: 3
}
]
},
{
name: {
"en": "Multiattack Rate",
"jp": "連撃率"
},
id: 4,
minValue: 1,
maxValue: 4,
suffix: '%',
secondary: [
{
name: {
"en": "C.A. DMG",
"jp": "奥義ダメ"
},
id: 3,
minValue: 2,
maxValue: 4,
suffix: '%'
},
{
name: {
"en": "Elemental ATK",
"jp": "全属性攻撃力"
},
id: 13,
minValue: 1,
maxValue: 5,
suffix: '%'
},
{
name: {
"en": "Double Attack Rate",
"jp": "DA確率"
},
id: 5,
minValue: 1,
maxValue: 2,
suffix: '%'
},
{
name: {
"en": "Triple Attack Rate",
"jp": "TA確率"
},
id: 6,
minValue: 1,
maxValue: 2,
suffix: '%'
}
]
},
{
name: {
"en": "EXP Up",
"jp": "EXP UP"
},
id: 18,
minValue: 5,
maxValue: 10,
suffix: '%'
},
{
name: {
"en": "Rupies",
"jp": "獲得ルピ"
},
id: 19,
minValue: 10,
maxValue: 20,
suffix: '%'
}
]
]