foundryvtt / foundryvtt

Public issue tracking and documentation for Foundry Virtual Tabletop - software connecting RPG gamers in a shared multiplayer environment with an intuitive interface and powerful API.
https://foundryvtt.com/
202 stars 10 forks source link

Implement V1 of the ActiveEffect system, adding an EmbeddedEntity in the Actor model which can modify the actor data #1657

Closed aaclayton closed 3 years ago

aaclayton commented 4 years ago

Overview and Requirements

Active Effect Data Model

/**
 * The Active Effect embedded document represents temporary status conditions.
 * These effects can be present on an Actor or an Item.
 * @extends {EmbeddedDocument}
 */
class ActiveEffect extends EmbeddedDocument {
  static get schema() {
    const schema = super.schema;

    // Visual effect icon
    schema.icon = {
      type: String,
      required: false,
      validate: Validators.hasImageExtension,
      validationError: "An ActiveEffect icon must have an image file extension."
    };

    // Human-readable label
    schema.label = {
      type: String,
      required: false,
      default: ""
    };

    // The origin entity which created the effect
    schema.origin = {
      type: String, // uuid
      required: false
    };

    // A data object which contains the effect contents
    schema.data = {
      type: [EffectData],
      required: true,
      default: []
    };

    // The effect duration
    schema.duration = {
      type: EffectDuration,
      required: true,
      default: {}
    };
    return schema;
  }
}

/**
 * An inner object schema which defines the structure of the Active Effect duration attribute.
 * @extends {AbstractBaseDocument}
 */
class EffectDuration extends AbstractBaseDocument {
  static get schema() {
    return {
      startTime: {
        type: Number,
        required: true,
        default: Date.now
      },
      ms: {
        type: Number,
        required: false
      },
      combat: {
        type: String,
        required: false
      },
      rounds: {
        type: Number,
        required: false
      },
      turns: {
        type: Number,
        required: false
      },
      startRound: {
        type: Number,
        required: false
      },
      startTurn: {
        type: Number,
        required: false
      }
    }
  }
}

/**
 * The data structure of a single individual change in the ActiveEffect
 * @extends {AbstractBaseDocument}
 */
class EffectData extends AbstractBaseDocument {
  static get schema() {
    return {
      key: {
        type: String,
        required: true
      },
      value: {
        required: true
      },
      mode: {
        type: Number,
        required: true,
        default: ACTIVE_EFFECT_MODES.ADD,
        validate: m => Validators.valueInArray(m, Object.values(ACTIVE_EFFECT_MODES))
      }
    }
  }
}

Example 1 - Bless

const effectData = {
  label: "Bless",
  icon: "icons/svg/angel.svg",
  source: actor.uuid,
  data: [
    {key: "bonuses.abilities.check", value: "+1d4"},
    {key: "bonuses.abilities.save", value: "+1d4"},
    {key: "bonuses.mwak.attack", value: "+1d4"},
    {key: "bonuses.rwak.attack", value: "+1d4"},
    {key: "bonuses.msak.attack", value: "+1d4"},
    {key: "bonuses.rsak.attack", value: "+1d4"}
  ],
  duration: {
    combat: game.combat._id,
    rounds: 10
  }
}

Example 2 - Headband of Intellect

const effectData = {
  label: "Headband of Intellect",
  icon: "icons/svg/mind.svg",
  data: [
    {key: "abilities.int.value", value: 19, mode: ACTIVE_EFFECT_MODES.OVERRIDE}
  ]
}

Active Effect CRUD API

const actor = game.actors.getName("Jeronimo");
const effect = a.createEmbeddedEntity("ActiveEffect", effectData);
a.updateEmbeddedEntity("ActiveEffect", {_id: effect._id, label: "Super Bless!"});
a.deleteEmbeddedEntity("ActiveEffect", effect._id);

Actor Data Preparation Workflow

Due to the changes introduced by the ActiveEffect embedded entity there are a few important changes for the way that Actor data is prepared in 0.7.1 and beyond.

Firstly, we need to keep track of the original un-modified Actor data which is stored as Actor#_data. This un-modified data is then prepared and modified by active effects to become Actor#data which is used as normal. When updates occur, those updates are diff'ed against the un-modified _data object.

The prepareData() method of the Actor has been re-structured to follow this workflow:

  /** @override */
  prepareData() {
    this.data = duplicate(this._data);
    if (!this.data.img) this.data.img = CONST.DEFAULT_TOKEN;
    if ( !this.data.name ) this.data.name = "New " + this.entity;
    this.prepareBaseData();
    this.prepareEmbeddedEntities();
    this.applyItemEffects();
    this.applyActiveEffects();
    this.prepareDerivedData();
  }

This includes new methods:

Backwards Compatible Shim

If you want to release a system version which can work for both 0.7.1 and 0.6.5, put the following prepareData() method in your system Actor class and structure the workflow accordingly.

  /**
   * @override
   * @deprecated after 0.7.1
   */
  prepareData() {
    this.data = duplicate(this._data);
    if (!this.data.img) this.data.img = CONST.DEFAULT_TOKEN;
    if ( !this.data.name ) this.data.name = "New " + this.entity;
    this.prepareBaseData();
    this.prepareEmbeddedEntities();
    this.applyItemEffects();
    if ( !isNewerVersion("0.7.1", game.data.version) ) this.applyActiveEffects();
    this.prepareDerivedData();
  }

Active Effect Application

Each ActiveEffect instance is applied to the Actor which owns it during the applyActiveEffects() function. Any effects which use ADD, MULTIPLY, or OVERRIDE modes are automatically applied (unless the system overrides the logic) while effects which apply a CUSTOM mode are funneled to the _applyCustomEffect() method. This method can either (1) be overriden by the system implementation OR (2) be hooked into by modules which can handle special custom effect types.


Remaining Work and Open Questions Below

How to handle Owned Item effects?

How to deal with active effects on owned items. There isn't a precedent for embedded entities having embedded entities. It is possible, but arguably/possibly not desirable. This will be a common use case though so it needs a solution.

Option 1: Item don't contain full Active Effects, only the basis for them

In this case an ActiveEffect on an item would be different than one on an actor - it would only have the structural data of the mode, data, and duration which would be expanded to become a true active effect when applied to a character.

Option 2: Item effects don't exist

There are just Actor effects and those effects denote an OwnedItem#uuid as their source.

Option 3: ??? something better ???


Key Events and Workflows

aaclayton commented 3 years ago

Originally in GitLab by @DavidHtx1

mentioned in issue tposney/dae#78

aaclayton commented 3 years ago

Originally in GitLab by @Forien

Ok, so I must have misunderstood this bit:

duration: {
    combat: game.combat._id,
    rounds: 10
  }

But I think I get it now. Can't wait!

aaclayton commented 3 years ago

Thanks for the feedback @Forien - it's not too late because this will be ongoing work throughout 0.7.x.

Owned Items are not in scope for this set of changes, that will be part 2 coming in #3394 as part of 0.7.2, so I won't comment on that much except to say that's also what I envision happening with Owned Items, they provide the structure that becomes an Active Effect applied to some actor (either yourself or someone else).

Regarding duration - I think the core implementation of Active Effects has to deal with core concepts only. Providing some basic structure for effect duration that is linked to combat rounds is enough of a universal need to do it in core. Core Combat has the concept of encounters (_ids), turns, and rounds - so core active effects can also think about those 3 things.

Beyond that - technicalities about whether the effect expires at the start of the round, the end of the round, the middle of the round, what happens when it ends, etc... those are all implementation details left to the game system and modules to work out.

aaclayton commented 3 years ago

Originally in GitLab by @Forien

I think it's a little late for that, but reading description I have some thoughts:

  1. Owned Items Effects – I think that they should only contain basis for actuall effects that systems/modules can then call to be activated on owner/target. Some items must be used, some cause permanent change as long as are carried, some need to be equipped, it all should be determined by systems/modules, so core solution I think should be to contain data and implement API calls to activate effects on owner/target. And I think items should contain those information when for example they are passed around. So Option 1 I think.
  2. Duration – depending on system (and sometimes within the same system), some effects last until the end of current round, start of the next round, or until the start of character's next turn. But it might be start of turn for Effect's "Bearer" or Effect's "Source". Systems are quite different here. I have no solution in mind, just think it's worth thinking about. Maybe some default per-system setting that can be thrown into CONFIG and then per-effect duration mode flag?
aaclayton commented 3 years ago

marked this issue as related to #3394

aaclayton commented 3 years ago

marked this issue as related to #3393

aaclayton commented 3 years ago

marked this issue as related to #3392

aaclayton commented 3 years ago

marked this issue as related to #3391

aaclayton commented 3 years ago

marked this issue as related to #3390

aaclayton commented 3 years ago

marked this issue as related to #3389

aaclayton commented 3 years ago

marked this issue as related to #3388

aaclayton commented 3 years ago

Originally in GitLab by @tposney

Another thought. It would be nice for modules to be able to register a callback (sort of thing) for active effects so that modules can participate in the prepareData actions or the in the item effect activation activities. I started to type this but then realised I am struggling to find an excellent example of why calculations would be changed other than through active effects.

aaclayton commented 3 years ago

Originally in GitLab by @tposney

Combat Turn Duration for Effects Not sure what round/turn would mean for effect duration. If it is the 3rd turn of the 2nd round that could be a problem since actors can be added to the tracker, or the order changed with re-rolls, so a simple number would not work.

aaclayton commented 3 years ago

Originally in GitLab by @tposney

Okay, I struggled with wokring out what level of comments to make, so apologies if these are all at the wrong lebel.

Actor Effects:

Would you consider having a target id as well as a source id? That would facilitate dealing with losing conentration/dieing to cancel effects as all the effects you created are stored on actor. I would assume the effect is duplicated on the target actor.

What about systems like simple world building, where attributes don't exist until they are defined in the game world. What sort of error checking/validation could/should be applied to the effects data. DE knows all of the fields that are valid to modify (breaks for simple world building), from the template data and the actor.getRollData() return values.

It turns out that being able to reference a macro as an active effect is really useful/powerful. Storing the macro within the active effect is also really useful. It should be possible to reference the source and destination context and item within the macro. The macro would be called twice, once when activated and once when cancelled. This can solve lots of the edge cases for effects.

Owned Item Effects.
Are owned item effects effects that modify an actor's data or the owned item's data? Do you want to be able to support effects like flame blade (+ to damage for a specific item)?

An interpretation is that an effect is a set of transformations applied in the scope of a source and destination context, where the source context is one set of actor data (actor.getRollData()?) and the destination context is the Actor.data. For actor effects the source and desination contexts are always trivially implied. For item effects the contexts are only defined at the time of application to an actor and can vary depending on how the item effect is applied, to self or a target. And so don't contain the actor reference, application of the effect creates an actor effect, but need to deal with removing the effect because the item is no longer present.

Under what conditions are owned item effects triggered? It seems that the equipped/attuned, equipped, always active model seems to cover the required cases with the exception of on use. If there is a more general source/target for owned item effects then on use could be an extra activation condition, applyOnRoll.

UI
What is displayed on the character sheet? Modified or pre-effects values and what does editing the field mean (presumably changes the base data)
Updates that don't do a diff can be a problem if the modified data is displayed.

Key Events and Workflow

Random Aside. I noticed you closed the "saving throws should use save bonus rather than save mod", how do you have an active effect which is +3 to dex saving throws?

aaclayton commented 4 years ago

mentioned in issue dnd5e#502

aaclayton commented 4 years ago

Originally in GitLab by @tposney

Is there a projected version for this?

aaclayton commented 4 years ago

Originally in GitLab by @tposney

Would love to retire dynamic items. Please remember effects like melee/ranged/spell attack rolls which currently have no actor level data, only per item. Also, adding resistances/immunities should tie into damage application from the chat log.