More item types
Some checks failed
test / run-tests-client (push) Successful in 43s
test / run-tests-backend (push) Failing after 14s

This commit is contained in:
BinarySandia04 2024-10-18 15:11:08 +02:00
parent dedbde13db
commit 73d10a7846
16 changed files with 301 additions and 110 deletions

View File

@ -7,7 +7,6 @@ const BookSchema = new Schema({
description: { type: String }, description: { type: String },
system: {type: String, required: true}, system: {type: String, required: true},
image: { type: String }, image: { type: String },
contents: [ {type: mongoose.Types.ObjectId, ref: "Concept"} ],
}); });
module.exports = mongoose.model('Book', BookSchema); module.exports = mongoose.model('Book', BookSchema);

View File

@ -307,9 +307,50 @@ span.artifact {
} }
.form-element label { .form-element label {
flex-grow: 1; flex-grow: 0;
margin-right: 6px;
margin-left: 6px;
} }
.form-element.centered { .form-element.centered {
justify-content: center; justify-content: center;
} }
.grow {
flex-grow: 1;
}
.subsection.border:first-child {
border-left: none;
}
.subsection.border {
border-left: 1px solid var(--color-border);
}
.subsection {
margin-left: 5px;
margin-right: 5px;
height: 32px;
display: flex;
align-items: left;
justify-content: left;
}
.subsection.left {
align-items: left;
justify-content: left;
}
.subsection.right {
align-items: right;
justify-content: right;
}
.subsection.center {
align-items: center;
justify-content: center;
}

View File

@ -71,8 +71,8 @@ function PopulateContext(val){
animate(contextMenuElement, { animate(contextMenuElement, {
opacity: [0, 1], opacity: [0, 1],
translateY: [20, -2] translateY: [-20, -2]
}, {delay: (elementNum / 2) * 0.1, duration: 0.25}).finished.then(() => { }, {duration: 0.15}).finished.then(() => {
}); });
elementNum++; elementNum++;

View File

@ -0,0 +1,31 @@
function GetKey(from, key){
let k = key.split('.');
let obj = from;
for(let i = 0; i < k.length; i++){
if(typeof obj !== 'object'){
// We found a literal before ending!
return;
}
if(Object.keys(obj).includes(k[i])){
obj = obj[k[i]];
} else return;
}
return obj;
}
function SetKey(from, key, value){
let k = key.split('.');
let obj = from;
for(let i = 0; i < k.length - 1; i++){
if(!Object.keys(obj).includes(k[i])){
obj[k[i]] = {};
}
obj = obj[k[i]];
}
obj[k[k.length - 1]] = value;
}
export {
GetKey,
SetKey
}

View File

@ -2,6 +2,7 @@
import { onMounted, ref, watch } from 'vue'; import { onMounted, ref, watch } from 'vue';
import { AddContextMenu } from '@/services/ContextMenu'; import { AddContextMenu } from '@/services/ContextMenu';
import { AddTooltip } from '@/services/Tooltip'; import { AddTooltip } from '@/services/Tooltip';
import { GetKey } from '@/services/Utils';
import { marked } from "marked"; import { marked } from "marked";
const props = defineProps(['element', 'context', 'tooltip', 'icon']); const props = defineProps(['element', 'context', 'tooltip', 'icon']);
@ -14,7 +15,8 @@ const icon = ref("icons/game-icons/ffffff/lorc/crossed-swords.svg")
async function updateElement(){ async function updateElement(){
element.value = props.element; element.value = props.element;
// Do whatever // Do whatever
let desc = element.value.info.description; let desc = undefined;
GetKey(element.value, "info.description", (val) => desc = val);
desc = desc ? marked.parse(desc) : ''; desc = desc ? marked.parse(desc) : '';
if(props.icon) icon.value = await props.icon(element.value); if(props.icon) icon.value = await props.icon(element.value);

View File

@ -11,6 +11,7 @@ const selected = ref(initialSelect);
onMounted(() => { onMounted(() => {
let context = []; let context = [];
if(props.selected == undefined) selected.value = "undefined";
watch(() => props.selected, () => { watch(() => props.selected, () => {
selected.value = props.selected; selected.value = props.selected;
}); });
@ -21,7 +22,7 @@ onMounted(() => {
action: () => { action: () => {
HideContextMenu(); HideContextMenu();
selected.value = name; selected.value = name;
selectCallback(name); if(selectCallback) selectCallback(name);
} }
}); });
}); });
@ -34,12 +35,27 @@ onMounted(() => {
<template> <template>
<div class="dropdown" ref="dropdown"> <div class="dropdown" ref="dropdown">
<span>{{ selected }}</span> <span>{{ selected }}</span>
<img class="icon" src="/icons/iconoir/regular/nav-arrow-down.svg" draggable="false" ref="closeButton">
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.dropdown { .dropdown {
background-color: #181818; flex-grow: 1;
padding: 5px; display: flex;
background-color: var(--color-background-softer);
border: none;
padding: 4px 8px 4px 8px;
margin: 0 6px 0px 6px;
border-radius: 6px;
color: var(--color-text);
transition: 300ms background-color;
border: solid 1px var(--color-border);
.icon {
margin-left: auto;
justify-content: right;
}
} }
</style> </style>

View File

@ -0,0 +1,8 @@
<script setup>
</script>
<template>
<div class="form-element">
<slot></slot>
</div>
</template>

View File

@ -23,7 +23,9 @@ defineExpose({
<style lang="scss" scoped> <style lang="scss" scoped>
.number-input { .number-input {
max-width: 70px; flex-grow: 1;
flex-shrink: 1;
text-align: center; text-align: center;
width: 100px;
} }
</style> </style>

View File

@ -71,6 +71,7 @@ defineExpose({
<style scoped lang="scss"> <style scoped lang="scss">
.tags-container { .tags-container {
display: flex; display: flex;
flex-grow: 1;
flex-wrap: wrap; flex-wrap: wrap;
padding-left: 4px; padding-left: 4px;
padding-right: 4px; padding-right: 4px;

View File

@ -110,13 +110,5 @@
"systems": { "systems": {
"title": "Select a game system", "title": "Select a game system",
"not-selected": "No game system selected" "not-selected": "No game system selected"
},
"database": {
"title": "Database",
"tabs": {
"items": "Items",
"spells": "Spells",
"features": "Features"
}
} }
} }

View File

@ -16,18 +16,13 @@ function Main(api){
book: { type: "ObjectId", ref: "Book"} book: { type: "ObjectId", ref: "Book"}
}); });
let entityModel = Api.createModel('entity', {
dndModule.router.get('/testing', (req, res) => { });
/*
let item = itemModel.create({ let characterModel = Api.createModel('character', {
name: "Test item!",
type: "The test item" });
})
*/
res.json({
status: "ok"
})
})
dndModule.router.get('/item/list', (req, res) => { dndModule.router.get('/item/list', (req, res) => {
const campaign = req.query.campaign; const campaign = req.query.campaign;
@ -50,7 +45,6 @@ function Main(api){
res.json({status: "ok", item}); res.json({status: "ok", item});
}); });
}); });
dndModule.router.get('/item/get', (req, res) => { dndModule.router.get('/item/get', (req, res) => {
const campaign = req.query.campaign; const campaign = req.query.campaign;
let id = req.query.id; let id = req.query.id;
@ -59,7 +53,6 @@ function Main(api){
res.json({status: "ok", concept}); res.json({status: "ok", concept});
}); });
}) })
dndModule.router.put('/item/update', (req, res) => { dndModule.router.put('/item/update', (req, res) => {
const campaign = req.query.campaign; const campaign = req.query.campaign;
let id = req.query.id; let id = req.query.id;

View File

@ -68,7 +68,7 @@ function ConfigureBookmarks(){
<img class="icon bookmark-icon" draggable="false" src="/icons/game-icons/ffffff/lorc/book-cover.svg"> <img class="icon bookmark-icon" draggable="false" src="/icons/game-icons/ffffff/lorc/book-cover.svg">
</div> </div>
<div class="bookmark"> <div class="bookmark">
<img class="icon bookmark-icon" draggable="false" src="/icons/game-icons/ffffff/lorc/power-lightning.svg"> <img class="icon bookmark-icon" draggable="false" src="/icons/game-icons/ffffff/lorc/scroll-unfurled.svg">
</div> </div>
<div class="bookmark"> <div class="bookmark">
<img class="icon bookmark-icon" draggable="false" src="/icons/game-icons/ffffff/lorc/feather.svg"> <img class="icon bookmark-icon" draggable="false" src="/icons/game-icons/ffffff/lorc/feather.svg">

View File

@ -16,7 +16,7 @@ const PluginData = Global('dnd-5e').Data;
onMounted(() => { onMounted(() => {
SetupHandle(id, handle); SetupHandle(id, handle);
SetSize(id, {width: 250, height: 320}); SetSize(id, {width: 250, height: 410});
ResetPosition(id, "center"); ResetPosition(id, "center");
}); });
@ -73,6 +73,18 @@ function ConfirmSelection(){
<span>Tool</span> <span>Tool</span>
<input type="radio" name="selector" value="Tool"> <input type="radio" name="selector" value="Tool">
</div> </div>
<div class="radio-item">
<img class="icon" src="/icons/game-icons/000000/lorc/scroll-unfurled.svg">
<span>Spell</span>
<input type="radio" name="selector" value="Spell">
</div>
<div class="radio-item">
<img class="icon" src="/icons/game-icons/000000/delapouite/round-star.svg">
<span>Feature</span>
<input type="radio" name="selector" value="Feature">
</div>
</div> </div>
<button class="btn-primary sound-click submit" v-on:click.prevent="ConfirmSelection"> <button class="btn-primary sound-click submit" v-on:click.prevent="ConfirmSelection">

View File

@ -1,4 +1,6 @@
<script setup> <script setup>
import { marked } from "marked";
import WindowHandle from '@/views/partials/WindowHandle.vue'; import WindowHandle from '@/views/partials/WindowHandle.vue';
import { onMounted, ref, shallowRef, watch } from 'vue'; import { onMounted, ref, shallowRef, watch } from 'vue';
@ -7,6 +9,7 @@ import ConceptList from '@/views/partials/ConceptList.vue';
import Tabs from '@/views/partials/Tabs.vue'; import Tabs from '@/views/partials/Tabs.vue';
import FixedBottomButtons from '@/views/partials/FixedBottomButtons.vue'; import FixedBottomButtons from '@/views/partials/FixedBottomButtons.vue';
import { Global } from '@/services/PluginGlobals'; import { Global } from '@/services/PluginGlobals';
import { GetKey } from '@/services/Utils.js';
import { FetchConcepts, GetConcepts } from './../data.js' import { FetchConcepts, GetConcepts } from './../data.js'
@ -19,17 +22,34 @@ const Api = Global('dnd-5e').Api;
const PluginData = Global('dnd-5e').Data; const PluginData = Global('dnd-5e').Data;
let id = data.id; let id = data.id;
const elements = shallowRef([]);
const weapons = shallowRef([]);
const equipment = shallowRef([]);
const consumables = shallowRef([]);
const containers = shallowRef([]);
const tools = shallowRef([]);
const spells = shallowRef([]);
const features = shallowRef([]);
onMounted(() => { onMounted(() => {
SetupHandle(id, handle); SetupHandle(id, handle);
SetSize(id, {width: 700, height: 800}); SetSize(id, {width: 800, height: 800});
ResetPosition(id, "center"); ResetPosition(id, "center");
SetResizable(id, true); SetResizable(id, true);
SetMinSize(id, {width: 350, height: 300}); SetMinSize(id, {width: 800, height: 300});
watch(GetConcepts, () => { watch(GetConcepts, () => {
elements.value = GetConcepts(); let elements = GetConcepts();
weapons.value = elements.filter((e) => e.type == "Weapon");
equipment.value = elements.filter((e) => e.type == "Equipment");
consumables.value = elements.filter((e) => e.type == "Consumable");
containers.value = elements.filter((e) => e.type == "Container");
tools.value = elements.filter((e) => e.type == "Tool");
spells.value = elements.filter((e) => e.type == "Spell");
features.value = elements.filter((e) => e.type == "Feature");
console.log(elements);
console.log(elements);
}); });
FetchConcepts(); FetchConcepts();
@ -55,15 +75,18 @@ function ElementContext(element){
} }
function ElementTooltip(element){ function ElementTooltip(element){
let descHtml = GetKey(element, 'info.description');
if(descHtml) descHtml = marked.parse(descHtml);
else descHtml = '';
return `<div class='document item'> return `<div class='document item'>
<h2>${element.name}</h2> <h2>${element.name}</h2>
<img src='${element.info.icon}'></img> <img src='${GetKey(element, "info.icon")}'></img>
<div class='document'>${element.info.description ?? ''}</div> <div class='document'>${descHtml}</div>
</div>`; </div>`;
} }
function ElementIcon(element){ function ElementIcon(element){
return element.info ? element.info.icon : 'icons/game-icons/ffffff/lorc/crossed-swords.svg' return GetKey(element, "info.icon") ? GetKey(element, "info.icon") : 'icons/game-icons/ffffff/lorc/crossed-swords.svg'
} }
</script> </script>
@ -74,13 +97,71 @@ function ElementIcon(element){
<div class="main-container"> <div class="main-container">
<Tabs :rows="[ <Tabs :rows="[
{id: 'items', value: 'database.tabs.items'}, {id: 'weapons', value: 'plugins.dnd-5e.database.tabs.weapons'},
{id: 'spells', value: 'database.tabs.spells'}, {id: 'equipment', value: 'plugins.dnd-5e.database.tabs.equipment'},
{id: 'features', value: 'database.tabs.features'} {id: 'consumables', value: 'plugins.dnd-5e.database.tabs.consumables'},
{id: 'containers', value: 'plugins.dnd-5e.database.tabs.containers'},
{id: 'tools', value: 'plugins.dnd-5e.database.tabs.tools'},
{id: 'spells', value: 'plugins.dnd-5e.database.tabs.spells'},
{id: 'features', value: 'plugins.dnd-5e.database.tabs.features'},
]"> ]">
<template #items> <template #weapons>
<ConceptList <ConceptList
:elements="elements" :elements="weapons"
:open="OpenConcept"
:context="ElementContext"
:tooltip="ElementTooltip"
:icon="ElementIcon"
></ConceptList>
</template>
<template #equipment>
<ConceptList
:elements="equipment"
:open="OpenConcept"
:context="ElementContext"
:tooltip="ElementTooltip"
:icon="ElementIcon"
></ConceptList>
</template>
<template #consumables>
<ConceptList
:elements="consumables"
:open="OpenConcept"
:context="ElementContext"
:tooltip="ElementTooltip"
:icon="ElementIcon"
></ConceptList>
</template>
<template #containers>
<ConceptList
:elements="containers"
:open="OpenConcept"
:context="ElementContext"
:tooltip="ElementTooltip"
:icon="ElementIcon"
></ConceptList>
</template>
<template #tools>
<ConceptList
:elements="tools"
:open="OpenConcept"
:context="ElementContext"
:tooltip="ElementTooltip"
:icon="ElementIcon"
></ConceptList>
</template>
<template #spells>
<ConceptList
:elements="spells"
:open="OpenConcept"
:context="ElementContext"
:tooltip="ElementTooltip"
:icon="ElementIcon"
></ConceptList>
</template>
<template #features>
<ConceptList
:elements="features"
:open="OpenConcept" :open="OpenConcept"
:context="ElementContext" :context="ElementContext"
:tooltip="ElementTooltip" :tooltip="ElementTooltip"

View File

@ -10,9 +10,13 @@ import { AddContextMenu, HideContextMenu, ShowContextMenu } from '@/services/Con
import Tabs from '@/views/partials/Tabs.vue'; import Tabs from '@/views/partials/Tabs.vue';
import MarkdownEditor from '@/views/partials/MarkdownEditor.vue'; import MarkdownEditor from '@/views/partials/MarkdownEditor.vue';
import Tags from '@/views/partials/Tags.vue'; import Tags from '@/views/partials/Tags.vue';
import NumberInput from '@/views/partials/NumberInput.vue'; import Input from '@/views/partials/Input.vue';
import { GetKey, SetKey } from '@/services/Utils.js';
import { Global } from '@/services/PluginGlobals'; import { Global } from '@/services/PluginGlobals';
import Dropdown from '@/views/partials/Dropdown.vue';
import FormElement from '@/views/partials/FormElement.vue';
const props = defineProps(['data']); const props = defineProps(['data']);
const data = props.data; const data = props.data;
const api = Global('dnd-5e').Api; const api = Global('dnd-5e').Api;
@ -31,6 +35,7 @@ const properties = ref(null);
const quantity = ref(null); const quantity = ref(null);
const weight = ref(null); const weight = ref(null);
const price = ref(null); const price = ref(null);
const item_type_name = ref("");
function GenRarities(){ function GenRarities(){
let rarities = []; let rarities = [];
@ -81,7 +86,7 @@ function Upload(){
} }
function SetParam(param, value){ function SetParam(param, value){
concept.value.info[param] = value; SetKey(concept.value, `info.${param}`, value);
Upload(); Upload();
} }
@ -101,18 +106,16 @@ function InitValues(){
let rarities = GenRarities(); let rarities = GenRarities();
let weapon_types = GenTypes(["", "Melee", "Ranged", "Martial Melee", "Martial Ranged", "Natural", "Improvised", "Siege Weapon"]); let weapon_types = GenTypes(["", "Melee", "Ranged", "Martial Melee", "Martial Ranged", "Natural", "Improvised", "Siege Weapon"]);
if(!concept.value.data) concept.value.data = {}; icon_selector.value.icon = GetKey(concept.value, "info.icon");
if(!concept.value.info) concept.value.info = {}; rarity.value.innerHTML = `<span class='important ${GetKey(concept.value, "info.rarity") ? GetKey(concept.value, "info.rarity").replace(/\s+/g, '-').toLowerCase() : ""}'>${GetKey(concept.value, "info.rarity")}</span>`;
weaponType.value.innerHTML = `<span class='important'>${GetKey(concept.value, "info.weapon_type")}</span>`;
if(concept.value.info.icon) icon_selector.value.icon = concept.value.info.icon; description.value.text = GetKey(concept.value, "info.description");
if(concept.value.info.rarity) rarity.value.innerHTML = `<span class='important ${concept.value.info.rarity.replace(/\s+/g, '-').toLowerCase()}'>${concept.value.info.rarity}</span>`; properties.value.selected = GetKey(concept.value, "info.properties");
if(concept.value.info.weapon_type) weaponType.value.innerHTML = `<span class='important'>${concept.value.info.weapon_type}</span>`; quantity.value.Set(GetKey(concept.value, "info.quantity"));
if(concept.value.info.description) description.value.text = concept.value.info.description; weight.value.Set(GetKey(concept.value, "info.weight"));
if(concept.value.info.properties) properties.value.selected = concept.value.info.properties; price.value.Set(GetKey(concept.value, "info.price"));
item_type_name.value = GetKey(concept.value, "type");
if(concept.value.info.quantity) quantity.value.Set(concept.value.info.quantity); item_name.value.innerHTML = GetKey(concept.value, "name");
if(concept.value.info.weight) weight.value.Set(concept.value.info.weight);
if(concept.value.info.price) price.value.Set(concept.value.info.price);
quantity.value.OnUpdate((val) => SetParam('quantity', val)); quantity.value.OnUpdate((val) => SetParam('quantity', val));
weight.value.OnUpdate((val) => SetParam('weight', val)); weight.value.OnUpdate((val) => SetParam('weight', val));
@ -135,34 +138,32 @@ function InitValues(){
} }
onMounted(() => { onMounted(() => {
SetupHandle(id, handle); SetupHandle(id, handle);
SetSize(id, {width: 600, height: 700}); SetSize(id, {width: 600, height: 700});
SetResizable(id, true); SetResizable(id, true);
SetMinSize(id, {width: 400, height: 300}); SetMinSize(id, {width: 400, height: 300});
ResetPosition(id, "center"); ResetPosition(id, "center");
item_type.value = data.item_type;
if(data.item_create){
dndModule.router.post('/item/create', {}, {
data: {
type: data.item_type,
name: "New " + data.item_type
},
}).then(response => {
concept.value = response.data.concept;
InitValues();
}).catch(err => console.log(err));
} else {
// Get concept
GetConcept(data.item_id).then(response => {
concept.value = response.data.concept;
InitValues();
}).catch(err => console.log(err));
}
}); });
item_type.value = data.item_type;
if(data.item_create){
dndModule.router.post('/item/create', {}, {
data: {
type: data.item_type,
name: "New " + data.item_type
},
}).then(response => {
concept.value = response.data.concept;
InitValues();
}).catch(err => console.log(err));
} else {
// Get concept
GetConcept(data.item_id).then(response => {
concept.value = response.data.concept;
InitValues();
}).catch(err => console.log(err));
}
</script> </script>
@ -174,10 +175,13 @@ onMounted(() => {
<div class="item-header"> <div class="item-header">
<IconSelector :window="id" ref="icon_selector" :done="IconSelected"></IconSelector> <IconSelector :window="id" ref="icon_selector" :done="IconSelected"></IconSelector>
<div class="header-info"> <div class="header-info">
<h1 contenteditable="true" spellcheck="false" ref="item_name">{{ concept.name }}</h1>
<div class="row"> <div class="row">
<div class="grow subsection" ref="weaponType"></div> <h1 class="grow subsection left" contenteditable="true" spellcheck="false" ref="item_name"></h1>
<div class="grow subsection" ref="rarity"></div> <h1 class="subsection right">{{ item_type_name }}</h1>
</div>
<div class="row">
<div class="grow subsection center border" ref="weaponType"></div>
<div class="grow subsection center border" ref="rarity"></div>
</div> </div>
</div> </div>
</div> </div>
@ -189,18 +193,18 @@ onMounted(() => {
<div class="description-container"> <div class="description-container">
<div class="description-sidebar"> <div class="description-sidebar">
<div class="form-container"> <div class="form-container">
<div class="form-element"> <FormElement>
<label>{{$t('general.quantity')}}</label> <label>{{$t('general.quantity')}}</label>
<NumberInput ref="quantity"></NumberInput> <Input ref="quantity"></Input>
</div> </FormElement>
<div class="form-element"> <FormElement>
<label>{{$t('general.weight')}}</label> <label>{{$t('general.weight')}}</label>
<NumberInput ref="weight"></NumberInput> <Input ref="weight"></Input>
</div> </FormElement>
<div class="form-element"> <FormElement>
<label>{{$t('general.price')}}</label> <label>{{$t('general.price')}}</label>
<NumberInput ref="price"></NumberInput> <Input ref="price"></Input>
</div> </FormElement>
</div> </div>
</div> </div>
<div class="description"> <div class="description">
@ -210,8 +214,21 @@ onMounted(() => {
</template> </template>
<template #details> <template #details>
<h2 class="section">Properties</h2> <h2 class="section">Properties</h2>
<Tags ref="properties" :items="['Amunnition','Finesse','Heavy','Light','Loading','Range','Reach','Special','Thrown','Two-Handed','Versatile']" :done="PropertiesChanged"></Tags> <FormElement>
<label>Properties</label>
<Tags ref="properties" :items="['Amunnition','Finesse','Heavy','Light','Loading','Range','Reach','Special','Thrown','Two-Handed','Versatile']" :done="PropertiesChanged"></Tags>
</FormElement>
<h2 class="section">Usage</h2>
<FormElement>
<label>Range</label>
<Input></Input><label>/</label><Input></Input><Dropdown :options="['ft', 'm']" :selected="'ft'"></Dropdown>
</FormElement>
<h2 class="section">Damage</h2> <h2 class="section">Damage</h2>
<FormElement>
<label>Damage</label>
<Dropdown :options="['None','Acid','Bludgeoning','Cold','Fire','Force','Lightning','Necrotic','Piercing','Poison','Psychic','Radiant','Slashing','Thunder','Healing','Healing (Temp)']"></Dropdown>
<Input></Input>
</FormElement>
</template> </template>
</Tabs> </Tabs>
</div> </div>
@ -245,7 +262,7 @@ h2.section {
width: 100%; width: 100%;
.description-sidebar { .description-sidebar {
min-width: 200px; max-width: 200px;
} }
.description { .description {
@ -285,10 +302,6 @@ h2.section {
width: 100%; width: 100%;
} }
.grow {
flex-grow: 1;
}
.window-wrapper { .window-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
@ -296,15 +309,4 @@ h2.section {
user-select: none; user-select: none;
} }
.subsection {
height: 32px;
display: flex;
align-items: center;
justify-content: center;
&:first-child {
border-right: 1px solid var(--color-border);
}
}
</style> </style>

View File

@ -1,3 +1,14 @@
{ {
"test": "Test" "database": {
"title": "Database",
"tabs": {
"weapons": "Weapons",
"equipment": "Equipment",
"consumables": "Consumables",
"containers": "Containers",
"tools": "Tools",
"spells": "Spells",
"features": "Features"
}
}
} }