diff --git a/lang/en.json b/lang/en.json index fb6d5d7a54..1a5ac50209 100644 --- a/lang/en.json +++ b/lang/en.json @@ -3836,6 +3836,13 @@ "Name": "Default Skills", "Hint": "The default skills that appear on NPC sheets regardless of level of proficiency." }, + "INITIATIVESCORE": { + "Name": "Initiative Score", + "Hint": "Use a creature's initiative score (10 + bonus) rather than rolling for initiative.", + "All": "Use Score for Everyone", + "None": "Always Roll for Initiative", + "NPCs": "Use Score for GM NPCs" + }, "LEVELING": { "Name": "Leveling Mode", "Hint": "Determine how the players gain new levels.", diff --git a/module/documents/actor/actor.mjs b/module/documents/actor/actor.mjs index 3d25a83eed..51a56e1d87 100644 --- a/module/documents/actor/actor.mjs +++ b/module/documents/actor/actor.mjs @@ -606,6 +606,7 @@ export default class Actor5e extends SystemDocumentMixin(Actor) { init.total = init.mod + initBonus + abilityBonus + globalCheckBonus + (flags.initiativeAlert && isLegacy ? 5 : 0) + (Number.isNumeric(init.prof.term) ? init.prof.flat : 0); + init.score = 10 + init.total; } /* -------------------------------------------- */ @@ -2024,8 +2025,9 @@ export default class Actor5e extends SystemDocumentMixin(Actor) { /** * @typedef {D20RollOptions} InitiativeRollOptions - * @param {D20Roll.ADV_MODE} [advantageMode] A specific advantage mode to apply. - * @property {string} [flavor] Special flavor text to apply to the created message. + * @property {D20Roll.ADV_MODE} [advantageMode] A specific advantage mode to apply. + * @property {number} [fixed] Fixed initiative value to use rather than rolling. + * @property {string} [flavor] Special flavor text to apply to the created message. */ /** @@ -2038,8 +2040,15 @@ export default class Actor5e extends SystemDocumentMixin(Actor) { if ( this._cachedInitiativeRoll ) return this._cachedInitiativeRoll.clone(); const config = this.getInitiativeRollConfig(options); if ( !config ) return null; - const formula = ["1d20"].concat(config.parts).join(" + "); - return new CONFIG.Dice.D20Roll(formula, config.data, config.options); + + // Create a normal D20 roll + if ( config.options?.fixed === undefined ) { + const formula = ["1d20"].concat(config.parts).join(" + "); + return new CONFIG.Dice.D20Roll(formula, config.data, config.options); + } + + // Create a basic roll with the fixed score + return new CONFIG.Dice.BasicRoll(String(config.options.fixed), config.data, config.options); } /* -------------------------------------------- */ @@ -2075,7 +2084,12 @@ export default class Actor5e extends SystemDocumentMixin(Actor) { const tiebreaker = game.settings.get("dnd5e", "initiativeDexTiebreaker"); if ( tiebreaker && Number.isNumeric(ability?.value) ) parts.push(String(ability.value / 100)); + // Fixed initiative score + const scoreMode = game.settings.get("dnd5e", "initiativeScore"); + const useScore = (scoreMode === "all") || ((scoreMode === "npcs") && game.user.isGM && (this.type === "npc")); + options = foundry.utils.mergeObject({ + fixed: useScore ? init.score : undefined, flavor: options.flavor ?? game.i18n.localize("DND5E.Initiative"), halflingLucky: flags.halflingLucky ?? false, maximum: init.roll.max, @@ -2101,7 +2115,7 @@ export default class Actor5e extends SystemDocumentMixin(Actor) { /** * Roll initiative for this Actor with a dialog that provides an opportunity to elect advantage or other bonuses. * @param {Partial} [rollOptions={}] Options forwarded to the Actor#getInitiativeRoll method. - * @returns {Promise} A promise which resolves once initiative has been rolled for the Actor + * @returns {Promise} A promise which resolves once initiative has been rolled for the Actor. */ async rollInitiativeDialog(rollOptions={}) { const config = { @@ -2112,13 +2126,22 @@ export default class Actor5e extends SystemDocumentMixin(Actor) { subject: this }; if ( !config.rolls[0] ) return; - const dialog = { options: { title: game.i18n.localize("DND5E.InitiativeRoll") } }; - const message = { rollMode: game.settings.get("core", "rollMode") }; - const rolls = await CONFIG.Dice.D20Roll.build(config, dialog, message); - if ( !rolls.length ) return; - // Temporarily cache the configured roll and use it to roll initiative for the Actor - this._cachedInitiativeRoll = rolls[0]; + // Display the roll configuration dialog + if ( config.rolls[0].options?.fixed === undefined ) { + const dialog = { options: { title: game.i18n.localize("DND5E.InitiativeRoll") } }; + const message = { rollMode: game.settings.get("core", "rollMode") }; + const rolls = await CONFIG.Dice.D20Roll.build(config, dialog, message); + if ( !rolls.length ) return; + this._cachedInitiativeRoll = rolls[0]; + } + + // Just create a basic roll with the fixed score + else { + const { data, options } = config.rolls[0]; + this._cachedInitiativeRoll = new CONFIG.Dice.BasicRoll(String(options.fixed), data, options); + } + await this.rollInitiative({ createCombatants: true }); } diff --git a/module/settings.mjs b/module/settings.mjs index 0bc380cf4a..d6f0f765d4 100644 --- a/module/settings.mjs +++ b/module/settings.mjs @@ -210,6 +210,21 @@ export function registerSystemSettings() { type: Boolean }); + // Use initiative scores for NPCs + game.settings.register("dnd5e", "initiativeScore", { + name: "SETTINGS.DND5E.INITIATIVESCORE.Name", + hint: "SETTINGS.DND5E.INITIATIVESCORE.Hint", + scope: "world", + config: true, + default: "none", + type: String, + choices: { + none: "SETTINGS.DND5E.INITIATIVESCORE.None", + npcs: "SETTINGS.DND5E.INITIATIVESCORE.NPCs", + all: "SETTINGS.DND5E.INITIATIVESCORE.All" + } + }); + // Record Currency Weight game.settings.register("dnd5e", "currencyWeight", { name: "SETTINGS.5eCurWtN",