WIP group sheet

feature/applicationv2
macniel 2025-10-16 00:06:50 +02:00
parent 9984db4ca6
commit 5e41285b1c
7 changed files with 252 additions and 254 deletions

View File

@ -18,6 +18,7 @@ export class GroupDataModel extends foundry.abstract.TypeDataModel {
quantity: new NumberField(),
item: new DocumentIdField(Item)
}),
groupId: new DocumentIdField(Actor),
characters: new ArrayField(
new DocumentIdField(Actor)
),
@ -25,4 +26,13 @@ export class GroupDataModel extends foundry.abstract.TypeDataModel {
}
}
_onCreate(data, options, userId) {
super._onCreate(data, options, userId);
Folder.implementation.createDocuments([{name: data.name, type: "Actor"}]).then((
folder
) => {
this.parent.update({"system.groupId": folder[0]._id});
})
}
}

View File

@ -45,10 +45,11 @@ export class SkillDataModel extends BaseItem {
/**
* Handle clickable rolls.
* @param {Event} event The originating click event
* @param {RollMode} rollMode
* @private
*/
async roll() {
async roll(rollMode = null) {
rollMode = rollMode ?? game.settings.get('core', 'rollMode');
const owner = this.parent.parent
let roll1 = new Roll("3d20", owner.getRollData());
@ -63,13 +64,13 @@ export class SkillDataModel extends BaseItem {
evaluated1.toMessage({
speaker: ChatMessage.getSpeaker({actor: owner}),
flavor: ` ${dsaDieRollEvaluated.meisterlich ? 'Meisterlich geschafft' : 'Geschafft'} mit ${dsaDieRollEvaluated.tap} Punkten übrig`,
rollMode: game.settings.get('core', 'rollMode'),
rollMode,
})
} else { // misserfolg
evaluated1.toMessage({
speaker: ChatMessage.getSpeaker({actor: owner}),
flavor: ` ${dsaDieRollEvaluated.meisterlich ? 'Gepatzt' : ''} mit ${Math.abs(dsaDieRollEvaluated.tap)} Punkten daneben`,
rollMode: game.settings.get('core', 'rollMode'),
rollMode,
})
}
}

View File

@ -1,19 +1,51 @@
export class GroupSheet extends foundry.appv1.sheets.ActorSheet {
/**@override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
const {HandlebarsApplicationMixin} = foundry.applications.api
const {ActorSheetV2} = foundry.applications.sheets
export class GroupSheet extends HandlebarsApplicationMixin(ActorSheetV2) {
/** @inheritDoc */
static DEFAULT_OPTIONS = {
position: {width: 520, height: 480},
classes: ['dsa41', 'sheet', 'actor', 'group'],
width: 520,
height: 480,
tabs: [
{
navSelector: '.sheet-tabs',
contentSelector: '.sheet-body',
initial: 'description',
tag: 'form',
form: {
submitOnChange: true,
closeOnSubmit: false,
handler: GroupSheet.#onSubmitForm
},
],
});
actions: {
roll: GroupSheet.#dieRoll,
editImage: ActorSheetV2.DEFAULT_OPTIONS.actions.editImage,
openEmbeddedDocument: GroupSheet.#openEmbeddedDocument,
openActorDocument: GroupSheet.#openActorDocument,
removeFromParty: GroupSheet.#removeFromParty,
}
}
static TABS = {
sheet: {
tabs: [
{id: 'members', group: 'sheet', label: 'Gruppenmitglieder'},
{id: 'inventory', group: 'sheet', label: 'Gruppeninventar'},
// settings is only added on context
],
initial: 'members'
}
}
/** @inheritDoc */
static PARTS = {
form: {
template: `systems/DSA_4-1/templates/actor/group/main-sheet.hbs`
},
members: {
template: `systems/DSA_4-1/templates/actor/group/tab-members.hbs`
},
inventory: {
template: `systems/DSA_4-1/templates/actor/group/tab-inventory.hbs`
},
}
static async onDroppedData(group, sheet, data) {
if (data.type === "Actor") {
@ -63,17 +95,52 @@ export class GroupSheet extends foundry.appv1.sheets.ActorSheet {
return s
}
/** @override */
get template() {
return `systems/DSA_4-1/templates/actor/group-sheet.hbs`;
static async #openEmbeddedDocument(evt) {
const {documentId} = evt.srcElement.dataset
this.document.items.get(documentId).sheet.render(true)
}
static async #openActorDocument(evt) {
const {id} = evt.srcElement.dataset
evt.stopPropagation()
game.actors.get(id).sheet.render(true)
}
static async #dieRoll(evt) {
console.log(evt)
}
static async #removeFromParty(evt) {
const dataset = evt.srcElement.dataset;
const group = this.document;
const charactersWithoutMember = group.system.characters.filter(id => id !== dataset.id)
group.update({
system: {
characters: charactersWithoutMember
}
})
}
/**
* 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) // Note: formData.object
}
/** @override */
async getData() {
const context = super.getData();
async _prepareContext(options) {
const context = await super._prepareContext(options)
// Use a safe clone of the actor data for further operations.
const groupData = context.data;
const groupData = context.document;
// Add the actor's data to context.data for easier access, as well as flags.
context.system = groupData.system;
@ -101,8 +168,11 @@ export class GroupSheet extends foundry.appv1.sheets.ActorSheet {
}
}
for (const characterId of groupData.system.characters) {
const character = await game.actors.get(characterId)
// TODO hook on changes in the given folder
const characters = await game.folders.get(groupData.system.groupId).contents
console.log(characters)
for (const character of characters) {
character.items.filter((i) => i.type === "Advantage").filter((i) => hiddenFields.includes(this.#stringToKeyFieldName(i.name))).map((advantage) => {
const n = this.#stringToKeyFieldName(advantage.name)
@ -154,7 +224,7 @@ export class GroupSheet extends foundry.appv1.sheets.ActorSheet {
}
context.equipments = [];
const actorData = context.data;
const actorData = context.document;
Object.values(actorData.items).forEach((item, index) => {
if (item.type === "Equipment") {
context.equipments.push({
@ -173,110 +243,8 @@ export class GroupSheet extends foundry.appv1.sheets.ActorSheet {
return await context;
}
openEmbeddedDocument(documentId) {
this.object.items.get(documentId).sheet.render(true)
}
_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,
}
}
async _onTalentRoll(event) {
event.preventDefault();
const dataset = event.currentTarget.dataset;
const actor = await game.actors.get(dataset.actorId);
console.log(dataset, actor)
if (dataset.rolleigenschaft1) {
let roll1 = new Roll("3d20", 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: actor}),
flavor: ` ${dsaDieRollEvaluated.meisterlich ? 'Meisterlich geschafft' : 'Geschafft'} mit ${dsaDieRollEvaluated.tap} Punkten übrig`,
}, {rollMode: "gmroll"})
} else { // misserfolg
evaluated1.toMessage({
speaker: ChatMessage.getSpeaker({actor: actor}),
flavor: ` ${dsaDieRollEvaluated.meisterlich ? 'Gepatzt' : ''} mit ${Math.abs(dsaDieRollEvaluated.tap)} Punkten daneben`,
}, {rollMode: "gmroll"})
}
}
}
activateListeners(html) {
super.activateListeners(html);
html.on('click', '.owneroption.clickable', async (evt) => {
const dataset = evt.target.dataset;
const group = this.object;
switch (dataset.operation) {
case "removeFromParty":
const charactersWithoutMember = group.system.characters.filter(id => id !== dataset.id)
await group.update({
system: {
characters: charactersWithoutMember
}
})
}
});
html.on('click', '.header.clickable', async (evt) => {
const {id, operation} = evt.currentTarget.dataset;
if (operation === "openActorSheet") {
evt.stopPropagation();
(await game.actors.get(id)).sheet.render(true);
}
})
html.on('click', '.equipment', (evt) => {
this.openEmbeddedDocument(evt.target.dataset.id);
evt.stopPropagation();
})
html.on('click', ".rollable", (evt) => {
this._onTalentRoll(evt)
evt.stopPropagation()
})
new ContextMenu(html, '.equipment', [
_onRender(context, options) {
new ContextMenu(this.element, ".equipment", [
{
name: "Aus dem Gruppeninventar entfernen",
icon: '<i class="fa-solid fa-trash"></i>',
@ -285,6 +253,9 @@ export class GroupSheet extends foundry.appv1.sheets.ActorSheet {
},
condition: () => true
}
]);
], {
jQuery: false
});
}
}

View File

@ -1,121 +0,0 @@
<form class="{{cssClass}} {{actor.type}} flexcol" autocomplete="off">
{{!-- Sheet Header --}}
<header class="sheet-header">
<input class="sheet-name" type="text" name="actor.name" value="{{actor.name}}"/>
</header>
{{!-- Sheet Tab Navigation --}}
<nav class="sheet-tabs tabs" data-group="primary">
<a class="item" data-tab="characters">Charaktere</a>
<a class="item" data-tab="inventory">Inventar</a>
{{#if owner}}<a class="item" data-tab="settings">Sichtbarkeiten</a>{{/if}}
</nav>
{{!-- Sheet Body --}}
<section class="sheet-body">
<div class="tab characters" data-group="primary" data-tab="characters">
{{#unless isGM}}
<div class="characters-overview minimal">
{{#each characters}}
{{#if this.isVisible}}
<div class="character">
<div class="header clickable" data-id="{{this.id}}" data-operation="openActorSheet">
<img class="profile-img" src="{{this.img}}" style="object-fit: cover;"
title="{{this.name}}" alt="{{this.name}}"/>
<span class="name">{{this.name}}</span>
</div>
</div>
{{/if}}
{{/each}}
</div>
{{else}}
<div class="characters-overview">
<table>
<thead>
<tr>
<th></th>
{{#each this.fields.head}}
<th>
<div class="character">
<div class="header clickable" data-id="{{this.id}}"
data-operation="openActorSheet">
<img class="profile-img" src="{{this.img}}" style="object-fit: cover;"
title="{{this.name}}" alt="{{this.name}}"/>
<span class="name">{{this.name}}</span>
</div>
{{#if this.isLimited}}
<div class="mini-attributes">
{{#each this.attributes}}
<div class="mini-attribute" data-actor-id="{{actor.id}}"
data-attribute="{{this.name}}" data-roll-data="{{this.value}}">
<span class="value">{{this.value}}</span>
<span class="name">{{this.name}}</span>
</div>
{{/each}}
</div>
{{/if}}
</div>
</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each this.fields}}
{{#unless (eq @key "head")}}
<tr>
<th>
{{ @key}}
</th>
{{#each this}}
<td>
{{#if (eq this "-")}}
{{else}}
{{#if this.taw}}
<div class="rollable" data-actor-id="{{this.actor}}"
data-taw="{{this.taw}}"
data-name="{{this.name}}"
data-eigenschaft1="{{this.eigenschaft1}}"
data-eigenschaft2="{{this.eigenschaft2}}"
data-eigenschaft3="{{this.eigenschaft3}}"
data-rollEigenschaft1="{{this.rollEigenschaft1}}"
data-rollEigenschaft2="{{this.rollEigenschaft2}}"
data-rollEigenschaft3="{{this.rollEigenschaft3}}">
{{this.taw}} ({{this.eigenschaft1}}, {{this.eigenschaft2}}
, {{this.eigenschaft3}})
</div>
{{else}}
Ja
{{/if}}
{{/if}}
</td>
{{/each}}
</tr>
{{/unless}}
{{/each}}
</tbody>
</table>
</div>
{{/unless}}
</div>
<div class="tab inventory" data-group="primary" data-tab="inventory">
{{> 'systems/DSA_4-1/templates/ui/partial-equipment-group-button.hbs' equipments}}
</div>
{{#if owner}}
<div class="tab settings" data-group="primary" data-tab="settings">
{{#each settings}}
<div>
<label><input name="system.settings.{{@key}}" type="checkbox" {{checked this}}> {{@key}}</label>
</div>
{{/each}}
</div>
{{/if}}
</section>
</form>

View File

@ -0,0 +1,20 @@
<div>
<header class="sheet-header">
<input class="sheet-name" type="text" name="name" value="{{name}}"/>
</header>
{{!-- Sheet Tab Navigation --}}
<nav class="sheet-tabs tabs{{#if verticalTabs}} vertical{{/if}}"
aria-roledescription="{{localize "SHEETS.FormNavLabel"}}">
{{#each tabs as |tab|}}
<a data-action="tab" data-group="{{tab.group}}" data-tab="{{tab.id}}"
{{#if tab.cssClass}}class="{{tab.cssClass}}"{{/if}}
{{#if tab.tooltip}}data-tooltip="{{tab.tooltip}}"{{/if}}>
{{#if tab.icon}}<i class="{{tab.icon}}" inert></i>{{/if}}
{{#if tab.label}}<span>{{localize tab.label}}</span>{{/if}}
</a>
{{/each}}
</nav>
</div>

View File

@ -0,0 +1,28 @@
<section class="tab {{tabs.inventory.id}} {{tabs.inventory.cssClass}}"
data-tab="{{tabs.inventory.id}}"
data-group="{{tabs.inventory.group}}">
<table class="inventory-table">
<thead>
<tr>
<th colspan="2"></th>
<th>Anzahl</th>
<th>Gewicht</th>
</tr>
</thead>
<tbody>
{{#each inventoryItems}}
<tr class="equipment" data-id="{{this.id}}" draggable="true">
<td class="icon"><img alt="" src="{{this.icon}}" width="16" height="16"></td>
<td class="name">{{this.name}}</td>
<td class="quantity">{{this.quantity}}</td>
<td class="weight">{{this.weight}}</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
</section>

View File

@ -0,0 +1,89 @@
<section class="tab {{tabs.members.id}} {{tabs.members.cssClass}}"
data-tab="{{tabs.members.id}}"
data-group="{{tabs.members.group}}">
{{#unless isGM}}
<div class="characters-overview minimal">
{{#each this.characters}}
{{#if this.isVisible}}
<div class="character">
<div class="header clickable" data-id="{{this.id}}" data-operation="openActorSheet">
<img class="profile-img" src="{{this.img}}" style="object-fit: cover;"
title="{{this.name}}" alt="{{this.name}}"/>
<span class="name">{{this.name}}</span>
</div>
</div>
{{/if}}
{{/each}}
</div>
{{else}}
<div class="characters-overview">
<table>
<thead>
<tr>
<th></th>
{{#each this.fields.head}}
<th>
<div class="character">
<div class="header clickable" data-id="{{this.id}}"
data-operation="openActorSheet">
<img class="profile-img" src="{{this.img}}" style="object-fit: cover;"
title="{{this.name}}" alt="{{this.name}}"/>
<span class="name">{{this.name}}</span>
</div>
{{#if this.isLimited}}
<div class="mini-attributes">
{{#each this.attributes}}
<div class="mini-attribute" data-actor-id="{{actor.id}}"
data-attribute="{{this.name}}" data-roll-data="{{this.value}}">
<span class="value">{{this.value}}</span>
<span class="name">{{this.name}}</span>
</div>
{{/each}}
</div>
{{/if}}
</div>
</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each this.fields}}
{{#unless (eq @key "head")}}
<tr>
<th>
{{ @key}}
</th>
{{#each this}}
<td>
{{#if (eq this "-")}}
{{else}}
{{#if this.taw}}
<div class="rollable" data-actor-id="{{this.actor}}"
data-taw="{{this.taw}}"
data-name="{{this.name}}"
data-eigenschaft1="{{this.eigenschaft1}}"
data-eigenschaft2="{{this.eigenschaft2}}"
data-eigenschaft3="{{this.eigenschaft3}}"
data-rollEigenschaft1="{{this.rollEigenschaft1}}"
data-rollEigenschaft2="{{this.rollEigenschaft2}}"
data-rollEigenschaft3="{{this.rollEigenschaft3}}">
{{this.taw}} ({{this.eigenschaft1}}, {{this.eigenschaft2}}
, {{this.eigenschaft3}})
</div>
{{else}}
Ja
{{/if}}
{{/if}}
</td>
{{/each}}
</tr>
{{/unless}}
{{/each}}
</tbody>
</table>
</div>
{{/unless}}
</section>