Files
vermine2047/module/system/fight.mjs
T
uberwald df852f17d1 fix: aligner le JS avec l'API Foundry v14
- effects.mjs: remove e._getSourceName() (removed in v14, now a getter)
- fight.mjs: convert VermineCombatTracker from AppV1 (get template/getData)
  to AppV2 PARTS + _prepareContext/_prepareTurnContext
- fight.mjs: VermineCombat.rollInitiative v14 options-object signature
- hooks.mjs: renderPause -> renderGamePause; getSceneControlButtons now
  uses controls.tokens and tools record (v14 names/structure)
- roll.mjs: ChatMessage.create uses rolls array (v14 removed 'roll' key)
- item.mjs: getRollData returns system when no actor (v14 expects object)
- vermine2047.mjs: correct unregisterSheet API for ActorSheetV2
- base-item-sheet.mjs: call super._onRender
2026-08-01 21:33:38 +02:00

335 lines
11 KiB
JavaScript

/**
* Handles combat-related dice rolls for Vermine2047.
* Uses d10-based system with success counting as per official rules.
*/
export class VermineFight {
/**
* Performs a d10-based test according to Vermine2047 official rules.
* Each die result is compared to a difficulty threshold.
* Each result >= difficulty counts as 1 Success.
*
* @param {number} enemyAchievement - Opponent's achievement score
* @param {number} enemyConservation - Opponent's conservation score
* @param {string} skillKey - The skill key being tested
* @param {number} skill - The skill value
* @param {Object} params - Additional test parameters
* @param {number} [params.difficulty=7] - Difficulty threshold (3-10)
* @param {boolean} [params.spleen] - Whether to use spleen rule
* @param {boolean} [params.purpose] - Whether to use purpose rule
* @param {number} [params.usure] - Wear/usage modifier
* @param {number} [params.trait] - Trait bonus
* @param {boolean} [params.specialization] - Whether specialization applies
* @param {Actor} actor - The actor performing the test
* @returns {Promise<Object>} Roll data including successes count
*/
async performTest(enemyAchievement, enemyConservation, skillKey, skill, params, actor) {
// Use d10 as per Vermine2047 official rules
const basePool = Math.max(skill, 1)
const dicePool = (params.spleen != undefined || params.purpose != undefined) ? String(basePool + 1) : String(basePool)
const difficulty = params.difficulty || 7; // Default difficulty
const r = new Roll(dicePool + `d10`);
let diceString = '';
let dicePoolHint = '';
let discardedRoll = false;
let bonus = 0;
let bonusText = '/+';
let currentSkillScore = skill;
r.roll(); // dice are rolled
if (params.usure != undefined) {
currentSkillScore += params.usure;
bonus += params.usure;
}
if (params.specialization != undefined) {
currentSkillScore += 2;
}
if (params.trait != undefined) {
currentSkillScore += params.trait;
bonus += params.trait;
}
bonusText += bonus;
let targetText = game.i18n.format('VERMINE.Selected') + ' : ' + game.i18n.format(skillKey) + " " + skill + bonusText;
if (params.specialization != undefined) {
targetText += " (S)";
}
// tri par ordre croissant
r.terms[0].results.sort((a, b) => a.result - b.result);
if (params.purpose != undefined) {
discardedRoll = r.terms[0].results.shift();
dicePoolHint = ' - ' + game.i18n.format('VERMINE.PurposeTrait');
} else if (params.spleen != undefined) {
discardedRoll = r.terms[0].results.pop();
dicePoolHint = ' - ' + game.i18n.format('VERMINE.SpleenTrait');
}
const discardedRollText = (discardedRoll.result != undefined) ? '<div class="discarded-roll">' + discardedRoll.result + '</div>' : "";
// Count successes (dice >= difficulty) as per Vermine2047 rules
let successes = 0;
for (let i = 0; i < r.terms[0].results.length; i++) {
let result = r.terms[0].results[i].result;
if (result >= difficulty) {
successes++;
}
diceString += '<li class="roll die d10 die-' + i + (result >= difficulty ? ' success' : '') + '">' + result + '</li>';
}
let hintText = game.i18n.format('VERMINE.ConfrontationHint');
// Build a dynamic html using the variables from above.
const html = `
<div class="vermine2047 roll confrontation">
<div class="dice-roll">
<div class="dice-result">
<div class="dice-formula">
` + dicePool + `d10 ` + dicePoolHint + `
</div>
<div class="dice-tooltip expanded">
<section class="tooltip-part">
<div class="parameters">
` + targetText + `
</div>
<div class="dice flexrow flex-between items-center">
<ol class="dice-rolls">` + diceString + `</ol>
<div class="discards text-right">` + discardedRollText + `</div>
</div>
</section>
</div>` +
`<p class="step1-text" id="step1">` + hintText + `</p>
<div class="row">
<a class="inline-block button add-to-achievement">` + game.i18n.format('VERMINE.Achievement') + `</a>
<a class="inline-block button add-to-conservation">` + game.i18n.format('VERMINE.Conservation') + `</a>
<a class="inline-block button reset"><i class="fa-solid fa-rotate-right"></i></a>
<a class="inline-block button resolve"><i class="fa-solid fa-check"></i></a>
</div>
<div class="success-count">
<strong>` + game.i18n.format('VERMINE.success_count') + `:</strong> <span class="success-value">` + successes + `</span>
</div>
</div>
</div>
</div>
`;
// Check if the dice3d module exists (Dice So Nice). If it does, post a roll in that and then
// send to chat after the roll has finished. If not just send to chat.
if (game.dice3d) {
game.dice3d.showForRoll(r).then((displayed) => {
this.sendToChat(html, r, actor);
});
} else {
this.sendToChat(html, r, actor);
};
// Return roll data for further processing with d10 success counting
return {
roll: r,
successes: successes,
difficulty: difficulty,
dicePool: parseInt(dicePool, 10),
total: r.total
};
}
async sendToChat(content, roll, actor) {
let conf = {
user: game.user._id,
content: content,
roll: roll,
// sound: 'sounds/dice.wav'
};
if (actor)
conf.speaker = ChatMessage.getSpeaker({ actor: actor });
// Send's Chat Message to foundry, if items are missing they will appear as false or undefined and this not be rendered.
ChatMessage.create(conf).then((msg) => {
return msg;
});
}
static instance = null;
static get() {
if (!VermineFight.instance)
VermineFight.instance = new VermineFight();
return VermineFight.instance;
}
// data injected to char data
static previousValues = {
dicePool: 4,
skills: [],
cskills: [],
cephalic: false,
achievementReroll: 0,
conservationReroll: 0
};
static rollerTemplate = 'systems/vermine2047/templates/fight.html';
static CombatResultTemplate = 'systems/vermine2047/templates/fight-result.html';
}
export class VermineCombat extends Combat {
_encounterCheck() {
// encounter check
}
async rollInitiative(ids, {formula=null, updateTurn=true, messageMode, messageOptions={}}={}) {
// roll initiative
return super.rollInitiative(ids, {formula, updateTurn, messageMode, messageOptions});
}
nextRound() {
/*let combatants = this.combatants.contents
for (let c of combatants) {
let actor = game.actors.get( c.actorId )
actor.clearRoundModifiers()
}*/
super.nextRound();
}
/************************************************************************************/
startCombat() {
/*let combatants = this.combatants.contents
for (let c of combatants) {
let actor = game.actors.get( c.actorId )
actor.storeVitaliteCombat()
}*/
return super.startCombat();
}
/************************************************************************************/
_onDelete() {
/*let combatants = this.combatants.contents
for (let c of combatants) {
let actor = game.actors.get(c.actorId)
actor.clearInitiative()
actor.displayRecuperation()
}
super._onDelete()*/
}
}
export class VermineCombatTracker extends foundry.applications.sidebar.tabs.CombatTracker {
/** @override */
static PARTS = {
main: {
template: "systems/vermine2047/templates/combat-tracker.hbs",
root: true
}
};
/** @override */
async _prepareContext(options) {
const context = await super._prepareContext(options);
context.user = game.user;
const combat = this.viewed;
const hasCombat = combat !== null;
const combats = this.combats;
const currentIdx = combats.indexOf(combat);
const isPlayerTurn = combat?.combatant?.players?.includes(game.user);
const canControl = combat?.turn && combat.turn.between(1, combat.turns.length - 2)
? combat.canUserModify(game.user, "update", { turn: 0 })
: combat?.canUserModify(game.user, "update", { round: 0 });
Object.assign(context, {
combat,
hasCombat,
combatCount: combats.length,
currentIndex: currentIdx + 1,
previousId: combats[currentIdx - 1]?.id,
nextId: combats[currentIdx + 1]?.id,
round: combat?.round,
control: isPlayerTurn && canControl,
linked: combat?.scene !== null,
cssClass: combats.length > 7 ? "cycle" : combats.length ? "tabbed" : "",
cssId: "combat",
tabName: "combat",
labels: {
scope: game.i18n.localize(`COMBAT.${combat?.scene ? "Linked" : "Unlinked"}`)
},
turns: []
});
if ( combat ) {
for ( const [i, combatant] of combat.turns.entries() ) {
if ( !combatant.visible ) continue;
context.turns.push(await this._prepareTurnContext(combat, combatant, i));
}
}
return context;
}
/** @override */
async _prepareTurnContext(combat, combatant, index) {
const turn = await super._prepareTurnContext(combat, combatant, index);
const actor = combatant.actor;
turn.attitude = combatant.getFlag("world", "attitude") || "active";
turn.isPlayer = actor?.hasPlayerOwner ?? false;
turn.isNpc = !!actor && !actor.hasPlayerOwner;
turn.owner = combatant.isOwner;
turn.defeated = combatant.isDefeated;
turn.hasRolled = combatant.initiative !== null;
turn.hasResource = combatant.resource !== null && combatant.resource !== undefined;
turn.effects = (turn.effects?.icons ?? []).map(e => e.img);
return turn;
}
_onRender(context, options) {
super._onRender(context, options);
this.element.querySelectorAll("[data-attitude]").forEach(el => {
el.addEventListener("click", this._setStatut.bind(this));
});
}
/**
* @description Use to put an attitude to an actor
* @param {*} event
*/
async _setStatut(event) {
event.preventDefault();
event.stopPropagation();
const btn = event.currentTarget;
const attitude = btn.dataset.attitude;
const li = btn.closest(".combatant");
const combat = this.viewed;
const combatant = combat.combatants.get(li.dataset.combatantId);
let flag = combatant.getFlag("world", "attitude");
if (flag == attitude) {
await combatant.setFlag("world", "attitude", null);
} else {
await combatant.setFlag("world", "attitude", attitude);
}
let actor = await game.actors.get(combatant.actorId)
// combatant attitude set
}
}
export class VermineCombatant extends Combatant {
constructor(data, context) {
super(data, context)
if (this.token) {
this.attitude = this.token.flags.world?.attitude || "active"
} else {
this.attitude = "active"
}
}
_getInitiativeFormula() {
return String(CONFIG.Combat.initiative.formula || game.system.initiative)
}
}