diff --git a/src/assets/ui/other-svg/dice-twenty-faces-twenty.svg b/src/assets/ui/other-svg/dice-twenty-faces-twenty.svg new file mode 100644 index 00000000..a9850857 --- /dev/null +++ b/src/assets/ui/other-svg/dice-twenty-faces-twenty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/lang/en.json b/src/lang/en.json index 157fad7e..26607709 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -370,13 +370,15 @@ "DL.DialogWarningActorsNotSelected": "Actor(s) not selected", "DL.DialogWarningActorsNotTargeted": "Actor(s) not targeted", "DL.DialogWarningAfflictionFromEffect": "Affliction is applied from effect. Remove the effect to remove the affliction.", - "DL.DialogWarningAlreadyMadeWILLImmune": "You've already made a Will challenge roll against {creature} and you are immune to its {trait} trait.", + "DL.DialogWarningAlreadyMadeWILL": "You've already made a Will challenge roll against {creature}.", "DL.DialogWarningAlreadyMadeWILLFrightened": "You've already made a Will challenge roll against {creature} and you are already frightened.", "DL.DialogWarningBlindedChallengeFailer": "You're blinded and perception challenge rolls result in failure.", + "DL.DialogWarningCannotIncreaseDifficulty": "Cannot increase difficulty by {steps} steps, because difficulty would be greater than maximum allowed {maxAllowed}.", "DL.DialogWarningCreatureArmor": "You can't add armor to a creature. Change the Defense value manually.", "DL.DialogWarningDazedFailer": "You're dazed and cannot use actions.", "DL.DialogWarningDefenselessFailer": "You're defenseless and cannot use actions, and challenge rolls using attributes result in failure.", "DL.DialogWarningInvalidTarget": "No frightening or horrifying creature targeted or you've already performed a Will challenge roll.", + "DL.DialogWarningInvalidCreatureDifficulty": "Invalid creature difficulty: {difficulty}.", "DL.DialogWarningStunnedFailer": "You're stunned and cannot use actions or move, and all your challenge rolls result in failure.", "DL.DialogWarningSurprisedFailer": "You're surprised and cannot use actions or move, and all your challenge rolls result in failure.", "DL.DialogWarningTargetNotHorrifying": "{target} not horrifying.", @@ -405,6 +407,7 @@ "DL.EndRoundDelete": "Delete End of the Round", "DL.ExtraEffect": "Extra Effect", "DL.ExtraEffect20": "Extra Effect 20+", + "DL.FearRollAgainst": "Fear Roll against {creature}", "DL.FeatureAdd": "Add feature", "DL.FeatureDelete": "Delete feature", "DL.FeatureEdit": "Edit feature", @@ -422,14 +425,14 @@ "DL.GMnoteSave": "Save GM Note", "DL.HalfSpeed": "Half Speed", "DL.ImmuneAffliction": "Immune to Affliction", - "DL.ImmuneToTarget": "Immune to {creature}", "DL.ImmuneToHorrifyingOneMinute": "You become immune to {creature}'s horrifying trait for 1 minute.", "DL.ImmuneAttribute": "Immune Attribute", "DL.ImmuneCharacteristic": "Immune Characteristic", - "DL.ImmunityLastsUntilTheEndOfTheRound": "Immunity to target horrifying trait lasts until the end of the round.", - "DL.ImmunityLastsUntilTheEndOfNextRound": "Immunity to target horrifying trait lasts until the end of the next round.", - "DL.ImmunityLastsRounds": "Immunity to target horrifying trait lasts {rounds} more rounds.", - "DL.ImmunityLastsSeconds": "Immunity to target horrifying trait lasts {seconds} seconds.", + "DL.FearRoll": "Fear Roll", + "DL.FearRollLastsUntilTheEndOfTheRound": "Fear Roll against target lasts until the end of the round.", + "DL.FearRollLastsUntilTheEndOfNextRound": "Fear Roll against target lasts until the end of the next round.", + "DL.FearRollLastsRounds": "Fear Roll against target lasts {rounds} more rounds.", + "DL.FearRollLastsSeconds": "Fear Roll against target lasts {seconds} seconds.", "DL.AfflictionImmunityEffectName": "{affliction} Immunity", "DL.IsDarkMagic": "Is Dark Magic", "DL.IsFrightening": "Is Frightening", @@ -467,7 +470,6 @@ "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:", @@ -643,6 +645,8 @@ "DL.SettingDSNLabel": "Dice So Nice! Settings", "DL.SettingDamageScrollText": "Enable Scroll Text for Damage/Health", "DL.SettingDamageScrollTextHint": "When checked, scrolling text appears above tokens about damage/heal value.", + "DL.SettingEnableItemMacro": "Enable Item Macro", + "DL.SettingEnableItemMacroHint": "When checked item macros will be executed.", "DL.SettingEnableQuickDraw": "Enable one-click draw for rolltables.", "DL.SettingEnableQuickDrawHint": "Draw directly form sidebar, compendia or sheets (right-click).", "DL.SettingFinesseAutoSelect": "Finesse weapon attack attribute auto select", diff --git a/src/lang/pt-BR.json b/src/lang/pt-BR.json index 71a1dd15..b4987926 100644 --- a/src/lang/pt-BR.json +++ b/src/lang/pt-BR.json @@ -265,10 +265,10 @@ "DL.CreatureEditMagic": "Editar Feitiço", "DL.CreatureEditSpecialActions": "Editar Ação Especial", "DL.CreatureEditSpecialAttacks": "Editar Ataque Especial", - "DL.CreatureFrightening": "Amedron.", + "DL.CreatureFrightening": "Amedrontador", "DL.CreatureFrighteningDescription": "Uma criatura sem o traço amedrontador ou horripilante deve fazer uma rolagem de desafio de Vontade ao ver pela primeira vez uma ou mais criaturas com esse traço. A rolagem é feita com 1 revés se houver quatro ou mais criaturas amedrontadoras ao mesmo tempo. Se fracassar, a criatura fica amedrontada por uma quantidade de rodadas igual a 1d3 + seu total de Insanidade ou ganha 1 ponto de Insanidade se já estiver amedrontada. Após fazer essa rolagem, seja qual for o resultado, a criatura não poderá mais ser afetada por esse traço da criatura, ou criaturas, que viu até completar um descanso.", "DL.CreatureHealth": "Vida", - "DL.CreatureHorrifying": "Horrip.", + "DL.CreatureHorrifying": "Horripilante", "DL.CreatureHorrifyingDescription": "Uma criatura sem o traço horripilante deve fazer uma rolagem de desafio de Vontade ao ver pela primeira vez uma ou mais criaturas com esse traço. A rolagem é feita com 1 revés se houver quatro ou mais criaturas horripilantes ao mesmo tempo. Se fracassar, a criatura ganha 1 ponto de Insanidade, ou 1d3 se já estiver amedrontada. Após fazer essa rolagem, seja qual for o resultado, a criatura não poderá mais ser afetada por esse traço da criatura, ou criaturas, que viu até completar um descanso. Independentemente do resultado da rolagem de desafio, criaturas que não possuem os traços amedrontadora ou horripilante fazem rolagens de ataque contra criaturas horripilantes com 1 revés.", "DL.CreatureInsanity": "Insanidade", "DL.CreatureIntellect": "Intelecto", @@ -370,7 +370,7 @@ "DL.DialogWarningActorsNotSelected": "Ator(es) não selecionado(s)", "DL.DialogWarningActorsNotTargeted": "Ator(es) não é(são) alvo(s)", "DL.DialogWarningAfflictionFromEffect": "A aflição é aplicada por efeito. Remova o efeito para remover a aflição.", - "DL.DialogWarningAlreadyMadeWILLImmune": "Você já fez uma rolagem de Vontade contra {creature} e está imune ao seu traço {trait}.", + "DL.DialogWarningAlreadyMadeWILL": "Você já fez uma rolagem de Vontade contra {creature}.", "DL.DialogWarningAlreadyMadeWILLFrightened": "Você já fez uma rolagem de Vontade contra {creature} e já está amedrontado.", "DL.DialogWarningBlindedChallengeFailer": "Você está cego e rolagens de desafio de Percepção resultam em fracasso.", "DL.DialogWarningCreatureArmor": "Você não pode adicionar armadura a uma criatura. Altere o valor de Defesa manualmente.", @@ -405,6 +405,12 @@ "DL.EndRoundDelete": "Excluir Final da Rodada", "DL.ExtraEffect": "Efeito Extra", "DL.ExtraEffect20": "Efeito Extra 20+", + "DL.FearRoll": "Rolagem de Medo", + "DL.FearRollAgainst": "Rolagem de Medo contra {creature}", + "DL.FearRollLastsUntilTheEndOfTheRound": "Rolagem de Medo contra o alvo irá durar até o fim da rodada.", + "DL.FearRollLastsUntilTheEndOfNextRound": "Rolagem de Medo contra o alvo irá durar até o final da próxima rodada.", + "DL.FearRollLastsRounds": "Rolagem de Medo contra o alvo irá durar mais {rounds} rodadas.", + "DL.FearRollLastsSeconds": "Rolagem de Medo contra o alvo irá durar {seconds} segundos.", "DL.FeatureAdd": "Adicionar Propriedade", "DL.FeatureDelete": "Excluir Propriedade", "DL.FeatureEdit": "Editar Propriedade", @@ -422,14 +428,9 @@ "DL.GMnoteSave": "Salvar Nota do MJ", "DL.HalfSpeed": "Metade da Velocidade", "DL.ImmuneAffliction": "Imune a aflição", - "DL.ImmuneToTarget": "Imune a {creature}", "DL.ImmuneToHorrifyingOneMinute": "Você se torna imune ao traço horripilante de {creature} por 1 minuto.", "DL.ImmuneAttribute": "Atributo Imune", "DL.ImmuneCharacteristic": "Característica Imune", - "DL.ImmunityLastsUntilTheEndOfTheRound": "Imunidade ao traço horripilante do alvo irá durar até o final da rodada.", - "DL.ImmunityLastsUntilTheEndOfNextRound": "Imunidade ao traço horripilante do alvo irá durar até o final da próxima rodada.", - "DL.ImmunityLastsRounds": "Imunidade ao traço horripilante do alvo irá durar mais {rounds} rodadas.", - "DL.ImmunityLastsSeconds": "Imunidade ao traço horripilante do alvo irá durar mais {seconds} segundos.", "DL.AfflictionImmunityEffectName": "Imunidade a {affliction}", "DL.IsDarkMagic": "É Magia das Trevas", "DL.IsFrightening": "É Amedrontador", @@ -467,7 +468,6 @@ "DL.LanguagesTitle": "Idiomas", "DL.LanguagesWrite": "Escrever", "DL.LanguagesWriteShort": "E", - "DL.LookOutCreatures": "Cuidado, criaturas!", "DL.MacroApplyAfflicationTitle": "Aplicar Aflições", "DL.MacroCancel": "Cancelar", "DL.MacroChallengeChoose": "Selecionar Atributo:", diff --git a/src/module/active-effects/item-effects.js b/src/module/active-effects/item-effects.js index 00a81f00..f50eaa8a 100644 --- a/src/module/active-effects/item-effects.js +++ b/src/module/active-effects/item-effects.js @@ -87,7 +87,7 @@ export class DLActiveEffects { effectDataList = DLActiveEffects.generateEffectDataFromArmor(doc, actor) break case 'creaturerole': - effectDataList = DLActiveEffects.generateEffectDataFromRole(doc) + effectDataList = DLActiveEffects.generateEffectDataFromRole(doc, actor, operation) break default: return await Promise.resolve(0) @@ -302,9 +302,26 @@ export class DLActiveEffects { /* -------------------------------------------- */ - static generateEffectDataFromRole(item) { + static bumpDifficulty(actor, steps, operation) { + const difficultyIndex = (operation === 'update') ? CONFIG.DL.difficultyScale.indexOf(actor.system.difficultyBase) : CONFIG.DL.difficultyScale.indexOf(actor.system.difficulty) + if (difficultyIndex === -1) { + ui.notifications.warn(game.i18n.format('DL.DialogWarningInvalidCreatureDifficulty', {difficulty: actor.system.difficulty})) + return null + } + + const calculatedDifficulty = CONFIG.DL.difficultyScale[difficultyIndex + parseInt(steps)] + if (calculatedDifficulty === undefined) { + ui.notifications.warn(game.i18n.format('DL.DialogWarningCannotIncreaseDifficulty', {steps: steps, maxAllowed: CONFIG.DL.difficultyScale[CONFIG.DL.difficultyScale.length-1]})) + return null + } + + return (operation === 'update') ? (calculatedDifficulty - actor.system.difficultyBase) : (calculatedDifficulty - actor.system.difficulty) + } + + static generateEffectDataFromRole(item, actor, operation) { const priority = 5 const data = item.system + const calculatedDifficulty = DLActiveEffects.bumpDifficulty(actor, data.characteristics.difficulty, operation) const effectData = { name: item.name, @@ -344,7 +361,7 @@ export class DLActiveEffects { // addEffect('system.characteristics.corruption.value', data.characteristics.corruption.value, priority), // addEffect('system.characteristics.insanity.value', data.characteristics.insanity.value, priority), - addEffect('system.difficulty', data.characteristics.difficulty, priority), + addEffect('system.difficulty', calculatedDifficulty, priority), overrideEffect('system.characteristics.size', data.characteristics.size, priority), overrideEffect('system.frightening', data.frightening, priority, true), overrideEffect('system.horrifying', data.horrifying, priority, true), diff --git a/src/module/actor/actor.js b/src/module/actor/actor.js index c667663d..ff00843f 100644 --- a/src/module/actor/actor.js +++ b/src/module/actor/actor.js @@ -475,9 +475,9 @@ export class DemonlordActor extends Actor { return result } - isImmuneToTarget(target) { + isFearRollCompleted(target) { let actor = this - const immuneArray = actor.appliedEffects.filter(x => x.name === game.i18n.format('DL.ImmuneToTarget', { + const immuneArray = actor.appliedEffects.filter(x => x.name === game.i18n.format('DL.FearRollAgainst', { creature: target.name })) let result = false @@ -503,17 +503,19 @@ getTargetAttackBane(target) { const attacker = this if (!target) return 0 if (!game.settings.get('demonlord', 'horrifyingBane') || attacker.isImmuneToAffliction('frightened')) return 0 - const optionalRuleBaneValue = game.settings.get('demonlord', 'optionalRuleBaneValue') ? 1 : 2 const ignoreLevelDependentBane = game.settings.get('demonlord', 'optionalRuleLevelDependentBane') && ((attacker.system?.level >= 3 && attacker.system?.level <= 6 && target?.system?.difficulty <= 25) || (attacker.system?.level >= 7 && target?.system?.difficulty <= 50)) ? false : true let baneValue = game.settings.get('demonlord', 'optionalRuleTraitMode2025') - ? (ignoreLevelDependentBane && !attacker.system.horrifying && !attacker.system.frightening && (target?.system.frightening || target?.system.horrifying) && !attacker.isImmuneToTarget(target) && 1) || 0 - : (ignoreLevelDependentBane && !attacker.system.horrifying && !attacker.system.frightening && target?.system.horrifying && !attacker.isImmuneToTarget(target) && 1) || 0 + ? (ignoreLevelDependentBane && !attacker.system.horrifying && !attacker.system.frightening && (target?.system.frightening || target?.system.horrifying) && 1) || 0 + : (ignoreLevelDependentBane && !attacker.system.horrifying && !attacker.system.frightening && target?.system.horrifying && 1) || 0 // Adjust bane if source of affliction can be seen, actor already has 1 (frightened), we need to add the difference - if (attacker.isFrightenedFrom(target)) baneValue += optionalRuleBaneValue + // 0 - no bane + // 1 - creature is horrifying, creature is frightening (only 2025 trtait mode) + // 2 - creature firhtened + if (attacker.isFrightenedFrom(target)) baneValue += 2 return baneValue } @@ -527,11 +529,13 @@ getTargetAttackBane(target) { const attacker = this const defender = token ? token.actor : null let defenderToken = [] + const itemMacroEnabled = game.settings.get('demonlord', 'enableItemMacro') if (token) defenderToken.push(token) // Get attacker attribute and defender attribute name let attackAttribute = item.system.action?.attack?.toLowerCase() const defenseAttribute = item.system.action?.against?.toLowerCase() + if (itemMacroEnabled) await item.executeDLMacro({},{pass : 'preRollAttack', attackAttribute : attackAttribute, defenseAttribute: defenseAttribute, targetActorUuid: defender?.uuid}) if (game.settings.get('demonlord', 'finesseAutoSelect') && attackAttribute === '' && item.system.properties?.toLowerCase().includes('finesse')) { if (this.system.attributes.strength.value > this.system.attributes.agility.value) attackAttribute = 'strength' @@ -598,6 +602,12 @@ getTargetAttackBane(target) { hitTargets: hitTarget, attackRoll: attackRoll }) + + if (itemMacroEnabled) { + const successfullHit = (defender && attackRoll?.total >= targetNumber) ? true : false + const plus20 = attackRoll?.total >= 20 && (targetNumber ? attackRoll?.total > targetNumber + (game.settings.get('demonlord', 'optionalRuleExceedsByFive') ? 5 : 4) : true) + await item.executeDLMacro({},{pass : 'postRollAttack', attackRoll : attackRoll, targetNumber: targetNumber, successfullHit: successfullHit, plus20: plus20, targetActorUuid: defender?.uuid}) + } } /** @@ -772,15 +782,18 @@ getTargetAttackBane(target) { const talentData = talent.system const targets = tokenManager.targets const target = targets[0] + const itemMacroEnabled = game.settings.get('demonlord', 'enableItemMacro') let attackRoll = null if (!talentData?.action?.attack) { + if (itemMacroEnabled) await talent.executeDLMacro({},{pass : 'preRollTalent', targetActorUuid: target?.actor.uuid}) await this.activateTalent(talent, true) + if (itemMacroEnabled) await talent.executeDLMacro({},{pass : 'postRollTalent', targetActorUuid: target?.actor.uuid}) } else { await this.activateTalent(talent, Boolean(talentData.action?.damageActive)) const attackAttribute = talentData.action.attack.toLowerCase() - const defenseAttribute = talentData.action?.attack?.toLowerCase() + const defenseAttribute = talentData.action?.against?.toLowerCase() const attacker = this const modifiers = [ @@ -804,7 +817,7 @@ getTargetAttackBane(target) { this.getTargetAttackBane(target.actor)) const boonsReroll = parseInt(this.system.bonuses.rerollBoon1Dice) - + if (itemMacroEnabled) await talent.executeDLMacro({},{pass : 'preRollTalent', attackAttribute: attackAttribute, defenseAttribute: defenseAttribute, targetActorUuid: target?.actor.uuid}) attackRoll = new Roll(this.rollFormula(modifiers, boons, boonsReroll), this.system) await attackRoll.evaluate() @@ -813,6 +826,25 @@ getTargetAttackBane(target) { if (specialDuration === 'NextD20Roll' || specialDuration === 'NextAttackRoll') await effect?.delete() } + if (itemMacroEnabled) { + let successfullHit = false + let targetNumber + if (targets.length) { + const defender = target.actor + targetNumber = + defenseAttribute === 'defense' + ? defender?.system.characteristics.defense + : defender?.system.attributes[defenseAttribute]?.value || '' + successfullHit = defender && attackRoll?.total >= targetNumber ? true : false + } + const plus20 = + attackRoll?.total >= 20 && + (targetNumber + ? attackRoll?.total > targetNumber + (game.settings.get('demonlord', 'optionalRuleExceedsByFive') ? 5 : 4) + : true) + + await talent.executeDLMacro({},{pass : 'postRollTalent', attackRoll: attackRoll, targetNumber: targetNumber, successfullHit: successfullHit, plus20: plus20, targetActorUuid: target?.actor.uuid}) + } } Hooks.call('DL.UseTalent', { @@ -865,8 +897,11 @@ getTargetAttackBane(target) { const spellData = spell.system const attackAttribute = spellData?.action?.attack?.toLowerCase() const defenseAttribute = spellData?.action?.against?.toLowerCase() + const itemMacroEnabled = game.settings.get('demonlord', 'enableItemMacro') let attackRoll + const targetActorUuid = (target.length) ? target[0].actor.uuid : null + if (itemMacroEnabled) await spell.executeDLMacro({},{pass : 'preRollSpell', attackAttribute: attackAttribute, defenseAttribute: defenseAttribute, targetActorUuid: targetActorUuid}) if (attackAttribute) { const attacker = this @@ -934,6 +969,26 @@ getTargetAttackBane(target) { concentrate['statuses'] = [concentrate.id] ActiveEffect.create(concentrate, {parent: this}) } + + if (itemMacroEnabled) { + let successfullHit = false + let targetNumber + if (target.length) { + const defender = target[0].actor + targetNumber = + defenseAttribute === 'defense' + ? defender?.system.characteristics.defense + : defender?.system.attributes[defenseAttribute]?.value || '' + successfullHit = defender && attackRoll?.total >= targetNumber ? true : false + } + const plus20 = + attackRoll?.total >= 20 && + (targetNumber + ? attackRoll?.total > targetNumber + (game.settings.get('demonlord', 'optionalRuleExceedsByFive') ? 5 : 4) + : true) + await spell.executeDLMacro( {}, { pass: 'postRollSpell', attackRoll: attackRoll, successfullHit: successfullHit, plus20: plus20, targetActorUuid: targetActorUuid, }, ) + } + return attackRoll } @@ -986,13 +1041,16 @@ getTargetAttackBane(target) { const targets = tokenManager.targets const target = targets[0] let attackRoll = null + const itemMacroEnabled = game.settings.get('demonlord', 'enableItemMacro') if (!itemData?.action?.attack) { + if (itemMacroEnabled) await item.executeDLMacro({},{pass : 'preRollItem', targetActorUuid: target?.actor.uuid}) postItemToChat(this, item, null, null, null) + if (itemMacroEnabled) await item.executeDLMacro({},{pass : 'postRollItem', targetActorUuid: target?.actor.uuid}) return } else { const attackAttribute = itemData.action.attack.toLowerCase() - const defenseAttribute = itemData.action?.attack?.toLowerCase() + const defenseAttribute = itemData.action?.against?.toLowerCase() const attacker = this const modifiers = [ @@ -1017,6 +1075,7 @@ getTargetAttackBane(target) { const boonsReroll = parseInt(this.system.bonuses.rerollBoon1Dice) + if (itemMacroEnabled) await item.executeDLMacro({},{pass : 'preRollItem', attackAttribute : attackAttribute, defenseAttribute: defenseAttribute, targetActorUuid: target?.actor.uuid}) attackRoll = new Roll(this.rollFormula(modifiers, boons, boonsReroll), this.system) await attackRoll.evaluate() @@ -1024,6 +1083,16 @@ getTargetAttackBane(target) { const specialDuration = foundry.utils.getProperty(effect, `flags.${game.system.id}.specialDuration`) if (specialDuration === 'NextD20Roll' || specialDuration === 'NextAttackRoll') await effect?.delete() } + if (itemMacroEnabled) { + const defender = target?.actor + const targetNumber = + defenseAttribute === 'defense' + ? defender?.system.characteristics.defense + : defender?.system.attributes[defenseAttribute]?.value || '' + const successfullHit = (target && attackRoll?.total >= targetNumber) ? true : false + const plus20 = attackRoll?.total >= 20 && (targetNumber ? attackRoll?.total > targetNumber + (game.settings.get('demonlord', 'optionalRuleExceedsByFive') ? 5 : 4) : true) + await item.executeDLMacro({},{pass : 'preRollItem', attackRoll : attackRoll, targetNumber: targetNumber, successfullHit: successfullHit, plus20: plus20, targetActorUuid: target?.actor.uuid}) + } } postItemToChat(this, item, attackRoll, target?.actor, parseInt(inputBoons) || 0) return attackRoll @@ -1090,7 +1159,8 @@ getTargetAttackBane(target) { } else ui.notifications.warn(game.i18n.localize('DL.DialogWarningActorImmuneFrightened')) } else { const frightenedEffect = actor.effects.find(e => e.statuses?.has('frightened')) - await frightenedEffect.update({ 'duration.rounds': newValue }) + // Only update effect duration if lasts longer than the current one + if ((frightenedEffect.duration.startTime + frightenedEffect.duration.rounds * 10) < (game.time.worldTime + newValue * 10)) await frightenedEffect.update({ 'duration.rounds': newValue }) if (!isStunned) await stunnedChallengeRoll() } if (actor.system.characteristics.insanity.value === actor.system.characteristics.insanity.max) await this.goingMad() @@ -1208,18 +1278,10 @@ getTargetAttackBane(target) { }), ) - const traitType = - targets[0].actor.system.frightening && targets[0].actor.system.horrifying ? - game.i18n.localize('DL.CreatureHorrifying').toLowerCase() : - targets[0].actor.system.frightening ? - game.i18n.localize('DL.CreatureFrightening').toLowerCase() : - game.i18n.localize('DL.CreatureHorrifying').toLowerCase() - - if (this.isImmuneToTarget(targets[0].actor)) + if (this.isFearRollCompleted(targets[0].actor)) return ui.notifications.warn( - game.i18n.format('DL.DialogWarningAlreadyMadeWILLImmune', { - creature: targets[0].actor.name, - trait: traitType + game.i18n.format('DL.DialogWarningAlreadyMadeWILL', { + creature: targets[0].actor.name }), ) } @@ -1227,7 +1289,7 @@ getTargetAttackBane(target) { const validTargetArray = targets.filter( target => (target.actor.system.horrifying || target.actor.system.frightening) && - !actor.isImmuneToTarget(target.actor) && + !actor.isFearRollCompleted(target.actor) && !ignoreTarget(target.actor), ) @@ -1239,9 +1301,9 @@ getTargetAttackBane(target) { for (const target of validTargetArray) { content += `• ${target.actor.name}
` - if (target.actor.system.horrifying && !actor.isImmuneToTarget(target.actor) && !ignoreTarget(target.actor)) + if (target.actor.system.horrifying && !actor.isFearRollCompleted(target.actor) && !ignoreTarget(target.actor)) isHorrifying = true - if (target.actor.system.frightening && !actor.isImmuneToTarget(target.actor) && !ignoreTarget(target.actor)) + if (target.actor.system.frightening && !actor.isFearRollCompleted(target.actor) && !ignoreTarget(target.actor)) isFrightening = true } @@ -1256,7 +1318,7 @@ getTargetAttackBane(target) { const question = validTargetArray.length === 1 ? game.i18n.localize('DL.DialogDoYouSeeThisCreatureFirstTime') : game.i18n.localize('DL.DialogDoYouSeeTheseCreaturesFirstTime') const isLineOfSight = await foundry.applications.api.DialogV2.confirm({ window: { - title: game.i18n.localize('DL.LookOutCreatures'), + title: game.i18n.localize('DL.FearRoll'), }, content: question + content, position: { @@ -1403,10 +1465,10 @@ getTargetAttackBane(target) { } const imuneToFrightenedEffect = new ActiveEffect({ - name: game.i18n.format('DL.ImmuneToTarget', { + name: game.i18n.format('DL.FearRollAgainst', { creature: target.actor.name }), - icon: 'systems/demonlord/assets/icons/effects/immune.svg', + icon: 'systems/demonlord/assets/ui/other-svg/dice-twenty-faces-twenty.svg', duration: { rounds: 1, }, @@ -1416,9 +1478,6 @@ getTargetAttackBane(target) { specialDuration: 'RestComplete', }, }, - description: game.i18n.format('DL.ImmuneToHorrifyingOneMinute', { - creature: target.actor.name - }), origin: target.actor.uuid }) @@ -1437,9 +1496,9 @@ getTargetAttackBane(target) { target: targets[0].actor.name }), ) - if (this.isImmuneToTarget(targets[0].actor)) + if (this.isFearRollCompleted(targets[0].actor)) return ui.notifications.warn( - game.i18n.format('DL.DialogWarningAlreadyMadeWILLImmune', { + game.i18n.format('DL.DialogWarningAlreadyMadeWILL', { creature: targets[0].actor.name, trait: game.i18n.localize('DL.CreatureHorrifying').toLowerCase() }), @@ -1459,7 +1518,7 @@ getTargetAttackBane(target) { !target.actor.system.horrifying || ignoreTarget(target.actor) || actor.isFrightenedFrom(target.actor) || - actor.isImmuneToTarget(target.actor) + actor.isFearRollCompleted(target.actor) ), ) if (validTargetArray.length === 0) return ui.notifications.warn(game.i18n.localize('DL.DialogWarningInvalidTarget')) @@ -1472,7 +1531,7 @@ getTargetAttackBane(target) { const question = validTargetArray.length === 1 ? game.i18n.localize('DL.DialogDoYouStartYourTurnWithLOSCreature') : game.i18n.localize('DL.DialogDoYouStartYourTurnWithLOSCreatures') const isLineOfSight = await foundry.applications.api.DialogV2.confirm({ window: { - title: game.i18n.localize('DL.LookOutCreatures'), + title: game.i18n.localize('DL.FearRoll'), }, content: question + content, position: { @@ -1544,10 +1603,10 @@ getTargetAttackBane(target) { if (roll.total >= targetNumber) { const imuneToFrightenedEffect = new ActiveEffect({ - name: game.i18n.format('DL.ImmuneToTarget', { + name: game.i18n.format('DL.FearRollAgainst', { creature: target.actor.name }), - icon: 'systems/demonlord/assets/icons/effects/immune.svg', + icon: 'systems/demonlord/assets/ui/other-svg/dice-twenty-faces-twenty.svg', duration: { rounds: 6, }, diff --git a/src/module/actor/sheets/base-actor-sheet.js b/src/module/actor/sheets/base-actor-sheet.js index 0ae3136f..2812f155 100644 --- a/src/module/actor/sheets/base-actor-sheet.js +++ b/src/module/actor/sheets/base-actor-sheet.js @@ -134,6 +134,7 @@ export default class DLBaseActorSheet extends HandlebarsApplicationMixin(ActorSh context.hideTurnMode = game.settings.get('demonlord', 'optionalRuleInitiativeMode') === 's' ? false : true context.hideFortune = game.settings.get('demonlord', 'fortuneHide') ? true : false context.isTraitMode2025 = game.settings.get('demonlord', 'optionalRuleTraitMode2025') + context.disableTurnSwitch = (game.combat?.turn === null) || game.user.isGM ? false : true //context.tabs = this._getTabs(options.parts) context.tabs = this._prepareTabs('primary') diff --git a/src/module/chat/effect-messages.js b/src/module/chat/effect-messages.js index 7df032b9..c19cafc6 100644 --- a/src/module/chat/effect-messages.js +++ b/src/module/chat/effect-messages.js @@ -12,10 +12,11 @@ import { capitalize } from '../utils/utils' * @returns {Map} * @private */ -function _remapEffects(effects) { +function _remapEffects(effects, removeFrightenedEffect = false) { let m = new Map() // Active Auras module support effects = game.modules.get('ActiveAuras')?.active ? effects.filter(e => !e.flags?.ActiveAuras || foundry.utils.getProperty(e, `flags.ActiveAuras.isAura`) === undefined) : effects + effects = removeFrightenedEffect ? effects.filter(e => e.name != game.i18n.localize('DL.frightened')) : effects effects.forEach(effect => effect.changes.forEach(change => { const obj = { @@ -90,13 +91,16 @@ const changeListToMsgDefender = (m, keys, title, anonymize, f = plusify) => { * @returns {*} */ export function buildAttackEffectsMessage(attacker, defender, item, attackAttribute, defenseAttribute, inputBoons, plus20, inputModifier) { + const applyBane = attacker.getTargetAttackBane(defender) + const baneValue = applyBane <= 1 ? applyBane : 3 + // We remove Frightened Effect from ChatCard, because source is already frightened from target and later we add the correct effect name + const removeFrightenedEffect = baneValue === 3 && (defender?.system.frightening || defender?.system.horrifying) ? true : false const attackerEffects = Array.from(attacker.allApplicableEffects()).filter(effect => !effect.disabled) - let m = _remapEffects(attackerEffects) + let m = _remapEffects(attackerEffects, removeFrightenedEffect) const defenderEffects = defender ? Array.from(defender.allApplicableEffects()).filter(effect => !effect.disabled) : [] let d = _remapEffects(defenderEffects) let defenderBoonsArray = [`system.bonuses.defense.boons.${defenseAttribute}`,"system.bonuses.defense.boons.all"] - const applyHorrifyingBane = attacker.getTargetAttackBane(defender) let otherBoons = '' let modifiers = '' let inputBoonsMsg = inputBoons ? _toMsg(game.i18n.localize('DL.DialogInput'), plusify(inputBoons)) : '' @@ -154,27 +158,45 @@ export function buildAttackEffectsMessage(attacker, defender, item, attackAttrib let attributeText = 'DL.Attribute' + capitalize(attackAttribute) let attributeModMsg = attributeMod ? _toMsg(`${game.i18n.localize(attributeText)}`, plusify(attributeMod)) : '' - let revealHorrifyingBane = game.settings.get('demonlord', 'optionalRuleRevealHorrifyingBane') - let creatureType - - if (!game.settings.get('demonlord', 'optionalRuleTraitMode2025')) - creatureType = game.i18n.localize('DL.CreatureHorrifying') - else - creatureType = - defender?.system.frightening && defender?.system.horrifying - ? game.i18n.localize('DL.CreatureHorrifying') - : defender?.system.frightening - ? game.i18n.localize('DL.CreatureFrightening') - : defender?.system.horrifying - ? game.i18n.localize('DL.CreatureHorrifying') - : '' - - const horrifyingText = applyHorrifyingBane > 1 ? game.i18n.localize('DL.CanSeeSoureOfAffliction') : `${game.i18n.localize(creatureType)} [${game.i18n.localize('DL.ActionTarget')}]` - let horrifyingHTMLPlayer = revealHorrifyingBane - ? '
' + _toMsg(horrifyingText, applyHorrifyingBane*-1) + '
' - : '
' + _toMsg(`${game.i18n.localize('DL.OtherUnknown')} [${game.i18n.localize('DL.ActionTarget')}]`, applyHorrifyingBane*-1) + '
' + let creatureType = '' + const isFrightening = defender?.system.frightening + const isHorrifying = defender?.system.horrifying + const isTraitMode2025 = game.settings.get('demonlord', 'optionalRuleTraitMode2025') + + if ((isFrightening || isHorrifying) && isTraitMode2025) { + creatureType = isHorrifying ? game.i18n.localize('DL.CreatureHorrifying') : game.i18n.localize('DL.CreatureFrightening') + } else if (isFrightening && isHorrifying) { + creatureType = game.i18n.localize('DL.CreatureHorrifying') + '/' + game.i18n.localize('DL.CreatureFrightening') + } else if (isHorrifying) { + creatureType = game.i18n.localize('DL.CreatureHorrifying') + } - let horrifyingHTMLGM = '
' + _toMsg(horrifyingText, applyHorrifyingBane*-1) + '
' + const revealHorrifyingBane = game.settings.get('demonlord', 'optionalRuleRevealHorrifyingBane') + // We add the correct effect and its bane(s) to the chatcard. + const horrifyingTextGM = + baneValue > 1 + ? game.i18n.localize('DL.CanSeeSoureOfAffliction') + : `${game.i18n.localize(creatureType)} [${game.i18n.localize('DL.ActionTarget')}]` + let horrifyingHTMLGM + if (game.settings.get('demonlord', 'optionalRuleTraitMode2025')) + horrifyingHTMLGM = + baneValue > 1 + ? _toMsg(horrifyingTextGM, baneValue * -1) + + _toMsg(`${game.i18n.localize(creatureType)} [${game.i18n.localize('DL.ActionTarget')}]`, -1) + : _toMsg(horrifyingTextGM, baneValue * -1) + else if (creatureType !== game.i18n.localize('DL.CreatureFrightening')) + horrifyingHTMLGM = + baneValue > 1 + ? _toMsg(horrifyingTextGM, baneValue * -1) + + _toMsg(`${game.i18n.localize(creatureType)} [${game.i18n.localize('DL.ActionTarget')}]`, -1) + : _toMsg(horrifyingTextGM, baneValue * -1) + else horrifyingHTMLGM = _toMsg(horrifyingTextGM, baneValue * -1) + + horrifyingHTMLGM = '
' + horrifyingHTMLGM + '
' + const creatureTypeUnknown = game.i18n.localize('DL.OtherUnknown') + const horrifyingHTMLPlayer = revealHorrifyingBane + ? '
' + _toMsg(horrifyingTextGM, baneValue * -1) + '
' + : '
' + _toMsg(horrifyingTextGM, baneValue * -1).replace(creatureType, creatureTypeUnknown) + '
' let gmOnlyResult = changeListToMsgDefender(d, defenderBoonsArray, '', false) let playerOnlyResult = changeListToMsgDefender(d, defenderBoonsArray, '', true) @@ -185,8 +207,8 @@ export function buildAttackEffectsMessage(attacker, defender, item, attackAttrib (itemAttributePenalty ? _toMsg(itemAttributeRequirement, plusify(itemAttributePenalty)) : '') + changeListToMsg(m, [`system.bonuses.attack.boons.${attackAttribute}`, "system.bonuses.attack.boons.all"], '') + otherBoons + - (applyHorrifyingBane ? horrifyingHTMLPlayer : '') + - (applyHorrifyingBane ? horrifyingHTMLGM : '') + + (applyBane ? horrifyingHTMLPlayer : '') + + (applyBane ? horrifyingHTMLGM : '') + (playerOnlyMsg ? playerOnlyMsg : '') + (gmOnlyMsg ? gmOnlyMsg : '') boonsMsg = boonsMsg+inputBoonsMsg ? `  ${game.i18n.localize('DL.TalentAttackBoonsBanes')}
` + boonsMsg+inputBoonsMsg : '' diff --git a/src/module/chat/roll-messages.js b/src/module/chat/roll-messages.js index 6cbd0c63..acfd9d60 100644 --- a/src/module/chat/roll-messages.js +++ b/src/module/chat/roll-messages.js @@ -610,6 +610,7 @@ export async function postCustomTextToChat(actor, roll, options, attribute = {}) break } + data['resultBoxClass'] = roll?.total ? roll.total >= targetNumber ? 'SUCCESS' : 'FAILURE' : '' data['actorInfo'] = buildActorInfo(actor) const rollMode = game.settings.get('core', 'rollMode') const chatData = getChatBaseData(actor, rollMode) diff --git a/src/module/combat/combat-tracker.js b/src/module/combat/combat-tracker.js index 06a0957b..158b1307 100644 --- a/src/module/combat/combat-tracker.js +++ b/src/module/combat/combat-tracker.js @@ -238,6 +238,14 @@ calculateEncounterDifficulty(combatants) { } } + const trackerHeader = html.querySelector(".combat-tracker-header") + if (this.initiativeMethod === 's') { + if (game.combat?.turn === null) + trackerHeader.innerHTML = trackerHeader.innerHTML + `
${game.i18n.localize('DL.TurnChooseTurn')}
` + else + trackerHeader.innerHTML = trackerHeader.innerHTML + `
 
` + } + html.querySelectorAll('.combatant')?.forEach(el => { // For each combatant in the tracker, change the initiative selector const combId = el.getAttribute('data-combatant-id') @@ -246,6 +254,8 @@ calculateEncounterDifficulty(combatants) { const multipleCombatants = game.combat.getCombatantsByToken(combatant.token) + const title = (game.user.isGM || combatant.actor.isOwner) && game.combat?.turn === null ? i18n('DL.TurnChangeTurn') : '' + const style = (game.user.isGM || combatant.actor.isOwner) && game.combat?.turn === null ? 'font-weight: bold; cursor: pointer;' : 'font-weight: normal; cursor: auto; opacity: 0.5;' if (combatant.actor?.system.fastAndSlowTurn && multipleCombatants.length == 2) { // The combatant has a double initiative, so we display "Fast" and "Slow" @@ -261,7 +271,7 @@ calculateEncounterDifficulty(combatants) { // Change initiative by clicking on the name if (this.initiativeMethod === 's') el.getElementsByClassName('token-initiative')[0].innerHTML = - `${init}` + `${init}` } if (this.initiativeMethod === 'h' && game.user.isGM) @@ -346,8 +356,7 @@ calculateEncounterDifficulty(combatants) { const combId = li.dataset.combatantId const combatant = combatants.get(combId) if (!combatant) return - - if (game.user.isGM || combatant.actor.isOwner) { + if (game.user.isGM || (combatant.actor.isOwner && game.combat?.turn === null)) { await combatant.actor.update({'system.fastturn': !combatant.actor.system.fastturn}) const initChatMessage = await createInitChatMessage(combatant, {}) if (initChatMessage) ChatMessage.create(initChatMessage) diff --git a/src/module/combat/combat.js b/src/module/combat/combat.js index c9a2147b..46c4c575 100644 --- a/src/module/combat/combat.js +++ b/src/module/combat/combat.js @@ -256,10 +256,26 @@ async rollInitiativeGroup(ids, { formula = null, updateTurn = true, messageOptio }) } + async allowTurnOrderChangeInTurns(_updated) { + if (game.settings.get('demonlord', 'optionalRuleInitiativeMode') !== 's') return + if (game.combat.getFlag('demonlord', 'allowTurnOrderChange') == undefined && _updated.current.turn === 0) + { + await game.combat.update({turn : null}) + game.combat.setFlag('demonlord', 'allowTurnOrderChange', true) + } else if (_updated.current.turn > 0) game.combat.unsetFlag('demonlord', 'allowTurnOrderChange') + } + + async allowTurnOrderChangeInRounds() { + if (game.settings.get('demonlord', 'optionalRuleInitiativeMode') !== 's') return + await game.combat.update({turn : null}) + game.combat.setFlag('demonlord', 'allowTurnOrderChange', true) + } + /** @override */ async nextTurn() { const _updatedTurn = await super.nextTurn() await this._handleTurnEffects() + await this.allowTurnOrderChangeInTurns(_updatedTurn) return _updatedTurn } @@ -267,6 +283,7 @@ async rollInitiativeGroup(ids, { formula = null, updateTurn = true, messageOptio async previousTurn() { const _updatedTurn = await super.previousTurn() await this._handleTurnEffects() + await this.allowTurnOrderChangeInTurns(_updatedTurn) return _updatedTurn } @@ -282,6 +299,7 @@ async rollInitiativeGroup(ids, { formula = null, updateTurn = true, messageOptio } const _updatedRound = await super.nextRound() await this._handleTurnEffects() + await this.allowTurnOrderChangeInRounds() return _updatedRound } diff --git a/src/module/config.js b/src/module/config.js index 9a7df087..2d8d383c 100644 --- a/src/module/config.js +++ b/src/module/config.js @@ -125,3 +125,5 @@ DL.defaultItemIcons = { creaturerole: 'icons/equipment/back/cape-layered-blue-accent.webp', relic: 'icons/commodities/treasure/sceptre-jeweled-gold.webp' } + +DL.difficultyScale = [1,5,10,25,50,100,250,500,750,1000,1500] \ No newline at end of file diff --git a/src/module/dialog/roll-dialog.js b/src/module/dialog/roll-dialog.js index 36f72675..abf7e2a4 100644 --- a/src/module/dialog/roll-dialog.js +++ b/src/module/dialog/roll-dialog.js @@ -37,10 +37,10 @@ function prepareReminderHTML(text) if (game.settings.get('demonlord', 'launchDialogReminder')) { if (targets.length === 1 && (targets[0]?.actor.system.horrifying || targets[0]?.actor.system.frightening)) { if (actor.isFrightenedFrom(targets[0]?.actor)) content += `
${game.i18n.localize('DL.YouAreAttackingSoureceOfFrightenedAffliction')}
` - else if (actor.isImmuneToTarget(targets[0]?.actor)) { - if (!game.settings.get('demonlord', 'optionalRuleTraitMode2025')) content += prepareReminderHTML(game.i18n.localize('DL.YouCannotBeAffectedUntilYouCompleteARest')) - else { - const immuneArray = actor.appliedEffects.filter(x => x.name === game.i18n.format('DL.ImmuneToTarget', { + else if (actor.isFearRollCompleted(targets[0]?.actor)) { + if (game.settings.get('demonlord', 'optionalRuleTraitMode2025')) + { + const immuneArray = actor.appliedEffects.filter(x => x.name === game.i18n.format('DL.FearRollAgainst', { creature: targets[0].actor.name })) let effect @@ -49,12 +49,12 @@ if (game.settings.get('demonlord', 'launchDialogReminder')) { } if (game.combat) { const remainingRounds = calcEffectRemainingRounds(effect, game.combat.round) - const immuneText = (remainingRounds === 1) ? game.i18n.localize('DL.ImmunityLastsUntilTheEndOfNextRound') : (remainingRounds === 0) ? game.i18n.localize('DL.ImmunityLastsUntilTheEndOfTheRound') : game.i18n.format('DL.ImmunityLastsRounds', { + const immuneText = (remainingRounds === 1) ? game.i18n.localize('DL.FearRollLastsUntilTheEndOfNextRound') : (remainingRounds === 0) ? game.i18n.localize('DL.FearRollLastsUntilTheEndOfTheRound') : game.i18n.format('DL.FearRollLastsRounds', { rounds: remainingRounds }) content += prepareReminderHTML(immuneText) } else { - content += prepareReminderHTML(game.i18n.format('DL.ImmunityLastsSeconds', { + content += prepareReminderHTML(game.i18n.format('DL.FearRollLastsSeconds', { seconds: calcEffectRemainingSeconds(effect, game.time.worldTime) })) } @@ -62,7 +62,7 @@ if (game.settings.get('demonlord', 'launchDialogReminder')) { } const ignoreLevelDependentBane = (game.settings.get('demonlord', 'optionalRuleLevelDependentBane') && ((actor.system?.level >= 3 && actor.system?.level <= 6 && targets[0]?.actor.system?.difficulty <= 25) || (actor.system?.level >= 7 && targets[0]?.actor.system?.difficulty <= 50))) ? false : true - if (!actor.isFrightenedFrom(targets[0]?.actor) && !actor.isImmuneToTarget(targets[0]?.actor) && !actor.isImmuneToAffliction('frightened') && ignoreLevelDependentBane) { + if (!actor.isFrightenedFrom(targets[0]?.actor) && !actor.isFearRollCompleted(targets[0]?.actor) && !actor.isImmuneToAffliction('frightened') && ignoreLevelDependentBane) { if (game.settings.get('demonlord', 'optionalRuleTraitMode2025') && targets[0]?.actor.system.horrifying) content += prepareReminderHTML(game.i18n.localize('DL.YouHaventMadeWillChallengeRollAgainstTarget')) else if ( diff --git a/src/module/item-macro/ItemMacro.js b/src/module/item-macro/ItemMacro.js new file mode 100644 index 00000000..4e635f93 --- /dev/null +++ b/src/module/item-macro/ItemMacro.js @@ -0,0 +1,66 @@ +/** + * Modified version of the awesome https://github.com/Foundry-Workshop/Item-Macro + * Big thanks to Forien & Kekilla0 + */ + +export class DLItemMacro extends Macro { + constructor(data, context) { + super(data, context) + + this.item = context.item + } + + #executeChat(speaker) { + return ui.chat.processMessage(this.command, { speaker }).catch(err => { + Hooks.onError('Macro#_executeChat', err, { + msg: 'There was an error in your chat message syntax.', + log: 'error', + notify: 'error', + command: this.command, + }) + }) + } + + async #executeScript(args = null) { + const item = this.item + const speaker = ChatMessage.getSpeaker({ actor: item.actor }) + const actor = item.actor ?? game.actors.get(speaker.actor) + + /* MMH@TODO Check the types returned by linked and unlinked */ + const token = canvas.tokens?.get(speaker.token) + const character = game.user.character + + //build script execution + const scriptFunction = Object.getPrototypeOf(async function () {}).constructor + const body = this.command + + if (game.user.isGM) { + const fn = new scriptFunction('item', 'speaker', 'actor', 'token', 'character', 'args', body) + + //attempt script execution + try { + return await fn.bind(this)(item, speaker, actor, token, character, args) + } catch (err) { + ui.notifications.error('DLItemMacro Execution failed') + } + } else { + game.socket.emit('system.demonlord', { + request: 'runMacro', + itemuuid : item.uuid, + speaker : speaker, + actoruuid: actor.uuid, + characteruuid: character.uuid, + args: args + }) + } + } + + execute(scope = {}, args = null) { + switch (this.type) { + case 'chat': + return this.#executeChat(scope.speaker) + case 'script': + return this.#executeScript(args) + } + } +} diff --git a/src/module/item-macro/ItemMacroConfig.js b/src/module/item-macro/ItemMacroConfig.js new file mode 100644 index 00000000..d7ea3d50 --- /dev/null +++ b/src/module/item-macro/ItemMacroConfig.js @@ -0,0 +1,54 @@ +/** + * Modified version of the awesome https://github.com/Foundry-Workshop/Item-Macro + * Big thanks to Forien & Kekilla0 + */ +import { DLItemMacro } from "./ItemMacro" +/** + * @extends {MacroConfig} + */ +export class DLItemMacroConfig extends foundry.applications.sheets.MacroConfig { + /** @override */ + static DEFAULT_OPTIONS = { + actions: { + execute: DLItemMacroConfig._onExecute + }, + } + + /** @override */ + // eslint-disable-next-line no-shadow + constructor({document, item}, ...args) { + super({document}, ...args) + this.item = item + } + + static async openConfig(item) { + const macro = new DLItemMacroConfig({document: item.getDLMacro(), item}) + macro.render(true) + } + + /** @override */ + async _processSubmitData(event, form, submitData) { + await this.updateMacro(submitData) + } + + static async _onExecute(event) { + await this.submit() + this.item.executeDLMacro(event) + } + + async updateMacro({command, type}) { + const item = this.item + + const newMacro = new DLItemMacro({ + name: item.name, + type, + scope: "global", + command, + author: game.user.id, + }, {item}) + + await item.setDLMacro(newMacro) + + this.object = newMacro + } +} \ No newline at end of file diff --git a/src/module/item/item.js b/src/module/item/item.js index fe5f9f34..45bec2fa 100644 --- a/src/module/item/item.js +++ b/src/module/item/item.js @@ -2,6 +2,7 @@ import {deleteActorNestedItems, PathLevel} from './nested-objects' import {DemonlordActor} from '../actor/actor' import { DLEndOfRound } from '../dialog/endofround' import { getChatBaseData } from '../chat/base-messages' +import { DLItemMacro } from '../item-macro/ItemMacro' export class DemonlordItem extends Item { /** @override */ @@ -215,4 +216,29 @@ export class DemonlordItem extends Item { return ancestry } + + getDLMacro() { + const hasMacro = this.hasDLMacro() + const flag = this.getFlag('demonlord', 'macro') + if (hasMacro) return new DLItemMacro(flag, { item: this }) + return new DLItemMacro({ img: this.img, name: this.name, scope: 'global', type: 'script' }, { item: this }) + } + + hasDLMacro() { + const flag = this.getFlag('demonlord', 'macro') + return !!flag?.command + } + + async setDLMacro(macro) { + if (macro instanceof DLItemMacro) { + const data = macro.toObject() + return await this.setFlag('demonlord', 'macro', data) + } + } + + executeDLMacro(scope = {}, args = null) { + if (!this.hasDLMacro()) return + return this.getDLMacro().execute(scope, args) + } + } diff --git a/src/module/item/sheets/base-item-sheet.js b/src/module/item/sheets/base-item-sheet.js index 230f1e5b..bfe113f2 100644 --- a/src/module/item/sheets/base-item-sheet.js +++ b/src/module/item/sheets/base-item-sheet.js @@ -18,6 +18,7 @@ import { DamageType } from '../nested-objects'; import { DLStatEditor } from '../../dialog/stat-editor' +import { DLItemMacroConfig } from '../../item-macro/ItemMacroConfig' const { TextEditor } = foundry.applications.ux //eslint-disable-line no-shadow @@ -722,6 +723,23 @@ export default class DLBaseItemSheet extends HandlebarsApplicationMixin(ItemShee // Autoselect text in inputs when focused e.querySelectorAll('input')?.forEach(el => el.addEventListener('focus', ev => ev.currentTarget.select())) + + if (game.user.isGM && game.settings.get('demonlord', 'enableItemMacro')) { + const hasMacro = this.document.hasDLMacro() + const macroLink = { + style: hasMacro ? 'color: darkorange;text-shadow: 0 0 8px darkorange; cursor: pointer;' : 'color: var(--button-text-color); text-shadow: 0 0 8px var(--button-text-color); cursor: pointer;', + icon: "fa-solid fa-code", + tooltip: hasMacro ? game.i18n.localize("MACRO.Edit") : game.i18n.localize("SIDEBAR.ACTIONS.CREATE.Macro") + } + + let macroLinkIndicator = `` + e.querySelector("macrolink")?.remove() + e.querySelector(".header-control")?.insertAdjacentHTML("beforebegin", macroLinkIndicator) + // eslint-disable-next-line no-unused-vars + e.querySelector("macrolink")?.addEventListener('click', async ev => { + DLItemMacroConfig.openConfig(this.document) + }) + } } /* -------------------------------------------- */ diff --git a/src/module/settings.js b/src/module/settings.js index 9dd94fbe..85274c44 100644 --- a/src/module/settings.js +++ b/src/module/settings.js @@ -979,4 +979,12 @@ export const registerSettings = function () { type: Boolean, config: true, }) + game.settings.register('demonlord', 'enableItemMacro', { + name: game.i18n.localize('DL.SettingEnableItemMacro'), + hint: game.i18n.localize('DL.SettingEnableItemMacroHint'), + default: false, + scope: 'world', + type: Boolean, + config: true, + }) } diff --git a/src/module/utils/handlebars-helpers.js b/src/module/utils/handlebars-helpers.js index 048454d8..75d1d93a 100644 --- a/src/module/utils/handlebars-helpers.js +++ b/src/module/utils/handlebars-helpers.js @@ -120,6 +120,21 @@ export function registerHandlebarsHelpers() { else return game.i18n.localize(tooltip) }) + Handlebars.registerHelper('dLocalizeWithSuffix', function (groupName, str) { + let result + switch (groupName) { + case 'WeaponHands': + if (!str.length) result = '' + else result = i18n(`DL.WeaponHands${str.capitalize()}`) + break + case 'SpellType': + if (!str.length) result = '―' + else result = i18n(`DL.SpellType${str.capitalize()}`) + break + } + return result + }) + Handlebars.registerHelper('enrichHTMLUnrolled', async (x) => await TextEditor.enrichHTML(x, { unrolled: true })) Handlebars.registerHelper('lookupAttributeModifier', (attributeName, actorData) => actorData?.system?.attributes[attributeName.toLowerCase()]?.modifier diff --git a/src/module/utils/socket.js b/src/module/utils/socket.js index 765b9ecb..c37a0c52 100644 --- a/src/module/utils/socket.js +++ b/src/module/utils/socket.js @@ -1,22 +1,53 @@ /* global fromUuidSync */ +import { DLItemMacro } from '../item-macro/ItemMacro' export function activateSocketListener() { game.socket.on('system.demonlord', async (...[message]) => { - let actor = fromUuidSync(message.tokenuuid).actor // Execute it once if multiple GMs are connected. if (game.users.activeGM?.isSelf) { switch (message.request) { case 'createEffect': - await actor.createEmbeddedDocuments('ActiveEffect', [message.effectData]) + { + let actor = fromUuidSync(message.tokenuuid).actor + await actor.createEmbeddedDocuments('ActiveEffect', [message.effectData]) + } break case 'deleteEffect': - await actor.deleteEmbeddedDocuments('ActiveEffect', message.effectData) + { + let actor = fromUuidSync(message.tokenuuid).actor + await actor.deleteEmbeddedDocuments('ActiveEffect', message.effectData) + } break case 'increaseDamage': - await actor.increaseDamage(message.increment) - break - default: + { + let actor = fromUuidSync(message.tokenuuid).actor + await actor.increaseDamage(message.increment) + } break + case 'runMacro': { + const item = fromUuidSync(message.itemuuid) + const actor = fromUuidSync(message.actoruuid) + const character = fromUuidSync(message.characteruuid) + // eslint-disable-next-line no-unused-vars + const token = canvas.tokens?.get(message.speaker.token) + const body = item.getDLMacro()?.command + const scriptFunction = Object.getPrototypeOf(async function () {}).constructor + const fn = new scriptFunction('item', 'speaker', 'actor', 'token', 'character', 'args', body) + //attempt script execution + try { + return await fn.bind(DLItemMacro)( + item, + message.speaker, + actor, + message.token, + character, + message.args, + body, + ) + } catch (err) { + ui.notifications.error('DLItemMacro Execution failed') + } + } } } }) -} \ No newline at end of file +} diff --git a/src/styles/components/_chat.scss b/src/styles/components/_chat.scss index 8ff5755d..e034e825 100644 --- a/src/styles/components/_chat.scss +++ b/src/styles/components/_chat.scss @@ -171,7 +171,7 @@ .tooltiptext { visibility: hidden; width: 260px; - background-color: rgba(0, 0, 0, 0.9); + background-color: rgba(0, 0, 0, 0.85); color: #fff; text-align: left; border: 1px solid rgba(0, 0, 0, 0.5); @@ -180,7 +180,7 @@ position: absolute; z-index: 1; left: 7px; - top: 24px; + top: -7px; } } @@ -243,7 +243,7 @@ .tooltiptext { display: block; width: 260px; - background-color: rgba(0, 0, 0, 0.5); + background-color: rgba(0, 0, 0, 0.85); color: #fff; text-align: left; border: 1px solid rgba(0, 0, 0, 0.5); @@ -252,7 +252,7 @@ position: relative; z-index: 1; left: -30px; - top: 10px; + top: -30px; } } diff --git a/src/styles/v2/_actors.scss b/src/styles/v2/_actors.scss index 7ed6dd22..58a609fb 100644 --- a/src/styles/v2/_actors.scss +++ b/src/styles/v2/_actors.scss @@ -154,6 +154,9 @@ aside { position: absolute; top: 40px; left: 0px; + text-overflow: ellipsis; + overflow: hidden; + width: 80px; } } @@ -164,6 +167,9 @@ aside { position: absolute; top: 40px; right: 0px; + text-overflow: ellipsis; + overflow: hidden; + width: 80px; } } diff --git a/src/styles/v2/_sheets.scss b/src/styles/v2/_sheets.scss index d7e111d2..29f89c80 100644 --- a/src/styles/v2/_sheets.scss +++ b/src/styles/v2/_sheets.scss @@ -951,6 +951,13 @@ span.text-vs { &.off { opacity: 1; } + &.on_disabled { + opacity: 0; + } + + &.off_disabled { + opacity: 0.5; + } } .toggleInput:checked ~ .toggleText { @@ -961,5 +968,12 @@ span.text-vs { &.off { opacity: 0; } + &.on_disabled { + opacity: 0.5; + } + + &.off_disabled { + opacity: 0; + } } } diff --git a/src/templates/actor/parts/character-sheet-sidemenu.hbs b/src/templates/actor/parts/character-sheet-sidemenu.hbs index cab9e270..23a299e6 100644 --- a/src/templates/actor/parts/character-sheet-sidemenu.hbs +++ b/src/templates/actor/parts/character-sheet-sidemenu.hbs @@ -91,7 +91,7 @@
{{ifThen (gte ownership 2) system.characteristics.insanity.max '?'}}
{{ifThen (gte ownership 2) system.characteristics.insanity.value '?'}}
{{#if (gte ownership 2)}} - + {{/if}} {{else}}
@@ -156,10 +156,15 @@
diff --git a/src/templates/actor/tabs/combat.hbs b/src/templates/actor/tabs/combat.hbs index 41fe1d9f..4d5336d7 100644 --- a/src/templates/actor/tabs/combat.hbs +++ b/src/templates/actor/tabs/combat.hbs @@ -73,7 +73,7 @@ {{/if}} {{#if system.hands}}
- {{localize "DL.WeaponHands"}} {{system.hands}} + {{localize "DL.WeaponHands"}} {{dLocalizeWithSuffix "WeaponHands" system.hands}}
{{/if}} {{#if system.description}} diff --git a/src/templates/actor/tabs/magic.hbs b/src/templates/actor/tabs/magic.hbs index f25fb26b..926f801b 100644 --- a/src/templates/actor/tabs/magic.hbs +++ b/src/templates/actor/tabs/magic.hbs @@ -32,7 +32,7 @@ -
{{defaultValue system.spelltype "―"}}
+
{{dLocalizeWithSuffix "SpellType" system.spelltype}}
{{system.rank}}
{{defaultValue (plusify system.action.boonsbanes) 0}} @@ -60,7 +60,7 @@ {{spell.name}}
- {{spellbook.tradition}} {{system.spelltype}} {{system.rank}} + {{spellbook.tradition}} {{dLocalizeWithSuffix "SpellType" system.spelltype}} {{system.rank}}

diff --git a/src/templates/chat/text.hbs b/src/templates/chat/text.hbs index 3bbf11f5..027907b8 100644 --- a/src/templates/chat/text.hbs +++ b/src/templates/chat/text.hbs @@ -14,7 +14,7 @@
{{data.resultText}}
-
+
{{data.source}}
{{data.diceTotal}}