import { RdDUtility } from "../rdd-utility.js"; import { ReglesOptionnelles } from "../settings/regles-optionnelles.js"; import { STATUSES } from "../settings/status-effects.js"; import { ITEM_TYPES } from "../constants.js"; import { RdDBaseActorReve } from "./base-actor-reve.js"; import { RdDDice } from "../rdd-dice.js"; import { RdDItemBlessure } from "../item/blessure.js"; import { ChatUtility } from "../chat-utility.js"; import { Misc } from "../misc.js"; import { RdDBaseActor } from "./base-actor.js"; import { CARACS } from "../rdd-carac.js"; /** * Classe de base pour les acteurs qui peuvent subir des blessures * - créatures * - humanoides */ export class RdDBaseActorSang extends RdDBaseActorReve { async _preUpdate(changed, options, user) { const updatedEndurance = changed?.system?.sante?.endurance if (updatedEndurance && options.diff) { await this.setEffect(STATUSES.StatusUnconscious, updatedEndurance.value == 0) } } prepareActorData() { this.system.sante.vie.max = Math.ceil((this.getTaille() + this.getConstitution()) / 2) this.system.sante.vie.value = Math.min(this.system.sante.vie.value, this.system.sante.vie.max) super.prepareActorData() this.system.attributs.encombrement.value = this.getEncombrementMax() } getCarac() { const carac = super.getCarac() foundry.utils.mergeObject(carac, this.getCaracChanceActuelle()) foundry.utils.mergeObject(carac, this.getCaracVie()) return carac } getForce() { return Misc.toInt(this.system.carac.force?.value) } getConstitution() { return Misc.toInt(this.system.carac.constitution?.value) } getVolonte() { return Misc.toInt(this.system.carac.volonte?.value) } getCaracVie() { return { [CARACS.VIE]: { label: "Vie", value: this.getVieMax(), type: "number" } } } getVieMax() { return Misc.toInt(this.system.sante.vie?.max) } getEnduranceMax() { return Math.max(1, this.getTaille() + this.getConstitution()) } getFatigueMax() { return this.getEnduranceMax() * 2 } getProtectionNaturelle() { return Misc.toInt(this.system.attributs?.protection?.value) } getFatigueActuelle() { if (ReglesOptionnelles.isUsing("appliquer-fatigue")) { return Math.max(0, Math.min(this.getFatigueMax(), Misc.toInt(this.system.sante.fatigue?.value))) } return 0; } isCumulFatigueCauseSommeil(cumulFatigue) { return ReglesOptionnelles.isUsing("appliquer-fatigue") ? (this.getFatigueRestante() <= cumulFatigue) : (this.getEnduranceActuelle() <= cumulFatigue) } getFatigueRestante() { return this.getFatigueMax() - this.getFatigueActuelle() } getFatigueMin() { return this.system.sante.endurance.max - this.system.sante.endurance.value } malusFatigue() { if (ReglesOptionnelles.isUsing("appliquer-fatigue")) { return RdDUtility.calculMalusFatigue(this.getFatigueActuelle(), this.getEnduranceMax()) } return 0; } /* -------------------------------------------- */ isSurenc() { return this.computeMalusSurEncombrement() < 0 } computeMalusSurEncombrement() { return Math.min(0, Math.floor(this.getEncombrementMax() - this.encTotal)); } isDead() { return this.system.sante.vie.value < -this.getSConst() } nbBlessuresLegeres() { return this.itemTypes[ITEM_TYPES.blessure].filter(it => it.isLegere()).length } nbBlessuresGraves() { return this.itemTypes[ITEM_TYPES.blessure].filter(it => it.isGrave()).length } nbBlessuresCritiques() { return this.itemTypes[ITEM_TYPES.blessure].filter(it => it.isCritique()).length } /* -------------------------------------------- */ computeResumeBlessure() { const nbLegeres = this.nbBlessuresLegeres() const nbGraves = this.nbBlessuresGraves() const nbCritiques = this.nbBlessuresCritiques() if (nbLegeres + nbGraves + nbCritiques == 0) { return "Aucune blessure"; } let resume = "Blessures:"; if (nbLegeres > 0) { resume += " " + nbLegeres + " légère" + (nbLegeres > 1 ? "s" : ""); } if (nbGraves > 0) { if (nbLegeres > 0) resume += ","; resume += " " + nbGraves + " grave" + (nbGraves > 1 ? "s" : ""); } if (nbCritiques > 0) { if (nbGraves > 0 || nbLegeres > 0) resume += ","; resume += " une CRITIQUE !"; } return resume; } blessuresASoigner() { return [] } async computeArmure(attackerRoll) { return this.getProtectionNaturelle() } async remiseANeuf() { } async ajoutExperience(rollData, hideChatMessage = 'show') { } /* -------------------------------------------- */ async onAppliquerJetEncaissement(encaissement, attackerToken) { const santeOrig = foundry.utils.duplicate(this.system.sante); const blessure = this.nouvelleBlessure(encaissement.gravite, { localisation: encaissement.dmg?.loc.label ?? '', origine: attackerToken?.name ?? '' }) if (blessure.system.gravite == encaissement.gravite) { blessure.system.vie = encaissement.vie blessure.system.endurance = encaissement.endurance } else { // aggravation du fait du nombre de blessures blessure.system.vie = blessure.system.vie const rollPerteEndurance = new Roll(blessure.system.endurance) await rollPerteEndurance.evaluate() blessure.system.endurance = rollPerteEndurance.total } const isCritique = blessure.system.gravite >= 6; if (isCritique) { blessure.system.endurance = this.getEnduranceActuelle() } // Will update the result table if (blessure.system.gravite > 6) { this.setEffect(STATUSES.StatusComma, true) encaissement.mort = "à seconde blessure critique" } const perteVie = await this.santeIncDec("vie", -encaissement.vie) const perteEndurance = await this.santeIncDec("endurance", -encaissement.endurance, isCritique) await this.createEmbeddedDocuments('Item', [blessure]) foundry.utils.mergeObject(encaissement, { resteEndurance: perteEndurance.newValue, sonne: perteEndurance.sonne, jetEndurance: perteEndurance.jetEndurance, endurance: perteEndurance.perte, vie: santeOrig.vie.value - perteVie.newValue, blessure: blessure }) } /* -------------------------------------------- */ async santeIncDec(name, inc, isCritique = false) { if (name == 'fatigue' && !ReglesOptionnelles.isUsing("appliquer-fatigue")) { return } if (!this.system.sante[name]) { return } const sante = foundry.utils.duplicate(this.system.sante) const compteur = sante[name] const result = { sonne: false } let perteEndurance = 0 let minValue = name == "vie" ? -this.getSConst() - 1 : 0; result.newValue = Math.max(minValue, Math.min(compteur.value + inc, compteur.max)); let fatigue = 0; if (name == "endurance") { if (result.newValue == 0 && inc < 0 && !isCritique) { // perte endurance et endurance devient 0 (sauf critique) -> -1 vie sante.vie.value-- result.perteVie = true } result.newValue = Math.max(0, result.newValue); if (inc > 0) { // le max d'endurance s'applique seulement à la récupération result.newValue = Math.min(result.newValue, this._computeEnduranceMax()) } perteEndurance = compteur.value - result.newValue; result.perte = perteEndurance if (sante.fatigue && inc < 0) { // Each endurance lost -> fatigue lost fatigue = perteEndurance; } } compteur.value = result.newValue; // If endurance lost, then the same amount of fatigue cannot be recovered if (ReglesOptionnelles.isUsing("appliquer-fatigue") && sante.fatigue && fatigue > 0) { sante.fatigue.value = Math.max(sante.fatigue.value + fatigue, this.getFatigueMin()); } await this.update({ "system.sante": sante }) if (perteEndurance > 1) { // Peut-être sonné si 2 points d'endurance perdus d'un coup foundry.utils.mergeObject(result, await this.jetEndurance(result.newValue)); } else if (name == "endurance" && inc > 0) { await this.setSonne(false) } if (this.isDead()) { await this.setEffect(STATUSES.StatusComma, true) } return result } async callbackPremiersSoins(blessureId, rollData, soigneurId) { await this.onRollTachePremiersSoins(blessureId, rollData.v2 ? rollData.current.tache.tache : rollData.tache, rollData.rolled.isETotal, soigneurId) } async onRollTachePremiersSoins(blessureId, tache, isETotal, soigneurId) { if (!this.isOwner) { return RdDBaseActor.remoteActorCall({ tokenId: this.token?.id, actorId: this.id, method: 'onRollTachePremiersSoins', args: [blessureId, tache, isETotal, soigneurId] }) } const blessure = this.getItem(blessureId, 'blessure') if (blessure && !blessure.system.premierssoins.done) { if (isETotal) { await blessure.update({ 'system.difficulte': blessure.system.difficulte - 1, 'system.premierssoins.tache': Math.max(0, tache.system.points_de_tache_courant) }) } else { const bonus = tache.system.points_de_tache_courant - tache.system.points_de_tache await blessure.update({ 'system.premierssoins': { done: (bonus >= 0), bonus: Math.max(0, bonus), tache: Math.max(0, tache.system.points_de_tache_courant) } }) if (bonus >= 0 && soigneurId) { const soigneur = game.actors.get(soigneurId) await soigneur.deleteEmbeddedDocuments('Item', [tache.id], { render: true }) } } } } async onRollSoinsComplets(blessureId, rollData) { if (!this.isOwner) { return RdDBaseActor.remoteActorCall({ tokenId: this.token?.id, actorId: this.id, method: 'onRollSoinsComplets', args: [blessureId, rollData] }) } const blessure = this.getItem(blessureId, 'blessure') if (blessure && blessure.system.premierssoins.done && !blessure.system.soinscomplets.done) { // TODO: update de la blessure: passer par le MJ! if (rollData.rolled.isETotal) { await blessure.setSoinsBlessure({ difficulte: blessure.system.difficulte - 1, premierssoins: { done: false, bonus: 0 }, soinscomplets: { done: false, bonus: 0 }, }) } else { // soins complets finis await blessure.setSoinsBlessure({ soinscomplets: { done: true, bonus: Math.max(0, rollData.rolled.ptTache) }, }) } } } /* -------------------------------------------- */ _computeEnduranceMax() { const diffVie = this.system.sante.vie.max - this.system.sante.vie.value; const maxEndVie = this.system.sante.endurance.max - (diffVie * 2); const nbGraves = this.countBlessures(it => it.isGrave()) > 0 const nbCritiques = this.countBlessures(it => it.isCritique()) > 0 const maxEndGraves = Math.floor(this.system.sante.endurance.max / (2 * nbGraves)); const maxEndCritiques = nbCritiques > 0 ? 1 : this.system.sante.endurance.max; return Math.max(0, Math.min(maxEndVie, maxEndGraves, maxEndCritiques)); } async onCreateItem(item, options, id) { await this.changeItemEffects(item) await super.onCreateItem(item, options, id) } async onUpdateItem(item, updates, options, id) { await this.changeItemEffects(item); await super.onUpdateItem(item, updates, options, id) } async onDeleteItem(item, options, id) { await this.changeItemEffects(item); await super.onDeleteItem(item, options, id) } async changeItemEffects(item) { switch (item.type) { case ITEM_TYPES.blessure: await this.changeStateBleeding(); break; case ITEM_TYPES.maladie: case ITEM_TYPES.poison: await this.changeStateMalade(); break; } } async changeStateBleeding() { const bleeding = this.itemTypes[ITEM_TYPES.blessure].find(it => it.isBleeding()) await this.setEffect(STATUSES.StatusBleeding, bleeding ? true : false) } async changeStateMalade() { const maladiePoisons = this.getMaladiesPoisons() await this.setEffect(STATUSES.StatusMalade, maladiePoisons.length > 0) } getMaladiesPoisons() { return this.items.filter(it => [ITEM_TYPES.maladie, ITEM_TYPES.poison].includes(it.type)) } /* -------------------------------------------- */ nouvelleBlessure(gravite, options = { origine: undefined, localisation: '' }) { if (gravite < 0) return if (gravite > 0) { while (this.countBlessures(it => it.system.gravite == gravite) >= RdDItemBlessure.maxBlessures(gravite) && gravite <= 6) { // Aggravation gravite += 2 } } return RdDItemBlessure.prepareBlessure(gravite, options); } async supprimerBlessure({ gravite }) { const toDelete = this.itemTypes[ITEM_TYPES.blessure].find(it => it.system.gravite == gravite)?.id if (toDelete) { await this.deleteEmbeddedDocuments('Item', [toDelete]); } } async supprimerBlessures(filterToDelete) { const toDelete = this.filterItems(filterToDelete, ITEM_TYPES.blessure) .map(it => it.id) await this.deleteEmbeddedDocuments('Item', toDelete) } countBlessures(filter = it => !it.isContusion()) { return this.filterItems(filter, ITEM_TYPES.blessure).length } /* -------------------------------------------- */ async jetDeVie() { if (this.isDead()) { ChatMessage.create({ content: `Jet de Vie: ${this.getAlias()} est déjà mort, ce n'est pas la peine d'en rajouter !!!!!`, whisper: ChatUtility.getOwners(this) }) return } const jetDeVie = await RdDDice.roll("1d20"); const sConst = this.getSConst(); const vie = this.system.sante.vie.value; const isCritique = this.nbBlessuresCritiques() > 0; const isGrave = this.nbBlessuresGraves(); const isEchecTotal = jetDeVie.total == 20; const isSuccess = jetDeVie.total == 1 || jetDeVie.total <= vie; const perte = isSuccess ? 0 : 1 + (isEchecTotal ? vie + sConst : 0) const prochainJet = (jetDeVie.total == 1 && vie > 0 ? 20 : 1) * (isCritique ? 1 : isGrave > 0 ? sConst : 0) let msgText = `Jet de Vie: ${jetDeVie.total} / ${vie}` if (isSuccess) { msgText += "
Réussi, pas de perte de point de vie." } else { msgText += `
Echoué, perte ${perte} point de vie`; await this.santeIncDec("vie", -perte); } if (this.isDead()) { msgText += `
${this.getAlias()} est mort !!!!`; } else if (prochainJet > 0) { msgText += `
Prochain jet de vie dans ${prochainJet} ${isCritique ? 'round' : 'minute'}${prochainJet > 1 ? 's' : ''} ${isCritique ? '(état critique)' : '(état grave)'}` } ChatMessage.create({ content: msgText, whisper: ChatUtility.getOwners(this) }) } /* -------------------------------------------- */ async jetEndurance(resteEndurance = undefined) { const jetEndurance = (await RdDDice.roll("1d20")).total; const sonne = jetEndurance == 20 || jetEndurance > (resteEndurance ?? this.system.sante.endurance.value) if (sonne) { await this.setSonne(true) } return { jetEndurance, sonne } } async finDeRoundBlessures() { const nbGraves = this.itemTypes[ITEM_TYPES.blessure] .filter(it => it.isGrave() && !it.system.premierssoins.done).length; if (nbGraves > 0) { // Gestion blessure graves : -1 pt endurance par blessure grave await this.santeIncDec("endurance", -nbGraves); } } async setSonne(sonne = true) { if (!game.combat && sonne) { // TODO: vérifier si comportement toujours valable ui.notifications.info(`${this.getAlias()} est hors combat, il ne reste donc pas sonné`); return } await this.setEffect(STATUSES.StatusStunned, sonne) } isSonne() { return this.getEffectsByStatus(STATUSES.StatusStunned).length > 0 } isEffectAllowed(effectId) { return true } /* -------------------------------------------- */ async computeEtatGeneral() { this.system.compteurs.etat.value = this.malusVie() + this.malusFatigue() + this.malusEthylisme() } getEtatGeneral(options = { ethylisme: false }) { return this.system.compteurs.etat.value } malusVie() { return Math.min(this.system.sante.vie.value - this.system.sante.vie.max, 0) } malusEthylisme() { return 0 } }