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;
}
}
}