Functioning setup

This commit is contained in:
WBHarry 2026-04-18 15:02:41 +02:00
parent 4b92001f97
commit 0cb7ede933
24 changed files with 350 additions and 72 deletions

View file

@ -32,6 +32,11 @@ CONFIG.Dice.daggerheart = {
FateRoll: FateRoll
};
CONFIG.RegionBehavior.dataModels = {
...CONFIG.RegionBehavior.dataModels,
...data.regionBehaviors
};
Object.assign(CONFIG.Dice.termTypes, dice.diceTypes);
CONFIG.Actor.documentClass = documents.DhpActor;

View file

@ -109,6 +109,12 @@
"customFormula": "Custom Formula",
"formula": "Formula"
},
"area": {
"sectionTitle": "Areas",
"shape": "Shape",
"size": "Size"
},
"displayInChat": "Display in chat",
"deleteTriggerTitle": "Delete Trigger",
"deleteTriggerContent": "Are you sure you want to delete the {trigger} trigger?",
@ -2433,6 +2439,7 @@
"maxWithThing": "Max {thing}",
"missingDragDropThing": "Drop {thing} here",
"multiclass": "Multiclass",
"name": "Name",
"newCategory": "New Category",
"newThing": "New {thing}",
"next": "Next",

View file

@ -19,7 +19,8 @@ export default class DHActionConfig extends DHActionBaseConfig {
return context;
}
static async addEffect(_event) {
static async addEffect(event) {
const { areaIndex } = event.target.dataset;
if (!this.action.effects) return;
const data = this.action.toObject();
@ -27,7 +28,10 @@ export default class DHActionConfig extends DHActionBaseConfig {
game.system.api.data.activeEffects.BaseEffect.getDefaultObject()
]);
data.effects.push({ _id: created[0]._id });
if (areaIndex !== undefined)
data.area[areaIndex].effects.push(created[0]._id);
else
data.effects.push({ _id: created[0]._id });
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
this.action.item.effects.get(created[0]._id).sheet.render(true);
}
@ -52,9 +56,20 @@ export default class DHActionConfig extends DHActionBaseConfig {
static removeEffect(event, button) {
if (!this.action.effects) return;
const index = button.dataset.index,
const { areaIndex, index } = button.dataset;
let effectId = null;
if (areaIndex !== undefined) {
effectId = this.action.area[areaIndex].effects[index];
const data = this.action.toObject();
data.area[areaIndex].effects.splice(index, 1);
this.constructor.updateForm.bind(this)(null, null, { object: foundry.utils.flattenObject(data) });
}
else {
effectId = this.action.effects[index]._id;
this.constructor.removeElement.bind(this)(event, button);
this.constructor.removeElement.bind(this)(event, button);
}
this.action.item.deleteEmbeddedDocuments('ActiveEffect', [effectId]);
}

View file

@ -95,4 +95,48 @@ export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer {
});
return inBounds.length === 1 ? inBounds[0] : null;
}
static getTemplateShape({ type, angle, range, direction } = {}) {
const { line, rectangle, inFront, cone } = CONFIG.DH.GENERAL.templateTypes;
const usedAngle =
type === cone.id ? (angle ?? CONFIG.MeasuredTemplate.defaults.angle) : type === inFront.id ? '180' : undefined;
const { grid, distance } = CONFIG.Scene.documentClass.schema.fields.grid.fields;
const sceneGridSize = canvas.scene?.grid.size ?? grid.size.initial;
const sceneGridDistance = canvas.scene?.grid.distance ?? distance.getInitialValue();
const dimensionConstant = sceneGridSize / sceneGridDistance;
const rangeNumber = Number(range);
const settings = canvas.scene?.rangeSettings;
const baseDistance = (!Number.isNaN(rangeNumber) ? rangeNumber : (settings ? settings[range] : 0)) * dimensionConstant;
const length = baseDistance;
const radius = length;
const shapeWidth = type === line.id ? 5 * dimensionConstant : type === rectangle.id ? length : undefined;
const shapeType = type === inFront.id ? cone.id : type;
const { width, height } = game.canvas.scene.dimensions;
return {
x: width / 2,
y: height / 2,
base: {
type: 'token',
x: 0,
y: 0,
width: 1,
height: 1,
shape: game.canvas.grid.isHexagonal ? CONST.TOKEN_SHAPES.ELLIPSE_1 : CONST.TOKEN_SHAPES.RECTANGLE_1
},
t: shapeType,
length: length,
width: shapeWidth,
height: length,
angle: usedAngle,
radius: radius,
direction: direction ?? 0,
type: shapeType
};
}
}

View file

@ -115,3 +115,10 @@ export const advantageState = {
value: 1
}
};
export const areaTypes = {
placed: {
id: 'placed',
label: 'Placed Area'
}
};

View file

@ -80,12 +80,30 @@ export const groupAttackRange = {
/* circle|cone|rect|ray used to be CONST.MEASURED_TEMPLATE_TYPES. Hardcoded for now */
export const templateTypes = {
CIRCLE: 'circle',
CONE: 'cone',
RECTANGLE: 'rectangle',
LINE: 'line',
EMANATION: 'emanation',
INFRONT: 'inFront'
circle: {
id: 'circle',
label: 'Circle'
},
cone: {
id: 'cone',
label: 'Cone'
},
rectangle: {
id: 'rectangle',
label: 'Rectangle'
},
line: {
id: 'line',
label: 'Line'
},
emanation: {
id: 'emanation',
label: 'Emanation'
},
inFront: {
id: 'inFront',
label: 'In Front'
}
};
export const rangeInclusion = {
@ -1092,3 +1110,18 @@ export const fallAndCollisionDamage = {
damageFormula: '1d20 + 5'
}
};
export const simpleDispositions = {
[-1]: {
id: -1,
label: 'TOKEN.DISPOSITION.HOSTILE'
},
[0]: {
id: 0,
label: 'TOKEN.DISPOSITION.NEUTRAL'
},
[1]: {
id: 1,
label: 'TOKEN.DISPOSITION.FRIENDLY'
}
};

View file

@ -15,3 +15,4 @@ export * as chatMessages from './chat-message/_modules.mjs';
export * as fields from './fields/_module.mjs';
export * as items from './item/_module.mjs';
export * as scenes from './scene/_module.mjs';
export * as regionBehaviors from './regionBehavior/_module.mjs';

View file

@ -15,7 +15,7 @@ const fields = foundry.data.fields;
*/
export default class DHBaseAction extends ActionMixin(foundry.abstract.DataModel) {
static extraSchemas = ['cost', 'uses', 'range'];
static extraSchemas = ['area', 'cost', 'uses', 'range'];
/** @inheritDoc */
static defineSchema() {

View file

@ -93,7 +93,10 @@ export default class BaseEffect extends foundry.data.ActiveEffectTypeDataModel {
max: new fields.NumberField({ integer: true, label: 'DAGGERHEART.GENERAL.max' })
},
{ nullable: true, initial: null }
)
),
targetDispositions: new fields.SetField(new fields.NumberField({
choices: CONFIG.DH.GENERAL.simpleDispositions,
}), { label: 'Affected Dispositions' }),
};
}

View file

@ -1,3 +1,4 @@
export { default as AreaField } from './areaField.mjs';
export { default as CostField } from './costField.mjs';
export { default as CountdownField } from './countdownField.mjs';
export { default as UsesField } from './usesField.mjs';

View file

@ -0,0 +1,35 @@
const fields = foundry.data.fields;
export default class AreaField extends fields.ArrayField {
/**
* Action Workflow order
*/
static order = 150;
/** @inheritDoc */
constructor(options = {}, context = {}) {
const element = new fields.SchemaField({
type: new fields.StringField({
nullable: false,
choices: CONFIG.DH.ACTIONS.areaTypes,
initial: CONFIG.DH.ACTIONS.areaTypes.placed.id,
label: 'DAGGERHEART.GENERAL.type'
}),
shape: new fields.StringField({
nullable: false,
choices: CONFIG.DH.GENERAL.templateTypes,
initial: CONFIG.DH.GENERAL.templateTypes.circle.id,
label: 'DAGGERHEART.ACTIONS.Config.area.shape'
}),
/* Could be opened up to allow numbers to be input aswell. Probably best handled via an autocomplete in that case to allow the select options but also free text */
size: new fields.StringField({
nullable: false,
choices: CONFIG.DH.GENERAL.range,
initial: CONFIG.DH.GENERAL.range.veryClose.id,
label: 'DAGGERHEART.ACTIONS.Config.area.size'
}),
effects: new fields.ArrayField(new fields.DocumentIdField()),
});
super(element, options, context);
}
}

View file

@ -0,0 +1 @@
export { default as applyActiveEffect } from './applyActiveEffect.mjs';

View file

@ -0,0 +1,38 @@
export default class DhApplyActiveEffect extends CONFIG.RegionBehavior.dataModels.applyActiveEffect {
static async #getApplicableEffects(token) {
const effects = await Promise.all(this.effects.map(fromUuid));
return (effects).filter(effect => !effect.system.targetDispositions.size || effect.system.targetDispositions.has(token.disposition));
}
static async #onTokenEnter(event) {
if ( !event.user.isSelf ) return;
const {token, movement} = event.data;
const actor = token.actor;
if ( !actor ) return;
const resumeMovement = movement ? token.pauseMovement() : undefined;
const effects = await DhApplyActiveEffect.#getApplicableEffects.bind(this)(event.data.token);
const toCreate = [];
for ( const effect of effects ) {
const data = effect.toObject();
delete data._id;
if ( effect.compendium ) {
data._stats.duplicateSource = null;
data._stats.compendiumSource = effect.uuid;
} else {
data._stats.duplicateSource = effect.uuid;
data._stats.compendiumSource = null;
}
data._stats.exportSource = null;
data.origin = this.parent.uuid;
toCreate.push(data);
}
if ( toCreate.length ) await actor.createEmbeddedDocuments("ActiveEffect", toCreate);
await resumeMovement?.();
}
/** @override */
static events = {
...CONFIG.RegionBehavior.dataModels.applyActiveEffect.events,
[CONST.REGION_EVENTS.TOKEN_ENTER]: this.#onTokenEnter,
};
}

View file

@ -145,6 +145,7 @@ export default class DHRoll extends Roll {
roll: this,
parent: chatData.parent,
targetMode: chatData.targetMode,
areas: chatData.action?.area,
metagamingSettings
});
}

View file

@ -137,6 +137,10 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
element.addEventListener('click', this.onApplyEffect.bind(this))
);
html.querySelectorAll('.action-areas').forEach(element =>
element.addEventListener('click', this.onCreateAreas.bind(this))
);
html.querySelectorAll('.roll-target').forEach(element => {
element.addEventListener('mouseenter', this.hoverTarget);
element.addEventListener('mouseleave', this.unhoverTarget);
@ -249,6 +253,73 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage {
this.system.action?.workflow.get('effects')?.execute(config, targets, true);
}
async onCreateAreas(event) {
let selectedArea = null;
if (this.system.action.area.length === 1)
selectedArea = this.system.action.area[0];
else if(this.system.action.area.length > 1) {
/* Pop a selection. Possibly a context menu? */
new foundry.applications.ux.ContextMenu.implementation(
event.target,
'.scene-environment',
this.system.action.area.map((area, index) => ({
name: index,
callback: () => {
if (scene.flags.daggerheart.sceneEnvironments[0] !== environment.uuid) {
const newEnvironments = scene.flags.daggerheart.sceneEnvironments;
const newFirst = newEnvironments.splice(
newEnvironments.findIndex(x => x === environment.uuid),
1
)[0];
newEnvironments.unshift(newFirst);
emitAsGM(
GMUpdateEvent.UpdateDocument,
scene.update.bind(scene),
{ 'flags.daggerheart.sceneEnvironments': newEnvironments },
scene.uuid
);
}
environment.sheet.render({ force: true });
}
})),
{
jQuery: false,
fixed: true
}
);
CONFIG.ux.ContextMenu.triggerContextMenu(event, '.scene-environment');
}
if(!selectedArea) return;
const effects = selectedArea.effects.map(effect => this.system.action.item.effects.get(effect).uuid);
const { shape: type, size: range } = this.system.action.area[0];
const shapeData = CONFIG.Canvas.layers.regions.layerClass.getTemplateShape({ type, range });
await canvas.regions.placeRegion(
{
name: 'Test',
shapes: [shapeData],
restriction: { enabled: false, type: 'move', priority: 0 },
behaviors: [{
name: game.i18n.localize('TYPES.RegionBehavior.applyActiveEffect'),
type: 'applyActiveEffect',
system: {
effects: effects
}
}],
displayMeasurements: true,
locked: false,
ownership: { default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE },
visibility: CONST.REGION_VISIBILITY.ALWAYS
},
{ create: true }
);
}
filterPermTargets(targets) {
return targets.filter(t => fromUuidSync(t.actorId)?.canUserModify(game.user, 'update'));
}

View file

@ -12,7 +12,7 @@ export default function DhTemplateEnricher(match, _options) {
)?.id
: params.range;
if (!Object.values(CONFIG.DH.GENERAL.templateTypes).find(x => x === type) || !range) return match[0];
if (!CONFIG.DH.GENERAL.templateTypes[type] || !range) return match[0];
const label = game.i18n.localize(`DAGGERHEART.CONFIG.TemplateTypes.${type}`);
const rangeDisplay = Number.isNaN(Number(range))
@ -49,8 +49,6 @@ export default function DhTemplateEnricher(match, _options) {
}
export const renderMeasuredTemplate = async event => {
const { LINE, RECTANGLE, INFRONT, CONE } = CONFIG.DH.GENERAL.templateTypes;
const button = event.currentTarget,
type = button.dataset.type,
range = button.dataset.range,
@ -59,49 +57,16 @@ export const renderMeasuredTemplate = async event => {
if (!type || !range || !game.canvas.scene) return;
const usedType = type === 'inFront' ? 'cone' : type;
const usedAngle =
type === CONE ? (angle ?? CONFIG.MeasuredTemplate.defaults.angle) : type === INFRONT ? '180' : undefined;
let baseDistance = getTemplateDistance(range);
const { grid, distance } = CONFIG.Scene.documentClass.schema.fields.grid.fields;
const sceneGridSize = canvas.scene?.grid.size ?? grid.size.initial;
const sceneGridDistance = canvas.scene?.grid.distance ?? distance.getInitialValue();
const dimensionConstant = sceneGridSize / sceneGridDistance;
baseDistance *= dimensionConstant;
const length = baseDistance;
const radius = length;
const shapeWidth = type === LINE ? 5 * dimensionConstant : type === RECTANGLE ? length : undefined;
const { width, height } = game.canvas.scene.dimensions;
const shapeData = {
x: width / 2,
y: height / 2,
base: {
type: 'token',
x: 0,
y: 0,
width: 1,
height: 1,
shape: game.canvas.grid.isHexagonal ? CONST.TOKEN_SHAPES.ELLIPSE_1 : CONST.TOKEN_SHAPES.RECTANGLE_1
},
t: usedType,
length: length,
width: shapeWidth,
height: length,
angle: usedAngle,
radius: radius,
direction: direction,
type: usedType
};
const shapeData = CONFIG.Canvas.layers.regions.layerClass.getTemplateShape({
type,
angle,
range,
direction,
});
await canvas.regions.placeRegion(
{
name: usedType.capitalize(),
name: type.capitalize(),
shapes: [shapeData],
restriction: { enabled: false, type: 'move', priority: 0 },
behaviors: [],
@ -112,12 +77,4 @@ export const renderMeasuredTemplate = async event => {
},
{ create: true }
);
};
const getTemplateDistance = range => {
const rangeNumber = Number(range);
if (!Number.isNaN(rangeNumber)) return rangeNumber;
const settings = canvas.scene?.rangeSettings;
return settings ? settings[range] : 0;
};
};

View file

@ -16,7 +16,8 @@ export default class RegisterHandlebarsHelpers {
empty: this.empty,
pluralize: this.pluralize,
positive: this.positive,
isNullish: this.isNullish
isNullish: this.isNullish,
debug: this.debug,
});
}
static add(a, b) {
@ -92,4 +93,9 @@ export default class RegisterHandlebarsHelpers {
static isNullish(a) {
return a === null || a === undefined;
}
static debug(a) {
console.log(a);
return a;
}
}

View file

@ -29,6 +29,7 @@ export const preloadHandlebarsTemplates = async function () {
'systems/daggerheart/templates/actionTypes/uses.hbs',
'systems/daggerheart/templates/actionTypes/roll.hbs',
'systems/daggerheart/templates/actionTypes/save.hbs',
'systems/daggerheart/templates/actionTypes/area.hbs',
'systems/daggerheart/templates/actionTypes/cost.hbs',
'systems/daggerheart/templates/actionTypes/range-target.hbs',
'systems/daggerheart/templates/actionTypes/effect.hbs',

View file

@ -624,9 +624,14 @@
display: flex;
gap: 5px;
margin-top: 8px;
button {
height: 32px;
flex: 1;
&.no-flex {
flex: 0;
}
}
}

View file

@ -0,0 +1,42 @@
<fieldset class="one-column" data-key="area">
<legend>
{{localize "DAGGERHEART.ACTIONS.Config.area.sectionTitle"}}
<a><i class="fa-solid fa-plus icon-button" data-action="addElement"></i></a>
</legend>
{{#each source as |area index|}}
<div class="nest-inputs">
{{formField ../fields.type value=area.type name=(concat "area." index ".type") localize=true}}
{{formField ../fields.shape value=area.shape name=(concat "area." index ".shape") localize=true}}
{{formField ../fields.size value=area.size name=(concat "area." index ".size") localize=true}}
<a class="btn" data-tooltip="{{localize "CONTROLS.CommonDelete"}}" data-action="removeElement" data-index="{{index}}"><i class="fas fa-trash"></i></a>
</div>
<div class="flexcol">
<div class="flexrow">
<span>{{localize "DAGGERHEART.GENERAL.Effect.plural"}}</span>
<a><i class="fa-solid fa-plus icon-button" data-action="addEffect" data-area-index="{{index}}"></i></a>
</div>
{{#each area.effects as |effectId index|}}
<input type="hidden" name={{concat "area." @../key ".effects." index "._id"}} value="{{effect._id}}">
<div class="inventory-item single-img" data-effect-id="{{effectId}}" data-area-index="{{index}}" data-action="editEffect">
<div class="inventory-item-header">
{{#with (@root.getEffectDetails effectId) as | details |}}
<div class="img-portait">
<img class="item-img" src="{{img}}">
</div>
<div class="item-label">
<div class="item-name">{{name}}</div>
</div>
{{/with}}
<input type="hidden" name="effects.{{index}}._id" value="{{effectId}}">
<div class="controls">
<a data-tooltip="{{localize "CONTROLS.CommonDelete"}}" data-action="removeEffect" data-index="{{@../key}}" data-area-index="{{index}}"><i class="fas fa-trash"></i></a>
</div>
</div>
</div>
{{/each}}
</div>
{{/each}}
</fieldset>

View file

@ -1,12 +1,14 @@
<fieldset class="one-column">
<legend>{{localize "DAGGERHEART.GENERAL.range"}}{{#if fields.target}} & {{localize "DAGGERHEART.GENERAL.Target.single"}}{{/if}}</legend>
{{formField fields.range value=source.range label=(localize "DAGGERHEART.GENERAL.range") name=(concat path "range") localize=true}}
{{#if fields.target}}
<div class="nest-inputs">
{{#if (and source.target.type (not (eq source.target.type 'self')))}}
{{ formField fields.target.amount value=source.target.amount label=(localize "DAGGERHEART.GENERAL.amount") name=(concat path "target.amount") localize=true}}
{{/if}}
{{ formField fields.target.type value=source.target.type label=(localize "DAGGERHEART.GENERAL.Target.single") name=(concat path "target.type") localize=true }}
</div>
<div class="nest-inputs">
{{formField fields.range value=source.range label=(localize "DAGGERHEART.GENERAL.range") name=(concat path "range") localize=true}}
{{#if (and source.target.type (not (eq source.target.type 'self')))}}
{{ formField fields.target.amount value=source.target.amount label=(localize "DAGGERHEART.GENERAL.amount") name=(concat path "target.amount") localize=true}}
{{/if}}
{{ formField fields.target.type value=source.target.type label=(localize "DAGGERHEART.GENERAL.Target.single") name=(concat path "target.type") localize=true }}
</div>
{{else}}
{{formField fields.range value=source.range label=(localize "DAGGERHEART.GENERAL.range") name=(concat path "range") localize=true}}
{{/if}}
</fieldset>

View file

@ -6,4 +6,5 @@
{{> 'systems/daggerheart/templates/actionTypes/uses.hbs' fields=fields.uses.fields source=source.uses}}
{{> 'systems/daggerheart/templates/actionTypes/cost.hbs' fields=fields.cost.element.fields source=source.cost costOptions=costOptions}}
{{> 'systems/daggerheart/templates/actionTypes/range-target.hbs' fields=(object range=fields.range target=fields.target.fields) source=(object target=source.target range=source.range)}}
{{> 'systems/daggerheart/templates/actionTypes/area.hbs' fields=fields.area.element.fields source=source.area}}
</section>

View file

@ -2,6 +2,7 @@
{{formGroup fields.tint value=source.tint rootId=rootId placeholder="#ffffff"}}
{{formGroup fields.description value=source.description rootId=rootId}}
{{formGroup fields.disabled value=source.disabled rootId=rootId}}
{{formGroup systemFields.targetDispositions value=source.system.targetDispositions name=(concat "system.targetDispositions") localize=true}}
{{#if isActorEffect}}
<div class="form-group">

View file

@ -14,4 +14,5 @@
{{/unless}}
{{/if}}
{{#if (and hasEffect)}}<button class="duality-action-effect">{{localize "DAGGERHEART.UI.Chat.attackRoll.applyEffect"}}</button>{{/if}}
{{#if areas.length}}<button class="action-areas no-flex"><i class="fa-solid fa-crosshairs"></i></button>{{/if}}
</div>