Initial
This commit is contained in:
237
resources/app/client/data/documents/combatant.js
Normal file
237
resources/app/client/data/documents/combatant.js
Normal file
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* The client-side Combatant document which extends the common BaseCombatant model.
|
||||
*
|
||||
* @extends foundry.documents.BaseCombatant
|
||||
* @mixes ClientDocumentMixin
|
||||
*
|
||||
* @see {@link Combat} The Combat document which contains Combatant embedded documents
|
||||
* @see {@link CombatantConfig} The application which configures a Combatant.
|
||||
*/
|
||||
class Combatant extends ClientDocumentMixin(foundry.documents.BaseCombatant) {
|
||||
|
||||
/**
|
||||
* The token video source image (if any)
|
||||
* @type {string|null}
|
||||
* @internal
|
||||
*/
|
||||
_videoSrc = null;
|
||||
|
||||
/**
|
||||
* The current value of the special tracked resource which pertains to this Combatant
|
||||
* @type {object|null}
|
||||
*/
|
||||
resource = null;
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Properties */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A convenience alias of Combatant#parent which is more semantically intuitive
|
||||
* @type {Combat|null}
|
||||
*/
|
||||
get combat() {
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* This is treated as a non-player combatant if it has no associated actor and no player users who can control it
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isNPC() {
|
||||
return !this.actor || !this.hasPlayerOwner;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Eschew `ClientDocument`'s redirection to `Combat#permission` in favor of special ownership determination.
|
||||
* @override
|
||||
*/
|
||||
get permission() {
|
||||
if ( game.user.isGM ) return CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER;
|
||||
return this.getUserLevel(game.user);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
get visible() {
|
||||
return this.isOwner || !this.hidden;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A reference to the Actor document which this Combatant represents, if any
|
||||
* @type {Actor|null}
|
||||
*/
|
||||
get actor() {
|
||||
if ( this.token ) return this.token.actor;
|
||||
return game.actors.get(this.actorId) || null;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A reference to the Token document which this Combatant represents, if any
|
||||
* @type {TokenDocument|null}
|
||||
*/
|
||||
get token() {
|
||||
const scene = this.sceneId ? game.scenes.get(this.sceneId) : this.parent?.scene;
|
||||
return scene?.tokens.get(this.tokenId) || null;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* An array of non-Gamemaster Users who have ownership of this Combatant.
|
||||
* @type {User[]}
|
||||
*/
|
||||
get players() {
|
||||
return game.users.filter(u => !u.isGM && this.testUserPermission(u, "OWNER"));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Has this combatant been marked as defeated?
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isDefeated() {
|
||||
return this.defeated || !!this.actor?.statuses.has(CONFIG.specialStatusEffects.DEFEATED);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Methods */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
testUserPermission(user, permission, {exact=false}={}) {
|
||||
if ( user.isGM ) return true;
|
||||
return this.actor?.canUserModify(user, "update") || false;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get a Roll object which represents the initiative roll for this Combatant.
|
||||
* @param {string} formula An explicit Roll formula to use for the combatant.
|
||||
* @returns {Roll} The unevaluated Roll instance to use for the combatant.
|
||||
*/
|
||||
getInitiativeRoll(formula) {
|
||||
formula = formula || this._getInitiativeFormula();
|
||||
const rollData = this.actor?.getRollData() || {};
|
||||
return Roll.create(formula, rollData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Roll initiative for this particular combatant.
|
||||
* @param {string} [formula] A dice formula which overrides the default for this Combatant.
|
||||
* @returns {Promise<Combatant>} The updated Combatant.
|
||||
*/
|
||||
async rollInitiative(formula) {
|
||||
const roll = this.getInitiativeRoll(formula);
|
||||
await roll.evaluate();
|
||||
return this.update({initiative: roll.total});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
prepareDerivedData() {
|
||||
// Check for video source and save it if present
|
||||
this._videoSrc = VideoHelper.hasVideoExtension(this.token?.texture.src) ? this.token.texture.src : null;
|
||||
|
||||
// Assign image for combatant (undefined if the token src image is a video)
|
||||
this.img ||= (this._videoSrc ? undefined : (this.token?.texture.src || this.actor?.img));
|
||||
this.name ||= this.token?.name || this.actor?.name || game.i18n.localize("COMBAT.UnknownCombatant");
|
||||
|
||||
this.updateResource();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update the value of the tracked resource for this Combatant.
|
||||
* @returns {null|object}
|
||||
*/
|
||||
updateResource() {
|
||||
if ( !this.actor || !this.combat ) return this.resource = null;
|
||||
return this.resource = foundry.utils.getProperty(this.actor.system, this.parent.settings.resource) || null;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Acquire the default dice formula which should be used to roll initiative for this combatant.
|
||||
* Modules or systems could choose to override or extend this to accommodate special situations.
|
||||
* @returns {string} The initiative formula to use for this combatant.
|
||||
* @protected
|
||||
*/
|
||||
_getInitiativeFormula() {
|
||||
return String(CONFIG.Combat.initiative.formula || game.system.initiative);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Database Lifecycle Events */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static async _preCreateOperation(documents, operation, _user) {
|
||||
const combatant = operation.parent?.combatant;
|
||||
if ( !combatant ) return;
|
||||
const combat = operation.parent.clone();
|
||||
combat.updateSource({combatants: documents.map(d => d.toObject())});
|
||||
combat.setupTurns();
|
||||
operation.combatTurn = Math.max(combat.turns.findIndex(t => t.id === combatant.id), 0);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static async _preUpdateOperation(_documents, operation, _user) {
|
||||
const combatant = operation.parent?.combatant;
|
||||
if ( !combatant ) return;
|
||||
const combat = operation.parent.clone();
|
||||
combat.updateSource({combatants: operation.updates});
|
||||
combat.setupTurns();
|
||||
if ( operation.turnEvents !== false ) {
|
||||
operation.combatTurn = Math.max(combat.turns.findIndex(t => t.id === combatant.id), 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static async _preDeleteOperation(_documents, operation, _user) {
|
||||
const combatant = operation.parent?.combatant;
|
||||
if ( !combatant ) return;
|
||||
|
||||
// Simulate new turns
|
||||
const combat = operation.parent.clone();
|
||||
for ( const id of operation.ids ) combat.combatants.delete(id);
|
||||
combat.setupTurns();
|
||||
|
||||
// If the current combatant was deleted
|
||||
if ( operation.ids.includes(combatant?.id) ) {
|
||||
const {prevSurvivor, nextSurvivor} = operation.parent.turns.reduce((obj, t, i) => {
|
||||
let valid = !operation.ids.includes(t.id);
|
||||
if ( combat.settings.skipDefeated ) valid &&= !t.isDefeated;
|
||||
if ( !valid ) return obj;
|
||||
if ( i < this.turn ) obj.prevSurvivor = t;
|
||||
if ( !obj.nextSurvivor && (i >= this.turn) ) obj.nextSurvivor = t;
|
||||
return obj;
|
||||
}, {});
|
||||
const survivor = nextSurvivor || prevSurvivor;
|
||||
if ( survivor ) operation.combatTurn = combat.turns.findIndex(t => t.id === survivor.id);
|
||||
}
|
||||
|
||||
// Otherwise maintain the same combatant turn
|
||||
else operation.combatTurn = Math.max(combat.turns.findIndex(t => t.id === combatant.id), 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user