Compare commits

..

18 Commits

Author SHA1 Message Date
Stefan Zermatten
3801b17fde Attacks can now critical hit. criticalHitTarget overrides the roll required 2021-02-22 14:07:12 +02:00
Stefan Zermatten
88133a2fa3 Saving throws now work in actions 2021-02-22 12:38:21 +02:00
Stefan Zermatten
d00eedac19 Rolls now work in actions 2021-02-22 11:55:08 +02:00
Stefan Zermatten
6571fb860a Toggles now work in actions to make choices based on action context 2021-02-22 11:36:30 +02:00
Stefan Zermatten
8148f4d701 Fixed: Library nodes are published in order to prevent disordered building of the tree 2021-02-21 17:05:09 +02:00
Stefan Zermatten
523c34b719 Fixed: Slots that use conditions now only hide on falsey value (false, 0, '') 2021-02-21 17:01:24 +02:00
Stefan Zermatten
e833fba870 Fixed: bug that stopped buffs being deleted 2021-02-21 16:35:35 +02:00
Stefan Zermatten
f3e191c12e Fixed: Inserting properties to the tree now animate correctly to the inserted property 2021-02-20 16:00:40 +02:00
Stefan Zermatten
33415275a3 Item tiles are now smaller in the inventory view 2021-02-20 15:53:58 +02:00
Stefan Zermatten
3b1151d987 Fixed notes without summaries are no longer oversized 2021-02-20 15:52:15 +02:00
Stefan Zermatten
4288f98f7c Fixed: stats with no ability selected have an ability modifier of 0 instead of NaN 2021-02-20 15:50:25 +02:00
Stefan Zermatten
1a2ef8a4a2 Fixed: markdown images no longer overflow their container width 2021-02-20 15:45:45 +02:00
Stefan Zermatten
10e9a5faa8 Notes now show both summary and description in viewer 2021-02-20 15:41:30 +02:00
Stefan Zermatten
53594c0004 Fixed: items in containers not following tree order 2021-02-20 15:38:51 +02:00
Stefan Zermatten
e068675b46 Fixed and improved: Discord webhooks are working again with a new format 2021-02-20 15:27:20 +02:00
Stefan Zermatten
067f5df36e Fixed: Emptying the search bar in slot filler dialog now correctly clears the search 2021-02-20 10:17:35 +02:00
Stefan Zermatten
6113d86059 Fixed typo in calling a function in Constants autovalue 2021-02-16 11:19:50 +02:00
Stefan Zermatten
e3862bcdd9 Fixed constants autovalue 2021-02-16 10:49:18 +02:00
33 changed files with 488 additions and 127 deletions

View File

@@ -2,23 +2,12 @@ import spendResources from '/imports/api/creature/actions/spendResources.js'
import embedInlineCalculations from '/imports/api/creature/computation/afterComputation/embedInlineCalculations.js';
export default function applyAction({prop, log}){
spendResources(prop);
// If this is not the top level action, we can add its name to the log
if (log.content.length){
log.content.push({name: prop.name});
}
let content = { name: prop.name };
if (prop.summary){
log.content.push({
description: embedInlineCalculations(
prop.summary, prop.summaryCalculations
),
});
}
if (prop.description){
log.content.push({
description: embedInlineCalculations(
prop.description, prop.descriptionCalculations
),
});
content.description = embedInlineCalculations(
prop.summary, prop.summaryCalculations
);
}
log.content.push(content);
spendResources({prop, log});
}

View File

@@ -16,10 +16,16 @@ export default function applyAdjustment({
try {
var {result, errors} = evaluateString(prop.amount, scope, 'reduce');
if (typeof result !== 'number') {
log.content.push({error: errors.join(', ') || 'Something went wrong'});
log.content.push({
name: 'Attribute damage',
error: errors.join(', ') || 'Something went wrong',
});
}
} catch (e){
log.content.push({error: e.toString()});
log.content.push({
name: 'Attribute damage',
error: e.toString(),
});
}
if (damageTargets) {
damageTargets.forEach(target => {
@@ -33,12 +39,14 @@ export default function applyAdjustment({
value: result
});
log.content.push({
name: 'Attribute damage',
resultPrefix: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''}`,
result: `${-result}`,
});
});
} else {
log.content.push({
name: 'Attribute damage',
resultPrefix: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''}`,
result: `${-result}`,
});

View File

@@ -3,12 +3,21 @@ import roll from '/imports/parser/roll.js';
export default function applyAttack({
prop,
log,
actionContext,
creature,
}){
let result = roll(1, 20)[0] + prop.rollBonusResult;
let value = roll(1, 20)[0];
actionContext.attackRoll = {value};
let criticalHitTarget = creature.variables.criticalHitTarget &&
creature.variables.criticalHitTarget.currentValue || 20;
let criticalHit = value >= criticalHitTarget;
if (criticalHit) actionContext.criticalHit = {value: true};
let result = value + prop.rollBonusResult;
actionContext.toHit = {value: result};
log.content.push({
// If this is not the first item in the log content, give it a name
name: log.content.length ? prop.name + ' attack' : undefined,
name: criticalHit ? 'Critical Hit!' : 'To Hit',
resultPrefix: `1d20 [${value}] + ${prop.rollBonusResult} = `,
result,
details: 'to hit',
});
}

View File

@@ -1,6 +1,7 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import dealDamage from '/imports/api/creature/creatureProperties/methods/dealDamage.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
import { CompilationContext } from '/imports/parser/parser.js';
export default function applyDamage({
prop,
@@ -14,8 +15,19 @@ export default function applyDamage({
...creature.variables,
...actionContext,
};
if (targets.length === 1){
scope.target = targets[0].variables;
}
let criticalHit = !!(
actionContext.criticalHit &&
actionContext.criticalHit.value &&
prop.damageType !== 'healing' // Can't critically heal
);
let context = new CompilationContext({
doubleRolls: criticalHit,
});
try {
var {result, errors} = evaluateString(prop.amount, scope, 'reduce');
var {result, errors} = evaluateString(prop.amount, scope, 'reduce', context);
if (typeof result !== 'number') {
log.content.push({
error: errors.join(', '),
@@ -26,8 +38,13 @@ export default function applyDamage({
error: e.toString(),
});
}
let suffix = (criticalHit ? ' critical ' : '') +
prop.damageType +
(prop.damageType !== 'healing' ? ' damage': '');
if (damageTargets && damageTargets.length) {
damageTargets.forEach(target => {
let name = prop.damageType === 'healing' ? 'Healing' : 'Damage';
if (prop.target === 'each'){
result = evaluateString(prop.amount, scope, 'reduce');
}
@@ -38,25 +55,24 @@ export default function applyDamage({
});
if (target._id === creature._id){
log.content.push({
name,
result: damageDealt,
details: `${prop.damageType}`+
`${prop.damageType !== 'healing' ? ' damage': ''} to self`,
details: suffix + 'to self',
});
} else {
log.content.push({
name,
resultPrefix: 'Dealt ',
result: damageDealt,
details: `${prop.damageType}` +
`${prop.damageType !== 'healing'? ' damage': ''}` +
`${target.name && ' to '}${target.name}`,
details: suffix + `${target.name && ' to '}${target.name}`,
});
insertCreatureLog.call({
log: {
content: [{
name,
resultPrefix: 'Recieved ',
result: damageDealt,
details: `${prop.damageType}` +
`${prop.damageType !== 'healing'? ' damage': ''}`
details: suffix,
}],
creatureId: target._id,
}
@@ -65,8 +81,9 @@ export default function applyDamage({
});
} else {
log.content.push({
name: prop.damageType === 'healing' ? 'Healing' : 'Damage',
result,
details: `${prop.damageType}${prop.damageType !== 'healing'? ' damage': ''}`,
details: suffix,
});
}
}

View File

@@ -1,8 +1,11 @@
import applyAction from '/imports/api/creature/actions/applyAction.js';
import applyAdjustment from '/imports/api/creature/actions/applyAdjustment.js';
import applyAttack from '/imports/api/creature/actions/applyAttack.js';
import applyDamage from '/imports/api/creature/actions/applyDamage.js';
import applyBuff from '/imports/api/creature/actions/applyBuff.js';
import applyDamage from '/imports/api/creature/actions/applyDamage.js';
import applyRoll from '/imports/api/creature/actions/applyRoll.js';
import applyToggle from '/imports/api/creature/actions/applyToggle.js';
import applySave from '/imports/api/creature/actions/applySave.js';
function applyProperty(options){
let prop = options.prop;
@@ -11,8 +14,13 @@ function applyProperty(options){
if (prop.applied === true){
return false;
}
// Only ignore toggles if they wont be computed
} else if (prop.type === 'toggle') {
if (prop.disabled) return false;
if (prop.enabled) return true;
if (!prop.condition) return false;
// Ignore inactive props of other types
} else if (prop.inactive === true){
} else if (prop.deactivatedBySelf === true){
return false;
}
switch (prop.type){
@@ -33,40 +41,40 @@ function applyProperty(options){
case 'buff':
applyBuff(options);
break;
case 'toggle':
return applyToggle(options);
case 'roll':
// applyRoll(options);
applyRoll(options);
break;
case 'savingThrow':
// applySavingThrow(options);
break;
return applySave(options);
}
return true;
}
export default function applyProperties({
forest,
creature,
targets,
actionContext,
log,
}){
function applyPropertyAndWalkChildren({prop, child, targets, ...options}){
let shouldKeepWalking = applyProperty({ prop, targets, ...options });
if (shouldKeepWalking){
applyProperties({ forest: child.children, targets, ...options,});
}
}
export default function applyProperties({ forest, targets, ...options}){
forest.forEach(child => {
let walkChildren = applyProperty({
prop: child.node,
children: child.children,
creature,
targets,
actionContext,
log,
});
if (walkChildren){
applyProperties({
forest: child.children,
creature,
targets,
actionContext,
log,
let prop = child.node;
if (shouldSplit(prop)){
targets.forEach(target => {
let targets = [target]
applyPropertyAndWalkChildren({ targets, prop, child, ...options});
});
} else {
applyPropertyAndWalkChildren({prop, child, targets, ...options});
}
});
}
function shouldSplit(prop){
if (prop.target === 'each'){
return true;
}
}

View File

@@ -0,0 +1,32 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
export default function applyRoll({
prop,
creature,
actionContext,
log,
}){
let scope = {
...creature.variables,
...actionContext,
};
try {
var {result, errors} = evaluateString(prop.roll, scope, 'reduce');
actionContext[prop.variableName] = result;
log.content.push({
name: prop.name,
resultPrefix: prop.variableName + ' = ' + prop.roll + ' = ',
result,
});
if (errors.length) {
log.content.push({
error: errors.join(', '),
});
}
} catch (e){
log.content.push({
error: e.toString(),
});
}
}

View File

@@ -0,0 +1,78 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import CreaturesProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import roll from '/imports/parser/roll.js';
export default function applySave({
prop,
creature,
actionContext,
log,
}){
let scope = {
...creature.variables,
...actionContext,
};
try {
// Calculate the DC
var {result, errors} = evaluateString(prop.dc, scope, 'reduce');
let dc = result;
log.content.push({
name: prop.name,
resultPrefix: ' DC ',
result,
});
if (errors.length) {
log.content.push({
error: errors.join(', '),
});
return false;
}
if (prop.target === 'self'){
let save = CreaturesProperties.findOne({
'ancestors.id': creature._id,
type: 'skill',
skillType: 'save',
variableName: prop.stat,
removed: {$ne: true},
inactive: {$ne: true},
});
if (!save){
log.content.push({
error: 'No saving throw found: ' + prop.stat,
});
return;
}
let value, values, resultPrefix;
if (save.advantage === 1){
values = roll(2, 20).sort().reverse();
value = values[0];
resultPrefix = `Advantage: 1d20 [${values[0]},~~${values[1]}~~] + ${save.value} = `
} else if (save.advantage === -1){
values = roll(2, 20).sort();
value = values[0];
resultPrefix = `Disadvantage: 1d20 [${values[0]},~~${values[1]}~~] + ${save.value} = `
} else {
values = roll(1, 20);
value = values[0];
resultPrefix = `1d20 [${value}] + ${save.value} = `
}
actionContext.savingThrowRoll = {value};
let result = value + save.value;
actionContext.savingThrow = {value: result};
let saveSuccess = result >= dc;
log.content.push({
name: 'Save',
resultPrefix,
result,
details: saveSuccess ? 'Passed' : 'Failed'
});
return !saveSuccess;
} else {
// TODO
}
} catch (e){
log.content.push({
error: e.toString(),
});
}
}

View File

@@ -0,0 +1,35 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
export default function applyToggle({
prop,
creature,
actionContext,
log,
}){
let scope = {
...creature.variables,
...actionContext,
};
if (Number.isFinite(+prop.condition)){
return !!+prop.condition;
}
try {
var {result, errors} = evaluateString(prop.condition, scope, 'reduce');
if (typeof result !== 'number' && typeof result !== 'boolean') {
log.content.push({
error: errors.join(', '),
});
return false;
}
log.content.push({
name: prop.name,
resultPrefix: prop.condition + ' = ',
result,
});
return !!result;
} catch (e){
log.content.push({
error: e.toString(),
});
}
}

View File

@@ -3,7 +3,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/Creatures.js';
import CreatureLogs, { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
@@ -60,7 +60,6 @@ export function doActionWork({
}){
// Create the log
let log = CreatureLogSchema.clean({
name: action.name,
creatureId: creature._id,
creatureName: creature.name,
});

View File

@@ -2,28 +2,30 @@ 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';
export default function spendResources(action){
export default function spendResources({prop, log}){
// Check Uses
if (action.usesUsed >= action.usesResult){
if (prop.usesUsed >= prop.usesResult){
throw new Meteor.Error('Insufficient Uses',
'This action has no uses left');
'This prop has no uses left');
}
// Resources
if (action.insufficientResources){
if (prop.insufficientResources){
throw new Meteor.Error('Insufficient Resources',
'This creature doesn\'t have sufficient resources to perform this action');
'This creature doesn\'t have sufficient resources to perform this prop');
}
// Items
let itemQuantityAdjustments = [];
action.resources.itemsConsumed.forEach(itemConsumed => {
let spendLog = [];
let gainLog = [];
prop.resources.itemsConsumed.forEach(itemConsumed => {
if (!itemConsumed.itemId){
throw new Meteor.Error('Ammo not selected',
'No ammo was selected for this action');
'No ammo was selected for this prop');
}
let item = CreatureProperties.findOne(itemConsumed.itemId);
if (!item || item.ancestors[0].id !== action.ancestors[0].id){
if (!item || item.ancestors[0].id !== prop.ancestors[0].id){
throw new Meteor.Error('Ammo not found',
'The action\'s ammo was not found on the creature');
'The prop\'s ammo was not found on the creature');
}
if (!item.equipped){
throw new Meteor.Error('Ammo not equipped',
@@ -35,19 +37,36 @@ export default function spendResources(action){
operation: 'increment',
value: itemConsumed.quantity,
});
let logName = item.name;
if (itemConsumed.quantity > 1 || itemConsumed.quantity < -1){
logName = item.plural || logName;
}
if (itemConsumed.quantity > 0){
spendLog.push(logName + ': ' + itemConsumed.quantity);
} else if (itemConsumed.quantity < 0){
gainLog.push(logName + ': ' + -itemConsumed.quantity);
}
});
// No more errors should be thrown after this line
// Now that we have confirmed that there are no errors, do actual work
//Items
itemQuantityAdjustments.forEach(adjustQuantityWork);
// Use uses
CreatureProperties.update(action._id, {
$inc: {usesUsed: 1}
}, {
selector: action
});
if (prop.usesResult){
CreatureProperties.update(prop._id, {
$inc: {usesUsed: 1}
}, {
selector: prop
});
log.content.push({
name: 'Uses left',
result: prop.usesResult - prop.usesUsed - 1,
});
}
// Damage stats
action.resources.attributesConsumed.forEach(attConsumed => {
prop.resources.attributesConsumed.forEach(attConsumed => {
if (!attConsumed.quantity) return;
let stat = CreatureProperties.findOne(attConsumed.statId);
damagePropertyWork({
@@ -55,5 +74,20 @@ export default function spendResources(action){
operation: 'increment',
value: attConsumed.quantity,
});
if (attConsumed.quantity > 0){
spendLog.push(stat.name + ': ' + attConsumed.quantity);
} else if (attConsumed.quantity < 0){
gainLog.push(stat.name + ': ' + -attConsumed.quantity);
}
});
// Log all the spending
if (gainLog.length) log.content.push({
name: 'Gained',
description: gainLog.join('\n'),
});
if (spendLog.length) log.content.push({
name: 'Spent',
description: spendLog.join('\n'),
});
}

View File

@@ -1,7 +1,7 @@
import { parse, CompilationContext } from '/imports/parser/parser.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
export default function evaluateString(string, scope, fn = 'compile'){
export default function evaluateString(string, scope, fn = 'compile', context){
let errors = [];
if (!string){
errors.push('No string provided');
@@ -18,7 +18,9 @@ export default function evaluateString(string, scope, fn = 'compile'){
errors.push(e);
return {result: string, errors};
}
let context = new CompilationContext();
if (!context){
context = new CompilationContext({});
}
let result = node[fn](scope, context);
if (result instanceof ConstantNode){
return {result: result.value, errors: context.errors}

View File

@@ -76,7 +76,7 @@ function combineAttribute(stat, aggregator, memo){
function combineSkill(stat, aggregator, memo){
// Skills are based on some ability Modifier
let ability = memo.statsByVariableName[stat.ability]
let ability = stat.ability && memo.statsByVariableName[stat.ability]
if (stat.ability && ability){
if (!ability.computationDetails.computed){
computeStat(ability, memo);
@@ -87,6 +87,8 @@ function combineSkill(stat, aggregator, memo){
[ability._id],
ability.dependencies,
);
} else {
stat.abilityMod = 0;
}
// Combine all the child proficiencies
stat.proficiency = stat.baseProficiency || 0;

View File

@@ -43,6 +43,13 @@ let CreaturePropertySchema = new SimpleSchema({
optional: true,
index: 1,
},
// Denormalised flag if this property was made inactive because of its own
// state
deactivatedBySelf: {
type: Boolean,
optional: true,
index: 1,
},
// Denormalised list of all properties or creatures this property depends on
dependencies: {
type: Array,

View File

@@ -18,7 +18,7 @@ const insertProperty = new ValidatedMethod({
run({creatureProperty}) {
let rootCreature = getRootCreatureAncestor(creatureProperty);
assertEditPermission(rootCreature, this.userId);
insertPropertyWork({
return insertPropertyWork({
property: creatureProperty,
creature: rootCreature,
});

View File

@@ -20,9 +20,16 @@ export default function recomputeInactiveProperties(ancestorId){
CreatureProperties.update({
'ancestors.id': ancestorId,
'_id': {$in: disabledIds},
$or: [{inactive: {$ne: true}}, {deactivatedByAncestor: true}],
$or: [
{inactive: {$ne: true}},
{deactivatedBySelf: {$ne: true}},
{deactivatedByAncestor: true},
],
}, {
$set: {inactive: true},
$set: {
inactive: true,
deactivatedBySelf: true,
},
$unset: {deactivatedByAncestor: 1},
}, {
multi: true,
@@ -31,7 +38,10 @@ export default function recomputeInactiveProperties(ancestorId){
// Decendants of inactive properties
CreatureProperties.update({
'ancestors.id': {$eq: ancestorId, $in: disabledIds},
$or: [{inactive: {$ne: true}}, {deactivatedByAncestor: {$ne: true}}],
$or: [
{inactive: {$ne: true}},
{deactivatedByAncestor: {$ne: true}},
],
}, {
$set: {
inactive: true,
@@ -46,7 +56,10 @@ export default function recomputeInactiveProperties(ancestorId){
CreatureProperties.update({
'ancestors.id': {$eq: ancestorId, $nin: disabledIds},
'_id': {$nin: disabledIds},
$or: [{inactive: true}, {deactivatedByAncestor: true}],
$or: [
{inactive: true},
{deactivatedByAncestor: true},
],
}, {
$unset: {
inactive: 1,

View File

@@ -18,13 +18,10 @@ if (Meteor.isServer){
let CreatureLogs = new Mongo.Collection('creatureLogs');
let CreatureLogSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
content: {
type: Array,
defaultValue: [],
maxCount: 25,
},
'content.$': {
type: LogContentSchema,
@@ -68,11 +65,45 @@ function removeOldLogs(creatureId){
});
}
function logToMessageData(log){
let embed = {
fields: [],
};
log.content.forEach(c => {
let field = {};
let descriptionField = {};
if (c.name) field.name = c.name;
let valueArray = [];
if (c.error) valueArray.push(`*${c.error}*`);
if (c.resultPrefix) valueArray.push(`${c.resultPrefix}`);
if (c.result) valueArray.push(`\`${c.result}\``);
if (c.details) valueArray.push(c.details);
if (valueArray.length) field.value = valueArray.join(' ');
if (c.description){
if (!field.value){
field.value = c.description;
} else {
descriptionField.value = c.description;
}
}
if (field.name || field.value){
if (!field.name) field.name = '\u200b';
if (!field.value) field.value = '\u200b';
embed.fields.push(field);
}
if (descriptionField.value){
descriptionField.name = '\u200b';
embed.fields.push(descriptionField);
}
});
return { embeds: [embed] };
}
function logWebhook({log, creature}){
if (Meteor.isServer){
sendWebhookAsCreature({
creature,
content: log.text,
data: logToMessageData(log),
});
}
}

View File

@@ -38,20 +38,21 @@ let ConstantSchema = new SimpleSchema({
return;
}
let string = calc.value;
if (!string) return [];
// Evaluate the calculation with no scope
let {result, context} = parseString(string);
// Any existing errors will result in an early failure
if (context.errors.length) return context.errors;
if (context && context.errors.length) return context.errors;
// Ban variables in constants if necessary
result && result.traverse(node => {
if (node instanceof SymbolNode || node instanceof AccessorNode){
context.storeError()({
context.storeError({
type: 'error',
message: 'Variables can\'t be used to define a constant'
});
}
});
return context.errors;
return context && context.errors || [];
}
},
'errors.$':{
@@ -62,7 +63,7 @@ let ConstantSchema = new SimpleSchema({
function parseString(string, fn = 'compile'){
let context = new CompilationContext();
if (!string){
return {result: string, errors: []};
return {result: string, context};
}
// Parse the string using mathjs
@@ -72,7 +73,7 @@ function parseString(string, fn = 'compile'){
} catch (e) {
let message = prettifyParseError(e);
context.storeError({type: 'error', message});
return {result: string, errors: context.errors};
return {context};
}
let result = node[fn]({/*empty scope*/}, context);
return {result, context}

View File

@@ -1,5 +1,6 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
/**
* Rolls are children to actions or other rolls, they are triggered with 0 or
@@ -20,6 +21,17 @@ import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
* child rolls are applied
*/
let RollSchema = new SimpleSchema({
name: {
type: String,
defaultValue: 'New Roll',
},
// The technical, lowercase, single-word name used in formulae
variableName: {
type: String,
regEx: VARIABLE_NAME_REGEX,
min: 2,
defaultValue: 'newRoll',
},
// The roll, can be simplified, but only computed in context
roll: {
type: String,

View File

@@ -8,11 +8,22 @@ let SavingThrowSchema = new SimpleSchema ({
type: String,
optional: true,
},
// The computed DC
dc: {
type: String,
optional: true,
},
// The variable name of ability the save to roll
// Who this saving throw applies to
target: {
type: String,
defaultValue: 'every',
allowedValues: [
'self', // the character who took the action
'each', // rolled once for `each` target
'every', // rolled once and applied to `every` target
],
},
// The variable name of save to roll
stat: {
type: String,
optional: true,

View File

@@ -1,27 +1,25 @@
import Discord from 'discord.js'
export default function sendWebhook({webhookURL, message, options}){
export default function sendWebhook({webhookURL, data = {}}){
//webhookURL = https://discordapp.com/api/webhooks/<id>/<token>
let urlArray = webhookURL.split('/');
let token = urlArray.pop();
let id = urlArray.pop();
// prevent discord mention exploit
options.disableMentions = 'all';
data.disableMentions = 'all';
const hook = new Discord.WebhookClient(id, token);
// Send a message using the webhook
hook.send(message, options)
console.log(JSON.stringify(data, null, 2));
hook.send(data);
}
export function sendWebhookAsCreature({creature, content, embeds}){
export function sendWebhookAsCreature({creature, data = {}}){
if (!creature || !creature.settings || !creature.settings.discordWebhook) return;
data.username = creature.name;
data.avatarURL = creature.avatarPicture;
sendWebhook({
webhookURL: creature.settings.discordWebhook,
message: content,
options: {
username: creature.name,
avatarURL: creature.avatarPicture,
embeds,
},
data,
});
}

View File

@@ -52,6 +52,8 @@ Meteor.publish('library', function(libraryId){
libraryCursor,
LibraryNodes.find({
'ancestors.id': libraryId,
}, {
sort: {order: 1},
}),
];
});

View File

@@ -73,6 +73,7 @@ Meteor.publish('slotFillers', function(slotId){
}
}
} else {
delete filter.$text
options = {sort: {
name: 1,
order: 1,

View File

@@ -1,6 +1,9 @@
<template lang="html">
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="compiledMarkdown" />
<div
class="markdown"
v-html="compiledMarkdown"
/>
</template>
<script>
@@ -21,3 +24,10 @@
},
}
</script>
<style lang="css">
.markdown img {
max-width: 100%;
margin: 8px 0;
}
</style>

View File

@@ -320,7 +320,7 @@
<script>
import Creatures from '/imports/api/creature/Creatures.js';
import { softRemoveProperty } from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import AttributeCard from '/imports/ui/properties/components/attributes/AttributeCard.vue';
import AbilityListTile from '/imports/ui/properties/components/attributes/AbilityListTile.vue';

View File

@@ -205,8 +205,8 @@
creatureProperty.parent = {collection: 'creatures', id: that.creatureId};
creatureProperty.ancestors = [ {collection: 'creatures', id: that.creatureId}];
setDocToLastOrder({collection: CreatureProperties, doc: creatureProperty});
let creaturePropertyId = insertProperty.call({creatureProperty});
return creaturePropertyId;
let id = insertProperty.call({creatureProperty});
return `tree-node-${id}`;
}
});
},
@@ -217,10 +217,11 @@
elementId: 'insert-creature-property-fab',
callback(libraryNode){
if (!libraryNode) return;
insertPropertyFromLibraryNode.call({
let id = insertPropertyFromLibraryNode.call({
nodeId: libraryNode._id,
parentRef: {collection: 'creatures', id: that.creatureId},
});
return `tree-node-${id}`;
}
});
},

View File

@@ -120,7 +120,7 @@ export default {
'ancestors.id': this.creatureId,
type: 'propertySlot',
$or: [
{slotConditionResult: true},
{slotConditionResult: {$nin: [false, 0, '']}},
{slotConditionResult: {$exists: false}},
],
removed: {$ne: true},

View File

@@ -1,26 +1,19 @@
<template lang="html">
<v-card
class="ma-2"
class="ma-2 log-entry"
>
<v-card-title
v-if="model.name"
class="pa-2"
>
<h3>
{{ model.name }}
</h3>
</v-card-title>
<v-card-text
v-if="model.text || (model.content && model.content.length)"
class="pa-2"
>
{{ model.text }}
<div
v-for="(content, index) in model.content"
:key="index"
class="content-line"
>
{{ content.name }}
<h4>
{{ content.name }}
</h4>
<span
v-if="content.error"
class="error"
@@ -35,7 +28,7 @@
>{{ content.details }}</span>
<markdown-text
v-if="content.description"
class="details"
class="description"
:markdown="content.description"
/>
</div>
@@ -69,3 +62,9 @@ export default {
display: inline-block;
}
</style>
<style lang="css">
.log-entry .description > p:last-of-type{
margin-bottom: 0;
}
</style>

View File

@@ -59,6 +59,8 @@ export default {
removed: {$ne: true},
equipped: {$ne: true},
deactivatedByAncestor: {$ne: true},
}, {
sort: {order: 1},
});
},
}

View File

@@ -1,6 +1,5 @@
<template lang="html">
<v-list
two-line
dense
class="item-list"
>

View File

@@ -10,7 +10,7 @@
<v-card-title class="title">
{{ model.name }}
</v-card-title>
<v-card-text>
<v-card-text v-if="model.summary">
<property-description
:string="model.summary"
:calculations="model.summaryCalculations"

View File

@@ -1,5 +1,21 @@
<template lang="html">
<div class="roll-form">
<div class="layout row wrap">
<text-field
label="Name"
:value="model.name"
:error-messages="errors.name"
@change="change('name', ...arguments)"
/>
<text-field
label="Variable name"
:value="model.variableName"
style="flex-basis: 300px;"
hint="Use this name in action formulae to refer to the result of this roll"
:error-messages="errors.variableName"
@change="change('variableName', ...arguments)"
/>
</div>
<text-field
ref="focusFirst"
label="Roll"

View File

@@ -8,6 +8,7 @@
@change="change('name', ...arguments)"
/>
<text-field
ref="focusFirst"
label="DC"
:value="model.dc"
:error-messages="errors.dc"
@@ -21,6 +22,15 @@
:error-messages="errors.stat"
@change="change('stat', ...arguments)"
/>
<smart-select
label="Target"
:hint="targetOptionHint"
:items="targetOptions"
:value="model.target"
:error-messages="errors.target"
:menu-props="{auto: true, lazy: true}"
@change="change('target', ...arguments)"
/>
<smart-combobox
label="Tags"
class="mr-2"
@@ -40,5 +50,34 @@ import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormM
export default {
mixins: [saveListMixin, propertyFormMixin],
computed: {
targetOptions(){
return [
{
text: 'Self',
value: 'self',
}, {
text: 'Roll once for each target',
value: 'each',
}, {
text: 'Roll once and apply to every target',
value: 'every',
},
];
},
targetOptionHint(){
let hints = {
self: 'The damage will be applied to the character\'s own attribute when taking the action',
target: 'The damage will be applied to the target of the action',
each: 'The damage will be rolled separately for each of the targets of the action',
every: 'The damage will be rolled once and applied to each of the targets of the action',
};
if (this.parentTarget === 'singleTarget'){
hints.each = hints.target;
hints.every = hints.target;
}
return hints[this.model.target];
}
},
};
</script>

View File

@@ -1,6 +1,12 @@
<template lang="html">
<div class="note-viewer">
<property-name :value="model.name" />
<property-description
:string="model.summary"
:calculations="model.summaryCalculations"
:inactive="model.inactive"
/>
<v-divider class="mt-3 mb-3" />
<property-description
:string="model.description"
:calculations="model.descriptionCalculations"