import {PlayerCharacterDataModel} from "../data/character.mjs"; import {ActionManager} from "./actions/action-manager.mjs"; export class CharacterSheet extends ActorSheet { /**@override */ static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { classes: ['dsa41', 'sheet', 'actor', 'character'], width: 1100, height: 480, tabs: [ { navSelector: '.sheet-tabs', contentSelector: '.sheet-body', initial: 'description', }, ], }); } /** @override */ get template() { return `systems/DSA_4-1/templates/actor/actor-character-sheet.hbs`; } /** @override */ async getData() { const context = super.getData(); // Use a safe clone of the actor data for further operations. const actorData = context.data; // Add the actor's data to context.data for easier access, as well as flags. context.system = actorData.system; context.flags = actorData.flags; this.#addSkillsToContext(context) this.#addAdvantagesToContext(context) this.#addAttributesToContext(context) this.#addEquipmentsToContext(context) await this.#addCombatStatistics(context) this.#addActionsToContext(context) return context; } static getElementByName(collection, id) { const array = Array.from(collection); for (const element of array) { if (element._id === id) { return element; } } } #addSkillsToContext(context) { const actorData = context.data; context.skills = {}; context.flatSkills = []; Object.values(actorData.items).forEach((item, index) => { if (item.type === "Skill") { const talentGruppe = item.system.gruppe; const eigenschaften = Object.values(item.system.probe); const werte = [ {name: eigenschaften[0], value: this.prepareEigenschaftRoll(actorData, eigenschaften[0])}, {name: eigenschaften[1], value: this.prepareEigenschaftRoll(actorData, eigenschaften[1])}, {name: eigenschaften[2], value: this.prepareEigenschaftRoll(actorData, eigenschaften[2])} ] if (context.skills[talentGruppe] == null) { context.skills[talentGruppe] = []; } const obj = { type: "talent", gruppe: talentGruppe, name: item.name, taw: "" + item.system.taw, tawPath: `system.items.${index}.taw`, werte, rollEigenschaft1: werte[0].value, rollEigenschaft2: werte[1].value, rollEigenschaft3: werte[2].value, eigenschaft1: werte[0].name, eigenschaft2: werte[1].name, eigenschaft3: werte[2].name, probe: `(${eigenschaften.join("/")})`, at: item.system.at, pa: item.system.pa, id: item._id, }; context.skills[talentGruppe].push(obj); context.flatSkills.push(obj); } } ); } #addAttributesToContext(context) { const actorData = context.data; context.attributes = [ { eigenschaft: "mu", name: "MU", tooltip: "Mut", wert: actorData.system.attribute.mu.aktuell ?? 0, }, { eigenschaft: "kl", name: "KL", tooltip: "Klugheit", wert: actorData.system.attribute.kl.aktuell ?? 0, }, { eigenschaft: "in", name: "IN", tooltip: "Intuition", wert: actorData.system.attribute.in.aktuell ?? 0, }, { eigenschaft: "ch", name: "CH", tooltip: "Charisma", wert: actorData.system.attribute.ch.aktuell ?? 0, }, { eigenschaft: "ff", name: "FF", tooltip: "Fingerfertigkeit", wert: actorData.system.attribute.ff.aktuell ?? 0, }, { eigenschaft: "ge", name: "GE", tooltip: "Geschicklichkeit", wert: actorData.system.attribute.ge.aktuell ?? 0, }, { eigenschaft: "ko", name: "KO", tooltip: "Konstitution", wert: actorData.system.attribute.ko.aktuell ?? 0, }, { eigenschaft: "kk", name: "KK", tooltip: "Körperkraft", wert: actorData.system.attribute.kk.aktuell ?? 0, }, ]; } #addAdvantagesToContext(context) { context.advantages = []; const actorData = context.data; Object.values(actorData.items).forEach((item) => { if (item.type === "Advantage") { context.advantages.push({ id: item._id, name: item.name, value: item.system.value, options: item.system.auswahl, description: item.system.description, }); } } ); } #findEquipmentOnSlot(slot, setNumber) { return this.object.items.get(this.object.system.heldenausruestung[setNumber][slot]) } #findTalentsOfEquipment(equipment) { } #addActionsToContext(context) { const am = new ActionManager(this.object) context.actions = am.evaluate() } #isWorn(itemId, setId) { const slots = PlayerCharacterDataModel.getSlots() const set = this.object.system.heldenausruestung[setId] for (const slot of slots) { const equipmentSlotId = set[slot] if (equipmentSlotId === itemId) { return slot } } return false } async #addCombatStatistics(context) { const actorData = context.data; context.inidice = actorData.system.ini.wuerfel; context.inivalue = actorData.system.ini.aktuell; context.inimod = actorData.system.ini.mod; context.aupper = Math.min((context.actor.system.aup.aktuell / context.actor.system.aup.max) * 100, 100); context.lepper = Math.min((context.actor.system.lep.aktuell / context.actor.system.lep.max) * 100, 100); context.lepcurrent = context.actor.system.lep.aktuell ?? 0 context.aupcurrent = context.actor.system.aup.aktuell ?? 0 const fernkampf = this.#findEquipmentOnSlot("fernkampf", 0) const links = this.#findEquipmentOnSlot("links", 0) const rechts = this.#findEquipmentOnSlot("rechts", 0) context.attacks = []; if (fernkampf) { const fkitems = fernkampf.system.rangedSkills.map(async (skillInQuestion) => await this.object.items.getName(skillInQuestion)) fkitems.forEach(async skill => { const obj = await skill console.log(this.object.system.fk, obj.system.at); context.attacks.push({ name: obj.name, using: fernkampf.name, atroll: `1d20 + ${this.object.system.fk + obj.system.at}`, at: `1w20 + ${this.object.system.fk + obj.system.at}`, iniroll: `(${context.inidice})d6 + ${context.inivalue + fernkampf.system.iniModifier ?? 0}`, ini: `${context.inidice}w6 + ${context.inivalue + fernkampf.system.iniModifier ?? 0}`, }) }) } if (links) { const meitems = links.system.meleeSkills.map(async (skillInQuestion) => await this.object.items.getName(skillInQuestion)) meitems.forEach(async skill => { const obj = await skill context.attacks.push({ name: obj.name, using: links.name, atroll: `1d20 + ${this.object.system.at + obj.system.at + links.system.attackModifier}`, at: `1w20 + ${this.object.system.at + obj.system.at + links.system.attackModifier}`, paroll: `1d20 + ${this.object.system.pa + obj.system.pa + links.system.parryModifier}`, pa: `1w20 + ${this.object.system.pa + obj.system.pa + links.system.parryModifier}`, iniroll: `(${context.inidice})d6 + ${context.inivalue + links.system.iniModifier ?? 0}`, ini: `${context.inidice}w6 + ${context.inivalue + links.system.iniModifier ?? 0}`, }) }) } if (rechts) { const meitems = rechts.system.meleeSkills.map(async (skillInQuestion) => await this.object.items.getName(skillInQuestion)) meitems.forEach(async skill => { const obj = await skill console.log(this.object.system.at) context.attacks.push({ name: obj.name, using: rechts.name, atroll: `1d20 + ${this.object.system.at + obj.system.at + rechts.system.attackModifier}`, at: `1w20 + ${this.object.system.at + obj.system.at + rechts.system.attackModifier}`, paroll: `1d20 + ${this.object.system.pa + obj.system.pa + rechts.system.parryModifier}`, pa: `1w20 + ${this.object.system.pa + obj.system.pa + rechts.system.parryModifier}`, iniroll: `(${context.inidice})d6 + ${context.inivalue + rechts.system.iniModifier ?? 0}`, ini: `${context.inidice}w6 + ${context.inivalue + rechts.system.iniModifier ?? 0}`, }) }) } // add weapons to sidebar } prepareEigenschaftRoll(actorData, name) { if (name) { return actorData.system.attribute[name.toLowerCase()].aktuell } else { return 0 } } #addEquipmentsToContext(context) { context.equipments = []; const actorData = context.data; context.carryingweight = 0; Object.values(actorData.items).forEach((item, index) => { if (item.type === "Equipment") { context.equipments.push({ index: index, id: item._id, quantity: item.system.quantity, name: item.name, icon: item.img ?? "", weight: item.system.weight ?? 0, worn: this.#isWorn(item._id, 0) }) context.carryingweight += item.system.quantity * item.system.weight; } }) context.maxcarryingcapacity = actorData.system.attribute.kk.aktuell context.carryingpercentage = Math.min((context.carryingweight / context.maxcarryingcapacity) * 100, 100); const maxSets = 3 const romanNumerals = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"] context.sets = [] for (let setIndex = 0; setIndex < maxSets; setIndex++) { context.sets.push({ tab: "set" + (setIndex + 1), name: romanNumerals[setIndex], index: setIndex, slots: [ { target: "links", id: this.object.system.heldenausruestung[setIndex]?.links, name: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.links)?.name, icon: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.links)?.img }, { target: "rechts", id: this.object.system.heldenausruestung[setIndex]?.rechts, name: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.rechts)?.name, icon: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.rechts)?.img }, { target: "brust", id: this.object.system.heldenausruestung[setIndex]?.brust, name: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.brust)?.name, icon: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.brust)?.img }, { target: "ruecken", id: this.object.system.heldenausruestung[setIndex]?.ruecken, name: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.ruecken)?.name, icon: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.ruecken)?.img }, { target: "kopf", id: this.object.system.heldenausruestung[setIndex]?.kopf, name: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.kopf)?.name, icon: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.kopf)?.img }, { target: "fernkampf", id: this.object.system.heldenausruestung[setIndex]?.fernkampf, name: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.fernkampf)?.name, icon: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.fernkampf)?.img }, { target: "munition", id: this.object.system.heldenausruestung[setIndex]?.munition, name: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.munition)?.name, icon: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.munition)?.img }, { target: "armlinks", id: this.object.system.heldenausruestung[setIndex]?.armlinks, name: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.armlinks)?.name, icon: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.armlinks)?.img }, { target: "armrechts", id: this.object.system.heldenausruestung[setIndex]?.armrechts, name: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.armrechts)?.name, icon: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.armrechts)?.img }, { target: "bauch", id: this.object.system.heldenausruestung[setIndex]?.bauch, name: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.bauch)?.name, icon: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.bauch)?.img }, { target: "beinlinks", id: this.object.system.heldenausruestung[setIndex]?.beinlinks, name: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.beinlinks)?.name, icon: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.beinlinks)?.img }, { target: "beinrechts", id: this.object.system.heldenausruestung[setIndex]?.beinrechts, name: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.beinrechts)?.name, icon: this.object.items.get(actorData.system.heldenausruestung[setIndex]?.beinrechts)?.img } ] }) } } async _onTalentRoll(event) { event.preventDefault(); const dataset = event.currentTarget.dataset; console.log(dataset) if (dataset.rolleigenschaft1) { let roll1 = new Roll("3d20", this.actor.getRollData()); let evaluated1 = (await roll1.evaluate()) const dsaDieRollEvaluated = this._evaluateRoll(evaluated1.terms[0].results, { taw: dataset.taw, werte: [dataset.rolleigenschaft1, dataset.rolleigenschaft2, dataset.rolleigenschaft3], }) if (dsaDieRollEvaluated.tap >= 0) { // erfolg evaluated1.toMessage({ speaker: ChatMessage.getSpeaker({actor: this.actor}), flavor: ` ${dsaDieRollEvaluated.meisterlich ? 'Meisterlich geschafft' : 'Geschafft'} mit ${dsaDieRollEvaluated.tap} Punkten übrig`, rollMode: game.settings.get('core', 'rollMode'), }) } else { // misserfolg evaluated1.toMessage({ speaker: ChatMessage.getSpeaker({actor: this.actor}), flavor: ` ${dsaDieRollEvaluated.meisterlich ? 'Gepatzt' : ''} mit ${Math.abs(dsaDieRollEvaluated.tap)} Punkten daneben`, rollMode: game.settings.get('core', 'rollMode'), }) } } } _evaluateRoll(rolledDice, { taw, lowerThreshold = 1, upperThreshold = 20, countToMeisterlich = 3, countToPatzer = 3, werte = [] }) { let tap = taw; let meisterlichCounter = 0; let patzerCounter = 0; let failCounter = 0; rolledDice.forEach((rolledDie, index) => { if (tap < 0 && rolledDie.result > werte[index]) { tap -= rolledDie.result - werte[index]; if (tap < 0) { // konnte nicht vollständig ausgeglichen werden failCounter++; } } else if (rolledDie.result > werte[index]) { // taw ist bereits aufgebraucht und wert kann nicht ausgeglichen werden tap -= rolledDie.result - werte[index]; failCounter++; } if (rolledDie.result <= lowerThreshold) meisterlichCounter++; if (rolledDie.result > upperThreshold) patzerCounter++; }) return { tap, meisterlich: meisterlichCounter === countToMeisterlich, patzer: patzerCounter === countToPatzer, } } _onAttributeRoll(event) { event.preventDefault(); const dataset = event.currentTarget.dataset; if (dataset.roll) { let label = dataset.label ? `[Attribut] ${dataset.label}` : ''; let roll = new Roll(dataset.roll, this.actor.getRollData()); roll.toMessage({ speaker: ChatMessage.getSpeaker({actor: this.actor}), flavor: label, rollMode: game.settings.get('core', 'rollMode'), }); return roll; } } openEmbeddedDocument(documentId) { this.object.items.get(documentId).sheet.render(true) } _onRoll(event) { event.preventDefault(); const dataset = event.currentTarget.dataset; if (dataset.roll) { let label = dataset.label ? `${dataset.label}` : ''; let roll = new Roll(dataset.roll, this.actor.getRollData()); roll.toMessage({ speaker: ChatMessage.getSpeaker({actor: this.actor}), flavor: label, rollMode: game.settings.get('core', 'rollMode'), }); return roll; } } #getEquipmentset(setId) { const equipmentSet = this.object.system.heldenausruestung[setId] const updateObject = {} // TODO: there's got to be a better angle! updateObject[`system.heldenausruestung.${setId}.links`] = equipmentSet.links; updateObject[`system.heldenausruestung.${setId}.rechts`] = equipmentSet.rechts; updateObject[`system.heldenausruestung.${setId}.brust`] = equipmentSet.brust; updateObject[`system.heldenausruestung.${setId}.bauch`] = equipmentSet.bauch; updateObject[`system.heldenausruestung.${setId}.ruecken`] = equipmentSet.ruecken; updateObject[`system.heldenausruestung.${setId}.kopf`] = equipmentSet.kopf; updateObject[`system.heldenausruestung.${setId}.fernkampf`] = equipmentSet.fernkampf; updateObject[`system.heldenausruestung.${setId}.munition`] = equipmentSet.munition; updateObject[`system.heldenausruestung.${setId}.armlinks`] = equipmentSet.armlinks; updateObject[`system.heldenausruestung.${setId}.armrechts`] = equipmentSet.armrechts; updateObject[`system.heldenausruestung.${setId}.beinlinks`] = equipmentSet.beinlinks; updateObject[`system.heldenausruestung.${setId}.beinrechts`] = equipmentSet.beinrechts; return updateObject; } showAdjustAttributeDialog(attributeName, attributeField, previousValue) { const thisActor = this; const myContent = ` Value: `; function updateAttribute(html) { const value = html.find("input#attributeValue").val(); const attribute = {} attribute[attributeField.toLowerCase()] = { aktuell: value } thisActor.object.update({system: {attribute}}) } new Dialog({ title: `${attributeName} ändern auf`, content: myContent, buttons: { button1: { label: "Ändern", callback: (html) => { updateAttribute(html) }, icon: `` } } }).render(true); } activateListeners(html) { super.activateListeners(html); const tabs = new Tabs({ navSelector: ".paperdoll-tabs.tabs", contentSelector: ".sheet-body.paperdoll-sets", initial: "set1" }); tabs.bind(html[0]); html.on('click', '.attribute.rollable', (evt) => { this._onAttributeRoll(evt); }); html.on('click', '.talent.rollable', (evt) => { this._onTalentRoll(evt); }); html.on('click', '.sidebar-element.rollable', (evt) => { this._onRoll(evt); }); html.on('click', '.talent .name', (evt) => { this.openEmbeddedDocument(evt.target.dataset.id); evt.stopPropagation(); }) html.on('click', '.advantage .name', (evt) => { this.openEmbeddedDocument(evt.target.dataset.id); evt.stopPropagation(); }) html.on('click', '.equipment', (evt) => { this.openEmbeddedDocument(evt.target.parentElement.dataset.id); evt.stopPropagation(); }) html.on('dragstart', '.equipment', (evt) => { evt.originalEvent.dataTransfer.setData("application/json", JSON.stringify({ documentId: evt.currentTarget.dataset.id })); }) html.on('drop', '.equipped', async (evt) => { const {actor, target, setId} = evt.currentTarget.dataset; try { const {documentId} = JSON.parse(evt.originalEvent.dataTransfer.getData("application/json")); if (actor === this.object._id && documentId) { // managing equipped items //const slot = this.#isWorn(documentId, setId) const updateObject = this.#getEquipmentset(setId) updateObject[`system.heldenausruestung.${setId}.${target}`] = documentId; console.log(updateObject); await this.object.update(updateObject); } evt.stopPropagation(); } catch (e) { } }) new ContextMenu(html, '.talent.rollable', [ { name: "Entfernen", icon: '', callback: (event) => { this.object.deleteEmbeddedDocuments('Item', [event[0].dataset.id]) }, condition: () => true } ]); new ContextMenu(html, '.attribute.rollable', [ { name: "Anpassen", icon: '', callback: (event) => { this.showAdjustAttributeDialog(event[0].dataset.name, event[0].dataset.label, event[0].dataset.value) }, condition: () => true } ]); let handler = ev => this._onDragStart(ev); // Find all items on the character sheet. html.find('.talent.rollable').each((i, li) => { // Add draggable attribute and dragstart listener. li.setAttribute("draggable", true); li.addEventListener("dragstart", handler, false); }); new ContextMenu(html, '.equipment', [ { name: "Aus dem Inventar entfernen", icon: '', callback: (event) => { // TODO find id on heldenausruestung to remove the worn items as well this.object.deleteEmbeddedDocuments('Item', [event[0].dataset.id]) }, condition: () => true } ]); new ContextMenu(html, '.equipped', [ { name: "Gegenstand vom Set entfernen", callback: (event) => { const {setId, target, actor} = event[0].dataset const updateObject = this.#getEquipmentset(setId) updateObject[`system.heldenausruestung.${setId}.${target}`] = null; this.object.update(updateObject); }, condition: () => true } ]); } #handleDroppedSkill(actor, skill) { const array = Array.from(actor.items); for (let i = 0; i < array.length; i++) { if (array[i].name === skill.name) { return false; } } } #handleDroppedAdvantage(actor, advantage) { const array = Array.from(actor.items); for (let i = 0; i < array.length; i++) { if (array[i].name === advantage.name) { // TODO: adjust for uniqueness return false; } } } #handleDroppedEquipment(actor, equipment) { const array = Array.from(actor.items); for (let i = 0; i < array.length; i++) { if (array[i].name === equipment.name) { // TODO: adjust item quantity if item is the same console.log(equipment); return false; } } } static onDroppedData(actor, characterSheet, data) { const uuid = foundry.utils.parseUuid(data.uuid); const collection = uuid.collection.index ?? uuid.collection; const document = CharacterSheet.getElementByName(collection, uuid.id); const { name, type } = document console.log(name, type) switch (type) { case "Skill": return characterSheet.#handleDroppedSkill(actor, document); // on false cancel this whole operation case "Advantage": return characterSheet.#handleDroppedAdvantage(actor, document); case "Equipment": return characterSheet.#handleDroppedEquipment(actor, document); default: return false; } } }