diff --git a/src/lang/en.json b/src/lang/en.json index a36e0276..6af53757 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -174,6 +174,9 @@ "DL.AvailabilityR": "Rare", "DL.AvailabilityU": "Uncommon", "DL.BackgroundTitle": "Background", + "DL.BecomeFrightenedForRound" : "Your become frightened {round} round.", + "DL.BecomeFrightenedForRounds" : "Your become frightened {round} rounds.", + "DL.BecomeStunned" : "Your become stunned for {round} round.", "DL.ButtonEdit": "Edit", "DL.ButtonView": "View", "DL.ChallengeRequestRollText": "Request Challenge Roll", @@ -383,6 +386,9 @@ "DL.FeatureDelete": "Delete feature", "DL.FeatureEdit": "Edit feature", "DL.FeaturesTitle": "Features", + "DL.FourOrMoreCreatures": "Four or more creatures at once", + "DL.FourOrMoreCreaturesInSight": "Sees four or more creatures at once", + "DL.GainedInsanity" : "You gain {insanity} Insanity.", "DL.GMTools": "GM Tools", "DL.GMnote": "GM Notes", "DL.GMnoteEdit": "Edit GM Note", @@ -426,6 +432,7 @@ "DL.LanguagesTitle": "Languages", "DL.LanguagesWrite": "Write", "DL.LanguagesWriteShort": "W", + "DL.LookOutCreatures": "Look out creatures!", "DL.MacroApplyAfflicationTitle": "Apply Afflictions", "DL.MacroCancel": "Cancel", "DL.MacroChallengeChoose": "Choose Attribute:", diff --git a/src/module/active-effects/effects.js b/src/module/active-effects/effects.js index d269ca5e..3ebee57a 100644 --- a/src/module/active-effects/effects.js +++ b/src/module/active-effects/effects.js @@ -120,17 +120,17 @@ export function prepareActiveEffectCategories(effects, showCreateButtons = false if (e.duration.turns > 0) { const rr = calcEffectRemainingRounds(e, game.combat.round) const rt = calcEffectRemainingTurn(e, game.combat.turn) - const sr = `${rr} ${Math.abs(rr) > 1 ? i18n("COMBAT.Rounds") : i18n("COMBAT.Round")}` - const st = `${rt} ${Math.abs(rt) > 1 ? i18n("COMBAT.Turns") : i18n("COMBAT.Turn")}` + const sr = Math.abs(rr) > 1 ? `${rr} ${i18n("COMBAT.DURATION.ROUNDS.many")}` : `${rr} ${i18n("COMBAT.DURATION.ROUNDS.one")}` + const st = Math.abs(rt) > 1 ? `${rt} ${i18n("COMBAT.DURATION.TURNS.many")}` : `${rt} ${i18n("COMBAT.DURATION.TURNS.one")}` e.dlRemaining = sr + ' ' + st } else { const r = calcEffectRemainingRounds(e, game.combat.round) - e.dlRemaining = `${r} ${Math.abs(r) > 1 ? i18n("COMBAT.Rounds") : i18n("COMBAT.Round")}` + e.dlRemaining = Math.abs(r) > 1 ? `${r} ${i18n("COMBAT.DURATION.ROUNDS.many")}` : `${r} ${i18n("COMBAT.DURATION.ROUNDS.one")}` } } else { const r = calcEffectRemainingSeconds(e, game.time.worldTime) - e.dlRemaining = `${r} ${i18n("TIME.Seconds")}` + e.dlRemaining = Math.abs(r) > 1 ? `${r} ${i18n("TIME.Second.other")}` : `${r} ${i18n("TIME.Second.one")}` } } else { e.dlRemaining = e.duration.label diff --git a/src/module/actor/actor.js b/src/module/actor/actor.js index 7e3b0253..baa1b5f6 100644 --- a/src/module/actor/actor.js +++ b/src/module/actor/actor.js @@ -6,6 +6,7 @@ import {DLActiveEffects} from '../active-effects/item-effects' import {DLAfflictions} from '../active-effects/afflictions' import {capitalize, plusify} from '../utils/utils' import launchRollDialog from '../dialog/roll-dialog' +import launchFrightenedDialog from '../dialog/frightened-dialog' import { postAttackToChat, postAttributeToChat, @@ -14,7 +15,8 @@ import { postSpellToChat, postTalentToChat, postFortuneToChat, - postRestToChat + postRestToChat, + postPlainTextToChat } from '../chat/roll-messages' import {handleCreateAncestry, handleCreatePath, handleCreateRole, handleCreateRelic } from '../item/nested-objects' import {TokenManager} from '../pixi/token-manager' @@ -951,6 +953,137 @@ export class DemonlordActor extends Actor { /* -------------------------------------------- */ + async goingMad() { + const compendia = await game.packs.get('sdlc-1000.tables') + if (compendia) { + const compendiumRollTables = await compendia.getDocuments() + const tableMadness = compendiumRollTables.find(i => i.name === 'Madness') + if (tableMadness) await tableMadness.draw() + } + } + + async stunnedChallengeRoll() { + let actor = this + if (!actor.isImmuneToAffliction('stunned')) { + const attribute = actor.getAttribute('will') + let roll = await actor.rollAttributeChallenge(attribute, 0, 0) + const targetNumber = game.settings.get('demonlord', 'optionalRuleDieRollsMode') === 'b' ? 11 : 10 + if (roll._total < targetNumber) { + const stunnedEffect = CONFIG.statusEffects.find(e => e.id === 'stunned') + stunnedEffect['statuses'] = stunnedEffect.id + stunnedEffect.duration.rounds = 1 + await ActiveEffect.create(stunnedEffect, { + parent: actor, + }) + } + let durationRoll = new Roll(`1d1`) + await durationRoll.evaluate() + postPlainTextToChat(actor, 'stunned',durationRoll) + } else ui.notifications.warn(game.i18n.localize('DL.DialogWarningActorImmune')) + } + + async setInsanityValue(roll) { + let actor = this + let increment = roll._total + let newValue = + actor.system.characteristics.insanity.value + increment < actor.system.characteristics.insanity.max + ? actor.system.characteristics.insanity.value + increment + : actor.system.characteristics.insanity.max + const isFrightened = actor.effects.find(e => e.statuses?.has('frightened')) === undefined ? false : true + const isStunned = actor.effects.find(e => e.statuses?.has('stunned')) === undefined ? false : true + await actor.update({ 'system.characteristics.insanity.value': newValue }) + if (!isFrightened) { + const frightenedEffect = CONFIG.statusEffects.find(e => e.id === 'frightened') + frightenedEffect['statuses'] = frightenedEffect.id + frightenedEffect.duration.rounds = newValue + + if (!actor.isImmuneToAffliction('frightened')) { + await ActiveEffect.create(frightenedEffect, { + parent: actor, + }) + } else ui.notifications.warn(game.i18n.localize('DL.DialogWarningActorImmune')) + } else { + const frightenedEffect = actor.effects.find(e => e.statuses?.has('frightened')) + await frightenedEffect.update({ 'duration.rounds': newValue }) + if (!isStunned) await this.stunnedChallengeRoll() + } + } + + async rollFrightened() { + let actor = this + const isStunned = actor.effects.find(e => e.statuses?.has('stunned')) === undefined ? false : true + launchFrightenedDialog(async (dHtml, creatureTrait) => { + let fourOrMore = dHtml.currentTarget.querySelector("input[id='fourOrMore']").checked ? -1 : 0 + if (fourOrMore) { + let fourAndMoreEffect = new ActiveEffect({ + name: game.i18n.localize('DL.FourOrMoreCreaturesInSight'), + icon: 'icons/svg/terror.svg', + changes: [{ key: 'system.bonuses.challenge.boons.will', value: -1, mode: CONST.ACTIVE_EFFECT_MODES.ADD }], + flags: { demonlord: { specialDuration: 'NextChallengeRoll' } }, + }) + + await ActiveEffect.create(fourAndMoreEffect, { + parent: actor, + }) + } + + if (!isStunned) { + const attribute = actor.getAttribute('will') + let roll = await actor.rollAttributeChallenge(attribute, 0, 0) + const targetNumber = game.settings.get('demonlord', 'optionalRuleDieRollsMode') === 'b' ? 11 : 10 + if (roll._total < targetNumber) await actor.rollDurationAndSanity(creatureTrait) + } + // Stunned -> autofail + else await actor.rollDurationAndSanity(creatureTrait) + }) + } + + async rollDurationAndSanity(creatureTrait) { + const actor = this + const isHorrifying = creatureTrait === 'h' || creatureTrait === 'fh' ? true : false + const isFrightening = creatureTrait === 'f' ? true : false + const isFrightened = actor.effects.find(e => e.statuses?.has('frightened')) === undefined ? false : true + + if (isFrightening) { + if (!isFrightened) { + if (!actor.isImmuneToAffliction('frightened')) { + let durationRoll = actor.system.characteristics.insanity.value + ? new Roll(`1d3+${actor.system.characteristics.insanity.value}`) + : new Roll(`1d3`) + await durationRoll.evaluate() + const frightenedEffect = CONFIG.statusEffects.find(e => e.id === 'frightened') + frightenedEffect['statuses'] = frightenedEffect.id + frightenedEffect.duration.rounds = durationRoll.total + await ActiveEffect.create(frightenedEffect, { + parent: actor, + }) + postPlainTextToChat(actor,'frightenedForRounds', durationRoll) + } else ui.notifications.warn(game.i18n.localize('DL.DialogWarningActorImmune')) + } else + { + let durationRoll = new Roll(`1d1`) + await durationRoll.evaluate() + this.setInsanityValue(durationRoll) + postPlainTextToChat(actor,'frightenedForRounds', durationRoll) + } + } + + if (isHorrifying) { + if (!isFrightened) { + let durationRoll = new Roll(`1d3`) + await durationRoll.evaluate() + this.setInsanityValue(durationRoll) + } else { + let insanityIncreaseRoll = new Roll('1d3') + await insanityIncreaseRoll.evaluate() + postPlainTextToChat(actor, 'gainedInsanity', insanityIncreaseRoll) + this.setInsanityValue(insanityIncreaseRoll) + } + } + + if (actor.system.characteristics.insanity.value === actor.system.characteristics.insanity.max) await this.goingMad() + } + async createItemCreate(event) { event.preventDefault() diff --git a/src/module/actor/sheets/character-sheet.js b/src/module/actor/sheets/character-sheet.js index 2c653848..1634d3b1 100644 --- a/src/module/actor/sheets/character-sheet.js +++ b/src/module/actor/sheets/character-sheet.js @@ -12,6 +12,7 @@ export default class DLCharacterSheet extends DLBaseActorSheet { }, actions: { rollCorruption: this.onRollCorruption, + rollFrightened: this.onrollFrightened, editStatBar: this.onEditStatBar, editLanguages: this.onEditLanguages, //editReligion: this.onEditReligion // Unused? @@ -215,6 +216,10 @@ export default class DLCharacterSheet extends DLBaseActorSheet { return await this.actor.rollCorruption() } + static async onrollFrightened() { + return await this.actor.rollFrightened() + } + /** Edit HealthBar, Insanity and Corruption */ static async onEditStatBar() { const actor = this.actor diff --git a/src/module/chat/roll-messages.js b/src/module/chat/roll-messages.js index 6e0376d6..c0fda309 100644 --- a/src/module/chat/roll-messages.js +++ b/src/module/chat/roll-messages.js @@ -47,13 +47,13 @@ export function postAttackToChat(attacker, defender, item, attackRoll, attackAtt const attackAttributeImmune = attacker?.getAttribute(attackAttribute)?.immune const defenseAttributeImmune = defender?.getAttribute(defenseAttribute)?.immune - const voidRoll = attackAttributeImmune || defenseAttributeImmune const targetNumber = defenseAttribute === 'defense' ? defender?.system.characteristics.defense : defender?.getAttribute(defenseAttribute)?.value || '' + const voidRoll = attackAttributeImmune || defenseAttributeImmune || !targetNumber const plus20 = attackRoll?.total >= 20 && (targetNumber ? attackRoll?.total > targetNumber + (game.settings.get('demonlord', 'optionalRuleExceedsByFive') ? 5 : 4) : true) const didHit = voidRoll ? false : attackRoll?.total >= targetNumber @@ -66,7 +66,7 @@ export function postAttackToChat(attacker, defender, item, attackRoll, attackAtt diceTotal = '?' // resultText = '' } - const resultBoxClass = voidRoll ? 'FAILURE' : (resultText === '' ? '' : didHit ? 'SUCCESS' : 'FAILURE') + const resultBoxClass = voidRoll ? '' : (resultText === '' ? '' : didHit ? 'SUCCESS' : 'FAILURE') const defenseShow = game.settings.get('demonlord', 'attackShowDefense') const againstNumber = ((defender?.type === 'character' && defender?.isPC) || defenseShow) && targetNumber ? targetNumber : '?' @@ -165,7 +165,7 @@ export function postAttributeToChat(actor, attribute, challengeRoll, inputBoons, const templateData = { actor: actor, tokenId: actor.token ? actor.token.uuid : null, - item: {name: attribute?.toUpperCase()}, + item: {name: game.i18n.localize(CONFIG.DL.attributes[attribute]?.toUpperCase())}, diceData: formatDice(challengeRoll), data: {}, } @@ -216,7 +216,6 @@ export function postTalentToChat(actor, talent, attackRoll, target, inputBoons, const attackAttributeImmune = actor?.getAttribute(attackAttribute)?.immune const defenseAttributeImmune = target?.getAttribute(defenseAttribute)?.immune - const voidRoll = attackAttributeImmune || defenseAttributeImmune let usesText = '' if (parseInt(talentData?.uses?.value) >= 0 && parseInt(talentData?.uses?.max) > 0) { @@ -226,6 +225,7 @@ export function postTalentToChat(actor, talent, attackRoll, target, inputBoons, } const targetNumber = talentData?.action?.attack ? actor.getTargetNumber(talent) : '' + const voidRoll = attackAttributeImmune || defenseAttributeImmune || !targetNumber const plus20 = attackRoll?.total >= 20 && (targetNumber ? attackRoll?.total > targetNumber + (game.settings.get('demonlord', 'optionalRuleExceedsByFive') ? 5 : 4) : true) let resultText = @@ -241,7 +241,7 @@ export function postTalentToChat(actor, talent, attackRoll, target, inputBoons, diceTotal = '?' // resultText = '' } - const resultBoxClass = voidRoll ? 'FAILURE' : (resultText === '' ? '' : attackRoll?.total >= +targetNumber ? 'SUCCESS' : 'FAILURE') + const resultBoxClass = voidRoll ? '' : (resultText === '' ? '' : attackRoll?.total >= +targetNumber ? 'SUCCESS' : 'FAILURE') const defenseShow = game.settings.get('demonlord', 'attackShowDefense') const againstNumber = ((target?.actor?.type === 'character' && target?.actor?.isPC) || defenseShow) && targetNumber ? targetNumber : '?' @@ -338,7 +338,6 @@ export async function postSpellToChat(actor, spell, attackRoll, target, inputBoo const attackAttributeImmune = actor?.getAttribute(attackAttribute)?.immune const defenseAttributeImmune = target?.getAttribute(defenseAttribute)?.immune - const voidRoll = attackAttributeImmune || defenseAttributeImmune let uses = parseInt(spellData?.castings?.value) let usesMax = parseInt(spellData?.castings?.max) @@ -346,6 +345,7 @@ export async function postSpellToChat(actor, spell, attackRoll, target, inputBoo if (uses >= 0 && usesMax > 0) usesText = game.i18n.localize('DL.SpellCastingsUses') + ': ' + uses + ' / ' + usesMax const targetNumber = actor.getTargetNumber(spell) + const voidRoll = attackAttributeImmune || defenseAttributeImmune || !targetNumber const plus20 = attackRoll?.total >= 20 && (targetNumber ? attackRoll?.total > targetNumber + (game.settings.get('demonlord', 'optionalRuleExceedsByFive') ? 5 : 4) : true) let resultText = @@ -360,7 +360,7 @@ export async function postSpellToChat(actor, spell, attackRoll, target, inputBoo diceTotal = '?' // resultText = '' } - const resultBoxClass = voidRoll ? 'FAILURE' : (resultText === '' ? '' : attackRoll?.total >= +targetNumber ? 'SUCCESS' : 'FAILURE') + const resultBoxClass = voidRoll ? '' : (resultText === '' ? '' : attackRoll?.total >= +targetNumber ? 'SUCCESS' : 'FAILURE') const defenseShow = game.settings.get('demonlord', 'attackShowDefense') const againstNumber = ((target?.actor?.type === 'character' && target?.actor?.isPC) || defenseShow) && targetNumber ? targetNumber : '?' @@ -460,6 +460,37 @@ export async function postSpellToChat(actor, spell, attackRoll, target, inputBoo /* -------------------------------------------- */ +export function postPlainTextToChat(actor, text, roll) { + const templateData = { + actor: actor, + data: {}, + } + + const rollMode = game.settings.get('core', 'rollMode') + const chatData = getChatBaseData(actor, rollMode) + const data = templateData.data + data['actorInfo'] = buildActorInfo(actor) + data['text'] = text + switch (text) { + case 'frightenedForRounds': + data['text'] = Math.abs(roll._total === 1) ? game.i18n.format('DL.BecomeFrightenedForRound', {round: roll._total}) : + game.i18n.format('DL.BecomeFrightenedForRounds', {round: roll._total}) + break + case 'stunned': + data['text'] = game.i18n.format('DL.BecomeStunned', {round: roll._total}) + break + case 'gainedInsanity': + data['text'] = game.i18n.format('DL.GainedInsanity', {insanity: roll._total}) + break + } + if (roll) chatData.rolls = [roll] + const template = 'systems/demonlord/templates/chat/text.hbs' + foundry.applications.handlebars.renderTemplate(template, templateData).then(content => { + chatData.content = content + ChatMessage.create(chatData) + }) +} + export async function postCorruptionToChat(actor, corruptionRoll) { const templateData = { actor: actor, @@ -490,10 +521,8 @@ export async function postCorruptionToChat(actor, corruptionRoll) { chatData.content = await foundry.applications.handlebars.renderTemplate(template, templateData) chatData.sound = CONFIG.sounds.dice await ChatMessage.create(chatData) - - // Get mark of darkess if roll < corruption value if (corruptionRoll.total < actor.system.characteristics.corruption.value) { - const compendiumRollTables = await game.packs.get('demonlord.sotdl roll tabels').getDocuments() + const compendiumRollTables = await game.packs.get('demonlord.sotdl-roll-tables').getDocuments() const tableMarkOfDarkess = compendiumRollTables.find(i => i.name === 'Mark of Darkness') const result = await tableMarkOfDarkess.draw() let resultText = result.results[0].text diff --git a/src/module/dialog/frightened-dialog.js b/src/module/dialog/frightened-dialog.js new file mode 100644 index 00000000..c740c946 --- /dev/null +++ b/src/module/dialog/frightened-dialog.js @@ -0,0 +1,40 @@ +const { DialogV2 } = foundry.applications.api + +export default function launchFrightenedDialog(callback) { + const d = new DialogV2({ + window: { + title: game.i18n.localize('DL.LookOutCreatures'), + }, + options : { closeOnSubmit: true}, + position: { + width: 500, + }, + content: ` +