2021-07-13 02:03:10 +02:00
// SPDX-FileCopyrightText: 2021 Johannes Loher
//
// SPDX-License-Identifier: MIT
2022-08-25 03:31:30 +02:00
import { mathEvaluator } from "../expression-evaluation/evaluator" ;
2022-11-04 21:47:18 +01:00
import { getGame } from "../utils/utils" ;
2021-07-13 02:03:10 +02:00
2022-11-04 21:47:18 +01:00
import type { DS4Actor } from "./actor/actor" ;
import type { DS4Item } from "./item/item" ;
2022-11-03 21:41:44 +01:00
2021-07-13 02:03:10 +02:00
declare global {
interface DocumentClassConfig {
ActiveEffect : typeof DS4ActiveEffect ;
}
2022-08-25 03:31:30 +02:00
interface FlagConfig {
ActiveEffect : {
ds4 ? : {
itemEffectConfig ? : {
applyToItems? : boolean ;
itemName? : string ;
condition? : string ;
} ;
} ;
} ;
}
2021-07-13 02:03:10 +02:00
}
2021-07-23 12:29:01 +02:00
type PromisedType < T > = T extends Promise < infer U > ? U : T ;
2021-08-19 03:04:40 +02:00
2021-07-13 02:03:10 +02:00
export class DS4ActiveEffect extends ActiveEffect {
2021-07-23 17:32:26 +02:00
/ * *
* A fallback icon that can be used if no icon is defined for the effect .
* /
static FALLBACK_ICON = "icons/svg/aura.svg" ;
2021-07-23 12:29:01 +02:00
/ * *
* A cached reference to the source document to avoid recurring database lookups
* /
protected source : PromisedType < ReturnType < typeof fromUuid > > | undefined = undefined ;
2021-08-19 03:04:40 +02:00
/ * *
* Whether or not this effect is currently surpressed .
* /
get isSurpressed ( ) : boolean {
const originatingItem = this . originatingItem ;
if ( ! originatingItem ) {
return false ;
}
return originatingItem . isNonEquippedEuipable ( ) ;
}
/ * *
* The item which this effect originates from if it has been transferred from an item to an actor .
* /
get originatingItem ( ) : DS4Item | undefined {
2022-11-03 21:41:44 +01:00
if ( ! ( this . parent instanceof Actor ) ) {
2021-08-19 03:04:40 +02:00
return ;
}
2021-09-23 12:45:53 +02:00
const itemIdRegex = /Item\.([a-zA-Z0-9]+)/ ;
const itemId = this . data . origin ? . match ( itemIdRegex ) ? . [ 1 ] ;
2021-08-19 03:04:40 +02:00
if ( ! itemId ) {
return ;
}
return this . parent . items . get ( itemId ) ;
}
/ * *
* The number of times this effect should be applied .
* /
get factor ( ) : number {
return this . originatingItem ? . activeEffectFactor ? ? 1 ;
}
2022-08-25 03:31:30 +02:00
override apply ( document : DS4Actor | DS4Item , change : EffectChangeData ) : unknown {
change . value = Roll . replaceFormulaData ( change . value , document . data ) ;
2021-07-13 02:03:10 +02:00
try {
2022-10-31 22:58:04 +01:00
change . value = DS4ActiveEffect . safeEval ( change . value ) . toString ( ) ;
2021-07-13 02:03:10 +02:00
} catch ( e ) {
// this is a valid case, e.g., if the effect change simply is a string
}
2022-08-25 03:31:30 +02:00
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error In the types and foundry's documentation, only actors are allowed, but the implementation actually works for all kinds of documents
return super . apply ( document , change ) ;
2021-07-13 02:03:10 +02:00
}
2021-07-20 02:35:55 +02:00
/ * *
2021-07-23 12:29:01 +02:00
* Gets the current source name based on the cached source object .
* /
async getCurrentSourceName ( ) : Promise < string > {
const game = getGame ( ) ;
const origin = await this . getSource ( ) ;
if ( origin === null ) return game . i18n . localize ( "None" ) ;
return origin . name ? ? game . i18n . localize ( "Unknown" ) ;
}
/ * *
2022-01-22 14:15:39 +01:00
* Gets the source document for this effect . Uses the cached { @link DS4ActiveEffect # source } if it has already been
2021-07-23 12:29:01 +02:00
* set .
2021-07-20 02:35:55 +02:00
* /
2021-07-23 12:29:01 +02:00
protected async getSource ( ) : ReturnType < typeof fromUuid > {
if ( this . source === undefined ) {
this . source = this . data . origin !== undefined ? await fromUuid ( this . data . origin ) : null ;
}
return this . source ;
2021-07-20 02:35:55 +02:00
}
2021-08-19 03:04:40 +02:00
/ * *
* Create a new { @link DS4ActiveEffect } using default data .
*
2021-09-22 12:32:18 +02:00
* @param parent The parent { @link DS4Actor } or { @link DS4Item } of the effect .
2021-08-19 03:04:40 +02:00
* @returns A promise that resolved to the created effect or udifined of the creation was prevented .
* /
2021-09-22 12:32:18 +02:00
static async createDefault ( parent : DS4Actor | DS4Item ) : Promise < DS4ActiveEffect | undefined > {
2021-08-19 03:04:40 +02:00
const createData = {
label : getGame ( ) . i18n . localize ( ` DS4.NewEffectLabel ` ) ,
icon : this.FALLBACK_ICON ,
} ;
2021-09-22 12:32:18 +02:00
return this . create ( createData , { parent , pack : parent.pack ? ? undefined } ) ;
2021-08-19 03:04:40 +02:00
}
2022-10-31 22:58:04 +01:00
static safeEval ( expression : string ) : number | ` ${ number | boolean } ` {
const result = mathEvaluator . evaluate ( expression ) ;
if ( ! Number . isNumeric ( result ) ) {
throw new Error ( ` mathEvaluator.evaluate produced a non-numeric result from expression " ${ expression } " ` ) ;
}
return result as number | ` ${ number | boolean } ` ;
}
2022-08-25 03:31:30 +02:00
2022-11-03 21:41:44 +01:00
/ * *
* Apply the given effects to the gicen Actor or item .
* @param document The Actor or Item to which to apply the effects
* @param effetcs The effects to apply
* @param predicate Apply only changes that fullfill this predicate
* /
static applyEffetcs (
document : DS4Actor | DS4Item ,
effetcs : DS4ActiveEffect [ ] ,
predicate : ( change : EffectChangeData ) = > boolean = ( ) = > true ,
) : void {
const overrides : Record < string , unknown > = { } ;
// Organize non-disabled and -surpressed effects by their application priority
const changesWithEffect = effetcs . flatMap ( ( e ) = > e . getFactoredChangesWithEffect ( predicate ) ) ;
changesWithEffect . sort ( ( a , b ) = > ( a . change . priority ? ? 0 ) - ( b . change . priority ? ? 0 ) ) ;
// Apply all changes
for ( const changeWithEffect of changesWithEffect ) {
const result = changeWithEffect . effect . apply ( document , changeWithEffect . change ) ;
if ( result !== null ) overrides [ changeWithEffect . change . key ] = result ;
}
// Expand the set of final overrides
document . overrides = foundry . utils . expandObject ( {
. . . foundry . utils . flattenObject ( document . overrides ) ,
. . . overrides ,
} ) ;
}
2022-08-25 03:31:30 +02:00
/ * *
* Get the array of changes for this effect , considering the { @link DS4ActiveEffect # factor } .
* @param predicate An optional predicate to filter which changes should be considered
* @returns The array of changes from this effect , considering the factor .
* /
2022-11-03 21:41:44 +01:00
protected getFactoredChangesWithEffect (
2022-08-25 03:31:30 +02:00
predicate : ( change : EffectChangeData ) = > boolean = ( ) = > true ,
) : EffectChangeDataWithEffect [ ] {
if ( this . data . disabled || this . isSurpressed ) {
return [ ] ;
}
return this . data . changes . filter ( predicate ) . flatMap ( ( change ) = > {
change . priority = change . priority ? ? change . mode * 10 ;
return Array < EffectChangeDataWithEffect > ( this . factor ) . fill ( { effect : this , change } ) ;
} ) ;
}
2021-07-13 02:03:10 +02:00
}
2022-08-25 03:31:30 +02:00
2022-11-03 21:41:44 +01:00
type EffectChangeData = foundry . data . ActiveEffectData [ "changes" ] [ number ] ;
type EffectChangeDataWithEffect = { effect : DS4ActiveEffect ; change : EffectChangeData } ;