async def _show_reference_options(ctx, destination): embed = EmbedWithAuthor(ctx) embed.title = "Rules" categories = ', '.join(a['type'] for a in compendium.rule_references) embed.description = f"Use `{ctx.prefix}{ctx.invoked_with} <category>` to look at all actions of " \ f"a certain type.\nCategories: {categories}" for actiontype in compendium.rule_references: embed.add_field(name=actiontype['fullName'], value=', '.join(a['name'] for a in actiontype['items']), inline=False) await destination.send(embed=embed)
async def spell(self, ctx, *, name: str): """Looks up a spell.""" spell, metadata = await select_spell_full(ctx, name, return_metadata=True, extra_choices=compendium.nspell_names, selectkey=self.nsrd_selectkey_obj) metadata['homebrew'] = spell.source == 'homebrew' await self.add_training_data("spell", name, spell.name, metadata=metadata, srd=spell.srd) if not (metadata['homebrew'] or spell.srd): return await self._non_srd(ctx, spell, "spell") embed = EmbedWithAuthor(ctx) color = embed.colour embed.title = spell.name embed.description = f"*{spell.get_level()} {spell.get_school().lower()}. " \ f"({', '.join(itertools.chain(spell.classes, spell.subclasses))})*" if spell.ritual: time = f"{spell.time} (ritual)" else: time = spell.time embed.add_field(name="Casting Time", value=time) embed.add_field(name="Range", value=spell.range) embed.add_field(name="Components", value=spell.components) embed.add_field(name="Duration", value=spell.duration) text = spell.description higher_levels = spell.higherlevels if len(text) > 1020: pieces = [text[:1020]] + [text[i:i + 2040] for i in range(1020, len(text), 2040)] else: pieces = [text] embed.add_field(name="Description", value=pieces[0], inline=False) embed_queue = [embed] if len(pieces) > 1: for piece in pieces[1:]: temp_embed = discord.Embed() temp_embed.colour = color temp_embed.description = piece embed_queue.append(temp_embed) if higher_levels: add_fields_from_long_text(embed_queue[-1], "At Higher Levels", higher_levels) if spell.source == 'homebrew': embed_queue[-1].set_footer(text="Homebrew content.", icon_url=HOMEBREW_ICON) else: embed_queue[-1].set_footer(text=f"Spell | {spell.source} {spell.page}") if spell.image: embed_queue[0].set_thumbnail(url=spell.image) destination = await self._get_destination(ctx) for embed in embed_queue: await destination.send(embed=embed)
async def feat(self, ctx, *, name: str): """Looks up a feat.""" choices = compendium.feats + compendium.nfeat_names result = await self._lookup_search(ctx, choices, name, lambda e: e['name'], search_type='feat') if not result: return embed = EmbedWithAuthor(ctx) embed.title = result['name'] if result['prerequisite']: embed.add_field(name="Prerequisite", value=result['prerequisite'], inline=False) if result['ability']: embed.add_field(name="Ability Improvement", value=f"Increase your {result['ability']} score by 1, up to a maximum of 20.", inline=False) add_fields_from_long_text(embed, "Description", result['desc']) embed.set_footer(text=f"Feat | {result['source']} {result['page']}") await (await self._get_destination(ctx)).send(embed=embed)
async def background(self, ctx, *, name: str): """Looks up a background.""" choices = compendium.backgrounds + compendium.nbackground_names result = await self._lookup_search(ctx, choices, name, lambda e: e.name, search_type='background', is_obj=True) if not result: return embed = EmbedWithAuthor(ctx) embed.title = result.name embed.set_footer(text=f"Background | {result.source} {result.page}") ignored_fields = ['suggested characteristics', 'personality trait', 'ideal', 'bond', 'flaw', 'specialty', 'harrowing event'] for trait in result.traits: if trait['name'].lower() in ignored_fields: continue text = trait['text'] text = textwrap.shorten(text, width=1020, placeholder="...") embed.add_field(name=trait['name'], value=text, inline=False) await (await self._get_destination(ctx)).send(embed=embed)
async def subclass(self, ctx, name: str): """Looks up a subclass.""" choices = compendium.subclasses + compendium.nsubclass_names result = await self._lookup_search(ctx, choices, name, lambda e: e['name'], search_type='subclass') if not result: return embed = EmbedWithAuthor(ctx) embed.title = result['name'] embed.description = f"*Source: {result['source']}*" for level_features in result['subclassFeatures']: for feature in level_features: for entry in feature['entries']: if not isinstance(entry, dict): continue if not entry.get('type') == 'entries': continue text = parse_data_entry(entry['entries']) embed.add_field(name=entry['name'], value=(text[:1019] + "...") if len(text) > 1023 else text, inline=False) embed.set_footer(text=f"Use {ctx.prefix}classfeat to look up a feature if it is cut off.") await (await self._get_destination(ctx)).send(embed=embed)
async def race(self, ctx, *, name: str): """Looks up a race.""" choices = compendium.fancyraces + compendium.nrace_names result = await self._lookup_search(ctx, choices, name, lambda e: e.name, search_type='race', is_obj=True) if not result: return embed = EmbedWithAuthor(ctx) embed.title = result.name embed.description = f"Source: {result.source}" embed.add_field(name="Speed", value=result.get_speed_str()) embed.add_field(name="Size", value=result.size) if result.ability: embed.add_field(name="Ability Bonuses", value=result.get_asi_str()) for t in result.get_traits(): add_fields_from_long_text(embed, t['name'], t['text']) await (await self._get_destination(ctx)).send(embed=embed)
async def genChar(self, ctx, final_level, race=None, _class=None, subclass=None, background=None): loadingMessage = await ctx.channel.send( "Generating character, please wait...") color = random.randint(0, 0xffffff) # Name Gen # DMG name gen name = self.old_name_gen() # Stat Gen # 4d6d1 # reroll if too low/high stats = self.stat_gen() await ctx.author.send("**Stats for {0}:** `{1}`".format(name, stats)) # Race Gen # Racial Features race = race or random.choice(compendium.fancyraces) embed = EmbedWithAuthor(ctx) embed.title = race.name embed.description = f"Source: {race.source}" embed.add_field(name="Speed", value=race.get_speed_str()) embed.add_field(name="Size", value=race.size) embed.add_field(name="Ability Bonuses", value=race.get_asi_str()) for t in race.get_traits(): f_text = t['text'] f_text = [f_text[i:i + 1024] for i in range(0, len(f_text), 1024)] embed.add_field(name=t['name'], value=f_text[0]) for piece in f_text[1:]: embed.add_field(name="** **", value=piece) embed.colour = color await ctx.author.send(embed=embed) # Class Gen # Class Features _class = _class or random.choice(compendium.classes) subclass = subclass or (random.choice(_class['subclasses']) if _class['subclasses'] else None) embed = EmbedWithAuthor(ctx) embed.title = f"{_class['name']} ({subclass['name']})" embed.add_field(name="Hit Die", value=f"1d{_class['hd']['faces']}") embed.add_field(name="Saving Throws", value=', '.join( ABILITY_MAP.get(p) for p in _class['proficiency'])) levels = [] starting_profs = f"You are proficient with the following items, " \ f"in addition to any proficiencies provided by your race or background.\n" \ f"Armor: {', '.join(_class['startingProficiencies'].get('armor', ['None']))}\n" \ f"Weapons: {', '.join(_class['startingProficiencies'].get('weapons', ['None']))}\n" \ f"Tools: {', '.join(_class['startingProficiencies'].get('tools', ['None']))}\n" \ f"Skills: Choose {_class['startingProficiencies']['skills']['choose']} from " \ f"{', '.join(_class['startingProficiencies']['skills']['from'])}" equip_choices = '\n'.join( f"• {i}" for i in _class['startingEquipment']['default']) gold_alt = f"Alternatively, you may start with {_class['startingEquipment']['goldAlternative']} gp " \ f"to buy your own equipment." if 'goldAlternative' in _class['startingEquipment'] else '' starting_items = f"You start with the following items, plus anything provided by your background.\n" \ f"{equip_choices}\n" \ f"{gold_alt}" for level in range(1, final_level + 1): level_str = [] level_features = _class['classFeatures'][level - 1] for feature in level_features: level_str.append(feature.get('name')) levels.append(', '.join(level_str)) embed.add_field(name="Starting Proficiencies", value=starting_profs) embed.add_field(name="Starting Equipment", value=starting_items) level_features_str = "" for i, l in enumerate(levels): level_features_str += f"`{i + 1}` {l}\n" embed.description = level_features_str embed.colour = color await ctx.author.send(embed=embed) embed = EmbedWithAuthor(ctx) level_resources = {} for table in _class.get('classTableGroups', []): relevant_row = table['rows'][final_level - 1] for i, col in enumerate(relevant_row): level_resources[table['colLabels'][i]] = parse_data_entry( [col]) for res_name, res_value in level_resources.items(): embed.add_field(name=res_name, value=res_value) embed.colour = color await ctx.author.send(embed=embed) embed_queue = [EmbedWithAuthor(ctx)] num_subclass_features = 0 num_fields = 0 def inc_fields(ftext): nonlocal num_fields num_fields += 1 if num_fields > 25: embed_queue.append(EmbedWithAuthor(ctx)) num_fields = 0 if len(str(embed_queue[-1].to_dict())) + len(ftext) > 5800: embed_queue.append(EmbedWithAuthor(ctx)) num_fields = 0 for level in range(1, final_level + 1): level_features = _class['classFeatures'][level - 1] for f in level_features: if f.get('gainSubclassFeature'): num_subclass_features += 1 text = parse_data_entry(f['entries']) text = [text[i:i + 1024] for i in range(0, len(text), 1024)] inc_fields(text[0]) embed_queue[-1].add_field(name=f['name'], value=text[0]) for piece in text[1:]: inc_fields(piece) embed_queue[-1].add_field(name="\u200b", value=piece) if subclass: for num in range(num_subclass_features): level_features = subclass['subclassFeatures'][num] for feature in level_features: for entry in feature.get('entries', []): if not isinstance(entry, dict): continue if not entry.get('type') == 'entries': continue fe = { 'name': entry['name'], 'text': parse_data_entry(entry['entries']) } text = [ fe['text'][i:i + 1024] for i in range(0, len(fe['text']), 1024) ] inc_fields(text[0]) embed_queue[-1].add_field(name=fe['name'], value=text[0]) for piece in text[1:]: inc_fields(piece) embed_queue[-1].add_field(name="\u200b", value=piece) for embed in embed_queue: embed.colour = color await ctx.author.send(embed=embed) # Background Gen # Inventory/Trait Gen background = background or random.choice(compendium.backgrounds) embed = EmbedWithAuthor(ctx) embed.title = background.name embed.description = f"*Source: {background.source}*" ignored_fields = [ 'suggested characteristics', 'specialty', 'harrowing event' ] for trait in background.traits: if trait['name'].lower() in ignored_fields: continue text = trait['text'] text = [text[i:i + 1024] for i in range(0, len(text), 1024)] embed.add_field(name=trait['name'], value=text[0]) for piece in text[1:]: embed.add_field(name="\u200b", value=piece) embed.colour = color await ctx.author.send(embed=embed) out = "{6}\n{0}, {1} {7} {2} {3}. {4} Background.\nStat Array: `{5}`\nI have PM'd you full character details.".format( name, race.name, _class['name'], final_level, background.name, stats, ctx.message.author.mention, subclass['name'] if subclass else "") await loadingMessage.edit(content=out)
async def item_lookup(self, ctx, *, name): """Looks up an item.""" try: pack = await Pack.from_ctx(ctx) custom_items = pack.get_search_formatted_items() pack_id = pack.id except NoActiveBrew: custom_items = [] pack_id = None choices = list(itertools.chain(compendium.items, custom_items)) if ctx.guild: async for servpack in Pack.server_active(ctx): if servpack.id != pack_id: choices.extend(servpack.get_search_formatted_items()) # #881 - display nSRD names choices.extend(compendium.nitem_names) result, metadata = await search_and_select(ctx, choices, name, lambda e: e['name'], selectkey=self.nsrd_selectkey, return_metadata=True) metadata['homebrew'] = result.get('source') == 'homebrew' await self.add_training_data("item", name, result['name'], metadata=metadata, srd=result['srd']) if not (metadata['homebrew'] or result['srd']): return await self._non_srd(ctx, result, "item") embed = EmbedWithAuthor(ctx) item = result name = item['name'] proptext = "" if not item.get('source') == 'homebrew': damage = '' extras = '' properties = [] if 'type' in item: type_ = ', '.join( i for i in ([ITEM_TYPES.get(t, 'n/a') for t in item['type'].split(',')] + ["Wondrous Item" if item.get('wondrous') else '']) if i) for iType in item['type'].split(','): if iType in ('M', 'R', 'GUN'): damage = f"{item.get('dmg1', 'n/a')} {DMGTYPES.get(item.get('dmgType'), 'n/a')}" \ if 'dmg1' in item and 'dmgType' in item else '' type_ += f', {item.get("weaponCategory")}' if iType == 'S': damage = f"AC +{item.get('ac', 'n/a')}" if iType == 'LA': damage = f"AC {item.get('ac', 'n/a')} + DEX" if iType == 'MA': damage = f"AC {item.get('ac', 'n/a')} + DEX (Max 2)" if iType == 'HA': damage = f"AC {item.get('ac', 'n/a')}" if iType == 'SHP': # ships for p in ("CREW", "PASS", "CARGO", "DMGT", "SHPREP"): a = PROPS.get(p, 'n/a') proptext += f"**{a.title()}**: {compendium.itemprops[p]}\n" extras = f"Speed: {item.get('speed')}\nCarrying Capacity: {item.get('carryingcapacity')}\n" \ f"Crew {item.get('crew')}, AC {item.get('vehAc')}, HP {item.get('vehHp')}" if 'vehDmgThresh' in item: extras += f", Damage Threshold {item['vehDmgThresh']}" if iType == 'siege weapon': extras = f"Size: {SIZES.get(item.get('size'), 'Unknown')}\n" \ f"AC {item.get('ac')}, HP {item.get('hp')}\n" \ f"Immunities: {item.get('immune')}" else: type_ = ', '.join( i for i in ("Wondrous Item" if item.get('wondrous') else '', item.get('technology')) if i) rarity = str(item.get('rarity')).replace('None', '') if 'tier' in item: if rarity: rarity += f', {item["tier"]}' else: rarity = item['tier'] type_and_rarity = type_ + (f", {rarity}" if rarity else '') value = (item.get('value', 'n/a') + (', ' if 'weight' in item else '')) if 'value' in item else '' weight = (item.get('weight', 'n/a') + (' lb.' if item.get('weight') == '1' else ' lbs.')) \ if 'weight' in item else '' weight_and_value = value + weight for prop in item.get('property', []): if not prop: continue a = b = prop a = PROPS.get(a, 'n/a') if b in compendium.itemprops: proptext += f"**{a.title()}**: {compendium.itemprops[b]}\n" if b == 'V': a += " (" + item.get('dmg2', 'n/a') + ")" if b in ('T', 'A'): a += " (" + item.get('range', 'n/a') + "ft.)" if b == 'RLD': a += " (" + item.get('reload', 'n/a') + " shots)" properties.append(a) properties = ', '.join(properties) damage_and_properties = f"{damage} - {properties}" if properties else damage damage_and_properties = (' --- ' + damage_and_properties) if weight_and_value and damage_and_properties else \ damage_and_properties meta = f"*{type_and_rarity}*\n{weight_and_value}{damage_and_properties}\n{extras}" text = item['desc'] if 'reqAttune' in item: if item['reqAttune'] is True: # can be truthy, but not true embed.add_field(name="Attunement", value=f"Requires Attunement") else: embed.add_field(name="Attunement", value=f"Requires Attunement {item['reqAttune']}", inline=False) embed.set_footer(text=f"Item | {item.get('source', 'Unknown')} {item.get('page', 'Unknown')}") else: meta = item['meta'] text = item['desc'] if 'image' in item: embed.set_thumbnail(url=item['image']) add_homebrew_footer(embed) embed.title = name embed.description = meta # no need to render, has been prerendered if proptext: text = f"{text}\n{proptext}" if len(text) > 5500: text = text[:5500] + "..." add_fields_from_long_text(embed, "Description", text) await Stats.increase_stat(ctx, "items_looked_up_life") await (await self._get_destination(ctx)).send(embed=embed)
async def _class(self, ctx, name: str, level: int = None): """Looks up a class, or all features of a certain level.""" if level is not None and not 0 < level < 21: return await ctx.send("Invalid level.") choices = compendium.classes + compendium.nclass_names result = await self._lookup_search(ctx, choices, name, lambda e: e['name'], search_type='class') if not result: return embed = EmbedWithAuthor(ctx) if level is None: embed.title = result['name'] embed.add_field(name="Hit Die", value=f"1d{result['hd']['faces']}") embed.add_field(name="Saving Throws", value=', '.join(ABILITY_MAP.get(p) for p in result['proficiency'])) levels = [] starting_profs = f"You are proficient with the following items, " \ f"in addition to any proficiencies provided by your race or background.\n" \ f"Armor: {', '.join(result['startingProficiencies'].get('armor', ['None']))}\n" \ f"Weapons: {', '.join(result['startingProficiencies'].get('weapons', ['None']))}\n" \ f"Tools: {', '.join(result['startingProficiencies'].get('tools', ['None']))}\n" \ f"Skills: Choose {result['startingProficiencies']['skills']['choose']} from " \ f"{', '.join(result['startingProficiencies']['skills']['from'])}" equip_choices = '\n'.join(f"• {i}" for i in result['startingEquipment']['default']) gold_alt = f"Alternatively, you may start with {result['startingEquipment']['goldAlternative']} gp " \ f"to buy your own equipment." if 'goldAlternative' in result['startingEquipment'] else '' starting_items = f"You start with the following items, plus anything provided by your background.\n" \ f"{equip_choices}\n" \ f"{gold_alt}" for level in range(1, 21): level_str = [] level_features = result['classFeatures'][level - 1] for feature in level_features: level_str.append(feature.get('name')) levels.append(', '.join(level_str)) embed.add_field(name="Starting Proficiencies", value=starting_profs, inline=False) embed.add_field(name="Starting Equipment", value=starting_items, inline=False) level_features_str = "" for i, l in enumerate(levels): level_features_str += f"`{i + 1}` {l}\n" embed.description = level_features_str embed.set_footer(text=f"Use {ctx.prefix}classfeat to look up a feature.") else: embed.title = f"{result['name']}, Level {level}" level_resources = {} level_features = result['classFeatures'][level - 1] for table in result.get('classTableGroups', []): relevant_row = table['rows'][level - 1] for i, col in enumerate(relevant_row): level_resources[table['colLabels'][i]] = parse_data_entry([col]) for res_name, res_value in level_resources.items(): if res_value != '0': embed.add_field(name=res_name, value=res_value) for f in level_features: text = parse_data_entry(f['entries']) embed.add_field(name=f['name'], value=(text[:1019] + "...") if len(text) > 1023 else text, inline=False) embed.set_footer(text=f"Use {ctx.prefix}classfeat to look up a feature if it is cut off.") await (await self._get_destination(ctx)).send(embed=embed)
async def cast(self, ctx, caster, targets, args, combat=None): """ Casts this spell. :param ctx: The context of the casting. :param caster: The caster of this spell. :type caster: :class:`~cogs5e.models.sheet.statblock.StatBlock` :param targets: A list of targets :type targets: list of :class:`~cogs5e.models.sheet.statblock.StatBlock` :param args: Args :type args: :class:`~utils.argparser.ParsedArguments` :param combat: The combat the spell was cast in, if applicable. :return: {embed: Embed} """ # generic args l = args.last('l', self.level, int) i = args.last('i', type_=bool) title = args.last('title') # meta checks if not self.level <= l <= 9: raise SpellException("Invalid spell level.") # caster spell-specific overrides dc_override = None ab_override = None spell_override = None spellbook_spell = caster.spellbook.get_spell(self) if spellbook_spell is not None: dc_override = spellbook_spell.dc ab_override = spellbook_spell.sab spell_override = spellbook_spell.mod if not i: # if I'm a warlock, and I didn't have any slots of this level anyway (#655) # automatically scale up to the next level s.t. our slots are not 0 if l > 0 \ and l == self.level \ and not caster.spellbook.get_max_slots(l) \ and not caster.spellbook.can_cast(self, l): l = next((sl for sl in range(l, 6) if caster.spellbook.get_max_slots(sl)), l) # only scale up to l5 args['l'] = l # can I cast this spell? if not caster.spellbook.can_cast(self, l): embed = EmbedWithAuthor(ctx) embed.title = "Cannot cast spell!" if not caster.spellbook.get_slots(l): # out of spell slots err = f"You don't have enough level {l} slots left! Use `-l <level>` to cast at a different level, " \ f"`{ctx.prefix}g lr` to take a long rest, or `-i` to ignore spell slots!" elif self.name not in caster.spellbook: # don't know spell err = f"You don't know this spell! Use `{ctx.prefix}sb add {self.name}` to add it to your spellbook, " \ f"or pass `-i` to ignore restrictions." else: # ? err = "Not enough spell slots remaining, or spell not in known spell list!\n" \ f"Use `{ctx.prefix}game longrest` to restore all spell slots if this is a character, " \ f"or pass `-i` to ignore restrictions." embed.description = err if l > 0: embed.add_field(name="Spell Slots", value=caster.spellbook.remaining_casts_of(self, l)) return {"embed": embed} # use resource caster.spellbook.cast(self, l) # character setup character = None if isinstance(caster, PlayerCombatant): character = caster.character elif isinstance(caster, Character): character = caster # base stat stuff mod_arg = args.last("mod", type_=int) stat_override = '' if mod_arg is not None: mod = mod_arg if character: prof_bonus = character.stats.prof_bonus else: prof_bonus = 0 dc_override = 8 + mod + prof_bonus ab_override = mod + prof_bonus spell_override = mod elif character and any(args.last(s, type_=bool) for s in STAT_ABBREVIATIONS): base = next(s for s in STAT_ABBREVIATIONS if args.last(s, type_=bool)) mod = character.stats.get_mod(base) dc_override = 8 + mod + character.stats.prof_bonus ab_override = mod + character.stats.prof_bonus spell_override = mod stat_override = f" with {verbose_stat(base)}" if spell_override is None and (caster.spellbook.sab is None or caster.spellbook.dc is None): raise SpellException("This caster does not have the ability to cast spells.") # begin setup embed = discord.Embed() if title: embed.title = title.replace('[sname]', self.name) else: embed.title = f"{caster.get_title_name()} casts {self.name}{stat_override}!" if targets is None: targets = [None] # concentration noconc = args.last("noconc", type_=bool) conc_conflict = None conc_effect = None if all((self.concentration, isinstance(caster, Combatant), combat, not noconc)): duration = args.last('dur', self.get_combat_duration(), int) conc_effect = initiative.Effect.new(combat, caster, self.name, duration, "", True) effect_result = caster.add_effect(conc_effect) conc_conflict = effect_result['conc_conflict'] if self.automation and self.automation.effects: title = f"{caster.name} cast {self.name}!" await self.automation.run(ctx, embed, caster, targets, args, combat, self, conc_effect=conc_effect, ab_override=ab_override, dc_override=dc_override, spell_override=spell_override, title=title) else: text = self.description if len(text) > 1020: text = f"{text[:1020]}..." embed.add_field(name="Description", value=text, inline=False) if l != self.level and self.higherlevels: embed.add_field(name="At Higher Levels", value=self.higherlevels, inline=False) embed.set_footer(text="No spell automation found.") if l > 0 and not i: embed.add_field(name="Spell Slots", value=caster.spellbook.remaining_casts_of(self, l)) if conc_conflict: conflicts = ', '.join(e.name for e in conc_conflict) embed.add_field(name="Concentration", value=f"Dropped {conflicts} due to concentration.") if self.image: embed.set_thumbnail(url=self.image) if self.source == 'homebrew': add_homebrew_footer(embed) return {"embed": embed}