async def racefeat(self, ctx, *, name: str): """Looks up a racial feature.""" guild_settings = await self.get_settings(ctx.guild) pm = guild_settings.get("pm_result", False) destination = ctx.author if pm else ctx.channel result, metadata = await search_and_select(ctx, c.rfeats, name, lambda e: e['name'], return_metadata=True) await self.add_training_data("racefeat", name, result['name'], metadata=metadata) embed = EmbedWithAuthor(ctx) embed.title = result['name'] desc = result['text'] desc = [desc[i:i + 1024] for i in range(0, len(desc), 1024)] embed.description = ''.join(desc[:2]) for piece in desc[2:]: embed.add_field(name="** **", value=piece) await destination.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 _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.") result: gamedata.Class = await self._lookup_search3(ctx, {'class': compendium.classes}, name) embed = EmbedWithAuthor(ctx) embed.url = result.url if level is None: embed.title = result.name embed.add_field(name="Hit Points", value=result.hit_points) levels = [] for level in range(1, 21): level = result.levels[level - 1] levels.append(', '.join([feature.name for feature in level])) embed.add_field(name="Starting Proficiencies", value=result.proficiencies, inline=False) embed.add_field(name="Starting Equipment", value=result.equipment, inline=False) level_features_str = "" for i, l in enumerate(levels): level_features_str += f"`{i + 1}` {l}\n" embed.description = level_features_str handle_source_footer(embed, result, f"Use {ctx.prefix}classfeat to look up a feature.", add_source_str=False) else: embed.title = f"{result.name}, Level {level}" level_features = result.levels[level - 1] for resource, value in zip(result.table.headers, result.table.levels[level - 1]): if value != '0': embed.add_field(name=resource, value=value) for f in level_features: embed.add_field(name=f.name, value=trim_str(f.text, 1024), inline=False) handle_source_footer(embed, result, f"Use {ctx.prefix}classfeat to look up a feature if it is cut off.", add_source_str=False) await (await self._get_destination(ctx)).send(embed=embed)
async def rule(self, ctx, *, name: str): """Looks up a rule.""" guild_settings = await self.get_settings(ctx.message.server) pm = guild_settings.get("pm_result", False) destination = ctx.message.author if pm else ctx.message.channel result = await search_and_select(ctx, c.rules, name, lambda e: e['name']) embed = EmbedWithAuthor(ctx) embed.title = result['name'] desc = result['desc'] desc = [desc[i:i + 1024] for i in range(0, len(desc), 1024)] embed.description = ''.join(desc[:2]) for piece in desc[2:]: embed.add_field(name="** **", value=piece) await self.bot.send_message(destination, embed=embed)
async def subclass(self, ctx, *, name: str): """Looks up a subclass.""" result: gamedata.Subclass = await self._lookup_search3(ctx, {'class': compendium.subclasses}, name, query_type='subclass') embed = EmbedWithAuthor(ctx) embed.url = result.url embed.title = result.name embed.description = f"*Source: {result.source_str()}*" for level in result.levels: for feature in level: text = trim_str(feature.text, 1024) embed.add_field(name=feature.name, value=text, inline=False) handle_source_footer(embed, result, f"Use {ctx.prefix}classfeat to look up a feature if it is cut off.", add_source_str=False) await (await self._get_destination(ctx)).send(embed=embed)
async def token(self, ctx, *, name=None): """Shows a token for a monster or player. May not support all monsters.""" if name is None: token_cmd = self.bot.get_command('playertoken') if token_cmd is None: return await ctx.send("Error: SheetManager cog not loaded.") return await ctx.invoke(token_cmd) monster, metadata = await select_monster_full(ctx, name, return_metadata=True) metadata['homebrew'] = monster.source == 'homebrew' await self.add_training_data("monster", name, monster.name, metadata=metadata) url = monster.get_image_url() if not monster.source == 'homebrew': embed = EmbedWithAuthor(ctx) embed.title = monster.name embed.description = f"{monster.size} monster." embed.set_image(url=url) embed.set_footer(text="This command may not support all monsters.") await ctx.send(embed=embed) else: if not url: return await ctx.channel.send("This monster has no image.") try: processed = await generate_token(url) except Exception as e: return await ctx.channel.send(f"Error generating token: {e}") file = discord.File(processed, filename="image.png") await ctx.channel.send( "I generated this token for you! If it seems wrong, you can make your own at " "<http://rolladvantage.com/tokenstamp/>!", file=file)
async def spell(self, ctx, *, name: str): """Looks up a spell.""" choices = await get_spell_choices(ctx, filter_by_license=False) spell = await self._lookup_search3(ctx, {'spell': choices}, name) embed = EmbedWithAuthor(ctx) embed.url = spell.url color = embed.colour embed.title = spell.name school_level = f"{spell.get_level()} {spell.get_school().lower()}" if spell.level > 0 \ else f"{spell.get_school().lower()} cantrip" embed.description = f"*{school_level}. " \ f"({', '.join(itertools.chain(spell.classes, spell.subclasses))})*" if spell.ritual: time = f"{spell.time} (ritual)" else: time = spell.time meta = f"**Casting Time**: {time}\n" \ f"**Range**: {spell.range}\n" \ f"**Components**: {spell.components}\n" \ f"**Duration**: {spell.duration}" embed.add_field(name="Meta", value=meta) higher_levels = spell.higherlevels pieces = chunk_text(spell.description) embed.add_field(name="Description", value=pieces[0], inline=False) embed_queue = [embed] if len(pieces) > 1: for i, piece in enumerate(pieces[1::2]): temp_embed = discord.Embed() temp_embed.colour = color if (next_idx := (i + 1) * 2) < len( pieces ): # this is chunked into 1024 pieces, and descs can handle 2 temp_embed.description = piece + pieces[next_idx] else: temp_embed.description = piece embed_queue.append(temp_embed)
async def patron_roscoe(self, ctx): embed = EmbedWithAuthor(ctx) embed.title = "Roscoe's Feast" embed.description = "*6th level conjuration. (Cleric, Druid)*" embed.add_field(name="Casting Time", value="10 minutes") embed.add_field(name="Range", value="30 feet") embed.add_field(name="Components", value="V, S, M (A gem encrusted bowl worth 1000 gold pieces)") embed.add_field(name="Duration", value="Instantaneous") embed.add_field( name="Description", value="You call forth the Avatar of Roscoe who brings with him a magnificent feast of chicken and waffles.\n" "The feast takes 1 hour to consume and disappears at the end of that time, and the beneficial effects " "don't set in until this hour is over. Up to twelve creatures can partake of the feast.\n" "A creature that partakes of the feast gains several benefits. " "The creature is cured of all diseases and poison, becomes immune to poison and being frightened, and " "makes all Wisdom saving throws with advantage. Its hit point maximum also increases by 2d10, and it " "gains the same number of hit points. These benefits last for 24 hours.") embed.set_footer(text=f"Spell | Thanks Roscoe!") await ctx.send(embed=embed)
async def background(self, ctx, *, name: str): """Looks up a background.""" try: guild_id = ctx.message.server.id pm = self.settings.get(guild_id, {}).get("pm_result", False) srd = self.settings.get(guild_id, {}).get("srd", False) except: pm = False srd = False result = await search_and_select(ctx, c.backgrounds, name, lambda e: e['name'], srd=srd) if not result['srd'] and srd: return await self.send_srd_error(ctx, result) embed = EmbedWithAuthor(ctx) embed.title = result['name'] embed.description = f"*Source: {result.get('source', 'Unknown')}*" ignored_fields = [ 'suggested characteristics', 'personality trait', 'ideal', 'bond', 'flaw', 'specialty', 'harrowing event' ] for trait in result['trait']: if trait['name'].lower() in ignored_fields: continue text = '\n'.join(t for t in trait['text'] if t) text = textwrap.shorten(text, width=1020, placeholder="...") embed.add_field(name=trait['name'], value=text) # do stuff here if pm: await self.bot.send_message(ctx.message.author, embed=embed) else: await self.bot.say(embed=embed)
async def token(self, ctx, *, name=None): """Shows a monster's image.""" if name is None: token_cmd = self.bot.get_command('playertoken') if token_cmd is None: return await ctx.send("Error: SheetManager cog not loaded.") return await ctx.invoke(token_cmd) choices = await get_monster_choices(ctx, filter_by_license=False) monster = await self._lookup_search3(ctx, {'monster': choices}, name) await Stats.increase_stat(ctx, "monsters_looked_up_life") url = monster.get_image_url() embed = EmbedWithAuthor(ctx) embed.title = monster.name embed.description = f"{monster.size} monster." if not url: return await ctx.channel.send("This monster has no image.") embed.set_image(url=url) await ctx.send(embed=embed)
async def condition(self, ctx, *, name: str): """Looks up a condition.""" guild_settings = await self.get_settings(ctx.guild) pm = guild_settings.get("pm_result", False) destination = ctx.author if pm else ctx.channel result, metadata = await search_and_select(ctx, c.conditions, name, lambda e: e['name'], return_metadata=True) await self.add_training_data("condition", name, result['name'], metadata=metadata) embed = EmbedWithAuthor(ctx) embed.title = result['name'] embed.description = result['desc'] await destination.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 subclass(self, ctx, name: str): """Looks up a subclass.""" guild_settings = await self.get_settings(ctx.guild) pm = guild_settings.get("pm_result", False) destination = ctx.author if pm else ctx.channel result, metadata = await search_and_select(ctx, c.subclasses, name, lambda e: e['name'], return_metadata=True) await self.add_training_data("subclass", name, result['name'], metadata=metadata) 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) embed.set_footer( text= f"Use {ctx.prefix}classfeat to look up a feature if it is cut off." ) await destination.send(embed=embed)
async def rule(self, ctx, *, name: str = None): """Looks up a rule.""" destination = await self._get_destination(ctx) if name is None: return await self._show_reference_options(ctx, destination) options = [] for actiontype in compendium.rule_references: if name == actiontype['type']: return await self._show_action_options(ctx, actiontype, destination) else: options.extend(actiontype['items']) result, metadata = await search_and_select(ctx, options, name, lambda e: e['fullName'], return_metadata=True) await self.add_training_data("reference", name, result['fullName'], metadata=metadata) embed = EmbedWithAuthor(ctx) embed.title = result['fullName'] embed.description = f"*{result['short']}*" add_fields_from_long_text(embed, "Description", result['desc']) embed.set_footer(text=f"Rule | {result['source']}") await destination.send(embed=embed)
async def race(self, ctx, *, name: str): """Looks up a race.""" guild_settings = await self.get_settings(ctx.guild) pm = guild_settings.get("pm_result", False) destination = ctx.author if pm else ctx.channel result, metadata = await search_and_select(ctx, c.fancyraces, name, lambda e: e.name, return_metadata=True) await self.add_training_data("race", name, result.name, metadata=metadata) 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(): 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) await destination.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. :rtype: CastResult """ # generic args l = args.last('l', self.level, int) i = args.last('i', type_=bool) title = args.last('title') nopact = args.last('nopact', type_=bool) # 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 is_prepared = True 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 is_prepared = spellbook_spell.prepared if not i: # if I'm a warlock, and I didn't have any slots of this level anyway (#655) # automatically scale up to our pact slot level (or the next available level s.t. max > 0) if l > 0 \ and l == self.level \ and not caster.spellbook.get_max_slots(l) \ and not caster.spellbook.can_cast(self, l): if caster.spellbook.pact_slot_level is not None: l = caster.spellbook.pact_slot_level else: 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 " f"level, `{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 " f"spellbook, 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 CastResult(embed=embed, success=False, automation_result=None) # #1000: is this spell prepared (soft check)? if not is_prepared: skip_prep_conf = await confirm( ctx, f"{self.name} is not prepared. Do you want to cast it anyway? (Reply with yes/no)", delete_msgs=True) if not skip_prep_conf: embed = EmbedWithAuthor( ctx, title=f"Cannot cast spell!", description= f"{self.name} is not prepared! Prepare it on your character sheet and use " f"`{ctx.prefix}update` to mark it as prepared, or use `-i` to ignore restrictions." ) return CastResult(embed=embed, success=False, automation_result=None) # use resource caster.spellbook.cast(self, l, pact=not nopact) # base stat stuff mod_arg = args.last("mod", type_=int) with_arg = args.last("with") stat_override = '' if mod_arg is not None: mod = mod_arg prof_bonus = caster.stats.prof_bonus dc_override = 8 + mod + prof_bonus ab_override = mod + prof_bonus spell_override = mod elif with_arg is not None: if with_arg not in STAT_ABBREVIATIONS: raise InvalidArgument( f"{with_arg} is not a valid stat to cast with.") mod = caster.stats.get_mod(with_arg) dc_override = 8 + mod + caster.stats.prof_bonus ab_override = mod + caster.stats.prof_bonus spell_override = mod stat_override = f" with {verbose_stat(with_arg)}" # begin setup embed = discord.Embed() if title: embed.title = title.replace('[name]', caster.name) \ .replace('[aname]', self.name) \ .replace('[sname]', self.name) \ .replace('[verb]', 'casts') # #1514, [aname] is action name now, #1587, add verb to action/cast 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, BaseCombatant), combat, not noconc)): duration = args.last('dur', self.get_combat_duration(), int) conc_effect = Effect.new(combat, caster, self.name, duration, "", True) effect_result = caster.add_effect(conc_effect) conc_conflict = effect_result['conc_conflict'] # run automation_result = None if self.automation and self.automation.effects: title = f"{caster.name} cast {self.name}!" automation_result = 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: # no automation, display spell description phrase = args.join('phrase', '\n') if phrase: embed.description = f"*{phrase}*" embed.add_field(name="Description", value=smart_trim(self.description), inline=False) embed.set_footer(text="No spell automation found.") if l != self.level and self.higherlevels: embed.add_field(name="At Higher Levels", value=smart_trim(self.higherlevels), inline=False) 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 'thumb' in args: embed.set_thumbnail(url=maybe_http_url(args.last('thumb', ''))) elif self.image: embed.set_thumbnail(url=self.image) add_fields_from_args(embed, args.get('f')) gamedata.lookuputils.handle_source_footer(embed, self, add_source_str=False) return CastResult(embed=embed, success=True, automation_result=automation_result)
async def _class(self, ctx, name: str, level: int = None): """Looks up a class, or all features of a certain level.""" try: guild_id = ctx.message.server.id pm = self.settings.get(guild_id, {}).get("pm_result", False) srd = self.settings.get(guild_id, {}).get("srd", False) except: pm = False srd = False destination = ctx.message.author if pm else ctx.message.channel if level is not None and not 0 < level < 21: return await self.bot.say("Invalid level.") result = await search_and_select(ctx, c.classes, name, lambda e: e['name'], srd=srd) if not result['srd'] and srd: return await self.send_srd_error(ctx, result) 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) 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.set_footer(text="Use !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['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(): 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) embed.set_footer( text="Use !classfeat to look up a feature if it is cut off.") await self.bot.send_message(destination, embed=embed)
async def send_character_details(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 = [roll('4d6kh3').total for _ in range(6)] await ctx.author.send("**Stats for {0}:** `{1}`".format(name, stats)) # Race Gen # Racial Features race = race or random.choice(await get_race_choices(ctx)) embed = EmbedWithAuthor(ctx) embed.title = race.name embed.add_field(name="Speed", value=race.speed) embed.add_field(name="Size", value=race.size) for t in race.traits: embeds.add_fields_from_long_text(embed, t.name, t.text) embed.set_footer(text=f"Race | {race.source_str()}") embed.colour = color await ctx.author.send(embed=embed) # Class Gen # Class Features # class _class = _class or random.choice(await available(ctx, compendium.classes, 'class')) subclass = subclass or (random.choice(subclass_choices) if (subclass_choices := await available(ctx, _class.subclasses, 'class')) else None) embed = EmbedWithAuthor(ctx) embed.title = _class.name embed.add_field(name="Hit Points", value=_class.hit_points) levels = [] for level in range(1, final_level + 1): level = _class.levels[level - 1] levels.append(', '.join([feature.name for feature in level])) embed.add_field(name="Starting Proficiencies", value=_class.proficiencies, inline=False) embed.add_field(name="Starting Equipment", value=_class.equipment, inline=False) level_features_str = "" for i, l in enumerate(levels): level_features_str += f"`{i + 1}` {l}\n" embed.description = level_features_str await ctx.author.send(embed=embed) # level table embed = EmbedWithAuthor(ctx) embed.title = f"{_class.name}, Level {final_level}" for resource, value in zip(_class.table.headers, _class.table.levels[final_level - 1]): if value != '0': embed.add_field(name=resource, value=value) embed.colour = color await ctx.author.send(embed=embed) # features embed_queue = [EmbedWithAuthor(ctx)] 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 def add_levels(source): for level in range(1, final_level + 1): level_features = source.levels[level - 1] for f in level_features: for field in embeds.get_long_field_args(f.text, f.name): inc_fields(field['value']) embed_queue[-1].add_field(**field) add_levels(_class) if subclass: add_levels(subclass) for embed in embed_queue: embed.colour = color await ctx.author.send(embed=embed) # Background Gen # Inventory/Trait Gen background = background or random.choice(await available(ctx, compendium.backgrounds, 'background')) embed = EmbedWithAuthor(ctx) embed.title = background.name embed.set_footer(text=f"Background | {background.source_str()}") ignored_fields = ['suggested characteristics', 'personality trait', 'ideal', 'bond', 'flaw', 'specialty', 'harrowing event'] for trait in background.traits: if trait.name.lower() in ignored_fields: continue text = textwrap.shorten(trait.text, width=1020, placeholder="...") embed.add_field(name=trait.name, value=text, inline=False) embed.colour = color await ctx.author.send(embed=embed) out = f"{ctx.author.mention}\n" \ f"{name}, {race.name} {subclass.name if subclass else ''} {_class.name} {final_level}. " \ f"{background.name} Background.\n" \ f"Stat Array: `{stats}`\nI have PM'd you full character details." await loadingMessage.edit(content=out)
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: phrase = args.join('phrase', '\n') if phrase: embed.description = f"*{phrase}*" 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 'thumb' in args: embed.set_thumbnail(url=args.last('thumb')) elif self.image: embed.set_thumbnail(url=self.image) add_fields_from_args(embed, args.get('f')) if self.source == 'homebrew': add_homebrew_footer(embed) return {"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: cogs5e.models.caster.Spellcaster :param targets: A list of targets (Combatants) :param args: Args :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) phrase = args.join('phrase', '\n') title = args.last('title') # meta checks if not self.level <= l <= 9: raise SpellException("Invalid spell level.") if not (caster.can_cast(self, l) or i): embed = EmbedWithAuthor(ctx) embed.title = "Cannot cast spell!" embed.description = "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, " \ "or pass `-i` to ignore restrictions." if l > 0: embed.add_field(name="Spell Slots", value=caster.remaining_casts_of(self, l)) return {"embed": embed} if not i: caster.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) dc_override = None ab_override = None spell_override = None stat_override = '' if mod_arg is not None: mod = mod_arg dc_override = 8 + mod + character.get_prof_bonus() ab_override = mod + character.get_prof_bonus() spell_override = mod elif character and any(args.last(s, type_=bool) for s in ("str", "dex", "con", "int", "wis", "cha")): base = next(s for s in ("str", "dex", "con", "int", "wis", "cha") if args.last(s, type_=bool)) mod = character.get_mod(base) dc_override = 8 + mod + character.get_prof_bonus() ab_override = mod + character.get_prof_bonus() spell_override = mod stat_override = f" with {verbose_stat(base)}" # begin setup embed = discord.Embed() if title: embed.title = title.replace('[sname]', self.name) elif targets: embed.title = f"{caster.get_name()} casts {self.name}{stat_override} at..." else: embed.title = f"{caster.get_name()} casts {self.name}{stat_override}!" if targets is None: targets = [None] if phrase: embed.description = f"*{phrase}*" conc_conflict = None conc_effect = None if self.concentration and isinstance(caster, Combatant) and combat: 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: 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) else: text = self.description if len(text) > 1020: text = f"{text[:1020]}..." embed.add_field(name="Description", value=text) if l != self.level and self.higherlevels: embed.add_field(name="At Higher Levels", value=self.higherlevels) embed.set_footer(text="No spell automation found.") if l > 0 and not i: embed.add_field(name="Spell Slots", value=caster.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}
async def handle_required_license(ctx, err): """ Logs a unlicensed search and displays a prompt. :type ctx: discord.ext.commands.Context :type err: cogs5e.models.errors.RequiresLicense """ result = err.entity await ctx.bot.mdb.analytics_nsrd_lookup.update_one( { "type": result.entity_type, "name": result.name }, {"$inc": { "num_lookups": 1 }}, upsert=True) embed = EmbedWithAuthor(ctx) if not err.has_connected_ddb: # was the user blocked from nSRD by a feature flag? ddb_user = await ctx.bot.ddb.get_ddb_user(ctx, ctx.author.id) if ddb_user is None: blocked_by_ff = False else: blocked_by_ff = not (await ctx.bot.ldclient.variation( "entitlements-enabled", ddb_user.to_ld_dict(), False)) if blocked_by_ff: # get the message from feature flag # replacements: # $entity_type$, $entity_name$, $source$, $long_source$ unavailable_title = await ctx.bot.ldclient.variation( "entitlements-disabled-header", ddb_user.to_ld_dict(), f"{result.name} is not available") unavailable_desc = await ctx.bot.ldclient.variation( "entitlements-disabled-message", ddb_user.to_ld_dict(), f"{result.name} is currently unavailable") embed.title = unavailable_title \ .replace('$entity_type$', result.entity_type) \ .replace('$entity_name$', result.name) \ .replace('$source$', result.source) \ .replace('$long_source$', long_source_name(result.source)) embed.description = unavailable_desc \ .replace('$entity_type$', result.entity_type) \ .replace('$entity_name$', result.name) \ .replace('$source$', result.source) \ .replace('$long_source$', long_source_name(result.source)) else: embed.title = f"Connect your D&D Beyond account to view {result.name}!" embed.url = "https://www.dndbeyond.com/account" embed.description = \ "It looks like you don't have your Discord account connected to your D&D Beyond account!\n" \ "Linking your account means that you'll be able to use everything you own on " \ "D&D Beyond in Avrae for free - you can link your accounts " \ "[here](https://www.dndbeyond.com/account)." embed.set_footer( text= "Already linked your account? It may take up to a minute for Avrae to recognize the " "link.") else: embed.title = f"Purchase {result.name} on D&D Beyond to view it here!" embed.description = \ f"To see and search this {result.entity_type}'s full details, unlock **{result.name}** by " \ f"purchasing {long_source_name(result.source)} on D&D Beyond.\n\n" \ f"[Go to Marketplace]({result.marketplace_url})" embed.url = result.marketplace_url embed.set_footer( text= "Already purchased? It may take up to a minute for Avrae to recognize the " "purchase.") await ctx.send(embed=embed)
async def item_lookup(self, ctx, *, name): """Looks up an item.""" try: guild_id = ctx.message.server.id pm = self.settings.get(guild_id, {}).get("pm_result", False) srd = self.settings.get(guild_id, {}).get("srd", False) except: pm = False srd = False self.bot.db.incr('items_looked_up_life') result = await search_and_select(ctx, c.items, name, lambda e: e['name'], srd=srd) embed = EmbedWithAuthor(ctx) item = result if not item['srd'] and srd: return await self.send_srd_error(ctx, result) name = item['name'] damage = '' extras = '' properties = [] proptext = "" 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()}**: {c.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 c.itemprops: proptext += f"**{a.title()}**: {c.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 embed.title = name desc = f"*{type_and_rarity}*\n{weight_and_value}{damage_and_properties}\n{extras}" embed.description = parse_data_entry(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']}") text = parse_data_entry(item.get('entries', [])) if proptext: text = f"{text}\n{proptext}" if len(text) > 5500: text = text[:5500] + "..." field_name = "Description" for piece in [text[i:i + 1024] for i in range(0, len(text), 1024)]: embed.add_field(name=field_name, value=piece) field_name = "** **" embed.set_footer( text= f"Item | {item.get('source', 'Unknown')} {item.get('page', 'Unknown')}" ) if pm: await self.bot.send_message(ctx.message.author, embed=embed) else: await self.bot.say(embed=embed)
async def monster(self, ctx, *, name: str): """Looks up a monster. Generally requires a Game Master role to show full stat block. Game Master Roles: GM, DM, Game Master, Dungeon Master""" try: guild_id = ctx.message.server.id pm = self.settings.get(guild_id, {}).get("pm_result", False) srd = self.settings.get(guild_id, {}).get("srd", False) visible_roles = ['gm', 'game master', 'dm', 'dungeon master'] if self.settings.get(guild_id, {}).get("req_dm_monster", True): visible = True if any( ro in [str(r).lower() for r in ctx.message.author.roles] for ro in visible_roles) else False else: visible = True except: visible = True pm = False srd = False self.bot.db.incr('monsters_looked_up_life') monster = await select_monster_full(ctx, name, srd=srd) embed_queue = [EmbedWithAuthor(ctx)] color = embed_queue[-1].colour embed_queue[-1].title = monster.name if not monster.srd and srd: e = EmbedWithAuthor(ctx) e.title = monster.name e.description = "Description not available." return await self.bot.say(embed=e) def safe_append(title, desc): if len(desc) < 1024: embed_queue[-1].add_field(name=title, value=desc) elif len(desc) < 2048: # noinspection PyTypeChecker # I'm adding an Embed to a list of Embeds, shut up. embed_queue.append( discord.Embed(colour=color, description=desc, title=title)) else: # noinspection PyTypeChecker embed_queue.append(discord.Embed(colour=color, title=title)) trait_all = [ desc[i:i + 2040] for i in range(0, len(desc), 2040) ] embed_queue[-1].description = trait_all[0] for t in trait_all[1:]: # noinspection PyTypeChecker embed_queue.append( discord.Embed(colour=color, description=t)) if visible: embed_queue[-1].description = monster.get_meta() if monster.traits: trait = "" for a in monster.traits: trait += f"**{a.name}:** {a.desc}\n" if trait: safe_append("Special Abilities", trait) if monster.actions: action = "" for a in monster.actions: action += f"**{a.name}:** {a.desc}\n" if action: safe_append("Actions", action) if monster.reactions: reaction = "" for a in monster.reactions: reaction += f"**{a.name}:** {a.desc}\n" if reaction: safe_append("Reactions", reaction) if monster.legactions: legendary = "" for a in monster.legactions: if a.name: legendary += f"**{a.name}:** {a.desc}\n" else: legendary += f"{a.desc}\n" if legendary: safe_append("Legendary Actions", legendary) else: hp = monster.hp ac = monster.ac size = monster.size _type = monster.race if hp < 10: hp = "Very Low" elif 10 <= hp < 50: hp = "Low" elif 50 <= hp < 100: hp = "Medium" elif 100 <= hp < 200: hp = "High" elif 200 <= hp < 400: hp = "Very High" elif 400 <= hp: hp = "Ludicrous" if ac < 6: ac = "Very Low" elif 6 <= ac < 9: ac = "Low" elif 9 <= ac < 15: ac = "Medium" elif 15 <= ac < 17: ac = "High" elif 17 <= ac < 22: ac = "Very High" elif 22 <= ac: ac = "Untouchable" languages = len(monster.languages) embed_queue[-1].description = f"{size} {_type}.\n" \ f"**AC:** {ac}.\n**HP:** {hp}.\n**Speed:** {monster.speed}\n" \ f"{monster.get_hidden_stat_array()}\n" \ f"**Languages:** {languages}\n" if monster.traits: embed_queue[-1].add_field(name="Special Abilities", value=str(len(monster.traits))) if monster.actions: embed_queue[-1].add_field(name="Actions", value=str(len(monster.actions))) if monster.reactions: embed_queue[-1].add_field(name="Reactions", value=str(len(monster.reactions))) if monster.legactions: embed_queue[-1].add_field(name="Legendary Actions", value=str(len(monster.legactions))) if monster.source == 'homebrew': embed_queue[-1].set_footer( text="Homebrew content.", icon_url="https://avrae.io/static/homebrew.png") embed_queue[0].set_thumbnail(url=monster.get_image_url()) for embed in embed_queue: if pm: await self.bot.send_message(ctx.message.author, embed=embed) else: await self.bot.say(embed=embed)
async def genChar(self, ctx, final_level, race=None, _class=None, subclass=None, background=None): loadingMessage = await self.bot.send_message(ctx.message.channel, "Generating character, please wait...") color = random.randint(0, 0xffffff) # Name Gen # DMG name gen name = self.nameGen() # Stat Gen # 4d6d1 # reroll if too low/high stats = self.genStats() await self.bot.send_message(ctx.message.author, "**Stats for {0}:** `{1}`".format(name, stats)) # Race Gen # Racial Features race = race or random.choice([r for r in c.fancyraces if r.source in ('PHB', 'VGM', 'MTF')]) 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 self.bot.send_message(ctx.message.author, embed=embed) # Class Gen # Class Features _class = _class or random.choice([cl for cl in c.classes if not 'UA' in cl.get('source')]) subclass = subclass or random.choice([s for s in _class['subclasses'] if not 'UA' in s['source']]) 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 self.bot.send_message(ctx.message.author, embed=embed) embed = EmbedWithAuthor(ctx) level_resources = {} for table in _class['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 self.bot.send_message(ctx.message.author, embed=embed) embed_queue = [EmbedWithAuthor(ctx)] num_subclass_features = 0 num_fields = 0 def inc_fields(text): 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(text) > 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) 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 self.bot.send_message(ctx.message.author, embed=embed) # Background Gen # Inventory/Trait Gen background = background or random.choice(c.backgrounds) embed = EmbedWithAuthor(ctx) embed.title = background['name'] embed.description = f"*Source: {background.get('source', 'Unknown')}*" ignored_fields = ['suggested characteristics', 'specialty', 'harrowing event'] for trait in background['trait']: if trait['name'].lower() in ignored_fields: continue text = '\n'.join(t for t in trait['text'] if t) 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 self.bot.send_message(ctx.message.author, 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']) await self.bot.edit_message(loadingMessage, out)
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 serve(self, ctx, name): # get the personal alias/snippet if self.is_alias: personal_obj = await helpers.get_personal_alias_named(ctx, name) check_coro = _servalias_before_edit else: personal_obj = await helpers.get_personal_snippet_named(ctx, name) check_coro = _servsnippet_before_edit if personal_obj is None: return await ctx.send( f"You do not have {a_or_an(self.obj_name)} named `{name}`.") await check_coro(ctx, name) # If the alias is a workshop alias we need to get the workshopCollection and set it as active. if not isinstance(personal_obj, self.personal_cls): await personal_obj.load_collection(ctx) collection = personal_obj.collection response = await confirm( ctx, f"This action will subscribe the server to the `{collection.name}` workshop collection, found at " f"<{collection.url}>. This will add {collection.alias_count} aliases and " f"{collection.snippet_count} snippets to the server. Do you want to continue? (Reply with yes/no)" ) if not response: return await ctx.send("Ok, aborting.") await collection.set_server_active( ctx) # this loads the aliases/snippets embed = EmbedWithAuthor(ctx) embed.title = f"Subscribed to {collection.name}" embed.url = collection.url embed.description = collection.description if collection.aliases: embed.add_field(name="Server Aliases", value=", ".join( sorted(a.name for a in collection.aliases))) if collection.snippets: embed.add_field(name="Server Snippets", value=", ".join( sorted(a.name for a in collection.snippets))) return await ctx.send(embed=embed) # else it's a personal alias/snippet if self.is_alias: existing_server_obj = await personal.Servalias.get_named( personal_obj.name, ctx) server_obj = personal.Servalias.new(personal_obj.name, personal_obj.code, ctx.guild.id) else: existing_server_obj = await personal.Servsnippet.get_named( personal_obj.name, ctx) server_obj = personal.Servsnippet.new(personal_obj.name, personal_obj.code, ctx.guild.id) # check if it overwrites anything if existing_server_obj is not None and not await confirm( ctx, f"There is already an existing server {self.obj_name} named `{name}`. Do you want to overwrite it? " f"(Reply with yes/no)"): return await ctx.send("Ok, aborting.") await server_obj.commit(ctx.bot.mdb) out = f'Server {self.obj_name} `{server_obj.name}` added.' \ f'```py\n{ctx.prefix}{self.obj_copy_command} {server_obj.name} {server_obj.code}\n```' if len(out) > 2000: out = f'Server {self.obj_name} `{server_obj.name}` added.' \ f'Command output too long to display.' await ctx.send(out)
async def spell(self, ctx, *, name: str): """Looks up a spell.""" try: guild_id = ctx.message.server.id pm = self.settings.get(guild_id, {}).get("pm_result", False) srd = self.settings.get(guild_id, {}).get("srd", False) except: pm = False srd = False self.bot.db.incr('spells_looked_up_life') result = await search_and_select(ctx, c.spells, name, lambda e: e['name'], return_key=True, srd=srd) result = getSpell(result) spellDesc = [] embed = EmbedWithAuthor(ctx) color = embed.colour spell = copy.copy(result) def parseschool(school): if school == "A": return "abjuration" if school == "EV": return "evocation" if school == "EN": return "enchantment" if school == "I": return "illusion" if school == "D": return "divination" if school == "N": return "necromancy" if school == "T": return "transmutation" if school == "C": return "conjuration" return school def parsespelllevel(level): if level == "0": return "cantrip" if level == "2": return level + "nd level" if level == "3": return level + "rd level" if level == "1": return level + "st level" return level + "th level" spell['school'] = parseschool(spell.get('school')) spell['ritual'] = spell.get('ritual', 'no').lower() embed.title = spell['name'] if spell.get("source") == "UAMystic": embed.description = "*{level} Mystic Talent. ({classes})*".format( **spell) else: spell['level'] = parsespelllevel(spell['level']) embed.description = "*{level} {school}. ({classes})*".format( **spell) embed.add_field(name="Casting Time", value=spell['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']) embed.add_field(name="Ritual", value=spell['ritual']) if isinstance(spell['text'], list): for a in spell["text"]: if a is '': continue spellDesc.append( a.replace("At Higher Levels: ", "**At Higher Levels:** "). replace( "This spell can be found in the Elemental Evil Player's Companion", "")) else: spellDesc.append(spell['text'].replace( "At Higher Levels: ", "**At Higher Levels:** " ).replace( "This spell can be found in the Elemental Evil Player's Companion", "")) text = '\n'.join(spellDesc) if "**At Higher Levels:** " in text: text, higher_levels = text.split("**At Higher Levels:** ", 1) elif "At Higher Levels" in text: text, higher_levels = text.split("At Higher Levels", 1) text = text.strip('*\n') higher_levels = higher_levels.strip('* \n.:') else: higher_levels = None if not spell['srd'] and srd: text = "No description available." higher_levels = '' 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]) 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: embed_queue[-1].add_field(name="At Higher Levels", value=higher_levels) for embed in embed_queue: if pm: await self.bot.send_message(ctx.message.author, embed=embed) else: await self.bot.say(embed=embed)
async def spell(self, ctx, *, name: str): """Looks up a spell.""" if name.lower().strip() == 'roscoe\'s feast': return await ctx.invoke(self.bot.get_command('patron_roscoe')) guild_settings = await self.get_settings(ctx.guild) pm = guild_settings.get("pm_result", False) srd = guild_settings.get("srd", False) self.bot.rdb.incr('spells_looked_up_life') spell, metadata = await select_spell_full(ctx, name, srd=srd, search_func=ml_spell_search, return_metadata=True) metadata['srd'] = srd metadata['homebrew'] = spell.source == 'homebrew' await self.add_training_data("spell", name, spell.name, metadata=metadata) 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 not spell.srd and srd: text = "No description available." higher_levels = '' 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]) 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: embed_queue[-1].add_field(name="At Higher Levels", value=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) for embed in embed_queue: if pm: await ctx.author.send(embed=embed) else: await ctx.send(embed=embed)
async def send_srd_error(self, ctx, data): e = EmbedWithAuthor(ctx) e.title = data['name'] e.description = "Description not available." return await self.bot.say(embed=e)
async def item_lookup(self, ctx, *, name): """Looks up an item.""" guild_settings = await self.get_settings(ctx.guild) pm = guild_settings.get("pm_result", False) srd = guild_settings.get("srd", False) self.bot.rdb.incr('items_looked_up_life') try: pack = await Pack.from_ctx(ctx) custom_items = pack.get_search_formatted_items() except NoActiveBrew: custom_items = [] choices = list(itertools.chain(c.items, custom_items)) if ctx.guild: async for servpack in ctx.bot.mdb.packs.find({"server_active": str(ctx.guild.id)}): choices.extend(Pack.from_dict(servpack).get_search_formatted_items()) def get_homebrew_formatted_name(_item): if _item.get('source') == 'homebrew': return f"{_item['name']} ({HOMEBREW_EMOJI})" return _item['name'] result, metadata = await search_and_select(ctx, choices, name, lambda e: e['name'], srd=srd, selectkey=get_homebrew_formatted_name, return_metadata=True) metadata['srd'] = srd metadata['homebrew'] = result.get('source') == 'homebrew' await self.add_training_data("item", name, result['name'], metadata=metadata) embed = EmbedWithAuthor(ctx) item = result if not item['srd'] and srd: return await self.send_srd_error(ctx, 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()}**: {c.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 c.itemprops: proptext += f"**{a.title()}**: {c.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']}") 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] + "..." field_name = "Description" for piece in [text[i:i + 1024] for i in range(0, len(text), 1024)]: embed.add_field(name=field_name, value=piece) field_name = "** **" if pm: await ctx.author.send(embed=embed) else: await ctx.send(embed=embed)