Best JavaScript code snippet using ng-mocks
pathfinder.service.ts
Source:pathfinder.service.ts
1import { Injectable } from '@angular/core'2import { PathfinderCharacter } from '../models/pathfinder/base'3import { SheetService } from './sheet.service'4import { StorageService } from './storage.service'5import { moveItemInArray } from '@angular/cdk/drag-drop'6import { HttpService } from './http.service'7import { DiceService } from './dice.service'8import { PathfinderArmor } from '../models/pathfinder/armor'9import { PathfinderAttack } from '../models/pathfinder/attack'10import { BtOverviewBlock } from '../models/common/block'11import { PathfinderCompanion } from '../models/pathfinder/companion'12import { PathfinderSkill } from '../models/pathfinder/skill'13import { BtNote } from '../models/common/note'14import { PathfinderFeat } from '../models/pathfinder/feat'15import { PathfinderSave } from '../models/pathfinder/save'16import { BtAttack } from '../models/common/attack'17import { BtCondition } from '../models/common/condition'18import { BtConditionEffect } from '../models/common/condition-effect'19import { BtConsumable } from '../models/common/consumable'20import { BtStat } from '../models/common/stat'21import { PathfinderKlass } from '../models/pathfinder/klass'22import { BtList } from '../models/common/list'23import { Dnd5eSpendable } from '../models/dnd5e/spendable'24import { PathfinderSpell } from '../models/pathfinder/spell'25import { BtValuable } from '../models/common/valuable'26import { PathfinderWeapon } from '../models/pathfinder/weapon'27import { PathfinderCoreConditions } from '../models/pathfinder/core-conditions'28import { BtPlayerTool } from '../models/common/player-tool.model';29import { CampaignService } from './campaign.service';30import { DicePackage } from '../models/dice/package'31import { map } from 'rxjs/operators'32import { HomebrewKitBase } from '../models/homebrew-kits/base'33@Injectable({34 providedIn: 'root'35})36export class PathfinderService {37 constructor(38 public sheetSvc: SheetService,39 public store: StorageService,40 private http: HttpService,41 private diceSvc: DiceService,42 private campaignSvc: CampaignService,43 ) { }44 public payload = (docId: string) => {45 const self: any = {}46 self.model = new PathfinderCharacter()47 self.methods = {}48 self.migrations = {}49 self.meta = {50 subscriptions: {},51 undefinedErrorCount: 0,52 }53 self.locals = {54 ready: false,55 document_id: docId,56 forbidden: false,57 document_failed: false,58 title_busy: false,59 beta_user: false,60 product_is_premium: false,61 this_product_key: 'pathfinder',62 collaborators: [],63 tabs: { showing_nav: false, active: 'general', list: [64 { title: 'Overview', id: 'overview' },65 { title: 'General', id: 'general' },66 { title: 'Abilities', id: 'abilities' },67 { title: 'Combat', id: 'combat' },68 { title: 'Skills', id: 'skills' },69 { title: 'Equipment', id: 'equipment' },70 { title: 'Items', id: 'items' },71 { title: 'Lists', id: 'lists' },72 { title: 'Journal', id: 'journal' },73 { title: 'Feats', id: 'feats' },74 { title: 'Spells', id: 'spells' },75 { title: 'Conditions', id: 'conditions' },76 { title: 'Companions', id: 'companions' },77 { title: 'Dice', id: 'dice' },78 { title: 'Settings', id: 'settings' }79 ], hidden_list: [80 { title: 'Find Spells', id: 'spell_list' }81 ]},82 permission: {83 writer: false84 },85 search: {86 feats: '',87 spells: '',88 },89 feedback: {},90 /*91 * This is the selection object. It contains arrays and objects that help power the select elements on the page.92 * This data is either too small to import from JSON or it needs to be on the page on load.93 */94 /* TODO: make it so that prestige classes can only be selected as a second class, not a primary class */95 selection: {96 level: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],97 klass: [98 'Alchemist','Antipaladin','Barbarian','Bard','Cavalier','Cleric','Druid','Fighter','Gunslinger','Inquisitor','Magus','Monk','Ninja','Oracle','Paladin','Ranger','Rogue','Samurai','Sorcerer','Summoner','Witch','Wizard',99 'Arcane Archer','Arcane Trickster','Assassin','Dragon Disciple','Duelist','Eldritch Knight','Loremaster','Mystic Theurge','Pathfinder Chronicler','Shadowdancer',100 'Arcanist','Bloodrager','Brawler','Hunter','Investigator','Shaman','Skald','Slayer','Swashbuckler','Warpriest',101 'Kineticist', 'Medium', 'Mesmerist', 'Occultist', 'Psychic', 'Spiritualist',102 'Adept','Aristocrat','Commoner','Expert','Warrior'103 ],104 race: ['Human', 'Half-Orc', 'Elf', 'Half-Elf', 'Dwarf', 'Gnome', 'Halfling'],105 size: ['Colossal', 'Gargantuan', 'Huge', 'Large', 'Medium', 'Small', 'Tiny', 'Diminutive', 'Fine'],106 size_mod: [-8, -4, -2, -1, 0, 1, 2, 4, 8],107 companion_types: ['Animal Companion', 'Familiar', 'Shapeshift'],108 progression: ['Slow', 'Medium', 'Fast'],109 advancement: {110 Slow: [0,3000,7500,14000,23000,35000,53000,77000,115000,160000,235000,330000,475000,665000,955000,1350000,1900000,2700000,3850000,5350000],111 Medium: [0,2000,5000,9000,15000,23000,35000,51000,75000,105000,155000,220000,315000,445000,635000,890000,1300000,1800000,2550000,3600000],112 Fast: [0,1300,3300,6000,10000,15000,23000,34000,50000,71000,105000,145000,210000,295000,425000,600000,850000,1200000,1700000,2400000]113 },114 'abilities': ['STR','DEX','CON','INT','WIS','CHA'],115 'weapon': {116 'type': ['--', 'Bludgeoning', 'Piercing', 'Slashing'],117 'melee_or_ranged': ['Melee', 'Ranged'],118 'crit_mult': ['x2', 'x3', 'x4', 'x5'],119 'crit_range': ['20', '19-20', '18-20', '17-20', '16-20', '15-20']120 },121 'armor': {122 'type': ['Armor', 'Shield', 'Magic', 'Other']123 },124 'babs': [0, -5, -10, -15],125 'currency': [126 { 'name': 'CP', 'id': 'copper', 'label': 'Copper Pieces' },127 { 'name': 'SP', 'id': 'silver', 'label': 'Silver Pieces' },128 { 'name': 'GP', 'id': 'gold', 'label': 'Gold Pieces' },129 { 'name': 'PP', 'id': 'platinum', 'label': 'Platinum Pieces' }130 ],131 'reverse_currency': {132 'copper': { 'unit': 'CP', 'multiplier': 1 },133 'silver': { 'unit': 'SP', 'multiplier': 10 },134 'gold': { 'unit': 'GP', 'multiplier': 100 },135 'platinum': { 'unit': 'PP', 'multiplier': 1000 }136 },137 'block_column_ids': [1, 2],138 'block_reverse_lookup': {139 'attacks_block': 'Attacks',140 'vitals_block': 'Vitals (HP/AC)',141 'abilities_block': 'Abilities',142 'conditions_block': 'Conditions',143 'skills_block': 'Skills',144 'lists_block': 'Lists',145 'custom_stats_block': 'Custom Stats',146 'consumables_block': 'Consumables',147 'valuables_block': 'Valuables',148 'feats_block': 'Feats',149 'spells_block': 'Spells',150 'weight_block': 'Weight',151 'companions_block': 'Companions',152 'powers_block': 'Powers'153 },154 'blocks': [155 { 'name': 'Attacks', 'type': 'attacks_block' },156 { 'name': 'Vitals (HP/AC)', 'type': 'vitals_block' },157 { 'name': 'Abilities', 'type': 'abilities_block' },158 { 'name': 'Conditions', 'type': 'conditions_block' },159 { 'name': 'Skills', 'type': 'skills_block' },160 { 'name': 'Lists', 'type': 'lists_block' },161 { 'name': 'Custom Stats', 'type': 'custom_stats_block' },162 { 'name': 'Consumables', 'type': 'consumables_block' },163 { 'name': 'Valuables', 'type': 'valuables_block' },164 { 'name': 'Feats', 'type': 'feats_block' },165 { 'name': 'Spells', 'type': 'spells_block' },166 { 'name': 'Weight', 'type': 'weight_block' },167 { 'name': 'Companions', 'type': 'companions_block' },168 { 'name': 'Powers', 'type': 'powers_block' }169 ],170 'skills': {171 'Acrobatics': 'DEX',172 'Appraise': 'INT',173 'Bluff': 'CHA',174 'Climb': 'STR',175 'Craft': 'INT',176 'Diplomacy': 'CHA',177 'Disable Device': 'DEX',178 'Disguise': 'CHA',179 'Escape Artist': 'DEX',180 'Fly': 'DEX',181 'Handle Animal': 'CHA',182 'Heal': 'WIS',183 'Intimidate': 'CHA',184 'Knowledge Arcana': 'INT',185 'Knowledge Dungeoneering': 'INT',186 'Knowledge Engineering': 'INT',187 'Knowledge Geography': 'INT',188 'Knowledge History': 'INT',189 'Knowledge Local': 'INT',190 'Knowledge Nature': 'INT',191 'Knowledge Nobility': 'INT',192 'Knowledge Planes': 'INT',193 'Knowledge Religion': 'INT',194 'Linguistics': 'INT',195 'Perception': 'WIS',196 'Perform': 'CHA',197 'Profession': 'WIS',198 'Ride': 'DEX',199 'Sense Motive': 'WIS',200 'Sleight of Hand': 'DEX',201 'Spellcraft': 'INT',202 'Stealth': 'DEX',203 'Survival': 'WIS',204 'Swim': 'STR',205 'Use Magic Device': 'CHA'206 },207 'styles': ['None', 'Wielding Two-Handed', 'Two-Weapon Fighting'],208 'spell_classes': [209 { 'name': 'Alchemist' },210 { 'name': 'Antipaladin' },211 { 'name': 'Bard' },212 { 'name': 'Cleric' },213 { 'name': 'Druid' },214 { 'name': 'Inquisitor' },215 { 'name': 'Magus' },216 { 'name': 'Oracle' },217 { 'name': 'Paladin' },218 { 'name': 'Ranger' },219 { 'name': 'Sorcerer' },220 { 'name': 'Summoner' },221 { 'name': 'Witch' },222 { 'name': 'Wizard' }223 ],224 'conditionStats': [225 'STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA',226 'FORT', 'REF', 'WILL', 'All Saves',227 'HP', 'SR', 'CMB', 'CMD', 'Speed', 'Initiative', 'AC', 'Dex Bonus to AC', 'Dodge Bonus to AC', 'Natural Armor Bonus', 'Weight',228 'All Skills', 'Acrobatics', 'Appraise', 'Bluff', 'Climb', 'Craft', 'Diplomacy', 'Disable Device', 'Disguise', 'Escape Artist', 'Fly', 'Handle Animal', 'Heal', 'Intimidate', 'Knowledge Arcana', 'Knowledge Dungeoneering', 'Knowledge Engineering', 'Knowledge Geography', 'Knowledge History', 'Knowledge Local', 'Knowledge Nature', 'Knowledge Nobility', 'Knowledge Planes', 'Knowledge Religion', 'Linguistics', 'Perception', 'Perform', 'Profession', 'Ride', 'Sense Motive', 'Sleight of Hand', 'Spellcraft', 'Stealth', 'Survival', 'Swim', 'Use Magic Device',229 'Attack', 'Melee Attack', 'Ranged Attack', 'Full Attack', 'Standard Attack', 'Mainhand Attack', 'Offhand Attack', 'Damage', 'Melee Damage', 'Ranged Damage', 'Full Attack Damage', 'Standard Attack Damage', 'Mainhand Damage', 'Offhand Damage'230 ],231 'spell_schools': ['Abjuration', 'Conjuration', 'Divination', 'Enchantment', 'Evocation', 'Illusion', 'Necromancy', 'Transmutation'],232 'spell_levels': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],233 xp_log: {234 amount: 0,235 text: ''236 },237 plain_dice: ['d4', 'd6', 'd8', 'd10', 'd12', 'd20'],238 }239 /*240 * This is the data object. It contains arrays and objects that help power calculations in the sheet.241 * This data is either loaded via JSON or it is evolving here and will eventually move to JSON.242 */243 /* TO DO: note what parts of the core conditions are calculated, what aren't. Most of the ability checks are NOT calculated */,244 data: {}245 }246 /*247 * Common functions across all BTT products248 */249 self.methods.onModelReady = async () => {250 if (self.model.prefs.show_confirmation) {251 self.methods.turnOnConfirmation()252 }253 self.locals.tabs.active = self.model.prefs.tab254 self.methods.getTitle()255 self.locals.data.is_touch_device = this.sheetSvc.isTouchDevice()256 self.methods.sanityCheck()257 checkCampaignId()258 await self.methods.loadBaseData()259 await self.methods.loadClassData()260 finishedLoading()261 await self.methods.loadSpells()262 }263 self.methods.onUnfrozen = (): void => {264 checkCampaignId()265 finishedLoading()266 self.methods.getTitle()267 }268 self.methods.getTitle = () => {269 // $window.document.title = self.model.name + ' | Beyond Tabletop'270 }271 self.methods.updateTitle = async () => {272 await Promise.resolve()273 self.touch()274 if (self.locals.permission.writer) {275 this.store.updatePlayerToolTitle(self.locals.user.firebase_id, self.locals.document_id, self.model.name)276 }277 }278 /*279 * Tab- and nav-related functions.280 */281 self.methods.switchTab = (index) => {282 self.locals.tabs.active = index283 if (self.locals.permission.writer) {284 self.model.prefs.tab = index285 self.locals.tabs.showing_nav = false286 self.locals.data.editing_overview = false287 }288 }289 self.methods.listTabs = () => {290 return self.locals.tabs.list291 }292 self.methods.isTabActive = (id) => {293 return self.locals.tabs.active === id294 }295 self.methods.tabClass = (id) => {296 return self.methods.isTabActive(id) ? 'active' : ''297 }298 self.methods.goToNextTab = () => {299 if (self.locals.data.is_touch_device) {300 let index = self.locals.tabs.active301 if (index === self.locals.tabs.list.length-1) {302 self.methods.switchTab(self.locals.tabs.list[0])303 } else {304 self.methods.switchTab(self.locals.tabs.list[index+1])305 }306 }307 }308 self.methods.goToPreviousTab = () => {309 if (self.locals.data.is_touch_device) {310 let index = self.locals.tabs.active311 if (index === 0) {312 self.methods.switchTab(self.locals.tabs.list[self.locals.tabs.list.length-1])313 } else {314 self.methods.switchTab(self.locals.tabs.list[index-1])315 }316 }317 }318 self.methods.toggleNav = () => {319 self.locals.tabs.showing_nav = ! self.locals.tabs.showing_nav320 }321 self.methods.getActiveNavItem = () => {322 return self.locals.tabs.active323 }324 self.methods.toggleHelp = () => {325 self.model.prefs.help = !self.model.prefs.help326 }327 self.methods.getBodyClasses = () => {328 let klasses = []329 klasses.push(self.model.prefs.help ? 'help-on' : 'help-off')330 return klasses.join(' ')331 }332 const finishedLoading = () => {333 self.locals.ready = true334 }335 const checkCampaignId = (): void => {336 const id = this.store.getOpenCampaignId()337 if (id && id !== self.model.campaign_id) {338 self.model.campaign_id = id339 }340 }341 /*342 * Common/shared functions343 */344 self.methods.remove = this.sheetSvc.remove345 self.methods.removeByObject = this.sheetSvc.removeByObject346 self.methods.levelize = this.sheetSvc.levelize347 self.methods.levelizeWithoutNum = this.sheetSvc.levelizeWithoutNum348 self.methods.turnOnConfirmation = this.sheetSvc.turnOnConfirmation349 self.methods.turnOffConfirmation = this.sheetSvc.turnOffConfirmation350 self.methods.isAdmin = () => this.sheetSvc.isAdmin(self.locals.user)351 self.methods.selectionReverseLookup = this.sheetSvc.selectionReverseLookup352 self.methods.increment = (parent, slug, dir) => {353 parent[slug] += dir354 self.touch()355 }356 /*357 * JSON load functions358 */359 self.methods.loadBaseData = async () => {360 self.locals.data = await this.http.getLocalAsPromise('/assets/data/pathfinder/base.json')361 }362 self.methods.loadClassData = async () => {363 self.locals.data.klass = await this.http.getLocalAsPromise('/assets/data/pathfinder/classes.json')364 self.methods.onClassChange(true)365 }366 self.methods.loadSpells = async () => {367 const data = await this.http.getLocalAsPromise('/assets/data/pathfinder/spells.json')368 self.locals.data.spells = data.spells369 }370 self.methods.displaySpellDescription = spell => {371 return spell.text372 }373 self.methods.sanityCheck = () => {374 self.methods.listKlasses().forEach(klass => klass.level = klass.level || 1)375 }376 /******************************************************377 * Accessor Methods378 ******************************************************/379 self.methods.$add = function (parent, slug, construct, init = {}) {380 self.touch()381 parent[slug] = parent[slug] || []382 const item = new construct(init)383 parent[slug].push(item)384 }385 // Armor386 // ---------------------------------------------------387 self.methods.listArmors = () => {388 return self.model.armors || []389 }390 self.methods.addArmor = name => {391 const json = self.locals.data.armors.find(x => x.name === name) || {}392 json.pos = self.methods.listArmors().length393 self.methods.$add(self.model, 'armors', PathfinderArmor, json)394 }395 // Attacks396 // ---------------------------------------------------397 self.methods.listAttacks = () => {398 return self.model.attacks || []399 }400 self.methods.listFirstTwoAttacks = () => self.methods.listAttacks().filter((x, i) => i < 2)401 self.methods.addAttack = (json: any = {}) => {402 json.pos = self.methods.listAttacks().length403 self.methods.$add(self.model, 'attacks', PathfinderAttack, json)404 }405 // Blocks406 // ---------------------------------------------------407 self.methods.listBlocks = () => {408 return self.model.blocks || []409 }410 self.methods.listBlocksForColumn = (column) => {411 return self.methods.listBlocks().filter(x => x.column === column)412 }413 self.methods.addBlock = (column_id) => {414 self.methods.$add(self.model, 'blocks', BtOverviewBlock, {415 name: 'Block Name',416 type: 'attacks_block',417 column: column_id,418 pos: self.methods.listBlocks().length419 })420 }421 // Companions422 // ---------------------------------------------------423 self.methods.listCompanions = () => {424 return self.model.companions || []425 }426 self.methods.getCompanion = (index) => {427 return self.model.companions[index]428 }429 self.methods.addCompanion = () => {430 self.methods.$add(self.model, 'companions', PathfinderCompanion)431 }432 self.methods.listCompanionSkills = (companion) => {433 return companion.skills || []434 }435 self.methods.addCompanionSkill = (companion) => {436 self.methods.$add(companion, 'skills', PathfinderSkill)437 }438 self.methods.listCompanionSpecials = (companion) => {439 return companion.specials || []440 }441 self.methods.addCompanionSpecial = (companion) => {442 self.methods.$add(companion, 'specials', BtNote)443 }444 self.methods.listCompanionFeats = (companion) => {445 return companion.feats || []446 }447 self.methods.addCompanionFeat = (companion) => {448 self.methods.$add(companion, 'feats', PathfinderFeat)449 }450 self.methods.listCompanionSaves = (companion) => {451 return companion.saves || []452 }453 self.methods.addCompanionSave = (companion) => {454 self.methods.$add(companion, 'saves', PathfinderSave)455 }456 self.methods.listCompanionAttacks = (companion) => {457 return companion.attacks || []458 }459 self.methods.addCompanionAttack = (companion) => {460 self.methods.$add(companion, 'attacks', BtAttack)461 }462 // Conditions463 // ---------------------------------------------------464 self.methods.listConditions = () => {465 return self.model.conditions || []466 }467 self.methods.listUserConditions = () => self.methods.listConditions().filter(x => x.source === 'user')468 self.methods.addCondition = () => {469 self.methods.$add(self.model, 'conditions', BtCondition, {470 pos: self.methods.listConditions().length471 })472 }473 self.methods.listConditionEffects = (condition) => {474 return condition.effects || []475 }476 self.methods.addConditionEffect = (condition) => {477 self.methods.$add(condition, 'effects', BtConditionEffect, {478 pos: self.methods.listConditionEffects(condition).length479 })480 }481 // Consumables482 // ---------------------------------------------------483 self.methods.listConsumables = () => {484 return self.model.consumables || []485 }486 self.methods.addConsumable = (json: any = {}) => {487 json.pos = self.methods.listConsumables().length488 self.methods.$add(self.model, 'consumables', BtConsumable, json)489 }490 // Custom Dice491 // ---------------------------------------------------492 self.methods.listCustomDice = () => {493 return self.model.custom_dice || []494 }495 self.methods.addCustomDice = () => {496 self.methods.$add(self.model, 'custom_dice', BtNote, {497 pos: self.methods.listCustomDice().length498 })499 }500 self.methods.getDieClass = (die: string): string[] => {501 return [`sw-${die}`, `sw-${die}-active`]502 }503 // Custom Stats504 // ---------------------------------------------------505 self.methods.listCustomStats = () => {506 return self.model.custom_stats || []507 }508 self.methods.addCustomStat = () => {509 self.methods.$add(self.model, 'custom_stats', BtStat, {510 name: '',511 pos: self.methods.listCustomStats().length,512 })513 }514 // Experiences515 // ---------------------------------------------------516 self.methods.listExperiences = () => {517 return self.model.experiences || []518 }519 self.methods.addExperience = () => {520 self.methods.$add(self.model, 'experiences', BtConsumable, {521 ...self.locals.selection.xp_log,522 created_at: Date.now(),523 pos: self.methods.listExperiences().length,524 })525 self.locals.selection.xp_log.amount = 0526 self.locals.selection.xp_log.text = ''527 }528 // Feats529 // ---------------------------------------------------530 self.methods.listFeats = () => {531 return self.model.feats || []532 }533 self.methods.addFeat = (json: any = {}) => {534 json.pos = self.methods.listFeats().length535 self.methods.$add(self.model, 'feats', PathfinderFeat, json)536 }537 self.methods.listFeatTexts = (feat) => {538 return feat.text || []539 }540 self.methods.listOverviewFeatTexts = (feat) => self.methods.listFeatTexts(feat).filter(x => x.text)541 self.methods.listFilteredSRDFeats = () => {542 const search = self.locals.search.feats.toLowerCase()543 const feats = (self.locals.data.feats || [])544 if (search) {545 return feats.filter(x => {546 return x.name.toLowerCase().includes(search) || x.summary.toLowerCase().includes(search)547 })548 }549 return feats550 }551 self.methods.getFeatText = (feat, index) => {552 return feat.text[index]553 }554 self.methods.addFeatText = (feat) => {555 self.methods.$add(feat, 'text', BtNote, {556 pos: self.methods.listFeatTexts(feat).length557 })558 }559 // Homebrew Kits560 // ---------------------------------------------------561 // TO come562 // Klasses563 // ---------------------------------------------------564 self.methods.listKlasses = (): PathfinderKlass[] => {565 return self.model.klasses || []566 }567 self.methods.listCasterKlasses = () => self.methods.listKlasses().filter(x => self.methods.isListedCaster(x) || self.methods.isHomebrewClass(x))568 self.methods.addKlass = () => {569 self.methods.$add(self.model, 'klasses', PathfinderKlass)570 }571 self.methods.listSpellsPerDay = (klass) => {572 return klass.spells_per_day || []573 }574 self.methods.nonNullSpellsPerDay = klass => {575 return self.methods.listSpellsPerDay(klass).filter(x => x !== null)576 }577 self.methods.nonZerothSpellsPerDay = klass => {578 return self.methods.listSpellsPerDay(klass).filter((x, i) => i > 0)579 }580 self.methods.getSpellsPerDay = (klass, index) => {581 return klass.spells_per_day[index]582 }583 self.methods.listSpellsKnown = (klass) => {584 return klass.spells_known || []585 }586 self.methods.getSpellsKnown = (klass, index) => {587 return klass.spells_known[index]588 }589 // Lists590 // ---------------------------------------------------591 self.methods.listLists = () => {592 return self.model.lists || []593 }594 self.methods.listListsBySpecial = special => {595 return self.methods.listLists().filter(x => x.special === special)596 }597 self.methods.addList = (name) => {598 self.methods.$add(self.model, 'lists', BtList, {599 name: name || '',600 pos: self.methods.listLists().length,601 })602 }603 self.methods.listListItems = (list) => {604 return list.items || []605 }606 self.methods.addListItem = (list, text) => {607 self.methods.$add(list, 'items', BtNote, {608 text: text || '',609 pos: self.methods.listListItems(list).length,610 })611 }612 // Notes613 // ---------------------------------------------------614 self.methods.listNotes = () => {615 return self.model.notes || []616 }617 self.methods.getNote = (index) => {618 return self.model.notes[index]619 }620 self.methods.addNote = () => {621 self.methods.$add(self.model, 'notes', BtNote, {622 pos: self.methods.listNotes().length623 })624 setDefaultActiveNote()625 }626 self.methods.removeNote = (note) => {627 this.sheetSvc.removeByObject(self.model.notes, note)628 setDefaultActiveNote()629 }630 const setDefaultActiveNote = () => {631 const lastNote = self.methods.listNotes()[Math.max(self.methods.listNotes().length - 1, 0)]632 if (lastNote) {633 self.methods.setActiveNote(lastNote)634 }635 }636 const resortNotes = () => {637 self.methods.listNotes().sort((a, b) => b.opened_at - a.opened_at)638 }639 self.methods.setActiveNote = (note: BtNote): void => {640 self.model.prefs.active_note = note.id641 note.opened_at = Date.now()642 resortNotes()643 self.touch()644 }645 // Powers646 // ---------------------------------------------------647 self.methods.listPowers = () => self.model.powers || []648 self.methods.getPower = index => self.model.powers[index]649 self.methods.addPower = () => {650 self.methods.$add(self.model, 'powers', Dnd5eSpendable, {651 pos: self.methods.listPowers().length652 })653 }654 // Profile655 // ---------------------------------------------------656 self.methods.listProfiles = () => {657 return self.model.profile || []658 }659 self.methods.addProfile = () => {660 self.methods.$add(self.model, 'profile', BtNote)661 }662 // Saves663 // ---------------------------------------------------664 self.methods.listSaves = () => {665 return self.model.saves || []666 }667 self.methods.getSave = (index) => {668 return self.model.saves[index]669 }670 self.methods.addSave = () => {671 self.methods.$add(self.model, 'saves', PathfinderSave)672 }673 // Skills674 // ---------------------------------------------------675 self.methods.listSkills = () => {676 return self.model.skills || []677 }678 self.methods.addSkill = () => {679 self.methods.$add(self.model, 'skills', PathfinderSkill)680 }681 self.methods.listSkillsForOverview = () => {682 return self.methods.listSkills().filter(x => self.methods.shouldShowSkillOnOverview(x))683 }684 // Custom Skills685 // ---------------------------------------------------686 self.methods.listCustomSkills = () => {687 return self.model.custom_skills || []688 }689 self.methods.getCustomSkill = (index) => {690 return self.model.custom_skills[index]691 }692 self.methods.addCustomSkill = () => {693 self.methods.$add(self.model, 'custom_skills', PathfinderSkill)694 }695 self.methods.listCustomSkillsForOverview = () => {696 return self.methods.listCustomSkills().filter(x => self.methods.shouldShowSkillOnOverview(x))697 }698 // Spells699 // ---------------------------------------------------700 self.methods.listSpells = () => {701 return self.model.spells || []702 }703 self.methods.addSpell = (json: any) => {704 const default_spell: any = { level: self.locals.data.new_spell_level }705 self.locals.selection.spell_classes.forEach(klass => default_spell[klass.name] = self.locals.data.new_spell_level)706 json = json || default_spell707 json.prepared = 0708 json.remaining = 0709 json.pos = self.methods.listSpells().length710 json.level = self.methods.getSpellLevelForKlass(json)711 self.methods.$add(self.model, 'spells', PathfinderSpell, json)712 }713 self.methods.addCustomSpell = () => {714 self.methods.addSpell()715 self.methods.switchTab('spells')716 const spell = self.model.spells[self.model.spells.length - 1]717 spell.$state = 'showing-detail editing'718 }719 self.methods.listSpellElements = (spell) => {720 return spell.elements || []721 }722 self.methods.listSpellsForLevel = (level) => {723 return self.methods.listSpells().filter(x => x.level === level)724 }725 self.methods.listOverviewSpellsForLevel = (level) => {726 return self.methods.listSpellsForLevel(level).filter(x => !self.model.casting.prepared || x.prepared > 0)727 }728 self.methods.filterAvailableSpellsForLevel = (level) => {729 if (self.locals.ready) {730 return self.locals.data.spells.filter(x => x.level === level)731 }732 }733 self.methods.spellAppearsInSpellbook = (spell) => {734 let spell_names = self.methods.listSpells(spell.level).map(x => x.name)735 return spell_names.includes(spell.name)736 }737 self.methods.removeFromSpellbook = (spell) => {738 let spell_names = self.methods.listSpells(spell.level).map(x => x.name)739 let index = spell_names.indexOf(spell.name)740 if (index > -1) {741 this.sheetSvc.remove(self.methods.spells, index)742 }743 }744 self.methods.getSpellLevelForKlass = (spell) => {745 let klass_name = self.methods.getSpellCastingKlass()746 let level = spell.level747 if (typeof spell[klass_name] === 'number') {748 level = spell[klass_name]749 }750 return level751 }752 self.methods.levelSchool = spell => {753 if (spell.level > 0) {754 return `${this.sheetSvc.levelize(spell.level)}-level ${spell.school}`755 } else {756 return `${spell.school} cantrip`757 }758 }759 // this will get spells based on the level of the last listed caster760 // for multiclass characters which is bad and needs to be fixed soon761 self.methods.getSpellCastingKlass = () => {762 let name = 'Wizard'763 const klass: any = self.methods.listKlasses().filter((klass: any) => self.methods.isClassSpellcaster(klass.name))[0]764 if (klass) {765 name = klass.name766 }767 return name768 }769 self.methods.listSRDSpells = () => {770 return (self.locals.data.spells || [])771 }772 self.methods.listSRDSpellsForLevel = (level) => {773 const spells = self.methods.listSRDSpells()774 return spells.filter(s => {775 return self.methods.shouldShowSRDSpell(s, level) && spellIncludesFoundText(s)776 })777 }778 const spellIncludesFoundText = (spell: any) => {779 const text = self.locals.search.spells.toLowerCase()780 return spell.name.toLowerCase().includes(text) || spell.text.toLowerCase().includes(text) || spell.summary.toLowerCase().includes(text) || spell.school.toLowerCase().includes(text)781 }782 self.methods.anySRDSpellsForLevel = (level) => {783 if (self.locals.ready) {784 return self.methods.listSRDSpellsForLevel(level).length > 0785 }786 }787 self.methods.listSpellsForLevel = (level) => {788 if (!self.locals.ready) { return [] }789 return self.methods.listSpells().filter(x => x.level === level)790 }791 self.methods.anySpellsForLevel = (level) => {792 if (!self.locals.ready) { return false }793 return self.methods.listSpells().some(x => x.level === level)794 }795 self.methods.spellSquareSchoolClass = spell => {796 if (spell.school) {797 return `school-${spell.school.toLowerCase()}`798 }799 }800 self.methods.saveEditedSpell = spell => {801 spell.$state = 'showing-detail'802 }803 self.methods.filteredSpellLevels = () => {804 return self.locals.selection.spell_levels.filter(level => self.methods.anySpellsForLevel(level))805 }806 self.methods.filteredSRDSpellLevels = () => {807 return self.locals.selection.spell_levels.filter(level => self.methods.anySRDSpellsForLevel(level))808 }809 // Valuables810 // ---------------------------------------------------811 self.methods.listValuables = () => {812 return self.model.valuables || []813 }814 self.methods.getValuable = (index) => {815 return self.model.valuables[index]816 }817 self.methods.addValuable = () => {818 self.methods.$add(self.model, 'valuables', BtValuable, {819 pos: self.methods.listValuables().length820 })821 }822 // Weapons823 // ---------------------------------------------------824 self.methods.listWeapons = () => {825 return self.model.weapons || []826 }827 self.methods.addWeapon = name => {828 const json = self.locals.data.weapons.find(x => x.name === name) || {}829 json.pos = self.methods.listWeapons().length830 self.methods.$add(self.model, 'weapons', PathfinderWeapon, json)831 }832 /*833 * Array Functions: Get834 */835 self.methods.getKlassData = (name) => {836 if (self.locals.ready) {837 return self.locals.data.klass[name]838 }839 }840 self.methods.getHighestLevel = () => {841 let level = 1842 if (self.locals.ready) {843 level = self.methods.listKlasses().reduce((acc, klass) => Math.max(acc, klass.level), level)844 }845 return level846 }847 self.methods.getTotalLevel = (creature = self.model) => {848 let level = 0849 if (creature instanceof PathfinderCharacter) {850 level = self.methods.listKlasses().reduce((acc, klass) => acc + klass.level, level)851 } else {852 level = (creature.level || 1)853 }854 return level855 }856 self.methods.getAllLevelsArray = () => {857 let result = []858 if (self.locals.ready) {859 result = self.methods.listKlasses().map(x => x.level)860 }861 return result862 }863 self.methods.getLevelsForKlass = (name: string): number[] => {864 if (!self.locals.ready) { return [] }865 let klass_data = self.methods.getKlassData(name)866 const levelsForKlass = !klass_data ? 20 : klass_data.levels.length867 return this.sheetSvc.sizedArray(levelsForKlass, 1)868 }869 self.methods.getAllKlassesArray = () => {870 let result = []871 if (self.locals.ready) {872 result = self.methods.listKlasses().map(x => x.name)873 }874 return result875 }876 self.methods.getAllKlasses = () => {877 let result = self.methods.getAllKlassesArray()878 return result.join(' / ')879 }880 self.methods.getAbilityTotal = (ability, include_conditions = true) => {881 let entangled_penalty = 0882 let fatigued_exhausted_penalty = 0883 let grappled_penalty = 0884 if (include_conditions && ability.name === 'DEX') {885 entangled_penalty = self.model.core_conditions.entangled ? -4 : 0886 fatigued_exhausted_penalty = self.model.core_conditions.fatigued ? -2 : 0887 fatigued_exhausted_penalty = self.model.core_conditions.exhausted ? -6 : fatigued_exhausted_penalty888 grappled_penalty = self.model.core_conditions.grappled ? -4 : 0889 }890 if (include_conditions && ability.name === 'STR') {891 fatigued_exhausted_penalty = self.model.core_conditions.fatigued ? -2 : 0892 fatigued_exhausted_penalty = self.model.core_conditions.exhausted ? -6 : fatigued_exhausted_penalty893 }894 ability.$total = ability.value + ability.misc + entangled_penalty + fatigued_exhausted_penalty + grappled_penalty + (ability.auto || 0)895 if (include_conditions && (ability.name === 'STR' || ability.name === 'DEX') && self.model.core_conditions.paralyzed) {896 ability.$total = 0897 }898 return ability.$total899 }900 self.methods.getAbilityMod = (ability, include_conditions = true) => {901 return Math.floor((self.methods.getAbilityTotal(ability, include_conditions) - 10) / 2)902 }903 self.methods.getSkillTotal = (skill, creature = self.model) => {904 let class_bonus = skill.class_skill && skill.ranks > 0 ? 3 : 0905 let armor_penalty = 0906 let blinded_penalty = self.model.core_conditions.blinded ? -4 : 0907 let frightened_penalty = self.model.core_conditions.frightened ? -2 : 0908 let panicked_penalty = self.model.core_conditions.panicked ? -2 : 0909 let shaken_penalty = self.model.core_conditions.shaken ? -2 : 0910 let sickened_penalty = self.model.core_conditions.sickened ? -2 : 0911 const isSelf = creature === self.model912 if (skill.ability === 'DEX' || skill.ability === 'STR') {913 if (self.locals.ready) {914 self.methods.listArmors().filter(armor => armor.active).forEach(armor => {915 armor_penalty = armor_penalty + armor.penalty916 })917 armor_penalty = Math.abs(armor_penalty) * -1918 self.locals.data.alterations_by_stat.skill.armor.value = armor_penalty919 }920 }921 let penalties = blinded_penalty + armor_penalty + frightened_penalty + shaken_penalty + panicked_penalty + sickened_penalty922 return skill.ranks + skill.misc + (skill.auto || 0) + self.methods.getAbilityMod(creature.abilities[skill.ability], isSelf) + class_bonus + (isSelf ? penalties : 0)923 }924 self.methods.getSkillRanksPerLevel = () => {925 let ranks_per_level = []926 if (self.locals.ready) {927 ranks_per_level = self.methods.listKlasses().map(klass => self.methods.getKlassData(klass.name)).map(data => {928 return self.methods.getAbilityMod(self.model.abilities.INT) + (!data ? 0 : parseInt(data.skill_ranks))929 })930 }931 return ranks_per_level.join(' / ')932 }933 self.methods.getTotalSkillRanks = () => {934 let total = 0935 if (self.locals.ready) {936 total = self.methods.listKlasses().reduce((acc, klass) => {937 const data = self.methods.getKlassData(klass.name)938 return acc + ((self.methods.getAbilityMod(self.model.abilities.INT) + (!data ? 0 : parseInt(data.skill_ranks)) + self.model.combat.skill_ranks.misc) * klass.level)939 }, total)940 }941 return total942 }943 self.methods.getRemainingSkillRanks = () => {944 let used = 0945 if (self.locals.ready) {946 used = self.methods.listSkills().reduce((acc, skill) => acc + skill.ranks, used)947 used = self.methods.listCustomSkills().reduce((acc, skill) => acc + skill.ranks, used)948 }949 return self.methods.getTotalSkillRanks() - used950 }951 self.methods.getShortSkillName = (name) => {952 return name.replace('Knowledge', 'K.').replace('Dungeoneering', 'Dungeon')953 }954 self.methods.getSaveTotal = (save, creature = self.model, include_conditions = true) => {955 save.$total = save.base + save.misc + (save.auto || 0) + self.methods.getAbilityMod(creature.abilities[save.ability], include_conditions) + (creature.combat.all_saves || 0)956 if (include_conditions) {957 let class_bonus = 0958 let haste_bonus = 0959 if (save.ability === 'DEX') {960 haste_bonus = self.model.core_conditions.haste ? 1 : 0961 }962 const frightened_penalty = self.model.core_conditions.frightened ? -2 : 0963 const panicked_penalty = self.model.core_conditions.panicked ? -2 : 0964 const shaken_penalty = self.model.core_conditions.shaken ? -2 : 0965 const sickened_penalty = self.model.core_conditions.sickened ? -2 : 0966 /* getting rid of this until i support all class stuff auto calculated */967 // if (self.methods.getHighestKlass() === 'Paladin' && self.methods.getHighestLevel() > 1) {968 // class_bonus = Math.max(self.model.abilities['CHA'].mod, 0)969 // self.locals.data.alterations_by_stat.save.divine_grace.value = class_bonus970 // } else {971 // self.locals.data.alterations_by_stat.save.divine_grace.value = 0972 // }973 save.$total = save.$total + class_bonus + haste_bonus + frightened_penalty + shaken_penalty + panicked_penalty + sickened_penalty974 }975 return save.$total976 }977 self.methods.getSizeMod = (size) => {978 let size_mod = 0979 if (self.locals.ready) {980 size_mod = self.locals.selection.size_mod[self.locals.selection.size.indexOf(size)]981 }982 return size_mod983 }984 self.methods.getActiveNote = () => {985 if (self.locals.ready) {986 let note = self.methods.listNotes().find(x => x.id === self.model.prefs.active_note)987 if (!note) {988 note = self.methods.getNote(0)989 }990 return [note]991 }992 }993 self.methods.getTotalXP = () => {994 let xp = parseInt(self.model.basic.xp)995 if (isNaN(xp)) {996 xp = 0997 }998 if (self.locals.ready) {999 self.methods.listExperiences().reduce((acc, experience) => acc + experience.amount, xp)1000 }1001 return xp1002 }1003 self.methods.getHPFromCON = (creature = self.model) => {1004 return self.methods.getAbilityMod(creature.abilities.CON) * self.methods.getTotalLevel(creature)1005 }1006 self.methods.getHPRemaining = (creature = self.model) => {1007 return self.methods.getHPTotal(creature) - creature.combat.hp.damage1008 }1009 self.methods.getHPTotal = (creature = self.model) => {1010 return self.methods.getHPFromCON(creature) + creature.combat.hp.value + creature.combat.hp.temporary + creature.combat.hp.misc + (creature.combat.hp.auto || 0)1011 }1012 self.methods.getHPRingValue = (creature = self.model) => {1013 let wounds_percent = Math.min(Math.floor(creature.combat.hp.damage * 100 / self.methods.getHPTotal(creature)), 100)1014 return Math.max(Math.floor(wounds_percent * 6.29), 0)1015 }1016 self.methods.HPRingColor = (creature = self.model) => {1017 let wounds_percent = Math.min(Math.floor(creature.combat.hp.damage * 100 / self.methods.getHPTotal(creature)), 100)1018 return wounds_percent >= 75 ? '#c61515' : '#26e265'1019 }1020 self.methods.getTotalInit = (creature = self.model, include_conditions = true) => {1021 const init = creature.combat.init1022 const deafened_penalty = include_conditions && self.model.core_conditions.deafened ? -4 : 01023 init.$total = init.misc + self.methods.getAbilityMod(creature.abilities['DEX']) + deafened_penalty + (init.auto || 0)1024 return init.$total1025 }1026 self.methods.getTotalAC = function(creature = self.model, include_conditions = true) {1027 const ac = creature.combat.ac1028 let dex_total = (ac.dex.auto || 0) + self.methods.getAbilityMod(creature.abilities.DEX, include_conditions)1029 let subtotal = 01030 if (creature === self.model) {1031 ac.armor = self.methods.listArmors().filter(armor => armor.active).reduce((acc, armor) => {1032 dex_total = Math.min(dex_total, armor.dex)1033 return acc + armor.bonus1034 }, 0)1035 subtotal = ac.armor1036 }1037 subtotal = subtotal + 10 + ac.natural.value + (ac.auto || 0) + ac.natural.auto + ac.deflection + ac.dodge.value + ac.dodge.auto + ac.misc + dex_total1038 if (self.locals.ready && include_conditions) {1039 let haste_bonus = self.model.core_conditions.haste ? 1 : 01040 let blinded_penalty = self.model.core_conditions.blinded ? -2 : 01041 let cowering_penalty = self.model.core_conditions.cowering ? -2 : 01042 let pinned_penalty = self.model.core_conditions.pinned ? -4 : 01043 let stunned_penalty = self.model.core_conditions.stunned ? -2 : 01044 if (self.methods.losesDexBonusToAC()) {1045 dex_total = 01046 }1047 if (dex_total < self.methods.getAbilityMod(creature.abilities.DEX)) {1048 self.locals.data.alterations_by_stat.dex_to_ac.armor.value = dex_total1049 } else {1050 self.locals.data.alterations_by_stat.dex_to_ac.armor.value = 01051 }1052 subtotal = subtotal + haste_bonus + blinded_penalty + cowering_penalty + pinned_penalty + stunned_penalty1053 }1054 return subtotal1055 }1056 self.methods.getFlatAC = (ac) => {1057 let blinded_penalty = self.model.core_conditions.blinded ? -2 : 01058 let cowering_penalty = self.model.core_conditions.cowering ? -2 : 01059 let pinned_penalty = self.model.core_conditions.pinned ? -4 : 01060 let stunned_penalty = self.model.core_conditions.stunned ? -2 : 01061 return 10 + ac.armor + ac.natural.value + ac.natural.auto + ac.deflection + ac.misc + blinded_penalty + cowering_penalty + pinned_penalty + stunned_penalty1062 }1063 self.methods.getTouchAC = (ac) => {1064 ac.dex.$total = (ac.dex.auto || 0) + self.methods.getAbilityMod(self.model.abilities.DEX)1065 if (self.locals.ready) {1066 self.methods.listArmors().filter(armor => armor.active).forEach(armor => {1067 ac.dex.$total = Math.min(ac.dex.$total, armor.dex)1068 })1069 }1070 let haste_bonus = self.model.core_conditions.haste ? 1 : 01071 let blinded_penalty = self.model.core_conditions.blinded ? -2 : 01072 let cowering_penalty = self.model.core_conditions.cowering ? -2 : 01073 let pinned_penalty = self.model.core_conditions.pinned ? -4 : 01074 let stunned_penalty = self.model.core_conditions.stunned ? -2 : 01075 if (self.methods.losesDexBonusToAC()) {1076 ac.dex.$total = 01077 }1078 return 10 + ac.misc + ac.dex.$total + ac.deflection + ac.dodge.value + ac.dodge.auto + haste_bonus + blinded_penalty + cowering_penalty + pinned_penalty + stunned_penalty1079 }1080 self.methods.chooseBestPath = (a, b) => {1081 let paths = ['weak','medium','strong']1082 let best = Math.max(paths.indexOf(a), paths.indexOf(b))1083 return paths[best]1084 }1085 self.methods.getBABArray = (bab) => {1086 const babs = [bab]1087 let i = 01088 for (let b = babs[0]; b > 0; b -= 5) {1089 babs[i] = b1090 i = i + 11091 }1092 return babs1093 }1094 self.methods.displayBAB = (bab) => {1095 return self.methods.getBABArray(bab).join('/')1096 }1097 self.methods.getBABOffset = (offset) => {1098 return self.model.combat.bab.value + offset1099 }1100 self.methods.adjustSystemAttacks = () => {1101 if (self.locals.ready) {1102 const babs = self.methods.getBABArray(self.model.combat.bab.value)1103 const top_bab = babs[0]1104 self.methods.removeSystemAttacks()1105 self.locals.selection.babs = []1106 babs.forEach(bab => {1107 self.locals.selection.babs.push(bab - top_bab)1108 self.methods.addAttack({1109 attack_bonus: 0,1110 weapon: self.model.combat.full_attack,1111 bab: bab - top_bab,1112 source: 'system'1113 })1114 })1115 }1116 }1117 self.methods.removeSystemAttacks = () => {1118 self.methods.listAttacks().filter(x => x.source === 'system').forEach(attack => {1119 this.sheetSvc.removeByObject(self.model.attacks, attack)1120 })1121 }1122 self.methods.getTotalCMB = (cmb, creature = self.model, include_conditions = true) => {1123 cmb.$total = creature.combat.bab.value + (self.methods.getSizeMod(creature.basic.size) * -1) + self.methods.getAbilityMod(creature.abilities['STR'], include_conditions) + cmb.misc + (cmb.auto || 0)1124 if (include_conditions) {1125 let grappled_penalty = self.model.core_conditions.grappled ? -2 : 01126 cmb.$total = cmb.$total + grappled_penalty1127 }1128 return cmb.$total1129 }1130 self.methods.getTotalCMD = (cmd, creature = self.model, include_conditions = true) => {1131 const ac = creature.combat.ac1132 cmd.$total = 10 + creature.combat.bab.value + (self.methods.getSizeMod(creature.basic.size) * -1) + self.methods.getAbilityMod(creature.abilities['STR'], include_conditions) + self.methods.getAbilityMod(creature.abilities['DEX'], include_conditions) + cmd.misc + ac.deflection + ac.dodge.value + ac.dodge.auto + (cmd.auto || 0)1133 if (include_conditions) {1134 let grappled_penalty = self.model.core_conditions.grappled ? -2 : 01135 cmd.$total = cmd.$total + grappled_penalty1136 }1137 return cmd.$total1138 }1139 self.methods.getBothCombatScores = (CMB_stat, CMD_stat) => {1140 let CMB = self.methods.getTotalCMB(CMB_stat)1141 let CMD = self.methods.getTotalCMD(CMD_stat)1142 let grapple_CMB = self.model.core_conditions.grappled ? ' (' + (parseInt(CMB) + 2) + ' grapple)' : ''1143 let grapple_CMD = self.model.core_conditions.grappled ? ' (' + (parseInt(CMD) + 2) + ' grapple)' : ''1144 return CMB + grapple_CMB + ' / ' + CMD + grapple_CMD1145 }1146 self.methods.getNextXPByLevel = () => {1147 let next_xp = '--'1148 let stat = self.methods.getTotalLevel()1149 if (self.locals.ready && stat < 20) {1150 next_xp = self.locals.selection.advancement[self.model.basic.progression][stat]1151 }1152 return next_xp1153 }1154 self.methods.getTotalSpeed = (speed = self.model.combat.speed) => {1155 let current_speed = speed.base1156 const anyArmorEquipped = self.methods.listArmors().some(armor => armor.active && armor.type === 'Armor')1157 if (anyArmorEquipped) {1158 current_speed = current_speed - Math.abs(speed.armor)1159 }1160 if (self.model.core_conditions.haste) {1161 current_speed = current_speed + Math.min(30, current_speed)1162 }1163 if (self.model.core_conditions.entangled) {1164 current_speed = Math.floor(current_speed / 2)1165 }1166 if (self.model.core_conditions.exhausted) {1167 current_speed = Math.floor(current_speed / 2)1168 }1169 speed.$total = current_speed + (speed.auto || 0)1170 return speed.$total1171 }1172 self.methods.getTotalSR = () => {1173 const sr = self.model.combat.sr1174 sr.$total = sr.misc + (sr.auto || 0)1175 return sr.$total1176 }1177 self.methods.getMiscValueAutoTotal = (stat) => {1178 stat.$total = stat.auto + stat.misc + stat.value1179 return stat.$total1180 }1181 self.methods.getArmorBonus = (type) => {1182 let ac_bonus = 01183 if (self.locals.ready) {1184 self.methods.listArmors().filter(armor => armor.active && armor.type === type).forEach(armor => {1185 ac_bonus = ac_bonus + armor.bonus1186 })1187 self.model.combat.ac[type.toLowerCase()] = ac_bonus1188 }1189 return ac_bonus1190 }1191 self.methods.getEquipmentWeight = () => {1192 let armorWeight = 01193 let weaponWeight = 01194 if (self.locals.ready) {1195 armorWeight = self.methods.listArmors().filter(armor => armor.active).reduce((acc, armor) => acc + armor.weight, armorWeight)1196 weaponWeight = self.methods.listWeapons().reduce((acc, weapon) => acc + weapon.weight, weaponWeight)1197 }1198 return armorWeight + weaponWeight1199 }1200 self.methods.getLootWeight = () => {1201 let lootWeight = 01202 if (self.locals.ready) {1203 lootWeight = self.methods.listValuables().reduce((acc, valuable) => acc + valuable.weight * valuable.amount, lootWeight)1204 }1205 return lootWeight1206 }1207 self.methods.getTotalWeight = (weight) => {1208 const armorWeight = self.methods.getEquipmentWeight()1209 const lootWeight = self.methods.getLootWeight()1210 weight.$total = armorWeight + lootWeight + weight.misc + (weight.auto || 0)1211 return weight.$total1212 }1213 self.methods.getWeightCapacity = () => {1214 let str = self.methods.getAbilityTotal(self.model.abilities.STR)1215 let multiplier = 11216 let load = self.methods.getTotalWeight(self.model.combat.weight)1217 let result = ''1218 if (self.locals.ready) {1219 if (str < 1) {1220 return 'Too weak to move'1221 }1222 if (str > 100) {1223 return 'Too strong to calculate'1224 }1225 if (str > 29) {1226 let reduction = self.methods.reduceSTRForCapacity(str)1227 multiplier = reduction.multiplier1228 str = reduction.str1229 }1230 let capacity_array = self.locals.data.carrying_capacity[str].map(x => x * multiplier)1231 if (load <= capacity_array[0]) {1232 result = 'Light load'1233 }1234 if (load > capacity_array[0] && load <= capacity_array[1]) {1235 result = 'Medium load'1236 /* Add -3 check penalty and max dex is 3 */1237 }1238 if (load > capacity_array[1] && load <= capacity_array[2]) {1239 result = 'Heavy load'1240 /* Add -6 check penalty and max dex is 1 */1241 }1242 if (load > capacity_array[2]) {1243 result = 'Larger than Heavy load'1244 }1245 }1246 return result1247 }1248 self.methods.printWealth = (total) => {1249 let gold = Math.floor(total / 100)1250 let silver = Math.floor((total % 100) / 10)1251 let copper = total % 101252 let result = []1253 if (gold > 0) {1254 result.push(gold + ' GP')1255 }1256 if (silver > 0) {1257 result.push(silver + ' SP')1258 }1259 if (copper > 0) {1260 result.push(copper + ' CP')1261 }1262 return result.join(', ')1263 }1264 self.methods.calculateTreasure = () => {1265 let wealth = 01266 if (self.locals.ready) {1267 wealth = Object.keys(self.locals.selection.reverse_currency).reduce((acc, key) => {1268 return acc + (self.model.treasure[key] * self.locals.selection.reverse_currency[key].multiplier)1269 }, wealth)1270 }1271 return wealth1272 }1273 self.methods.calculateValuables = () => {1274 let wealth = 01275 if (self.locals.ready) {1276 wealth = self.methods.listValuables().reduce((acc, valuable) => {1277 return acc + (valuable.value * valuable.amount * self.locals.selection.reverse_currency[valuable.currency].multiplier)1278 }, wealth)1279 }1280 return wealth1281 }1282 self.methods.calculateWealth = () => {1283 return self.methods.calculateValuables() + self.methods.calculateTreasure()1284 }1285 self.methods.getTreasureWealth = () => {1286 return self.methods.printWealth(self.methods.calculateTreasure())1287 }1288 self.methods.getValuablesWealth = () => {1289 return self.methods.printWealth(self.methods.calculateValuables())1290 }1291 self.methods.getTotalWealth = () => {1292 return self.methods.printWealth(self.methods.calculateWealth())1293 }1294 self.methods.getSpellbookLevelClass = (index) => {1295 return !!self.locals.data.spellbook && self.locals.data.spellbook[index] ? 'showing-spells' : 'hiding-spells'1296 }1297 self.methods.getSpellListLevelClass = (index) => {1298 return !!self.locals.data.spell_list && self.locals.data.spell_list[index] ? 'showing-spells' : 'hiding-spells'1299 }1300 self.methods.toggleSpellbookLevel = (index) => {1301 self.locals.data.spellbook[index] = !self.locals.data.spellbook[index]1302 }1303 self.methods.toggleSpellListLevel = (index) => {1304 self.locals.data.spell_list[index] = !self.locals.data.spell_list[index]1305 }1306 self.methods.toggleSpellState = function (spell, state) {1307 spell.$state = (spell.$state === state ? '' : state)1308 }1309 self.methods.spellAtAGlance = spell => {1310 const components = []1311 const lookup = {1312 subschool: 'Subschool',1313 casting_time: 'Casting Time',1314 components: 'Components',1315 duration: 'Duration',1316 effect: 'Effect',1317 range: 'Rage',1318 saving_throw: 'Saving Throw',1319 spell_resistance: 'SR',1320 targets: 'Targets'1321 }1322 Object.keys(lookup).forEach(key => {1323 if (!!spell[key]) {1324 components.push(`${lookup[key]}: ${spell[key]}`)1325 }1326 })1327 return components.join(' / ')1328 }1329 self.methods.getWeaponById = (id: string) => {1330 if (!self.locals.ready) { return {} }1331 const weapon = self.methods.listWeapons().find(x => x.id === id) || {}1332 return weapon1333 }1334 self.methods.getWeaponNameById = (id: string) => {1335 const weapon = self.methods.listWeapons().find(x => x.id === id) || {}1336 return weapon.name1337 }1338 self.methods.getWeaponFromAttack = (attack) => {1339 if (attack === undefined) {1340 return null1341 }1342 return self.methods.getWeaponById(attack.weapon)1343 }1344 self.methods.getAttackName = attack => {1345 const weapon = self.methods.getWeaponFromAttack(attack)1346 return weapon.name1347 }1348 self.methods.getAttackModifier = (weapon) => {1349 if (weapon === undefined) { return null }1350 let melee_or_ranged_bonus = self.model.combat.attack.melee1351 let ability = 'STR'1352 if (weapon.melee_or_ranged === 'Ranged' || (weapon.melee_or_ranged === 'Melee' && weapon.reverse_ability)) {1353 melee_or_ranged_bonus = self.model.combat.attack.ranged1354 ability = 'DEX'1355 }1356 let ability_value = self.methods.getAbilityMod(self.model.abilities[ability])1357 let haste_bonus = self.model.core_conditions.haste ? 1 : 01358 let dazzled_penalty = self.model.core_conditions.dazzled ? -1 : 01359 let entangled_penalty = self.model.core_conditions.entangled ? -2 : 01360 let frightened_penalty = self.model.core_conditions.frightened ? -2 : 01361 let grappled_penalty = self.model.core_conditions.grappled ? -2 : 01362 let shaken_penalty = self.model.core_conditions.shaken ? -2 : 01363 let sickened_penalty = self.model.core_conditions.sickened ? -2 : 01364 let attack = ability_value + self.model.combat.attack.misc + melee_or_ranged_bonus + weapon.attack_bonus + haste_bonus + dazzled_penalty + entangled_penalty + frightened_penalty + shaken_penalty + grappled_penalty + sickened_penalty1365 return attack1366 }1367 self.methods.getAttackRoll = (attack) => {1368 let is_wielding_two_weapons = self.model.combat.style === 'Two-Weapon Fighting'1369 let mainhand_or_offhand_bonus = 01370 let weapon = self.methods.getWeaponById(attack.weapon)1371 if (is_wielding_two_weapons) {1372 if (attack.weapon === self.model.combat.mainhand) {1373 mainhand_or_offhand_bonus = self.model.combat.attack.mainhand1374 }1375 if (attack.weapon === self.model.combat.offhand) {1376 mainhand_or_offhand_bonus = self.model.combat.attack.offhand1377 }1378 }1379 let mod = self.methods.getAttackModifier(weapon)1380 /* This assumes you have the two-weapon fighting feat and the weapon is light WHICH IS BAD */1381 let two_weapon_penalty = is_wielding_two_weapons ? -2 : 01382 return mod + self.methods.getBABOffset(attack.bab) + attack.attack_bonus + two_weapon_penalty + mainhand_or_offhand_bonus + self.model.combat.attack.full1383 }1384 self.methods.printAttackRoll = (attack) => {1385 return 'd20 + ' + self.methods.getAttackRoll(attack)1386 }1387 self.methods.getSingleAttackValue = (weapon) => {1388 let mod = self.methods.getAttackModifier(weapon)1389 return mod + self.model.combat.bab.value + self.model.combat.attack.standard1390 }1391 self.methods.getSingleDamageValue = (weapon) => {1392 let mod = self.methods.getDamageModifier(weapon)1393 return mod + self.model.combat.damage.standard1394 }1395 self.methods.printSingleAttackRoll = (weapon) => {1396 return 'd20 + ' + self.methods.getSingleAttackValue(weapon)1397 }1398 self.methods.printSingleDamageRoll = (weapon) => {1399 return self.methods.weaponDamageDie(weapon) + ' + ' + self.methods.getSingleDamageValue(weapon)1400 }1401 self.methods.weaponDamageDie = weapon => {1402 return self.model.basic.size === 'Small' ? weapon.dmg_s : weapon.dmg_m1403 }1404 self.methods.getDamageModifier = (weapon) => {1405 if (weapon === undefined) {1406 return null1407 }1408 let str = self.methods.getAbilityMod(self.model.abilities.STR)1409 const sickened_penalty = self.model.core_conditions.sickened ? -2 : 01410 if (self.model.combat.style === 'Wielding Two-Handed') {1411 str = Math.floor(str * 1.5)1412 }1413 let melee_or_ranged_bonus = self.model.combat.damage.melee1414 if (weapon.melee_or_ranged === 'Ranged' && !weapon.reverse_ability) {1415 melee_or_ranged_bonus = self.model.combat.damage.ranged1416 str = 01417 }1418 const damage = str + weapon.damage_bonus + self.model.combat.damage.misc + melee_or_ranged_bonus + sickened_penalty1419 return damage1420 }1421 self.methods.getDamageRoll = (attack) => {1422 let is_wielding_two_weapons = self.model.combat.style === 'Two-Weapon Fighting'1423 let mainhand_or_offhand_bonus = 01424 let weapon = self.methods.getWeaponById(attack.weapon)1425 let offhand_modifier = 11426 if (is_wielding_two_weapons) {1427 if (attack.weapon === -1) {1428 weapon = self.methods.getWeaponById(self.model.combat.mainhand)1429 mainhand_or_offhand_bonus = self.model.combat.damage.mainhand1430 } else {1431 if (attack.weapon === self.model.combat.mainhand) {1432 mainhand_or_offhand_bonus = self.model.combat.damage.mainhand1433 }1434 if (attack.weapon === self.model.combat.offhand) {1435 /* half STR */1436 offhand_modifier = 21437 mainhand_or_offhand_bonus = self.model.combat.damage.offhand1438 }1439 }1440 }1441 let mod = self.methods.getDamageModifier(weapon)1442 mod = Math.floor(mod / offhand_modifier)1443 return mod + mainhand_or_offhand_bonus + self.model.combat.damage.full1444 }1445 self.methods.printDamageRoll = (attack) => {1446 let weapon = self.methods.getWeaponById(attack.weapon)1447 return self.methods.weaponDamageDie(weapon) + ' + ' + self.methods.getDamageRoll(attack)1448 }1449 self.methods.getDiceFullAttack = () => {1450 return self.methods.listAttacks()1451 .map(attack => self.methods.getAttackRoll(attack))1452 }1453 self.methods.getDiceFullAttackDamageMods = () => {1454 return self.methods.listAttacks()1455 .map(attack => self.methods.getDamageRoll(attack))1456 }1457 self.methods.getDiceFullAttackDamageSides = () => {1458 return self.methods.listAttacks()1459 .map(attack => self.methods.getWeaponFromAttack(attack))1460 .map(weapon => self.methods.weaponDamageDie(weapon))1461 }1462 self.methods.companionAttackAbilityMod = (attack, companion) => {1463 return self.methods.getAbilityMod(companion.abilities[attack.ability || 'STR'], false)1464 }1465 self.methods.getCompanionAttackBonus = (attack, companion) => {1466 const multiplier = companion.attacks.length === 1 && attack.ability === 'STR' ? 1.5 : 11467 return attack.attack_bonus + Math.floor(self.methods.companionAttackAbilityMod(attack, companion) * multiplier) + companion.combat.bab.value + (self.methods.getSizeMod(companion.basic.size) * -1)1468 }1469 self.methods.featAppearsInFeatList = (feat) => {1470 let feat_names = self.methods.listFeats().map(x => x.name)1471 return feat_names.includes(feat.name)1472 }1473 self.methods.removeFeat = (feat) => {1474 let feat_names = self.methods.listFeats().map(x => x.name)1475 let index = feat_names.indexOf(feat.name)1476 if (index > -1) {1477 this.sheetSvc.remove(self.model.feats, index)1478 }1479 }1480 self.methods.spellAppearsInSpellbook = (spell) => {1481 let spell_names = self.methods.listSpells(parseInt(spell.level)).map(x => x.name)1482 return spell_names.includes(spell.name)1483 }1484 self.methods.removeFromSpellbook = (spell) => {1485 let spell_names = self.methods.listSpells().map(x => x.name)1486 let index = spell_names.indexOf(spell.name)1487 if (index > -1) {1488 this.sheetSvc.remove(self.model.spells, index)1489 }1490 }1491 self.methods.foundSpellClasses = spell => {1492 return {1493 added: self.methods.spellAppearsInSpellbook(spell),1494 [spell.$state]: true1495 }1496 }1497 self.methods.getCompanionAbilityTotal = (stat, name) => {1498 return stat.value + stat.misc1499 }1500 self.methods.getCompanionAbilityMod = (name) => {1501 let stat = self.model.companion.abilities[name]1502 return Math.floor((self.methods.getAbilityTotal(stat) - 10) / 2)1503 }1504 self.methods.getCompanionSaveTotal = (stat) => {1505 return stat.base + stat.misc + self.methods.getCompanionAbilityMod(stat.ability)1506 }1507 /*1508 * Notices/Reporting1509 */1510 self.methods.getNoticeTypeForFull = (attack, attack_or_damage) => {1511 let notices = [attack_or_damage]1512 notices.push(attack_or_damage === 'Attack' ? 'Full Attack' : 'Full Attack Damage')1513 let is_wielding_two_weapons = self.model.combat.style === 'Two-Weapon Fighting'1514 let mainhand_or_offhand = ''1515 const weapon = self.methods.getWeaponById(attack.weapon)1516 if (!weapon) { return }1517 if (is_wielding_two_weapons) {1518 /* TODO: what if it's both the mainhand and the offhand? */1519 if (attack.weapon === self.model.combat.mainhand) {1520 notices.push('Mainhand ' + attack_or_damage)1521 }1522 if (attack.weapon === self.model.combat.offhand) {1523 notices.push('Offhand ' + attack_or_damage)1524 }1525 }1526 notices.push((weapon.melee_or_ranged === 'Ranged' ? 'Ranged ' : 'Melee ') + attack_or_damage)1527 return notices1528 }1529 self.methods.getNoticeTypeForStandard = (weapon, attack_or_damage) => {1530 let notices = [attack_or_damage]1531 notices.push(attack_or_damage === 'Attack' ? 'Standard Attack' : 'Standard Attack Damage')1532 notices.push((weapon.melee_or_ranged === 'Ranged' ? 'Ranged ' : 'Melee ') + attack_or_damage)1533 return notices1534 }1535 self.methods.getCalculatedCoreConditionsArray = (name) => {1536 if (!self.locals.ready) { return [] }1537 let conditions = self.locals.data.conditions_by_stat[name] || {}1538 return Object.keys(conditions).filter(key => self.model.core_conditions[key]).map(key => self.methods.getDisplayForValue(`${conditions[key]} from ${key}`))1539 }1540 self.methods.getCalculatedCoreConditions = (name) => {1541 return self.methods.getCalculatedCoreConditionsArray(name).join(', ')1542 }1543 self.methods.getCalculatedConditionsArray = (name: any) => {1544 let result = []1545 let relevant_stats = [name]1546 if (self.locals.ready) {1547 if (typeof name === 'object') {1548 relevant_stats = Object.keys(name).map(x => name[x])1549 }1550 self.methods.listConditions().filter(x => x.active).forEach(condition => {1551 self.methods.listConditionEffects(condition).forEach(effect => {1552 result = relevant_stats.filter(x => x === effect.name).map(() => `${self.methods.getModifierFromEffect(effect)} from ${condition.name}`)1553 })1554 })1555 }1556 return result1557 }1558 self.methods.getCalculatedConditions = (name) => {1559 return self.methods.getCalculatedConditionsArray(name).join(', ')1560 }1561 self.methods.getCalculatedAlterationsArray = (name) => {1562 let result = []1563 if (self.locals.ready) {1564 const alterations = self.locals.data.alterations_by_stat[name] || {}1565 result = Object.keys(alterations).map(x => alterations[x]).filter(x => x.value ! == 0).map(alteration => {1566 return self.methods.getDisplayForValue(alteration.value) + alteration.text1567 })1568 }1569 return result1570 }1571 self.methods.getCalculatedAlterations = (name) => {1572 return self.methods.getCalculatedAlterationsArray(name).join(', ')1573 }1574 self.methods.getCalculatedNotice = (name) => {1575 return [1576 ...self.methods.getCalculatedCoreConditionsArray(name),1577 ...self.methods.getCalculatedConditionsArray(name),1578 ...self.methods.getCalculatedAlterationsArray(name),1579 ].join(', ')1580 }1581 self.methods.getActiveConditions = () => {1582 let active_conditions = []1583 if (self.locals.ready) {1584 Object.keys((new PathfinderCoreConditions).getProto()).filter(key => self.model.core_conditions[key]).forEach(key => {1585 active_conditions.push(key)1586 })1587 self.methods.listConditions().filter(x => x.active).forEach(condition => {1588 active_conditions.push(condition.name)1589 })1590 if (active_conditions.length === 0) {1591 active_conditions.push('None')1592 }1593 }1594 return active_conditions.join(', ')1595 }1596 self.methods.getNameForEffectStat = (stat) => {1597 const selection = self.locals.selection.conditionStats.find(x => x.path === stat)1598 return selection.name1599 }1600 self.methods.getModifierFromEffect = (effect) => {1601 let condition_data = self.locals.data.custom_conditions.find(x => x.name === effect.name)1602 let result = 01603 if (condition_data) {1604 if (condition_data.wildcard) {1605 result = 0 // TODO FIX THIS1606 } else if (condition_data.modifier) {1607 result = effect.value1608 } else {1609 let target = self.methods.validateAndReturnStat(condition_data.target)1610 result = target.obj[target.key]1611 }1612 }1613 return result1614 }1615 self.methods.getSpecialLabelForStat = (weapon: any) => {1616 return weapon.melee_or_ranged === 'Melee' ? 'Finesse?' : 'Thrown?'1617 }1618 self.methods.onSortableDrop = (e) => {1619 moveItemInArray(e.container.data, e.previousIndex, e.currentIndex)1620 self.touch()1621 }1622 /*1623 * Helper functions (like booleans and commonly used stuff)1624 */1625 self.methods.rerunMigration = () => {1626 self.model.version = self.model.version - 11627 location.reload()1628 }1629 self.methods.setSkillAbility = async (skill) => {1630 await Promise.resolve()1631 self.touch()1632 skill.ability = self.locals.selection.skills[skill.name]1633 }1634 self.methods.losesDexBonusToAC = () => {1635 return self.model.core_conditions.blinded || self.model.core_conditions.cowering || self.model.core_conditions.stunned1636 }1637 self.methods.isClassSpellcaster = (name) => {1638 return self.locals.selection.spell_classes.some(x => x.name === name)1639 }1640 self.methods.shouldShowSRDSpell = (spell, level) => {1641 let classes_match = Object.keys(self.locals.data.filtering.by_class).some(name => self.locals.data.filtering.by_class[name] && (typeof spell[name] === 'number') && spell[name] === level)1642 // let schools_match = false1643 // for (let school in self.locals.data.filtering.by_school) {1644 // if (spell.school === school) {1645 // schools_match = self.locals.data.filtering.by_school[school]1646 // }1647 // }1648 // return schools_match && classes_match1649 return classes_match1650 }1651 self.methods.shouldShowTwoWeaponFighting = () => {1652 return self.model.combat.style === 'Two-Weapon Fighting'1653 }1654 self.methods.shouldShowSkillOnOverview = (skill) => {1655 return skill.class_skill || skill.ranks > 01656 }1657 self.methods.anyBattlemapsPresent = () => {1658 return Object.keys(this.store.tools).some(key => this.store.tools[key].meta.toolType === 'battlemap' && this.store.tools[key].meta.watching)1659 }1660 self.methods.getDisplayForValue = (mod) => {1661 if (mod > 0) {1662 mod = '+' + mod1663 }1664 return mod1665 }1666 self.methods.isHomebrewClass = (klass) => {1667 return !self.methods.getKlassData(klass.name)1668 }1669 self.methods.isSpontaneousCaster = (klass) => {1670 if (!self.locals.ready) { return true }1671 const klassData = self.methods.getKlassData(klass.name)1672 const levelData = klassData && klassData.levels[klass.level - 1] || {}1673 if (!klassData || !klassData.levels[klass.level - 1]) {1674 return true1675 }1676 return levelData.spells_known && levelData.spells_known.length > 01677 }1678 self.methods.listSpontaneousCasters = () => {1679 return self.methods.listKlasses().filter(x => self.methods.isSpontaneousCaster(x))1680 }1681 self.methods.isListedCaster = (klass) => {1682 let listed = false1683 if (self.locals.ready) {1684 listed = !!self.methods.getKlassData(klass.name)1685 }1686 return listed1687 }1688 self.methods.showCustomSpellsKnown = (klass) => {1689 let custom = true1690 if (self.locals.ready) {1691 const klassData = self.methods.getKlassData(klass.name)1692 const levelData = klassData && klassData.levels[klass.level - 1]1693 custom = !klassData || !levelData1694 }1695 return custom1696 }1697 self.methods.reduceSTRForCapacity = (str) => {1698 let multiplier = 01699 while (str > 29) {1700 str = str - 101701 multiplier = multiplier + 41702 }1703 return {str: str, multiplier: multiplier}1704 }1705 self.methods.anyActiveConditions = () => {1706 const condition_properties = Object.keys((new PathfinderCoreConditions).getProto())1707 return self.locals.ready && (self.methods.listConditions().some(x => x.active) || condition_properties.some(x => self.model.core_conditions[x]))1708 }1709 self.methods.addCharacterAsToken = () => {1710 this.store.addCharacterAsToken(self, 'pathfinder')1711 }1712 self.methods.getFullAttackDamageDiceIconClass = () => {1713 let weapon = self.methods.getWeaponById(self.model.combat.full_attack)1714 if (weapon !== null) {1715 return this.diceSvc.getDiceIconClass(self.methods.weaponDamageDie(weapon))1716 }1717 }1718 self.methods.showRollResult = (rolls) => {1719 const result = this.diceSvc.printRollResult(rolls, self.model.name)1720 self.locals.data.last_dice_rolled = result1721 }1722 self.methods.rollManyDice = (sides, modsArray, name) => {1723 const packs = modsArray.map((mod, index) => {1724 const text = typeof sides === 'object' ? sides[index] : sides1725 return this.diceSvc.rollCustomDice({ text, name }, mod)1726 }).map(result => {1727 return result.rolls.reduce((acc, pack) => {1728 acc.modifier += pack.modifier1729 acc.name = pack.name1730 acc.result += pack.result1731 acc.record.list = [...acc.record.list, ...pack.record.list]1732 acc.record.total += pack.record.total1733 return acc1734 }, {1735 modifier: 0,1736 result: 0,1737 record: {1738 list: [],1739 total: 0,1740 }1741 }) as DicePackage1742 })1743 self.methods.showRollResult(packs)1744 this.store.addRollsToChat(packs, name)1745 }1746 self.methods.rollFullAttackDamage = () => {1747 self.methods.rollManyDice(self.methods.getDiceFullAttackDamageSides(), self.methods.getDiceFullAttackDamageMods(), 'full attack damage')1748 }1749 self.methods.rollOneDice = (sides, modifier, name, phrasing) => {1750 const dice = this.diceSvc.getDicePackage(sides, modifier, name, phrasing)1751 self.methods.showRollResult([dice])1752 this.store.addRollsToChat([dice], name)1753 return [dice]1754 }1755 self.methods.rollInitiative = () => {1756 const results = self.methods.rollOneDice(20, self.methods.getTotalInit(), 'initiative')1757 self.model.combat.init.value = results[0].result1758 this.store.sendInitiativeToMap(results[0].result, self.model.id)1759 }1760 self.methods.rollCustomDice = (custom_dice) => {1761 self.touch()1762 const result = this.diceSvc.rollCustomDice(custom_dice)1763 self.locals.data.last_dice_rolled = self.model.name + result.text1764 this.store.addCustomRollToChat(result.text)1765 return result1766 }1767 /******************************************************1768 * Campaign Connection stuff1769 ******************************************************/1770 self.methods.connectedCampaignName = (): string => {1771 const tool: BtPlayerTool = (self.locals.tools || []).find((x: BtPlayerTool) => x.id === self.model.campaign_id)1772 return tool ? tool.title : ''1773 }1774 self.methods.connectCampaign = (): void => {1775 const existingSelf = this.store.tools[self.model.campaign_id]1776 const campaign = !!existingSelf ? existingSelf : this.campaignSvc.payload(self.model.campaign_id)1777 this.store.setupToolController(campaign, 'campaign')1778 }1779 self.methods.campaignConnected = (): boolean => {1780 return this.store.isToolOpen(self.model.campaign_id)1781 }1782 /******************************************************1783 * Custom Conditions & Validation1784 ******************************************************/1785 self.methods.addBonusSpellsPerDay = (a, b) => {1786 let result = null1787 if (a === null || b === null || a === undefined) {1788 result = null1789 } else if (typeof a === 'string' || typeof b === 'string') {1790 result = 01791 } else if (typeof a === 'number' && typeof b === 'number') {1792 result = a + b1793 }1794 return result1795 }1796 /*1797 * Custom Conditions Validation1798 */1799 self.methods.validateAndReturnStat = (stat) => {1800 let arr = stat.split(/\.|\[|\]/gi),1801 valid = true,1802 schema = self.model,1803 id = ''1804 arr.forEach(node => {1805 /*1806 * If nodename is a number, it's in an array1807 * If schema at node is number or null, it's the id we want1808 */1809 if (typeof schema[node] === 'number' || schema[node] === null) {1810 id = node1811 } else if (schema[node] !== undefined && id === '') {1812 schema = schema[node]1813 } else {1814 valid = false1815 }1816 })1817 if (valid) {1818 return { obj: schema, key: id }1819 } else {1820 return undefined1821 }1822 }1823 self.methods.onFormulaChange = (effect) => {1824 let formula = self.methods.validateAndReturnFormula(effect.formula)1825 effect.valid = formula !== undefined1826 }1827 self.methods.validateAndReturnFormula = (formula) => {1828 let valid = formula !== null && formula !== '' && formula.match(/n|l/gi) !== null1829 formula = formula.replace(/n|l/gi, '0')1830 if (valid) {1831 formula.split('').forEach((char: string) => {1832 const isValidCharacter = char.match(/(\+|\-|\s|\d|\.|\*|\/|\%|\(|\))/gi) !== null1833 if (!isValidCharacter) {1834 valid = false1835 }1836 })1837 if (valid) {1838 try {1839 eval(formula)1840 } catch(err) {1841 if (err) {1842 valid = false1843 }1844 }1845 }1846 }1847 if (formula === null) {1848 valid = false1849 }1850 if (valid) {1851 return formula1852 } else {1853 return undefined1854 }1855 }1856 self.methods.getActiveEffectsForStat = (condition, name) => {1857 let effects = self.methods.listConditionEffects(condition).filter(x => x.name === name)1858 effects.forEach(effect => self.methods.activateEffect(effect))1859 }1860 self.methods.isConditionValid = (condition) => {1861 return self.methods.listConditionEffects(condition).every(effect => effect.valid)1862 }1863 self.methods.setEffectFormula = (source, target, formula, effect) => {1864 if (source !== undefined && target !== undefined && formula !== undefined) {1865 effect.valid = true1866 // If the target already has a value, that means a previous effect is active on this1867 // stat, which means we need to pull the 'source' from the target and calc on that1868 if (target.obj[target.key] !== null) {1869 source = target1870 }1871 formula = effect.formula.replace('n', source.obj[source.key])1872 try {1873 target.obj[target.key] = Math.floor(eval(formula))1874 } catch (err) {1875 console.log(err)1876 effect.valid = false1877 }1878 } else {1879 effect.valid = false1880 }1881 }1882 self.methods.unsetEffect = (target, effect, reset_value) => {1883 target.obj[target.key] = reset_value1884 if (self.locals.ready) {1885 // if there are other active conditions whose effects have our target name as their target name1886 // we need to activate those effects1887 self.methods.listConditions().filter(x => x.active).forEach(condition => self.methods.getActiveEffectsForStat(condition, effect.name))1888 }1889 }1890 // Returns boolean effect.valid1891 self.methods.activateEffect = (effect) => {1892 let source, target, formula1893 let condition_data = self.locals.data.custom_conditions.find(x => x.name === effect.name)1894 if (condition_data) {1895 if (condition_data.wildcard) {1896 let list = self.model[condition_data.list]1897 if (list !== undefined) {1898 formula = self.methods.validateAndReturnFormula(effect.formula)1899 list.forEach(stat => {1900 stat[condition_data.target] = stat[condition_data.target] + effect.value1901 })1902 } else {1903 effect.valid = false1904 }1905 } else if (condition_data.modifier) {1906 target = self.methods.validateAndReturnStat(condition_data.target)1907 target.obj[target.key] = target.obj[target.key] + effect.value1908 } else {1909 source = self.methods.validateAndReturnStat(condition_data.source)1910 target = self.methods.validateAndReturnStat(condition_data.target)1911 formula = self.methods.validateAndReturnFormula(effect.formula)1912 self.methods.setEffectFormula(source, target, formula, effect)1913 }1914 } else {1915 effect.valid = false1916 }1917 return effect.valid1918 }1919 self.methods.deactivateEffect = (effect) => {1920 let target1921 let condition_data = self.locals.data.custom_conditions.find(x => x.name === effect.name)1922 if (condition_data) {1923 if (condition_data.wildcard) {1924 let list = self.model[condition_data.list]1925 list.forEach(stat => {1926 target = { obj: stat, key: condition_data.target }1927 self.methods.unsetEffect(target, effect, 0)1928 })1929 } else if (condition_data.modifier) {1930 target = self.methods.validateAndReturnStat(condition_data.target)1931 self.methods.unsetEffect(target, effect, 0)1932 } else {1933 target = self.methods.validateAndReturnStat(condition_data.target)1934 self.methods.unsetEffect(target, effect, null)1935 }1936 }1937 }1938 self.methods.toggleCondition = async (condition) => {1939 await Promise.resolve()1940 self.touch()1941 let valid = self.methods.isConditionValid(condition)1942 if (valid && condition.active) {1943 valid = self.methods.listConditionEffects(condition).every(effect => self.methods.activateEffect(effect))1944 }1945 if (!condition.active || !valid) {1946 condition.active = false1947 self.methods.listConditionEffects(condition).forEach(effect => self.methods.deactivateEffect(effect))1948 }1949 }1950 // self.methods.reportAuto = (path) => {1951 // let stat = self.methods.validateAndReturnStat(path)1952 // console.log(path + ': ' + stat.obj[stat.key])1953 // }1954 self.methods.resetAutoValues = () => {1955 /* this is an emergency switch to set all stat auto values to null */1956 self.methods.listConditions().filter(x => x.active).forEach(condition => {1957 condition.active = false1958 self.methods.toggleCondition(condition)1959 })1960 self.locals.data.custom_conditions.filter(x => x.wildcard === undefined).forEach(condition => {1961 let stat = self.methods.validateAndReturnStat(condition.target)1962 stat.obj[stat.key] = 01963 })1964 }1965 /*1966 * Watcher functions1967 */1968 self.methods.onClassChange = async (should_skip_reset = true) => {1969 await Promise.resolve()1970 self.touch()1971 if (!self.locals.ready) { return false }1972 // Reset filtering by class1973 Object.keys(self.locals.data.filtering.by_class)1974 .forEach(x => self.locals.data.filtering.by_class[x] = false)1975 // Show spells from all classes who are listed1976 self.methods.listKlasses().map(x => x.name).filter(name => !!self.locals.data.filtering.by_class[name]).forEach(name => self.locals.data.filtering.by_class[name] = true)1977 self.methods.updateSpells(false)1978 if (!should_skip_reset) {1979 if (!self.model.prefs.homebrew) {1980 self.methods.enforceLevelCap()1981 }1982 self.methods.updateBABAndSaves()1983 self.methods.updateSkills()1984 }1985 }1986 self.methods.onLevelChange = async () => {1987 await Promise.resolve()1988 self.touch()1989 if (!self.locals.ready) {1990 return false1991 }1992 self.methods.updateBABAndSaves()1993 self.methods.updateSpells(true)1994 }1995 self.methods.enforceLevelCap = () => {1996 self.methods.listKlasses().forEach(klass => {1997 const data = self.methods.getKlassData(klass.name)1998 if (data) {1999 klass.level = Math.min(data.levels.length, klass.level)2000 }2001 })2002 }2003 self.methods.resetSingleValueArray = (array, reset) => {2004 array.forEach(x => x.value = reset)2005 }2006 const updateChunkyStat = (acc, name, data, levelPosition) => {2007 acc[name].value += self.locals.data[`${name === 'bab' ? 'bab' : 'save'}_types`][data[name]][levelPosition]2008 acc[name].update = true2009 }2010 self.methods.updateBABAndSaves = () => {2011 const reductionData = self.methods.listKlasses().reduce((acc, klass) => {2012 const data = self.methods.getKlassData(klass.name)2013 const levelPosition = klass.level - 12014 const outOfRange = data && (levelPosition >= data.levels.length || levelPosition < 0)2015 if (!data || outOfRange) { return acc }2016 ['bab', 'fort', 'ref', 'will'].forEach(name => updateChunkyStat(acc, name, data, levelPosition))2017 return acc2018 }, {2019 bab: { value: 0, update: false },2020 ref: { value: 0, update: false },2021 will: { value: 0, update: false },2022 fort: { value: 0, update: false },2023 })2024 if (reductionData.bab.update) {2025 self.model.combat.bab.value = reductionData.bab.value2026 }2027 self.methods.listSaves().forEach(save => {2028 const name = save.name.toLowerCase()2029 if (reductionData[name].update) {2030 save.base = reductionData[name].value2031 }2032 })2033 }2034 self.methods.updateSkills = () => {2035 const skillsArray = self.methods.listSkills()2036 skillsArray.forEach(skill => skill.class_skill = false)2037 self.methods.listKlasses()2038 .map(klass => self.methods.getKlassData(klass.name))2039 .filter(data => !!data)2040 .forEach(data => {2041 data.skills2042 .map(name => skillsArray.find(skill => skill.name === name))2043 .filter(skill => !!skill)2044 .forEach(skill => skill.class_skill = true)2045 })2046 }2047 /*2048 * Gets data from locals.data for spells_known and assigns it to a temporary spot in the model2049 * Gets the character's last class and loads JSON spell data for that class automatically2050 */2051 self.methods.updateSpells = (should_skip_new_spell_load) => {2052 /* Make a note somehwere that clerics can cast 1 domain spell of each level that is not - starting with 1st level */2053 let load_spells_class = null2054 self.methods.listKlasses().forEach(klass => {2055 const data = self.methods.getKlassData(klass.name)2056 const levelData = data && data.levels[klass.level - 1]2057 if (!data || !levelData) { return }2058 let ability_mod = self.methods.getAbilityMod(self.model.abilities[klass.spell_ability])2059 load_spells_class = self.methods.isClassSpellcaster(klass.name) ? klass.name : null2060 klass.spell_ability = data.spell_ability || 'INT'2061 const spd = levelData.spells_per_day || 02062 const knwn = levelData.spells_known || 02063 self.methods.listSpellsPerDay(klass).forEach((slot: any, index: number) => {2064 let bonus = Math.floor((ability_mod - index) / 4) + 12065 if (bonus < 0 || index === 0) {2066 bonus = 02067 }2068 slot.value = self.methods.addBonusSpellsPerDay(spd[index], bonus)2069 slot.remaining = slot.value2070 })2071 self.methods.listSpellsKnown(klass).forEach((known: any, index: number) => {2072 self.methods.getSpellsKnown(klass, index).value = (knwn[index] == undefined) ? null : knwn[index]2073 })2074 })2075 }2076 const checkWeaponAndFallback = (id) => {2077 const foundWeapon = self.methods.listWeapons().find(x => x.id === id)2078 const firstWeapon = self.methods.listWeapons()[0]2079 return !!foundWeapon ? foundWeapon.id : firstWeapon.id2080 }2081 self.methods.onWeaponsChange = () => {2082 if (self.methods.listWeapons().length > 0) {2083 self.model.combat.mainhand = checkWeaponAndFallback(self.model.combat.mainhand)2084 self.model.combat.offhand = checkWeaponAndFallback(self.model.combat.offhand)2085 self.model.combat.full_attack = checkWeaponAndFallback(self.model.combat.full_attack)2086 self.methods.listAttacks().forEach(attack => attack.weapon = checkWeaponAndFallback(attack.weapon))2087 }2088 }2089 return self2090 }...
campaign.service.ts
Source:campaign.service.ts
1import { Injectable } from '@angular/core'2import { SheetService } from './sheet.service'3import { CampaignBase } from '../models/campaign/base'4import { StorageService } from './storage.service'5import { moveItemInArray } from '@angular/cdk/drag-drop'6import { SharingService } from './sharing.service'7import { DiceService } from './dice.service'8import { CampaignTool } from '../models/campaign/tool'9import { CampaignChat } from '../models/campaign/chat'10import { CampaignPlayer } from '../models/campaign/player'11import { CampaignAudioCue } from '../models/campaign/audio-cue'12import { BtList } from '../models/common/list'13import { BtNote } from '../models/common/note'14import { CampaignLog } from '../models/campaign/log'15import { CampaignQuest } from '../models/campaign/quest'16import { CampaignNpc } from '../models/campaign/npc'17import { CampaignNpcDetail } from '../models/campaign/npc-detail'18import { CampaignFoe } from '../models/campaign/foe'19import { CampaignPayload } from '../models/campaign/payload'20import { BtPlayerTool } from '../models/common/player-tool.model'21import { BtPermission } from '../models/common/permission.model'22import { format, parse } from 'date-fns'23import { CampaignCommand } from '../models/campaign/command.model'24import { BtUser } from '../models/common/user.model'25import { HttpService } from './http.service'26import { InterfaceService } from './interface.service'27import { take } from 'rxjs/operators'28@Injectable({29 providedIn: 'root'30})31export class CampaignService {32 constructor(33 public sheetSvc: SheetService,34 public store: StorageService,35 public sharer: SharingService,36 private diceSvc: DiceService,37 public http: HttpService,38 public interfaceSvc: InterfaceService,39 ) { }40 public payload = (docId: string): CampaignPayload => {41 const self: CampaignPayload = {} as CampaignPayload42 self.model = new CampaignBase43 self.methods = {}44 self.meta = {45 subscriptions: {},46 undefinedErrorCount: 0,47 }48 self.locals = {49 player_nickname: {50 busy: false,51 name: '',52 },53 document_id: docId,54 ready: false,55 forbidden: false,56 model_loaded: false,57 document_failed: false,58 error: false,59 choosing_tools: false,60 tabs: { active: 'summary', showing_nav: false, list: [61 { title: 'Summary', id: 'summary', role: 'writer|owner' },62 { title: 'Adventure', id: 'adventure', role: 'owner' },63 { title: 'Lists', id: 'lists', role: 'writer|owner' },64 { title: 'NPCs', id: 'npcs', role: 'owner' },65 { title: 'Enemies', id: 'enemies', role: 'owner' },66 { title: 'Monsters', id: 'monsters', role: 'owner' },67 { title: 'Audio Cues', id: 'audio', role: 'owner' },68 { title: 'Settings', id: 'settings', role: 'writer|owner' },69 ], tools: [] },70 available_space: 'area-below-desktop',71 showing_chat: false,72 chat_minimized: false,73 chatSettings: false,74 mute_audio: false,75 tool_owner: false,76 rolls: [],77 permissions: [],78 player_emails: {},79 collaborators: [],80 btt_files: [],81 available_tools: [],82 adding_tool_type: 'battlemap|dnd5e|pathfinder|rpg',83 readable_types: {84 'battlemap': 'Battlemap',85 'dnd5e': 'D&D 5E Sheet',86 'pathfinder': 'Pathfinder Sheet',87 'rpg': 'RPG Sheet',88 },89 empty_map: { title: 'No Map Set' },90 modals: {91 main: false,92 tools: false,93 },94 legacyDateFormat: 'MMM d, yyyy - H:mm',95 dateFormat: 'yyyy-MM-dd',96 chatbox: '',97 command_list: {98 help: (chat: CampaignChat, command: CampaignCommand): CampaignChat => {99 chat.type = 'html'100 chat.text = '/roll or /r to roll dice with a valid dice expression.\nExample: /roll 2d8 + 4\n\n/secretroll works the same way, but only you and the campaign creator (GM) can see the results\nExample: /secretroll 8d6'101 return chat102 },103 roll: (chat: CampaignChat, command: CampaignCommand): CampaignChat => {104 chat.type = 'diceroll'105 chat.text = self.methods.interpretDiceroll(command)106 return chat107 },108 secretroll: (chat: CampaignChat, command: CampaignCommand): CampaignChat => {109 chat.type = 'secret_diceroll'110 chat.text = self.methods.interpretDiceroll(command)111 return chat112 },113 },114 active_note: '',115 active_npc: '',116 selection: {117 audio_types: [118 { label: 'Youtube ID', value: 'youtube' },119 { label: 'MP3 URL', value: 'mp3' },120 { label: 'OGG URL', value: 'ogg' },121 ],122 monster_levels: [123 'Any', 'CR 1 or Less', 'CR 2', 'CR 3', 'CR 4', 'CR 5', 'CR 6', 'CR 7', 'CR 8', 'CR 9', 'CR 10', 'CR 11', 'CR 12', 'CR 13', 'CR 14', 'CR 15', 'CR 16', 'CR 17', 'CR 18', 'CR 19', 'CR 20 or More',124 ]125 },126 monster_search: '',127 monster_level: 'Any',128 user: {},129 player: {},130 monsters: [],131 }132 // Command shortcuts133 self.locals.command_list['r'] = self.locals.command_list['roll']134 //***************************************************************************135 // Controller stuff136 //***************************************************************************137 self.methods.onModelReady = (): void => {138 self.locals.model_loaded = true139 loadMonsterData()140 self.methods.getTitle()141 self.methods.doIOwnThisTool()142 }143 self.methods.onUnfrozen = (): void => {144 self.locals.model_loaded = true145 finishedLoading()146 }147 self.methods.getTitle = (): void => {148 // $window.document.title = self.model.name + ' | Beyond Tabletop'149 }150 self.methods.updateTitle = async (): Promise<void> => {151 await Promise.resolve()152 self.touch()153 if (self.locals.permission.writer) {154 this.store.updatePlayerToolTitle(self.locals.user.firebase_id, self.locals.document_id, self.model.name)155 }156 }157 const loadMonsterData = async (): Promise<void> => {158 const data = await this.http.getLocalAsPromise('/assets/data/dnd5e/monsters.json')159 self.locals.monsters = data.monsters160 finishedLoading()161 }162 const finishedLoading = (): void => {163 self.locals.ready = true164 self.methods.goToBottomOfChat()165 }166 self.methods.onPlayerUpdate = (array: BtPlayerTool[]): void => {167 self.locals.available_tools = array.filter(tool => self.methods.isValidType(tool.tool_type)).map(tool => self.methods.createTool(tool))168 // Now we check for name changes169 self.methods.listTools().forEach((savedTool: BtPlayerTool): void => {170 const title = (self.locals.available_tools.find(x => x.id === savedTool.id) || {}).title171 savedTool.title = title || savedTool.title172 })173 }174 self.methods.shareDocument = (): void => {175 this.sharer.openSharingModal({176 id: self.locals.document_id,177 role: self.locals.player.role,178 title: self.model.name,179 tool_type: 'campaign',180 }, 'campaign')181 }182 /******************************************************183 * Accessor Methods184 ******************************************************/185 self.methods.$add = (parent: any, slug: string, construct: any, init: any = {}): void => {186 self.touch()187 parent[slug] = parent[slug] || []188 const item = new construct(init)189 parent[slug].push(item)190 }191 // Tools192 // ---------------------------------------------------193 self.methods.listTools = (): CampaignTool[] => self.model.tools || []194 self.methods.addTool = (tool: BtPlayerTool): void => {195 tool.$added = true196 const match: CampaignTool = self.methods.listTools().find(x => x.id === tool.id)197 if (!match) {198 self.methods.$add(self.model, 'tools', CampaignTool, tool)199 }200 }201 self.methods.removeTool = (tool: CampaignTool): void => {202 const match: BtPlayerTool = self.locals.available_tools.find(x => x.id === tool.id)203 if (!!match) {204 match.$added = false205 }206 self.methods.removeByObject(self.model.tools, tool)207 }208 self.methods.removeToolById = (id: string): void => {209 const tool: CampaignTool = self.methods.listTools().find(x => x.id === id)210 if (!!tool) {211 self.methods.removeTool(tool)212 }213 }214 self.methods.addToolFromModal = (tool: BtPlayerTool): void => {215 self.methods.addTool(tool)216 if (typeof self.locals.modals.callback === 'function') {217 self.locals.modals.callback(tool)218 }219 }220 // Chats221 // ---------------------------------------------------222 self.methods.listChats = (): CampaignChat[] => self.model.chats || []223 self.methods.sendChat = (): void => {224 if (!self.locals.chatbox) { return }225 const chat = self.methods.interpretChat() // check for false226 if (!!chat) {227 self.methods.addChat(chat)228 }229 self.locals.chatbox = ''230 }231 self.methods.addChat = (chat: CampaignChat): void => {232 self.methods.$add(self.model, 'chats', CampaignChat, chat)233 self.methods.pruneChats()234 self.methods.goToBottomOfChat()235 }236 // keeps the chat log from getting too long. Cuts out the oldest chats237 // anything over 100 is TOO LONG238 self.methods.pruneChats = (): void => {239 const index = self.methods.listChats().length - 100240 if (index > 0) {241 self.model.chats.splice(0, index)242 }243 }244 self.methods.toggleChat = (): void => {245 self.locals.chat_minimized = !self.locals.chat_minimized246 self.locals.player.chat_minimized = self.locals.chat_minimized247 self.touch()248 }249 self.methods.toggleChatSettings = (): void => {250 self.locals.chatSettings = !self.locals.chatSettings251 }252 self.methods.chatStyles = (): any => {253 const styles: any = {}254 if (typeof self.locals.player.font_size === 'number') {255 styles['font-size'] = `${self.locals.player.font_size}px`256 }257 return styles258 }259 // Players260 // ---------------------------------------------------261 self.methods.listPlayers = (): CampaignPlayer[] => self.model.players || []262 self.methods.addPlayer = (player: CampaignPlayer): void => self.methods.$add(self.model, 'players', CampaignPlayer, player)263 self.methods.removePlayer = async (player: CampaignPlayer): Promise<void> => {264 player.$busy = true265 await this.store.deleteFirebaseToolFromPlayer(self.locals.document_id, player.id)266 await this.store.deleteFirebaseUserPermission(self.locals.document_id, player.id)267 self.methods.removeByObject(self.model.players, player)268 }269 self.methods.prunePlayerById = (id: string): void => {270 const player = self.methods.listPlayers().find(x => x.id === id)271 if (!!player) {272 self.methods.removeByObject(self.model.players, player)273 }274 }275 // Audio Cues276 // ---------------------------------------------------277 self.methods.listAudioCues = (): CampaignAudioCue[] => self.model.audio_cues || []278 self.methods.listLoadedAudioCues = (): CampaignAudioCue[] => self.methods.listAudioCues().filter(x => x.loaded)279 self.methods.listLoadedActiveAudioCues = (): CampaignAudioCue[] => self.methods.listAudioCues().filter(x => x.loaded && x.$active !== false)280 self.methods.addAudioCue = (): void => {281 self.methods.$add(self.model, 'audio_cues', CampaignAudioCue, {282 pos: self.methods.listAudioCues().length283 })284 }285 self.methods.toggleCue = async (cue: CampaignAudioCue, local = false): Promise<void> => {286 await Promise.resolve()287 if (local) {288 if (cue.$active === undefined) { cue.$active = true }289 cue.$active = !cue.$active290 return291 }292 cue.loaded = !cue.loaded293 if (cue.loaded) {294 self.methods.addChat({295 text: `⪠Now playing audio: ${cue.name} â«`296 })297 }298 self.touch()299 }300 self.methods.toggleVolume = (cue: CampaignAudioCue, volumePropertyName = 'volume') => {301 cue[volumePropertyName] = (cue[volumePropertyName] || 4) - 1302 if (cue[volumePropertyName] < 1) {303 cue[volumePropertyName] = 4304 }305 self.methods.setCueVolume(cue)306 self.touch()307 }308 self.methods.setLocalVolume = async () => {309 await Promise.resolve()310 self.touch()311 self.methods.listLoadedActiveAudioCues().forEach((cue: CampaignAudioCue) => self.methods.setCueVolume(cue))312 }313 self.methods.setCueVolume = (cue: CampaignAudioCue) => {314 const volumePropertyName = cue.$volume === undefined ? 'volume' : '$volume'315 if (cue.$player && cue.audio_type === 'youtube') {316 cue.$player.setVolume((cue[volumePropertyName] - 1) * self.locals.player.audio_mult)317 }318 if (cue.$player && cue.audio_type !== 'youtube') {319 cue.$player.volume = ((cue[volumePropertyName] - 1) * self.locals.player.audio_mult) / 100320 }321 }322 self.methods.volumeIcon = (cue: CampaignAudioCue, volumePropertyName = 'volume') => {323 switch (cue[volumePropertyName]) {324 case 4: return 'volume_up'325 case 3: return 'volume_down'326 case 2: return 'volume_mute'327 case 1: return 'volume_off'328 default: return 'volume_up'329 }330 }331 self.methods.toggleRepeat = (cue: CampaignAudioCue) => {332 cue.loop = !cue.loop333 self.touch()334 }335 // Lists336 // ---------------------------------------------------337 self.methods.listLists = (): BtList[] => {338 return (self.model.lists || []).filter(x => x.author_id === self.locals.user.firebase_id)339 }340 self.methods.listListsBySpecial = (special: boolean): BtList[] => {341 return self.methods.listLists().filter(x => x.special === special)342 }343 self.methods.addList = (name: string): void => {344 self.methods.$add(self.model, 'lists', BtList, {345 name: name || '',346 pos: self.methods.listListsBySpecial(false).length,347 author_id: self.locals.user.firebase_id,348 })349 }350 // List Items351 // ---------------------------------------------------352 self.methods.listListItems = (list: BtList): BtNote[] => list.items || []353 self.methods.getListItem = (list: BtList, index: number): BtNote => list.items[index]354 self.methods.addListItem = (list: BtList, text: string): void => {355 self.methods.$add(list, 'items', BtNote, {356 text: text || '',357 pos: self.methods.listListItems(list).length,358 })359 }360 self.methods.addNote = (list: BtList, text: string): void => {361 self.methods.$add(list, 'items', BtNote, {362 text: text || '',363 pos: self.methods.listListItems(list).length,364 })365 const item = this.sheetSvc.lastFromArray(self.methods.listListItems(list))366 if (item) {367 self.methods.setActiveNote(item)368 }369 }370 // Logs371 // ---------------------------------------------------372 self.methods.listLogs = (): CampaignLog[] => self.model.logs || []373 self.methods.addLog = (): void => {374 self.methods.$add(self.model, 'logs', CampaignLog, {375 created_at: self.methods.currentTime(),376 pos: self.methods.listLogs().length,377 })378 }379 self.methods.getMostRecentLogInArray = (): CampaignLog[] => {380 const log = self.methods.listLogs()[0]381 return log ? [log] : []382 }383 self.methods.getLogSummary = (log: CampaignLog): string => {384 let summary = log.summary385 if (!summary) {386 summary = (log.text || '').substr(0, 500)387 }388 const index = summary.indexOf('\n')389 if (index > -1) {390 summary = summary.substr(0, index)391 }392 return summary393 }394 // Quests395 // ---------------------------------------------------396 self.methods.listQuests = (): CampaignQuest[] => self.model.quests || []397 self.methods.listKnownQuests = (): CampaignQuest[] => self.methods.listQuests().filter(x => x.known && !x.completed_at)398 self.methods.anyKnownQuests = (): boolean => self.methods.listKnownQuests().length > 0399 self.methods.addQuest = (): void => {400 self.methods.$add(self.model, 'quests', CampaignQuest, {401 pos: self.methods.listQuests().length,402 })403 }404 self.methods.startQuest = (quest: CampaignQuest): void => {405 quest.started_at = self.methods.currentTime()406 self.touch()407 }408 self.methods.completeQuest = (quest: CampaignQuest): void => {409 quest.completed_at = self.methods.currentTime()410 self.touch()411 }412 self.methods.questIsNotStarted = (quest: CampaignQuest): boolean => !quest.started_at413 self.methods.questIsIncomplete = (quest: CampaignQuest): boolean => !!quest.started_at && !quest.completed_at414 // NPCs415 // ---------------------------------------------------416 self.methods.listNPCs = (): CampaignNpc[] => self.model.npcs || []417 self.methods.listKnownNPCs = (): CampaignNpc[] => self.methods.listNPCs().filter(x => x.known === true)418 self.methods.anyKnownNPCs = (): boolean => self.methods.listKnownNPCs().length > 0419 self.methods.isActiveNPC = (npc: CampaignNpc): boolean => npc.id === self.locals.active_npc420 self.methods.addNPC = (npc: CampaignNpc = new CampaignNpc): void => {421 npc.pos = self.methods.listNPCs().length422 npc.name = npc.name ? npc.name : `${this.sheetSvc.randomName()} ${this.sheetSvc.randomName()}`423 self.methods.$add(self.model, 'npcs', CampaignNpc, npc)424 self.locals.active_npc = self.model.npcs[self.model.npcs.length - 1].id425 }426 self.methods.toggleActiveNPC = (npc: CampaignNpc): void => {427 self.locals.active_npc = self.methods.isActiveNPC(npc) ? null : npc.id428 }429 self.methods.getNPCClass = (npc: CampaignNpc): string => self.methods.isActiveNPC(npc) ? 'active' : 'inactive'430 self.methods.getAbilityMod = (detail: CampaignNpcDetail): number => {431 if (typeof detail.value === 'number') {432 return Math.floor((detail.value - 10) / 2)433 }434 }435 self.methods.randomName = (npc: CampaignNpc): void => {436 npc.name = `${this.sheetSvc.randomName()} ${this.sheetSvc.randomName()}`437 self.touch()438 }439 // NPC Details440 // ---------------------------------------------------441 self.methods.listNPCDetails = (npc: CampaignNpc): CampaignNpcDetail[] => npc.details || []442 self.methods.listNPCDetailsByGroup = (npc: CampaignNpc, group: string): CampaignNpcDetail[] => self.methods.listNPCDetails(npc).filter(x => x.group === group)443 self.methods.addNPCDetail = (npc: CampaignNpc, group: string): void => {444 self.methods.$add(npc, 'details', CampaignNpcDetail, {445 pos: self.methods.listNPCDetailsByGroup(npc, group).length,446 group: group,447 })448 }449 self.methods.listKnownNPCDetails = (npc: CampaignNpc, group: string): CampaignNpcDetail[] => self.methods.listNPCDetails(npc).filter(x => x.known && x.group === group)450 self.methods.anyDetailsVisible = (npc: CampaignNpc): boolean => self.methods.listNPCDetails(npc).some(d => d.known)451 // Foes452 // ---------------------------------------------------453 self.methods.listFoes = (): CampaignFoe[] => self.model.foes || []454 self.methods.listKnownFoes = (): CampaignFoe[] => self.methods.listFoes().filter(x => x.known === true)455 self.methods.anyKnownFoes = (): boolean => self.methods.listKnownFoes().length > 0456 self.methods.addFoe = (foe: CampaignFoe = new CampaignFoe): void => {457 foe.pos = self.methods.listFoes().length458 foe.name = foe.name ? foe.name : `${this.sheetSvc.randomName()} ${this.sheetSvc.randomName()}`459 self.methods.$add(self.model, 'foes', CampaignFoe, foe)460 self.locals.active_npc = self.model.foes[self.model.foes.length - 1].id461 }462 self.methods.moveNPC = (npc: CampaignNpc | CampaignFoe): void => {463 if (npc instanceof CampaignNpc) {464 self.methods.addFoe(npc)465 self.methods.removeByObject(self.model.npcs, npc)466 } else {467 self.methods.addNPC(npc)468 self.methods.removeByObject(self.model.foes, npc)469 }470 }471 self.methods.cloneNPC = (npc: CampaignNpc | CampaignFoe): void => {472 const npcCopy = { ...npc };473 npcCopy.name = `Copy of ${npc.name}`474 if (npc instanceof CampaignFoe) {475 self.methods.addFoe(npcCopy)476 } else {477 self.methods.addNPC(npcCopy)478 }479 self.locals.active_npc = null480 }481 self.methods.filteredMonsters = (): any[] => {482 return self.locals.monsters.filter(x => monsterIncludesText(x, self.locals.monster_search) && monsterWithinCR(x))483 }484 const monsterIncludesText = (monster: any, text: string): boolean => {485 text = text.toLowerCase()486 return monster.name.toLowerCase().includes(text)487 }488 const monsterWithinCR = (monster): boolean => {489 const targetNumber = parseInt(self.locals.monster_level.replace(/[^\d]*(\d+)[^\d]*/gi, '$1'))490 let monsterNumber = 0491 try {492 monsterNumber = eval(monster.challenge)493 } catch {494 monsterNumber = 0495 }496 if (targetNumber === 1 && monsterNumber <= 1) { monsterNumber = 1 }497 if (targetNumber === 20 && monsterNumber >= 20) { monsterNumber = 20 }498 return self.locals.monster_level === 'Any' || targetNumber === monsterNumber499 }500 //***************************************************************************501 // Helper methods502 //***************************************************************************503 self.methods.openChooser = (): void => { self.locals.choosing_tools = true }504 self.methods.closeChooser = (): void => { self.locals.choosing_tools = false }505 self.methods.backgroundStyle = this.sheetSvc.backgroundStyle506 self.methods.removeByObject = (array, item) => {507 self.touch()508 this.sheetSvc.removeByObject(array, item)509 }510 self.methods.selectionReverseLookup = this.sheetSvc.selectionReverseLookup511 self.methods.canDeletePlayer = (player): boolean => self.methods.isGM() && !self.methods.isGM(player)512 self.methods.shareMapWithCampaignPlayers = async (map: BtPlayerTool): Promise<void> => {513 this.store.documentPermissions$(map.id).pipe(take(1)).subscribe((permissions: BtPermission[]) => {514 const map_player_ids = permissions.map(x => x.id)515 self.methods.listPlayers().filter(player => !map_player_ids.includes(player.id)).forEach((player: CampaignPlayer) => {516 this.sharer.shareDocument(517 map.id,518 'battlemap',519 {520 firebase_id: player.id,521 email: self.locals.player_emails[player.id],522 name: player.name,523 } as BtUser,524 map.title525 )526 })527 })528 }529 self.methods.onSortableDrop = (e): void => {530 moveItemInArray(e.container.data, e.previousIndex, e.currentIndex)531 self.touch()532 }533 self.methods.truncate = (text: string, limit: number): string => text.substring(0, limit + 1)534 self.methods.currentTime = (): string => format(new Date(), self.locals.dateFormat)535 self.methods.displayDate = (str: string): string => {536 try {537 return format(parse(str, self.locals.dateFormat, new Date()), 'MMM d')538 } catch {539 try {540 return format(parse(str, self.locals.legacyDateFormat, new Date()), 'MMM d')541 } catch {542 return str543 }544 }545 }546 //***************************************************************************547 // Modal methods548 //***************************************************************************549 self.methods.openToolsModal = (callback: Function): void => {550 self.locals.modals.main = true551 self.locals.modals.tools = true552 self.locals.modals.callback = callback553 }554 self.methods.closeAllModals = (): void => {555 Object.keys(self.locals.modals).forEach((key: string) => self.locals.modals[key] = false)556 }557 self.methods.showingModal = (slug: string): boolean => self.locals.modals[slug]558 self.methods.getSiteModalClasses = (): any => ({'showing-modal': self.methods.showingModal('main')})559 //***************************************************************************560 // Tab methods561 //***************************************************************************562 self.methods.getTabById = (id: string): any => self.locals.tabs.list.find(x => x.id === id) || {title: ''}563 self.methods.switchTab = (id: string): any => {564 self.touch()565 self.locals.showing_chat = false566 self.locals.tabs.active = id567 self.locals.player.tab = id568 // $window.document.title = self.methods.getTabById(id).title + ' | ' + self.model.name + ' | Beyond Tabletop'569 }570 self.methods.listTabs = (): any[] => self.locals.tabs.list.filter(tab => !tab.role || tab.role.includes(self.locals.player.role))571 self.methods.toggleNav = (): void => { self.locals.tabs.showing_nav = !self.locals.tabs.showing_nav }572 self.methods.isTabActive = (id: string): boolean => self.locals.tabs.active === id573 self.methods.tabClass = (id: string): any => ({ active: self.methods.isTabActive(id) })574 self.methods.getActiveNavItem = (): string => {575 const tab = self.locals.tabs.list.find(x => x.id === self.locals.tabs.active) || { title: '' }576 return tab.title577 }578 self.methods.getTableTabsClass = (): any => ({ 'full-height': !self.locals.tabs.list.some(x => x.id === self.locals.tabs.active) })579 self.methods.switchTabByTool = (tool: CampaignTool): void => {580 const tab = self.locals.tabs.list.find(x => x.id === tool.id)581 !!tab ? self.methods.switchTab(tool.id) : self.methods.openTool(tool)582 // having to call openTool means the tool is saying it's open but it's really not583 // This happens when you open a table, open a tool, then navigate584 // away from the table entirely without refreshing. If you then585 // come back to the table, tools stored in locals will say they're586 // still good but they won't be.587 // ANGULAR 7 NOTE: the above comment may not be true anymore588 }589 self.methods.setPlayerLastTab = (): void => {590 if (self.locals.tabs.list.some(x => x.id === self.locals.player.tab)) {591 self.methods.switchTab(self.locals.player.tab)592 }593 }594 //***************************************************************************595 // Player methods596 //***************************************************************************597 self.methods.isGM = (player: CampaignPlayer = self.locals.player): boolean => player.role === 'owner'598 self.methods.currentPlayer = (): CampaignPlayer => self.methods.listPlayers().find(x => x.id === self.locals.user.firebase_id) || {}599 self.methods.getPlayerById = (id: string): CampaignPlayer => self.methods.listPlayers().find(x => x.id === id)600 self.methods.getPlayerName = (player: CampaignPlayer): string => {601 if (!!player) {602 return player.nickname || player.name603 }604 }605 self.methods.getPlayerNameById = (id: string): string => {606 const player = self.methods.getPlayerById(id)607 return self.methods.getPlayerName(player)608 }609 self.methods.getPlayerClasses = (player: CampaignPlayer): any => ({ busy: player.$busy })610 self.methods.canLinkPlayerSheet = (player: CampaignPlayer): boolean => {611 return (self.methods.isGM() || player.id === self.locals.player.id) && !self.methods.playerHasSheet(player)612 }613 self.methods.canOpenPlayerSheet = (player: CampaignPlayer): boolean => {614 const sheet = self.methods.getPlayerSheet(player)615 return !!sheet616 }617 self.methods.canRemovePlayerSheet = (player: CampaignPlayer): boolean => {618 const sheet = self.methods.getPlayerSheet(player)619 return !!sheet && (self.methods.doIOwnTool(sheet) || self.methods.isGM() || self.locals.player.id === player.id)620 }621 self.methods.canAddSheetAsTokenToMap = (player: CampaignPlayer): boolean => {622 return (self.methods.isGM() || player.id === self.locals.player.id) && self.methods.hasOpenCampaignMap() && self.methods.playerHasOpenSheet(player)623 }624 self.methods.addPlayerSheetAsToken = (player: CampaignPlayer): void => {625 this.store.addCharacterAsTokenById(player.sheet_id)626 this.interfaceSvc.showNotice('Token added!')627 }628 self.methods.getPlayerSheet = (player: CampaignPlayer): CampaignTool => {629 return self.methods.listTools().find(x => x.id === player.sheet_id)630 }631 self.methods.playerHasSheet = (player: CampaignPlayer): boolean => {632 const sheet = self.methods.getPlayerSheet(player)633 return !!sheet634 }635 self.methods.playerHasOpenSheet = (player: CampaignPlayer): boolean => {636 const sheet = self.methods.getPlayerSheet(player)637 return !!sheet && sheet.$disabled638 }639 self.methods.getPlayerSheetName = (player: CampaignPlayer): string => {640 const sheet = self.methods.getPlayerSheet(player) || { title: '' }641 return sheet.title642 }643 //***************************************************************************644 // Tool methods645 //***************************************************************************646 self.methods.getAvailableTools = (): BtPlayerTool => self.locals.available_tools.filter(t => !t.$added && self.locals.adding_tool_type.includes(t.tool_type))647 self.methods.openTool = (tool: CampaignTool): void => {648 tool.$disabled = true649 // Add to tabs list for nav650 self.locals.tabs.tools.push({ id: tool.id, tool_type: tool.tool_type })651 self.locals.tabs.list.push({ title: tool.title, id: tool.id, role: 'writer|owner' })652 self.methods.switchTab(tool.id)653 }654 self.methods.openToolById = (id: string): void => {655 const tool = self.methods.listTools().find(x => x.id === id)656 if (tool) {657 self.methods.openTool(tool)658 }659 }660 self.methods.doIOwnThisTool = (): void => {661 self.locals.tool_owner = self.locals.permission.role === 'owner'662 }663 self.methods.doIOwnTool = (tool: CampaignTool): boolean => {664 const matching_tool = self.locals.available_tools.find(x => x.id === tool.id)665 if (matching_tool) {666 return tool.$role === 'owner'667 }668 }669 self.methods.isToolAdded = (id: string): boolean => !!self.methods.listTools().find(x => x.id === id)670 self.methods.isValidType = (type: string): boolean => type && ['battlemap', 'dnd5e', 'pathfinder', 'rpg'].includes(type)671 self.methods.createTool = (document: BtPlayerTool): any => {672 return { tool_type: document.tool_type, id: document.id, title: document.title, $added: self.methods.isToolAdded(document.id) }673 }674 self.methods.setToolTitle = (doc_id: string, title: string): void => {675 [676 ...self.methods.listTools(),677 ...self.locals.available_tools,678 ...self.locals.tabs.list679 ].filter(x => x.id === doc_id).forEach(x => x.title = title)680 }681 const getCampaignMap = (): CampaignTool => self.methods.listTools().find((x: CampaignTool) => x.tool_type === 'battlemap')682 self.methods.canRemoveTableMap = (): boolean => self.methods.hasCampaignMap() && self.methods.isGM()683 self.methods.hasCampaignMap = (): boolean => !!getCampaignMap()684 self.methods.hasOpenCampaignMap = (): boolean => {685 const map = getCampaignMap()686 return !!map && map.$disabled687 }688 self.methods.getCampaignMapArray = (): CampaignTool[] => {689 const map = getCampaignMap()690 return map ? [map] : [self.locals.empty_map]691 }692 self.methods.onMapToolClick = (map: CampaignTool): void => {693 // messy694 if (!map.id) {695 if (self.methods.isGM()) {696 self.locals.adding_tool_type = 'battlemap'697 self.methods.openToolsModal(tool => {698 self.methods.closeAllModals()699 self.methods.shareMapWithCampaignPlayers(tool)700 })701 }702 } else if (map.$disabled) {703 self.methods.switchTabByTool(map)704 } else {705 self.methods.openTool(map)706 }707 }708 self.methods.onPlayerSheetClick = (id: string): void => {709 const tool = self.methods.listTools().find(x => x.id === id)710 if (!tool) { return }711 if (tool.$disabled) {712 self.methods.switchTabByTool(tool)713 } else {714 self.methods.openTool(tool)715 }716 }717 self.methods.addSheetToPlayer = (player: CampaignPlayer): void => {718 self.locals.adding_tool_type = 'dnd5e|pathfinder|rpg'719 self.methods.openToolsModal((tool: CampaignTool) => {720 player.sheet_id = tool.id721 self.methods.closeAllModals()722 })723 }724 self.methods.removeSheetFromPlayer = (player: CampaignPlayer): void => {725 self.methods.removeToolById(player.sheet_id)726 player.sheet_id = null727 }728 //***************************************************************************729 // Dice methods730 //***************************************************************************731 self.methods.interpretDiceroll = (command: CampaignCommand): any => {732 const formula = command.params.replace(/\s/gi, '')733 const { text } = this.diceSvc.rollCustomDice({734 text: formula,735 name: formula,736 })737 return text738 }739 self.methods.addRollToChat = ({ player_id, name, packs }): void => {740 if (!self.locals.ready) { return }741 self.methods.addChat({742 text: this.diceSvc.printRollResult(packs, ''),743 name,744 player_id,745 type: 'diceroll',746 })747 }748 self.methods.addCustomRollToChat = (result_string: string, id: string): void => {749 if (!self.locals.ready) { return }750 self.methods.addChat({751 text: result_string,752 name: self.methods.getPlayerNameById(id),753 player_id: id,754 type: 'diceroll'755 })756 }757 self.methods.rollFoeDice = (foe: CampaignFoe, attack: CampaignNpcDetail, slug: string): void => {758 const { text, result } = this.diceSvc.rollCustomDice({759 text: attack[slug],760 name: `${attack.name} (${slug === 'value' ? 'attack' : 'damage'})`,761 })762 if (result) {763 attack.$error = false764 self.methods.addChat({765 text,766 name: foe.name,767 player_id: null,768 type: 'diceroll'769 })770 } else {771 attack.$error = true772 }773 }774 //***************************************************************************775 // Chat methods776 //***************************************************************************777 self.methods.buildCommandObject = (chat: string): CampaignCommand => {778 const split = chat.split(' ')779 return {780 name: split.splice(0, 1)[0].substr(1),781 params: split.join(' ')782 } as CampaignCommand783 }784 self.methods.interpretKeypress = ($event: KeyboardEvent): void => {785 if ($event.key === 'Enter') {786 $event.preventDefault()787 self.methods.sendChat()788 }789 }790 self.methods.interpretChat = (): CampaignChat => {791 let newChat = new CampaignChat({792 text: self.locals.chatbox,793 name: self.methods.getPlayerName(self.locals.player),794 type: 'text',795 player_id: self.locals.player.id796 })797 // If the first char in chat is /, check if command exists798 if (self.locals.chatbox.charAt(0) === '/') {799 const command: CampaignCommand = self.methods.buildCommandObject(self.locals.chatbox)800 if (!!self.locals.command_list[command.name]) {801 newChat = self.locals.command_list[command.name](newChat, command)802 }803 }804 return newChat805 }806 self.methods.goToBottomOfChat = (): void => {807 setTimeout(() => {808 const elm = document.getElementById('chat-output')809 if (elm) {810 elm.scrollTop = elm.scrollHeight811 }812 }, 100)813 }814 self.methods.clearChat = (): void => { self.model.chats = [] }815 self.methods.isMyChat = (chat: CampaignChat): boolean => chat.player_id === self.locals.player.id816 self.methods.getChatClasses = (): any => ({ 'show-on-mobile': self.locals.showing_chat })817 self.methods.getChatName = (chat: CampaignChat): string => self.methods.getPlayerNameById(chat.player_id) || chat.name818 self.methods.getChatStyle = (chat: CampaignChat): any => {819 const player = self.methods.getPlayerById(chat.player_id)820 if (player) {821 return { color: player.color }822 }823 }824 self.methods.getNotesInArray = (): BtList[] => {825 if (!self.locals.ready) { return [] }826 const notes: BtList = self.methods.listListsBySpecial(true)[0]827 return notes ? [notes] : []828 }829 self.methods.getActiveNote = (): BtNote[] => {830 if (!self.locals.ready) { return [] }831 let note832 const list = self.methods.listListsBySpecial(true)[0]833 if (list) {834 note = self.methods.listListItems(list).find(x => x.id === self.locals.active_note) || self.methods.getListItem(list, 0)835 }836 return note ? [note] : []837 }838 self.methods.createNotes = (): void => {839 self.methods.$add(self.model, 'lists', BtList, {840 name: 'Notes',841 author_id: self.locals.user.firebase_id,842 special: true,843 })844 }845 const resortNotes = () => {846 const list = self.methods.listListsBySpecial(true)[0]847 if (list) {848 (list.items || []).sort((a, b) => b.opened_at - a.opened_at)849 }850 }851 self.methods.setActiveNote = (item: BtNote): void => {852 self.locals.active_note = item.id853 item.opened_at = Date.now()854 resortNotes()855 self.touch()856 }857 return self858 }...
methods.py
Source:methods.py
...58 arith_special = _arith_method_FRAME59 comp_special = _comp_method_FRAME60 bool_special = _arith_method_FRAME61 return arith_flex, comp_flex, arith_special, comp_special, bool_special62def add_special_arithmetic_methods(cls):63 """64 Adds the full suite of special arithmetic methods (``__add__``,65 ``__sub__``, etc.) to the class.66 Parameters67 ----------68 cls : class69 special methods will be defined and pinned to this class70 """71 _, _, arith_method, comp_method, bool_method = _get_method_wrappers(cls)72 new_methods = _create_methods(73 cls, arith_method, comp_method, bool_method, special=True74 )75 # inplace operators (I feel like these should get passed an `inplace=True`76 # or just be removed77 def _wrap_inplace_method(method):78 """79 return an inplace wrapper for this method80 """81 def f(self, other):82 result = method(self, other)83 # this makes sure that we are aligned like the input84 # we are updating inplace so we want to ignore is_copy85 self._update_inplace(86 result.reindex_like(self, copy=False)._data, verify_is_copy=False87 )88 return self89 name = method.__name__.strip("__")90 f.__name__ = f"__i{name}__"91 return f92 new_methods.update(93 dict(94 __iadd__=_wrap_inplace_method(new_methods["__add__"]),95 __isub__=_wrap_inplace_method(new_methods["__sub__"]),96 __imul__=_wrap_inplace_method(new_methods["__mul__"]),97 __itruediv__=_wrap_inplace_method(new_methods["__truediv__"]),98 __ifloordiv__=_wrap_inplace_method(new_methods["__floordiv__"]),99 __imod__=_wrap_inplace_method(new_methods["__mod__"]),100 __ipow__=_wrap_inplace_method(new_methods["__pow__"]),101 )102 )103 new_methods.update(104 dict(105 __iand__=_wrap_inplace_method(new_methods["__and__"]),106 __ior__=_wrap_inplace_method(new_methods["__or__"]),107 __ixor__=_wrap_inplace_method(new_methods["__xor__"]),108 )109 )110 _add_methods(cls, new_methods=new_methods)111def add_flex_arithmetic_methods(cls):112 """113 Adds the full suite of flex arithmetic methods (``pow``, ``mul``, ``add``)114 to the class.115 Parameters116 ----------117 cls : class118 flex methods will be defined and pinned to this class119 """120 flex_arith_method, flex_comp_method, _, _, _ = _get_method_wrappers(cls)121 new_methods = _create_methods(122 cls, flex_arith_method, flex_comp_method, bool_method=None, special=False123 )124 new_methods.update(125 dict(126 multiply=new_methods["mul"],127 subtract=new_methods["sub"],128 divide=new_methods["div"],129 )130 )131 # opt out of bool flex methods for now132 assert not any(kname in new_methods for kname in ("ror_", "rxor", "rand_"))133 _add_methods(cls, new_methods=new_methods)134def _create_methods(cls, arith_method, comp_method, bool_method, special):135 # creates actual methods based upon arithmetic, comp and bool method136 # constructors.137 have_divmod = issubclass(cls, ABCSeries)138 # divmod is available for Series139 new_methods = dict(140 add=arith_method(cls, operator.add, special),141 radd=arith_method(cls, radd, special),142 sub=arith_method(cls, operator.sub, special),143 mul=arith_method(cls, operator.mul, special),144 truediv=arith_method(cls, operator.truediv, special),145 floordiv=arith_method(cls, operator.floordiv, special),146 # Causes a floating point exception in the tests when numexpr enabled,147 # so for now no speedup148 mod=arith_method(cls, operator.mod, special),149 pow=arith_method(cls, operator.pow, special),150 # not entirely sure why this is necessary, but previously was included151 # so it's here to maintain compatibility152 rmul=arith_method(cls, rmul, special),153 rsub=arith_method(cls, rsub, special),154 rtruediv=arith_method(cls, rtruediv, special),155 rfloordiv=arith_method(cls, rfloordiv, special),156 rpow=arith_method(cls, rpow, special),157 rmod=arith_method(cls, rmod, special),158 )159 new_methods["div"] = new_methods["truediv"]160 new_methods["rdiv"] = new_methods["rtruediv"]161 if have_divmod:162 # divmod doesn't have an op that is supported by numexpr163 new_methods["divmod"] = arith_method(cls, divmod, special)164 new_methods["rdivmod"] = arith_method(cls, rdivmod, special)165 new_methods.update(166 dict(167 eq=comp_method(cls, operator.eq, special),168 ne=comp_method(cls, operator.ne, special),169 lt=comp_method(cls, operator.lt, special),170 gt=comp_method(cls, operator.gt, special),171 le=comp_method(cls, operator.le, special),172 ge=comp_method(cls, operator.ge, special),173 )174 )175 if bool_method:176 new_methods.update(177 dict(178 and_=bool_method(cls, operator.and_, special),179 or_=bool_method(cls, operator.or_, special),180 # For some reason ``^`` wasn't used in original.181 xor=bool_method(cls, operator.xor, special),182 rand_=bool_method(cls, rand_, special),183 ror_=bool_method(cls, ror_, special),184 rxor=bool_method(cls, rxor, special),185 )186 )187 if special:188 dunderize = lambda x: f"__{x.strip('_')}__"189 else:190 dunderize = lambda x: x191 new_methods = {dunderize(k): v for k, v in new_methods.items()}192 return new_methods193def _add_methods(cls, new_methods):194 for name, method in new_methods.items():195 # For most methods, if we find that the class already has a method196 # of the same name, it is OK to over-write it. The exception is197 # inplace methods (__iadd__, __isub__, ...) for SparseArray, which198 # retain the np.ndarray versions.199 force = not (issubclass(cls, ABCSparseArray) and name.startswith("__i"))200 if force or name not in cls.__dict__:...
use-payment-method-registration.ts
Source:use-payment-method-registration.ts
1/**2 * External dependencies3 */4import { __, sprintf } from '@wordpress/i18n';5import {6 getPaymentMethods,7 getExpressPaymentMethods,8} from '@woocommerce/blocks-registry';9import { useState, useEffect, useRef, useCallback } from '@wordpress/element';10import { useShallowEqual } from '@woocommerce/base-hooks';11import { CURRENT_USER_IS_ADMIN, getSetting } from '@woocommerce/settings';12/**13 * Internal dependencies14 */15import { useEditorContext } from '../../editor-context';16import { useShippingDataContext } from '../shipping';17import { useCustomerDataContext } from '../customer';18import type {19 PaymentMethodsDispatcherType,20 PaymentMethods,21 ExpressPaymentMethods,22 PaymentMethodConfig,23 ExpressPaymentMethodConfig,24} from './types';25import { useStoreCart } from '../../../hooks/cart/use-store-cart';26import { useStoreNotices } from '../../../hooks/use-store-notices';27import { useEmitResponse } from '../../../hooks/use-emit-response';28/**29 * This hook handles initializing registered payment methods and exposing all30 * registered payment methods that can be used in the current environment (via31 * the payment method's `canMakePayment` property).32 *33 * @param {function(Object):undefined} dispatcher A dispatcher for setting registered payment methods to an external state.34 * @param {Object} registeredPaymentMethods Registered payment methods to process.35 * @param {Array} paymentMethodsSortOrder Array of payment method names to sort by. This should match keys of registeredPaymentMethods.36 * @param {string} noticeContext Id of the context to append notices to.37 *38 * @return {boolean} Whether the payment methods have been initialized or not. True when all payment methods have been initialized.39 */40const usePaymentMethodRegistration = (41 dispatcher: PaymentMethodsDispatcherType,42 registeredPaymentMethods: PaymentMethods | ExpressPaymentMethods,43 paymentMethodsSortOrder: string[],44 noticeContext: string45) => {46 const [ isInitialized, setIsInitialized ] = useState( false );47 const { isEditor } = useEditorContext();48 const { selectedRates } = useShippingDataContext();49 const { billingData, shippingAddress } = useCustomerDataContext();50 const selectedShippingMethods = useShallowEqual( selectedRates );51 const paymentMethodsOrder = useShallowEqual( paymentMethodsSortOrder );52 const {53 cartTotals,54 cartNeedsShipping,55 paymentRequirements,56 } = useStoreCart();57 const canPayArgument = useRef( {58 cartTotals,59 cartNeedsShipping,60 billingData,61 shippingAddress,62 selectedShippingMethods,63 paymentRequirements,64 } );65 const { addErrorNotice } = useStoreNotices();66 useEffect( () => {67 canPayArgument.current = {68 cartTotals,69 cartNeedsShipping,70 billingData,71 shippingAddress,72 selectedShippingMethods,73 paymentRequirements,74 };75 }, [76 cartTotals,77 cartNeedsShipping,78 billingData,79 shippingAddress,80 selectedShippingMethods,81 paymentRequirements,82 ] );83 const refreshCanMakePayments = useCallback( async () => {84 let availablePaymentMethods = {};85 const addAvailablePaymentMethod = (86 paymentMethod: PaymentMethodConfig | ExpressPaymentMethodConfig87 ) => {88 availablePaymentMethods = {89 ...availablePaymentMethods,90 [ paymentMethod.name ]: paymentMethod,91 };92 };93 for ( let i = 0; i < paymentMethodsOrder.length; i++ ) {94 const paymentMethodName = paymentMethodsOrder[ i ];95 const paymentMethod = registeredPaymentMethods[ paymentMethodName ];96 if ( ! paymentMethod ) {97 continue;98 }99 // See if payment method should be available. This always evaluates to true in the editor context.100 try {101 const canPay = isEditor102 ? true103 : await Promise.resolve(104 paymentMethod.canMakePayment(105 canPayArgument.current106 )107 );108 if ( !! canPay ) {109 if (110 typeof canPay === 'object' &&111 canPay !== null &&112 canPay.error113 ) {114 throw new Error( canPay.error.message );115 }116 addAvailablePaymentMethod( paymentMethod );117 }118 } catch ( e ) {119 if ( CURRENT_USER_IS_ADMIN || isEditor ) {120 const errorText = sprintf(121 /* translators: %s the id of the payment method being registered (bank transfer, Stripe...) */122 __(123 `There was an error registering the payment method with id '%s': `,124 'woo-gutenberg-products-block'125 ),126 paymentMethod.paymentMethodId127 );128 addErrorNotice( `${ errorText } ${ e }`, {129 context: noticeContext,130 id: `wc-${ paymentMethod.paymentMethodId }-registration-error`,131 } );132 }133 }134 }135 // Re-dispatch available payment methods to store.136 dispatcher( availablePaymentMethods );137 // Note: some payment methods use the `canMakePayment` callback to initialize / setup.138 // Example: Stripe CC, Stripe Payment Request.139 // That's why we track "is initialized" state here.140 setIsInitialized( true );141 }, [142 addErrorNotice,143 dispatcher,144 isEditor,145 noticeContext,146 paymentMethodsOrder,147 registeredPaymentMethods,148 ] );149 // Determine which payment methods are available initially and whenever150 // shipping methods or cart totals change.151 // Some payment methods (e.g. COD) can be disabled for specific shipping methods.152 useEffect( () => {153 refreshCanMakePayments();154 }, [155 refreshCanMakePayments,156 cartTotals,157 selectedShippingMethods,158 paymentRequirements,159 ] );160 return isInitialized;161};162/**163 * Custom hook for setting up payment methods (standard, non-express).164 *165 * @param {function(Object):undefined} dispatcher166 *167 * @return {boolean} True when standard payment methods have been initialized.168 */169export const usePaymentMethods = (170 dispatcher: PaymentMethodsDispatcherType171): boolean => {172 const standardMethods: PaymentMethods = getPaymentMethods() as PaymentMethods;173 const { noticeContexts } = useEmitResponse();174 // Ensure all methods are present in order.175 // Some payment methods may not be present in paymentGatewaySortOrder if they176 // depend on state, e.g. COD can depend on shipping method.177 const displayOrder = new Set( [178 ...( getSetting( 'paymentGatewaySortOrder', [] ) as [ ] ),179 ...Object.keys( standardMethods ),180 ] );181 return usePaymentMethodRegistration(182 dispatcher,183 standardMethods,184 Array.from( displayOrder ),185 noticeContexts.PAYMENTS186 );187};188/**189 * Custom hook for setting up express payment methods.190 *191 * @param {function(Object):undefined} dispatcher192 *193 * @return {boolean} True when express payment methods have been initialized.194 */195export const useExpressPaymentMethods = (196 dispatcher: PaymentMethodsDispatcherType197): boolean => {198 const expressMethods: ExpressPaymentMethods = getExpressPaymentMethods() as ExpressPaymentMethods;199 const { noticeContexts } = useEmitResponse();200 return usePaymentMethodRegistration(201 dispatcher,202 expressMethods,203 Object.keys( expressMethods ),204 noticeContexts.EXPRESS_PAYMENTS205 );...
twitter_globals.py
Source:twitter_globals.py
1'''2 This module is automatically generated using `update.py`3 .. data:: POST_ACTIONS4 List of twitter method names that require the use of POST5'''6POST_ACTIONS = [7 # Status Methods8 'update', 'retweet', 'update_with_media', 'statuses/lookup',9 # Direct Message Methods10 'new',11 # Account Methods12 'update_profile_image', 'update_delivery_device', 'update_profile',13 'update_profile_background_image', 'update_profile_colors',14 'update_location', 'end_session', 'settings',15 'update_profile_banner', 'remove_profile_banner',16 # Notification Methods17 'leave', 'follow',18 # Status Methods, Block Methods, Direct Message Methods,19 # Friendship Methods, Favorite Methods20 'destroy', 'destroy_all',21 # Block Methods, Friendship Methods, Favorite Methods22 'create', 'create_all',23 # Users Methods24 'users/lookup', 'report_spam',25 # Streaming Methods26 'filter', 'user', 'site',27 # OAuth Methods28 'token', 'access_token',29 'request_token', 'invalidate_token',30 # Upload Methods31 'media/upload',...
fanfou_globals.py
Source:fanfou_globals.py
1"""2 List of Fanfou method names that require the use of POST.3"""4POST_ACTIONS = [5 # Status Methods6 'update',7 # Direct-messages Methods8 'new',9 # Account Methods10 'update_notify_num', 'update_profile', 'update_profile_image',11 # Blocks Methods, Friendships Methods, Favorites Methods,12 # Saved-searches Methods13 'create',14 # Statuses Methods, Blocks Methods, Direct-messages Methods,15 # Friendships Methods, Favorites Methods, Saved-searches Methods16 'destroy',17 # Friendships Methods18 'accept', 'deny',19 # Users Methods20 'cancel_recommendation',21 # Photo Methods22 'upload',23 # OAuth Methods24 'token', 'access_token',25 'request_token', 'invalidate_token',...
Using AI Code Generation
1import { MockBuilder, MockRender } from 'ng-mocks';2import { AppModule } from './app.module';3import { AppComponent } from './app.component';4describe('AppComponent', () => {5 beforeEach(() => MockBuilder(AppComponent, AppModule));6 it('should create the app', () => {7 const fixture = MockRender(AppComponent);8 const app = fixture.point.componentInstance;9 expect(app).toBeTruthy();10 });11 it(`should have as title 'app'`, () => {12 const fixture = MockRender(AppComponent);13 const app = fixture.point.componentInstance;14 expect(app.title).toEqual('app');15 });16 it('should render title in a h1 tag', () => {17 const fixture = MockRender(AppComponent);18 const compiled = fixture.debugElement.nativeElement;19 expect(compiled.querySelector('h1').textContent).toContain(20 );21 });22});23import { NgModule } from '@angular/core';24import { BrowserModule } from '@angular/platform-browser';25import { AppComponent } from './app.component';26@NgModule({27 imports: [BrowserModule],28})29export class AppModule {}30import { Component } from '@angular/core';31@Component({32})33export class AppComponent {34 title = 'app';35}36/* You can add global styles to this file, and also import other style files */
Using AI Code Generation
1import { mock } from 'ng-mocks';2import { AppComponent } from './app.component';3describe('AppComponent', () => {4 let component: AppComponent;5 beforeEach(() => {6 component = mock(AppComponent);7 });8 it('should create', () => {9 expect(component).toBeTruthy();10 });11});12import { Component } from '@angular/core';13@Component({14})15export class AppComponent {16 title = 'ng-mocks';17}18< h1 > {{ title }} < /h1>19/* You can add global styles to this file, and also import other style files */20h1 {21 color: #369;22 font-family: Arial, Helvetica, sans-serif;23 font-size: 250%;24}25import { TestBed, async } from '@angular/core/testing';26import { RouterTestingModule } from '@angular/router/testing';27import { AppComponent } from './app.component';28describe('AppComponent', () => {29 beforeEach(async(() => {30 TestBed.configureTestingModule({31 imports: [32 }).compileComponents();33 }));34 it('should create the app', () => {35 const fixture = TestBed.createComponent(AppComponent);36 const app = fixture.debugElement.componentInstance;37 expect(app).toBeTruthy();38 });39 it(`should have as title 'ng-mocks'`, () => {40 const fixture = TestBed.createComponent(AppComponent);41 const app = fixture.debugElement.componentInstance;42 expect(app.title).toEqual('ng-mocks');43 });44 it('should render title in a h1 tag', () => {45 const fixture = TestBed.createComponent(AppComponent);46 fixture.detectChanges();47 const compiled = fixture.debugElement.nativeElement;48 expect(compiled.querySelector('h1').textContent).toContain('ng-mocks');49 });50});51import { BrowserModule } from '@angular/platform-browser';52import { NgModule } from '@angular/core';53import { AppComponent } from './app.component';54@NgModule({55 imports: [56})57export class AppModule { }58import { NgModule } from '@angular
Using AI Code Generation
1import { mockNg } from 'ng-mocks';2import { mockNg } from 'ng-mocks';3describe('AppComponent', () => {4 let component: AppComponent;5 let fixture: ComponentFixture<AppComponent>;6 let mockNg: any;7 beforeEach(async(() => {8 TestBed.configureTestingModule({9 }).compileComponents();10 }));11 beforeEach(() => {12 fixture = TestBed.createComponent(AppComponent);13 component = fixture.componentInstance;14 fixture.detectChanges();15 });16 it('should create', () => {17 expect(component).toBeTruthy();18 });19});
Using AI Code Generation
1import { mock } from 'ng-mocks';2import { Component, Input } from '@angular/core';3@Component({4})5class TestComponent {6 @Input() public name: string;7}8const testComponent = mock(TestComponent);9testComponent.name = 'test';
Using AI Code Generation
1import { mockProvider } from 'ng-mocks';2describe('Test suite', () => {3 it('should run this test', () => {4 const mock = mockProvider(SomeService, {5 method: () => 'mocked value',6 });7 const instance = TestBed.inject(SomeService);8 expect(instance.method()).toEqual('mocked value');9 });10});
Using AI Code Generation
1import { mock } from 'ng-mocks';2mock(YourClass, 'method', () => 'mocked value');3mock(YourClass, 'method', () => 'mocked value');4mock(YourClass, 'method', () => 'mocked value');5mock(YourClass, 'method', (context) => 'mocked value');6mock(YourClass, 'method', (context, ...args) => 'mocked value');7mock(YourClass, 'method', (context, ...args) => 'mocked value');8mock(YourClass, 'method', (context, ...args) => 'mocked value');9mock(YourClass, 'method', (context, ...args) => 'mocked value');10mock(YourClass, 'method', (context, ...args) => 'mocked value');11mock(YourClass, 'method', (context, ...args) => 'mocked value');12mock(YourClass, 'method', (context, ...args) => 'mocked value');13mock(YourClass, 'method', (context, ...args) => 'mocked value');14mock(YourClass, 'method', (context, ...args) => 'mocked value');15mock(YourClass, 'method', (context, ...args) => 'mocked value');
Using AI Code Generation
1import { methods } from 'ng-mocks';2class MockService {3 methodOne() {4 return 'methodOne';5 }6 methodTwo() {7 return 'methodTwo';8 }9 methodThree() {10 return 'methodThree';11 }12}13const mockService = methods(MockService, {14 methodOne: () => 'methodOneMocked',15 methodTwo: () => 'methodTwoMocked'16});17const mockService2: MockInstance<MockService> = methods(MockService, {18 methodOne: () => 'methodOneMocked',19 methodTwo: () => 'methodTwoMocked'20});21const mockService3: MockInstance<MockService> = methods(MockService, {22 methodOne: () => 'methodOneMocked',23 methodTwo: () => 'methodTwoMocked'24});25spyOn(mockService3, 'methodThree').and.returnValue('methodThreeMocked');26const mockService4: MockInstance<MockService> = methods(MockService, {27 methodOne: () => 'methodOneMocked',28 methodTwo: () => 'methodTwoMocked'29});30spyOnProperty(mockService4, 'methodThree').and.returnValue('methodThreeMocked');31const mockService5: MockInstance<MockService> = methods(MockService, {32 methodOne: () => 'methodOneMocked',33 methodTwo: () => 'methodTwoMocked'34});35mockService5.methodThree = jasmine.createSpy('methodThree').and.returnValue('methodThreeMocked');36const mockService6: MockInstance<MockService> = methods(MockService, {37 methodOne: () => 'methodOneMocked',38 methodTwo: () => 'methodTwoMocked'39});40mockService6.methodThree = jasmine.createSpyObj('methodThree', ['
Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.
You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.
Get 100 minutes of automation test minutes FREE!!