Began adding creature templates to libraries
This commit is contained in:
@@ -117,7 +117,7 @@ function insertPropertyFromNode(nodeId, root, parentId) {
|
||||
return node;
|
||||
}
|
||||
|
||||
function storeLibraryNodeReferences(nodes) {
|
||||
export function storeLibraryNodeReferences(nodes) {
|
||||
nodes.forEach(node => {
|
||||
if (node.libraryNodeId) return;
|
||||
node.libraryNodeId = node._id;
|
||||
@@ -126,7 +126,7 @@ function storeLibraryNodeReferences(nodes) {
|
||||
|
||||
// Covert node references into actual nodes
|
||||
// TODO: check permissions for each library a reference node references
|
||||
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) {
|
||||
export function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) {
|
||||
depth += 1;
|
||||
// New nodes added this function
|
||||
let newNodes = [];
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||
import { PropTask } from '/imports/api/engine/action/tasks/Task';
|
||||
import recalculateInlineCalculations from '/imports/api/engine/action/functions/recalculateInlineCalculations';
|
||||
import getPropertyTitle from '/imports/api/utility/getPropertyTitle';
|
||||
|
||||
export default async function applyCreatureTemplateProperty(
|
||||
task: PropTask, action: EngineAction, result, userInput
|
||||
): Promise<void> {
|
||||
const prop = task.prop;
|
||||
//Log the Creature that is about to be summoned
|
||||
let logValue = prop.description?.value
|
||||
if (prop.description?.text) {
|
||||
recalculateInlineCalculations(prop.description, action, 'reduce', userInput);
|
||||
logValue = prop.description?.value;
|
||||
}
|
||||
// There are no targets for creature templates
|
||||
// Creatures are always summoned as children of the action's creature
|
||||
result.appendLog({
|
||||
name: getPropertyTitle(prop),
|
||||
value: logValue
|
||||
}, []);
|
||||
|
||||
result.appendLog({
|
||||
name: 'Warning',
|
||||
value: 'Creature summoning is not yet implemented...'
|
||||
}, []);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import adjustment from './applyAdjustmentProperty';
|
||||
import branch from './applyBranchProperty';
|
||||
import buff from './applyBuffProperty';
|
||||
import buffRemover from './applyBuffRemoverProperty';
|
||||
import creature from './applyCreatureTemplateProperty';
|
||||
import damage from './applyDamageProperty';
|
||||
import folder from './applyFolderProperty';
|
||||
import note from './applyNoteProperty';
|
||||
@@ -24,6 +25,7 @@ const applyPropertyByType: {
|
||||
branch,
|
||||
buff,
|
||||
buffRemover,
|
||||
creature,
|
||||
damage,
|
||||
folder,
|
||||
note,
|
||||
|
||||
40
app/imports/api/properties/CreatureTemplates.ts
Normal file
40
app/imports/api/properties/CreatureTemplates.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
|
||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema';
|
||||
|
||||
// Creature templates represent creatures that don't yet exist
|
||||
// Used to store creatures in the library, or as templates for another creature to summon
|
||||
const CreatureTemplateSchema = createPropertySchema({
|
||||
name: {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.name,
|
||||
optional: true,
|
||||
},
|
||||
description: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
picture: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.url,
|
||||
},
|
||||
avatarPicture: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.url,
|
||||
},
|
||||
});
|
||||
|
||||
const ComputedOnlyCreatureTemplateSchema = createPropertySchema({
|
||||
description: {
|
||||
type: 'computedOnlyInlineCalculationField',
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
const ComputedCreatureTemplateSchema = new SimpleSchema({})
|
||||
.extend(CreatureTemplateSchema)
|
||||
.extend(ComputedOnlyCreatureTemplateSchema);
|
||||
|
||||
export { CreatureTemplateSchema, ComputedCreatureTemplateSchema, ComputedOnlyCreatureTemplateSchema };
|
||||
@@ -2,15 +2,16 @@ import SimpleSchema from 'simpl-schema';
|
||||
import { ComputedOnlyActionSchema } from '/imports/api/properties/Actions';
|
||||
import { ComputedOnlyAdjustmentSchema } from '/imports/api/properties/Adjustments';
|
||||
import { ComputedOnlyAttributeSchema } from '/imports/api/properties/Attributes';
|
||||
import { ComputedOnlyBuffSchema } from '/imports/api/properties/Buffs';
|
||||
import { ComputedOnlyBuffRemoverSchema } from '/imports/api/properties/BuffRemovers';
|
||||
import { ComputedOnlyBranchSchema } from '/imports/api/properties/Branches';
|
||||
import { ComputedOnlyClassSchema } from '/imports/api/properties/Classes';
|
||||
import { ComputedOnlyBuffRemoverSchema } from '/imports/api/properties/BuffRemovers';
|
||||
import { ComputedOnlyBuffSchema } from '/imports/api/properties/Buffs';
|
||||
import { ComputedOnlyClassLevelSchema } from '/imports/api/properties/ClassLevels';
|
||||
import { ComputedOnlyClassSchema } from '/imports/api/properties/Classes';
|
||||
import { ComputedOnlyConstantSchema } from '/imports/api/properties/Constants';
|
||||
import { ComputedOnlyContainerSchema } from '/imports/api/properties/Containers';
|
||||
import { ComputedOnlyDamageSchema } from '/imports/api/properties/Damages';
|
||||
import { ComputedOnlyCreatureTemplateSchema } from '/imports/api/properties/CreatureTemplates';
|
||||
import { ComputedOnlyDamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers';
|
||||
import { ComputedOnlyDamageSchema } from '/imports/api/properties/Damages';
|
||||
import { ComputedOnlyEffectSchema } from '/imports/api/properties/Effects';
|
||||
import { ComputedOnlyFeatureSchema } from '/imports/api/properties/Features';
|
||||
import { ComputedOnlyFolderSchema } from '/imports/api/properties/Folders';
|
||||
@@ -23,8 +24,8 @@ import { ComputedOnlyRollSchema } from '/imports/api/properties/Rolls';
|
||||
import { ComputedOnlySavingThrowSchema } from '/imports/api/properties/SavingThrows';
|
||||
import { ComputedOnlySkillSchema } from '/imports/api/properties/Skills';
|
||||
import { ComputedOnlySlotSchema } from '/imports/api/properties/Slots';
|
||||
import { ComputedOnlySpellSchema } from '/imports/api/properties/Spells';
|
||||
import { ComputedOnlySpellListSchema } from '/imports/api/properties/SpellLists';
|
||||
import { ComputedOnlySpellSchema } from '/imports/api/properties/Spells';
|
||||
import { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles';
|
||||
import { ComputedOnlyTriggerSchema } from '/imports/api/properties/Triggers';
|
||||
|
||||
@@ -32,13 +33,14 @@ const propertySchemasIndex = {
|
||||
action: ComputedOnlyActionSchema,
|
||||
adjustment: ComputedOnlyAdjustmentSchema,
|
||||
attribute: ComputedOnlyAttributeSchema,
|
||||
branch: ComputedOnlyBranchSchema,
|
||||
buff: ComputedOnlyBuffSchema,
|
||||
buffRemover: ComputedOnlyBuffRemoverSchema,
|
||||
branch: ComputedOnlyBranchSchema,
|
||||
class: ComputedOnlyClassSchema,
|
||||
classLevel: ComputedOnlyClassLevelSchema,
|
||||
constant: ComputedOnlyConstantSchema,
|
||||
container: ComputedOnlyContainerSchema,
|
||||
creature: ComputedOnlyCreatureTemplateSchema,
|
||||
damage: ComputedOnlyDamageSchema,
|
||||
damageMultiplier: ComputedOnlyDamageMultiplierSchema,
|
||||
effect: ComputedOnlyEffectSchema,
|
||||
@@ -53,8 +55,8 @@ const propertySchemasIndex = {
|
||||
roll: ComputedOnlyRollSchema,
|
||||
savingThrow: ComputedOnlySavingThrowSchema,
|
||||
skill: ComputedOnlySkillSchema,
|
||||
spellList: ComputedOnlySpellListSchema,
|
||||
spell: ComputedOnlySpellSchema,
|
||||
spellList: ComputedOnlySpellListSchema,
|
||||
toggle: ComputedOnlyToggleSchema,
|
||||
trigger: ComputedOnlyTriggerSchema,
|
||||
any: new SimpleSchema({}),
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ComputedClassSchema } from '/imports/api/properties/Classes';
|
||||
import { ComputedClassLevelSchema } from '/imports/api/properties/ClassLevels';
|
||||
import { ConstantSchema } from '/imports/api/properties/Constants';
|
||||
import { ComputedContainerSchema } from '/imports/api/properties/Containers';
|
||||
import { ComputedCreatureTemplateSchema } from '/imports/api/properties/CreatureTemplates';
|
||||
import { ComputedDamageSchema } from '/imports/api/properties/Damages';
|
||||
import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers';
|
||||
import { ComputedEffectSchema } from '/imports/api/properties/Effects';
|
||||
@@ -33,17 +34,20 @@ const propertySchemasIndex = {
|
||||
action: ComputedActionSchema,
|
||||
adjustment: ComputedAdjustmentSchema,
|
||||
attribute: ComputedAttributeSchema,
|
||||
branch: ComputedBranchSchema,
|
||||
buff: ComputedBuffSchema,
|
||||
buffRemover: ComputedBuffRemoverSchema,
|
||||
branch: ComputedBranchSchema,
|
||||
class: ComputedClassSchema,
|
||||
classLevel: ComputedClassLevelSchema,
|
||||
constant: ConstantSchema,
|
||||
container: ComputedContainerSchema,
|
||||
creature: ComputedCreatureTemplateSchema,
|
||||
damage: ComputedDamageSchema,
|
||||
damageMultiplier: DamageMultiplierSchema,
|
||||
effect: ComputedEffectSchema,
|
||||
feature: ComputedFeatureSchema,
|
||||
folder: ComputedFolderSchema,
|
||||
item: ComputedItemSchema,
|
||||
note: ComputedNoteSchema,
|
||||
pointBuy: ComputedPointBuySchema,
|
||||
proficiency: ProficiencySchema,
|
||||
@@ -52,12 +56,10 @@ const propertySchemasIndex = {
|
||||
roll: ComputedRollSchema,
|
||||
savingThrow: ComputedSavingThrowSchema,
|
||||
skill: ComputedSkillSchema,
|
||||
spellList: ComputedSpellListSchema,
|
||||
spell: ComputedSpellSchema,
|
||||
spellList: ComputedSpellListSchema,
|
||||
toggle: ComputedToggleSchema,
|
||||
trigger: ComputedTriggerSchema,
|
||||
container: ComputedContainerSchema,
|
||||
item: ComputedItemSchema,
|
||||
any: new SimpleSchema({}),
|
||||
};
|
||||
|
||||
|
||||
@@ -2,17 +2,20 @@ import SimpleSchema from 'simpl-schema';
|
||||
import { ActionSchema } from '/imports/api/properties/Actions';
|
||||
import { AdjustmentSchema } from '/imports/api/properties/Adjustments';
|
||||
import { AttributeSchema } from '/imports/api/properties/Attributes';
|
||||
import { BuffSchema } from '/imports/api/properties/Buffs';
|
||||
import { BuffRemoverSchema } from '/imports/api/properties/BuffRemovers';
|
||||
import { BranchSchema } from '/imports/api/properties/Branches';
|
||||
import { ClassSchema } from '/imports/api/properties/Classes';
|
||||
import { BuffRemoverSchema } from '/imports/api/properties/BuffRemovers';
|
||||
import { BuffSchema } from '/imports/api/properties/Buffs';
|
||||
import { ClassLevelSchema } from '/imports/api/properties/ClassLevels';
|
||||
import { ClassSchema } from '/imports/api/properties/Classes';
|
||||
import { ConstantSchema } from '/imports/api/properties/Constants';
|
||||
import { DamageSchema } from '/imports/api/properties/Damages';
|
||||
import { ContainerSchema } from '/imports/api/properties/Containers';
|
||||
import { CreatureTemplateSchema } from '/imports/api/properties/CreatureTemplates';
|
||||
import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers';
|
||||
import { DamageSchema } from '/imports/api/properties/Damages';
|
||||
import { EffectSchema } from '/imports/api/properties/Effects';
|
||||
import { FeatureSchema } from '/imports/api/properties/Features';
|
||||
import { FolderSchema } from '/imports/api/properties/Folders';
|
||||
import { ItemSchema } from '/imports/api/properties/Items';
|
||||
import { NoteSchema } from '/imports/api/properties/Notes';
|
||||
import { PointBuySchema } from '/imports/api/properties/PointBuys';
|
||||
import { ProficiencySchema } from '/imports/api/properties/Proficiencies';
|
||||
@@ -25,24 +28,25 @@ import { SpellListSchema } from '/imports/api/properties/SpellLists';
|
||||
import { SpellSchema } from '/imports/api/properties/Spells';
|
||||
import { ToggleSchema } from '/imports/api/properties/Toggles';
|
||||
import { TriggerSchema } from '/imports/api/properties/Triggers';
|
||||
import { ContainerSchema } from '/imports/api/properties/Containers';
|
||||
import { ItemSchema } from '/imports/api/properties/Items';
|
||||
|
||||
const propertySchemasIndex = {
|
||||
action: ActionSchema,
|
||||
adjustment: AdjustmentSchema,
|
||||
attribute: AttributeSchema,
|
||||
branch: BranchSchema,
|
||||
buff: BuffSchema,
|
||||
buffRemover: BuffRemoverSchema,
|
||||
branch: BranchSchema,
|
||||
class: ClassSchema,
|
||||
classLevel: ClassLevelSchema,
|
||||
constant: ConstantSchema,
|
||||
container: ContainerSchema,
|
||||
creature: CreatureTemplateSchema,
|
||||
damage: DamageSchema,
|
||||
damageMultiplier: DamageMultiplierSchema,
|
||||
effect: EffectSchema,
|
||||
feature: FeatureSchema,
|
||||
folder: FolderSchema,
|
||||
item: ItemSchema,
|
||||
note: NoteSchema,
|
||||
pointBuy: PointBuySchema,
|
||||
proficiency: ProficiencySchema,
|
||||
@@ -51,12 +55,10 @@ const propertySchemasIndex = {
|
||||
roll: RollSchema,
|
||||
savingThrow: SavingThrowSchema,
|
||||
skill: SkillSchema,
|
||||
spellList: SpellListSchema,
|
||||
spell: SpellSchema,
|
||||
spellList: SpellListSchema,
|
||||
toggle: ToggleSchema,
|
||||
trigger: TriggerSchema,
|
||||
container: ContainerSchema,
|
||||
item: ItemSchema,
|
||||
any: new SimpleSchema({}),
|
||||
};
|
||||
|
||||
|
||||
@@ -111,5 +111,6 @@ import '/imports/api/tabletop/methods/insertTabletop';
|
||||
import '/imports/api/tabletop/methods/updateTabletop';
|
||||
import '/imports/api/tabletop/methods/addCreaturesToTabletop';
|
||||
import '/imports/api/tabletop/methods/updateTabletopSharing';
|
||||
import '/imports/api/tabletop/methods/addCreaturesFromLibraryToTabletop';
|
||||
|
||||
export default Tabletops;
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import { assertUserInTabletop } from './shared/tabletopPermissions';
|
||||
import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures';
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes';
|
||||
import { getFilter, renewDocIds } from '/imports/api/parenting/parentingFunctions';
|
||||
import { reifyNodeReferences, storeLibraryNodeReferences } from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||
|
||||
const addCreaturesFromLibraryToTabletop = new ValidatedMethod({
|
||||
|
||||
name: 'tabletops.addCreaturesFromLibraryToTabletop',
|
||||
|
||||
validate: new SimpleSchema({
|
||||
'libraryNodeIds': {
|
||||
type: Array,
|
||||
},
|
||||
'libraryNodeIds.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
tabletopId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
}).validator(),
|
||||
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 1,
|
||||
timeInterval: 5_000,
|
||||
},
|
||||
|
||||
run({ libraryNodeIds, tabletopId }) {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('tabletops.addCreatures.denied',
|
||||
'You need to be logged in to remove a tabletop');
|
||||
}
|
||||
assertUserHasPaidBenefits(this.userId);
|
||||
assertUserInTabletop(tabletopId, this.userId);
|
||||
|
||||
for (const nodeId of libraryNodeIds) {
|
||||
const creatureNode = LibraryNodes.findOne({
|
||||
_id: nodeId,
|
||||
type: 'creature',
|
||||
removed: { $ne: true },
|
||||
});
|
||||
|
||||
if (!creatureNode) {
|
||||
if (Meteor.isClient) return {};
|
||||
else {
|
||||
throw new Meteor.Error(
|
||||
'Insert property from library failed',
|
||||
`No library creature with id '${nodeId}' was found`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const creatureId = Creatures.insert({
|
||||
...creatureNode,
|
||||
type: 'monster',
|
||||
tabletopId,
|
||||
owner: this.userId,
|
||||
writers: [this.userId],
|
||||
dirty: true,
|
||||
});
|
||||
insertSubProperties(creatureNode, creatureId);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function insertSubProperties(node, creatureId: string) {
|
||||
let nodes = LibraryNodes.find({
|
||||
...getFilter.descendants(node),
|
||||
removed: { $ne: true },
|
||||
}).fetch();
|
||||
|
||||
for (const node of nodes) {
|
||||
node.root = {
|
||||
'_id': creatureId,
|
||||
collection: 'creatures',
|
||||
};
|
||||
}
|
||||
|
||||
// Convert all references into actual nodes
|
||||
nodes = reifyNodeReferences(nodes);
|
||||
|
||||
// set libraryNodeIds
|
||||
storeLibraryNodeReferences(nodes);
|
||||
|
||||
// Give the docs new IDs without breaking internal references
|
||||
renewDocIds({
|
||||
docArray: nodes,
|
||||
collectionMap: { 'libraryNodes': 'creatureProperties' }
|
||||
});
|
||||
|
||||
// Insert the creature properties
|
||||
// @ts-expect-error Batch insert not defined
|
||||
CreatureProperties.batchInsert(nodes);
|
||||
return node;
|
||||
}
|
||||
|
||||
export default addCreaturesFromLibraryToTabletop;
|
||||
@@ -21,6 +21,7 @@ import LibraryBrowserDialog from '/imports/client/ui/library/LibraryBrowserDialo
|
||||
|
||||
// Lazily load less common dialogs
|
||||
const ArchiveDialog = () => import('/imports/client/ui/creature/archive/ArchiveDialog.vue');
|
||||
const CreatureFromLibraryDialog = () => import('/imports/client/ui/tabletop/CreatureFromLibraryDialog.vue');
|
||||
const DeleteUserAccountDialog = () => import('/imports/client/ui/user/DeleteUserAccountDialog.vue');
|
||||
const DependencyGraphDialog = () => import('/imports/client/ui/creature/dependencyGraph/DependencyGraphDialog.vue');
|
||||
const InviteDialog = () => import('/imports/client/ui/user/InviteDialog.vue');
|
||||
@@ -43,6 +44,7 @@ export default {
|
||||
CharacterCreationDialog,
|
||||
CharacterSheetDialog,
|
||||
CreatureFormDialog,
|
||||
CreatureFromLibraryDialog,
|
||||
CreaturePropertyDialog,
|
||||
CreaturePropertyFromLibraryDialog,
|
||||
CreatureRootDialog,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<template lang="html">
|
||||
<div class="creature-template-form">
|
||||
<v-row dense>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<text-field
|
||||
label="Picture URL"
|
||||
hint="A link to a high resolution image"
|
||||
:value="model.picture"
|
||||
:error-messages="errors.picture"
|
||||
@change="change('picture', ...arguments)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<text-field
|
||||
label="Avatar picture URL"
|
||||
hint="A link to a smaller, square image to use as an avatar"
|
||||
:value="model.avatarPicture"
|
||||
:error-messages="errors.avatarPicture"
|
||||
@change="change('avatarPicture', ...arguments)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<inline-computation-field
|
||||
label="Description"
|
||||
hint="A brief description of the creature shown when the creature is added to a tabletop"
|
||||
:model="model.description"
|
||||
:error-messages="errors['description.text']"
|
||||
@change="({path, value, ack}) =>
|
||||
$emit('change', {path: ['description', ...path], value, ack})"
|
||||
/>
|
||||
|
||||
<form-sections
|
||||
v-if="$slots.default"
|
||||
type="creatureTemplate"
|
||||
>
|
||||
<slot />
|
||||
</form-sections>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import propertyFormMixin from '/imports/client/ui/properties/forms/shared/propertyFormMixin';
|
||||
|
||||
export default {
|
||||
mixins: [propertyFormMixin],
|
||||
}
|
||||
</script>
|
||||
@@ -8,6 +8,7 @@ import ClassForm from '/imports/client/ui/properties/forms/ClassForm.vue';
|
||||
import ClassLevelForm from '/imports/client/ui/properties/forms/ClassLevelForm.vue';
|
||||
import ConstantForm from '/imports/client/ui/properties/forms/ConstantForm.vue';
|
||||
import ContainerForm from '/imports/client/ui/properties/forms/ContainerForm.vue';
|
||||
import CreatureTemplateForm from '/imports/client/ui/properties/forms/CreatureTemplateForm.vue';
|
||||
import DamageForm from '/imports/client/ui/properties/forms/DamageForm.vue';
|
||||
import DamageMultiplierForm from '/imports/client/ui/properties/forms/DamageMultiplierForm.vue';
|
||||
import EffectForm from '/imports/client/ui/properties/forms/EffectForm.vue';
|
||||
@@ -38,6 +39,7 @@ export default {
|
||||
container: ContainerForm,
|
||||
class: ClassForm,
|
||||
classLevel: ClassLevelForm,
|
||||
creature: CreatureTemplateForm,
|
||||
damage: DamageForm,
|
||||
damageMultiplier: DamageMultiplierForm,
|
||||
effect: EffectForm,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<template lang="html">
|
||||
<div class="creature-viewer">
|
||||
<v-row dense>
|
||||
<property-description
|
||||
name="Description"
|
||||
:model="model.description"
|
||||
/>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import propertyViewerMixin from '/imports/client/ui/properties/viewers/shared/propertyViewerMixin'
|
||||
export default {
|
||||
mixins: [propertyViewerMixin],
|
||||
}
|
||||
</script>
|
||||
@@ -8,6 +8,7 @@ import ContainerViewer from '/imports/client/ui/properties/viewers/ContainerView
|
||||
import ClassViewer from '/imports/client/ui/properties/viewers/ClassViewer.vue';
|
||||
import ClassLevelViewer from '/imports/client/ui/properties/viewers/ClassLevelViewer.vue';
|
||||
import ConstantViewer from '/imports/client/ui/properties/viewers/ConstantViewer.vue';
|
||||
import CreatureTemplateViewer from '/imports/client/ui/properties/viewers/CreatureTemplateViewer.vue';
|
||||
import DamageViewer from '/imports/client/ui/properties/viewers/DamageViewer.vue';
|
||||
import DamageMultiplierViewer from '/imports/client/ui/properties/viewers/DamageMultiplierViewer.vue';
|
||||
import EffectViewer from '/imports/client/ui/properties/viewers/EffectViewer.vue';
|
||||
@@ -38,6 +39,7 @@ export default {
|
||||
class: ClassViewer,
|
||||
classLevel: ClassLevelViewer,
|
||||
constant: ConstantViewer,
|
||||
creature: CreatureTemplateViewer,
|
||||
damage: DamageViewer,
|
||||
damageMultiplier: DamageMultiplierViewer,
|
||||
effect: EffectViewer,
|
||||
|
||||
458
app/imports/client/ui/tabletop/CreatureFromLibraryDialog.vue
Normal file
458
app/imports/client/ui/tabletop/CreatureFromLibraryDialog.vue
Normal file
@@ -0,0 +1,458 @@
|
||||
<template lang="html">
|
||||
<dialog-base
|
||||
dark-body
|
||||
>
|
||||
<template slot="toolbar">
|
||||
<v-toolbar-title>
|
||||
Insert creatures
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-text-field
|
||||
v-model="searchInput"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
regular
|
||||
clearable
|
||||
hide-details
|
||||
class="flex-grow-0"
|
||||
style="flex-basis: 300px;"
|
||||
:loading="searchLoading"
|
||||
@change="searchValue = searchInput || undefined"
|
||||
@click:clear="searchValue = undefined"
|
||||
/>
|
||||
</template>
|
||||
<v-fade-transition>
|
||||
<div
|
||||
v-if="!$subReady.creatureTemplates"
|
||||
class="fill-height layout justify-center align-center"
|
||||
>
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="64"
|
||||
/>
|
||||
</div>
|
||||
<v-expansion-panels
|
||||
v-else
|
||||
accordion
|
||||
tile
|
||||
multiple
|
||||
hover
|
||||
>
|
||||
<v-expansion-panel
|
||||
v-for="libraryNode in [...selectedExcludedNodes, ...libraryNodes]"
|
||||
:key="libraryNode._id"
|
||||
:model="libraryNode"
|
||||
:data-id="libraryNode._id"
|
||||
:class="{disabled: isDisabled(libraryNode) || libraryNode._disabledBySlotFillerCondition}"
|
||||
>
|
||||
<v-expansion-panel-header>
|
||||
<template #default="{ open }">
|
||||
<v-layout
|
||||
align-center
|
||||
class="flex-grow-0 mr-2"
|
||||
>
|
||||
<v-checkbox
|
||||
v-if="libraryNode._disabledByAlreadyAdded"
|
||||
class="my-0 py-0"
|
||||
hide-details
|
||||
:input-value="true"
|
||||
disabled
|
||||
/>
|
||||
<v-checkbox
|
||||
v-else
|
||||
v-model="selectedNodeIds"
|
||||
class="my-0 py-0"
|
||||
hide-details
|
||||
:color="libraryNode._disabledBySlotFillerCondition ? 'error' : ''"
|
||||
:disabled="isDisabled(libraryNode)"
|
||||
:value="libraryNode._id"
|
||||
@click.stop
|
||||
/>
|
||||
</v-layout>
|
||||
<v-layout column>
|
||||
<v-layout align-center>
|
||||
<tree-node-view :model="libraryNode" />
|
||||
<div
|
||||
v-if="libraryNode._disabledBySlotFillerCondition"
|
||||
class="error--text text-no-wrap text-truncate"
|
||||
>
|
||||
{{ libraryNode._conditionError }}
|
||||
</div>
|
||||
</v-layout>
|
||||
<div class="text-caption text-no-wrap text-truncate">
|
||||
{{ libraryNames[libraryNode.ancestors[0].id ] }}
|
||||
</div>
|
||||
</v-layout>
|
||||
<div
|
||||
v-if="libraryNode.slotQuantityFilled !== undefined && libraryNode.slotQuantityFilled !== 1"
|
||||
class="text-overline flex-grow-0 text-no-wrap"
|
||||
:class="{
|
||||
'error--text': isDisabled(libraryNode) &&
|
||||
libraryNode._disabledByQuantityFilled
|
||||
}"
|
||||
>
|
||||
{{ libraryNode.slotQuantityFilled }} slots
|
||||
</div>
|
||||
<template v-if="open">
|
||||
<v-btn
|
||||
icon
|
||||
class="flex-grow-0"
|
||||
@click.stop="openPropertyDetails(libraryNode._id)"
|
||||
>
|
||||
<v-icon>mdi-window-restore</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</template>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<library-node-expansion-content :id="libraryNode._id" />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-fade-transition>
|
||||
<v-layout
|
||||
v-if="(!$subReady.creatureTemplates && !searchValue) || currentLimit < countAll"
|
||||
column
|
||||
align-center
|
||||
justify-center
|
||||
class="ma-3 mt-8"
|
||||
>
|
||||
<v-btn
|
||||
:loading="!$subReady.creatureTemplates"
|
||||
color="accent"
|
||||
outlined
|
||||
@click="loadMore"
|
||||
>
|
||||
Load More
|
||||
</v-btn>
|
||||
</v-layout>
|
||||
<v-layout
|
||||
align-center
|
||||
justify-center
|
||||
class="text-caption text--disabled mt-8 mb-2"
|
||||
>
|
||||
Can't find what you're looking for?
|
||||
</v-layout>
|
||||
<v-layout
|
||||
align-center
|
||||
justify-center
|
||||
wrap
|
||||
class="mx-4 mb-4"
|
||||
>
|
||||
<v-btn
|
||||
v-if="!dummySlot"
|
||||
text
|
||||
small
|
||||
data-id="library-browser-button"
|
||||
@click="openLibraryBrowser"
|
||||
>
|
||||
Browse community libraries
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="!dummySlot"
|
||||
text
|
||||
small
|
||||
data-id="custom-button"
|
||||
@click="insertCustomFiller"
|
||||
>
|
||||
Insert New Creature
|
||||
</v-btn>
|
||||
</v-layout>
|
||||
|
||||
<template slot="actions">
|
||||
<v-btn
|
||||
text
|
||||
@click="$store.dispatch('popDialogStack')"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
color="primary"
|
||||
:disabled="!dummySlot && !selectedNodeIds.length"
|
||||
@click="$store.dispatch('popDialogStack', selectedNodeIds)"
|
||||
>
|
||||
Insert
|
||||
</v-btn>
|
||||
</template>
|
||||
</dialog-base>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes';
|
||||
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
|
||||
import TreeNodeView from '/imports/client/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import Libraries from '/imports/api/library/Libraries';
|
||||
import LibraryNodeExpansionContent from '/imports/client/ui/library/LibraryNodeExpansionContent.vue';
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES';
|
||||
import { clone, difference } from 'lodash';
|
||||
import getDefaultSlotFiller from '/imports/api/library/methods/getDefaultSlotFiller';
|
||||
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode';
|
||||
import insertProperty from '/imports/api/creature/creatureProperties/methods/insertProperty';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
TreeNodeView,
|
||||
LibraryNodeExpansionContent,
|
||||
},
|
||||
props: {
|
||||
slotId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
creatureId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
dummySlot: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedNodeIds: [],
|
||||
searchInput: undefined,
|
||||
searchValue: undefined,
|
||||
autoSelectRan: false,
|
||||
}
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['creatureId'],
|
||||
},
|
||||
computed: {
|
||||
tagsSearched() {
|
||||
let or = [];
|
||||
let not = [];
|
||||
if (this.model.slotTags && this.model.slotTags.length) {
|
||||
or.push(this.model.slotTags);
|
||||
}
|
||||
this.model.extraTags?.forEach(extras => {
|
||||
if (extras.tags?.length) {
|
||||
if (extras.operation === 'OR') {
|
||||
or.push(extras.tags);
|
||||
} else if (extras.operation === 'NOT') {
|
||||
not.push(extras.tags);
|
||||
}
|
||||
}
|
||||
});
|
||||
return { or, not };
|
||||
},
|
||||
slotPropertyTypeName() {
|
||||
if (!this.model) return;
|
||||
if (!this.model.slotType) return 'Property';
|
||||
let propName = getPropertyName(this.model.slotType);
|
||||
return propName;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeCount(val) {
|
||||
// Still loading fillers
|
||||
if (!this._subs['creatureTemplates'].ready()) return;
|
||||
// Can load more, and not showing enough active choices, so load more
|
||||
if (
|
||||
this.currentLimit < this.countAll
|
||||
&& val < 25
|
||||
) {
|
||||
this.loadMore();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
loadMore() {
|
||||
if (this.currentLimit >= this.countAll) return;
|
||||
this._subs['creatureTemplates'].setData('limit', this.currentLimit + 50);
|
||||
},
|
||||
openPropertyDetails(id) {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'library-node-dialog',
|
||||
elementId: id,
|
||||
data: {
|
||||
_id: id,
|
||||
},
|
||||
});
|
||||
},
|
||||
openLibraryBrowser() {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'library-browser-dialog',
|
||||
elementId: 'library-browser-button',
|
||||
});
|
||||
},
|
||||
isDisabled(node) {
|
||||
return node._disabledByAlreadyAdded ||
|
||||
(
|
||||
node._disabledByQuantityFilled &&
|
||||
!this.selectedNodeIds.includes(node._id)
|
||||
)
|
||||
},
|
||||
insertCustomFiller() {
|
||||
if (!this.model) return;
|
||||
const prop = getDefaultSlotFiller(this.model);
|
||||
const parentRef = { id: this.slotId, collection: 'creatureProperties' };
|
||||
const order = this.model.order + 0.5;
|
||||
const $store = this.$store;
|
||||
$store.commit('pushDialogStack', {
|
||||
component: 'insert-property-dialog',
|
||||
elementId: 'custom-button',
|
||||
data: {
|
||||
parentDoc: this.model,
|
||||
creatureId: this.creatureId,
|
||||
prop,
|
||||
noBackdropClose: true,
|
||||
},
|
||||
callback(result) {
|
||||
if (!result) return;
|
||||
if (Array.isArray(result)){
|
||||
let nodeIds = result;
|
||||
insertPropertyFromLibraryNode.call({ nodeIds, parentRef, order });
|
||||
setTimeout(() => $store.dispatch('popDialogStack'), 200);
|
||||
} else if (typeof result === 'object') {
|
||||
let creatureProperty = result;
|
||||
creatureProperty.order = order;
|
||||
insertProperty.call({ creatureProperty, parentRef });
|
||||
setTimeout(() => $store.dispatch('popDialogStack'), 200);
|
||||
|
||||
/* Maybe replace the dialog with the edit version?
|
||||
* It's a bit jank, but a common use case
|
||||
$store.commit('replaceDialog', {
|
||||
component: 'creature-property-dialog',
|
||||
//elementId: `?`,
|
||||
data: {
|
||||
_id,
|
||||
startInEditTab: true,
|
||||
},
|
||||
});
|
||||
*/
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
'creatureTemplates'() {
|
||||
return [this.slotId || this.dummySlot?._id, this.searchValue || undefined, !!this.dummySlot]
|
||||
},
|
||||
'selectedFillers'() {
|
||||
return [this.slotId || this.dummySlot?._id, this.selectedNodeIds, !!this.dummySlot]
|
||||
},
|
||||
},
|
||||
searchLoading() {
|
||||
return !!this.searchValue && !this.$subReady.creatureTemplates;
|
||||
},
|
||||
model() {
|
||||
if (this.slotId) {
|
||||
return CreatureProperties.findOne(this.slotId);
|
||||
} else if (this.dummySlot) {
|
||||
let model = clone(this.dummySlot)
|
||||
if (!model.quantityExpected) model.quantityExpected = {};
|
||||
model.quantityExpected.value = +model.quantityExpected.calculation;
|
||||
model.spaceLeft = model.quantityExpected.value;
|
||||
return model;
|
||||
}
|
||||
},
|
||||
variables() {
|
||||
if (!this.creatureId) return {};
|
||||
return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {};
|
||||
},
|
||||
currentLimit() {
|
||||
return this._subs['creatureTemplates'].data('limit') || 50;
|
||||
},
|
||||
countAll() {
|
||||
return this._subs['creatureTemplates'].data('countAll');
|
||||
},
|
||||
activeCount() {
|
||||
if (!this.libraryNodes) return;
|
||||
return this.libraryNodes.length;
|
||||
},
|
||||
libraryNodeFilter() {
|
||||
const filterString = this._subs['creatureTemplates'].data('libraryNodeFilter');
|
||||
if (!filterString) return;
|
||||
return EJSON.parse(filterString);
|
||||
},
|
||||
alreadyAdded() {
|
||||
let added = new Set();
|
||||
if (!this.model.unique) return added;
|
||||
let ancestorId;
|
||||
if (this.model.unique === 'uniqueInSlot') {
|
||||
ancestorId = this.model._id;
|
||||
} else if (this.model.unique === 'uniqueInCreature') {
|
||||
ancestorId = this.creatureId;
|
||||
}
|
||||
CreatureProperties.find({
|
||||
'ancestors.id': ancestorId,
|
||||
libraryNodeId: { $exists: true },
|
||||
removed: { $ne: true },
|
||||
}, {
|
||||
fields: { libraryNodeId: 1 },
|
||||
}).forEach(prop => {
|
||||
added.add(prop.libraryNodeId);
|
||||
});
|
||||
return added;
|
||||
},
|
||||
totalQuantitySelected() {
|
||||
let quantitySelected = 0;
|
||||
LibraryNodes.find({
|
||||
_id: { $in: this.selectedNodeIds }
|
||||
}, {
|
||||
fields: { slotQuantityFilled: 1 },
|
||||
}).forEach(node => {
|
||||
if (Number.isFinite(node.slotQuantityFilled)) {
|
||||
quantitySelected += node.slotQuantityFilled;
|
||||
} else {
|
||||
quantitySelected += 1;
|
||||
}
|
||||
});
|
||||
return quantitySelected;
|
||||
},
|
||||
spaceLeft() {
|
||||
if (!this.model.quantityExpected || this.model.quantityExpected.value === 0) return undefined;
|
||||
return this.model.spaceLeft - this.totalQuantitySelected;
|
||||
},
|
||||
libraryNames() {
|
||||
let names = {};
|
||||
Libraries.find().forEach(lib => names[lib._id] = lib.name)
|
||||
return names;
|
||||
},
|
||||
libraryNodes() {
|
||||
if (!this.libraryNodeFilter) return [];
|
||||
if (!this.$subReady.creatureTemplates) return [];
|
||||
let nodes = LibraryNodes.find(this.libraryNodeFilter, {
|
||||
sort: { name: 1, order: 1 }
|
||||
}).fetch();
|
||||
|
||||
// Only run the auto-select once
|
||||
if (!this.autoSelectRan) {
|
||||
this.autoSelectRan = true;
|
||||
// If we have exactly one active node and no selected nodes, pre-select it
|
||||
if (
|
||||
nodes.length === 1
|
||||
&& !nodes[0]._disabled
|
||||
&& !this.selectedNodeIds?.length
|
||||
) {
|
||||
this.selectedNodeIds = [nodes[0]._id];
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
},
|
||||
selectedExcludedNodes() {
|
||||
const displayedIds = this.libraryNodes.map(node => node._id);
|
||||
const excludedNodeIds = difference(this.selectedNodeIds, displayedIds);
|
||||
return LibraryNodes.find({ _id: { $in: excludedNodeIds } });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
resolveimport { toString } from '/imports/parser/toString';
|
||||
@@ -60,7 +60,10 @@
|
||||
</v-icon>
|
||||
Add Character
|
||||
</v-btn>
|
||||
<v-btn disabled>
|
||||
<v-btn
|
||||
data-id="creatures-from-library"
|
||||
@click="addCreatureFromLibrary"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-plus
|
||||
</v-icon>
|
||||
@@ -104,6 +107,7 @@ import { assertEditPermission } from '/imports/api/creature/creatures/creaturePe
|
||||
import ActionCard from '/imports/client/ui/tabletop/TabletopActionCard.vue';
|
||||
import SelectedCreatureBar from '/imports/client/ui/tabletop/selectedCreatureBar/SelectedCreatureBar.vue';
|
||||
import TabletopCreatureListItem from '/imports/client/ui/tabletop/TabletopCreatureListItem.vue';
|
||||
import addCreaturesFromLibraryToTabletop from '/imports/api/tabletop/methods/addCreaturesFromLibraryToTabletop';
|
||||
|
||||
const getProperties = function (creatureId, selector = {}) {
|
||||
return CreatureProperties.find({
|
||||
@@ -206,6 +210,25 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
addCreatureFromLibrary(){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-from-library-dialog',
|
||||
elementId: 'creatures-from-library',
|
||||
data: {},
|
||||
callback: (libraryNodeIds) => {
|
||||
if (!libraryNodeIds) return;
|
||||
addCreaturesFromLibraryToTabletop.call({
|
||||
tabletopId: this.model._id,
|
||||
libraryNodeIds,
|
||||
}, error => {
|
||||
if (error) {
|
||||
console.error(error)
|
||||
snackbar({ text: error.reason || error.message || error.toString() });
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
openCharacterSheetDialog(){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'character-sheet-dialog',
|
||||
|
||||
@@ -270,7 +270,7 @@ export default {
|
||||
|
||||
// Get the folders that could hide a property
|
||||
const folderIds = CreatureProperties.find({
|
||||
'ancestors.id': this.creatureId,
|
||||
'root.id': this.creatureId,
|
||||
type: 'folder',
|
||||
groupStats: true,
|
||||
hideStatsGroup: true,
|
||||
@@ -280,7 +280,7 @@ export default {
|
||||
|
||||
// Get the properties that need to be shown as an icon
|
||||
const filter = {
|
||||
'ancestors.id': this.creatureId,
|
||||
'root.id': this.creatureId,
|
||||
'parentId': {
|
||||
$nin: folderIds,
|
||||
},
|
||||
|
||||
@@ -71,6 +71,14 @@ const PROPERTIES = Object.freeze({
|
||||
examples: 'Coin pouch, backpack',
|
||||
suggestedParents: ['folder'],
|
||||
},
|
||||
creature: {
|
||||
icon: 'mdi-account',
|
||||
name: 'Creature',
|
||||
docsPath: 'property/creature',
|
||||
helpText: 'A creature is a template for a creature that might become a real creature once added to a tabletop or summoned by a character',
|
||||
examples: 'Monsters, raised undead',
|
||||
suggestedParents: [],
|
||||
},
|
||||
damage: {
|
||||
icon: '$vuetify.icons.damage',
|
||||
name: 'Damage',
|
||||
|
||||
Reference in New Issue
Block a user