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
- Fenêtre de jets V2
- 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
- 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 { RdDPossessionV2 } from "./rdd-possession-v2.mjs";
import { Apprecier, MORAL, SITUATION_MORAL } from "./moral/apprecier.mjs";
import { Distance } from "./combat/distance.mjs";
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 ajustement = (arme.parent?.getEtatGeneral() ?? 0) + ecaillesEfficacite
actions.push({
const attaque = {
label: arme.name + (main ? ' ' + main : ''),
action: 'attaque',
initOnly: false,
@@ -217,7 +218,9 @@ export class RdDActor extends RdDBaseActorSang {
dommages: dommages,
forceRequise: forceRequise,
initiative: RdDInitiative.getRollInitiative(caracValue, niveau, ajustement)
})
};
attaque.isDistance = Distance.typeAttaqueDistance(attaque),
actions.push(attaque)
}
addAttaque(RdDItemArme.empoignade(this), ATTAQUE_TYPE.CORPS_A_CORPS)
+55 -12
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"
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 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 activite = Distance.ajustementMouvement(defenderToken)
const total = Distance.diff(portee, taille, activite)
return {
isVisible: isVisible,
distance: distance,
portee: portee,
taille: taille,
activite: activite,
total: total
}
const diffDefense = Distance.ajustementDefense(attaque)
return { typeAttaque, isVisible, distance, portee, taille, activite, total, diffDefense }
}
return undefined
}
static distance(token, defenderToken) {
@@ -56,8 +73,34 @@ export class Distance {
return { msg: "à déterminer (0 immobile, -3 actif, -4 en mouvement, -5 en zig-zag)", diff: -3 };
}
static diff(...ajustements){
return ajustements.map(it => it.diff).filter(d => !Number.isNaN(d)).reduce(Misc.sum(), 0)
static diff(...ajustements) {
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 { RdDInitiative } from "./initiative.mjs";
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";
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 { RdDInitiative } from "./initiative.mjs";
import { RdDItemArme } from "./item/arme.js";
import { Distance } from "./combat/distance.mjs";
/* -------------------------------------------- */
export class RdDItemCompetenceCreature extends RdDItem {
@@ -45,12 +46,12 @@ export class RdDItemCompetenceCreature extends RdDItem {
equipe: true,
mortalite: this.system.mortalite,
dommages: this.system.dommages,
//dmg: this.system.dommages,
initiative: initative
};
}
attaque.isDistance = Distance.typeAttaqueDistance(attaque)
return attaque
}
return undefined;
return undefined
}
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),
isComestible: this.item.getUtilisationCuisine(),
options: RdDSheetUtility.mergeDocumentRights({}, this.item, this.isEditable),
competences: [SANS_COMPETENCE, ...competences],
competences: [new RdDItemCompetence(SANS_COMPETENCE), ...competences],
categories: RdDItem.getCategories(this.item.type),
isAppreciable: Apprecier.isAppreciable(this.item)
}
+1 -1
View File
@@ -632,7 +632,7 @@ export class RdDCombat {
attacker: _token,
defender: defenderToken,
},
Distance.ajustements(_token, defenderToken, rollData.arme)
Distance.ajustements(_token, defenderToken, { arme: rollData.arme, main: rollData.competence.system.categorie })
)
ChatMessage.create({
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.tactique = TACTIQUES[0]
attaque.initialDiff = attaque.comp?.system.default_diffLibre ?? 0
if (RollPartAttaque.isAttaqueDistance(attaque)) {
attaque.distance = Distance.ajustements(rollData.active.token,
rollData.opponent.token,
attaque.arme)
}
attaque.distance = Distance.ajustements(rollData.active.token, rollData.opponent.token, attaque)
return attaque
}
static isAttaqueDistance(attaque) {
return ['(lancer)', '(tir)'].includes(attaque.main)
}
prepareContext(rollData) {
this.filterAttaquesEmpoignade(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_COMP: return part.filterComps(rollData, [current.comp.name])
case PART_DIFF: {
if (RollPartAttaque.isAttaqueDistance(current)) {
if (Distance.typeAttaqueDistance(current)) {
part.setDiff(rollData, { type: DIFF.DEFAUT })
}
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 { CARACS } from "../rdd-carac.js"
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 }
visible(rollData) { return this.isRollType(rollData, ROLL_TYPE_DEFENSE) && rollData.attackerRoll != undefined }
static getDiffAttaque(attackerRoll) {
// TODO: rollDataV2?
return attackerRoll.diffLibre;
}
loadRefs(rollData) {
const refs = this.getRefs(rollData)
const attackerRoll = rollData.attackerRoll
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 isEmpoignadeEnCours = isEmpoignade && defenseur.itemTypes[ITEM_TYPES.empoignade].find(it =>
[it.system.empoigneurid, it.system.empoigneid].includes(rollData.ids.opponentId) &&
it.system.pointsemp != 0)
const esquives = (refs.isDistance == ATTAQUE_TYPE.TIR || isEmpoignadeEnCours)
const esquives = (refs.distance?.typeAttaque == ATTAQUE_TYPE.TIR || isEmpoignadeEnCours)
? []
: defenseur.getCompetencesEsquive()
const parades = isEmpoignade
? [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 = [
...esquives.map(it => RollPartDefense.$extractEsquive(it, defenseur)),
...parades.map(it => RollPartDefense.$extractParade(it, attackerRoll?.arme, defenseur))
...esquives.map(it => RollPartDefense.$extractEsquive(it, defenseur, attackerRoll)),
...parades.map(it => RollPartDefense.$extractParade(it, defenseur, attackerRoll))
]
this.$selectDefense(rollData)
}
static $extractEsquive(esquive, defenseur) {
return {
static $extractEsquive(esquive, defenseur, attackerRoll) {
const defense = {
key: esquive.id,
label: esquive.name,
img: esquive.img,
// TODO: carac pour créatures
// TODO: carac pour créatures à vérifier
carac: defenseur.isPersonnage() ? CARACS.DEROBEE : esquive.name,
verb: "esquive",
comp: esquive,
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)
? armeDefense
: defenseur.getCompetence(armeDefense.system.competence)
return {
const defense = {
key: armeDefense.id,
label: 'Parade ' + armeDefense.name,
img: armeDefense.img,
@@ -78,6 +81,13 @@ export class RollPartDefense extends RollPartSelect {
typeParade: armeAttaque ? RdDItemArme.defenseArmeParade(armeDefense, armeAttaque) : 'norm',
isEsquive: false
}
if (attackerRoll.current.attaque.distance) {
defense.distance = {
ajustementBouclier: Distance.ajustementBouclier(armeDefense),
ajustementDefense: Distance.ajustementDefense(attackerRoll.current.attaque),
}
}
return defense
}
prepareContext(rollData) {
@@ -107,10 +117,11 @@ export class RollPartDefense extends RollPartSelect {
impactOtherPart(part, rollData) {
if (this.visible(rollData)) {
const refs = this.getRefs(rollData)
const current = this.getCurrent(rollData)
switch (part.code) {
case PART_CARAC: return part.filterCaracs(rollData, [current.carac])
case PART_COMP: return part.filterComps(rollData, [current.comp?.name])
case PART_CARAC: return part.filterCaracs(rollData, refs.defenses.length > 0 ? [current.carac] : ['impossible'])
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_SIGN: return part.setArme(rollData, this.isArmeDisparate(rollData), current.forceRequise)
}
@@ -129,16 +140,19 @@ export class RollPartDefense extends RollPartSelect {
getDiffDefense(rollData) {
const refs = this.getRefs(rollData)
if (refs.isDistance || !rollData.attackerRoll) {
// TODO: Déterminer la difficulté de parade
return { diff: 0, type: DIFF.LIBRE }
if (refs.distance) {
const current = this.getCurrent(rollData)
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 diff = attackerRoll.v2
? attackerRoll.selected.diff.value
: attackerRoll.diff
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'>
<img src="systems/foundryvtt-reve-de-dragon/assets/actions/defense.svg"/>
Se défendre
{{#if (or (eq attackerRoll.attaqueCategorie 'tir') (eq attackerRoll.attaqueCategorie 'lancer'))}}
(difficulté à déterminer)
{{#if attackerRoll.current.attaque.isDistance}}
(à distance)
{{else}}
à {{attackerRoll.current.diff.value}}
{{/if}}
+1 -1
View File
@@ -66,7 +66,7 @@
{{#if current.distance}}
<subline>
<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>
</subline>
<subline>
+20 -3
View File
@@ -3,10 +3,27 @@
</roll-part-img>
<roll-part-detail>
<subline>
<select name="select-defense" {{#if rollData.type.retry}}disabled{{/if}}>
{{selectOptions refs.defenses selected=current.key valueAttr="key" labelAttr="label"}}
</select>
{{#if refs.defenses.length}}
<select name="select-defense" {{#if rollData.type.retry}}disabled{{/if}}>
{{selectOptions refs.defenses selected=current.key valueAttr="key" labelAttr="label"}}
</select>
{{else}}
<span>
<strong>Aucune défense disponible</strong> {{#if refs.distance}}contre cette attaque à distance {{refs.distance.typeAttaque}}{{/if}}
</span>
{{/if}}
</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}}
<subline>
<span class="status-surprise">