Initial commit

This commit is contained in:
2022-07-19 23:16:03 +02:00
parent 572ecfd0c4
commit 2b95821c84
91 changed files with 7643 additions and 1 deletions

View File

@@ -0,0 +1,336 @@
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {ActorSheet}
*/
import { CrucibleUtility } from "./crucible-utility.js";
/* -------------------------------------------- */
export class CrucibleActorSheet extends ActorSheet {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["fvtt-crucible-rpg", "sheet", "actor"],
template: "systems/fvtt-crucible-rpg/templates/actor-sheet.html",
width: 960,
height: 720,
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "combat" }],
dragDrop: [{ dragSelector: ".item-list .item", dropSelector: null }],
editScore: true
});
}
/* -------------------------------------------- */
async getData() {
const objectData = CrucibleUtility.data(this.object);
let actorData = duplicate(CrucibleUtility.templateData(this.object));
let formData = {
title: this.title,
id: objectData.id,
type: objectData.type,
img: objectData.img,
name: objectData.name,
editable: this.isEditable,
cssClass: this.isEditable ? "editable" : "locked",
data: actorData,
limited: this.object.limited,
skills: this.actor.getSkills( ),
weapons: this.actor.checkAndPrepareEquipments( duplicate(this.actor.getWeapons()) ),
armors: this.actor.checkAndPrepareEquipments( duplicate(this.actor.getArmors())),
equipments: this.actor.checkAndPrepareEquipments(duplicate(this.actor.getEquipmentsOnly()) ),
subActors: duplicate(this.actor.getSubActors()),
race: duplicate(this.actor.getRace()),
moneys: duplicate(this.actor.getMoneys()),
encCapacity: this.actor.getEncumbranceCapacity(),
containersTree: this.actor.containersTree,
encCurrent: this.actor.encCurrent,
options: this.options,
owner: this.document.isOwner,
editScore: this.options.editScore,
isGM: game.user.isGM
}
this.formData = formData;
console.log("PC : ", formData, this.object);
return formData;
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Everything below here is only needed if the sheet is editable
if (!this.options.editable) return;
html.bind("keydown", function(e) { // Ignore Enter in actores sheet
if (e.keyCode === 13) return false;
});
// Update Inventory Item
html.find('.item-edit').click(ev => {
const li = $(ev.currentTarget).parents(".item")
let itemId = li.data("item-id")
const item = this.actor.items.get( itemId );
item.sheet.render(true);
});
// Delete Inventory Item
html.find('.item-delete').click(ev => {
const li = $(ev.currentTarget).parents(".item")
CrucibleUtility.confirmDelete(this, li)
})
html.find('.item-add').click(ev => {
let dataType = $(ev.currentTarget).data("type")
this.actor.createEmbeddedDocuments('Item', [{ name: "NewItem", type: dataType }], { renderSheet: true })
})
html.find('.spec-group-activate').click(ev => {
const li = $(ev.currentTarget).parents(".item");
let itemId = li.data("item-id");
this.actor.specPowerActivate( itemId)
});
html.find('.spec-group-deactivate').click(ev => {
const li = $(ev.currentTarget).parents(".item");
let itemId = li.data("item-id");
this.actor.specPowerDeactivate( itemId)
});
html.find('.equip-activate').click(ev => {
const li = $(ev.currentTarget).parents(".item")
let itemId = li.data("item-id")
this.actor.equipActivate( itemId)
});
html.find('.equip-deactivate').click(ev => {
const li = $(ev.currentTarget).parents(".item")
let itemId = li.data("item-id")
this.actor.equipDeactivate( itemId)
});
html.find('.effect-used').click(ev => {
const li = $(ev.currentTarget).parents(".item");
let itemId = li.data("item-id");
this.actor.perkEffectUsed( itemId)
});
html.find('.perk-status').change(ev => {
const li = $(ev.currentTarget).parents(".item");
let itemId = li.data("item-id");
this.actor.updatePerkStatus( itemId, ev.currentTarget.value)
});
html.find('.power-cost-spent').change(ev => {
const li = $(ev.currentTarget).parents(".item");
let itemId = li.data("item-id");
this.actor.updatePowerSpentCost( itemId, ev.currentTarget.value)
});
html.find('.power-dmg-roll').click(ev => {
const li = $(ev.currentTarget).parents(".item")
let itemId = li.data("item-id")
this.actor.powerDmgRoll( itemId )
})
html.find('.perk-used').change(ev => {
const li = $(ev.currentTarget).parents(".item")
let itemId = li.data("item-id")
let index = Number($(ev.currentTarget).data("use-index") )
this.actor.updatePerkUsed( itemId, index, ev.currentTarget.checked )
});
html.find('.subactor-edit').click(ev => {
const li = $(ev.currentTarget).parents(".item");
let actorId = li.data("actor-id");
let actor = game.actors.get( actorId );
actor.sheet.render(true);
});
html.find('.subactor-delete').click(ev => {
const li = $(ev.currentTarget).parents(".item");
let actorId = li.data("actor-id");
this.actor.delSubActor(actorId);
});
html.find('.quantity-minus').click(event => {
const li = $(event.currentTarget).parents(".item");
this.actor.incDecQuantity( li.data("item-id"), -1 );
} );
html.find('.quantity-plus').click(event => {
const li = $(event.currentTarget).parents(".item");
this.actor.incDecQuantity( li.data("item-id"), +1 );
} );
html.find('.ammo-minus').click(event => {
const li = $(event.currentTarget).parents(".item")
this.actor.incDecAmmo( li.data("item-id"), -1 );
} );
html.find('.ammo-plus').click(event => {
const li = $(event.currentTarget).parents(".item")
this.actor.incDecAmmo( li.data("item-id"), +1 )
} );
html.find('.stun-minus').click(event => {
this.actor.modifyStun( -1 )
} )
html.find('.stun-plus').click(event => {
this.actor.modifyStun( 1 )
} )
html.find('.momentum-minus').click(event => {
this.actor.modifyMomentum( -1 )
} )
html.find('.momentum-plus').click(event => {
this.actor.modifyMomentum( 1 )
} )
html.find('.unarmed-attack').click((event) => {
this.actor.rollUnarmedAttack();
});
html.find('.generic-pool-roll').click((event) => {
this.openGenericRoll()
} );
html.find('.attack-melee').click((event) => {
this.actor.rollPool( 'com', false, "melee-atk");
});
html.find('.attack-ranged').click((event) => {
this.actor.rollPool( 'agi', false, "ranged-atk");
});
html.find('.defense-roll').click((event) => {
this.actor.rollPool( 'def', true);
});
html.find('.damage-melee').click((event) => {
this.actor.rollPool( 'str', false, "melee-dmg");
});
html.find('.damage-ranged').click((event) => {
this.actor.rollPool( 'per', false, "ranged-dmg");
});
html.find('.damage-resistance').click((event) => {
this.actor.rollPool( 'phy', false, "dmg-res");
});
html.find('.roll-stat').click((event) => {
const statId = $(event.currentTarget).data("stat-key");
this.actor.rollStat(statId);
});
html.find('.roll-mr').click((event) => {
this.actor.rollMR();
});
html.find('.roll-idr').click((event) => {
const diceValue = $(event.currentTarget).data("dice-value")
const li = $(event.currentTarget).parents(".item")
this.rollIDR( li.data("item-id"), diceValue)
})
html.find('.roll-spec').click((event) => {
const li = $(event.currentTarget).parents(".item");
const specId = li.data("item-id");
this.actor.rollSpec(specId);
});
html.find('.power-roll').click((event) => {
const li = $(event.currentTarget).parents(".item");
const powerId = li.data("item-id");
this.actor.rollPower(powerId);
});
html.find('.weapon-roll').click((event) => {
const li = $(event.currentTarget).parents(".item");
const weaponId = li.data("item-id");
this.actor.rollWeapon(weaponId);
});
html.find('.armor-roll').click((event) => {
const li = $(event.currentTarget).parents(".item");
const armorId = li.data("item-id");
this.actor.rollArmor(armorId);
});
html.find('.weapon-damage-roll').click((event) => {
const li = $(event.currentTarget).parents(".item");
const weaponId = li.data("item-id");
this.actor.rollWeapon(weaponId, true);
});
html.find('.weapon-damage').click((event) => {
const li = $(event.currentTarget).parents(".item");
const weapon = this.actor.getOwnedItem(li.data("item-id"));
this.actor.rollDamage(weapon, 'damage');
});
html.find('.lock-unlock-sheet').click((event) => {
this.options.editScore = !this.options.editScore;
this.render(true);
});
html.find('.item-link a').click((event) => {
const itemId = $(event.currentTarget).data("item-id");
const item = this.actor.getOwnedItem(itemId);
item.sheet.render(true);
});
html.find('.item-equip').click(ev => {
const li = $(ev.currentTarget).parents(".item");
this.actor.equipItem( li.data("item-id") );
this.render(true);
});
html.find('.power-activate').click(ev => {
const li = $(ev.currentTarget).parents(".item");
this.actor.activatePower( li.data("item-id") );
this.render(true);
});
html.find('.vice-virtue-activate').click(ev => {
const li = $(ev.currentTarget).parents(".item")
this.actor.activateViceOrVirtue( li.data("item-id") )
this.render(true);
})
html.find('.change-worstfear').change(ev => {
this.actor.manageWorstFear( ev.currentTarget.checked )
});
html.find('.change-desires').change(ev => {
this.actor.manageDesires( ev.currentTarget.checked )
});
html.find('.update-field').change(ev => {
const fieldName = $(ev.currentTarget).data("field-name");
let value = Number(ev.currentTarget.value);
this.actor.update( { [`${fieldName}`]: value } );
});
html.find('.perk-active').click(ev => {
const li = $(ev.currentTarget).parents(".item");
this.actor.activatePerk( li.data("item-id") );
this.render(true);
});
}
/* -------------------------------------------- */
/** @override */
setPosition(options = {}) {
const position = super.setPosition(options);
const sheetBody = this.element.find(".sheet-body");
const bodyHeight = position.height - 192;
sheetBody.css("height", bodyHeight);
return position;
}
/* -------------------------------------------- */
async _onDropItem(event, dragData) {
console.log(">>>>>> DROPPED!!!!")
let item = await CrucibleUtility.searchItem( dragData)
if (item == undefined) {
item = this.actor.items.get( dragData.data._id )
}
let ret = await this.actor.preprocessItem( event, item, true )
if ( ret ) {
super._onDropItem(event, dragData)
}
}
/* -------------------------------------------- */
/** @override */
_updateObject(event, formData) {
// Update the Actor
return this.object.update(formData);
}
}

397
modules/crucible-actor.js Normal file
View File

@@ -0,0 +1,397 @@
/* -------------------------------------------- */
import { CrucibleUtility } from "./crucible-utility.js";
import { CrucibleRollDialog } from "./crucible-roll-dialog.js";
/* -------------------------------------------- */
const coverBonusTable = { "nocover": 0, "lightcover": 2, "heavycover": 4, "entrenchedcover": 6 };
const statThreatLevel = ["agi", "str", "phy", "com", "def", "per"]
const __subkey2title = {
"melee-dmg": "Melee Damage", "melee-atk": "Melee Attack", "ranged-atk": "Ranged Attack",
"ranged-dmg": "Ranged Damage", "dmg-res": "Damare Resistance"
}
/* -------------------------------------------- */
/* -------------------------------------------- */
/**
* Extend the base Actor entity by defining a custom roll data structure which is ideal for the Simple system.
* @extends {Actor}
*/
export class CrucibleActor extends Actor {
/* -------------------------------------------- */
/**
* Override the create() function to provide additional SoS functionality.
*
* This overrided create() function adds initial items
* Namely: Basic skills, money,
*
* @param {Object} data Barebones actor data which this function adds onto.
* @param {Object} options (Unused) Additional options which customize the creation workflow.
*
*/
static async create(data, options) {
// Case of compendium global import
if (data instanceof Array) {
return super.create(data, options);
}
// If the created actor has items (only applicable to duplicated actors) bypass the new actor creation logic
if (data.items) {
let actor = super.create(data, options);
return actor;
}
if (data.type == 'character') {
const skills = await CrucibleUtility.loadCompendium("fvtt-crucible-rpg.skills");
data.items = skills.map(i => i.toObject());
}
if (data.type == 'npc') {
}
return super.create(data, options);
}
/* -------------------------------------------- */
prepareBaseData() {
}
/* -------------------------------------------- */
async prepareData() {
super.prepareData();
}
/* -------------------------------------------- */
prepareDerivedData() {
if (this.type == 'character') {
this.data.data.encCapacity = this.getEncumbranceCapacity()
this.buildContainerTree()
}
super.prepareDerivedData();
}
/* -------------------------------------------- */
_preUpdate(changed, options, user) {
super._preUpdate(changed, options, user);
}
/* -------------------------------------------- */
getEncumbranceCapacity() {
return this.data.data.statistics.str.value * 25
}
/* -------------------------------------------- */
getMoneys() {
let comp = this.data.items.filter(item => item.type == 'money');
return comp;
}
/* -------------------------------------------- */
getArmors() {
let comp = duplicate(this.data.items.filter(item => item.type == 'armor') || []);
return comp;
}
getRace() {
let race = this.data.items.filter(item => item.type == 'race')
return race[0] ?? [];
}
/* -------------------------------------------- */
checkAndPrepareEquipment(item) {
}
/* -------------------------------------------- */
checkAndPrepareEquipments(listItem) {
for (let item of listItem) {
this.checkAndPrepareEquipment(item)
}
return listItem
}
/* -------------------------------------------- */
getWeapons() {
let comp = duplicate(this.data.items.filter(item => item.type == 'weapon') || []);
return comp;
}
/* -------------------------------------------- */
getItemById(id) {
let item = this.data.items.find(item => item.id == id);
if (item) {
item = duplicate(item)
if (item.type == 'specialisation') {
item.data.dice = CrucibleUtility.getDiceFromLevel(item.data.level);
}
}
return item;
}
/* -------------------------------------------- */
getSkills() {
let comp = duplicate(this.data.items.filter(item => item.type == 'skill') || []);
for (let c of comp) {
c.data.dice = CrucibleUtility.getDiceFromLevel(c.data.level);
}
return comp;
}
/* -------------------------------------------- */
getRelevantAbility(statKey) {
let comp = duplicate(this.data.items.filter(item => item.type == 'skill' && item.data.data.ability == ability) || []);
return comp;
}
/* -------------------------------------------- */
async equipItem(itemId) {
let item = this.data.items.find(item => item.id == itemId);
if (item && item.data.data) {
let update = { _id: item.id, "data.equipped": !item.data.data.equipped };
await this.updateEmbeddedDocuments('Item', [update]); // Updates one EmbeddedEntity
}
}
/* -------------------------------------------- */
compareName(a, b) {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
}
/* ------------------------------------------- */
getEquipments() {
return this.data.items.filter(item => item.type == 'shield' || item.type == 'armor' || item.type == "weapon" || item.type == "equipment");
}
/* ------------------------------------------- */
getEquipmentsOnly() {
return duplicate(this.data.items.filter(item => item.type == "equipment") || [])
}
/* ------------------------------------------- */
async buildContainerTree() {
let equipments = duplicate(this.data.items.filter(item => item.type == "equipment") || [])
for (let equip1 of equipments) {
if (equip1.data.iscontainer) {
equip1.data.contents = []
equip1.data.contentsEnc = 0
for (let equip2 of equipments) {
if (equip1._id != equip2._id && equip2.data.containerid == equip1._id) {
equip1.data.contents.push(equip2)
let q = equip2.data.quantity ?? 1
equip1.data.contentsEnc += q * equip2.data.weight
}
}
}
}
// Compute whole enc
let enc = 0
for (let item of equipments) {
item.data.idrDice = CrucibleUtility.getDiceFromLevel(Number(item.data.idr))
if (item.data.equipped) {
if (item.data.iscontainer) {
enc += item.data.contentsEnc
} else if (item.data.containerid == "") {
let q = item.data.quantity ?? 1
enc += q * item.data.weight
}
}
}
for (let item of this.data.items) { // Process items/shields/armors
if ((item.type == "weapon" || item.type == "shield" || item.type == "armor") && item.data.data.equipped) {
let q = item.data.data.quantity ?? 1
enc += q * item.data.data.weight
}
}
// Store local values
this.encCurrent = enc
this.containersTree = equipments.filter(item => item.data.containerid == "") // Returns the root of equipements without container
// Manages slow effect
let overCapacity = Math.floor(this.encCurrent / this.getEncumbranceCapacity())
this.encHindrance = Math.floor(this.encCurrent / this.getEncumbranceCapacity())
//console.log("Capacity", overCapacity, this.encCurrent / this.getEncumbranceCapacity() )
let effect = this.data.items.find(item => item.type == "effect" && item.data.data.slow)
if (overCapacity >= 4) {
if (!effect) {
effect = await CrucibleUtility.getEffectFromCompendium("Slowed")
effect.data.slow = true
this.createEmbeddedDocuments('Item', [effect])
}
} else {
if (effect) {
this.deleteEmbeddedDocuments('Item', [effect.id])
}
}
}
/* -------------------------------------------- */
getAbility(abilKey) {
return this.data.data.abilities[abilKey];
}
/* -------------------------------------------- */
async addObjectToContainer(itemId, containerId) {
let container = this.data.items.find(item => item.id == containerId && item.data.data.iscontainer)
let object = this.data.items.find(item => item.id == itemId)
if (container) {
if (object.data.data.iscontainer) {
ui.notifications.warn("Only 1 level of container allowed")
return
}
let alreadyInside = this.data.items.filter(item => item.data.data.containerid && item.data.data.containerid == containerId);
if (alreadyInside.length >= container.data.data.containercapacity) {
ui.notifications.warn("Container is already full !")
return
} else {
await this.updateEmbeddedDocuments("Item", [{ _id: object.id, 'data.containerid': containerId }])
}
} else if (object && object.data.data.containerid) { // remove from container
console.log("Removeing: ", object)
await this.updateEmbeddedDocuments("Item", [{ _id: object.id, 'data.containerid': "" }]);
}
}
/* -------------------------------------------- */
async preprocessItem(event, item, onDrop = false) {
let dropID = $(event.target).parents(".item").attr("data-item-id") // Only relevant if container drop
let objectID = item.id || item._id
this.addObjectToContainer(objectID, dropID)
return true
}
/* -------------------------------------------- */
async equipGear(equipmentId) {
let item = this.data.items.find(item => item.id == equipmentId);
if (item && item.data.data) {
let update = { _id: item.id, "data.equipped": !item.data.data.equipped };
await this.updateEmbeddedDocuments('Item', [update]); // Updates one EmbeddedEntity
}
}
/* -------------------------------------------- */
getInitiativeScore(combatId, combatantId) {
if (this.type == 'character') {
this.rollMR(true, combatId, combatantId)
}
console.log("Init required !!!!")
return -1;
}
/* -------------------------------------------- */
getSubActors() {
let subActors = [];
for (let id of this.data.data.subactors) {
subActors.push(duplicate(game.actors.get(id)))
}
return subActors;
}
/* -------------------------------------------- */
async addSubActor(subActorId) {
let subActors = duplicate(this.data.data.subactors);
subActors.push(subActorId);
await this.update({ 'data.subactors': subActors });
}
/* -------------------------------------------- */
async delSubActor(subActorId) {
let newArray = [];
for (let id of this.data.data.subactors) {
if (id != subActorId) {
newArray.push(id);
}
}
await this.update({ 'data.subactors': newArray });
}
/* -------------------------------------------- */
syncRoll(rollData) {
let linkedRollId = CrucibleUtility.getDefenseState(this.id);
if (linkedRollId) {
rollData.linkedRollId = linkedRollId;
}
this.lastRollId = rollData.rollId;
CrucibleUtility.saveRollData(rollData);
}
/* -------------------------------------------- */
getOneSkill(skillId) {
let skill = this.data.items.find(item => item.type == 'skill' && item.id == skillId)
if (skill) {
skill = duplicate(skill);
}
return skill;
}
/* -------------------------------------------- */
async deleteAllItemsByType(itemType) {
let items = this.data.items.filter(item => item.type == itemType);
await this.deleteEmbeddedDocuments('Item', items);
}
/* -------------------------------------------- */
async addItemWithoutDuplicate(newItem) {
let item = this.data.items.find(item => item.type == newItem.type && item.name.toLowerCase() == newItem.name.toLowerCase())
if (!item) {
await this.createEmbeddedDocuments('Item', [newItem]);
}
}
/* -------------------------------------------- */
async incDecQuantity(objetId, incDec = 0) {
let objetQ = this.data.items.get(objetId)
if (objetQ) {
let newQ = objetQ.data.data.quantity + incDec
if (newQ >= 0) {
const updated = await this.updateEmbeddedDocuments('Item', [{ _id: objetQ.id, 'data.quantity': newQ }]) // pdates one EmbeddedEntity
}
}
}
/* -------------------------------------------- */
async incDecAmmo(objetId, incDec = 0) {
let objetQ = this.data.items.get(objetId)
if (objetQ) {
let newQ = objetQ.data.data.ammocurrent + incDec;
if (newQ >= 0 && newQ <= objetQ.data.data.ammomax) {
const updated = await this.updateEmbeddedDocuments('Item', [{ _id: objetQ.id, 'data.ammocurrent': newQ }]); // pdates one EmbeddedEntity
}
}
}
/* -------------------------------------------- */
getCommonRollData(abKey = undefined) {
let rollData = CrucibleUtility.getBasicRollData()
rollData.alias = this.name
rollData.actorImg = this.img
rollData.actorId = this.id
rollData.img = this.img
if (abilKey) {
rollData.getRelevantSkill(abKey)
rollData.ability = this.getAbility(abKey)
rollData.skillList = this.getRelevantSkill(abKey)
rollData.selectedKill = "0"
rollData.img = `systems/fvtt-crucible-rpg/images/icons/${rollData.ability.abbrev}.webp`
}
console.log("ROLLDATA", rollData)
return rollData
}
/* -------------------------------------------- */
async startRoll(rollData) {
this.syncRoll(rollData);
//console.log("ROLL DATA", rollData)
let rollDialog = await CrucibleRollDialog.create(this, rollData)
console.log(rollDialog)
rollDialog.render(true);
}
}

View File

@@ -0,0 +1,30 @@
import { CrucibleUtility } from "./crucible-utility.js";
/* -------------------------------------------- */
export class CrucibleCombat extends Combat {
/* -------------------------------------------- */
async rollInitiative(ids, formula = undefined, messageOptions = {} ) {
ids = typeof ids === "string" ? [ids] : ids;
for (let cId = 0; cId < ids.length; cId++) {
const c = this.combatants.get(ids[cId]);
let id = c._id || c.id;
let initBonus = c.actor ? c.actor.getInitiativeScore( this.id, id ) : -1;
await this.updateEmbeddedDocuments("Combatant", [ { _id: id, initiative: initBonus } ]);
}
return this;
}
/* -------------------------------------------- */
_onUpdate(changed, options, userId) {
}
/* -------------------------------------------- */
static async checkTurnPosition() {
while (game.combat.turn > 0) {
await game.combat.previousTurn()
}
}
}

View File

@@ -0,0 +1,117 @@
/* -------------------------------------------- */
import { CrucibleUtility } from "./crucible-utility.js";
import { CrucibleRollDialog } from "./crucible-roll-dialog.js";
/* -------------------------------------------- */
export class CrucibleCommands {
static init() {
if (!game.system.crucible.commands) {
const crucibleCommands = new CrucibleCommands();
//crucibleCommands.registerCommand({ path: ["/char"], func: (content, msg, params) => crucibleCommands.createChar(msg), descr: "Create a new character" });
//crucibleCommands.registerCommand({ path: ["/pool"], func: (content, msg, params) => crucibleCommands.poolRoll(msg), descr: "Generic Roll Window" });
game.system.crucible.commands = crucibleCommands;
}
}
constructor() {
this.commandsTable = {};
}
/* -------------------------------------------- */
registerCommand(command) {
this._addCommand(this.commandsTable, command.path, '', command);
}
/* -------------------------------------------- */
_addCommand(targetTable, path, fullPath, command) {
if (!this._validateCommand(targetTable, path, command)) {
return;
}
const term = path[0];
fullPath = fullPath + term + ' '
if (path.length == 1) {
command.descr = `<strong>${fullPath}</strong>: ${command.descr}`;
targetTable[term] = command;
}
else {
if (!targetTable[term]) {
targetTable[term] = { subTable: {} };
}
this._addCommand(targetTable[term].subTable, path.slice(1), fullPath, command)
}
}
/* -------------------------------------------- */
_validateCommand(targetTable, path, command) {
if (path.length > 0 && path[0] && command.descr && (path.length != 1 || targetTable[path[0]] == undefined)) {
return true;
}
console.warn("crucibleCommands._validateCommand failed ", targetTable, path, command);
return false;
}
/* -------------------------------------------- */
/* Manage chat commands */
processChatCommand(commandLine, content = '', msg = {}) {
// Setup new message's visibility
let rollMode = game.settings.get("core", "rollMode");
if (["gmroll", "blindroll"].includes(rollMode)) msg["whisper"] = ChatMessage.getWhisperRecipients("GM");
if (rollMode === "blindroll") msg["blind"] = true;
msg["type"] = 0;
let command = commandLine[0].toLowerCase();
let params = commandLine.slice(1);
return this.process(command, params, content, msg);
}
/* -------------------------------------------- */
process(command, params, content, msg) {
return this._processCommand(this.commandsTable, command, params, content, msg);
}
/* -------------------------------------------- */
_processCommand(commandsTable, name, params, content = '', msg = {}, path = "") {
console.log("===> Processing command")
let command = commandsTable[name];
path = path + name + " ";
if (command && command.subTable) {
if (params[0]) {
return this._processCommand(command.subTable, params[0], params.slice(1), content, msg, path)
}
else {
this.help(msg, command.subTable);
return true;
}
}
if (command && command.func) {
const result = command.func(content, msg, params);
if (result == false) {
CrucibleCommands._chatAnswer(msg, command.descr);
}
return true;
}
return false;
}
/* -------------------------------------------- */
static _chatAnswer(msg, content) {
msg.whisper = [game.user.id];
msg.content = content;
ChatMessage.create(msg);
}
/* -------------------------------------------- */
async poolRoll( msg) {
let rollData = CrucibleUtility.getBasicRollData()
rollData.alias = "Dice Pool Roll",
rollData.mode = "generic"
rollData.title = `Dice Pool Roll`;
let rollDialog = await CrucibleRollDialog.create( this, rollData);
rollDialog.render( true );
}
}

View File

@@ -0,0 +1,170 @@
import { CrucibleUtility } from "./crucible-utility.js";
/**
* Extend the basic ItemSheet with some very simple modifications
* @extends {ItemSheet}
*/
export class CrucibleItemSheet extends ItemSheet {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["fvtt-crucible-rpg", "sheet", "item"],
template: "systems/fvtt-crucible-rpg/templates/item-sheet.html",
dragDrop: [{ dragSelector: null, dropSelector: null }],
width: 620,
height: 550,
tabs: [{navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "description"}]
});
}
/* -------------------------------------------- */
_getHeaderButtons() {
let buttons = super._getHeaderButtons();
// Add "Post to chat" button
// We previously restricted this to GM and editable items only. If you ever find this comment because it broke something: eh, sorry!
buttons.unshift(
{
class: "post",
icon: "fas fa-comment",
onclick: ev => { }
})
return buttons
}
/* -------------------------------------------- */
/** @override */
setPosition(options = {}) {
const position = super.setPosition(options);
const sheetBody = this.element.find(".sheet-body");
const bodyHeight = position.height - 192;
sheetBody.css("height", bodyHeight);
if (this.item.type.includes('weapon')) {
position.width = 640;
}
return position;
}
/* -------------------------------------------- */
async getData() {
const objectData = CrucibleUtility.data(this.object);
let itemData = foundry.utils.deepClone(CrucibleUtility.templateData(this.object));
let formData = {
title: this.title,
id: this.id,
type: objectData.type,
img: objectData.img,
name: objectData.name,
editable: this.isEditable,
cssClass: this.isEditable ? "editable" : "locked",
data: itemData,
limited: this.object.limited,
options: this.options,
owner: this.document.isOwner,
isGM: game.user.isGM
}
this.options.editable = !(this.object.data.origin == "embeddedItem");
console.log("ITEM DATA", formData, this);
return formData;
}
/* -------------------------------------------- */
_getHeaderButtons() {
let buttons = super._getHeaderButtons();
buttons.unshift({
class: "post",
icon: "fas fa-comment",
onclick: ev => this.postItem()
});
return buttons
}
/* -------------------------------------------- */
postItem() {
let chatData = duplicate(CrucibleUtility.data(this.item));
if (this.actor) {
chatData.actor = { id: this.actor.id };
}
// Don't post any image for the item (which would leave a large gap) if the default image is used
if (chatData.img.includes("/blank.png")) {
chatData.img = null;
}
// JSON object for easy creation
chatData.jsondata = JSON.stringify(
{
compendium: "postedItem",
payload: chatData,
});
renderTemplate('systems/fvtt-crucible-rpg/templates/post-item.html', chatData).then(html => {
let chatOptions = CrucibleUtility.chatDataSetup(html);
ChatMessage.create(chatOptions)
});
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Everything below here is only needed if the sheet is editable
if (!this.options.editable) return;
// Update Inventory Item
html.find('.item-edit').click(ev => {
const li = $(ev.currentTarget).parents(".item");
const item = this.object.options.actor.getOwnedItem(li.data("item-id"));
item.sheet.render(true);
});
html.find('.delete-spec').click(ev => {
this.object.update({ "data.specialisation": [{ name: 'None' }] });
});
html.find('.delete-subitem').click(ev => {
this.deleteSubitem(ev);
});
html.find('.stat-choice-flag').click(ev => {
let idx = $(ev.currentTarget).data("stat-idx");
let array = duplicate(this.object.data.data.statincreasechoice);
array[Number(idx)].flag = !array[Number(idx)].flag;
this.object.update({ "data.statincreasechoice": array });
});
// Update Inventory Item
html.find('.item-delete').click(ev => {
const li = $(ev.currentTarget).parents(".item");
let itemId = li.data("item-id");
let itemType = li.data("item-type");
});
html.find('.view-subitem').click(ev => {
this.viewSubitem(ev);
});
html.find('.view-spec').click(ev => {
this.manageSpec();
});
}
/* -------------------------------------------- */
get template() {
let type = this.item.type;
return `systems/fvtt-crucible-rpg/templates/item-${type}-sheet.html`;
}
/* -------------------------------------------- */
/** @override */
_updateObject(event, formData) {
return this.object.update(formData)
}
}

25
modules/crucible-item.js Normal file
View File

@@ -0,0 +1,25 @@
import { CrucibleUtility } from "./crucible-utility.js";
export const defaultItemImg = {
skill: "systems/fvtt-crucible-rpg/images/icons/icon_skill.webp",
armor: "systems/fvtt-crucible-rpg/images/icons/icon_armour.webp",
weapon: "systems/fvtt-crucible-rpg/images/icons/icon_weapon.webp",
equipment: "systems/fvtt-crucible-rpg/images/icons/icon_equipment.webp",
race: "systems/fvtt-crucible-rpg/images/icons/icon_race.webp",
money: "systems/fvtt-crucible-rpg/images/icons/icon_money.webp",
}
/**
* Extend the basic ItemSheet with some very simple modifications
* @extends {ItemSheet}
*/
export class CrucibleItem extends Item {
constructor(data, context) {
if (!data.img) {
data.img = defaultItemImg[data.type];
}
super(data, context);
}
}

121
modules/crucible-main.js Normal file
View File

@@ -0,0 +1,121 @@
/**
* Crucible system
* Author: Uberwald
* Software License: Prop
*/
/* -------------------------------------------- */
/* -------------------------------------------- */
// Import Modules
import { CrucibleActor } from "./crucible-actor.js";
import { CrucibleItemSheet } from "./crucible-item-sheet.js";
import { CrucibleActorSheet } from "./crucible-actor-sheet.js";
import { CrucibleNPCSheet } from "./crucible-npc-sheet.js";
import { CrucibleUtility } from "./crucible-utility.js";
import { CrucibleCombat } from "./crucible-combat.js";
import { CrucibleItem } from "./crucible-item.js";
/* -------------------------------------------- */
/* Foundry VTT Initialization */
/* -------------------------------------------- */
/************************************************************************************/
Hooks.once("init", async function () {
console.log(`Initializing Crucible RPG`);
/* -------------------------------------------- */
// preload handlebars templates
CrucibleUtility.preloadHandlebarsTemplates();
/* -------------------------------------------- */
game.settings.register("fvtt-crucible-rpg", "dice-max-level", {
name: "Maximum level value for dices lists",
hint: "Set the maximum level value for dices lists",
scope: "world",
config: true,
default: 20,
type: Number
})
/* -------------------------------------------- */
// Set an initiative formula for the system
CONFIG.Combat.initiative = {
formula: "1d6",
decimals: 1
};
/* -------------------------------------------- */
game.socket.on("system.fvtt-crucible-rpg", data => {
CrucibleUtility.onSocketMesssage(data)
});
/* -------------------------------------------- */
// Define custom Entity classes
CONFIG.Combat.documentClass = CrucibleCombat
CONFIG.Actor.documentClass = CrucibleActor
CONFIG.Item.documentClass = CrucibleItem
//CONFIG.Token.objectClass = CrucibleToken
game.system.crucible = { };
/* -------------------------------------------- */
// Register sheet application classes
Actors.unregisterSheet("core", ActorSheet);
Actors.registerSheet("fvtt-crucible", CrucibleActorSheet, { types: ["character"], makeDefault: true });
Actors.registerSheet("fvtt-crucible", CrucibleNPCSheet, { types: ["npc"], makeDefault: false });
Items.unregisterSheet("core", ItemSheet);
Items.registerSheet("fvtt-crucible", CrucibleItemSheet, { makeDefault: true });
CrucibleUtility.init();
});
/* -------------------------------------------- */
function welcomeMessage() {
ChatMessage.create({
user: game.user.id,
whisper: [game.user.id],
content: `<div id="welcome-message-crucible"><span class="rdd-roll-part">
<strong>Welcome to the Crucible RPG.</strong>
` });
}
/* -------------------------------------------- */
/* Foundry VTT Initialization */
/* -------------------------------------------- */
Hooks.once("ready", function () {
CrucibleUtility.ready();
// User warning
if (!game.user.isGM && game.user.character == undefined) {
ui.notifications.info("Warning ! No character linked to your user !");
ChatMessage.create({
content: "<b>WARNING</b> The player " + game.user.name + " is not linked to a character !",
user: game.user._id
});
}
// CSS patch for v9
if (game.version) {
let sidebar = document.getElementById("sidebar");
sidebar.style.width = "min-content";
}
welcomeMessage();
});
/* -------------------------------------------- */
/* Foundry VTT Initialization */
/* -------------------------------------------- */
Hooks.on("chatMessage", (html, content, msg) => {
if (content[0] == '/') {
let regExp = /(\S+)/g;
let commands = content.match(regExp);
if (game.system.crucible.commands.processChatCommand(commands, content, msg)) {
return false;
}
}
return true;
});

View File

@@ -0,0 +1,133 @@
/**
* Extend the basic ActorSheet with some very simple modifications
* @extends {ActorSheet}
*/
import { CrucibleUtility } from "./crucible-utility.js";
/* -------------------------------------------- */
export class CrucibleNPCSheet extends ActorSheet {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["crucible-rpg", "sheet", "actor"],
template: "systems/fvtt-crucible-rpg/templates/npc-sheet.html",
width: 640,
height: 720,
tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "stats" }],
dragDrop: [{ dragSelector: ".item-list .item", dropSelector: null }],
editScore: false
});
}
/* -------------------------------------------- */
async getData() {
const objectData = CrucibleUtility.data(this.object);
this.actor.prepareTraitsAttributes();
let actorData = duplicate(CrucibleUtility.templateData(this.object));
let formData = {
title: this.title,
id: objectData.id,
type: objectData.type,
img: objectData.img,
name: objectData.name,
editable: this.isEditable,
cssClass: this.isEditable ? "editable" : "locked",
data: actorData,
effects: this.object.effects.map(e => foundry.utils.deepClone(e.data)),
limited: this.object.limited,
equipments: this.actor.getEquipments(),
weapons: this.actor.getWeapons(),
options: this.options,
owner: this.document.isOwner,
editScore: this.options.editScore,
isGM: game.user.isGM
}
this.formData = formData;
console.log("NPC : ", formData, this.object);
return formData;
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Everything below here is only needed if the sheet is editable
if (!this.options.editable) return;
// Update Inventory Item
html.find('.item-edit').click(ev => {
const li = $(ev.currentTarget).parents(".item");
let itemId = li.data("item-id");
const item = this.actor.items.get( itemId );
item.sheet.render(true);
});
// Delete Inventory Item
html.find('.item-delete').click(ev => {
const li = $(ev.currentTarget).parents(".item");
FraggedKingdomUtility.confirmDelete(this, li);
});
html.find('.trait-link').click((event) => {
const itemId = $(event.currentTarget).data("item-id");
const item = this.actor.getOwnedItem(itemId);
item.sheet.render(true);
});
html.find('.competence-label a').click((event) => {
const li = $(event.currentTarget).parents(".item");
const competenceId = li.data("item-id");
this.actor.rollSkill(competenceId);
});
html.find('.weapon-label a').click((event) => {
const li = $(event.currentTarget).parents(".item");
const armeId = li.data("item-id");
const statId = li.data("stat-id");
this.actor.rollWeapon(armeId, statId);
});
html.find('.npc-fight-roll').click((event) => {
this.actor.rollNPCFight();
});
html.find('.npc-skill-roll').click((event) => {
this.actor.rollGenericSkill();
});
html.find('.lock-unlock-sheet').click((event) => {
this.options.editScore = !this.options.editScore;
this.render(true);
});
html.find('.item-link a').click((event) => {
const itemId = $(event.currentTarget).data("item-id");
const item = this.actor.getOwnedItem(itemId);
item.sheet.render(true);
});
html.find('.item-equip').click(ev => {
const li = $(ev.currentTarget).parents(".item");
this.actor.equipItem( li.data("item-id") );
this.render(true);
});
}
/* -------------------------------------------- */
/** @override */
setPosition(options = {}) {
const position = super.setPosition(options);
const sheetBody = this.element.find(".sheet-body");
const bodyHeight = position.height - 192;
sheetBody.css("height", bodyHeight);
return position;
}
/* -------------------------------------------- */
/** @override */
_updateObject(event, formData) {
// Update the Actor
return this.object.update(formData);
}
}

View File

@@ -0,0 +1,107 @@
import { CrucibleUtility } from "./crucible-utility.js";
export class CrucibleRollDialog extends Dialog {
/* -------------------------------------------- */
static async create(actor, rollData) {
let options = { classes: ["CrucibleDialog"], width: 620, height: 480, 'z-index': 99999 };
let html = await renderTemplate('systems/fvtt-crucible-rpg/templates/roll-dialog-generic.html', rollData);
return new CrucibleRollDialog(actor, rollData, html, options);
}
/* -------------------------------------------- */
constructor(actor, rollData, html, options, close = undefined) {
let conf = {
title: (rollData.mode == "skill") ? "Skill" : "Roll",
content: html,
buttons: {
roll: {
icon: '<i class="fas fa-check"></i>',
label: "Roll !",
callback: () => { this.roll() }
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: "Cancel",
callback: () => { this.close() }
}
},
close: close
}
super(conf, options);
this.actor = actor;
this.rollData = rollData;
}
/* -------------------------------------------- */
roll() {
CrucibleUtility.rollCrucible(this.rollData)
}
/* -------------------------------------------- */
async refreshDialog() {
const content = await renderTemplate("systems/fvtt-crucible-rpg/templates/roll-dialog-generic.html", this.rollData)
this.data.content = content
this.render(true)
}
/* -------------------------------------------- */
activateListeners(html) {
super.activateListeners(html);
var dialog = this;
function onLoad() {
}
$(function () { onLoad(); });
html.find('#statDicesLevel').change((event) => {
this.rollData.statDicesLevel = Number(event.currentTarget.value)
});
html.find('#specDicesLevel').change(async (event) => {
this.rollData.specDicesLevel = Number(event.currentTarget.value)
CrucibleUtility.updateSpecDicePool(this.rollData)
this.refreshDialog()
});
html.find('.effect-clicked').change(async (event) => {
let toggled = event.currentTarget.checked
let effectIdx = $(event.currentTarget).data("effect-idx")
this.manageEffects(effectIdx, toggled)
this.refreshDialog()
});
html.find('.armor-clicked').change((event) => {
let toggled = event.currentTarget.checked
let armorIdx = $(event.currentTarget).data("armor-idx")
this.manageArmors(armorIdx, toggled)
this.refreshDialog()
});
html.find('.weapon-clicked').change((event) => {
let toggled = event.currentTarget.checked
let weaponIdx = $(event.currentTarget).data("weapon-idx")
this.manageWeapons(weaponIdx, toggled)
this.refreshDialog()
});
html.find('.equip-clicked').change((event) => {
let toggled = event.currentTarget.checked
let equipIdx = $(event.currentTarget).data("equip-idx")
this.manageEquip(equipIdx, toggled)
})
html.find('.pool-add-dice').click(async (event) => {
let diceKey = $(event.currentTarget).data("dice-key")
let diceLevel = $(event.currentTarget).data("dice-level")
CrucibleUtility.addDicePool(this.rollData, diceKey, diceLevel)
this.refreshDialog()
})
html.find('.pool-remove-dice').click(async (event) => {
let idx = $(event.currentTarget).data("dice-idx")
CrucibleUtility.removeFromDicePool(this.rollData, idx)
this.refreshDialog()
})
}
}

View File

@@ -0,0 +1,6 @@
import { CrucibleUtility } from "./crucible-utility.js";
/* -------------------------------------------- */
export class CrucibleToken extends Token {
}

458
modules/crucible-utility.js Normal file
View File

@@ -0,0 +1,458 @@
/* -------------------------------------------- */
import { CrucibleCombat } from "./crucible-combat.js";
import { CrucibleCommands } from "./crucible-commands.js";
/* -------------------------------------------- */
const __level2Dice = ["d0", "d4", "d6", "d8", "d10", "d12"];
const __name2DiceValue = { "0": 0, "d0": 0, "d4": 4, "d6": 6, "d8": 8, "d10": 10, "d12": 12 }
/* -------------------------------------------- */
export class CrucibleUtility {
/* -------------------------------------------- */
static async init() {
Hooks.on('renderChatLog', (log, html, data) => CrucibleUtility.chatListeners(html));
Hooks.on("dropCanvasData", (canvas, data) => {
CrucibleUtility.dropItemOnToken(canvas, data)
});
this.rollDataStore = {}
this.defenderStore = {}
this.diceList = [];
this.diceFoundryList = [];
this.optionsDiceList = "";
this.buildDiceLists();
CrucibleCommands.init();
Handlebars.registerHelper('count', function (list) {
return list.length;
})
Handlebars.registerHelper('includes', function (array, val) {
return array.includes(val);
})
Handlebars.registerHelper('upper', function (text) {
return text.toUpperCase();
})
Handlebars.registerHelper('lower', function (text) {
return text.toLowerCase()
})
Handlebars.registerHelper('upperFirst', function (text) {
if (typeof text !== 'string') return text
return text.charAt(0).toUpperCase() + text.slice(1)
})
Handlebars.registerHelper('notEmpty', function (list) {
return list.length > 0;
})
Handlebars.registerHelper('mul', function (a, b) {
return parseInt(a) * parseInt(b);
})
}
/*-------------------------------------------- */
static getSkills() {
return this.skills;
}
/* -------------------------------------------- */
static async ready() {
const specs = await CrucibleUtility.loadCompendium("fvtt-crucible-rpg.skills");
this.specs = specs.map(i => i.toObject());
}
/* -------------------------------------------- */
static async loadCompendiumData(compendium) {
const pack = game.packs.get(compendium);
return await pack?.getDocuments() ?? [];
}
/* -------------------------------------------- */
static async loadCompendium(compendium, filter = item => true) {
let compendiumData = await CrucibleUtility.loadCompendiumData(compendium);
return compendiumData.filter(filter);
}
/* -------------------------------------------- */
static async chatListeners(html) {
html.on("click", '.view-item-from-chat', event => {
game.system.crucible.creator.openItemView(event)
})
}
/* -------------------------------------------- */
static async preloadHandlebarsTemplates() {
const templatePaths = [
'systems/fvtt-crucible-rpg/templates/editor-notes-gm.html',
'systems/fvtt-crucible-rpg/templates/partial-roll-select-effects.html',
'systems/fvtt-crucible-rpg/templates/partial-options-statistics.html',
'systems/fvtt-crucible-rpg/templates/partial-options-level.html',
'systems/fvtt-crucible-rpg/templates/partial-options-range.html',
'systems/fvtt-crucible-rpg/templates/partial-options-equipment-types.html',
'systems/fvtt-crucible-rpg/templates/partial-equipment-effects.html',
'systems/fvtt-crucible-rpg/templates/partial-actor-stat-block.html',
'systems/fvtt-crucible-rpg/templates/partial-actor-status.html',
'systems/fvtt-crucible-rpg/templates/partial-item-nav.html',
'systems/fvtt-crucible-rpg/templates/partial-item-description.html',
'systems/fvtt-crucible-rpg/templates/partial-actor-equipment.html'
]
return loadTemplates(templatePaths);
}
/* -------------------------------------------- */
static removeChatMessageId(messageId) {
if (messageId) {
game.messages.get(messageId)?.delete();
}
}
static findChatMessageId(current) {
return CrucibleUtility.getChatMessageId(CrucibleUtility.findChatMessage(current));
}
static getChatMessageId(node) {
return node?.attributes.getNamedItem('data-message-id')?.value;
}
static findChatMessage(current) {
return CrucibleUtility.findNodeMatching(current, it => it.classList.contains('chat-message') && it.attributes.getNamedItem('data-message-id'));
}
static findNodeMatching(current, predicate) {
if (current) {
if (predicate(current)) {
return current;
}
return CrucibleUtility.findNodeMatching(current.parentElement, predicate);
}
return undefined;
}
/* -------------------------------------------- */
static templateData(it) {
return CrucibleUtility.data(it)?.data ?? {}
}
/* -------------------------------------------- */
static data(it) {
if (it instanceof Actor || it instanceof Item || it instanceof Combatant) {
return it.data;
}
return it;
}
/* -------------------------------------------- */
static createDirectOptionList(min, max) {
let options = {};
for (let i = min; i <= max; i++) {
options[`${i}`] = `${i}`;
}
return options;
}
/* -------------------------------------------- */
static buildListOptions(min, max) {
let options = ""
for (let i = min; i <= max; i++) {
options += `<option value="${i}">${i}</option>`
}
return options;
}
/* -------------------------------------------- */
static getTarget() {
if (game.user.targets && game.user.targets.size == 1) {
for (let target of game.user.targets) {
return target;
}
}
return undefined;
}
/* -------------------------------------------- */
static updateRollData(rollData) {
let id = rollData.rollId
let oldRollData = this.rollDataStore[id] || {}
let newRollData = mergeObject(oldRollData, rollData)
this.rollDataStore[id] = newRollData
}
/* -------------------------------------------- */
static saveRollData(rollData) {
game.socket.emit("system.crucible-rpg", {
name: "msg_update_roll", data: rollData
}); // Notify all other clients of the roll
this.updateRollData(rollData)
}
/* -------------------------------------------- */
static getRollData(id) {
return this.rollDataStore[id]
}
/* -------------------------------------------- */
static async onSocketMesssage(msg) {
console.log("SOCKET MESSAGE", msg.name)
if (msg.name == "msg_update_defense_state") {
this.updateDefenseState(msg.data.defenderId, msg.data.rollId)
}
if (msg.name == "msg_update_roll") {
this.updateRollData(msg.data)
}
if (msg.name == "msg_gm_item_drop" && game.user.isGM) {
let actor = game.actors.get(msg.data.actorId)
let item
if (msg.data.isPack) {
item = await fromUuid("Compendium." + msg.data.isPack + "." + msg.data.itemId)
} else {
item = game.items.get(msg.data.itemId)
}
this.addItemDropToActor(actor, item)
}
if (msg.name == "msg_reroll_hero") {
this.rerollHeroRemaining(msg.data.userId, msg.data.rollId)
}
}
/* -------------------------------------------- */
static chatDataSetup(content, modeOverride, isRoll = false, forceWhisper) {
let chatData = {
user: game.user.id,
rollMode: modeOverride || game.settings.get("core", "rollMode"),
content: content
};
if (["gmroll", "blindroll"].includes(chatData.rollMode)) chatData["whisper"] = ChatMessage.getWhisperRecipients("GM").map(u => u.id);
if (chatData.rollMode === "blindroll") chatData["blind"] = true;
else if (chatData.rollMode === "selfroll") chatData["whisper"] = [game.user];
if (forceWhisper) { // Final force !
chatData["speaker"] = ChatMessage.getSpeaker();
chatData["whisper"] = ChatMessage.getWhisperRecipients(forceWhisper);
}
return chatData;
}
/* -------------------------------------------- */
static async showDiceSoNice(roll, rollMode) {
if (game.modules.get("dice-so-nice")?.active) {
if (game.dice3d) {
let whisper = null;
let blind = false;
rollMode = rollMode ?? game.settings.get("core", "rollMode");
switch (rollMode) {
case "blindroll": //GM only
blind = true;
case "gmroll": //GM + rolling player
whisper = this.getUsers(user => user.isGM);
break;
case "roll": //everybody
whisper = this.getUsers(user => user.active);
break;
case "selfroll":
whisper = [game.user.id];
break;
}
await game.dice3d.showForRoll(roll, game.user, true, whisper, blind);
}
}
}
/* -------------------------------------------- */
static async rollCrucible(rollData) {
let actor = game.actors.get(rollData.actorId)
let diceFormulaTab = []
for (let dice of rollData.dicePool) {
let level = dice.level
if (dice.name == "stat") {
level += rollData.statLevelBonus
}
diceFormulaTab.push(this.getFoundryDiceFromLevel(level))
}
let diceFormula = '{' + diceFormulaTab.join(', ') + '}kh + ' + (rollData.stat?.mod || 0)
// Performs roll
let myRoll = rollData.roll
if (!myRoll || rollData.rerollHero || rollData.rerollMomentum) { // New rolls only of no rerolls
myRoll = new Roll(diceFormula).roll({ async: false })
await this.showDiceSoNice(myRoll, game.settings.get("core", "rollMode"))
rollData.roll = myRoll
}
// Final score and keep data
rollData.finalScore = myRoll.total
if (rollData.damages) {
let dmgFormula = this.getFoundryDiceFromLevel(rollData.damages.value)
let dmgRoll = new Roll(dmgFormula).roll({ async: false })
await this.showDiceSoNice(dmgRoll, game.settings.get("core", "rollMode"))
rollData.dmgResult = dmgRoll.total
}
this.createChatWithRollMode(rollData.alias, {
content: await renderTemplate(`systems/fvtt-crucible-rpg/templates/chat-generic-result.html`, rollData)
});
// Init stuf
if (rollData.isInit) {
let combat = game.combats.get(rollData.combatId)
combat.updateEmbeddedDocuments("Combatant", [{ _id: rollData.combatantId, initiative: rollData.finalScore }])
}
// Stun specific -> Suffere a stun level when dmg-res
if (rollData.subKey && rollData.subKey == "dmg-res") {
actor.modifyStun(+1)
}
//this.removeUsedPerkEffects( rollData) // Unused for now
this.removeOneUseEffects(rollData) // Unused for now
// And save the roll
this.saveRollData(rollData)
actor.lastRoll = rollData
}
/* -------------------------------------------- */
static getUsers(filter) {
return game.users.filter(filter).map(user => user.data._id);
}
/* -------------------------------------------- */
static getWhisperRecipients(rollMode, name) {
switch (rollMode) {
case "blindroll": return this.getUsers(user => user.isGM);
case "gmroll": return this.getWhisperRecipientsAndGMs(name);
case "selfroll": return [game.user.id];
}
return undefined;
}
/* -------------------------------------------- */
static getWhisperRecipientsAndGMs(name) {
let recep1 = ChatMessage.getWhisperRecipients(name) || [];
return recep1.concat(ChatMessage.getWhisperRecipients('GM'));
}
/* -------------------------------------------- */
static blindMessageToGM(chatOptions) {
let chatGM = duplicate(chatOptions);
chatGM.whisper = this.getUsers(user => user.isGM);
chatGM.content = "Blinde message of " + game.user.name + "<br>" + chatOptions.content;
console.log("blindMessageToGM", chatGM);
game.socket.emit("system.fvtt-crucible-rgp", { msg: "msg_gm_chat_message", data: chatGM });
}
/* -------------------------------------------- */
static async searchItem(dataItem) {
let item
if (dataItem.pack) {
item = await fromUuid("Compendium." + dataItem.pack + "." + dataItem.id)
} else {
item = game.items.get(dataItem.id)
}
return item
}
/* -------------------------------------------- */
static split3Columns(data) {
let array = [[], [], []];
if (data == undefined) return array;
let col = 0;
for (let key in data) {
let keyword = data[key];
keyword.key = key; // Self-reference
array[col].push(keyword);
col++;
if (col == 3) col = 0;
}
return array;
}
/* -------------------------------------------- */
static createChatMessage(name, rollMode, chatOptions) {
switch (rollMode) {
case "blindroll": // GM only
if (!game.user.isGM) {
this.blindMessageToGM(chatOptions);
chatOptions.whisper = [game.user.id];
chatOptions.content = "Message only to the GM";
}
else {
chatOptions.whisper = this.getUsers(user => user.isGM);
}
break;
default:
chatOptions.whisper = this.getWhisperRecipients(rollMode, name);
break;
}
chatOptions.alias = chatOptions.alias || name;
ChatMessage.create(chatOptions);
}
/* -------------------------------------------- */
static getBasicRollData() {
let rollData = {
rollId: randomID(16),
rollMode: game.settings.get("core", "rollMode"),
}
CrucibleUtility.updateWithTarget(rollData)
return rollData
}
/* -------------------------------------------- */
static updateWithTarget(rollData) {
let objectDefender
let target = CrucibleUtility.getTarget();
if (target) {
let defenderActor = game.actors.get(target.data.actorId)
objectDefender = CrucibleUtility.data(defenderActor)
objectDefender = mergeObject(objectDefender, target.data.actorData)
rollData.defender = objectDefender
rollData.attackerId = this.id
rollData.defenderId = objectDefender._id
defenderActor.addHindrancesList(rollData.effectsList)
}
}
/* -------------------------------------------- */
static createChatWithRollMode(name, chatOptions) {
this.createChatMessage(name, game.settings.get("core", "rollMode"), chatOptions);
}
/* -------------------------------------------- */
static async confirmDelete(actorSheet, li) {
let itemId = li.data("item-id");
let msgTxt = "<p>Are you sure to remove this Item ?";
let buttons = {
delete: {
icon: '<i class="fas fa-check"></i>',
label: "Yes, remove it",
callback: () => {
actorSheet.actor.deleteEmbeddedDocuments("Item", [itemId]);
li.slideUp(200, () => actorSheet.render(false));
}
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: "Cancel"
}
}
msgTxt += "</p>";
let d = new Dialog({
title: "Confirm removal",
content: msgTxt,
buttons: buttons,
default: "cancel"
});
d.render(true);
}
}