Merge pull request #131 from Dumbgenius/feature-custom-buffs

Feature - custom buffs
This commit is contained in:
Stefan Zermatten
2017-09-07 14:42:05 +02:00
committed by GitHub
42 changed files with 1041 additions and 50 deletions

View File

@@ -23,7 +23,7 @@ Schemas.Buff = new SimpleSchema({
type: {
type: String,
allowedValues: [
"inate",
"inate", //this should be "innate", but changing it could be problematic
"custom",
],
},
@@ -42,12 +42,26 @@ Schemas.Buff = new SimpleSchema({
allowedValues: _.pluck(colorOptions, "key"),
defaultValue: "q",
},
appliedBy: { //the charId of whoever applied the buff
type: String,
regEx: SimpleSchema.RegEx.Id,
},
appliedByDetails: {//the name and collection of the thing that applied the buff
type: Object,
optional: true,
},
"appliedByDetails.name": {
type: String,
},
"appliedByDetails.collection": {
type: String,
},
});
Buffs.attachSchema(Schemas.Buff);
Buffs.attachBehaviour("softRemovable");
makeParent(Buffs, ["name", "enabled"]); //parents of effects
makeParent(Buffs, ["name", "enabled"]); //parents of effects, attacks, proficiencies
Buffs.allow(CHARACTER_SUBSCHEMA_ALLOW);
Buffs.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -533,6 +533,7 @@ if (Meteor.isServer){
Attacks .remove({charId: character._id});
Buffs .remove({charId: character._id});
Classes .remove({charId: character._id});
CustomBuffs .remove({charId: character._id});
Effects .remove({charId: character._id});
Experiences .remove({charId: character._id});
Features .remove({charId: character._id});

View File

@@ -0,0 +1,42 @@
Conditions = new Mongo.Collection("conditions");
Schemas.Conditions = new SimpleSchema({
charId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
name: {
type: String,
optional: true,
trim: false,
},
description: {
type: String,
optional: true,
trim: false,
},
"lifeTime.total": {
type: Number,
defaultValue: 0, //0 is infinite
min: 0,
},
"lifeTime.spent": {
type: Number,
defaultValue: 0,
min: 0,
},
color: {
type: String,
allowedValues: _.pluck(colorOptions, "key"),
defaultValue: "q",
},
});
Conditions.attachSchema(Schemas.Conditions);
Conditions.attachBehaviour("softRemovable");
makeParent(Conditions, ["name"]); //parents of effects, attacks, proficiencies
Conditions.allow(CHARACTER_SUBSCHEMA_ALLOW);
Conditions.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -0,0 +1,53 @@
CustomBuffs = new Mongo.Collection("customBuffs");
Schemas.CustomBuff = new SimpleSchema({
charId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
name: {
type: String,
optional: true,
trim: false,
},
description: {
type: String,
optional: true,
trim: false,
},
target: {
type: String,
allowedValues: [
"self",
"others",
"both"
],
defaultValue: "self",
},
enabled: {
type: Boolean,
autoValue: function(){
return false;
//enabled is ALWAYS false on these, so that its children are also not enabled, so that the buff templates have no effects.
},
},
"lifeTime.total": {
type: Number,
defaultValue: 0, //0 is infinite
min: 0,
},
//the id of the feature, buff or item that creates this buff
parent: {
type: Schemas.Parent,
},
});
CustomBuffs.attachSchema(Schemas.CustomBuff);
CustomBuffs.attachBehaviour("softRemovable");
makeParent(CustomBuffs, ["name", "enabled"]); //parents of effects, attacks, proficiencies. Since this represents a template, "enabled" is always false.
makeChild(CustomBuffs); //children of lots of things
CustomBuffs.allow(CHARACTER_SUBSCHEMA_ALLOW);
CustomBuffs.deny(CHARACTER_SUBSCHEMA_DENY);

View File

@@ -18,6 +18,9 @@ openParentDialog = function({
} else if (parent.collection === "Spells") {
template = "spellDialog";
data = {spellId: parent.id};
} else if (parent.collection === "Buffs") {
template = "buffDialog";
data = {buffId: parent.id};
}
pushDialogStack({template, data, element, returnElement, callback});
};

View File

@@ -0,0 +1,33 @@
<!-- data is the CustomBuff -->
<template name="applyBuffDialog">
<div class="fit layout vertical applyBuffDialog">
<app-header fixed effects="waterfall">
<app-toolbar>
Apply Buff
</app-toolbar>
</app-header>
<div class="flex layout horizontal" style="height:100%">
<div class="flex" style="margin-right: 16px; height: 100%; max-width: 240px; overflow-y: auto;">
{{> characterPicker selfId=buff.charId includeSelf=canApplyToSelf writableOnly=true}}
</div>
<div class="flex buff-description" style="height: 100%; overflow-y: auto">
<hr style="margin: 16px 0 16px 0;">
{{#if buff.description}}
<div>{{#markdown}}{{evaluateString buff.charId buff.description}}{{/markdown}}</div>
<hr style="margin: 16px 0 16px 0;">
{{/if}}
{{> effectsViewList charId=buff.charId parentId=buff._id}}
{{> proficiencyViewList charId=buff.charId parentId=buff._id}}
{{> attacksViewList charId=buff.charId parentId=buff._id}}
</div>
</div>
<div class="buttons layout horizontal end-justified">
<paper-button id="cancelButton">
Cancel
</paper-button>
<paper-button id="applyButton" disabled={{cantApply}}>
Apply
</paper-button>
</div>
</div>
</template>

View File

@@ -0,0 +1,32 @@
Template.applyBuffDialog.onCreated(function(){
this.selectedTarget = new ReactiveVar("default");
});
Template.applyBuffDialog.helpers({
cantApply: function() {
return this.buff.target === "others" && Template.instance().selectedTarget.get() === "default"; //this is the only case where we can't apply a buff
},
canApplyToSelf: function() {
return this.buff.target !== "others"; //i.e. it is "self" or "both"
},
});
Template.applyBuffDialog.events({
"iron-select .characterPicker": function(event){
var detail = event.originalEvent.detail;
var value = detail.item.getAttribute("name");
Template.instance().selectedTarget.set(value);
},
"click #applyButton": function(event, instance){
var targetId = Template.instance().selectedTarget.get();
if (targetId === "default") {
if (this.buff.target === "others") return; //since we have "Select a character" selected
targetId = this.buff.charId; //otherwise, the default is to target self
}
popDialogStack(targetId);
},
"click #cancelButton": function(event, instance){
popDialogStack();
},
});

View File

@@ -1,15 +1,23 @@
<template name="buffDialog">
{{#with buff}}
{{#baseDialog title=name class=colorClass hideEdit=true}}
{{#baseDialog title=name class="white" hideColor=true startEditing=true editOnly=true}}
{{> buffDetails}}
{{else}}
{{> buffDetails}}
{{/baseDialog}}
{{/with}}
</template>
<template name="buffDetails">
<div>
{{appliedBy}}
</div>
<hr style="margin: 16px 0 16px 0;">
{{#if description}}
<div class="pre-wrap">{{evaluateString charId description}}</div>
<div>{{#markdown}}{{evaluateString charId description}}{{/markdown}}</div>
<hr style="margin: 16px 0 16px 0;">
{{/if}}
{{> effectsViewList charId=charId parentId=_id}}
{{> proficiencyViewList charId=charId parentId=_id}}
{{> attacksViewList charId=charId parentId=_id}}
</template>

View File

@@ -1,5 +1,50 @@
Template.buffDialog.onCreated(function(){
var buff = Buffs.findOne(this.buffId);
Meteor.subscribe("singleCharacterName", buff.charId); //so we can access the names of public characters
});
Template.buffDialog.helpers({
buff: function(){
return Buffs.findOne(this.buffId);
},
});
Template.buffDialog.events({
"click #deleteButton": function(event, instance){
Buffs.softRemoveNode(instance.data.buffId);
popDialogStack();
},
});
const typeDict = {
"Features": "feature",
"Items": "item",
"Spells": "spell",
}; //really, we should only need these three
Template.buffDetails.helpers({
appliedBy: function() {
if (this.type == "inate") {
return "Innate.";
} else {
var myName = Characters.findOne(this.charId).name;
var applierCharacter = Characters.findOne(this.appliedBy) || {name: "???"}
// "???" indicates that either we do not have read access to the buff-giver, or that the buff-giver does not exist.
if (applierCharacter.name === myName) {
var charName = "your "
} else {
if (applierCharacter.name && applierCharacter.name[applierCharacter.name.length - 1] === 's') {
var charName = applierCharacter.name + "' ";
} else {
var charName = applierCharacter.name + "'s ";
}
}
var type = typeDict[this.appliedByDetails.collection] + " ";
var applierThing = this.appliedByDetails.name;
return "Applied by " + charName + type + applierThing + ".";
}
},
});

View File

@@ -0,0 +1,15 @@
<template name="buffListItem">
<div class="item buffListItem layout horizontal center">
<div class="flex">
{{buff.name}}
</div>
{{#if canEditCharacter buff.charId}}
<paper-icon-button class="deleteButton"
role="button"
tabindex="0"
icon="delete">
</paper-icon-button>
{{/if}}
</div>
</template>

View File

@@ -0,0 +1,21 @@
Template.buffListItem.helpers({
name: function() {
return this.buff.name
}
});
Template.buffListItem.events({
"click .buffListItem": function(event){
var buffId = this.buff._id;
var charId = this.buff.charId;
pushDialogStack({
template: "buffDialog",
data: {buffId: buffId, charId: charId},
element: event.currentTarget,
});
},
"tap .deleteButton": function(event){
event.stopPropagation();
Buffs.remove(this.buff._id);
},
});

View File

@@ -0,0 +1,11 @@
.condition-library-dialog .item.selected {
background-color: #e4e4e4;
}
.condition-library-dialog table {
border-collapse: collapse;
}
.condition-library-dialog .library-condition td, tr {
position: relative;
}

View File

@@ -0,0 +1,34 @@
<template name="conditionLibraryDialog">
<div class="fit condition-library-dialog layout vertical">
<app-toolbar class="app-grey white-text">
<paper-icon-button id="backButton"
icon="arrow-back">
</paper-icon-button>
<div main-title>Conditions</div>
</app-toolbar>
<div class="flex scroll-y">
<div class="conditions" style="padding:8px">
<table style="width: 100%">
<tbody>
{{#each condition in conditions}}
{{>libraryCondition condition=condition selected=(isSelected condition)}}
{{/each}}
</tbody>
</table>
</div>
</div>
<div class="layout horizontal end-justified">
<paper-button class="cancelButton">Cancel</paper-button>
<paper-button class="okButton">OK</paper-button>
</div>
</div>
</template>
<template name="libraryCondition">
<tr class="item library-condition {{#if selected}}selected{{/if}}">
<td class="conditionName">
{{conditionName condition}}
<paper-ripple></paper-ripple>
</td>
</tr>
</template>

View File

@@ -0,0 +1,166 @@
Template.conditionLibraryDialog.onCreated(function(){
this.selectedCondition = new ReactiveVar();
});
Template.conditionLibraryDialog.helpers({
conditions: function(){
return Object.keys(LIBRARY_CONDITIONS)
},
isSelected(condition){
const selected = Template.instance().selectedCondition.get();
return selected && selected === condition;
},
});
Template.conditionLibraryDialog.events({
"click .cancelButton": function(event, template){
popDialogStack();
},
"click .okButton": function(event, template){
popDialogStack(template.selectedCondition.get());
},
"click .library-condition": function(event, template){
template.selectedCondition.set(this.condition);
},
"click #backButton": function(event, template){
popDialogStack();
},
});
Template.libraryCondition.helpers({
conditionName: function(name){
return LIBRARY_CONDITIONS[name].buff.name;
},
})
LIBRARY_CONDITIONS = {
//Conditions
blind: {
buff: {
name: "Blind",
description: "A blinded creature cant see and automatically fails any ability check that requires sight.\n\nAttack rolls against the creature have advantage, and the creatures attack rolls have disadvantage.",
},
},
deaf: {
buff: {
name: "Deaf",
description: "A deafened creature cant hear and automatically fails any ability check that requires hearing.",
},
},
frightened: {
buff: {
name: "Frightened",
description: "A frightened creature has disadvantage on ability checks and attack rolls while the source of its fear is within line of sight.\n\nThe creature cant willingly move closer to the source of its fear.",
}
},
grappled: {
buff:{
name: "Grappled",
description: "A grappled creatures speed becomes 0, and it cant benefit from any bonus to its speed.\n\nThe condition ends if the grappler is incapacitated.\n\nThe condition also ends if an effect removes the grappled creature from the reach of the grappler or grappling effect, such as when a creature is hurled away by the thunder wave spell.",
},
},
incapacitated: {
buff: {
name: "Incapacitated",
description: "An incapacitated creature cant take actions or reactions.",
}
},
invisible: {
buff: {
name: "Invisible",
description: "An invisible creature is impossible to see without the aid of magic or a special sense. For the purpose of hiding, the creature is heavily obscured. The creatures location can be detected by any noise it makes or any tracks it leaves.\n\nAttack rolls against the creature have disadvantage, and the creatures attack rolls have advantage.",
}
},
paralyzed: {
buff: {
name: "Paralyzed",
description: "A paralyzed creature is **incapacitated** and cant move or speak.\n\nAttack rolls against the creature have advantage.\n\nAny attack that hits the creature is a critical hit if the attacker is within 5 feet of the creature.",
},
},
petrified: {
buff: {
name: "Petrified",
description: "A petrified creature is transformed, along with any nonmagical object it is wearing or carrying, into a solid inanimate substance (usually stone). Its weight increases by a factor of ten, and it ceases aging.\n\nA petrified creature is **incapacitated** and cant move or speak, and is unaware of its surroundings.\n\nAttack rolls against the creature have advantage.\n\nThe creature is immune to poison and disease, although a poison or disease already in its system is suspended, not neutralized.",
},
},
poisoned: {
buff: {
name: "Poisoned",
description: "A poisoned creature has disadvantage on attack rolls and ability checks.",
},
},
prone: {
buff: {
name: "Prone",
description: "A prone creatures only movement option is to crawl, unless it stands up and thereby ends the condition.\n\nThe creature has disadvantage on attack rolls.\n\nAn attack roll against the creature has advantage if the attacker is within 5 feet of the creature. Otherwise, the attack roll has disadvantage.",
}
},
restrained: {
buff: {
name: "Restrained",
description: "A restrained creatures speed becomes 0, and it cant benefit from any bonus to its speed.\n\nAttack rolls against the creature have advantage, and the creatures attack rolls have disadvantage.\n\nThe creature has disadvantage on Dexterity saving throws.",
},
},
stunned: {
buff: {
name: "Stunned",
description: "A stunned creature is **incapacitated**, cant move, and can speak only falteringly\n\nThe creature automatically fails Strength and Dexterity saving throws.\n\nAttack rolls against the creature have advantage.",
},
},
unconscious: {
buff: {
name: "Unconscious",
description: "An unconscious creature is **incapacitated**, cant move or speak, and is unaware of its surroundings.\n\nThe creature drops whatever its holding and falls **prone**.\n\nThe creature automatically fails Strength and Dexterity saving throws.\n\nAttack rolls against the creature have advantage.\n\nAny attack that hits the creature is a critical hit if the attacker is within 5 feet of the creature.",
},
},
exhaustion1: {
buff: {
name: "Exhaustion - 1",
description: "Disadvantage on ability checks\n\nFinishing a long rest reduces a creatures exhaustion level by 1, provided that the creature has also ingested some food and drink.",
},
},
exhaustion2: {
buff: {
name: "Exhaustion - 2",
description: "Speed halved",
},
},
exhaustion3: {
buff: {
name: "Exhaustion - 3",
description: "Disadvantage on attack rolls and saving throws",
},
},
exhaustion4: {
buff: {
name: "Exhaustion - 4",
description: "Hit point maximum halved",
},
},
exhaustion5: {
buff: {
name: "Exhaustion - 5",
description: "Speed reduced to 0",
},
},
exhaustion6: {
buff: {
name: "Exhaustion - 6",
description: "You have died of exhaustion",
},
},
};

View File

@@ -0,0 +1,15 @@
<template name="conditionView">
<div class="item conditionView layout horizontal center">
<div class="flex">
{{condition.name}}
</div>
{{#if canEditCharacter condition.charId}}
<paper-icon-button class="deleteButton"
role="button"
tabindex="0"
icon="delete">
</paper-icon-button>
{{/if}}
</div>
</template>

View File

@@ -0,0 +1,15 @@
Template.conditionView.events({
"click .conditionView": function(event){
var condition = this.condition;
var charId = Template.parentData()._id;
pushDialogStack({
template: "conditionViewDialog",
data: {condition: condition},
element: event.currentTarget,
});
},
"tap .deleteButton": function(event){
event.stopPropagation();
Conditions.remove(this.condition._id);
},
});

View File

@@ -0,0 +1,14 @@
<template name="conditionViewDialog">
{{#baseDialog title=condition.name class="white" hideColor=true startEditing=true editOnly=true}}}
{{> conditionDetails condition=condition}}
{{else}}
{{> conditionDetails condition=condition}}
{{/baseDialog}}
</template>
<template name="conditionDetails">
{{#if condition.description}}
<div>{{#markdown}}{{evaluateString condition.charId condition.description}}{{/markdown}}</div>
{{/if}}
{{> effectsViewList charId=condition.charId parentId=condition._id}}
</template>

View File

@@ -0,0 +1,6 @@
Template.conditionViewDialog.events({
"click #deleteButton": function(event, instance){
Conditions.remove(instance.data.condition._id);
popDialogStack();
},
});

View File

@@ -0,0 +1,29 @@
<template name="customBuffEdit">
{{#baseEditDialog title=buff.name hideColor=true}}
<!--name-->
<paper-input id="buffNameInput" class="fullwidth" label="Name" value={{buff.name}}></paper-input>
<div class="layout horizontal center wrap justified">
<paper-dropdown-menu class=flex label="Target" style="flex-basis: 150px; max-width: 200px;">
<dicecloud-selector selected={{buff.target}} class="dropdown-content target-dropdown">
<paper-item name="self" style="width: 150px;">
Self only
</paper-item>
<paper-item name="others">
Others only
</paper-item>
<paper-item name="both">
Both
</paper-item>
</dicecloud-selector>
</paper-dropdown-menu>
</div>
<!--description-->
<paper-textarea label="Description" id="buffDescriptionInput" value={{buff.description}}></paper-textarea>
{{> effectsEditList parentId=buff._id parentCollection="CustomBuffs" charId=buff.charId name=name enabled=false}}
{{> attackEditList parentId=buff._id parentCollection="CustomBuffs" charId=buff.charId name=name enabled=false}}
{{> proficiencyEditList parentId=buff._id parentCollection="CustomBuffs" charId=buff.charId enabled=false}}
{{/baseEditDialog}}
</template>

View File

@@ -0,0 +1,47 @@
Template.customBuffEdit.helpers({
buff(){
return CustomBuffs.findOne(this.customBuffId);
},
});
const debounce = (f) => _.debounce(f, 300);
Template.customBuffEdit.events({
"input #buffNameInput": debounce(function(event){
const input = event.currentTarget;
var name = input.value;
if (!name){
input.invalid = true;
input.errorMessage = "Name is required";
} else {
input.invalid = false;
CustomBuffs.update(this.customBuffId, {
$set: {name: name}
}, {
removeEmptyStrings: false,
trimStrings: false,
});
}
}),
"input #buffDescriptionInput": debounce(function(event){
var description = event.currentTarget.value;
CustomBuffs.update(this.customBuffId, {
$set: {description: description}
}, {
removeEmptyStrings: false,
trimStrings: false,
});
}),
"iron-select .target-dropdown": function(event){
var detail = event.originalEvent.detail;
var value = detail.item.getAttribute("name");
const buff = CustomBuffs.findOne(this.customBuffId);
if (value === buff.target) return;
CustomBuffs.update(this.customBuffId, {$set: {target: value}});
},
"click #deleteButton": function(event, instance){
CustomBuffs.softRemoveNode(instance.data.customBuffId);
GlobalUI.deletedToast(instance.data.customBuffId, "Buffs", "Buff");
popDialogStack();
},
});

View File

@@ -0,0 +1,30 @@
<!--needs to be given charId, parentId and parentCollection-->
<template name="customBuffEditList">
{{#if buffs.count}}
<div class="buffs">
<div class="paper-font-title" style="margin-bottom: 8px;">
Buffs
</div>
<table class="wideTable" style="width: 100%;">
{{#each buff in buffs}}
{{> customBuffEditListItem buff=buff}}
{{/each}}
</table>
</div>
{{/if}}
<paper-button id="addBuffButton"
class="red-button"
raised>
Add Buff
</paper-button>
</template>
<template name="customBuffEditListItem">
<div class="buff layout horizontal center" data-id={{buff._id}}>
{{> customBuffView buff=buff}}
<div>
<paper-icon-button class="edit-buff" icon="create">
</paper-icon-button>
</div>
</div>
</template>

View File

@@ -0,0 +1,41 @@
Template.customBuffEditList.helpers({
buffs: function(){
var selector = {
"parent.id": this.parentId,
"charId": this.charId,
};
return CustomBuffs.find(selector);
}
});
Template.customBuffEditList.events({
"tap #addBuffButton": function(event, instance){
if (!_.isBoolean(this.enabled)) {
this.enabled = true;
}
const customBuffId = CustomBuffs.insert({
name: this.name || "New Buff",
charId: this.charId,
parent: {
id: this.parentId,
collection: this.parentCollection,
},
});
pushDialogStack({
template: "customBuffEdit",
data: {customBuffId},
element: event.currentTarget,
returnElement: () => instance.find(`tr.buff[data-id='${customBuffId}']`),
});
},
});
Template.customBuffEditListItem.events({
"tap .edit-buff": function(event, template){
pushDialogStack({
template: "customBuffEdit",
data: {customBuffId: this.buff._id},
element: event.currentTarget.parentElement.parentElement,
});
},
});

View File

@@ -0,0 +1,8 @@
<template name="customBuffView">
<div class="flex">{{buff.name}}</div>
<div class="flex">
{{#if canEditCharacter buff.charId}}
<paper-button class="apply-buff-button">Apply{{toSelf}}</paper-button>
{{/if}}
</div>
</template>

View File

@@ -0,0 +1,82 @@
const applyBuff = function(targetId, buff) {
var parent = global[buff.parent.collection].findOne(buff.parent.id);
//insert new buff
newBuffId = Buffs.insert({
charId: targetId,
name: buff.name,
description: buff.description,
lifeTime: {total: buff.lifeTime.total},
type: "custom",
appliedBy: buff.charId,
appliedByDetails: {
name: parent.name,
collection: buff.parent.collection,
},
});
//insert children
Attacks.find({"parent.id": buff._id}).forEach(function(doc){
temp = _.clone(doc);
temp.parent.id = newBuffId;
temp.parent.collection = "Buffs";
delete temp._id;
Attacks.insert(temp);
});
Effects.find({"parent.id": buff._id}).forEach(function(doc){
temp = _.clone(doc);
temp.parent.id = newBuffId;
temp.parent.collection = "Buffs";
delete temp._id;
Effects.insert(temp);
});
Proficiencies.find({"parent.id": buff._id}).forEach(function(doc){
temp = _.clone(doc);
temp.parent.id = newBuffId;
temp.parent.collection = "Buffs";
delete temp._id;
Proficiencies.insert(temp);
});
let target;
if (targetId == buff.charId) {
target = "self";
} else {
target = Characters.findOne(targetId) || {};
target = target && target.name || "target"
}
GlobalUI.toast(`${buff.name || "Buff"} applied to ${target}`);
};
Template.customBuffView.helpers({
toSelf: function() {
if (this.buff.target === "self") {
return " to self";
} else {
return "";
}
}
});
Template.customBuffView.events({
"click .apply-buff-button": function(){
if (this.buff.target !== "self") {
pushDialogStack({
template: "applyBuffDialog",
data: {buff: this.buff},
element: event.currentTarget,
callback: (targetId) => {
if (!targetId) return;
applyBuff(targetId, this.buff);
},
});
} else {
var targetId = this.buff.charId;
applyBuff(targetId, this.buff);
}
},
});

View File

@@ -0,0 +1,14 @@
<template name="customBuffViewList">
{{#if buffs.count}}
<div class="buffs">
<div class="paper-font-title" style="margin-bottom: 8px;">
Buffs
</div>
{{#each buff in buffs}}
<div class="layout horizontal center">
{{> customBuffView buff=buff}}
</div>
{{/each}}
</div>
{{/if}}
</template>

View File

@@ -0,0 +1,9 @@
Template.customBuffViewList.helpers({
buffs: function(){
var selector = {
"parent.id": this.parentId,
"charId": this.charId,
};
return CustomBuffs.find(selector);
}
});

View File

@@ -36,7 +36,9 @@
{{> effectsViewList charId=charId parentId=_id}}
{{> proficiencyViewList charId=charId parentId=_id}}
{{> attacksViewList charId=charId parentId=_id}}
{{> attacksViewList charId=charId parentId=_id}}
{{> customBuffViewList charId=charId parentId=_id}}
</template>
<template name="featureEdit">
@@ -77,5 +79,6 @@
{{> effectsEditList parentId=_id parentCollection="Features" charId=charId name=name enabled=enabled}}
{{> proficiencyEditList parentId=_id parentCollection="Features" charId=charId enabled=enabled}}
{{> attackEditList parentId=_id parentCollection="Features" charId=charId enabled=enabled name=name}}
{{> attackEditList parentId=_id parentCollection="Features" charId=charId enabled=enabled name=name}}
{{> customBuffEditList parentId=_id parentCollection="Features" charId=charId}}
</template>

View File

@@ -81,6 +81,7 @@
{{#if hasCharacters (evaluateShortString charId description)}}
<div class="bottom flex">
{{#markdown}}{{evaluateShortString charId description}}{{/markdown}}
{{> customBuffViewList charId=charId parentId=_id}}
</div>
{{/if}}
{{#if hasUses}}
@@ -159,4 +160,4 @@
</div>
</div>
</div>
</template>
</template>

View File

@@ -28,16 +28,16 @@
<div class="bottom green" style="padding: 0;">
{{> carryCapacityBar}}
</div>
{{#if encumberedBuffs.count}}
{{#if encumberedConditions.count}}
<div class="bottom list">
{{#each encumberedBuffs}}
{{#each condition in encumberedConditions}}
<div class="item-slot">
<div class="item buff layout horizontal center">
<div class="item condition layout horizontal center">
<div class="flex">
<iron-icon icon="work"
style="margin-right: 16px">
</iron-icon>
{{name}}
{{condition.name}}
</div>
</div>
</div>

View File

@@ -68,9 +68,8 @@ Template.inventory.helpers({
return weight;
},
encumberedBuffs: function(){
return Buffs.find({
return Conditions.find({
charId: this._id,
type: "inate",
name: {$in: [
"Encumbered",
"Heavily encumbered",
@@ -201,12 +200,10 @@ Template.inventory.events({
element: event.currentTarget.parentElement,
});
},
"click .buff": function(event, instance){
var buffId = this._id;
var charId = Template.parentData()._id;
"click .condition": function(event, instance){
pushDialogStack({
template: "buffDialog",
data: {buffId: buffId, charId: charId},
template: "conditionViewDialogDialog",
data: {condition: this.condition},
element: event.currentTarget,
});
},

View File

@@ -23,6 +23,7 @@
{{/if}}
{{> effectsViewList charId=charId parentId=_id}}
{{> attacksViewList charId=charId parentId=_id}}
{{> customBuffViewList charId=charId parentId=_id}}
</template>
<template name="itemEdit">
@@ -65,10 +66,13 @@
<paper-textarea id="itemDescriptionInput" label="Description" value={{description}}></paper-textarea>
{{> textareaBracketSuffix}}
</div>
<!--Effects-->
{{> effectsEditList parentId=_id parentCollection="Items" charId=charId enabled=equipped name=name}}
<!--Attacks-->
{{> attackEditList parentId=_id parentCollection="Items" charId=charId enabled=equipped name=name}}
<!-- Buffs -->
{{> customBuffEditList parentId=_id parentCollection="Items" charId=charId}}
</template>
<template name="containerDropdown">

View File

@@ -38,6 +38,7 @@
</div>
<div>{{#markdown}}{{evaluateSpellString charId parent.id description}}{{/markdown}}</div>
{{> attacksViewList charId=charId parentId=_id}}
{{> customBuffViewList charId=charId parentId=_id}}
</template>
<template name="spellEdit">
@@ -111,10 +112,12 @@
</paper-checkbox>
</div>
<!--Description-->
<div class="description-input layout horizontal end">
<paper-textarea id="descriptionInput" label="Description" style="width: calc(100% - 24px)" value={{description}}></paper-textarea>
{{> textareaBracketSuffix}}
</div>
{{> customBuffEditList parentId=_id parentCollection="Spells" charId=charId}}
{{> attackEditList parentId=_id parentCollection="Spells" charId=charId enabled=true name=name isSpell=true}}
</template>

View File

@@ -42,6 +42,34 @@
</div>
</paper-material>
</div>
<!--Condtions-->
<div>
<paper-material class="card">
<div class="top white subhead layout horizontal center">
<div class="flex">Conditions</div>
{{#if canEditCharacter _id}}
<paper-icon-button class="black54" id="addCondition" icon="add"></paper-icon-button>
{{/if}}
</div>
<div flex class="bottom list">
<div class="conditionsList">
{{#each condition in conditions}}
{{>conditionView condition=condition}}
{{/each}}
</div>
{{#if buffs.count}}
<div class="layout horizontal">
<div class="paper-font-subhead flex">Buffs</div>
</div>
{{/if}}
<div class="buffsList">
{{#each buff in buffs}}
{{>buffListItem buff=buff}}
{{/each}}
</div>
</div>
</paper-material>
</div>
<!--Skills-->
<div>
<paper-material class="card">

View File

@@ -1,3 +1,15 @@
Template.stats.helpers({
conditions: function() {
return Conditions.find({charId: this._id});
},
buffs: function() {
var selector = {
"charId": this._id,
};
return Buffs.find(selector);
},
})
Template.stats.events({
"click .stat-card": function(event, instance){
var charId = instance.data._id;
@@ -65,4 +77,17 @@ Template.stats.events({
element: event.currentTarget.parentElement.parentElement,
});
},
"click #addCondition": function(event, template){
pushDialogStack({
template: "conditionLibraryDialog",
element: event.currentTarget,
callback: (result) => {
if (!result) {
return;
}
else Meteor.call("giveCondition", this._id, result)
},
//returnElement: () => $(`[data-id='${itemId}']`).get(0),
})
},
});

View File

@@ -0,0 +1,17 @@
.characterPicker .character-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.characterPicker .partyHead {
font-weight: 500;
}
.characterPicker .partyHead iron-icon {
transition: transform 0.3s ease;
}
.characterPicker .partyHead iron-icon.open {
transform: rotate(90deg);
}

View File

@@ -0,0 +1,34 @@
<template name="characterPicker">
<dicecloud-selector class="characterPicker" selected={{selected}} selectable="paper-item" style="height: 100%; overflow-y: auto;">
{{#if selfId}}{{#if includeSelf}}
<paper-item class="short clickable" name={{selfId}}>
<div class="character-name">
Self
</div>
</paper-item>
{{/if}}{{/if}}
{{#each charactersWithNoParty}}
<paper-item class="short clickable" name={{_id}}>
<div class="character-name">
{{name}}
</div>
</paper-item>
{{/each}}
{{#each parties}}
<div class="paper-font-subhead partyHead clickable">
<iron-icon icon="chevron-right" class="{{#if isOpen _id}}open{{/if}}">
</iron-icon>
{{name}}
</div>
<iron-collapse opened={{isOpen _id}}>
{{#each charactersInParty}}
<paper-item class="short clickable" name={{_id}}>
<div class="character-name">
{{name}}
</div>
</paper-item>
{{/each}}
</iron-collapse>
{{/each}}
</dicecloud-selector>
</template>

View File

@@ -0,0 +1,53 @@
Template.characterPicker.onCreated(function() {
this.subscribe("characterList");
this.openedParties = new ReactiveVar(new Set());
});
Template.characterPicker.helpers({
parties() {
return Parties.find(
{owner: Meteor.userId()},
{sort: {name: 1}},
);
},
charactersInParty() {
var userId = Meteor.userId();
var selector = {
_id: {$in: this.characters, $ne: this.selfId},
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
};
if (this.writableOnly) {
selector.$or = [{writers: userId}, {owner: userId}];
}
return Characters.find(selector,{sort: {name: 1}});
},
charactersWithNoParty() {
var userId = Meteor.userId();
var charArrays = Parties.find({owner: userId}).map(p => p.characters);
var partyChars = _.uniq(_.flatten(charArrays));
var selector = {
_id: {$nin: partyChars, $ne: this.selfId},
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
};
if (this.writableOnly) {
selector.$or = [{writers: userId}, {owner: userId}];
}
return Characters.find(selector, {sort: {name: 1}});
},
isOpen(id) {
var openedParties = Template.instance().openedParties.get();
return openedParties.has(id);
},
});
Template.characterPicker.events({
"click .partyHead": function(event, instance){
var openedParties = instance.openedParties.get();
if (openedParties.has(this._id)){
openedParties.delete(this._id);
} else {
openedParties.add(this._id);
}
instance.openedParties.set(openedParties);
},
});

View File

@@ -34,7 +34,6 @@ Template.characterSideList.helpers({
},
isOpen(id) {
var openedParties = Template.instance().openedParties.get();
console.log(openedParties);
return openedParties.has(id);
},
});

View File

@@ -15,10 +15,12 @@
{{/unless}}
{{#unless hideColor}}
{{> colorDropdown}}
{{/unless}}
{{#unless editOnly}}
<paper-icon-button id="doneEditingButton"
icon="done">
</paper-icon-button>
{{/unless}}
<paper-icon-button id="doneEditingButton"
icon="done">
</paper-icon-button>
{{else}}
{{#if showEdit}}
<paper-icon-button id="editButton"

View File

@@ -26,28 +26,32 @@ Meteor.methods({
var condition = getCondition(conditionName);
//create the buff
var buff = _.extend(
{charId: charId, type: "inate"}, condition.buff
{
charId: charId,
},
condition.buff
);
//make sure the character doesn't already have the buff
var existingBuffs = Buffs.find(_.clone(buff)).count();
var existingBuffs = Conditions.find(_.clone(buff)).count();
if (existingBuffs) return;
//remove exclusive conditions
_.each(condition.exclusiveConditions, function(exCond) {
Meteor.call("removeCondition", charId, exCond);
});
//insert the buff
var buffId = Buffs.insert(buff);
var buffId = Conditions.insert(buff);
//extend and insert each effect
_.each(condition.effects, function(effect) {
var newEffect = {
stat: effect.stat,
operation: effect.operation,
calculation: effect.calculation,
value: effect.value,
charId: charId,
parent: {
id: buffId,
collection: "Buffs",
collection: "Conditions",
},
enabled: true,
};
@@ -73,11 +77,26 @@ Meteor.methods({
var condition = getCondition(conditionName);
//remove the buff
var buff = _.extend(
{charId: charId, type: "inate"}, condition.buff
{charId: charId}, condition.buff
);
Buffs.remove(buff);
Conditions.remove(buff);
//dont remove the effects, they get removed automatically through parenting
},
getConditions: function() {
return Object.keys(CONDITIONS);
},
getConditionName: function(conditionName) {
//get condition from constant
var condition = CONDITIONS[conditionName];
//check that condition exists
if (!condition) {
throw new Meteor.Error(
"Invalid condition",
conditionName + " is not a known condition"
);
}
return condition.buff.name;
},
});
trackEncumbranceConditions = function(charId, templateInstance) {
@@ -150,7 +169,7 @@ CONDITIONS = {
{
stat: "perception",
operation: "conditional",
calculation: "You fail your perception check if it requires sight",
calculation: "You fail your Perception check if it requires sight",
}
],
},
@@ -164,7 +183,7 @@ CONDITIONS = {
{
stat: "perception",
operation: "conditional",
calculation: "You fail your perception check if it requires hearing",
calculation: "You fail your Perception check if it requires hearing",
}
],
},
@@ -207,7 +226,7 @@ CONDITIONS = {
paralyzed: {
buff: {
name: "Paralyzed",
description: "A paralyzed creature is incapacitated and cant move or speak.\n\nAttack rolls against the creature have advantage.\n\nAny attack that hits the creature is a critical hit if the attacker is within 5 feet of the creature.",
description: "A paralyzed creature is **incapacitated** and cant move or speak.\n\nAttack rolls against the creature have advantage.\n\nAny attack that hits the creature is a critical hit if the attacker is within 5 feet of the creature.",
},
effects: [
{
@@ -232,7 +251,7 @@ CONDITIONS = {
petrified: {
buff: {
name: "Petrified",
description: "A petrified creature is transformed, along with any nonmagical object it is wearing or carrying, into a solid inanimate substance (usually stone). Its weight increases by a factor of ten, and it ceases aging.\n\nA petrified creature is incapacitated and cant move or speak, and is unaware of its surroundings.\n\nAttack rolls against the creature have advantage.\n\nThe creature is immune to poison and disease, although a poison or disease already in its system is suspended, not neutralized.",
description: "A petrified creature is transformed, along with any nonmagical object it is wearing or carrying, into a solid inanimate substance (usually stone). Its weight increases by a factor of ten, and it ceases aging.\n\nA petrified creature is **incapacitated** and cant move or speak, and is unaware of its surroundings.\n\nAttack rolls against the creature have advantage.\n\nThe creature is immune to poison and disease, although a poison or disease already in its system is suspended, not neutralized.",
},
effects: (function() {
var effects = [
@@ -294,7 +313,7 @@ CONDITIONS = {
stunned: {
buff: {
name: "Stunned",
description: "A stunned creature is incapacitated, cant move, and can speak only falteringly\n\nThe creature automatically fails Strength and Dexterity saving throws.\n\nAttack rolls against the creature have advantage.",
description: "A stunned creature is **incapacitated**, cant move, and can speak only falteringly\n\nThe creature automatically fails Strength and Dexterity saving throws.\n\nAttack rolls against the creature have advantage.",
},
effects: [
{
@@ -317,7 +336,7 @@ CONDITIONS = {
unconscious: {
buff: {
name: "Unconscious",
description: "An unconscious creature is incapacitated, cant move or speak, and is unaware of its surroundings.\n\nThe creature drops whatever its holding and falls prone.\n\nThe creature automatically fails Strength and Dexterity saving throws.\n\nAttack rolls against the creature have advantage.\n\nAny attack that hits the creature is a critical hit if the attacker is within 5 feet of the creature.",
description: "An unconscious creature is **incapacitated**, cant move or speak, and is unaware of its surroundings.\n\nThe creature drops whatever its holding and falls **prone**.\n\nThe creature automatically fails Strength and Dexterity saving throws.\n\nAttack rolls against the creature have advantage.\n\nAny attack that hits the creature is a critical hit if the attacker is within 5 feet of the creature.",
},
subConditions: ["incapacitated", "prone"],
},

View File

@@ -1,6 +1,6 @@
Meteor.startup(() => {
const collections = [
Attacks, Buffs, Classes, Effects, Experiences,
Attacks, Buffs, Classes, CustomBuffs, Effects, Experiences,
Features, Notes, Proficiencies, SpellLists, Spells,
Containers, Items,
];

View File

@@ -13,22 +13,40 @@ Meteor.publish("singleCharacter", function(characterId){
return [
Characters.find({_id: characterId}),
//get all the assets for this character including soft deleted ones
Actions.find ({charId: characterId}, {removed: true}),
Attacks.find ({charId: characterId}, {removed: true}),
Buffs.find ({charId: characterId}, {removed: true}),
Classes.find ({charId: characterId}, {removed: true}),
Containers.find ({charId: characterId}, {removed: true}),
Effects.find ({charId: characterId}, {removed: true}),
Experiences.find ({charId: characterId}, {removed: true}),
Features.find ({charId: characterId}, {removed: true}),
Items.find ({charId: characterId}, {removed: true}),
Notes.find ({charId: characterId}, {removed: true}),
Spells.find ({charId: characterId}, {removed: true}),
SpellLists.find ({charId: characterId}, {removed: true}),
TemporaryHitPoints.find({charId: characterId}, {removed: true}),
Proficiencies.find ({charId: characterId}, {removed: true}),
Actions.find ({charId: characterId}, {removed: true}),
Attacks.find ({charId: characterId}, {removed: true}),
Buffs.find ({charId: characterId}, {removed: true}),
Classes.find ({charId: characterId}, {removed: true}),
Conditions.find ({charId: characterId}, {removed: true}),
Containers.find ({charId: characterId}, {removed: true}),
CustomBuffs.find ({charId: characterId}, {removed: true}),
Effects.find ({charId: characterId}, {removed: true}),
Experiences.find ({charId: characterId}, {removed: true}),
Features.find ({charId: characterId}, {removed: true}),
Items.find ({charId: characterId}, {removed: true}),
Notes.find ({charId: characterId}, {removed: true}),
Spells.find ({charId: characterId}, {removed: true}),
SpellLists.find ({charId: characterId}, {removed: true}),
TemporaryHitPoints.find ({charId: characterId}, {removed: true}),
Proficiencies.find ({charId: characterId}, {removed: true}),
];
} else {
return [];
}
});
Meteor.publish("singleCharacterName", function(characterId){
userId = this.userId;
var char = Characters.findOne({
_id: characterId,
$or: [
{readers: userId},
{writers: userId},
{owner: userId},
{"settings.viewPermission": "public"},
],
});
if (char) {
return Characters.find(characterId, {fields:{"name": 1}});
}
});