Actions in tabletops show logs correctly

This commit is contained in:
ThaumRystra
2024-05-31 20:01:42 +02:00
parent 028c1b7463
commit 6e8c970287
11 changed files with 373 additions and 64 deletions

View File

@@ -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: {

View File

@@ -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: [],

View File

@@ -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

View File

@@ -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 });

View File

@@ -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 = {};

View 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';

View 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>

View 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>

View File

@@ -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>

View File

@@ -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,

View File

@@ -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
];
});
});