Skip to content

Commit

Permalink
Enable RuleElements to modify Actors or Items
Browse files Browse the repository at this point in the history
Closes #22.
  • Loading branch information
kmoschcau committed Nov 5, 2021
1 parent a0c5e71 commit eef0f3e
Show file tree
Hide file tree
Showing 11 changed files with 247 additions and 38 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- rule elements can now modify an actor or an item each ([#22](https://github.com/Wasteland-Ventures-Group/WV-VTT-module/issues/22))

### Changed

- rule elements are no longer autocorrecting and save the source as-is, provided
Expand Down
2 changes: 1 addition & 1 deletion src/handlebars/items/parts/ruleElements.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<button type="button" class="control rule-element-control fas fa-minus" data-action="delete" title="{{localize 'wv.ruleEngine.ruleElement.delete'}}"></button>
</div>
</div>
<textarea class="rule-element-source" name="sheet.rules.{{@index}}" required rows="7">{{jsonPretty element.source}}</textarea>
<textarea class="rule-element-source" name="sheet.rules.{{@index}}" required rows="8">{{jsonPretty element.source}}</textarea>
<ul class="rule-element-messages fa-ul">
{{#each element.messages as |message|}}
<li class="{{message.cssClass}}">
Expand Down
5 changes: 5 additions & 0 deletions src/lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,15 @@
"newName": "Neues Regelelement"
},
"errors": {
"logical": {
"noActor": "Das Ziel is auf \"actor\" gestellt, aber das Item hat keinen Actor.",
"wrongSelectedType": "Das Feld \"{path}\" von \"{name}\" hat nicht den benötigten Typ \"{type}\"."
},
"semantic": {
"missing": "Das Feld \"{property}\" fehlt an Stelle \"{path}\".",
"unknown": "Ein unbekannter Fehler is aufgetreten.",
"unknownRuleElement": "Der Typ des Regelelements ist nicht bekannt.",
"unknownTarget": "Das Ziel ist falsch. Es muss entweder \"item\" oder \"actor\" sein.",
"wrongType": "Das Feld \"{path}\" hat nicht den Typ \"{type}\"."
},
"syntax": "Syntaxfehler in Regelelementdefinition {number}: {message}"
Expand Down
5 changes: 5 additions & 0 deletions src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,15 @@
"newName": "New Rule Element"
},
"errors": {
"logical": {
"noActor": "The target is set to \"actor\", but the item has no actor.",
"wrongSelectedType": "The property \"{path}\" on \"{name}\" is not of the needed type \"{type}\"."
},
"semantic": {
"missing": "The property \"{property}\" is missing at \"{path}\".",
"unknown": "An unknown error occurred.",
"unknownRuleElement": "The type of RuleElement is unknown.",
"unknownTarget": "The target is wrong. It has to be either \"item\" or \"actor\".",
"wrongType": "The property \"{path}\" is not of type \"{type}\"."
},
"syntax": "Syntax error in rule element definition {number}: {message}"
Expand Down
2 changes: 1 addition & 1 deletion src/typescript/actor/wvActor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ export default class WvActor extends Actor {

rules
.sort((a, b) => a.priority - b.priority)
.forEach((rule) => rule.onPrepareEmbeddedEntities(this));
.forEach((rule) => rule.onPrepareEmbeddedEntities());
}

// Computations after items {{{2
Expand Down
18 changes: 18 additions & 0 deletions src/typescript/lang.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ export interface LangSchema {
};
/** Labels for different errors */
errors: {
/** Various logical errors */
logical: {
/**
* An error message when the target is set to "actor", but the item
* of the RuleElement has no actor.
*/
noActor: string;
/**
* An error message when the selected property on the target is of the
* wrong type. It should contain a reference to the target name with
* `name`, to the property with `path` and the expected type with
* `type`.
* @pattern (?=.*\{name\})(?=.*\{path\})(?=.*\{type\})
*/
wrongSelectedType: string;
};
/** Various semantic errors */
semantic: {
/**
Expand All @@ -122,6 +138,8 @@ export interface LangSchema {
unknown: string;
/** An error message for an unknown RuleElement type. */
unknownRuleElement: string;
/** An error message for an unknown RuleElement target. */
unknownTarget: string;
/**
* An error message for fields that are of the wrong type, containing
* a reference to the instance path with `path` and to the name of the
Expand Down
26 changes: 26 additions & 0 deletions src/typescript/ruleEngine/messages/wrongSelectedTypeMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getGame } from "../../foundryHelpers.js";
import RuleElementMessage from "../ruleElementMessage.js";

/** A warning about a wrong selected property type on the target */
export default class WrongSelectedTypeMessage extends RuleElementMessage {
constructor(
/** The name of the target document */
public docName: string | null,

/** The path to the property */
public propertyPath: string,

/** The name of the type, the property should be */
public typeName: string
) {
super("wv.ruleEngine.errors.logical.wrongSelectedType", "error");
}

override get message(): string {
return getGame().i18n.format(this.messageKey, {
name: this.docName,
path: this.propertyPath,
type: this.typeName
});
}
}
125 changes: 110 additions & 15 deletions src/typescript/ruleEngine/ruleElement.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type WvItem from "../item/wvItem.js";
import type { RuleElementId } from "./ruleElements.js";
import type RuleElementSource from "./ruleElementSource.js";
import type RuleElementMessage from "./ruleElementMessage.js";
import RuleElementMessage from "./ruleElementMessage.js";
import WrongSelectedTypeMessage from "./messages/wrongSelectedTypeMessage.js";

/**
* A rule engine element, allowing the modification of a data point, specified
* by a selector and a given value. How the data point is modified depends on
* the type of the element.
*/
export default class RuleElement implements RuleElementLike {
export default abstract class RuleElement implements RuleElementLike {
/**
* Create a RuleElement from the given data and owning item.
* @param source - the source data for the RuleElement
Expand All @@ -17,21 +18,21 @@ export default class RuleElement implements RuleElementLike {
* before creating the RuleElement
*/
constructor(
source: RuleElementSource,
source: KnownRuleElementSource,
public item: WvItem,
messages: RuleElementMessage[] = []
) {
this.messages = messages;
this.source = source;

this.validateSource();
this.validate();
}

/** Messages that were accumulated while validating the source */
messages: RuleElementMessage[];

/** The data of the RuleElement */
source: RuleElementSource;
source: KnownRuleElementSource;

/** Get the priority number of the RuleElement. */
get priority(): number {
Expand All @@ -43,6 +44,28 @@ export default class RuleElement implements RuleElementLike {
return this.source.selector;
}

/** Get the target property of the RuleElement. */
get target(): RuleElementTarget {
return this.source.target;
}

/**
* Get the target Document of the RuleElement.
* @throws if the target is "actor" and the RuleElement's Item has no Actor.
*/
get targetDoc(): Actor | Item {
switch (this.target) {
case "item":
return this.item;
case "actor":
if (this.item.actor === null) {
throw new Error("The actor of the RuleElement's item is null!");
}

return this.item.actor;
}
}

/** Get the value of the RuleElement. */
get value(): number {
return this.source.value;
Expand All @@ -68,15 +91,21 @@ export default class RuleElement implements RuleElementLike {
* RuleElement does not have errors.
* @param doc - the Document to modify
*/
onPrepareEmbeddedEntities(doc: Actor | Item): void {
onPrepareEmbeddedEntities(): void {
if (this.shouldNotModify()) return;

this._onPrepareEmbeddedEntities(doc);
this._onPrepareEmbeddedEntities();
}

/** Validate the input source and add any error messages to errors. */
protected validateSource(): void {
// NOOP
/** Validate the data and add any error messages to errors. */
protected validate(): void {
if (this.target === "actor" && this.item.actor === null) {
this.messages.push(
new RuleElementMessage("wv.ruleEngine.errors.logical.noActor", "error")
);

return;
}
}

/**
Expand All @@ -87,9 +116,54 @@ export default class RuleElement implements RuleElementLike {
* @param doc - the Document to modify
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected _onPrepareEmbeddedEntities(_doc: Actor | Item): void {
protected _onPrepareEmbeddedEntities(): void {
// NOOP
}

/**
* Check whether the selected property is of the given type.
*
* If the type is incorrect, an error message is added to the RuleElement.
* @param expectedType - the type to check for
* @returns whether the type is correct
* @throws if anything else than a TypeError is thrown by
* `foundry.utils.getProperty()`
*/
protected checkSelectedIsOfType(expectedType: string) {
let wrongType = false;

try {
const actualType = typeof foundry.utils.getProperty(
this.targetDoc.data.data,
this.selector
);
if (actualType !== expectedType) {
wrongType = true;
}
} catch (error) {
if (error instanceof TypeError) {
// This can happen, when the prefix part of a path finds a valid
// property, which has a non-object value and the selector has further
// parts.
wrongType = true;
} else {
throw error;
}
}

if (wrongType) {
// This has to be accessed in this way, because `checkSelectedIsOfType()`
// can end up being called when the `data` on an Actor or Item is not
// initialized yet.
const targetName = this.targetDoc.data?.name || null;

this.messages.push(
new WrongSelectedTypeMessage(targetName, this.selector, expectedType)
);
}

return !wrongType;
}
}

/** Check whether the given RuleElementLike has errors */
Expand All @@ -103,12 +177,29 @@ export function hasWarnings(element: RuleElementLike): boolean {
}

/**
* A version of the RuleElement raw data layout, where the type is definitely a
* known ID.
* A version of the RuleElement raw data layout, where the more complex types of
* each member are known to be correct.
*/
export type TypedRuleElementSource = RuleElementSource & {
export interface KnownRuleElementSource extends RuleElementSource {
target: RuleElementTarget;
type: RuleElementId;
};
}

/** The valid values of a RuleElement target property */
const RULE_ELEMENT_TARGETS = ["item", "actor"] as const;

/** The type of the valid values for a RuleElement target property */
export type RuleElementTarget = typeof RULE_ELEMENT_TARGETS[number];

/**
* A custom typeguard to check whether a given RuleElement target string has a
* valid value.
* @param target - the target string to check
* @returns whether the target string is valid
*/
export function isValidTarget(target?: string): target is RuleElementTarget {
return RULE_ELEMENT_TARGETS.includes(target as RuleElementTarget);
}

/**
* An unknown version of the RuleElement raw data layout, where each key might
Expand All @@ -118,6 +209,10 @@ export type UnknownRuleElementSource = {
[K in keyof RuleElementSource]?: unknown;
};

/**
* An interface that can be used to pass data around, when no RuleElement could
* be created.
*/
export interface RuleElementLike {
item: WvItem;
messages: RuleElementMessage[];
Expand Down
5 changes: 5 additions & 0 deletions src/typescript/ruleEngine/ruleElementSource.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export default interface RuleElementSource {
/** The selector of the element */
selector: string;

/**
* Whether the RuleElement applies to the the Document or the Owning document
*/
target: string;

/**
* The type identifier of the element. This has to be a simple string instead
* of a union for now, because we would need to bundle AJV to support it
Expand Down
Loading

0 comments on commit eef0f3e

Please sign in to comment.