Various initiative fixes + shield management messages
All checks were successful
Release Creation / build (release) Successful in 46s

This commit is contained in:
2026-04-12 01:07:58 +02:00
parent 42945d33db
commit c37d92af25
35 changed files with 589 additions and 200 deletions

View File

@@ -100,6 +100,14 @@ export class LethalFantasyCombat extends Combat {
return this.turns = turns;
}
async startCombat() {
this._playCombatSound("startEncounter")
const updateData = { round: 0, turn: 0 }
Hooks.callAll("combatStart", this, updateData)
await this.update(updateData)
return this
}
async rollInitiative(ids, options) {
console.log("%%%%%%%%% Roll Initiative", ids, options);
@@ -110,13 +118,12 @@ export class LethalFantasyCombat extends Combat {
let updates = [];
for (let cId of ids) {
const c = this.combatants.get(cId);
let user = game.users.find(u => u.active && u.character && u.character.id === c.actor.id);
if (user?.hasPlayerOwner) {
const playerOwner = game.users.find(u => u.active && !u.isGM && u.character?.id === c.actor.id);
if (game.user.isGM && playerOwner) {
console.log("Rolling initiative for", c.actor.name);
game.socket.emit(`system.${SYSTEM.id}`, { type: "rollInitiative", actorId: c.actor.id, combatId: this.id, combatantId: c.id });
game.socket.emit(`system.${SYSTEM.id}`, { type: "rollInitiative", userId: playerOwner.id, actorId: c.actor.id, combatId: this.id, combatantId: c.id });
} else {
user = game.users.find(u => u.active && u.isGM);
c.actor.system.rollInitiative(this.id, c.id);
await c.actor.system.rollInitiative(this.id, c.id);
}
}
@@ -196,12 +203,11 @@ export class LethalFantasyCombat extends Combat {
for (let c of this.combatants) {
if (nextRound >= c.initiative) {
let user = game.users.find(u => u.active && u.character && u.character.id === c.actor.id);
if (user?.hasPlayerOwner) {
game.socket.emit(`system.${SYSTEM.id}`, { type: "rollProgressionDice", progressionCount: c.system.progressionCount + 1, actorId: c.actor.id, combatId: this.id, combatantId: c.id });
const playerOwner = game.users.find(u => u.active && !u.isGM && u.character?.id === c.actor.id);
if (game.user.isGM && playerOwner) {
game.socket.emit(`system.${SYSTEM.id}`, { type: "rollProgressionDice", userId: playerOwner.id, progressionCount: c.system.progressionCount + 1, actorId: c.actor.id, combatId: this.id, combatantId: c.id });
} else {
user = game.users.find(u => u.active && u.isGM);
c.actor.system.rollProgressionDice(this.id, c.id);
await c.actor.system.rollProgressionDice(this.id, c.id);
}
}
}

View File

@@ -153,7 +153,7 @@ export default class LethalFantasyActor extends Actor {
}
/* *************************************************/
async prepareRoll(rollType, rollKey, rollDice, defenderId, defenderTokenId) {
async prepareRoll(rollType, rollKey, rollDice, defenderId, defenderTokenId, extraShieldDr = 0) {
console.log("Preparing roll", rollType, rollKey, rollDice, defenderId)
let rollTarget
switch (rollType) {
@@ -268,7 +268,7 @@ export default class LethalFantasyActor extends Actor {
rollTarget.magicUser = this.system.biodata.magicUser
rollTarget.actorModifiers = foundry.utils.duplicate(this.system.modifiers)
rollTarget.actorLevel = this.system.biodata.level
await this.system.roll(rollType, rollTarget, defenderId, defenderTokenId)
await this.system.roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr)
}
}

View File

@@ -346,58 +346,73 @@ export default class LethalFantasyRoll extends Roll {
favor: "none",
targetName
}
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/roll-dialog.hbs", dialogContext)
let rollContext
if (options.rollContext) {
rollContext = foundry.utils.duplicate(options.rollContext)
hasGrantedDice = !!rollContext.hasGrantedDice
pointBlank = !!rollContext.pointBlank
beyondSkill = !!rollContext.beyondSkill
letItFly = !!rollContext.letItFly
saveSpell = !!rollContext.saveSpell
rollContext.visibility ||= rollContext.rollMode || game.settings.get("core", "rollMode")
rollContext.modifier ||= modifier
rollContext.favor ||= "none"
rollContext.changeDice ||= `${dice}`
rollContext.attackerAim ||= "0"
} else {
const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-lethal-fantasy/templates/roll-dialog.hbs", dialogContext)
let position = game.user.getFlag(SYSTEM.id, "roll-dialog-pos") || { top: -1, left: -1 }
const label = game.i18n.localize("LETHALFANTASY.Roll.roll")
const rollContext = await foundry.applications.api.DialogV2.wait({
window: { title: "Roll dialog" },
classes: ["lethalfantasy"],
content,
position,
buttons: [
{
label: label,
callback: (event, button, dialog) => {
console.log("Roll context", event, button, dialog)
let position = dialog.position
game.user.setFlag(SYSTEM.id, "roll-dialog-pos", foundry.utils.duplicate(position))
const output = Array.from(button.form.elements).reduce((obj, input) => {
if (input.name) obj[input.name] = input.value
return obj
}, {})
return output
let position = game.user.getFlag(SYSTEM.id, "roll-dialog-pos") || { top: -1, left: -1 }
const label = game.i18n.localize("LETHALFANTASY.Roll.roll")
rollContext = await foundry.applications.api.DialogV2.wait({
window: { title: "Roll dialog" },
classes: ["lethalfantasy"],
content,
position,
buttons: [
{
label: label,
callback: (event, button, dialog) => {
console.log("Roll context", event, button, dialog)
let position = dialog.position
game.user.setFlag(SYSTEM.id, "roll-dialog-pos", foundry.utils.duplicate(position))
const output = Array.from(button.form.elements).reduce((obj, input) => {
if (input.name) obj[input.name] = input.value
return obj
}, {})
return output
},
},
},
],
actions: {
"selectGranted": (event, button, dialog) => {
hasGrantedDice = event.target.checked
},
"selectBeyondSkill": (event, button, dialog) => {
beyondSkill = button.checked
},
"selectPointBlank": (event, button, dialog) => {
pointBlank = button.checked
},
"selectLetItFly": (event, button, dialog) => {
letItFly = button.checked
},
"saveSpellCheck": (event, button, dialog) => {
saveSpell = button.checked
},
"gotoToken": (event, button, dialog) => {
let tokenId = $(button).data("tokenId")
let token = canvas.tokens?.get(tokenId)
if (token) {
canvas.animatePan({ x: token.x, y: token.y, duration: 200 })
canvas.tokens.releaseAll();
token.control({ releaseOthers: true });
],
actions: {
"selectGranted": (event, button, dialog) => {
hasGrantedDice = event.target.checked
},
"selectBeyondSkill": (event, button, dialog) => {
beyondSkill = button.checked
},
"selectPointBlank": (event, button, dialog) => {
pointBlank = button.checked
},
"selectLetItFly": (event, button, dialog) => {
letItFly = button.checked
},
"saveSpellCheck": (event, button, dialog) => {
saveSpell = button.checked
},
"gotoToken": (event, button, dialog) => {
let tokenId = $(button).data("tokenId")
let token = canvas.tokens?.get(tokenId)
if (token) {
canvas.animatePan({ x: token.x, y: token.y, duration: 200 })
canvas.tokens.releaseAll()
token.control({ releaseOthers: true })
}
}
}
},
rejectClose: false // Click on Close button will not launch an error
})
},
rejectClose: false // Click on Close button will not launch an error
})
}
// If the user cancels the dialog, exit
if (rollContext === null) return
@@ -547,6 +562,10 @@ export default class LethalFantasyRoll extends Roll {
rollFavor = null
}
if (options.forceNoD30) {
hasD30 = false
}
if (hasD30) {
let rollD30 = await new Roll("1D30").evaluate()
if (game?.dice3d) {
@@ -621,6 +640,7 @@ export default class LethalFantasyRoll extends Roll {
rollBase.options.rollData = foundry.utils.duplicate(rollData)
rollBase.options.defenderId = options.defenderId
rollBase.options.defenderTokenId = options.defenderTokenId
rollBase.options.extraShieldDr = options.extraShieldDr || 0
/**
* A hook event that fires after the roll has been made.
@@ -1296,7 +1316,7 @@ export default class LethalFantasyRoll extends Roll {
* @returns {Promise} - A promise that resolves when the message is created.
*/
async toMessage(messageData = {}, { rollMode, create = true } = {}) {
super.toMessage(
return await super.toMessage(
{
isSave: this.isSave,
isChallenge: this.isChallenge,
@@ -1313,7 +1333,7 @@ export default class LethalFantasyRoll extends Roll {
rollData: this.rollData,
...messageData,
},
{ rollMode: rollMode },
{ rollMode, create },
)
}

View File

@@ -274,7 +274,7 @@ export default class LethalFantasyCharacter extends foundry.abstract.TypeDataMod
* @param {"="|"+"|"++"|"-"|"--"} rollAdvantage If there is an avantage (+), a disadvantage (-), a double advantage (++), a double disadvantage (--) or a normal roll (=).
* @returns {Promise<null>} - A promise that resolves to null if the roll is cancelled.
*/
async roll(rollType, rollTarget, defenderId, defenderTokenId) {
async roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr = 0) {
const hasTarget = false
let roll = await LethalFantasyRoll.prompt({
rollType,
@@ -285,7 +285,8 @@ export default class LethalFantasyCharacter extends foundry.abstract.TypeDataMod
hasTarget,
target: false,
defenderId,
defenderTokenId
defenderTokenId,
extraShieldDr
})
if (!roll) return null

View File

@@ -141,7 +141,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
* @param {"="|"+"|"++"|"-"|"--"} rollAdvantage If there is an avantage (+), a disadvantage (-), a double advantage (++), a double disadvantage (--) or a normal roll (=).
* @returns {Promise<null>} - A promise that resolves to null if the roll is cancelled.
*/
async roll(rollType, rollTarget, defenderId = undefined, defenderTokenId = undefined) {
async roll(rollType, rollTarget, defenderId = undefined, defenderTokenId = undefined, extraShieldDr = 0) {
const hasTarget = false
let roll = await LethalFantasyRoll.prompt({
rollType,
@@ -152,14 +152,15 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
hasTarget,
target: false,
defenderId,
defenderTokenId
defenderTokenId,
extraShieldDr
})
if (!roll) return null
await roll.toMessage({}, { rollMode: roll.options.rollMode })
}
async prepareMonsterRoll(rollType, rollKey, rollDice = undefined, tokenId = undefined, damageModifier = undefined, defenderId = undefined, defenderTokenId = undefined) {
async prepareMonsterRoll(rollType, rollKey, rollDice = undefined, tokenId = undefined, damageModifier = undefined, defenderId = undefined, defenderTokenId = undefined, extraShieldDr = 0) {
let rollTarget
switch (rollType) {
case "monster-attack":
@@ -255,7 +256,7 @@ export default class LethalFantasyMonster extends foundry.abstract.TypeDataModel
if (rollTarget) {
rollTarget.tokenId = tokenId
console.log(rollTarget)
await this.roll(rollType, rollTarget, defenderId, defenderTokenId)
await this.roll(rollType, rollTarget, defenderId, defenderTokenId, extraShieldDr)
}
}

View File

@@ -124,10 +124,12 @@ export default class LethalFantasyUtils {
}
break
case "rollInitiative":
if (msg.userId && msg.userId !== game.user.id) break
actor = game.actors.get(msg.actorId)
actor.system.rollInitiative(msg.combatId, msg.combatantId)
break
case "rollProgressionDice":
if (msg.userId && msg.userId !== game.user.id) break
actor = game.actors.get(msg.actorId)
actor.system.rollProgressionDice(msg.combatId, msg.combatantId, msg.rollProgressionCount)
break
@@ -190,6 +192,9 @@ export default class LethalFantasyUtils {
const attackWeaponId = msg.attackWeaponId
const attackRollType = msg.attackRollType
const attackRollKey = msg.attackRollKey
const attackD30result = msg.attackD30result
const attackD30message = msg.attackD30message
const attackRerollContext = msg.attackRerollContext
const combatantId = msg.combatantId
const tokenId = msg.tokenId
@@ -289,6 +294,9 @@ export default class LethalFantasyUtils {
attackWeaponId,
attackRollType,
attackRollKey,
attackD30result,
attackD30message,
attackRerollContext,
defenderId: defender.id,
defenderTokenId
}
@@ -358,6 +366,9 @@ export default class LethalFantasyUtils {
attackWeaponId,
attackRollType,
attackRollKey,
attackD30result,
attackD30message,
attackRerollContext,
defenderId: defender.id,
defenderTokenId
}
@@ -368,6 +379,114 @@ export default class LethalFantasyUtils {
}
}
/* -------------------------------------------- */
static hasD30Reroll(d30Message) {
return /mulligan|re-?roll/i.test(d30Message || "")
}
/* -------------------------------------------- */
static getCombatBonusDiceChoices() {
return ["1d4", "1d6", "1d8", "1d10", "1d12", "1d20", "1d20e"]
}
/* -------------------------------------------- */
static getShieldReactionData(actor) {
if (!actor) return null
if (actor.type === "monster") {
const formula = actor.system.combat?.shieldDefenseDice
const damageReduction = actor.getShieldDR()
if (!formula || damageReduction <= 0) return null
return {
label: game.i18n.localize("LETHALFANTASY.Label.shieldDefenseDice"),
formula,
damageReduction
}
}
const equippedShields = actor.items.filter(item => item.type === "shield" && item.system.equipped)
if (equippedShields.length === 0) return null
const shield = equippedShields[0]
return {
label: shield.name,
formula: shield.system.defense,
damageReduction: actor.getShieldDR(),
shieldId: shield.id
}
}
/* -------------------------------------------- */
static async promptCombatBonusDie(actorName, sideLabel, currentRoll, opposingRoll) {
const choices = this.getCombatBonusDiceChoices()
const optionsHtml = choices.map(choice => `<option value="${choice}">${choice.toUpperCase()}</option>`).join("")
const content = `
<div class="grit-luck-dialog">
<div class="combat-status">
<p><strong>${actorName}</strong> currently has <strong>${currentRoll}</strong></p>
<p>Opposing ${sideLabel} roll: <strong>${opposingRoll}</strong></p>
</div>
<div class="weapon-selection">
<label for="bonus-die">Choose a bonus die:</label>
<select id="bonus-die" name="bonusDie" style="width: 100%; margin-top: 8px;">
${optionsHtml}
</select>
</div>
</div>
`
return await foundry.applications.api.DialogV2.wait({
window: { title: "Add Bonus Die" },
classes: ["lethalfantasy"],
content,
buttons: [
{
label: "Roll Bonus Die",
icon: "fa-solid fa-dice",
callback: (event, button, dialog) => button.form.elements.bonusDie.value
},
{
label: "Cancel",
icon: "fa-solid fa-xmark",
callback: () => null
}
],
rejectClose: false
})
}
/* -------------------------------------------- */
static async rollBonusDie(formula, actor, messageContent) {
const roll = new Roll(formula)
await roll.evaluate()
if (game?.dice3d) {
await game.dice3d.showForRoll(roll, game.user, true)
}
if (messageContent) {
await ChatMessage.create({
content: messageContent(roll.total, formula),
speaker: ChatMessage.getSpeaker({ actor })
})
}
return roll.total
}
/* -------------------------------------------- */
static async rerollConfiguredRoll(rerollContext = {}) {
const RollClass = CONFIG.Dice.rolls.find(r => r.name === "LethalFantasyRoll")
if (typeof RollClass?.prompt !== "function") {
ui.notifications.error("Lethal Fantasy roll class not available for reroll")
return null
}
return await RollClass.prompt({
...foundry.utils.duplicate(rerollContext),
rollContext: foundry.utils.duplicate(rerollContext.rollContext || {}),
forceNoD30: true,
hasTarget: false,
target: false
})
}
/* -------------------------------------------- */
static async offerGritLuckBonus(defender, attackRoll, currentDefenseRoll, attackerName, defenderName) {
let totalBonus = 0
@@ -538,7 +657,8 @@ export default class LethalFantasyUtils {
/* -------------------------------------------- */
static async compareAttackDefense(data) {
console.log("compareAttackDefense called with:", data)
const isAttackWin = data.attackRoll > data.defenseRoll
const outcome = data.outcome || (data.attackRoll > data.defenseRoll ? "hit" : "miss")
const isAttackWin = outcome !== "miss"
console.log("isAttackWin:", isAttackWin, "attackRoll:", data.attackRoll, "defenseRoll:", data.defenseRoll)
let damageButton = ""
@@ -548,10 +668,10 @@ export default class LethalFantasyUtils {
if (data.attackRollType === "weapon-attack") {
damageButton = `
<div class="attack-result-damage">
<button class="roll-damage-btn" data-attacker-id="${data.attackerId}" data-defender-id="${data.defenderId}" data-defender-token-id="${data.defenderTokenId || ""}" data-weapon-id="${data.attackWeaponId}" data-damage-type="small">
<button class="roll-damage-btn" data-attacker-id="${data.attackerId}" data-defender-id="${data.defenderId}" data-defender-token-id="${data.defenderTokenId || ""}" data-extra-shield-dr="${data.shieldDamageReduction || 0}" data-weapon-id="${data.attackWeaponId}" data-damage-type="small">
<i class="fa-solid fa-dice-d6"></i> Damage (Small)
</button>
<button class="roll-damage-btn" data-attacker-id="${data.attackerId}" data-defender-id="${data.defenderId}" data-defender-token-id="${data.defenderTokenId || ""}" data-weapon-id="${data.attackWeaponId}" data-damage-type="medium">
<button class="roll-damage-btn" data-attacker-id="${data.attackerId}" data-defender-id="${data.defenderId}" data-defender-token-id="${data.defenderTokenId || ""}" data-extra-shield-dr="${data.shieldDamageReduction || 0}" data-weapon-id="${data.attackWeaponId}" data-damage-type="medium">
<i class="fa-solid fa-dice-d20"></i> Damage (Medium)
</button>
</div>
@@ -559,7 +679,7 @@ export default class LethalFantasyUtils {
} else if (data.attackRollType === "monster-attack") {
damageButton = `
<div class="attack-result-damage">
<button class="roll-damage-btn" data-attacker-id="${data.attackerId}" data-defender-id="${data.defenderId}" data-defender-token-id="${data.defenderTokenId || ""}" data-attack-key="${data.attackRollKey}" data-damage-type="monster">
<button class="roll-damage-btn" data-attacker-id="${data.attackerId}" data-defender-id="${data.defenderId}" data-defender-token-id="${data.defenderTokenId || ""}" data-extra-shield-dr="${data.shieldDamageReduction || 0}" data-attack-key="${data.attackRollKey}" data-damage-type="monster">
<i class="fa-solid fa-burst"></i> Damage
</button>
</div>
@@ -588,9 +708,11 @@ export default class LethalFantasyUtils {
</div>
</div>
<div class="combat-result-text">
${isAttackWin ?
`<i class="fa-solid fa-circle-check"></i> <strong>${data.attackerName}</strong> hits <strong>${data.defenderName}</strong>!` :
`<i class="fa-solid fa-shield-halved"></i> <strong>${data.defenderName}</strong> parries the attack!`
${outcome === "shielded-hit"
? `<i class="fa-solid fa-shield"></i> <strong>${data.attackerName}</strong> still hits <strong>${data.defenderName}</strong>, but shield DR <strong>${data.shieldDamageReduction || 0}</strong> reduces the damage.`
: isAttackWin
? `<i class="fa-solid fa-circle-check"></i> <strong>${data.attackerName}</strong> hits <strong>${data.defenderName}</strong>!`
: `<i class="fa-solid fa-shield-halved"></i> <strong>${data.defenderName}</strong> parries the attack!`
}
</div>
${damageButton}