ZZZ-Plugin/model/damage/BuffManager.ts

358 lines
No EOL
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { ZZZAvatarInfo } from '../avatar.js'
import type { Calculator, skill } from './Calculator.ts'
import _ from 'lodash'
export enum elementEnum {
// 物理、火、冰、电、以太
Physical, Fire, Ice, Electric, Ether
}
export enum anomalyEnum {
// 伤害异常
, , , , , ,
// 状态异常异常持续时间buff应作用于对应的状态异常
,
}
/** 属性类型 */
export type element = keyof typeof elementEnum
/** 异常类型 */
export type anomaly = keyof typeof anomalyEnum
/** Buff来源 */
export type buffSource = '音擎' | '套装' | '技能' | '影画' | '核心被动' | '额外能力'
export enum buffTypeEnum {
// 通用乘区
, , , , , , 穿, 穿, ,
// 直伤乘区
, ,
// 异常乘区
, , , , ,
// 贯穿乘区
穿, 穿,
// 其他属性一般不直接影响伤害但可能用于buff是否生效判断/转模
, , ,
}
/** Buff增益类型 */
export type buffType = keyof typeof buffTypeEnum
export interface buff {
/** Buff状态true生效false无效 */
status: boolean
/** Buff名称 */
name: string
/** Buff来源 */
source: buffSource
/** Buff增益的类型 */
type: buffType
/**
* Buff增益数值可为数值、字符串、数组、函数
* @number
* - 一般情况下此值即为提高值
* - 当buff增益类型为**攻击力/冲击力/异常精通/异常掌控/防御力/生命值**时,若此值 **<1**,则将此值理解为**初始属性**的**百分比提高**
* @string
* 角色自身的buff提高值可能随技能/天赋等级提高而提高此时可以于data.json的"buff"中添加倍率数组同上支持百分比提高此时value即为键名其首字母必须为对应技能的基类参考技能类型命名标准
* @array
* 根据buff.source自动选择对应等级/星级的值同上支持百分比提高支持的source
* - 音擎:音擎进阶星级
* - 核心被动、额外能力:核心技等级
* @function
* 函数返回值即为提高值
*/
value: number | string | number[] | (({ avatar, buffM, calc }: {
avatar: ZZZAvatarInfo
buffM: BuffManager
calc: Calculator
}) => number)
/**
* Buff增益技能类型**生效范围**;参考技能类型命名标准
* - 当技能参数不存在**redirect**时,**range**作用范围向后覆盖生效
* - 当技能参数存在**redirect**时,**range**与**type**全匹配时生效,**redirect**向后覆盖生效
* - 若需全匹配的精细操作,可使用**include**与**exclude**参数
*/
range?: string[] | anomaly[] | "追加攻击"[]
/**
* Buff增益技能类型**生效技能**
* - 不同于**range**仅全匹配type时该值生效不会向后覆盖生效
* - 无**range**且无**include**则该buff对**exclude**以外的全部技能生效
* - **range**与**include**符合其一则认为buff生效
* - 当技能参数存在**redirect**时,**range**与**include**的区别在于**include**不会尝试匹配**redirect**
*/
include?: string[]
/**
* Buff增益技能类型**排除技能**
* - 与**include**相同仅全匹配type时该值生效不会向后覆盖生效
* - 优先级高于**range**与**include**
*/
exclude?: string[]
/** Buff增益属性类型无则对全部生效 */
element?: element | element[]
/**
* 检查buff是否生效
* @function
* 音擎会自动添加职业检查
* @number
* - buff.source为Set时判断套装数量>=该值
* - buff.source为Rank时判断影画数量>=该值
*/
check?: (({ avatar, buffM, calc }: {
avatar: ZZZAvatarInfo
buffM: BuffManager
calc: Calculator
}) => boolean) | number
/** Buff描述符 */
is: {
/** 是否团队增益 @default false */
team?: boolean
/** 为团队增益时,同名效果是否可叠加 @default false */
stack?: boolean
}
}
type filterable = 'name' | 'element' | 'type' | 'range' | 'source'
let depth = 0, weakMapCheck = new WeakMap<buff, boolean>()
/**
* Buff管理器
* 用于管理角色局内Buff
*/
export class BuffManager {
readonly avatar: ZZZAvatarInfo
readonly buffs: buff[] = []
/** 套装计数 */
setCount: { [name: string]: number } = {}
defaultBuff: Partial<buff> = {}
constructor(avatar: ZZZAvatarInfo) {
this.avatar = avatar
}
/** 注册并格式化buff */
new(buff: buff): buff[]
/** 注册并格式化buffs */
new(buffs: buff[]): buff[]
new(buff: buff | buff[]) {
if (Array.isArray(buff)) {
buff.forEach(b => this.new(b))
return this.buffs
}
// 简化参数
if (!buff.name && (buff.source || this.defaultBuff.source) === '套装' && this.defaultBuff.name && typeof buff.check === 'number')
buff.name = this.defaultBuff.name + buff.check
const oriBuff = buff
buff = _.merge({
status: true,
// is: {},
...this.defaultBuff
}, buff)
if (buff.range && !Array.isArray(buff.range))
buff.range = oriBuff.range = [buff.range]
if (!buff.source) {
if (buff.name.includes('核心') || buff.name.includes('天赋')) buff.source = oriBuff.source = '核心被动'
else if (buff.name.includes('额外能力')) buff.source = oriBuff.source = '额外能力'
else if (buff.name.includes('影')) buff.source = oriBuff.source = '影画'
else if (buff.name.includes('技')) buff.source = oriBuff.source = '技能'
}
if (!buff.name || !buff.value || !buff.source || !buffTypeEnum[buffTypeEnum[buff.type]])
return logger.warn('无效buff', buff)
// 音擎buff职业检查
if (buff.source === '音擎') {
const professionCheck = (avatar: ZZZAvatarInfo) => {
const weapon_profession = avatar.weapon?.profession
if (!weapon_profession) return true
return avatar.avatar_profession === weapon_profession
}
const oriCheck = typeof buff.check === 'function' && buff.check
buff.check = ({ avatar, buffM, calc }) => professionCheck(avatar) && (!oriCheck || oriCheck({ avatar, buffM, calc }))
// 影画buff影画数检查
} else if (buff.source === '影画') {
buff.check ??= oriBuff.check = +buff.name.match(/\d/)!?.[0]
}
this.buffs.push(buff)
return this.buffs
}
_filter<T extends filterable>(buffs: buff[], type: T, value: buff[T]): buff[]
_filter(buffs: buff[], obj: Partial<Pick<buff, filterable>> & { element: element, redirect?: skill['redirect'] }, calc?: Calculator): buff[]
_filter(buffs: buff[], fnc: (buff: buff, index: number) => boolean): buff[]
_filter<T extends filterable>(
buffs: buff[],
param:
| T
| (Partial<Pick<buff, filterable>> & { element: element, redirect?: skill['redirect'] })
| ((buff: buff, index: number) => boolean),
valueOcalc?: buff[T] | Calculator
) {
depth++
try {
if (typeof param === 'string') {
buffs = buffs.filter(buff => buff[param] === valueOcalc)
} else if (typeof param === 'object') {
buffs = buffs.filter(buff => {
if (buff.status === false) return false
const judge = (() => {
// 未传入calc时不判断range、include、exclude
if (typeof valueOcalc !== 'object' || Array.isArray(valueOcalc)) return true
// buff指定排除该技能
if (buff.exclude && buff.exclude.includes(valueOcalc.skill.type)) return false
// 11 10 01
if (buff.range || buff.include) {
// 11 01 存在include且满足时则直接返回true
if (buff.include && buff.include.includes(valueOcalc.skill.type)) return true
// 01 没有range则代表只有include直接返回false
if (!buff.range) return false
// 11 10 直接返回range的结果即可
const buffRange = buff.range
const skillRange = param.range?.filter(r => typeof r === 'string')
if (!skillRange?.length) return true // 对任意类型生效
// buff作用范围向后覆盖生效
// 存在重定向时range与type全匹配时生效redirect向后覆盖生效
else if (param.redirect) {
if (skillRange.some(ST => buffRange.some(BT => BT === ST))) return true
const redirect = Array.isArray(param.redirect) ? param.redirect : [param.redirect]
if (buffRange.some(BT => redirect.some(RT => RT.startsWith(BT)))) return true
return false
}
// 不存在重定向时range向后覆盖生效
return skillRange.some(ST => buffRange.some(BT => ST.startsWith(BT)))
}
// 00
return true
})()
if (!judge) return false
for (const key in param) {
if (key === 'redirect' || key === 'range') continue
if (key === 'element') {
if (!buff.element || !param.element) continue // 对任意属性生效
if (Array.isArray(buff.element)) {
if (buff.element.includes(param.element)) continue
return false
}
}
// @ts-ignore
if (buff[key] !== param[key]) return false
}
if (buff.check) {
if (typeof buff.check === 'number') {
if (buff.source === '套装' && (this.setCount[buff.name.replace(/\d$/, '')] < buff.check)) return false
else if (buff.source === '影画' && (this.avatar.rank < buff.check)) return false
} else if (valueOcalc) {
if (weakMapCheck.has(buff)) {
// console.log(`depth${depth} ${buff.name}${weakMapCheck.get(buff)}`)
if (!weakMapCheck.get(buff)) return false
} else {
weakMapCheck.set(buff, false)
if (!buff.check({
avatar: this.avatar,
buffM: this,
calc: valueOcalc as Calculator
})) return false
weakMapCheck.set(buff, true)
}
} else {
logger.debug('未传入calc' + buff.name)
return false
}
}
return true
})
} else {
buffs = buffs.filter(param)
}
} catch (e) {
logger.error(e)
}
if (--depth === 0) {
// console.log('重置weakMapCheck')
weakMapCheck = new WeakMap()
}
return buffs
}
/**
* 根据单个指定属性筛选buff不作进一步判断
*/
filter<T extends filterable>(type: T, value: buff[T]): buff[]
/**
* 根据多个指定属性筛选 **启用状态** 的buff
* - 对伤害类型range数组的筛选只要其中有一个符合即认为满足
* - 不存在重定向时range向后覆盖生效
* - 存在重定向时range与type全匹配时生效redirect向后覆盖生效
*/
filter(obj: Partial<Pick<buff, filterable>> & { element: element, redirect?: skill['redirect'] }, calc?: Calculator): buff[]
/**
* 根据指定函数筛选buff
*/
filter(fnc: (buff: buff, index: number) => boolean): buff[]
filter<T extends filterable>(
param:
| T
| (Partial<Pick<buff, filterable>> & { element: element, redirect?: skill['redirect'] })
| ((buff: buff, index: number) => boolean),
valueOcalc?: buff[T] | Calculator
) {
// @ts-ignore
return this._filter(this.buffs, param, valueOcalc)
}
/** 遍历buff列表 */
forEach(fnc: (buff: buff, index: number) => void) {
return this.buffs.forEach(fnc)
}
/** 查找指定buff */
find<T extends keyof buff>(type: T, value: buff[T]) {
return this.buffs.find(buff => buff[type] === value)
}
operator(buff: Partial<buff>, fnc: (buff: buff) => void): void
operator<T extends keyof buff>(key: T, value: buff[T], fnc: (buff: buff) => void): void
operator<T extends keyof buff>(key: T | Partial<buff>, value: buff[T] | ((buff: buff) => void), fnc?: (buff: buff) => void) {
const isMatch = typeof key === 'object' ?
(targetBuff: buff) => Object.entries(key).every(([k, v]) => targetBuff[k as keyof buff] === v) :
(targetBuff: buff) => targetBuff[key] === value
this.forEach(buff => isMatch(buff) && (fnc || value as (buff: buff) => void)(buff))
}
/** 关闭符合条件的所有buff */
close(buff: Partial<buff>): void
close<T extends keyof buff>(key: T, value: buff[T]): void
close<T extends keyof buff>(key: T | Partial<buff>, value?: buff[T] | Partial<buff>) {
if (typeof key === 'object')
this.operator(key, buff => buff.status = false)
else
this.operator(key, value as buff[T], buff => buff.status = false)
}
/** 开启符合条件的所有buff */
open(buff: Partial<buff>): void
open<T extends keyof buff>(key: T, value: buff[T]): void
open<T extends keyof buff>(key: T, value?: buff[T]) {
if (typeof key === 'object')
this.operator(key, buff => buff.status = true)
else
this.operator(key, value as buff[T], buff => buff.status = true)
}
/**
* 设置后续新增buff参数的默认值
* @param obj 直接覆盖默认值
*/
default(obj: Partial<buff>): void
/**
* 设置后续新增buff参数的默认值
* @param value 为undefined时删除默认值
*/
default<T extends keyof buff>(key: T, value?: buff[T]): void
default<T extends keyof buff>(param: T | Partial<buff>, value?: buff[T]): void {
if (typeof param === 'object') {
this.defaultBuff = param
} else {
if (value === undefined) delete this.defaultBuff[param]
else this.defaultBuff[param] = value
}
}
}