Merge branch 'version-2-dev' into version-2

This commit is contained in:
Stefan Zermatten
2022-08-15 15:42:54 +02:00
49 changed files with 1005 additions and 816 deletions

View File

@@ -1,53 +0,0 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
const damagePropertiesByName = new ValidatedMethod({
name: 'CreatureProperties.damagePropertiesByName',
validate: new SimpleSchema({
creatureId: SimpleSchema.RegEx.Id,
variableName: {
type: String,
},
operation: {
type: String,
allowedValues: ['set', 'increment']
},
value: Number,
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 20,
timeInterval: 5000,
},
run({creatureId, variableName, operation, value}) {
// Check permissions
let creature = Creatures.findOne(creatureId, {
fields: {
variables: 1,
owner: 1,
readers: 1,
writers: 1,
},
});
assertEditPermission(creature, this.userId);
CreatureProperties.find({
'ancestors.id': creatureId,
variableName,
removed: {$ne: false},
inactive: {$ne: true},
}).forEach(property => {
// Check if property can take damage
let schema = CreatureProperties.simpleSchema(property);
if (!schema.allowsKey('damage')) return;
// Damage the property
damagePropertyWork({property, operation, value});
});
}
});
export default damagePropertiesByName;

View File

@@ -2,8 +2,9 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
const damageProperty = new ValidatedMethod({ const damageProperty = new ValidatedMethod({
name: 'creatureProperties.damage', name: 'creatureProperties.damage',
@@ -20,58 +21,105 @@ const damageProperty = new ValidatedMethod({
numRequests: 20, numRequests: 20,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id, operation, value}) { run({ _id, operation, value }) {
// Check permissions
let property = CreatureProperties.findOne(_id); // Get action context
if (!property) throw new Meteor.Error( const prop = CreatureProperties.findOne(_id);
if (!prop) throw new Meteor.Error(
'Damage property failed', 'Property doesn\'t exist' 'Damage property failed', 'Property doesn\'t exist'
); );
let rootCreature = getRootCreatureAncestor(property); const creatureId = prop.ancestors[0].id;
assertEditPermission(rootCreature, this.userId); const actionContext = new ActionContext(creatureId, [creatureId], this);
// Check permissions
assertEditPermission(actionContext.creature, this.userId);
// Check if property can take damage // Check if property can take damage
let schema = CreatureProperties.simpleSchema(property); let schema = CreatureProperties.simpleSchema(prop);
if (!schema.allowsKey('damage')){ if (!schema.allowsKey('damage')){
throw new Meteor.Error( throw new Meteor.Error(
'Damage property failed', 'Damage property failed',
`Property of type "${property.type}" can't be damaged` `Property of type "${prop.type}" can't be damaged`
); );
} }
let result = damagePropertyWork({ property, operation, value });
const result = damagePropertyWork({ prop, operation, value, actionContext });
// Insert the log
actionContext.writeLog();
return result; return result;
}, },
}); });
export function damagePropertyWork({property, operation, value}){ export function damagePropertyWork({ prop, operation, value, actionContext }) {
let damage, newValue;
// Save the value to the scope before applying the before triggers
if (operation === 'increment') {
if (value >= 0) {
actionContext.scope['$damage'] = value;
} else {
actionContext.scope['$healing'] = -value;
}
} else {
actionContext.scope['$set'] = value;
}
applyTriggers(actionContext.triggers?.damageProperty?.before, prop, actionContext);
// fetch the value from the scope after the before triggers, in case they changed them
if (operation === 'increment') {
if (value >= 0) {
value = actionContext.scope['$damage'];
} else {
value = -actionContext.scope['$healing'];
}
} else {
value = actionContext.scope['$set'];
}
let damage, newValue, increment;
if (operation === 'set'){ if (operation === 'set'){
const total = property.total || 0; const total = prop.total || 0;
// Set represents what we want the value to be after damage // Set represents what we want the value to be after damage
// So we need the actual damage to get to that value // So we need the actual damage to get to that value
damage = total - value; damage = total - value;
// Damage can't exceed total value // Damage can't exceed total value
if (damage > total) damage = total; if (damage > total && !prop.ignoreLowerLimit) damage = total;
// Damage must be positive // Damage must be positive
if (damage < 0) damage = 0; if (damage < 0 && !prop.ignoreUpperLimit) damage = 0;
newValue = property.total - damage; newValue = prop.total - damage;
// Write the results
CreatureProperties.update(prop._id, {
$set: { damage, value: newValue, dirty: true }
}, {
selector: prop
});
} else if (operation === 'increment'){ } else if (operation === 'increment'){
let currentValue = property.value || 0; let currentValue = prop.value || 0;
let currentDamage = property.damage || 0; let currentDamage = prop.damage || 0;
let increment = value; increment = value;
// Can't increase damage above the remaining value // Can't increase damage above the remaining value
if (increment > currentValue) increment = currentValue; if (increment > currentValue && !prop.ignoreLowerLimit) increment = currentValue;
// Can't decrease damage below zero // Can't decrease damage below zero
if (-increment > currentDamage) increment = -currentDamage; if (-increment > currentDamage && !prop.ignoreUpperLimit) increment = -currentDamage;
damage = currentDamage + increment; damage = currentDamage + increment;
newValue = property.total - damage; newValue = prop.total - damage;
// Write the results
CreatureProperties.update(prop._id, {
$inc: { damage: increment, value: -increment },
$set: { dirty: true },
}, {
selector: prop
});
} }
// Write the results applyTriggers(actionContext.triggers?.damageProperty?.after, prop, actionContext);
CreatureProperties.update(property._id, {
$set: {damage, value: newValue, dirty: true} if (operation === 'set') {
}, { return damage;
selector: property } else if (operation === 'increment') {
}); return increment;
return damage; }
} }
export default damageProperty; export default damageProperty;

View File

@@ -1,70 +0,0 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
const dealDamage = new ValidatedMethod({
name: 'creatureProperties.dealDamage',
validate: new SimpleSchema({
creatureId: SimpleSchema.RegEx.Id,
damageType: {
type: String,
},
amount: Number,
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 20,
timeInterval: 5000,
},
run({creatureId, damageType, amount}) {
// permissions
let creature = Creatures.findOne(creatureId, {
fields: {
owner: 1,
readers: 1,
writers: 1,
},
});
assertEditPermission(creature, this.userId);
const totalDamage = dealDamageWork({creature, damageType, amount})
return totalDamage;
},
});
export function dealDamageWork({creature, damageType, amount}){
// Get all the health bars and do damage to them
let healthBars = CreatureProperties.find({
'ancestors.id': creature._id,
type: 'attribute',
attributeType:'healthBar',
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: -1},
});
//let multiplier = creature.damageMultipliers[damageType];
//if (multiplier === undefined) multiplier = 1;
//let totalDamage = Math.floor(amount * multiplier);
const totalDamage = amount;
let damageLeft = totalDamage;
if (damageType === 'healing') damageLeft = -totalDamage;
let propertyIds = [];
healthBars.forEach(healthBar => {
if (damageLeft === 0) return;
let damageAdded = damagePropertyWork({
property: healthBar,
operation: 'increment',
value: damageLeft,
});
damageLeft -= damageAdded;
propertyIds.push(healthBar._id);
});
return totalDamage;
}
export default dealDamage;

View File

@@ -1,7 +1,5 @@
import '/imports/api/creature/creatureProperties/methods/adjustQuantity.js'; import '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
import '/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js';
import '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import '/imports/api/creature/creatureProperties/methods/dealDamage.js';
import '/imports/api/creature/creatureProperties/methods/duplicateProperty.js'; import '/imports/api/creature/creatureProperties/methods/duplicateProperty.js';
import '/imports/api/creature/creatureProperties/methods/equipItem.js'; import '/imports/api/creature/creatureProperties/methods/equipItem.js';
import '/imports/api/creature/creatureProperties/methods/insertProperty.js'; import '/imports/api/creature/creatureProperties/methods/insertProperty.js';

View File

@@ -3,12 +3,9 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { groupBy, remove, union } from 'lodash'; import { union } from 'lodash';
import { import ActionContext from '/imports/api/engine/actions/ActionContext.js';
getCreature, getVariables, getPropertiesOfType import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
} from '/imports/api/engine/loadCreatures.js';
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import { applyTrigger } from '/imports/api/engine/actions/applyTriggers.js';
const restCreature = new ValidatedMethod({ const restCreature = new ValidatedMethod({
name: 'creature.methods.rest', name: 'creature.methods.rest',
@@ -27,59 +24,37 @@ const restCreature = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({creatureId, restType}) { run({ creatureId, restType }) {
// Get action context
const actionContext = new ActionContext(creatureId, [creatureId], this);
// Check permissions // Check permissions
let creature = getCreature(creatureId); assertEditPermission(actionContext.creature, this.userId);
assertEditPermission(creature, this.userId);
// Add the variables to the creature document // Join, sort, and apply before triggers
const variables = getVariables(creatureId); const beforeTriggers = union(
delete variables._id; actionContext.triggers.anyRest?.before, actionContext.triggers[restType]?.before
delete variables._creatureId; ).sort((a, b) => a.order - b.order);
creature.variables = variables; applyTriggers(beforeTriggers, null, actionContext);
const scope = creature.variables;
// Get the triggers // Rest
let triggers = getPropertiesOfType(creatureId, 'trigger'); actionContext.addLog({
remove(triggers, trigger => name: restType === 'shortRest' ? 'Short rest' : 'Long rest',
trigger.event !== 'anyRest' &&
trigger.event !== 'longRest' &&
trigger.event !== 'shortRest'
);
triggers = groupBy(triggers, 'event');
for (let type in triggers) {
triggers[type] = groupBy(triggers[type], 'timing')
}
// Create the log
const log = CreatureLogSchema.clean({
creatureId: creature._id,
creatureName: creature.name,
}); });
doRestWork(restType, actionContext);
const targets = [creature]; // Join, sort, and apply after triggers
const afterTriggers = union(
actionContext.triggers.anyRest?.after, actionContext.triggers[restType]?.after
).sort((a, b) => a.order - b.order);
applyTriggers(afterTriggers, null, actionContext);
applyTriggers(triggers, restType, 'before', { creature, targets, scope, log }); // Insert log
doRestWork(creature, restType); actionContext.writeLog();
applyTriggers(triggers, restType, 'after', { creature, targets, scope, log });
insertCreatureLogWork({log, creature, method: this});
}, },
}); });
function applyTriggers(triggers, restType, timing, opts) { function doRestWork(restType, actionContext) {
// Get matching triggers const creatureId = actionContext.creature._id;
let selectedTriggers = triggers[restType]?.[timing] || [];
// Get any rest triggers as well
selectedTriggers = union(selectedTriggers, triggers['anyRest']?.[timing]);
selectedTriggers.sort((a, b) => a.order - b.order);
// Apply the triggers
selectedTriggers.forEach(trigger => {
applyTrigger(trigger, opts)
});
}
function doRestWork(creature, restType) {
// Long rests reset short rest properties as well // Long rests reset short rest properties as well
let resetFilter; let resetFilter;
if (restType === 'shortRest'){ if (restType === 'shortRest'){
@@ -89,7 +64,7 @@ function doRestWork(creature, restType) {
} }
// Only apply to active properties // Only apply to active properties
let filter = { let filter = {
'ancestors.id': creature._id, 'ancestors.id': creatureId,
reset: resetFilter, reset: resetFilter,
removed: { $ne: true }, removed: { $ne: true },
inactive: { $ne: true }, inactive: { $ne: true },
@@ -123,7 +98,7 @@ function doRestWork(creature, restType) {
// Reset half hit dice on a long rest, starting with the highest dice // Reset half hit dice on a long rest, starting with the highest dice
if (restType === 'longRest'){ if (restType === 'longRest'){
let hitDice = CreatureProperties.find({ let hitDice = CreatureProperties.find({
'ancestors.id': creature._id, 'ancestors.id': creatureId,
type: 'attribute', type: 'attribute',
attributeType: 'hitDice', attributeType: 'hitDice',
removed: {$ne: true}, removed: {$ne: true},
@@ -132,7 +107,7 @@ function doRestWork(creature, restType) {
fields: { fields: {
hitDiceSize: 1, hitDiceSize: 1,
damage: 1, damage: 1,
value: 1, total: 1,
} }
}).fetch(); }).fetch();
// Use a collator to do sorting in natural order // Use a collator to do sorting in natural order
@@ -143,8 +118,8 @@ function doRestWork(creature, restType) {
let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize) let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
hitDice.sort(compare); hitDice.sort(compare);
// Get the total number of hit dice that can be recovered this rest // Get the total number of hit dice that can be recovered this rest
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.value || 0), 0); let totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0);
let resetMultiplier = creature.settings.hitDiceResetMultiplier || 0.5; let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5;
let recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1); let recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1);
// recover each hit dice in turn until the recoverable amount is used up // recover each hit dice in turn until the recoverable amount is used up
let amountToRecover, resultingDamage; let amountToRecover, resultingDamage;

View File

@@ -0,0 +1,78 @@
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import {
getCreature, getVariables, getPropertiesOfType
} from '/imports/api/engine/loadCreatures.js';
import { groupBy, remove } from 'lodash';
export default class ActionContext{
constructor(creatureId, targetIds = [], method) {
// Get the creature
this.creature = getCreature(creatureId)
if (!this.creature) {
throw new Meteor.Error('No Creature', `No creature could be found with id: ${creatureId}`)
}
// Create a log
this.log = CreatureLogSchema.clean({
creatureId: creatureId,
creatureName: this.creature.name,
});
// Get the variables of the acting creature
this.creature.variables = getVariables(creatureId);
delete this.creature.variables._id;
delete this.creature.variables._creatureId;
// Alias as scope
this.scope = this.creature.variables;
// Get the targets and their variables
this.targets = [];
targetIds.forEach(targetId => {
let target;
if (targetId === creatureId) {
target = this.creature;
} else {
target = getCreature(targetId);
target.variables = getVariables(targetId);
delete target.variables._id;
delete target.variables._creatureId;
}
this.targets.push(target);
});
// Store a reference to the method for inserting the log
this.method = method;
// Get triggers
this.triggers = getPropertiesOfType(creatureId, 'trigger');
// Remove deleted or inactive triggers
remove(this.triggers, trigger => trigger.removed || trigger.inactive);
// Sort triggers by order
this.triggers.sort((a, b) => a.order - b.order);
// Group the triggers into triggers.<event>.<timing> or
// triggers.doActionProperty.<propertyType>.<timing>
this.triggers = groupBy(this.triggers, 'event');
for (let event in this.triggers) {
if (event === 'doActionProperty') {
this.triggers[event] = groupBy(this.triggers[event], 'actionPropertyType');
for (let propertyType in this.triggers[event]) {
this.triggers[event][propertyType] = groupBy(this.triggers[event][propertyType], 'timing');
}
} else {
this.triggers[event] = groupBy(this.triggers[event], 'timing');
}
}
}
addLog(content) {
if (content.name || content.value){
this.log.content.push(content);
}
}
writeLog() {
insertCreatureLogWork({
log: this.log,
creature: this.creature,
method: this.method,
});
}
}

View File

@@ -7,7 +7,6 @@ import note from './applyPropertyByType/applyNote.js';
import roll from './applyPropertyByType/applyRoll.js'; import roll from './applyPropertyByType/applyRoll.js';
import savingThrow from './applyPropertyByType/applySavingThrow.js'; import savingThrow from './applyPropertyByType/applySavingThrow.js';
import toggle from './applyPropertyByType/applyToggle.js'; import toggle from './applyPropertyByType/applyToggle.js';
import applyTriggers from '/imports/api/engine/actions/applyTriggers.js';
const applyPropertyByType = { const applyPropertyByType = {
action, action,
@@ -22,9 +21,7 @@ const applyPropertyByType = {
toggle, toggle,
}; };
export default function applyProperty(node, opts, ...rest) { export default function applyProperty(node, actionContext, ...rest) {
applyTriggers(node, opts, 'before'); actionContext.scope[`#${node.node.type}`] = node.node;
opts.scope[`#${node.node.type}`] = node.node; applyPropertyByType[node.node.type]?.(node, actionContext, ...rest);
applyPropertyByType[node.node.type]?.(node, opts, ...rest);
applyTriggers(node, opts, 'after');
} }

View File

@@ -6,23 +6,24 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js'; import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js'; import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyAction(node, {creature, targets, scope, log}){ export default function applyAction(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node; const prop = node.node;
if (prop.target === 'self') targets = [creature]; let targets = actionContext.targets;
if (prop.target === 'self') targets = [actionContext.creature];
// Log the name and summary // Log the name and summary
let content = { name: prop.name }; let content = { name: prop.name };
if (prop.summary?.text){ if (prop.summary?.text){
recalculateInlineCalculations(prop.summary, scope, log); recalculateInlineCalculations(prop.summary, actionContext);
content.value = prop.summary.value; content.value = prop.summary.value;
} }
if (content.name || content.value){ actionContext.addLog(content);
log.content.push(content);
}
// Spend the resources // Spend the resources
const failed = spendResources({prop, log, scope}); const failed = spendResources(prop, actionContext);
if (failed) return; if (failed) return;
const attack = prop.attackRoll || prop.attackRollBonus; const attack = prop.attackRoll || prop.attackRollBonus;
@@ -31,28 +32,29 @@ export default function applyAction(node, {creature, targets, scope, log}){
if (attack && attack.calculation){ if (attack && attack.calculation){
if (targets.length){ if (targets.length){
targets.forEach(target => { targets.forEach(target => {
applyAttackToTarget({attack, target, scope, log}); applyAttackToTarget({attack, target, actionContext});
// Apply the children, but only to the current target // Apply the children, but only to the current target
applyChildren(node, {creature, targets: [target], scope, log}); actionContext.targets = [target];
applyChildren(node, actionContext);
}); });
} else { } else {
applyAttackWithoutTarget({attack, scope, log}); applyAttackWithoutTarget({attack, actionContext});
applyChildren(node, {creature, targets, scope, log}); applyChildren(node, actionContext);
} }
} else { } else {
applyChildren(node, {creature, targets, scope, log}); applyChildren(node, actionContext);
} }
} }
function applyAttackWithoutTarget({attack, scope, log}){ function applyAttackWithoutTarget({attack, actionContext}){
delete scope['$attackHit']; delete actionContext.scope['$attackHit'];
delete scope['$attackMiss']; delete actionContext.scope['$attackMiss'];
delete scope['$criticalHit']; delete actionContext.scope['$criticalHit'];
delete scope['$criticalMiss']; delete actionContext.scope['$criticalMiss'];
delete scope['$attackRoll']; delete actionContext.scope['$attackRoll'];
recalculateCalculation(attack, scope, log);
recalculateCalculation(attack, actionContext);
const scope = actionContext.scope;
let { let {
resultPrefix, resultPrefix,
result, result,
@@ -72,14 +74,15 @@ function applyAttackWithoutTarget({attack, scope, log}){
scope['$attackMiss'] = {value: true}; scope['$attackMiss'] = {value: true};
} }
log.content.push({ actionContext.addLog({
name, name,
value: `${resultPrefix}\n**${result}**`, value: `${resultPrefix}\n**${result}**`,
inline: true, inline: true,
}); });
} }
function applyAttackToTarget({attack, target, scope, log}){ function applyAttackToTarget({attack, target, actionContext}){
const scope = actionContext.scope;
delete scope['$attackHit']; delete scope['$attackHit'];
delete scope['$attackMiss']; delete scope['$attackMiss'];
delete scope['$criticalHit']; delete scope['$criticalHit'];
@@ -87,7 +90,7 @@ function applyAttackToTarget({attack, target, scope, log}){
delete scope['$attackDiceRoll']; delete scope['$attackDiceRoll'];
delete scope['$attackRoll']; delete scope['$attackRoll'];
recalculateCalculation(attack, scope, log); recalculateCalculation(attack, actionContext);
let { let {
resultPrefix, resultPrefix,
@@ -108,7 +111,7 @@ function applyAttackToTarget({attack, target, scope, log}){
name += ' (Disadvantage)'; name += ' (Disadvantage)';
} }
log.content.push({ actionContext.addLog({
name, name,
value: `${resultPrefix}\n**${result}**`, value: `${resultPrefix}\n**${result}**`,
inline: true, inline: true,
@@ -119,11 +122,11 @@ function applyAttackToTarget({attack, target, scope, log}){
scope['$attackHit'] = {value: true}; scope['$attackHit'] = {value: true};
} }
} else { } else {
log.content.push({ actionContext.addLog({
name: 'Error', name: 'Error',
value:'Target has no `armor`', value:'Target has no `armor`',
}); });
log.content.push({ actionContext.addLog({
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit', name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
value: `${resultPrefix}\n**${result}**`, value: `${resultPrefix}\n**${result}**`,
inline: true, inline: true,
@@ -177,14 +180,15 @@ function applyCrits(value, scope){
return {criticalHit, criticalMiss}; return {criticalHit, criticalMiss};
} }
function applyChildren(node, args){ function applyChildren(node, actionContext) {
node.children.forEach(child => applyProperty(child, args)); applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
} }
function spendResources({prop, log, scope}){ function spendResources(prop, actionContext){
// Check Uses // Check Uses
if (prop.usesLeft <= 0){ if (prop.usesLeft <= 0){
log.content.push({ actionContext.addLog({
name: 'Error', name: 'Error',
value: `${prop.name || 'action'} does not have enough uses left`, value: `${prop.name || 'action'} does not have enough uses left`,
}); });
@@ -192,7 +196,7 @@ function spendResources({prop, log, scope}){
} }
// Resources // Resources
if (prop.insufficientResources){ if (prop.insufficientResources){
log.content.push({ actionContext.addLog({
name: 'Error', name: 'Error',
value: 'This creature doesn\'t have sufficient resources to perform this action', value: 'This creature doesn\'t have sufficient resources to perform this action',
}); });
@@ -204,7 +208,7 @@ function spendResources({prop, log, scope}){
let gainLog = []; let gainLog = [];
try { try {
prop.resources.itemsConsumed.forEach(itemConsumed => { prop.resources.itemsConsumed.forEach(itemConsumed => {
recalculateCalculation(itemConsumed.quantity, scope, log); recalculateCalculation(itemConsumed.quantity, actionContext);
if (!itemConsumed.itemId){ if (!itemConsumed.itemId){
throw 'No ammo was selected for this prop'; throw 'No ammo was selected for this prop';
} }
@@ -235,7 +239,7 @@ function spendResources({prop, log, scope}){
} }
}); });
} catch (e){ } catch (e){
log.content.push({ actionContext.addLog({
name: 'Error', name: 'Error',
value: e, value: e,
}); });
@@ -253,7 +257,7 @@ function spendResources({prop, log, scope}){
}, { }, {
selector: prop selector: prop
}); });
log.content.push({ actionContext.addLog({
name: 'Uses left', name: 'Uses left',
value: prop.usesLeft - 1, value: prop.usesLeft - 1,
inline: true, inline: true,
@@ -262,18 +266,19 @@ function spendResources({prop, log, scope}){
// Damage stats // Damage stats
prop.resources.attributesConsumed.forEach(attConsumed => { prop.resources.attributesConsumed.forEach(attConsumed => {
recalculateCalculation(attConsumed.quantity, scope, log); recalculateCalculation(attConsumed.quantity, actionContext);
if (!attConsumed.quantity?.value) return; if (!attConsumed.quantity?.value) return;
let stat = scope[attConsumed.variableName]; let stat = actionContext.scope[attConsumed.variableName];
if (!stat){ if (!stat){
spendLog.push(stat.name + ': ' + ' not found'); spendLog.push(stat.name + ': ' + ' not found');
return; return;
} }
damagePropertyWork({ damagePropertyWork({
property: stat, prop: stat,
operation: 'increment', operation: 'increment',
value: attConsumed.quantity.value, value: attConsumed.quantity.value,
actionContext,
}); });
if (attConsumed.quantity.value > 0){ if (attConsumed.quantity.value > 0){
spendLog.push(stat.name + ': ' + attConsumed.quantity.value); spendLog.push(stat.name + ': ' + attConsumed.quantity.value);
@@ -283,12 +288,12 @@ function spendResources({prop, log, scope}){
}); });
// Log all the spending // Log all the spending
if (gainLog.length) log.content.push({ if (gainLog.length) actionContext.addLog({
name: 'Gained', name: 'Gained',
value: gainLog.join('\n'), value: gainLog.join('\n'),
inline: true, inline: true,
}); });
if (spendLog.length) log.content.push({ if (spendLog.length) actionContext.addLog({
name: 'Spent', name: 'Spent',
value: spendLog.join('\n'), value: spendLog.join('\n'),
inline: true, inline: true,

View File

@@ -1,41 +1,42 @@
import applyProperty from '../applyProperty.js'; import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js'; import recalculateCalculation from './shared/recalculateCalculation.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyAdjustment(node, { export default function applyAdjustment(node, actionContext){
creature, targets, scope, log applyNodeTriggers(node, 'before', actionContext);
}){
const prop = node.node; const prop = node.node;
const damageTargets = prop.target === 'self' ? [creature] : targets; const damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
if (!prop.amount) { if (!prop.amount) {
return applyChildren(node, {creature, targets, scope, log}); return applyChildren(node, actionContext);
} }
// Evaluate the amount // Evaluate the amount
recalculateCalculation(prop.amount, scope, log); recalculateCalculation(prop.amount, actionContext);
const value = +prop.amount.value; const value = +prop.amount.value;
if (!isFinite(value)) { if (!isFinite(value)) {
return applyChildren(node, {creature, targets, scope, log}); return applyChildren(node, actionContext);
} }
if (damageTargets?.length) { if (damageTargets?.length) {
damageTargets.forEach(target => { damageTargets.forEach(target => {
let stat = target.variables[prop.stat]; let stat = target.variables[prop.stat];
if (!stat?.type) { if (!stat?.type) {
log.content.push({ actionContext.addLog({
name: 'Error', name: 'Error',
value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set` value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`
}); });
return applyChildren(node, {creature, targets, scope, log}); return applyChildren(node, actionContext);
} }
damagePropertyWork({ damagePropertyWork({
property: stat, prop: stat,
operation: prop.operation, operation: prop.operation,
value: value, value,
actionContext,
}); });
log.content.push({ actionContext.addLog({
name: 'Attribute damage', name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` + value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`, ` ${value}`,
@@ -43,7 +44,7 @@ export default function applyAdjustment(node, {
}); });
}); });
} else { } else {
log.content.push({ actionContext.addLog({
name: 'Attribute damage', name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` + value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`, ` ${value}`,
@@ -51,9 +52,10 @@ export default function applyAdjustment(node, {
}); });
} }
return applyChildren(node, {creature, targets, scope, log}); return applyChildren(node, actionContext);
} }
function applyChildren(node, args){ function applyChildren(node, actionContext){
node.children.forEach(child => applyProperty(child, args)); applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
} }

View File

@@ -1,26 +1,27 @@
import applyProperty from '../applyProperty.js'; import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js'; import recalculateCalculation from './shared/recalculateCalculation.js';
import rollDice from '/imports/parser/rollDice.js'; import rollDice from '/imports/parser/rollDice.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyBranch(node, { export default function applyBranch(node, actionContext){
creature, targets, scope, log applyNodeTriggers(node, 'before', actionContext);
}){
const applyChildren = function(){ const applyChildren = function(){
node.children.forEach(child => applyProperty(child, { applyNodeTriggers(node, 'after', actionContext);
creature, targets, scope, log node.children.forEach(child => applyProperty(child, actionContext));
}));
}; };
const scope = actionContext.scope;
const targets = actionContext.targets;
const prop = node.node; const prop = node.node;
switch(prop.branchType){ switch(prop.branchType){
case 'if': case 'if':
recalculateCalculation(prop.condition, scope, log); recalculateCalculation(prop.condition, actionContext);
if (prop.condition?.value) applyChildren(); if (prop.condition?.value) applyChildren();
break; break;
case 'index': case 'index':
if (node.children.length){ if (node.children.length){
recalculateCalculation(prop.condition, scope, log); recalculateCalculation(prop.condition, actionContext);
if (!isFinite(prop.condition?.value)) { if (!isFinite(prop.condition?.value)) {
log.content.push({ actionContext.addLog({
name: 'Branch Error', name: 'Branch Error',
value: 'Index did not resolve into a valid number' value: 'Index did not resolve into a valid number'
}); });
@@ -29,49 +30,47 @@ export default function applyBranch(node, {
let index = Math.floor(prop.condition?.value); let index = Math.floor(prop.condition?.value);
if (index < 1) index = 1; if (index < 1) index = 1;
if (index > node.children.length) index = node.children.length; if (index > node.children.length) index = node.children.length;
applyProperty(node.children[index - 1], { applyNodeTriggers(node, 'after', actionContext);
creature, targets, scope, log applyProperty(node.children[index - 1], actionContext);
});
} }
break; break;
case 'hit': case 'hit':
if (scope['$attackHit']?.value){ if (scope['$attackHit']?.value){
if (!targets.length) log.content.push({value: '**On hit**'}); if (!targets.length) actionContext.addLog({value: '**On hit**'});
applyChildren(); applyChildren();
} }
break; break;
case 'miss': case 'miss':
if (scope['$attackMiss']?.value){ if (scope['$attackMiss']?.value){
if (!targets.length) log.content.push({value: '**On miss**'}); if (!targets.length) actionContext.addLog({value: '**On miss**'});
applyChildren(); applyChildren();
} }
break; break;
case 'failedSave': case 'failedSave':
if (scope['$saveFailed']?.value){ if (scope['$saveFailed']?.value){
if (!targets.length) log.content.push({value: '**On failed save**'}); if (!targets.length) actionContext.addLog({value: '**On failed save**'});
applyChildren(); applyChildren();
} }
break; break;
case 'successfulSave': case 'successfulSave':
if (scope['$saveSucceeded']?.value){ if (scope['$saveSucceeded']?.value){
if (!targets.length) log.content.push({value: '**On save**',}); if (!targets.length) actionContext.addLog({value: '**On save**',});
applyChildren(); applyChildren();
} }
break; break;
case 'random': case 'random':
if (node.children.length){ if (node.children.length){
let index = rollDice(1, node.children.length)[0] - 1; let index = rollDice(1, node.children.length)[0] - 1;
applyProperty(node.children[index], { applyNodeTriggers(node, 'after', actionContext);
creature, targets, scope, log applyProperty(node.children[index], actionContext);
});
} }
break; break;
case 'eachTarget': case 'eachTarget':
if (targets.length){ if (targets.length) {
targets.forEach(target => { targets.forEach(target => {
node.children.forEach(child => applyProperty(child, { applyNodeTriggers(node, 'after', actionContext);
creature, targets: [target], scope, log actionContext.targets = [target]
})); node.children.forEach(child => applyProperty(child, actionContext));
}); });
} else { } else {
applyChildren(); applyChildren();

View File

@@ -12,10 +12,12 @@ import symbol from '/imports/parser/parseTree/symbol.js';
import logErrors from './shared/logErrors.js'; import logErrors from './shared/logErrors.js';
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js'; import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js'; import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyBuff(node, {creature, targets, scope, log}){ export default function applyBuff(node, actionContext){
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node; const prop = node.node;
let buffTargets = prop.target === 'self' ? [creature] : targets; let buffTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
// Then copy the decendants of the buff to the targets // Then copy the decendants of the buff to the targets
let propList = [prop]; let propList = [prop];
@@ -26,7 +28,7 @@ export default function applyBuff(node, {creature, targets, scope, log}){
}); });
} }
addChildrenToPropList(node.children); addChildrenToPropList(node.children);
crystalizeVariables({propList, scope, log}); crystalizeVariables({propList, actionContext});
let oldParent = { let oldParent = {
id: prop.parent.id, id: prop.parent.id,
@@ -38,9 +40,9 @@ export default function applyBuff(node, {creature, targets, scope, log}){
//Log the buff //Log the buff
if (prop.name || prop.description?.value){ if (prop.name || prop.description?.value){
if (target._id === creature._id){ if (target._id === actionContext.creature._id){
// Targeting self // Targeting self
log.content.push({ actionContext.addLog({
name: prop.name, name: prop.name,
value: prop.description?.value, value: prop.description?.value,
}); });
@@ -58,6 +60,7 @@ export default function applyBuff(node, {creature, targets, scope, log}){
} }
} }
}); });
applyNodeTriggers(node, 'after', actionContext);
// Don't apply the children of the buff, they get copied to the target instead // Don't apply the children of the buff, they get copied to the target instead
} }
@@ -83,7 +86,7 @@ function copyNodeListToTarget(propList, target, oldParent){
* Replaces all variables with their resolved values * Replaces all variables with their resolved values
* except variables of the form `$target.thing.total` become `thing.total` * except variables of the form `$target.thing.total` become `thing.total`
*/ */
function crystalizeVariables({propList, scope, log}){ function crystalizeVariables({propList, actionContext}){
propList.forEach(prop => { propList.forEach(prop => {
computedSchemas[prop.type].computedFields().forEach( calcKey => { computedSchemas[prop.type].computedFields().forEach( calcKey => {
applyFnToKey(prop, calcKey, (prop, key) => { applyFnToKey(prop, calcKey, (prop, key) => {
@@ -104,7 +107,7 @@ function crystalizeVariables({propList, scope, log}){
} }
} else { } else {
// Can't strip symbols // Can't strip symbols
log.content.push({ actionContext.addLog({
name: 'Error', name: 'Error',
value: 'Variable `$target` should not be used without a property: $target.property', value: 'Variable `$target` should not be used without a property: $target.property',
}); });
@@ -112,8 +115,8 @@ function crystalizeVariables({propList, scope, log}){
return node; return node;
} else { } else {
// Resolve all other variables // Resolve all other variables
const {result, context} = resolve('reduce', node, scope); const {result, context} = resolve('reduce', node, actionContext.scope);
logErrors(context.errors, log); logErrors(context.errors, actionContext);
return result; return result;
} }
}); });

View File

@@ -1,27 +1,30 @@
import { some, intersection, difference } from 'lodash'; import { some, intersection, difference, remove } from 'lodash';
import applyProperty from '../applyProperty.js'; import applyProperty from '../applyProperty.js';
import { dealDamageWork } from '/imports/api/creature/creatureProperties/methods/dealDamage.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js'; import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
import resolve, { Context, toString } from '/imports/parser/resolve.js'; import resolve, { Context, toString } from '/imports/parser/resolve.js';
import logErrors from './shared/logErrors.js'; import logErrors from './shared/logErrors.js';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js'; import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import {
getPropertiesOfType
} from '/imports/api/engine/loadCreatures.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyDamage(node, { export default function applyDamage(node, actionContext){
creature, targets, scope, log applyNodeTriggers(node, 'before', actionContext);
}){
const applyChildren = function(){ const applyChildren = function(){
node.children.forEach(child => applyProperty(child, { applyNodeTriggers(node, 'after', actionContext);
creature, targets, scope, log node.children.forEach(child => applyProperty(child, actionContext));
}));
}; };
const prop = node.node; const prop = node.node;
const scope = actionContext.scope;
// Skip if there is no parse node to work with // Skip if there is no parse node to work with
if (!prop.amount?.parseNode) return; if (!prop.amount?.parseNode) return;
// Choose target // Choose target
let damageTargets = prop.target === 'self' ? [creature] : targets; let damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
// Determine if the hit is critical // Determine if the hit is critical
let criticalHit = scope['$criticalHit']?.value && let criticalHit = scope['$criticalHit']?.value &&
prop.damageType !== 'healing' // Can't critically heal prop.damageType !== 'healing' // Can't critically heal
@@ -36,19 +39,19 @@ export default function applyDamage(node, {
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage'; const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
// roll the dice only and store that string // roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.amount, log); applyEffectsToCalculationParseNode(prop.amount, actionContext.log);
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context); const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
if (rolled.parseType !== 'constant'){ if (rolled.parseType !== 'constant'){
logValue.push(toString(rolled)); logValue.push(toString(rolled));
} }
logErrors(context.errors, log); logErrors(context.errors, actionContext);
// Reset the errors so we don't log the same errors twice // Reset the errors so we don't log the same errors twice
context.errors = []; context.errors = [];
// Resolve the roll to a final value // Resolve the roll to a final value
const {result: reduced} = resolve('reduce', rolled, scope, context); const {result: reduced} = resolve('reduce', rolled, scope, context);
logErrors(context.errors, log); logErrors(context.errors, actionContext);
// Store the result // Store the result
if (reduced.parseType === 'constant'){ if (reduced.parseType === 'constant'){
@@ -94,15 +97,17 @@ export default function applyDamage(node, {
logValue logValue
}); });
actionContext.target = [target];
// Deal the damage to the target // Deal the damage to the target
let damageDealt = dealDamageWork({ let damageDealt = dealDamage({
creature: target, target,
damageType: prop.damageType, damageType: prop.damageType,
amount: damage, amount: damage,
actionContext
}); });
// Log the damage done // Log the damage done
if (target._id === creature._id){ if (target._id === actionContext.creature._id){
// Target is same as self, log damage as such // Target is same as self, log damage as such
logValue.push(`**${damageDealt}** ${suffix} to self`); logValue.push(`**${damageDealt}** ${suffix} to self`);
} else { } else {
@@ -123,7 +128,7 @@ export default function applyDamage(node, {
// There are no targets, just log the result // There are no targets, just log the result
logValue.push(`**${damage}** ${suffix}`); logValue.push(`**${damage}** ${suffix}`);
} }
log.content.push({ actionContext.addLog({
name: logName, name: logName,
value: logValue.join('\n'), value: logValue.join('\n'),
inline: true, inline: true,
@@ -178,3 +183,49 @@ function multiplierAppliesTo(damageProp){
return hasRequiredTags && hasNoExcludedTags; return hasRequiredTags && hasNoExcludedTags;
} }
} }
function dealDamage({target, damageType, amount, actionContext}){
// Get all the health bars and do damage to them
let healthBars = getPropertiesOfType(target._id, 'attribute');
// Keep only the healthbars that can take damage/healing
remove(healthBars, (bar) =>
bar.attributeType !== 'healthBar' ||
bar.inactive ||
bar.removed ||
bar.overridden ||
(amount >= 0 && bar.healthBarNoDamage) ||
(amount < 0 && bar.healthBarNoHealing)
);
// Sort healthbars by damage/healing order or tree order as a fallback
healthBars.sort((a, b) => {
let diff;
if (amount >= 0) {
diff = a.healthBarDamageOrder - b.healthBarDamageOrder;
} else {
diff = a.healthBarHealingOrder - b.healthBarHealingOrder;
}
if (Number.isFinite(diff)) {
return diff;
} else {
return a.order - b.order;
}
});
// Deal the damage to each healthbar in order until all damage is done
const totalDamage = amount;
let damageLeft = totalDamage;
if (damageType === 'healing') damageLeft = -totalDamage;
healthBars.forEach(healthBar => {
if (damageLeft === 0) return;
let damageAdded = damagePropertyWork({
prop: healthBar,
operation: 'increment',
value: damageLeft,
actionContext
});
damageLeft -= damageAdded;
});
return totalDamage;
}

View File

@@ -1,25 +1,27 @@
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js'; import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
import applyProperty from '../applyProperty.js'; import applyProperty from '../applyProperty.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyNote(node, {creature, targets, scope, log}){ export default function applyNote(node, actionContext){
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node; const prop = node.node;
// Log Name, summary // Log Name, summary
let content = { name: prop.name }; let content = { name: prop.name };
if (prop.summary?.text){ if (prop.summary?.text){
recalculateInlineCalculations(prop.summary, scope, log); recalculateInlineCalculations(prop.summary, actionContext);
content.value = prop.summary.value; content.value = prop.summary.value;
} }
if (content.name || content.value){ if (content.name || content.value){
log.content.push(content); actionContext.addLog(content);
} }
// Log description // Log description
if (prop.description?.text){ if (prop.description?.text){
recalculateInlineCalculations(prop.description, scope, log); recalculateInlineCalculations(prop.description, actionContext);
log.content.push({value: prop.description.value}); actionContext.addLog({value: prop.description.value});
} }
// Apply triggers
applyNodeTriggers(node, 'after', actionContext);
// Apply children // Apply children
node.children.forEach(child => applyProperty(child, { node.children.forEach(child => applyProperty(child, actionContext));
creature, targets, scope, log
}));
} }

View File

@@ -2,33 +2,34 @@ import applyProperty from '../applyProperty.js';
import logErrors from './shared/logErrors.js'; import logErrors from './shared/logErrors.js';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js'; import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
import resolve, { toString } from '/imports/parser/resolve.js'; import resolve, { toString } from '/imports/parser/resolve.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyRoll(node, {creature, targets, scope, log}){ export default function applyRoll(node, actionContext){
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node; const prop = node.node;
const applyChildren = function(){ const applyChildren = function(){
node.children.forEach(child => applyProperty(child, { applyNodeTriggers(node, 'after', actionContext);
creature, targets, scope, log node.children.forEach(child => applyProperty(child, actionContext));
}));
}; };
if (prop.roll?.calculation){ if (prop.roll?.calculation){
const logValue = []; const logValue = [];
// roll the dice only and store that string // roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.roll, log); applyEffectsToCalculationParseNode(prop.roll, actionContext);
const {result: rolled, context} = resolve('roll', prop.roll.parseNode, scope); const {result: rolled, context} = resolve('roll', prop.roll.parseNode, actionContext.scope);
if (rolled.parseType !== 'constant'){ if (rolled.parseType !== 'constant'){
logValue.push(toString(rolled)); logValue.push(toString(rolled));
} }
logErrors(context.errors, log); logErrors(context.errors, actionContext);
// Reset the errors so we don't log the same errors twice // Reset the errors so we don't log the same errors twice
context.errors = []; context.errors = [];
// Resolve the roll to a final value // Resolve the roll to a final value
const {result: reduced} = resolve('reduce', rolled, scope, context); const {result: reduced} = resolve('reduce', rolled, actionContext.scope, context);
logErrors(context.errors, log); logErrors(context.errors, actionContext);
// Store the result // Store the result
if (reduced.parseType === 'constant'){ if (reduced.parseType === 'constant'){
@@ -45,11 +46,11 @@ export default function applyRoll(node, {creature, targets, scope, log}){
} }
const value = reduced.value; const value = reduced.value;
scope[prop.variableName] = value; actionContext.scope[prop.variableName] = value;
logValue.push(`**${value}**`); logValue.push(`**${value}**`);
if (!prop.silent){ if (!prop.silent){
log.content.push({ actionContext.addLog({
name: prop.name, name: prop.name,
value: logValue.join('\n'), value: logValue.join('\n'),
inline: true, inline: true,

View File

@@ -2,38 +2,38 @@ import rollDice from '/imports/parser/rollDice.js';
import recalculateCalculation from './shared/recalculateCalculation.js'; import recalculateCalculation from './shared/recalculateCalculation.js';
import applyProperty from '../applyProperty.js'; import applyProperty from '../applyProperty.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js'; import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applySavingThrow(node, {creature, targets, scope, log}){ export default function applySavingThrow(node, actionContext){
applyNodeTriggers(node, 'before', actionContext);
const prop = node.node; const prop = node.node;
let saveTargets = prop.target === 'self' ? [creature] : targets; let saveTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
recalculateCalculation(prop.dc, scope, log); recalculateCalculation(prop.dc, actionContext);
const dc = (prop.dc?.value); const dc = (prop.dc?.value);
if (!isFinite(dc)){ if (!isFinite(dc)){
log.content.push({ actionContext.addLog({
name: 'Error', name: 'Error',
value: 'Saving throw requires a DC', value: 'Saving throw requires a DC',
}); });
return node.children.forEach(child => applyProperty(child, { return node.children.forEach(child => applyProperty(child, actionContext));
creature, targets, scope, log
}));
} }
log.content.push({ actionContext.addLog({
name: prop.name, name: prop.name,
value: `DC **${dc}**`, value: `DC **${dc}**`,
inline: true, inline: true,
}); });
const scope = actionContext.scope;
// If there are no save targets, apply all children as if the save both // If there are no save targets, apply all children as if the save both
// succeeeded and failed // succeeeded and failed
if (!saveTargets?.length){ if (!saveTargets?.length){
scope['$saveFailed'] = {value: true}; scope['$saveFailed'] = {value: true};
scope['$saveSucceeded'] = {value: true}; scope['$saveSucceeded'] = { value: true };
return node.children.forEach(child => applyProperty(child, { applyNodeTriggers(node, 'after', actionContext);
creature, targets, scope, log return node.children.forEach(child => applyProperty(child, actionContext));
}));
} }
// Each target makes the saving throw // Each target makes the saving throw
@@ -43,16 +43,16 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
delete scope['$saveDiceRoll']; delete scope['$saveDiceRoll'];
delete scope['$saveRoll']; delete scope['$saveRoll'];
const applyChildren = function(){ const applyChildren = function () {
node.children.forEach(child => applyProperty(child, { applyNodeTriggers(node, 'after', actionContext);
creature, targets: [target], scope, log actionContext.targets = [target]
})); node.children.forEach(child => applyProperty(child, actionContext));
}; };
const save = target.variables[prop.stat]; const save = target.variables[prop.stat];
if (!save){ if (!save){
log.content.push({ actionContext.addLog({
name: 'Saving throw error', name: 'Saving throw error',
value: 'No saving throw found: ' + prop.stat, value: 'No saving throw found: ' + prop.stat,
}); });
@@ -94,7 +94,7 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
} else { } else {
scope['$saveFailed'] = {value: true}; scope['$saveFailed'] = {value: true};
} }
log.content.push({ actionContext.addLog({
name: saveSuccess ? 'Successful save' : 'Failed save', name: saveSuccess ? 'Successful save' : 'Failed save',
value: resultPrefix + '\n**' + result + '**', value: resultPrefix + '\n**' + result + '**',
inline: true, inline: true,

View File

@@ -1,14 +1,13 @@
import applyProperty from '../applyProperty.js'; import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js'; import recalculateCalculation from './shared/recalculateCalculation.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyToggle(node, { export default function applyToggle(node, actionContext){
creature, targets, scope, log applyNodeTriggers(node, 'before', actionContext);
}){
const prop = node.node; const prop = node.node;
recalculateCalculation(prop.condition, scope, log); recalculateCalculation(prop.condition, actionContext);
if (prop.condition?.value) { if (prop.condition?.value) {
return node.children.forEach(child => applyProperty(child, { applyNodeTriggers(node, 'after', actionContext);
creature, targets, scope, log return node.children.forEach(child => applyProperty(child, actionContext));
}));
} }
} }

View File

@@ -2,7 +2,7 @@ import operator from '/imports/parser/parseTree/operator.js';
import { parse } from '/imports/parser/parser.js'; import { parse } from '/imports/parser/parser.js';
import logErrors from './logErrors.js'; import logErrors from './logErrors.js';
export default function applyEffectsToCalculationParseNode(calcObj, log){ export default function applyEffectsToCalculationParseNode(calcObj, actionContext){
if (!calcObj.effects) return; if (!calcObj.effects) return;
calcObj.effects.forEach(effect => { calcObj.effects.forEach(effect => {
if (effect.operation !== 'add') return; if (effect.operation !== 'add') return;
@@ -18,7 +18,7 @@ export default function applyEffectsToCalculationParseNode(calcObj, log){
fn: 'add' fn: 'add'
}); });
} catch (e){ } catch (e){
logErrors([e], log) logErrors([e], actionContext)
} }
}); });
} }

View File

@@ -1,7 +1,7 @@
export default function logErrors(errors, log){ export default function logErrors(errors, actionContext){
errors?.forEach(error => { errors?.forEach(error => {
if (error.type !== 'info'){ if (error.type !== 'info'){
log.content.push({name: 'Error', value: error.message}); actionContext.addLog({name: 'Error', value: error.message});
} }
}); });
} }

View File

@@ -2,10 +2,10 @@ import evaluateCalculation from '/imports/api/engine/computation/utility/evaluat
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js'; import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
import logErrors from './logErrors.js'; import logErrors from './logErrors.js';
export default function recalculateCalculation(calc, scope, log, context){ export default function recalculateCalculation(calc, actionContext, context){
if (!calc?.parseNode) return; if (!calc?.parseNode) return;
calc._parseLevel = 'reduce'; calc._parseLevel = 'reduce';
applyEffectsToCalculationParseNode(calc, log); applyEffectsToCalculationParseNode(calc, actionContext.log);
evaluateCalculation(calc, scope, context); evaluateCalculation(calc, actionContext.scope, context);
logErrors(calc.errors, log); logErrors(calc.errors, actionContext.log);
} }

View File

@@ -1,12 +1,12 @@
import embedInlineCalculations from '/imports/api/engine/computation/utility/embedInlineCalculations.js'; import embedInlineCalculations from '/imports/api/engine/computation/utility/embedInlineCalculations.js';
import recalculateCalculation from './recalculateCalculation.js' import recalculateCalculation from './recalculateCalculation.js'
export default function recalculateInlineCalculations(inlineCalcObj, scope, log){ export default function recalculateInlineCalculations(inlineCalcObj, actionContext){
// Skip if there are no calculations // Skip if there are no calculations
if (!inlineCalcObj?.inlineCalculations?.length) return; if (!inlineCalcObj?.inlineCalculations?.length) return;
// Recalculate each calculation with the current scope // Recalculate each calculation with the current scope
inlineCalcObj.inlineCalculations.forEach(calc => { inlineCalcObj.inlineCalculations.forEach(calc => {
recalculateCalculation(calc, scope, log); recalculateCalculation(calc, actionContext);
}); });
// Embed the new calculated values // Embed the new calculated values
embedInlineCalculations(inlineCalcObj); embedInlineCalculations(inlineCalcObj);

View File

@@ -6,18 +6,78 @@ import applyProperty from '/imports/api/engine/actions/applyProperty.js';
import { difference, intersection } from 'lodash'; import { difference, intersection } from 'lodash';
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js'; import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
export default function applyTriggers(node, { creature, targets, scope, log }, timing) { export function applyNodeTriggers(node, timing, actionContext) {
const prop = node.node; const prop = node.node;
const type = prop.type; const type = prop.type;
if (creature.triggers?.[type]?.[timing]) { const triggers = actionContext.triggers?.doActionProperty?.[type]?.[timing];
creature.triggers[type][timing].forEach(trigger => { if (triggers) {
if (triggerMatchTags(trigger, prop)) { triggers.forEach(trigger => {
applyTrigger(trigger, { creature, targets, scope, log }); applyTrigger(trigger, prop, actionContext);
}
}); });
} }
} }
export function applyTriggers(triggers = [], prop, actionContext) {
// Apply the triggers
triggers.forEach(trigger => {
applyTrigger(trigger, prop, actionContext)
});
}
export function applyTrigger(trigger, prop, actionContext) {
// If there is a prop we are applying the trigger from,
// don't fire if the tags don't match
if (prop && !triggerMatchTags(trigger, prop)) {
return;
}
// Prevent trigger from firing if it's inactive
if (trigger.inactive) {
return;
}
// Prevent triggers from firing if their condition is false
if (trigger.condition?.parseNode) {
recalculateCalculation(trigger.condition, actionContext);
if (!trigger.condition.value) return;
}
// Prevent triggers from firing themselves in a loop
if (trigger.firing) {
/*
log.content.push({
name: trigger.name || 'Trigger',
value: 'Trigger can\'t fire itself',
inline: true,
});
*/
return;
}
trigger.firing = true;
// Fire the trigger
const content = {
name: trigger.name || 'Trigger',
value: trigger.description,
inline: false,
}
if (trigger.description?.text){
recalculateInlineCalculations(trigger.description, actionContext);
content.value = trigger.description.value;
}
actionContext.addLog(content);
// Get all the trigger's properties and apply them
const properties = getPropertyDecendants(actionContext.creature._id, trigger._id);
properties.sort((a, b) => a.order - b.order);
const propertyForest = nodeArrayToTree(properties);
propertyForest.forEach(node => {
applyProperty(node, actionContext);
});
trigger.firing = false;
}
function triggerMatchTags(trigger, prop) { function triggerMatchTags(trigger, prop) {
let matched = false; let matched = false;
const propTags = getEffectivePropTags(prop); const propTags = getEffectivePropTags(prop);
@@ -49,51 +109,3 @@ function triggerMatchTags(trigger, prop) {
}); });
return matched; return matched;
} }
export function applyTrigger(trigger, { creature, targets, scope, log }) {
// Prevent triggers from firing if their condition is false
if (trigger.condition?.parseNode) {
recalculateCalculation(trigger.condition, scope, log);
if (!trigger.condition.value) return;
}
// Prevent triggers from firing themselves in a loop
if (trigger.firing) {
/*
log.content.push({
name: trigger.name || 'Trigger',
value: 'Trigger can\'t fire itself',
inline: true,
});
*/
return;
}
trigger.firing = true;
// Fire the trigger
const content = {
name: trigger.name || 'Trigger',
value: trigger.summary,
inline: false,
}
if (trigger.summary?.text){
recalculateInlineCalculations(trigger.summary, scope, log);
content.value = trigger.summary.value;
}
log.content.push(content);
// Get all the trigger's properties and apply them
const properties = getPropertyDecendants(creature._id, trigger._id);
properties.sort((a, b) => a.order - b.order);
const propertyForest = nodeArrayToTree(properties);
propertyForest.forEach(node => {
applyProperty(node, {
creature,
targets,
scope,
log,
});
});
trigger.firing = false;
}

View File

@@ -1,16 +1,15 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
import { import {
getCreature, getVariables, getProperyAncestors, getPropertyDecendants, getPropertiesOfType getProperyAncestors, getPropertyDecendants
} from '/imports/api/engine/loadCreatures.js'; } from '/imports/api/engine/loadCreatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import applyProperty from './applyProperty.js'; import applyProperty from './applyProperty.js';
import { groupBy, remove } from 'lodash'; import ActionContext from '/imports/api/engine/actions/ActionContext.js';
const doAction = new ValidatedMethod({ const doAction = new ValidatedMethod({
name: 'creatureProperties.doAction', name: 'creatureProperties.doAction',
@@ -37,32 +36,16 @@ const doAction = new ValidatedMethod({
numRequests: 10, numRequests: 10,
timeInterval: 5000, timeInterval: 5000,
}, },
run({actionId, targetIds = [], scope}) { run({ actionId, targetIds = [], scope }) {
// Get action context
let action = CreatureProperties.findOne(actionId); let action = CreatureProperties.findOne(actionId);
// Check permissions
const creatureId = action.ancestors[0].id; const creatureId = action.ancestors[0].id;
let creature = getCreature(action.ancestors[0].id); const actionContext = new ActionContext(creatureId, targetIds, this);
assertEditPermission(creature, this.userId);
// Check permissions
// Add the variables to the creature document assertEditPermission(actionContext.creature, this.userId);
const variables = getVariables(creatureId); actionContext.targets.forEach(target => {
delete variables._id;
delete variables._creatureId;
creature.variables = variables;
// Get all the targets and make sure we can edit them
let targets = [];
targetIds.forEach(targetId => {
let target = getCreature(targetId);
assertEditPermission(target, this.userId); assertEditPermission(target, this.userId);
// add the variables to the target documents
const variables = getVariables(creatureId);
delete variables._id;
delete variables._creatureId;
target.variables = variables;
targets.push(target);
}); });
const ancestors = getProperyAncestors(creatureId, action._id); const ancestors = getProperyAncestors(creatureId, action._id);
@@ -73,11 +56,11 @@ const doAction = new ValidatedMethod({
properties.sort((a, b) => a.order - b.order); properties.sort((a, b) => a.order - b.order);
// Do the action // Do the action
doActionWork({creature, targets, properties, ancestors, method: this, methodScope: scope}); doActionWork({properties, ancestors, actionContext, methodScope: scope});
// Recompute all involved creatures // Recompute all involved creatures
Creatures.update({ Creatures.update({
_id: { $in: [creature._id, ...targetIds] } _id: { $in: [creatureId, ...targetIds] }
}, { }, {
$set: {dirty: true}, $set: {dirty: true},
}); });
@@ -87,7 +70,7 @@ const doAction = new ValidatedMethod({
export default doAction; export default doAction;
export function doActionWork({ export function doActionWork({
creature, targets, properties, ancestors, method, methodScope = {}, log properties, ancestors, actionContext, methodScope = {},
}){ }){
// get the docs // get the docs
const ancestorScope = getAncestorScope(ancestors); const ancestorScope = getAncestorScope(ancestors);
@@ -96,36 +79,15 @@ export function doActionWork({
throw new Meteor.Error(`The action has ${propertyForest.length} top level properties, expected 1`); throw new Meteor.Error(`The action has ${propertyForest.length} top level properties, expected 1`);
} }
// Get the triggers // Include the ancestry and method scope in the context scope
const triggers = getPropertiesOfType(creature._id, 'trigger'); Object.assign(actionContext.scope, ancestorScope, methodScope);
remove(triggers, trigger => trigger.event !== 'doActionProperty');
creature.triggers = groupBy(triggers, 'actionPropertyType');
for (let type in creature.triggers) {
creature.triggers[type] = groupBy(creature.triggers[type], 'timing')
}
// Create the log
if (!log) log = CreatureLogSchema.clean({
creatureId: creature._id,
creatureName: creature.name,
});
// Apply the top level property, it is responsible for applying its children // Apply the top level property, it is responsible for applying its children
// recursively // recursively
const scope = { applyProperty(propertyForest[0], actionContext);
...creature.variables,
...ancestorScope,
...methodScope
}
applyProperty(propertyForest[0], {
creature,
targets,
scope,
log,
});
// Insert the log // Insert the log
insertCreatureLogWork({log, creature, method}); actionContext.writeLog();
} }
// Assumes ancestors are in tree order already // Assumes ancestors are in tree order already

View File

@@ -1,11 +1,53 @@
import '/imports/api/simpleSchemaConfig.js'; import '/imports/api/simpleSchemaConfig.js';
//import testTypes from './testTypes/index.js'; //import testTypes from './testTypes/index.js';
import { doActionWork } from './doAction.js'; import { doActionWork } from './doAction.js';
import createAction from './tests/createAction.testFn.js'; import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
function cleanProp(prop){
let schema = CreatureProperties.simpleSchema(prop);
return schema.clean(prop);
}
function cleanCreature(creature){
let schema = Creatures.simpleSchema(creature);
return schema.clean(creature);
}
// Fake ActionContext to test actions with
const creatureId = 'actionTestCreatureId';
const creatureName = 'Action Test Creature';
const testActionContext = {
creature: cleanCreature({
_id: creatureId,
}),
log: CreatureLogSchema.clean({
creatureId: creatureId,
creatureName: creatureName,
}),
scope: {},
addLog(content) {
if (content.name || content.value){
this.log.content.push(content);
}
},
writeLog: () => { },
}
const action = cleanProp({
type: 'action',
});
const actionAncestors = [];
describe('Do Action', function(){ describe('Do Action', function(){
it('Does an empty action', function(){ it('Does an empty action', function(){
doActionWork(createAction({properties: [{type: 'action'}]})); doActionWork({
properties: [action],
ancestors: actionAncestors,
actionContext: testActionContext,
methodScope: {},
});
}); });
//testTypes.forEach(test => it(test.text, test.fn)); //testTypes.forEach(test => it(test.text, test.fn));
}); });

View File

@@ -1,13 +1,16 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import {
getProperyAncestors, getPropertyDecendants
} from '/imports/api/engine/loadCreatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { doActionWork } from '/imports/api/engine/actions/doAction.js'; import { doActionWork } from '/imports/api/engine/actions/doAction.js';
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js'; import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
const doAction = new ValidatedMethod({ const doAction = new ValidatedMethod({
name: 'creatureProperties.doCastSpell', name: 'creatureProperties.doCastSpell',
@@ -39,47 +42,31 @@ const doAction = new ValidatedMethod({
numRequests: 10, numRequests: 10,
timeInterval: 5000, timeInterval: 5000,
}, },
run({spellId, slotId, targetIds = [], scope = {}}) { run({ spellId, slotId, targetIds = [], scope = {} }) {
// Get action context
let spell = CreatureProperties.findOne(spellId); let spell = CreatureProperties.findOne(spellId);
const creatureId = spell.ancestors[0].id;
const actionContext = new ActionContext(creatureId, targetIds, this);
// Check permissions // Check permissions
let creature = getRootCreatureAncestor(spell); assertEditPermission(actionContext.creature, this.userId);
actionContext.targets.forEach(target => {
assertEditPermission(creature, this.userId);
// Get all the targets and make sure we can edit them
let targets = [];
targetIds.forEach(targetId => {
let target = Creatures.findOne(targetId);
assertEditPermission(target, this.userId); assertEditPermission(target, this.userId);
targets.push(target);
}); });
// Fetch all the action's ancestor creatureProperties const ancestors = getProperyAncestors(creatureId, spell._id);
const ancestorIds = []; ancestors.sort((a, b) => a.order - b.order);
spell.ancestors.forEach(ref => {
if (ref.collection === 'creatureProperties') {
ancestorIds.push(ref.id);
}
});
// Get cursor of ancestors const properties = getPropertyDecendants(creatureId, spell._id);
const ancestors = CreatureProperties.find({ properties.push(spell);
_id: {$in: ancestorIds}, properties.sort((a, b) => a.order - b.order);
}, {
sort: {order: 1},
});
// Get cursor of the properties
const properties = CreatureProperties.find({
$or: [{_id: spell._id}, {'ancestors.id': spell._id}],
removed: {$ne: true},
}, {
sort: {order: 1},
});
// Spend the appropriate slot // Spend the appropriate slot
let slotLevel = spell.level || 0; let slotLevel = spell.level || 0;
let slot; let slot;
actionContext.scope['slotLevel'] = slotLevel;
if (slotId && !spell.castWithoutSpellSlots){ if (slotId && !spell.castWithoutSpellSlots){
slot = CreatureProperties.findOne(slotId); slot = CreatureProperties.findOne(slotId);
if (!slot){ if (!slot){
@@ -104,34 +91,32 @@ const doAction = new ValidatedMethod({
} }
slotLevel = slot.spellSlotLevel.value; slotLevel = slot.spellSlotLevel.value;
damagePropertyWork({ damagePropertyWork({
property: slot, prop: slot,
operation: 'increment', operation: 'increment',
value: 1, value: 1,
actionContext,
}); });
} }
scope['slotLevel'] = slotLevel;
// Post the slot level spent to the log // Post the slot level spent to the log
const log = CreatureLogSchema.clean({
creatureId: creature._id,
creatureName: creature.name,
});
if (slot?.spellSlotLevel?.value){ if (slot?.spellSlotLevel?.value){
log.content.push({ actionContext.addLog({
name: `Casting using a level ${slotLevel} spell slot` name: `Casting using a level ${slotLevel} spell slot`
}); });
} else if (slotLevel) { } else if (slotLevel) {
log.content.push({ actionContext.addLog({
name: `Casting at level ${slotLevel}` name: `Casting at level ${slotLevel}`
}); });
} }
// Do the action // Do the action
doActionWork({ creature, targets, properties, ancestors, method: this, methodScope: scope, log }); doActionWork({
properties, ancestors, actionContext, methodScope: scope,
});
// Force the characters involved to recalculate
Creatures.update({ Creatures.update({
_id: { $in: [creature._id, ...targetIds] } _id: { $in: [creatureId, ...targetIds] }
}, { }, {
$set: { dirty: true }, $set: { dirty: true },
}); });

View File

@@ -1,12 +1,12 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import rollDice from '/imports/parser/rollDice.js'; import rollDice from '/imports/parser/rollDice.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js'; import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
const doCheck = new ValidatedMethod({ const doCheck = new ValidatedMethod({
name: 'creatureProperties.doCheck', name: 'creatureProperties.doCheck',
@@ -24,34 +24,32 @@ const doCheck = new ValidatedMethod({
}, },
run({propId, scope}) { run({propId, scope}) {
const prop = CreatureProperties.findOne(propId); const prop = CreatureProperties.findOne(propId);
const creature = getRootCreatureAncestor(prop); const creatureId = prop.ancestors[0].id;
const actionContext = new ActionContext(creatureId, [creatureId], this);
Object.assign(actionContext.scope, scope);
// Check permissions // Check permissions
assertEditPermission(creature, this.userId); assertEditPermission(actionContext.creature, this.userId);
// Do the check // Do the check
doCheckWork({creature, prop, method: this, methodScope: scope}); doCheckWork({prop, actionContext});
}, },
}); });
export default doCheck; export default doCheck;
export function doCheckWork({ export function doCheckWork({prop, actionContext}){
creature, prop, method, methodScope = {}
}){
// Create the log
let log = CreatureLogSchema.clean({
creatureId: creature._id,
creatureName: creature.name,
});
rollCheck({prop, log, methodScope}); applyTriggers(actionContext.triggers.check?.before, prop, actionContext);
rollCheck(prop, actionContext);
applyTriggers(actionContext.triggers.check?.after, prop, actionContext);
// Insert the log // Insert the log
insertCreatureLogWork({log, creature, method}); actionContext.writeLog();
} }
function rollCheck({prop, log, methodScope}){ function rollCheck(prop, actionContext) {
const scope = actionContext.scope;
// get the modifier for the roll // get the modifier for the roll
let rollModifier; let rollModifier;
let logName = `${prop.name} check`; let logName = `${prop.name} check`;
@@ -77,7 +75,7 @@ function rollCheck({prop, log, methodScope}){
const rollModifierText = numberToSignedString(rollModifier, true); const rollModifierText = numberToSignedString(rollModifier, true);
let value, values, resultPrefix; let value, values, resultPrefix;
if (methodScope['$checkAdvantage'] === 1){ if (scope['$checkAdvantage'] === 1){
logName += ' (Advantage)'; logName += ' (Advantage)';
const [a, b] = rollDice(2, 20); const [a, b] = rollDice(2, 20);
if (a >= b) { if (a >= b) {
@@ -87,7 +85,7 @@ function rollCheck({prop, log, methodScope}){
value = b; value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `; resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
} }
} else if (methodScope['$checkAdvantage'] === -1){ } else if (scope['$checkAdvantage'] === -1){
logName += ' (Disadvantage)'; logName += ' (Disadvantage)';
const [a, b] = rollDice(2, 20); const [a, b] = rollDice(2, 20);
if (a <= b) { if (a <= b) {
@@ -103,7 +101,7 @@ function rollCheck({prop, log, methodScope}){
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = ` resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
} }
const result = (value + rollModifier) || 0; const result = (value + rollModifier) || 0;
log.content.push({ actionContext.addLog({
name: logName, name: logName,
value: `${resultPrefix} **${result}**`, value: `${resultPrefix} **${result}**`,
}); });

View File

@@ -1,26 +0,0 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
export default function createAction({
creature = {_id: 'creatureId'},
targets = [],
properties = [],
ancestors = [],
method
} = {}){
properties = properties.map(cleanProp);
ancestors = ancestors.map(cleanProp);
creature = cleanCreature(creature);
ancestors = ancestors.map(cleanCreature);
return {creature, targets, properties, ancestors, method};
}
function cleanProp(prop){
let schema = CreatureProperties.simpleSchema(prop);
return schema.clean(prop);
}
function cleanCreature(creature){
let schema = Creatures.simpleSchema(creature);
return schema.clean(creature);
}

View File

@@ -1,6 +0,0 @@
import applyAction from './applyAction.testFn.js';
export default [{
text: 'Applies actions',
fn: applyAction,
},];

View File

@@ -178,8 +178,8 @@ function getTargetListFromTags(tags, computation){
const targetTagIdLists = []; const targetTagIdLists = [];
if (!tags) return []; if (!tags) return [];
tags.forEach(tag => { tags.forEach(tag => {
const idList = computation.propsWithTag[tag]; const idList = computation.propsWithTag[tag] || [];
if (idList) targetTagIdLists.push(idList); targetTagIdLists.push(idList);
}); });
const targets = intersection(...targetTagIdLists); const targets = intersection(...targetTagIdLists);
return targets; return targets;

View File

@@ -86,7 +86,7 @@ function aggregateAbilityEffects({computation, skillNode, abilityNode}){
// to a skill from its ability // to a skill from its ability
if (link.data === 'effect'){ if (link.data === 'effect'){
if (![ if (![
'advantage', 'disadvantage', 'passiveAdd', 'fail' 'advantage', 'disadvantage', 'passiveAdd', 'fail', 'conditional'
].includes(linkedNode.data.operation)){ ].includes(linkedNode.data.operation)){
return; return;
} }

View File

@@ -59,6 +59,23 @@ let AttributeSchema = createPropertySchema({
type: String, type: String,
regEx: /^#([a-f0-9]{3}){1,2}\b$/i, regEx: /^#([a-f0-9]{3}){1,2}\b$/i,
optional: true, optional: true,
},
// Control how the health bar takes damage or healing
healthBarNoDamage: {
type: Boolean,
optional: true,
},
healthBarNoHealing: {
type: Boolean,
optional: true,
},
healthBarDamageOrder: {
type: SimpleSchema.Integer,
optional: true,
},
healthBarHealingOrder: {
type: SimpleSchema.Integer,
optional: true,
}, },
// The starting value, before effects // The starting value, before effects
baseValue: { baseValue: {
@@ -79,6 +96,16 @@ let AttributeSchema = createPropertySchema({
decimal: { decimal: {
type: Boolean, type: Boolean,
optional: true, optional: true,
},
// Can the total after damage be negative
ignoreLowerLimit: {
type: Boolean,
optional: true,
},
// Can the damage value be negative
ignoreUpperLimit: {
type: Boolean,
optional: true,
}, },
// Automatically zero the adjustment on these conditions // Automatically zero the adjustment on these conditions
reset: { reset: {

View File

@@ -5,8 +5,11 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
const eventOptions = { const eventOptions = {
doActionProperty: 'Do action', doActionProperty: 'Do action',
// receiveActionProperty: 'Receiving action property', // receiveActionProperty: 'Receiving action property',
check: 'Roll check',
// flipToggle: 'Toggle changed', // flipToggle: 'Toggle changed',
// adjustProperty: 'Attribute adjusted', // itemEquipped: 'Item equipped'
// itemUnequipped: 'Item unequipped'
damageProperty: 'Attribute damaged or healed',
anyRest: 'Short or long rest', anyRest: 'Short or long rest',
longRest: 'Long rest', longRest: 'Long rest',
shortRest: 'Short rest', shortRest: 'Short rest',
@@ -26,6 +29,7 @@ const actionPropertyTypeOptions = {
note: 'Note', note: 'Note',
roll: 'Roll', roll: 'Roll',
savingThrow: 'Saving throw', savingThrow: 'Saving throw',
spell: 'Spell',
toggle: 'Toggle', toggle: 'Toggle',
} }
@@ -40,10 +44,6 @@ let TriggerSchema = createPropertySchema({
optional: true, optional: true,
max: STORAGE_LIMITS.name, max: STORAGE_LIMITS.name,
}, },
summary: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
description: { description: {
type: 'inlineCalculationFieldToCompute', type: 'inlineCalculationFieldToCompute',
optional: true, optional: true,

View File

@@ -28,6 +28,7 @@ function computedOnlyField(field){
[`${field}.effects`]: { [`${field}.effects`]: {
type: Array, type: Array,
optional: true, optional: true,
removeBeforeCompute: true,
}, },
[`${field}.effects.$`]: { [`${field}.effects.$`]: {
type: Object, type: Object,

View File

@@ -110,8 +110,8 @@ export function getUserTier(user){
export function assertUserHasPaidBenefits(user){ export function assertUserHasPaidBenefits(user){
let tier = getUserTier(user); let tier = getUserTier(user);
if (!tier.paidBenefits){ if (!tier.paidBenefits){
throw new Meteor.Error('Creatures.methods.insert.denied', throw new Meteor.Error('no paid benefits',
`The ${tier.name} tier does not allow you to insert a creature`); `The ${tier.name} tier does not have the required benefits`);
} }
} }

View File

@@ -124,6 +124,7 @@ const expectedMigratedAttribute = {
damage: 3, damage: 3,
value: 17, value: 17,
constitutionMod: 2, constitutionMod: 2,
dirty: true,
} }
const exampleAttack = { const exampleAttack = {
@@ -221,6 +222,7 @@ describe('migrateProperty', function() {
prop: newAction, prop: newAction,
reversed: true, reversed: true,
}); });
delete reversedAction.dirty;
assert.deepEqual(action, exampleAction, 'action should not be bashed'); assert.deepEqual(action, exampleAction, 'action should not be bashed');
assert.deepEqual(exampleAction, reversedAction, 'operation should be reversible'); assert.deepEqual(exampleAction, reversedAction, 'operation should be reversible');
}); });

View File

@@ -24,10 +24,3 @@
}, },
} }
</script> </script>
<style lang="css">
.markdown img {
max-width: 100%;
margin: 8px 0;
}
</style>

View File

@@ -13,6 +13,16 @@
</template> </template>
<script lang="js"> <script lang="js">
import PROPERTIES from '/imports/constants/PROPERTIES.js';
const filterOptions = [];
for (let key in PROPERTIES) {
if (key === 'reference') continue;
filterOptions.push({
text: PROPERTIES[key].name,
value: key,
});
}
export default { export default {
props: { props: {
value: { value: {
@@ -22,27 +32,7 @@ export default {
}, },
data(){return { data(){return {
filterTerms: [], filterTerms: [],
filterOptions: [ filterOptions,
{text: 'Actions', value: 'action'},
{text: 'Attacks', value: 'attack'},
{text: 'Attributes', value: 'attribute'},
{text: 'Buffs', value: 'buff'},
{text: 'Class Levels', value: 'classLevel'},
{text: 'Damage Multipliers', value: 'damageMultiplier'},
{text: 'Effects', value: 'effect'},
{text: 'Experiences', value: 'experience'},
{text: 'Features', value: 'feature'},
{text: 'Folders', value: 'folder'},
{text: 'Notes', value: 'note'},
{text: 'Proficiencies', value: 'proficiency'},
{text: 'Rolls', value: 'roll'},
{text: 'Saving Throws', value: 'savingThrow'},
{text: 'Skills', value: 'skill'},
{text: 'Spell Lists', value: 'spellList'},
{text: 'Spells', value: 'spell'},
{text: 'Containers', value: 'container'},
{text: 'Items', value: 'item'},
],
}}, }},
computed: { computed: {
filter(){ filter(){

View File

@@ -201,6 +201,7 @@ export default {
}, },
librariesSelectedByCollections() { librariesSelectedByCollections() {
let ids = []; let ids = [];
if (!this.model.allowedLibraryCollections) return ids;
LibraryCollections.find({ LibraryCollections.find({
_id: { $in: this.model.allowedLibraryCollections } _id: { $in: this.model.allowedLibraryCollections }
}).forEach(collection => { }).forEach(collection => {

View File

@@ -64,6 +64,7 @@
}, },
noLinks: Boolean, noLinks: Boolean,
noIcons: Boolean, noIcons: Boolean,
editing: Boolean,
}, },
computed:{ computed:{
props(){ props(){
@@ -95,7 +96,10 @@
store.commit('pushDialogStack', { store.commit('pushDialogStack', {
component: 'creature-property-dialog', component: 'creature-property-dialog',
elementId: `breadcrumb-${id}`, elementId: `breadcrumb-${id}`,
data: {_id: id}, data: {
_id: id,
startInEditTab: this.editing,
},
}); });
} }
}, },
@@ -120,7 +124,10 @@
store.commit('pushDialogStack', { store.commit('pushDialogStack', {
component: 'creature-root-dialog', component: 'creature-root-dialog',
elementId: 'breadcrumb-root', elementId: 'breadcrumb-root',
data: {_id: this.model.ancestors[0].id}, data: {
_id: this.model.ancestors[0].id,
startInEditTab: this.editing,
},
}); });
} }
} }

View File

@@ -14,6 +14,20 @@
/> />
</template> </template>
<template v-if="model"> <template v-if="model">
<div
class="layout mb-4"
>
<template v-if="!embedded">
<breadcrumbs
:model="model"
:editing="editing"
/>
</template>
<v-spacer />
<v-chip disabled>
{{ typeName }}
</v-chip>
</div>
<v-fade-transition <v-fade-transition
mode="out-in" mode="out-in"
> >
@@ -51,17 +65,6 @@
</component> </component>
</div> </div>
<div v-else> <div v-else>
<div
class="layout mb-4"
>
<template v-if="!embedded">
<breadcrumbs :model="model" />
</template>
<v-spacer />
<v-chip disabled>
{{ typeName }}
</v-chip>
</div>
<component <component
:is="model.type + 'Viewer'" :is="model.type + 'Viewer'"
:key="_id" :key="_id"
@@ -200,7 +203,7 @@ export default {
watch: { watch: {
_id: { _id: {
immediate: true, immediate: true,
handler(newId){ handler(newId) {
this.$nextTick(() => { this.$nextTick(() => {
this.currentId = newId; this.currentId = newId;
}); });

View File

@@ -17,8 +17,11 @@
<v-card-title> <v-card-title>
{{ model.name }} {{ model.name }}
</v-card-title> </v-card-title>
<v-card-text v-if="model.description"> <v-card-text>
{{ model.description.value }} <property-description
text
:model="model.description"
/>
</v-card-text> </v-card-text>
<v-spacer /> <v-spacer />
<v-card-actions> <v-card-actions>
@@ -36,10 +39,12 @@
<script lang="js"> <script lang="js">
import CardHighlight from '/imports/ui/components/CardHighlight.vue'; import CardHighlight from '/imports/ui/components/CardHighlight.vue';
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue';
export default { export default {
components: { components: {
CardHighlight, CardHighlight,
PropertyDescription,
}, },
inject: { inject: {
theme: { theme: {

View File

@@ -41,7 +41,7 @@
style="height: 100%; transform-origin: left; transition: all 0.5s ease;" style="height: 100%; transform-origin: left; transition: all 0.5s ease;"
:style="{ :style="{
backgroundColor: barColor, backgroundColor: barColor,
transform: `scaleX(${value / maxValue})`, transform: `scaleX(${fillFraction})`,
}" }"
/> />
<div <div
@@ -92,87 +92,93 @@ import IncrementMenu from '/imports/ui/components/IncrementMenu.vue';
import isDarkColor from '/imports/ui/utility/isDarkColor.js'; import isDarkColor from '/imports/ui/utility/isDarkColor.js';
import chroma from 'chroma-js'; import chroma from 'chroma-js';
export default { export default {
components: { components: {
IncrementMenu IncrementMenu
}, },
inject: { inject: {
theme: { theme: {
default: { default: {
isDark: false, isDark: false,
},
}, },
}, },
props: { },
value: Number, props: {
maxValue: Number, value: Number,
name: String, maxValue: Number,
color: { name: String,
type: String, color: {
default() { type: String,
return this.$vuetify.theme.currentTheme.primary default() {
}, return this.$vuetify.theme.currentTheme.primary
}, },
midColor: { },
type: String, midColor: {
default: undefined, type: String,
}, default: undefined,
lowColor: { },
type: String, lowColor: {
default: undefined, type: String,
}, default: undefined,
_id: String, },
}, _id: String,
data() { },
return { data() {
editing: false, return {
hover: false, editing: false,
}; hover: false,
}, };
computed: { },
barColor() { computed: {
const fraction = this.value / this.maxValue; fillFraction() {
if (!Number.isFinite(fraction)) return this.color; let fraction = this.value / this.maxValue;
if (fraction > 0.5){ if (fraction < 0) fraction = 0;
return this.color; if (fraction > 1) fraction = 1;
} else if (this.midColor && this.lowColor) { return fraction;
return chroma.mix(this.lowColor, this.midColor, fraction * 2).hex(); },
} else if (this.midColor){ barColor() {
return this.midColor; const fraction = this.value / this.maxValue;
} if (!Number.isFinite(fraction)) return this.color;
if (fraction > 0.5){
return this.color; return this.color;
}, } else if (this.midColor && this.lowColor) {
barBackgroundColor(){ return chroma.mix(this.lowColor, this.midColor, fraction * 2).hex();
return chroma(this.barColor) } else if (this.midColor){
.darken(1.5) return this.midColor;
.desaturate(1.5)
.hex();
},
isTextLight(){
return isDarkColor(this.barBackgroundColor);
/* Change color at the halfway mark
const fraction = this.value / this.maxValue;
if (fraction >= 0.5){
return isDarkColor(this.barColor);
} else {
return isDarkColor(this.barBackgroundColor);
}
*/
} }
return this.color;
}, },
methods: { barBackgroundColor(){
edit() { return chroma(this.barColor)
this.editing = true; .darken(1.5)
}, .desaturate(1.5)
cancelEdit() { .hex();
this.editing = false; },
}, isTextLight(){
changeIncrementMenu(e){ return isDarkColor(this.barBackgroundColor);
this.$emit('change', e); /* Change color at the halfway mark
this.editing = false; const fraction = this.value / this.maxValue;
if (fraction >= 0.5){
return isDarkColor(this.barColor);
} else {
return isDarkColor(this.barBackgroundColor);
} }
}, */
}; }
},
methods: {
edit() {
this.editing = true;
},
cancelEdit() {
this.editing = false;
},
changeIncrementMenu(e){
this.$emit('change', e);
this.editing = false;
}
},
};
</script> </script>
<style> <style>

View File

@@ -8,7 +8,7 @@
<v-btn <v-btn
icon icon
small small
:disabled="model.value >= model.total || context.editPermission === false" :disabled="(model.value >= model.total && !model.ignoreUpperLimit) || context.editPermission === false"
@click="increment(1)" @click="increment(1)"
> >
<v-icon>mdi-chevron-up</v-icon> <v-icon>mdi-chevron-up</v-icon>
@@ -16,7 +16,7 @@
<v-btn <v-btn
icon icon
small small
:disabled="model.value <= 0 || context.editPermission === false" :disabled="(model.value <= 0 && !model.ignoreLowerLimit) || context.editPermission === false"
@click="increment(-1)" @click="increment(-1)"
> >
<v-icon>mdi-chevron-down</v-icon> <v-icon>mdi-chevron-down</v-icon>
@@ -28,7 +28,10 @@
<div class="text-h4"> <div class="text-h4">
{{ model.value }} {{ model.value }}
</div> </div>
<div class="text-h6 ml-2 max-value"> <div
v-if="model.total !== 0"
class="text-h6 ml-2 max-value"
>
/{{ model.total }} /{{ model.total }}
</div> </div>
</div> </div>

View File

@@ -78,7 +78,6 @@
<form-section <form-section
v-if="model.attributeType === 'healthBar'" v-if="model.attributeType === 'healthBar'"
name="Health Bar Settings" name="Health Bar Settings"
standalone
> >
<color-picker <color-picker
:value="model.healthBarColorMid" :value="model.healthBarColorMid"
@@ -90,6 +89,47 @@
label="Empty color" label="Empty color"
@input="value => $emit('change', {path: ['healthBarColorLow'], value})" @input="value => $emit('change', {path: ['healthBarColorLow'], value})"
/> />
<v-layout
wrap
class="mt-4"
>
<text-field
label="Damage order"
type="number"
style="max-width: 300px;"
hint="Lower ordered health bars will take damage before higher ordered ones"
class="mr-4"
:disabled="model.healthBarNoDamage"
:value="model.healthBarDamageOrder"
:error-messages="errors.healthBarDamageOrder"
@change="change('healthBarDamageOrder', ...arguments)"
/>
<smart-switch
label="Ignore damage"
:value="model.healthBarNoDamage"
:error-messages="errors.healthBarNoDamage"
@change="change('healthBarNoDamage', ...arguments)"
/>
</v-layout>
<v-layout wrap>
<text-field
label="Healing order"
type="number"
style="max-width: 300px;"
hint="Lower ordered health bars will take healing before higher ordered ones"
class="mr-4"
:disabled="model.healthBarNoHealing"
:value="model.healthBarHealingOrder"
:error-messages="errors.healthBarHealingOrder"
@change="change('healthBarHealingOrder', ...arguments)"
/>
<smart-switch
label="Ignore healing"
:value="model.healthBarNoHealing"
:error-messages="errors.healthBarNoHealing"
@change="change('healthBarNoHealing', ...arguments)"
/>
</v-layout>
</form-section> </form-section>
</v-expand-transition> </v-expand-transition>
@@ -121,6 +161,18 @@
:error-messages="errors.decimal" :error-messages="errors.decimal"
@change="change('decimal', ...arguments)" @change="change('decimal', ...arguments)"
/> />
<smart-switch
label="Can be damaged into negative values"
:value="model.ignoreLowerLimit"
:error-messages="errors.ignoreLowerLimit"
@change="change('ignoreLowerLimit', ...arguments)"
/>
<smart-switch
label="Can be incremented above total"
:value="model.ignoreUpperLimit"
:error-messages="errors.ignoreUpperLimit"
@change="change('ignoreUpperLimit', ...arguments)"
/>
<div <div
class="layout justify-center" class="layout justify-center"
style="align-self: stretch;" style="align-self: stretch;"

View File

@@ -1,5 +1,5 @@
<template lang="html"> <template lang="html">
<div class="slot-form"> <div class="trigger-form">
<text-field <text-field
ref="focusFirst" ref="focusFirst"
label="Name" label="Name"
@@ -8,64 +8,6 @@
@change="change('name', ...arguments)" @change="change('name', ...arguments)"
/> />
<v-layout align-center>
<v-btn
icon
style="margin-top: -30px;"
class="mr-2"
:loading="addExtraTagsLoading"
:disabled="extraTagsFull"
@click="addExtraTags"
>
<v-icon>
mdi-plus
</v-icon>
</v-btn>
<smart-combobox
label="Tags Required"
hint="The slot must be filled with a property which has all the listed tags"
multiple
chips
deletable-chips
:value="model.targetTags"
:error-messages="errors.targetTags"
@change="change('targetTags', ...arguments)"
/>
</v-layout>
<v-slide-x-transition group>
<div
v-for="(extras, i) in model.extraTags"
:key="extras._id"
class="extra-tags layout align-center justify-space-between"
>
<smart-select
label="Operation"
style="width: 90px; flex-grow: 0;"
:items="extraTagOperations"
:value="extras.operation"
:error-messages="errors.extraTags && errors.extraTags[i]"
@change="change(['extraTags', i, 'operation'], ...arguments)"
/>
<smart-combobox
label="Tags"
:hint="extras.operation === 'OR' ? 'The slot can be filled with a property that has all of these tags instead' : 'The slot cannot be filled with a property that has any of these tags'"
class="mx-2"
multiple
chips
deletable-chips
:value="extras.tags"
@change="change(['extraTags', i, 'tags'], ...arguments)"
/>
<v-btn
icon
style="margin-top: -30px;"
@click="$emit('pull', {path: ['extraTags', i]})"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
</v-slide-x-transition>
<smart-select <smart-select
label="Timing" label="Timing"
style="flex-basis: 300px;" style="flex-basis: 300px;"
@@ -95,6 +37,70 @@
@change="change('actionPropertyType', ...arguments)" @change="change('actionPropertyType', ...arguments)"
/> />
<v-layout
v-show="showTags"
align-center
>
<v-btn
icon
style="margin-top: -30px;"
class="mr-2"
:loading="addExtraTagsLoading"
:disabled="extraTagsFull"
@click="addExtraTags"
>
<v-icon>
mdi-plus
</v-icon>
</v-btn>
<smart-combobox
label="Tags Required"
hint="The trigger will be fired by a property which has all the listed tags"
multiple
chips
deletable-chips
:value="model.targetTags"
:error-messages="errors.targetTags"
@change="change('targetTags', ...arguments)"
/>
</v-layout>
<v-slide-x-transition
v-show="showTags"
group
>
<div
v-for="(extras, i) in model.extraTags"
:key="extras._id"
class="extra-tags layout align-center justify-space-between"
>
<smart-select
label="Operation"
style="width: 90px; flex-grow: 0;"
:items="extraTagOperations"
:value="extras.operation"
:error-messages="errors.extraTags && errors.extraTags[i]"
@change="change(['extraTags', i, 'operation'], ...arguments)"
/>
<smart-combobox
label="Tags"
:hint="extras.operation === 'OR' ? 'The trigger can be fired by a property that has all of these tags instead' : 'The trigger won\'t be fired by a property that has any of these tags'"
class="mx-2"
multiple
chips
deletable-chips
:value="extras.tags"
@change="change(['extraTags', i, 'tags'], ...arguments)"
/>
<v-btn
icon
style="margin-top: -30px;"
@click="$emit('pull', {path: ['extraTags', i]})"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
</v-slide-x-transition>
<computed-field <computed-field
label="Condition" label="Condition"
hint="A caclulation to determine if this trigger should fire" hint="A caclulation to determine if this trigger should fire"
@@ -105,15 +111,6 @@
$emit('change', {path: ['condition', ...path], value, ack})" $emit('change', {path: ['condition', ...path], value, ack})"
/> />
<inline-computation-field
label="Summary"
hint="This will appear in the feature card in the character sheet"
:model="model.summary"
:error-messages="errors.summary"
@change="({path, value, ack}) =>
$emit('change', {path: ['summary', ...path], value, ack})"
/>
<inline-computation-field <inline-computation-field
label="Description" label="Description"
hint="The rest of the description that doesn't fit in the summary goes here" hint="The rest of the description that doesn't fit in the summary goes here"
@@ -183,6 +180,11 @@ export default {
if (!this.model.extraTags) return false; if (!this.model.extraTags) return false;
let maxCount = TriggerSchema.get('extraTags', 'maxCount'); let maxCount = TriggerSchema.get('extraTags', 'maxCount');
return this.model.extraTags.length >= maxCount; return this.model.extraTags.length >= maxCount;
},
showTags() {
return this.model.event !== 'shortRest' &&
this.model.event !== 'longRest' &&
this.model.event !== 'anyRest';
} }
}, },
methods: { methods: {

View File

@@ -1,10 +1,18 @@
<template lang="html"> <template lang="html">
<div class="trigger-viewer"> <div class="trigger-viewer">
<v-row dense> <v-row dense>
<property-field
name="Timing"
:value="timingText"
/>
<property-field <property-field
name="Event" name="Event"
:value="eventText" :value="eventText"
/> />
<property-field
name="Event Type"
:value="actionPropertyText"
/>
<property-field <property-field
v-if="(model.targetTags && model.targetTags.length) || (model.extraTags && model.extraTags.length)" v-if="(model.targetTags && model.targetTags.length) || (model.extraTags && model.extraTags.length)"
name="Tags Required" name="Tags Required"
@@ -23,10 +31,6 @@
</div> </div>
</div> </div>
</property-field> </property-field>
<property-description
name="Summary"
:model="model.summary"
/>
<property-description <property-description
name="Description" name="Description"
:model="model.description" :model="model.description"
@@ -36,39 +40,34 @@
</template> </template>
<script lang="js"> <script lang="js">
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js' import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js'
import { getPropertyName } from '/imports/constants/PROPERTIES.js'; import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import FillSlotButton from '/imports/ui/creature/buildTree/FillSlotButton.vue'; import { timingOptions, eventOptions, actionPropertyTypeOptions } from '/imports/api/properties/Triggers.js';
const eventText = { export default {
doActionProperty: 'Do action property', mixins: [propertyViewerMixin],
receiveActionProperty: 'Receiving action property', inject: {
flipToggle: 'Toggle changed', context: {
adjustProperty: 'Attribute adjusted', default: {},
anyRest: 'Short or long rest', },
longRest: 'Long rest', },
shortRest: 'Short rest', computed: {
slotTypeName(){
if (!this.model.slotType) return;
return getPropertyName(this.model.slotType);
},
timingText(){
if (!this.model.timing) return;
return timingOptions[this.model.timing];
},
actionPropertyText(){
if (!this.model.actionPropertyType) return;
return actionPropertyTypeOptions[this.model.actionPropertyType];
},
eventText(){
if (!this.model.event) return;
return eventOptions[this.model.event];
},
} }
}
export default {
components: {
FillSlotButton,
},
mixins: [propertyViewerMixin],
inject: {
context: {
default: {},
},
},
computed: {
slotTypeName(){
if (!this.model.slotType) return;
return getPropertyName(this.model.slotType);
},
eventText(){
if (!this.model.event) return;
return eventText[this.model.event]
},
}
}
</script> </script>

View File

@@ -51,6 +51,7 @@
<div <div
v-if="calculation && calculation.effects" v-if="calculation && calculation.effects"
class="flex-grow-1" class="flex-grow-1"
style="max-width: 100%;"
> >
<inline-effect <inline-effect
v-if="typeof calculation.value === 'number'" v-if="typeof calculation.value === 'number'"

View File

@@ -0,0 +1,94 @@
.markdown {
width: 100%;
}
.markdown h1,
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
line-height: initial;
margin-bottom: 12px;
}
.markdown h1:not(:first-child),
.markdown h2:not(:first-child),
.markdown h3:not(:first-child),
.markdown h4:not(:first-child),
.markdown h5:not(:first-child),
.markdown h6:not(:first-child) {
margin-top: 24px;
}
:not(:first-child) .markdown h1 {
font-size: 2rem;
letter-spacing: -.0083333333em;
font-weight: 300;
}
.markdown h2 {
font-size: 1.5rem;
font-weight: 400;
}
.markdown h3 {
font-size: 1.25rem;
letter-spacing: .0073529412em;
font-weight: 400;
}
.markdown h4 {
font-size: 1rem;
letter-spacing: .0125em;
font-weight: 500;
}
.markdown h5 {
font-size: 0.95rem;
letter-spacing: .0125em;
font-weight: 500;
}
.markdown h6 {
font-size: 0.875rem;
letter-spacing: .0125em;
font-weight: 500;
}
.markdown pre code {
display: block;
overflow: auto;
}
.theme--dark.v-application .markdown hr {
border-color: rgba(255, 255, 255, 0.2);
}
.markdown img {
max-width: 100%;
margin: 8px 0;
}
.markdown table {
min-width: 100%;
border-spacing: 0;
margin-bottom: 16px;
}
.markdown th {
font-weight: initial;
padding: 4px;
}
.markdown td {
padding: 2px 4px;
}
.markdown tbody>tr:nth-child(odd) {
background-color: rgba(0, 0, 0, 0.1)
}
.theme--dark.v-application .markdown tbody>tr:nth-child(odd) {
background-color: rgba(255, 255, 255, 0.1)
}

View File

@@ -6,5 +6,6 @@ import './fitAvatars.css';
import './inheritBackgrounds.css'; import './inheritBackgrounds.css';
import './largeFormatInputs.css'; import './largeFormatInputs.css';
import './lineClamp.css'; import './lineClamp.css';
import './markdown.css';
import './speedDial.css'; import './speedDial.css';
import './toolbarFlex.css'; import './toolbarFlex.css';