Actions in tabletops show logs correctly
This commit is contained in:
@@ -43,13 +43,10 @@ let CreatureLogSchema = new SimpleSchema({
|
||||
type: String,
|
||||
index: 1,
|
||||
},
|
||||
// creatures targeted by any of the logged events
|
||||
targetIds: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
'targetIds.$': {
|
||||
// The tabletops this log is associated with
|
||||
tabletopId: {
|
||||
type: String,
|
||||
optional: true,
|
||||
index: 1,
|
||||
},
|
||||
creatureName: {
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface EngineAction {
|
||||
_decisions?: any[],
|
||||
creatureId: string;
|
||||
rootPropId?: string;
|
||||
tabletopId: string;
|
||||
targetIds?: string[];
|
||||
results: TaskResult[];
|
||||
taskCount: number;
|
||||
@@ -20,11 +21,18 @@ const ActionSchema = new SimpleSchema({
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
},
|
||||
rootPropId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
tabletopId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
optional: true,
|
||||
index: 1,
|
||||
},
|
||||
targetIds: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
|
||||
@@ -26,7 +26,7 @@ export default async function writeActionResults(action: EngineAction) {
|
||||
const logPromise = CreatureLogs.insertAsync({
|
||||
content: logContents,
|
||||
creatureId: action.creatureId,
|
||||
targetIds: allTargetIds,
|
||||
tabletopId: action.tabletopId,
|
||||
});
|
||||
|
||||
// Write the bulk updates
|
||||
|
||||
@@ -9,8 +9,29 @@ export const insertAction = new ValidatedMethod({
|
||||
validate: new SimpleSchema({
|
||||
action: ActionSchema
|
||||
}).validator({ clean: true }),
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 1000,
|
||||
},
|
||||
run: function ({ action }: { action: EngineAction }) {
|
||||
assertEditPermission(getCreature(action.creatureId), this.userId);
|
||||
const creature = getCreature(action.creatureId);
|
||||
assertEditPermission(getCreature(creature), this.userId);
|
||||
// Make sure the action shares the creature's tabletopId
|
||||
// It is assumed that if a character you control is in a tabletop, you have the rights
|
||||
// to do actions in that tabletop
|
||||
action.tabletopId = creature.tabletopId;
|
||||
|
||||
// Ensure that all the targeted creatures exist and share a tabletop
|
||||
if (action.targetIds) for (const targetId of action.targetIds) {
|
||||
const target = getCreature(targetId);
|
||||
if (!target) {
|
||||
throw new Meteor.Error('not-found', 'Target creature does not exist');
|
||||
}
|
||||
if (target.tabletopId !== action.tabletopId) {
|
||||
throw new Meteor.Error('permission-denied', 'Target creature does not share a tabletop with the acting creature');
|
||||
}
|
||||
}
|
||||
|
||||
// First remove all other actions on this creature
|
||||
// only do one action at a time, don't wait for this to finish
|
||||
EngineActions.remove({ creatureId: action.creatureId });
|
||||
|
||||
@@ -41,7 +41,8 @@ import Creatures from '/imports/api/creature/creatures/Creatures';
|
||||
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions';
|
||||
import { parse, prettifyParseError } from '/imports/parser/parser';
|
||||
import resolve, { toString } from '/imports/parser/resolve';
|
||||
import resolve from '/imports/parser/resolve';
|
||||
import toString from '/imports/parser/toString';
|
||||
import LogEntry from '/imports/client/ui/log/LogEntry.vue';
|
||||
import { Tracker } from 'meteor/tracker'
|
||||
|
||||
@@ -110,14 +111,14 @@ export default {
|
||||
if (this.history.length > 50) this.history.shift();
|
||||
this.historyIndex = this.history.length;
|
||||
},
|
||||
recalculate() {
|
||||
async recalculate() {
|
||||
this.inputHint = this.inputError = undefined;
|
||||
if (!this.input) return;
|
||||
let result;
|
||||
try {
|
||||
result = parse(this.input);
|
||||
} catch (e){
|
||||
if (e.constructor.name === 'EndOfInputError'){
|
||||
if (e?.constructor?.name === 'EndOfInputError'){
|
||||
this.inputError = '...';
|
||||
} else {
|
||||
let error = prettifyParseError(e);
|
||||
@@ -126,7 +127,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let {result: compiled} = resolve('compile', result, this.variables);
|
||||
let {result: compiled} = await resolve('compile', result, this.variables);
|
||||
this.inputHint = toString(compiled);
|
||||
return;
|
||||
} catch (e){
|
||||
@@ -146,7 +147,6 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
// @ts-ignore
|
||||
meteor: {
|
||||
logs() {
|
||||
const filter = {};
|
||||
|
||||
188
app/imports/client/ui/log/TabletopLog.vue
Normal file
188
app/imports/client/ui/log/TabletopLog.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<template lang="html">
|
||||
<div
|
||||
style="height: 100%; overflow: hidden;"
|
||||
class="character-log layout column justify-end"
|
||||
>
|
||||
<v-slide-y-reverse-transition
|
||||
group
|
||||
hide-on-leave
|
||||
class="card-raised-background flex layout column reverse align-end py-3 px-1"
|
||||
style="overflow: auto;"
|
||||
>
|
||||
<tabletop-log-entry
|
||||
v-for="log in logs"
|
||||
:key="log._id"
|
||||
:model="log"
|
||||
/>
|
||||
</v-slide-y-reverse-transition>
|
||||
<v-card>
|
||||
<v-text-field
|
||||
v-model="input"
|
||||
class="mx-2 mb-2"
|
||||
persistent-hint
|
||||
style="flex-grow: 0"
|
||||
append-outer-icon="mdi-send"
|
||||
:hint="inputHint"
|
||||
:error-messages="inputError"
|
||||
:disabled="!editPermission"
|
||||
:loading="submitLoading"
|
||||
@click:append-outer="submit"
|
||||
@keyup.enter="submit"
|
||||
@keyup.up="decrementHistory"
|
||||
@keyup.down="incrementHistory"
|
||||
/>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CreatureLogs, { logRoll } from '/imports/api/creature/log/CreatureLogs';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures';
|
||||
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions';
|
||||
import { parse, prettifyParseError } from '/imports/parser/parser';
|
||||
import resolve from '/imports/parser/resolve';
|
||||
import toString from '/imports/parser/toString';
|
||||
import TabletopLogEntry from '/imports/client/ui/log/TabletopLogEntry.vue';
|
||||
import { Tracker } from 'meteor/tracker'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TabletopLogEntry,
|
||||
},
|
||||
props: {
|
||||
tabletopId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data(){return {
|
||||
inputHint: undefined,
|
||||
inputError: undefined,
|
||||
input: undefined,
|
||||
history: [],
|
||||
historyIndex: 1,
|
||||
submitLoading: false,
|
||||
creatureId: undefined,
|
||||
}},
|
||||
watch: {
|
||||
input(value){
|
||||
this.input = value;
|
||||
this.recalculate();
|
||||
},
|
||||
creatureId() {
|
||||
Tracker.afterFlush(() => this.recalculate())
|
||||
},
|
||||
historyIndex(i) {
|
||||
if (typeof this.history[i] === 'string') {
|
||||
this.input = this.history[i];
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$root.$on('active-tabletop-character-change', (id) => {
|
||||
this.creatureId = id;
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
if (!this.input) return;
|
||||
if (this.submitLoading) return;
|
||||
const log = {
|
||||
roll: this.input,
|
||||
};
|
||||
if (this.tabletopId) log.tabletopId = this.tabletopId;
|
||||
if (this.creatureId) log.creatureId = this.creatureId;
|
||||
this.submitLoading = true;
|
||||
logRoll.call(log, (error) => {
|
||||
this.submitLoading = false;
|
||||
if (!error) {
|
||||
this.addHistory(this.input);
|
||||
this.input = '';
|
||||
this.inputError = undefined;
|
||||
return;
|
||||
}
|
||||
this.inputError = error.message || error.toString();
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
addHistory(string) {
|
||||
// Don't add duplicates back to back in history
|
||||
if (string === this.history[this.history.length - 1]) return;
|
||||
this.history.push(string);
|
||||
if (this.history.length > 50) this.history.shift();
|
||||
this.historyIndex = this.history.length;
|
||||
},
|
||||
async recalculate() {
|
||||
this.inputHint = this.inputError = undefined;
|
||||
if (!this.input) return;
|
||||
let result;
|
||||
try {
|
||||
result = parse(this.input);
|
||||
} catch (e){
|
||||
if (e?.constructor?.name === 'EndOfInputError'){
|
||||
this.inputError = '...';
|
||||
} else {
|
||||
let error = prettifyParseError(e);
|
||||
this.inputError = error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let {result: compiled} = await resolve('compile', result, this.variables);
|
||||
this.inputHint = toString(compiled);
|
||||
return;
|
||||
} catch (e){
|
||||
console.warn(e);
|
||||
this.inputError = 'Compilation error';
|
||||
return;
|
||||
}
|
||||
},
|
||||
incrementHistory() {
|
||||
if (this.historyIndex < this.history.length) {
|
||||
this.historyIndex += 1;
|
||||
}
|
||||
},
|
||||
decrementHistory() {
|
||||
if (this.historyIndex > 0) {
|
||||
this.historyIndex -= 1;
|
||||
}
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
logs() {
|
||||
const filter = {};
|
||||
if (this.tabletopId) {
|
||||
filter.tabletopId = this.tabletopId;
|
||||
} else if (this.creatureId) {
|
||||
filter.creatureId = this.creatureId;
|
||||
}
|
||||
return CreatureLogs.find(filter, {
|
||||
sort: {date: -1},
|
||||
limit: 100
|
||||
});
|
||||
},
|
||||
creature(){
|
||||
return Creatures.findOne(this.creatureId) || {};
|
||||
},
|
||||
variables(){
|
||||
return CreatureVariables.findOne({_creatureId: this.creatureId}) || {};
|
||||
},
|
||||
editPermission(){
|
||||
try {
|
||||
assertEditPermission(this.creature, Meteor.userId());
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
.log-tab p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
resolveimport { toString } from '/imports/parser/toString';
|
||||
63
app/imports/client/ui/log/TabletopLogContent.vue
Normal file
63
app/imports/client/ui/log/TabletopLogContent.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template lang="html">
|
||||
<div class="log-content">
|
||||
<div
|
||||
v-for="(content, index) in model"
|
||||
:key="index"
|
||||
class="content-line"
|
||||
>
|
||||
<h4
|
||||
class="content-name"
|
||||
style="min-height: 12px;"
|
||||
>
|
||||
{{ content.name }}
|
||||
</h4>
|
||||
<markdown-text
|
||||
v-if="content.value"
|
||||
class="content-value"
|
||||
:markdown="content.value"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
style="min-height: 12px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import MarkdownText from '/imports/client/ui/components/MarkdownText.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MarkdownText,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.content-line {
|
||||
min-height: 24px;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
/** change the first content line to have no margin top*/
|
||||
.content-line:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.content-line .details {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="css">
|
||||
.log-content .content-value > p:last-of-type{
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
59
app/imports/client/ui/log/TabletopLogEntry.vue
Normal file
59
app/imports/client/ui/log/TabletopLogEntry.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template lang="html">
|
||||
<v-card
|
||||
class="ma-2 log-entry"
|
||||
>
|
||||
<v-list-item
|
||||
v-if="model.creatureId"
|
||||
style="max-width: 220px;"
|
||||
dense
|
||||
>
|
||||
<v-list-item-avatar
|
||||
:color="model.color || 'grey'"
|
||||
size="32"
|
||||
>
|
||||
<img
|
||||
v-if="creature.avatarPicture"
|
||||
:src="creature.avatarPicture"
|
||||
:alt="creature.name"
|
||||
>
|
||||
<span v-else>
|
||||
{{ creature.name && creature.name[0] || '?' }}
|
||||
</span>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ creature.name }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-card-text
|
||||
v-if="model.text || (model.content && model.content.length)"
|
||||
class="px-2 pt-0 pb-2"
|
||||
>
|
||||
<tabletop-log-content :model="model.content" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import TabletopLogContent from '/imports/client/ui/log/TabletopLogContent.vue';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TabletopLogContent,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
showName: Boolean,
|
||||
},
|
||||
meteor: {
|
||||
creature() {
|
||||
return Creatures.findOne(this.model.creatureId);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,41 +0,0 @@
|
||||
<template lang="html">
|
||||
<character-log
|
||||
:tabletop-id="tabletopId"
|
||||
:creature-id="activeCreatureId"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import { insertTabletopLog } from '/imports/api/creature/log/CreatureLogs';
|
||||
import CharacterLog from '/imports/client/ui/log/CharacterLog.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CharacterLog,
|
||||
},
|
||||
inject: {
|
||||
context: {
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
tabletopId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeCreatureId: undefined,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$root.$on('active-tabletop-character-change', (id) => {
|
||||
this.activeCreatureId = id;
|
||||
});
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
@@ -10,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import TabletopLog from '/imports/client/ui/tabletop/TabletopLog.vue';
|
||||
import TabletopLog from '/imports/client/ui/log/TabletopLog.vue';
|
||||
export default {
|
||||
components: {
|
||||
TabletopLog,
|
||||
|
||||
@@ -4,6 +4,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
|
||||
import CreatureLogs from '/imports/api/creature/log/CreatureLogs';
|
||||
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
|
||||
import { loadCreature } from '/imports/api/engine/loadCreatures';
|
||||
import EngineActions from '/imports/api/engine/action/EngineActions';
|
||||
|
||||
Meteor.publish('tabletops', function () {
|
||||
var userId = this.userId;
|
||||
@@ -65,7 +66,7 @@ Meteor.publish('tabletop', function (tabletopId) {
|
||||
// Warning, this leaks data to users of the same tabletop who may not have
|
||||
// read permission of this specific creature, so publish as few fields as
|
||||
// possible
|
||||
let creatureSummaries = Creatures.find({
|
||||
let creatureSummariesCursor = Creatures.find({
|
||||
tabletopId,
|
||||
}, {
|
||||
fields: {
|
||||
@@ -78,29 +79,42 @@ Meteor.publish('tabletop', function (tabletopId) {
|
||||
settings: 1,
|
||||
propCount: 1,
|
||||
},
|
||||
limit: 110,
|
||||
limit: 110, // Party vs 100 creatures was a fun encounter to run, so let's support that
|
||||
});
|
||||
const creatureIds = creatureSummaries.map(c => c._id);
|
||||
const creatureIds = creatureSummariesCursor.map(c => c._id);
|
||||
|
||||
// Load all the creatures into memory
|
||||
creatureIds.forEach(creatureId => {
|
||||
loadCreature(creatureId, self);
|
||||
});
|
||||
const variables = CreatureVariables.find({
|
||||
const variablesCursor = CreatureVariables.find({
|
||||
_creatureId: { $in: creatureIds }
|
||||
}, {
|
||||
limit: 110,
|
||||
});
|
||||
let properties = CreatureProperties.find({
|
||||
'ancestors.id': { $in: creatureIds },
|
||||
const propertiesCursor = CreatureProperties.find({
|
||||
'root.id': { $in: creatureIds },
|
||||
removed: { $ne: true },
|
||||
}, {
|
||||
limit: 10_000,
|
||||
});
|
||||
const logs = CreatureLogs.find({
|
||||
const logsCursor = CreatureLogs.find({
|
||||
tabletopId,
|
||||
}, {
|
||||
limit: 100,
|
||||
sort: { date: -1 },
|
||||
});
|
||||
return [tabletopCursor, creatureSummaries, properties, logs, variables]
|
||||
})
|
||||
const actionsCursor = EngineActions.find({
|
||||
tabletopId,
|
||||
});
|
||||
|
||||
return [
|
||||
tabletopCursor,
|
||||
creatureSummariesCursor,
|
||||
propertiesCursor,
|
||||
logsCursor,
|
||||
variablesCursor,
|
||||
actionsCursor
|
||||
];
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user