diff --git a/css/hm3.css b/css/hm3.css index bef3dd27..0d23a985 100644 --- a/css/hm3.css +++ b/css/hm3.css @@ -17,6 +17,9 @@ margin: 10px 0; padding: 0; } +.grid-fixed-row { + grid-template-rows: 25px 5px; } + .grid-3col { grid-column: span 3 / span 3; grid-template-columns: repeat(3, minmax(0, 1fr)); } @@ -630,6 +633,9 @@ .hm3 .items .items-list .items-header .item-name { color: #191813; border-right: 1px solid #c9c7b8; } + .hm3 .items .items-list .items-header .item-name i { + font-size: 12px; + padding-left: 5px; } .hm3 .items .items-list .item-list { list-style: none; margin: 0; @@ -742,6 +748,11 @@ overflow: hidden; } .hm3 .combat .combat-stats .combat-stat { align-items: center; } + .hm3 .combat .combat-stats .combat-stat .combat-eff-fatigue { + flex: 0 0 25px; } + .hm3 .combat .combat-stats .combat-stat .combat-fatigue-input { + flex: 0 0 25px; + text-align: center; } .hm3 .combat .combat-stats .combat-stat .combat-stat-label { flex: 0 0 55px; font-weight: bold; @@ -751,9 +762,6 @@ background: #bbb; padding: 2px; text-align: center; } - .hm3 .combat .combat-stats .combat-stat .combat-stat-input { - flex: 0 0 40px; - text-align: center; } .hm3 .combat .weapons-list { list-style: none; @@ -1081,36 +1089,74 @@ text-align: center; } .hm3 .sheet-body .ability-scores { - flex: 0 0 260px; + flex: 0 0 80px; gap: 0; - margin: 10px; } - .hm3 .sheet-body .ability-scores .ability-scores-column { - border: 2px groove #eeede0; - padding: 3px; } - .hm3 .sheet-body .ability-scores .ability-scores-column .ability { - display: block; - align-items: center; + margin: 0; + justify-content: center; } + .hm3 .sheet-body .ability-scores .ability { + display: block; + align-items: center; + text-align: center; + padding: 3px 0 0 0; + flex: 0 0 80px; + border: 2px groove #eeede0; } + .hm3 .sheet-body .ability-scores .ability .ability-name { + margin: 0 0 2px; + font-size: 13px; } + .hm3 .sheet-body .ability-scores .ability .ability-value { + font-size: 25px; + font-weight: bold; + margin: 0 0 2px; } + .hm3 .sheet-body .ability-scores .ability .ability-box { + border-top: 1px solid black; } + .hm3 .sheet-body .ability-scores .ability .label { + flex: 0 0 40px; + font-weight: bold; + text-transform: uppercase; } + .hm3 .sheet-body .ability-scores .ability .value { + flex: 0 0 40px; + font-size: 20pt; } + +.hm3 .sheet-body .traits h2 { + flex: 1; + margin: 0; + line-height: 36px; + font-family: "Modesto Condensed", "Palatino Linotype", serif; + font-size: 20px; + font-weight: 700; + color: #4b4a44; + background-color: #bbb; + border-top: 2px groove #fff; + border-bottom: 2px groove #fff; + padding: 5px; } + +.hm3 .sheet-body .traits .traits-list { + list-style: none; + margin: 7px 2px; + padding: 0; + overflow-y: hidden; } + .hm3 .sheet-body .traits .traits-list .trait .trait-name { + flex: 0 0 150px; } + .hm3 .sheet-body .traits .traits-list .trait .trait-notes { + flex: 1 0 50px; + text-align: left; + padding-left: 5px; + border-left: 1px solid #c9c7b8; + border-right: 1px solid #c9c7b8; } + .hm3 .sheet-body .traits .traits-list .trait .trait-ele { + flex: 0 0 40px; + text-align: center; + border-left: 1px solid #c9c7b8; + border-right: 1px solid #c9c7b8; } + .hm3 .sheet-body .traits .traits-list .trait .item-controls { + flex: 0 0 45px; + text-align: right; + padding-right: 5px; } + .hm3 .sheet-body .traits .traits-list .trait .item-controls .item-control { + flex: 0 0 22px; + font-size: 12px; text-align: center; - padding: 3px 0 0 0; - border-bottom: 2px groove #eeede0; } - .hm3 .sheet-body .ability-scores .ability-scores-column .ability .ability-name { - margin: 0 0 2px; - font-size: 13px; } - .hm3 .sheet-body .ability-scores .ability-scores-column .ability .ability-value { - font-size: 25px; - font-weight: bold; - margin: 0 0 2px; } - .hm3 .sheet-body .ability-scores .ability-scores-column .ability .ability-box { - border-top: 1px solid black; } - .hm3 .sheet-body .ability-scores .ability-scores-column .ability .label { - flex: 0 0 40px; - font-weight: bold; - text-transform: uppercase; } - .hm3 .sheet-body .ability-scores .ability-scores-column .ability .value { - flex: 0 0 40px; - font-size: 20pt; } - .hm3 .sheet-body .ability-scores .ability-scores-column .ability:last-child { - border-bottom: none; } + color: #4b4a44; } .hm3 .sheet-body .profile-params { margin-top: 5px; @@ -1122,19 +1168,32 @@ border-bottom: 2px groove #eeede0; margin-top: 12px; padding: 5px 0; } - .hm3 .sheet-body .profile-params .profile-item .label { - flex: 0 0 70px; - font-weight: bold; - text-transform: uppercase; } - .hm3 .sheet-body .profile-params .profile-item .value { - flex: 0 0 150px; } + .hm3 .sheet-body .profile-params .profile-item .attribute { + align-items: center; } + .hm3 .sheet-body .profile-params .profile-item .attribute .label { + flex: 0 0 60px; + font-weight: bold; + text-transform: uppercase; + text-align: right; + padding-right: 4px; } + .hm3 .sheet-body .profile-params .profile-item .attribute .value { + flex: 0 0 150px; } .hm3 .sheet-body .profile-desc { margin-top: 5px; } .hm3 .sheet-body .profile-desc .label { - flex: 0 0 18px; + padding-left: 5px; + font-family: "Modesto Condensed", "Palatino Linotype", serif; + font-size: 16px; + margin: 2px 0; + align-items: center; + background: rgba(0, 0, 0, 0.05); + border: 2px groove #eeede0; font-weight: bold; - text-transform: uppercase; } + line-height: 24px; } + .hm3 .sheet-body .profile-desc .label i { + font-size: 12px; + padding-left: 5px; } .hm3 .sheet-body .profile-desc .description { margin-left: 5px; } @@ -1143,9 +1202,6 @@ object-fit: contain; background: white; } -.hm3 .sheet-body .profile .profile-desc { - margin-left: 5px; } - .hm3 .injuries-list { list-style: none; margin: 7px 0; @@ -1581,3 +1637,114 @@ margin-top: 10px; } .hm3 .armor-locations-list .armor-location { display: block; } + +.hm3 .effects .disabled { + color: #9a998f; } + +.hm3 .effects .effects-list { + list-style: none; + margin: 7px 0; + padding: 0; + overflow-y: hidden; } + .hm3 .effects .effects-list .effects-header { + margin: 2px 0; + padding: 0; + align-items: center; + background: rgba(0, 0, 0, 0.05); + border: 2px groove #eeede0; + font-weight: bold; + line-height: 24px; } + .hm3 .effects .effects-list .effects-header h3 { + margin: 0; + padding-left: 5px; + font-family: "Modesto Condensed", "Palatino Linotype", serif; + font-size: 20px; + font-weight: 700; + font-size: 16px; } + .hm3 .effects .effects-list .effects-header .effect-name { + color: #191813; + border-right: 1px solid #c9c7b8; } + .hm3 .effects .effects-list .effects-header .effect-name i { + font-size: 12px; + padding-left: 5px; } + .hm3 .effects .effects-list .effects-header .effect-controls { + flex: 0 0 45px; + text-align: right; + padding-right: 5px; + color: #9a998f; } + .hm3 .effects .effects-list .effects-header .effect-controls .active { + color: #4b4a44; } + .hm3 .effects .effects-list .effect-list { + list-style: none; + margin: 0; + padding: 0; } + .hm3 .effects .effects-list .effect { + line-height: 30px; + padding: 0 2px; + border-bottom: 1px solid #c9c7b8; } + .hm3 .effects .effects-list .effect .effect-image { + flex: 0 0 30px; + background-size: 30px; + margin-right: 5px; } + .hm3 .effects .effects-list .effect img { + display: block; } + .hm3 .effects .effects-list .effect .effect-name { + cursor: pointer; + max-height: 30px; + overflow: hidden; + border-right: 1px solid #c9c7b8; } + .hm3 .effects .effects-list .effect .effect-name h4 { + margin: 0; + white-space: nowrap; + overflow-x: hidden; } + .hm3 .effects .effects-list .effect:last-child { + border-bottom: none; } + .hm3 .effects .effects-list .effect-detail { + font-size: 12px; + color: #7a7971; + border-right: 1px solid #c9c7b8; + word-break: break-word; + white-space: nowrap; + overflow: hidden; } + .hm3 .effects .effects-list .effect-ele { + padding-left: 5px; + border-right: 1px solid #c9c7b8; } + .hm3 .effects .effects-list .effect-duration { + flex: 0 0 100px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } + .hm3 .effects .effects-list .effect-source { + flex: 0 0 100px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } + .hm3 .effects .effects-list .effect-changes { + flex: 1 0 60px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } + .hm3 .effects .effects-list .effect-controls { + flex: 0 0 45px; + text-align: right; } + .hm3 .effects .effects-list .effect-controls .effect-control { + flex: 0 0 22px; + font-size: 12px; + text-align: right; + color: #9a998f; } + .hm3 .effects .effects-list .effect-controls .effect-equip { + color: #9a998f; } + .hm3 .effects .effects-list .effect-controls .active { + color: #4b4a44; } + .hm3 .effects .effects-list .effect-controls-wide { + flex: 0 0 65px; + text-align: right; } + .hm3 .effects .effects-list .effect-controls-wide .effect-control { + flex: 0 0 22px; + font-size: 12px; + text-align: right; + color: #9a998f; } + .hm3 .effects .effects-list .effect-controls-wide .effect-equip { + color: #9a998f; } + .hm3 .effects .effects-list .effect-controls-wide .active { + color: #4b4a44; } diff --git a/images/help/combat-tab.jpg b/images/help/combat-tab.jpg new file mode 100644 index 00000000..2dab0fb0 Binary files /dev/null and b/images/help/combat-tab.jpg differ diff --git a/images/help/effects-tab.jpg b/images/help/effects-tab.jpg new file mode 100644 index 00000000..cd9d0321 Binary files /dev/null and b/images/help/effects-tab.jpg differ diff --git a/images/help/esoterics-tab.jpg b/images/help/esoterics-tab.jpg new file mode 100644 index 00000000..5c3f3d33 Binary files /dev/null and b/images/help/esoterics-tab.jpg differ diff --git a/images/help/facade-and-header-tab.jpg b/images/help/facade-and-header-tab.jpg new file mode 100644 index 00000000..7174a919 Binary files /dev/null and b/images/help/facade-and-header-tab.jpg differ diff --git a/images/help/gear-tab.jpg b/images/help/gear-tab.jpg new file mode 100644 index 00000000..0602cb7d Binary files /dev/null and b/images/help/gear-tab.jpg differ diff --git a/images/help/profile-tab.jpg b/images/help/profile-tab.jpg new file mode 100644 index 00000000..a3315711 Binary files /dev/null and b/images/help/profile-tab.jpg differ diff --git a/images/help/skills-magic-ritual.jpg b/images/help/skills-magic-ritual.jpg new file mode 100644 index 00000000..8b2629b1 Binary files /dev/null and b/images/help/skills-magic-ritual.jpg differ diff --git a/images/help/skills-tab.jpg b/images/help/skills-tab.jpg new file mode 100644 index 00000000..7f112586 Binary files /dev/null and b/images/help/skills-tab.jpg differ diff --git a/images/icons/svg/book.svg b/images/icons/svg/book.svg new file mode 100644 index 00000000..19a676db --- /dev/null +++ b/images/icons/svg/book.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/svg/dice-d20.svg b/images/icons/svg/dice-d20.svg new file mode 100644 index 00000000..498558c7 --- /dev/null +++ b/images/icons/svg/dice-d20.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/svg/dice-d6.svg b/images/icons/svg/dice-d6.svg new file mode 100644 index 00000000..652ec8c9 --- /dev/null +++ b/images/icons/svg/dice-d6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/svg/monster-silhouette.svg b/images/svg/monster-silhouette.svg new file mode 100644 index 00000000..963b5910 --- /dev/null +++ b/images/svg/monster-silhouette.svg @@ -0,0 +1,252 @@ + + + diff --git a/module/actor/actor.js b/module/actor/actor.js index 03ceb63f..634401a5 100644 --- a/module/actor/actor.js +++ b/module/actor/actor.js @@ -27,7 +27,10 @@ export class HarnMasterActor extends Actor { if (data.type === 'character') { // Request whether to initialize skills and armor locations if (options.skipDefaultItems) { - return super.create(data, options); + HarnMasterActor.setupDefaultDescription(data); + const actor = await super.create(data, options); + const updateData = HarnMasterActor.setupDefaultDescription('character'); + return actor.update(updateData); } new Dialog({ @@ -39,23 +42,29 @@ export class HarnMasterActor extends Actor { callback: async dlg => { await HarnMasterActor._createDefaultCharacterSkills(data); HarnMasterActor._createDefaultHumanoidLocations(data); - return super.create(data, options); // Follow through the the rest of the Actor creation process upstream + const actor = await super.create(data, options); // Follow through the the rest of the Actor creation process upstream + const updateData = HarnMasterActor.setupDefaultDescription('character'); + return actor.update(updateData); } }, no: { label: 'No', callback: async dlg => { - return super.create(data, options); // Do not add new items, continue with the rest of the Actor creation process upstream + HarnMasterActor.setupDefaultDescription(data); + const actor = await super.create(data, options); // Do not add new items, continue with the rest of the Actor creation process upstream + const updateData = HarnMasterActor.setupDefaultDescription('character'); + return actor.update(updateData); } }, }, default: 'yes' }).render(true); } else if (data.type === 'creature') { - // Create Creature Default Skills - this._createDefaultCreatureSkills(data).then(result => { - super.create(data, options); // Follow through the the rest of the Actor creation process upstream + this._createDefaultCreatureSkills(data).then(async result => { + const actor = await super.create(data, options); // Follow through the the rest of the Actor creation process upstream + const updateData = HarnMasterActor.setupDefaultDescription('creature'); + return actor.update(updateData); }); } else if (data.type === 'container') { const html = await renderTemplate("systems/hm3/templates/dialog/container-size.html", {}); @@ -69,574 +78,264 @@ export class HarnMasterActor extends Actor { const formdata = fd.toObject(); const maxCapacity = parseInt(formdata.maxCapacity); const actor = await super.create(data, options); // Follow through the the rest of the Actor creation process upstream - return actor.update({ - "img": "systems/hm3/images/icons/svg/chest.svg", - "bioImage": "systems/hm3/images/icons/svg/chest.svg", - "data.capacity.max": maxCapacity - }); + const updateData = HarnMasterActor.setupDefaultDescription('container'); + updateData['data.capacity.max'] = maxCapacity; + return actor.update(updateData); } }); } else { - super.create(data, options); // Follow through the the rest of the Actor creation process upstream + console.error(`HM3 | Unsupported actor type '${data.type}'`); } } - static async _createDefaultCharacterSkills(data) { - let itemData; - - const physicalSkills = await game.packs.find(p => p.collection === `hm3.std-skills-physical`).getContent(); - itemData = duplicate(physicalSkills.find(i => i.name === 'Climbing')); - data.items.push(new Item({name: itemData.name, type: itemData.type, img: itemData.img, data: itemData.data}).data); - itemData = duplicate(physicalSkills.find(i => i.name === 'Jumping')); - data.items.push(new Item({name: itemData.name, type: itemData.type, img: itemData.img, data: itemData.data}).data); - itemData = duplicate(physicalSkills.find(i => i.name === 'Stealth')); - data.items.push(new Item({name: itemData.name, type: itemData.type, img: itemData.img, data: itemData.data}).data); - itemData = duplicate(physicalSkills.find(i => i.name === 'Throwing')); - data.items.push(new Item({name: itemData.name, type: itemData.type, img: itemData.img, data: itemData.data}).data); - - const commSkills = await game.packs.find(p => p.collection === `hm3.std-skills-communication`).getContent(); - itemData = duplicate(commSkills.find(i => i.name === 'Awareness')); - data.items.push(new Item({name: itemData.name, type: itemData.type, img: itemData.img, data: itemData.data}).data); - itemData = duplicate(commSkills.find(i => i.name === 'Intrigue')); - data.items.push(new Item({name: itemData.name, type: itemData.type, img: itemData.img, data: itemData.data}).data); - itemData = duplicate(commSkills.find(i => i.name === 'Oratory')); - data.items.push(new Item({name: itemData.name, type: itemData.type, img: itemData.img, data: itemData.data}).data); - itemData = duplicate(commSkills.find(i => i.name === 'Rhetoric')); - data.items.push(new Item({name: itemData.name, type: itemData.type, img: itemData.img, data: itemData.data}).data); - itemData = duplicate(commSkills.find(i => i.name === 'Singing')); - data.items.push(new Item({name: itemData.name, type: itemData.type, img: itemData.img, data: itemData.data}).data); - - const combatSkills = await game.packs.find(p => p.collection === `hm3.std-skills-combat`).getContent(); - itemData = duplicate(combatSkills.find(i => i.name === 'Initiative')); - data.items.push(new Item({name: itemData.name, type: itemData.type, img: itemData.img, data: itemData.data}).data); - itemData = duplicate(combatSkills.find(i => i.name === 'Unarmed')); - data.items.push(new Item({name: itemData.name, type: itemData.type, img: itemData.img, data: itemData.data}).data); - itemData = duplicate(combatSkills.find(i => i.name === 'Dodge')); - data.items.push(new Item({name: itemData.name, type: itemData.type, img: itemData.img, data: itemData.data}).data); + static setupDefaultDescription(type) { + const updateData = {}; + if (type === 'character') { + updateData['data.description'] = '
Apparent Age | \n\n |
Culture | \n\n |
Social Class | \n\n |
Height | \n\n |
Frame | \n\n |
Weight | \n\n |
Appearance/Comeliness | \n\n |
Hair Color | \n\n |
Eye Color | \n\n |
Voice | \n\n |
Obvious Medical Traits | \n\n |
Apparent Occupation | \n\n |
Apparent Wealth | \n\n |
Weapons | \n\n |
Armour | \n\n |
Companions | \n\n |
Other obvious features | \n\n |
\n
'; + updateData['data.biography'] = '
Birthdate | \n\n |
Birthplace | \n\n |
Sibling Rank | \nx of y | \n
Parent(s) | \n\n |
Parent Occupation | \n\n |
Estrangement | \n\n |
Clanhead | \n\n |
Medical Traits | \n\n |
Psyche Traits | \n\n |
Habitat | \n\n |
Height | \n\n |
Weight | \n\n |
Diet | \n\n |
Lifespan | \n\n |
Group | \n\n |
Describe any special abilities.
\nDescribe methods of attack.
\nDescribe behavioral aspects.
'; + updateData['data.bioImage'] = 'systems/hm3/images/svg/monster-silhouette.svg'; + } else if (type === 'container') { + updateData['data.description'] = ''; + updateData['data.bioImage'] = 'systems/hm3/images/icons/svg/chest.svg'; + updateData['img'] = 'systems/hm3/images/icons/svg/chest.svg'; + } + return updateData; } - static async _createDefaultCreatureSkills(data) { - let itemData; + /** + * When prepareBaseData() runs, the Actor.items map is not available, or if it is, it + * is not dependable. The very next method will update the Actor.items map using + * information from the Actor.data.items array. So, at this point we may safely + * use Actor.data.items, so long as we remember that that data is going to be going + * through a prepareData() stage next. + * + * @override */ + prepareBaseData() { + super.prepareBaseData(); + const actorData = this.data; + const data = actorData.data; - const combatSkills = await game.packs.find(p => p.collection === `hm3.std-skills-combat`).getContent(); - itemData = duplicate(combatSkills.find(i => i.name === 'Initiative')); - data.items.push(new Item({name: itemData.name, type: itemData.type, img: itemData.img, data: itemData.data}).data); - itemData = duplicate(combatSkills.find(i => i.name === 'Unarmed')); - data.items.push(new Item({name: itemData.name, type: itemData.type, img: itemData.img, data: itemData.data}).data); - itemData = duplicate(combatSkills.find(i => i.name === 'Dodge')); - data.items.push(new Item({name: itemData.name, type: itemData.type, img: itemData.img, data: itemData.data}).data); - } + // Ephemeral data is kept together with other actor data, + // but it is not in the data model so it will not be saved. + if (!data.eph) data.eph = {}; + const eph = data.eph; - static _createDefaultHumanoidLocations(data) { - let armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Skull']) - data.items.push((new Item({ name: 'Skull', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Face']) - data.items.push((new Item({ name: 'Face', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Neck']) - data.items.push((new Item({ name: 'Neck', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Shoulder']) - data.items.push((new Item({ name: 'Left Shoulder', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Shoulder']) - data.items.push((new Item({ name: 'Right Shoulder', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Upper Arm']) - data.items.push((new Item({ name: 'Left Upper Arm', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Upper Arm']) - data.items.push((new Item({ name: 'Right Upper Arm', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Elbow']) - data.items.push((new Item({ name: 'Left Elbow', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Elbow']) - data.items.push((new Item({ name: 'Right Elbow', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Forearm']) - data.items.push((new Item({ name: 'Left Forearm', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Forearm']) - data.items.push((new Item({ name: 'Right Forearm', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Hand']) - data.items.push((new Item({ name: 'Left Hand', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Hand']) - data.items.push((new Item({ name: 'Right Hand', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Thorax']) - data.items.push((new Item({ name: 'Thorax', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Abdomen']) - data.items.push((new Item({ name: 'Abdomen', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Groin']) - data.items.push((new Item({ name: 'Groin', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Hip']) - data.items.push((new Item({ name: 'Left Hip', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Hip']) - data.items.push((new Item({ name: 'Right Hip', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Thigh']) - data.items.push((new Item({ name: 'Left Thigh', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Thigh']) - data.items.push((new Item({ name: 'Right Thigh', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Knee']) - data.items.push((new Item({ name: 'Left Knee', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Knee']) - data.items.push((new Item({ name: 'Right Knee', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Calf']) - data.items.push((new Item({ name: 'Left Calf', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Calf']) - data.items.push((new Item({ name: 'Right Calf', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Foot']) - data.items.push((new Item({ name: 'Left Foot', type: 'armorlocation', data: armorLocationData })).data); - armorLocationData = {}; - mergeObject(armorLocationData, game.system.model.Item.armorlocation); - mergeObject(armorLocationData, HM3.injuryLocations['Foot']) - data.items.push((new Item({ name: 'Right Foot', type: 'armorlocation', data: armorLocationData })).data); - } + if (actorData.type === 'container') { + this._prepareBaseContainerData(actorData); + return; + } - /** - * Augment the basic actor data with additional dynamic data. - */ - prepareData() { - super.prepareData(); + // Calculate endurance + if (!data.hasCondition) { + data.endurance = Math.round((data.abilities.strength.base + data.abilities.stamina.base + + data.abilities.will.base) / 3); + } - const actorData = this.data; - const data = actorData.data; - const flags = actorData.flags; - const items = this.items; + // Safety net: We divide things by endurance, so ensure it is > 0 + data.endurance = Math.max(data.endurance || 1, 1); + + data.encumbrance = Math.floor(data.totalWeight / data.endurance); + + // Setup temporary work values masking the base values + eph.move = data.move.base; + eph.fatigue = data.fatigue; + eph.strength = data.abilities.strength.base; + eph.stamina = data.abilities.stamina.base; + eph.dexterity = data.abilities.dexterity.base; + eph.agility = data.abilities.agility.base; + eph.eyesight = data.abilities.eyesight.base; + eph.hearing = data.abilities.hearing.base; + eph.smell = data.abilities.smell.base; + eph.voice = data.abilities.voice.base; + eph.intelligence = data.abilities.intelligence.base; + eph.will = data.abilities.will.base; + eph.aura = data.abilities.aura.base; + eph.morality = data.abilities.morality.base; + eph.comliness = data.abilities.comliness.base; + eph.totalInjuryLevels = data.totalInjuryLevels; + + eph.meleeAMLMod = 0; + eph.meleeDMLMod = 0; + eph.missileAMLMod = 0; // Make separate methods for each Actor type (character, npc, etc.) to keep // things organized. if (actorData.type === 'character') { - this._prepareCharacterData(actorData); + this._prepareBaseCharacterData(actorData); } else if (actorData.type === 'creature') { - this._prepareCreatureData(actorData); - } else if (actorData.type === 'container') { - this._prepareContainerData(actorData); + this._prepareBaseCreatureData(actorData); } + } + _prepareBaseCharacterData(actorData) { } - /** - * Prepare Container type specific data - */ - _prepareContainerData(actorData) { - const data = actorData.data; - if (data.description === '***INIT***') { - data.description = ''; - data.biography = ''; - } + _prepareBaseCreatureData(actorData) { + } - // Calculate container current capacity utilized - const tempData = {}; - this._calcGearWeightTotals(tempData); - data.capacity.value = tempData.totalGearWeight; - data.capacity.pct = Math.round((Math.max(data.capacity.max - data.capacity.value, 0) / data.capacity.max) * 100); + _prepareBaseContainerData(actorData) { + actorData.data.capacity.pct = Math.max(Math.round(1 - (actorData.data.eph.totalGearWeight / (actorData.data.capacity.max || 1))), 0); } - /** - * Prepare Character type specific data - */ - _prepareCharacterData(actorData) { + /** + * Perform data preparation after Items preparation and Active Effects have + * been applied. + * + * At this point the Actor.items map is guaranteed to be availabile, consisting + * of real Items. It is preferable for this method to use those items at this + * point (unlike the situation with prepareBaseData()). + * + * Note that all Active Effects have already been applied by this point, so + * nothing in this method will be affected further by Active Effects. + * + * @override */ + prepareDerivedData() { + super.prepareDerivedData(); + const actorData = this.data; const data = actorData.data; - if (data.description === '***INIT***') { - // Setup default description and biography - data.description = 'Apparent Age | \n\n |
Culture | \n\n |
Social Class | \n\n |
Height | \n\n |
Frame | \n\n |
Weight | \n\n |
Appearance/Comeliness | \n\n |
Hair Color | \n\n |
Eye Color | \n\n |
Voice | \n\n |
Obvious Medical Traits | \n\n |
Apparent Occupation | \n\n |
Apparent Wealth | \n\n |
Weapons | \n\n |
Armour | \n\n |
Companions | \n\n |
Other obvious features | \n\n |
\n
'; - data.biography = '
Birthdate | \n\n |
Birthplace | \n\n |
Sibling Rank | \nx of y | \n
Parent(s) | \n\n |
Parent Occupation | \n\n |
Estrangement | \n\n |
Clanhead | \n\n |
Medical Traits | \n\n |
Psyche Traits | \n\n |
Habitat | \n\n |
Height | \n\n |
Weight | \n\n |
Diet | \n\n |
Lifespan | \n\n |
Group | \n\n |
Describe any special abilities.
\nDescribe methods of attack.
\nDescribe behavioral aspects.
'; - } - - // Calc Endurance (never use condition with creatures) - data.endurance = Math.round((data.abilities.strength.base + data.abilities.stamina.base + - data.abilities.will.base) / 3); - - this._unequipUncarriedGear(data); - - if (data.description.startsWith('Birthdate:
')) { - // Set default creature description instead - data.description = 'Habitat:
\nHeight:
\nWeight:
\n' + - 'Diet:
\nLifespan:
\nGroup:
\nSpecial Abilities:
\n' + - 'Attacks:
\n'; - } - - this._calcInjuryTotal(data); - - this._calcGearWeightTotals(data); - - if (typeof data.loadRating === 'undefined') { - data.loadRating = 0; - } - - data.encumbrance = Math.floor(Math.max(data.totalGearWeight - data.loadRating, 0) / data.endurance); - - // Universal Penalty and Physical Penalty are used to calculate many - // things, including effectiveMasteryLevel for all skills, - // move, etc. - data.universalPenalty = data.totalInjuryLevels + data.fatigue; - data.physicalPenalty = data.universalPenalty + data.encumbrance; - - data.shockIndex.value = HarnMasterActor._normProb(data.endurance, data.universalPenalty * 3.5, data.universalPenalty); - if (canvas) this.getActiveTokens().forEach(token => { - if (token.bars) token._onUpdateBarAttributes(this.data, { "shockIndex.value": data.shockIndex.value }); - }); + data.move.effective = Math.max(eph.move - data.physicalPenalty, 0); // Setup effective abilities (accounting for UP and PP) this._setupEffectiveAbilities(data); - // Go through all skills calculating their EML - this._calcSkillEMLWithPenalties(this.data.items, data.universalPenalty, data.physicalPenalty); - - // Some properties are calculated from skills. Do that here. - this._setPropertiesFromSkills(this.data.items, data); - - // Calculate current Move speed. Cannot go below 0 - data.move.effective = Math.max(data.move.base - data.physicalPenalty, 0); - - this._setupGearData(data); - } - - _setupEffectiveAbilities(data) { - // Affected by physical penalty - data.abilities.strength.effective = Math.max(data.abilities.strength.base - data.physicalPenalty, 0); - data.abilities.stamina.effective = Math.max(data.abilities.stamina.base - data.physicalPenalty, 0); - data.abilities.agility.effective = Math.max(data.abilities.agility.base - data.physicalPenalty, 0); - data.abilities.dexterity.effective = Math.max(data.abilities.dexterity.base - data.physicalPenalty, 0); - data.abilities.eyesight.effective = Math.max(data.abilities.eyesight.base - data.physicalPenalty, 0); - data.abilities.hearing.effective = Math.max(data.abilities.hearing.base - data.physicalPenalty, 0); - data.abilities.smell.effective = Math.max(data.abilities.smell.base - data.physicalPenalty, 0); - data.abilities.voice.effective = Math.max(data.abilities.voice.base - data.physicalPenalty, 0); - - // Affected by universal penalty - data.abilities.intelligence.effective = Math.max(data.abilities.intelligence.base - data.universalPenalty, 0); - data.abilities.aura.effective = Math.max(data.abilities.aura.base - data.universalPenalty, 0); - data.abilities.will.effective = Math.max(data.abilities.will.base - data.universalPenalty, 0); - - // Not affected by any penalties - data.abilities.comliness.effective = Math.max(data.abilities.comliness.base, 0); - data.abilities.morality.effective = Math.max(data.abilities.morality.base, 0); - } - - _setupInjuryTargets(data) { - this.data.items.forEach(it => { - if (it.type === 'injury') { - // Injury Roll = HR*End (unaffected by UP or PP) - it.data.targetHealRoll = it.data.healRate * data.endurance; - } - }); - } - - /** - * Endurance is composed of two parts: the maximum and the current value. - * This method calculates the max. It tries to find if the "condition" - * skill is present, and if so uses that to calculate the max. Otherwise - * it calculates it based on ability scores. - * - * @param {Object} items - * @param {Object} data - */ - _calcEndurance(items, data) { - let hasCondition = false; - let conditionLevel = 0; - items.forEach(it => { - if (it.type === 'skill' && it.name.toLowerCase() === 'condition') { - hasCondition = true; - conditionLevel = it.data.masteryLevel; - } - }); - - // If we found the condition skill, then use that value for endurance. - // Otherwise, calculate it. - if (hasCondition) { - data.hasCondition = true; - data.endurance = Math.floor(conditionLevel / 5); - } else { - data.hasCondition = false; - data.endurance = Math.round((data.abilities.strength.base + data.abilities.stamina.base + - data.abilities.will.base) / 3); - } - - // Safety net: if endurance is ever <= 0, then set it to 1 - // so a bunch of other stuff doesn't go to infinity - if (data.endurance <= 0) { - data.endurance = 1; - } - } - - /** - * This is broken out into a separate method so we can invoke it very early. - * Lots of stuff will be dependent on whether gear is carried or equipped. - */ - _unequipUncarriedGear(data) { - this.data.items.forEach(it => { - if (it.type.endsWith('gear')) { - // If you aren't carrying the gear, it can't be equipped - if (!it.data.isCarried) { - it.data.isEquipped = false; - } - - if (it.data.container && it.data.container != 'on-person') { - // Anything in a container is unequipped automatically - it.data.isEquipped = false; - - // If an item is in a container, its "isCarried" flag must be the - // same as the container. - const container = this.items.get(it.data.container); - if (container) it.data.isCarried = container.data.data.isCarried; - } - } - }); - } - - /** - * Consolidated method to setup all gear, including misc gear, weapons, - * and missiles. (not armor yet) - */ - _setupGearData(data) { - this.data.items.forEach(it => { - if (it.type.endsWith('gear')) { - - // Collect all combat skills into a map for use later - let combatSkills = {}; - this.data.items.forEach(it => { - if (it.type === 'skill' || it.name.toLowerCase() === 'throwing') { - combatSkills[it.name] = { - 'name': it.name, - 'eml': it.data.effectiveMasteryLevel - }; - } - }); - - if (it.type === 'missilegear') { - // Reset mastery levels in case nothing matches - it.data.attackMasteryLevel = 0; - - let missileName = it.name; - - // If the associated skill is in our combat skills list, get EML from there - // and then calculate AML. - let assocSkill = it.data.assocSkill; - if (typeof combatSkills[assocSkill] != 'undefined') { - let skillEml = combatSkills[assocSkill].eml; - it.data.attackMasteryLevel = skillEml + it.data.attackModifier; - } - } else if (it.type === 'weapongear') { - // Reset mastery levels in case nothing matches - it.data.attackMasteryLevel = 0; - it.data.defenseMasteryLevel = 0; - let weaponName = it.name; - - // If associated skill is 'None', see if there is a skill with the - // same name as the weapon; if so, then set it to that skill. - if (it.data.assocSkill === 'None') { - // If no combat skill with this name exists, search for next weapon - if (typeof combatSkills[weaponName] === 'undefined') return; - - // A matching skill was found, set associated Skill to that combat skill - it.data.assocSkill = combatSkills[weaponName].name; - } + // Calculate Important Roll Targets + eph.stumbleTarget = Math.max(eph.agility, 0); + eph.fumbleTarget = Math.max(eph.dexterity, 0); - // At this point, we know the Associated Skill is not blank. If that - // associated skill is in our combat skills list, get EML from there - // and then calculate AML and DML. - let assocSkill = it.data.assocSkill; - if (typeof combatSkills[assocSkill] != 'undefined') { - let skillEml = combatSkills[assocSkill].eml; - it.data.attackMasteryLevel = skillEml + it.data.attack + it.data.attackModifier; - it.data.defenseMasteryLevel = skillEml + it.data.defense; - } - } - } + // Process all the final post activities for Items + this.items.forEach(it => { + it.prepareDerivedData(); }); - } - - _setPropertiesFromSkills(items, data) { - data.hasCondition = false; - items.forEach(it => { - if (it.type === 'skill') { - switch (it.name.toLowerCase()) { - case 'initiative': - data.initiative = it.data.effectiveMasteryLevel; - break; + // Calculate spell effective mastery level values + this._refreshSpellsAndInvocations(); - case 'dodge': - data.dodge = it.data.effectiveMasteryLevel; - break; + // Collect all combat skills into a map for use later + let combatSkills = {}; + this.items.forEach(it => { + if (it.data.type === 'skill' && + (it.data.data.type === 'Combat' || it.data.name.toLowerCase() === 'throwing')) { + combatSkills[it.data.name] = { + 'name': it.data.name, + 'eml': it.data.data.effectiveMasteryLevel } } }); - } - - _calcSkillEMLWithPenalties(items, universalPenalty, physicalPenalty) { - const pctUnivPen = universalPenalty * 5; - const pctPhysPen = physicalPenalty * 5; - items.forEach(it => { - if (it.type === 'skill') { - switch (it.data.type) { - case 'Combat': - case 'Physical': - it.data.effectiveMasteryLevel = Math.max(it.data.masteryLevel - pctPhysPen, 5); - break; + this._setupWeaponData(combatSkills); - default: - it.data.effectiveMasteryLevel = Math.max(it.data.masteryLevel - pctUnivPen, 5); + this._generateArmorLocationMap(data); - } - } else if (it.type === 'psionic') { - it.data.effectiveMasteryLevel = Math.max(it.data.masteryLevel - pctUnivPen, 5); - } - }); + return; } - _calcGearWeightTotals(data) { - data.totalWeaponWeight = 0; - data.totalMissileWeight = 0; - data.totalArmorWeight = 0; - data.totalMiscGearWeight = 0; + + /** + * Calculate the weight of the gear. Note that this is somewhat redundant + * with the calculation being done during item create/update/delete, + * but here we are generating a much more fine-grained set of data + * regarding the weight distribution. + */ + _calcGearWeightTotals() { + const eph = this.data.data.eph; + + eph.totalWeaponWeight = 0; + eph.totalMissileWeight = 0; + eph.totalArmorWeight = 0; + eph.totalMiscGearWeight = 0; let tempWeight = 0; - this.itemTypes.containergear.forEach(it => { - it.data.data.capacity.value = 0; + // Initialize all container capacity values + this.items.forEach(it => { + if (it.data.type === 'containergear') it.data.data.capacity.value = 0; }); - this.data.items.forEach(it => { - switch (it.type) { + this.items.forEach(it => { + const itemData = it.data; + const data = itemData.data; + if (itemData.type.endsWith('gear')) { + // If the gear is inside of a container, then the "carried" + // flag is inherited from the container. + if (data.container && data.container !== 'on-person') { + const container = this.data.items.find(i => i._id === data.container); + if (container) data.isCarried = container.data.isCarried; + } + } + + switch (itemData.type) { case 'weapongear': - if (!it.data.isCarried) break; - tempWeight = it.data.weight * it.data.quantity; + if (!data.isCarried) break; + tempWeight = data.weight * data.quantity; if (tempWeight < 0) tempWeight = 0; - data.totalWeaponWeight += tempWeight; + eph.totalWeaponWeight += tempWeight; break; case 'missilegear': - if (!it.data.isCarried) break; - tempWeight = it.data.weight * it.data.quantity; + if (!data.isCarried) break; + tempWeight = data.weight * data.quantity; if (tempWeight < 0) tempWeight = 0; - data.totalMissileWeight += tempWeight; + eph.totalMissileWeight += tempWeight; break; case 'armorgear': - if (!it.data.isCarried) break; - tempWeight = it.data.weight * it.data.quantity; + if (!data.isCarried) break; + tempWeight = data.weight * data.quantity; if (tempWeight < 0) tempWeight = 0; - data.totalArmorWeight += tempWeight; + eph.totalArmorWeight += tempWeight; break; case 'miscgear': case 'containergear': - if (!it.data.isCarried) break; - tempWeight = it.data.weight * it.data.quantity; + if (!data.isCarried) break; + tempWeight = data.weight * data.quantity; if (tempWeight < 0) tempWeight = 0; - data.totalMiscGearWeight += tempWeight; + eph.totalMiscGearWeight += tempWeight; break; } - if (it.type.endsWith('gear')) { - const cid = it.data.container; + if (itemData.type.endsWith('gear')) { + const cid = data.container; if (cid && cid != 'on-person') { const container = this.items.get(cid); - container.data.data.capacity.value = Math.round((container.data.data.capacity.value + tempWeight + Number.EPSILON)*100)/100; + if (container) container.data.data.capacity.value = + Math.round((container.data.data.capacity.value + tempWeight + Number.EPSILON) * 100) / 100; } } }); @@ -644,56 +343,135 @@ export class HarnMasterActor extends Actor { // It seems whenever doing math on floating point numbers, very small // amounts get introduced creating very long decimal values. // Correct any math weirdness; keep to two decimal points - data.totalArmorWeight = Math.round((data.totalArmorWeight + Number.EPSILON) * 100) / 100; - data.totalWeaponWeight = Math.round((data.totalWeaponWeight + Number.EPSILON) * 100) / 100; - data.totalMissileWeight = Math.round((data.totalMissileWeight + Number.EPSILON) * 100) / 100; - data.totalMiscGearWeight = Math.round((data.totalMiscGearWeight + Number.EPSILON) * 100) / 100; + eph.totalArmorWeight = Math.round((eph.totalArmorWeight + Number.EPSILON) * 100) / 100; + eph.totalWeaponWeight = Math.round((eph.totalWeaponWeight + Number.EPSILON) * 100) / 100; + eph.totalMissileWeight = Math.round((eph.totalMissileWeight + Number.EPSILON) * 100) / 100; + eph.totalMiscGearWeight = Math.round((eph.totalMiscGearWeight + Number.EPSILON) * 100) / 100; - data.totalGearWeight = data.totalWeaponWeight + data.totalMissileWeight + data.totalArmorWeight + data.totalMiscGearWeight; - data.totalGearWeight = Math.round((data.totalGearWeight + Number.EPSILON) * 100) / 100; + eph.totalGearWeight = eph.totalWeaponWeight + eph.totalMissileWeight + eph.totalArmorWeight + eph.totalMiscGearWeight; + eph.totalGearWeight = Math.round((eph.totalGearWeight + Number.EPSILON) * 100) / 100; } - _calcInjuryTotal(data) { - let totalInjuryLevels = 0; - this.data.items.forEach(it => { - if (it.type === 'injury') { - // Just make sure if injuryLevel is negative, we set it to zero - if (it.data.injuryLevel < 0) it.data.injuryLevel = 0; - - totalInjuryLevels += it.data.injuryLevel; - if (it.data.injuryLevel === 0) { - it.data.severity = ''; - } else if (it.data.injuryLevel == 1) { - it.data.severity = 'M1'; - } else if (it.data.injuryLevel <= 3) { - it.data.severity = 'S' + it.data.injuryLevel; - } else { - it.data.severity = 'G' + it.data.injuryLevel; + + /** + * Prepare Container type specific data + */ + _prepareBaseContainerData(actorData) { + const data = actorData.data; + if (data.description === '***INIT***') { + data.description = ''; + data.biography = ''; + } + + // Calculate container current capacity utilized + const tempData = {}; + // TODO! -- this._calcGearWeightTotals(tempData); + data.capacity.value = tempData.totalGearWeight; + data.capacity.max = data.capacity.max || 1; + data.capacity.pct = Math.round((Math.max(data.capacity.max - data.capacity.value, 0) / data.capacity.max) * 100); + } + + _prepareDerivedContainerData(actorData) { + + } + + _setupEffectiveAbilities(data) { + const eph = this.data.data.eph; + + // Affected by physical penalty + data.abilities.strength.effective = Math.max(Math.round(eph.strength + Number.EPSILON) - data.physicalPenalty, 0); + data.abilities.stamina.effective = Math.max(Math.round(eph.stamina + Number.EPSILON) - data.physicalPenalty, 0); + data.abilities.agility.effective = Math.max(Math.round(eph.agility + Number.EPSILON) - data.physicalPenalty, 0); + data.abilities.dexterity.effective = Math.max(Math.round(eph.dexterity + Number.EPSILON) - data.physicalPenalty, 0); + data.abilities.eyesight.effective = Math.max(Math.round(eph.eyesight + Number.EPSILON) - data.physicalPenalty, 0); + data.abilities.hearing.effective = Math.max(Math.round(eph.hearing + Number.EPSILON) - data.physicalPenalty, 0); + data.abilities.smell.effective = Math.max(Math.round(eph.smell + Number.EPSILON) - data.physicalPenalty, 0); + data.abilities.voice.effective = Math.max(Math.round(eph.voice + Number.EPSILON) - data.physicalPenalty, 0); + + // Affected by universal penalty + data.abilities.intelligence.effective = Math.max(Math.round(eph.intelligence + Number.EPSILON) - data.universalPenalty, 0); + data.abilities.aura.effective = Math.max(Math.round(eph.aura + Number.EPSILON) - data.universalPenalty, 0); + data.abilities.will.effective = Math.max(Math.round(eph.will + Number.EPSILON) - data.universalPenalty, 0); + + // Not affected by any penalties + data.abilities.comliness.effective = Math.max(Math.round(eph.comliness + Number.EPSILON), 0); + data.abilities.morality.effective = Math.max(Math.round(eph.morality + Number.EPSILON), 0); + } + + /** + * Consolidated method to setup all gear, including misc gear, weapons, + * and missiles. (not armor yet) + */ + _setupWeaponData(combatSkills) { + const eph = this.data.data.eph; + + // Just ensure we take care of any NaN or other falsy nonsense + if (!eph.missileAMLMod) eph.missileAMLMod = 0; + if (!eph.weaponAMLMod) eph.weaponAMLMod = 0; + if (!eph.weaponDMLMod) eph.weaponDMLMod = 0; + + this.items.forEach(it => { + const itemData = it.data.data; + if (it.data.type === 'missilegear') { + // Reset mastery levels in case nothing matches + itemData.attackMasteryLevel = Math.max(eph.missileAMLMod, 5); + + // If the associated skill is in our combat skills list, get EML from there + // and then calculate AML. + let assocSkill = itemData.assocSkill; + if (typeof combatSkills[assocSkill] !== 'undefined') { + let skillEml = combatSkills[assocSkill].eml; + itemData.attackMasteryLevel = Math.max((skillEml + itemData.attackModifier + eph.missileAMLMod) || 5, 5); + } + } else if (it.data.type === 'weapongear') { + // Reset mastery levels in case nothing matches + itemData.attackMasteryLevel = Math.max(eph.weaponAMLMod, 5); + itemData.defenseMasteryLevel = Math.max(eph.weaponDMLMod, 5); + let weaponName = it.data.name; + + // If associated skill is 'None', see if there is a skill with the + // same name as the weapon; if so, then set it to that skill. + if (itemData.assocSkill === 'None') { + // If no combat skill with this name exists, search for next weapon + if (typeof combatSkills[weaponName] === 'undefined') return; + + // A matching skill was found, set associated Skill to that combat skill + itemData.assocSkill = combatSkills[weaponName].name; + } + + // At this point, we know the Associated Skill is not blank. If that + // associated skill is in our combat skills list, get EML from there + // and then calculate AML and DML. + let assocSkill = itemData.assocSkill; + if (typeof combatSkills[assocSkill] !== 'undefined') { + let skillEml = combatSkills[assocSkill].eml; + itemData.attackMasteryLevel = Math.max((skillEml + itemData.attack + itemData.attackModifier + eph.meleeAMLMod) || 5, 5); + itemData.defenseMasteryLevel = Math.max((skillEml + itemData.defense + eph.meleeDMLMod) || 5, 5); } } }); - - data.totalInjuryLevels = totalInjuryLevels; } _refreshSpellsAndInvocations() { this._resetAllSpellsAndInvocations(); - this.data.items.forEach(it => { - if (it.type === 'skill' && it.data.type === 'Magic') { - this._setConvocationSpells(it.name, it.data.skillBase.value, it.data.masteryLevel, it.data.effectiveMasteryLevel); - } else if (it.type === 'skill' && it.data.type === 'Ritual') { - this._setRitualInvocations(it.name, it.data.skillBase.value, it.data.masteryLevel, it.data.effectiveMasteryLevel); + this.items.forEach(it => { + const itemData = it.data; + if (itemData.type === 'skill' && itemData.data.type === 'Magic') { + this._setConvocationSpells(itemData.name, itemData.data.skillBase.value, itemData.data.masteryLevel, itemData.data.effectiveMasteryLevel); + } else if (itemData.type === 'skill' && itemData.data.type === 'Ritual') { + this._setRitualInvocations(itemData.name, itemData.data.skillBase.value, itemData.data.masteryLevel, itemData.data.effectiveMasteryLevel); } }); } _resetAllSpellsAndInvocations() { - this.data.items.forEach(it => { - if (it.type === 'spell' || it.type === 'invocation') { - it.data.effectiveMasteryLevel = 0; - it.data.skillIndex = 0; - it.data.masteryLevel = 0; - it.data.effectiveMasteryLevel = 0; + this.items.forEach(it => { + const itemData = it.data; + if (itemData.type === 'spell' || itemData.type === 'invocation') { + itemData.data.effectiveMasteryLevel = 0; + itemData.data.skillIndex = 0; + itemData.data.masteryLevel = 0; + itemData.data.effectiveMasteryLevel = 0; } }) } @@ -702,12 +480,13 @@ export class HarnMasterActor extends Actor { if (!convocation || convocation.length == 0) return; let lcConvocation = convocation.toLowerCase(); - this.data.items.forEach(it => { - if (it.type === 'spell' && it.data.convocation && it.data.convocation.toLowerCase() === lcConvocation) { - it.data.effectiveMasteryLevel = Math.max(eml - (it.data.level * 5), 5); - it.data.skillIndex = Math.floor(ml/10); - it.data.masteryLevel = ml; - it.data.skillBase = sb; + this.items.forEach(it => { + const itemData = it.data; + if (itemData.type === 'spell' && itemData.data.convocation && itemData.data.convocation.toLowerCase() === lcConvocation) { + itemData.data.effectiveMasteryLevel = Math.max(eml - (itemData.data.level * 5), 5); + itemData.data.skillIndex = Math.floor(ml / 10); + itemData.data.masteryLevel = ml; + itemData.data.skillBase = sb; } }); } @@ -716,12 +495,13 @@ export class HarnMasterActor extends Actor { if (!diety || diety.length == 0) return; let lcDiety = diety.toLowerCase(); - this.data.items.forEach(it => { - if (it.type === 'invocation' && it.data.diety && it.data.diety.toLowerCase() === lcDiety) { - it.data.effectiveMasteryLevel = Math.max(eml - (it.data.circle * 5), 5); - it.data.skillIndex = Math.floor(ml/10); - it.data.masteryLevel = ml; - it.data.skillBase = sb; + this.items.forEach(it => { + const itemData = it.data; + if (itemData.type === 'invocation' && itemData.data.diety && itemData.data.diety.toLowerCase() === lcDiety) { + itemData.data.effectiveMasteryLevel = Math.max(eml - (itemData.data.circle * 5), 5); + itemData.data.skillIndex = Math.floor(ml / 10); + itemData.data.masteryLevel = ml; + itemData.data.skillBase = sb; } }); } @@ -742,35 +522,38 @@ export class HarnMasterActor extends Actor { } }); - this.data.items.forEach(it => { - if (it.type === 'armorgear' && it.data.isCarried && it.data.isEquipped) { + this.items.forEach(it => { + const itemData = it.data; + const data = itemData.data; + + if (itemData.type === 'armorgear' && data.isCarried && data.isEquipped) { // Go through all of the armor locations for this armor, // applying this armor's settings to each location // If locations doesn't exist, then just abandon and continue - if (typeof it.data.locations === 'undefined') { + if (typeof data.locations === 'undefined') { return; } - it.data.locations.forEach(l => { + data.locations.forEach(l => { // If the location is unknown, skip the rest if (typeof armorMap[l] != 'undefined') { // Add this armor's protection to the location - if (typeof it.data.protection != 'undefined') { - armorMap[l].blunt += it.data.protection.blunt; - armorMap[l].edged += it.data.protection.edged; - armorMap[l].piercing += it.data.protection.piercing; - armorMap[l].fire += it.data.protection.fire; + if (typeof data.protection != 'undefined') { + armorMap[l].blunt += data.protection.blunt; + armorMap[l].edged += data.protection.edged; + armorMap[l].piercing += data.protection.piercing; + armorMap[l].fire += data.protection.fire; } // if a material has been specified, add it to the layers - if (it.data.material.length > 0) { + if (data.material.length > 0) { if (armorMap[l].layers.length > 0) { armorMap[l].layers += ','; } - armorMap[l].layers += it.data.material; + armorMap[l].layers += data.material; } } @@ -785,109 +568,24 @@ export class HarnMasterActor extends Actor { // We now have a full map of all of the armor, let's apply it to // existing armor locations - this.data.items.forEach(it => { - if (it.type === 'armorlocation') { - let armorProt = armorArray.find(a => a.name === it.data.impactType); + this.items.forEach(it => { + const itemData = it.data; + const data = itemData.data; + if (itemData.type === 'armorlocation') { + let armorProt = armorArray.find(a => a.name === data.impactType); // We will ignore any armorProt if there is no armor values specified if (armorProt) { - it.data.blunt = armorProt.blunt; - it.data.edged = armorProt.edged; - it.data.piercing = armorProt.piercing; - it.data.fire = armorProt.fire; - it.data.layers = armorProt.layers; + data.blunt = armorProt.blunt; + data.edged = armorProt.edged; + data.piercing = armorProt.piercing; + data.fire = armorProt.fire; + data.layers = armorProt.layers; } } }); } - stdRoll(label, options = {}) { - - const rollData = { - label: label, - target: options.target, - fastforward: options.fastforward, - data: this.data, - speaker: ChatMessage.getSpeaker({ actor: this }) - }; - - return DiceHM3.d100StdRoll(rollData); - } - - d6Roll(label, options = {}) { - - const rollData = { - label: label, - target: options.target, - numdice: options.numdice, - fastforward: options.fastforward, - data: this.data, - speaker: ChatMessage.getSpeaker({ actor: this }) - }; - - return DiceHM3.d6Roll(rollData); - } - - damageRoll(weaponName) { - - const rollData = { - weapon: weaponName, - data: this.data, - speaker: ChatMessage.getSpeaker({ actor: this }) - }; - - return DiceHM3.damageRoll(rollData); - } - - missileDamageRoll(eventData) { - const rollData = { - name: eventData.missile, - aspect: eventData.aspect, - impactShort: eventData.impactShort, - impactMedium: eventData.impactMedium, - impactLong: eventData.impactLong, - impactExtreme: eventData.impactExtreme, - data: this.data, - speaker: ChatMessage.getSpeaker({ actor: this }) - } - - return DiceHM3.missileDamageRoll(rollData); - } - - missileAttackRoll(eventData) { - const rollData = { - name: eventData.missile, - target: eventData.target, - aspect: eventData.aspect, - rangeShort: eventData.rangeShort, - rangeMedium: eventData.rangeMedium, - rangeLong: eventData.rangeLong, - rangeExtreme: eventData.rangeExtreme, - data: this.data, - speaker: ChatMessage.getSpeaker({ actor: this }) - } - return DiceHM3.missileAttackRoll(rollData); - } - - injuryRoll() { - const rollData = { - actor: this, - speaker: ChatMessage.getSpeaker({ actor: this }) - }; - - return DiceHM3.injuryRoll(rollData); - } - - _d100StdRoll(stdRollData) { - const rollData = mergeObject({ data: this.data }, stdRollData); - return DiceHM3.d100StdRoll(rollData); - } - - _d6StdRoll(stdRollData) { - const rollData = mergeObject({ data: this.data }, stdRollData); - return DiceHM3.d6Roll(rollData); - } - /** @override */ _onDeleteEmbeddedEntity(embeddedName, child, options, userId) { if (embeddedName === "OwnedItem") { @@ -900,38 +598,40 @@ export class HarnMasterActor extends Actor { } } - static _normalcdf(x){ - var t=1/(1+.2316419*Math.abs(x)); - var d=.3989423*Math.exp(-x*x/2); - var prob=d*t*(.3193815+t*(-.3565638+t*(1.781478+t*(-1.821256+t*1.330274)))); - if (x>0) { - prob=1-prob + static _normalcdf(x) { + var t = 1 / (1 + .2316419 * Math.abs(x)); + var d = .3989423 * Math.exp(-x * x / 2); + var prob = d * t * (.3193815 + t * (-.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274)))); + if (x > 0) { + prob = 1 - prob } return prob - } - + } + static _normProb(z, mean, sd) { let prob; - if (sd==0) { - prob = z
Do you want to perform a Skill Development Roll (SDR), or just disable the flag?
' @@ -945,7 +1003,7 @@ export class HarnMasterBaseActorSheet extends ActorSheet { performSDR: { label: "Perform SDR", callback: async (html) => { - return await this.actor.skillDevRoll(item); + return await HarnMasterActor.skillDevRoll(item); } }, disableFlag: { @@ -968,7 +1026,7 @@ export class HarnMasterBaseActorSheet extends ActorSheet { const type = header.dataset.type; const data = duplicate(header.dataset); const li = $(header).parents(".item"); - const itemId=li.data("itemId"); + const itemId = li.data("itemId"); if (itemId) { const item = this.actor.items.get(itemId); @@ -1000,14 +1058,14 @@ export class HarnMasterBaseActorSheet extends ActorSheet { const chatTemplate = 'systems/hm3/templates/chat/esoteric-desc-card.html'; const html = await renderTemplate(chatTemplate, chatData); - + const messageData = { user: game.user._id, speaker: ChatMessage.getSpeaker(), content: html.trim(), type: CONST.CHAT_MESSAGE_TYPES.OTHER }; - + // Create a chat message return ChatMessage.create(messageData); } diff --git a/module/actor/character-sheet.js b/module/actor/character-sheet.js index 53e8e047..8aa5b2ee 100644 --- a/module/actor/character-sheet.js +++ b/module/actor/character-sheet.js @@ -10,7 +10,7 @@ export class HarnMasterCharacterSheet extends HarnMasterBaseActorSheet { static get defaultOptions() { return mergeObject(super.defaultOptions, { classes: ["hm3", "sheet", "actor", "character"], - width: 660, + width: 700, height: 640, tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "facade" }] }); diff --git a/module/actor/container-sheet.js b/module/actor/container-sheet.js index a4e6af4e..0501427b 100644 --- a/module/actor/container-sheet.js +++ b/module/actor/container-sheet.js @@ -10,7 +10,7 @@ export class HarnMasterContainerSheet extends HarnMasterBaseActorSheet { static get defaultOptions() { return mergeObject(super.defaultOptions, { classes: ["hm3", "sheet", "actor", "container"], - width: 660, + width: 700, height: 640, tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "facade" }] }); diff --git a/module/actor/creature-sheet.js b/module/actor/creature-sheet.js index accc68d0..45c6dd3d 100644 --- a/module/actor/creature-sheet.js +++ b/module/actor/creature-sheet.js @@ -10,7 +10,7 @@ export class HarnMasterCreatureSheet extends HarnMasterBaseActorSheet { static get defaultOptions() { return mergeObject(super.defaultOptions, { classes: ["hm3", "sheet", "actor", "creature"], - width: 660, + width: 700, height: 640, tabs: [{ navSelector: ".sheet-tabs", contentSelector: ".sheet-body", initial: "facade" }] }); diff --git a/module/combat.js b/module/combat.js index 2e79b284..dd93ae37 100644 --- a/module/combat.js +++ b/module/combat.js @@ -21,7 +21,8 @@ export async function missileAttack(attackToken, defendToken, missileItem) { } if (!isValidToken(attackToken)) { - console.error(`HM3 | meleeAttack attackToken=${attackToken} is not valid.`); + ui.notifications.error(`Attack token not valid.`); + console.error(`HM3 | missileAttack attackToken=${attackToken} is not valid.`); return null; } @@ -32,7 +33,8 @@ export async function missileAttack(attackToken, defendToken, missileItem) { } if (!isValidToken(defendToken)) { - console.error(`HM3 | meleeAttack defendToken=${defendToken} is not valid.`); + ui.notifications.error(`Defender token not valid.`); + console.error(`HM3 | missileAttack defendToken=${defendToken} is not valid.`); return null; } @@ -131,7 +133,8 @@ export async function missileAttack(attackToken, defendToken, missileItem) { if (game.settings.get('hm3', 'combatAudio')) { AudioHelper.play({src: "sounds/drums.wav", autoplay: true, loop: false}, true); } - return null; + + return chatTemplateData; } /** @@ -258,7 +261,8 @@ export async function meleeAttack(attackToken, defendToken, weaponItem=null) { if (game.settings.get('hm3', 'combatAudio')) { AudioHelper.play({src: "sounds/drums.wav", autoplay: true, loop: false}, true); } - return null; + + return chatTemplateData; } /** @@ -502,7 +506,8 @@ function defaultMeleeWeapon(token) { } /** - * Resume the attack with the defender performing the "Dodge" defense. Note that this defense is only applicable to melee attacks. + * Resume the attack with the defender performing the "Counterstrike" defense. + * Note that this defense is only applicable to melee attacks. * * @param {*} atkToken Token representing the attacker * @param {*} defToken Token representing the defender @@ -544,13 +549,15 @@ export async function meleeCounterstrikeResume(atkToken, defToken, atkWeaponName target: atkEffAML }); + const csEffEML = csDialogResult.weapon.data.data.attackMasteryLevel; + // Roll Counterstrike Attack const csRoll = DiceHM3.rollTest({ data: {}, diceSides: 100, diceNum: 1, modifier: csDialogResult.addlModifier, - target: csDialogResult.weapon.data.data.attackMasteryLevel + target: csEffEML }); // If we have "Dice So Nice" module, roll them dice! @@ -605,8 +612,8 @@ export async function meleeCounterstrikeResume(atkToken, defToken, atkWeaponName atkAspect: atkAspect, isAtkStumbleRoll: combatResult.outcome.atkStumble, isAtkFumbleRoll: combatResult.outcome.atkFumble, - isDefStumbleRoll: combatResult.outcome.defStumble, - isDefFumbleRoll: combatResult.outcome.defFumble, + isDefStumbleRoll: null, + isDefFumbleRoll: null, visibleAtkActorId: atkToken.actor.id, visibleDefActorId: defToken.actor.id } @@ -621,9 +628,9 @@ export async function meleeCounterstrikeResume(atkToken, defToken, atkWeaponName mlType: 'AML', addlModifierAbs: Math.abs(csDialogResult.addlModifier), addlModifierSign: csDialogResult.addlModifier < 0?'-':'+', - origEML: csDialogResult.weapon.data.data.attackMasteryLevel, - effEML: csDialogResult.weapon.data.data.attackMasteryLevel + csDialogResult.addlModifier, - effAML: csDialogResult.weapon.data.data.attackMasteryLevel + csDialogResult.addlModifier, + origEML: csEffEML, + effEML: csEffEML + csDialogResult.addlModifier, + effAML: csEffEML + csDialogResult.addlModifier, effDML: 0, attackRoll: csRoll.rollObj.total, atkRollResult: csRoll.description, @@ -640,8 +647,8 @@ export async function meleeCounterstrikeResume(atkToken, defToken, atkWeaponName dta: combatResult.outcome.dta, isAtkStumbleRoll: combatResult.outcome.defStumble, isAtkFumbleRoll: combatResult.outcome.defFumble, - isDefStumbleRoll: combatResult.outcome.atkStumble, - isDefFumbleRoll: combatResult.outcome.atkFumble, + isDefStumbleRoll: null, + isDefFumbleRoll: null, visibleAtkActorId: defToken.actor.id, visibleDefActorId: atkToken.actor.id } @@ -692,7 +699,7 @@ export async function meleeCounterstrikeResume(atkToken, defToken, atkWeaponName // Create a chat message await ChatMessage.create(messageData, messageOptions) - return null; + return {atk: atkChatData, cs: csChatData}; } /** @@ -814,7 +821,7 @@ export async function dodgeResume(atkToken, defToken, type, weaponName, effAML, AudioHelper.play({src: "systems/hm3/audio/swoosh1.ogg", autoplay: true, loop: false}, true); } - return null; + return chatData; } /** @@ -853,10 +860,12 @@ export async function blockResume(atkToken, defToken, type, weaponName, effAML, let defAvailWeapons = defToken.actor.itemTypes.weapongear; const shields = defAvailWeapons.filter(w => w.data.data.isEquipped && /shield|\bbuckler\b/i.test(w.name)); + let atkWeapon = null; // Missile Pre-processing. If attacker is using a high-velocity weapon, then defender // can only block with a shield. If attacker is using a low-velocity weapon, then defender // can either block with a shield (at full DML) or with a melee weapon (at 1/2 DML). if (type === 'missile') { + atkWeapon = atkToken.actor.itemTypes.missilegear.find(w => w.name === weaponName); const highVelocityMissile = /\bbow\b|shortbow|longbow|crossbow|\bsling\b|\barrow\b|\bbolt\b|\bbullet\b/i.test(weaponName); if (highVelocityMissile) { @@ -870,6 +879,8 @@ export async function blockResume(atkToken, defToken, type, weaponName, effAML, } else { prompt = `${weaponName} is a low-velocity missile, and can be blocked either by a shield (at full DML) or by a melee weapon (at ½ DML). Choose wisely.`; } + } else { + atkWeapon = atkToken.actor.itemTypes.weapongear.find(w => w.name === weaponName); } // pop up dialog asking for which weapon to use for blocking @@ -951,7 +962,11 @@ export async function blockResume(atkToken, defToken, type, weaponName, effAML, atkImpactRoll = new Roll(`${combatResult.outcome.atkDice}d6`).roll(); } - const weaponBroke = checkWeaponBreak(atkWeapon, defWeapon); + // If there was a block, check whether a weapon broke + let weaponBroke = {attackWeaponBroke: false, defendWeaponBroke: false}; + if (combatResult.outcome.block) { + weaponBroke = checkWeaponBreak(atkWeapon, defWeapon); + } const chatData = { title: `Attack Result`, @@ -1020,14 +1035,22 @@ export async function blockResume(atkToken, defToken, type, weaponName, effAML, AudioHelper.play({src: "systems/hm3/audio/shield-bash.ogg", autoplay: true, loop: false}, true); } - return null; + return chatData; } export function checkWeaponBreak(atkWeapon, defWeapon) { + if (!atkWeapon) { + console.warn(`Defend weapon is not specified`); + } + + if (!defWeapon) { + console.warn(`Attack weapon is not specified`); + } + // Weapon Break Check let atkWeaponBroke = false; let defWeaponBroke = false; - if (type === "melee" && game.settings.get('hm3', 'weaponDamage') && combatResult.outcome.block && defWeapon) { + if (game.settings.get('hm3', 'weaponDamage')) { const atkWeapon = atkToken.actor.itemTypes.weapongear.find(w => w.name === weaponName); if (atkWeapon) { const atkWeaponQuality = atkWeapon.data.data.weaponQuality; @@ -1166,7 +1189,7 @@ export async function ignoreResume(atkToken, defToken, type, weaponName, effAML, // Create a chat message await ChatMessage.create(messageData, messageOptions) - return null; + return chatData; } /** @@ -1195,40 +1218,43 @@ export function meleeCombatResult(atkResult, defResult, defense, atkAddlImpact=0 const result = { outcome: outcome, desc: 'Attack misses.', csDesc: 'Counterstrike misses.'}; - if (outcome.atkDice) { - result.desc = `Attacker strikes for ${diceFormula(outcome.atkDice, atkAddlImpact)} impact.`; - } else if (outcome.atkFumble & outcome.defFumble) { - result.desc = 'Both Attacker and Defender Fumble'; - } else if (outcome.atkFumble) { - result.desc = `Attacker fumbles.`; - } else if (outcome.defFumble) { - result.desc = `Defender fumbles.`; - } else if (outcome.defStumble && outcome.atkStumble) { - result.desc = `Both attacker and defender stumble.`; - } else if (outcome.atkStumble) { - result.desc = `Attacker stumbles.`; - } else if (outcome.defStumble) { - result.desc = `Defender stumbles.`; - } else if (outcome.block) { - result.desc = `Attack blocked.`; - } else if (outcome.dta) { - result.desc = `Defender gains Tactical Advantage.`; - } + if (defense !== 'counterstrike') { + if (outcome.atkDice) { + result.desc = `Attacker strikes for ${diceFormula(outcome.atkDice, atkAddlImpact)} impact.`; + } else if (outcome.atkFumble & outcome.defFumble) { + result.desc = 'Both Attacker and Defender Fumble'; + } else if (outcome.atkFumble) { + result.desc = `Attacker fumbles.`; + } else if (outcome.defFumble) { + result.desc = `Defender fumbles.`; + } else if (outcome.defStumble && outcome.atkStumble) { + result.desc = `Both attacker and defender stumble.`; + } else if (outcome.atkStumble) { + result.desc = `Attacker stumbles.`; + } else if (outcome.defStumble) { + result.desc = `Defender stumbles.`; + } else if (outcome.block) { + result.desc = `Attack blocked.`; + } else if (outcome.dta) { + result.desc = `Defender gains Tactical Advantage.`; + } + } else { + if (outcome.atkDice) { + result.desc = `Attacker strikes for ${diceFormula(outcome.atkDice, atkAddlImpact)} impact.`; + } else if (outcome.atkFumble) { + result.desc = `Attacker fumbles.`; + } else if (outcome.atkStumble) { + result.desc = `Attacker stumbles.`; + } - if (defense === 'counterstrike') { if (outcome.defDice) { result.csDesc = `Counterstriker strikes for ${diceFormula(outcome.defDice, defAddlImpact)} impact.`; - } else if (outcome.atkFumble && outcome.defFumble) { - result.desc = 'Attacker fumbles.'; - result.csDesc = 'Counterstriker fumbles.'; } else if (outcome.defFumble) { result.csDesc = 'Counterstriker fumbles.'; - } else if (outcome.atkStumble && outcome.defStumble) { - result.desc = 'Attacker stumbles.'; - result.csDesc = 'Counterstriker stumbles.'; } else if (outcome.defStumble) { result.csDesc = 'Counterstriker stumbles.'; } else if (outcome.block) { + result.desc = 'Attacker blocked.'; result.csDesc = `Counterstriker blocked.`; } else if (outcome.dta) { result.csDesc = `Counterstriker achieves Tactical Advantage!` @@ -1236,6 +1262,7 @@ export function meleeCombatResult(atkResult, defResult, defense, atkAddlImpact=0 result.csDesc = `Counterstrike misses.`; } } + return result; } @@ -1379,24 +1406,29 @@ export function getItem(itemName, type, actor) { * Calculates the distance from sourceToken to targetToken in "scene" units (e.g., feet). * * @param {Token} sourceToken - * @param {Token} targetToken + * @param {Token} targetToken + * @param {Boolean} gridUnits If true, return in grid units, not "scene" units */ export function rangeToTarget(sourceToken, targetToken, gridUnits=false) { if (!sourceToken || !targetToken || !canvas.scene || !canvas.scene.data.grid) return 9999; const sToken = canvas.tokens.get(sourceToken.id); const tToken = canvas.tokens.get(targetToken.id); - const snappedSource = canvas.grid.getSnappedPosition(sToken.x, sToken.y, 2); - const snappedDest = canvas.grid.getSnappedPosition(tToken.x, tToken.y, 2); - const ray = new Ray(snappedSource, snappedDest); - let distance = 0; - if (gridUnits || game.settings.get('hm3', 'distanceUnits') === 'grid') { - distance = Math.round(ray.distance / (canvas.dimensions.size || 1)); - } else { - const ratio = (canvas.dimensions.size / (canvas.dimensions.distance || 1)); - distance = Math.ceil(ray.distance / ratio); - } + const segments = []; + const source = {}; + const dest = {} + const s = canvas.grid.getCenter(sToken.x, sToken.y); + source.x = Math.round(s[0]); + source.y = Math.round(s[1]); + const d = canvas.grid.getCenter(tToken.x, tToken.y); + dest.x = Math.round(d[0]); + dest.y = Math.round(d[1]); + const ray = new Ray(source, dest); + segments.push({ray}); + const distances = canvas.grid.measureDistances(segments, {gridSpaces: true}); + const distance = distances[0]; console.log(`Distance = ${distance}, gridUnits=${gridUnits}`); + if (gridUnits) return Math.round(distance / canvas.dimensions.distance); return distance; } diff --git a/module/config.js b/module/config.js index 9c177273..4c1713f3 100644 --- a/module/config.js +++ b/module/config.js @@ -16,40 +16,169 @@ HM3.allowedActorFlags = []; HM3.skillTypes = ["Craft", "Physical", "Communication", "Combat", "Magic", "Ritual"]; -HM3.sunsigns = [ 'Ulandus', 'Ulandus-Aralius', 'Aralius', 'Aralius-Feniri', 'Feniri', 'Feniri-Ahnu', +HM3.traitTypes = ["Physical", "Psyche"]; + +HM3.sunsigns = ['Ulandus', 'Ulandus-Aralius', 'Aralius', 'Aralius-Feniri', 'Feniri', 'Feniri-Ahnu', 'Ahnu', 'Ahnu-Angberelius', 'Angberelius', 'Angberelius-Nadai', 'Nadai', 'Nadai-Hirin', 'Hirin', 'Hirin-Tarael', 'Tarael', 'Tarael-Tai', 'Tai', 'Tai-Skorus', 'Skorus', - 'Skorus-Masara', 'Masara', 'Masara-Lado', 'Lado', 'Lado-Ulandus' ]; + 'Skorus-Masara', 'Masara', 'Masara-Lado', 'Lado', 'Lado-Ulandus']; HM3.injuryLocations = { - "Custom": {impactType: "custom", probWeight: {"high": 1, "mid": 1, "low": 1}, isStumble: false, isFumble: false, isAmputate: false, effectiveImpact: { ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "G5"}}, - "Skull": {impactType: "skull", probWeight: {"high": 150, "mid": 50, "low": 0}, isStumble: false, isFumble: false, isAmputate: false, effectiveImpact: { ei1: "M1", ei5: "S2", ei9: "S3", ei13: "K4", ei17: "K5"}}, - "Face": {impactType: "face", probWeight: {"high": 150, "mid": 50, "low": 0}, isStumble: false, isFumble: false, isAmputate: false, effectiveImpact: {ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "K5"}}, - "Neck": {impactType: "neck", probWeight: {"high": 150, "mid": 50, "low": 0}, isStumble: false, isFumble: false, isAmputate: true, effectiveImpact: {ei1: "M1", ei5: "S2", ei9: "S3", ei13: "K4", ei17: "K5"}}, - "Shoulder": {impactType: "shoulder", probWeight: {"high": 60, "mid": 60, "low": 0}, isStumble: false, isFumble: true, isAmputate: false, effectiveImpact: {ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "K4"}}, - "Upper Arm": {impactType: "upperarm", probWeight: {"high": 60, "mid": 30, "low": 0}, isStumble: false, isFumble: true, isAmputate: true, effectiveImpact: {ei1: "M1", ei5: "M1", ei9: "S2", ei13: "S3", ei17: "G4"}}, - "Elbow": {impactType: "elbow", probWeight: {"high": 20, "mid": 10, "low": 0}, isStumble: false, isFumble: true, isAmputate: true, effectiveImpact: {ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "G5"}}, - "Forearm": {impactType: "forearm", probWeight: {"high": 40, "mid": 20, "low": 30}, isStumble: false, isFumble: true, isAmputate: true, effectiveImpact: {ei1: "M1", ei5: "M1", ei9: "S2", ei13: "S3", ei17: "G4"}}, - "Hand": {impactType: "hand", probWeight: {"high": 20, "mid": 20, "low": 30}, isStumble: false, isFumble: true, isAmputate: true, effectiveImpact: {ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "G5"}}, - "Thorax": {impactType: "thorax", probWeight: {"high": 100, "mid": 170, "low": 70}, isStumble: false, isFumble: false, isAmputate: false, effectiveImpact: {ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "K5"}}, - "Abdomen": {impactType: "abdomen", probWeight: {"high": 60, "mid": 100, "low": 100}, isStumble: false, isFumble: false, isAmputate: false, effectiveImpact: {ei1: "M1", ei5: "S2", ei9: "S3", ei13: "K4", ei17: "K5"}}, - "Groin": {impactType: "groin", probWeight: {"high": 0, "mid": 40, "low": 60}, isStumble: false, isFumble: false, isAmputate: true, effectiveImpact: {ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "G5"}}, - "Hip": {impactType: "hip", probWeight: {"high": 0, "mid": 30, "low": 70}, isStumble: true, isFumble: false, isAmputate: false, effectiveImpact: {ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "K4"}}, - "Thigh": {impactType: "thigh", probWeight: {"high": 0, "mid": 40, "low": 100}, isStumble: true, isFumble: false, isAmputate: true, effectiveImpact: {ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "K4"}}, - "Knee": {impactType: "knee", probWeight: {"high": 0, "mid": 10, "low": 40}, isStumble: true, isFumble: false, isAmputate: true, effectiveImpact: {ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "G5"}}, - "Calf": {impactType: "calf", probWeight: {"high": 0, "mid": 30, "low": 70}, isStumble: true, isFumble: false, isAmputate: true, effectiveImpact: {ei1: "M1", ei5: "M1", ei9: "S2", ei13: "S3", ei17: "G4"}}, - "Foot": {impactType: "foot", probWeight: {"high": 0, "mid": 20, "low": 40}, isStumble: true, isFumble: false, isAmputate: true, effectiveImpact: {ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "G5"}}, - "Wing": {impactType: "wing", probWeight: {"high": 150, "mid": 50, "low": 0}, isStumble: false, isFumble: true, isAmputate: true, effectiveImpact: {ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "G5"}}, - "Tentacle": {impactType: "tentacle", probWeight: {"high": 50, "mid": 150, "low": 0}, isStumble: false, isFumble: true, isAmputate: true, effectiveImpact: {ei1: "M1", ei5: "M1", ei9: "S2", ei13: "S3", ei17: "G4"}}, - "Tail": {impactType: "tail", probWeight: {"high": 0, "mid": 50, "low": 100}, isStumble: true, isFumble: false, isAmputate: true, effectiveImpact: {ei1: "M1", ei5: "M1", ei9: "S2", ei13: "S3", ei17: "G4"}} + "Custom": { impactType: "custom", probWeight: { "high": 1, "mid": 1, "low": 1 }, isStumble: false, isFumble: false, isAmputate: false, effectiveImpact: { ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "G5" } }, + "Skull": { impactType: "skull", probWeight: { "high": 150, "mid": 50, "low": 0 }, isStumble: false, isFumble: false, isAmputate: false, effectiveImpact: { ei1: "M1", ei5: "S2", ei9: "S3", ei13: "K4", ei17: "K5" } }, + "Face": { impactType: "face", probWeight: { "high": 150, "mid": 50, "low": 0 }, isStumble: false, isFumble: false, isAmputate: false, effectiveImpact: { ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "K5" } }, + "Neck": { impactType: "neck", probWeight: { "high": 150, "mid": 50, "low": 0 }, isStumble: false, isFumble: false, isAmputate: true, effectiveImpact: { ei1: "M1", ei5: "S2", ei9: "S3", ei13: "K4", ei17: "K5" } }, + "Shoulder": { impactType: "shoulder", probWeight: { "high": 60, "mid": 60, "low": 0 }, isStumble: false, isFumble: true, isAmputate: false, effectiveImpact: { ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "K4" } }, + "Upper Arm": { impactType: "upperarm", probWeight: { "high": 60, "mid": 30, "low": 0 }, isStumble: false, isFumble: true, isAmputate: true, effectiveImpact: { ei1: "M1", ei5: "M1", ei9: "S2", ei13: "S3", ei17: "G4" } }, + "Elbow": { impactType: "elbow", probWeight: { "high": 20, "mid": 10, "low": 0 }, isStumble: false, isFumble: true, isAmputate: true, effectiveImpact: { ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "G5" } }, + "Forearm": { impactType: "forearm", probWeight: { "high": 40, "mid": 20, "low": 30 }, isStumble: false, isFumble: true, isAmputate: true, effectiveImpact: { ei1: "M1", ei5: "M1", ei9: "S2", ei13: "S3", ei17: "G4" } }, + "Hand": { impactType: "hand", probWeight: { "high": 20, "mid": 20, "low": 30 }, isStumble: false, isFumble: true, isAmputate: true, effectiveImpact: { ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "G5" } }, + "Thorax": { impactType: "thorax", probWeight: { "high": 100, "mid": 170, "low": 70 }, isStumble: false, isFumble: false, isAmputate: false, effectiveImpact: { ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "K5" } }, + "Abdomen": { impactType: "abdomen", probWeight: { "high": 60, "mid": 100, "low": 100 }, isStumble: false, isFumble: false, isAmputate: false, effectiveImpact: { ei1: "M1", ei5: "S2", ei9: "S3", ei13: "K4", ei17: "K5" } }, + "Groin": { impactType: "groin", probWeight: { "high": 0, "mid": 40, "low": 60 }, isStumble: false, isFumble: false, isAmputate: true, effectiveImpact: { ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "G5" } }, + "Hip": { impactType: "hip", probWeight: { "high": 0, "mid": 30, "low": 70 }, isStumble: true, isFumble: false, isAmputate: false, effectiveImpact: { ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "K4" } }, + "Thigh": { impactType: "thigh", probWeight: { "high": 0, "mid": 40, "low": 100 }, isStumble: true, isFumble: false, isAmputate: true, effectiveImpact: { ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "K4" } }, + "Knee": { impactType: "knee", probWeight: { "high": 0, "mid": 10, "low": 40 }, isStumble: true, isFumble: false, isAmputate: true, effectiveImpact: { ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "G5" } }, + "Calf": { impactType: "calf", probWeight: { "high": 0, "mid": 30, "low": 70 }, isStumble: true, isFumble: false, isAmputate: true, effectiveImpact: { ei1: "M1", ei5: "M1", ei9: "S2", ei13: "S3", ei17: "G4" } }, + "Foot": { impactType: "foot", probWeight: { "high": 0, "mid": 20, "low": 40 }, isStumble: true, isFumble: false, isAmputate: true, effectiveImpact: { ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "G5" } }, + "Wing": { impactType: "wing", probWeight: { "high": 150, "mid": 50, "low": 0 }, isStumble: false, isFumble: true, isAmputate: true, effectiveImpact: { ei1: "M1", ei5: "S2", ei9: "S3", ei13: "G4", ei17: "G5" } }, + "Tentacle": { impactType: "tentacle", probWeight: { "high": 50, "mid": 150, "low": 0 }, isStumble: false, isFumble: true, isAmputate: true, effectiveImpact: { ei1: "M1", ei5: "M1", ei9: "S2", ei13: "S3", ei17: "G4" } }, + "Tail": { impactType: "tail", probWeight: { "high": 0, "mid": 50, "low": 100 }, isStumble: true, isFumble: false, isAmputate: true, effectiveImpact: { ei1: "M1", ei5: "M1", ei9: "S2", ei13: "S3", ei17: "G4" } } +}; + +HM3.stdSkills = { + "Sword": { "source": "HM3 Skills 19", "skillBase": { "formula": "@str, @dex, @dex, Angberelius:3, Ahnu, Nadai" }, "type": "Combat" }, + "Axe": { "source": "HM3 Skills 19", "skillBase": { "formula": "@str, @str, @dex, Ahnu, Feniri, Angberelius" }, "type": "Combat" }, + "Bow": { "source": "HM3 Skills 19", "skillBase": { "formula": "@str, @dex, @eye, Hirin, Tarael, Nadai" }, "type": "Combat" }, + "Shield": { "source": "HM3 Skills 19", "skillBase": { "formula": "@str, @dex, @dex, Ulandus, Lado, Masara" }, "type": "Combat" }, + "Flail": { "source": "HM3 Skills 19", "skillBase": { "formula": "@dex, @dex, @dex, Hirin, Tarael, Nadai" }, "type": "Combat" }, + "Sling": { "source": "HM3 Skills 19", "skillBase": { "formula": "@dex, @dex, @eye, Hirin, Tarael, Nadai" }, "type": "Combat" }, + "Riding": { "source": "HM3 Skills 18", "skillBase": { "formula": "@dex, @agl, @wil, Ulandus, Aralius" }, "type": "Combat" }, + "Initiative": { "source": "HM3 Skills 18", "skillBase": { "formula": "@agl, @wil, @wil" }, "type": "Combat" }, + "Unarmed": { "source": "HM3 Skills 18", "skillBase": { "formula": "@str, @dex, @agl, Madada:2, Lado:2, Ulandus:2" }, "type": "Combat" }, + "Polearm": { "source": "HM3 Skills 19", "skillBase": { "formula": "@str, @str, @dex, Angberelius, Aralius" }, "type": "Combat" }, + "Dagger": { "source": "HM3 Skills 19", "skillBase": { "formula": "@dex, @dex, @eye, Angberelius:2, Nadai:2" }, "type": "Combat" }, + "Blowgun": { "source": "HM3 Skills 19", "skillBase": { "formula": "@sta, @dex, @eye, Hirin:2, Tarael, Nadai" }, "type": "Combat" }, + "Spear": { "source": "HM3 Skills 19", "skillBase": { "formula": "@str, @str, @dex, Aralius, Feniri, Ulandus" }, "type": "Combat" }, + "Net": { "source": "HM3 Skills 19", "skillBase": { "formula": "@dex, @dex, @eye, Masara, Skorus, Lado" }, "type": "Combat" }, + "Club": { "source": "HM3 Skills 19", "skillBase": { "formula": "@str, @str, @dex, Ulandus, Aralius" }, "type": "Combat" }, + "Whip": { "source": "HM3 Skills 19", "skillBase": { "formula": "@dex, @dex, @eye, Hirin, Nadai" }, "type": "Combat" }, + "Dodge": { "source": "HM3 Skills 21", "skillBase": { "formula": "@agl, @agl, @agl" }, "type": "Combat" }, + "Acting": { "source": "HM3 Skills 11", "skillBase": { "formula": "@agl, @voi, @int, Tarael, Tai" }, "type": "Communication" }, + "Intrigue": { "source": "HM3 Skills 11", "skillBase": { "formula": "@int, @aur, @wil, Tai, Tarael, Skorus" }, "type": "Communication" }, + "Awareness": { "source": "HM3 Skills 11", "skillBase": { "formula": "@eye, @hrg, @sml, Hirin:2, Tarael:2" }, "type": "Communication" }, + "Oratory": { "source": "HM3 Skills 12", "skillBase": { "formula": "@cml, @voi, @int, Tarael" }, "type": "Communication" }, + "Script": { "source": "HM3 Skills 11", "skillBase": { "formula": "@dex, @eye, @int, Tarael, Tai" }, "type": "Communication" }, + "Rhetoric": { "source": "HM3 Skills 12", "skillBase": { "formula": "@voi, @int, @wil, Tai, Tarael, Skorus" }, "type": "Communication" }, + "Language": { "source": "HM3 Skills 10", "skillBase": { "formula": "@voi, @int, @wil, Tai" }, "type": "Communication" }, + "Musician": { "source": "HM3 Skills 12", "skillBase": { "formula": "@dex, @hrg, @hrg, Masara, Angberelius" }, "type": "Communication" }, + "Mental Conflict": { "source": "HM3 Skills 12", "skillBase": { "formula": "@aur, @wil, @wil" }, "type": "Communication" }, + "Singing": { "source": "HM3 Skills 12", "skillBase": { "formula": "@hrg, @voi, @voi, Masara" }, "type": "Communication" }, + "Lovecraft": { "source": "HM3 Skills 11", "skillBase": { "formula": "@cml, @agl, @voi, Masara, Angberelius" }, "type": "Communication" }, + "Physician": { "source": "HM3 Skills 17", "skillBase": { "formula": "@dex, @eye, @int, Masara:2, Skorus, Tai" }, "type": "Craft" }, + "Fishing": { "source": "HM3 Skills 14", "skillBase": { "formula": "@dex, @eye, @wil, Masara:2, Lado:2" }, "type": "Craft" }, + "Survival": { "source": "HM3 Skills 17", "skillBase": { "formula": "@str, @dex, @int, Ulandus:2, Aralius" }, "type": "Craft" }, + "Foraging": { "source": "HM3 Skills 15", "skillBase": { "formula": "@dex, @sml, @int, Ulandus:2, Aralius:2" }, "type": "Craft" }, + "Mathematics": { "source": "HM3 Skills 16", "skillBase": { "formula": "@int, @int, @wil, Tai:3, Tarael, Skorus" }, "type": "Craft" }, + "Folklore": { "source": "HM3 Skills 15", "skillBase": { "formula": "@voi, @int, @int, Tai:2" }, "type": "Craft" }, + "Jewelcraft": { "source": "HM3 Skills 16", "skillBase": { "formula": "@dex, @eye, @wil, Feniri:3, Tarael, Aralius" }, "type": "Craft" }, + "Tracking": { "source": "HM3 Skills 17", "skillBase": { "formula": "@eye, @sml, @wil, Ulandus:3, Aralius:3" }, "type": "Craft" }, + "Hunting": { "source": "HM3 Skills 16", "skillBase": { "formula": "@agl, @sml, @int, Ulandus:2, Aralius:2" }, "type": "Craft" }, + "Law": { "source": "HM3 Skills 16", "skillBase": { "formula": "@voi, @int, @wil, Tarael, Tai" }, "type": "Craft" }, + "Weaponcraft": { "source": "HM3 Skills 17", "skillBase": { "formula": "@str, @dex, @wil, Feniri:3, Ahnu, Angberelius" }, "type": "Craft" }, + "Mining": { "source": "HM3 Skills 16", "skillBase": { "formula": "@str, @eye, @int, Ulandus:2, Aralius:2, Feniri" }, "type": "Craft" }, + "Metalcraft": { "source": "HM3 Skills 16", "skillBase": { "formula": "@str, @dex, @wil, Feniri:3, Ahnu, Angberelius" }, "type": "Craft" }, + "Ceramics": { "source": "HM3 Skills 13", "skillBase": { "formula": "@dex, @dex, @eye, Ulandus:2, Aralius:2" }, "type": "Craft" }, + "Runecraft": { "source": "HM3 Skills 17", "skillBase": { "formula": "@int, @aur, @aur, Tai:2, Skorus" }, "type": "Craft" }, + "Tarotry": { "source": "HM3 Skills 17", "skillBase": { "formula": "@int, @aur, @wil, Tarael:2, Tai:2, Skorus, Hirin" }, "type": "Craft" }, + "Perfumery": { "source": "HM3 Skills 16", "skillBase": { "formula": "@sml, @sml, @int, Hirin, Skorus, Tarael" }, "type": "Craft" }, + "Fletching": { "source": "HM3 Skills 15", "skillBase": { "formula": "@dex, @dex, @eye, Hirin:2, Tarael, Nadai" }, "type": "Craft" }, + "Piloting": { "source": "HM3 Skills 17", "skillBase": { "formula": "@dex, @eye, @int, Lado:3, Masara" }, "type": "Craft" }, + "Weatherlore": { "source": "HM3 Skills 17", "skillBase": { "formula": "@int, @eye, @sml, Hirin, Tarael, Masada, Lado" }, "type": "Craft" }, + "Engineering": { "source": "HM3 Skills 14", "skillBase": { "formula": "@dex, @int, @int, Ulandus:2, Aralius:2, Feniri" }, "type": "Craft" }, + "Embalming": { "source": "HM3 Skills 14", "skillBase": { "formula": "@dex, @eye, @sml, Skorus, Ulandus" }, "type": "Craft" }, + "Brewing": { "source": "HM3 Skills 13", "skillBase": { "formula": "@dex, @sml, @sml, Skorus:3, Tai:2, Masara:2" }, "type": "Craft" }, + "Lockcraft": { "source": "HM3 Skills 16", "skillBase": { "formula": "@dex, @eye, @wil, Feniri" }, "type": "Craft" }, + "Masonry": { "source": "HM3 Skills 16", "skillBase": { "formula": "@str, @dex, @int, Ulandus:2, Aralius:2" }, "type": "Craft" }, + "Textilecraft": { "source": "HM3 Skills 17", "skillBase": { "formula": "@dex, @dex, @eye, Ulandus, Aralius" }, "type": "Craft" }, + "Cookery": { "source": "HM3 Skills 13", "skillBase": { "formula": "@dex, @sml, @sml, Skorus" }, "type": "Craft" }, + "Lore": { "source": "HM3 Skills 16", "skillBase": { "formula": "@eye, @int, @int, Tai:2" }, "type": "Craft" }, + "Drawing": { "source": "HM3 Skills 13", "skillBase": { "formula": "@dex, @eye, @eye, Skorus, Tai" }, "type": "Craft" }, + "Alchemy": { "source": "HM3 Skills 13", "skillBase": { "formula": "@sml, @int, @aur, Skorus:3, Tai:2, Masara:2" }, "type": "Craft" }, + "Milling": { "source": "HM3 Skills 16", "skillBase": { "formula": "@str, @dex, @sml, Ulandus" }, "type": "Craft" }, + "Timbercraft": { "source": "HM3 Skills 17", "skillBase": { "formula": "@str, @dex, @agl, Ulandus:3, Aralius" }, "type": "Craft" }, + "Hidework": { "source": "HM3 Skills 15", "skillBase": { "formula": "@dex, @sml, @wil, Ulandis, Aralius" }, "type": "Craft" }, + "Shipwright": { "source": "HM3 Skills 17", "skillBase": { "formula": "@str, @dex, @int, Lado:3, Masara" }, "type": "Craft" }, + "Astrology": { "source": "HM3 Skills 13", "skillBase": { "formula": "@eye, @int, @aur, Tarael" }, "type": "Craft" }, + "Woodcraft": { "source": "HM3 Skills 17", "skillBase": { "formula": "@dex, @dex, @wil, Ulandus:2, Aralius, Lado" }, "type": "Craft" }, + "Herblore": { "source": "HM3 Skills 15", "skillBase": { "formula": "@eye, @sml, @int, Ulandus:3, Aralius:2" }, "type": "Craft" }, + "Inkcraft": { "source": "HM3 Skills 16", "skillBase": { "formula": "@eye, @sml, @int, Skorus:2, Tai" }, "type": "Craft" }, + "Heraldry": { "source": "HM3 Skills 15", "skillBase": { "formula": "@dex, @eye, @wil, Skorus, Tai" }, "type": "Craft" }, + "Animalcraft": { "source": "HM3 Skills 13", "skillBase": { "formula": "@agl, @voi, @wil, Ulandus, Aralius" }, "type": "Craft" }, + "Seamanship": { "source": "HM3 Skills 17", "skillBase": { "formula": "@str, @dex, @agl, Lado:3, Masara, Skorus" }, "type": "Craft" }, + "Glassworking": { "source": "HM3 Skills 15", "skillBase": { "formula": "@dex, @eye, @wil, Feniri:2" }, "type": "Craft" }, + "Agriculture": { "source": "HM3 Skills 13", "skillBase": { "formula": "@str, @sta, @wil, Ulandus:2, Aralius:2" }, "type": "Craft" }, + "Lyahvi": { "source": "HM Magic, Shek-Pvar 6", "skillBase": { "formula": "@aur, @aur, @eye, Ulandus:-3, Aralius:-2,Feneri:-1, Angberelius, Nadai:2, Hirin:3, Tarael:2, Tai,Masara:-1, Lado:-2" }, "type": "Magic" }, + "Savorya": { "source": "HM Magic, Shek-pvar 6", "skillBase": { "formula": "@aur, @aur, @int, Ulandus:-1, Aralius:-2, Feneri:-3, Ahnu:-2, Angberelius:-1, Hirin:1, Tarael:2, Tai:3, Skorus:2, Masara" }, "type": "Magic" }, + "Peleahn": { "source": "HM Magic, Shek-pvar 6", "skillBase": { "formula": "@aur, @aur, @agl, Ulandus:-1, Feneri, Ahnu:2, Angberelius:3, Nadai:2, Hirin, Tai:-1, Skorus:-2, Masara:-3, Lado:-2" }, "type": "Magic" }, + "Jmorvi": { "source": "HM Magic, Shek-pvar 6", "skillBase": { "formula": "@aur, @aur, @str, Ulandus, Aralius:2, Feneri:3, Ahnu:2, Angberelius:1, Hirin:-1, Tarael:-2, Tai:-3, Skorus:-2, Masara:-1" }, "type": "Magic" }, + "Odivshe": { "source": "HM Magic, Shek-pvar 6", "skillBase": { "formula": "@aur, @aur, @dex, Ulandus, Feneri:-1, Ahnu:-2, Angberelius:-3, Nadai:-2, Hirin:-1, Tai:1, Skorus:2, Masara:3, Lado:2" }, "type": "Magic" }, + "Neutral": { "source": "HM Magic, Shek-pvar 6", "skillBase": { "formula": "@aur, @aur, @wil" }, "type": "Magic" }, + "Fyvria": { "source": "HM Magic, Shek-pvar 6", "skillBase": { "formula": "@aur, @aur, @sml, Ulandus:3, Aralius:2, Feneri:1, Angberelius:-1, Nadai:-2, Hirin:-3, Tarael:-2, Tai:-1, Masara, Lado:2" }, "type": "Magic" }, + "Climbing": { "source": "HM3 Skills 8", "skillBase": { "formula": "@str, @dex, @agl, Ulandus:2, Aralius:2" }, "type": "Physical" }, + "Swimming": { "source": "HM3 Skills 9", "skillBase": { "formula": "@sta, @dex, @agl, Skorus, Masara:3, Lado:3" }, "type": "Physical" }, + "Skiing": { "source": "HM3 Skills 9", "skillBase": { "formula": "@str, @dex, @agl, Masara:2, Skorus, Lado" }, "type": "Physical" }, + "Stealth": { "source": "HM3 Skills 9", "skillBase": { "formula": "@agl, @hrg, @wil, Hirin:2, Tarael:2, Tai:2" }, "type": "Physical" }, + "Jumping": { "source": "HM3 Skills 9", "skillBase": { "formula": "@str, @agl, @agl, Nadai:2, Hirin:2" }, "type": "Physical" }, + "Condition": { "source": "HM3 Skills 9", "skillBase": { "formula": "@str, @sta, @wil, Ulandus, Lado" }, "type": "Physical" }, + "Dancing": { "source": "HM3 Skills 9", "skillBase": { "formula": "@Dex, @agl, @agl, Tarael:2, Hirin, Tai" }, "type": "Physical" }, + "Acrobatics": { "source": "HM3 Skills 8", "skillBase": { "formula": "@str, @agl, @agl, Nadai:2, Hirin" }, "type": "Physical" }, + "Throwing": { "source": "HM3 Skills 10", "skillBase": { "formula": "@str, @dex, @eye, Hirin:2, Tarael, Nadai" }, "type": "Physical" }, + "Legerdemain": { "source": "HM3 Skills 9", "skillBase": { "formula": "@dex, @dex, @wil, Skorus:2, Tai:2, Tarael:2" }, "type": "Physical" }, + "Peoni": { "source": "HM Religion, Peoni 1", "skillBase": { "formula": "@voi, @int, @dex, Aralius:2, Angberelius, Ulandus" }, "type": "Ritual" }, + "Agrik": { "source": "HM Religion, Agrik 1", "skillBase": { "formula": "@voi, @int, @str, Nadai:2, Angberelius, Ahnu" }, "type": "Ritual" }, + "Ilvir": { "source": "HM Religion, Ilvir 1", "skillBase": { "formula": "@voi, @int, @aur, Skorus:2, Tai, Ulandus" }, "type": "Ritual" }, + "Siem": { "source": "HM Religion, Siem 1", "skillBase": { "formula": "@voi, @int, @aur, Hirin:2, Feniri, Ulandus" }, "type": "Ritual" }, + "Sarajin": { "source": "HM Religion, Sarajin 1", "skillBase": { "formula": "@voi, @int, @str, Feniri:2, Aralius, Lado" }, "type": "Ritual" }, + "Morgath": { "source": "HM Religion, Morgath 1", "skillBase": { "formula": "@voi, @int, @aur, Lado:2, Ahnu, Masara" }, "type": "Ritual" }, + "Halea": { "source": "HM Religion, Halea 1", "skillBase": { "formula": "@voi, @int, @cml, Tarael:2, Hirin, Masara" }, "type": "Ritual" }, + "Naveh": { "source": "HM Religion, Naveh 1", "skillBase": { "formula": "@voi, @int, @wil, Masara:2, Skorus, Tarael" }, "type": "Ritual" }, + "Larani": { "source": "HM Religion, Larani 1", "skillBase": { "formula": "@voi, @int, @wil, Angberelius:2, Ahnu, Feniri" }, "type": "Ritual" }, + "Save K'nor": { "source": "HM Religion, Save K'nor 1", "skillBase": { "formula": "@voi, @int, @int, Tai:2, Tarael, Skorus" }, "type": "Craft" } }; HM3.injuryLevels = ["NA", "M1", "S2", "S3", "G4", "G5", "K4", "K5"]; +HM3.activeEffectKey = { + 'data.eph.meleeAMLMod': 'Melee Attack', + 'data.eph.meleeDMLMod': 'Melee Defense', + 'data.eph.missileAMLMod': 'Missile Attack', + 'data.fatigue': 'Fatigue', + 'data.encumbrance': 'Encumbrance', + 'data.endurance': 'Endurance', + 'data.eph.totalInjuryLevels': 'Injury Level', + 'data.eph.move': 'Move', + 'data.eph.strength': 'Strength', + 'data.eph.stamina': 'Stamina', + 'data.eph.dexterity': 'Dexterity', + 'data.eph.agility': 'Agility', + 'data.eph.eyesight': 'Eyesight', + 'data.eph.hearing': 'Hearing', + 'data.eph.smell': 'Smell', + 'data.eph.voice': 'Voice', + 'data.eph.intelligence': 'Intelligence', + 'data.eph.will': 'Will', + 'data.eph.aura': 'Aura', + 'data.eph.morality': 'Morality', + 'data.eph.comliness': 'Comeliness' +}; + HM3.defaultMagicIconName = 'pentacle'; HM3.defaultRitualIconName = 'circle'; HM3.defaultMiscItemIconName = 'miscgear'; HM3.defaultPsionicsIconName = 'psionics'; +HM3.defaultArmorGearIconName = 'armor'; +HM3.defaultContainerIconName = 'sack'; HM3.magicIcons = [ ['pentacle', 'systems/hm3/images/icons/svg/pentacle.svg'], @@ -245,6 +374,10 @@ HM3.craftSkillIcons = [ ['woodcraft', 'systems/hm3/images/icons/svg/woodcraft.svg'] ]; +HM3.armorGearIcons = [ + ['armorgear', 'systems/hm3/images/icons/svg/armor.svg'], +]; + HM3.miscGearIcons = [ ['miscgear', 'systems/hm3/images/icons/svg/miscgear.svg'], ['coin', 'systems/hm3/images/icons/svg/coins.svg'], @@ -276,14 +409,15 @@ HM3.miscGearIcons = [ HM3.defaultItemIcons = new Map( HM3.physicalSkillIcons - .concat(HM3.commSkillIcons) - .concat(HM3.combatSkillIcons) - .concat(HM3.weaponSkillIcons) - .concat(HM3.craftSkillIcons) - .concat(HM3.miscGearIcons) - .concat(HM3.ritualIcons) - .concat(HM3.magicIcons) - .concat(HM3.psionicTalentIcons) + .concat(HM3.commSkillIcons) + .concat(HM3.combatSkillIcons) + .concat(HM3.weaponSkillIcons) + .concat(HM3.craftSkillIcons) + .concat(HM3.miscGearIcons) + .concat(HM3.armorGearIcons) + .concat(HM3.ritualIcons) + .concat(HM3.magicIcons) + .concat(HM3.psionicTalentIcons) ); HM3.meleeCombatTable = { diff --git a/module/dice-hm3.js b/module/dice-hm3.js index 78002b65..d21cdcbf 100644 --- a/module/dice-hm3.js +++ b/module/dice-hm3.js @@ -76,10 +76,10 @@ export class DiceHM3 { rollResult: roll.rollObj.total, showResult: false, description: roll.description, - notes: renderedNotes + notes: renderedNotes, + roll: roll }; - const html = await renderTemplate(chatTemplate, chatTemplateData); const messageData = { @@ -98,7 +98,7 @@ export class DiceHM3 { // Create a chat message await ChatMessage.create(messageData, messageOptions) - return roll; + return chatTemplateData; } @@ -160,6 +160,7 @@ export class DiceHM3 { * rollData is expected to contain the following values: * target: Target value to check against * modifier: Modifier to target value + * numdice: Number of d6 to roll * label: The label associated with the 'target' value * fastForward: If true, assume no modifier and don't present Dialog * speaker: the Speaker to use in Chat @@ -214,7 +215,8 @@ export class DiceHM3 { rollResult: roll.rollObj.dice[0].values.join(" + "), showResult: roll.rollObj.dice[0].values.length > 1, description: roll.description, - notes: renderedNotes + notes: renderedNotes, + roll: roll }; const html = await renderTemplate(chatTemplate, chatTemplateData); @@ -235,7 +237,7 @@ export class DiceHM3 { // Create a chat message await ChatMessage.create(messageData, messageOptions) - return roll; + return chatTemplateData; } @@ -307,7 +309,8 @@ export class DiceHM3 { rollResult: roll.result, showResult: true, description: isSuccess ? "Success" : "Failure", - notes: '' + notes: '', + sdrIncr: isSuccess ? (specMatch ? 2 : 1 ) : 0 }; if (specMatch && isSuccess) { @@ -332,7 +335,7 @@ export class DiceHM3 { // Create a chat message await ChatMessage.create(messageData, messageOptions); - return isSuccess ? (specMatch ? 2 : 1 ) : 0; + return chatTemplateData; } /*--------------------------------------------------------------------------------*/ @@ -402,7 +405,7 @@ export class DiceHM3 { if (game.settings.get("hm3", "combatAudio")) { AudioHelper.play({src: "systems/hm3/audio/grunt1.ogg", autoplay: true, loop: false}, true); } - return result; + return chatTemplateData; } /** @@ -530,7 +533,7 @@ export class DiceHM3 { static _calcInjury(location, impact, aspect, addToCharSheet, aim, dialogOptions) { const enableAmputate = game.settings.get('hm3', 'amputation'); const enableBloodloss = game.settings.get('hm3', 'bloodloss'); - const enableLimbInjury = game.settings.get('hm3', 'limbInjuries'); + const enableLimbInjuries = game.settings.get('hm3', 'limbInjuries'); const result = { isRandom: location === 'Random', @@ -646,14 +649,14 @@ export class DiceHM3 { // Optional Rule - Limb Injuries (Combat 14) if (armorLocation.data.isFumble) { - result.isFumble = enableLimbInjury && result.injuryLevel >= 4; - result.isFumbleRoll = enableLimbInjury || (!result.isFumble && result.injuryLevel >= 2); + result.isFumble = enableLimbInjuries && result.injuryLevel >= 4; + result.isFumbleRoll = enableLimbInjuries || (!result.isFumble && result.injuryLevel >= 2); } // Optional Rule - Limb Injuries (Combat 14) if (armorLocation.data.isStumble) { - result.isStumble = enableLimbInjury && result.injuryLevel >= 4; - result.isStumbleRoll = enableLimbInjury || (!result.isStumble && result.injuryLevel >= 2); + result.isStumble = enableLimbInjuries && result.injuryLevel >= 4; + result.isStumbleRoll = enableLimbInjuries || (!result.isStumble && result.injuryLevel >= 2); } return result; @@ -787,7 +790,8 @@ export class DiceHM3 { totalImpact: totalImpact, impactRoll: roll.rollObj.dice[0].values.join(" + "), rollValue: roll.rollObj.total, - notes: renderedNotes + notes: renderedNotes, + roll: roll }; const html = await renderTemplate(chatTemplate, chatTemplateData); @@ -807,7 +811,7 @@ export class DiceHM3 { // Create a chat message await ChatMessage.create(messageData, messageOptions) - return roll; + return chatTemplateData; } /** @@ -961,7 +965,8 @@ export class DiceHM3 { isCritical: roll.isCritical, rollValue: roll.rollObj.total, description: roll.description, - notes: renderedNotes + notes: renderedNotes, + roll: roll }; const html = await renderTemplate(chatTemplate, chatTemplateData); @@ -981,7 +986,7 @@ export class DiceHM3 { // Create a chat message await ChatMessage.create(messageData, messageOptions); - return roll; + return chatTemplateData; } static async missileAttackDialog(dialogOptions) { @@ -1131,7 +1136,8 @@ export class DiceHM3 { addlImpact: roll.addlImpact, totalImpact: totalImpact, rollValue: roll.rollObj.total, - notes: renderedNotes + notes: renderedNotes, + roll: roll }; const html = await renderTemplate(chatTemplate, chatTemplateData); @@ -1151,7 +1157,7 @@ export class DiceHM3 { // Create a chat message await ChatMessage.create(messageData, messageOptions) - return roll; + return chatTemplateData; } static async missileDamageDialog(dialogOptions) { diff --git a/module/effect.js b/module/effect.js new file mode 100644 index 00000000..b7593436 --- /dev/null +++ b/module/effect.js @@ -0,0 +1,117 @@ +//import { HM3ActiveEffect } from './hm3-active-effect.js'; + +/** + * Manage Active Effect instances through the Actor Sheet via effect control buttons. + * @param {MouseEvent} event The left-click event on the effect control + * @param {Actor|Item} owner The owning entity which manages this effect + */ +export async function onManageActiveEffect(event, owner) { + event.preventDefault(); + const a = event.currentTarget; + const li = a.closest("li"); + const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null; + switch (a.dataset.action) { + case "create": + const dlgTemplate = "systems/hm3/templates/dialog/active-effect-start.html"; + const dialogData = { + gameTime: game.time.worldTime + }; + if (game.combat) { + dialogData.combatId = game.combat.id; + dialogData.combatRound = game.combat.round; + dialogData.combatTurn = game.combat.turn; + } + const html = await renderTemplate(dlgTemplate, dialogData); + + // Create the dialog window + return Dialog.prompt({ + title: "Select Start Time", + content: html, + label: "OK", + callback: async (html) => { + const form = html.querySelector('#active-effect-start'); + const fd = new FormDataExtended(form); + const formdata = fd.toObject(); + const startType = formdata.startType; + + const aeData = { + label: "New Effect", + icon: "icons/svg/aura.svg", + origin: owner.uuid + }; + if (startType === 'nowGameTime') { + aeData['duration.startTime'] = dialogData.gameTime; + aeData['duration.seconds'] = 1; + } else if (startType === 'nowCombat') { + aeData['duration.combat'] = dialogData.combatId; + aeData['duration.startRound'] = dialogData.combatRound; + aeData['duration.startTurn'] = dialogData.combatTurn; + aeData['duration.rounds'] = 1; + aeData['duration.turns'] = 0; + } + return ActiveEffect.create(aeData, owner).create(); + }, + options: { jQuery: false } + }); + case "edit": + return effect.sheet.render(true); + case "delete": + return effect.delete(); + case "toggle": + const updateData = {}; + if (effect.data.disabled) { + // Enable the Active Effect + updateData['disabled'] = false; + + // Also set the timer to start now + updateData['duration.startTime'] = game.time.worldTime; + if (game.combat) { + updateData['duration.startRound'] = game.combat.round; + updateData['duration.startTurn'] = game.combat.turn; + } + } else { + // Disable the Active Effect + updateData['disabled'] = true; + } + return effect.update(updateData); + } +} + +/** + * This function searches all actors and tokens that are owned + * by the user and disables them if their duration has expired. + */ +export async function checkExpiredActiveEffects() { + // Handle game actors first + for (let actor of game.actors.values()) { + if (actor.owner && actor.effects && actor.effects.length) { + await disableExpiredAE(actor); + } + } + + // Next, handle tokens (only unlinked tokens) + for (let token of canvas.tokens.ownedTokens.values()) { + if (!token.data.actorLink && token.actor) { + await disableExpiredAE(token.actor); + } + } +} + +/** + * Checks all of the active effects for a single actor and disables + * them if their duration has expired. + * + * @param {Actor} actor + */ +async function disableExpiredAE(actor) { + for (let effect of actor.effects.values()) { + if (!effect.data.disabled) { + const duration = effect.duration; + if (duration.type !== 'none') { + if (duration.remaining <= 0) { + await effect.update({'disabled': true}); + } + } + } + } +} diff --git a/module/hm3-active-effect-config.js b/module/hm3-active-effect-config.js new file mode 100644 index 00000000..43deb702 --- /dev/null +++ b/module/hm3-active-effect-config.js @@ -0,0 +1,27 @@ +import { HM3 } from './config.js'; + +/** + * A form designed for creating and editing an Active Effect on an Actor or Item entity. + * @implements {FormApplication} + * + * @param {ActiveEffect} object The target active effect being configured + * @param {object} [options] Additional options which modify this application instance + */ +export class HM3ActiveEffectConfig extends ActiveEffectConfig { + + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + template: "systems/hm3/templates/effect/active-effect-config.html", + }); + } + + /* ----------------------------------------- */ + + /** @override */ + getData(options) { + const data = super.getData(); + data.keyChoices = HM3.activeEffectKey; + return data; + } +} diff --git a/module/hm3-active-effect.js b/module/hm3-active-effect.js new file mode 100644 index 00000000..34bec326 --- /dev/null +++ b/module/hm3-active-effect.js @@ -0,0 +1,11 @@ +import { HM3ActiveEffectConfig } from './hm3-active-effect-config.js'; + +export class HM3ActiveEffect extends ActiveEffect { + constructor(...args) { + super(...args); + } + + static create(...args) { + return new HM3ActiveEffect(...args); + } +} \ No newline at end of file diff --git a/module/hm3.js b/module/hm3.js index cba9ea6d..e65b134c 100644 --- a/module/hm3.js +++ b/module/hm3.js @@ -5,123 +5,179 @@ import { HarnMasterCreatureSheet } from "./actor/creature-sheet.js" import { HarnMasterContainerSheet } from "./actor/container-sheet.js" import { HarnMasterItem } from "./item/item.js"; import { HarnMasterItemSheet } from "./item/item-sheet.js"; +import { HM3ActiveEffectConfig } from "./hm3-active-effect-config.js"; import { HM3 } from "./config.js"; import { DiceHM3 } from "./dice-hm3.js"; import { registerSystemSettings } from "./settings.js"; import * as migrations from "./migrations.js"; import * as macros from "./macros.js"; import * as combat from "./combat.js"; - -Hooks.once('init', async function() { - - console.log(`HM3 | Initializing the HM3 Game System\n${HM3.ASCII}`); - - game.hm3 = { - HarnMasterActor, - HarnMasterItem, - config: HM3, - macros: macros, - migrations: migrations - }; - - /** - * Set an initiative formula for the system - * @type {String} - */ - CONFIG.Combat.initiative = { - formula: "@initiative", - decimals: 2 - }; - - CONFIG.HM3 = HM3; - - // Register system settings - registerSystemSettings(); - - // Define custom Entity classes - CONFIG.Actor.entityClass = HarnMasterActor; - CONFIG.Item.entityClass = HarnMasterItem; - - // Register sheet application classes - Actors.unregisterSheet("core", ActorSheet); - Actors.registerSheet("hm3", HarnMasterCharacterSheet, { - types: ["character"], - makeDefault: true, - label: "Default HarnMaster Character Sheet" - }); - Actors.registerSheet("hm3", HarnMasterCreatureSheet, { - types: ["creature"], - makeDefault: true, - label: "Default HarnMaster Creature Sheet" - }); - Actors.registerSheet("hm3", HarnMasterContainerSheet, { - types: ["container"], - makeDefault: true, - label: "Default HarnMaster Container Sheet" - }); - - Items.unregisterSheet("core", ItemSheet); - Items.registerSheet("hm3", HarnMasterItemSheet, { makeDefault: true }); - - // If you need to add Handlebars helpers, here are a few useful examples: - Handlebars.registerHelper('concat', function() { - var outStr = ''; - for (var arg in arguments) { - if (typeof arguments[arg] != 'object') { - outStr += arguments[arg]; - } - } - return outStr; - }); - - Handlebars.registerHelper('toLowerCase', function(str) { - return str.toLowerCase(); - }); +import * as effect from "./effect.js"; + +Hooks.once('init', async function () { + + console.log(`HM3 | Initializing the HM3 Game System\n${HM3.ASCII}`); + + game.hm3 = { + HarnMasterActor, + HarnMasterItem, + config: HM3, + macros: macros, + migrations: migrations + }; + + /** + * Set an initiative formula for the system + * @type {String} + */ + CONFIG.Combat.initiative = { + formula: "@initiative", + decimals: 2 + }; + + CONFIG.HM3 = HM3; + + // Register system settings + registerSystemSettings(); + + // Define custom ActiveEffect class +// CONFIG.ActiveEffect.entityClass = HM3ActiveEffect; + CONFIG.ActiveEffect.sheetClass = HM3ActiveEffectConfig; + + // Define custom Entity classes + CONFIG.Actor.entityClass = HarnMasterActor; + CONFIG.Item.entityClass = HarnMasterItem; + + // Register sheet application classes + Actors.unregisterSheet("core", ActorSheet); + Actors.registerSheet("hm3", HarnMasterCharacterSheet, { + types: ["character"], + makeDefault: true, + label: "Default HarnMaster Character Sheet" + }); + Actors.registerSheet("hm3", HarnMasterCreatureSheet, { + types: ["creature"], + makeDefault: true, + label: "Default HarnMaster Creature Sheet" + }); + Actors.registerSheet("hm3", HarnMasterContainerSheet, { + types: ["container"], + makeDefault: true, + label: "Default HarnMaster Container Sheet" + }); + + Items.unregisterSheet("core", ItemSheet); + Items.registerSheet("hm3", HarnMasterItemSheet, { makeDefault: true }); + + // If you need to add Handlebars helpers, here are a few useful examples: + Handlebars.registerHelper('concat', function () { + var outStr = ''; + for (var arg in arguments) { + if (typeof arguments[arg] != 'object') { + outStr += arguments[arg]; + } + } + return outStr; + }); + + Handlebars.registerHelper('toLowerCase', function (str) { + return str.toLowerCase(); + }); }); Hooks.on("renderChatMessage", (app, html, data) => { - // Display action buttons - combat.displayChatActionButtons(app, html, data); + // Display action buttons + combat.displayChatActionButtons(app, html, data); }); Hooks.on('renderChatLog', (app, html, data) => HarnMasterActor.chatListeners(html)); Hooks.on('renderChatPopout', (app, html, data) => HarnMasterActor.chatListeners(html)); +/** + * Active Effects need to expire at certain times, so keep track of that here + */ +Hooks.on('updateWorldTime', async (currentTime, change) => { + // Disable any expired active effects (WorldTime-based durations). + await effect.checkExpiredActiveEffects(); +}); + +Hooks.on('updateCombat', async (combat, updateData) => { + // Called when the combat object is updated. Possibly because of a change in round + // or turn. updateData will have specifics of what changed. + await effect.checkExpiredActiveEffects(); +}); + /** * Once the entire VTT framework is initialized, check to see if * we should perform a data migration. */ -Hooks.once("ready", function() { - // Determine whether a system migration is required - const currentVersion = game.settings.get("hm3", "systemMigrationVersion"); - const NEEDS_MIGRATION_VERSION = "0.7.14"; // Anything older than this must be migrated - - let needMigration = currentVersion === null || (isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion)); - if ( needMigration && game.user.isGM ) { - migrations.migrateWorld(); - } - - Hooks.on("hotbarDrop", (bar, data, slot) => macros.createHM3Macro(data, slot)); - HM3.ready = true; +Hooks.once("ready", function () { + // Determine whether a system migration is required + const currentVersion = game.settings.get("hm3", "systemMigrationVersion"); + const NEEDS_MIGRATION_VERSION = "0.7.14"; // Anything older than this must be migrated + + let needMigration = currentVersion === null || (isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion)); + if (needMigration && game.user.isGM) { + migrations.migrateWorld(); + } + + Hooks.on("hotbarDrop", (bar, data, slot) => macros.createHM3Macro(data, slot)); + HM3.ready = true; + if (game.settings.get("hm3", "showWelcomeDialog")) { + welcomeDialog().then(showAgain => { + + game.settings.set("hm3", "showWelcomeDialog", showAgain); + }); + } + + refreshAllActors(); }); +function refreshAllActors() { + game.actors.forEach(actor => { + actor.handleRefreshItems(); + }); + + canvas.tokens.ownedTokens.forEach(token => { + if (!token.data.actorLink) token.actor.handleRefreshItems(); + }); +} + // Since HM3 does not have the concept of rolling for initiative, // this hook simply prepopulates the initiative value. This ensures // that no die roll is needed. Hooks.on('preCreateCombatant', (combat, combatant, options, id) => { - if (!combatant.initiative) { - let token = canvas.tokens.get(combatant.tokenId); - combatant.initiative = token.actor.data.data.initiative; - } + if (!combatant.initiative) { + let token = canvas.tokens.get(combatant.tokenId); + combatant.initiative = token.actor.data.data.initiative; + } }); +async function welcomeDialog() { + const dlgTemplate = 'systems/hm3/templates/dialog/welcome.html'; + const html = await renderTemplate(dlgTemplate, {}); + + // Create the dialog window + return Dialog.prompt({ + title: 'Welcome!', + content: html, + label: 'OK', + callback: html => { + const form = html.querySelector("#welcome"); + const fd = new FormDataExtended(form); + const data = fd.toObject(); + return data.showOnStartup; + }, + options: { jQuery: false } + }); +} /*-------------------------------------------------------*/ /* Handlebars FUNCTIONS */ /*-------------------------------------------------------*/ -Handlebars.registerHelper("multiply", function(op1, op2) { - return op1 * op2; +Handlebars.registerHelper("multiply", function (op1, op2) { + return op1 * op2; }); -Handlebars.registerHelper("endswith", function(op1, op2) { - return op1.endsWith(op2); +Handlebars.registerHelper("endswith", function (op1, op2) { + return op1.endsWith(op2); }); diff --git a/module/item/item-sheet.js b/module/item/item-sheet.js index cb284a68..71297850 100644 --- a/module/item/item-sheet.js +++ b/module/item/item-sheet.js @@ -1,3 +1,6 @@ +import { onManageActiveEffect } from '../effect.js'; +import * as utility from '../utility.js'; + /** * Extend the basic ItemSheet with some very simple modifications * @extends {ItemSheet} @@ -32,15 +35,15 @@ export class HarnMasterItemSheet extends ItemSheet { data.hasRitualSkills = false; data.hasMagicSkills = false; - data.containers = {'On Person': 'on-person'}; + data.containers = { 'On Person': 'on-person' }; // Containers are not allowed in other containers. So if this item is a container, // don't show any other containers. if (this.actor && this.item.data.type !== 'containergear') { this.actor.items.forEach(it => { - if (it.type === 'containergear') { - data.containers[it.name] = it.id; - } + if (it.type === 'containergear') { + data.containers[it.name] = it.id; + } }); } @@ -50,10 +53,10 @@ export class HarnMasterItemSheet extends ItemSheet { data.convocations = []; if (this.actor) { this.actor.itemTypes.skill.forEach(it => { - if (it.data.data.type === 'Magic') { - data.convocations.push(it.data.name); - data.hasMagicSkills = true; - } + if (it.data.data.type === 'Magic') { + data.convocations.push(it.data.name); + data.hasMagicSkills = true; + } }); } } else if (this.item.data.type === 'invocation') { @@ -61,15 +64,15 @@ export class HarnMasterItemSheet extends ItemSheet { data.dieties = []; if (this.actor) { this.actor.itemTypes.skill.forEach(it => { - if (it.data.data.type === 'Ritual') { - data.dieties.push(it.data.name); - data.hasRitualSkills = true; - } + if (it.data.data.type === 'Ritual') { + data.dieties.push(it.data.name); + data.hasRitualSkills = true; + } }); } } else if (this.item.data.type === 'weapongear' || - this.item.data.type === 'missilegear') { - + this.item.data.type === 'missilegear') { + // Weapons need a list of combat skills data.combatSkills = []; @@ -87,18 +90,30 @@ export class HarnMasterItemSheet extends ItemSheet { this.actor.itemTypes.skill.forEach(it => { if (it.data.data.type === 'Combat') { - const lcName = it.data.name.toLowerCase(); - // Ignore the 'Dodge' and 'Initiative' skills, - // since you never want a weapon based on those skills. - if (!(lcName === 'initiative' || lcName === 'dodge')) { - data.combatSkills.push(it.data.name); - data.hasCombatSkills = true; - } + const lcName = it.data.name.toLowerCase(); + // Ignore the 'Dodge' and 'Initiative' skills, + // since you never want a weapon based on those skills. + if (!(lcName === 'initiative' || lcName === 'dodge')) { + data.combatSkills.push(it.data.name); + data.hasCombatSkills = true; + } } }); } } + data.effects = {}; + this.item.effects.forEach(effect => { + effect._getSourceName().then(()=> { + data.effects[effect.id] = { + 'source': effect.sourceName, + 'duration': utility.aeDuration(effect), + 'data': effect.data, + 'changes': utility.aeChanges(effect) + } + }) + }); + return data; } @@ -131,10 +146,14 @@ export class HarnMasterItemSheet extends ItemSheet { html.on("keypress", ".properties", ev => { var keycode = (ev.keyCode ? ev.keyCode : ev.which); if (keycode == '13') { - super.close(); + super.close(); } }); - + + html.find(".effect-control").click(ev => { + if ( this.item.isOwned ) return ui.notifications.warn("You cannot change an Item's Effects after it is associated with an Actor. To modify this Effect, go to the Actor's Effects tab.") + onManageActiveEffect(ev, this.item) + }); // Add Inventory Item html.find('.armorgear-location-add').click(this._armorgearLocationAdd.bind(this)); @@ -146,7 +165,7 @@ export class HarnMasterItemSheet extends ItemSheet { async _armorgearLocationAdd(event) { const dataset = event.currentTarget.dataset; const data = this.item.data.data; - + await this._onSubmit(event); // Submit any unsaved changes // Clone the existing locations list if it exists, otherwise set to empty array @@ -161,13 +180,13 @@ export class HarnMasterItemSheet extends ItemSheet { } // Update the list on the server - return this.item.update({"data.locations": locations}); + return this.item.update({ "data.locations": locations }); } async _armorgearLocationDelete(event) { const dataset = event.currentTarget.dataset; const data = this.item.data.data; - + await this._onSubmit(event); // Submit any unsaved changes // Clone the location list (we don't want to touch the actual list) @@ -180,6 +199,6 @@ export class HarnMasterItemSheet extends ItemSheet { } // Update the list on the server - return this.item.update({"data.locations": locations}); + return this.item.update({ "data.locations": locations }); } } diff --git a/module/item/item.js b/module/item/item.js index 3f90f25b..d2445129 100644 --- a/module/item/item.js +++ b/module/item/item.js @@ -6,149 +6,246 @@ import * as utility from '../utility.js'; * @extends {Item} */ export class HarnMasterItem extends Item { - /** - * Augment the basic Item data model with additional dynamic data. - */ - prepareData() { - super.prepareData(); - - // Get the Item's data - const itemData = this.data; - const actorData = this.actor ? this.actor.data : {}; - const data = itemData.data; - - let img = null; - switch (itemData.type) { - case 'armorlocation': - this._prepareArmorLocationData(itemData); - break; - - case 'skill': - utility.calcSkillBase(this); - - // Determine Icon. If the skill's current icon is not a standard icon - // then don't change it; if it is a standard icon, then we can safely - // change it if necessary. - if (data.type === 'Physical' && utility.isStdIcon(itemData.img, HM3.weaponSkillIcons)) img = utility.getImagePath(itemData.name); - else if (data.type === 'Communication' && utility.isStdIcon(itemData.img, HM3.commSkillIcons)) img = utility.getImagePath(itemData.name); - else if (data.type === 'Combat' && utility.isStdIcon(itemData.img, HM3.combatSkillIcons)) img = utility.getImagePath(itemData.name); - else if (data.type === 'Craft' && utility.isStdIcon(itemData.img, HM3.craftSkillIcons)) img = utility.getImagePath(itemData.name); - else if (data.type === 'Magic' && utility.isStdIcon(itemData.img, HM3.magicIcons)) { - img = utility.getImagePath(itemData.name); - if (img === DEFAULT_TOKEN) { - img = utility.getImagePath(HM3.defaultMagicIconName); - } + /** + * Augment the basic Item data model with additional dynamic data. + */ + prepareData() { + super.prepareData(); + + // Get the Item's data + const itemData = this.data; + const actorData = this.actor ? this.actor.data : {}; + const data = itemData.data; + + let img = null; + let tempWeight = 0; + + // Handle marking gear as equipped or carried + if (itemData.type.endsWith('gear')) { + // If you aren't carrying the gear, it can't be equipped + if (!data.isCarried) { + data.isEquipped = false; + } + + // Check if the item is in a container + if (data.container && data.container !== 'on-person') { + // Anything in a container is unequipped automatically + data.isEquipped = false; + } } - else if (data.type === 'Ritual' && utility.isStdIcon(itemData.img, HM3.ritualIcons)) { - img = utility.getImagePath(itemData.name); - if (img === DEFAULT_TOKEN) { - img = utility.getImagePath(HM3.defaultRitualIconName); - } + + switch (itemData.type) { + case 'armorlocation': + this._prepareArmorLocationData(itemData); + break; + + case 'skill': + utility.calcSkillBase(this); + + // Determine Icon. If the skill's current icon is not a standard icon + // then don't change it; if it is a standard icon, then we can safely + // change it if necessary. + if (data.type === 'Physical' && utility.isStdIcon(itemData.img, HM3.weaponSkillIcons)) img = utility.getImagePath(itemData.name); + else if (data.type === 'Communication' && utility.isStdIcon(itemData.img, HM3.commSkillIcons)) img = utility.getImagePath(itemData.name); + else if (data.type === 'Combat' && utility.isStdIcon(itemData.img, HM3.combatSkillIcons)) img = utility.getImagePath(itemData.name); + else if (data.type === 'Craft' && utility.isStdIcon(itemData.img, HM3.craftSkillIcons)) img = utility.getImagePath(itemData.name); + else if (data.type === 'Magic' && utility.isStdIcon(itemData.img, HM3.magicIcons)) { + img = utility.getImagePath(itemData.name); + if (img === CONST.DEFAULT_TOKEN) { + img = utility.getImagePath(HM3.defaultMagicIconName); + } + } + else if (data.type === 'Ritual' && utility.isStdIcon(itemData.img, HM3.ritualIcons)) { + img = utility.getImagePath(itemData.name); + if (img === DEFAULT_TOKEN) { + img = utility.getImagePath(HM3.defaultRitualIconName); + } + } + + + // Handle using Condition Skill for Endurance if it is present + if (itemData.name.toLowerCase() === 'condition' && this.actor) { + this.actor.data.data.hasCondition = true; + this.actor.data.data.endurance = Math.floor(data.masteryLevel / 5) || 1; + } + break; + + case 'psionic': + utility.calcSkillBase(this); + + if (utility.isStdIcon(itemData.img, HM3.psionicTalentIcons)) { + img = utility.getImagePath(itemData.name); + if (img === DEFAULT_TOKEN) { + img = utility.getImagePath(HM3.defaultPsionicsIconName); + } + } + break; + + case 'spell': + if (utility.isStdIcon(itemData.img, HM3.magicIcons)) { + img = utility.getImagePath(itemData.name); + if (img === DEFAULT_TOKEN) { + img = utility.getImagePath(data.convocation); + if (img === DEFAULT_TOKEN) { + img = utility.getImagePath(HM3.defaultMagicIconName); + } + } + } + break; + + case 'invocation': + if (utility.isStdIcon(itemData.img, HM3.ritualIcons)) { + img = utility.getImagePath(itemData.name); + if (img === DEFAULT_TOKEN) { + img = utility.getImagePath(data.diety); + if (img === DEFAULT_TOKEN) { + img = utility.getImagePath(HM3.defaultRitualIconName); + } + } + } + break; + + case 'armorgear': + if (itemData.img === DEFAULT_TOKEN) { + if (utility.isStdIcon(itemData.img, HM3.armorGearIcons)) { + img = utility.getImagePath(itemData.name); + } + } + break; + + case 'weapongear': + if (itemData.img === DEFAULT_TOKEN) { + if (utility.isStdIcon(itemData.img, HM3.weaponSkillIcons)) { + img = utility.getImagePath(itemData.name); + } + } + break; + + case 'missilegear': + if (itemData.img === DEFAULT_TOKEN) { + if (utility.isStdIcon(itemData.img, HM3.weaponSkillIcons)) { + img = utility.getImagePath(itemData.name); + } + } + break; + + case 'miscgear': + if (itemData.img === DEFAULT_TOKEN) { + if (utility.isStdIcon(itemData.img, HM3.miscGearIcons)) { + img = utility.getImagePath(itemData.name); + if (img === DEFAULT_TOKEN) { + img = utility.getImagePath(HM3.defaultMiscItemIconName); + } + } + } + break; + + case 'containergear': + if (itemData.img === DEFAULT_TOKEN) { + if (utility.isStdIcon(itemData.img, HM3.miscGearIcons)) { + img = utility.getImagePath(itemData.name); + if (img === DEFAULT_TOKEN) { + img = utility.getImagePath(HM3.defaultContainerIconName); + } + } + } + break; } - break; - - case 'psionic': - utility.calcSkillBase(this); - - if (utility.isStdIcon(itemData.img, HM3.psionicTalentIcons)) { - img = utility.getImagePath(itemData.name); - if (img === DEFAULT_TOKEN) { - img = utility.getImagePath(HM3.defaultPsionicsIconName); - } + + if (img && img != itemData.img) { + itemData.img = img; } - break; - - case 'spell': - if (utility.isStdIcon(itemData.img, HM3.magicIcons)) { - img = utility.getImagePath(itemData.name); - if (img === DEFAULT_TOKEN) { - img = utility.getImagePath(data.convocation); - if (img === DEFAULT_TOKEN) { - img = utility.getImagePath(HM3.defaultMagicIconName); + } + + prepareDerivedData() { + const itemData = this.data; + const actorData = this.actor ? this.actor.data : {}; + const data = itemData.data; + + const pctUnivPen = this.actor ? (actorData.data.universalPenalty * 5) || 0 : 0; + const pctPhysPen = this.actor ? (actorData.data.physicalPenalty * 5) || 0 : 0; + + if (itemData.type === 'skill') { + if (!data.masteryLevel || data.masteryLevel < 0) data.masteryLevel = 0; + + // Set EML for skills based on UP/PP + switch (data.type) { + case 'Combat': + case 'Physical': + data.effectiveMasteryLevel = Math.max(data.masteryLevel - pctPhysPen, 5); + break; + + default: + data.effectiveMasteryLevel = Math.max(data.masteryLevel - pctUnivPen, 5); + } - } - } - break; - - case 'invocation': - if (utility.isStdIcon(itemData.img, HM3.ritualIcons)) { - img = utility.getImagePath(itemData.name); - if (img === DEFAULT_TOKEN) { - img = utility.getImagePath(data.diety); - if (img === DEFAULT_TOKEN) { - img = utility.getImagePath(HM3.defaultRitualIconName); + + // Set some actor properties from skills + const lcSkillName = itemData.name.toLowerCase(); + if (lcSkillName === 'initiative') { + if (this.actor) actorData.data.initiative = data.effectiveMasteryLevel; + } else if (lcSkillName === 'dodge') { + if (this.actor) actorData.data.dodge = data.effectiveMasteryLevel; } - } - } - break; - - case 'weapongear': - case 'missilegear': - if (itemData.img === DEFAULT_TOKEN) { - if (utility.isStdIcon(itemData.img, HM3.weaponSkillIcons)) { - img = utility.getImagePath(itemData.name); - } - } - break; - - case 'miscgear': - if (itemData.img === DEFAULT_TOKEN) { - if (utility.isStdIcon(itemData.img, HM3.miscGearIcons)) { - img = utility.getImagePath(itemData.name); - if (img === DEFAULT_TOKEN) { - img = utility.getImagePath(HM3.defaultMiscItemIconName); + } else if (itemData.type === 'psionic') { + if (!data.masteryLevel || data.masteryLevel < 0) data.masteryLevel = 0; + data.effectiveMasteryLevel = Math.max(data.masteryLevel - pctUnivPen, 5); + } else if (itemData.type === 'injury') { + // Just make sure if injuryLevel is negative, we set it to zero + if (!data.injuryLevel || data.injuryLevel < 0) data.injuryLevel = 0; + + if (data.injuryLevel === 0) { + data.severity = ''; + } else if (data.injuryLevel == 1) { + data.severity = 'M1'; + } else if (data.injuryLevel <= 3) { + data.severity = `S${data.injuryLevel}`; + } else { + data.severity = `G${data.injuryLevel}`; } - } } - break; } - if (img && img != itemData.img) { - // this.update({'data.img': img}); - itemData.img = img; - } - } - - _prepareArmorLocationData(itemData) { - // If impactType isn't custom, then set all properties from the selected impactType - if (itemData.data.impactType != "custom") { - Object.keys(HM3.injuryLocations).forEach(key => { - if (HM3.injuryLocations[key].impactType === itemData.data.impactType) { - mergeObject(itemData.data, HM3.injuryLocations[key]); + _prepareArmorLocationData(itemData) { + // If impactType isn't custom, then set all properties from the selected impactType + if (itemData.data.impactType != "custom") { + Object.keys(HM3.injuryLocations).forEach(key => { + if (HM3.injuryLocations[key].impactType === itemData.data.impactType) { + mergeObject(itemData.data, HM3.injuryLocations[key]); + } + }); } - }); - } - if (isNaN(itemData.data.probWeight['low'])) { - itemData.data.probWeight['low'] = 0; - } + if (isNaN(itemData.data.probWeight['low'])) { + itemData.data.probWeight['low'] = 0; + } - if (isNaN(itemData.data.probWeight['mid'])) { - itemData.data.probWeight['mid'] = 0; - } + if (isNaN(itemData.data.probWeight['mid'])) { + itemData.data.probWeight['mid'] = 0; + } - if (isNaN(itemData.data.probWeight['high'])) { - itemData.data.probWeight['high'] = 0; - } + if (isNaN(itemData.data.probWeight['high'])) { + itemData.data.probWeight['high'] = 0; + } - if (isNaN(itemData.data.armorQuality)) { - itemData.data.armorQuality = 0; - } + if (isNaN(itemData.data.armorQuality)) { + itemData.data.armorQuality = 0; + } - if (isNaN(itemData.data.blunt)) { - itemData.data.blunt = 0; - } + if (isNaN(itemData.data.blunt)) { + itemData.data.blunt = 0; + } - if (isNaN(itemData.data.piercing)) { - itemData.data.piercing = 0; - } + if (isNaN(itemData.data.piercing)) { + itemData.data.piercing = 0; + } - if (isNaN(itemData.data.edged)) { - itemData.data.edged = 0; - } + if (isNaN(itemData.data.edged)) { + itemData.data.edged = 0; + } - if (isNaN(itemData.data.fire)) { - itemData.data.fire = 0; + if (isNaN(itemData.data.fire)) { + itemData.data.fire = 0; + } } - } } diff --git a/module/macros.js b/module/macros.js index ded804ae..148372c5 100644 --- a/module/macros.js +++ b/module/macros.js @@ -9,7 +9,7 @@ import * as combat from './combat.js'; * @returns {Promise} */ export async function createHM3Macro(data, slot) { - if (data.type !== "Item") return; + if (data.type !== "Item") return null; if (!data.data) return ui.notifications.warn("No macro exists for that type of object."); const item = data.data; @@ -73,7 +73,7 @@ function askWeaponMacro(name, slot, img) { content: html.trim(), buttons: { enhAttackButton: { - label: "Enhanced Attack", + label: "Automated Combat", callback: async (html) => { return await applyMacro(name, `game.hm3.macros.weaponAttack("${name}");`, slot, img, {"hm3.itemMacro": false}); } @@ -113,7 +113,7 @@ function askMissileMacro(name, slot, img) { content: html.trim(), buttons: { enhAttackButton: { - label: "Enhanced Attack", + label: "Automated Combat", callback: async (html) => { return await applyMacro(name, `game.hm3.macros.missileAttack("${name}");`, slot, img, {"hm3.itemMacro": false}); } @@ -137,18 +137,18 @@ function askMissileMacro(name, slot, img) { }); } -export function skillRoll(itemName, noDialog = false, myActor=null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function skillRoll(itemName, noDialog = false, myActor=null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); - return; + return null; } const item = combat.getItem(itemName, 'skill', actor); if (!item) { ui.notifications.warn(`${itemName} could not be found in the list of skills for ${actor.name}.`); - return; + return null; } const stdRollData = { @@ -157,8 +157,8 @@ export function skillRoll(itemName, noDialog = false, myActor=null) { notesData: { up: actor.data.data.universalPenalty, pp: actor.data.data.physicalPenalty, - il: actor.data.data.totalInjuryLevels || 0, - fatigue: actor.data.data.fatigue, + il: actor.data.data.eph.totalInjuryLevels || 0, + fatigue: actor.data.data.eph.fatigue, eml: item.data.data.effectiveMasteryLevel, ml: item.data.data.masteryLevel, sb: item.data.data.skillBase.value, @@ -168,21 +168,27 @@ export function skillRoll(itemName, noDialog = false, myActor=null) { fastforward: noDialog, notes: item.data.data.notes }; - return actor._d100StdRoll(stdRollData); + if (actor.isToken) { + stdRollData.token = actor.token.id; + } else { + stdRollData.actor = actor.id; + } + + return await DiceHM3.d100StdRoll(stdRollData); } -export function castSpellRoll(itemName, noDialog = false, myActor=null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function castSpellRoll(itemName, noDialog = false, myActor=null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); - return; + return null; } const item = combat.getItem(itemName, 'spell', actor); if (!item) { ui.notifications.warn(`${itemName} could not be found in the list of spells for ${actor.name}.`); - return; + return null; } const stdRollData = { @@ -191,8 +197,8 @@ export function castSpellRoll(itemName, noDialog = false, myActor=null) { notesData: { up: actor.data.data.universalPenalty, pp: actor.data.data.physicalPenalty, - il: actor.data.data.totalInjuryLevels || 0, - fatigue: actor.data.data.fatigue, + il: actor.data.data.eph.totalInjuryLevels || 0, + fatigue: actor.data.data.eph.fatigue, eml: item.data.data.effectiveMasteryLevel, ml: item.data.data.masteryLevel, sb: item.data.data.skillBase, @@ -205,21 +211,27 @@ export function castSpellRoll(itemName, noDialog = false, myActor=null) { fastforward: noDialog, notes: item.data.data.notes }; - return actor._d100StdRoll(stdRollData); + if (actor.isToken) { + stdRollData.token = actor.token.id; + } else { + stdRollData.actor = actor.id; + } + + return await DiceHM3.d100StdRoll(stdRollData); } -export function invokeRitualRoll(itemName, noDialog = false, myActor = null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function invokeRitualRoll(itemName, noDialog = false, myActor = null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); - return; + return null; } const item = combat.getItem(itemName, 'invocation', actor); if (!item) { ui.notifications.warn(`${itemName} could not be found in the list of ritual invocations for ${actor.name}.`); - return; + return null; } const stdRollData = { @@ -228,8 +240,8 @@ export function invokeRitualRoll(itemName, noDialog = false, myActor = null) { notesData: { up: actor.data.data.universalPenalty, pp: actor.data.data.physicalPenalty, - il: actor.data.data.totalInjuryLevels || 0, - fatigue: actor.data.data.fatigue, + il: actor.data.data.eph.totalInjuryLevels || 0, + fatigue: actor.data.data.eph.fatigue, eml: item.data.data.effectiveMasteryLevel, ml: item.data.data.masteryLevel, sb: item.data.data.skillBase, @@ -242,21 +254,27 @@ export function invokeRitualRoll(itemName, noDialog = false, myActor = null) { fastforward: noDialog, notes: item.data.data.notes }; - return actor._d100StdRoll(stdRollData); + if (actor.isToken) { + stdRollData.token = actor.token.id; + } else { + stdRollData.actor = actor.id; + } + + return await DiceHM3.d100StdRoll(stdRollData); } -export function usePsionicRoll(itemName, noDialog = false, myActor=null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function usePsionicRoll(itemName, noDialog = false, myActor=null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); - return; + return null; } const item = combat.getItem(itemName, 'psionic', actor); if (!item) { ui.notifications.warn(`${itemName} could not be found in the list of psionic talents for ${actor.name}.`); - return; + return null; } const stdRollData = { @@ -265,8 +283,8 @@ export function usePsionicRoll(itemName, noDialog = false, myActor=null) { notesData: { up: actor.data.data.universalPenalty, pp: actor.data.data.physicalPenalty, - il: actor.data.data.totalInjuryLevels || 0, - fatigue: actor.data.data.fatigue, + il: actor.data.data.eph.totalInjuryLevels || 0, + fatigue: actor.data.data.eph.fatigue, eml: item.data.data.effectiveMasteryLevel, ml: item.data.data.masteryLevel, sb: item.data.data.skillBase.value, @@ -278,15 +296,21 @@ export function usePsionicRoll(itemName, noDialog = false, myActor=null) { fastforward: noDialog, notes: item.data.data.notes }; - return actor._d100StdRoll(stdRollData); + if (actor.isToken) { + stdRollData.token = actor.token.id; + } else { + stdRollData.actor = actor.id; + } + + return await DiceHM3.d100StdRoll(stdRollData); } -export function testAbilityD6Roll(ability, noDialog = false, myActor=null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function testAbilityD6Roll(ability, noDialog = false, myActor=null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); - return; + return null; } let abilities; @@ -296,9 +320,9 @@ export function testAbilityD6Roll(ability, noDialog = false, myActor=null) { abilities = Object.keys(game.system.model.Actor.creature.abilities); } else { ui.notifications.warn(`${actor.name} does not have ability scores.`); - return; + return null; } - if (!ability || !abilities.includes(ability)) return; + if (!ability || !abilities.includes(ability)) return null; const stdRollData = { @@ -310,15 +334,21 @@ export function testAbilityD6Roll(ability, noDialog = false, myActor=null) { fastforward: noDialog, notes: '' }; - return actor._d6StdRoll(stdRollData); + if (actor.isToken) { + stdRollData.token = actor.token.id; + } else { + stdRollData.actor = actor.id; + } + + return await DiceHM3.d6Roll(stdRollData); } -export function testAbilityD100Roll(ability, noDialog = false, myActor = null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function testAbilityD100Roll(ability, noDialog = false, myActor = null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); - return; + return null; } let abilities; @@ -328,9 +358,9 @@ export function testAbilityD100Roll(ability, noDialog = false, myActor = null) { abilities = Object.keys(game.system.model.Actor.creature.abilities); } else { ui.notifications.warn(`${actor.name} does not have ability scores.`); - return; + return null; } - if (!ability || !abilities.includes(ability)) return; + if (!ability || !abilities.includes(ability)) return null; const stdRollData = { label: `d100 ${ability[0].toUpperCase()}${ability.slice(1)} Roll`, @@ -340,36 +370,42 @@ export function testAbilityD100Roll(ability, noDialog = false, myActor = null) { fastforward: noDialog, notes: '' }; - return actor._d100StdRoll(stdRollData); + if (actor.isToken) { + stdRollData.token = actor.token.id; + } else { + stdRollData.actor = actor.id; + } + + return await DiceHM3.d100StdRoll(stdRollData); } -export function weaponDamageRoll(itemName, aspect=null, myActor = null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function weaponDamageRoll(itemName, aspect=null, myActor = null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); - return; + return null; } if (aspect) { if (!['Edged', 'Piercing', 'Blunt'].includes(aspect)) { ui.notifications.warn(`Invalid aspect requested on damage roll: ${aspect}`); - return; + return null; } } const item = combat.getItem(itemName, 'weapongear', actor); if (!item) { ui.notifications.warn(`${itemName} could not be found in the list of melee weapons for ${actor.name}.`); - return; + return null; } const rollData = { notesData: { up: actor.data.data.universalPenalty, pp: actor.data.data.physicalPenalty, - il: actor.data.data.totalInjuryLevels || 0, - fatigue: actor.data.data.fatigue, + il: actor.data.data.eph.totalInjuryLevels || 0, + fatigue: actor.data.data.eph.fatigue, weaponName: item.data.name }, weapon: item.data.name, @@ -378,27 +414,33 @@ export function weaponDamageRoll(itemName, aspect=null, myActor = null) { aspect: aspect ? aspect : null, notes: item.data.data.notes }; - return DiceHM3.damageRoll(rollData); + if (actor.isToken) { + rollData.token = actor.token.id; + } else { + rollData.actor = actor.id; + } + + return await DiceHM3.damageRoll(rollData); } -export function missileDamageRoll(itemName, range=null, myActor = null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function missileDamageRoll(itemName, range=null, myActor = null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); - return; + return null; } const item = combat.getItem(itemName, 'missilegear', actor); if (!item) { ui.notifications.warn(`${itemName} could not be found in the list of melee weapons for ${actor.name}.`); - return; + return null; } if (range) { if (!['Short', 'Medium', 'Long', 'Extreme'].includes(range)) { ui.notifications.warn(`Invalid range requested on damage roll: ${range}`); - return; + return null; } } @@ -406,8 +448,8 @@ export function missileDamageRoll(itemName, range=null, myActor = null) { notesData: { up: actor.data.data.universalPenalty, pp: actor.data.data.physicalPenalty, - il: actor.data.data.totalInjuryLevels || 0, - fatigue: actor.data.data.fatigue, + il: actor.data.data.eph.totalInjuryLevels || 0, + fatigue: actor.data.data.eph.fatigue, missileName: item.data.name, aspect: item.data.data.weaponAspect }, @@ -422,81 +464,27 @@ export function missileDamageRoll(itemName, range=null, myActor = null) { speaker: speaker, notes: item.data.data.notes }; - return DiceHM3.missileDamageRoll(rollData); -} - -export function weaponAttack(itemName = null, noDialog = false, myToken = null, forceAllow=false) { - const speaker = myToken ? ChatMessage.getSpeaker({token: myToken}) : ChatMessage.getSpeaker(); - const combatant = getTokenInCombat(myToken, forceAllow); - if (!combatant) return null; - - const targets = game.user.targets; - if (!targets || targets.size !== 1) { - ui.notifications.warn(`You must have one selected target.`); - return; - } - - const targetToken = Array.from(game.user.targets)[0]; - - let weapon = null; - if (itemName) { - weapon = combat.getItem(itemName, 'weapongear', combatant.actor); + if (actor.isToken) { + rollData.token = actor.token.id; + } else { + rollData.actor = actor.id; } - - return combat.meleeAttack(combatant.token, targetToken, weapon); -} - -export function missileAttack(itemName = null, noDialog = false, myToken = null, forceAllow=false) { - const speaker = myToken ? ChatMessage.getSpeaker({token: myToken}) : ChatMessage.getSpeaker(); - const combatant = getTokenInCombat(myToken, forceAllow); - if (!combatant) return null; - const targets = game.user.targets; - if (!targets || targets.size !== 1) { - ui.notifications.warn(`You must have one selected target.`); - return; - } - - const targetToken = Array.from(game.user.targets)[0]; - - let missile = null; - if (itemName) { - missile = combat.getItem(itemName, 'missilegear', combatant.actor); - } - - return combat.missileAttack(combatant.token, targetToken, missile); -} - -export function weaponAttackResume(atkTokenId, defTokenId, action, effAML, aim, aspect, impactMod) { - const atkToken = canvas.tokens.get(atkTokenId); - if (!atkToken) { - ui.notifications.warn(`Attacker ${atkToken.name} could not be found on canvas.`); - return null; - } - - const speaker = ChatMessage.getSpeaker({token: atkToken}); - - const defToken = canvas.tokens.get(defTokenId); - if (!defToken) { - ui.notifications.warn(`Defender ${defToken.name} could not be found on canvas.`); - return null; - } - - return combat.meleeAttackResume(atkToken, defToken, action, effAML, aim, aspect, impactMod); + return await DiceHM3.missileDamageRoll(rollData); } -export function weaponAttackRoll(itemName, noDialog = false, myActor = null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function weaponAttackRoll(itemName, noDialog = false, myActor = null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); - return; + return null; } const item = combat.getItem(itemName, 'weapongear', actor); if (!item) { ui.notifications.warn(`${itemName} could not be found in the list of melee weapons for ${actor.name}.`); - return; + return null; } const stdRollData = { @@ -505,8 +493,8 @@ export function weaponAttackRoll(itemName, noDialog = false, myActor = null) { notesData: { up: actor.data.data.universalPenalty, pp: actor.data.data.physicalPenalty, - il: actor.data.data.totalInjuryLevels || 0, - fatigue: actor.data.data.fatigue, + il: actor.data.data.eph.totalInjuryLevels || 0, + fatigue: actor.data.data.eph.fatigue, ml: item.data.data.masteryLevel, sb: item.data.data.skillBase, si: item.data.data.skillIndex, @@ -519,21 +507,27 @@ export function weaponAttackRoll(itemName, noDialog = false, myActor = null) { fastforward: noDialog, notes: item.data.data.notes }; - return actor._d100StdRoll(stdRollData); + if (actor.isToken) { + stdRollData.token = actor.token.id; + } else { + stdRollData.actor = actor.id; + } + + return await DiceHM3.d100StdRoll(stdRollData); } -export function weaponDefendRoll(itemName, noDialog = false, myActor = null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function weaponDefendRoll(itemName, noDialog = false, myActor = null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); - return; + return null; } const item = combat.getItem(itemName, 'weapongear', actor); if (!item) { ui.notifications.warn(`${itemName} could not be found in the list of melee weapons for ${actor.name}.`); - return; + return null; } const stdRollData = { @@ -542,8 +536,8 @@ export function weaponDefendRoll(itemName, noDialog = false, myActor = null) { notesData: { up: actor.data.data.universalPenalty, pp: actor.data.data.physicalPenalty, - il: actor.data.data.totalInjuryLevels || 0, - fatigue: actor.data.data.fatigue, + il: actor.data.data.eph.totalInjuryLevels || 0, + fatigue: actor.data.data.eph.fatigue, ml: item.data.data.masteryLevel, sb: item.data.data.skillBase, si: item.data.data.skillIndex, @@ -555,43 +549,42 @@ export function weaponDefendRoll(itemName, noDialog = false, myActor = null) { fastforward: noDialog, notes: item.data.data.notes }; - return actor._d100StdRoll(stdRollData); + if (actor.isToken) { + stdRollData.token = actor.token.id; + } else { + stdRollData.actor = actor.id; + } + + return await DiceHM3.d100StdRoll(stdRollData); } -export function missileAttackRoll(itemName, myActor = null) { +export async function missileAttackRoll(itemName, myActor = null) { const actor = getActor(myActor); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); - return; + return null; } const speaker = ChatMessage.getSpeaker({actor: actor}); - const targetToken = getSingleTarget(); - - const range = combat.rangeToTarget(actor.token, targetToken); - const item = combat.getItem(itemName, 'missilegear', actor); if (!item) { ui.notifications.warn(`${itemName} could not be found in the list of missile weapons for ${actor.name}.`); - return; + return null; } const rollData = { notesData: { up: actor.data.data.universalPenalty, pp: actor.data.data.physicalPenalty, - il: actor.data.data.totalInjuryLevels || 0, - fatigue: actor.data.data.fatigue, + il: actor.data.data.eph.totalInjuryLevels || 0, + fatigue: actor.data.data.eph.fatigue, missileName: item.data.name }, name: item.data.name, - attackerName: actor.token.data.name, - defenderName: targetToken.data.name, target: item.data.data.attackMasteryLevel, aspect: item.data.data.weaponAspect, - range: range, rangeShort: item.data.data.range.short, rangeMedium: item.data.data.range.medium, rangeLong: item.data.data.range.long, @@ -600,15 +593,21 @@ export function missileAttackRoll(itemName, myActor = null) { speaker: speaker, notes: item.data.data.notes } - return DiceHM3.missileAttackRoll(rollData); + if (actor.isToken) { + rollData.token = actor.token.id; + } else { + rollData.actor = actor.id; + } + + return await DiceHM3.missileAttackRoll(rollData); } -export function injuryRoll(myActor = null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function injuryRoll(myActor = null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); - return; + return null; } const rollData = { @@ -621,18 +620,18 @@ export function injuryRoll(myActor = null) { return DiceHM3.injuryRoll(rollData); } -export function healingRoll(itemName, noDialog = false, myActor = null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function healingRoll(itemName, noDialog = false, myActor = null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); - return; + return null; } const item = combat.getItem(itemName, 'injury', actor); if (!item) { ui.notifications.warn(`${itemName} could not be found in the list of injuries for ${actor.name}.`); - return; + return null; } const stdRollData = { @@ -641,8 +640,8 @@ export function healingRoll(itemName, noDialog = false, myActor = null) { notesData: { up: actor.data.data.universalPenalty, pp: actor.data.data.physicalPenalty, - il: actor.data.data.totalInjuryLevels || 0, - fatigue: actor.data.data.fatigue, + il: actor.data.data.eph.totalInjuryLevels || 0, + fatigue: actor.data.data.eph.fatigue, endurance: actor.data.data.endurance, injuryName: item.data.name, healRate: item.data.data.healRate @@ -651,15 +650,21 @@ export function healingRoll(itemName, noDialog = false, myActor = null) { fastforward: noDialog, notes: item.data.data.notes }; - return actor._d100StdRoll(stdRollData); + if (actor.isToken) { + stdRollData.token = actor.token.id; + } else { + stdRollData.actor = actor.id; + } + + return await DiceHM3.d100StdRoll(stdRollData); } -export function dodgeRoll(noDialog = false, myActor = null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function dodgeRoll(noDialog = false, myActor = null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); - return; + return null; } const stdRollData = { @@ -670,15 +675,21 @@ export function dodgeRoll(noDialog = false, myActor = null) { fastforward: noDialog, notes: '' }; - return actor._d100StdRoll(stdRollData); + if (actor.isToken) { + stdRollData.token = actor.token.id; + } else { + stdRollData.actor = actor.id; + } + + return await DiceHM3.d100StdRoll(stdRollData); } -export function shockRoll(noDialog = false, myActor = null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function shockRoll(noDialog = false, myActor = null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); - return; + return null; } const stdRollData = { @@ -690,55 +701,73 @@ export function shockRoll(noDialog = false, myActor = null) { fastforward: noDialog, notes: '' }; - return actor._d6StdRoll(stdRollData); + if (actor.isToken) { + stdRollData.token = actor.token.id; + } else { + stdRollData.actor = actor.id; + } + + return await DiceHM3.d6Roll(stdRollData); } -export function stumbleRoll(noDialog = false, myActor = null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function stumbleRoll(noDialog = false, myActor = null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); - return; + return null; } const stdRollData = { label: `Stumble Roll`, - target: actor.data.data.stumbleTarget, + target: actor.data.data.eph.stumbleTarget, numdice: 3, notesData: {}, speaker: speaker, fastforward: noDialog, notes: '' }; - return actor._d6StdRoll(stdRollData); + if (actor.isToken) { + stdRollData.token = actor.token.id; + } else { + stdRollData.actor = actor.id; + } + + return await DiceHM3.d6Roll(stdRollData); } -export function fumbleRoll(noDialog = false, myActor = null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function fumbleRoll(noDialog = false, myActor = null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); - return; + return null; } const stdRollData = { label: `Fumble Roll`, - target: actor.data.data.fumbleTarget, + target: actor.data.data.eph.fumbleTarget, numdice: 3, notesData: {}, speaker: speaker, fastforward: noDialog, notes: '' }; - return actor._d6StdRoll(stdRollData); + if (actor.isToken) { + stdRollData.token = actor.token.id; + } else { + stdRollData.actor = actor.id; + } + + return await DiceHM3.d6Roll(stdRollData); } -export function genericDamageRoll(myActor = null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function genericDamageRoll(myActor = null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); - return; + return null; } const rollData = { @@ -748,11 +777,17 @@ export function genericDamageRoll(myActor = null) { notesData: {}, notes: '' }; - return DiceHM3.damageRoll(rollData); + if (actor.isToken) { + rollData.token = actor.token.id; + } else { + rollData.actor = actor.id; + } + + return await DiceHM3.damageRoll(rollData); } -export function changeFatigue(newValue, myActor = null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function changeFatigue(newValue, myActor = null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor || !actor.owner) { ui.notifications.warn(`You are not an owner of ${actor.name}, so you may not change fatigue.`); @@ -769,14 +804,14 @@ export function changeFatigue(newValue, myActor = null) { if (!isNaN(value)) updateData['data.fatigue'] = value; } if (typeof updateData['data.fatigue'] !== 'undefined') { - actor.update(updateData); + await actor.update(updateData); } return true; } -export function changeMissileQuanity(missileName, newValue, myActor = null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function changeMissileQuanity(missileName, newValue, myActor = null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor.owner) { ui.notifications.warn(`You are not an owner of ${actor.name}, so you may not change ${missileName} quantity.`); @@ -801,13 +836,13 @@ export function changeMissileQuanity(missileName, newValue, myActor = null) { if (typeof updateData['data.quantity'] !== 'undefined') { updateData['_id'] = missile._id; - actor.updateOwnedItem(updateData); + await actor.updateOwnedItem(updateData); } return true; } -export function setSkillDevelopmentFlag(skillName, myActor = null) { - const speaker = myActor ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); +export async function setSkillDevelopmentFlag(skillName, myActor = null) { + const speaker = typeof myActor === 'object' ? ChatMessage.getSpeaker({actor: myActor}) : ChatMessage.getSpeaker(); const actor = getActor(myActor, speaker); if (!actor) { ui.notifications.warn(`No actor for this action could be determined.`); @@ -827,12 +862,172 @@ export function setSkillDevelopmentFlag(skillName, myActor = null) { if (!skill.data.data.improveFlag) { const updateData = { 'data.improveFlag': true }; - skill.update(updateData); + await skill.update(updateData); } return true; } +/*--------------------------------------------------------------*/ +/* AUTOMATED COMBAT */ +/*--------------------------------------------------------------*/ + +export async function weaponAttack(itemName = null, noDialog = false, myToken = null, forceAllow=false) { + const speaker = myToken ? ChatMessage.getSpeaker({token: myToken}) : ChatMessage.getSpeaker(); + const combatant = getTokenInCombat(myToken, forceAllow); + if (!combatant) return null; + + const targetToken = getUserTargetedToken(combatant); + if (!targetToken) return null; + + let weapon = null; + if (itemName) { + weapon = combat.getItem(itemName, 'weapongear', combatant.actor); + } + + return await combat.meleeAttack(combatant.token, targetToken, weapon); +} + +export async function missileAttack(itemName = null, noDialog = false, myToken = null, forceAllow=false) { + const speaker = myToken ? ChatMessage.getSpeaker({token: myToken}) : ChatMessage.getSpeaker(); + const combatant = getTokenInCombat(myToken, forceAllow); + if (!combatant) return null; + + const targetToken = getUserTargetedToken(combatant); + if (!targetToken) return null; + + let missile = null; + if (itemName) { + missile = combat.getItem(itemName, 'missilegear', combatant.actor); + } + + return await combat.missileAttack(combatant.token, targetToken, missile); +} + +/** + * Resume the attack with the defender performing the "Counterstrike" defense. + * Note that this defense is only applicable to melee attacks. + * + * @param {*} atkTokenId Token representing the attacker + * @param {*} defTokenId Token representing the defender/counterstriker + * @param {*} atkWeaponName Name of the weapon the attacker is using + * @param {*} atkEffAML The effective AML (Attack Mastery Level) of the attacker after modifiers applied + * @param {*} atkAim Attack aim ("High", "Mid", "Low") + * @param {*} atkAspect Weapon aspect ("Blunt", "Edged", "Piercing") + * @param {*} atkImpactMod Additional modifier to impact + */ +export async function meleeCounterstrikeResume(atkTokenId, defTokenId, atkWeaponName, atkEffAML, atkAim, atkAspect, atkImpactMod) { + const atkToken = canvas.tokens.get(atkTokenId); + if (!atkToken) { + ui.notifications.warn(`Attacker ${atkToken.name} could not be found on canvas.`); + return null; + } + + const speaker = ChatMessage.getSpeaker({token: atkToken}); + + const defToken = canvas.tokens.get(defTokenId); + if (!defToken) { + ui.notifications.warn(`Defender ${defToken.name} could not be found on canvas.`); + return null; + } + + return await combat.meleeCounterstrikeResume(atkToken, defToken, atkWeaponName, atkEffAML, atkAim, atkAspect, atkImpactMod); +} + +/** + * Resume the attack with the defender performing the "Dodge" defense. + * + * @param {*} atkTokenId Token representing the attacker + * @param {*} defTokenId Token representing the defender + * @param {*} type Type of attack: "melee" or "missile" + * @param {*} weaponName Name of the weapon the attacker is using + * @param {*} effAML The effective AML (Attack Mastery Level) of the attacker after modifiers applied + * @param {*} aim Attack aim ("High", "Mid", "Low") + * @param {*} aspect Weapon aspect ("Blunt", "Edged", "Piercing") + * @param {*} impactMod Additional modifier to impact + */ +export async function dodgeResume(atkTokenId, defTokenId, type, weaponName, effAML, aim, aspect, impactMod) { + const atkToken = canvas.tokens.get(atkTokenId); + if (!atkToken) { + ui.notifications.warn(`Attacker ${atkToken.name} could not be found on canvas.`); + return null; + } + + const speaker = ChatMessage.getSpeaker({token: atkToken}); + + const defToken = canvas.tokens.get(defTokenId); + if (!defToken) { + ui.notifications.warn(`Defender ${defToken.name} could not be found on canvas.`); + return null; + } + + return await combat.dodgeResume(atkToken, defToken, type, weaponName, effAML, aim, aspect, impactMod); +} + +/** + * Resume the attack with the defender performing the "Block" defense. + * + * @param {*} atkTokenId Token representing the attacker + * @param {*} defTokenId Token representing the defender + * @param {*} type Type of attack: "melee" or "missile" + * @param {*} weaponName Name of the weapon the attacker is using + * @param {*} effAML The effective AML (Attack Mastery Level) of the attacker after modifiers applied + * @param {*} aim Attack aim ("High", "Mid", "Low") + * @param {*} aspect Weapon aspect ("Blunt", "Edged", "Piercing") + * @param {*} impactMod Additional modifier to impact + */ +export async function blockResume(atkTokenId, defTokenId, type, weaponName, effAML, aim, aspect, impactMod) { + const atkToken = canvas.tokens.get(atkTokenId); + if (!atkToken) { + ui.notifications.warn(`Attacker ${atkToken.name} could not be found on canvas.`); + return null; + } + + const speaker = ChatMessage.getSpeaker({token: atkToken}); + + const defToken = canvas.tokens.get(defTokenId); + if (!defToken) { + ui.notifications.warn(`Defender ${defToken.name} could not be found on canvas.`); + return null; + } + + return await combat.blockResume(atkToken, defToken, type, weaponName, effAML, aim, aspect, impactMod); +} + +/** + * Resume the attack with the defender performing the "Ignore" defense. + * + * @param {*} atkTokenId Token representing the attacker + * @param {*} defTokenId Token representing the defender + * @param {*} type Type of attack: "melee" or "missile" + * @param {*} weaponName Name of the weapon the attacker is using + * @param {*} effAML The effective AML (Attack Mastery Level) of the attacker after modifiers applied + * @param {*} aim Attack aim ("High", "Mid", "Low") + * @param {*} aspect Weapon aspect ("Blunt", "Edged", "Piercing") + * @param {*} impactMod Additional modifier to impact + */ +export async function ignoreResume(atkTokenId, defTokenId, type, weaponName, effAML, aim, aspect, impactMod) { + const atkToken = canvas.tokens.get(atkTokenId); + if (!atkToken) { + ui.notifications.warn(`Attacker ${atkToken.name} could not be found on canvas.`); + return null; + } + + const speaker = ChatMessage.getSpeaker({token: atkToken}); + + const defToken = canvas.tokens.get(defTokenId); + if (!defToken) { + ui.notifications.warn(`Defender ${defToken.name} could not be found on canvas.`); + return null; + } + + return await combat.ignoreResume(atkToken, defToken, type, weaponName, effAML, aim, aspect, impactMod); +} + +/*--------------------------------------------------------------*/ +/* UTILITY FUNCTIONS */ +/*--------------------------------------------------------------*/ + /** * Determines the identity of the current token/actor that is in combat. If token * is specified, tries to use token (and will allow it regardless if user is GM.), @@ -867,21 +1062,40 @@ function getTokenInCombat(token=null, forceAllow=false) { return { token: token, actor: combatant.actor}; } -function getSingleTarget() { - const numTargets = canvas.tokens.controlled.length; - if (numTargets === 0) { - ui.notifications.warn(`No selected actors on the canvas.`); +function getSingleSelectedToken() { + const numTargets = canvas.tokens?.controlled?.length; + if (!numTargets) { + ui.notifications.warn(`No selected tokens on the canvas.`); return null; } if (numTargets > 1) { - ui.notifications.warn(`There are ${numTargets} selected actors on the canvas, please select only one`); + ui.notifications.warn(`There are ${numTargets} selected tokens on the canvas, please select only one`); return null; } return canvas.tokens.controlled[0]; } +function getUserTargetedToken(combatant) { + const targets = game.user.targets; + if (!targets?.size) { + ui.notifications.warn(`No targets selected, you must select exactly one target, combat aborted.`); + return null; + } else if (targets.size > 1) { + ui.notifications.warn(`${targets} targets selected, you must select exactly one target, combat aborted.`); + } + + const targetToken = Array.from(game.user.targets)[0]; + + if (combatant?.token && targetToken.id === combatant.token.id) { + ui.notifications.warn(`You have targetted the combatant, they cannot attack themself, combat aborted.`); + return null; + } + + return targetToken; +} + function getActor(actor, speaker) { let resultActor = null; @@ -904,12 +1118,15 @@ function getActor(actor, speaker) { } else { // The actor must actually be either an Actor or Token id, or actor name if (actor.startsWith('Actor$')) { + // We have a real actor id, so grab it from game.actors resultActor = game.actors.get(actor.slice(6)); } else if (actor.startsWith('Token$')) { - for (let tokActor in Object.values(game.actors.tokens)) { - if (tokActor.token.data.name === actor.slice(6)) return tokActor; - } + // We have a token id, so grab it from canvas.tokens + const tokId = actor.slice(6); + const token = canvas.tokens.get(tokId); + resultActor = token.actor; } else { + // This must be an actor name, grab it from game.actors by name resultActor = game.actors.getName(actor); } } diff --git a/module/settings.js b/module/settings.js index 1fbedaad..843c7b82 100644 --- a/module/settings.js +++ b/module/settings.js @@ -9,6 +9,16 @@ export const registerSystemSettings = function () { default: 0 }); + game.settings.register("hm3", "showWelcomeDialog", { + name: "Show Welcome Dialog On Start", + hint: "Display the welcome dialog box when the user logs in.", + scope: "client", + config: true, + type: Boolean, + default: true + + }); + game.settings.register("hm3", "weaponDamage", { name: "Weapon Damage", hint: "Enable optional combat rule that allows weapons to be damaged or destroyed on successful block (Combat 12)", diff --git a/module/utility.js b/module/utility.js index 23001388..954e8f61 100644 --- a/module/utility.js +++ b/module/utility.js @@ -129,7 +129,7 @@ export function calcSkillBase(item) { case 'mor': sumAbilities += actorData.abilities.morality.base; break; - + default: sb.isFormulaValid = false; return; @@ -162,7 +162,7 @@ export function calcSkillBase(item) { // specify the sunsign as a dual sunsign, in which case the two parts // must be separated either by a dash or a forward slash let actorSS = actorData.sunsign.trim().toLowerCase().split(/[-\/]/); - + // Call 'trim' function on all strings in actorSS actorSS.map(Function.prototype.call, String.prototype.trim); @@ -261,7 +261,7 @@ export function getAssocSkill(name, skillsItemArray, defaultSkill) { if (!name || !skillsItemArray || !skillsItemArray.length) return defaultSkill; const skills = skillsItemArray.map(s => s.data.name); - + const lcName = name.toLowerCase(); const re = /\[([^\)]+)\]/i; @@ -289,7 +289,7 @@ export function getAssocSkill(name, skillsItemArray, defaultSkill) { export function isStdIcon(iconPath, iconArray) { if (!iconPath || !iconArray) return false; - if (iconPath === DEFAULT_TOKEN) return true; + if (iconPath === CONST.DEFAULT_TOKEN) return true; let result = false; iconArray.forEach(i => { @@ -308,7 +308,7 @@ export function isStdIcon(iconPath, iconArray) { export function stringReplacer(template, values) { var keys = Object.keys(values); var func = Function(...keys, "return `" + template + "`;"); - + return func(...keys.map(k => values[k])); } @@ -318,16 +318,127 @@ export function stringReplacer(template, values) { * * @param {Integer} num */ -export function romanize (num) { +export function romanize(num) { if (isNaN(num)) return NaN; var digits = String(+num).split(""), - key = ["","C","CC","CCC","CD","D","DC","DCC","DCCC","CM", - "","X","XX","XXX","XL","L","LX","LXX","LXXX","XC", - "","I","II","III","IV","V","VI","VII","VIII","IX"], + key = ["", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM", + "", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC", + "", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"], roman = "", i = 3; while (i--) roman = (key[+digits.pop() + (i * 10)] || "") + roman; return Array(+digits.join("") + 1).join("M") + roman; } + +export function aeDuration(effect) { + const d = effect.data.duration; + + // Time-based duration + if (Number.isNumeric(d.seconds)) { + const start = (d.startTime || game.time.worldTime); + const elapsed = game.time.worldTime - start; + const remaining = Math.max(d.seconds - elapsed, 0); + //const normDuration = toNormTime(d.seconds); + const normRemaining = toNormTime(remaining); + return { + type: "seconds", + duration: d.seconds, + remaining: remaining, + label: normRemaining, + //normDuration: normDuration, + //normRemaining: normRemaining + }; + } + + // Turn-based duration + else if (d.rounds || d.turns) { + + // Determine the current combat duration + const cbt = game.combat; + const c = { round: cbt?.round ?? 0, turn: cbt?.turn ?? 0, nTurns: cbt?.turns.length ?? 1 }; + + // Determine how many rounds and turns have elapsed + let elapsedRounds = Math.max(c.round - (d.startRound || 0), 0); + let elapsedTurns = c.turn - (d.startTurn || 0); + if (elapsedTurns < 0) { + elapsedRounds -= 1; + elapsedTurns += c.nTurns; + } + + // Compute the number of rounds and turns that are remaining + let remainingRounds = (d.rounds || 0) - elapsedRounds; + let remainingTurns = (d.turns || 0) - elapsedTurns; + if (remainingTurns < 0) { + remainingRounds -= 1; + remainingTurns += c.nTurns; + } else if (remainingTurns > c.nTurns) { + remainingRounds += Math.floor(remainingTurns / c.nTurns); + remainingTurns %= c.nTurns; + } + + // Total remaining duration + if (remainingRounds < 0) { + remainingRounds = 0; + remainingTurns = 0; + } + const duration = (c.rounds || 0) + ((c.turns || 0) / 100) + const remaining = remainingRounds + (remainingTurns / 100); + + // Remaining label + const label = [ + remainingRounds > 0 ? `${remainingRounds} Rounds` : null, + remainingTurns > 0 ? `${remainingTurns} Turns` : null, + (remainingRounds + remainingTurns) === 0 ? "None" : null + ].filterJoin(", "); + return { + type: "turns", + duration: duration, + remaining: remaining, + label: label + } + } + + // No duration + else return { + type: "none", + duration: null, + remaining: null, + label: 'None' + } +} + +export function aeChanges(effect) { + if (!effect.data.changes || !effect.data.changes.length) { + return 'No Changes'; + } + + return effect.data.changes.map(ch => { + const modes = CONST.ACTIVE_EFFECT_MODES; + const key = ch.key; + const val = ch.value; + switch ( ch.mode ) { + case modes.ADD: + return `${HM3.activeEffectKey[key]} ${val<0?'-':'+'} ${Math.abs(val)}`; + case modes.MULTIPLY: + return `${HM3.activeEffectKey[key]} x ${val}`; + case modes.OVERRIDE: + return `${HM3.activeEffectKey[key]} = ${val}`; + case modes.UPGRADE: + return `${HM3.activeEffectKey[key]} >= ${val}`; + case modes.DOWNGRADE: + return `${HM3.activeEffectKey[key]} <= ${val}`; + default: + return `${HM3.activeEffectKey[key]} custom`; + } + }).join(', '); +} + +function toNormTime(seconds) { + const normHours = Math.floor(seconds / 3600); + const remSeconds = seconds % 3600; + const normMinutes = Number(Math.floor(remSeconds / 60)).toString().padStart(2, '0'); + const normSeconds = Number(remSeconds % 60).toString().padStart(2, '0'); + return `${normHours}:${normMinutes}:${normSeconds}`; +} \ No newline at end of file diff --git a/packs/std-missile-weapons.db b/packs/std-missile-weapons.db new file mode 100644 index 00000000..303522a3 --- /dev/null +++ b/packs/std-missile-weapons.db @@ -0,0 +1,11 @@ +{"name":"Shorkana (thrown)","permission":{"default":0,"v9BoeW8JnfRjUTit":3},"type":"missilegear","data":{"notes":"","description":"","source":"HM3 Combat 16","macros":{},"quantity":1,"value":48,"weight":2,"isCarried":true,"isEquipped":true,"container":"on-person","arcane":{"isArtifact":false,"isAttuned":false,"charges":-1,"ego":0},"assocSkill":"Axe","weaponQuality":10,"attackMasteryLevel":0,"weaponAspect":"Piercing","attackModifier":0,"range":{"short":15,"medium":30,"long":60,"extreme":120},"impact":{"short":5,"medium":4,"long":3,"extreme":3}},"flags":{},"img":"systems/hm3/images/icons/svg/axe.svg","effects":[],"_id":"5WAlAv9EzbX39fZq"} +{"name":"Javelin (thrown)","permission":{"default":0,"v9BoeW8JnfRjUTit":3},"type":"missilegear","data":{"notes":"","description":"","source":"HM3 Combat 16","macros":{},"quantity":1,"value":36,"weight":3,"isCarried":true,"isEquipped":true,"container":"on-person","arcane":{"isArtifact":false,"isAttuned":false,"charges":-1,"ego":0},"assocSkill":"Spear","weaponQuality":10,"attackMasteryLevel":0,"weaponAspect":"Piercing","attackModifier":0,"range":{"short":40,"medium":80,"long":160,"extreme":320},"impact":{"short":7,"medium":6,"long":5,"extreme":4}},"flags":{},"img":"systems/hm3/images/icons/svg/spear.svg","effects":[],"_id":"7VYcH7DZFpNpkmaR"} +{"name":"Taburi (thrown)","permission":{"default":0,"v9BoeW8JnfRjUTit":3},"type":"missilegear","data":{"notes":"","description":"","source":"HM3 Combat 16","macros":{},"quantity":1,"value":20,"weight":1,"isCarried":true,"isEquipped":true,"container":"on-person","arcane":{"isArtifact":false,"isAttuned":false,"charges":-1,"ego":0},"assocSkill":"Dagger","weaponQuality":10,"attackMasteryLevel":0,"weaponAspect":"Piercing","attackModifier":0,"range":{"short":20,"medium":40,"long":80,"extreme":160},"impact":{"short":4,"medium":3,"long":2,"extreme":2}},"flags":{},"img":"systems/hm3/images/icons/svg/dagger.svg","effects":[],"_id":"INpysFyBlKZMmZgo"} +{"_id":"NZU4YyWvyo5TNECs","name":"Bolt (Crossbow)","permission":{"default":0,"v9BoeW8JnfRjUTit":3},"type":"missilegear","data":{"notes":"","description":"","source":"HM3 Combat 16","macros":{},"quantity":1,"value":1.25,"weight":0.2,"isCarried":true,"isEquipped":true,"container":"on-person","arcane":{"isArtifact":false,"isAttuned":false,"charges":-1,"ego":0},"assocSkill":"Crossbow","weaponQuality":10,"attackMasteryLevel":0,"weaponAspect":"Piercing","attackModifier":0,"range":{"short":100,"medium":200,"long":400,"extreme":800},"impact":{"short":8,"medium":7,"long":6,"extreme":5}},"flags":{},"img":"systems/hm3/images/icons/svg/arrow.svg","effects":[]} +{"name":"Stone (Staff Sling)","permission":{"default":0,"v9BoeW8JnfRjUTit":3},"type":"missilegear","data":{"notes":"","description":"","source":"HM3 Combat 16","macros":{},"quantity":1,"value":0,"weight":0.1,"isCarried":true,"isEquipped":true,"container":"on-person","arcane":{"isArtifact":false,"isAttuned":false,"charges":-1,"ego":0},"assocSkill":"Sling","weaponQuality":0,"attackMasteryLevel":0,"weaponAspect":"Blunt","attackModifier":0,"range":{"short":125,"medium":250,"long":500,"extreme":1000},"impact":{"short":5,"medium":4,"long":3,"extreme":3}},"flags":{},"img":"systems/hm3/images/icons/svg/stones.svg","effects":[],"_id":"OfAc4yKUgnOrMgBY"} +{"name":"Stone (Sling)","permission":{"default":0,"v9BoeW8JnfRjUTit":3},"type":"missilegear","data":{"notes":"","description":"","source":"HM3 Combat 16","macros":{},"quantity":1,"value":0,"weight":0.1,"isCarried":true,"isEquipped":true,"container":"on-person","arcane":{"isArtifact":false,"isAttuned":false,"charges":-1,"ego":0},"assocSkill":"Sling","weaponQuality":0,"attackMasteryLevel":0,"weaponAspect":"Blunt","attackModifier":0,"range":{"short":75,"medium":150,"long":300,"extreme":600},"impact":{"short":4,"medium":3,"long":2,"extreme":2}},"flags":{},"img":"systems/hm3/images/icons/svg/stones.svg","effects":[],"_id":"OoEtTD4tyVBgBJ79"} +{"name":"Arrow (Heartbow)","permission":{"default":0,"v9BoeW8JnfRjUTit":3},"type":"missilegear","data":{"notes":"","description":"","source":"HM3 Combat 16","macros":{},"quantity":1,"value":1.25,"weight":0.2,"isCarried":true,"isEquipped":true,"container":"on-person","arcane":{"isArtifact":false,"isAttuned":false,"charges":-1,"ego":0},"assocSkill":"Bow","weaponQuality":10,"attackMasteryLevel":0,"weaponAspect":"Piercing","attackModifier":0,"range":{"short":150,"medium":300,"long":600,"extreme":1200},"impact":{"short":9,"medium":8,"long":7,"extreme":6}},"flags":{},"img":"systems/hm3/images/icons/svg/arrow.svg","effects":[],"_id":"P2m5fisMz96MMX61"} +{"name":"Arrow (Longbow)","permission":{"default":0,"v9BoeW8JnfRjUTit":3},"type":"missilegear","data":{"notes":"","description":"","source":"HM3 Combat 16","macros":{},"quantity":1,"value":1.25,"weight":0.2,"isCarried":true,"isEquipped":true,"container":"on-person","arcane":{"isArtifact":false,"isAttuned":false,"charges":-1,"ego":0},"assocSkill":"Bow","weaponQuality":10,"attackMasteryLevel":0,"weaponAspect":"Piercing","attackModifier":0,"range":{"short":125,"medium":250,"long":500,"extreme":1000},"impact":{"short":8,"medium":7,"long":6,"extreme":5}},"flags":{},"img":"systems/hm3/images/icons/svg/arrow.svg","effects":[],"_id":"ZYOjgrGk7ZJJxj5C"} +{"name":"Arrow (Shortbow)","permission":{"default":0,"v9BoeW8JnfRjUTit":3},"type":"missilegear","data":{"notes":"","description":"","source":"HM3 Combat 16","macros":{},"quantity":1,"value":1.25,"weight":0.2,"isCarried":true,"isEquipped":true,"container":"on-person","arcane":{"isArtifact":false,"isAttuned":false,"charges":-1,"ego":0},"assocSkill":"Bow","weaponQuality":10,"attackMasteryLevel":0,"weaponAspect":"Piercing","attackModifier":0,"range":{"short":100,"medium":200,"long":400,"extreme":800},"impact":{"short":6,"medium":5,"long":4,"extreme":3}},"flags":{},"img":"systems/hm3/images/icons/svg/arrow.svg","effects":[],"_id":"e3HhLeRYoF5cGCtm"} +{"name":"Spear (thrown)","permission":{"default":0,"v9BoeW8JnfRjUTit":3},"type":"missilegear","data":{"notes":"","description":"","source":"HM3 Combat 16","macros":{},"quantity":1,"value":60,"weight":5,"isCarried":true,"isEquipped":true,"container":"on-person","arcane":{"isArtifact":false,"isAttuned":false,"charges":-1,"ego":0},"assocSkill":"Spear","weaponQuality":10,"attackMasteryLevel":0,"weaponAspect":"Piercing","attackModifier":0,"range":{"short":30,"medium":60,"long":120,"extreme":240},"impact":{"short":8,"medium":7,"long":6,"extreme":5}},"flags":{},"img":"systems/hm3/images/icons/svg/spear.svg","effects":[],"_id":"l1bsf1Iov6vauHFd"} +{"name":"Dart (Blowgun)","permission":{"default":0,"v9BoeW8JnfRjUTit":3},"type":"missilegear","data":{"notes":"","description":"","source":"HM3 Combat 16","macros":{},"quantity":1,"value":1.25,"weight":0.2,"isCarried":true,"isEquipped":true,"container":"on-person","arcane":{"isArtifact":false,"isAttuned":false,"charges":-1,"ego":0},"assocSkill":"Blowgun","weaponQuality":10,"attackMasteryLevel":0,"weaponAspect":"Piercing","attackModifier":0,"range":{"short":25,"medium":50,"long":100,"extreme":200},"impact":{"short":0,"medium":0,"long":0,"extreme":0}},"flags":{},"img":"systems/hm3/images/icons/svg/arrow.svg","effects":[],"_id":"vRGrKO0RYWcpI7vL"} diff --git a/packs/system-help.db b/packs/system-help.db new file mode 100644 index 00000000..12fed133 --- /dev/null +++ b/packs/system-help.db @@ -0,0 +1,20 @@ +{"name":"Sheet - Combat Tab","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"\nThe Combat Tab centralizes all information related to combat in a single tab. This tab contains combat statistics, weapons, armor, and injuries.
\nMore generally, this tab deals with combat, which is described more fully in the following articles:
\nThe Combat Statistics section provides access to both key data for use in combat, as well as quick access to important rolls that are used often in combat.
\nFatigue occurs in many different situations, from casting spells, to using psionic talents, to simply physical stress, but ultimately its greatest effect is in penalizing combat and causing shock. Fatigue is a user-input field. However, various other effects can change the effective fatigue value, so both effective fatigue and base fatigue are shown. The player enters the base fatigue, and effective fatigue is calculated.
\nThere are a number of assisted rolls in this section:
\nVarious fields in this section are clickable and will perform various assisted rolls. For further information on how these assisted rolls are used, please see the section @JournalEntry[Combat - Assisted].
\nAll melee and missile weapons have a field named \"Associated Skill\". This field indicates which skill is used when using that weapon. This is used to determine AML/DML. If there is a weapon specialization that applies, that would be the best choice; if not, the base skill would apply.
\nFor missile weapons where there is no particular skill, you can choose the \"Throwing\" skill.
\nYou may want to consider creating a generic \"Thrown Item\" missile weapon that represents any otherwise unspecified thrown item (possibly with a large quantity figure, such as 999). This will allow you to arbitrarily throw things such as oil flasks, chairs, etc.
\nIn order to model melee weapons which can be either used single- or two-handed, it is recommended to create two separate weapons: one for the weapon when used single-handed, and the other for the weapon when used two-handed. The entry for the one-handed weapon may be given an attack adjustment based on the \"hand mode\" of the weapon.
\nMost weapons used to launch missiles can also be used in a melee context. For instance, a bow can be used in melee as a type of staff for both attack and defense. A spear can be either used in melee combat, or thrown as a missile. In the case of items that can serve either purpose, the suggestion is to have two separate entries: one for the item as a melee weapon (e.g., \"Spear (melee)\") and one for the item as a missile (e.g., \"Spear (missile)\"). If this is the case, you may set the weight of the melee version of the item to 0, since the weight is being tracked in the missile entry for the weapon.
\nIn the case of a bow or other device used to throw a missile, the weight of the bow is separate from the weight of the arrows, so they would be tracked separately. The bow should be tracked as a melee weapon, while the arrows are tracked as a missle weapon.
\nCertain weapons do not have some weapon aspects. For example, a Round Shield has a blunt aspect, but does not have edged or piercing aspects. When this is the case, you should indicate this by placing a -1 in the entry for the aspect that is unavailable for that weapon on the melee weapon gear detail sheet (use the edit icon to open the detail sheet).
\nSince all weapons that show up on the combat page must have an entry in the gear tab, there are melee weapons specifically created for hand, foot, and head. These melee weapons can be placed in the gear tab and equipped in order to provide the ability to perform unarmed combat.
\nThe same technique can be used with creatures in order to indicate claws and other sorts of melee attacks. These would be specified as melee weapon \"gear\", but with a weight of 0 and very likely always equipped.
\nThis section contains equipped weapons that can be used in close, or melee, combat. This section only lists weapons used in combat; for information on how to add new weapons, see @JournalEntry[Sheet - Gear Tab].
\nThis field contains an icon representing the weapon and its name. If the name of the weapon is clicked, Automated Combat will be initiated with that weapon. See @JournalEntry[Combat - Automated].
\nThe weapon aspects blunt (B), edged (E), and piercing (P) indicate the base impact when an attack succeeds. The appropriate aspect may be cliced to begin rolling damage for that aspect using Assisted Combat.
\nIn order to attack with a weapon, you may click on the AML value to begin Assisted Combat with that weapon.
\nIn order to block with a weapon, you may click on the DML value to begin Assisted Combat to block with that weapon.
\nThis section is used to track equipped missile weapons, which are weapons used to attack a target at a distance. This section only details the actual missile (e.g., an arrow), not the device used to throw the missile if any (e.g., a bow).
\nVarious fields in this section are clickable and will perform various assisted rolls as described below.
\nNote that because missile ranges differ, some missiles that may seem similar will have separate entries. For instance, \"Arrow (Longbow)\" and \"Arrow (Shortbow)\" would be two separate entries, since the range for these two missiles is radically different. In practice it is unlikely you will have both a longbow and shortbow, so distinguishing between these two is not normally an issue.
\nThis field contains an icon representing the missile and its name. If the name of the missile is clicked, Automated Combat will be initiated with that weapon. See @JournalEntry[Combat - Automated].
\nMissile quantity is an important component of missile weapons: there is generally not an infinite quantity of missiles available to the attacker. The number of available missiles is tracked here (and can be modified in the Gear section, see @JournalEntry[Sheet - Gear Tab].
\nNote that once the missile quantity reaches 0, the missile may no longer be used until the quantity is increased above zero. Quantity will be reduced by one every time an attack is made with a missile weapon (regardless of whether it is successful or not).
\nEach missile weapon is associated with a particular skill, which is indicated here. For instance, an Arrow might be associated with a Bow skill.
\nMissiles have different ranges, but all missile weapons fall into four missile range categories: short, medium, long, and extreme. This section describes these ranges, using the form range/impact. Range is the maximum distance in feet to be considered at that range, and impact is the base impact of the missile at that range.
\nMissile weapons generally use only a single aspect, for example piercing, so no aspect is described.
\nClicking on the range will begin a Damage Roll using Assisted Combat at that range.
\nIn order to attack with a missile, you may click on the AML value to begin Assisted Combat with that missile.
\nThis section tracks the characters injuries. Injuries can be either an actual contusion, cut, piercing, burn, etc., but it can also include bloodloss, shock, infection, and other ailments. This section tracks all of that.
\nFor each injury, the injury level and healing rate are tracked. Note that each injury is treated separately; if you have two injuries to the head, it will show up as two separate entries.
\nThe healing rate for each injury is clickable. If clicked, it will perform a \"Healing Roll\" for that injury; If successful, the IL of the injury may be reduced.
\nNote that the injuries here will affect the Universal Penalty.
\nArmor locations are specifically identified body parts of a character or creature that can be targeted and injured. Each of these body parts will have an armor protection rating and a likelihood of having that body part hit during combat.
\nStandard Body Parts
\nBecause of the large number of variable settings associated with armor locations, a set of \"standard\" types has been created; use of these standard types will ease the burden of setting up these body locations. The standard types are:
\nWhen selecting one of those values, all of the other selections will be ignored and the settings taken from the standards for the given type. If you wish to specify your own settings, select \"Custom\".
\nBecause of the way that layering of armor in HarnMaster works, determining the armor protection values for a large set of locations can become a daunting task, especially when the player takes off and puts on various parts of armor. In order to assist with this, whenever armor is equipped (worn), it automatically calculates the protection values for those areas it covers.
\nIMPORTANT: If you have setup a custom protection value for a location, and you equip armor that covers that location, your custom values will be wiped out in favor of the values from the armor. Either you may have armor equpped, in which case it will wipe out any custom values, or you may use your custom values instead. They can't be mixed.
\nOne purpose of the Armor Locations section is to provide a list of locations that may be hit during combat. The system uses a technique called Probability Weighting to determine the likelihood of hitting a given area. In essence, instead of making each body part equally likely to be hit, you can specify the weighted likelihood that a given area will be hit.
\nAn easy way of thinking of this is to say that you are going to have 100 points, and will parcel them out across all body parts. If you say the Left Arm gets 5 points, then you are essentially saying there is a 5% chance of that body part getting hit.
\nThe same exact probabliity would happen if you chose to use 1000 points total, and gave 50 to the Left Arm. That would still be 5% probability.
\nYou may decide how many points overall and how many to give to each body part. There is no need to specify up front the total number of points; when the time comes to determine what part gets chosen, the system will add up all of the parts and then determine which gets hit according to its weight.
\nNote that there are three weightings you will need to provide: for High Aim, Low Aim, and Mid Aim.
\nIt is highly recommended you look at some examples before attempting to set these on your own.
\n\n
","img":"systems/hm3/images/icons/svg/book.svg","_id":"0HFubXxOFYP7e8xX"} +{"name":"Sheet - Façade Tab","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"\n
The Façade tab contains the publicly visible information about the character or creature. You can think of this as the information that would be avaialble to a casual observer. The tab contains a picture as well as descriptive text.
\nWhen considering what text or picture to include, consider what information a casual observer would be able to discerne. Gross physical characteristics, obvious physical or behavioral traits, and any other aspects of the character or creature are appropriate. Depending on the character, some of the information in this public description may be misleading or even false.
\nIf a player has only limited access to the character or creature, then this Façade tab will be the only tab visible to them.
\nDescriptions of biographical information, personality, character history, or other private information belong on the @JournalEntry[6HtyeakXK8cSTXvO]{Sheet - Profile Tab}
","img":"systems/hm3/images/icons/svg/book.svg","_id":"0ZQF74bH8D3iwoDc"} +{"name":"Features","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"The following is a partial list of features provided by this system:
\n\n
Top: @JournalEntry[00 Welcome]
\n","img":"systems/hm3/images/icons/svg/book.svg","_id":"2jzjWTs6B9G4WDUv"} +{"name":"Modeling Esoteric Effects","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"
There are many situations in which the use of Spells, Invocations, Psionics, or Artifacts needs to create an effect. There are multiple ways this can be done; you should use your imagination and the tools available to you to model them as you see fit. The following are some examples.
\nTo model armor that is magical and can absorb more damage, simply increase the damage absorption above what is normal for that piece of armor. For example, a kurbul breastplate artifact may absorb damage as if it were plate.
\nIf the shield is an all-around shield that can absorb damage, you can model this as an additional piece of armor with zero weight but affecting all body parts, that is equipped when the spell is active, and unequipped otherwise.
\nIf the sheid is a literal shield that pulls in attacks like a magnet, this can be modeled as an Effect that increases Melee DML. When the shield is equpped, also enable the Effect, and when the shield is unequipped, disable the effect.
\nIf the weapon is attuned to the user, and egos are in alignment, the weapon can be modeled with an increase in the weapon's attack modifier. Similarly, if the weapon is in opposition or cursed, a negative modifier can be applied.
\nCertain remedies help deaden the pain of injuries, allowing the character to remain effective longer. These can be modeled as an Effect that reduces Injury Level (perhaps multiplies IL by 0.5) for the duration of the effect.
\n","img":"systems/hm3/images/icons/svg/book.svg","_id":"8GC8cf9osW2e0BnF"} +{"name":"Sheet - Gear Tab","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"\n
The Gear tab provides a single location where all physical items carried by the character or creature are described. Armor, weapons, missiles, containers, and misc. gear are all available here.
\nAll items are either carried loose \"On Person\", or may be placed into containers. Each container carried opens up a new section in this tab where the items carried in that container are specified.
\nGear may be designated as either carried, meaning the character or creature is actually carrying the item, or uncarried, which means the item is owned by the character, but not being physically carried. For instance, the item may have been dropped on the ground, or placed in a cart. The character still owns it, but it does not count against encumbrance.
\nThe suitcase icon designates whether the item is carried or not.
\nCertain gear may be equipped, including armor and weapons. When an item is equipped, it is actively in use by the character or creature, and will show up in the Combat tab, and will be avaialble for attacks or defense. Only carried items may be equipped.
\nThe shield icon designates whether the item is equipped or not.
\nAny items placed into containers are by definition unequipped. Once taken out of a container and placed On Person, they may again be equipped.
\nAlso, any item that is not carried is also, by definition, unequipped.
\nArmor which is equipped will increase the armor rating of various body parts, depending on which body parts are covered by the armor and its protective values. These values are automatically applied to the armor locations on the Combat tab when the armor is equipped, and removed when the armor is unequipped.
\nMelee and missile weapons which are equipped will show up in the Melee Weapons or Missile Weapons sections of the Combat tab, and may be used for attack and defense as appropriate.
\nIf not otherwise designated, all equipment is placed in the \"On Person\" section. This section represents all of the loose gear carried about the person, incluidng clothing, belts, etc. Armor and weapons that are On Person may be equipped.
\nIn the header of the On Person section is a Capacity label that specifies the total weight of all gear carried (including in containers). Also, the maximum weight is also indiated. If the character carries more than the maximum weight allowed, the Capacity value will be shown in red.
\nWhen a container is added to a character, a new section appears in the Gear tab for the container. Items in the container's section are considered to be contained in the container. You may drag and drop items into and out of containers as you wish.
\nThe container consists of two parts: the physical container object (e.g., a sack), which is a separate item placed in the On Person section, and the container's section describing the contents of the container. The container's item will have weight, and may be designated as carried or not. If a container item is uncarried, all of the items in the container are also considered uncarried.
\nLimitation: You may not place containers within other containers. All container objects must be in the On Person section.
\nIn the header of the container's section is a \"Capacity\" field, which shows the total weight of all items in the container, and the maximum capacity of the container. If the total weight of all items in a container exceeds its capacity, this field will turn red.
\n","img":"systems/hm3/images/icons/svg/book.svg","_id":"8LNCScB57ZAdYRhD"} +{"name":"Sheet - Contents Tab","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"
The Contents tab, only available on Container actors, provides a list of physical items (\"Gear\") that are contained within the container.
\n\n
Top: @JournalEntry[00 Welcome]
\n","img":"systems/hm3/images/icons/svg/book.svg","_id":"8VFz9BzlQWP5xHu9"} +{"name":"Sheet - Effect Tab","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"\n
Effects are modifications to an actor's abilities or skill which may be permanent or temporary. Some effects could be turned on for a while but then disabled until the next time they are invoked.
\nTypical uses for Effects are magic items: artifacts that absorb a certain number of fatigue points, or that improve an actor's attack rolls, defense rolls, or that affect the impact of weapons. Other ways effects can be used include handling steeds (half encumbrance and fatigue when mounted), environmental effects (-20 to all missile attacks due to fog), etc.
\nEffects are particularly useful when setup to expire at some future point. For instance, an artifact may reduce fatigue by half while worn (indefinite duration), or a character in combat may become berserk for 5 rounds (+20 to AML for all melee attacks), or a potion might deaden pain of injuries (reducing total injury level by half) for 2 hours. You can specify these durations, either in game time or in terms of combat rounds/turns.
\nUltimately, all Effects are associated with actors. The entity that is affected by the Effect is the actor. But the Effect may be attached either directly to an actor, or it may be attached to an item (which is then associated with an actor). When an Effect is attached to an item, then when the item is added to an actor, all Effects from the item are inherited by the actor.
\nWhen you create an effect, you also specify its duration.
\nUnspecified - this effect is active until you specifically disable it
\nNow (Game time) - this effect is active for a certain number of game seconds/minutes/hours. You will want to also install the add-on module \"About Time\" to ensure that game time progresses.
\nNow (Current Combat) - this effect will be active for a given number of rounds of combat. No separate add-on module is required for this.
\nNote that when creating an effect on an item, start time has no meaning. Start time is only meaningful when affecting a character or creature.
\nWhen an effect is first inherited by a character or creature from an item, or when the effect is created on a character or creature, the start time begins at that moment. The effect will continue operating until its duration expires.
\nIn addition, if a disabled effect is re-enabled, the start time will be set to the current time and the effect will begin.
\nFor every effect, you can choose an appropriate \"mode\". The useful modes are:
\nThe following effects are availble:
\n* For these effects, only the \"Add\" mode is applicable. All other modes have no effect.
\n","img":"systems/hm3/images/icons/svg/book.svg","_id":"ADgJFxAxAcqcF5q2"} +{"name":"Characters","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"
Characters represent oftentimes humanoid tool-wielding actors in your game. Examples of Character actors are PCs, Human and Non-Human NPCs, Gargun, some Ivastu, Ilme, and Amorvin.
\nCharacter actors have the most complex character sheet which includes sections for ritual and magic as well as calculations for physical penalty.
\nThe character sheet contains fields to record the race, gender, occupation, and move rate (measured in feet) for the character. The base movement rate is the actor's agility times five. The effective movement rate is the base movement rate minus five times the actor's physical penalty.
\nCharacters also have a Shock Index. This is meant as a gross indicator for the \"health\" of the character, and is described in @JournalEntry[Shock Index].
\nThe following articles provide details on various parts of the character sheet:
\n\n
Top: @JournalEntry[00 Welcome]
\n","img":"systems/hm3/images/icons/svg/book.svg","_id":"BvmWEdDHSifdaM0z"} +{"_id":"CT4P5XgVp0c0oz2a","name":"00 Welcome","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"folder":"","flags":{},"content":"
Welcome to HârnMaster 3 on Foundry VTT. This system has been designed to enhace the experience of playing HârnMaster by brining support for the game to the Foundry VTT platform. A wide aray of features are included, some of which are detailed in @JournalEntry[Features].
\nIt is highly recommended that you read the documentation for Foundry VTT and/or watch the various YouTube videos on using the system; I will not describe the capabilites or use of Foundry VTT here, except insofar as this system specifically uses those capabilites.
\nThis system is not designed to play the game for you or replace the rules. A specific design criteria was to automate, but provide plenty of opportunity for the GM and players to take over at any point and perform manual rolls or role-play the various aspects of the game. Feel free to use the automation when useful, and to abandon it when special conditions warrant. Just because something is automated does not mean it has to be used if situations in the game or GM/Player desires warrant.
\nActors and Items
\nFoundry VTT supports two major features in gameplay: Actors and Items. In this system Actors represent characters, creatures, and external containers. A major feature of actors are they can be placed on the playing canvas and you can move them around and even enter into combat with them (although entering combat with a container is largely pointless). Items represent either physical gear (such as a sword, piece of armor, or money) or actor features (such as traits, skills, magic spells, etc.).
\nThe three actors that are defined are:
\nItems represent physical objects or features within the game. All items, with the exception of Injuries and Armor Locations, may have Effects associated with them. See @JournalEntry[Effects] for more information.
\nThe following items are defined:
\nAlthough many modules can be used with Foundry VTT to enhance play, the following modules are specifically recommended because they interact well with features of this system.
\n","img":"systems/hm3/images/icons/svg/book.svg"} +{"name":"Combat - Automated","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"
HarnMaster combat consists of a number of phases and steps which require making various decisions and performing rolls. These activities, especially in combats with many combatants, can become complex and time consuming (i.e., slow).
\nAutomated Combat attempts to relieve these problems by leading the players through a rapid set of simple decisions, and performing all of the rolls and table lookups for the players and GM. So long as the normal combat workflow is followed, the automation will ensure that combats are performed comparatively rapidly.
\nThe players and GM must realize that automated combat assumes that the combat is being performed using the normal, usual rules. If the GM has special house rules for combat, or the players are in an unusual situation where special combat rules apply, automated combat may not work well or at all. In that case, the players and GM might consider utilizing the limited automation from assisted combat rather than attempting automated combat.
\nOne major difference between automated combat and assisted combat is that automated combat requires that there be a combat initiated, and it must be the attacker's turn to perform an attack. Assisted combat has no concept of combat turns, and can therefore be used even when outside of formal combat situations.
\nAutomated combat can only be initiated by an attacker who is in a combat (managed by the combat tracker), and it must be the attacker's turn to attack. The attacker must select a single target token (there are a variety of ways of doing this, including double right-click, clicking on the bullseye in the Token HUD, etc.). Then, the attacker begins an attack (either by clicking on the weapon name in the character sheet or via one of the automated combat macro commands). GMs can initiate assisted combat at any time with any token.
\nAutomated combat requires that the player initiating combat have a single other token marked as a target. A player can mark a token as a target either by pressing the bullseye button in the Token HUD, or double-right clicking on a token. GMs cannot use the double-right click option and instead must use the bullseye button or a module such as T is for Target.
\nAutomated combat can be initiated in one of two ways. Either by double-clicking on the token to open its Sheet and clicking on the weapon name you would like to attack with or by pressing a previously setup macro on the Hotbar for that weapon. See @JournalEntry[Combat - Overview] for a discussion of setting up macros for combat.
\nAfter a player initiates automated combat, the attacker is presented with a dialog to select weapon aspect, aim (low, mid, high), and to add additional modifiers.
\nNext, in the chat tab of the sidebar, the defender is presented with options to defend: dodge, counterstrike, block or ignore. In the case of Counterstrike or block, the defender then has to choose a weapon gear (weapon or shield) to perform the defense with.
\nOnce the defense is chosen, dice are rolled and the results compared. If the attack results in an injury, then the defender is presented with an injury button, and presses the injury button to roll it.
\nIf either the defender or attacker stumbles or fumbles, they are presented with buttons to roll the appropriate skill check.
\n","img":"systems/hm3/images/icons/svg/book.svg","_id":"GBqpdEyL0dZo9m06"} +{"name":"Macros","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"
A variety of macro functions are avaialble to expose various system capabilities to assist you in performing your own automation of the game. Macro functions can be used in your own script macros, and then attached to the macro bar or accessed in other ways.
\nCertain script macros can be automatically created for you by dragging the appropriate item from the character sheet to the macro bar. For example, dragging a weapon to the macro bar will allow you to setup automated combat for that weapon.
\nThe following macro functions are available:
\nskillRoll(itemName, noDialog) | \nroll against skill EML | \n
castSpellRoll(itemName, noDialog) | \nspell casting roll against spell EML | \n
invokeRitualRoll(itemName, noDialog) | \nritual invocation roll against invocation EML | \n
usePsionicRoll(itemName, noDialog) | \npsionic talent roll against talent EML | \n
testAbilityD6Roll(abilityName, noDialog) | \n3d6 roll against a specific ability | \n
testAbilityD100Roll(ability, noDialog) | \n1d100 roll against a specific ability | \n
weaponDamageRoll(itemName) | \ndamage roll, using attributes from a particular melee weapon | \n
missileDamageRoll(itemName) | \ndamage roll, using attributes from a particular missile weapon | \n
weaponAttackRoll(itemName, noDialog) | \nweapon attack roll, using attributes from a particular melee weapon | \n
weaponDefendRoll(itemName, noDialog) | \nweapon defend roll, using attributes from a particular melee weapon | \n
missileAttackRoll(itemName) | \nmissile attack roll, using attributes froma particular missile weapon | \n
injuryRoll() | \ninjury roll | \n
healingRoll(itemName, noDialog) | \nhealing roll for the specific injury specified | \n
dodgeRoll(noDialog) | \ndodge roll | \n
shockRoll(noDialog) | \nshock roll | \n
stumbleRoll(noDialog) | \nstumble roll | \n
fumbleRoll(noDialog) | \nfumble roll | \n
genericDamageRoll() | \ndamage roll not specific to any weapon (you must enter details of the attack) | \n
changeMissileQuanity(missileName, newQty) | \nChange missile quantity: +XX to increase, -XX to decrease, XX to change to exact value | \n
changeFatigue(newValue) | \nChange fatigue value: +XX to increase, -XX to decrease, XX to change to exact value | \n
setSkillDevelopmentFlag(skillName) | \nSet the flag for a Skill Development Roll (only set, not unset) | \n
weaponAttack(weaponName) | \nInitiate melee attack from current combatant against targetted token with weapon | \n
weaponAttackResume(atkTokenId, defTokenId, action, attackAML, attackAim, attackWpnAspect, attackImpactModification) | \nContinue attack with defender. \"action\" can be one of \"dodge\", \"block\", \"counterstrike\", or \"ignore\". | \n
missileAttack(missileName) | \n\n Initiate missile attack from current combatant against targetted token with missile \n | \n
\n
\n
itemName = name (or id) of the item to affect
\nnoDialog = [optional] flag whether or not to display dialog; if noDialog is true, all defaults will be used. Note that not all rolls support this, because some require input to operate.
","img":"systems/hm3/images/icons/svg/book.svg","_id":"IhucNzUySzrQInMO"} +{"name":"Shock Index","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"HarnMaster specifically does not include the concept of \"hit points\", or an indication of remaining health. You can track how much injury you have taken, but there is no specific, definite indication of how \"healthy\" you are. The Shock Index provides this information.
\nThe Shock Index indicates the probability that you will make your next shock roll. Since the shock roll determines whether you become incapable of action (fall unconcious or go into shock), it represents a total disability of the character. By tracking the likelihood of succeeding in the shock roll, the shock index allows you to gague the general overall \"health\" of the character or creature.
\nThe shock index is shown on the character sheet in the upper right hand corner, just below the universal and physical pentalties. It can also be shown on the character token on the canvas, so it is conveniently visible during combat.
","img":"systems/hm3/images/icons/svg/book.svg","_id":"Nt2jkVeqMGk8p2Ur"} +{"name":"Sheet - Esoterics Tab","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"\nThe Esoterics tab is where you can record any magic-like capability: Shek Pvar spells, Religious Invocations or Psionic talents.
\nThe Magic Spells section is where your Shek Pvar actors can record their spells.
\nThe spell support allows you to record the Convocation of each spell, which matches the convocation in @JournalEntry[Sheet - Skills Tab].
\nThe level of a spell follows the basic rules for magic where the level is multiplied by 5 and subtracted from the convocation ML.
\nThe note is output to the chat when the spell is cast. This is a good area to summarize the spell's effects.
\nThe description is for the full description of the spell.
\nIf the spell effects fit with what is possible with @JournalEntry[Sheet - Effect Tab] then you can add them to Spell Items prior to assigning them to an actor. Once the spell is assigned to an actor, you cannot add or edit Effects.
\nThe Ritual Invocations section is where your priest/priestess character actors can record their invocations.
\nThe invocation support allows you to record the diety of each spell, which matches the deity of the ritual in @JournalEntry[Sheet - Skills Tab].
\nThe circle of an invocation follows the basic rules for religion where the circle is multiplied by 5 and subtracted from the ritual ML.
\nThe note is output to the chat when the invocation is cast. This is a good area to summarize the invocation's effects.
\nThe description is for the full description of the invocation.
\nIf the spell effects fit with what is possible with @JournalEntry[Sheet - Effect Tab] then you can add them to Invocation Items prior to assigning them to an actor. Once the invocation is assigned to an actor, you cannot add or edit Effects.
\nThe Psionic Talents section is where your character actors can record their psionic talents if you are playing with the optional rules for psionics.
\nThe psionic item sheet allows you to record the skill base, skill base formula, and mastery level of each talent, similar to any other skill.
\nThe circle of an invocation follows the basic rules for religion where the circle is multiplied by 5 and subtracted from the ritual ML.
\nThe note is output to the chat when the psionic talent is used. This is a good area to summarize the talent's effects.
\nThe description is for the full description of the talent.
\nIf the psionic talent's effects fit with what is possible with @JournalEntry[Sheet - Effect Tab] then you can add them to a psionic talent Item prior to assigning it to an actor. Once the talent is assigned to an actor, you cannot add or edit Active Effects.
\n","img":"systems/hm3/images/icons/svg/book.svg","_id":"QKKVvbriASKXwoOU"} +{"name":"Sheet - Skills Tab","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"\n
The heart and soul of HarnMaster characters and creatures are their skills, and this tab is where those skills are recorded.
\nAt the top of this tab is a filter bar. Because the list of skills over time can become quite extensive, this filter bar allows you to quickly find and utilize a particular skill. Just type in any part of a skill name, and the list of skills will reduce to only those that match the filter criteria.
\nEach of the skills consists of three values, as described in HarnMaster Skills: Skill Base, Mastery Level, and Effective Mastery Level. Although the Mastery Level is entered by the player, the Effective Mastery Level is calculated by taking the Mastery Level and applying a modifier based on the Universal Penalty or Physical Penalty. Please see HarnMaster Skills for information on how these modifiers apply.
\nAs characters use their skills, they gain the opportunity to improve those skills. To make this process easier to track, when you use a skill and the GM decides you will have the opportunity to increase the skill, you click on the star icon next to that skill. The icon will enable, to remind you that at the end of your gaming session you may perform a skill development roll.
\nIf a skill has a SDR icon enabled, and the player clicks on it, a prompt will appear to either perform an SDR, or simply toggle off the SDR indicator.
\nIf an SDR is performed, a roll will be made and if successful the skill will automatically be increased by 1.
\nIf the skill name contains parenthesis anywhere in the name, the skill will be considered a specialization. In that case, the if the SDR is successful the skill will increase by 2 instead of 1.
\nWhenever a skill roll is performed, any note associated with that skill will be displayed on the chat card for that skill roll.
\nIn the notes field, it is possible to add content that performs variable substitution when the note is displayed on a chat card. This is indicated by the format ${variable}
. The mechanism is identical to the use of Template Literals in JavaScript, including the ability to perform expressions. The following variable values are available:
Variable | \nNotes | \n
up | \nUniversal Penalty | \n
pp | \nPhysical Penalty | \n
il | \nTotal Injury Levels | \n
fatigue | \nEffective Fatigue | \n
eml | \nCurrent Skill Effective Mastery Level | \n
ml | \nCurrent Skill Mastery Level | \n
sb | \nCurrent Skill Skill Base | \n
si | \nCurrent Skill Index | \n
Note that this substitution only occurs when the note is displayed in a chat card, it is not evaluated at any other time.
\nThe skills detail page provides a mechanism for specifying the skill base and mastery level for a skill.
\nFor each skill, you may optionally provide a formula to calculate the skill base. If a valid formula is provided, the skill base input will become read-only in favor of being calculated via the formula.
\nThe format of the formula is:
\n@<ability>, @<ability>, @<ability> [, sunsign, ...] [, modifier]\n
For example, the following is a valid formula:
\n@str, @dex, @agl, Ulandus:2, Aralius, 3\n
This formula indicates:
\n(NOTE: Only the highest sunsign modifier will be added, never multiple modifiers.)
\nValid ability identifiers are: @str
, @sta
, @dex
, @agl
, @int
, @aur
, @wil
, @eye
, @hrg
, @sml
, @voi
, @cml
, and @mor
.
\n
The remainder of this tab is divided into six sections:
\nPhysical Skills are described in HârnMaster Skills 8-10. All physical skills are subject to 5 x Physical Penalty reduction of EML.
\nThere is one special physical skill, @Compendium[hm3.std-skills-physical.CJHE6RelYvl2GdOY]{Condition}. Condition is described in Skills 9 and is an optional rule.
\nIf the Condition skill is included in an actor's physical skills, then Endurance is set equal to Condition SB. This will affect shock rolls and calculations of physical penalty.
\ne.g. If you have a Condition of 15 but a calculated Endurance of 14, then your Endurance will be set to 15 and carrying 29 lbs of gear will give you an Encumbrance penalty of 1 (29÷15) instead of 2 (29÷14)
\nCommunication skills are described in HârnMaster Skills 10-12. All communication skills are subject to Universal Penalty.
\nCombat skills are described in HârnMaster 3 Skills 18-19. All combat skills are subject to Physical Penalty.
\nCraft skills are described in HârnMaster 3 Skills 13-17. All Craft skills are subject to Universal Penalty.
\nHarnMaster has a rich and detailed magic system described in the supplement HarnMaster Magic. As part of that mechanism, each Shek-Pvar has proficiency in one or more Convocations, which are here treated as skills. If a character is a Shek-Pvar of the Peleahn convocation, they would have two Magic proficiencies: one for Peleahn, and the other for Neutral (all Shek-Pvar have Neutral proficiency in addition to their primary convocation).
\nIn order to create spells in the @JournalEntry[Sheet - Esoterics Tab], you will need to create proficiency in the appropriate convocations here.
\nHârnMaster Religion provide rich and detailed description of rituals, ritual invocations, and dieties in Hârn. This section allows characters to record their skill in a paricular religion.
\nCharacters can choose which diety (if any) they wish to worship. The Ritual skill, which is specific to each diety, indicates the knowledge the character has of their religion. After initiation into a religion a character would be presumed to open the Ritual ML at OMLx1.
\nIt is possible that a character might worship multiple dieties. In that case, the character would have multiple Ritual skills, one for each diety.
\nIn addition to the skill statistics, each ritual skill also records the Piety associated with that diety. If a character worships multiple dieties, they will have separate Piety values for each diety.
\nRitual skills are described in HârnMaster Religion 7. All Ritual skills are subject to the Universal Penalty * 5 reduction to EML.
\n","img":"systems/hm3/images/icons/svg/book.svg","_id":"TuTuhLdeiS4uOzpT"} +{"name":"Sheet - Profile Tab","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"\n
The Profile tab is where you record all the private biographical information about a character or creature. Its ability stats, non-visible information like birthday, sunsign, social class, as well as the character backstory. In the case of creatures it could be things like habitat, diet, and combat tactics.
\nThe top section of the Profile Tab contains the character or creature ability scores. Each ability score box consists of the ability name at the top, with the effective ability score in large bold numbers below that, and the base ability score at the bottom in smaller font. The effective ability score is determined by taking the base ability score and applying the Physical Penalty or Universal Penalty as appropriate.
\nBeside the base abililty score are two dice icons: or . The icon performs a 3d6 roll against the ability score, with values less than or equal to the ability as success, otherwise failure (no criticals). The icon performs a 1d100 roll against the ability score x 5; this roll may have modifiers applied, and may have critical results in addition to success or failure. Your GM will decide when to roll one or the other.
\nCertain characteristics are only applicable to characters. These include Sunsign, Race, and Gender.
\nSunsigns are used in skills to determine bonuses to the skill base, while the other fields are only descriptive in nature and are not otherwise used in any calculations.
\nLoad Rating is only applicable to creatures.
\nLoad Rating is described in Combat 21. This represents the base load a creature can carry before any encumbrance rules apply. It is specified in the Profile tab for creatures.
\nPhysical traits are anything visible on the character or any aspect which in some way impacts physical aspects of the character.
\nAnything from the Medical table on Character 9 would be a candidate for a physical trait.
\nSome physical traits may have an effect on a character, such as modifying an ability or group of skills e.g. melee skills. All effects for an actor are displayed on the @JournalEntry[Sheet - Effect Tab]. Note that if the effect is permanent, such as losing a leg, you could also just permanently make the ability adjustment in the character sheet. You don't have to use an effect.
\nNote that Effects cannot be added or edited from the edit trait dialog. To create a trait with an effect, you have to create the trait in the Items Sidebar, then drag and drop it onto the character.
\nPsyche traits are any mental aspects of the character which you want to record.
\nAnything from the Psyche table on Character 9 would be a candidate for a psyche trait.
\nSome psyche traits may have an effect on a character, such as modifying an ability or group of skills e.g. melee skills. All effects for an actor are displayed on the @JournalEntry[Sheet - Effect Tab]. Note that if the effect is permanent, such as a psychic gift that increases your aura, you could also just permanently make the ability adjustment in the character sheet. You don't have to use an effect.
\nNote that Effects cannot be added or edited from the edit trait dialog. To create a trait with an effect, you have to create the trait in the Items Sidebar, then drag and drop it onto the character.
\nThe Biography section is where you record any miscellaneous biographical information, as well as character or creature backstory. Generally this is private information that the casual observer would not be able to discern. Information in this section will not be visible to others unless they are given observer or owner privileges on the character or creature.
\nFor characters, this can include any of the highly detailed backstory information contained in the HarnMaster rules. Birthdate, social class, parentage, clan relationship and estrangement, friends and enemies, and all similar information would be appropriate here. Some of this information may contradict (intentionally) information in the public description.
\nFor creatures, this may include preferred attack patterns, habitat, special abilities, or any other relevant information.
","img":"systems/hm3/images/icons/svg/book.svg","_id":"jBtc38nbP83MSHLA"} +{"name":"Sheet - Header","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"At the top of every page is the sheet header, containing important information regarding the actor. This header differs slightly among the actors, but contains the following elements:
\nThis icon provides a visual clue to the actor, usually a headshot. This may be, but is not necessarily, the same as the avatar used on the canvas.
\nThe name can be either the personal name (in the case of Player Characters or important NPCs), or a type name (such as Militia or Tribesman or City Watch). In the case of creatures, this is almost always a type name, although when used with Species is may also be a personal name for a particular creature.
\nThese are the standard HarnMaster penalties as described in Skills 6. These penalties are automatically calculated based on encumbrance, injury level, and fatigue. They are automatically applied to abilities, move, skills, attacks, and defenses. Staying aware of the state of these two penalties is very important.
\nThe shock index describes the health of the character or creature, and is described in @JournalEntry[Shock Index]
\nThis describes the maximum capacity of the container and the current weight of the contents.
\nThe move rate is described in Combat 2; However, note that in this system the move rate is expressed in feet rather than in hexes as described in the rules, so multiply the move rate in the rules by 5 to get the number of feet. For instance, if the rules specify a character has a move rate of 12, then the move rate here would be 60.
\nMove rate is divided into the base move rate (entered by the player) and effective move rate, which is affected by various penalties. All calculations should normally use the effective move rate.
\nThe apparent occupation pursued by the character. Note that this may not be the same as their true occupation; for instance, a character may be posing as a merchant, but are actually a royal assassin. The apparent occupation would be placed here.
\nIn some cases, creatures are unique and will have a unique name, while being part of a class of creatures. This is often the case with Ivashu. This field is optional if the name field represents the class of creature (for instance, \"Bear\").
\n\n
","img":"systems/hm3/images/icons/svg/book.svg","_id":"kAGcUrWUIqtrEOsM"} +{"name":"Containers","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"
Container actors represent generally stationary collections of items. They can be treasure chests, piles of refuse, or a shopkeep's inventory. If you create a container, perhaps call it \"Party Belongings\" or similar and grant ownership access level to all the players, then it can be an easy way for PCs to swap items with each other.
\n\n
Top: @JournalEntry[00 Welcome]
\n","img":"systems/hm3/images/icons/svg/book.svg","_id":"o5ChksAIVGuEpP4N"} +{"name":"Creatures","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"
Creatures represent actors which do not generally use tools/wear armor. Creature actors lack morality and comeliness attributes. Their melee weapons are typically attached to their bodies, and their hide is their armor. This is a simpler character sheet that doesn't offer the default hit locations of a humanoid, but where the GM must enter the hit location table manually. The GM should input the armor protection values directly on the hit locations instead of via the gear tab.
\nCreatures also have a Shock Index. This is meant as a gross indicator for the \"health\" of the creature, and is described in @JournalEntry[Shock Index].
\nCreatures will often have multiple instances of their token in a scene at one time, and therefore most creature actors will not have their token linked to their actor sheet. This will allow injuries to be tracked individually for each token, and every new token added to a scene will start with no injuries.
\nExamples of Creature actors are bears, dragons, bats, deer, etc. Most entries in the Bestiary.
\nNote that one field only available for creatures is the \"Load Rating\" (LR), as described in the HarnMaster rules, Combat 21. This represents the base load a creature can carry before any encumbrance rules apply. It is specified in the Profile tab for creatures.
\n\n
Top: @JournalEntry[00 Welcome]
\n","img":"systems/hm3/images/icons/svg/book.svg","_id":"tZXHR20rgc5BBm0Z"} +{"name":"Combat - Assisted","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"
Assisted Combat is the term for combat where the players and GM completely control the flow of combat, and use automatic die rolling to perform the individual steps.
\nFor example, if there are two players each with a combatant, Alpha and Beta, the sequence might look like this:
\nThe combat would continue in this vein until completion.
\nNote that although there is some automation and rolling of dice, the flow of combat is largely coordinated by the players and GM. The players are expected to be well-versed in the rules and have several appropriate macros setup on their macro bar to speed up the flow of combat.
\nA significant benefit of this approach is the high-degree of flexibility it grants the players and GM. Because the flow of combat is completely dictated by the players, unusual or novel attacks or defenses may be employed without difficulty, while still maintaining some of the benefits of automation.
\nThe parts that play a roll in assisted combat are:
\n","img":"systems/hm3/images/icons/svg/book.svg","_id":"v2OKHgprn88OLfbV"} +{"name":"Combat - Overview","permission":{"default":0,"kpJv1acyf3Ez8dBj":3},"flags":{},"content":"
Combat in HarnMaster is a complex dance performed between the attacker and defender. Each combat round, attackers and defenders perform multiple activities and respond to various events. Within a single combat turn, there are decisions to be made on what weapons to use, what sort of attack to make, what aspect of weapon to use, what sort of defense to perform, multiple dice rolls for attacks, defense, table lookups to determine results of combat, and then more rolls to determine damage, lookup damage location, determine armor and damage absorption, and finaly determine injury and effects (including shock, fumbling, and stumbling).
\nAll of these decisions and activities can make all but the most trivial combat encounters very slow. This system was designed to specifically address this issue and increase the speed and ease of combat encounters.
\nBecause of the different needs players and GMs have, there are two complimentary mechnaisms available for combat: assisted combat and automated combat.
\nAssisted combat can be used anytime, whether in or out of formal combat. The system is designed for flexibility; users can easily insert their own activities or special rules at any point. There is very limited automation, slightly more than automated dice rolling, with the exception of Injury rolls which are comprehensive. In particular, assisted combat does not include the combat tables, which still must be consulted in order to determine outcome of combat.
\nBecause assisted combat has no requirement for the combat tracker, attacks may be initiated by anyone at any time. There is no relationship between the various rolls; the users are expected to know when to perform a particular roll in assisted combat. There is no concept of a \"target\"; players must track their targets themselves, and track what actions need to be taken and where they are in a combat situation, and determine appropriate next steps.
\nFor more information on assisted combat, see @JournalEntry[Combat - Assisted].
\nAutomated combat may only be used in a formal combat situation, with the combat tracker active. Only the combatant whose turn it is may perform attacks. Automated combat requires that the attacker designate a single target (by marking the token as \"targetted\"). Exactly one target token must be designated, no more or less. Automated combat is then initiated, and from there it guides the characters (both attacker and defender) through a series of automated steps, prompting for decisions along the way on how to respond. All dice rolls and combat table lookups are handled, and the players and GM are presented with the results of combat quickly. By leading the players and GM through the combat situation, combat encounters become relatively quick and ... \"painless\".
\nBut a drawback to automated combat is that it is relatively inflexible. It assumes the combat situation is following the \"normal\" rules, and no unusual activities are occuring outside of the standard combat flow. For this reason, it may not be appropriate for all situations. In that case, you can always fall back on Assisted Combat.
\nFor more information on automated combat, see @JournalEntry[Combat - Automated].
\nFor either assisted or automated combat, use of the macro bar is very highly suggested. The macro bar will allow you to initiate combat with the press of a single hotkey button, rather than needing to open up the character sheet and select the weapon.
\nSetting up a macro for combat is simple. Simply open the character sheet and drag the desired weapon to an open location on the hotbar. A dialog will appear asking you what sort of macro you would like to create: automated attack, or assisted attack, defense, or damage. Once you select one, a new macro will be created in that hotbar location for that weapon. You may continue to do this with other spots on the hotbar, assigning them other macros.
\nFor a list of all of the macro functions available, see @JournalEntry[Macros].
\n\n
","img":"systems/hm3/images/icons/svg/book.svg","_id":"wpxPTGayzW3jfChK"} diff --git a/scss/components/_combat.scss b/scss/components/_combat.scss index 3dab9830..222a76e2 100644 --- a/scss/components/_combat.scss +++ b/scss/components/_combat.scss @@ -20,6 +20,15 @@ .combat-stat { align-items: center; + .combat-eff-fatigue { + flex: 0 0 25px; + } + + .combat-fatigue-input { + flex: 0 0 25px; + text-align: center; + } + .combat-stat-label { flex: 0 0 55px; font-weight: bold; @@ -32,11 +41,6 @@ padding: 2px; text-align: center; } - - .combat-stat-input { - flex: 0 0 40px; - text-align: center; - } } } diff --git a/scss/components/_effects.scss b/scss/components/_effects.scss new file mode 100644 index 00000000..bf0f0d3f --- /dev/null +++ b/scss/components/_effects.scss @@ -0,0 +1,166 @@ +.effects { + .disabled { + color: #9a998f; + } + + .effects-list { + list-style: none; + margin: 7px 0; + padding: 0; + overflow-y: hidden; + + .effects-header { + margin: 2px 0; + padding: 0; + align-items: center; + background: rgba(0, 0, 0, 0.05); + border: 2px groove #eeede0; + font-weight: bold; + line-height: 24px; + + h3 { + margin: 0; + padding-left: 5px; + font-family: "Modesto Condensed", "Palatino Linotype", serif; + font-size: 20px; + font-weight: 700; + font-size: 16px; + } + + .effect-name { + color: #191813; + border-right: 1px solid #c9c7b8; + + i { + font-size: 12px; + padding-left: 5px; + } + } + + .effect-controls { + flex: 0 0 45px; + text-align: right; + padding-right: 5px; + color: #9a998f; + + .active { + color: #4b4a44; + } + } + } + + .effect-list { + list-style: none; + margin: 0; + padding: 0; + } + + .effect { + line-height: 30px; + padding: 0 2px; + border-bottom: 1px solid #c9c7b8; + + .effect-image { + flex: 0 0 30px; + background-size: 30px; + margin-right: 5px; + } + + img { + display: block; + } + + .effect-name { + cursor: pointer; + max-height: 30px; + overflow: hidden; + border-right: 1px solid #c9c7b8; + + h4 { + margin: 0; + white-space: nowrap; + overflow-x: hidden; + } + } + } + + .effect:last-child { + border-bottom: none; + } + + .effect-detail { + font-size: 12px; + color: #7a7971; + border-right: 1px solid #c9c7b8; + word-break: break-word; + white-space: nowrap; + overflow: hidden; + } + + .effect-ele { + padding-left: 5px; + border-right: 1px solid #c9c7b8; + } + + .effect-duration { + flex: 0 0 100px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .effect-source { + flex: 0 0 100px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .effect-changes { + flex: 1 0 60px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .effect-controls { + flex: 0 0 45px; + text-align: right; + + .effect-control { + flex: 0 0 22px; + font-size: 12px; + text-align: right; + color: #9a998f; + } + + .effect-equip { + color: #9a998f; + } + + .active { + color: #4b4a44; + } + } + + .effect-controls-wide { + flex: 0 0 65px; + text-align: right; + + .effect-control { + flex: 0 0 22px; + font-size: 12px; + text-align: right; + color: #9a998f; + } + + .effect-equip { + color: #9a998f; + } + + .active { + color: #4b4a44; + } + } + } +} diff --git a/scss/components/_items.scss b/scss/components/_items.scss index 3f23ad12..a60e132a 100644 --- a/scss/components/_items.scss +++ b/scss/components/_items.scss @@ -44,6 +44,11 @@ .item-name { color: #191813; border-right: 1px solid #c9c7b8; + + i { + font-size: 12px; + padding-left: 5px; + } } } diff --git a/scss/components/_profile.scss b/scss/components/_profile.scss index 20d0b1bf..1e0ccfef 100644 --- a/scss/components/_profile.scss +++ b/scss/components/_profile.scss @@ -1,19 +1,17 @@ .sheet-body { .ability-scores { - flex: 0 0 260px; + flex: 0 0 80px; gap: 0; - margin: 10px; + margin: 0; + justify-content: center; - .ability-scores-column { - border: 2px groove #eeede0; - padding: 3px; - .ability { display: block; align-items: center; text-align: center; padding: 3px 0 0 0; - border-bottom: 2px groove #eeede0; + flex: 0 0 80px; + border: 2px groove #eeede0; .ability-name { margin: 0 0 2px; @@ -40,11 +38,64 @@ font-size: 20pt; } } + + } - .ability:last-child { - border-bottom: none; - } + .traits { + h2 { + flex: 1; + margin: 0; + line-height: 36px; + font-family: "Modesto Condensed", "Palatino Linotype", serif; + font-size: 20px; + font-weight: 700; + color: #4b4a44; + background-color: #bbb; + border-top: 2px groove #fff; + border-bottom: 2px groove #fff; + padding: 5px; + } + .traits-list { + list-style: none; + margin: 7px 2px; + padding: 0; + overflow-y: hidden; + + .trait { + + .trait-name { + flex: 0 0 150px; + } + + .trait-notes { + flex: 1 0 50px; + text-align: left; + padding-left: 5px; + border-left: 1px solid #c9c7b8; + border-right: 1px solid #c9c7b8; + } + + .trait-ele { + flex: 0 0 40px; + text-align: center; + border-left: 1px solid #c9c7b8; + border-right: 1px solid #c9c7b8; + } + + .item-controls { + flex: 0 0 45px; + text-align: right; + padding-right: 5px; + + .item-control { + flex: 0 0 22px; + font-size: 12px; + text-align: center; + color: #4b4a44; + } + } + } } } @@ -60,14 +111,20 @@ margin-top: 12px; padding: 5px 0; - .label { - flex: 0 0 70px; - font-weight: bold; - text-transform: uppercase; - } + .attribute { + align-items: center; - .value { - flex: 0 0 150px; + .label { + flex: 0 0 60px; + font-weight: bold; + text-transform: uppercase; + text-align: right; + padding-right: 4px; + } + + .value { + flex: 0 0 150px; + } } } } @@ -76,9 +133,20 @@ margin-top: 5px; .label { - flex: 0 0 18px; + padding-left: 5px; + font-family: "Modesto Condensed", "Palatino Linotype", serif; + font-size: 16px; + margin: 2px 0; + align-items: center; + background: rgba(0, 0, 0, 0.05); + border: 2px groove #eeede0; font-weight: bold; - text-transform: uppercase; + line-height: 24px; + + i { + font-size: 12px; + padding-left: 5px; + } } .description { @@ -100,9 +168,5 @@ object-fit: contain; background: white; } - - .profile-desc { - margin-left: 5px; - } } } \ No newline at end of file diff --git a/scss/global/_grid.scss b/scss/global/_grid.scss index 69b58791..6b457b2d 100644 --- a/scss/global/_grid.scss +++ b/scss/global/_grid.scss @@ -8,6 +8,10 @@ padding: 0; } +.grid-fixed-row { + grid-template-rows: 25px 5px; +} + .grid-3col { grid-column: span 3 / span 3; grid-template-columns: repeat(3, minmax(0, 1fr)); diff --git a/scss/hm3.scss b/scss/hm3.scss index 822a7bc8..144b96dc 100644 --- a/scss/hm3.scss +++ b/scss/hm3.scss @@ -29,4 +29,5 @@ @import 'components/spell'; @import 'components/armorloc'; @import 'components/gear'; + @import 'components/effects' } \ No newline at end of file diff --git a/system.json b/system.json index 2afc54dc..5c433f32 100644 --- a/system.json +++ b/system.json @@ -2,8 +2,8 @@ "name": "hm3", "title": "HarnMaster 3", "description": "The HarnMaster 3 system for FoundryVTT!", - "version": "1.0.4", - "minimumCoreVersion": "0.7.1", + "version": "1.1.0", + "minimumCoreVersion": "0.7.9", "compatibleCoreVersion": "0.7.9", "templateVersion": 2, "author": "Tom Rodriguez", @@ -62,17 +62,31 @@ }, { "label": "Std Melee Weapons", - "name": "std-weapons", + "name": "std-melee-weapons", "path": "packs/std-melee-weapons.db", "system": "hm3", "entity": "Item" }, + { + "label": "Std Missile Weapons", + "name": "std-missile-weapons", + "path": "packs/std-missile-weapons.db", + "system": "hm3", + "entity": "Item" + }, { "label": "Std Armor", "name": "std-armor", "path": "packs/std-armor.db", "system": "hm3", "entity": "Item" + }, + { + "label": "System Help", + "name": "system-help", + "path": "packs/system-help.db", + "system": "hm3", + "entity": "JournalEntry" } ], "languages": [ @@ -89,6 +103,6 @@ "secondaryTokenAttribute": null, "url": "https://github.com/toastygm/HarnMaster-3-FoundryVTT/", "manifest": "https://raw.githubusercontent.com/toastygm/HarnMaster-3-FoundryVTT/master/system.json", - "download": "https://github.com/toastygm/HarnMaster-3-FoundryVTT/archive/v1.0.4.zip", + "download": "https://github.com/toastygm/HarnMaster-3-FoundryVTT/archive/v1.1.0.zip", "license": "LICENSE.txt" } diff --git a/system.test.json b/system.test.json new file mode 100644 index 00000000..b69a9b24 --- /dev/null +++ b/system.test.json @@ -0,0 +1,101 @@ +{ + "name": "hm3", + "title": "HarnMaster 3", + "description": "The HarnMaster 3 system for FoundryVTT!", + "version": "1.0.3.1", + "minimumCoreVersion": "0.7.1", + "compatibleCoreVersion": "0.7.8", + "templateVersion": 2, + "author": "Tom Rodriguez", + "esmodules": ["module/hm3.js"], + "styles": ["css/hm3.css"], + "scripts": [], + "packs": [ + { + "label": "Std Skills - Physical", + "name": "std-skills-physical", + "path": "packs/std-skills-physical.db", + "system": "hm3", + "entity": "Item" + }, + { + "label": "Std Skills - Communication", + "name": "std-skills-communication", + "path": "packs/std-skills-communication.db", + "system": "hm3", + "entity": "Item" + }, + { + "label": "Std Skills - Combat", + "name": "std-skills-combat", + "path": "packs/std-skills-combat.db", + "system": "hm3", + "entity": "Item" + }, + { + "label": "Std Skills - Crafts & Lore", + "name": "std-skills-crafts-and-lore", + "path": "packs/std-skills-crafts-and-lore.db", + "system": "hm3", + "entity": "Item" + }, + { + "label": "Std Skills - Religion", + "name": "std-skills-religion", + "path": "packs/std-skills-religion.db", + "system": "hm3", + "entity": "Item" + }, + { + "label": "Std Skills - Magic", + "name": "std-skills-magic", + "path": "packs/std-skills-magic.db", + "system": "hm3", + "entity": "Item" + }, + { + "label": "Std Psionics", + "name": "std-psionics", + "path": "packs/std-psionics.db", + "system": "hm3", + "entity": "Item" + }, + { + "label": "Std Melee Weapons", + "name": "std-weapons", + "path": "packs/std-melee-weapons.db", + "system": "hm3", + "entity": "Item" + }, + { + "label": "Std Armor", + "name": "std-armor", + "path": "packs/std-armor.db", + "system": "hm3", + "entity": "Item" + }, + { + "label": "HM3 System Help", + "name": "hm3-system-help", + "path": "packs/hm3-system-help.db", + "system": "hm3", + "entity": "JournalEntry" + } + ], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + } + ], + "socket": true, + "gridDistance": 5, + "gridUnits": "ft", + "primaryTokenAttribute": "shockIndex", + "secondaryTokenAttribute": null, + "url": "https://github.com/toastygm/HarnMaster-3-FoundryVTT/", + "manifest": "https://raw.githubusercontent.com/toastygm/HarnMaster-3-FoundryVTT/tom/system.test.json", + "download": "https://github.com/toastygm/HarnMaster-3-FoundryVTT/archive/v1.0.3.1.zip", + "license": "LICENSE.txt" +} diff --git a/template.json b/template.json index af662e5f..9b565f25 100644 --- a/template.json +++ b/template.json @@ -75,6 +75,7 @@ "description": "***INIT***", "universalPenalty": 0, "physicalPenalty": 0, + "totalInjuryLevels": 0, "biography": "", "macros": {} } @@ -95,6 +96,9 @@ }, "container": { "templates": [], + "bioImage": "systems/hm3/images/icons/svg/chest.svg", + "description": "", + "macros": {}, "capacity": { "max": 0, "value": 0 @@ -103,7 +107,7 @@ }, "Item": { "types": ["skill", "spell", "invocation", "psionic", "weapongear", "containergear", - "missilegear", "armorgear", "miscgear", "injury", "armorlocation"], + "missilegear", "armorgear", "miscgear", "injury", "armorlocation", "trait"], "templates": { "base": { "notes": "", @@ -250,6 +254,10 @@ "mid": 1, "low": 1 } + }, + "trait": { + "templates": ["base"], + "type": "Physical" } } } diff --git a/templates/actor/character-sheet.html b/templates/actor/character-sheet.html index f5f5e976..f9bf99ff 100644 --- a/templates/actor/character-sheet.html +++ b/templates/actor/character-sheet.html @@ -1,45 +1,39 @@