Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ZO add 4p disc effects with simple conditional system #2657

Merged
merged 7 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions libs/common/util/src/lib/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,23 @@ export function objFilterKeys<K extends string, K2 extends string, V>(
) as Record<K2, V>
}

export function objFilter<K extends string | number, V>(
obj: Record<K, V>,
f: (v: V, k: K, i: number) => boolean
): Record<K, V>
export function objFilter<K extends string | number, V>(
obj: Partial<Record<K, V>>,
f: (v: V, k: K, i: number) => boolean
): Partial<Record<K, V>>
export function objFilter<K extends string | number, V>(
obj: Record<K, V>,
f: (v: V, k: K, i: number) => boolean
): Record<K, V> {
return Object.fromEntries(
Object.entries(obj).filter(([k, v], i) => f(v as V, k as K, i))
) as Record<K, V>
}
frzyc marked this conversation as resolved.
Show resolved Hide resolved

export function objMap<K extends string | number, V, V2>(
obj: Record<K, V>,
f: (v: V, k: K, i: number) => V2
Expand All @@ -79,15 +96,6 @@ export function objMap<K extends string | number, V, V2>(
) as Record<K, V2>
}

export function objFilter<K extends string | number, V>(
obj: Record<K, V>,
f: (v: V, k: K, i: number) => boolean
): Record<K, V> {
return Object.fromEntries(
Object.entries(obj).filter(([k, v], i) => f(v as V, k as K, i))
) as Record<K, V>
}

/**
* Generate an object from an array of keys, and a function that maps the key to a value
* @param keys
Expand Down
10 changes: 9 additions & 1 deletion libs/zzz/consts/src/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { objKeyMap } from '@genshin-optimizer/common/util'
import { allDiscMainStatKeys, allDiscSubStatKeys } from './disc'
import type { DiscCondKey } from './disc'
import {
allDiscCondKeys,
allDiscMainStatKeys,
allDiscSubStatKeys,
} from './disc'

export const otherStatKeys = [
// Used by calc, likely will be bundled into pando
Expand Down Expand Up @@ -124,3 +129,6 @@ const elementalData: Record<AttributeKey, string> = {
Object.entries(elementalData).forEach(([e, name]) => {
statKeyTextMap[`${e}_dmg_`] = `${name} DMG Bonus`
})

export type CondKey = DiscCondKey
export const allCondKeys = Object.keys(allDiscCondKeys) as CondKey[]
210 changes: 210 additions & 0 deletions libs/zzz/consts/src/disc.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { objMultiplication } from '@genshin-optimizer/common/util'
import type { PandoStatKey } from './common'

export const allDiscSlotKeys = ['1', '2', '3', '4', '5', '6'] as const
Expand Down Expand Up @@ -197,3 +198,212 @@ export const discSetNames: Record<DiscSetKey, string> = {
BranchBladeSong: 'Branch & Blade Song',
AstralVoice: 'Astral Voice',
}

export const allDiscCondKeys = {
AstralVoice: {
key: 'AstralVoice',
text: (val: number) => `${val} Stacks of Astral`,
min: 1,
max: 3,
},
BranchBladeSong: {
key: 'BranchBladeSong',
text: `When any squad member applies Freeze or triggers the Shatter effect on an enemy`,
min: 1,
max: 1,
},
ChaosJazz: {
key: 'ChaosJazz',
text: 'When off-field',
min: 1,
max: 1,
},
ChaoticMetal: {
key: 'ChaoticMetal',
text: 'Whenever a squad member inflicts Corruption on an enemy',
min: 1,
max: 1,
},
FangedMetal: {
key: 'FangedMetal',
text: 'Whenever a squad member inflicts Assault on an enemy',
min: 1,
max: 1,
},
HormonePunk: {
key: 'HormonePunk',
text: 'Upon entering or switching into combat',
min: 1,
max: 1,
},
InfernoMetal: {
key: 'InfernoMetal',
text: 'Upon hitting a Burning enemy',
min: 1,
max: 1,
},
PolarMetal: {
key: 'PolarMetal',
text: 'Whenever a squad member Freezes or Shatters an enemy',
min: 1,
max: 1,
},
ProtoPunk: {
key: 'ProtoPunk',
text: 'When any squad member triggers a Defensive Assist or Evasive Assist',
min: 1,
max: 1,
},
PufferElectro: {
key: 'PufferElectro',
text: 'Launching an Ultimate',
min: 1,
max: 1,
},
SwingJazz: {
key: 'SwingJazz',
text: 'Launching a Chain Attack or Ultimate',
min: 1,
max: 1,
},
ThunderMetal: {
key: 'ThunderMetal',
text: 'As long as an enemy in combat is Shocked',
min: 1,
max: 1,
},
WoodpeckerElectro: {
key: 'WoodpeckerElectro',
text: (val: number) =>
`${val}x Triggering a critical hit with a Basic Attack, Dodge Counter, or EX Special Attack`,
min: 1,
max: 3,
},
} as const

export type DiscCondKey = keyof typeof allDiscCondKeys
export const disc4PeffectSheets: Partial<
Record<
DiscSetKey,
{
condMeta: (typeof allDiscCondKeys)[DiscCondKey]
getStats: (
conds: Partial<Record<DiscCondKey, number>>,
stats: Record<string, number>
) => Record<string, number> | undefined
}
>
> = {
AstralVoice: {
condMeta: allDiscCondKeys.AstralVoice,
getStats: (conds) => {
return conds['AstralVoice']
? (objMultiplication({ dmg_: 0.08 }, conds['AstralVoice']) as Record<
string,
number
>)
: undefined
},
},
BranchBladeSong: {
condMeta: allDiscCondKeys.BranchBladeSong,
getStats: (conds, stats) => {
const ret: Record<string, number> = {}
if (stats['anomMas'] >= 115) ret['crit_dmg_'] = 0.3
if (conds['BranchBladeSong']) ret['crit_'] = 0.12
return ret
},
},
ChaosJazz: {
condMeta: allDiscCondKeys.ChaosJazz,
getStats: (conds) => {
const ret: Record<string, number> = {
fire_dmg_: 0.15,
electric_dmg_: 0.15,
}
if (conds['ChaosJazz']) ret['dmg_'] = 0.2 // TODO: Should be EX Special Attacks and Assist Attacks
return ret
},
},
ChaoticMetal: {
condMeta: allDiscCondKeys.ChaoticMetal,
getStats: (conds) => {
if (conds['ChaoticMetal']) return { dmg_: 0.18 } //enemy takes 18% more DMG
return undefined
},
},
FangedMetal: {
condMeta: allDiscCondKeys.FangedMetal,
getStats: (conds) => {
if (conds['FangedMetal']) return { dmg_: 0.35 } // equipper deals 35% additional DMG
return undefined
},
},
// FreedomBlues: reduce the target's Anomaly Buildup RES to the equipper's Attribute by 35%
HormonePunk: {
condMeta: allDiscCondKeys.HormonePunk,
getStats: (conds) => {
if (conds['HormonePunk']) return { cond_atk_: 0.15 } // ATK increased by 25%
return undefined
frzyc marked this conversation as resolved.
Show resolved Hide resolved
},
},
InfernoMetal: {
condMeta: allDiscCondKeys.InfernoMetal,
getStats: (conds) => {
if (conds['InfernoMetal']) return { crit_: 0.28 } //equipper's CRIT Rate is increased by 28%
return undefined
},
},
PolarMetal: {
condMeta: allDiscCondKeys.PolarMetal,
getStats: (conds) => {
const ret = { dmg_: 0.28 } // TODO: Basic Attack and Dash Attack DMG increases by 28%
if (conds['PolarMetal']) {
objMultiplication(ret, 2)
}
return ret
},
},
ProtoPunk: {
condMeta: allDiscCondKeys.ProtoPunk,
getStats: (conds) => {
if (conds['ProtoPunk']) return { dmg_: 0.15 } // all squad members deal 15% increased DMG
return undefined
},
},
PufferElectro: {
condMeta: allDiscCondKeys.PufferElectro,
getStats: (conds) => {
const ret: Record<string, number> = { dmg_: 0.2 } //Ultimate DMG increases by 20%
if (conds['PufferElectro']) ret['cond_atk_'] = 0.15 // Launching an Ultimate increases the equipper's ATK by 15%
return ret
},
},
// ShockstarDisco: 15% more Daze
// SoulRock: the equipper takes 40% less DMG
SwingJazz: {
condMeta: allDiscCondKeys.SwingJazz,
getStats: (conds) => {
if (conds['SwingJazz']) return { dmg_: 0.15 } //increases all squad members' DMG by 15%
return undefined
},
},
ThunderMetal: {
condMeta: allDiscCondKeys.ThunderMetal,
getStats: (conds) => {
if (conds['ThunderMetal']) return { cond_atk_: 0.27 } // equipper's ATK is increased by 27%
return undefined
},
},
WoodpeckerElectro: {
condMeta: allDiscCondKeys.WoodpeckerElectro,
getStats: (conds) => {
if (conds['WoodpeckerElectro'])
return objMultiplication(
{ cond_atk_: 0.15 },
conds['WoodpeckerElectro']
) as Record<string, number>
return undefined
},
},
}
8 changes: 8 additions & 0 deletions libs/zzz/db/src/Database/DataManagers/CharacterDataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@genshin-optimizer/common/util'
import type {
CharacterKey,
CondKey,
DiscMainStatKey,
DiscSetKey,
FormulaKey,
Expand Down Expand Up @@ -42,6 +43,7 @@ export type CharacterData = {
levelHigh: number
setFilter2: DiscSetKey[]
setFilter4: DiscSetKey[]
conditionals: Partial<Record<CondKey, number>>
}

function initialCharacterData(key: CharacterKey): CharacterData {
Expand All @@ -65,6 +67,7 @@ function initialCharacterData(key: CharacterKey): CharacterData {
levelHigh: 15,
setFilter2: [],
setFilter4: [],
conditionals: {},
}
}
export class CharacterDataManager extends DataManager<
Expand Down Expand Up @@ -95,6 +98,7 @@ export class CharacterDataManager extends DataManager<
levelHigh,
setFilter2,
setFilter4,
conditionals,
} = obj as CharacterData

if (!allCharacterKeys.includes(characterKey)) return undefined // non-recoverable
Expand Down Expand Up @@ -154,6 +158,9 @@ export class CharacterDataManager extends DataManager<
setFilter2 = validateArr(setFilter2, allDiscSetKeys, [])
setFilter4 = validateArr(setFilter4, allDiscSetKeys, [])

if (typeof conditionals !== 'object') conditionals = {}
conditionals = objFilter(conditionals, (value) => typeof value === 'number')

const char: CharacterData = {
key: characterKey,
level,
Expand All @@ -171,6 +178,7 @@ export class CharacterDataManager extends DataManager<
levelHigh,
setFilter2,
setFilter4,
conditionals,
}
return char
}
Expand Down
5 changes: 4 additions & 1 deletion libs/zzz/page-optimize/src/BuildsDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
Typography,
} from '@mui/material'
import { useCallback, useMemo } from 'react'
import { useCharacterContext } from './CharacterContext'
import { StatsDisplay } from './StatsDisplay'

export function BuildsDisplay({
Expand Down Expand Up @@ -53,16 +54,18 @@ function Build({
baseStats: Stats
}) {
const { database } = useDatabaseContext()
const character = useCharacterContext()
const sum = useMemo(
() =>
applyCalc(
baseStats,
character?.conditionals ?? {},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

semi-unrelated to PR:
changing conditionals causes the displayed stat to change on generated builds, but does not change the calculated value.

should we consider just grabbing the final result from the calculation instead of cache?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the calculated value dictate the order of the build, but you raise a good point. probably something to address for the future for sure.

Object.values(build.discIds)
.map((d) => database.discs.get(d))
.filter(notEmpty)
.map(convertDiscToStats)
),
[baseStats, build.discIds, database.discs]
[baseStats, build.discIds, character, database.discs]
frzyc marked this conversation as resolved.
Show resolved Hide resolved
)
const onEquip = useCallback(() => {
Object.values(build.discIds).forEach((dId) => {
Expand Down
Loading