Files
vermine2047/module/system/fight.mjs
T
uberwald 90de66d668
Release Creation / build (release) Failing after 1m28s
fix: Dice So Nice import v14 compat, override Foundry text colors for readability
- hooks.mjs: replace static dice-so-nice import with dynamic import
  using game.modules.get('dice-so-nice').id (path removed in v14)
- hooks.mjs: fix permission condition (|| -> &&), jQuery -> vanilla JS
- less/base.less: override --color-text-* and --button-text-color
  for both .themed.theme-dark (AppV2) and body.theme-dark (legacy apps)
- target #settings-config buttons + labels + hints for dark grays
2026-07-12 22:05:45 +02:00

286 lines
9.4 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 = undefined, messageOptions = {}) {
// roll initiative
return super.rollInitiative(ids, formula, 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 {
get template() {
return "systems/vermine2047/templates/combat-tracker.hbs";
}
async getData(options) {
const context = await super.getData(options);
if (!context.hasCombat) {
return context;
}
return context;
}
activateListeners(html) {
super.activateListeners(html);
html.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)
}
}