Hunted the last of the \t's to extinction

This commit is contained in:
Stefan Zermatten
2022-10-09 16:56:28 +02:00
parent 2fa913b09a
commit aa8f2d230d
19 changed files with 1044 additions and 1024 deletions

View File

@@ -4,33 +4,33 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let CreatureFolders = new Mongo.Collection('creatureFolders');
let creatureFolderSchema = new SimpleSchema({
name: {
type: String,
trim: false,
optional: true,
max: STORAGE_LIMITS.name,
},
creatures: {
type: Array,
defaultValue: [],
},
'creatures.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
archived: {
type: Boolean,
optional: true,
},
order: {
type: Number,
defaultValue: 0,
},
name: {
type: String,
trim: false,
optional: true,
max: STORAGE_LIMITS.name,
},
creatures: {
type: Array,
defaultValue: [],
},
'creatures.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
archived: {
type: Boolean,
optional: true,
},
order: {
type: Number,
defaultValue: 0,
},
});
CreatureFolders.attachSchema(creatureFolderSchema);

View File

@@ -2,49 +2,49 @@ import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let ExperienceSchema = new SimpleSchema({
title: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// Potentially long description of the event
description: {
type: String,
optional: true,
max: STORAGE_LIMITS.description,
},
// The real-world date that it occured
date: {
type: Date,
autoValue: function () {
// If the date isn't set, set it to now
if (!this.isSet) {
return new Date();
}
},
},
// The date in-world of this event
worldDate: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// Tags to better find this entry later
tags: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
// ID of the journal this entry belongs to
journalId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
}
title: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// Potentially long description of the event
description: {
type: String,
optional: true,
max: STORAGE_LIMITS.description,
},
// The real-world date that it occured
date: {
type: Date,
autoValue: function () {
// If the date isn't set, set it to now
if (!this.isSet) {
return new Date();
}
},
},
// The date in-world of this event
worldDate: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// Tags to better find this entry later
tags: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
// ID of the journal this entry belongs to
journalId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
}
});
export { ExperienceSchema };

View File

@@ -20,15 +20,15 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let Libraries = new Mongo.Collection('libraries');
let LibrarySchema = new SimpleSchema({
name: {
type: String,
max: STORAGE_LIMITS.name,
},
description: {
type: String,
optional: true,
max: STORAGE_LIMITS.summary,
},
name: {
type: String,
max: STORAGE_LIMITS.name,
},
description: {
type: String,
optional: true,
max: STORAGE_LIMITS.summary,
},
});
LibrarySchema.extend(SharingSchema);
@@ -38,96 +38,96 @@ Libraries.attachSchema(LibrarySchema);
export default Libraries;
const insertLibrary = new ValidatedMethod({
name: 'libraries.insert',
mixins: [
simpleSchemaMixin,
],
schema: LibrarySchema.omit('owner'),
run(library) {
if (!this.userId) {
throw new Meteor.Error('Libraries.methods.insert.denied',
'You need to be logged in to insert a library');
}
let tier = getUserTier(this.userId);
if (!tier.paidBenefits) {
throw new Meteor.Error('Libraries.methods.insert.denied',
`The ${tier.name} tier does not allow you to insert a library`);
}
library.owner = this.userId;
return Libraries.insert(library);
},
name: 'libraries.insert',
mixins: [
simpleSchemaMixin,
],
schema: LibrarySchema.omit('owner'),
run(library) {
if (!this.userId) {
throw new Meteor.Error('Libraries.methods.insert.denied',
'You need to be logged in to insert a library');
}
let tier = getUserTier(this.userId);
if (!tier.paidBenefits) {
throw new Meteor.Error('Libraries.methods.insert.denied',
`The ${tier.name} tier does not allow you to insert a library`);
}
library.owner = this.userId;
return Libraries.insert(library);
},
});
const updateLibraryName = new ValidatedMethod({
name: 'libraries.updateName',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
name: {
type: String,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id, name }) {
let library = Libraries.findOne(_id);
assertEditPermission(library, this.userId);
Libraries.update(_id, { $set: { name } });
},
name: 'libraries.updateName',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
name: {
type: String,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id, name }) {
let library = Libraries.findOne(_id);
assertEditPermission(library, this.userId);
Libraries.update(_id, { $set: { name } });
},
});
const updateLibraryDescription = new ValidatedMethod({
name: 'libraries.updateDescription',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
description: {
type: String,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id, description }) {
let library = Libraries.findOne(_id);
assertEditPermission(library, this.userId);
Libraries.update(_id, { $set: { description } });
},
name: 'libraries.updateDescription',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
description: {
type: String,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id, description }) {
let library = Libraries.findOne(_id);
assertEditPermission(library, this.userId);
Libraries.update(_id, { $set: { description } });
},
});
const removeLibrary = new ValidatedMethod({
name: 'libraries.remove',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id }) {
let library = Libraries.findOne(_id);
assertOwnership(library, this.userId);
this.unblock();
removeLibaryWork(_id)
}
name: 'libraries.remove',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.id
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ _id }) {
let library = Libraries.findOne(_id);
assertOwnership(library, this.userId);
this.unblock();
removeLibaryWork(_id)
}
});
export function removeLibaryWork(libraryId) {
Libraries.remove(libraryId);
LibraryNodes.remove({ 'ancestors.id': libraryId });
Libraries.remove(libraryId);
LibraryNodes.remove({ 'ancestors.id': libraryId });
}
export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, removeLibrary };

View File

@@ -3,79 +3,79 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
let BuffSchema = createPropertySchema({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
hideRemoveButton: {
type: Boolean,
optional: true,
},
// How many rounds this buff lasts
duration: {
type: 'fieldToCompute',
optional: true,
},
target: {
type: String,
allowedValues: [
'self',
'target',
],
defaultValue: 'target',
},
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
// Prevent the children from being crystalized
skipCrystalization: {
type: Boolean,
optional: true,
},
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
hideRemoveButton: {
type: Boolean,
optional: true,
},
// How many rounds this buff lasts
duration: {
type: 'fieldToCompute',
optional: true,
},
target: {
type: String,
allowedValues: [
'self',
'target',
],
defaultValue: 'target',
},
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
// Prevent the children from being crystalized
skipCrystalization: {
type: Boolean,
optional: true,
},
});
let ComputedOnlyBuffSchema = createPropertySchema({
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,
max: STORAGE_LIMITS.description,
},
duration: {
type: 'computedOnlyField',
optional: true,
},
durationSpent: {
type: Number,
optional: true,
min: 0,
},
appliedBy: {
type: Object,
optional: true,
},
'appliedBy.name': {
type: String,
max: STORAGE_LIMITS.name,
},
'appliedBy.id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
'appliedBy.collection': {
type: String,
max: STORAGE_LIMITS.collectionName,
},
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,
max: STORAGE_LIMITS.description,
},
duration: {
type: 'computedOnlyField',
optional: true,
},
durationSpent: {
type: Number,
optional: true,
min: 0,
},
appliedBy: {
type: Object,
optional: true,
},
'appliedBy.name': {
type: String,
max: STORAGE_LIMITS.name,
},
'appliedBy.id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
'appliedBy.collection': {
type: String,
max: STORAGE_LIMITS.collectionName,
},
});
const ComputedBuffSchema = new SimpleSchema()
.extend(BuffSchema)
.extend(ComputedOnlyBuffSchema);
.extend(BuffSchema)
.extend(ComputedOnlyBuffSchema);
export { BuffSchema, ComputedOnlyBuffSchema, ComputedBuffSchema };

View File

@@ -3,69 +3,69 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
let ContainerSchema = createPropertySchema({
name: {
type: String,
optional: true,
trim: false,
max: STORAGE_LIMITS.name,
},
carried: {
type: Boolean,
defaultValue: true,
optional: true,
},
contentsWeightless: {
type: Boolean,
optional: true,
},
weight: {
type: Number,
min: 0,
optional: true,
},
value: {
type: Number,
min: 0,
optional: true,
},
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
name: {
type: String,
optional: true,
trim: false,
max: STORAGE_LIMITS.name,
},
carried: {
type: Boolean,
defaultValue: true,
optional: true,
},
contentsWeightless: {
type: Boolean,
optional: true,
},
weight: {
type: Number,
min: 0,
optional: true,
},
value: {
type: Number,
min: 0,
optional: true,
},
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
});
const ComputedOnlyContainerSchema = createPropertySchema({
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,
},
// Weight of all the contents, zero if `contentsWeightless` is true
contentsWeight: {
type: Number,
optional: true,
removeBeforeCompute: true,
},
// Weight of all the carried contents (some sub-containers might not be carried)
// zero if `contentsWeightless` is true
carriedWeight: {
type: Number,
optional: true,
removeBeforeCompute: true,
},
contentsValue: {
type: Number,
optional: true,
removeBeforeCompute: true,
},
carriedValue: {
type: Number,
optional: true,
removeBeforeCompute: true,
},
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,
},
// Weight of all the contents, zero if `contentsWeightless` is true
contentsWeight: {
type: Number,
optional: true,
removeBeforeCompute: true,
},
// Weight of all the carried contents (some sub-containers might not be carried)
// zero if `contentsWeightless` is true
carriedWeight: {
type: Number,
optional: true,
removeBeforeCompute: true,
},
contentsValue: {
type: Number,
optional: true,
removeBeforeCompute: true,
},
carriedValue: {
type: Number,
optional: true,
removeBeforeCompute: true,
},
});
const ComputedContainerSchema = new SimpleSchema()
.extend(ComputedOnlyContainerSchema)
.extend(ContainerSchema);
.extend(ComputedOnlyContainerSchema)
.extend(ContainerSchema);
export { ContainerSchema, ComputedOnlyContainerSchema, ComputedContainerSchema };

View File

@@ -3,69 +3,69 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
const ItemSchema = createPropertySchema({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// Plural name of the item, if there is more than one
plural: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
// Number currently held
quantity: {
type: SimpleSchema.Integer,
min: 0,
defaultValue: 1
},
// Weight per item in the stack
weight: {
type: Number,
min: 0,
optional: true,
},
// Value per item in the stack, in gold pieces
value: {
type: Number,
min: 0,
optional: true,
},
// If this item is equipped, it requires attunement
requiresAttunement: {
type: Boolean,
optional: true,
},
attuned: {
type: Boolean,
optional: true,
},
// Show increment/decrement buttons in item lists
showIncrement: {
type: Boolean,
optional: true,
},
// Unequipped items shouldn't affect creature stats
equipped: {
type: Boolean,
defaultValue: false,
},
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// Plural name of the item, if there is more than one
plural: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
description: {
type: 'inlineCalculationFieldToCompute',
optional: true,
},
// Number currently held
quantity: {
type: SimpleSchema.Integer,
min: 0,
defaultValue: 1
},
// Weight per item in the stack
weight: {
type: Number,
min: 0,
optional: true,
},
// Value per item in the stack, in gold pieces
value: {
type: Number,
min: 0,
optional: true,
},
// If this item is equipped, it requires attunement
requiresAttunement: {
type: Boolean,
optional: true,
},
attuned: {
type: Boolean,
optional: true,
},
// Show increment/decrement buttons in item lists
showIncrement: {
type: Boolean,
optional: true,
},
// Unequipped items shouldn't affect creature stats
equipped: {
type: Boolean,
defaultValue: false,
},
});
let ComputedOnlyItemSchema = createPropertySchema({
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,
},
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,
},
});
const ComputedItemSchema = new SimpleSchema()
.extend(ItemSchema)
.extend(ComputedOnlyItemSchema);
.extend(ItemSchema)
.extend(ComputedOnlyItemSchema);
export { ItemSchema, ComputedItemSchema, ComputedOnlyItemSchema };

View File

@@ -3,99 +3,99 @@ import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
const magicSchools = [
'abjuration',
'conjuration',
'divination',
'enchantment',
'evocation',
'illusion',
'necromancy',
'transmutation',
'abjuration',
'conjuration',
'divination',
'enchantment',
'evocation',
'illusion',
'necromancy',
'transmutation',
];
let SpellSchema = new SimpleSchema({})
.extend(ActionSchema)
.extend({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// If it's always prepared, it doesn't count against the number of spells
// prepared in a spell list, and enabled should be true
alwaysPrepared: {
type: Boolean,
optional: true,
},
prepared: {
type: Boolean,
optional: true,
},
// This spell ignores spell slot rules
castWithoutSpellSlots: {
type: Boolean,
optional: true,
},
hasAttackRoll: {
type: Boolean,
optional: true,
},
castingTime: {
type: String,
optional: true,
defaultValue: 'action',
max: STORAGE_LIMITS.spellDetail,
},
range: {
type: String,
optional: true,
max: STORAGE_LIMITS.spellDetail,
},
duration: {
type: String,
optional: true,
defaultValue: 'Instantaneous',
max: STORAGE_LIMITS.spellDetail,
},
verbal: {
type: Boolean,
optional: true,
},
somatic: {
type: Boolean,
optional: true,
},
concentration: {
type: Boolean,
optional: true,
},
material: {
type: String,
optional: true,
max: STORAGE_LIMITS.spellDetail,
},
ritual: {
type: Boolean,
optional: true,
},
level: {
type: SimpleSchema.Integer,
defaultValue: 1,
max: 9,
min: 0,
},
school: {
type: String,
defaultValue: 'abjuration',
allowedValues: magicSchools,
},
});
.extend(ActionSchema)
.extend({
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
// If it's always prepared, it doesn't count against the number of spells
// prepared in a spell list, and enabled should be true
alwaysPrepared: {
type: Boolean,
optional: true,
},
prepared: {
type: Boolean,
optional: true,
},
// This spell ignores spell slot rules
castWithoutSpellSlots: {
type: Boolean,
optional: true,
},
hasAttackRoll: {
type: Boolean,
optional: true,
},
castingTime: {
type: String,
optional: true,
defaultValue: 'action',
max: STORAGE_LIMITS.spellDetail,
},
range: {
type: String,
optional: true,
max: STORAGE_LIMITS.spellDetail,
},
duration: {
type: String,
optional: true,
defaultValue: 'Instantaneous',
max: STORAGE_LIMITS.spellDetail,
},
verbal: {
type: Boolean,
optional: true,
},
somatic: {
type: Boolean,
optional: true,
},
concentration: {
type: Boolean,
optional: true,
},
material: {
type: String,
optional: true,
max: STORAGE_LIMITS.spellDetail,
},
ritual: {
type: Boolean,
optional: true,
},
level: {
type: SimpleSchema.Integer,
defaultValue: 1,
max: 9,
min: 0,
},
school: {
type: String,
defaultValue: 'abjuration',
allowedValues: magicSchools,
},
});
const ComputedOnlySpellSchema = new SimpleSchema()
.extend(ComputedOnlyActionSchema);
.extend(ComputedOnlyActionSchema);
const ComputedSpellSchema = new SimpleSchema()
.extend(SpellSchema)
.extend(ComputedOnlySpellSchema);
.extend(SpellSchema)
.extend(ComputedOnlySpellSchema);
export { SpellSchema, ComputedOnlySpellSchema, ComputedSpellSchema };

View File

@@ -1,12 +1,12 @@
import SimpleSchema from 'simpl-schema';
const ColorSchema = new SimpleSchema({
color: {
type: String,
// match hex colors of the form #A23 or #A23f56
regEx: /^#([a-f0-9]{3}){1,2}\b$/i,
optional: true,
},
color: {
type: String,
// match hex colors of the form #A23 or #A23f56
regEx: /^#([a-f0-9]{3}){1,2}\b$/i,
optional: true,
},
});
export default ColorSchema;

View File

@@ -1,26 +1,26 @@
import SimpleSchema from 'simpl-schema';
const DeathSavesSchema = new SimpleSchema({
pass: {
type: SimpleSchema.Integer,
min: 0,
max: 3,
defaultValue: 0,
},
fail: {
type: SimpleSchema.Integer,
min: 0,
max: 3,
defaultValue: 0,
},
canDeathSave: {
type: Boolean,
defaultValue: true,
},
stable: {
type: Boolean,
defaultValue: false,
},
pass: {
type: SimpleSchema.Integer,
min: 0,
max: 3,
defaultValue: 0,
},
fail: {
type: SimpleSchema.Integer,
min: 0,
max: 3,
defaultValue: 0,
},
canDeathSave: {
type: Boolean,
defaultValue: true,
},
stable: {
type: Boolean,
defaultValue: false,
},
});
export default DeathSavesSchema;

View File

@@ -3,36 +3,36 @@ import '/imports/api/sharing/sharing.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
let SharingSchema = new SimpleSchema({
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1
},
readers: {
type: Array,
defaultValue: [],
index: 1,
maxCount: STORAGE_LIMITS.readersCount,
},
'readers.$': {
type: String,
regEx: SimpleSchema.RegEx.Id
},
writers: {
type: Array,
defaultValue: [],
index: 1,
maxCount: STORAGE_LIMITS.writersCount,
},
'writers.$': {
type: String,
regEx: SimpleSchema.RegEx.Id
},
public: {
type: Boolean,
defaultValue: false,
index: 1,
},
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1
},
readers: {
type: Array,
defaultValue: [],
index: 1,
maxCount: STORAGE_LIMITS.readersCount,
},
'readers.$': {
type: String,
regEx: SimpleSchema.RegEx.Id
},
writers: {
type: Array,
defaultValue: [],
index: 1,
maxCount: STORAGE_LIMITS.writersCount,
},
'writers.$': {
type: String,
regEx: SimpleSchema.RegEx.Id
},
public: {
type: Boolean,
defaultValue: false,
index: 1,
},
});
export default SharingSchema;

View File

@@ -11,321 +11,321 @@ const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LI
const defaultLibraryCollections = process.env.DEFAULT_LIBRARY_COLLECTIONS && process.env.DEFAULT_LIBRARY_COLLECTIONS.split(',') || [];
const userSchema = new SimpleSchema({
username: {
type: String,
optional: true,
max: 30,
min: 4,
},
emails: {
type: Array,
optional: true,
},
'emails.$': {
type: Object,
},
'emails.$.address': {
type: String,
regEx: SimpleSchema.RegEx.Email,
},
'emails.$.verified': {
type: Boolean,
},
registered_emails: {
type: Array,
optional: true,
},
'registered_emails.$': {
type: Object,
blackbox: true,
},
createdAt: {
type: Date
},
services: {
type: Object,
optional: true,
blackbox: true,
},
roles: {
type: Array,
optional: true,
},
'roles.$': {
type: String
},
// In order to avoid an 'Exception in setInterval callback' from Meteor
heartbeat: {
type: Date,
optional: true,
},
apiKey: {
type: String,
index: 1,
optional: true,
},
darkMode: {
type: Boolean,
optional: true,
},
subscribedLibraries: {
type: Array,
defaultValue: defaultLibraries,
maxCount: 100,
},
'subscribedLibraries.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
subscribedLibraryCollections: {
type: Array,
defaultValue: defaultLibraryCollections,
maxCount: 100,
},
'subscribedLibraryCollections.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
subscribedCharacters: {
type: Array,
defaultValue: [],
max: 100,
},
'subscribedCharacters.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
fileStorageUsed: {
type: Number,
optional: true,
},
profile: {
type: Object,
blackbox: true,
optional: true,
},
preferences: {
type: Object,
optional: true,
defaultValue: {},
},
'preferences.swapAbilityScoresAndModifiers': {
type: Boolean,
optional: true,
},
'preferences.hidePropertySelectDialogHelp': {
type: Boolean,
optional: true,
},
username: {
type: String,
optional: true,
max: 30,
min: 4,
},
emails: {
type: Array,
optional: true,
},
'emails.$': {
type: Object,
},
'emails.$.address': {
type: String,
regEx: SimpleSchema.RegEx.Email,
},
'emails.$.verified': {
type: Boolean,
},
registered_emails: {
type: Array,
optional: true,
},
'registered_emails.$': {
type: Object,
blackbox: true,
},
createdAt: {
type: Date
},
services: {
type: Object,
optional: true,
blackbox: true,
},
roles: {
type: Array,
optional: true,
},
'roles.$': {
type: String
},
// In order to avoid an 'Exception in setInterval callback' from Meteor
heartbeat: {
type: Date,
optional: true,
},
apiKey: {
type: String,
index: 1,
optional: true,
},
darkMode: {
type: Boolean,
optional: true,
},
subscribedLibraries: {
type: Array,
defaultValue: defaultLibraries,
maxCount: 100,
},
'subscribedLibraries.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
subscribedLibraryCollections: {
type: Array,
defaultValue: defaultLibraryCollections,
maxCount: 100,
},
'subscribedLibraryCollections.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
subscribedCharacters: {
type: Array,
defaultValue: [],
max: 100,
},
'subscribedCharacters.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
fileStorageUsed: {
type: Number,
optional: true,
},
profile: {
type: Object,
blackbox: true,
optional: true,
},
preferences: {
type: Object,
optional: true,
defaultValue: {},
},
'preferences.swapAbilityScoresAndModifiers': {
type: Boolean,
optional: true,
},
'preferences.hidePropertySelectDialogHelp': {
type: Boolean,
optional: true,
},
});
Meteor.users.attachSchema(userSchema);
Meteor.users.generateApiKey = new ValidatedMethod({
name: 'users.generateApiKey',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run() {
if (Meteor.isClient) return;
var user = Meteor.users.findOne(this.userId);
if (!user) return;
if (user && user.apiKey) return;
var apiKey = Random.id(30);
Meteor.users.update(this.userId, { $set: { apiKey } });
},
name: 'users.generateApiKey',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run() {
if (Meteor.isClient) return;
var user = Meteor.users.findOne(this.userId);
if (!user) return;
if (user && user.apiKey) return;
var apiKey = Random.id(30);
Meteor.users.update(this.userId, { $set: { apiKey } });
},
});
Meteor.users.setDarkMode = new ValidatedMethod({
name: 'users.setDarkMode',
validate: new SimpleSchema({
darkMode: { type: Boolean },
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ darkMode }) {
if (!this.userId) return;
Meteor.users.update(this.userId, { $set: { darkMode } });
},
name: 'users.setDarkMode',
validate: new SimpleSchema({
darkMode: { type: Boolean },
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ darkMode }) {
if (!this.userId) return;
Meteor.users.update(this.userId, { $set: { darkMode } });
},
});
Meteor.users.sendVerificationEmail = new ValidatedMethod({
name: 'users.sendVerificationEmail',
validate: new SimpleSchema({
userId: {
type: String,
optional: true,
},
address: {
type: String,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ userId, address }) {
userId = this.userId || userId;
let user = Meteor.users.findOne(userId);
if (!user) {
throw new Meteor.Error('User not found',
'Can\'t send a validation email to a user that does not exist');
}
if (!some(user.emails, email => email.address === address)) {
throw new Meteor.Error('Email address not found',
'The specified email address wasn\'t found on this user account');
}
Accounts.sendVerificationEmail(userId, address);
}
name: 'users.sendVerificationEmail',
validate: new SimpleSchema({
userId: {
type: String,
optional: true,
},
address: {
type: String,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ userId, address }) {
userId = this.userId || userId;
let user = Meteor.users.findOne(userId);
if (!user) {
throw new Meteor.Error('User not found',
'Can\'t send a validation email to a user that does not exist');
}
if (!some(user.emails, email => email.address === address)) {
throw new Meteor.Error('Email address not found',
'The specified email address wasn\'t found on this user account');
}
Accounts.sendVerificationEmail(userId, address);
}
});
Meteor.users.canPickUsername = new ValidatedMethod({
name: 'users.canPickUsername',
validate: userSchema.pick('username').validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ username }) {
if (Meteor.isClient) return;
let user = Accounts.findUserByUsername(username);
// You can pick your own username
if (user && user._id === this.userId) {
return false;
}
return !!user;
}
name: 'users.canPickUsername',
validate: userSchema.pick('username').validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ username }) {
if (Meteor.isClient) return;
let user = Accounts.findUserByUsername(username);
// You can pick your own username
if (user && user._id === this.userId) {
return false;
}
return !!user;
}
});
Meteor.users.setUsername = new ValidatedMethod({
name: 'users.setUsername',
validate: userSchema.pick('username').validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ username }) {
if (!this.userId) throw 'Can only set your username if logged in';
if (Meteor.isClient) return;
return Accounts.setUsername(this.userId, username)
}
name: 'users.setUsername',
validate: userSchema.pick('username').validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ username }) {
if (!this.userId) throw 'Can only set your username if logged in';
if (Meteor.isClient) return;
return Accounts.setUsername(this.userId, username)
}
});
Meteor.users.setPreference = new ValidatedMethod({
name: 'users.setPreference',
validate: new SimpleSchema({
preference: {
type: String,
},
value: {
type: SimpleSchema.oneOf(Boolean),
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ preference, value }) {
if (!this.userId) throw 'You can only set preferences once logged in';
let prefPath = `preferences.${preference}`
if (value == true) {
return Meteor.users.update(this.userId, {
$set: { [prefPath]: true },
});
} else {
return Meteor.users.update(this.userId, {
$unset: { [prefPath]: 1 },
});
}
},
name: 'users.setPreference',
validate: new SimpleSchema({
preference: {
type: String,
},
value: {
type: SimpleSchema.oneOf(Boolean),
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ preference, value }) {
if (!this.userId) throw 'You can only set preferences once logged in';
let prefPath = `preferences.${preference}`
if (value == true) {
return Meteor.users.update(this.userId, {
$set: { [prefPath]: true },
});
} else {
return Meteor.users.update(this.userId, {
$unset: { [prefPath]: 1 },
});
}
},
});
Meteor.users.subscribeToLibrary = new ValidatedMethod({
name: 'users.subscribeToLibrary',
validate: new SimpleSchema({
libraryId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
subscribe: {
type: Boolean,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ libraryId, subscribe }) {
if (!this.userId) throw 'Can only subscribe if logged in';
if (subscribe) {
return Meteor.users.update(this.userId, {
$addToSet: { subscribedLibraries: libraryId },
});
} else {
return Meteor.users.update(this.userId, {
$pullAll: { subscribedLibraries: libraryId },
});
}
}
name: 'users.subscribeToLibrary',
validate: new SimpleSchema({
libraryId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
subscribe: {
type: Boolean,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ libraryId, subscribe }) {
if (!this.userId) throw 'Can only subscribe if logged in';
if (subscribe) {
return Meteor.users.update(this.userId, {
$addToSet: { subscribedLibraries: libraryId },
});
} else {
return Meteor.users.update(this.userId, {
$pullAll: { subscribedLibraries: libraryId },
});
}
}
});
Meteor.users.subscribeToLibraryCollection = new ValidatedMethod({
name: 'users.subscribeToLibraryCollection',
validate: new SimpleSchema({
libraryCollectionId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
subscribe: {
type: Boolean,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ libraryCollectionId, subscribe }) {
if (!this.userId) throw 'Can only subscribe if logged in';
if (subscribe) {
return Meteor.users.update(this.userId, {
$addToSet: { subscribedLibraryCollections: libraryCollectionId },
});
} else {
return Meteor.users.update(this.userId, {
$pullAll: { subscribedLibraryCollections: libraryCollectionId },
});
}
}
name: 'users.subscribeToLibraryCollection',
validate: new SimpleSchema({
libraryCollectionId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
subscribe: {
type: Boolean,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ libraryCollectionId, subscribe }) {
if (!this.userId) throw 'Can only subscribe if logged in';
if (subscribe) {
return Meteor.users.update(this.userId, {
$addToSet: { subscribedLibraryCollections: libraryCollectionId },
});
} else {
return Meteor.users.update(this.userId, {
$pullAll: { subscribedLibraryCollections: libraryCollectionId },
});
}
}
});
Meteor.users.findUserByUsernameOrEmail = new ValidatedMethod({
name: 'users.findUserByUsernameOrEmail',
validate: new SimpleSchema({
usernameOrEmail: {
type: String,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ usernameOrEmail }) {
if (Meteor.isClient) return;
let user = Accounts.findUserByUsername(usernameOrEmail) ||
Accounts.findUserByEmail(usernameOrEmail);
return user && user._id;
}
name: 'users.findUserByUsernameOrEmail',
validate: new SimpleSchema({
usernameOrEmail: {
type: String,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ usernameOrEmail }) {
if (Meteor.isClient) return;
let user = Accounts.findUserByUsername(usernameOrEmail) ||
Accounts.findUserByEmail(usernameOrEmail);
return user && user._id;
}
});

View File

@@ -4,7 +4,7 @@ function id(x) { return x[0]; }
import node from './parseTree/_index.js';
import moo from 'moo';
import moo from 'moo';
const lexer = moo.compile({
number: /[0-9]+(?:\.[0-9]+)?/,

View File

@@ -4,47 +4,47 @@ import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js';
import { SyncedCron } from 'meteor/littledata:synced-cron';
Meteor.startup(() => {
const collections = [
CreatureProperties,
LibraryNodes,
];
const collections = [
CreatureProperties,
LibraryNodes,
];
/**
* Deletes all soft removed documents that were removed more than 1 day ago
* and were not restored
* @return {Number} Number of documents removed
*/
const deleteOldSoftRemovedDocs = function () {
const now = new Date();
const yesterday = new Date(now.getTime() - (24 * 60 * 60 * 1000));
collections.forEach(collection => {
collection.remove({
removed: true,
removedAt: { $lt: yesterday } // dates *before* yesterday
}, function (error) {
if (error) {
console.error(JSON.stringify(error, null, 2));
}
});
});
};
/**
* Deletes all soft removed documents that were removed more than 1 day ago
* and were not restored
* @return {Number} Number of documents removed
*/
const deleteOldSoftRemovedDocs = function () {
const now = new Date();
const yesterday = new Date(now.getTime() - (24 * 60 * 60 * 1000));
collections.forEach(collection => {
collection.remove({
removed: true,
removedAt: { $lt: yesterday } // dates *before* yesterday
}, function (error) {
if (error) {
console.error(JSON.stringify(error, null, 2));
}
});
});
};
SyncedCron.add({
name: 'deleteSoftRemovedDocs',
schedule: function (parser) {
return parser.text('every 10 minutes');
},
job: deleteOldSoftRemovedDocs,
});
SyncedCron.add({
name: 'deleteSoftRemovedDocs',
schedule: function (parser) {
return parser.text('every 10 minutes');
},
job: deleteOldSoftRemovedDocs,
});
SyncedCron.start();
SyncedCron.start();
// Add a method to manually trigger removal
Meteor.methods({
deleteOldSoftRemovedDocs() {
assertAdmin(this.userId);
this.unblock();
deleteOldSoftRemovedDocs();
},
});
// Add a method to manually trigger removal
Meteor.methods({
deleteOldSoftRemovedDocs() {
assertAdmin(this.userId);
this.unblock();
deleteOldSoftRemovedDocs();
},
});
});

View File

@@ -12,46 +12,46 @@
<script lang="js">
export default {
props: {
wideColumns: Boolean,
},
props: {
wideColumns: Boolean,
},
};
</script>
<style lang="css">
.column-layout {
column-count: 12;
column-fill: balance;
column-gap: 0;
column-width: 240px;
transform: translateZ(0);
padding: 4px;
column-count: 12;
column-fill: balance;
column-gap: 0;
column-width: 240px;
transform: translateZ(0);
padding: 4px;
}
.column-layout.wide-columns {
column-count: 12;
column-fill: balance;
column-gap: 0;
column-width: 320px;
transform: translateZ(0);
padding: 4px;
column-count: 12;
column-fill: balance;
column-gap: 0;
column-width: 320px;
transform: translateZ(0);
padding: 4px;
}
.column-layout>div,
.column-layout>span>div {
/*
/*
Table and width set because firefox does not support break-inside: avoid
*/
display: table;
table-layout: fixed;
width: 100%;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
transform: translateX(0);
-webkit-transform: translateX(0);
-webkit-column-break-inside: avoid;
page-break-inside: avoid;
break-inside: avoid;
padding: 4px;
display: table;
table-layout: fixed;
width: 100%;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
transform: translateX(0);
-webkit-transform: translateX(0);
-webkit-column-break-inside: avoid;
page-break-inside: avoid;
break-inside: avoid;
padding: 4px;
}
</style>

View File

@@ -19,72 +19,72 @@ import TreeNodeList from '/imports/ui/components/tree/TreeNodeList.vue';
import { organizeDoc, reorderDoc } from '/imports/api/parenting/organizeMethods.js';
export default {
components: {
TreeNodeList,
},
props: {
root: {
type: Object,
default: undefined,
},
organize: Boolean,
selectedNode: {
type: Object,
default: undefined,
},
filter: {
type: Object,
default: undefined,
},
group: {
type: String,
default: 'creatureProperties'
},
expanded: Boolean,
},
meteor: {
children() {
const children = nodesToTree({
collection: CreatureProperties,
ancestorId: this.root.id,
filter: this.filter,
includeFilteredDocAncestors: true,
includeFilteredDocDescendants: true,
});
this.$emit('length', children.length);
return children;
},
},
methods: {
reordered({ doc, newIndex }) {
reorderDoc.call({
docRef: {
id: doc._id,
collection: 'creatureProperties',
},
order: newIndex,
});
},
reorganized({ doc, parent, newIndex }) {
let parentRef;
if (parent) {
parentRef = {
id: parent._id,
collection: 'creatureProperties',
};
} else {
parentRef = this.root;
}
organizeDoc.call({
docRef: {
id: doc._id,
collection: 'creatureProperties',
},
parentRef,
order: newIndex,
});
},
},
components: {
TreeNodeList,
},
props: {
root: {
type: Object,
default: undefined,
},
organize: Boolean,
selectedNode: {
type: Object,
default: undefined,
},
filter: {
type: Object,
default: undefined,
},
group: {
type: String,
default: 'creatureProperties'
},
expanded: Boolean,
},
meteor: {
children() {
const children = nodesToTree({
collection: CreatureProperties,
ancestorId: this.root.id,
filter: this.filter,
includeFilteredDocAncestors: true,
includeFilteredDocDescendants: true,
});
this.$emit('length', children.length);
return children;
},
},
methods: {
reordered({ doc, newIndex }) {
reorderDoc.call({
docRef: {
id: doc._id,
collection: 'creatureProperties',
},
order: newIndex,
});
},
reorganized({ doc, parent, newIndex }) {
let parentRef;
if (parent) {
parentRef = {
id: parent._id,
collection: 'creatureProperties',
};
} else {
parentRef = this.root;
}
organizeDoc.call({
docRef: {
id: doc._id,
collection: 'creatureProperties',
},
parentRef,
order: newIndex,
});
},
},
};
</script>

View File

@@ -2,65 +2,65 @@ import { parse, stringify } from 'css-box-shadow';
// Only supports border radius defined like "20px" or "100%"
const transformedRadius = (radiusString, deltaWidth, deltaHeight) => {
if (/^\d+\.?\d*px$/.test(radiusString)) {
//The radius is defined in pixel units, so get the radius as a number
const rad = +radiusString.match(/\d+\.?\d*/)[0];
// Set the x and y radius of the "to" element, compensating for scale
return `${rad / deltaWidth}px / ${rad / deltaHeight}px`;
} else if (/^\d+\.?\d*%$/.test(radiusString)) {
//The radius is defined as a percentage, so just use it as is
return radiusString;
}
if (/^\d+\.?\d*px$/.test(radiusString)) {
//The radius is defined in pixel units, so get the radius as a number
const rad = +radiusString.match(/\d+\.?\d*/)[0];
// Set the x and y radius of the "to" element, compensating for scale
return `${rad / deltaWidth}px / ${rad / deltaHeight}px`;
} else if (/^\d+\.?\d*%$/.test(radiusString)) {
//The radius is defined as a percentage, so just use it as is
return radiusString;
}
};
const transformedBoxShadow = (shadowString, deltaWidth, deltaHeight) => {
if (shadowString === 'none') return shadowString;
if (shadowString[0] === 'r') {
let strings = shadowString.match(/rgba\([^)]+\)[^,]+/g);
strings = strings.map(string => {
// Move color to end
let m = string.match(/(rgba\([^)]+\))([^,]+)/);
return `${m[2].trim()} ${m[1]}`;
});
shadowString = strings.join(', ');
}
let scaleAverage = (deltaWidth + deltaHeight) / 2;
let shadows = parse(shadowString);
shadows.forEach(shadow => {
shadow.offsetX /= deltaWidth;
shadow.offsetY /= deltaHeight;
shadow.blurRadius /= scaleAverage;
shadow.spreadRadius /= scaleAverage;
})
return stringify(shadows);
if (shadowString === 'none') return shadowString;
if (shadowString[0] === 'r') {
let strings = shadowString.match(/rgba\([^)]+\)[^,]+/g);
strings = strings.map(string => {
// Move color to end
let m = string.match(/(rgba\([^)]+\))([^,]+)/);
return `${m[2].trim()} ${m[1]}`;
});
shadowString = strings.join(', ');
}
let scaleAverage = (deltaWidth + deltaHeight) / 2;
let shadows = parse(shadowString);
shadows.forEach(shadow => {
shadow.offsetX /= deltaWidth;
shadow.offsetY /= deltaHeight;
shadow.blurRadius /= scaleAverage;
shadow.spreadRadius /= scaleAverage;
})
return stringify(shadows);
}
export default function mockElement({ source, target, offset = { x: 0, y: 0 } }) {
if (!source || !target) throw `Can't mock without ${source ? 'target' : 'source'}`;
let sourceRect = source.getBoundingClientRect();
let targetRect = target.getBoundingClientRect();
if (!source || !target) throw `Can't mock without ${source ? 'target' : 'source'}`;
let sourceRect = source.getBoundingClientRect();
let targetRect = target.getBoundingClientRect();
// Get how must the target change to become the source
const deltaWidth = sourceRect.width / targetRect.width;
const deltaHeight = sourceRect.height / targetRect.height;
const deltaLeft = sourceRect.left - targetRect.left + offset.x;
const deltaTop = sourceRect.top - targetRect.top + offset.y;
// Mock the source
target.style.transform = `translate(${deltaLeft}px, ${deltaTop}px) ` +
`scale(${deltaWidth}, ${deltaHeight})`;
// Mock the background color unless it's completely transparent
let backgroundColor = getComputedStyle(source).backgroundColor
if (backgroundColor !== 'rgba(0, 0, 0, 0)') {
target.style.backgroundColor = backgroundColor;
}
// Edge might not combine all border radii into a single value,
// So we just sample the top left one if we need to
let oldRadius = getComputedStyle(source).borderRadius ||
getComputedStyle(source).borderTopLeftRadius;
let borderRadius = transformedRadius(oldRadius, deltaWidth, deltaHeight);
target.style.borderRadius = borderRadius;
let boxShadow = transformedBoxShadow(
getComputedStyle(source).boxShadow, deltaWidth, deltaHeight
);
target.style.setProperty('box-shadow', boxShadow, 'important');
// Get how must the target change to become the source
const deltaWidth = sourceRect.width / targetRect.width;
const deltaHeight = sourceRect.height / targetRect.height;
const deltaLeft = sourceRect.left - targetRect.left + offset.x;
const deltaTop = sourceRect.top - targetRect.top + offset.y;
// Mock the source
target.style.transform = `translate(${deltaLeft}px, ${deltaTop}px) ` +
`scale(${deltaWidth}, ${deltaHeight})`;
// Mock the background color unless it's completely transparent
let backgroundColor = getComputedStyle(source).backgroundColor
if (backgroundColor !== 'rgba(0, 0, 0, 0)') {
target.style.backgroundColor = backgroundColor;
}
// Edge might not combine all border radii into a single value,
// So we just sample the top left one if we need to
let oldRadius = getComputedStyle(source).borderRadius ||
getComputedStyle(source).borderTopLeftRadius;
let borderRadius = transformedRadius(oldRadius, deltaWidth, deltaHeight);
target.style.borderRadius = borderRadius;
let boxShadow = transformedBoxShadow(
getComputedStyle(source).boxShadow, deltaWidth, deltaHeight
);
target.style.setProperty('box-shadow', boxShadow, 'important');
}

View File

@@ -25,41 +25,41 @@ import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyV
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
export default {
mixins: [propertyViewerMixin],
computed: {
reset() {
let reset = this.model.reset
if (reset === 'shortRest') {
return `Reset${this.model.resetMultiplier && ' x' + this.model.resetMultiplier
} on a short rest`;
} else if (reset === 'longRest') {
return `Reset${this.model.resetMultiplier && ' x' + this.model.resetMultiplier
} on a long rest`;
} else {
return undefined;
}
}
},
methods: {
numberToSignedString,
}
mixins: [propertyViewerMixin],
computed: {
reset() {
let reset = this.model.reset
if (reset === 'shortRest') {
return `Reset${this.model.resetMultiplier && ' x' + this.model.resetMultiplier
} on a short rest`;
} else if (reset === 'longRest') {
return `Reset${this.model.resetMultiplier && ' x' + this.model.resetMultiplier
} on a long rest`;
} else {
return undefined;
}
}
},
methods: {
numberToSignedString,
}
}
</script>
<style lang="css" scoped>
.ability-value {
font-weight: 600;
font-size: 24px !important;
color: rgba(0, 0, 0, 0.54);
font-weight: 600;
font-size: 24px !important;
color: rgba(0, 0, 0, 0.54);
}
.mod,
.ability-value {
text-align: center;
width: 100%;
text-align: center;
width: 100%;
}
.attribute-value {
text-align: center;
text-align: center;
}
</style>

20
app/jsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Node",
"target": "ES2020",
"jsx": "react",
"strictNullChecks": true,
"strictFunctionTypes": true,
"baseUrl": ".",
"paths": {
"/*": [
"./*"
]
}
},
"exclude": [
"node_modules",
"**/node_modules/*"
]
}

View File

@@ -1,43 +1,43 @@
{
"name": "DiceCloud",
"icons": [
{
"src": "\/android-chrome-36x36.png?v=lk6WXp6Pmj",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-chrome-48x48.png?v=lk6WXp6Pmj",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-chrome-72x72.png?v=lk6WXp6Pmj",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-chrome-96x96.png?v=lk6WXp6Pmj",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-chrome-144x144.png?v=lk6WXp6Pmj",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-chrome-192x192.png?v=lk6WXp6Pmj",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
],
"start_url": "\/",
"display": "standalone"
"name": "DiceCloud",
"icons": [
{
"src": "\/android-chrome-36x36.png?v=lk6WXp6Pmj",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-chrome-48x48.png?v=lk6WXp6Pmj",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-chrome-72x72.png?v=lk6WXp6Pmj",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-chrome-96x96.png?v=lk6WXp6Pmj",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-chrome-144x144.png?v=lk6WXp6Pmj",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-chrome-192x192.png?v=lk6WXp6Pmj",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
],
"start_url": "\/",
"display": "standalone"
}