async def select_monster_full(ctx, name, cutoff=5, return_key=False, pm=False, message=None, list_filter=None, return_metadata=False): """ Gets a Monster from the compendium and active bestiary/ies. """ try: bestiary = await Bestiary.from_ctx(ctx) custom_monsters = bestiary.monsters bestiary_id = bestiary.id except NoActiveBrew: custom_monsters = [] bestiary_id = None choices = list(itertools.chain(c.monster_mash, custom_monsters)) if ctx.guild: async for servbestiary in ctx.bot.mdb.bestiaries.find({"server_active": str(ctx.guild.id)}, ['monsters']): choices.extend( Monster.from_bestiary(m) for m in servbestiary['monsters'] if servbestiary['_id'] != bestiary_id) def get_homebrew_formatted_name(monster): if monster.source == 'homebrew': return f"{monster.name} ({HOMEBREW_EMOJI})" return monster.name return await search_and_select(ctx, choices, name, lambda e: e.name, cutoff, return_key, pm, message, list_filter, selectkey=get_homebrew_formatted_name, return_metadata=return_metadata)
def migrate(bestiary): sha256 = hashlib.sha256() hash_str = json.dumps(bestiary['monsters']).encode() \ + bestiary['name'].encode() \ + str(bestiary.get('desc')).encode() sha256.update(hash_str) active = [bestiary['owner']] if bestiary.get('active') else [] server_active = [] for serv_sub in bestiary.get('server_active', []): server_active.append({ "subscriber_id": bestiary['owner'], "guild_id": serv_sub }) for monster in bestiary['monsters']: for key in ('traits', 'actions', 'reactions', 'legactions'): for trait in monster[key]: if 'attacks' in trait: del trait[ 'attacks'] # why did we store this in the first place? monsters = [Monster.from_bestiary(m) for m in bestiary['monsters']] new_bestiary = Bestiary(None, sha256.hexdigest(), bestiary['critterdb_id'], [bestiary['owner']], active, server_active, bestiary['name'], monsters, bestiary.get('desc')) return new_bestiary
def __init__(self): self.cfeats = self.load_json('srd-classfeats.json', []) self.classes = self.load_json('srd-classes.json', []) self.conditions = self.load_json('conditions.json', []) self.feats = self.load_json('srd-feats.json', []) self.itemprops = self.load_json('itemprops.json', {}) self.monsters = self.load_json('srd-bestiary.json', []) self.names = self.load_json('names.json', []) self.rules = self.load_json('rules.json', []) self.spells = [Spell.from_data(r) for r in self.load_json('srd-spells.json', [])] self.backgrounds = [Background.from_data(b) for b in self.load_json('srd-backgrounds.json', [])] self.items = [i for i in self.load_json('srd-items.json', []) if i.get('type') is not '$'] self.monster_mash = [Monster.from_data(m) for m in self.monsters] self.subclasses = self.load_subclasses() srd_races = self.load_json('srd-races.json', []) self.fancyraces = [Race.from_data(r) for r in srd_races] self.rfeats = [] for race in srd_races: for entry in race['entries']: if isinstance(entry, dict) and 'name' in entry: temp = {'name': "{}: {}".format(race['name'], entry['name']), 'text': parse_data_entry(entry['entries']), 'srd': race['srd']} self.rfeats.append(temp)
async def bestiary_from_critterdb(self, url): log.info(f"Getting bestiary ID {url}...") index = 1 creatures = [] async with aiohttp.ClientSession() as session: for _ in range(100): # 100 pages max log.info(f"Getting page {index} of {url}...") async with session.get( f"http://critterdb.com/api/publishedbestiaries/{url}/creatures/{index}" ) as resp: if not 199 < resp.status < 300: raise ExternalImportError( "Error importing bestiary. Are you sure the link is right?" ) raw = await resp.json() if not raw: break creatures.extend(raw) index += 1 async with session.get( f"http://critterdb.com/api/publishedbestiaries/{url}" ) as resp: raw = await resp.json() name = raw['name'] parsed_creatures = [Monster.from_critterdb(c) for c in creatures] return Bestiary(url, name, parsed_creatures)
async def from_critterdb(cls, ctx, url): log.info(f"Getting bestiary ID {url}...") index = 1 creatures = [] sha256_hash = hashlib.sha256() async with aiohttp.ClientSession() as session: for _ in range(100): # 100 pages max log.info(f"Getting page {index} of {url}...") async with session.get( f"http://critterdb.com/api/publishedbestiaries/{url}/creatures/{index}" ) as resp: if not 199 < resp.status < 300: raise ExternalImportError( "Error importing bestiary: HTTP error. Are you sure the link is right?" ) try: raw_creatures = await resp.json() sha256_hash.update(await resp.read()) except (ValueError, aiohttp.ContentTypeError): raise ExternalImportError( "Error importing bestiary: bad data. Are you sure the link is right?" ) if not raw_creatures: break creatures.extend(raw_creatures) index += 1 async with session.get( f"http://critterdb.com/api/publishedbestiaries/{url}" ) as resp: try: raw = await resp.json() except (ValueError, aiohttp.ContentTypeError): raise ExternalImportError( "Error importing bestiary metadata. Are you sure the link is right?" ) name = raw['name'] desc = raw['description'] sha256_hash.update(name.encode() + desc.encode()) # try and find a bestiary by looking up upstream|hash # if it exists, return it # otherwise commit a new one to the db and return that sha256 = sha256_hash.hexdigest() log.debug(f"Bestiary hash: {sha256}") existing_bestiary = await ctx.bot.mdb.bestiaries.find_one({ "upstream": url, "sha256": sha256 }) if existing_bestiary: log.info("This bestiary already exists, subscribing") existing_bestiary = Bestiary.from_dict(existing_bestiary) await existing_bestiary.subscribe(ctx) return existing_bestiary parsed_creatures = [Monster.from_critterdb(c) for c in creatures] b = cls(None, sha256, url, [], [], [], name, parsed_creatures, desc) await b.write_to_db(ctx) return b
async def load_monsters(self, ctx): if not self._monsters: bestiary = await ctx.bot.mdb.bestiaries.find_one( {"_id": self.id}, projection=['monsters']) self._monsters = [ Monster.from_bestiary(m) for m in bestiary['monsters'] ] return self._monsters
def load_common(self): self.backgrounds = [Background.from_data(b) for b in self.srd_backgrounds] self.fancyraces = [Race.from_data(r) for r in self.srd_races] self.monster_mash = [Monster.from_data(m) for m in self.monsters] self.spells = [Spell.from_data(s) for s in self.srd_spells] self.items = [i for i in self.srd_items if i.get('type') is not '$'] self.rfeats = self.load_rfeats() self.subclasses = self.load_subclasses()
def migrate_monster(old_monster): def spaced_to_camel(spaced): return re.sub(r"\s+(\w)", lambda m: m.group(1).upper(), spaced.lower()) for old_key in ('raw_saves', 'raw_skills'): if old_key in old_monster: del old_monster[old_key] if 'spellcasting' in old_monster and old_monster['spellcasting']: old_spellcasting = old_monster.pop('spellcasting') old_monster['spellbook'] = Spellbook( {}, {}, [SpellbookSpell(s) for s in old_spellcasting['spells']], old_spellcasting['dc'], old_spellcasting['attackBonus'], old_spellcasting['casterLevel']).to_dict() else: old_monster['spellbook'] = Spellbook({}, {}, []).to_dict() base_stats = BaseStats(0, old_monster.pop('strength'), old_monster.pop('dexterity'), old_monster.pop('constitution'), old_monster.pop('intelligence'), old_monster.pop('wisdom'), old_monster.pop('charisma')) old_monster['ability_scores'] = base_stats.to_dict() old_saves = old_monster.pop('saves') saves = Saves.default(base_stats) save_updates = {} for save, value in old_saves.items(): if value != saves[save]: save_updates[save] = value saves.update(save_updates) old_monster['saves'] = saves.to_dict() old_skills = old_monster.pop('skills') skills = Skills.default(base_stats) skill_updates = {} for skill, value in old_skills.items(): name = spaced_to_camel(skill) if value != skills[name]: skill_updates[name] = value skills.update(skill_updates) old_monster['skills'] = skills.to_dict() new_monster = Monster.from_bestiary(old_monster) return new_monster
def __init__(self): with open('./res/conditions.json', 'r') as f: self.conditions = json.load(f) with open('./res/rules.json', 'r') as f: self.rules = json.load(f) with open('./res/feats.json', 'r') as f: self.feats = json.load(f) with open('./res/races.json', 'r') as f: _raw = json.load(f) self.rfeats = [] self.fancyraces = [Race.from_data(r) for r in _raw] for race in _raw: for entry in race['entries']: if isinstance(entry, dict) and 'name' in entry: temp = { 'name': "{}: {}".format(race['name'], entry['name']), 'text': parse_data_entry(entry['entries']), 'srd': race['srd'] } self.rfeats.append(temp) with open('./res/classes.json', 'r') as f: self.classes = json.load(f) with open('./res/classfeats.json') as f: self.cfeats = json.load(f) with open('./res/bestiary.json', 'r') as f: self.monsters = json.load(f) self.monster_mash = [Monster.from_data(m) for m in self.monsters] with open('./res/spells.json', 'r') as f: self.spells = [Spell.from_data(r) for r in json.load(f)] with open('./res/items.json', 'r') as f: _items = json.load(f) self.items = [i for i in _items if i.get('type') is not '$'] with open('./res/backgrounds.json', 'r') as f: self.backgrounds = [Background.from_data(b) for b in json.load(f)] self.subclasses = self.load_subclasses() with open('./res/itemprops.json', 'r') as f: self.itemprops = json.load(f) with open('./res/names.json', 'r') as f: self.names = json.load(f)
def from_dict(cls, d): if 'monsters' in d: d['monsters'] = [Monster.from_bestiary(m) for m in d['monsters']] if 'published' not in d: # versions prior to v1.5.11 don't have this tag, default to True d['published'] = True return cls(**d)
def _monster_factory(data): ability_scores = BaseStats( data['stats']['proficiencyBonus'] or 0, data['stats']['abilityScores']['strength'] or 10, data['stats']['abilityScores']['dexterity'] or 10, data['stats']['abilityScores']['constitution'] or 10, data['stats']['abilityScores']['intelligence'] or 10, data['stats']['abilityScores']['wisdom'] or 10, data['stats']['abilityScores']['charisma'] or 10) cr = { 0.125: '1/8', 0.25: '1/4', 0.5: '1/2' }.get(data['stats']['challengeRating'], str(data['stats']['challengeRating'])) num_hit_die = data['stats']['numHitDie'] hit_die_size = data['stats']['hitDieSize'] con_by_level = num_hit_die * ability_scores.get_mod('con') hp = floor(((hit_die_size + 1) / 2) * num_hit_die) + con_by_level hitdice = f"{num_hit_die}d{hit_die_size} + {con_by_level}" proficiency = data['stats']['proficiencyBonus'] if proficiency is None: raise ExternalImportError( f"Monster's proficiency bonus is nonexistent ({data['name']}).") skills = Skills.default(ability_scores) skill_updates = {} for skill in data['stats']['skills']: name = spaced_to_camel(skill['name']) if skill['proficient']: mod = skills[name].value + proficiency else: mod = skill.get('value') if mod is not None: skill_updates[name] = mod skills.update(skill_updates) saves = Saves.default(ability_scores) save_updates = {} for save in data['stats']['savingThrows']: name = save['ability'].lower() + 'Save' if save['proficient']: mod = saves.get(name).value + proficiency else: mod = save.get('value') if mod is not None: save_updates[name] = mod saves.update(save_updates) attacks = [] traits, atks = parse_critterdb_traits(data, 'additionalAbilities') attacks.extend(atks) actions, atks = parse_critterdb_traits(data, 'actions') attacks.extend(atks) reactions, atks = parse_critterdb_traits(data, 'reactions') attacks.extend(atks) legactions, atks = parse_critterdb_traits(data, 'legendaryActions') attacks.extend(atks) attacks = AttackList.from_dict(attacks) spellcasting = parse_critterdb_spellcasting(traits, ability_scores) return Monster(data['name'], data['stats']['size'], data['stats']['race'], data['stats']['alignment'], data['stats']['armorClass'], data['stats']['armorType'], hp, hitdice, data['stats']['speed'], ability_scores, cr, data['stats']['experiencePoints'], None, ', '.join(data['stats']['senses']), data['stats']['damageVulnerabilities'], data['stats']['damageResistances'], data['stats']['damageImmunities'], data['stats']['conditionImmunities'], saves, skills, data['stats']['languages'], traits, actions, reactions, legactions, data['stats']['legendaryActionsPerRound'], True, 'homebrew', attacks, data['flavor']['nameIsProper'], data['flavor']['imageUrl'], spellcasting=spellcasting)
def __init__(self): with open('./res/conditions.json', 'r') as f: self.conditions = json.load(f) with open('./res/rules.json', 'r') as f: self.rules = json.load(f) with open('./res/feats.json', 'r') as f: self.feats = json.load(f) with open('./res/races.json', 'r') as f: _raw = json.load(f) self.rfeats = [] self.races = copy.deepcopy(_raw) self.fancyraces = [Race.from_data(r) for r in self.races] for race in _raw: for entry in race['entries']: if isinstance(entry, dict) and 'name' in entry: temp = { 'name': "{}: {}".format(race['name'], entry['name']), 'text': parse_data_entry(entry['entries']), 'srd': race['srd'] } self.rfeats.append(temp) with open('./res/classes.json', 'r', encoding='utf-8-sig') as f: _raw = json.load(f) self.cfeats = [] self.classes = copy.deepcopy(_raw) for _class in _raw: for level in _class.get('classFeatures', []): for feature in level: fe = { 'name': f"{_class['name']}: {feature['name']}", 'text': parse_data_entry(feature['entries']), 'srd': _class['srd'] } self.cfeats.append(fe) options = [ e for e in feature['entries'] if isinstance(e, dict) and e['type'] == 'options' ] for option in options: for opt_entry in option.get('entries', []): fe = { 'name': f"{_class['name']}: {feature['name']}: {_resolve_name(opt_entry)}", 'text': f"{_parse_prereqs(opt_entry)}{parse_data_entry(opt_entry['entries'])}", 'srd': _class['srd'] } self.cfeats.append(fe) for subclass in _class.get('subclasses', []): for level in subclass.get('subclassFeatures', []): for feature in level: options = [ f for f in feature.get('entries', []) if isinstance(f, dict) and f['type'] == 'options' ] # battlemaster only for option in options: for opt_entry in option.get('entries', []): fe = { 'name': f"{_class['name']}: {option['name']}: " f"{_resolve_name(opt_entry)}", 'text': parse_data_entry(opt_entry['entries']), 'srd': subclass.get('srd', False) } self.cfeats.append(fe) for entry in feature.get('entries', []): if not isinstance(entry, dict): continue if not entry.get('type') == 'entries': continue fe = { 'name': f"{_class['name']}: {subclass['name']}: {entry['name']}", 'text': parse_data_entry(entry['entries']), 'srd': subclass.get('srd', False) } self.cfeats.append(fe) options = [ e for e in entry['entries'] if isinstance(e, dict) and e['type'] == 'options' ] for option in options: for opt_entry in option.get('entries', []): fe = { 'name': f"{_class['name']}: {subclass['name']}: {entry['name']}: " f"{_resolve_name(opt_entry)}", 'text': parse_data_entry( opt_entry['entries']), 'srd': subclass.get('srd', False) } self.cfeats.append(fe) with open('./res/bestiary.json', 'r') as f: self.monsters = json.load(f) self.monster_mash = [Monster.from_data(m) for m in self.monsters] with open('./res/spells.json', 'r') as f: self.spells = json.load(f) with open('./res/items.json', 'r') as f: _items = json.load(f) self.items = [i for i in _items if i.get('type') is not '$'] with open('./res/auto_spells.json', 'r') as f: self.autospells = json.load(f) with open('./res/backgrounds.json', 'r') as f: self.backgrounds = json.load(f) self.subclasses = self.load_subclasses() with open('./res/itemprops.json', 'r') as f: self.itemprops = json.load(f)
def from_raw(cls, _id, raw): monsters = [Monster.from_bestiary(m) for m in raw['monsters']] return cls(_id, raw['name'], monsters)
def from_dict(cls, d): if 'monsters' in d: d['monsters'] = [Monster.from_bestiary(m) for m in d['monsters']] return cls(**d)