Skip to content

Commit

Permalink
Fixed bunch of stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
Haxxer committed Oct 6, 2023
1 parent 28e39d9 commit 74b9f1b
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 23 deletions.
12 changes: 10 additions & 2 deletions languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
},
"OverkillDamage": {
"Title": "Overkill Damage!",
"MeleeLabel1": "You can destroy a total of {max_targets} \"{name}\" minions within your weapon's range.",
"RangedLabel1": "You can destroy a total of {max_targets} \"{name}\" minions directly behind the original target and within your weapon's range.",
"MeleeLabel1": "You can destroy a total of {max_targets} \"{name}\" minions within your attack's range.",
"RangedLabel1": "You can destroy a total of {max_targets} \"{name}\" minions directly behind the original target and within your attack's range.",
"Label2": "Total targets remaining: {total_targets}/{max_targets}",
"Label3": "Once you're happy with your targets, press OK."
}
Expand Down Expand Up @@ -41,6 +41,14 @@
"Title": "Enable Ranged Overkill Damage",
"Hint": "When enabled, ranged attacks also trigger overkill damage. The MDCM rules allow this, so long the subsequent targets are in a line directly behind the original target."
},
"EnableSpellOverkill": {
"Title": "Enable Spell Attack Overkill Damage",
"Hint": "When enabled, melee and ranged spell attacks also trigger overkill damage. By default, MCDM rules does not allow this."
},
"EnableOverkillMessage": {
"Title": "Enable Overkill Message",
"Hint": "When enabled, overkill chat messages are shown."
},
"EnableGroupAttacks": {
"Title": "Enable Minion Group Attacks",
"Hint": "When enabled, minion group attacks automatically prompt to ask how many minions are attacking the target."
Expand Down
20 changes: 20 additions & 0 deletions scripts/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ CONSTANTS["SETTING_KEYS"] = {
DEBUG: "debug",
ENABLE_OVERKILL_DAMAGE: "enableOverkillDamage",
ENABLE_RANGED_OVERKILL: "enableRangedOverkill",
ENABLE_SPELL_OVERKILL: "enableSpellOverkill",
ENABLE_OVERKILL_MESSAGE: "enableOverkillMessage",
ENABLE_GROUP_ATTACKS: "enableGroupAttacks",
ENABLE_GROUP_ATTACK_BONUS: "enableGroupAttackBonus",
ENABLE_MINION_SUPER_SAVE: "enableMinionSuperSave",
Expand Down Expand Up @@ -64,6 +66,24 @@ CONSTANTS["SETTINGS"] = {
type: Boolean
},

[CONSTANTS.SETTING_KEYS.ENABLE_SPELL_OVERKILL]: {
name: "MINIONMANAGER.Settings.EnableSpellOverkill.Title",
hint: "MINIONMANAGER.Settings.EnableSpellOverkill.Hint",
scope: "world",
config: true,
default: false,
type: Boolean
},

[CONSTANTS.SETTING_KEYS.ENABLE_OVERKILL_MESSAGE]: {
name: "MINIONMANAGER.Settings.EnableOverkillMessage.Title",
hint: "MINIONMANAGER.Settings.EnableOverkillMessage.Hint",
scope: "world",
config: true,
default: true,
type: Boolean
},

[CONSTANTS.SETTING_KEYS.ENABLE_GROUP_ATTACKS]: {
name: "MINIONMANAGER.Settings.EnableGroupAttacks.Title",
hint: "MINIONMANAGER.Settings.EnableGroupAttacks.Hint",
Expand Down
17 changes: 12 additions & 5 deletions scripts/initiative.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function refreshInitiativeGroupGraphics(t) {
existingSprite.turn = minionTurn;
}

const diameter = 24
const diameter = 24 * (canvas.grid.size / 100) * tokenDocument.width;
const x = (tokenDocument.width * canvas.grid.size) - diameter - 4;
const y = (tokenDocument.height * canvas.grid.size) - diameter - 4;

Expand All @@ -40,7 +40,7 @@ export function refreshInitiativeGroupGraphics(t) {
background.width = diameter;
background.height = diameter;
background.beginFill(0xFFFFFF)
.drawCircle((diameter / 2), (diameter / 2), (diameter / 2) - 2)
.drawCircle((diameter / 2), (diameter / 2), Math.max(1, (diameter-2)/2))
.endFill();

sprite._background = background;
Expand Down Expand Up @@ -98,17 +98,24 @@ export function initializeInitiative() {

Hooks.on("refreshToken", refreshInitiativeGroupGraphics);

Hooks.on("preCreateCombatant", (doc) => {
const createdCombatantToken = doc.token;
const groupNumber = getProperty(createdCombatantToken, CONSTANTS.FLAGS.GROUP_NUMBER);
if (!groupNumber || !game.combats.viewed) return true;
return !game.combats.viewed.combatants.some(combatant => getProperty(combatant.token, CONSTANTS.FLAGS.GROUP_NUMBER) === groupNumber);
})

let tokenBeingDeleted = false;
Hooks.on("preDeleteToken", (doc) => {
tokenBeingDeleted = doc.id;
})

Hooks.on("preDeleteCombatant", (combatant) => {
if (tokenBeingDeleted !== combatant.tokenId) return;
if (tokenBeingDeleted !== combatant.tokenId) return true;
const existingSubCombatants = foundry.utils.deepClone(getProperty(combatant, CONSTANTS.FLAGS.COMBATANTS));
if (!existingSubCombatants?.length) return;
if (!existingSubCombatants?.length) return true;
const newToken = existingSubCombatants.map(uuid => fromUuidSync(uuid)).filter(foundToken => foundToken?.actor?.id);
if (!newToken.length) return;
if (!newToken.length) return true;
combatant.update({
actorId: newToken[0].actor.id,
tokenId: newToken[0].id
Expand Down
9 changes: 4 additions & 5 deletions scripts/interface.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import CONSTANTS from "./constants.js";
import { refreshInitiativeGroupGraphics } from "./initiative.js";
import * as api from "./api.js";
import { libWrapper } from "./libwrapper/shim.js";

export function initializeInterface() {

Expand Down Expand Up @@ -40,7 +41,7 @@ export function initializeInterface() {

const extra = $(`<div class="control-icon grouped-initiative"></div>`);

for (const index in Array(9).fill(0)) {
for (let index = 0; index < 9; index++) {

const newGroupNumber = Number(index) + 1;

Expand All @@ -53,10 +54,10 @@ export function initializeInterface() {
const tokensKeptInOldGroup = canvas.scene.tokens.filter(oldToken => {
const tokenGroupNumber = getProperty(oldToken, CONSTANTS.FLAGS.GROUP_NUMBER);
return !newTokens.includes(oldToken) && existingGroupNumber && tokenGroupNumber && existingGroupNumber === tokenGroupNumber;
})
});

const existingCombatantGroup = game.combats.viewed.combatants.find(combatant => {
return getProperty(combatant.token, CONSTANTS.FLAGS.GROUP_NUMBER) === existingGroupNumber;
return existingGroupNumber && getProperty(combatant.token, CONSTANTS.FLAGS.GROUP_NUMBER) === existingGroupNumber;
});

if (existingCombatantGroup && !tokensKeptInOldGroup.length) {
Expand Down Expand Up @@ -137,9 +138,7 @@ export function initializeInterface() {
turn.css = turn.css.replace("defeated", "");
}
}

return data;

}, "WRAPPER");

Hooks.on("preUpdateCombatant", (combatant, data) => {
Expand Down
23 changes: 23 additions & 0 deletions scripts/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,26 @@ export function log(msg, args = {}) {
export function getSetting(key) {
return game.settings.get(CONSTANTS.MODULE_NAME, key);
}

export function isValidOverkillItem(item) {

if (!getSetting(CONSTANTS.SETTING_KEYS.ENABLE_OVERKILL_DAMAGE)) return false;

const actionType = item.system?.actionType;

const spellsAllowed = getSetting(CONSTANTS.SETTING_KEYS.ENABLE_SPELL_OVERKILL)
const isValidMeleeAttack = actionType === "mwak" || (spellsAllowed && actionType === "msak");
const isValidRangedAttack = actionType === "rwak" || (spellsAllowed && actionType === "rsak");

if (!actionType || !(isValidMeleeAttack || isValidRangedAttack)) return false;
if (!getSetting(CONSTANTS.SETTING_KEYS.ENABLE_RANGED_OVERKILL) && isValidRangedAttack) return false;

return { isValidMeleeAttack, isValidRangedAttack };

}

export function getActiveGM(){
return game.users
.filter(u => u.active && u.isGM)
.sort((a, b) => a.isGM && !b.isGM ? -1 : 1)?.[0];
}
103 changes: 103 additions & 0 deletions scripts/libwrapper/shim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// SPDX-License-Identifier: MIT
// Copyright © 2021 fvtt-lib-wrapper Rui Pinheiro


'use strict';

// A shim for the libWrapper library
export let libWrapper = undefined;

export const VERSIONS = [1,12,2];
export const TGT_SPLIT_RE = new RegExp("([^.[]+|\\[('([^'\\\\]|\\\\.)+?'|\"([^\"\\\\]|\\\\.)+?\")\\])", 'g');
export const TGT_CLEANUP_RE = new RegExp("(^\\['|'\\]$|^\\[\"|\"\\]$)", 'g');

// Main shim code
Hooks.once('init', () => {
// Check if the real module is already loaded - if so, use it
if(globalThis.libWrapper && !(globalThis.libWrapper.is_fallback ?? true)) {
libWrapper = globalThis.libWrapper;
return;
}

// Fallback implementation
libWrapper = class {
static get is_fallback() { return true };

static get WRAPPER() { return 'WRAPPER' };
static get MIXED() { return 'MIXED' };
static get OVERRIDE() { return 'OVERRIDE' };

static register(package_id, target, fn, type="MIXED", {chain=undefined, bind=[]}={}) {
const is_setter = target.endsWith('#set');
target = !is_setter ? target : target.slice(0, -4);
const split = target.match(TGT_SPLIT_RE).map((x)=>x.replace(/\\(.)/g, '$1').replace(TGT_CLEANUP_RE,''));
const root_nm = split.splice(0,1)[0];

let obj, fn_name;
if(split.length == 0) {
obj = globalThis;
fn_name = root_nm;
}
else {
const _eval = eval;
fn_name = split.pop();
obj = split.reduce((x,y)=>x[y], globalThis[root_nm] ?? _eval(root_nm));
}

let iObj = obj;
let descriptor = null;
while(iObj) {
descriptor = Object.getOwnPropertyDescriptor(iObj, fn_name);
if(descriptor) break;
iObj = Object.getPrototypeOf(iObj);
}
if(!descriptor || descriptor?.configurable === false) throw new Error(`libWrapper Shim: '${target}' does not exist, could not be found, or has a non-configurable descriptor.`);

let original = null;
const wrapper = (chain ?? (type.toUpperCase?.() != 'OVERRIDE' && type != 3)) ?
function(...args) { return fn.call(this, original.bind(this), ...bind, ...args); } :
function(...args) { return fn.call(this, ...bind, ...args); }
;

if(!is_setter) {
if(descriptor.value) {
original = descriptor.value;
descriptor.value = wrapper;
}
else {
original = descriptor.get;
descriptor.get = wrapper;
}
}
else {
if(!descriptor.set) throw new Error(`libWrapper Shim: '${target}' does not have a setter`);
original = descriptor.set;
descriptor.set = wrapper;
}

descriptor.configurable = true;
Object.defineProperty(obj, fn_name, descriptor);
}
}

//************** USER CUSTOMIZABLE:
// Set up the ready hook that shows the "libWrapper not installed" warning dialog. Remove if undesired.
{
//************** USER CUSTOMIZABLE:
// Package ID & Package Title - by default attempts to auto-detect, but you might want to hardcode your package ID and title here to avoid potential auto-detect issues
const [PACKAGE_ID, PACKAGE_TITLE] = (()=>{
const match = (import.meta?.url ?? Error().stack)?.match(/\/(worlds|systems|modules)\/(.+)(?=\/)/i);
if(match?.length !== 3) return [null,null];
const dirs = match[2].split('/');
if(match[1] === 'worlds') return dirs.find(n => n && game.world.id === n) ? [game.world.id, game.world.title] : [null,null];
if(match[1] === 'systems') return dirs.find(n => n && game.system.id === n) ? [game.system.id, game.system.title ?? game.system.data.title] : [null,null];
const id = dirs.find(n => n && game.modules.has(n));
const mdl = game.modules.get(id);
return [id, mdl?.title ?? mdl?.data?.title];
})();

if(!PACKAGE_ID || !PACKAGE_TITLE) {
console.error("libWrapper Shim: Could not auto-detect package ID and/or title. The libWrapper fallback warning dialog will be disabled.");
}
}
});
22 changes: 11 additions & 11 deletions scripts/plugins/midiqol.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as lib from "../lib.js";
import CONSTANTS from "../constants.js";
import * as api from "../api.js";
import { isValidOverkillItem } from "../lib.js";

export default {

Expand Down Expand Up @@ -79,15 +80,8 @@ export default {

Hooks.on("midi-qol.preDamageRollComplete", async (workflow) => {

if (!lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_OVERKILL_DAMAGE)) return true;

// Overkill management
const actionType = workflow.item.system?.actionType;
const isWeaponMeleeAttack = actionType === "mwak";
const isWeaponRangedAttack = actionType === "rwak";

if (!actionType || !(isWeaponMeleeAttack || isWeaponRangedAttack)) return true;
if (!lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_RANGED_OVERKILL) && isWeaponRangedAttack) return true;
const validAttack = lib.isValidOverkillItem(workflow.item);
if(!validAttack) return true;
if (!workflow.hitTargets.size) return true;

const hitTarget = Array.from(workflow.hitTargets)[0]
Expand Down Expand Up @@ -116,7 +110,7 @@ export default {
.slice(0, maxAdditionalTargets)
.forEach(_token => _token.setTarget(true, { releaseOthers: false }));

const label1Localization = "MINIONMANAGER.Dialogs.OverkillDamage." + (isWeaponRangedAttack ? "RangedLabel1" : "MeleeLabel1");
const label1Localization = "MINIONMANAGER.Dialogs.OverkillDamage." + (validAttack.isValidRangedAttack ? "RangedLabel1" : "MeleeLabel1");
const label1 = game.i18n.format(label1Localization, {
max_targets: maxAdditionalTargets + 1,
total_targets: game.user.targets.size,
Expand Down Expand Up @@ -159,7 +153,13 @@ export default {
workflow.damageTotal,
new Set([...userTargets].slice(0, maxAdditionalTargets)),
workflow.item
)
);

if(lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_OVERKILL_MESSAGE)) {
ChatMessage.create({
content: `<h2>${game.i18n.localize("MINIONMANAGER.Dialogs.OverkillDamage.Title")}</h2><p>${label1}</p>`
});
}

return true;

Expand Down
39 changes: 39 additions & 0 deletions scripts/plugins/vanilla.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as lib from "../lib.js";
import CONSTANTS from "../constants.js";
import * as api from "../api.js";
import { getActiveGM } from "../lib.js";

export default {

Expand Down Expand Up @@ -123,6 +124,44 @@ export default {

});


Hooks.on("dnd5e.rollDamage", async (item, damageRoll) => {

const validAttack = lib.isValidOverkillItem(item);
if(!validAttack) return true;

const hitTargets = Array.from(game.user.targets)

if(!hitTargets.length) return true;

const hitTarget = hitTargets[0];

if (!api.isMinion(hitTarget) || api.isMinion(item.parent)) return true;

let damageTotal = damageRoll.total;

const minionHP = getProperty(hitTarget.actor, "system.attributes.hp.value");

if ((minionHP - damageTotal) > 0) return true;

damageTotal -= minionHP;

let maxAdditionalTargets = Math.ceil(damageTotal / minionHP);

const label1Localization = "MINIONMANAGER.Dialogs.OverkillDamage." + (validAttack.isValidRangedAttack ? "RangedLabel1" : "MeleeLabel1");
const label1 = game.i18n.format(label1Localization, {
max_targets: maxAdditionalTargets + 1,
total_targets: game.user.targets.size,
name: hitTarget.actor.name
});

if(lib.getSetting(CONSTANTS.SETTING_KEYS.ENABLE_OVERKILL_MESSAGE)) {
ChatMessage.create({
content: `<h2>${game.i18n.localize("MINIONMANAGER.Dialogs.OverkillDamage.Title")}</h2><p>${label1}</p>`
});
}
});

}

}

0 comments on commit 74b9f1b

Please sign in to comment.