Gestion défense à distance

This commit is contained in:
2025-12-11 03:12:49 +01:00
parent 4f4e00b298
commit 30090872d4
12 changed files with 129 additions and 58 deletions
+1
View File
@@ -7,6 +7,7 @@
- le poids des monnaies est de nouveau décimal - le poids des monnaies est de nouveau décimal
- Fenêtre de jets V2 - Fenêtre de jets V2
- affichage des ajustements dans la fenêtre d'attaque avec arme à distance - affichage des ajustements dans la fenêtre d'attaque avec arme à distance
- gestion des défenses contre les armes à distance
- Corrections liées à Foundry v12/v13 - Corrections liées à Foundry v12/v13
- Fix ajout de notes dans le journal par un joueur (/chrono, ou clic sur le libelé de l'heure dans l'horloge) - Fix ajout de notes dans le journal par un joueur (/chrono, ou clic sur le libelé de l'heure dans l'horloge)
+5 -2
View File
@@ -54,6 +54,7 @@ import { PART_OEUVRE } from "./roll/roll-part-oeuvre.mjs";
import { PART_CUISINE } from "./roll/roll-part-cuisine.mjs"; import { PART_CUISINE } from "./roll/roll-part-cuisine.mjs";
import { RdDPossessionV2 } from "./rdd-possession-v2.mjs"; import { RdDPossessionV2 } from "./rdd-possession-v2.mjs";
import { Apprecier, MORAL, SITUATION_MORAL } from "./moral/apprecier.mjs"; import { Apprecier, MORAL, SITUATION_MORAL } from "./moral/apprecier.mjs";
import { Distance } from "./combat/distance.mjs";
export const MAINS_DIRECTRICES = ['Droitier', 'Gaucher', 'Ambidextre'] export const MAINS_DIRECTRICES = ['Droitier', 'Gaucher', 'Ambidextre']
@@ -205,7 +206,7 @@ export class RdDActor extends RdDBaseActorSang {
const niveau = comp?.system.niveau ?? (['(lancer)', '(tir)'].includes(main) ? -8 : -6) const niveau = comp?.system.niveau ?? (['(lancer)', '(tir)'].includes(main) ? -8 : -6)
const ajustement = (arme.parent?.getEtatGeneral() ?? 0) + ecaillesEfficacite const ajustement = (arme.parent?.getEtatGeneral() ?? 0) + ecaillesEfficacite
actions.push({ const attaque = {
label: arme.name + (main ? ' ' + main : ''), label: arme.name + (main ? ' ' + main : ''),
action: 'attaque', action: 'attaque',
initOnly: false, initOnly: false,
@@ -217,7 +218,9 @@ export class RdDActor extends RdDBaseActorSang {
dommages: dommages, dommages: dommages,
forceRequise: forceRequise, forceRequise: forceRequise,
initiative: RdDInitiative.getRollInitiative(caracValue, niveau, ajustement) initiative: RdDInitiative.getRollInitiative(caracValue, niveau, ajustement)
}) };
attaque.isDistance = Distance.typeAttaqueDistance(attaque),
actions.push(attaque)
} }
addAttaque(RdDItemArme.empoignade(this), ATTAQUE_TYPE.CORPS_A_CORPS) addAttaque(RdDItemArme.empoignade(this), ATTAQUE_TYPE.CORPS_A_CORPS)
+52 -9
View File
@@ -1,22 +1,39 @@
import { ITEM_TYPES } from "../constants.js"
import { Grammar } from "../grammar.js"
import { ATTAQUE_TYPE } from "../item/arme.js"
import { Misc } from "../misc.js" import { Misc } from "../misc.js"
export class Distance { export class Distance {
static ajustements(token, defenderToken, arme){ static typeAttaqueDistance(attaque) {
function mapTypeAttaque(value) {
switch (value) {
case ATTAQUE_TYPE.LANCER: case 'lancer': return ATTAQUE_TYPE.LANCER
case ATTAQUE_TYPE.TIR: case 'tir': return ATTAQUE_TYPE.TIR
}
return undefined
}
switch (attaque.comp.type) {
case ITEM_TYPES.competence: return mapTypeAttaque(attaque.main)
case ITEM_TYPES.competencecreature: return mapTypeAttaque(attaque.arme.system.categorie)
}
return undefined
}
static ajustements(token, defenderToken, attaque) {
const typeAttaque = Distance.typeAttaqueDistance(attaque)
if (typeAttaque) {
const distance = Distance.distance(token, defenderToken) const distance = Distance.distance(token, defenderToken)
const isVisible = Distance.isVisible(token, defenderToken) const isVisible = Distance.isVisible(token, defenderToken)
const portee = Distance.ajustementPortee(distance, arme) const portee = Distance.ajustementPortee(distance, attaque.arme)
const taille = Distance.ajustementTaille(defenderToken.actor) const taille = Distance.ajustementTaille(defenderToken.actor)
const activite = Distance.ajustementMouvement(defenderToken) const activite = Distance.ajustementMouvement(defenderToken)
const total = Distance.diff(portee, taille, activite) const total = Distance.diff(portee, taille, activite)
return { const diffDefense = Distance.ajustementDefense(attaque)
isVisible: isVisible, return { typeAttaque, isVisible, distance, portee, taille, activite, total, diffDefense }
distance: distance,
portee: portee,
taille: taille,
activite: activite,
total: total
} }
return undefined
} }
static distance(token, defenderToken) { static distance(token, defenderToken) {
@@ -60,4 +77,30 @@ export class Distance {
return ajustements.map(it => it.diff).filter(d => !Number.isNaN(d)).reduce(Misc.sum(), 0) return ajustements.map(it => it.diff).filter(d => !Number.isNaN(d)).reduce(Misc.sum(), 0)
} }
static ajustementDefense(attaque) {
switch (Grammar.toLowerCaseNoAccentNoSpace(attaque.comp.name)) {
case 'arbalete': return { label: 'Parade carreau', value: -5 }
case 'arc': return { label: 'Parade flêche', value: -5 }
case 'fronde': return { label: 'Parade bille de fronde', value: -4 }
case 'daguedejet': return { label: 'Parade/Esquive dague lancée', value: -2 }
case 'javelot': return { label: 'Parade/Esquive javelot lancé', value: -2 }
case 'fouet': return { label: 'Parade/Esquive fouet', value: 0 }
}
return { label: 'Parade/esquive projectile', value: 0 }
}
static ajustementBouclier(bouclier) {
if (bouclier?.system.categorie_parade == 'boucliers') {
if (bouclier.system.force < 10) {
return { label: 'Bouclier léger', value: -3 }
}
if (bouclier.system.force < 12) {
return { label: 'Bouclier moyen', value: 0 }
}
return { label: 'Bouclier lourd', value: 3 }
}
/* esquive */
return undefined
}
} }
+1 -1
View File
@@ -2,7 +2,7 @@
import { Grammar } from "./grammar.js"; import { Grammar } from "./grammar.js";
import { RdDInitiative } from "./initiative.mjs"; import { RdDInitiative } from "./initiative.mjs";
import { RdDItem } from "./item.js"; import { RdDItem } from "./item.js";
import { CATEGORIES_COMPETENCE_COMBAT, CATEGORIES_COMPETENCES, SANS_COMPETENCE } from "./item/base-items.js"; import { CATEGORIES_COMPETENCES, SANS_COMPETENCE } from "./item/base-items.js";
import { Misc } from "./misc.js"; import { Misc } from "./misc.js";
const competenceTroncs = [["Esquive", "Dague", "Corps à corps"], const competenceTroncs = [["Esquive", "Dague", "Corps à corps"],
+4 -3
View File
@@ -2,6 +2,7 @@ import { ITEM_TYPES } from "./constants.js";
import { RdDItem } from "./item.js"; import { RdDItem } from "./item.js";
import { RdDInitiative } from "./initiative.mjs"; import { RdDInitiative } from "./initiative.mjs";
import { RdDItemArme } from "./item/arme.js"; import { RdDItemArme } from "./item/arme.js";
import { Distance } from "./combat/distance.mjs";
/* -------------------------------------------- */ /* -------------------------------------------- */
export class RdDItemCompetenceCreature extends RdDItem { export class RdDItemCompetenceCreature extends RdDItem {
@@ -45,12 +46,12 @@ export class RdDItemCompetenceCreature extends RdDItem {
equipe: true, equipe: true,
mortalite: this.system.mortalite, mortalite: this.system.mortalite,
dommages: this.system.dommages, dommages: this.system.dommages,
//dmg: this.system.dommages,
initiative: initative initiative: initative
}; }
attaque.isDistance = Distance.typeAttaqueDistance(attaque)
return attaque return attaque
} }
return undefined; return undefined
} }
getCategorieAttaque() { getCategorieAttaque() {
+1 -1
View File
@@ -104,7 +104,7 @@ export class RdDItemSheetV1 extends foundry.appv1.sheets.ItemSheet {
descriptionmj: await RdDTextEditor.enrichHTML(this.item.system.descriptionmj, this.item), descriptionmj: await RdDTextEditor.enrichHTML(this.item.system.descriptionmj, this.item),
isComestible: this.item.getUtilisationCuisine(), isComestible: this.item.getUtilisationCuisine(),
options: RdDSheetUtility.mergeDocumentRights({}, this.item, this.isEditable), options: RdDSheetUtility.mergeDocumentRights({}, this.item, this.isEditable),
competences: [SANS_COMPETENCE, ...competences], competences: [new RdDItemCompetence(SANS_COMPETENCE), ...competences],
categories: RdDItem.getCategories(this.item.type), categories: RdDItem.getCategories(this.item.type),
isAppreciable: Apprecier.isAppreciable(this.item) isAppreciable: Apprecier.isAppreciable(this.item)
} }
+1 -1
View File
@@ -632,7 +632,7 @@ export class RdDCombat {
attacker: _token, attacker: _token,
defender: defenderToken, defender: defenderToken,
}, },
Distance.ajustements(_token, defenderToken, rollData.arme) Distance.ajustements(_token, defenderToken, { arme: rollData.arme, main: rollData.competence.system.categorie })
) )
ChatMessage.create({ ChatMessage.create({
content: await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-info-distance.hbs', info), content: await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-info-distance.hbs', info),
+2 -10
View File
@@ -69,18 +69,10 @@ export class RollPartAttaque extends RollPartSelect {
attaque.key = `${attaque.action}::${attaque.label}` attaque.key = `${attaque.action}::${attaque.label}`
attaque.tactique = TACTIQUES[0] attaque.tactique = TACTIQUES[0]
attaque.initialDiff = attaque.comp?.system.default_diffLibre ?? 0 attaque.initialDiff = attaque.comp?.system.default_diffLibre ?? 0
if (RollPartAttaque.isAttaqueDistance(attaque)) { attaque.distance = Distance.ajustements(rollData.active.token, rollData.opponent.token, attaque)
attaque.distance = Distance.ajustements(rollData.active.token,
rollData.opponent.token,
attaque.arme)
}
return attaque return attaque
} }
static isAttaqueDistance(attaque) {
return ['(lancer)', '(tir)'].includes(attaque.main)
}
prepareContext(rollData) { prepareContext(rollData) {
this.filterAttaquesEmpoignade(rollData) this.filterAttaquesEmpoignade(rollData)
const current = this.getCurrent(rollData) const current = this.getCurrent(rollData)
@@ -167,7 +159,7 @@ export class RollPartAttaque extends RollPartSelect {
case PART_CARAC: return part.filterCaracs(rollData, [current.carac.key]) case PART_CARAC: return part.filterCaracs(rollData, [current.carac.key])
case PART_COMP: return part.filterComps(rollData, [current.comp.name]) case PART_COMP: return part.filterComps(rollData, [current.comp.name])
case PART_DIFF: { case PART_DIFF: {
if (RollPartAttaque.isAttaqueDistance(current)) { if (Distance.typeAttaqueDistance(current)) {
part.setDiff(rollData, { type: DIFF.DEFAUT }) part.setDiff(rollData, { type: DIFF.DEFAUT })
} }
else { else {
+36 -22
View File
@@ -1,4 +1,5 @@
import { ITEM_TYPES, RDD_CONFIG } from "../constants.js" import { Distance } from "../combat/distance.mjs"
import { ITEM_TYPES } from "../constants.js"
import { ATTAQUE_TYPE, RdDItemArme } from "../item/arme.js" import { ATTAQUE_TYPE, RdDItemArme } from "../item/arme.js"
import { CARACS } from "../rdd-carac.js" import { CARACS } from "../rdd-carac.js"
import { DIFF, ROLL_TYPE_DEFENSE } from "./roll-constants.mjs" import { DIFF, ROLL_TYPE_DEFENSE } from "./roll-constants.mjs"
@@ -19,53 +20,55 @@ export class RollPartDefense extends RollPartSelect {
isValid(rollData) { return this.isRollType(rollData, ROLL_TYPE_DEFENSE) && rollData.attackerRoll != undefined } isValid(rollData) { return this.isRollType(rollData, ROLL_TYPE_DEFENSE) && rollData.attackerRoll != undefined }
visible(rollData) { return this.isRollType(rollData, ROLL_TYPE_DEFENSE) && rollData.attackerRoll != undefined } visible(rollData) { return this.isRollType(rollData, ROLL_TYPE_DEFENSE) && rollData.attackerRoll != undefined }
static getDiffAttaque(attackerRoll) {
// TODO: rollDataV2?
return attackerRoll.diffLibre;
}
loadRefs(rollData) { loadRefs(rollData) {
const refs = this.getRefs(rollData) const refs = this.getRefs(rollData)
const attackerRoll = rollData.attackerRoll const attackerRoll = rollData.attackerRoll
const defenseur = rollData.active.actor const defenseur = rollData.active.actor
refs.isDistance = [ATTAQUE_TYPE.TIR, ATTAQUE_TYPE.LANCER].find(it => it == attackerRoll?.main) refs.distance = rollData.attackerRoll.current.attaque.distance
const isEmpoignade = attackerRoll.dmg.isEmpoignade const isEmpoignade = attackerRoll.dmg.isEmpoignade
const isEmpoignadeEnCours = isEmpoignade && defenseur.itemTypes[ITEM_TYPES.empoignade].find(it => const isEmpoignadeEnCours = isEmpoignade && defenseur.itemTypes[ITEM_TYPES.empoignade].find(it =>
[it.system.empoigneurid, it.system.empoigneid].includes(rollData.ids.opponentId) && [it.system.empoigneurid, it.system.empoigneid].includes(rollData.ids.opponentId) &&
it.system.pointsemp != 0) it.system.pointsemp != 0)
const esquives = (refs.isDistance == ATTAQUE_TYPE.TIR || isEmpoignadeEnCours) const esquives = (refs.distance?.typeAttaque == ATTAQUE_TYPE.TIR || isEmpoignadeEnCours)
? [] ? []
: defenseur.getCompetencesEsquive() : defenseur.getCompetencesEsquive()
const parades = isEmpoignade const parades = isEmpoignade
? [RdDItemArme.empoignade(defenseur)] ? [RdDItemArme.empoignade(defenseur)]
: defenseur.items.filter(it => it.isParade() && (!refs.isDistance || it.isBouclier())) : defenseur.items.filter(it => it.isParade() && (!refs.distance || it.isBouclier()))
refs.defenses = [ refs.defenses = [
...esquives.map(it => RollPartDefense.$extractEsquive(it, defenseur)), ...esquives.map(it => RollPartDefense.$extractEsquive(it, defenseur, attackerRoll)),
...parades.map(it => RollPartDefense.$extractParade(it, attackerRoll?.arme, defenseur)) ...parades.map(it => RollPartDefense.$extractParade(it, defenseur, attackerRoll))
] ]
this.$selectDefense(rollData) this.$selectDefense(rollData)
} }
static $extractEsquive(esquive, defenseur) { static $extractEsquive(esquive, defenseur, attackerRoll) {
return { const defense = {
key: esquive.id, key: esquive.id,
label: esquive.name, label: esquive.name,
img: esquive.img, img: esquive.img,
// TODO: carac pour créatures // TODO: carac pour créatures à vérifier
carac: defenseur.isPersonnage() ? CARACS.DEROBEE : esquive.name, carac: defenseur.isPersonnage() ? CARACS.DEROBEE : esquive.name,
verb: "esquive", verb: "esquive",
comp: esquive, comp: esquive,
isEsquive: true isEsquive: true
} }
if (attackerRoll.current.attaque.distance) {
defense.distance = {
ajustementDefense: Distance.ajustementDefense(attackerRoll.current.attaque),
}
}
return defense
} }
static $extractParade(armeDefense, armeAttaque, defenseur) { static $extractParade(armeDefense, defenseur, attackerRoll) {
const armeAttaque = attackerRoll?.arme
const comp = (ITEM_TYPES.competencecreature == armeDefense.type) const comp = (ITEM_TYPES.competencecreature == armeDefense.type)
? armeDefense ? armeDefense
: defenseur.getCompetence(armeDefense.system.competence) : defenseur.getCompetence(armeDefense.system.competence)
return { const defense = {
key: armeDefense.id, key: armeDefense.id,
label: 'Parade ' + armeDefense.name, label: 'Parade ' + armeDefense.name,
img: armeDefense.img, img: armeDefense.img,
@@ -78,6 +81,13 @@ export class RollPartDefense extends RollPartSelect {
typeParade: armeAttaque ? RdDItemArme.defenseArmeParade(armeDefense, armeAttaque) : 'norm', typeParade: armeAttaque ? RdDItemArme.defenseArmeParade(armeDefense, armeAttaque) : 'norm',
isEsquive: false isEsquive: false
} }
if (attackerRoll.current.attaque.distance) {
defense.distance = {
ajustementBouclier: Distance.ajustementBouclier(armeDefense),
ajustementDefense: Distance.ajustementDefense(attackerRoll.current.attaque),
}
}
return defense
} }
prepareContext(rollData) { prepareContext(rollData) {
@@ -107,10 +117,11 @@ export class RollPartDefense extends RollPartSelect {
impactOtherPart(part, rollData) { impactOtherPart(part, rollData) {
if (this.visible(rollData)) { if (this.visible(rollData)) {
const refs = this.getRefs(rollData)
const current = this.getCurrent(rollData) const current = this.getCurrent(rollData)
switch (part.code) { switch (part.code) {
case PART_CARAC: return part.filterCaracs(rollData, [current.carac]) case PART_CARAC: return part.filterCaracs(rollData, refs.defenses.length > 0 ? [current.carac] : ['impossible'])
case PART_COMP: return part.filterComps(rollData, [current.comp?.name]) case PART_COMP: return part.filterComps(rollData, refs.defenses.length > 0 ? [current.comp?.name] : ['impossible'])
case PART_DIFF: return part.setDiff(rollData, this.getDiffDefense(rollData)) case PART_DIFF: return part.setDiff(rollData, this.getDiffDefense(rollData))
case PART_SIGN: return part.setArme(rollData, this.isArmeDisparate(rollData), current.forceRequise) case PART_SIGN: return part.setArme(rollData, this.isArmeDisparate(rollData), current.forceRequise)
} }
@@ -129,16 +140,19 @@ export class RollPartDefense extends RollPartSelect {
getDiffDefense(rollData) { getDiffDefense(rollData) {
const refs = this.getRefs(rollData) const refs = this.getRefs(rollData)
if (refs.isDistance || !rollData.attackerRoll) { if (refs.distance) {
// TODO: Déterminer la difficulté de parade const current = this.getCurrent(rollData)
return { diff: 0, type: DIFF.LIBRE } const diff = (current.distance?.ajustementBouclier?.value ?? 0)
+ (current.distance?.ajustementDefense?.value ?? 0)
return { diff: diff, type: DIFF.DEFENSE }
} }
else { if (rollData.attackerRoll) {
const attackerRoll = rollData.attackerRoll const attackerRoll = rollData.attackerRoll
const diff = attackerRoll.v2 const diff = attackerRoll.v2
? attackerRoll.selected.diff.value ? attackerRoll.selected.diff.value
: attackerRoll.diff : attackerRoll.diff
return { diff: diff ?? 0, type: DIFF.DEFENSE } return { diff: diff ?? 0, type: DIFF.DEFENSE }
} }
return { diff: 0, type: DIFF.LIBRE }
} }
} }
+2 -2
View File
@@ -31,8 +31,8 @@
<a class='chat-card-button button-defense'> <a class='chat-card-button button-defense'>
<img src="systems/foundryvtt-reve-de-dragon/assets/actions/defense.svg"/> <img src="systems/foundryvtt-reve-de-dragon/assets/actions/defense.svg"/>
Se défendre Se défendre
{{#if (or (eq attackerRoll.attaqueCategorie 'tir') (eq attackerRoll.attaqueCategorie 'lancer'))}} {{#if attackerRoll.current.attaque.isDistance}}
(difficulté à déterminer) (à distance)
{{else}} {{else}}
à {{attackerRoll.current.diff.value}} à {{attackerRoll.current.diff.value}}
{{/if}} {{/if}}
+1 -1
View File
@@ -66,7 +66,7 @@
{{#if current.distance}} {{#if current.distance}}
<subline> <subline>
<span> <span>
<strong>Attaque à distance</strong> {{current.main}} proposé de <strong>{{current.distance.total}}</strong> à valider par le Gardien <strong>Attaque à distance</strong> {{current.main}} difficulté proposée de <strong>{{current.distance.total}}</strong> à valider par le Gardien
</span> </span>
</subline> </subline>
<subline> <subline>
+17
View File
@@ -3,10 +3,27 @@
</roll-part-img> </roll-part-img>
<roll-part-detail> <roll-part-detail>
<subline> <subline>
{{#if refs.defenses.length}}
<select name="select-defense" {{#if rollData.type.retry}}disabled{{/if}}> <select name="select-defense" {{#if rollData.type.retry}}disabled{{/if}}>
{{selectOptions refs.defenses selected=current.key valueAttr="key" labelAttr="label"}} {{selectOptions refs.defenses selected=current.key valueAttr="key" labelAttr="label"}}
</select> </select>
{{else}}
<span>
<strong>Aucune défense disponible</strong> {{#if refs.distance}}contre cette attaque à distance {{refs.distance.typeAttaque}}{{/if}}
</span>
{{/if}}
</subline> </subline>
{{#if refs.distance}}
<subline>Défense contre une attaque à distance {{refs.distance.typeAttaque}}</subline>
{{log 'defense' this}}
{{#if current.distance.ajustementDefense}}
<subline>{{current.distance.ajustementDefense.label}}: {{current.distance.ajustementDefense.value}}</subline>
{{/if}}
{{#if current.distance.ajustementBouclier}}
<subline>{{current.distance.ajustementBouclier.label}}: {{current.distance.ajustementBouclier.value}}</subline>
{{/if}}
<subline>Activité du défenseur: 0 si immobile et prêt, -3 si actif (à ajouter manuellement aux conditions)</subline>
{{/if}}
{{#if rollData.active.effets}} {{#if rollData.active.effets}}
<subline> <subline>
<span class="status-surprise"> <span class="status-surprise">