foundry-dsa41-game/src/module/sheets/characterSheet.mjs

653 lines
26 KiB
JavaScript

import Advsf from "./character/advsf.mjs"
import Combat from "./character/combat.mjs"
import Effects from "./character/effects.mjs"
import Equipment from "./character/equipment.mjs"
import Liturgies from "./character/liturgies.mjs"
import Meta from "./character/meta.mjs"
import Skills from "./character/skills.mjs"
import Social from "./character/social.mjs";
import Spells from "./character/spells.mjs"
import {CombatActionDialog} from "../dialog/combatAction.mjs";
import {ActionManager} from "./actions/action-manager.mjs";
import {DefenseActionDialog} from "../dialog/defenseAction.mjs";
import {RestingDialog} from "../dialog/restingDialog.mjs";
import {LiturgyDialog} from "../dialog/liturgyDialog.mjs";
import {TalentDialog} from "../dialog/talentDialog.mjs";
import {AttributeDialog} from "../dialog/attributeDialog.mjs";
import {ItemBrowserDialog} from "../dialog/itemBrowserDialog.mjs";
import * as EquipmentDocument from "../documents/equipment.mjs";
const {HandlebarsApplicationMixin, DocumentSheetV2} = foundry.applications.api
const {ActorSheetV2} = foundry.applications.sheets
class CharacterSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
/** @inheritDoc */
static DEFAULT_OPTIONS = {
position: {width: 1100, height: 640},
classes: ['dsa41', 'sheet', 'actor', 'character'],
tag: 'form',
form: {
submitOnChange: true,
closeOnSubmit: false,
handler: CharacterSheet.#onSubmitForm
},
window: {
resizable: true,
},
actions: {
rollCombatSkill: CharacterSheet.#rollCombatSkill,
rollSkill: CharacterSheet.#rollSkill,
rollFlaw: CharacterSheet.#rollFlaw,
rollAttribute: CharacterSheet.#rollAttribute,
editImage: DocumentSheetV2.DEFAULT_OPTIONS.actions.editImage,
openEmbeddedDocument: CharacterSheet.#openEmbeddedDocument,
openCultureDocument: CharacterSheet.#openCultureDocument,
openSpeciesDocument: CharacterSheet.#openSpeciesDocument,
openCombatAction: CharacterSheet.#openCombatAction,
openLiturgyDialog: CharacterSheet.#openLiturgyDialog,
progressCooldown: CharacterSheet.#progressCooldown,
cancelCooldown: CharacterSheet.#cancelCooldown,
activateCooldown: CharacterSheet.#activateCooldown,
rest: CharacterSheet.#startResting,
removeEffect: CharacterSheet.#removeEffect,
rollDamage: CharacterSheet.#rollDamage,
openItemBrowser: CharacterSheet.#openItemBrowser,
newItem: CharacterSheet.#addNewItem
}
}
static TABS = {
sheet: {
tabs: [],
initial: 'meta'
},
}
/** @inheritDoc */
static PARTS = {
form: {
template: `systems/DSA_4-1/templates/actor/character/main-sheet.hbs`
},
meta: {
template: Meta.template
},
social: {
template: Social.template
},
advsf: {
template: Advsf.template
},
combat: {
template: Combat.template
},
equipment: {
template: Equipment.template,
scrollable: ['.inventory']
},
skills: {
template: Skills.template
},
spells: {
template: Spells.template
},
liturgies: {
template: Liturgies.template
},
effects: {
template: Effects.template
},
set1: {
template: "systems/DSA_4-1/templates/actor/character/tab-set.hbs"
},
set2: {
template: "systems/DSA_4-1/templates/actor/character/tab-set.hbs"
},
set3: {
template: "systems/DSA_4-1/templates/actor/character/tab-set.hbs"
},
}
/**
*
* @param {PointerEvent} event
*/
static #rollSkill(event) {
const {id} = event.target.dataset
new TalentDialog(this.document, id).render(true)
}
/**
*
* @param {PointerEvent} event
*/
static #rollCombatSkill(event) {
const {id} = event.target.dataset
const skill = this.document.items.get(id)
if (skill?.system?.roll) {
skill.system.roll("publicroll", event.shiftKey ? "PARRY" : "ATTACK")
}
}
static #rollAttribute(event, target) {
new AttributeDialog(this.document, {
name: target.dataset.name,
value: target.dataset.value,
symbol: target.dataset.symbol,
}).render(true)
}
static async #rollFlaw(event, target) {
new AttributeDialog(this.document, target.dataset.itemId).render(true)
}
static async #progressCooldown(event, target) {
const {cooldownId} = target.dataset
const cooldowns = this.document.system.cooldowns
const cooldown = this.document.system.cooldowns[cooldownId]
cooldowns.splice(cooldownId, 1)
if (cooldown) {
cooldown.current = cooldown.current + 1
}
cooldowns.push(cooldown)
this.document.update({"system.cooldowns": cooldowns.sort((a, b) => a.current - b.current)})
ui.notifications.info(`Abklingzeit von ${cooldown.data.title} um 1 Aktion reduziert`)
}
static async #cancelCooldown(event, target) {
const {cooldownId} = target.dataset
const cooldown = this.document.system.cooldowns[cooldownId]
const cooldowns = this.document.system.cooldowns
cooldowns.splice(cooldownId, 1)
this.document.update({"system.cooldowns": cooldowns.sort((a, b) => a.current - b.current)})
ui.notifications.info(`${cooldown.data.maneuver.name} abgebrochen`)
}
static async #activateCooldown(event, target) {
const {cooldownId} = target.dataset
const cooldowns = this.document.system.cooldowns
const cooldown = this.document.system.cooldowns[cooldownId]
if (cooldown && cooldown.current >= cooldown.start) {
const am = new ActionManager(this.document)
console.log(cooldown.data.maneuver)
const action = new Function(`return ${cooldown.data.maneuver}`)
if (action) {
action()(this.document.system.cooldowns, {...cooldown.data, actor: this.document})
}
}
cooldowns.splice(cooldownId, 1)
this.document.update({"system.cooldowns": cooldowns.sort((a, b) => a.current - b.current)})
ui.notifications.info(`${cooldown.data.title} ausgeführt`)
}
/**
*
* @param {MouseEvent} event
*/
static #openEmbeddedDocument(event) {
let dataset = event.target.dataset
if (!dataset.itemId && !dataset.id) {
dataset = event.target.parentElement.dataset
}
const id = dataset.itemId ?? dataset.id
this.document.items.get(id).sheet.render(true)
}
static #openCultureDocument() {
this.document.itemTypes["Culture"]?.[0]?.sheet.render(true)
}
static #openSpeciesDocument() {
this.document.itemTypes["Species"]?.[0]?.sheet.render(true)
}
static #openCombatAction(event, target) {
let {weapon, skill} = target.dataset
switch (target.dataset.mode) {
case "attack":
new CombatActionDialog(this.document, {weapon, skill}).render(true)
break
case "defense":
new DefenseActionDialog(this.document, {weapon, skill}).render(true)
break
}
}
static #openLiturgyDialog(event, target) {
const {id, lkp, deity} = target.dataset
new LiturgyDialog(this.document, lkp, id, deity).render(true)
}
static #startResting(event, target) {
const dialog = new RestingDialog(this.document)
dialog.render(true)
}
static async #removeEffect(event, target) {
const {actorId, effectId} = target.dataset
if (actorId === this.document._id) {
const item = this.document.items.get(effectId)
if (item.type === "ActiveEffect") {
this.document.deleteEmbeddedDocuments("Item", [effectId])
}
}
}
static async #openItemBrowser(event, target) {
new ItemBrowserDialog(this.document).render(true)
}
static async #addNewItem(event, target) {
let item = new EquipmentDocument.Equipment({
name: "Neuer Gegenstand",
type: "Equipment",
})
const items = await this.document.createEmbeddedDocuments("Item", [item])
items[0].sheet.render(true)
}
_configureRenderOptions(options) {
super._configureRenderOptions(options)
if (options.window) {
options.window.title = this.document.name
}
return options
}
/**
* Handle form submission
* @this {AdvantageSheet}
* @param {SubmitEvent} event
* @param {HTMLFormElement} form
* @param {FormDataExtended} formData
*/
static async #onSubmitForm(event, form, formData) {
event.preventDefault()
await this.document.update(formData.object)
}
static async #rollDamage(event, target) {
let {weapon, isRanged} = target.dataset
isRanged = isRanged == "true"
weapon = this.document.items.get(weapon)
if (weapon) {
const damageFormula = isRanged ? weapon.system.rangedAttackDamage : weapon.system.meleeAttackDamage
const calculation = await foundry.applications.api.DialogV2.prompt({
window: {title: game.i18n.format("COMBAT_DIALOG_TP.windowTitle")},
content: `<div><label><span>${game.i18n.format("COMBAT_DIALOG_TP.regularFormula")}</span><input type="text" name="formula" value="${damageFormula}"></label></div><div><label><span>${game.i18n.format("COMBAT_DIALOG_TP.bonusDamage")}</span><input type="text" name="bonusDamage" value="0"></label></div>`,
ok: {
label: game.i18n.format("COMBAT_DIALOG_TP.buttonText"),
callback: (event, button, dialog) => {
return {
formula: button.form.elements.formula.value,
bonusDamage: button.form.elements.bonusDamage.value
}
}
}
});
const sanitisedFormula = calculation.formula.replace(/wW/g, "d")
const suffix = calculation.bonusDamage >= 0 ? "+" + calculation.bonusDamage : calculation.bonusDamage
let r = new Roll(sanitisedFormula + suffix, this.document.getRollData());
const label = `Schadenswurf`
await r.toMessage({
speaker: ChatMessage.getSpeaker({actor: this.document}),
flavor: label,
rollMode: game.settings.get('core', 'rollMode'),
})
}
}
_getTabsConfig(group) {
const tabs = foundry.utils.deepClone(super._getTabsConfig(group))
Meta._getTabConfig(tabs, this)
Social._getTabConfig(tabs, this)
Advsf._getTabConfig(tabs, this)
Combat._getTabConfig(tabs, this)
Equipment._getTabConfig(tabs, this)
Skills._getTabConfig(tabs, this)
Spells._getTabConfig(tabs, this)
Liturgies._getTabConfig(tabs, this)
Effects._getTabConfig(tabs, this)
return tabs
}
async _preparePartContext(partId, context) {
switch (partId) {
case "form":
const actorData = context.document
context.system = actorData.system
context.actorId = actorData._id
context.isOwner = actorData.isOwner
context.flags = actorData.flags
context.derived = context.document.system
context.professions = actorData.items.filter(p => p.type === 'Profession').map(p => {
// is tarnidentitaet revealed?
if (p.system.revealed) {
return {
id: p.id,
name: p.name,
alias: p.system.alias,
}
} else {
return {
id: p.id,
name: p.system.alias ?? p.name,
alias: p.name,
}
}
})
context.spezies = ""
if (actorData.itemTypes["Species"]?.[0]) {
const speciesData = actorData.itemTypes["Species"][0]
if (actorData.system.meta.geschlecht === "männlich") {
context.spezies = speciesData.system.masculineDemonym
} else {
context.spezies = speciesData.system.feminineDemonym
}
}
context.kultur = ""
if (actorData.itemTypes["Culture"]?.[0]) {
const cultureData = actorData.itemTypes["Culture"][0]
if (actorData.system.geschlecht === "männlich") {
context.kultur = cultureData.system.masculineDemonym
} else {
context.kultur = cultureData.system.feminineDemonym
}
}
context.originalName = actorData.name
context.name = context.derived.name ?? actorData.name
context.img = actorData.img
context.effects = actorData.effects ?? []
context.maxWounds = actorData.system.wunden.max ?? 3
context.wounds = actorData.system.wunden.gesamt ?? 0
context.woundsFilled = []
for (let i = 1; i <= context.maxWounds; i++) {
context.woundsFilled[i] = i <= context.wounds
}
context.zonenruestung = game.settings.get("DSA_4-1", "optional_ruestungzonen")
context.trefferzonen = game.settings.get("DSA_4-1", "optional_trefferzonen")
context.ausdauer = game.settings.get("DSA_4-1", "optional_ausdauer")
context.colorfulDice = game.settings.get('DSA_4-1', 'optional_colorfuldice')
context.inidice = actorData.system.ini.wuerfel
context.inivalue = actorData.system.ini.aktuell
context.inimod = actorData.system.ini.mod
context.aupper = Math.min((actorData.system.aup.aktuell / actorData.system.aup.max) * 100, 100)
context.lepper = Math.min((actorData.system.lep.aktuell / actorData.system.lep.max) * 100, 100)
context.keper = Math.min((actorData.system.kap.aktuell / actorData.system.kap.max) * 100, 100)
context.aspper = Math.min((actorData.system.asp.aktuell / actorData.system.asp.max) * 100, 100)
context.lepcurrent = actorData.system.lep.aktuell ?? 0
context.aupcurrent = actorData.system.aup.aktuell ?? 0
context.aspcurrent = actorData.system.asp.aktuell ?? 0
context.kapcurrent = actorData.system.kap.aktuell ?? 0
const fernkampf = actorData.findEquipmentOnSlot("fernkampf", actorData.system.setEquipped, actorData)
const links = actorData.findEquipmentOnSlot("links", actorData.system.setEquipped, actorData)
const rechts = actorData.findEquipmentOnSlot("rechts", actorData.system.setEquipped, actorData)
context.attacks = [];
if (fernkampf) {
const fkitems = fernkampf.system.rangedSkills.map((skillInQuestion) => actorData.items.find(p => p.name === skillInQuestion))
fkitems.forEach(skill => {
context.attacks.push({
name: skill.name,
id: fernkampf._id,
skillId: skill._id,
using: fernkampf.name,
isRanged: true,
at: `${this.document.system.fk.aktuell + skill.system.at}`,
tp: `${fernkampf.system.rangedAttackDamage}`,
ini: `${context.inidice}w6 + ${context.inivalue + fernkampf.system.iniModifier ?? 0}`,
})
})
}
if (links) {
const meitems = []
links.system.meleeSkills.forEach((skillInQuestion) => {
const item = actorData.items.find(p => p.name === skillInQuestion)
if (item) {
meitems.push(item)
}
})
meitems.forEach(skill => {
const obj = skill
context.attacks.push({
name: obj.name,
id: links._id,
skillId: skill._id,
using: links.name,
isRanged: false,
at: `${this.document.system.at.links.aktuell + obj.system.at + links.system.attackModifier}`,
pa: `${this.document.system.pa.links.aktuell + obj.system.pa + links.system.parryModifier}`,
tp: `${links.system.meleeAttackDamage}`,
ini: `${context.inidice}w6 + ${context.inivalue + links.system.iniModifier ?? 0}`,
})
})
}
if (rechts) {
const meitems = []
rechts.system.meleeSkills.forEach((skillInQuestion) => {
const item = actorData.items.find(p => p.name === skillInQuestion)
if (item) {
meitems.push(item)
}
})
meitems.forEach(skill => {
const obj = skill
context.attacks.push({
name: obj.name,
id: rechts._id,
skillId: skill._id,
using: rechts.name,
isRanged: false,
at: `${this.document.system.at.rechts.aktuell + obj.system.at + rechts.system.attackModifier}`,
pa: `${this.document.system.pa.rechts.aktuell + obj.system.pa + rechts.system.parryModifier}`,
tp: `${rechts.system.meleeAttackDamage}`,
ini: `${context.inidice}w6 + ${context.inivalue + rechts.system.iniModifier ?? 0}`,
})
})
}
context.cooldowns = actorData.system.cooldowns ?? []
context.cooldowns.forEach(cooldown => {
let weapon = null
let target = null
let tooltip = cooldown.data.title
if (cooldown.data.weapon) {
weapon = this.document.itemTypes["Equipment"].find(p => p._id === cooldown.data.weapon)
tooltip += `<br/>Waffe: ${weapon.name}`
}
if (cooldown.data.target) {
target = game.actors.get(game.scenes.current.tokens.find(p => p._id === cooldown.data.target).actorId)
tooltip += `<br/>Ziel: ${target.name}`
}
cooldown.title = cooldown.data.title
cooldown.progress = ((cooldown.current / cooldown.start) * 100) + "%"
if (cooldown.start - cooldown.current > 0) {
cooldown.tooltip = tooltip + `<br/>Aktionen verbleibend: ${cooldown.start - cooldown.current}<hr/><i class="fa-solid fa-computer-mouse"></i>: 1 Aktion aufwenden`
} else {
cooldown.tooltip = tooltip + `<br/>Aktionen verbleibend: ${cooldown.start - cooldown.current}<hr/><i class="fa-solid fa-computer-mouse"></i>: Aktion durchführen`
}
})
context.hasSpells = actorData.itemTypes["Spell"].length > 0
context.hasLiturgies = actorData.itemTypes["Liturgy"].length > 0
context.attributes = [
{
eigenschaft: "mu",
name: "MU",
tooltip: "Mut",
wert: context.derived.attribute.mu.aktuell ?? 0,
},
{
eigenschaft: "kl",
name: "KL",
tooltip: "Klugheit",
wert: context.derived.attribute.kl.aktuell ?? 0,
},
{
eigenschaft: "in",
name: "IN",
tooltip: "Intuition",
wert: context.derived.attribute.in.aktuell ?? 0,
},
{
eigenschaft: "ch",
name: "CH",
tooltip: "Charisma",
wert: context.derived.attribute.ch.aktuell ?? 0,
},
{
eigenschaft: "ff",
name: "FF",
tooltip: "Fingerfertigkeit",
wert: context.derived.attribute.ff.aktuell ?? 0,
},
{
eigenschaft: "ge",
name: "GE",
tooltip: "Gewandtheit",
wert: context.derived.attribute.ge.aktuell ?? 0,
},
{
eigenschaft: "ko",
name: "KO",
tooltip: "Konstitution",
wert: context.derived.attribute.ko.aktuell ?? 0,
},
{
eigenschaft: "kk",
name: "KK",
tooltip: "Körperkraft",
wert: context.derived.attribute.kk.aktuell ?? 0,
},
]
break;
case "meta":
await Meta._prepareContext(context, this.document)
break
case "social":
await Social._prepareContext(context, this.document)
break
case "advsf":
await Advsf._prepareContext(context, this.document)
break
case "combat":
await Combat._prepareContext(context, this.document)
break
case "equipment":
await Equipment._prepareContext(context, this.document, this)
break
case "skills":
await Skills._prepareContext(context, this.document)
break
case "spells":
await Spells._prepareContext(context, this.document)
break
case "liturgies":
await Liturgies._prepareContext(context, this.document)
break
case "effects":
await Effects._prepareContext(context, this.document)
break
}
return context
}
_onRender(context, options) {
Meta._onRender(context, options, this.element)
Social._onRender(context, options, this.element)
Advsf._onRender(context, options, this)
Combat._onRender(context, options, this.element)
Effects._onRender(context, options, this.element)
Equipment._onRender(context, options, this)
Liturgies._onRender(context, options, this.element)
Skills._onRender(context, options, this.element)
Spells._onRender(context, options, this.element)
}
async _canDragDrop() {
return true
}
async _onDrop(event) {
const data = TextEditor.implementation.getDragEventData(event)
const targetDocument = this.actor.itemTypes["Equipment"].find(p => p._id === event.target.dataset['itemId'])
// Dropped Documents
const documentClass = foundry.utils.getDocumentClass(data.type)
if (documentClass) {
const document = await documentClass.fromDropData(data)
if (document.type === "Equipment" || document.type === "Advantage" || document.type === "Spell" || document.type === "Liturgy" || document.type === "ActiveEffect" || document.type === "SpecialAbility") {
// No duplication by moving items from one actor to another
if ((targetDocument?.name ?? false) === document.name && targetDocument._id !== document._id && await foundry.applications.api.DialogV2.confirm({
content: `<span>Gegenstände der Art <strong>${document.name}</strong> (Neue Anzahl: ${targetDocument.system.quantity + document.system.quantity}) zusammenlegen?</span>`,
rejectClose: false,
modal: true,
window: {
title: `Gegenstände zusammenlegen`
}
})) {
// combine
await targetDocument.update({"system.quantity": targetDocument.system.quantity + document.system.quantity})
await this.actor.deleteEmbeddedDocuments('Item', [document._id])
return false
} else {
if (document.parent && document.parent !== this.actor) {
document.parent.items.get(document._id).delete()
}
await this._onDropDocument(event, document)
}
}
}
}
}
export default CharacterSheet