diff --git a/eslint.config.mjs b/eslint.config.mjs index 16a335a..f47a550 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -34,6 +34,7 @@ export default [ fromUuid: `readonly`, Combat: `readonly`, Combatant: `readonly`, + canvas: `readonly`, }, }, }, diff --git a/module/Apps/sidebar/CombatTracker.mjs b/module/Apps/sidebar/CombatTracker.mjs index 05f5f29..483cdd6 100644 --- a/module/Apps/sidebar/CombatTracker.mjs +++ b/module/Apps/sidebar/CombatTracker.mjs @@ -1,5 +1,3 @@ -import { Logger } from "../../utils/Logger.mjs"; - const { CombatTracker } = foundry.applications.sidebar.tabs; export class RipCryptCombatTracker extends CombatTracker { @@ -7,15 +5,13 @@ export class RipCryptCombatTracker extends CombatTracker { * @override */ async _prepareTurnContext(combat, combatant, index) { - Logger.debug(`_prepareTurnContext`); const turn = await super._prepareTurnContext(combat, combatant, index); - // if ( !this.viewed ) return; + turn.hasDecimals = true; + turn.initiative = combatant.dynamicInitiative; - // ! TODO: This is causing an error while the combat is not active, but is fine when it's active, this needs to be fixed - Logger.debug(turn, combatant); const groupKey = combatant.groupKey; - if (groupKey) { + if (groupKey && combat.started) { turn.active ||= combat.combatant.groupKey === groupKey; if (turn.active && !turn.css.includes(`active`)) { turn.css += `active`; @@ -23,5 +19,15 @@ export class RipCryptCombatTracker extends CombatTracker { }; return turn; - } + }; + + async _onRender(...args) { + await super._onRender(...args); + + // Purge the combat controls that I don't want to exist because they don't + // make sense in the system. + this.element.querySelector(`[data-action="resetAll"]`)?.remove(); + this.element.querySelector(`[data-action="rollNPC"]`)?.remove(); + this.element.querySelector(`[data-action="rollAll"]`)?.remove(); + }; }; diff --git a/module/documents/combat.mjs b/module/documents/combat.mjs index 6be6f22..acfedbe 100644 --- a/module/documents/combat.mjs +++ b/module/documents/combat.mjs @@ -31,15 +31,102 @@ export class RipCryptCombat extends Combat { * - Coin Flip result (if disposition matches flip result, then 0, otherwise, 0.5) */ _sortCombatants(a, b) { - // The distance from fate - return super._sortCombatants(a, b) * -1; + const ia = Number.isNumeric(a.dynamicInitiative) ? a.dynamicInitiative : -Infinity; + const ib = Number.isNumeric(b.dynamicInitiative) ? b.dynamicInitiative : -Infinity; + + const delta = ia - ib; + if (Math.sign(delta) !== 0) { + return delta; + }; + + // fallback to alphabetical sort + return a.name < b.name ? -1 : 1; }; - // nextTurn() { - // // Make it skip all combatants with the same initiative value - // }; + async nextTurn() { + if ( this.round === 0 ) {return this.nextRound()} - // previousTurn() { - // // Go back a step - // }; + const turn = this.turn ?? -1; + + const groupKey = this.combatant.groupKey; + + // Determine the next turn number + let nextTurn = null; + for (let i = turn + 1; i < this.turns.length; i++) { + const combatant = this.turns[i]; + if (combatant.groupKey !== groupKey) { + nextTurn = i; + break; + }; + }; + + // Maybe advance to the next round + if ( (nextTurn === null) || (nextTurn >= this.turns.length) ) {return this.nextRound()} + + const advanceTime = this.getTimeDelta(this.round, this.turn, this.round, nextTurn); + + // Update the document, passing data through a hook first + const updateData = {round: this.round, turn: nextTurn}; + const updateOptions = {direction: 1, worldTime: {delta: advanceTime}}; + Hooks.callAll(`combatTurn`, this, updateData, updateOptions); + await this.update(updateData, updateOptions); + return this; + }; + + async previousTurn() { + if (this.round === 0) { return this } + if ((this.turn === 0) || (this.turns.length === 0)) {return this.previousRound()} + + const currentTurn = (this.turn ?? this.turns.length) - 1; + let previousTurn = null; + const groupKey = this.combatant.groupKey; + for (let i = currentTurn; i >= 0; i--) { + const combatant = this.turns[i]; + if (combatant.groupKey !== groupKey) { + previousTurn = i; + break; + } + } + + const advanceTime = this.getTimeDelta(this.round, this.turn, this.round, previousTurn); + + // Update the document, passing data through a hook first + const updateData = {round: this.round, turn: previousTurn}; + const updateOptions = {direction: -1, worldTime: {delta: advanceTime}}; + Hooks.callAll(`combatTurn`, this, updateData, updateOptions); + await this.update(updateData, updateOptions); + return this; + }; + + /** + * Update display of Token combat turn markers. + * @protected + * @internal + */ + _updateTurnMarkers() { + return super._updateTurnMarkers(); + /* eslint-disable no-unreachable */ + if (!canvas.ready) { return }; + + const tokenGroup = this.combatant?.groupKey; + for (const token of canvas.tokens.turnMarkers) { + const actor = token.actor ?? token.baseActor; + if (actor?.groupKey !== tokenGroup) { + token.renderFlags.set({refreshTurnMarker: true}); + } + } + + if (!this.active) { return }; + const currentToken = this.combatant?.token?._object; + console.log(this.combatant.token._object) + if (!tokenGroup && currentToken) { + currentToken.renderFlags.set({refreshTurnMarker: true}); + } else { + for (const combatant of this.groups.get(tokenGroup)) { + console.log(combatant.token._object); + combatant.token._object.renderFlags.set({ refreshTurnMarker: true }); + } + } + /* eslint-enable no-unreachable */ + } }; diff --git a/module/documents/combatant.mjs b/module/documents/combatant.mjs index d925fc6..648a7aa 100644 --- a/module/documents/combatant.mjs +++ b/module/documents/combatant.mjs @@ -2,17 +2,34 @@ import { distanceBetweenFates } from "../utils/distanceBetweenFates.mjs"; export class RipCryptCombatant extends Combatant { - async _preCreate(data, options, user) { - const allowed = await super._preCreate(data, options, user); - if (allowed === false) { return false }; + get disposition() { + switch (this.token.disposition) { + case CONST.TOKEN_DISPOSITIONS.HOSTILE: + return `hostile`; + case CONST.TOKEN_DISPOSITIONS.FRIENDLY: + return `friendly`; + }; + return `unknown`; + }; + + get dynamicInitiative() { + let total = 0; const start = game.settings.get(`ripcrypt`, `currentFate`); const end = this.actor?.system?.fate || this.baseActor?.system?.fate; - const fateDistance = distanceBetweenFates(start, end); + total += distanceBetweenFates(start, end); - this.updateSource({ - initiative: fateDistance, - }); + const whoFirst = game.settings.get(`ripcrypt`, `whoFirst`); + const disposition = this.disposition; + if (whoFirst) { + if (disposition === `unknown`) { + total += 0.25; + } else if (whoFirst !== disposition) { + total += 0.5; + }; + } + + return total; }; get groupKey() { diff --git a/module/hooks/init.mjs b/module/hooks/init.mjs index a6cbf58..2577eb2 100644 --- a/module/hooks/init.mjs +++ b/module/hooks/init.mjs @@ -35,6 +35,7 @@ import { RipCryptCombatTracker } from "../Apps/sidebar/CombatTracker.mjs"; Hooks.once(`init`, () => { Logger.log(`Initializing`); + CONFIG.Combat.initiative.decimals = 1; CONFIG.ui.crypt = DelveTourApp; // #region Settings diff --git a/module/settings/metaSettings.mjs b/module/settings/metaSettings.mjs index 1f2abaf..9e6ec4d 100644 --- a/module/settings/metaSettings.mjs +++ b/module/settings/metaSettings.mjs @@ -18,4 +18,15 @@ export function registerMetaSettings() { ui.crypt.render({ parts: [ `fate` ] }); }, }); + game.settings.register(`ripcrypt`, `whoFirst`, { + scope: `world`, + type: String, + config: false, + requiresReload: false, + initial: `friendly`, + onChange: async () => { + await game.combat.setupTurns(); + await ui.combat.render({ parts: [ `tracker` ] }); + }, + }); };