205 lines
9.1 KiB
JavaScript
205 lines
9.1 KiB
JavaScript
/**
|
||
* Extends the actor to process special things from L5R.
|
||
*/
|
||
export class CombatL5r5e extends Combat {
|
||
// game.combat.settings.resource = "fatigue.value"; // nope :/
|
||
// constructor(...args) {
|
||
// super(...args);
|
||
// console.log(args);
|
||
// }
|
||
|
||
/**
|
||
* Roll initiative for one or multiple Combatants within the Combat entity
|
||
* @param {string|string[]} ids A Combatant id or Array of ids for which to roll
|
||
* @param {string|null} [formula] A non-default initiative formula to roll. Otherwise, the system default is used.
|
||
* @param {boolean} [updateTurn] Update the Combat turn after adding new initiative scores to keep the turn on
|
||
* the same Combatant.
|
||
* @param {object} [messageOptions] Additional options with which to customize created Chat Messages
|
||
* @return {Promise<Combat>} A promise which resolves to the updated Combat entity once updates are complete.
|
||
*/
|
||
async rollInitiative(ids, { formula = null, updateTurn = true, messageOptions = {} } = {}) {
|
||
if (!Array.isArray(ids)) {
|
||
ids = [ids];
|
||
}
|
||
|
||
// Get global modifiers
|
||
const cfg = {
|
||
difficulty: game.settings.get("l5r5e", "initiative-difficulty-value"),
|
||
difficultyHidden: game.settings.get("l5r5e", "initiative-difficulty-hidden"),
|
||
};
|
||
|
||
// SkillId from DicePicker or global
|
||
const skillId = messageOptions.skillId
|
||
? messageOptions.skillId
|
||
: CONFIG.l5r5e.initiativeSkills[game.settings.get("l5r5e", "initiative-encounter")];
|
||
const skillCat = CONFIG.l5r5e.skills.get(skillId);
|
||
|
||
// Get score for each combatant
|
||
const networkActors = [];
|
||
const updatedCombatants = [];
|
||
for (const combatantId of ids) {
|
||
const combatant = game.combat.combatants.find((c) => c.id === combatantId);
|
||
if (!combatant || !combatant.actor || combatant.isDefeated) {
|
||
continue;
|
||
}
|
||
|
||
// Skip non character types (army)
|
||
if (!combatant.actor.isCharacterType) {
|
||
updatedCombatants.push({
|
||
_id: combatant.id,
|
||
initiative: 0,
|
||
});
|
||
continue;
|
||
}
|
||
|
||
// Prepared is a boolean or if null we get the info in the actor sheet
|
||
const isPc = combatant.actor.isCharacter;
|
||
const isPrepared = combatant.actor.isPrepared;
|
||
const actorSystem = combatant.actor.system;
|
||
|
||
// A character’s initiative value is based on their state of preparedness when the conflict began.
|
||
// If the character was ready for the conflict, their base initiative value is their focus attribute.
|
||
// If the character was unprepared (such as when surprised), their base initiative value is their vigilance attribute.
|
||
// Minion NPCs can generate initiative value without a check, using their focus or vigilance attribute
|
||
let initiative =
|
||
isPrepared === "true" ? actorSystem.focus : actorSystem.is_compromised ? 1 : actorSystem.vigilance;
|
||
|
||
// Roll only for PC and Adversary
|
||
if (isPc || combatant.actor.isAdversary) {
|
||
// DicePicker management
|
||
// formula is empty on the fist call (combat tab buttons)
|
||
if (!formula && !combatant.initiative) {
|
||
// if a player is currently active for this actor
|
||
const havePlayer = combatant.players.some((u) => u.active);
|
||
const isMyCharacter = combatant.players.some((u) => u._id === game.user.id);
|
||
|
||
if (game.user.isGM && havePlayer && !isMyCharacter) {
|
||
// Open the DP on player side
|
||
networkActors.push(combatant.actor);
|
||
continue;
|
||
} else if (isMyCharacter || (game.user.isGM && !havePlayer)) {
|
||
// Open the DP locally
|
||
new game.l5r5e.DicePickerDialog({
|
||
actor: combatant.actor,
|
||
skillId: skillId,
|
||
difficulty: cfg.difficulty,
|
||
difficultyHidden: cfg.difficultyHidden,
|
||
isInitiativeRoll: true,
|
||
}).render(true);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// Roll formula
|
||
const createFormula = [];
|
||
if (!formula) {
|
||
createFormula.push(`${actorSystem.rings[actorSystem.stance]}dr`);
|
||
const skillValue = isPc ? actorSystem.skills[skillCat][skillId] : actorSystem.skills[skillCat];
|
||
if (skillValue > 0) {
|
||
createFormula.push(`${skillValue}ds`);
|
||
}
|
||
}
|
||
|
||
let roll;
|
||
let rnkMessage;
|
||
const flavor =
|
||
game.i18n.localize("l5r5e.dice.chat.initiative_roll") +
|
||
" (" +
|
||
game.i18n.localize(`l5r5e.conflict.initiative.prepared_${isPrepared}`) +
|
||
")";
|
||
|
||
if (messageOptions.rnkRoll instanceof game.l5r5e.RollL5r5e && ids.length === 1) {
|
||
// Specific RnK
|
||
roll = messageOptions.rnkRoll;
|
||
rnkMessage = await roll.toMessage({ flavor }, { rollMode: messageOptions.rollMode || null });
|
||
} else {
|
||
// Regular
|
||
roll = new game.l5r5e.RollL5r5e(formula ?? createFormula.join("+"));
|
||
roll.item = messageOptions.item;
|
||
roll.actor = combatant.actor;
|
||
roll.l5r5e.isInitiativeRoll = true;
|
||
roll.l5r5e.stance = actorSystem.stance;
|
||
roll.l5r5e.skillId = skillId;
|
||
roll.l5r5e.skillCatId = skillCat;
|
||
roll.l5r5e.difficulty =
|
||
messageOptions.difficulty !== undefined ? messageOptions.difficulty : cfg.difficulty;
|
||
roll.l5r5e.difficultyHidden =
|
||
messageOptions.difficultyHidden !== undefined
|
||
? messageOptions.difficultyHidden
|
||
: cfg.difficultyHidden;
|
||
roll.l5r5e.voidPointUsed = !!messageOptions.useVoidPoint;
|
||
roll.l5r5e.skillAssistance = messageOptions.skillAssistance || 0;
|
||
|
||
await roll.roll();
|
||
rnkMessage = await roll.toMessage({ flavor });
|
||
}
|
||
|
||
// Ugly but work... I need the new message
|
||
if (ids.length === 1) {
|
||
messageOptions.rnkMessage = rnkMessage;
|
||
}
|
||
|
||
// if the character succeeded on their Initiative check, they add 1 to their base initiative value,
|
||
// plus an additional amount equal to their bonus successes.
|
||
const successes = roll.l5r5e.summary.totalSuccess;
|
||
if (successes >= roll.l5r5e.difficulty) {
|
||
initiative = initiative + 1 + Math.max(successes - roll.l5r5e.difficulty, 0);
|
||
}
|
||
}
|
||
|
||
updatedCombatants.push({
|
||
_id: combatant.id,
|
||
initiative: initiative,
|
||
});
|
||
}
|
||
|
||
// If any network actor users to notify
|
||
if (!foundry.utils.isEmpty(networkActors)) {
|
||
game.l5r5e.sockets.openDicePicker({
|
||
actors: networkActors,
|
||
dpOptions: {
|
||
skillId: skillId,
|
||
difficulty: cfg.difficulty,
|
||
difficultyHidden: cfg.difficultyHidden,
|
||
isInitiativeRoll: true,
|
||
},
|
||
});
|
||
}
|
||
|
||
// Update all combatants at once
|
||
await this.updateEmbeddedDocuments("Combatant", updatedCombatants);
|
||
return this;
|
||
}
|
||
|
||
/**
|
||
* Define how the array of Combatants is sorted in the displayed list of the tracker.
|
||
* This method can be overridden by a system or module which needs to display combatants in an alternative order.
|
||
* By default, sort by initiative, falling back to name
|
||
* @private
|
||
*/
|
||
_sortCombatants(a, b) {
|
||
// if tie : sort by honor, less honorable first
|
||
if (a.initiative === b.initiative) {
|
||
// skip if no actor or if armies
|
||
if (!a.actor || !b.actor || a.actor.isArmy || b.actor.isArmy) {
|
||
return 0;
|
||
}
|
||
|
||
// if tie again : Character > Adversary > Minion
|
||
if (a.actor.system.social.honor === b.actor.system.social.honor) {
|
||
return CombatL5r5e._getWeightByActorType(a.actor) - CombatL5r5e._getWeightByActorType(b.actor);
|
||
}
|
||
return a.actor.system.social.honor - b.actor.system.social.honor;
|
||
}
|
||
return b.initiative - a.initiative;
|
||
}
|
||
|
||
/**
|
||
* Basic weight system for sorting Character > Adversary > Minion
|
||
* @private
|
||
*/
|
||
static _getWeightByActorType(actor) {
|
||
return actor.type === "npc" ? (actor.type === "minion" ? 3 : 2) : 1;
|
||
}
|
||
}
|