Condensed logs to a single card per action

This commit is contained in:
Stefan Zermatten
2021-02-11 15:48:23 +02:00
parent d7083cf242
commit 439eadf079
16 changed files with 281 additions and 135 deletions

View File

@@ -1,11 +1,9 @@
import spendResources from '/imports/api/creature/actions/spendResources.js' import spendResources from '/imports/api/creature/actions/spendResources.js'
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
export default function applyAction({prop, creature}){ export default function applyAction({prop, log}){
spendResources(prop); spendResources(prop);
insertCreatureLog.call({ // If this is not the top level action, we can add its name to the log
log: { if (log.content.length){
text: prop.name, log.content.push({name: prop.name});
creatureId: creature._id}, }
});
} }

View File

@@ -1,12 +1,12 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js'; import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
import damagePropertiesByName from '/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js'; import damagePropertiesByName from '/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js';
export default function applyAdjustment({ export default function applyAdjustment({
prop, prop,
creature, creature,
targets, targets,
actionContext actionContext,
log
}){ }){
let damageTargets = prop.target === 'self' ? [creature] : targets; let damageTargets = prop.target === 'self' ? [creature] : targets;
let scope = { let scope = {
@@ -16,16 +16,10 @@ export default function applyAdjustment({
try { try {
var {result, errors} = evaluateString(prop.amount, scope, 'reduce'); var {result, errors} = evaluateString(prop.amount, scope, 'reduce');
if (typeof result !== 'number') { if (typeof result !== 'number') {
return insertCreatureLog.call({ log: { log.content.push({error: errors.join(', ') || 'Something went wrong'});
text: errors.join(', ') || 'Something went wrong',
creatureId: creature._id,
}});
} }
} catch (e){ } catch (e){
return insertCreatureLog.call({ log: { log.content.push({error: e.toString()});
text: e.toString(),
creatureId: creature._id,
}});
} }
if (damageTargets) { if (damageTargets) {
damageTargets.forEach(target => { damageTargets.forEach(target => {
@@ -38,19 +32,15 @@ export default function applyAdjustment({
operation: prop.operation || 'increment', operation: prop.operation || 'increment',
value: result value: result
}); });
insertCreatureLog.call({ log.content.push({
log: { resultPrefix: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''}`,
text: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''} ${-result}`, result: `${-result}`,
creatureId: target._id,
}
}); });
}); });
} else { } else {
insertCreatureLog.call({ log.content.push({
log: { resultPrefix: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''}`,
text: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''} ${-result}`, result: `${-result}`,
creatureId: creature._id,
}
}); });
} }
} }

View File

@@ -1,18 +1,14 @@
import roll from '/imports/parser/roll.js'; import roll from '/imports/parser/roll.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
export default function applyAttack({ export default function applyAttack({
prop, prop,
//children, log,
creature,
//targets,
//actionContext
}){ }){
let result = roll(1, 20)[0] + prop.rollBonusResult; let result = roll(1, 20)[0] + prop.rollBonusResult;
insertCreatureLog.call({ log.content.push({
log: { // If this is not the first item in the log content, give it a name
text: `${prop.name} attack. ${result} to hit`, name: log.content.length ? prop.name + ' attack' : undefined,
creatureId: creature._id, result,
} details: 'to hit',
}); });
} }

View File

@@ -1,12 +1,13 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js'; import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
import dealDamage from '/imports/api/creature/creatureProperties/methods/dealDamage.js'; import dealDamage from '/imports/api/creature/creatureProperties/methods/dealDamage.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
export default function applyDamage({ export default function applyDamage({
prop, prop,
creature, creature,
targets, targets,
actionContext actionContext,
log,
}){ }){
let damageTargets = prop.target === 'self' ? [creature] : targets; let damageTargets = prop.target === 'self' ? [creature] : targets;
let scope = { let scope = {
@@ -16,18 +17,16 @@ export default function applyDamage({
try { try {
var {result, errors} = evaluateString(prop.amount, scope, 'reduce'); var {result, errors} = evaluateString(prop.amount, scope, 'reduce');
if (typeof result !== 'number') { if (typeof result !== 'number') {
return insertCreatureLog.call({ log: { log.content.push({
text: errors.join(', '), error: errors.join(', '),
creatureId: creature._id, });
}});
} }
} catch (e){ } catch (e){
return insertCreatureLog.call({ log: { log.content.push({
text: e.toString(), error: e.toString(),
creatureId: creature._id, });
}});
} }
if (damageTargets) { if (damageTargets && damageTargets.length) {
damageTargets.forEach(target => { damageTargets.forEach(target => {
if (prop.target === 'each'){ if (prop.target === 'each'){
result = evaluateString(prop.amount, scope, 'reduce'); result = evaluateString(prop.amount, scope, 'reduce');
@@ -44,20 +43,17 @@ export default function applyDamage({
} }
}); });
if (target._id !== creature._id){ if (target._id !== creature._id){
insertCreatureLog.call({ log.content.push({
log: { result: damageDealt,
text: `Dealt ${damageDealt} ${prop.damageType}${prop.damageType !== 'healing'? ' damage': ''}`, details: `${prop.damageType}${prop.damageType !== 'healing'? ' damage': ''}` +
creatureId: creature._id, `${target.name && ' to '}${target.name}`,
}
}); });
} }
}); });
} else { } else {
insertCreatureLog.call({ log.content.push({
log: { result,
text: `${result} ${prop.damageType}${prop.damageType !== 'healing'? ' damage': ''}`, details: `${prop.damageType}${prop.damageType !== 'healing'? ' damage': ''}`,
creatureId: creature._id,
}
}); });
} }
} }

View File

@@ -6,46 +6,49 @@ import applyBuff from '/imports/api/creature/actions/applyBuff.js';
function applyProperty(options){ function applyProperty(options){
let prop = options.prop; let prop = options.prop;
if ( if (prop.type === 'buff'){
prop.disabled === true || // ignore disabled props // ignore only applied buffs
prop.equipped === false || // ignore unequipped items if (prop.applied === true){
prop.toggleResult === false || // ignore untoggled toggles return false;
prop.applied === true // ignore buffs that are already applied }
){ // Ignore inactive props of other types
} else if (prop.inactive === true){
return false; return false;
} }
switch (prop.type){ switch (prop.type){
case 'action': case 'action':
case 'spell': case 'spell':
applyAction(options); applyAction(options);
return true; break;
case 'attack': case 'attack':
applyAction(options); applyAction(options);
applyAttack(options); applyAttack(options);
return true; break;
case 'damage': case 'damage':
applyDamage(options); applyDamage(options);
return true; break;
case 'adjustment': case 'adjustment':
applyAdjustment(options); applyAdjustment(options);
return true; break;
case 'buff': case 'buff':
applyBuff(options); applyBuff(options);
return false; break;
case 'roll': case 'roll':
// applyRoll(options); // applyRoll(options);
return true; break;
case 'savingThrow': case 'savingThrow':
// applySavingThrow(options); // applySavingThrow(options);
return false; break;
} }
return true;
} }
export default function applyProperties({ export default function applyProperties({
forest, forest,
creature, creature,
targets, targets,
actionContext actionContext,
log,
}){ }){
forest.forEach(child => { forest.forEach(child => {
let walkChildren = applyProperty({ let walkChildren = applyProperty({
@@ -53,14 +56,16 @@ export default function applyProperties({
children: child.children, children: child.children,
creature, creature,
targets, targets,
actionContext actionContext,
log,
}); });
if (walkChildren){ if (walkChildren){
applyProperties({ applyProperties({
forest: child.children, forest: child.children,
creature, creature,
targets, targets,
actionContext actionContext,
log,
}); });
} }
}); });

View File

@@ -66,6 +66,7 @@ const castSpellWithSlot = new ValidatedMethod({
context: {slotLevel}, context: {slotLevel},
creature, creature,
target, target,
method: this,
}); });
// Note this only recomputes the top-level creature, not the nearest one // Note this only recomputes the top-level creature, not the nearest one
recomputeCreatureByDoc(creature); recomputeCreatureByDoc(creature);

View File

@@ -3,6 +3,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/Creatures.js'; import Creatures from '/imports/api/creature/Creatures.js';
import CreatureLogs, { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
@@ -13,10 +14,15 @@ const doAction = new ValidatedMethod({
name: 'creatureProperties.doAction', name: 'creatureProperties.doAction',
validate: new SimpleSchema({ validate: new SimpleSchema({
actionId: SimpleSchema.RegEx.Id, actionId: SimpleSchema.RegEx.Id,
targetId: { targetIds: {
type: Array,
defaultValue: [],
maxCount: 10,
optional: true,
},
'targetIds.$': {
type: String, type: String,
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
optional: true,
}, },
}).validator(), }).validator(),
mixins: [RateLimiterMixin], mixins: [RateLimiterMixin],
@@ -24,26 +30,41 @@ const doAction = new ValidatedMethod({
numRequests: 10, numRequests: 10,
timeInterval: 5000, timeInterval: 5000,
}, },
run({actionId, targetId}) { run({actionId, targetIds = []}) {
let action = CreatureProperties.findOne(actionId); let action = CreatureProperties.findOne(actionId);
// Check permissions // Check permissions
let creature = getRootCreatureAncestor(action); let creature = getRootCreatureAncestor(action);
assertEditPermission(creature, this.userId); assertEditPermission(creature, this.userId);
let target = undefined; let targets = [];
if (targetId) { targetIds.forEach(targetId => {
target = Creatures.findOne(targetId); let target = Creatures.findOne(targetId);
assertEditPermission(target, this.userId); assertEditPermission(target, this.userId);
} targets.push(target);
doActionWork({action, creature, target}); });
// Note this only recomputes the top-level creature, not the nearest one doActionWork({action, creature, targets, method: this});
// recompute creatures
recomputeCreatureByDoc(creature); recomputeCreatureByDoc(creature);
if (target){ targets.forEach(target => {
recomputeCreatureByDoc(target); recomputeCreatureByDoc(target);
} });
}, },
}); });
export function doActionWork({action, creature, target, context = {}}){ export function doActionWork({
action,
creature,
targets,
context = {},
method
}){
// Create the log
let log = CreatureLogSchema.clean({
name: action.name,
creatureId: creature._id,
creatureName: creature.name,
});
let decendantForest = nodesToTree({ let decendantForest = nodesToTree({
collection: CreatureProperties, collection: CreatureProperties,
ancestorId: action._id, ancestorId: action._id,
@@ -56,8 +77,10 @@ export function doActionWork({action, creature, target, context = {}}){
forest: startingForest, forest: startingForest,
actionContext: context, actionContext: context,
creature, creature,
target, targets,
log,
}); });
insertCreatureLogWork({log, creature, method});
} }
export default doAction; export default doAction;

View File

@@ -23,7 +23,6 @@ export default function evaluateString(string, scope, fn = 'compile'){
errors.push('...'); errors.push('...');
return {result: string, errors}; return {result: string, errors};
} }
console.log(node);
let context = new CompilationContext(); let context = new CompilationContext();
let result = node[fn](scope, context); let result = node[fn](scope, context);
if (result instanceof ConstantNode){ if (result instanceof ConstantNode){

View File

@@ -1,5 +1,6 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import Creatures from '/imports/api/creature/Creatures.js'; import Creatures from '/imports/api/creature/Creatures.js';
import LogContentSchema from '/imports/api/creature/log/LogContentSchema.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import {assertEditPermission} from '/imports/api/creature/creaturePermissions.js'; import {assertEditPermission} from '/imports/api/creature/creaturePermissions.js';
@@ -13,13 +14,16 @@ if (Meteor.isServer){
let CreatureLogs = new Mongo.Collection('creatureLogs'); let CreatureLogs = new Mongo.Collection('creatureLogs');
let CreatureLogSchema = new SimpleSchema({ let CreatureLogSchema = new SimpleSchema({
text: { name: {
type: String, type: String,
optional: true,
}, },
type: { content: {
type: String, type: Array,
allowedValues: ['roll', 'change', 'damage', 'info'], defaultValue: [],
defaultValue: 'info', },
'content.$': {
type: LogContentSchema,
}, },
// The real-world date that it occured, usually sorted by date // The real-world date that it occured, usually sorted by date
date: { date: {
@@ -37,6 +41,10 @@ let CreatureLogSchema = new SimpleSchema({
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
index: 1, index: 1,
}, },
creatureName: {
type: String,
optional: true,
},
}); });
CreatureLogs.attachSchema(CreatureLogSchema); CreatureLogs.attachSchema(CreatureLogSchema);
@@ -73,7 +81,7 @@ const insertCreatureLog = new ValidatedMethod({
timeInterval: 5000, timeInterval: 5000,
}, },
validate: new SimpleSchema({ validate: new SimpleSchema({
log: CreatureLogSchema.omit('type', 'date'), log: CreatureLogSchema.omit('date'),
}).validator(), }).validator(),
run({log}){ run({log}){
const creatureId = log.creatureId; const creatureId = log.creatureId;
@@ -87,21 +95,27 @@ const insertCreatureLog = new ValidatedMethod({
}}); }});
assertEditPermission(creature, this.userId); assertEditPermission(creature, this.userId);
// Build the new log // Build the new log
if (typeof log === 'string'){ let id = insertCreatureLogWork({log, creature, method: this})
log = {text: log};
}
log.date = new Date();
// Insert it
let id = CreatureLogs.insert(log);
if (Meteor.isServer){
this.unblock();
removeOldLogs(creatureId);
logWebhook({log, creature});
}
return id; return id;
}, },
}); });
export function insertCreatureLogWork({log, creature, method}){
// Build the new log
if (typeof log === 'string'){
log = {text: log};
}
log.date = new Date();
// Insert it
let id = CreatureLogs.insert(log);
if (Meteor.isServer){
method.unblock();
removeOldLogs(creature._id);
logWebhook({log, creature});
}
return id;
}
function equalIgnoringWhitespace(a, b){ function equalIgnoringWhitespace(a, b){
if (typeof a !== 'string' || typeof b !== 'string') return a === b; if (typeof a !== 'string' || typeof b !== 'string') return a === b;
@@ -136,38 +150,42 @@ const logRoll = new ValidatedMethod({
}}); }});
assertEditPermission(creature, this.userId); assertEditPermission(creature, this.userId);
let parsedResult = parse(roll); let parsedResult = parse(roll);
let logText; let logContent;
if (parsedResult === null) { if (parsedResult === null) {
logText = 'Unexpected end of input'; logContent = [{error: 'Unexpected end of input'}];
} }
else try { else try {
logText = []; logContent = [];
let rollContext = new CompilationContext(); let rollContext = new CompilationContext();
let compiled = parsedResult.compile(creature.variables, rollContext); let compiled = parsedResult.compile(creature.variables, rollContext);
let compiledString = compiled.toString(); let compiledString = compiled.toString();
if (!equalIgnoringWhitespace(compiledString, roll)) logText.push(roll); if (!equalIgnoringWhitespace(compiledString, roll)) logContent.push({
logText.push(compiledString); result: roll
});
logContent.push({
details: compiledString
});
let rolled = compiled.roll(creature.variables, rollContext); let rolled = compiled.roll(creature.variables, rollContext);
let rolledString = rolled.toString(); let rolledString = rolled.toString();
if (rolledString !== compiledString) logText.push(rolled.toString()); if (rolledString !== compiledString) logContent.push({
details: rolled.toString()
});
let result = rolled.reduce(creature.variables, rollContext); let result = rolled.reduce(creature.variables, rollContext);
let resultString = result.toString(); let resultString = result.toString();
if (resultString !== rolledString) logText.push(resultString); if (resultString !== rolledString) logContent.push({
logText = logText.join('\n\n'); result: resultString
});
} catch (e){ } catch (e){
logText = 'Calculation error'; logContent = [{error: 'Calculation error'}];
} }
const log = { const log = {
text: logText, content: logContent,
creatureId, creatureId,
date: new Date(), date: new Date(),
}; };
let id = CreatureLogs.insert(log);
if (Meteor.isServer){ let id = insertCreatureLogWork({log, creature, method: this});
this.unblock();
removeOldLogs(creatureId);
logWebhook({log, creature});
}
return id; return id;
}, },
}); });

View File

@@ -0,0 +1,54 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import RollDetailsSchema from '/imports/api/properties/subSchemas/RollDetailsSchema.js';
let LogContentSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
error: {
type: String,
optional: true,
},
resultPrefix: {
type: String,
optional: true,
},
result: {
type: String,
optional: true,
},
expandedResult: {
type: String,
optional: true,
},
details: {
type: String,
optional: true,
},
context: {
type: Object,
optional: true,
},
'context.errors':{
type: Array,
defaultValue: [],
},
'context.errors.$': {
type: ErrorSchema,
},
'context.rolls': {
type: Array,
defaultValue: [],
},
'context.rolls.$': {
type: RollDetailsSchema,
},
'context.doubleRolls': {
type: Boolean,
optional: true,
},
});
export default LogContentSchema;

View File

@@ -0,0 +1,19 @@
import SimpleSchema from 'simpl-schema';
const RollDetailsSchema = new SimpleSchema({
number: {
type: Number,
},
diceSize: {
type: Number,
},
values: {
type: Array,
defaultValue: [],
},
'values.$': {
type: Number,
},
});
export default RollDetailsSchema;

View File

@@ -30,14 +30,12 @@ export default class RollNode extends ParseNode {
return new ErrorNode({ return new ErrorNode({
node: this, node: this,
error: 'Number of dice is not an integer', error: 'Number of dice is not an integer',
previousNodes: [this, left, right],
}); });
} }
if (!right.isInteger){ if (!right.isInteger){
return new ErrorNode({ return new ErrorNode({
node: this, node: this,
error: 'Dice size is not an integer', error: 'Dice size is not an integer',
previousNodes: [this, left, right],
}); });
} }
let number = left.value; let number = left.value;

View File

@@ -10,7 +10,7 @@
</template> </template>
<script> <script>
import LogTab from '/imports/ui/creature/character/CharacterLog.vue'; import LogTab from '/imports/ui/log/CharacterLog.vue';
export default { export default {
components: { components: {
LogTab, LogTab,

View File

@@ -7,15 +7,11 @@
class="log flex layout column reverse align-end pa-3" class="log flex layout column reverse align-end pa-3"
style="overflow: auto;" style="overflow: auto;"
> >
<v-card <log-entry
v-for="log in logs" v-for="log in logs"
:key="log._id" :key="log._id"
class="ma-2" :model="log"
> />
<v-card-text class="pa-2">
<markdown-text :markdown="log.text" />
</v-card-text>
</v-card>
</div> </div>
<v-card> <v-card>
<v-text-field <v-text-field
@@ -39,11 +35,11 @@ import CreatureLogs, { logRoll } from '/imports/api/creature/log/CreatureLogs.js
import Creatures from '/imports/api/creature/Creatures.js'; import Creatures from '/imports/api/creature/Creatures.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { parse } from '/imports/parser/parser.js'; import { parse } from '/imports/parser/parser.js';
import MarkdownText from '/imports/ui/components/MarkdownText.vue'; import LogEntry from '/imports/ui/log/LogEntry.vue';
export default { export default {
components: { components: {
MarkdownText, LogEntry,
}, },
props: { props: {
creatureId: { creatureId: {

View File

@@ -0,0 +1,54 @@
<template lang="html">
<v-card
class="ma-2"
>
<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 }}
<span
v-if="content.error"
class="error"
>{{ content.error }}</span>
{{ content.resultPrefix }}
<span
v-if="content.result"
class="subheading font-weight-bold mx-1"
>{{ content.result }}</span>
{{ content.details }}
</div>
</v-card-text>
</v-card>
</template>
<script>
export default {
props: {
model: {
type: Object,
required: true,
},
},
}
</script>
<style lang="css" scoped>
.content-line {
min-height: 24px;
}
</style>

View File

@@ -39,8 +39,7 @@ export default {
computed: { computed: {
// We can't rely on autoValue running in every form, so recalculate errors // We can't rely on autoValue running in every form, so recalculate errors
clientErrors(){ clientErrors(){
let validationContext = ConstantSchema.newContext(); let cleanModel = ConstantSchema.clean(this.model);
let cleanModel = validationContext.clean(this.model);
return cleanModel.errors; return cleanModel.errors;
} }
} }