Printing implemented, needs print button on sheet

This commit is contained in:
Stefan Zermatten
2022-11-07 16:18:35 +02:00
parent c436309ba8
commit 008ef62517
16 changed files with 1618 additions and 542 deletions

View File

@@ -31,14 +31,45 @@
v-else
light
>
<printed-stats :creature-id="creatureId" />
<printed-features :creature-id="creatureId" />
<printed-inventory :creature-id="creatureId" />
<printed-spells
v-if="!creature.settings.hideSpellsTab"
:creature-id="creatureId"
/>
<printed-character :creature-id="creatureId" />
<div class="page pa-3">
<div class="px-3 d-flex align-center">
<div class="logo-background" />
<div class="creature-name mr-3">
{{ creature.name }}
</div>
<div class="text-right flex mr-4">
<div v-if="creature.alignment || background">
{{ creature.alignment }} {{ background }}
</div>
<dir v-if="race || creature.gender">
{{ race }} {{ creature.gender }}
</dir>
<div v-if="level && classes && classes.length === 1">
Level {{ level }} {{ classes[0].name }}
</div>
<div v-else-if="level">
Level {{ level }} ({{ classes.map(c => `${c.name} ${c.level}`).join(', ') }})
</div>
</div>
<qrcode-vue
style="height: 100px"
render-as="svg"
:value="creatureUrl"
/>
</div>
<div
class="text-right mt-3 mr-4"
style="font-size: 8pt; margin-bottom: -4px;"
>
{{ creatureUrl }}
</div>
<printed-stats :creature-id="creatureId" />
<printed-inventory :creature-id="creatureId" />
<printed-spells
v-if="!creature.settings.hideSpellsTab"
:creature-id="creatureId"
/>
</div>
</v-theme-provider>
</v-fade-transition>
</div>
@@ -46,25 +77,59 @@
<script lang="js">
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import PrintedStats from '/imports/ui/creature/character/printedCharacterSheet/PrintedStats.vue';
import PrintedFeatures from '/imports/ui/creature/character/printedCharacterSheet/PrintedFeatures.vue';
import PrintedInventory from '/imports/ui/creature/character/printedCharacterSheet/PrintedInventory.vue';
import PrintedSpells from '/imports/ui/creature/character/printedCharacterSheet/PrintedSpells.vue';
import PrintedCharacter from '/imports/ui/creature/character/printedCharacterSheet/PrintedCharacter.vue';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
import QrcodeVue from 'qrcode.vue'
export default {
components: {
PrintedStats,
PrintedFeatures,
PrintedInventory,
PrintedSpells,
PrintedCharacter,
QrcodeVue,
},
computed: {
creatureId() {
return this.$route.params.id
}
},
creatureUrl() {
let props = this.$router.resolve({
name: 'characterSheet',
params: { id: this.creatureId},
});
return new URL(props?.href, document.baseURI).href
},
level() {
return this.variables?.level?.value;
},
highestLevels(){
let highestLevels = {};
let highestLevelsList = [];
this.classLevels.forEach(classLevel => {
let name = classLevel.variableName;
if (
!highestLevels[name] ||
highestLevels[name].level < classLevel.level
){
highestLevels[name] = classLevel;
}
});
for (let name in highestLevels){
highestLevelsList.push(highestLevels[name]);
}
highestLevelsList.sort((a, b) => a.level - b.level);
return highestLevelsList;
},
classes() {
return [
...this.highestLevels,
...this.classProperties
].sort((a, b) => a.order - b.order);
},
},
reactiveProvide: {
name: 'context',
@@ -104,6 +169,55 @@ export default {
creature() {
return Creatures.findOne(this.creatureId);
},
variables() {
return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {};
},
race() {
if (this.variables?.race?.value?.valueType === 'string') return this.variables.race.value.value;
const prop = CreatureProperties.findOne({
'ancestors.id': this.creatureId,
tags: 'race',
removed: { $ne: true },
inactive: { $ne: true },
overridden: { $ne: true },
});
if (prop?.name) return prop.name;
return '';
},
background() {
if (this.variables?.background?.value?.valueType === 'string') return this.variables.background.value.value;
const prop = CreatureProperties.findOne({
'ancestors.id': this.creatureId,
tags: 'background',
removed: { $ne: true },
inactive: { $ne: true },
overridden: { $ne: true },
});
if (prop?.name) return prop.name;
return '';
},
classProperties(){
return CreatureProperties.find({
'ancestors.id': this.creatureId,
type: 'class',
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1}
}).fetch();
},
classLevels() {
const classVariableNames = this.classProperties.map(c => c.variableName)
return CreatureProperties.find({
'ancestors.id': this.creatureId,
type: 'classLevel',
variableName: {$nin: classVariableNames},
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1}
});
},
editPermission() {
try {
assertEditPermission(this.creature, Meteor.userId());
@@ -120,5 +234,104 @@ export default {
.character-sheet-printed {
background: white;
color: black;
font-size: 11pt;
}
.page {
padding: 4px;
}
.character-sheet-printed .inactive {
opacity: 1 !important;
}
.character-sheet-printed .creature-name {
font-size: 24pt;
background-color: white;
}
.character-sheet-printed .logo-background {
width: 60px;
height: 60px;
margin-right: 8px;
background-image: url(/crown-dice-logo-cropped-transparent.png);
background-size: contain;
background-position: 0 center;
}
.character-sheet-printed .v-divider {
border-color: rgba(0,0,0,0.3);
max-width: unset;
}
.character-sheet-printed .double-border {
position: relative;
padding: 11px 10px;
page-break-inside: avoid;
}
.character-sheet-printed .double-border::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border-image-source: url(/images/print/doubleLineImageBorder.png);
border-image-slice: 110 126 fill;
border-image-width: 16px;
border-image-repeat: stretch;
box-sizing: content-box;
z-index: -1;
}
.character-sheet-printed .octagon-border {
position: relative;
padding: 4px 20px;
page-break-inside: avoid;
}
.character-sheet-printed .octagon-border::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border-image: url(/images/print/octagonBorder.png) 124 118 fill;
border-image-width: 22px;
z-index: -1;
}
.character-sheet-printed .stats .label {
font-size: 10pt;
font-variant: small-caps;
}
.character-sheet-printed .label {
font-size: 14pt;
font-variant: all-small-caps;
font-weight: 600;
}
.character-sheet-printed .span-all {
column-span: all;
}
@media screen {
.character-sheet-printed {
display: flex;
flex-direction: column;
align-items: center;
}
.character-sheet-printed .page {
width: 210mm;
}
}
@media print {
header {
display: none !important;
}
nav {
display: none !important;
}
.v-main {
padding: 0 !important;
}
}
</style>

View File

@@ -1,58 +0,0 @@
<template>
<div class="build-tab">
<column-layout wide-columns>
<div>
<creature-summary :creature="creature" />
</div>
<div
v-for="note in notes"
:key="note._id"
>
<note-card
:model="note"
/>
</div>
</column-layout>
</div>
</template>
<script lang="js">
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import NoteCard from '/imports/ui/properties/components/persona/NoteCard.vue';
import CreatureSummary from '/imports/ui/creature/character/CreatureSummary.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export default {
components: {
ColumnLayout,
CreatureSummary,
NoteCard,
},
props: {
creatureId: {
type: String,
required: true,
},
},
meteor: {
notes(){
return CreatureProperties.find({
'ancestors.id': this.creatureId,
type: 'note',
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1},
});
},
creature(){
return Creatures.findOne(this.creatureId);
},
},
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,60 +0,0 @@
<template lang="html">
<div class="features">
<column-layout wide-columns>
<div
v-for="feature in features"
:key="feature._id"
>
<feature-card
:model="feature"
:data-id="feature._id"
@click="featureClicked(feature)"
/>
</div>
</column-layout>
</div>
</template>
<script lang="js">
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import FeatureCard from '/imports/ui/properties/components/features/FeatureCard.vue';
export default {
components: {
ColumnLayout,
FeatureCard,
},
props: {
creatureId: {
type: String,
required: true,
},
},
meteor: {
features() {
return CreatureProperties.find({
'ancestors.id': this.creatureId,
type: 'feature',
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: { order: 1 }
});
},
},
methods: {
featureClicked({ _id }) {
this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog',
elementId: `${_id}`,
data: { _id },
});
},
},
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,90 +1,84 @@
<template lang="html">
<div class="inventory">
<div
class="inventory"
style="page-break-before: always;"
>
<column-layout wide-columns>
<div>
<v-card>
<v-list>
<v-list-item>
<v-list-item-avatar>
<v-icon>$vuetify.icons.injustice</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>
Weight Carried
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-list-item-title>
{{ weightCarried }} lb
</v-list-item-title>
</v-list-item-action>
</v-list-item>
<v-list-item>
<v-list-item-avatar>
<v-icon>$vuetify.icons.cash</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>
Net worth
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-list-item-title>
<coin-value :value="variables && variables.valueTotal && variables.valueTotal.value|| 0" />
</v-list-item-title>
</v-list-item-action>
</v-list-item>
<v-list-item v-if="variables && variables.itemsAttuned && variables.itemsAttuned.value">
<v-list-item-avatar>
<v-icon>$vuetify.icons.spell</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>
Items attuned
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-list-item-title>
{{ variables.itemsAttuned.value }}
</v-list-item-title>
</v-list-item-action>
</v-list-item>
</v-list>
</v-card>
</div>
<div>
<toolbar-card transparent-toolbar>
<v-toolbar-title slot="toolbar">
Equipped
</v-toolbar-title>
<v-card-text class="px-0">
<item-list
equipment
:items="equippedItems"
:parent-ref="equipmentParentRef"
<div class="span-all">
<div class="double-border">
<div class="label text-center">
Inventory
</div>
<div class="d-flex inventory-stat">
<v-icon>$vuetify.icons.injustice</v-icon>
Weight Carried:
{{ weightCarried }} lb
</div>
<div class="d-flex inventory-stat">
<v-icon>$vuetify.icons.cash</v-icon>
Net worth:
<coin-value
class="ml-2"
:value="variables && variables.valueTotal && variables.valueTotal.value|| 0"
/>
</v-card-text>
</toolbar-card>
</div>
<div class="d-flex inventory-stat">
<v-icon>$vuetify.icons.spell</v-icon>
Items attuned:
{{ variables.itemsAttuned && variables.itemsAttuned.value }}
</div>
</div>
</div>
<div>
<toolbar-card transparent-toolbar>
<v-toolbar-title slot="toolbar">
Carried
</v-toolbar-title>
<v-card-text class="px-0">
<item-list
:items="carriedItems"
:parent-ref="carriedParentRef"
/>
</v-card-text>
</toolbar-card>
<div class="span-all">
<div class="octagon-border label text-center">
Equipped
</div>
</div>
<div
v-for="container in containersWithoutAncestorContainers"
:key="container._id"
v-for="item in equippedItems"
:key="item._id"
>
<container-card :model="container" />
<printed-item
class="double-border"
:model="item"
/>
</div>
<div class="span-all">
<div class="octagon-border label text-center">
Carried
</div>
</div>
<div
v-for="item in carriedItems"
:key="item._id"
>
<printed-item
class="double-border"
:model="item"
/>
</div>
<template
v-for="container in containersWithoutAncestorContainers"
>
<div
:key="container._id"
class="span-all container-header"
>
<printed-container
class="octagon-border"
:model="container"
/>
</div>
<div
v-for="item in container.items"
:key="item._id"
>
<printed-item
class="double-border"
:model="item"
/>
</div>
</template>
</column-layout>
</div>
</template>
@@ -93,22 +87,20 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import ContainerCard from '/imports/ui/properties/components/inventory/ContainerCard.vue';
import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
import ItemList from '/imports/ui/properties/components/inventory/ItemList.vue';
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js';
import CoinValue from '/imports/ui/components/CoinValue.vue';
import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js';
import CreatureVariables from '../../../../api/creature/creatures/CreatureVariables';
import PrintedItem from '/imports/ui/creature/character/printedCharacterSheet/components/PrintedItem.vue';
import PrintedContainer from '/imports/ui/creature/character/printedCharacterSheet/components/PrintedContainer.vue';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
export default {
components: {
ColumnLayout,
ContainerCard,
ToolbarCard,
ItemList,
CoinValue,
PrintedItem,
PrintedContainer,
},
props: {
creatureId: {
@@ -154,6 +146,17 @@ export default {
inactive: { $ne: true },
}, {
sort: { order: 1 },
}).map(c => {
c.items = CreatureProperties.find({
'parent.id': c._id,
type: { $in: ['item', 'container'] },
removed: { $ne: true },
equipped: { $ne: true },
deactivatedByAncestor: { $ne: true },
}, {
sort: { order: 1 },
}).fetch();
return c;
});
},
carriedItems() {
@@ -229,5 +232,39 @@ export default {
</script>
<style lang="css" scoped>
.octagon-border {
position: relative;
padding: 4px 20px;
page-break-inside: avoid;
}
.octagon-border::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border-image: url(/images/print/octagonBorder.png) 124 118 fill;
border-image-width: 22px;
z-index: -1;
}
.label {
font-size: 14pt;
font-variant: small-caps;
flex-grow: 1;
}
.inventory-stat {
font-size: 12pt;
line-height: 32px;
}
.inventory-stat > .v-icon {
margin-right: 8px;
}
.container-header {
page-break-after: avoid;
page-break-inside: avoid;
}
</style>

View File

@@ -1,23 +1,38 @@
<template lang="html">
<div class="spells">
<div
class="spells"
style="page-break-before: always;"
>
<column-layout wide-columns>
<div v-if="spellsWithoutList.length">
<v-card>
<spell-list
:spells="spellsWithoutList"
:parent-ref="{id: creatureId, collection: 'creatures'}"
/>
</v-card>
<div class="span-all">
<div class="label text-center octagon-border">
Spells
</div>
</div>
<div
v-for="spellList in spellListsWithoutAncestorSpellLists"
:key="spellList._id"
v-for="spell in spellsWithoutList"
:key="spell._id"
>
<spellList-card
:model="spellList"
:organize="organize"
/>
<printed-spell :model="spell" />
</div>
<template
v-for="spellList in spellListsWithoutAncestorSpellLists"
>
<div
:key="spellList._id"
class="span-all"
>
<printed-spell-list
:model="spellList"
/>
</div>
<div
v-for="spell in spellList.spells"
:key="spell._id"
>
<printed-spell :model="spell" />
</div>
</template>
</column-layout>
</div>
</template>
@@ -25,14 +40,14 @@
<script lang="js">
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import SpellListCard from '/imports/ui/properties/components/spells/SpellListCard.vue';
import SpellList from '/imports/ui/properties/components/spells/SpellList.vue';
import PrintedSpell from '/imports/ui/creature/character/printedCharacterSheet/components/PrintedSpell.vue';
import PrintedSpellList from '/imports/ui/creature/character/printedCharacterSheet/components/PrintedSpellList.vue';
export default {
components: {
ColumnLayout,
SpellList,
SpellListCard,
PrintedSpell,
PrintedSpellList,
},
props: {
creatureId: {
@@ -84,21 +99,25 @@ export default {
inactive: { $ne: true },
}, {
sort: { order: 1 }
}).map(sl => {
sl.spells = CreatureProperties.find({
'ancestors.id': sl._id,
type: 'spell',
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: {
level: 1,
order: 1,
}
}).fetch();
return sl;
});
},
},
computed: {
spellListIds() {
return this.spellLists.map(spellList => spellList._id);
},
},
methods: {
clickProperty(_id) {
this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog',
elementId: `spell-list-tile-${_id}`,
data: { _id },
});
return this.spellLists?.map(spellList => spellList._id);
},
},
}

View File

@@ -1,7 +1,5 @@
<template lang="html">
<div class="stats-tab ma-2">
<health-bar-card-container :creature-id="creatureId" />
<div class="stats">
<column-layout>
<div
v-if="abilities.length"
@@ -56,7 +54,7 @@
:class="stat.variableName == 'armor' && 'shield-number-label'"
>
<div
:class="stat.variableName == 'armor' ? 'shield-border' : 'octogon-border'"
:class="stat.variableName == 'armor' ? 'shield-border' : 'octagon-border'"
class="number big-number"
>
{{ stat.value }}
@@ -71,7 +69,7 @@
:key="modifier._id"
class="number-label"
>
<div class="number octogon-border big-number">
<div class="number octagon-border big-number">
{{ numberToSignedString(modifier.value) }}
</div>
<div class="label double-border">
@@ -84,14 +82,37 @@
:key="check._id"
class="number-label"
>
<div class="number octogon-border big-number">
<div class="number octagon-border big-number">
{{ numberToSignedString(check.value) }}
</div>
<div class="label double-border">
{{ check.name }}
</div>
</div>
<div
v-for="healthBar in healthBars"
:key="healthBar._id"
class="m-2"
>
<div class="double-border">
<div class="label">
Total: {{ healthBar.total }}
</div>
<div style="height: 60px;" />
<div
style="text-align: center;"
class="label"
>
{{ healthBar.name }}
</div>
</div>
</div>
<div>
<printed-damage-multipliers
class="double-border"
:multipliers="multipliers"
/>
</div>
<div
v-if="hitDice.length"
class="hit-dice m-2"
@@ -109,7 +130,7 @@
{{ hitDie.total }}{{ hitDie.hitDiceSize }}
</span>
</div>
<div style="height: 80px;" />
<div style="height: 60px;" />
<div
style="text-align: center;"
class="label"
@@ -122,182 +143,238 @@
<div
v-for="resource in resources"
:key="resource._id"
class="resource"
>
<div>{{ resource.total }}</div>
<div>{{ resource.name }}</div>
<div
class="double-border"
:class="resource.total <= 8 && 'mb-2'"
>
<div
v-if="resource.total <= 8"
class="label"
>
{{ resource.name }}
</div>
<div
v-if="resource.total > 8"
>
total: {{ resource.total }}
<div style="height: 60px;" />
</div>
<div
v-if="resource.total <= 8"
class="d-flex justify-end"
>
<div
v-for="i in resource.total"
:key="i"
class="resource-bubble"
/>
</div>
<div
v-if="resource.total > 8"
class="label text-center"
>
{{ resource.name }}
</div>
</div>
</div>
<damage-multiplier-card
v-if="multipliers && multipliers.length"
:multipliers="multipliers"
/>
<div
v-if="spellSlots && spellSlots.length"
>
<div>Spell Slots</div>
<div
v-for="spellSlot in spellSlots"
:key="spellSlot._id"
>
<div>
{{ spellSlot.name }}
<div class="double-border">
<div class="label text-center">
Spell Slots
</div>
<div
v-if="spellSlot.total > 6"
v-for="spellSlot in spellSlots"
:key="spellSlot._id"
class="mb-7"
:class="spellSlot.total <= 8 && 'mb-7'"
>
{{ spellSlot.total }}
</div>
<div
v-else
>
<v-icon
v-for="i in spellSlot.total"
:key="i"
<div class="label">
{{ spellSlot.name }}
</div>
<div
v-if="spellSlot.total > 8"
>
mdi-radiobox-blank
</v-icon>
Total: {{ spellSlot.total }}
</div>
<div
v-else
class="d-flex"
>
<div
v-for="i in spellSlot.total"
:key="i"
class="resource-bubble"
/>
</div>
</div>
</div>
</div>
<div
v-if="savingThrows.length"
class="saving-throws"
>
<div>
<v-list>
<v-subheader>Saving Throws</v-subheader>
<skill-list-tile
v-for="save in savingThrows"
:key="save._id"
:model="save"
:data-id="save._id"
/>
</v-list>
<div
class="double-border"
>
<printed-skill
v-for="save in savingThrows"
:key="save._id"
:model="save"
:data-id="save._id"
/>
<div class="label text-center">
Saving Throws
</div>
</div>
</div>
<div
v-if="skills.length"
class="skills"
>
<div>
<v-list>
<v-subheader>Skills</v-subheader>
<skill-list-tile
v-for="skill in skills"
:key="skill._id"
:model="skill"
:data-id="skill._id"
/>
</v-list>
<div
class="double-border"
>
<printed-skill
v-for="skill in skills"
:key="skill._id"
:model="skill"
:data-id="skill._id"
/>
<div class="label text-center">
Skills
</div>
</div>
</div>
<div
v-if="weapons && weapons.length"
>
<div
class="double-border"
>
<printed-skill
v-for="weapon in weapons"
:key="weapon._id"
hide-modifier
:model="weapon"
:data-id="weapon._id"
/>
<div class="label text-center">
Weapons
</div>
</div>
</div>
<div
v-if="armors && armors.length"
>
<div
class="double-border"
>
<printed-skill
v-for="armor in armors"
:key="armor._id"
hide-modifier
:model="armor"
:data-id="armor._id"
/>
<div class="label text-center">
Armor
</div>
</div>
</div>
<div
v-if="tools && tools.length"
>
<div
class="double-border"
>
<printed-skill
v-for="tool in tools"
:key="tool._id"
hide-modifier
:model="tool"
:data-id="tool._id"
/>
<div class="label text-center">
Tools
</div>
</div>
</div>
<div
v-if="languages && languages.length"
>
<div
class="double-border"
>
<printed-skill
v-for="language in languages"
:key="language._id"
hide-modifier
:model="language"
:data-id="language._id"
/>
<div class="label text-center">
Languages
</div>
</div>
</div>
<div
v-for="note in notes"
:key="note._id"
>
<div class="double-border">
<div class="label text-center">
{{ note.name }}
</div>
<property-description
text
:model="note.summary"
/>
</div>
</div>
<div
v-for="action in actions"
:key="action._id"
class="action"
>
<action-card
:model="action"
:data-id="action._id"
/>
<div class="double-border">
<printed-action
:model="action"
/>
</div>
</div>
<div
v-if="weapons && weapons.length"
class="weapon-proficiencies"
v-for="feature in features"
:key="feature._id"
>
<v-card>
<v-list>
<v-subheader>
Weapons
</v-subheader>
<skill-list-tile
v-for="weapon in weapons"
:key="weapon._id"
hide-modifier
:model="weapon"
:data-id="weapon._id"
/>
</v-list>
</v-card>
</div>
<div
v-if="armors && armors.length"
class="armor-proficiencies"
>
<v-card>
<v-list>
<v-subheader>
Armor
</v-subheader>
<skill-list-tile
v-for="armor in armors"
:key="armor._id"
hide-modifier
:model="armor"
:data-id="armor._id"
/>
</v-list>
</v-card>
</div>
<div
v-if="tools && tools.length"
class="tool-proficiencies"
>
<v-card>
<v-list>
<v-subheader>
Tools
</v-subheader>
<skill-list-tile
v-for="tool in tools"
:key="tool._id"
hide-modifier
:model="tool"
:data-id="tool._id"
/>
</v-list>
</v-card>
</div>
<div
v-if="languages && languages.length"
class="language-proficiencies"
>
<v-card>
<v-list>
<v-subheader>
Languages
</v-subheader>
<skill-list-tile
v-for="language in languages"
:key="language._id"
hide-modifier
:model="language"
:data-id="language._id"
/>
</v-list>
</v-card>
<div class="double-border">
<div class="label text-center">
{{ feature.name }}
</div>
<property-description
text
:model="feature.summary"
/>
</div>
</div>
</column-layout>
</div>
</column-layout>
</div>
</template>
<script lang="js">
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import DamageMultiplierCard from '/imports/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue';
import HealthBarCardContainer from '/imports/ui/properties/components/attributes/HealthBarCardContainer.vue';
import SkillListTile from '/imports/ui/properties/components/skills/SkillListTile.vue';
import ActionCard from '/imports/ui/properties/components/actions/ActionCard.vue';
import PrintedAction from '/imports/ui/creature/character/printedCharacterSheet/components/PrintedAction.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import PrintedSkill from '/imports/ui/creature/character/printedCharacterSheet/components/PrintedSkill.vue';
import PrintedDamageMultipliers from '/imports/ui/creature/character/printedCharacterSheet/components/PrintedDamageMultipliers.vue';
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue';
const getProperties = function (creature, filter, options = {
sort: { order: 1 }
@@ -335,10 +412,10 @@ const getSkillOfType = function (creature, type) {
export default {
components: {
ColumnLayout,
DamageMultiplierCard,
HealthBarCardContainer,
SkillListTile,
ActionCard,
PrintedDamageMultipliers,
PrintedAction,
PrintedSkill,
PropertyDescription,
},
props: {
creatureId: {
@@ -372,6 +449,9 @@ export default {
sort: { order: 1 }
});
},
healthBars() {
return getAttributeOfType(this.creature, 'healthBar');
},
modifiers() {
return getAttributeOfType(this.creature, 'modifier');
},
@@ -412,7 +492,9 @@ export default {
return getSkillOfType(this.creature, 'language');
},
actions() {
return getProperties(this.creature, { type: 'action' });
return getProperties(this.creature, { type: 'action' }, {
sort: { actionType: 1, order: 1 }
});
},
appliedBuffs() {
return getProperties(this.creature, { type: 'buff' });
@@ -424,6 +506,12 @@ export default {
sort: { value: 1, order: 1 }
});
},
features() {
return getProperties(this.creature, { type: 'feature' });
},
notes(){
return getProperties(this.creature, { type: 'note', summary: {$exists: true} });
},
},
methods: {
numberToSignedString,
@@ -432,141 +520,120 @@ export default {
</script>
<style lang="css" scoped>
.double-border {
position: relative;
padding: 11px 10px;
}
.double-border::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border-image-source: url(/images/print/doubleLineImageBorder.png);
border-image-slice: 110 126 fill;
border-image-width: 16px;
border-image-repeat: stretch;
box-sizing: content-box;
z-index: -1;
}
.octogon-border {
position: relative;
padding: 4px 20px;
}
.octogon-border::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border-image: url(/images/print/octogonBorder.png) 124 118 fill;
border-image-width: 22px;
z-index: -1;
}
.shield-border {
min-width: 64px !important;
display: flex;
align-items: center;
justify-content: center;
position: relative;
aspect-ratio: 0.87;
padding: 12px;
}
.shield-border::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: url(/images/print/shieldBorder.png);
background-size: contain;
background-position: center;
background-repeat: no-repeat;
z-index: -1;
}
.shield-number-label {
align-items: center !important;
}
.big-number {
font-size: 20pt;
}
.ability {
display: flex;
align-items: start;
margin: 4px 0;
}
.ability .score {
display: flex;
flex-direction: column;
align-items: center;
}
.ability .top {
min-width: 64px;
text-align: center;
margin-bottom: -10px;
padding: 14px;
z-index: 1;
}
.ability .bottom {
font-size: 10pt;
position: relative;
padding: 0 16px;
z-index: 2;
}
.ability .bottom::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border: solid white;
border-image-source: url(/images/print/upwardPointingBorder.png);
border-image-slice: 0 85 fill;
border-image-width: 0 16px;
border-image-outset: 0px 0px;
border-image-repeat: stretch;
box-sizing: content-box;
z-index: -1;
}
.ability .name {
margin-top: 10px;
margin-left: -16px;
padding-left: 20px;
}
.shield-border {
min-width: 64px !important;
display: flex;
align-items: center;
justify-content: center;
position: relative;
aspect-ratio: 0.87;
padding: 12px;
}
.shield-border::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: url(/images/print/shieldBorder.png);
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
z-index: -1;
}
.shield-number-label {
align-items: center !important;
}
.big-number {
font-size: 20pt;
}
.ability {
display: flex;
align-items: start;
margin: 4px 0;
}
.ability .score {
display: flex;
flex-direction: column;
align-items: center;
}
.ability .top {
min-width: 64px;
text-align: center;
margin-bottom: -10px;
padding: 14px;
z-index: 1;
}
.ability .bottom {
font-size: 10pt;
position: relative;
padding: 0 16px;
z-index: 2;
}
.ability .bottom::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border: solid white;
border-image-source: url(/images/print/upwardPointingBorder.png);
border-image-slice: 0 85 fill;
border-image-width: 0 16px;
border-image-outset: 0px 0px;
border-image-repeat: stretch;
box-sizing: content-box;
z-index: -1;
}
.ability .name {
margin-top: 10px;
margin-left: -16px;
padding-left: 20px;
}
.number-label {
display: flex;
align-items: flex-start;
}
.number-label {
display: flex;
align-items: flex-start;
}
.label {
font-size: 10pt;
font-variant: small-caps;
flex-grow: 1;
}
.label {
font-size: 10pt;
font-variant: small-caps;
flex-grow: 1;
}
.number-label .label {
margin-top: 4px;
margin-left: -30px;
padding-left: 34px;
z-index: -1;
}
.number-label .label {
margin-top: 4px;
margin-left: -30px;
padding-left: 34px;
z-index: -1;
}
.number-label .number {
min-width: 72px;
text-align: center;
z-index: 1;
}
.number-label .number {
min-width: 72px;
text-align: center;
z-index: 1;
}
.number-label .box {
width: 48px;
height: 48px;
margin-left: 10px;
z-index: 1;
}
.number-label .box {
width: 48px;
height: 48px;
margin-left: 10px;
z-index: 1;
}
.resource-bubble {
margin-bottom: -20px;
margin-top: 4px;
margin-right: 4px;
background-color: white;
border: solid black 2px;
border-radius: 50%;
width: 24px;
height: 24px;
}
</style>

View File

@@ -0,0 +1,247 @@
<template lang="html">
<div
class="action-card"
:class="cardClasses"
>
<div class="label text-center">
{{ actionTypeName }}
</div>
<div class="d-flex align-center">
<div class="avatar">
<div
v-if="rollBonus"
>
<template v-if="rollBonus && !rollBonusTooLong">
{{ rollBonus }}
</template>
<property-icon
v-else
:model="model"
color="rgba(0,0,0,0.7)"
/>
</div>
<property-icon
v-else
:model="model"
color="rgba(0,0,0,0.7)"
/>
</div>
<div
class="action-header flex d-flex column justify-center pl-1"
>
<div class="action-title my-1">
{{ model.name || propertyName }}
</div>
</div>
</div>
<div
v-if="Number.isFinite(model.uses)"
class="action-sub-title d-flex align-center"
>
{{ model.uses }} uses
</div>
<div class="pb-3">
<div
v-if="model.resources && model.resources.attributesConsumed.length ||
model.resources.itemsConsumed.length"
class="resources my-2"
>
<div
v-for="attributeConsumed in model.resources.attributesConsumed"
:key="attributeConsumed._id"
class="layout align-center justify-start"
>
Cost: {{ attributeConsumed.quantity && attributeConsumed.quantity.value }} {{ attributeConsumed.statName || attributeConsumed.variableName }}
</div>
<div
v-for="itemConsumed in model.resources.itemsConsumed"
:key="itemConsumed._id"
>
<template v-if="itemConsumed.itemName">
Uses: {{ itemConsumed.quantity && itemConsumed.quantity.value || 0 }} {{ itemConsumed.itemName || itemConsumed.tag }}
</template>
</div>
</div>
<template v-if="model.summary">
<markdown-text :markdown="model.summary.value || model.summary.text" />
</template>
<v-divider v-if="children && children.length" />
<tree-node-list
v-if="children && children.length"
start-expanded
:children="children"
@selected="e => $emit('sub-click', e)"
/>
</div>
</div>
</template>
<script lang="js">
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import AttributeConsumedView from '/imports/ui/properties/components/actions/AttributeConsumedView.vue';
import ItemConsumedView from '/imports/ui/properties/components/actions/ItemConsumedView.vue';
import PropertyIcon from '/imports/ui/properties/shared/PropertyIcon.vue';
import MarkdownText from '/imports/ui/components/MarkdownText.vue';
import TreeNodeList from '/imports/ui/components/tree/TreeNodeList.vue';
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { some } from 'lodash';
export default {
components: {
AttributeConsumedView,
ItemConsumedView,
MarkdownText,
PropertyIcon,
TreeNodeList,
},
inject: {
context: {
default: {},
},
theme: {
default: {
isDark: false,
},
},
},
props: {
model: {
type: Object,
required: true,
},
},
data() {
return {
activated: undefined,
doActionLoading: false,
hovering: false,
}
},
computed: {
rollBonus() {
if (!this.model.attackRoll) return;
return numberToSignedString(this.model.attackRoll.value);
},
rollBonusTooLong() {
return this.rollBonus && this.rollBonus.length > 3;
},
propertyName() {
return getPropertyName(this.model.type);
},
cardClasses() {
return {
'theme--dark': this.theme.isDark,
'theme--light': !this.theme.isDark,
'muted-text': this.model.insufficientResources,
'active': this.activated,
'elevation-8': this.hovering,
}
},
actionTypeName() {
return {
'action': 'Action',
'bonus': 'Bonus Action',
'attack': 'Attack',
'reaction': 'Reaction',
'free': 'Free Action',
'long': 'Long Action'
}[this.model.actionType] || this.model.actionType
}
},
meteor: {
children() {
const indicesOfTerminatingProps = [];
const decendants = CreatureProperties.find({
'ancestors.id': this.model._id,
'removed': { $ne: true },
}, {
sort: {order: 1}
}).map(prop => {
// Get all the props we don't want to show the decendants of and
// where they might appear in the ancestor list
if (prop.type === 'buff' || prop.type === 'folder') {
indicesOfTerminatingProps.push({
id: prop._id,
ancestorIndex: prop.ancestors.length,
});
}
return prop;
}).filter(prop => {
// Filter out folders entirely
if (prop.type === 'folder') return false;
// Filter out decendants of terminating props
return !some(indicesOfTerminatingProps, buffIndex => {
return prop.ancestors[buffIndex.ancestorIndex]?.id === buffIndex.id;
});
});
return nodeArrayToTree(decendants);
},
},
}
</script>
<style lang="css" scoped>
.action-card {
transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1),
transform 0.075s ease;
}
.avatar {
font-size: 18pt;
text-align: center;
min-width: 40px;
min-height: 40px;
}
.label {
font-size: 10pt;
font-variant: small-caps;
flex-grow: 1;
}
.action-title {
font-size: 16px;
font-weight: 400;
line-height: 24px;
position: relative;
text-align: left;
transition: .3s cubic-bezier(.25, .8, .5, 1);
width: 100%;
}
.resources {
font-size: 10pt;
}
.action-child {
height: 32px;
}
.theme--light.muted-text {
color: rgba(0, 0, 0, .3) !important;
}
.theme--dark.muted-text {
color: hsla(0, 0%, 100%, .3) !important;
}
.action-card {
transition: transform 0.15s cubic;
}
</style>
<style lang="css">
.action-card.theme--light.muted-text .v-icon {
color: rgba(0, 0, 0, .3) !important;
}
.action-card.theme--dark.muted-text .v-icon {
color: hsla(0, 0%, 100%, .3) !important;
}
.action-card .property-description>p:last-of-type {
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,135 @@
<template>
<div class="container">
<div class="d-flex justify-center">
<property-icon
class="ml-2"
color="rgba(0,0,0,0.7)"
:model="model"
/>
<div class="label">
{{ model.name }}
</div>
</div>
<div
v-if="model.value !== undefined || model.weight !== undefined"
class="weight-value my-2 d-flex justify-space-between"
>
<div class="value ml-4">
<div
v-if="model.value !== undefined"
>
<v-layout align-center>
<v-icon
class="mr-2"
small
>
$vuetify.icons.two_coins
</v-icon>
<coin-value
class="mr-2"
:value="model.value"
/>
</v-layout>
<v-layout
align-center
class="mb-2"
>
<v-icon
class="mr-2"
small
>
$vuetify.icons.cash
</v-icon>
<coin-value
:value="model.contentsValue"
/>
<span
class="ml-1"
>
contents
</span>
</v-layout>
</div>
</div>
<div class="weight ml-4">
<div
v-if="model.weight !== undefined"
>
<v-layout align-center>
<v-icon
class="mr-2"
small
>
$vuetify.icons.weight
</v-icon>
{{ model.weight }} lb
</v-layout>
<v-layout
align-center
class="mb-2"
>
<v-icon
class="mr-2"
small
>
$vuetify.icons.injustice
</v-icon>
{{ model.contentsWeight }} lb
<span
class="ml-1"
>
contents
</span>
</v-layout>
</div>
</div>
</div>
<property-description
text
:model="model.description"
/>
</div>
</template>
<script lang="js">
import treeNodeViewMixin from '/imports/ui/properties/treeNodeViews/treeNodeViewMixin.js';
import PROPERTIES from '/imports/constants/PROPERTIES.js';
import CoinValue from '/imports/ui/components/CoinValue.vue';
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue';
import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js';
export default {
components: {
CoinValue,
PropertyDescription,
},
mixins: [treeNodeViewMixin],
inject: {
context: { default: {} }
},
props: {
preparingSpells: Boolean,
},
data() {
return {
incrementLoading: false,
}
},
computed: {
hasClickListener() {
return this.$listeners && !!this.$listeners.click;
},
},
}
</script>
<style lang="css" scoped>
.item-avatar {
min-width: 32px;
}
</style>

View File

@@ -0,0 +1,71 @@
<template lang="html">
<div>
<div
v-for="(multiplier, multiplierIndex) in multipliers"
:key="multiplier._id"
:data-id="multiplier._id"
@click="$emit('click-multiplier', {_id: multiplier._id})"
>
<v-divider v-if="multiplierIndex" />
<div>
<div
v-if="multiplier.name"
class="label text-center"
>
{{ multiplier.name }}
</div>
<div class="font-weight-medium">
{{ title(multiplier) }}
</div>
<div class="d-flex flex-wrap align-center">
{{ multiplier.damageTypes.join(', ') }}
</div>
<div
v-if="multiplier.includeTags && multiplier.includeTags.length"
class="d-flex flex-wrap align-center"
>
<div>
For:
</div>
{{ multiplier.includeTags.join(', ') }}
</div>
<div
v-if="multiplier.excludeTags && multiplier.excludeTags.length"
class="d-flex flex-wrap align-center"
>
<div>
Except:
</div>
{{ multiplier.excludeTags.join(', ') }}
</div>
</div>
</div>
</div>
</template>
<script lang="js">
export default {
props: {
multipliers:{
type: Array,
required: true,
}
},
methods: {
title(prop){
switch (prop.value){
case 0: return 'Immunity';
case 0.5: return 'Resistance';
case 2: return 'Vulnerability';
}
}
}
}
</script>
<style lang="css" scoped>
.label {
font-size: 10pt;
font-variant: small-caps;
}
</style>

View File

@@ -0,0 +1,173 @@
<template>
<div class="item">
<div class="d-flex justify-space-between">
<div class="label">
{{ title }}
<template v-if="attunementText">
({{ attunementText }})
</template>
</div>
<property-icon
class="ml-2"
color="rgba(0,0,0,0.7)"
:model="model"
/>
</div>
<div
v-if="model.value !== undefined || model.weight !== undefined"
class="weight-value my-2 d-flex justify-space-between"
>
<div class="value ml-4">
<div
v-if="model.value !== undefined"
>
<v-layout
v-if="model.quantity > 1"
align-center
class="mb-2"
>
<v-icon
class="mr-2"
small
>
$vuetify.icons.cash
</v-icon>
<coin-value
:value="model.value * model.quantity"
/>
</v-layout>
<v-layout align-center>
<v-icon
class="mr-2"
small
>
$vuetify.icons.two_coins
</v-icon>
<coin-value
class="mr-2"
:value="model.value"
/>
<span
v-if="model.quantity > 1"
class="ml-1"
>
each
</span>
</v-layout>
</div>
</div>
<div class="weight ml-4">
<div
v-if="model.weight !== undefined"
>
<v-layout
v-if="model.quantity > 1"
align-center
class="mb-2"
>
<v-icon
class="mr-2"
small
>
$vuetify.icons.injustice
</v-icon>
{{ totalWeight }} lb
</v-layout>
<v-layout align-center>
<v-icon
class="mr-2"
small
>
$vuetify.icons.weight
</v-icon>
{{ model.weight }} lb
<span
v-if="model.quantity > 1"
class="ml-1"
>
each
</span>
</v-layout>
</div>
</div>
</div>
<property-description
text
:model="model.description"
/>
</div>
</template>
<script lang="js">
import treeNodeViewMixin from '/imports/ui/properties/treeNodeViews/treeNodeViewMixin.js';
import PROPERTIES from '/imports/constants/PROPERTIES.js';
import CoinValue from '/imports/ui/components/CoinValue.vue';
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue';
import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js';
export default {
components: {
CoinValue,
PropertyDescription,
},
mixins: [treeNodeViewMixin],
inject: {
context: { default: {} }
},
props: {
preparingSpells: Boolean,
},
data() {
return {
incrementLoading: false,
}
},
computed: {
hasClickListener() {
return this.$listeners && !!this.$listeners.click;
},
title() {
let model = this.model;
if (!model) return;
if (model.quantity !== 1) {
if (model.plural) {
return `${model.quantity} ${model.plural}`;
} else if (model.name) {
return `${model.quantity} ${model.name}`;
}
} else if (model.name) {
return model.name;
}
let prop = PROPERTIES[model.type]
return prop && prop.name;
},
totalValue() {
return stripFloatingPointOddities(this.model.value * this.model.quantity);
},
totalWeight() {
return stripFloatingPointOddities(this.model.weight * this.model.quantity);
},
attunementText() {
if (this.model.requiresAttunement) {
if (this.model.attuned) return 'Attuned';
return 'Requires attunement';
}
return undefined;
}
},
}
</script>
<style lang="css" scoped>
.item-avatar {
min-width: 32px;
}
.item .label {
font-size: 14pt;
font-variant: all-small-caps;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,105 @@
<template lang="html">
<div
class="printed-skill pl-0 d-flex align-center"
>
<div class="d-flex align-center">
<div
v-if="!hideModifier"
class="d-flex align-center"
>
<proficiency-icon
:value="model.proficiency"
class="prof-icon"
/>
<div class="prof-mod ml-2 mr-4 text-right">
{{ displayedModifier }}
</div>
<v-icon
v-if="model.advantage > 0"
size="20px"
>
mdi-chevron-double-up
</v-icon>
<v-icon
v-if="model.advantage < 0"
size="20px"
>
mdi-chevron-double-down
</v-icon>
</div>
<proficiency-icon
v-else
:value="model.proficiency"
class="prof-icon mr-2"
/>
<div class="text-truncate">
{{ model.name }}
<template v-if="model.conditionalBenefits && model.conditionalBenefits.length">
*
</template>
<template v-if="'passiveBonus' in model">
({{ passiveScore }})
</template>
</div>
</div>
</div>
</template>
<script lang="js">
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import ProficiencyIcon from '/imports/ui/properties/shared/ProficiencyIcon.vue';
export default {
components: {
ProficiencyIcon,
},
inject: {
context: {
default: {},
},
},
props: {
model: {
type: Object,
required: true,
},
hideModifier: Boolean,
},
data() {
return {
checkLoading: false,
}
},
computed: {
displayedModifier() {
let mod = this.model.value;
if (this.model.fail) {
return 'fail';
} else {
return numberToSignedString(mod);
}
},
passiveScore() {
return 10 + this.model.value + this.model.passiveBonus;
}
},
}
</script>
<style lang="css" scoped>
.printed-skill{
min-height: 30px;
}
.prof-icon {
min-width: 30px;
}
.prof-mod {
min-width: 24px;
}
.v-icon.theme--light {
color: rgba(0, 0, 0, 0.7) !important;
}
</style>

View File

@@ -0,0 +1,82 @@
<template lang="html">
<div
class="double-border"
>
<div
v-if="model.name"
class="label"
>
{{ model.name }}
</div>
<div v-if="model.level">
{{ levelText }} {{ model.school }} {{ model.ritual ? '(ritual)' : '' }}
</div>
<div v-else>
{{ model.school }} cantrip
</div>
<div>
Casting Time: {{ model.castingTime }}
</div>
<div>
Range: {{ model.range }}
</div>
<div>
Components: {{ spellComponents }}
</div>
<div>
Duration: {{ model.duration }}
</div>
<property-description
text
:model="model.summary"
/>
<v-divider class="my-2" />
<property-description
text
:model="model.description"
/>
</div>
</template>
<script lang="js">
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue';
const levelText = [
'cantrip', '1st-level', '2nd-level', '3rd-level', '4th-level', '5th-level',
'6th-level', '7th-level', '8th-level', '9th-level'
];
export default {
components: {
PropertyDescription,
},
props: {
model: {
type: Object,
required: true,
},
},
computed: {
levelText() {
return levelText[this.model.level]
},
spellComponents() {
let components = [];
if (this.model.ritual) components.push('Ritual');
if (this.model.concentration) components.push('Concentration');
if (this.model.verbal) components.push('Verbal');
if (this.model.somatic) components.push('Somatic');
if (this.model.material) components.push(`Material (${this.model.material})`);
return components.join(', ');
},
}
}
</script>
<style lang="css" scoped>
.label {
font-size: 14pt;
font-variant: all-small-caps;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<div class="octagon-border">
<div class="label text-center">
{{ model.name }}
</div>
<div>
Spell Save DC: {{ model.dc && model.dc.value }}
</div>
<div>
Spell Attack Bonus: {{ model.attackRollBonus && model.attackRollBonus.value }}
</div>
<div>
Maximum prepared spells: {{ model.maxPrepared && model.maxPrepared.value }}
</div>
<property-description
text
:model="model.description"
/>
</div>
</template>
<script lang="js">
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue';
export default {
components: {
PropertyDescription,
},
props: {
model: {
type: Object,
required: true,
},
},
computed: {
}
}
</script>

45
app/package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "dicecloud",
"version": "2.0.42",
"version": "2.0.43",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -594,9 +594,9 @@
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"base64-js": {
"version": "1.5.1",
@@ -893,12 +893,12 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
},
"core-js": {
"version": "2.6.12",
@@ -958,7 +958,7 @@
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"decompress-response": {
"version": "6.0.0",
@@ -1005,7 +1005,7 @@
"delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
},
"detect-libc": {
"version": "2.0.1",
@@ -1715,7 +1715,7 @@
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
},
"http-signature": {
"version": "1.2.0",
@@ -2090,7 +2090,7 @@
"lodash.omit": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
"integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg=="
"integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA="
},
"lodash.template": {
"version": "4.5.0",
@@ -3008,9 +3008,9 @@
"integrity": "sha1-IBvZSSceGfbgrwodwMzFg95HxjA="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -3156,7 +3156,7 @@
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"object-inspect": {
"version": "1.12.2",
@@ -3352,6 +3352,11 @@
"yargs": "^15.3.1"
}
},
"qrcode.vue": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/qrcode.vue/-/qrcode.vue-1.7.0.tgz",
"integrity": "sha512-R7t6Y3fDDtcU7L4rtqwGUDP9xD64gJhIwpfjhRCTKmBoYF6SS49PIJHRJ048cse6OI7iwTwgyy2C46N9Ygoc6g=="
},
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
@@ -3396,7 +3401,7 @@
"strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
}
}
},
@@ -3470,7 +3475,7 @@
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
},
"require-from-string": {
"version": "2.0.2",
@@ -3570,7 +3575,7 @@
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"setimmediate": {
"version": "1.0.5",
@@ -3660,7 +3665,7 @@
"simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
"requires": {
"is-arrayish": "^0.3.1"
}
@@ -4096,7 +4101,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"uuid": {
"version": "3.4.0",
@@ -4243,7 +4248,7 @@
"which-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q=="
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
},
"which-typed-array": {
"version": "1.1.8",

View File

@@ -44,6 +44,7 @@
"ngraph.path": "^1.4.0",
"pretty-bytes": "^6.0.0",
"qrcode": "^1.5.1",
"qrcode.vue": "^1.7.0",
"request": "^2.88.2",
"sharp": "^0.30.7",
"simpl-schema": "^1.13.1",
@@ -124,4 +125,4 @@
"vuetify/no-deprecated-classes": "error"
}
}
}
}

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB