Compare commits
45 Commits
2.0-beta.3
...
2.0-beta.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d66c06107 | ||
|
|
21629138f0 | ||
|
|
59a488256b | ||
|
|
766519b4a3 | ||
|
|
e7f73d0e54 | ||
|
|
193d5eec50 | ||
|
|
9284c9ad76 | ||
|
|
f86152675f | ||
|
|
cbac5264cd | ||
|
|
34e3325464 | ||
|
|
79c9e67ce2 | ||
|
|
4c2aabf90d | ||
|
|
48331d3806 | ||
|
|
45f05d0d34 | ||
|
|
58629c92f4 | ||
|
|
719af548f0 | ||
|
|
f2a1861279 | ||
|
|
38c3b6ff1f | ||
|
|
23e848fe40 | ||
|
|
4d6cdf50bd | ||
|
|
1cf9f3b5fd | ||
|
|
8164b79667 | ||
|
|
360df79004 | ||
|
|
a8f163ff33 | ||
|
|
36b3b80850 | ||
|
|
1d22f4c054 | ||
|
|
99e4e8d6bb | ||
|
|
2bb3265356 | ||
|
|
263f2d8424 | ||
|
|
ee0e764294 | ||
|
|
13fc0c0b12 | ||
|
|
ecfeeaccd9 | ||
|
|
b324fb1f03 | ||
|
|
8d34cc1369 | ||
|
|
839c2488b2 | ||
|
|
fd79bc2bb3 | ||
|
|
1050442606 | ||
|
|
53ed271ea2 | ||
|
|
6ccbf204eb | ||
|
|
d44d4e0315 | ||
|
|
2b8f7e4927 | ||
|
|
65e7ce6dce | ||
|
|
24cc87d6f7 | ||
|
|
03578b2066 | ||
|
|
6ea882a053 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build
|
||||
@@ -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;
|
||||
@@ -2,8 +2,9 @@ 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 getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.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({
|
||||
name: 'creatureProperties.damage',
|
||||
@@ -20,58 +21,105 @@ const damageProperty = new ValidatedMethod({
|
||||
numRequests: 20,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, operation, value}) {
|
||||
// Check permissions
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
if (!property) throw new Meteor.Error(
|
||||
run({ _id, operation, value }) {
|
||||
|
||||
// Get action context
|
||||
const prop = CreatureProperties.findOne(_id);
|
||||
if (!prop) throw new Meteor.Error(
|
||||
'Damage property failed', 'Property doesn\'t exist'
|
||||
);
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
const creatureId = prop.ancestors[0].id;
|
||||
const actionContext = new ActionContext(creatureId, [creatureId], this);
|
||||
|
||||
// Check permissions
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
|
||||
// Check if property can take damage
|
||||
let schema = CreatureProperties.simpleSchema(property);
|
||||
let schema = CreatureProperties.simpleSchema(prop);
|
||||
if (!schema.allowsKey('damage')){
|
||||
throw new Meteor.Error(
|
||||
'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;
|
||||
},
|
||||
});
|
||||
|
||||
export function damagePropertyWork({property, operation, value}){
|
||||
let damage, newValue;
|
||||
export function damagePropertyWork({ prop, operation, value, actionContext }) {
|
||||
|
||||
// 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'){
|
||||
const total = property.total || 0;
|
||||
const total = prop.total || 0;
|
||||
// Set represents what we want the value to be after damage
|
||||
// So we need the actual damage to get to that value
|
||||
damage = total - value;
|
||||
// Damage can't exceed total value
|
||||
if (damage > total) damage = total;
|
||||
if (damage > total && !prop.ignoreLowerLimit) damage = total;
|
||||
// Damage must be positive
|
||||
if (damage < 0) damage = 0;
|
||||
newValue = property.total - damage;
|
||||
if (damage < 0 && !prop.ignoreUpperLimit) damage = 0;
|
||||
newValue = prop.total - damage;
|
||||
// Write the results
|
||||
CreatureProperties.update(prop._id, {
|
||||
$set: { damage, value: newValue, dirty: true }
|
||||
}, {
|
||||
selector: prop
|
||||
});
|
||||
} else if (operation === 'increment'){
|
||||
let currentValue = property.value || 0;
|
||||
let currentDamage = property.damage || 0;
|
||||
let increment = value;
|
||||
let currentValue = prop.value || 0;
|
||||
let currentDamage = prop.damage || 0;
|
||||
increment = 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
|
||||
if (-increment > currentDamage) increment = -currentDamage;
|
||||
if (-increment > currentDamage && !prop.ignoreUpperLimit) increment = -currentDamage;
|
||||
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
|
||||
CreatureProperties.update(property._id, {
|
||||
$set: {damage, value: newValue, dirty: true}
|
||||
}, {
|
||||
selector: property
|
||||
});
|
||||
return damage;
|
||||
applyTriggers(actionContext.triggers?.damageProperty?.after, prop, actionContext);
|
||||
|
||||
if (operation === 'set') {
|
||||
return damage;
|
||||
} else if (operation === 'increment') {
|
||||
return increment;
|
||||
}
|
||||
}
|
||||
|
||||
export default damageProperty;
|
||||
|
||||
@@ -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;
|
||||
@@ -24,7 +24,9 @@ export default function getSlotFillFilter({slot, libraryIds}){
|
||||
slotFillerType: 'classLevel',
|
||||
}]
|
||||
});
|
||||
filter.variableName = slot.variableName;
|
||||
if (slot.variableName) {
|
||||
filter.variableName = slot.variableName;
|
||||
}
|
||||
|
||||
// Only search for levels the class needs
|
||||
if (slot.missingLevels && slot.missingLevels.length) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
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/dealDamage.js';
|
||||
import '/imports/api/creature/creatureProperties/methods/duplicateProperty.js';
|
||||
import '/imports/api/creature/creatureProperties/methods/equipItem.js';
|
||||
import '/imports/api/creature/creatureProperties/methods/insertProperty.js';
|
||||
|
||||
@@ -38,6 +38,11 @@ let CreatureSettingsSchema = new SimpleSchema({
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Hide calculation errors
|
||||
hideCalculationErrors: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// How much each hitDice resets on a long rest
|
||||
hitDiceResetMultiplier: {
|
||||
type: Number,
|
||||
|
||||
@@ -3,12 +3,9 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import { groupBy, remove, rest, union } from 'lodash';
|
||||
import {
|
||||
getCreature, getVariables, getPropertiesOfType
|
||||
} 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';
|
||||
import { union } from 'lodash';
|
||||
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
|
||||
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
const restCreature = new ValidatedMethod({
|
||||
name: 'creature.methods.rest',
|
||||
@@ -27,59 +24,37 @@ const restCreature = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({creatureId, restType}) {
|
||||
run({ creatureId, restType }) {
|
||||
// Get action context
|
||||
const actionContext = new ActionContext(creatureId, [creatureId], this);
|
||||
// Check permissions
|
||||
let creature = getCreature(creatureId);
|
||||
assertEditPermission(creature, this.userId);
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
|
||||
// Add the variables to the creature document
|
||||
const variables = getVariables(creatureId);
|
||||
delete variables._id;
|
||||
delete variables._creatureId;
|
||||
creature.variables = variables;
|
||||
const scope = creature.variables;
|
||||
// Join, sort, and apply before triggers
|
||||
const beforeTriggers = union(
|
||||
actionContext.triggers.anyRest?.before, actionContext.triggers[restType]?.before
|
||||
).sort((a, b) => a.order - b.order);
|
||||
applyTriggers(beforeTriggers, null, actionContext);
|
||||
|
||||
// Get the triggers
|
||||
let triggers = getPropertiesOfType(creatureId, 'trigger');
|
||||
remove(triggers, trigger =>
|
||||
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,
|
||||
// Rest
|
||||
actionContext.addLog({
|
||||
name: restType === 'shortRest' ? 'Short rest' : 'Long rest',
|
||||
});
|
||||
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 });
|
||||
doRestWork(creature, restType);
|
||||
applyTriggers(triggers, restType, 'after', { creature, targets, scope, log });
|
||||
|
||||
insertCreatureLogWork({log, creature, method: this});
|
||||
// Insert log
|
||||
actionContext.writeLog();
|
||||
},
|
||||
});
|
||||
|
||||
function applyTriggers(triggers, restType, timing, opts) {
|
||||
// Get matching triggers
|
||||
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) {
|
||||
function doRestWork(restType, actionContext) {
|
||||
const creatureId = actionContext.creature._id;
|
||||
// Long rests reset short rest properties as well
|
||||
let resetFilter;
|
||||
if (restType === 'shortRest'){
|
||||
@@ -89,7 +64,7 @@ function doRestWork(creature, restType) {
|
||||
}
|
||||
// Only apply to active properties
|
||||
let filter = {
|
||||
'ancestors.id': creature._id,
|
||||
'ancestors.id': creatureId,
|
||||
reset: resetFilter,
|
||||
removed: { $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
|
||||
if (restType === 'longRest'){
|
||||
let hitDice = CreatureProperties.find({
|
||||
'ancestors.id': creature._id,
|
||||
'ancestors.id': creatureId,
|
||||
type: 'attribute',
|
||||
attributeType: 'hitDice',
|
||||
removed: {$ne: true},
|
||||
@@ -132,7 +107,7 @@ function doRestWork(creature, restType) {
|
||||
fields: {
|
||||
hitDiceSize: 1,
|
||||
damage: 1,
|
||||
value: 1,
|
||||
total: 1,
|
||||
}
|
||||
}).fetch();
|
||||
// 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)
|
||||
hitDice.sort(compare);
|
||||
// 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 resetMultiplier = creature.settings.hitDiceResetMultiplier || 0.5;
|
||||
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0);
|
||||
let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5;
|
||||
let recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1);
|
||||
// recover each hit dice in turn until the recoverable amount is used up
|
||||
let amountToRecover, resultingDamage;
|
||||
|
||||
78
app/imports/api/engine/actions/ActionContext.js
Normal file
78
app/imports/api/engine/actions/ActionContext.js
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,19 @@ import action from './applyPropertyByType/applyAction.js';
|
||||
import adjustment from './applyPropertyByType/applyAdjustment.js';
|
||||
import branch from './applyPropertyByType/applyBranch.js';
|
||||
import buff from './applyPropertyByType/applyBuff.js';
|
||||
import buffRemover from './applyPropertyByType/applyBuffRemover.js';
|
||||
import damage from './applyPropertyByType/applyDamage.js';
|
||||
import note from './applyPropertyByType/applyNote.js';
|
||||
import roll from './applyPropertyByType/applyRoll.js';
|
||||
import savingThrow from './applyPropertyByType/applySavingThrow.js';
|
||||
import toggle from './applyPropertyByType/applyToggle.js';
|
||||
import applyTriggers from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
const applyPropertyByType = {
|
||||
action,
|
||||
adjustment,
|
||||
branch,
|
||||
buff,
|
||||
buffRemover,
|
||||
damage,
|
||||
note,
|
||||
roll,
|
||||
@@ -22,9 +23,7 @@ const applyPropertyByType = {
|
||||
toggle,
|
||||
};
|
||||
|
||||
export default function applyProperty(node, opts, ...rest) {
|
||||
applyTriggers(node, opts, 'before');
|
||||
opts.scope[`#${node.node.type}`] = node.node;
|
||||
applyPropertyByType[node.node.type]?.(node, opts, ...rest);
|
||||
applyTriggers(node, opts, 'after');
|
||||
export default function applyProperty(node, actionContext, ...rest) {
|
||||
actionContext.scope[`#${node.node.type}`] = node.node;
|
||||
applyPropertyByType[node.node.type]?.(node, actionContext, ...rest);
|
||||
}
|
||||
|
||||
@@ -6,23 +6,24 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
|
||||
import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
|
||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.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;
|
||||
if (prop.target === 'self') targets = [creature];
|
||||
if (prop.target === 'self') actionContext.targets = [actionContext.creature];
|
||||
const targets = actionContext.targets;
|
||||
|
||||
// Log the name and summary
|
||||
let content = { name: prop.name };
|
||||
if (prop.summary?.text){
|
||||
recalculateInlineCalculations(prop.summary, scope, log);
|
||||
recalculateInlineCalculations(prop.summary, actionContext);
|
||||
content.value = prop.summary.value;
|
||||
}
|
||||
if (content.name || content.value){
|
||||
log.content.push(content);
|
||||
}
|
||||
actionContext.addLog(content);
|
||||
|
||||
// Spend the resources
|
||||
const failed = spendResources({prop, log, scope});
|
||||
const failed = spendResources(prop, actionContext);
|
||||
if (failed) return;
|
||||
|
||||
const attack = prop.attackRoll || prop.attackRollBonus;
|
||||
@@ -31,28 +32,29 @@ export default function applyAction(node, {creature, targets, scope, log}){
|
||||
if (attack && attack.calculation){
|
||||
if (targets.length){
|
||||
targets.forEach(target => {
|
||||
applyAttackToTarget({attack, target, scope, log});
|
||||
applyAttackToTarget({attack, target, actionContext});
|
||||
// Apply the children, but only to the current target
|
||||
applyChildren(node, {creature, targets: [target], scope, log});
|
||||
actionContext.targets = [target];
|
||||
applyChildren(node, actionContext);
|
||||
});
|
||||
} else {
|
||||
applyAttackWithoutTarget({attack, scope, log});
|
||||
applyChildren(node, {creature, targets, scope, log});
|
||||
applyAttackWithoutTarget({attack, actionContext});
|
||||
applyChildren(node, actionContext);
|
||||
}
|
||||
} else {
|
||||
applyChildren(node, {creature, targets, scope, log});
|
||||
applyChildren(node, actionContext);
|
||||
}
|
||||
}
|
||||
|
||||
function applyAttackWithoutTarget({attack, scope, log}){
|
||||
delete scope['$attackHit'];
|
||||
delete scope['$attackMiss'];
|
||||
delete scope['$criticalHit'];
|
||||
delete scope['$criticalMiss'];
|
||||
delete scope['$attackRoll'];
|
||||
|
||||
recalculateCalculation(attack, scope, log);
|
||||
function applyAttackWithoutTarget({attack, actionContext}){
|
||||
delete actionContext.scope['$attackHit'];
|
||||
delete actionContext.scope['$attackMiss'];
|
||||
delete actionContext.scope['$criticalHit'];
|
||||
delete actionContext.scope['$criticalMiss'];
|
||||
delete actionContext.scope['$attackRoll'];
|
||||
|
||||
recalculateCalculation(attack, actionContext);
|
||||
const scope = actionContext.scope;
|
||||
let {
|
||||
resultPrefix,
|
||||
result,
|
||||
@@ -72,14 +74,15 @@ function applyAttackWithoutTarget({attack, scope, log}){
|
||||
scope['$attackMiss'] = {value: true};
|
||||
}
|
||||
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name,
|
||||
value: `${resultPrefix}\n**${result}**`,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
function applyAttackToTarget({attack, target, scope, log}){
|
||||
function applyAttackToTarget({attack, target, actionContext}){
|
||||
const scope = actionContext.scope;
|
||||
delete scope['$attackHit'];
|
||||
delete scope['$attackMiss'];
|
||||
delete scope['$criticalHit'];
|
||||
@@ -87,7 +90,7 @@ function applyAttackToTarget({attack, target, scope, log}){
|
||||
delete scope['$attackDiceRoll'];
|
||||
delete scope['$attackRoll'];
|
||||
|
||||
recalculateCalculation(attack, scope, log);
|
||||
recalculateCalculation(attack, actionContext);
|
||||
|
||||
let {
|
||||
resultPrefix,
|
||||
@@ -108,7 +111,7 @@ function applyAttackToTarget({attack, target, scope, log}){
|
||||
name += ' (Disadvantage)';
|
||||
}
|
||||
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name,
|
||||
value: `${resultPrefix}\n**${result}**`,
|
||||
inline: true,
|
||||
@@ -119,11 +122,11 @@ function applyAttackToTarget({attack, target, scope, log}){
|
||||
scope['$attackHit'] = {value: true};
|
||||
}
|
||||
} else {
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value:'Target has no `armor`',
|
||||
});
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
|
||||
value: `${resultPrefix}\n**${result}**`,
|
||||
inline: true,
|
||||
@@ -177,14 +180,15 @@ function applyCrits(value, scope){
|
||||
return {criticalHit, criticalMiss};
|
||||
}
|
||||
|
||||
function applyChildren(node, args){
|
||||
node.children.forEach(child => applyProperty(child, args));
|
||||
function applyChildren(node, actionContext) {
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
|
||||
function spendResources({prop, log, scope}){
|
||||
function spendResources(prop, actionContext){
|
||||
// Check Uses
|
||||
if (prop.usesLeft <= 0){
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: `${prop.name || 'action'} does not have enough uses left`,
|
||||
});
|
||||
@@ -192,7 +196,7 @@ function spendResources({prop, log, scope}){
|
||||
}
|
||||
// Resources
|
||||
if (prop.insufficientResources){
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: 'This creature doesn\'t have sufficient resources to perform this action',
|
||||
});
|
||||
@@ -204,7 +208,7 @@ function spendResources({prop, log, scope}){
|
||||
let gainLog = [];
|
||||
try {
|
||||
prop.resources.itemsConsumed.forEach(itemConsumed => {
|
||||
recalculateCalculation(itemConsumed.quantity, scope, log);
|
||||
recalculateCalculation(itemConsumed.quantity, actionContext);
|
||||
if (!itemConsumed.itemId){
|
||||
throw 'No ammo was selected for this prop';
|
||||
}
|
||||
@@ -235,7 +239,7 @@ function spendResources({prop, log, scope}){
|
||||
}
|
||||
});
|
||||
} catch (e){
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: e,
|
||||
});
|
||||
@@ -253,7 +257,7 @@ function spendResources({prop, log, scope}){
|
||||
}, {
|
||||
selector: prop
|
||||
});
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Uses left',
|
||||
value: prop.usesLeft - 1,
|
||||
inline: true,
|
||||
@@ -262,18 +266,19 @@ function spendResources({prop, log, scope}){
|
||||
|
||||
// Damage stats
|
||||
prop.resources.attributesConsumed.forEach(attConsumed => {
|
||||
recalculateCalculation(attConsumed.quantity, scope, log);
|
||||
recalculateCalculation(attConsumed.quantity, actionContext);
|
||||
|
||||
if (!attConsumed.quantity?.value) return;
|
||||
let stat = scope[attConsumed.variableName];
|
||||
let stat = actionContext.scope[attConsumed.variableName];
|
||||
if (!stat){
|
||||
spendLog.push(stat.name + ': ' + ' not found');
|
||||
return;
|
||||
}
|
||||
damagePropertyWork({
|
||||
property: stat,
|
||||
prop: stat,
|
||||
operation: 'increment',
|
||||
value: attConsumed.quantity.value,
|
||||
actionContext,
|
||||
});
|
||||
if (attConsumed.quantity.value > 0){
|
||||
spendLog.push(stat.name + ': ' + attConsumed.quantity.value);
|
||||
@@ -283,12 +288,12 @@ function spendResources({prop, log, scope}){
|
||||
});
|
||||
|
||||
// Log all the spending
|
||||
if (gainLog.length) log.content.push({
|
||||
if (gainLog.length) actionContext.addLog({
|
||||
name: 'Gained',
|
||||
value: gainLog.join('\n'),
|
||||
inline: true,
|
||||
});
|
||||
if (spendLog.length) log.content.push({
|
||||
if (spendLog.length) actionContext.addLog({
|
||||
name: 'Spent',
|
||||
value: spendLog.join('\n'),
|
||||
inline: true,
|
||||
|
||||
@@ -1,41 +1,42 @@
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import recalculateCalculation from './shared/recalculateCalculation.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, {
|
||||
creature, targets, scope, log
|
||||
}){
|
||||
export default function applyAdjustment(node, actionContext){
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
const damageTargets = prop.target === 'self' ? [creature] : targets;
|
||||
const damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
||||
|
||||
if (!prop.amount) {
|
||||
return applyChildren(node, {creature, targets, scope, log});
|
||||
return applyChildren(node, actionContext);
|
||||
}
|
||||
|
||||
// Evaluate the amount
|
||||
recalculateCalculation(prop.amount, scope, log);
|
||||
recalculateCalculation(prop.amount, actionContext);
|
||||
|
||||
const value = +prop.amount.value;
|
||||
if (!isFinite(value)) {
|
||||
return applyChildren(node, {creature, targets, scope, log});
|
||||
return applyChildren(node, actionContext);
|
||||
}
|
||||
|
||||
if (damageTargets?.length) {
|
||||
damageTargets.forEach(target => {
|
||||
let stat = target.variables[prop.stat];
|
||||
if (!stat?.type) {
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
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({
|
||||
property: stat,
|
||||
prop: stat,
|
||||
operation: prop.operation,
|
||||
value: value,
|
||||
value,
|
||||
actionContext,
|
||||
});
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Attribute damage',
|
||||
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
|
||||
` ${value}`,
|
||||
@@ -43,7 +44,7 @@ export default function applyAdjustment(node, {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Attribute damage',
|
||||
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
|
||||
` ${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){
|
||||
node.children.forEach(child => applyProperty(child, args));
|
||||
function applyChildren(node, actionContext){
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import recalculateCalculation from './shared/recalculateCalculation.js';
|
||||
import rollDice from '/imports/parser/rollDice.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applyBranch(node, {
|
||||
creature, targets, scope, log
|
||||
}){
|
||||
export default function applyBranch(node, actionContext){
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const applyChildren = function(){
|
||||
node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
};
|
||||
const scope = actionContext.scope;
|
||||
const targets = actionContext.targets;
|
||||
const prop = node.node;
|
||||
switch(prop.branchType){
|
||||
case 'if':
|
||||
recalculateCalculation(prop.condition, scope, log);
|
||||
recalculateCalculation(prop.condition, actionContext);
|
||||
if (prop.condition?.value) applyChildren();
|
||||
break;
|
||||
case 'index':
|
||||
if (node.children.length){
|
||||
recalculateCalculation(prop.condition, scope, log);
|
||||
recalculateCalculation(prop.condition, actionContext);
|
||||
if (!isFinite(prop.condition?.value)) {
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Branch Error',
|
||||
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);
|
||||
if (index < 1) index = 1;
|
||||
if (index > node.children.length) index = node.children.length;
|
||||
applyProperty(node.children[index - 1], {
|
||||
creature, targets, scope, log
|
||||
});
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
applyProperty(node.children[index - 1], actionContext);
|
||||
}
|
||||
break;
|
||||
case 'hit':
|
||||
if (scope['$attackHit']?.value){
|
||||
if (!targets.length) log.content.push({value: '**On hit**'});
|
||||
if (!targets.length) actionContext.addLog({value: '**On hit**'});
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'miss':
|
||||
if (scope['$attackMiss']?.value){
|
||||
if (!targets.length) log.content.push({value: '**On miss**'});
|
||||
if (!targets.length) actionContext.addLog({value: '**On miss**'});
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'failedSave':
|
||||
if (scope['$saveFailed']?.value){
|
||||
if (!targets.length) log.content.push({value: '**On failed save**'});
|
||||
if (!targets.length) actionContext.addLog({value: '**On failed save**'});
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'successfulSave':
|
||||
if (scope['$saveSucceeded']?.value){
|
||||
if (!targets.length) log.content.push({value: '**On save**',});
|
||||
if (!targets.length) actionContext.addLog({value: '**On save**',});
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'random':
|
||||
if (node.children.length){
|
||||
let index = rollDice(1, node.children.length)[0] - 1;
|
||||
applyProperty(node.children[index], {
|
||||
creature, targets, scope, log
|
||||
});
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
applyProperty(node.children[index], actionContext);
|
||||
}
|
||||
break;
|
||||
case 'eachTarget':
|
||||
if (targets.length){
|
||||
if (targets.length) {
|
||||
targets.forEach(target => {
|
||||
node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets: [target], scope, log
|
||||
}));
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
actionContext.targets = [target]
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
});
|
||||
} else {
|
||||
applyChildren();
|
||||
|
||||
@@ -12,21 +12,27 @@ import symbol from '/imports/parser/parseTree/symbol.js';
|
||||
import logErrors from './shared/logErrors.js';
|
||||
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.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;
|
||||
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
|
||||
let propList = [prop];
|
||||
function addChildrenToPropList(children){
|
||||
function addChildrenToPropList(children, { skipCrystalize } = {}){
|
||||
children.forEach(child => {
|
||||
if (skipCrystalize) child.node._skipCrystalize = true;
|
||||
propList.push(child.node);
|
||||
addChildrenToPropList(child.children);
|
||||
// recursively add the child's children, but don't crystalize nested buffs
|
||||
addChildrenToPropList(child.children, {
|
||||
skipCrystalize: skipCrystalize || child.node.type === 'buff'
|
||||
});
|
||||
});
|
||||
}
|
||||
addChildrenToPropList(node.children);
|
||||
crystalizeVariables({propList, scope, log});
|
||||
crystalizeVariables({propList, actionContext});
|
||||
|
||||
let oldParent = {
|
||||
id: prop.parent.id,
|
||||
@@ -38,9 +44,9 @@ export default function applyBuff(node, {creature, targets, scope, log}){
|
||||
|
||||
//Log the buff
|
||||
if (prop.name || prop.description?.value){
|
||||
if (target._id === creature._id){
|
||||
if (target._id === actionContext.creature._id){
|
||||
// Targeting self
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: prop.name,
|
||||
value: prop.description?.value,
|
||||
});
|
||||
@@ -58,6 +64,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
|
||||
}
|
||||
@@ -83,8 +90,12 @@ function copyNodeListToTarget(propList, target, oldParent){
|
||||
* Replaces all variables with their resolved values
|
||||
* except variables of the form `$target.thing.total` become `thing.total`
|
||||
*/
|
||||
function crystalizeVariables({propList, scope, log}){
|
||||
function crystalizeVariables({propList, actionContext}){
|
||||
propList.forEach(prop => {
|
||||
if (prop._skipCrystalize) {
|
||||
delete prop._skipCrystalize;
|
||||
return;
|
||||
}
|
||||
computedSchemas[prop.type].computedFields().forEach( calcKey => {
|
||||
applyFnToKey(prop, calcKey, (prop, key) => {
|
||||
const calcObj = get(prop, key);
|
||||
@@ -104,7 +115,7 @@ function crystalizeVariables({propList, scope, log}){
|
||||
}
|
||||
} else {
|
||||
// Can't strip symbols
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: 'Variable `$target` should not be used without a property: $target.property',
|
||||
});
|
||||
@@ -112,8 +123,8 @@ function crystalizeVariables({propList, scope, log}){
|
||||
return node;
|
||||
} else {
|
||||
// Resolve all other variables
|
||||
const {result, context} = resolve('reduce', node, scope);
|
||||
logErrors(context.errors, log);
|
||||
const {result, context} = resolve('reduce', node, actionContext.scope);
|
||||
logErrors(context.errors, actionContext);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { findLast, difference, intersection, filter } from 'lodash';
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import { getProperyAncestors, getPropertiesOfType } from '/imports/api/engine/loadCreatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { softRemove } from '/imports/api/parenting/softRemove.js';
|
||||
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
|
||||
|
||||
export default function applyBuffRemover(node, actionContext) {
|
||||
// Apply triggers
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
|
||||
const prop = node.node;
|
||||
|
||||
// Log Name
|
||||
if (prop.name){
|
||||
actionContext.addLog({ name: prop.name });
|
||||
}
|
||||
|
||||
// Remove buffs
|
||||
if (prop.targetParentBuff) {
|
||||
// Remove nearest ancestor buff
|
||||
const ancestors = getProperyAncestors(actionContext.creature._id, prop._id);
|
||||
const nearestBuff = findLast(ancestors, ancestor => ancestor.type === 'buff');
|
||||
if (!nearestBuff) {
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: 'Buff remover does not have a parent buff to remove',
|
||||
});
|
||||
return;
|
||||
}
|
||||
removeBuff(nearestBuff, actionContext);
|
||||
} else {
|
||||
// Get all the buffs targeted by tags
|
||||
const allBuffs = getPropertiesOfType(actionContext.creature._id, 'buff');
|
||||
const targetedBuffs = filter(allBuffs, buff => {
|
||||
if (buff.inactive) return false;
|
||||
if (buffRemoverMatchTags(prop, buff)) return true;
|
||||
});
|
||||
// Remove the buffs
|
||||
if (prop.removeAll) {
|
||||
// Remove all matching buffs
|
||||
targetedBuffs.forEach(buff => {
|
||||
removeBuff(buff, actionContext);
|
||||
});
|
||||
} else {
|
||||
// Sort in reverse order
|
||||
targetedBuffs.sort((a, b) => b.order - a.order);
|
||||
// Remove the one with the highest order
|
||||
const buff = targetedBuffs[0];
|
||||
if (buff) {
|
||||
removeBuff(buff, actionContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply triggers
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
// Apply children
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
|
||||
function removeBuff(buff, actionContext) {
|
||||
actionContext.addLog({
|
||||
name: 'Removed',
|
||||
value: `${buff.name || 'Buff'}`
|
||||
});
|
||||
softRemove({ _id: buff._id, collection: CreatureProperties });
|
||||
}
|
||||
|
||||
function buffRemoverMatchTags(buffRemover, prop) {
|
||||
let matched = false;
|
||||
const propTags = getEffectivePropTags(prop);
|
||||
// Check the target tags
|
||||
if (
|
||||
!buffRemover.targetTags?.length ||
|
||||
difference(buffRemover.targetTags, propTags).length === 0
|
||||
) {
|
||||
matched = true;
|
||||
}
|
||||
// Check the extra tags
|
||||
buffRemover.extraTags?.forEach(extra => {
|
||||
if (extra.operation === 'OR') {
|
||||
if (matched) return;
|
||||
if (
|
||||
!extra.tags.length ||
|
||||
difference(extra.tags, propTags).length === 0
|
||||
) {
|
||||
matched = true;
|
||||
}
|
||||
} else if (extra.operation === 'NOT') {
|
||||
if (
|
||||
extra.tags.length &&
|
||||
intersection(extra.tags, propTags)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
return matched;
|
||||
}
|
||||
@@ -1,27 +1,30 @@
|
||||
import { some, intersection, difference } from 'lodash';
|
||||
import { some, intersection, difference, remove } from 'lodash';
|
||||
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 resolve, { Context, toString } from '/imports/parser/resolve.js';
|
||||
import logErrors from './shared/logErrors.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, {
|
||||
creature, targets, scope, log
|
||||
}){
|
||||
export default function applyDamage(node, actionContext){
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const applyChildren = function(){
|
||||
node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
};
|
||||
|
||||
const prop = node.node;
|
||||
const scope = actionContext.scope;
|
||||
|
||||
// Skip if there is no parse node to work with
|
||||
if (!prop.amount?.parseNode) return;
|
||||
|
||||
// Choose target
|
||||
let damageTargets = prop.target === 'self' ? [creature] : targets;
|
||||
let damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
||||
// Determine if the hit is critical
|
||||
let criticalHit = scope['$criticalHit']?.value &&
|
||||
prop.damageType !== 'healing' // Can't critically heal
|
||||
@@ -36,19 +39,19 @@ export default function applyDamage(node, {
|
||||
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
|
||||
|
||||
// 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);
|
||||
if (rolled.parseType !== 'constant'){
|
||||
logValue.push(toString(rolled));
|
||||
}
|
||||
logErrors(context.errors, log);
|
||||
logErrors(context.errors, actionContext);
|
||||
|
||||
// Reset the errors so we don't log the same errors twice
|
||||
context.errors = [];
|
||||
|
||||
// Resolve the roll to a final value
|
||||
const {result: reduced} = resolve('reduce', rolled, scope, context);
|
||||
logErrors(context.errors, log);
|
||||
logErrors(context.errors, actionContext);
|
||||
|
||||
// Store the result
|
||||
if (reduced.parseType === 'constant'){
|
||||
@@ -94,15 +97,17 @@ export default function applyDamage(node, {
|
||||
logValue
|
||||
});
|
||||
|
||||
actionContext.target = [target];
|
||||
// Deal the damage to the target
|
||||
let damageDealt = dealDamageWork({
|
||||
creature: target,
|
||||
let damageDealt = dealDamage({
|
||||
target,
|
||||
damageType: prop.damageType,
|
||||
amount: damage,
|
||||
actionContext
|
||||
});
|
||||
|
||||
// Log the damage done
|
||||
if (target._id === creature._id){
|
||||
if (target._id === actionContext.creature._id){
|
||||
// Target is same as self, log damage as such
|
||||
logValue.push(`**${damageDealt}** ${suffix} to self`);
|
||||
} else {
|
||||
@@ -123,7 +128,7 @@ export default function applyDamage(node, {
|
||||
// There are no targets, just log the result
|
||||
logValue.push(`**${damage}** ${suffix}`);
|
||||
}
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: logName,
|
||||
value: logValue.join('\n'),
|
||||
inline: true,
|
||||
@@ -178,3 +183,49 @@ function multiplierAppliesTo(damageProp){
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.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;
|
||||
|
||||
// Log Name, summary
|
||||
let content = { name: prop.name };
|
||||
if (prop.summary?.text){
|
||||
recalculateInlineCalculations(prop.summary, scope, log);
|
||||
recalculateInlineCalculations(prop.summary, actionContext);
|
||||
content.value = prop.summary.value;
|
||||
}
|
||||
if (content.name || content.value){
|
||||
log.content.push(content);
|
||||
actionContext.addLog(content);
|
||||
}
|
||||
// Log description
|
||||
if (prop.description?.text){
|
||||
recalculateInlineCalculations(prop.description, scope, log);
|
||||
log.content.push({value: prop.description.value});
|
||||
recalculateInlineCalculations(prop.description, actionContext);
|
||||
actionContext.addLog({value: prop.description.value});
|
||||
}
|
||||
// Apply triggers
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
// Apply children
|
||||
node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
|
||||
@@ -2,33 +2,34 @@ import applyProperty from '../applyProperty.js';
|
||||
import logErrors from './shared/logErrors.js';
|
||||
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.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 applyChildren = function(){
|
||||
node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
};
|
||||
|
||||
if (prop.roll?.calculation){
|
||||
const logValue = [];
|
||||
|
||||
// roll the dice only and store that string
|
||||
applyEffectsToCalculationParseNode(prop.roll, log);
|
||||
const {result: rolled, context} = resolve('roll', prop.roll.parseNode, scope);
|
||||
applyEffectsToCalculationParseNode(prop.roll, actionContext);
|
||||
const {result: rolled, context} = resolve('roll', prop.roll.parseNode, actionContext.scope);
|
||||
if (rolled.parseType !== 'constant'){
|
||||
logValue.push(toString(rolled));
|
||||
}
|
||||
logErrors(context.errors, log);
|
||||
logErrors(context.errors, actionContext);
|
||||
|
||||
// Reset the errors so we don't log the same errors twice
|
||||
context.errors = [];
|
||||
|
||||
// Resolve the roll to a final value
|
||||
const {result: reduced} = resolve('reduce', rolled, scope, context);
|
||||
logErrors(context.errors, log);
|
||||
const {result: reduced} = resolve('reduce', rolled, actionContext.scope, context);
|
||||
logErrors(context.errors, actionContext);
|
||||
|
||||
// Store the result
|
||||
if (reduced.parseType === 'constant'){
|
||||
@@ -45,11 +46,11 @@ export default function applyRoll(node, {creature, targets, scope, log}){
|
||||
}
|
||||
const value = reduced.value;
|
||||
|
||||
scope[prop.variableName] = value;
|
||||
actionContext.scope[prop.variableName] = value;
|
||||
logValue.push(`**${value}**`);
|
||||
|
||||
if (!prop.silent){
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: prop.name,
|
||||
value: logValue.join('\n'),
|
||||
inline: true,
|
||||
|
||||
@@ -2,38 +2,38 @@ import rollDice from '/imports/parser/rollDice.js';
|
||||
import recalculateCalculation from './shared/recalculateCalculation.js';
|
||||
import applyProperty from '../applyProperty.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;
|
||||
|
||||
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);
|
||||
if (!isFinite(dc)){
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: 'Saving throw requires a DC',
|
||||
});
|
||||
return node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
return node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: prop.name,
|
||||
value: `DC **${dc}**`,
|
||||
inline: true,
|
||||
});
|
||||
const scope = actionContext.scope;
|
||||
|
||||
// If there are no save targets, apply all children as if the save both
|
||||
// succeeeded and failed
|
||||
if (!saveTargets?.length){
|
||||
scope['$saveFailed'] = {value: true};
|
||||
scope['$saveSucceeded'] = {value: true};
|
||||
return node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
scope['$saveSucceeded'] = { value: true };
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
return node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
|
||||
// Each target makes the saving throw
|
||||
@@ -43,16 +43,16 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
|
||||
delete scope['$saveDiceRoll'];
|
||||
delete scope['$saveRoll'];
|
||||
|
||||
const applyChildren = function(){
|
||||
node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets: [target], scope, log
|
||||
}));
|
||||
const applyChildren = function () {
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
actionContext.targets = [target]
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
};
|
||||
|
||||
const save = target.variables[prop.stat];
|
||||
|
||||
if (!save){
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Saving throw error',
|
||||
value: 'No saving throw found: ' + prop.stat,
|
||||
});
|
||||
@@ -94,7 +94,7 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
|
||||
} else {
|
||||
scope['$saveFailed'] = {value: true};
|
||||
}
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: saveSuccess ? 'Successful save' : 'Failed save',
|
||||
value: resultPrefix + '\n**' + result + '**',
|
||||
inline: true,
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import recalculateCalculation from './shared/recalculateCalculation.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applyToggle(node, {
|
||||
creature, targets, scope, log
|
||||
}){
|
||||
export default function applyToggle(node, actionContext){
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
recalculateCalculation(prop.condition, scope, log);
|
||||
recalculateCalculation(prop.condition, actionContext);
|
||||
if (prop.condition?.value) {
|
||||
return node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
return node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import operator from '/imports/parser/parseTree/operator.js';
|
||||
import { parse } from '/imports/parser/parser.js';
|
||||
import logErrors from './logErrors.js';
|
||||
|
||||
export default function applyEffectsToCalculationParseNode(calcObj, log){
|
||||
export default function applyEffectsToCalculationParseNode(calcObj, actionContext){
|
||||
if (!calcObj.effects) return;
|
||||
calcObj.effects.forEach(effect => {
|
||||
if (effect.operation !== 'add') return;
|
||||
@@ -18,7 +18,7 @@ export default function applyEffectsToCalculationParseNode(calcObj, log){
|
||||
fn: 'add'
|
||||
});
|
||||
} catch (e){
|
||||
logErrors([e], log)
|
||||
logErrors([e], actionContext)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default function logErrors(errors, log){
|
||||
export default function logErrors(errors, actionContext){
|
||||
errors?.forEach(error => {
|
||||
if (error.type !== 'info'){
|
||||
log.content.push({name: 'Error', value: error.message});
|
||||
actionContext.addLog({name: 'Error', value: error.message});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 logErrors from './logErrors.js';
|
||||
|
||||
export default function recalculateCalculation(calc, scope, log, context){
|
||||
export default function recalculateCalculation(calc, actionContext, context){
|
||||
if (!calc?.parseNode) return;
|
||||
calc._parseLevel = 'reduce';
|
||||
applyEffectsToCalculationParseNode(calc, log);
|
||||
evaluateCalculation(calc, scope, context);
|
||||
logErrors(calc.errors, log);
|
||||
applyEffectsToCalculationParseNode(calc, actionContext.log);
|
||||
evaluateCalculation(calc, actionContext.scope, context);
|
||||
logErrors(calc.errors, actionContext.log);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import embedInlineCalculations from '/imports/api/engine/computation/utility/embedInlineCalculations.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
|
||||
if (!inlineCalcObj?.inlineCalculations?.length) return;
|
||||
// Recalculate each calculation with the current scope
|
||||
inlineCalcObj.inlineCalculations.forEach(calc => {
|
||||
recalculateCalculation(calc, scope, log);
|
||||
recalculateCalculation(calc, actionContext);
|
||||
});
|
||||
// Embed the new calculated values
|
||||
embedInlineCalculations(inlineCalcObj);
|
||||
|
||||
@@ -6,24 +6,78 @@ import applyProperty from '/imports/api/engine/actions/applyProperty.js';
|
||||
import { difference, intersection } from 'lodash';
|
||||
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 type = prop.type;
|
||||
if (creature.triggers?.[type]?.[timing]) {
|
||||
creature.triggers[type][timing].forEach(trigger => {
|
||||
// Tags
|
||||
if (!triggerMatchTags(trigger, prop)) return;
|
||||
// Condition
|
||||
if (trigger.condition?.parseNode) {
|
||||
recalculateCalculation(trigger.condition, scope, log);
|
||||
if (!trigger.condition.value) return;
|
||||
}
|
||||
// Apply
|
||||
applyTrigger(trigger, { creature, targets, scope, log });
|
||||
const triggers = actionContext.triggers?.doActionProperty?.[type]?.[timing];
|
||||
if (triggers) {
|
||||
triggers.forEach(trigger => {
|
||||
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) {
|
||||
let matched = false;
|
||||
const propTags = getEffectivePropTags(prop);
|
||||
@@ -55,44 +109,3 @@ function triggerMatchTags(trigger, prop) {
|
||||
});
|
||||
return matched;
|
||||
}
|
||||
|
||||
export function applyTrigger(trigger, { creature, targets, scope, log }) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
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 { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
|
||||
import {
|
||||
getCreature, getVariables, getProperyAncestors, getPropertyDecendants, getPropertiesOfType
|
||||
getProperyAncestors, getPropertyDecendants
|
||||
} from '/imports/api/engine/loadCreatures.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import applyProperty from './applyProperty.js';
|
||||
import { groupBy, remove } from 'lodash';
|
||||
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
|
||||
|
||||
const doAction = new ValidatedMethod({
|
||||
name: 'creatureProperties.doAction',
|
||||
@@ -37,32 +36,16 @@ const doAction = new ValidatedMethod({
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({actionId, targetIds = [], scope}) {
|
||||
run({ actionId, targetIds = [], scope }) {
|
||||
// Get action context
|
||||
let action = CreatureProperties.findOne(actionId);
|
||||
// Check permissions
|
||||
const creatureId = action.ancestors[0].id;
|
||||
let creature = getCreature(action.ancestors[0].id);
|
||||
assertEditPermission(creature, this.userId);
|
||||
|
||||
// Add the variables to the creature document
|
||||
const variables = getVariables(creatureId);
|
||||
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);
|
||||
const actionContext = new ActionContext(creatureId, targetIds, this);
|
||||
|
||||
// Check permissions
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
actionContext.targets.forEach(target => {
|
||||
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);
|
||||
@@ -73,11 +56,11 @@ const doAction = new ValidatedMethod({
|
||||
properties.sort((a, b) => a.order - b.order);
|
||||
|
||||
// Do the action
|
||||
doActionWork({creature, targets, properties, ancestors, method: this, methodScope: scope});
|
||||
doActionWork({properties, ancestors, actionContext, methodScope: scope});
|
||||
|
||||
// Recompute all involved creatures
|
||||
Creatures.update({
|
||||
_id: { $in: [creature._id, ...targetIds] }
|
||||
_id: { $in: [creatureId, ...targetIds] }
|
||||
}, {
|
||||
$set: {dirty: true},
|
||||
});
|
||||
@@ -87,7 +70,7 @@ const doAction = new ValidatedMethod({
|
||||
export default doAction;
|
||||
|
||||
export function doActionWork({
|
||||
creature, targets, properties, ancestors, method, methodScope = {}, log
|
||||
properties, ancestors, actionContext, methodScope = {},
|
||||
}){
|
||||
// get the docs
|
||||
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`);
|
||||
}
|
||||
|
||||
// Get the triggers
|
||||
const triggers = getPropertiesOfType(creature._id, 'trigger');
|
||||
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,
|
||||
});
|
||||
// Include the ancestry and method scope in the context scope
|
||||
Object.assign(actionContext.scope, ancestorScope, methodScope);
|
||||
|
||||
// Apply the top level property, it is responsible for applying its children
|
||||
// recursively
|
||||
const scope = {
|
||||
...creature.variables,
|
||||
...ancestorScope,
|
||||
...methodScope
|
||||
}
|
||||
applyProperty(propertyForest[0], {
|
||||
creature,
|
||||
targets,
|
||||
scope,
|
||||
log,
|
||||
});
|
||||
applyProperty(propertyForest[0], actionContext);
|
||||
|
||||
// Insert the log
|
||||
insertCreatureLogWork({log, creature, method});
|
||||
actionContext.writeLog();
|
||||
}
|
||||
|
||||
// Assumes ancestors are in tree order already
|
||||
|
||||
@@ -1,11 +1,53 @@
|
||||
import '/imports/api/simpleSchemaConfig.js';
|
||||
//import testTypes from './testTypes/index.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(){
|
||||
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));
|
||||
});
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
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 {
|
||||
getProperyAncestors, getPropertyDecendants
|
||||
} from '/imports/api/engine/loadCreatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
import { doActionWork } from '/imports/api/engine/actions/doAction.js';
|
||||
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
|
||||
|
||||
const doAction = new ValidatedMethod({
|
||||
name: 'creatureProperties.doCastSpell',
|
||||
@@ -39,47 +42,31 @@ const doAction = new ValidatedMethod({
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({spellId, slotId, targetIds = [], scope = {}}) {
|
||||
run({ spellId, slotId, targetIds = [], scope = {} }) {
|
||||
// Get action context
|
||||
let spell = CreatureProperties.findOne(spellId);
|
||||
const creatureId = spell.ancestors[0].id;
|
||||
const actionContext = new ActionContext(creatureId, targetIds, this);
|
||||
|
||||
// Check permissions
|
||||
let creature = getRootCreatureAncestor(spell);
|
||||
|
||||
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(actionContext.creature, this.userId);
|
||||
actionContext.targets.forEach(target => {
|
||||
assertEditPermission(target, this.userId);
|
||||
targets.push(target);
|
||||
});
|
||||
|
||||
// Fetch all the action's ancestor creatureProperties
|
||||
const ancestorIds = [];
|
||||
spell.ancestors.forEach(ref => {
|
||||
if (ref.collection === 'creatureProperties') {
|
||||
ancestorIds.push(ref.id);
|
||||
}
|
||||
});
|
||||
const ancestors = getProperyAncestors(creatureId, spell._id);
|
||||
ancestors.sort((a, b) => a.order - b.order);
|
||||
|
||||
// Get cursor of ancestors
|
||||
const ancestors = CreatureProperties.find({
|
||||
_id: {$in: ancestorIds},
|
||||
}, {
|
||||
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},
|
||||
});
|
||||
const properties = getPropertyDecendants(creatureId, spell._id);
|
||||
properties.push(spell);
|
||||
properties.sort((a, b) => a.order - b.order);
|
||||
|
||||
// Spend the appropriate slot
|
||||
let slotLevel = spell.level || 0;
|
||||
let slot;
|
||||
|
||||
actionContext.scope['slotLevel'] = slotLevel;
|
||||
|
||||
if (slotId && !spell.castWithoutSpellSlots){
|
||||
slot = CreatureProperties.findOne(slotId);
|
||||
if (!slot){
|
||||
@@ -104,34 +91,32 @@ const doAction = new ValidatedMethod({
|
||||
}
|
||||
slotLevel = slot.spellSlotLevel.value;
|
||||
damagePropertyWork({
|
||||
property: slot,
|
||||
prop: slot,
|
||||
operation: 'increment',
|
||||
value: 1,
|
||||
actionContext,
|
||||
});
|
||||
}
|
||||
|
||||
scope['slotLevel'] = slotLevel;
|
||||
|
||||
// Post the slot level spent to the log
|
||||
const log = CreatureLogSchema.clean({
|
||||
creatureId: creature._id,
|
||||
creatureName: creature.name,
|
||||
});
|
||||
if (slot?.spellSlotLevel?.value){
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: `Casting using a level ${slotLevel} spell slot`
|
||||
});
|
||||
} else if (slotLevel) {
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: `Casting at level ${slotLevel}`
|
||||
});
|
||||
}
|
||||
|
||||
// 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({
|
||||
_id: { $in: [creature._id, ...targetIds] }
|
||||
_id: { $in: [creatureId, ...targetIds] }
|
||||
}, {
|
||||
$set: { dirty: true },
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
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 { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import rollDice from '/imports/parser/rollDice.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({
|
||||
name: 'creatureProperties.doCheck',
|
||||
@@ -24,34 +24,32 @@ const doCheck = new ValidatedMethod({
|
||||
},
|
||||
run({propId, scope}) {
|
||||
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
|
||||
assertEditPermission(creature, this.userId);
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
|
||||
// Do the check
|
||||
doCheckWork({creature, prop, method: this, methodScope: scope});
|
||||
doCheckWork({prop, actionContext});
|
||||
},
|
||||
});
|
||||
|
||||
export default doCheck;
|
||||
|
||||
export function doCheckWork({
|
||||
creature, prop, method, methodScope = {}
|
||||
}){
|
||||
// Create the log
|
||||
let log = CreatureLogSchema.clean({
|
||||
creatureId: creature._id,
|
||||
creatureName: creature.name,
|
||||
});
|
||||
export function doCheckWork({prop, actionContext}){
|
||||
|
||||
rollCheck({prop, log, methodScope});
|
||||
applyTriggers(actionContext.triggers.check?.before, prop, actionContext);
|
||||
rollCheck(prop, actionContext);
|
||||
applyTriggers(actionContext.triggers.check?.after, prop, actionContext);
|
||||
|
||||
// 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
|
||||
let rollModifier;
|
||||
let logName = `${prop.name} check`;
|
||||
@@ -77,7 +75,7 @@ function rollCheck({prop, log, methodScope}){
|
||||
const rollModifierText = numberToSignedString(rollModifier, true);
|
||||
|
||||
let value, values, resultPrefix;
|
||||
if (methodScope['$checkAdvantage'] === 1){
|
||||
if (scope['$checkAdvantage'] === 1){
|
||||
logName += ' (Advantage)';
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a >= b) {
|
||||
@@ -87,7 +85,7 @@ function rollCheck({prop, log, methodScope}){
|
||||
value = b;
|
||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
||||
}
|
||||
} else if (methodScope['$checkAdvantage'] === -1){
|
||||
} else if (scope['$checkAdvantage'] === -1){
|
||||
logName += ' (Disadvantage)';
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a <= b) {
|
||||
@@ -103,7 +101,7 @@ function rollCheck({prop, log, methodScope}){
|
||||
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
|
||||
}
|
||||
const result = (value + rollModifier) || 0;
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: logName,
|
||||
value: `${resultPrefix} **${result}**`,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import applyAction from './applyAction.testFn.js';
|
||||
|
||||
export default [{
|
||||
text: 'Applies actions',
|
||||
fn: applyAction,
|
||||
},];
|
||||
@@ -31,9 +31,10 @@ function childrenActive(prop){
|
||||
switch (prop.type){
|
||||
// Only equipped items have active children
|
||||
case 'item': return !!prop.equipped;
|
||||
// The children of actions are always inactive
|
||||
// The children of actions, spells, and triggers are always inactive
|
||||
case 'action': return false;
|
||||
case 'spell': return false;
|
||||
case 'trigger': return false;
|
||||
// The children of notes are always inactive
|
||||
case 'note': return false;
|
||||
// Other children are active
|
||||
|
||||
@@ -10,8 +10,10 @@ export default function computeToggleDependencies(node, dependencyGraph){
|
||||
prop.enabled
|
||||
) return;
|
||||
walkDown(node.children, child => {
|
||||
child.node._computationDetails.toggleAncestors.push(prop);
|
||||
// Only for children that aren't inactive
|
||||
if (child.node.inactive) return;
|
||||
// The child nodes depend on the toggle condition compuation
|
||||
child.node._computationDetails.toggleAncestors.push(prop);
|
||||
dependencyGraph.addLink(child.node._id, prop._id, 'toggle');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -105,7 +105,8 @@ function linkBuff(dependencyGraph, prop){
|
||||
dependOnCalc({dependencyGraph, prop, key: 'duration'});
|
||||
}
|
||||
|
||||
function linkClassLevel(dependencyGraph, prop){
|
||||
function linkClassLevel(dependencyGraph, prop) {
|
||||
if (prop.inactive) return;
|
||||
// The variableName of the prop depends on the prop
|
||||
if (prop.variableName && prop.level){
|
||||
dependencyGraph.addLink(prop.variableName, prop._id, 'classLevel');
|
||||
@@ -121,11 +122,16 @@ function linkDamage(dependencyGraph, prop){
|
||||
dependOnCalc({dependencyGraph, prop, key: 'amount'});
|
||||
}
|
||||
|
||||
function linkEffects(dependencyGraph, prop, computation){
|
||||
function linkEffects(dependencyGraph, prop, computation) {
|
||||
// The effect depends on its amount calculation
|
||||
dependOnCalc({dependencyGraph, prop, key: 'amount'});
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'amount' });
|
||||
// Inactive effects aren't going to impact their targeted stats
|
||||
if (prop.inactive) return;
|
||||
// The stats depend on the effect
|
||||
if (prop.targetByTags){
|
||||
if (prop.inactive) {
|
||||
// Inactive effects apply to no stats
|
||||
return;
|
||||
} else if (prop.targetByTags){
|
||||
getEffectTagTargets(prop, computation).forEach(targetId => {
|
||||
const targetProp = computation.propsById[targetId];
|
||||
if (
|
||||
@@ -178,8 +184,8 @@ function getTargetListFromTags(tags, computation){
|
||||
const targetTagIdLists = [];
|
||||
if (!tags) return [];
|
||||
tags.forEach(tag => {
|
||||
const idList = computation.propsWithTag[tag];
|
||||
if (idList) targetTagIdLists.push(idList);
|
||||
const idList = computation.propsWithTag[tag] || [];
|
||||
targetTagIdLists.push(idList);
|
||||
});
|
||||
const targets = intersection(...targetTagIdLists);
|
||||
return targets;
|
||||
@@ -221,13 +227,14 @@ function linkRoll(dependencyGraph, prop){
|
||||
}
|
||||
|
||||
function linkVariableName(dependencyGraph, prop){
|
||||
// The variableName of the prop depends on the prop
|
||||
if (prop.variableName){
|
||||
// The variableName of the prop depends on the prop if the prop is active
|
||||
if (prop.variableName && !prop.inactive){
|
||||
dependencyGraph.addLink(prop.variableName, prop._id, 'definition');
|
||||
}
|
||||
}
|
||||
|
||||
function linkDamageMultiplier(dependencyGraph, prop){
|
||||
function linkDamageMultiplier(dependencyGraph, prop) {
|
||||
if (prop.inactive) return;
|
||||
prop.damageTypes.forEach(damageType => {
|
||||
// Remove all non-letter characters from the damage name
|
||||
const damageName = damageType.replace(/[^a-z]/gi, '')
|
||||
@@ -237,6 +244,7 @@ function linkDamageMultiplier(dependencyGraph, prop){
|
||||
|
||||
function linkProficiencies(dependencyGraph, prop){
|
||||
// The stats depend on the proficiency
|
||||
if (prop.inactive) return;
|
||||
prop.stats.forEach(statName => {
|
||||
if (!statName) return;
|
||||
dependencyGraph.addLink(statName, prop._id, prop.type);
|
||||
@@ -248,6 +256,10 @@ function linkSavingThrow(dependencyGraph, prop){
|
||||
}
|
||||
|
||||
function linkSkill(dependencyGraph, prop){
|
||||
// Depends on base value
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
|
||||
// Link dependents
|
||||
if (prop.inactive) return;
|
||||
linkVariableName(dependencyGraph, prop);
|
||||
// The prop depends on the variable references as the ability
|
||||
if (prop.ability){
|
||||
@@ -255,9 +267,6 @@ function linkSkill(dependencyGraph, prop){
|
||||
}
|
||||
// Skills depend on the creature's proficiencyBonus
|
||||
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
|
||||
|
||||
// Depends on base value
|
||||
dependOnCalc({dependencyGraph, prop, key: 'baseValue'});
|
||||
}
|
||||
|
||||
function linkSlot(dependencyGraph, prop){
|
||||
|
||||
@@ -89,6 +89,10 @@ export function buildComputationFromProps(properties, creature, variables){
|
||||
// Walk the property trees computing things that need to be inherited
|
||||
walkDown(forest, node => {
|
||||
computeInactiveStatus(node);
|
||||
});
|
||||
// Inactive status must be complete for the whole tree before toggle deps
|
||||
// are calculated
|
||||
walkDown(forest, node => {
|
||||
computeToggleDependencies(node, dependencyGraph);
|
||||
computeSlotQuantityFilled(node, dependencyGraph);
|
||||
});
|
||||
|
||||
@@ -86,7 +86,7 @@ function aggregateAbilityEffects({computation, skillNode, abilityNode}){
|
||||
// to a skill from its ability
|
||||
if (link.data === 'effect'){
|
||||
if (![
|
||||
'advantage', 'disadvantage', 'passiveAdd', 'fail'
|
||||
'advantage', 'disadvantage', 'passiveAdd', 'fail', 'conditional'
|
||||
].includes(linkedNode.data.operation)){
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function getAggregatorResult(node){
|
||||
if (aggregator.set !== undefined) {
|
||||
result = aggregator.set;
|
||||
}
|
||||
if (!node.definingProp?.decimal && Number.isFinite(result)){
|
||||
if (!node.data.definingProp?.decimal && Number.isFinite(result)){
|
||||
result = Math.floor(result);
|
||||
} else if (Number.isFinite(result)){
|
||||
result = stripFloatingPointOddities(result);
|
||||
|
||||
@@ -51,17 +51,30 @@ function compute(computation, node){
|
||||
|
||||
function pushDependenciesToStack(nodeId, graph, stack, computation){
|
||||
graph.forEachLinkedNode(nodeId, linkedNode => {
|
||||
if (linkedNode._visitedChildren && !linkedNode._visited){
|
||||
const pather = path.nba(graph, {
|
||||
oriented: true
|
||||
});
|
||||
const loop = pather.find(nodeId, nodeId);
|
||||
computation.errors.push({
|
||||
type: 'dependencyLoop',
|
||||
details: {
|
||||
nodes: loop.map(node => node.id)
|
||||
},
|
||||
});
|
||||
if (linkedNode._visitedChildren && !linkedNode._visited) {
|
||||
// This is a dependency loop, find a path from the node to itself
|
||||
// and store that path as a dependency loop error
|
||||
const pather = path.nba(graph, { oriented: true });
|
||||
let loop = [];
|
||||
// Pather doesn't like going from a node to iteself, so find all the
|
||||
// paths going from the next node back to the original node
|
||||
// and return the shortest one
|
||||
graph.forEachLinkedNode(nodeId, nextNode => {
|
||||
const newLoop = pather.find(nextNode.id, nodeId);
|
||||
if (!newLoop.length) return;
|
||||
if (!loop.length || newLoop.length < loop.length - 1) {
|
||||
loop = [linkedNode, ...newLoop];
|
||||
}
|
||||
}, true);
|
||||
|
||||
if (loop.length) {
|
||||
computation.errors.push({
|
||||
type: 'dependencyLoop',
|
||||
details: {
|
||||
nodes: loop.map(node => node.id)
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
stack.push(linkedNode);
|
||||
}, true);
|
||||
|
||||
@@ -46,7 +46,6 @@ export function getSingleProperty(creatureId, propertyId) {
|
||||
'removed': {$ne: true},
|
||||
}, {
|
||||
sort: { order: 1 },
|
||||
fields: { icon: 0 },
|
||||
});
|
||||
// console.timeEnd(`Cache miss on creature properties: ${creatureId}`);
|
||||
return prop;
|
||||
@@ -65,7 +64,6 @@ export function getProperties(creatureId) {
|
||||
'removed': {$ne: true},
|
||||
}, {
|
||||
sort: { order: 1 },
|
||||
fields: { icon: 0 },
|
||||
}).fetch();
|
||||
// console.timeEnd(`Cache miss on creature properties: ${creatureId}`);
|
||||
return props;
|
||||
@@ -90,7 +88,6 @@ export function getPropertiesOfType(creatureId, propType) {
|
||||
'type': propType,
|
||||
}, {
|
||||
sort: { order: 1 },
|
||||
fields: { icon: 0 },
|
||||
}).fetch();
|
||||
// console.timeEnd(`Cache miss on creature properties: ${creatureId}`);
|
||||
return props;
|
||||
@@ -103,11 +100,7 @@ export function getCreature(creatureId) {
|
||||
if (creature) return creature;
|
||||
}
|
||||
// console.time(`Cache miss on Creature: ${creatureId}`);
|
||||
const creature = Creatures.findOne(creatureId, {
|
||||
denormalizedStats: 1,
|
||||
variables: 1,
|
||||
dirty: 1,
|
||||
});
|
||||
const creature = Creatures.findOne(creatureId);
|
||||
// console.timeEnd(`Cache miss on Creature: ${creatureId}`);
|
||||
return creature;
|
||||
}
|
||||
@@ -149,6 +142,7 @@ export function getProperyAncestors(creatureId, propertyId) {
|
||||
// Fetch from database
|
||||
return CreatureProperties.find({
|
||||
_id: { $in: ancestorIds },
|
||||
removed: {$ne: true},
|
||||
}, {
|
||||
sort: { order: 1 },
|
||||
}).fetch();
|
||||
@@ -175,6 +169,8 @@ export function getPropertyDecendants(creatureId, propertyId) {
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': propertyId,
|
||||
removed: { $ne: true },
|
||||
}, {
|
||||
sort: { order: 1 },
|
||||
}).fetch();
|
||||
}
|
||||
}
|
||||
@@ -199,7 +195,6 @@ class LoadedCreature {
|
||||
removed: { $ne: true },
|
||||
}, {
|
||||
sort: { order: 1 },
|
||||
fields: { icon: 0 },
|
||||
}).observeChanges({
|
||||
added(id, fields) {
|
||||
fields._id = id;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// https://github.com/VeliovGroup/Meteor-Files/blob/master/docs/aws-s3-integration.md
|
||||
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { each, clone } from 'lodash';
|
||||
import { Random } from 'meteor/random';
|
||||
@@ -37,8 +36,9 @@ if (Meteor.isServer && Meteor.settings.useS3) {
|
||||
secretAccessKey: s3Conf.secret,
|
||||
endpoint: s3Conf.endpoint,
|
||||
sslEnabled: true, // optional
|
||||
maxRetries: 10,
|
||||
httpOptions: {
|
||||
timeout: 6000,
|
||||
timeout: 12000,
|
||||
agent: false
|
||||
}
|
||||
});
|
||||
@@ -48,7 +48,7 @@ if (Meteor.isServer && Meteor.settings.useS3) {
|
||||
storagePath,
|
||||
onBeforeUpload,
|
||||
onAfterUpload,
|
||||
debug = Meteor.isProduction,
|
||||
debug = !Meteor.isProduction,
|
||||
allowClientCode = false,
|
||||
}){
|
||||
const collection = new FilesCollection({
|
||||
@@ -222,7 +222,7 @@ if (Meteor.isServer && Meteor.settings.useS3) {
|
||||
storagePath,
|
||||
onBeforeUpload,
|
||||
onAfterUpload,
|
||||
debug = Meteor.isProduction,
|
||||
debug = !Meteor.isProduction,
|
||||
allowClientCode = false,
|
||||
}){
|
||||
const collection = new FilesCollection({
|
||||
|
||||
@@ -59,6 +59,23 @@ let AttributeSchema = createPropertySchema({
|
||||
type: String,
|
||||
regEx: /^#([a-f0-9]{3}){1,2}\b$/i,
|
||||
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
|
||||
baseValue: {
|
||||
@@ -79,6 +96,16 @@ let AttributeSchema = createPropertySchema({
|
||||
decimal: {
|
||||
type: Boolean,
|
||||
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
|
||||
reset: {
|
||||
|
||||
79
app/imports/api/properties/BuffRemovers.js
Normal file
79
app/imports/api/properties/BuffRemovers.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||
|
||||
let BuffRemoverSchema = createPropertySchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
// This will remove just the nearest ancestor buff
|
||||
targetParentBuff: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// The following only applies when not targeting the parent buff
|
||||
// Which character to remove buffs from
|
||||
target: {
|
||||
type: String,
|
||||
allowedValues: [
|
||||
'self',
|
||||
'target',
|
||||
],
|
||||
defaultValue: 'target',
|
||||
},
|
||||
// remove 1 or remove all
|
||||
removeAll: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
defaultValue: true,
|
||||
},
|
||||
// Buffs to remove based on tags:
|
||||
targetTags: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
maxCount: STORAGE_LIMITS.tagCount,
|
||||
},
|
||||
'targetTags.$': {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.tagLength,
|
||||
},
|
||||
extraTags: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
maxCount: STORAGE_LIMITS.extraTagsCount,
|
||||
},
|
||||
'extraTags.$': {
|
||||
type: Object,
|
||||
},
|
||||
'extraTags.$._id': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
autoValue(){
|
||||
if (!this.isSet) return Random.id();
|
||||
}
|
||||
},
|
||||
'extraTags.$.operation': {
|
||||
type: String,
|
||||
allowedValues: ['OR', 'NOT'],
|
||||
defaultValue: 'OR',
|
||||
},
|
||||
'extraTags.$.tags': {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: STORAGE_LIMITS.tagCount,
|
||||
},
|
||||
'extraTags.$.tags.$': {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.tagLength,
|
||||
},
|
||||
});
|
||||
|
||||
let ComputedOnlyBuffRemoverSchema = createPropertySchema({});
|
||||
|
||||
const ComputedBuffRemoverSchema = new SimpleSchema()
|
||||
.extend(BuffRemoverSchema)
|
||||
.extend(ComputedOnlyBuffRemoverSchema);
|
||||
|
||||
export { BuffRemoverSchema, ComputedOnlyBuffRemoverSchema, ComputedBuffRemoverSchema };
|
||||
@@ -12,6 +12,10 @@ let BuffSchema = createPropertySchema({
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
hideRemoveButton: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// How many rounds this buff lasts
|
||||
duration: {
|
||||
type: 'fieldToCompute',
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||
|
||||
// Folders organize a character sheet into a tree, particularly to group things
|
||||
// like 'race' and 'background'
|
||||
let FolderSchema = new SimpleSchema({
|
||||
let FolderSchema = new createPropertySchema({
|
||||
name: {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
});
|
||||
|
||||
const ComputedOnlyFolderSchema = new SimpleSchema({});
|
||||
const ComputedOnlyFolderSchema = new createPropertySchema({});
|
||||
|
||||
export { FolderSchema, ComputedOnlyFolderSchema };
|
||||
|
||||
@@ -5,8 +5,11 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
const eventOptions = {
|
||||
doActionProperty: 'Do action',
|
||||
// receiveActionProperty: 'Receiving action property',
|
||||
check: 'Roll check',
|
||||
// flipToggle: 'Toggle changed',
|
||||
// adjustProperty: 'Attribute adjusted',
|
||||
// itemEquipped: 'Item equipped'
|
||||
// itemUnequipped: 'Item unequipped'
|
||||
damageProperty: 'Attribute damaged or healed',
|
||||
anyRest: 'Short or long rest',
|
||||
longRest: 'Long rest',
|
||||
shortRest: 'Short rest',
|
||||
@@ -22,10 +25,12 @@ const actionPropertyTypeOptions = {
|
||||
adjustment: 'Attribute damage',
|
||||
branch: 'Branch',
|
||||
buff: 'Buff',
|
||||
buffRemover: 'Buff Removed',
|
||||
damage: 'Damage',
|
||||
note: 'Note',
|
||||
roll: 'Roll',
|
||||
savingThrow: 'Saving throw',
|
||||
spell: 'Spell',
|
||||
toggle: 'Toggle',
|
||||
}
|
||||
|
||||
@@ -40,10 +45,6 @@ let TriggerSchema = createPropertySchema({
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
summary: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
description: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js';
|
||||
import { ComputedOnlyAdjustmentSchema } from '/imports/api/properties/Adjustments.js';
|
||||
import { ComputedOnlyAttributeSchema } from '/imports/api/properties/Attributes.js';
|
||||
import { ComputedOnlyBuffSchema } from '/imports/api/properties/Buffs.js';
|
||||
import { ComputedOnlyBuffRemoverSchema } from '/imports/api/properties/BuffRemovers.js';
|
||||
import { ComputedOnlyBranchSchema } from '/imports/api/properties/Branches.js';
|
||||
import { ComputedOnlyClassSchema } from '/imports/api/properties/Classes.js';
|
||||
import { ComputedOnlyClassLevelSchema } from '/imports/api/properties/ClassLevels.js';
|
||||
@@ -32,6 +33,7 @@ const propertySchemasIndex = {
|
||||
adjustment: ComputedOnlyAdjustmentSchema,
|
||||
attribute: ComputedOnlyAttributeSchema,
|
||||
buff: ComputedOnlyBuffSchema,
|
||||
buffRemover: ComputedOnlyBuffRemoverSchema,
|
||||
branch: ComputedOnlyBranchSchema,
|
||||
class: ComputedOnlyClassSchema,
|
||||
classLevel: ComputedOnlyClassLevelSchema,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ComputedActionSchema } from '/imports/api/properties/Actions.js';
|
||||
import { ComputedAdjustmentSchema } from '/imports/api/properties/Adjustments.js';
|
||||
import { ComputedAttributeSchema } from '/imports/api/properties/Attributes.js';
|
||||
import { ComputedBuffSchema } from '/imports/api/properties/Buffs.js';
|
||||
import { ComputedBuffRemoverSchema } from '/imports/api/properties/BuffRemovers.js';
|
||||
import { ComputedBranchSchema } from '/imports/api/properties/Branches.js';
|
||||
import { ComputedClassSchema } from '/imports/api/properties/Classes.js';
|
||||
import { ComputedClassLevelSchema } from '/imports/api/properties/ClassLevels.js';
|
||||
@@ -32,6 +33,7 @@ const propertySchemasIndex = {
|
||||
adjustment: ComputedAdjustmentSchema,
|
||||
attribute: ComputedAttributeSchema,
|
||||
buff: ComputedBuffSchema,
|
||||
buffRemover: ComputedBuffRemoverSchema,
|
||||
branch: ComputedBranchSchema,
|
||||
class: ComputedClassSchema,
|
||||
classLevel: ComputedClassLevelSchema,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ActionSchema } from '/imports/api/properties/Actions.js';
|
||||
import { AdjustmentSchema } from '/imports/api/properties/Adjustments.js';
|
||||
import { AttributeSchema } from '/imports/api/properties/Attributes.js';
|
||||
import { BuffSchema } from '/imports/api/properties/Buffs.js';
|
||||
import { BuffRemoverSchema } from '/imports/api/properties/BuffRemovers.js';
|
||||
import { BranchSchema } from '/imports/api/properties/Branches.js';
|
||||
import { ClassSchema } from '/imports/api/properties/Classes.js';
|
||||
import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js';
|
||||
@@ -32,6 +33,7 @@ const propertySchemasIndex = {
|
||||
adjustment: AdjustmentSchema,
|
||||
attribute: AttributeSchema,
|
||||
buff: BuffSchema,
|
||||
buffRemover: BuffRemoverSchema,
|
||||
branch: BranchSchema,
|
||||
class: ClassSchema,
|
||||
classLevel: ClassLevelSchema,
|
||||
|
||||
@@ -28,6 +28,7 @@ function computedOnlyField(field){
|
||||
[`${field}.effects`]: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
[`${field}.effects.$`]: {
|
||||
type: Object,
|
||||
|
||||
@@ -110,8 +110,8 @@ export function getUserTier(user){
|
||||
export function assertUserHasPaidBenefits(user){
|
||||
let tier = getUserTier(user);
|
||||
if (!tier.paidBenefits){
|
||||
throw new Meteor.Error('Creatures.methods.insert.denied',
|
||||
`The ${tier.name} tier does not allow you to insert a creature`);
|
||||
throw new Meteor.Error('no paid benefits',
|
||||
`The ${tier.name} tier does not have the required benefits`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,12 @@ const PROPERTIES = Object.freeze({
|
||||
helpText: 'When a buff is activated as a child of an action, it will copy the properties under itself onto a target character.',
|
||||
suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'],
|
||||
},
|
||||
buffRemover: {
|
||||
icon: '$vuetify.icons.buffRemover',
|
||||
name: 'Remove Buff',
|
||||
helpText: 'Removes a buff from the target character',
|
||||
suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'],
|
||||
},
|
||||
branch: {
|
||||
icon: 'mdi-file-tree',
|
||||
name: 'Branch',
|
||||
|
||||
@@ -59,6 +59,10 @@ const SVG_ICONS = Object.freeze({
|
||||
name: 'buff',
|
||||
shape: 'M331.924 20.385c-36.708.887-82.53 60.972-116.063 147.972h.003c30.564-65.57 71.17-106.39 97.348-99.378 28.058 7.516 37.11 69.42 24.847 148.405-.895-.32-1.773-.642-2.672-.96.893.367 1.765.738 2.65 1.106-2.988 19.215-7.22 39.424-12.767 60.12-2.77 10.332-5.763 20.39-8.936 30.14-24.996-3.82-52.374-9.537-80.82-17.16-105.856-28.36-186.115-72.12-179.307-97.53 4.257-15.884 42.167-23.775 95.908-20.29-74.427-8.7-128.912-2.044-135.035 20.803-9.038 33.73 89.168 89.372 219.147 124.2 24.436 6.55 48.267 11.897 70.918 16.042-28.965 75.878-68.293 126.078-96.653 118.48-21.817-5.85-35.995-45.443-36.316-100.206-4.79 75.476 9.278 131.945 40.66 140.356 38.836 10.407 91.394-54.998 127.896-152.98 80.12 10.74 138.958 4.278 145.38-19.682 6.384-23.82-41.025-58.44-115.102-89.03 20.713-109.022 8.483-198.5-31.96-209.34-2.968-.796-6.013-1.144-9.124-1.07zm40.568 213.086c44.65 22.992 71.146 47.135 67.07 62.348-4.055 15.13-38.104 20.457-87.333 16.303 3.415-10.604 6.64-21.502 9.63-32.663 4.176-15.588 7.713-30.965 10.632-45.986z',
|
||||
},
|
||||
'perpendicular-rings-crossed-out': {
|
||||
name: 'buffRemover',
|
||||
shape: 'm 342.85706,29.520771 -10e-4,0.0017 C 311.09371,29.014264 281.87055,64.906082 247.60595,132.05856 L 172.54472,18.060082 142.22614,39.094548 433.13139,479.99983 459.83281,455.56605 385.30374,345.07055 c 60.47709,6.76324 95.40448,9.77027 101.5894,-10.08043 7.33528,-23.5444 -38.64515,-60.03954 -111.4339,-93.57921 25.07382,-108.10207 16.44748,-197.999126 -23.52751,-210.454283 -2.93364,-0.914533 -5.96321,-1.384872 -9.07467,-1.435856 z M 318.01727,76.44599 c 1.43939,0.165657 2.83703,0.458385 4.19071,0.879905 27.7335,8.636553 34.29121,70.855545 18.86666,149.284215 -0.88143,-0.35568 -1.74564,-0.7136 -2.63114,-1.06745 0.87755,0.40257 1.73435,0.80776 2.60387,1.21101 -3.29527,16.73399 -2.43292,17.48734 -7.73765,35.32955 L 249.50725,134.94386 C 277.16641,91.939481 296.57992,73.978789 318.01727,76.44599 Z M 21.309482,189.96942 c -10.385032,33.3398 85.507398,92.87964 213.982728,132.89854 18.67653,5.81939 37.00521,10.88752 54.75098,15.23025 -7.08963,-10.99585 -14.12857,-21.92647 -21.25144,-32.93262 -2.34002,-0.62085 -4.72317,-1.30046 -7.07998,-1.94408 -8.56911,-2.33994 -17.21776,-4.79347 -26.04044,-7.54193 C 131.03978,263.09208 52.602729,216.14328 60.425516,191.02722 65.316857,175.32699 103.51348,168.9655 157.07104,174.60556 121.54273,171.56302 37.239273,147.38028 21.309482,189.96942 Z M 374.83456,244.0657 c 43.69065,24.76623 69.19628,49.95346 64.51274,64.99048 -4.48269,14.38828 -26.92387,13.08335 -72.99123,7.92393 L 362.747,311.5007 c -5.21026,-7.91039 -3.28718,-12.58648 -0.38327,-21.91074 4.79853,-15.40771 8.94901,-30.63076 12.46879,-45.52239 z m -74.68362,109.69514 c -31.13564,67.76298 -69.48034,110.7402 -95.97392,102.48874 -21.56445,-6.72129 -34.14164,-46.85287 -32.26347,-101.58445 -7.81672,75.22256 3.97274,132.21064 34.99162,141.87493 32.83746,10.2293 77.9122,-34.67 115.55909,-108.20499 -7.40453,-11.47654 -14.88401,-23.0444 -22.31332,-34.57423 z'
|
||||
},
|
||||
'roll': {
|
||||
name: 'roll',
|
||||
shape: 'M 339.33314,69.985523 152.23146,95.161367 297.9199,159.07076 Z m 13.13639,6.106743 -41.41324,89.085234 142.77019,70.18694 z M 286.72755,169.97878 132.07338,102.13811 116.91912,287.72473 Z m 23.20215,10.78603 19.43763,205.72115 132.11738,-131.21375 z m -14.47505,0.7893 -172.49845,119.61061 192.24446,89.36907 z M 115.16567,131.2299 49.03503,273.48548 l 52.96523,18.92787 z m 334.97786,155.72184 -114.74252,113.9631 48.61189,28.29247 z m -329.82146,28.96669 45.63657,144.20053 139.66241,-58.06123 z m -17.69094,-7.89455 -46.061724,-16.46047 81.264174,127.69506 z m 220.42858,102.47108 -107.73294,44.78793 150.00722,-20.18447 z',
|
||||
|
||||
@@ -124,6 +124,7 @@ const expectedMigratedAttribute = {
|
||||
damage: 3,
|
||||
value: 17,
|
||||
constitutionMod: 2,
|
||||
dirty: true,
|
||||
}
|
||||
|
||||
const exampleAttack = {
|
||||
@@ -221,6 +222,7 @@ describe('migrateProperty', function() {
|
||||
prop: newAction,
|
||||
reversed: true,
|
||||
});
|
||||
delete reversedAction.dirty;
|
||||
assert.deepEqual(action, exampleAction, 'action should not be bashed');
|
||||
assert.deepEqual(exampleAction, reversedAction, 'operation should be reversible');
|
||||
});
|
||||
|
||||
@@ -16,8 +16,11 @@ const LIBRARY_NODE_TREE_FIELDS = {
|
||||
ancestors: 1,
|
||||
tags: 1,
|
||||
slotFillerCondition: 1,
|
||||
removed: 1,
|
||||
removedAt: 1,
|
||||
// SlotFillers
|
||||
slotQuantityFilled: 1,
|
||||
slotFillerType: 1,
|
||||
// Effect
|
||||
operation: 1,
|
||||
targetTags: 1,
|
||||
|
||||
@@ -100,21 +100,18 @@ Meteor.publish('classFillers', function(classId){
|
||||
}
|
||||
|
||||
// Get all the ids of libraries the user can access
|
||||
const user = Meteor.users.findOne(userId, {
|
||||
fields: {subscribedLibraries: 1}
|
||||
});
|
||||
const subs = user && user.subscribedLibraries || [];
|
||||
let libraries = Libraries.find({
|
||||
const creatureId = classProp.ancestors[0].id;
|
||||
const libraryIds = getCreatureLibraryIds(creatureId, userId);
|
||||
const libraries = Libraries.find({
|
||||
$or: [
|
||||
{owner: userId},
|
||||
{writers: userId},
|
||||
{readers: userId},
|
||||
{_id: {$in: subs}},
|
||||
{ owner: userId },
|
||||
{ writers: userId },
|
||||
{ readers: userId },
|
||||
{ _id: { $in: libraryIds }, public: true },
|
||||
]
|
||||
}, {
|
||||
fields: {_id: 1, name: 1},
|
||||
sort: { name: 1 }
|
||||
});
|
||||
let libraryIds = libraries.map(lib => lib._id);
|
||||
|
||||
// Build a filter for nodes in those libraries that match the slot
|
||||
let filter = getSlotFillFilter({slot: classProp, libraryIds});
|
||||
|
||||
@@ -18,7 +18,7 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
<style lang="css">
|
||||
.column-layout {
|
||||
column-count: 12;
|
||||
column-fill: balance;
|
||||
@@ -35,7 +35,7 @@ export default {
|
||||
transform: translateZ(0);
|
||||
padding: 4px;
|
||||
}
|
||||
.column-layout >>> > div {
|
||||
.column-layout > div, .column-layout > span > div {
|
||||
/*
|
||||
Table and width set because firefox does not support break-inside: avoid
|
||||
*/
|
||||
|
||||
@@ -24,10 +24,3 @@
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
.markdown img {
|
||||
max-width: 100%;
|
||||
margin: 8px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,6 +13,16 @@
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
props: {
|
||||
value: {
|
||||
@@ -22,27 +32,7 @@ export default {
|
||||
},
|
||||
data(){return {
|
||||
filterTerms: [],
|
||||
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'},
|
||||
],
|
||||
filterOptions,
|
||||
}},
|
||||
computed: {
|
||||
filter(){
|
||||
|
||||
@@ -201,6 +201,7 @@ export default {
|
||||
},
|
||||
librariesSelectedByCollections() {
|
||||
let ids = [];
|
||||
if (!this.model.allowedLibraryCollections) return ids;
|
||||
LibraryCollections.find({
|
||||
_id: { $in: this.model.allowedLibraryCollections }
|
||||
}).forEach(collection => {
|
||||
|
||||
@@ -28,11 +28,11 @@
|
||||
</v-btn>
|
||||
<div
|
||||
class="layout align-center justify-start pr-1"
|
||||
style="flex-grow: 0;"
|
||||
>
|
||||
<!--{{node && node.order}}-->
|
||||
<div
|
||||
v-if="isSlot"
|
||||
class="text-truncate"
|
||||
>
|
||||
<span
|
||||
:class="{
|
||||
@@ -47,15 +47,38 @@
|
||||
:model="node"
|
||||
/>
|
||||
</div>
|
||||
<tree-node-view
|
||||
<template
|
||||
v-else
|
||||
:model="node"
|
||||
/>
|
||||
>
|
||||
<tree-node-view
|
||||
:model="node"
|
||||
/>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
icon
|
||||
:disabled="context.editPermission === false"
|
||||
@click.stop="remove(node)"
|
||||
>
|
||||
<v-icon>
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-if="condenseChild">
|
||||
<span class="mr-4">:</span>
|
||||
<tree-node-view
|
||||
:model="children[0].node"
|
||||
/>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
icon
|
||||
:disabled="context.editPermission === false"
|
||||
@click.stop="remove(children[0].node)"
|
||||
>
|
||||
<v-icon>
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,98 +115,118 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
/**
|
||||
* TreeNode's are list item views of character properties. Every property which
|
||||
* can belong to the character is shown in the tree view of the character
|
||||
* the tree view shows off the full character structure, and where each part of
|
||||
* character comes from.
|
||||
**/
|
||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import FillSlotButton from '/imports/ui/creature/buildTree/FillSlotButton.vue';
|
||||
import { some } from 'lodash';
|
||||
/**
|
||||
* TreeNode's are list item views of character properties. Every property which
|
||||
* can belong to the character is shown in the tree view of the character
|
||||
* the tree view shows off the full character structure, and where each part of
|
||||
* character comes from.
|
||||
**/
|
||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import FillSlotButton from '/imports/ui/creature/buildTree/FillSlotButton.vue';
|
||||
import { some } from 'lodash';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
|
||||
import restoreProperty from '/imports/api/creature/creatureProperties/methods/restoreProperty.js';
|
||||
import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
|
||||
|
||||
export default {
|
||||
name: 'BuildTreeNode',
|
||||
components: {
|
||||
TreeNodeView,
|
||||
FillSlotButton,
|
||||
export default {
|
||||
name: 'BuildTreeNode',
|
||||
components: {
|
||||
TreeNodeView,
|
||||
FillSlotButton,
|
||||
},
|
||||
inject: {
|
||||
context: { default: {} }
|
||||
},
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
children: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data(){return {
|
||||
expanded: false,
|
||||
/* expand if there's a slot needing attention:
|
||||
this.node._descendantCanFill || (
|
||||
this.node.type === 'propertySlot' &&
|
||||
this. node.quantityExpected?.value === 0 ||
|
||||
(this.node.quantityExpected?.value > 1 && this.node.spaceLeft > 0)
|
||||
)
|
||||
*/
|
||||
}},
|
||||
computed: {
|
||||
condenseChild(){
|
||||
return this.node.type === 'propertySlot' &&
|
||||
this.children.length === 1 &&
|
||||
this.children[0].node.type !== 'propertySlot' &&
|
||||
this.node.quantityExpected &&
|
||||
this.node.quantityExpected.value === 1;
|
||||
},
|
||||
isSlot(){
|
||||
return this.node.type === 'propertySlot';
|
||||
},
|
||||
canFill(){
|
||||
return !!this.node._canFill;
|
||||
},
|
||||
canFillWithOne(){
|
||||
return this.isSlot &&
|
||||
this.node.quantityExpected &&
|
||||
this.node.quantityExpected.value === 1 &&
|
||||
this.node.spaceLeft === 1
|
||||
},
|
||||
canFillWithMany(){
|
||||
return this.isSlot && (
|
||||
!this.node.quantityExpected ||
|
||||
this.node.quantityExpected.value === 0 ||
|
||||
(this.node.quantityExpected.value > 1 && this.node.spaceLeft > 0)
|
||||
);
|
||||
},
|
||||
hasChildren(){
|
||||
return !!this.children && !!this.computedChildren.length || this.lazy && !this.expanded;
|
||||
},
|
||||
showExpanded(){
|
||||
return this.canExpand && this.expanded;
|
||||
},
|
||||
computedChildren(){
|
||||
if (this.condenseChild){
|
||||
return this.children[0].children;
|
||||
}
|
||||
return this.children;
|
||||
},
|
||||
canExpand(){
|
||||
return !!this.computedChildren.length || this.canFillWithMany;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'node._ancestorOfMatchedDocument'(value){
|
||||
this.expanded = !!value ||
|
||||
some(this.selectedNode?.ancestors, ref => ref.id === this.node._id);
|
||||
},
|
||||
'selectedNode.ancestors'(value){
|
||||
this.expanded = !!some(value, ref => ref.id === this.node._id) || this.expanded;
|
||||
},
|
||||
children: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
beforeCreate() {
|
||||
this.$options.components.BuildTreeNodeList = require('./BuildTreeNodeList.vue').default
|
||||
},
|
||||
};
|
||||
},
|
||||
data(){return {
|
||||
expanded: false,
|
||||
/* expand if there's a slot needing attention:
|
||||
this.node._descendantCanFill || (
|
||||
this.node.type === 'propertySlot' &&
|
||||
this. node.quantityExpected?.value === 0 ||
|
||||
(this.node.quantityExpected?.value > 1 && this.node.spaceLeft > 0)
|
||||
)
|
||||
*/
|
||||
}},
|
||||
computed: {
|
||||
condenseChild(){
|
||||
return this.node.type === 'propertySlot' &&
|
||||
this.children.length === 1 &&
|
||||
this.children[0].node.type !== 'propertySlot' &&
|
||||
this.node.quantityExpected &&
|
||||
this.node.quantityExpected.value === 1;
|
||||
},
|
||||
isSlot(){
|
||||
return this.node.type === 'propertySlot';
|
||||
},
|
||||
canFill(){
|
||||
return !!this.node._canFill;
|
||||
},
|
||||
canFillWithOne(){
|
||||
return this.isSlot &&
|
||||
this.node.quantityExpected &&
|
||||
this.node.quantityExpected.value === 1 &&
|
||||
this.node.spaceLeft === 1
|
||||
},
|
||||
canFillWithMany(){
|
||||
return this.isSlot && (
|
||||
!this.node.quantityExpected ||
|
||||
this.node.quantityExpected.value === 0 ||
|
||||
(this.node.quantityExpected.value > 1 && this.node.spaceLeft > 0)
|
||||
);
|
||||
},
|
||||
hasChildren(){
|
||||
return !!this.children && !!this.computedChildren.length || this.lazy && !this.expanded;
|
||||
},
|
||||
showExpanded(){
|
||||
return this.canExpand && this.expanded;
|
||||
},
|
||||
computedChildren(){
|
||||
if (this.condenseChild){
|
||||
return this.children[0].children;
|
||||
}
|
||||
return this.children;
|
||||
},
|
||||
canExpand(){
|
||||
return !!this.computedChildren.length || this.canFillWithMany;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'node._ancestorOfMatchedDocument'(value){
|
||||
this.expanded = !!value ||
|
||||
some(this.selectedNode?.ancestors, ref => ref.id === this.node._id);
|
||||
},
|
||||
'selectedNode.ancestors'(value){
|
||||
this.expanded = !!some(value, ref => ref.id === this.node._id) || this.expanded;
|
||||
},
|
||||
},
|
||||
beforeCreate() {
|
||||
this.$options.components.BuildTreeNodeList = require('./BuildTreeNodeList.vue').default
|
||||
},
|
||||
methods: {
|
||||
remove(model) {
|
||||
const _id = model._id;
|
||||
softRemoveProperty.call({_id});
|
||||
snackbar({
|
||||
text: `Deleted ${getPropertyTitle(model)}`,
|
||||
callbackName: 'undo',
|
||||
callback(){
|
||||
restoreProperty.call({_id});
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
v-model="fab"
|
||||
color="primary"
|
||||
fab
|
||||
small
|
||||
data-id="insert-creature-property-fab"
|
||||
class="insert-creature-property-fab"
|
||||
small
|
||||
>
|
||||
<transition
|
||||
name="fab-rotate"
|
||||
@@ -48,12 +48,13 @@
|
||||
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
|
||||
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
||||
|
||||
function getParentAndOrderFromSelectedTreeNode(creatureId){
|
||||
function getParentAndOrderFromSelectedTreeNode(creatureId, $store){
|
||||
// find the parent based on the currently selected property
|
||||
let el = document.querySelector('.tree-tab .tree-node-title.primary--text');
|
||||
let selectedComponent = el && el.parentElement.__vue__.$parent;
|
||||
let parentRef, order;
|
||||
if (selectedComponent){
|
||||
const onTreeTab = $store.getters.tabNameById(creatureId) === 'tree';
|
||||
if (onTreeTab && selectedComponent){
|
||||
if (selectedComponent.showExpanded){
|
||||
parentRef = {
|
||||
id: selectedComponent.node._id,
|
||||
@@ -156,7 +157,7 @@
|
||||
let creatureId = this.creatureId;
|
||||
let fab = hideFab();
|
||||
|
||||
let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId);
|
||||
let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId, this.$store);
|
||||
let parent;
|
||||
try {
|
||||
parent = fetchDocByRef(parentRef);
|
||||
|
||||
@@ -14,17 +14,19 @@
|
||||
<v-fade-transition
|
||||
mode="out-in"
|
||||
>
|
||||
<v-app-bar-title :key="$store.state.pageTitle">
|
||||
<div>
|
||||
{{ $store.state.pageTitle }}
|
||||
</div>
|
||||
</v-app-bar-title>
|
||||
<v-toolbar-title :key="$store.state.pageTitle">
|
||||
{{ $store.state.pageTitle }}
|
||||
</v-toolbar-title>
|
||||
</v-fade-transition>
|
||||
<v-spacer />
|
||||
<v-fade-transition
|
||||
mode="out-in"
|
||||
>
|
||||
<div :key="$route.meta.title">
|
||||
<v-layout
|
||||
:key="$route.meta.title"
|
||||
class="flex-shrink-0 flex-grow-0"
|
||||
justify-end
|
||||
>
|
||||
<template v-if="creature">
|
||||
<shared-icon :model="creature" />
|
||||
<v-menu
|
||||
@@ -68,7 +70,7 @@
|
||||
</v-menu>
|
||||
<v-app-bar-nav-icon @click="toggleRightDrawer" />
|
||||
</template>
|
||||
</div>
|
||||
</v-layout>
|
||||
</v-fade-transition>
|
||||
<v-fade-transition
|
||||
slot="extension"
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
<v-container fluid>
|
||||
<v-row dense>
|
||||
<v-col cols="12">
|
||||
<slot-cards-to-fill :creature-id="creatureId" />
|
||||
<character-errors
|
||||
class="mt-4"
|
||||
:creature-id="creatureId"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<slot-cards-to-fill :creature-id="creatureId" />
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<v-col
|
||||
cols="12"
|
||||
@@ -12,7 +18,51 @@
|
||||
lg="6"
|
||||
>
|
||||
<v-card class="pb-4">
|
||||
<v-card-title>Slots</v-card-title>
|
||||
<v-card-title style="height: 68px;">
|
||||
Slots
|
||||
<v-spacer />
|
||||
<v-scale-transition>
|
||||
<v-menu
|
||||
bottom
|
||||
left
|
||||
>
|
||||
<template #activator="{ on }">
|
||||
<v-badge
|
||||
v-show="hiddenCount"
|
||||
slot="activator"
|
||||
color="primary"
|
||||
overlap
|
||||
:value="hiddenCount"
|
||||
:content="hiddenCount"
|
||||
>
|
||||
<v-btn
|
||||
icon
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon>mdi-file-hidden</v-icon>
|
||||
</v-btn>
|
||||
</v-badge>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-subheader>
|
||||
<v-icon class="mr-2">
|
||||
mdi-file-hidden
|
||||
</v-icon>
|
||||
{{ hiddenCount }} hidden {{ hiddenCount > 1 ? 'slots' : 'slot' }}
|
||||
</v-subheader>
|
||||
<v-list-item
|
||||
v-for="slot in hiddenSlots"
|
||||
:key="slot._id"
|
||||
@click="unhideSlot(slot._id)"
|
||||
>
|
||||
<v-list-item-title>
|
||||
{{ getPropertyTitle(slot) }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-scale-transition>
|
||||
</v-card-title>
|
||||
<build-tree-node-list
|
||||
:children="slotBuildTree"
|
||||
class="mx-2"
|
||||
@@ -126,6 +176,10 @@ import BuildTreeNodeList from '/imports/ui/creature/buildTree/BuildTreeNodeList.
|
||||
import SlotCardsToFill from '/imports/ui/creature/slots/SlotCardsToFill.vue';
|
||||
import CreatureVariables from '../../../../api/creature/creatures/CreatureVariables';
|
||||
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
|
||||
import CharacterErrors from '/imports/ui/creature/character/errors/CharacterErrors.vue';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import updateCreatureProperty from '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js';
|
||||
import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
|
||||
|
||||
function traverse(tree, callback, parents = []){
|
||||
tree.forEach(node => {
|
||||
@@ -136,6 +190,7 @@ function traverse(tree, callback, parents = []){
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CharacterErrors,
|
||||
BuildTreeNodeList,
|
||||
SlotCardsToFill,
|
||||
},
|
||||
@@ -169,7 +224,10 @@ export default {
|
||||
...this.highestLevels,
|
||||
...this.classProperties
|
||||
].sort((a, b) => a.order - b.order);
|
||||
}
|
||||
},
|
||||
hiddenCount() {
|
||||
return this.hiddenSlots.length;
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
creature(){
|
||||
@@ -178,6 +236,29 @@ export default {
|
||||
variables() {
|
||||
return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {};
|
||||
},
|
||||
hiddenSlots(){
|
||||
return CreatureProperties.find({
|
||||
type: 'propertySlot',
|
||||
'ancestors.id': this.creatureId,
|
||||
ignored: true,
|
||||
$and: [
|
||||
{
|
||||
$or: [
|
||||
{'slotCondition.value': {$nin: [false, 0, '']}},
|
||||
{'slotCondition.value': {$exists: false}},
|
||||
]
|
||||
},{
|
||||
$or: [
|
||||
{ 'quantityExpected.value': {$in: [false, 0, '', undefined]} },
|
||||
{ 'quantityExpected.value': {exists: false} },
|
||||
{spaceLeft: {$gt: 0}},
|
||||
]
|
||||
},
|
||||
],
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}).fetch();
|
||||
},
|
||||
classProperties(){
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': this.creatureId,
|
||||
@@ -303,6 +384,19 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
getPropertyTitle,
|
||||
unhideSlot(_id) {
|
||||
updateCreatureProperty.call({
|
||||
_id,
|
||||
path: ['ignored'],
|
||||
value: false,
|
||||
}, error => {
|
||||
if (error){
|
||||
console.error(error);
|
||||
snackbar({text: error.reason || error.message || error.toString()});
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
<div
|
||||
class="stats-tab ma-2"
|
||||
>
|
||||
<character-errors
|
||||
class="mx-2 mt-4"
|
||||
:creature-id="creatureId"
|
||||
/>
|
||||
|
||||
<health-bar-card-container :creature-id="creatureId" />
|
||||
|
||||
<column-layout>
|
||||
@@ -51,7 +46,7 @@
|
||||
{{ buff.name }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-list-item-action v-if="!buff.hideRemoveButton">
|
||||
<v-btn
|
||||
icon
|
||||
@click.stop="softRemove(buff._id)"
|
||||
@@ -374,7 +369,6 @@
|
||||
import ToggleCard from '/imports/ui/properties/components/toggles/ToggleCard.vue';
|
||||
import doCastSpell from '/imports/api/engine/actions/doCastSpell.js';
|
||||
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import CharacterErrors from '/imports/ui/creature/character/errors/CharacterErrors.vue';
|
||||
|
||||
const getProperties = function(creature, filter, options = {
|
||||
sort: {order: 1}
|
||||
@@ -419,7 +413,6 @@
|
||||
SpellSlotListTile,
|
||||
ActionCard,
|
||||
ToggleCard,
|
||||
CharacterErrors,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
|
||||
@@ -1,57 +1,109 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="creature && creature.computeErrors"
|
||||
class="character-sheet-errors"
|
||||
>
|
||||
<template v-for="(error, index) in creature.computeErrors">
|
||||
<dependency-loop-error
|
||||
v-if="error.type === 'dependencyLoop'"
|
||||
:key="index + 'dependencyLoopError'"
|
||||
:model="error"
|
||||
/>
|
||||
<v-alert
|
||||
v-else
|
||||
:key="index + 'otherError'"
|
||||
outlined
|
||||
dense
|
||||
type="error"
|
||||
<div v-if="creature && errors && errors.length">
|
||||
<v-btn
|
||||
fab
|
||||
small
|
||||
absolute
|
||||
right
|
||||
color="warning"
|
||||
class="mr-4"
|
||||
style="margin-top: -20px;"
|
||||
@click="expanded = !expanded"
|
||||
>
|
||||
<v-icon
|
||||
v-if="expanded"
|
||||
style="color: rgba(0,0,0,0.8);"
|
||||
>
|
||||
{{ error.type }}
|
||||
</v-alert>
|
||||
</template>
|
||||
mdi-close
|
||||
</v-icon>
|
||||
<v-icon
|
||||
v-else
|
||||
style="color: rgba(0,0,0,0.8);"
|
||||
>
|
||||
mdi-alert-circle-outline
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
<v-slide-y-transition>
|
||||
<div
|
||||
v-if="expanded"
|
||||
class="character-sheet-errors"
|
||||
>
|
||||
<template v-for="(error, index) in errors">
|
||||
<dependency-loop-error
|
||||
v-if="error.type === 'dependencyLoop'"
|
||||
:key="index + 'dependencyLoopError'"
|
||||
:model="error"
|
||||
/>
|
||||
<v-alert
|
||||
v-else
|
||||
:key="index + 'otherError'"
|
||||
border="bottom"
|
||||
colored-border
|
||||
elevation="2"
|
||||
type="error"
|
||||
>
|
||||
{{ error.type }}
|
||||
</v-alert>
|
||||
</template>
|
||||
</div>
|
||||
</v-slide-y-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import DependencyLoopError from '/imports/ui/creature/character/errors/DependencyLoopError.vue';
|
||||
import updateCreature from '/imports/api/creature/creatures/methods/updateCreature.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DependencyLoopError,
|
||||
},
|
||||
inject: {
|
||||
context: { default: {} },
|
||||
theme: {
|
||||
default: {
|
||||
isDark: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
}
|
||||
},
|
||||
data() { return {
|
||||
expanded: false,
|
||||
}},
|
||||
meteor: {
|
||||
creature() {
|
||||
if (!this.creatureId) return;
|
||||
return Creatures.findOne(this.creatureId, {fields: {computeErrors: 1}});
|
||||
return Creatures.findOne(this.creatureId, {fields: {computeErrors: 1, settings: 1}});
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
errors() {
|
||||
if (!this.creature || !this.creature.computeErrors) return;
|
||||
return this.creature.computeErrors.map(error => {
|
||||
error.text = error.type;
|
||||
if (error.type === 'dependencyLoop') {
|
||||
error.text = 'Dependency Loop Detected';
|
||||
if (!this.creature || !this.creature.computeErrors) return [];
|
||||
return this.creature.computeErrors;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
expanded(value) {
|
||||
if (this.context.editPermission === false) return;
|
||||
updateCreature.call({
|
||||
_id: this.creatureId,
|
||||
path: ['settings', 'hideCalculationErrors'],
|
||||
value: !value || null,
|
||||
}, (error) => {
|
||||
if (error){
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.expanded = !this.creature.settings.hideCalculationErrors;
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<v-alert
|
||||
v-if="model"
|
||||
outlined
|
||||
dense
|
||||
border="bottom"
|
||||
colored-border
|
||||
elevation="2"
|
||||
type="warning"
|
||||
class="dependency-loop-error"
|
||||
>
|
||||
@@ -54,6 +54,13 @@ export default {
|
||||
components: {
|
||||
TreeNodeView,
|
||||
},
|
||||
inject: {
|
||||
theme: {
|
||||
default: {
|
||||
isDark: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
@@ -86,7 +93,7 @@ export default {
|
||||
elementId: `breadcrumb-${id}`,
|
||||
data: {_id: id},
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
},
|
||||
noLinks: Boolean,
|
||||
noIcons: Boolean,
|
||||
editing: Boolean,
|
||||
},
|
||||
computed:{
|
||||
props(){
|
||||
@@ -95,7 +96,10 @@
|
||||
store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `breadcrumb-${id}`,
|
||||
data: {_id: id},
|
||||
data: {
|
||||
_id: id,
|
||||
startInEditTab: this.editing,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -120,7 +124,10 @@
|
||||
store.commit('pushDialogStack', {
|
||||
component: 'creature-root-dialog',
|
||||
elementId: 'breadcrumb-root',
|
||||
data: {_id: this.model.ancestors[0].id},
|
||||
data: {
|
||||
_id: this.model.ancestors[0].id,
|
||||
startInEditTab: this.editing,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,20 @@
|
||||
/>
|
||||
</template>
|
||||
<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
|
||||
mode="out-in"
|
||||
>
|
||||
@@ -51,17 +65,6 @@
|
||||
</component>
|
||||
</div>
|
||||
<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
|
||||
:is="model.type + 'Viewer'"
|
||||
:key="_id"
|
||||
@@ -200,7 +203,7 @@ export default {
|
||||
watch: {
|
||||
_id: {
|
||||
immediate: true,
|
||||
handler(newId){
|
||||
handler(newId) {
|
||||
this.$nextTick(() => {
|
||||
this.currentId = newId;
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
:style="`border: solid 1px ${accentColor};`"
|
||||
hover
|
||||
class="slot-card d-flex flex-column"
|
||||
style="max-width: 400px;"
|
||||
@mouseover="hover = true"
|
||||
@mouseleave="hover = false"
|
||||
@click="$emit('click')"
|
||||
@@ -18,7 +17,10 @@
|
||||
{{ model.name }}
|
||||
</v-card-title>
|
||||
<v-card-text v-if="model.description">
|
||||
{{ model.description.value }}
|
||||
<property-description
|
||||
text
|
||||
:model="model.description"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-spacer />
|
||||
<v-card-actions>
|
||||
@@ -36,10 +38,12 @@
|
||||
|
||||
<script lang="js">
|
||||
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
|
||||
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CardHighlight,
|
||||
PropertyDescription,
|
||||
},
|
||||
inject: {
|
||||
theme: {
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
<template>
|
||||
<div class="slots-to-fill">
|
||||
<v-slide-y-transition
|
||||
<column-layout wide-columns class="slots-to-fill">
|
||||
<v-fade-transition
|
||||
group
|
||||
leave-absolute
|
||||
hide-on-leave
|
||||
>
|
||||
<slot-card
|
||||
<div
|
||||
v-for="slot in slots"
|
||||
:key="slot._id"
|
||||
:model="slot"
|
||||
class="ma-1"
|
||||
hover
|
||||
style="display: inline-block !important; transition: all 0.3s !important;"
|
||||
@ignore="ignoreSlot(slot._id)"
|
||||
@click="fillSlot(slot._id)"
|
||||
/>
|
||||
</v-slide-y-transition>
|
||||
</div>
|
||||
style="transition: all 0.3s !important"
|
||||
>
|
||||
<slot-card
|
||||
:model="slot"
|
||||
hover
|
||||
@ignore="ignoreSlot(slot._id)"
|
||||
@click="fillSlot(slot._id)"
|
||||
/>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
</column-layout>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import SlotCard from '/imports/ui/creature/slots/SlotCard.vue';
|
||||
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
|
||||
import updateCreatureProperty from '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js';
|
||||
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
@@ -28,6 +32,7 @@ import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
export default {
|
||||
components: {
|
||||
SlotCard,
|
||||
ColumnLayout,
|
||||
},
|
||||
inject: {
|
||||
context: { default: {} }
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
style="height: 100%; transform-origin: left; transition: all 0.5s ease;"
|
||||
:style="{
|
||||
backgroundColor: barColor,
|
||||
transform: `scaleX(${value / maxValue})`,
|
||||
transform: `scaleX(${fillFraction})`,
|
||||
}"
|
||||
/>
|
||||
<div
|
||||
@@ -92,87 +92,93 @@ import IncrementMenu from '/imports/ui/components/IncrementMenu.vue';
|
||||
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
|
||||
import chroma from 'chroma-js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IncrementMenu
|
||||
},
|
||||
inject: {
|
||||
theme: {
|
||||
default: {
|
||||
isDark: false,
|
||||
},
|
||||
export default {
|
||||
components: {
|
||||
IncrementMenu
|
||||
},
|
||||
inject: {
|
||||
theme: {
|
||||
default: {
|
||||
isDark: false,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
value: Number,
|
||||
maxValue: Number,
|
||||
name: String,
|
||||
color: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.$vuetify.theme.currentTheme.primary
|
||||
},
|
||||
},
|
||||
props: {
|
||||
value: Number,
|
||||
maxValue: Number,
|
||||
name: String,
|
||||
color: {
|
||||
type: String,
|
||||
default() {
|
||||
return this.$vuetify.theme.currentTheme.primary
|
||||
},
|
||||
midColor: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
lowColor: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
_id: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
hover: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
barColor() {
|
||||
const fraction = this.value / this.maxValue;
|
||||
if (!Number.isFinite(fraction)) return this.color;
|
||||
if (fraction > 0.5){
|
||||
return this.color;
|
||||
} else if (this.midColor && this.lowColor) {
|
||||
return chroma.mix(this.lowColor, this.midColor, fraction * 2).hex();
|
||||
} else if (this.midColor){
|
||||
return this.midColor;
|
||||
}
|
||||
},
|
||||
midColor: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
lowColor: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
_id: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
hover: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
fillFraction() {
|
||||
let fraction = this.value / this.maxValue;
|
||||
if (fraction < 0) fraction = 0;
|
||||
if (fraction > 1) fraction = 1;
|
||||
return fraction;
|
||||
},
|
||||
barColor() {
|
||||
const fraction = this.value / this.maxValue;
|
||||
if (!Number.isFinite(fraction)) return this.color;
|
||||
if (fraction > 0.5){
|
||||
return this.color;
|
||||
},
|
||||
barBackgroundColor(){
|
||||
return chroma(this.barColor)
|
||||
.darken(1.5)
|
||||
.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);
|
||||
}
|
||||
*/
|
||||
} else if (this.midColor && this.lowColor) {
|
||||
return chroma.mix(this.lowColor, this.midColor, fraction * 2).hex();
|
||||
} else if (this.midColor){
|
||||
return this.midColor;
|
||||
}
|
||||
return this.color;
|
||||
},
|
||||
methods: {
|
||||
edit() {
|
||||
this.editing = true;
|
||||
},
|
||||
cancelEdit() {
|
||||
this.editing = false;
|
||||
},
|
||||
changeIncrementMenu(e){
|
||||
this.$emit('change', e);
|
||||
this.editing = false;
|
||||
barBackgroundColor(){
|
||||
return chroma(this.barColor)
|
||||
.darken(1.5)
|
||||
.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);
|
||||
}
|
||||
},
|
||||
};
|
||||
*/
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
edit() {
|
||||
this.editing = true;
|
||||
},
|
||||
cancelEdit() {
|
||||
this.editing = false;
|
||||
},
|
||||
changeIncrementMenu(e){
|
||||
this.$emit('change', e);
|
||||
this.editing = false;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<v-btn
|
||||
icon
|
||||
small
|
||||
:disabled="model.value >= model.total || context.editPermission === false"
|
||||
:disabled="(model.value >= model.total && !model.ignoreUpperLimit) || context.editPermission === false"
|
||||
@click="increment(1)"
|
||||
>
|
||||
<v-icon>mdi-chevron-up</v-icon>
|
||||
@@ -16,7 +16,7 @@
|
||||
<v-btn
|
||||
icon
|
||||
small
|
||||
:disabled="model.value <= 0 || context.editPermission === false"
|
||||
:disabled="(model.value <= 0 && !model.ignoreLowerLimit) || context.editPermission === false"
|
||||
@click="increment(-1)"
|
||||
>
|
||||
<v-icon>mdi-chevron-down</v-icon>
|
||||
@@ -28,7 +28,10 @@
|
||||
<div class="text-h4">
|
||||
{{ model.value }}
|
||||
</div>
|
||||
<div class="text-h6 ml-2 max-value">
|
||||
<div
|
||||
v-if="model.total !== 0"
|
||||
class="text-h6 ml-2 max-value"
|
||||
>
|
||||
/{{ model.total }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,6 @@
|
||||
<form-section
|
||||
v-if="model.attributeType === 'healthBar'"
|
||||
name="Health Bar Settings"
|
||||
standalone
|
||||
>
|
||||
<color-picker
|
||||
:value="model.healthBarColorMid"
|
||||
@@ -90,6 +89,47 @@
|
||||
label="Empty color"
|
||||
@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>
|
||||
</v-expand-transition>
|
||||
|
||||
@@ -121,6 +161,18 @@
|
||||
:error-messages="errors.decimal"
|
||||
@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
|
||||
class="layout justify-center"
|
||||
style="align-self: stretch;"
|
||||
|
||||
@@ -36,22 +36,34 @@
|
||||
@change="change('target', ...arguments)"
|
||||
/>
|
||||
</v-expand-transition>
|
||||
<smart-combobox
|
||||
label="Tags"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
/>
|
||||
<form-section
|
||||
v-if="$slots.children"
|
||||
name="Children"
|
||||
standalone
|
||||
>
|
||||
<slot name="children" />
|
||||
</form-section>
|
||||
<form-sections>
|
||||
<form-section
|
||||
v-if="$slots.children"
|
||||
name="Children"
|
||||
standalone
|
||||
>
|
||||
<slot name="children" />
|
||||
</form-section>
|
||||
<form-section
|
||||
name="Advanced"
|
||||
>
|
||||
<smart-switch
|
||||
label="Hide remove button"
|
||||
:value="model.hideRemoveButton"
|
||||
:error-messages="errors.hideRemoveButton"
|
||||
@change="change('hideRemoveButton', ...arguments)"
|
||||
/>
|
||||
<smart-combobox
|
||||
label="Tags"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
/>
|
||||
</form-section>
|
||||
</form-sections>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -60,12 +72,6 @@
|
||||
|
||||
export default {
|
||||
mixins: [propertyFormMixin],
|
||||
props: {
|
||||
parentTarget: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data(){return {
|
||||
targetOptions: [
|
||||
{
|
||||
|
||||
172
app/imports/ui/properties/forms/BuffRemoverForm.vue
Normal file
172
app/imports/ui/properties/forms/BuffRemoverForm.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<template lang="html">
|
||||
<div class="buff-remover-form">
|
||||
<text-field
|
||||
ref="focusFirst"
|
||||
label="Name"
|
||||
:value="model.name"
|
||||
:error-messages="errors.name"
|
||||
@change="change('name', ...arguments)"
|
||||
/>
|
||||
<smart-switch
|
||||
label="Remove parent buff"
|
||||
:value="model.targetParentBuff"
|
||||
:error-messages="errors.targetParentBuff"
|
||||
@change="change('targetParentBuff', ...arguments)"
|
||||
/>
|
||||
<v-expand-transition>
|
||||
<div v-if="!model.targetParentBuff">
|
||||
<smart-switch
|
||||
:label="model.removeAll ? 'Remove All. All matching buffs will be removed' : 'Remove All. Only 1 matching buff will be removed'"
|
||||
:value="model.removeAll"
|
||||
:error-messages="errors.removeAll"
|
||||
@change="change('removeAll', ...arguments)"
|
||||
/>
|
||||
<smart-select
|
||||
label="Target"
|
||||
:items="targetOptions"
|
||||
:value="model.target"
|
||||
:error-messages="errors.target"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="change('target', ...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 effect will apply to properties that have all the listed tags"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
persistent-hint
|
||||
:value="model.targetTags"
|
||||
:error-messages="errors.targetTags"
|
||||
@change="change('targetTags', ...arguments)"
|
||||
/>
|
||||
</v-layout>
|
||||
<v-slide-x-transition
|
||||
v-if="!model.targetParentBuff"
|
||||
group
|
||||
>
|
||||
<div
|
||||
v-for="(extras, i) in model.extraTags"
|
||||
:key="extras._id"
|
||||
class="target-tags layout align-center justify-space-between"
|
||||
>
|
||||
<smart-select
|
||||
label="Operation"
|
||||
style="width: 90px; flex-grow: 0;"
|
||||
:items="['OR', 'NOT']"
|
||||
: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 effect will also target properties that have all of these tags instead' : 'The effect will ignore properties that have any of these tags'"
|
||||
class="mx-2"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
persistent-hint
|
||||
: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>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
<form-sections>
|
||||
<form-section
|
||||
v-if="$slots.children"
|
||||
name="Children"
|
||||
standalone
|
||||
>
|
||||
<slot name="children" />
|
||||
</form-section>
|
||||
<form-section
|
||||
name="Advanced"
|
||||
>
|
||||
<smart-combobox
|
||||
label="Tags"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||
:value="model.tags"
|
||||
@change="change('tags', ...arguments)"
|
||||
/>
|
||||
</form-section>
|
||||
</form-sections>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
|
||||
import {
|
||||
BuffRemoverSchema
|
||||
} from '/imports/api/properties/BuffRemovers.js';
|
||||
|
||||
export default {
|
||||
mixins: [propertyFormMixin],
|
||||
data(){return {
|
||||
addExtraTagsLoading: false,
|
||||
extraTagOperations: ['OR', 'NOT'],
|
||||
targetOptions: [
|
||||
{
|
||||
text: 'Self',
|
||||
value: 'self',
|
||||
}, {
|
||||
text: 'Target',
|
||||
value: 'target',
|
||||
},
|
||||
],
|
||||
}},
|
||||
computed: {
|
||||
extraTagsFull(){
|
||||
if (!this.model.extraTags) return false;
|
||||
let maxCount = BuffRemoverSchema.get('extraTags', 'maxCount');
|
||||
return this.model.extraTags.length >= maxCount;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
acknowledgeAddResult(){
|
||||
this.addExtraTagsLoading = false;
|
||||
},
|
||||
addExtraTags(){
|
||||
this.addExtraTagsLoading = true;
|
||||
this.$emit('push', {
|
||||
path: ['extraTags'],
|
||||
value: {
|
||||
_id: Random.id(),
|
||||
operation: 'OR',
|
||||
tags: [],
|
||||
},
|
||||
ack: this.acknowledgeAddResult,
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template lang="html">
|
||||
<div class="slot-form">
|
||||
<div class="trigger-form">
|
||||
<text-field
|
||||
ref="focusFirst"
|
||||
label="Name"
|
||||
@@ -8,64 +8,6 @@
|
||||
@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
|
||||
label="Timing"
|
||||
style="flex-basis: 300px;"
|
||||
@@ -95,6 +37,70 @@
|
||||
@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
|
||||
label="Condition"
|
||||
hint="A caclulation to determine if this trigger should fire"
|
||||
@@ -105,15 +111,6 @@
|
||||
$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
|
||||
label="Description"
|
||||
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;
|
||||
let maxCount = TriggerSchema.get('extraTags', 'maxCount');
|
||||
return this.model.extraTags.length >= maxCount;
|
||||
},
|
||||
showTags() {
|
||||
return this.model.event !== 'shortRest' &&
|
||||
this.model.event !== 'longRest' &&
|
||||
this.model.event !== 'anyRest';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -2,6 +2,7 @@ const ActionForm = () => import('/imports/ui/properties/forms/ActionForm.vue');
|
||||
const AdjustmentForm = () => import('/imports/ui/properties/forms/AdjustmentForm.vue');
|
||||
const AttributeForm = () => import('/imports/ui/properties/forms/AttributeForm.vue');
|
||||
const BuffForm = () => import('/imports/ui/properties/forms/BuffForm.vue');
|
||||
const BuffRemoverForm = () => import('/imports/ui/properties/forms/BuffRemoverForm.vue');
|
||||
const BranchForm = () => import('/imports/ui/properties/forms/BranchForm.vue');
|
||||
const ClassForm = () => import('/imports/ui/properties/forms/ClassForm.vue');
|
||||
const ClassLevelForm = () => import('/imports/ui/properties/forms/ClassLevelForm.vue');
|
||||
@@ -31,6 +32,7 @@ export default {
|
||||
adjustment: AdjustmentForm,
|
||||
attribute: AttributeForm,
|
||||
buff: BuffForm,
|
||||
buffRemover: BuffRemoverForm,
|
||||
branch: BranchForm,
|
||||
constant: ConstantForm,
|
||||
container: ContainerForm,
|
||||
|
||||
97
app/imports/ui/properties/viewers/BuffRemoverViewer.vue
Normal file
97
app/imports/ui/properties/viewers/BuffRemoverViewer.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template lang="html">
|
||||
<div class="buff-remover-viewer">
|
||||
<v-row dense>
|
||||
<property-field
|
||||
v-if="model.target === 'self'"
|
||||
name="Target"
|
||||
value="Self"
|
||||
/>
|
||||
<template v-if="!model.targetParentBuff">
|
||||
<property-field
|
||||
v-if="model.target === 'self'"
|
||||
name="Target"
|
||||
value="Self"
|
||||
/>
|
||||
<property-field
|
||||
name="When applied"
|
||||
:value="model.removeAll ? 'Remove all matching buffs' : 'Remove 1 matching buff'"
|
||||
/>
|
||||
<property-field
|
||||
name="Targeted tags"
|
||||
>
|
||||
<div>
|
||||
<div class="d-flex flex-wrap">
|
||||
<v-chip
|
||||
v-for="(tag, index) in model.targetTags"
|
||||
:key="index"
|
||||
class="ma-1"
|
||||
>
|
||||
{{ tag }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<div
|
||||
v-for="ex in model.extraTags"
|
||||
:key="ex._id"
|
||||
>
|
||||
<span class="ma-2">
|
||||
{{ ex.operation }}
|
||||
</span>
|
||||
<div class="d-flex flex-wrap">
|
||||
<v-chip
|
||||
v-for="(extraTag, index) in ex.tags"
|
||||
:key="index"
|
||||
class="ma-1"
|
||||
>
|
||||
{{ extraTag }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</property-field>
|
||||
</template>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js'
|
||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||
|
||||
export default {
|
||||
mixins: [propertyViewerMixin],
|
||||
computed: {
|
||||
reset(){
|
||||
let reset = this.model.reset
|
||||
if (reset === 'shortRest'){
|
||||
return `Reset${
|
||||
this.model.resetMultiplier && ' x' + this.model.resetMultiplier
|
||||
} on a short rest`;
|
||||
} else if (reset === 'longRest'){
|
||||
return `Reset${
|
||||
this.model.resetMultiplier && ' x' + this.model.resetMultiplier
|
||||
} on a long rest`;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
numberToSignedString,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.ability-value {
|
||||
font-weight: 600;
|
||||
font-size: 24px !important;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
}
|
||||
.mod, .ability-value {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
.attribute-value {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,18 @@
|
||||
<template lang="html">
|
||||
<div class="trigger-viewer">
|
||||
<v-row dense>
|
||||
<property-field
|
||||
name="Timing"
|
||||
:value="timingText"
|
||||
/>
|
||||
<property-field
|
||||
name="Event"
|
||||
:value="eventText"
|
||||
/>
|
||||
<property-field
|
||||
name="Event Type"
|
||||
:value="actionPropertyText"
|
||||
/>
|
||||
<property-field
|
||||
v-if="(model.targetTags && model.targetTags.length) || (model.extraTags && model.extraTags.length)"
|
||||
name="Tags Required"
|
||||
@@ -23,10 +31,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</property-field>
|
||||
<property-description
|
||||
name="Summary"
|
||||
:model="model.summary"
|
||||
/>
|
||||
<property-description
|
||||
name="Description"
|
||||
:model="model.description"
|
||||
@@ -36,39 +40,34 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js'
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
import FillSlotButton from '/imports/ui/creature/buildTree/FillSlotButton.vue';
|
||||
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js'
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
import { timingOptions, eventOptions, actionPropertyTypeOptions } from '/imports/api/properties/Triggers.js';
|
||||
|
||||
const eventText = {
|
||||
doActionProperty: 'Do action property',
|
||||
receiveActionProperty: 'Receiving action property',
|
||||
flipToggle: 'Toggle changed',
|
||||
adjustProperty: 'Attribute adjusted',
|
||||
anyRest: 'Short or long rest',
|
||||
longRest: 'Long rest',
|
||||
shortRest: 'Short rest',
|
||||
export default {
|
||||
mixins: [propertyViewerMixin],
|
||||
inject: {
|
||||
context: {
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
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>
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
<div
|
||||
v-if="calculation && calculation.effects"
|
||||
class="flex-grow-1"
|
||||
style="max-width: 100%;"
|
||||
>
|
||||
<inline-effect
|
||||
v-if="typeof calculation.value === 'number'"
|
||||
|
||||
@@ -2,6 +2,7 @@ const ActionViewer = () => import ('/imports/ui/properties/viewers/ActionViewer.
|
||||
const AdjustmentViewer = () => import ('/imports/ui/properties/viewers/AdjustmentViewer.vue');
|
||||
const AttributeViewer = () => import ('/imports/ui/properties/viewers/AttributeViewer.vue');
|
||||
const BuffViewer = () => import ('/imports/ui/properties/viewers/BuffViewer.vue');
|
||||
const BuffRemoverViewer = () => import ('/imports/ui/properties/viewers/BuffRemoverViewer.vue');
|
||||
const BranchViewer = () => import ('/imports/ui/properties/viewers/BranchViewer.vue');
|
||||
const ContainerViewer = () => import ('/imports/ui/properties/viewers/ContainerViewer.vue');
|
||||
const ClassViewer = () => import ('/imports/ui/properties/viewers/ClassViewer.vue');
|
||||
@@ -31,6 +32,7 @@ export default {
|
||||
adjustment: AdjustmentViewer,
|
||||
attribute: AttributeViewer,
|
||||
buff: BuffViewer,
|
||||
buffRemover: BuffRemoverViewer,
|
||||
branch: BranchViewer,
|
||||
container: ContainerViewer,
|
||||
class: ClassViewer,
|
||||
|
||||
94
app/imports/ui/styles/markdown.css
Normal file
94
app/imports/ui/styles/markdown.css
Normal 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)
|
||||
}
|
||||
@@ -6,5 +6,6 @@ import './fitAvatars.css';
|
||||
import './inheritBackgrounds.css';
|
||||
import './largeFormatInputs.css';
|
||||
import './lineClamp.css';
|
||||
import './markdown.css';
|
||||
import './speedDial.css';
|
||||
import './toolbarFlex.css';
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import dialogStackStore from '/imports/ui/dialogStack/dialogStackStore.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
const tabs = ['stats', 'features', 'inventory', 'spells', 'journal', 'build', 'tree'];
|
||||
const tabsWithoutSpells = ['stats', 'features', 'inventory', 'journal', 'build', 'tree'];
|
||||
|
||||
Vue.use(Vuex);
|
||||
const store = new Vuex.Store({
|
||||
@@ -16,13 +19,21 @@ const store = new Vuex.Store({
|
||||
showDetailsDialog: false,
|
||||
},
|
||||
getters: {
|
||||
// ...
|
||||
tabById: (state) => (id) => {
|
||||
if (id in state.characterSheetTabs){
|
||||
return state.characterSheetTabs[id];
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
tabNameById: (state) => (id) => {
|
||||
const tabNumber = state.characterSheetTabs[id];
|
||||
const creature = Creatures.findOne(id);
|
||||
if (creature?.settings?.hideSpellsTab) {
|
||||
return tabsWithoutSpells[tabNumber];
|
||||
} else {
|
||||
return tabs[tabNumber]
|
||||
}
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dicecloud",
|
||||
"version": "2.0.33",
|
||||
"version": "2.0.38",
|
||||
"description": "Unofficial Online Realtime D&D 5e App",
|
||||
"license": "GPL-3.0",
|
||||
"repository": {
|
||||
@@ -11,7 +11,8 @@
|
||||
"scripts": {
|
||||
"run": "meteor",
|
||||
"debug": "meteor --inspect",
|
||||
"test": "meteor test --driver-package meteortesting:mocha --port 3001"
|
||||
"test": "meteor test --driver-package meteortesting:mocha --port 3001",
|
||||
"build": "meteor build ../build --architecture os.linux.x86_64"
|
||||
},
|
||||
"engines": {
|
||||
"node": "14.0.x",
|
||||
|
||||
Reference in New Issue
Block a user