diff --git a/assets/ui/chance.svg b/assets/ui/chance.svg new file mode 100644 index 00000000..a3d14d01 --- /dev/null +++ b/assets/ui/chance.svg @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + Layer 1 + + + + Layer 1 + + + + diff --git a/assets/ui/destinee.svg b/assets/ui/destinee.svg new file mode 100644 index 00000000..e24ce40e --- /dev/null +++ b/assets/ui/destinee.svg @@ -0,0 +1,90 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/assets/ui/encaisser.svg b/assets/ui/encaisser.svg new file mode 100644 index 00000000..8d752e50 --- /dev/null +++ b/assets/ui/encaisser.svg @@ -0,0 +1,66 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/assets/ui/recul.svg b/assets/ui/recul.svg new file mode 100644 index 00000000..ea20bc0d --- /dev/null +++ b/assets/ui/recul.svg @@ -0,0 +1,66 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/changelog.md b/changelog.md index e9a7cf88..cefc6aec 100644 --- a/changelog.md +++ b/changelog.md @@ -20,6 +20,10 @@ - affichage du statut de surprise du défenseur - prise en compte des significatives (demi-surprises, armes disparates, particulière en finesse) + - gestion de l'appel à la chance + - gestion de l'utilisation de la destinée + - gestion du recul depuis le messages + - gestion de l'encaissement depuis le messages - impossible de faire un jet "actif" en surprise totale (attaque, parade, ...) ## 13.0.8 - Le renouveau d'Illysis diff --git a/css/foundryvtt-reve-de-dragon.css b/css/foundryvtt-reve-de-dragon.css index 2a3846c8..36ac4ced 100644 --- a/css/foundryvtt-reve-de-dragon.css +++ b/css/foundryvtt-reve-de-dragon.css @@ -424,7 +424,7 @@ select, .system-foundryvtt-reve-de-dragon .roll-dialog { font-family: CaslonAntique; display: grid; - grid-template-areas: "header header header header header header header" "action action action action action action action" "type separation separation separation separation separation separation" "type carac carac carac comp comp resume" "type choix choix choix choix choix modifiers" "type resolution resolution resolution resolution resolution modifiers" "type chances chances chances chances chances buttons" "footer footer footer footer footer footer footer"; + grid-template-areas: "header header header header header header header" "action action action action action action action" "type separation separation separation separation separation separation" "type carac carac carac comp comp resume" "type choix choix choix choix choix conditions" "type resolution resolution resolution resolution resolution conditions" "type chances chances chances chances chances buttons" "footer footer footer footer footer footer footer"; grid-template-columns: 2rem 1rem 1fr 1fr 2fr 2fr 3fr; gap: 0.2rem; } @@ -450,7 +450,7 @@ select, grid-area: resolution; } .system-foundryvtt-reve-de-dragon .roll-dialog roll-conditions { - grid-area: modifiers; + grid-area: conditions; } .system-foundryvtt-reve-de-dragon .roll-dialog roll-chances { grid-area: chances; @@ -655,6 +655,80 @@ select, width: 1.5rem; text-align: center; } +.system-foundryvtt-reve-de-dragon .chat-message div.roll-chat { + font-family: CaslonAntique; + display: grid; + grid-template-areas: "img header buttons" "img resume buttons" "details details details" "actions actions actions"; + grid-template-columns: 3rem 1fr 1.4rem; + grid-template-rows: max-content max-content max-content max-content; + gap: 0 0.5rem; +} +.system-foundryvtt-reve-de-dragon .chat-message div.roll-chat div.chat-img { + grid-area: img; + display: flex; + flex-direction: column; +} +.system-foundryvtt-reve-de-dragon .chat-message div.roll-chat div.chat-img img { + border: 0; + max-height: 3rem; + max-width: 3rem; + object-fit: contain; + height: 100%; +} +.system-foundryvtt-reve-de-dragon .chat-message div.roll-chat div.chat-header { + grid-area: header; + font-weight: bold; + font-size: 0.9rem; +} +.system-foundryvtt-reve-de-dragon .chat-message div.roll-chat div.chat-resume { + grid-area: resume; + text-align: justify; +} +.system-foundryvtt-reve-de-dragon .chat-message div.roll-chat div.chat-details { + grid-area: details; + text-align: justify; + display: flex; + flex-direction: column; +} +.system-foundryvtt-reve-de-dragon .chat-message div.roll-chat div.chat-actions { + grid-area: actions; + display: flex; + flex-direction: column; +} +.system-foundryvtt-reve-de-dragon .chat-message div.roll-chat div.chat-actions a { + display: flex; + flex-direction: row; +} +.system-foundryvtt-reve-de-dragon .chat-message div.roll-chat div.chat-actions a img { + margin-right: 0.5rem; +} +.system-foundryvtt-reve-de-dragon .chat-message div.roll-chat div.chat-buttons { + grid-area: buttons; + display: flex; + flex-direction: column; +} +.system-foundryvtt-reve-de-dragon .chat-message div.roll-chat div.chat-buttons a { + border-radius: 0.2rem; + cursor: pointer; + padding: 0.2rem; + position: relative; + box-shadow: inset 1x 1px #a6827e; + color: var(--color-controls); + border: 1px ridge #846109; + display: inline-block; + align-items: center; +} +.system-foundryvtt-reve-de-dragon .chat-message div.roll-chat div.chat-buttons a img { + max-width: 1rem; + max-height: 1rem; +} +.system-foundryvtt-reve-de-dragon .chat-message div.roll-chat div.chat-buttons a:hover { + background: var(--background-custom-button-hover); +} +.system-foundryvtt-reve-de-dragon .chat-message div.roll-chat div.chat-buttons a:active { + position: relative; + top: 1px; +} .system-foundryvtt-reve-de-dragon .window-header { background: rgba(0, 0, 0, 0.75); } @@ -943,16 +1017,18 @@ select, } .system-foundryvtt-reve-de-dragon .flex-grow, .system-foundryvtt-reve-de-dragon .flex-grow-3 { + display: flex; flex-grow: 3; } -.system-foundryvtt-reve-de-dragon .flex-grow-2 { - flex-grow: 2; +.system-foundryvtt-reve-de-dragon .flex-grow-0-5 { + flex-grow: 0.5; } .system-foundryvtt-reve-de-dragon .flex-grow-1 { flex-grow: 1; } -.system-foundryvtt-reve-de-dragon .flex-grow-0-5 { - flex-grow: 0.5; +.system-foundryvtt-reve-de-dragon .flex-grow-2 { + display: flex; + flex-grow: 2; } .system-foundryvtt-reve-de-dragon .voyage-liste-survies { max-width: 12rem; @@ -995,14 +1071,16 @@ select, justify-content: center; text-align: center; } -.system-foundryvtt-reve-de-dragon .item-actions-controls, -.system-foundryvtt-reve-de-dragon .equipement-actions { +.system-foundryvtt-reve-de-dragon :is(.item-actions-controls, .equipement-actions) { margin: 0; - flex-grow: 2; + flex-grow: 1.2; align-items: end; justify-content: flex-end; text-align: right; } +.system-foundryvtt-reve-de-dragon .liste-equipement :is(.equipement-actions, .item-actions-controls) { + flex-grow: 2; +} .system-foundryvtt-reve-de-dragon .blessure-control { flex-grow: 1; flex-direction: row; @@ -1236,15 +1314,6 @@ select, margin-right: 0.2rem; margin-left: 0.2rem; } -.system-foundryvtt-reve-de-dragon .flex-grow-1 { - flex-grow: 1; -} -.system-foundryvtt-reve-de-dragon .flex-grow-2 { - flex-grow: 2; -} -.system-foundryvtt-reve-de-dragon .flex-grow-3 { - flex-grow: 3; -} .system-foundryvtt-reve-de-dragon fieldset { border-style: groove; border-width: 0.1rem; @@ -1991,11 +2060,13 @@ select, flex-grow: 2; } .system-foundryvtt-reve-de-dragon #sidebar { - font-size: 1rem; background: #695541 url(../assets/ui/bg_sid_dark.webp) no-repeat right bottom; background-position: 100%; color: rgba(220, 220, 220, 0.75); } +.system-foundryvtt-reve-de-dragon #sidebar .chat-message { + font-size: 1rem; +} .system-foundryvtt-reve-de-dragon #sidebar-tabs > .collapsed, .system-foundryvtt-reve-de-dragon #chat-controls .chat-control-icon { color: rgba(220, 220, 220, 0.75); @@ -2606,6 +2677,22 @@ select, border: 2px ridge #846109; display: inline-block; } +.system-foundryvtt-reve-de-dragon .chat-card-button img, +.system-foundryvtt-reve-de-dragon .chat-card-button-pushed img { + max-width: 1rem; + max-height: 1rem; +} +.system-foundryvtt-reve-de-dragon .chat-card-info { + font-size: 1.1rem; + display: flex; + flex-direction: row; +} +.system-foundryvtt-reve-de-dragon .chat-card-info img { + margin: 0 0.5rem; + max-width: 1rem; + max-height: 1rem; + filter: invert(0.8); +} .system-foundryvtt-reve-de-dragon .chat-card-button { text-shadow: 1px 1px #4d3534; box-shadow: inset 1x 1px #a6827e; diff --git a/less/foundryvtt-reve-de-dragon.less b/less/foundryvtt-reve-de-dragon.less index 479e8a2d..f204ceab 100644 --- a/less/foundryvtt-reve-de-dragon.less +++ b/less/foundryvtt-reve-de-dragon.less @@ -7,6 +7,7 @@ @import "item/munition.less"; @import "item/tarot.less"; @import "roll-dialog.less"; + @import "roll-chat.less"; .window-header{ background: rgba(0,0,0,0.75); } @@ -333,16 +334,18 @@ flex-shrink: 0; } .flex-grow, .flex-grow-3 { + display: flex; flex-grow: 3; } - .flex-grow-2 { - flex-grow: 2; + .flex-grow-0-5 { + flex-grow: 0.5; } .flex-grow-1 { flex-grow: 1; } - .flex-grow-0-5 { - flex-grow: 0.5; + .flex-grow-2 { + display: flex; + flex-grow: 2; } .voyage-liste-survies { max-width: 12rem; @@ -386,14 +389,16 @@ justify-content: center; text-align: center; } - .item-actions-controls, - .equipement-actions { + :is(.item-actions-controls, .equipement-actions) { margin: 0; - flex-grow: 2; + flex-grow: 1.2; align-items: end; justify-content: flex-end; text-align: right; } + .liste-equipement :is(.equipement-actions, .item-actions-controls) { + flex-grow: 2; + } .blessure-control { flex-grow: 1; @@ -622,16 +627,6 @@ } } } - - .flex-grow-1 { - flex-grow: 1; - } - .flex-grow-2 { - flex-grow: 2; - } - .flex-grow-3 { - flex-grow: 3; - } fieldset { border-style: groove; border-width: 0.1rem; @@ -1451,7 +1446,9 @@ /* ======================================== */ /* Sidebar CSS */ #sidebar { - font-size: 1rem; + .chat-message{ + font-size: 1rem; + } background: rgb(105,85,65) url(../assets/ui/bg_sid_dark.webp) no-repeat right bottom; background-position: 100%; color: rgba(220,220,220,0.75); @@ -1961,7 +1958,23 @@ border: 2px ridge #846109; display: inline-block; + img { + max-width: 1rem; + max-height: 1rem; + } } + .chat-card-info{ + font-size: 1.1rem; + display: flex; + flex-direction: row; + + img { + margin: 0 0.5rem; + max-width: 1rem; + max-height: 1rem; + filter: invert(0.8); + } + } .chat-card-button{ text-shadow: 1px 1px #4d3534; diff --git a/less/roll-chat.less b/less/roll-chat.less new file mode 100644 index 00000000..c5da339c --- /dev/null +++ b/less/roll-chat.less @@ -0,0 +1,87 @@ +.chat-message { + div.roll-chat { + font-family: CaslonAntique; + display: grid; + grid-template-areas: + "img header buttons" + "img resume buttons" + "details details details" + "actions actions actions"; + grid-template-columns: 3rem 1fr 1.4rem; + grid-template-rows: max-content max-content max-content max-content; + gap: 0 0.5rem; + + div.chat-img { + grid-area: img; + display: flex; + flex-direction: column; + img { + border: 0; + max-height: 3rem; + max-width: 3rem; + object-fit: contain; + height: 100%; + } + } + div.chat-header { + grid-area: header; + font-weight: bold; + font-size: 0.9rem; + } + div.chat-resume { + grid-area: resume; + text-align: justify; + } + div.chat-details { + grid-area: details; + text-align: justify; + display: flex; + flex-direction: column; + } + div.chat-actions { + grid-area: actions; + display: flex; + flex-direction: column; + a { + display: flex; + flex-direction: row; + img { + margin-right: 0.5rem; + } + } + } + + div.chat-buttons { + grid-area: buttons; + display: flex; + flex-direction: column; + + a { + border-radius: 0.2rem; + cursor: pointer; + padding: 0.2rem; + position: relative; + box-shadow: inset 1x 1px #a6827e; + color: var(--color-controls); + border: 1px ridge #846109; + display: inline-block; + align-items: center; + + img { + max-width: 1rem; + max-height: 1rem; + } + } + + a:hover { + background: var(--background-custom-button-hover); + } + + a:active{ + position:relative; + top:1px; + } + + } + } +} \ No newline at end of file diff --git a/less/roll-dialog.less b/less/roll-dialog.less index 9afd9f70..c5628f3a 100644 --- a/less/roll-dialog.less +++ b/less/roll-dialog.less @@ -6,8 +6,8 @@ "action action action action action action action" "type separation separation separation separation separation separation" "type carac carac carac comp comp resume" - "type choix choix choix choix choix modifiers" - "type resolution resolution resolution resolution resolution modifiers" + "type choix choix choix choix choix conditions" + "type resolution resolution resolution resolution resolution conditions" "type chances chances chances chances chances buttons" "footer footer footer footer footer footer footer"; grid-template-columns: 2rem 1rem 1fr 1fr 2fr 2fr 3fr; @@ -22,7 +22,7 @@ roll-choix { grid-area: choix; } roll-table { grid-area: resolution; } - roll-conditions { grid-area: modifiers; } + roll-conditions { grid-area: conditions; } roll-chances { grid-area: chances; } roll-resume { grid-area: resume; } roll-buttons { grid-area: buttons; } diff --git a/module/actor.js b/module/actor.js index ed0189b1..e683cdf5 100644 --- a/module/actor.js +++ b/module/actor.js @@ -46,7 +46,7 @@ import { PAS_DE_DRACONIC, POSSESSION_SANS_DRACONIC } from "./item/base-items.js" import { RdDRollResult } from "./rdd-roll-result.js"; import { RdDInitiative } from "./initiative.mjs"; import RollDialog from "./roll/roll-dialog.mjs"; -import { OptionsAvancees, ROLL_DIALOG_V2 } from "./settings/options-avancees.js"; +import { OptionsAvancees, ROLL_DIALOG_V2, ROLL_DIALOG_V2_TEST } from "./settings/options-avancees.js"; export const MAINS_DIRECTRICES = ['Droitier', 'Gaucher', 'Ambidextre'] @@ -506,7 +506,7 @@ export class RdDActor extends RdDBaseActorSang { 'system.sante.fatigue.value': 0, 'system.compteurs.ethylisme': { value: 1, nb_doses: 0, jet_moral: false } }) - await this.removeEffects(e => e.id != STATUSES.StatusDemiReve); + await this.removeEffects(e => !e.statuses?.has(STATUSES.StatusDemiReve)); await this.supprimerBlessures(it => true); await ChatMessage.create({ whisper: ChatUtility.getOwners(this), @@ -2513,29 +2513,27 @@ export class RdDActor extends RdDBaseActorSang { } /* -------------------------------------------- */ - async computeArmure(attackerRoll) { - let dmg = (attackerRoll.dmg.dmgArme ?? 0) + (attackerRoll.dmg.dmgActor ?? 0); - let armeData = attackerRoll.arme; + async computeArmure(dmg) { + let baseDmg = (dmg.dmgArme ?? 0) + (dmg.dmgActor ?? 0); let protection = 0; - const armures = this.items.filter(it => it.type == "armure" && it.system.equipe); - for (const armure of armures) { - protection += await RdDDice.rollTotal(armure.system.protection.toString()); - if (dmg > 0 && attackerRoll.dmg.encaisserSpecial != "noarmure") { - await armure.deteriorerArmure(dmg) - dmg = 0; + if (dmg.encaisserSpecial != "noarmure") { + const armures = this.items.filter(it => it.type == "armure" && it.system.equipe) + + for (const armure of armures) { + protection += await RdDDice.rollTotal(armure.system.protection.toString()); + if (baseDmg > 0 && dmg.encaisserSpecial != "noarmure") { + await armure.deteriorerArmure(baseDmg) + baseDmg = 0; + } + } + protection -= Math.min(dmg.penetration, protection) + protection += this.getProtectionNaturelle(); + // Gestion des cas particuliers sur la fenêtre d'encaissement + if (dmg.encaisserSpecial == "chute") { + protection = Math.min(protection, 2); } } - const penetration = Misc.toInt(armeData?.system.penetration ?? 0); - protection = Math.max(protection - penetration, 0); - protection += this.getProtectionNaturelle(); - // Gestion des cas particuliers sur la fenêtre d'encaissement - if (attackerRoll.dmg.encaisserSpecial == "noarmure") { - protection = 0; - } - if (attackerRoll.dmg.encaisserSpecial == "chute") { - protection = Math.min(protection, 2); - } - console.log("Final protect", protection, attackerRoll); + console.log("Final protect", protection, dmg) return protection; } @@ -3049,7 +3047,7 @@ export class RdDActor extends RdDBaseActorSang { /* -------------------------------------------- */ - async _rollArtV2(oeuvreId, callbackAction = async (actor, rd) => await actor._resultArtV2(rd)) { + async _rollArtV2(oeuvreId) { const oeuvre = this.items.get(oeuvreId) const rollData = { title: `Interpretation de ${oeuvre.name} par ${this.name}`, @@ -3066,23 +3064,13 @@ export class RdDActor extends RdDBaseActorSang { } await RollDialog.create(rollData, { onRollDone: (dialog) => { - this._onCloseRollDialog() - dialog.close() - }, - customChatMessage: true, - callbacks: [callbackAction] + if (!OptionsAvancees.isUsing(ROLL_DIALOG_V2_TEST)) + dialog.close() + } }) } - async _resultArtV2(artData) { - const niveau = artData.oeuvre.system.niveau ?? 0; - const baseQualite = (artData.rolled.isSuccess ? niveau : artData.competence.system.niveau); - artData.qualiteFinale = Math.min(baseQualite, niveau) + artData.rolled.ptQualite; - - await RdDRollResult.displayRollData(artData, this.name, `chat-resultat-${artData.art}.hbs`); - } /* -------------------------------------------- */ - async rollOeuvre(id) { if (OptionsAvancees.isUsing(ROLL_DIALOG_V2)) { return await this._rollArtV2(id) @@ -3168,7 +3156,7 @@ export class RdDActor extends RdDBaseActorSang { async _resultArt(artData) { const niveau = artData.oeuvre.system.niveau ?? 0; const baseQualite = (artData.rolled.isSuccess ? niveau : artData.competence.system.niveau); - artData.qualiteFinale = Math.min(baseQualite, niveau) + artData.rolled.ptQualite; + artData.qualiteFinale = Math.min(baseQualite, niveau) + artData.rolled.ptQualite await RdDRollResult.displayRollData(artData, this.name, `chat-resultat-${artData.art}.hbs`); } @@ -3189,7 +3177,7 @@ export class RdDActor extends RdDBaseActorSang { async _resultRecetteCuisine(cuisine) { const niveauRecette = cuisine.oeuvre.system.niveau ?? 0; const baseQualite = (cuisine.rolled.isSuccess ? niveauRecette : cuisine.competence.system.niveau); - cuisine.qualiteFinale = Math.min(baseQualite, niveauRecette) + cuisine.rolled.ptQualite; + cuisine.qualiteFinale = Math.min(baseQualite, niveauRecette) + cuisine.rolled.ptQualite cuisine.exotismeFinal = Math.min(Math.min(cuisine.qualiteFinale, cuisine.oeuvre.system.exotisme ?? 0), 0); cuisine.sust = cuisine.oeuvre.system.sust * Math.min(cuisine.proportions, cuisine.proportionsMax ?? cuisine.proportions) const platCuisine = { diff --git a/module/actor/base-actor-reve.js b/module/actor/base-actor-reve.js index 98974363..4199ea53 100644 --- a/module/actor/base-actor-reve.js +++ b/module/actor/base-actor-reve.js @@ -118,8 +118,7 @@ export class RdDBaseActorReve extends RdDBaseActor { .filter(it => it != undefined); } - - async computeArmure(attackerRoll) { return this.getProtectionNaturelle() } + async computeArmure(dmg) { return this.getProtectionNaturelle() } async remiseANeuf() { } async appliquerAjoutExperience(rollData, hideChatMessage = 'show') { } @@ -227,12 +226,12 @@ export class RdDBaseActorReve extends RdDBaseActor { isEffectAllowed(effectId) { return false } getEffects(filter = e => true, forceRequise = undefined) { - const effects = this.getEmbeddedCollection("ActiveEffect").filter(filter) + const effects = this.getEmbeddedCollection("ActiveEffect") + const selected = effects.filter(filter) if (forceRequise && this.isForceInsuffisante(forceRequise)) { - /// TODO - effects.push(StatusEffects.prepareActiveEffect(STATUSES.StatusForceWeak)) + selected.push(StatusEffects.prepareActiveEffect(STATUSES.StatusForceWeak)) } - return effects + return selected } getEffectByStatus(statusId) { @@ -257,7 +256,8 @@ export class RdDBaseActorReve extends RdDBaseActor { async removeEffects(filter = e => true) { if (game.user.isGM) { - const ids = this.getEffects(filter).map(it => it.id); + const effectsToRemove = this.getEffects(filter); + const ids = effectsToRemove.map(it => it.id); await this.deleteEmbeddedDocuments('ActiveEffect', ids); } } @@ -495,29 +495,37 @@ export class RdDBaseActorReve extends RdDBaseActor { /* -------------------------------------------- */ async encaisser() { await RdDEncaisser.encaisser(this) } - async encaisserDommages(rollData, attacker = undefined, show = undefined, attackerToken = undefined, defenderToken = undefined) { + async encaisserDommages(dmg, attacker = undefined, show = undefined, attackerToken = undefined, defenderToken = undefined) { if (attacker && !await attacker.accorder(this, 'avant-encaissement')) { - return; + return } - const armure = await this.computeArmure(rollData); + if (!Misc.isOwnerPlayer(this)) { + return RdDBaseActor.remoteActorCall({ + tokenId: attackerToken?.id ?? this.token?.id, + actorId: this.id, + method: 'encaisserDommages', args: [dmg, attacker, show, attackerToken, defenderToken] + }) + } + + const armure = await this.computeArmure(dmg) if (ReglesOptionnelles.isUsing('validation-encaissement-gr')) { - await this.encaisserDommagesValidationGR(rollData, armure, show, attackerToken, defenderToken); + await this.encaisserDommagesValidationGR(dmg, armure, show, attackerToken, defenderToken); } else { - const jet = await RdDUtility.jetEncaissement(this, rollData, armure, { showDice: SHOW_DICE }); + const jet = await RdDUtility.jetEncaissement(this, dmg, armure, { showDice: SHOW_DICE }); await this.$onEncaissement(jet, show, attackerToken, defenderToken) } } - async encaisserDommagesValidationGR(rollData, armure, show, attackerToken, defenderToken) { + async encaisserDommagesValidationGR(dmg, armure, show, attackerToken, defenderToken) { if (!game.user.isGM) { RdDBaseActor.remoteActorCall({ tokenId: this.token?.id, actorId: this.id, - method: 'encaisserDommagesValidationGR', args: [rollData, armure, show, attackerToken, defenderToken] + method: 'encaisserDommagesValidationGR', args: [dmg, armure, show, attackerToken, defenderToken] }) } else { - DialogValidationEncaissement.validerEncaissement(this, rollData, armure, + DialogValidationEncaissement.validerEncaissement(this, dmg, armure, jet => this.$onEncaissement(jet, show, attackerToken, defenderToken)); } } @@ -554,15 +562,37 @@ export class RdDBaseActorReve extends RdDBaseActor { } } + async encaisserRecul(force, dmgArme = 0) { + const diffRecul = this.getTaille() - force - dmgArme + const rolled = await RdDResolutionTable.roll(10, diffRecul) + if (rolled.isSuccess) { + return 'encaisse' + } + if (rolled.isETotal || (await this.rollEquilibre(diffRecul)).isEchec) { + await this.setEffect(STATUSES.StatusProne, true) + return 'chute' + } + return 'recul' + } + + /* -------------------------------------------- */ + async rollEquilibre(diff) { + // TODO: accrobatie optionnelle sur jet d'équilibre? + if (ReglesOptionnelles.isSet('acrobatie-pour-recul')){ + diff += Math.max(0, this.getCompetence('acrobatie')?.system.niveau ?? 0) + } + return await RdDResolutionTable.roll(this.getAgilite(), diff); + } + /* -------------------------------------------- */ async accorder(entite, when = 'avant-encaissement') { if (when != game.settings.get(SYSTEM_RDD, "accorder-entite-cauchemar") || entite == undefined || !entite.isEntite([ENTITE_INCARNE]) || entite.isEntiteAccordee(this)) { - return true; + return true } - const rolled = await RdDResolutionTable.roll(this.getReveActuel(), - Number(entite.getNiveau())); + const rolled = await RdDResolutionTable.roll(this.getReveActuel(), - Number(entite.getNiveau())) const rollData = { alias: this.getAlias(), rolled: rolled, @@ -571,11 +601,11 @@ export class RdDBaseActorReve extends RdDBaseActor { }; if (rolled.isSuccess) { - await entite.setEntiteReveAccordee(this); + await entite.setEntiteReveAccordee(this) } - await RdDRollResult.displayRollData(rollData, this, 'chat-resultat-accorder-cauchemar.hbs'); - await this.appliquerAjoutExperience(rollData, true); + await RdDRollResult.displayRollData(rollData, this, 'chat-resultat-accorder-cauchemar.hbs') + await this.appliquerAjoutExperience(rollData, true) return rolled.isSuccess; } diff --git a/module/actor/creature.js b/module/actor/creature.js index 6724f38b..95c42364 100644 --- a/module/actor/creature.js +++ b/module/actor/creature.js @@ -1,6 +1,4 @@ -import { Grammar } from "../grammar.js"; import { ITEM_TYPES } from "../constants.js"; -import { LIST_CARAC_AUTRES } from "../rdd-carac.js"; import { RdDBaseActorSang } from "./base-actor-sang.js"; export class RdDCreature extends RdDBaseActorSang { @@ -45,5 +43,4 @@ export class RdDCreature extends RdDBaseActorSang { } return undefined } - } diff --git a/module/chat-utility.js b/module/chat-utility.js index 83a749c1..c1649e3c 100644 --- a/module/chat-utility.js +++ b/module/chat-utility.js @@ -197,8 +197,12 @@ export class ChatUtility { static async onCreateChatMessage(chatMessage, options, id) { if (chatMessage.isAuthor) { - await chatMessage.setFlag(SYSTEM_RDD, 'rdd-timestamp', game.system.rdd.calendrier.getTimestamp()); + await ChatUtility.setTimestamp(chatMessage) await chatMessage.update({ content: await RdDTextEditor.enrichHTML(chatMessage.content, undefined, { showLink: false }) }) } } + + static async setTimestamp(chatMessage) { + await chatMessage.setFlag(SYSTEM_RDD, 'rdd-timestamp', game.system.rdd.calendrier.getTimestamp()); + } } diff --git a/module/dialog-validation-encaissement.js b/module/dialog-validation-encaissement.js index 55daa7a3..825b4c06 100644 --- a/module/dialog-validation-encaissement.js +++ b/module/dialog-validation-encaissement.js @@ -7,18 +7,17 @@ import { RdDUtility } from "./rdd-utility.js"; */ export class DialogValidationEncaissement extends Dialog { - static async validerEncaissement(actor, rollData, armure, onEncaisser) { - const encaissement = await RdDUtility.jetEncaissement(actor, rollData, armure, { showDice: HIDE_DICE }); + static async validerEncaissement(actor, dmg, armure, onEncaisser) { + const encaissement = await RdDUtility.jetEncaissement(actor, dmg, armure, { showDice: HIDE_DICE }); const html = await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/dialog-validation-encaissement.hbs', { actor: actor, - rollData: rollData, encaissement: encaissement }); - new DialogValidationEncaissement(html, actor, rollData, armure, encaissement, onEncaisser).render(true); + new DialogValidationEncaissement(html, actor, dmg, armure, encaissement, onEncaisser).render(true); } /* -------------------------------------------- */ - constructor(html, actor, rollData, armure, encaissement, onEncaisser) { + constructor(html, actor, dmg, armure, encaissement, onEncaisser) { // Common conf let buttons = { "valider": { label: "Valider", callback: html => this.onValider() }, @@ -42,11 +41,11 @@ export class DialogValidationEncaissement extends Dialog { super(dialogConf, dialogOptions); this.actor = actor - this.rollData = rollData; - this.armure = armure; - this.encaissement = encaissement; - this.onEncaisser = onEncaisser; - this.forceDiceResult = {total: encaissement.roll.result }; + this.dmg = dmg + this.armure = armure + this.encaissement = encaissement + this.onEncaisser = onEncaisser + this.forceDiceResult = {total: encaissement.roll.result } } /* -------------------------------------------- */ @@ -55,14 +54,14 @@ export class DialogValidationEncaissement extends Dialog { this.html = html; this.html.find('input.encaissement-roll-result').keyup(async event => { this.forceDiceResult.total = event.currentTarget.value; - this.encaissement = await RdDUtility.jetEncaissement(this.actor, this.rollData, this.armure, { showDice: HIDE_DICE, forceDiceResult: this.forceDiceResult}); + this.encaissement = await RdDUtility.jetEncaissement(this.actor, this.dmg, this.armure, { showDice: HIDE_DICE, forceDiceResult: this.forceDiceResult}); this.html.find('label.encaissement-total').text(this.encaissement.total); this.html.find('label.encaissement-blessure').text(this.encaissement.blessures) }); } async onValider() { - this.encaissement = await RdDUtility.jetEncaissement(this.actor, this.rollData, this.armure, { showDice: SHOW_DICE, forceDiceResult: this.forceDiceResult}); + this.encaissement = await RdDUtility.jetEncaissement(this.actor, this.dmg, this.armure, { showDice: SHOW_DICE, forceDiceResult: this.forceDiceResult}); this.onEncaisser(this.encaissement) } } diff --git a/module/misc.js b/module/misc.js index 9b43636d..ac0e933a 100644 --- a/module/misc.js +++ b/module/misc.js @@ -34,6 +34,9 @@ export class Misc { return ((n % m) + m) % m; } + static inRange(value, min,max){ + return Math.max(min, Math.min(value, max)) + } static sum() { return (a, b) => Number(a) + Number(b); } diff --git a/module/rdd-bonus.js b/module/rdd-bonus.js index 8fa1a4d0..f8a9d969 100644 --- a/module/rdd-bonus.js +++ b/module/rdd-bonus.js @@ -1,5 +1,6 @@ import { RdDItemArme } from "./item/arme.js"; import { RdDPossession } from "./rdd-possession.js"; +import { ReglesOptionnelles } from "./settings/regles-optionnelles.js"; const conditionsTactiques = [ { key: '', label: '', dmg: 0, attaque: 0, parade: 0, esquive: true, isTactique: false }, @@ -35,11 +36,14 @@ export class RdDBonus { /* -------------------------------------------- */ static dmg(rollData, actor, isEntiteIncarnee = false) { + const diff = rollData.diffLibre; const dmgArme = RdDBonus.dmgArme(rollData.arme, rollData.arme?.system.dommagesReels) - const forceRequise = RdDItemArme.valeurMain(rollData.arme?.system.force ?? 0, RdDItemArme.getMainAttaque(rollData.competence)) + const forceRequise = rollData.arme ? RdDItemArme.valeurMain(rollData.arme.system.force ?? 0, RdDItemArme.getMainAttaque(rollData.competence)) : 0 let dmg = { total: 0, dmgArme: dmgArme, + diff: diff, + dmgDiffLibre: ReglesOptionnelles.isUsing('degat-ajout-malus-libre') ? Math.abs(diff ?? 0) : 0, penetration: RdDBonus._peneration(rollData), dmgTactique: RdDBonus.dmgBonus(rollData.tactique), dmgParticuliere: RdDBonus._dmgParticuliere(rollData), @@ -51,6 +55,28 @@ export class RdDBonus { dmg.total = dmg.dmgSurprise + dmg.dmgTactique + dmg.dmgArme + dmg.dmgActor + dmg.dmgParticuliere + dmg.dmgForceInsuffisante return dmg; } + + static dmgRollV2(rollData, current) { + const actor = rollData.active.actor + const attaque = current.attaque + const arme = attaque.arme + const dmgArme = RdDBonus.dmgArme(arme, attaque.dommagesArme) + const dmg = { + total: 0, + dmgArme: dmgArme, + penetration: arme.penetration(), + diff: attaque.diff, + dmgTactique: current.tactique?.dmg ?? 0, + dmgParticuliere: 0, // TODO RdDBonus._dmgParticuliere(rollData), + dmgSurprise: rollData.opponent?.surprise?.dmg ?? 0, + mortalite: RdDBonus.mortalite(current.dmg?.mortalite, arme.system.mortalite, rollData.opponent?.actor?.isEntite()), + dmgActor: RdDBonus.bonusDmg(actor, attaque.carac.key, dmgArme, attaque.forceRequise), + dmgForceInsuffisante: Math.min(0, actor.getForce() - attaque.forceRequise), + dmgDiffLibre: ReglesOptionnelles.isUsing('degat-ajout-malus-libre') ? Math.abs(attaque.diff ?? 0) : 0 + } + dmg.total = dmg.dmgSurprise + dmg.dmgTactique + dmg.dmgArme + dmg.dmgActor + dmg.dmgParticuliere + dmg.dmgForceInsuffisante + dmg.dmgDiffLibre + return dmg + } /* -------------------------------------------- */ static description(condition) { diff --git a/module/rdd-combat.js b/module/rdd-combat.js index 6e384eea..de91a987 100644 --- a/module/rdd-combat.js +++ b/module/rdd-combat.js @@ -17,9 +17,9 @@ import { RdDItemCompetenceCreature } from "./item-competencecreature.js"; import { RdDInitiative } from "./initiative.mjs"; import RollDialog from "./roll/roll-dialog.mjs"; import { PART_DEFENSE } from "./roll/roll-part-defense.mjs"; -import { PART_ATTAQUE } from "./roll/roll-part-attaque.mjs"; import { RollDialogAdapter } from "./roll/roll-dialog-adapter.mjs"; import { ROLL_TYPE_ATTAQUE, ROLL_TYPE_DEFENSE } from "./roll/roll-constants.mjs"; +import { OptionsAvancees, ROLL_DIALOG_V2_TEST } from "./settings/options-avancees.js"; /* -------------------------------------------- */ const premierRoundInit = [ @@ -373,7 +373,7 @@ export class RdDCombat { if (Misc.isOwnerPlayer(defender)) { let attackerRoll = msg.attackerRoll; let attacker = msg.attackerId ? game.actors.get(msg.attackerId) : undefined; - defender.encaisserDommages(attackerRoll, attacker, msg.attackerToken); + defender.encaisserDommages(attackerRoll.dmg, attacker, msg.attackerToken); const rddCombat = RdDCombat.rddCombatForAttackerAndDefender(msg.attackerId, msg.attackerToken.id, msg.defenderToken.id); rddCombat?.removeChatMessageActionsPasseArme(attackerRoll.passeArme); } @@ -403,16 +403,19 @@ export class RdDCombat { /* -------------------------------------------- */ static registerChatCallbacks(html) { for (let button of [ - '.defense-button', - '.parer-button', - '.esquiver-button', + '.button-defense', + '.button-parade', + '.button-esquive', + '.button-encaisser', '.particuliere-attaque', - '.encaisser-button', '.appel-chance-defense', '.appel-destinee-defense', '.appel-chance-attaque', '.appel-destinee-attaque', '.echec-total-attaque', + // '.appel-chance', + // '.chat-encaissement', + // '.resister-recul', ]) { $(html).on("click", button, event => { const rddCombat = RdDCombat.rddCombatForAttackerAndDefender( @@ -472,11 +475,11 @@ export class RdDCombat { switch (button) { case '.particuliere-attaque': return await this.choixParticuliere(attackerRoll, event.currentTarget.attributes['data-mode'].value); - case '.defense-button': return this.defenseV2(attackerRoll); + case '.button-defense': return this.defenseV2(attackerRoll); - case '.parer-button': return this.parade(attackerRoll, armeParadeId); - case '.esquiver-button': return this.esquive(attackerRoll, compId, competence); - case '.encaisser-button': return this.encaisser(attackerRoll, defenderRoll); + case '.button-parade': return this.parade(attackerRoll, armeParadeId); + case '.button-esquive': return this.esquive(attackerRoll, compId, competence); + case '.button-encaisser': return this.encaisser(attackerRoll, defenderRoll); case '.echec-total-attaque': return this._onEchecTotal(attackerRoll); case '.appel-chance-attaque': return this.attacker.rollAppelChance( @@ -486,11 +489,9 @@ export class RdDCombat { () => this.defenseChanceuse(attackerRoll, defenderRoll), () => this.afficherOptionsDefense(attackerRoll, defenderRoll, { defenseChance: true })); case '.appel-destinee-attaque': return this.attacker.appelDestinee( - () => this.attaqueSignificative(attackerRoll), - () => { }); + () => this.attaqueSignificative(attackerRoll)); case '.appel-destinee-defense': return this.defender.appelDestinee( - () => this.defenseDestinee(defenderRoll), - () => { }); + () => this.defenseDestinee(defenderRoll)); } } @@ -807,8 +808,6 @@ export class RdDCombat { /* -------------------------------------------- */ async _sendMessageDefense(attackerRoll, defenderRoll, essaisPrecedents = undefined) { - console.log("RdDCombat._sendMessageDefense", attackerRoll, defenderRoll, essaisPrecedents, " / ", this.attacker, this.target, this.attackerId, attackerRoll.competence.system.categorie); - this.removeChatMessageActionsPasseArme(attackerRoll.passeArme); if (essaisPrecedents) { foundry.utils.mergeObject(attackerRoll.essais, essaisPrecedents, { overwrite: true }); @@ -969,10 +968,8 @@ export class RdDCombat { } async defenseV2(attackerRoll) { - // this._prepareParade(attackerRoll, arme, competence); - const rollData = - { + await this.doRollDefense({ ids: { actorId: this.defender.id, actorTokenId: this.defenderTokenId, @@ -980,23 +977,31 @@ export class RdDCombat { opponentId: this.attackerId, }, type: { allowed: ['defense'], current: 'defense' }, - attaque: RollDialogAdapter.mapActionAttaque(attackerRoll), + attackerRoll: RollDialogAdapter.mapActionAttaque(attackerRoll), passeArme: attackerRoll.passeArme, - } - - await RollDialog.create(rollData, { - onRollDone: (dialog) => { dialog.close() }, - customChatMessage: true, - callbacks: [async (roll) => { - this.removeChatMessageActionsPasseArme(roll.passeArme) - // defense: esquive / arme de parade / competence de défense - if (!RdDCombat.isParticuliere(roll)) - await roll.active.actor.incDecItemUse(roll.current[PART_DEFENSE].defense?.id,) - await this._onDefense(roll) - }] }) } + async doRollDefense(rollData, callbacks = []) { + await RollDialog.create(rollData, { + onRollDone: (dialog) => { + if (!OptionsAvancees.isUsing(ROLL_DIALOG_V2_TEST)) + dialog.close() + }, + customChatMessage: true, + callbacks: [ + async (roll) => { + this.removeChatMessageActionsPasseArme(roll.passeArme); + // defense: esquive / arme de parade / competence de défense + if (!RdDCombat.isParticuliere(roll)) { + await roll.active.actor.incDecItemUse(roll.current[PART_DEFENSE].defense?.id); + } + await this._onDefense(roll); + }, + ...callbacks + ] + }) + } /* -------------------------------------------- */ _prepareParade(attackerRoll, armeParade, competenceParade) { @@ -1025,57 +1030,42 @@ export class RdDCombat { } async _onDefense(rollData) { + const isEsquive = rollData.current[PART_DEFENSE].isEsquive + const isParade = !isEsquive if (RdDCombat.isReussite(rollData)) { - await this._onDefenseNormale(rollData) + if (isParade) { + await this.computeDeteriorationArme(rollData) + } + if (RdDCombat.isParticuliere(rollData)) { - await this._onDefenseParticuliere(rollData) + await this._onDefenseParticuliere(rollData, isEsquive) } } - else { - await this._onDefenseEchec(dialog, rollData) - } + this.removeChatMessageActionsPasseArme(rollData.passeArme) } - async _onDefenseParticuliere(rollData) { - console.log("RdDCombat._onDefenseParticuliere >>>", rollData); - if (/*TODO: parade?*/!rollData.attackerRoll?.isPart) { + async _onDefenseParticuliere(rollData, isEsquive) { + if (isEsquive) { + ChatUtility.createChatWithRollMode( + { content: "Vous pouvez esquiver une deuxième fois!" }, + this.defender) + } + else if (/*TODO: parade?*/!rollData.attackerRoll?.particuliere) { // TODO: attaquant doit jouer résistance et peut être désarmé p132 ChatUtility.createChatWithRollMode( { content: `(à gérer) L'attaquant doit jouer résistance et peut être désarmé (p132)` }, this.defender) } - ChatUtility.createChatWithRollMode( - { content: "Vous pouvez esquiver une deuxième fois!" }, - this.defender); } async _onDefenseNormale(rollData) { console.log("RdDCombat._onDefenseNormale >>>", rollData); - // console.log("RdDCombat._onEsquiveNormal >>>", rollData); - // await RdDRollResult.displayRollData(rollData, this.defender, 'chat-resultat-esquive.hbs'); - // this.removeChatMessageActionsPasseArme(rollData.passeArme); - //TODO await this.computeRecul(rollData); await this.computeDeteriorationArme(rollData); await RdDRollResult.displayRollData(rollData, this.defender, 'chat-resultat-parade.hbs'); this.removeChatMessageActionsPasseArme(rollData.passeArme); } - async _onDefenseEchec(rollData) { - console.log("RdDCombat._onDefenseEchec >>>", rollData); - // console.log("RdDCombat._onEsquiveEchec >>>", rollData); - - // await RdDRollResult.displayRollData(rollData, this.defender, 'chat-resultat-esquive.hbs'); - - // this.removeChatMessageActionsPasseArme(rollData.passeArme); - // this._sendMessageDefense(rollData.attackerRoll, rollData, { defense: true }) - - await RdDRollResult.displayRollData(rollData, this.defender, 'chat-resultat-parade.hbs'); - - this.removeChatMessageActionsPasseArme(rollData.passeArme); - this._sendMessageDefense(rollData.attackerRoll, rollData, { defense: true }); - } - @@ -1214,7 +1204,7 @@ export class RdDCombat { // Est-ce une parade normale? if (defenderRoll.arme && attackerRoll && !defenderRoll.rolled.isPart) { // Est-ce que l'attaque est une particulière en force ou une charge - if (defenderRoll.needResist || this._isForceOuCharge(attackerRoll)) { + if (defenderRoll.needResist || this._isForceOuCharge(attackerRoll, defenderRoll.v2)) { defenderRoll.show = defenderRoll.show || {} @@ -1262,7 +1252,7 @@ export class RdDCombat { finalLevel: Misc.toInt(defenderRoll.competence.system.niveau) - dmg, showDice: HIDE_DICE }); - defenderRoll.show.desarme = desarme.rolled.isEchec; + defenderRoll.show.desarme = desarme.rolled.isEchec } } } @@ -1270,41 +1260,19 @@ export class RdDCombat { /* -------------------------------------------- */ async computeRecul(defenderRoll) { // Calcul du recul (p. 132) + if (!ReglesOptionnelles.isUsing('recul')) { + return + } const attackerRoll = defenderRoll.attackerRoll; - if (ReglesOptionnelles.isUsing('recul') && this._isForceOuCharge(attackerRoll)) { - const impact = this._computeImpactRecul(attackerRoll); - const rollRecul = await RdDResolutionTable.rollData({ caracValue: 10, finalLevel: impact }); - if (rollRecul.rolled.isSuccess) { - defenderRoll.show.recul = 'encaisse'; - } else if (rollRecul.rolled.isETotal || this._isReculCauseChute(impact)) { - defenderRoll.show.recul = 'chute'; - await this.defender.setEffect(STATUSES.StatusProne, true); - } - else { - defenderRoll.show.recul = 'recul'; - } + if (this._isForceOuCharge(attackerRoll, defenderRoll.v2)) { + defenderRoll.show.recul = this.defender.encaisserRecul(this.attacker.getForce(), attackerRoll.dmg.dmgArme) } } - /* -------------------------------------------- */ - async _isReculCauseChute(impact) { - const agilite = this.defender.getAgilite(); - const chute = await RdDResolutionTable.rollData({ caracValue: agilite, finalLevel: impact }); - return chute.rolled.isEchec; + _isForceOuCharge(attaque, isRollV2 = false /* TODO: delete roll V1 */) { + return attaque.particuliere == 'force' || 'charge' == (isRollV2 ? attaque.tactique?.key : attaque.tactique) } - /* -------------------------------------------- */ - _isForceOuCharge(attaque) { - return attaque.particuliere == 'force' || attaque.tactique == 'charge'; - } - - /* -------------------------------------------- */ - _computeImpactRecul(attaque) { - const taille = this.defender.getTaille(); - const force = this.attacker.getForce(); - const dommages = attaque.arme.system.dommagesReels ?? attaque.arme.system.dommages; - return taille - (force + dommages); - } /* -------------------------------------------- */ async encaisser(attackerRoll, defenderRoll) { @@ -1314,12 +1282,16 @@ export class RdDCombat { this._onEchecTotal(defenderRoll); } + await this.doRollEncaissement(attackerRoll, defenderRoll); + this.removeChatMessageActionsPasseArme(attackerRoll.passeArme); + } + + async doRollEncaissement(attackerRoll, defenderRoll) { if (Misc.isOwnerPlayer(this.defender)) { attackerRoll.attackerId = this.attackerId; attackerRoll.defenderTokenId = this.defenderToken.id; - await this.computeRecul(defenderRoll); - await this.defender.encaisserDommages(attackerRoll, this.attacker, defenderRoll?.show, this.attackerToken, this.defenderToken); + await this.defender.encaisserDommages(attackerRoll.dmg, this.attacker, defenderRoll?.show, this.attackerToken, this.defenderToken); } else { // envoi à un GM: les joueurs n'ont pas le droit de modifier les personnages qu'ils ne possèdent pas game.socket.emit(SYSTEM_SOCKET_ID, { @@ -1330,9 +1302,8 @@ export class RdDCombat { attackerToken: this.attackerToken, defenderToken: this.defenderToken } - }); + }) } - this.removeChatMessageActionsPasseArme(attackerRoll.passeArme); } /* -------------------------------------------- */ @@ -1366,6 +1337,3 @@ export class RdDCombat { } } -function newFunction(attackerRoll) { - return attackerRoll.diffLibre; -} diff --git a/module/rdd-main.js b/module/rdd-main.js index e08389af..4408de30 100644 --- a/module/rdd-main.js +++ b/module/rdd-main.js @@ -86,6 +86,7 @@ import { RdDCombatManager, RdDCombat } from "./rdd-combat.js" import { Migrations } from './migrations.js' import RollDialog from "./roll/roll-dialog.mjs" +import ChatRollResult from "./roll/chat-roll-result.mjs" /** * RdD system @@ -292,6 +293,7 @@ export class SystemReveDeDragon { TMRRencontres.init() ExportScriptarium.init() RollDialog.init() + ChatRollResult.init() } initSettings() { @@ -346,18 +348,7 @@ export class SystemReveDeDragon { }) } - static async setupAccueil() { - let exists = game.scenes.find(j => j.name == "Accueil RdD"); - if (!exists) { - const scenes = await SystemCompendiums.loadCompendium("foundryvtt-reve-de-dragon.scenes-rdd") - let newDocuments = scenes.filter(i => i.name == "Accueil RdD"); - await game.scenes.documentClass.create(newDocuments); - game.scenes.find(i => i.name == "Accueil RdD").activate(); - } - } - async onReady() { - /* -------------------------------------------- */ /* Foundry VTT Initialization */ /* -------------------------------------------- */ @@ -376,7 +367,8 @@ export class SystemReveDeDragon { StatusEffects.onReady() RdDDice.onReady() RollDialog.onReady() - RdDStatBlockParser.parseStatBlock() + ChatRollResult.onReady() + /* -------------------------------------------- */ /* Affiche/Init le calendrier */ game.system.rdd.calendrier.display() @@ -389,7 +381,17 @@ export class SystemReveDeDragon { }) } - SystemReveDeDragon.setupAccueil() + this.setupAccueil() + } + + async setupAccueil() { + let exists = game.scenes.find(j => j.name == "Accueil RdD"); + if (!exists) { + const scenes = await SystemCompendiums.loadCompendium("foundryvtt-reve-de-dragon.scenes-rdd") + let newDocuments = scenes.filter(i => i.name == "Accueil RdD"); + await game.scenes.documentClass.create(newDocuments); + game.scenes.find(i => i.name == "Accueil RdD").activate(); + } } /* -------------------------------------------- */ diff --git a/module/rdd-resolution-table.js b/module/rdd-resolution-table.js index 83d5b783..99d51688 100644 --- a/module/rdd-resolution-table.js +++ b/module/rdd-resolution-table.js @@ -157,7 +157,7 @@ export class RdDResolutionTable { /* -------------------------------------------- */ static significativeRequise(chances) { - chances.roll = Math.floor(chances.score / 2); + chances.roll = Math.min(chances.part + 1, chances.sign) foundry.utils.mergeObject(chances, reussites.find(x => x.code == 'sign'), { overwrite: true }); } @@ -188,7 +188,7 @@ export class RdDResolutionTable { static computeReussite(chances, roll, diviseur) { const reussite = reussites.find(x => x.condition(chances, roll)) if (diviseur > 1 && reussite.isSuccess) { - if (chances > roll * diviseur){ + if (chances > roll * diviseur) { return reussiteInsuffisante } } diff --git a/module/rdd-roll-encaisser.js b/module/rdd-roll-encaisser.js index 9b5728bc..e11b3a23 100644 --- a/module/rdd-roll-encaisser.js +++ b/module/rdd-roll-encaisser.js @@ -66,12 +66,11 @@ export class RdDEncaisser extends Dialog { /* -------------------------------------------- */ performEncaisser(mortalite) { this.actor.encaisserDommages({ - dmg: { - total: Number(this.modifier), - ajustement: Number(this.modifier), - encaisserSpecial: this.encaisserSpecial, - mortalite: mortalite - } + total: Number(this.modifier), + ajustement: Number(this.modifier), + encaisserSpecial: this.encaisserSpecial, + mortalite: mortalite, + penetration: 0 }) } } diff --git a/module/rdd-roll.js b/module/rdd-roll.js index 58a1fec0..7ae8900d 100644 --- a/module/rdd-roll.js +++ b/module/rdd-roll.js @@ -9,6 +9,7 @@ import { RdDResolutionTable } from "./rdd-resolution-table.js"; import { ReglesOptionnelles } from "./settings/regles-optionnelles.js"; import { Grammar } from "./grammar.js"; import { ACTOR_TYPES } from "./constants.js"; +import { RdDUtility } from "./rdd-utility.js"; /** * Extend the base Dialog entity to select roll parameters diff --git a/module/rdd-utility.js b/module/rdd-utility.js index 69b802b5..05c2f61e 100644 --- a/module/rdd-utility.js +++ b/module/rdd-utility.js @@ -610,35 +610,33 @@ export class RdDUtility { /* -------------------------------------------- */ static async getLocalisation(type = 'personnage') { - let result = await RdDDice.rollTotal("1d20"); - let txt = "" + const loc = { result: await RdDDice.rollTotal("1d20")}; if (type == 'personnage') { - if (result <= 3) txt = "Jambe, genou, pied, jarret"; - else if (result <= 7) txt = "Hanche, cuisse, fesse"; - else if (result <= 9) txt = "Ventre, reins"; - else if (result <= 12) txt = "Poitrine, dos"; - else if (result <= 14) txt = "Avant-bras, main, coude"; - else if (result <= 18) txt = "Epaule, bras, omoplate"; - else if (result == 19) txt = "Tête"; - else if (result == 20) txt = "Tête (visage)"; + if (loc.result <= 3) loc.txt = "Jambe, genou, pied, jarret"; + else if (loc.result <= 7) loc.txt = "Hanche, cuisse, fesse"; + else if (loc.result <= 9) loc.txt = "Ventre, reins"; + else if (loc.result <= 12) loc.txt = "Poitrine, dos"; + else if (loc.result <= 14) loc.txt = "Avant-bras, main, coude"; + else if (loc.result <= 18) loc.txt = "Epaule, bras, omoplate"; + else if (loc.result == 19) loc.txt = "Tête"; + else if (loc.result == 20) loc.txt = "Tête (visage)"; } else { - if (result <= 7) txt = "Jambes/Pattes"; - else if (result <= 18) txt = "Corps"; - else if (result <= 20) txt = "Tête"; + if (loc.result <= 7) loc.txt = "Jambes/Pattes"; + else if (loc.result <= 18) loc.txt = "Corps"; + else if (loc.result <= 20) loc.txt = "Tête"; } - - return { result: result, label: txt }; + return loc } /* -------------------------------------------- */ - static async jetEncaissement(actor, rollData, armure, options = { showDice: HIDE_DICE }) { - const diff = Math.abs(rollData.diffLibre); - let formula = RdDUtility.formuleEncaissement(diff, options) + static async jetEncaissement(actor, dmg, armure, options = { showDice: HIDE_DICE }) { + const diff = Math.abs(dmg.diff) + const formula = RdDUtility.formuleEncaissement(diff, options) const roll = await RdDDice.roll(formula, options); RdDUtility.remplaceDeMinParDifficulte(roll, diff, options); - return await RdDUtility.prepareEncaissement(actor, rollData, roll, armure); + return await RdDUtility.prepareEncaissement(actor, dmg, roll, armure); } static remplaceDeMinParDifficulte(roll, diff, options) { @@ -661,7 +659,7 @@ export class RdDUtility { } } - static formuleEncaissement(diff, options) { + static formuleEncaissement(diff) { // Chaque dé fait au minimum la difficulté libre if (ReglesOptionnelles.isUsing('degat-minimum-malus-libre')) { return `2d10min${diff}` @@ -670,25 +668,22 @@ export class RdDUtility { } /* -------------------------------------------- */ - static async prepareEncaissement(actor, rollData, roll, armure) { - // La difficulté d'ataque s'ajoute aux dégâts - const bonusDegatsDiffLibre = ReglesOptionnelles.isUsing('degat-ajout-malus-libre') ? Math.abs(rollData.diffLibre ?? 0) : 0 - const jetTotal = roll.total + rollData.dmg.total - armure + bonusDegatsDiffLibre - const encaissement = RdDUtility._selectEncaissement(jetTotal, rollData.dmg.mortalite); + static async prepareEncaissement(actor, dmg, roll, armure) { + const jetTotal = roll.total + dmg.total - armure + const encaissement = RdDUtility._selectEncaissement(jetTotal, dmg.mortalite); const over20 = Math.max(jetTotal - 20, 0); - encaissement.dmg = rollData.dmg + encaissement.dmg = dmg if (ReglesOptionnelles.isUsing('localisation-aleatoire')) { - encaissement.dmg.loc = rollData.dmg.loc ?? await RdDUtility.getLocalisation(actor.type) + encaissement.dmg.loc = dmg.loc ?? await RdDUtility.getLocalisation(actor.type) encaissement.dmg.loc.label = encaissement.dmg.loc.label ?? 'Corps;' } else { encaissement.dmg.loc = { label: '' } } - encaissement.dmg.bonusDegatsDiffLibre = bonusDegatsDiffLibre - encaissement.roll = roll; - encaissement.armure = armure; - encaissement.penetration = rollData.arme?.system.penetration ?? 0; - encaissement.total = jetTotal; + encaissement.roll = roll + encaissement.armure = armure + encaissement.penetration = dmg.penetration + encaissement.total = jetTotal encaissement.vie = await RdDUtility._evaluatePerte(encaissement.vie, over20); encaissement.endurance = await RdDUtility._evaluatePerte(encaissement.endurance, over20); return encaissement; diff --git a/module/roll/chat-roll-result.mjs b/module/roll/chat-roll-result.mjs new file mode 100644 index 00000000..7c22ad21 --- /dev/null +++ b/module/roll/chat-roll-result.mjs @@ -0,0 +1,180 @@ +import { ChatUtility } from "../chat-utility.js" +import RollDialog from "./roll-dialog.mjs" +import { RdDCarac } from "../rdd-carac.js" +import { RdDCombat } from "../rdd-combat.js" +import { ROLL_TYPE_ATTAQUE, ROLL_TYPE_DEFENSE } from "./roll-constants.mjs" +import { RdDResolutionTable } from "../rdd-resolution-table.js" + +export default class ChatRollResult { + static init() { + ChatRollResult.instance = new ChatRollResult() + + Hooks.on('renderChatLog', (log, html, chatLog) => ChatRollResult.instance.chatListeners(html)) + } + + static onReady() { + foundry.applications.handlebars.loadTemplates({ + 'partial-appel-chance': 'systems/foundryvtt-reve-de-dragon/templates/roll/result/partial-appel-chance.hbs', + 'partial-encaissement': 'systems/foundryvtt-reve-de-dragon/templates/roll/result/partial-encaissement.hbs', + 'partial-recul-choc': 'systems/foundryvtt-reve-de-dragon/templates/roll/result/partial-recul-choc.hbs', + 'partial-info-appel-moral': 'systems/foundryvtt-reve-de-dragon/templates/roll/result/partial-info-appel-moral.hbs', + }) + } + + async display(roll) { + this.prepareDisplay(roll) + + const chatMessage = await ChatUtility.createChatWithRollMode( + { + content: await this.buildRollHtml(roll) + }, + roll.active.actor, + roll.current?.rollmode?.key + ) + const save = RollDialog.saveParts(roll) + ChatUtility.setMessageData(chatMessage, 'rollData', save) + return chatMessage + } + + prepareDisplay(roll) { + roll.done = roll.done || {} + roll.show = roll.show || {} + roll.show.chance = this.isAppelChancePossible(roll) + roll.show.encaissement = this.isShowEncaissement(roll) + roll.show.recul = this.getReculChoc(roll) + + } + + isAppelChancePossible(roll) { + return roll.active.actor.isPersonnage() && + roll.rolled.isEchec && + RdDCarac.isActionPhysique(roll.current.carac?.key) + } + + isShowEncaissement(roll) { + return roll.rolled.isEchec && + roll.attackerRoll?.dmg.mortalite != 'empoignade' + } + + getReculChoc(roll, defender = roll.active.actor, attacker = roll.opponent.actor) { + const attaque = roll.attackerRoll + if (attaque && + (roll.rolled.isEchec || !roll.current.defense.isEsquive) && + (attaque.particuliere == 'force' || 'charge' == attaque.tactique?.key)) { + const taille = defender.system.carac.taille.value + const impact = attacker.system.carac.force.value + roll.attackerRoll?.dmg.dmgArme + return { + raison: 'charge' == attaque.tactique?.key ? 'charge' : 'particulière en force', + taille: taille, + impact: impact, + chances: RdDResolutionTable.computeChances(10, taille-impact).norm, + diff: taille - impact + } + } + return undefined + } + + async buildRollHtml(roll) { + const template = `systems/foundryvtt-reve-de-dragon/templates/roll/result/chat-${roll.type.current}.hbs` + return await foundry.applications.handlebars.renderTemplate(template, roll) + } + + async chatListeners(html) { + $(html).on("click", '.appel-chance', event => this.onClickAppelChance(event)) + $(html).on("click", '.appel-destinee', event => this.onClickAppelDestinee(event)) + $(html).on("click", '.encaissement', event => this.onClickEncaissement(event)) + $(html).on("click", '.resister-recul', event => this.onClickRecul(event)) + + } + + getCombat(roll) { + switch (roll.type.current) { + case ROLL_TYPE_DEFENSE: + return RdDCombat.rddCombatForAttackerAndDefender(roll.ids.opponentId, roll.ids.opponentTokenId, roll.ids.actorTokenId) + case ROLL_TYPE_ATTAQUE: + return RdDCombat.rddCombatForAttackerAndDefender(roll.ids.actorId, roll.ids.actorTokenId, roll.ids.opponentId) + } + return undefined + } + + async updateChatMessage(chatMessage, savedRoll) { + ChatUtility.setMessageData(chatMessage, 'rollData', savedRoll) + const copy = foundry.utils.duplicate(savedRoll) + RollDialog.loadRollData(copy) + this.prepareDisplay(copy) + chatMessage.update({ content: await this.buildRollHtml(copy) }) + chatMessage.render(true) + } + + onClickAppelChance(event) { + const chatMessage = ChatUtility.getChatMessage(event) + const savedRoll = ChatUtility.getMessageData(chatMessage, 'rollData') + const actor = game.actors.get(savedRoll.ids.actorId) + actor.rollAppelChance( + () => this.onAppelChanceSuccess(savedRoll, chatMessage), + () => this.onAppelChanceEchec(savedRoll, chatMessage)) + event.preventDefault() + } + + onAppelChanceSuccess(savedRoll, chatMessage) { + const reRoll = foundry.utils.duplicate(savedRoll) + reRoll.type.retry = true + const callbacks = [r => ChatUtility.removeChatMessageId(chatMessage.id)] + // TODO: annuler les effets + switch (reRoll.type.current) { + case ROLL_TYPE_DEFENSE: + this.getCombat(reRoll)?.doRollDefense(reRoll, callbacks) + break + case ROLL_TYPE_ATTAQUE: + // TODO + this.getCombat(reRoll)?.doRollAttaque(reRoll, callbacks) + break + default: { + RollDialog.create(reRoll, { callbacks: callbacks }) + } + } + } + + async onAppelChanceEchec(savedRoll, chatMessage) { + savedRoll.type.retry = true + await this.updateChatMessage(chatMessage, savedRoll) + } + + onClickAppelDestinee(event) { + const chatMessage = ChatUtility.getChatMessage(event) + const savedRoll = ChatUtility.getMessageData(chatMessage, 'rollData') + const actor = game.actors.get(savedRoll.ids.actorId) + + actor.appelDestinee(async () => { + const reRoll = foundry.utils.duplicate(savedRoll) + reRoll.type.retry = true + RdDResolutionTable.significativeRequise(reRoll.rolled) + await this.updateChatMessage(chatMessage, reRoll) + }) + } + + async onClickEncaissement(event) { + const chatMessage = ChatUtility.getChatMessage(event) + const savedRoll = ChatUtility.getMessageData(chatMessage, 'rollData') + const attaque = savedRoll.attackerRoll + const defender = game.actors.get(savedRoll.ids.actorId) + const attacker = game.actors.get(savedRoll.ids.opponentId) + const defenderToken = savedRoll.ids.actorTokenId ? canvas.tokens.get(savedRoll.ids.actorTokenId) : undefined + const attackerToken = savedRoll.ids.opponentTokenId ? canvas.tokens.get(savedRoll.ids.opponentTokenId) : undefined + await defender?.encaisserDommages(attaque.dmg, attacker, undefined, attackerToken, defenderToken) + + savedRoll.done.encaissement = true + await this.updateChatMessage(chatMessage, savedRoll) + } + + async onClickRecul(event) { + const chatMessage = ChatUtility.getChatMessage(event) + const savedRoll = ChatUtility.getMessageData(chatMessage, 'rollData') + const defender = game.actors.get(savedRoll.ids.actorId) + const attacker = game.actors.get(savedRoll.ids.opponentId) + savedRoll.done.recul = await defender.encaisserRecul(attacker.getForce(), savedRoll.attackerRoll.dmg.dmgArme) + // const reculChoc = this.getReculChoc(savedRoll, defender, attacker) + await this.updateChatMessage(chatMessage, savedRoll) + } + +} \ No newline at end of file diff --git a/module/roll/roll-basic-parts.mjs b/module/roll/roll-basic-parts.mjs index 212462b5..3a1c5746 100644 --- a/module/roll/roll-basic-parts.mjs +++ b/module/roll/roll-basic-parts.mjs @@ -41,9 +41,7 @@ export class RollBasicParts { initFrom(rollData) { return { selected: {}, - type: { - current: rollData.type.current - }, + type: rollData.type, ids: { sceneId: rollData.ids.sceneId, actorId: rollData.active.id, diff --git a/module/roll/roll-chat-result.mjs b/module/roll/roll-chat-result.mjs deleted file mode 100644 index 045b2b6e..00000000 --- a/module/roll/roll-chat-result.mjs +++ /dev/null @@ -1,15 +0,0 @@ -export class RollChatResult { - constructor(rollType) { - this.rollType = rollType - } - - async display(rollData) { - const template = this.rollType.chatResultTemplate() - const chatContent = await renderTemplate(template, rollData) - - ChatMessage.create({ - whisper: ChatUtility.getOwners(this), - content: chatContent - }) - } -} \ No newline at end of file diff --git a/module/roll/roll-dialog-adapter.mjs b/module/roll/roll-dialog-adapter.mjs index 76b88c9d..55beea84 100644 --- a/module/roll/roll-dialog-adapter.mjs +++ b/module/roll/roll-dialog-adapter.mjs @@ -6,13 +6,12 @@ import { ReglesOptionnelles } from "../settings/regles-optionnelles.js"; import { PART_OEUVRE } from "./roll-part-oeuvre.mjs"; import { RdDItemArme } from "../item/arme.js"; import { RdDBonus } from "../rdd-bonus.js"; -import { ROLL_TYPE_ATTAQUE } from "./roll-constants.mjs"; /* -------------------------------------------- */ export class RollDialogAdapter { - async rollDice(rollData, rollTitle) { - const chances = this.computeChances({ + static async rollDice(rollData, rollTitle) { + const chances = RollDialogAdapter.computeChances({ carac: rollData.current.carac.value, diff: rollData.current.totaldiff, bonus: rollData.current.bonus, @@ -21,13 +20,13 @@ export class RollDialogAdapter { rollMode: rollData.current.rollmode.key }) - const rolled = await this.rollChances(rollData, chances) - this.adjustRollDataForV1(rollData, rolled, rollTitle) + const rolled = await RollDialogAdapter.rollChances(rollData, chances) + RollDialogAdapter.adjustRollDataForV1(rollData, rolled, rollTitle) return rolled } - computeChances({ carac, diff, bonus, sign, showDice, rollMode }) { + static computeChances({ carac, diff, bonus, sign, showDice, rollMode }) { const chances = foundry.utils.duplicate(RdDResolutionTable.computeChances(carac, diff)) RdDResolutionTable._updateChancesWithBonus(chances, bonus, diff) RdDResolutionTable._updateChancesFactor(chances, sign) @@ -36,7 +35,7 @@ export class RollDialogAdapter { return chances } - async rollChances(rollData, chances) { + static async rollChances(rollData, chances) { const rolled = await RdDResolutionTable.rollChances(chances, rollData.current.sign, rollData.current.resultat) @@ -47,7 +46,7 @@ export class RollDialogAdapter { return rolled } - adjustRollDataForV1(rollData, rolled, rollTitle) { + static adjustRollDataForV1(rollData, rolled, rollTitle) { // temporaire pour être homogène roll v1 rollData.alias = rollData.active.actor.getAlias() // pour experience diff --git a/module/roll/roll-dialog.mjs b/module/roll/roll-dialog.mjs index 2c9e61d9..2ecc326e 100644 --- a/module/roll/roll-dialog.mjs +++ b/module/roll/roll-dialog.mjs @@ -25,7 +25,7 @@ import { RollPartMeditation } from "./roll-part-meditation.mjs"; import { RollPartMoral } from "./roll-part-moral.mjs"; import { RollPartOpponent } from "./roll-part-opponent.mjs"; import { RollPartSurEnc } from "./roll-part-surenc.mjs"; -import { RollPartTricher } from "./roll-part-tricher.mjs"; +import { PART_TRICHER, RollPartTricher } from "./roll-part-tricher.mjs"; import { RollPartTache } from "./roll-part-tache.mjs"; import { RollPartOeuvre } from "./roll-part-oeuvre.mjs"; import { RollPartSort } from "./roll-part-sort.mjs"; @@ -38,7 +38,8 @@ import { RollPartDefense } from "./roll-part-defense.mjs"; import { RollDialogAdapter } from "./roll-dialog-adapter.mjs"; import { ROLLDIALOG_SECTION } from "./roll-part.mjs"; import { ROLL_TYPE_COMP } from "./roll-constants.mjs"; -import { RollChatResult } from "./roll-chat-result.mjs"; +import ChatRollResult from "./chat-roll-result.mjs"; + const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api const doNothing = (dialog) => { } @@ -48,11 +49,12 @@ const ALL_ROLL_TYPES = [ new RollTypeTache(), new RollTypeAttaque(), new RollTypeDefense(), - // new RollTypeResistance ?? new RollTypeSort(), new RollTypeMeditation(), new RollTypeOeuvre(), new RollTypeJeu(), + // new RollTypeResistance ?? + // new RollTypeFixedCarac ?? ] const BASIC_PARTS = new RollBasicParts() @@ -182,6 +184,8 @@ export default class RollDialog extends HandlebarsApplicationMixin(ApplicationV2 'roll-button': 'systems/foundryvtt-reve-de-dragon/templates/roll/roll-button.hbs', }) + ChatRollResult.onReady() + foundry.applications.handlebars.loadTemplates(ALL_ROLL_TYPES.map(m => m.template)) foundry.applications.handlebars.loadTemplates(ROLL_PARTS.map(p => p.template)) ROLL_PARTS.forEach(p => p.onReady()) @@ -246,9 +250,52 @@ export default class RollDialog extends HandlebarsApplicationMixin(ApplicationV2 // rien pour l'instant } + /** pre-configure les paramètres des différentes parties de la fenêtre (par exemple, prépare les listes de caractéristiques/compétences */ + static $prepareRollData(rollData) { + rollData.current = rollData.current ?? {} + rollData.selected = rollData.selected ?? {} + rollData.type = rollData.type ?? {} + rollData.type.retry = rollData.type.retry ?? false + BASIC_PARTS.restore(rollData) + + const potential = ALL_ROLL_TYPES.find(m => m.code == rollData.type.current)?.code + const allowed = rollData.type.retry && potential + ? [potential] + : (rollData.type.allowed ?? ALL_ROLL_TYPES.filter(m => m.isAllowed(rollData) && m.visible(rollData)).map(m => m.code)) + const rollType = allowed.find(c => c == rollData.type.current) ?? (allowed.length > 0 ? allowed[0].code : ROLL_TYPE_COMP); + + rollData.type.allowed = allowed + rollData.type.current = rollType + ALL_ROLL_TYPES.find(m => m.code == rollType).setRollDataType(rollData) + + rollData.refs = foundry.utils.mergeObject(rollData.refs ?? {}, Object.fromEntries(ROLL_PARTS.map(p => [p.code, {}]))); + rollData.options = rollData.options ?? { rollMode: game.settings.get("core", "rollMode") } + + ROLL_PARTS.forEach(p => p.initialize(rollData)) + ROLL_PARTS.forEach(p => p.restore(rollData)) + ROLL_PARTS.filter(p => p.isValid(rollData)) + .forEach(p => { + p.loadRefs(rollData) + p.prepareContext(rollData) + }) + return rollData + } + + static saveParts(rollData) { + const target = BASIC_PARTS.initFrom(rollData) + ROLL_PARTS.filter(p => p.isActive(rollData)) + .forEach(p => p.storeClean(rollData, target)) + target.attackerRoll = rollData.attackerRoll + target.rolled = rollData.rolled + target.result = rollData.result + target.done = target.done ?? {} + return target + } + constructor(rollData, rollOptions) { super() - this.rollData = rollData + + this.rollData = RollDialog.$prepareRollData(rollData) this.rollOptions = { callbacks: [ async r => await r.active.actor.appliquerAjoutExperience(r), @@ -258,37 +305,8 @@ export default class RollDialog extends HandlebarsApplicationMixin(ApplicationV2 customChatMessage: rollOptions.customChatMessage, onRollDone: rollOptions.onRollDone ?? doNothing } - this.$loadParts() - } - - /** pre-configure les paramètres des différentes parties de la fenêtre (par exemple, prépare les listes de caractéristiques/compétences */ - $loadParts() { - const rollData = this.rollData; - rollData.current = rollData.current ?? {} - rollData.selected = rollData.selected ?? {} - rollData.type = rollData.type ?? {} - rollData.type.retry = rollData.type.retry ?? false - BASIC_PARTS.restore(rollData) - - const loadedType = ALL_ROLL_TYPES.find(m => m.code == rollData.type?.current)?.code - const allowedTypes = ALL_ROLL_TYPES.filter(m => m.isAllowed(rollData) && m.visible(rollData)).map(m => m.code) - - rollData.type.allowed = rollData.type.retry ? [loadedType] : rollData.type.allowed ?? ALL_ROLL_TYPES.map(m => m.code) - rollData.type.current = allowedTypes.find(m => m == rollData.type?.current) ?? (allowedTypes.length > 0 ? allowedTypes[0] : ROLL_TYPE_COMP) - - this.getSelectedType().setRollDataType(rollData) - - rollData.refs = this.$prepareRefs(rollData) - rollData.options = rollData.options ?? { showDice: true, rollMode: game.settings.get("core", "rollMode") } - - ROLL_PARTS.forEach(p => p.initialize(rollData)) - ROLL_PARTS.forEach(p => p.restore(rollData)) - ROLL_PARTS.filter(p => p.isValid(rollData)) - .forEach(p => { - p.loadRefs(rollData) - p.prepareContext(rollData) - }) - this.selectType(); + this.chatRollResult = new ChatRollResult(); + this.selectType() } selectType() { @@ -301,23 +319,21 @@ export default class RollDialog extends HandlebarsApplicationMixin(ApplicationV2 ROLL_PARTS.find(it => it.code == PART_COMP).filterComps(this.rollData) } - $prepareRefs(rollData) { - return foundry.utils.mergeObject(rollData.refs ?? {}, Object.fromEntries(ROLL_PARTS.map(p => [p.code, {}]))); + + static getActiveParts(rollData) { + return ROLL_PARTS.filter(p => p.isActive(rollData)) } - $saveParts() { - const target = BASIC_PARTS.initFrom(this.rollData) - ROLL_PARTS.filter(p => p.isActive(this.rollData)) - .forEach(p => p.store(this.rollData, target)) - return target - } + // get title() { + // return this.rollData.title ?? `Jet de dés de ${this.rollData.active.actor.name}` + // } - getActiveParts() { - return ROLL_PARTS.filter(p => p.isActive(this.rollData)) - } - - get title() { - return this.rollData.title ?? `Jet de dés de ${this.rollData.active.actor.name}` + rollTitle(rollData) { + return rollData.label ?? ROLL_PARTS + .filter(it => it.section == ROLLDIALOG_SECTION.ACTION) + .filter(it => it.isActive(rollData)) + .map(it => it.title(rollData)) + .reduce(Misc.joining(' ')) } async _onRender(context, options) { @@ -340,13 +356,13 @@ export default class RollDialog extends HandlebarsApplicationMixin(ApplicationV2 )) Promise.all( - this.getActiveParts().map(async p => await p._onRender(this, context, options)) + RollDialog.getActiveParts(this.rollData).map(async p => await p._onRender(this, context, options)) ) } - getAjustements() { - return this.getActiveParts() - .map(p => p.getAjustements(this.rollData)) + static getAjustements(rollData) { + return RollDialog.getActiveParts(rollData) + .map(p => p.getAjustements(rollData)) .reduce((a, b) => a.concat(b)) .sort((a, b) => a.diff == undefined ? 1 : b.diff == undefined ? -1 : 0) } @@ -360,19 +376,19 @@ export default class RollDialog extends HandlebarsApplicationMixin(ApplicationV2 const types = ALL_ROLL_TYPES.filter(m => m.isAllowed(rollData) && m.visible(rollData)) .map(m => m.toTypeData(rollData)) - BASIC_PARTS.loadSurprises(this.rollData, this.getSelectedType().code) - this.rollData.type.label = this.getSelectedType()?.title(this.rollData) + BASIC_PARTS.loadSurprises(rollData, this.getSelectedType().code) + rollData.type.label = this.getSelectedType()?.title(rollData) //TOCHECK: set type.label ? - const visibleRollParts = this.getActiveParts() + const visibleRollParts = RollDialog.getActiveParts(rollData) visibleRollParts.forEach(p => p.applyExternalImpacts(visibleRollParts, rollData)) this.setSpecialComp(visibleRollParts); visibleRollParts.forEach(p => p.prepareContext(rollData)) - this.calculAjustements() + RollDialog.calculAjustements(rollData) - const templates = this.getActiveParts().map(p => p.toTemplateData()) + const templates = RollDialog.getActiveParts(rollData).map(p => p.toTemplateData()) const context = await super._prepareContext() return foundry.utils.mergeObject( { @@ -386,16 +402,16 @@ export default class RollDialog extends HandlebarsApplicationMixin(ApplicationV2 const specialComp = visibleRollParts.map(p => p.getSpecialComp(this.rollData)) .reduce((a, b) => a.concat(b)) if (specialComp.length > 0) { - const rollPartComp = this.getActiveParts() + const rollPartComp = RollDialog.getActiveParts(this.rollData) .find(it => it.code == PART_COMP); rollPartComp?.setSpecialComp(this.rollData, specialComp) } } - calculAjustements() { - this.rollData.ajustements = this.getAjustements() - this.rollData.ajustements.forEach(it => it.isDiff = it.diff != undefined) - this.rollData.current.totaldiff = this.rollData.ajustements + static calculAjustements(rollData) { + rollData.ajustements = RollDialog.getAjustements(rollData) + rollData.ajustements.forEach(it => it.isDiff = it.diff != undefined) + rollData.current.totaldiff = rollData.ajustements .map(adj => adj.diff) .filter(d => d != undefined) .reduce(Misc.sum(), 0) @@ -406,20 +422,26 @@ export default class RollDialog extends HandlebarsApplicationMixin(ApplicationV2 } async roll() { - this.calculAjustements() - const rollData = this.rollData - console.info('Roll parts:', this.$saveParts()) - const rolled = await this.$rollDice(rollData) - rollData.rolled = rolled - Promise.all(this.rollOptions.callbacks.map(async callback => await callback(rollData))) - if (!this.rollOptions.customChatMessage) { - const rollChatResult = new RollChatResult(this.getSelectedType()) - await rollChatResult.display(this.rollData) - rollData.active.actor.$onRollCompetence(this.rollData) - } + // ROLL_PARTS.filter(p => p.isActive(this.rollData)) + // .forEach(p => p.validate(this.rollData)) + + const roll = RollDialog.saveParts(this.rollData) + RollDialog.loadRollData(roll) + roll.current.resultat = this.rollData.current[PART_TRICHER]?.resultat ?? -1 + roll.rolled = await this.$rollDice(roll) + roll.result = this.getSelectedType(roll).getResult(roll) + console.info('RollDialog.roll:', roll) + await Promise.all(this.rollOptions.callbacks.map(async callback => await callback(roll))) + await this.chatRollResult.display(roll) + this.rollOptions.onRollDone(this) } + static loadRollData(roll) { + RollDialog.$prepareRollData(roll) + RollDialog.calculAjustements(roll) + roll.v2 = true + } async defaultCallback(rollData, rolled) { await rollData.active.actor.appliquerAjoutExperience(rollData) @@ -427,15 +449,6 @@ export default class RollDialog extends HandlebarsApplicationMixin(ApplicationV2 } async $rollDice(rollData) { - const adapter = new RollDialogAdapter(ROLL_PARTS); - return await adapter.rollDice(rollData, this.rollTitle(rollData)); - } - - rollTitle(rollData) { - return ROLL_PARTS - .filter(it => it.section == ROLLDIALOG_SECTION.ACTION) - .filter(it => it.isActive(rollData)) - .map(it => it.title(rollData)) - .reduce(Misc.joining(' ')) + return await RollDialogAdapter.rollDice(rollData, this.rollTitle(rollData)) } } diff --git a/module/roll/roll-part-attaque.mjs b/module/roll/roll-part-attaque.mjs index ec78c3ef..ca2f41e5 100644 --- a/module/roll/roll-part-attaque.mjs +++ b/module/roll/roll-part-attaque.mjs @@ -1,10 +1,9 @@ import { RdDBonus } from "../rdd-bonus.js" -import { StatusEffects } from "../settings/status-effects.js" +import { ReglesOptionnelles } from "../settings/regles-optionnelles.js" import { ROLL_TYPE_ATTAQUE } from "./roll-constants.mjs" import { PART_CARAC } from "./roll-part-carac.mjs" import { PART_COMP } from "./roll-part-comp.mjs" import { RollPartSelect } from "./roll-part-select.mjs" -import { PART_SIGN } from "./roll-part-sign.mjs" import { ROLLDIALOG_SECTION } from "./roll-part.mjs" export const PART_ATTAQUE = 'attaque' @@ -28,6 +27,16 @@ export class RollPartAttaque extends RollPartSelect { } } + store(rollData, targetData) { + super.store(rollData, targetData) + this.getSaved(targetData).dmg = this.getCurrent(rollData).dmg + } + + restore(rollData) { + super.restore(rollData) + this.getCurrent(rollData).dmg = this.getSaved(rollData).dmg + } + choices(refs) { return refs.attaques } static $extractAttaque(attaque, actor) { @@ -41,28 +50,7 @@ export class RollPartAttaque extends RollPartSelect { prepareContext(rollData) { const current = this.getCurrent(rollData) - current.dmg = this.$dmgRollV2(rollData, current) - } - - $dmgRollV2(rollData, current) { - const actor = rollData.active.actor - const attaque = current.attaque - const arme = attaque.arme - const dmgArme = RdDBonus.dmgArme(arme, attaque.dommagesArme) - - const dmg = { - total: 0, - dmgArme: dmgArme, - penetration: arme.penetration(), - dmgTactique: current.tactique?.dmg ?? 0, - dmgParticuliere: 0, // TODO RdDBonus._dmgParticuliere(rollData), - dmgSurprise: rollData.opponent?.surprise?.dmg ?? 0, - mortalite: RdDBonus.mortalite(current.dmg?.mortalite, arme.system.mortalite, rollData.opponent?.actor?.isEntite()), - dmgActor: RdDBonus.bonusDmg(actor, attaque.carac.key, dmgArme, attaque.forceRequise), - dmgForceInsuffisante: Math.min(0, actor.getForce() - attaque.forceRequise) - } - dmg.total = dmg.dmgSurprise + dmg.dmgTactique + dmg.dmgArme + dmg.dmgActor + dmg.dmgParticuliere + dmg.dmgForceInsuffisante - return dmg + current.dmg = RdDBonus.dmgRollV2(rollData, current) } getAjustements(rollData) { diff --git a/module/roll/roll-part-carac.mjs b/module/roll/roll-part-carac.mjs index 4475c47f..b143859f 100644 --- a/module/roll/roll-part-carac.mjs +++ b/module/roll/roll-part-carac.mjs @@ -43,6 +43,7 @@ export class RollPartCarac extends RollPartSelect { // ? refs.all.filter(it => allowed.includes(Grammar.toLowerCaseNoAccent(it.key))) ? refs.all.filter(it => allowed.includes(it.key)) : refs.all + this.$selectCarac(rollData) } prepareContext(rollData) { diff --git a/module/roll/roll-part-comp.mjs b/module/roll/roll-part-comp.mjs index b7b261e3..0463307f 100644 --- a/module/roll/roll-part-comp.mjs +++ b/module/roll/roll-part-comp.mjs @@ -52,6 +52,7 @@ export class RollPartComp extends RollPartSelect { // ? refs.all.filter(it => allowed.includes(Grammar.toLowerCaseNoAccent(it.label))) ? refs.all.filter(it => allowed.includes(it.label)) : refs.all + this.$selectComp(rollData) } prepareContext(rollData) { diff --git a/module/roll/roll-part-conditions.mjs b/module/roll/roll-part-conditions.mjs index d686b047..909d7233 100644 --- a/module/roll/roll-part-conditions.mjs +++ b/module/roll/roll-part-conditions.mjs @@ -1,4 +1,5 @@ import { SYSTEM_RDD } from "../constants.js"; +import { Misc } from "../misc.js"; import { ROLLDIALOG_SECTION, RollPart } from "./roll-part.mjs"; const CONDITIONS = "conditions" @@ -50,7 +51,7 @@ export class RollPartConditions extends RollPart { const current = this.getCurrent(rollData) current.min = game.settings.get(SYSTEM_RDD, this.settingMin()) current.max = game.settings.get(SYSTEM_RDD, this.settingMax()) - current.value = current.value ?? 0 + current.value = Misc.inRange(current.value ?? 0, current.min, current.max) } getAjustements(rollData) { @@ -63,12 +64,13 @@ export class RollPartConditions extends RollPart { async _onRender(rollDialog, context, options) { const input = rollDialog.element.querySelector(`roll-section[name="${this.code}"] input[name="${this.code}"]`) - - input?.addEventListener("change", e => { - const current = this.getCurrent(rollDialog.rollData) - current.value = parseInt(e.currentTarget.value) - rollDialog.render() - }) + + input?.addEventListener("input", e => this.onInputChange(e, rollDialog)) + } + + onInputChange(event, rollDialog) { + this.getCurrent(rollDialog.rollData).value = parseInt(event.currentTarget.value) + rollDialog.render() } } \ No newline at end of file diff --git a/module/roll/roll-part-defense.mjs b/module/roll/roll-part-defense.mjs index dda56974..82d33c51 100644 --- a/module/roll/roll-part-defense.mjs +++ b/module/roll/roll-part-defense.mjs @@ -27,14 +27,14 @@ export class RollPartDefense extends RollPartSelect { loadRefs(rollData) { const refs = this.getRefs(rollData) - const attaque = rollData.attaque + const attackerRoll = rollData.attackerRoll const defenseur = rollData.active.actor - refs.isDistance = [ATTAQUE_TYPE.TIR, ATTAQUE_TYPE.LANCER].find(it => it == attaque?.main) + refs.isDistance = [ATTAQUE_TYPE.TIR, ATTAQUE_TYPE.LANCER].find(it => it == attackerRoll?.main) const esquives = refs.isDistance == ATTAQUE_TYPE.TIR ? [] : defenseur.getCompetencesEsquive() .map(it => RollPartDefense.$extractEsquive(it, defenseur)) const parades = defenseur.items.filter(it => it.isParade() && (!refs.isDistance || it.isBouclier())) - .map(it => RollPartDefense.$extractParade(it, attaque?.attaque.arme, defenseur)) + .map(it => RollPartDefense.$extractParade(it, attackerRoll?.attaque.arme, defenseur)) refs.defenses = [...esquives, ...parades].filter(it => it != undefined) this.$selectDefense(rollData) @@ -48,7 +48,8 @@ export class RollPartDefense extends RollPartSelect { // TODO: carac pour créatures carac: defenseur.isPersonnage() ? CARACS.DEROBEE : esquive.name, verb: "esquive", - comp: esquive + comp: esquive, + isEsquive: true } } @@ -66,7 +67,8 @@ export class RollPartDefense extends RollPartSelect { comp: comp, arme: armeDefense, forceRequise: armeDefense ? RdDItemArme.valeurMain(armeDefense.system.force ?? 0, RdDItemArme.getMainAttaque(comp)) : 0, - typeParade: armeAttaque ? RdDItemArme.defenseArmeParade(armeDefense, armeAttaque) : 'norm' + typeParade: armeAttaque ? RdDItemArme.defenseArmeParade(armeDefense, armeAttaque) : 'norm', + isEsquive: false } } @@ -111,7 +113,7 @@ export class RollPartDefense extends RollPartSelect { isArmeDisparate(rollData) { const armeDefense = this.getCurrent(rollData).arme if (armeDefense) { - const armeAttaque = rollData.attaque?.attaque.arme + const armeAttaque = rollData.attackerRoll?.attaque.arme return RdDItemArme.defenseArmeParade(armeAttaque, armeDefense) == 'sign' } return false @@ -120,12 +122,12 @@ export class RollPartDefense extends RollPartSelect { getDiffDefense(rollData) { const current = this.getCurrent(rollData) const refs = this.getRefs(rollData) - if (refs.isDistance || !rollData.attaque) { + if (refs.isDistance || !rollData.attackerRoll) { // Déterminer la difficulté de parade return { diff: 0, type: DIFF.LIBRE } } else { - return { diff: rollData.attaque.diff ?? 0, type: DIFF.DEFENSE } + return { diff: rollData.attackerRoll.diff ?? 0, type: DIFF.DEFENSE } } } } diff --git a/module/roll/roll-part-diff.mjs b/module/roll/roll-part-diff.mjs index d4af6449..8ec99049 100644 --- a/module/roll/roll-part-diff.mjs +++ b/module/roll/roll-part-diff.mjs @@ -1,5 +1,6 @@ import { DIFF, DIFFS, ROLL_TYPE_MEDITATION, ROLL_TYPE_OEUVRE, ROLL_TYPE_SORT, ROLL_TYPE_TACHE } from "./roll-constants.mjs"; import { ROLLDIALOG_SECTION, RollPart } from "./roll-part.mjs"; +import { Misc } from "../misc.js"; export const PART_DIFF = "diff" @@ -41,8 +42,8 @@ export class RollPartDiff extends RollPart { { type: diffType.key, label: diffType?.label ?? '', - value: current.value ?? 0, disabled: !diffType.libre, + value: Misc.inRange(current.value ?? 0, -10, diffType.max), min: -10, max: diffType.max }, @@ -67,11 +68,12 @@ export class RollPartDiff extends RollPart { async _onRender(rollDialog, context, options) { const input = rollDialog.element.querySelector(`roll-section[name="${this.code}"] input[name="${this.code}"]`) - input?.addEventListener("change", e => { - this.getCurrent(rollDialog.rollData).value = parseInt(e.currentTarget.value) - rollDialog.render() - }) + input?.addEventListener("input", e => this.onInputChange(e, rollDialog)) } + onInputChange(event, rollDialog) { + this.getCurrent(rollDialog.rollData).value = parseInt(event.currentTarget.value) + rollDialog.render() + } } \ No newline at end of file diff --git a/module/roll/roll-part-oeuvre.mjs b/module/roll/roll-part-oeuvre.mjs index 2ae95e75..d59dd63a 100644 --- a/module/roll/roll-part-oeuvre.mjs +++ b/module/roll/roll-part-oeuvre.mjs @@ -1,5 +1,4 @@ import { ITEM_TYPES } from "../constants.js" -import { Grammar } from "../grammar.js" import { Misc } from "../misc.js" import { CARACS } from "../rdd-carac.js" import { ROLL_TYPE_OEUVRE } from "./roll-constants.mjs" @@ -59,6 +58,7 @@ export class RollPartOeuvre extends RollPartSelect { label: oeuvre.name, art: art, caracs: art.caracs(oeuvre), + qualite: oeuvre.system.niveau, value: -oeuvre.system.niveau, oeuvre: oeuvre, comp: actor.getCompetence(art.competence(oeuvre)) diff --git a/module/roll/roll-part-tricher.mjs b/module/roll/roll-part-tricher.mjs index e1909be2..c0e869c8 100644 --- a/module/roll/roll-part-tricher.mjs +++ b/module/roll/roll-part-tricher.mjs @@ -1,6 +1,7 @@ +import { Misc } from "../misc.js" import { ROLLDIALOG_SECTION, RollPart } from "./roll-part.mjs" -const PART_TRICHER = "tricher" +export const PART_TRICHER = "tricher" export class RollPartTricher extends RollPart { @@ -11,21 +12,20 @@ export class RollPartTricher extends RollPart { prepareContext(rollData) { const current = this.getCurrent(rollData) - if (current.resultat == undefined) { - current.resultat = -1 - } + current.resultat = Misc.inRange(current.resultat == undefined ? -1 : current.resultat, -1, 100) } getAjustements(rollData) { - rollData.current.resultat = this.getCurrent(rollData).resultat return [] } async _onRender(rollDialog, context, options) { const input = rollDialog.element.querySelector(`roll-section[name="${this.code}"] input[name="${this.code}"]`) - input?.addEventListener("change", e => { - this.getCurrent(rollDialog.rollData).resultat = parseInt(e.currentTarget.value) - }) + input?.addEventListener("input", e => this.onInputChange(e, rollDialog)) + } + + onInputChange(event, rollDialog) { + this.getCurrent(rollDialog.rollData).resultat = parseInt(event.currentTarget.value) } } diff --git a/module/roll/roll-part.mjs b/module/roll/roll-part.mjs index 18d4610c..3543dd72 100644 --- a/module/roll/roll-part.mjs +++ b/module/roll/roll-part.mjs @@ -1,5 +1,3 @@ -import { Misc } from "../misc.js" - export const ROLLDIALOG_SECTION = { ACTION: 'action', CARAC: 'carac', @@ -53,7 +51,23 @@ export class RollPart { } restore(rollData) { } - store(rollData, targetData) { } + + storeClean(rollData, targetData) { + this.store(rollData, targetData) + if (rollData.selected[this.code]) { + const toDelete = Object.entries(rollData.selected[this.code]) + .map(([k, v]) => v == undefined ? k : undefined) + .filter(k => k != undefined) + toDelete.forEach(k => delete rollData.selected[this.code][k]) + if (Object.keys(rollData.selected[this.code]).length == 0) { + delete rollData.selected[this.code] + } + } + } + + store(rollData, targetData) { + this.setSaved(targetData, { key: this.getCurrent(rollData).key }) + } /** * le texte à ajouter dans la barre de titre @@ -70,6 +84,9 @@ export class RollPart { loadRefs(rollData) { } prepareContext(rollData) { } + + /** permet de sauvegarder dans rollData les informations (cas des champs edit) */ + validate(rollData) {} /** ---- cross roll-part filtering ---- */ applyImpact(rollData, filter) { } diff --git a/module/roll/roll-type-oeuvre.mjs b/module/roll/roll-type-oeuvre.mjs index 82388883..1cfbc90d 100644 --- a/module/roll/roll-type-oeuvre.mjs +++ b/module/roll/roll-type-oeuvre.mjs @@ -15,5 +15,14 @@ export class RollTypeOeuvre extends RollType { onSelect(rollData) { this.setDiffType(rollData, DIFF.AUCUN) } + + getResult(rollData){ + const current = rollData.current[PART_OEUVRE] + const qualite = rollData.rolled.isSuccess ? current.qualite : Math.min(current.qualite, current.comp.system.niveau) + return { + qualite: qualite + rollData.rolled.ptQualite + } + } + } diff --git a/module/roll/roll-type.mjs b/module/roll/roll-type.mjs index ad5fdd5b..ef7547bc 100644 --- a/module/roll/roll-type.mjs +++ b/module/roll/roll-type.mjs @@ -52,4 +52,8 @@ export class RollType { rollData.current[PART_DIFF].type = type this.setRollDataType(rollData) } + + getResult(rollData){ + return undefined + } } diff --git a/module/settings/options-avancees.js b/module/settings/options-avancees.js index 90d9f37d..720a3923 100644 --- a/module/settings/options-avancees.js +++ b/module/settings/options-avancees.js @@ -3,9 +3,11 @@ import { Misc } from "../misc.js" export const EXPORT_CSV_SCRIPTARIUM = 'export-csv-scriptarium' export const ROLL_DIALOG_V2 = 'roll-dialog-v2' +export const ROLL_DIALOG_V2_TEST = 'roll-dialog-v2-test' const OPTIONS_AVANCEES = [ { group: 'Fenêtres', name: ROLL_DIALOG_V2, descr: "Utiliser les nouvelles fenêtres de jet", default: false }, + { group: 'Fenêtres', name: ROLL_DIALOG_V2_TEST, descr: "Mode de test des nouvelles fenêtres", default: false }, { group: 'Menus', name: EXPORT_CSV_SCRIPTARIUM, descr: "Proposer le menu d'export csv Scriptarium", default: false }, ] @@ -56,7 +58,7 @@ export class OptionsAvancees extends FormApplication { return formData } - static getSettingKey(name){ + static getSettingKey(name) { return `${SYSTEM_RDD}.${this._getId(name)}` } diff --git a/module/settings/regles-optionnelles.js b/module/settings/regles-optionnelles.js index ce759ca9..1207c66b 100644 --- a/module/settings/regles-optionnelles.js +++ b/module/settings/regles-optionnelles.js @@ -13,6 +13,7 @@ const listeReglesOptionnelles = [ { group: 'Règles de combat', name: 'localisation-aleatoire', descr: "Proposer une localisation aléatoire des blessures" }, { group: 'Règles de combat', name: 'recul', descr: "Appliquer le recul en cas de particulière en force ou de charge" }, + { group: 'Règles de combat', name: 'acrobatie-pour-recul', descr: "L'acrobatie aide à ne pas chuter en cas de recul" , default: false }, { group: 'Règles de combat', name: 'resistanceArmeParade', descr: "Faire le jet de résistance des armes lors de parades pouvant les endommager" }, { group: 'Règles de combat', name: 'deteriorationArmure', descr: "Tenir compte de la détérioration des armures" }, { group: 'Règles de combat', name: 'defenseurDesarme', descr: "Le défenseur peut être désarmé en parant une particulière en force ou une charge avec une arme autre qu'un bouclier" }, diff --git a/templates/actor/commerce-inventaire.hbs b/templates/actor/commerce-inventaire.hbs index 980c1044..902ea6a4 100644 --- a/templates/actor/commerce-inventaire.hbs +++ b/templates/actor/commerce-inventaire.hbs @@ -16,7 +16,7 @@ {{/if}} {{/unless}} -