async def monster_save(self, ctx, monster_name, save_stat, *args): """Rolls a save for a monster. __Valid Arguments__ adv/dis -b [conditional bonus] -phrase [flavor text] -title [title] *note: [name] and [cname] will be replaced automatically* -thumb [thumbnail URL] -dc [dc] -rr [iterations] -h (hides name and image of monster)""" monster: Monster = await select_monster_full(ctx, monster_name) embed = discord.Embed() embed.colour = random.randint(0, 0xffffff) args = await helpers.parse_snippets(args, ctx) args = argparse(args) if not args.last('h', type_=bool): embed.set_thumbnail(url=monster.get_image_url()) checkutils.run_save(save_stat, monster, args, embed) if monster.source == 'homebrew': embeds.add_homebrew_footer(embed) await ctx.send(embed=embed) await try_delete(ctx.message)
async def item_lookup(self, ctx, *, name): """Looks up an item.""" choices = await get_item_choices(ctx, filter_by_license=False) item = await self._lookup_search3(ctx, {'magic-item': choices}, name, query_type='item') embed = EmbedWithAuthor(ctx) embed.title = item.name embed.url = item.url embed.description = item.meta if item.attunement: if item.attunement 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.attunement}", inline=False) text = trim_str(item.desc, 5500) add_fields_from_long_text(embed, "Description", text) if item.image: embed.set_thumbnail(url=item.image) embed.set_footer(text=f"Item | {item.source_str()}") if item.homebrew: add_homebrew_footer(embed) await Stats.increase_stat(ctx, "items_looked_up_life") await (await self._get_destination(ctx)).send(embed=embed)
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) 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) embed_queue[-1].set_footer(text=f"Spell | {spell.source_str()}") if spell.homebrew: add_homebrew_footer(embed_queue[-1]) if spell.image: embed_queue[0].set_thumbnail(url=spell.image) await Stats.increase_stat(ctx, "spells_looked_up_life") destination = await self._get_destination(ctx) for embed in embed_queue: await destination.send(embed=embed)
async def monster_atk(self, ctx, monster_name, atk_name=None, *, args=''): """Rolls a monster's attack. __Valid Arguments__ -t "<target>" - Sets targets for the attack. You can pass as many as needed. Will target combatants if channel is in initiative. -t "<target>|<args>" - Sets a target, and also allows for specific args to apply to them. (e.g, -t "OR1|hit" to force the attack against OR1 to hit) adv/dis -ac [target ac] -b [to hit bonus] -d [damage bonus] -d# [applies damage to the first # hits] -rr [times to reroll] -t [target] -phrase [flavor text] crit (automatically crit) -h (hides monster name, image, and rolled values) """ if atk_name is None or atk_name == 'list': return await ctx.invoke(self.monster_atk_list, monster_name) await try_delete(ctx.message) monster = await select_monster_full(ctx, monster_name) attacks = monster.attacks monster_name = monster.get_title_name() attack = await search_and_select(ctx, attacks, atk_name, lambda a: a.name) args = await helpers.parse_snippets(args, ctx) args = argparse(args) if not args.last('h', type_=bool): name = monster_name image = args.last('thumb') or monster.get_image_url() else: name = "An unknown creature" image = None embed = discord.Embed() if args.last('title') is not None: embed.title = args.last('title') \ .replace('[name]', name) \ .replace('[aname]', attack.name) else: embed.title = '{} attacks with {}!'.format(name, a_or_an(attack.name)) if image: embed.set_thumbnail(url=image) caster, targets, combat = await targetutils.maybe_combat(ctx, monster, args) await attack.automation.run(ctx, embed, caster, targets, args, combat=combat, title=embed.title) if combat: await combat.final() _fields = args.get('f') embeds.add_fields_from_args(embed, _fields) embed.colour = random.randint(0, 0xffffff) if monster.source == 'homebrew': embeds.add_homebrew_footer(embed) await ctx.send(embed=embed)
async def monster_cast(self, ctx, monster_name, spell_name, *args): """ Casts a spell as a monster. __Valid Arguments__ -i - Ignores Spellbook restrictions, for demonstrations or rituals. -l <level> - Specifies the level to cast the spell at. noconc - Ignores concentration requirements. -h - Hides rolled values. **__Save Spells__** -dc <Save DC> - Overrides the spell save DC. -save <Save type> - Overrides the spell save type. -d <damage> - Adds additional damage. pass - Target automatically succeeds save. fail - Target automatically fails save. adv/dis - Target makes save at advantage/disadvantage. **__Attack Spells__** See `!a`. **__All Spells__** -phrase <phrase> - adds flavor text. -title <title> - changes the title of the cast. Replaces [sname] with spell name. -thumb <url> - adds an image to the cast. -dur <duration> - changes the duration of any effect applied by the spell. -mod <spellcasting mod> - sets the value of the spellcasting ability modifier. int/wis/cha - different skill base for DC/AB (will not account for extra bonuses) """ await try_delete(ctx.message) monster: Monster = await select_monster_full(ctx, monster_name) args = await helpers.parse_snippets(args, ctx) args = argparse(args) if not args.last('i', type_=bool): spell = await select_spell_full( ctx, spell_name, list_filter=lambda s: s.name in monster.spellbook) else: spell = await select_spell_full(ctx, spell_name) caster, targets, combat = await targetutils.maybe_combat( ctx, monster, args) result = await spell.cast(ctx, caster, targets, args, combat=combat) # embed display embed = result['embed'] embed.colour = random.randint(0, 0xffffff) if not args.last('h', type_=bool) and 'thumb' not in args: embed.set_thumbnail(url=monster.get_image_url()) if monster.source == 'homebrew': embeds.add_homebrew_footer(embed) # save changes: combat state if combat: await combat.final() await ctx.send(embed=embed)
async def monster_atk(self, ctx, monster_name, atk_name=None, *, args=''): """Rolls a monster's attack. __Valid Arguments__ -t "<target>" - Sets targets for the attack. You can pass as many as needed. Will target combatants if channel is in initiative. -t "<target>|<args>" - Sets a target, and also allows for specific args to apply to them. (e.g, -t "OR1|hit" to force the attack against OR1 to hit) adv/dis -ac [target ac] -b [to hit bonus] -d [damage bonus] -d# [applies damage to the first # hits] -rr [times to reroll] -t [target] -phrase [flavor text] crit (automatically crit) -h (hides monster name, image, and rolled values) """ if atk_name is None or atk_name == 'list': return await self.monster_atk_list(ctx, monster_name) await try_delete(ctx.message) monster = await select_monster_full(ctx, monster_name) attacks = monster.attacks attack = await search_and_select(ctx, attacks, atk_name, lambda a: a.name) args = await helpers.parse_snippets(args, ctx) args = argparse(args) embed = discord.Embed() if not args.last('h', type_=bool): embed.set_thumbnail(url=monster.get_image_url()) caster, targets, combat = await targetutils.maybe_combat( ctx, monster, args) await attackutils.run_attack(ctx, embed, args, caster, attack, targets, combat) embed.colour = random.randint(0, 0xffffff) if monster.source == 'homebrew': embeds.add_homebrew_footer(embed) await ctx.send(embed=embed)
async def monster_check(self, ctx, monster_name, check, *args): """Rolls a check for a monster. __Valid Arguments__ *adv/dis* *-b [conditional bonus]* -phrase [flavor text] -title [title] *note: [name] and [cname] will be replaced automatically* -dc [dc] -rr [iterations] str/dex/con/int/wis/cha (different skill base; e.g. Strength (Intimidation)) -h (hides name and image of monster) An italicized argument means the argument supports ephemeral arguments - e.g. `-b1` applies a bonus to one check. """ monster: Monster = await select_monster_full(ctx, monster_name) skill_key = await search_and_select(ctx, SKILL_NAMES, check, lambda s: s) embed = discord.Embed() embed.colour = random.randint(0, 0xffffff) args = await helpers.parse_snippets(args, ctx) args = argparse(args) checkutils.run_check(skill_key, monster, args, embed) if args.last('image') is not None: embed.set_thumbnail(url=args.last('image')) elif not args.last('h', type_=bool): embed.set_thumbnail(url=monster.get_image_url()) if monster.source == 'homebrew': embeds.add_homebrew_footer(embed) await ctx.send(embed=embed) await try_delete(ctx.message)
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 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)
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 monster_save(self, ctx, monster_name, save_stat, *args): """Rolls a save for a monster. __Valid Arguments__ adv/dis -b [conditional bonus] -phrase [flavor text] -title [title] *note: [name] and [cname] will be replaced automatically* -dc [dc] -rr [iterations] -h (hides name and image of monster)""" monster: Monster = await select_monster_full(ctx, monster_name) monster_name = monster.get_title_name() try: save = monster.saves.get(save_stat) save_name = f"{verbose_stat(save_stat[:3]).title()} Save" except ValueError: return await ctx.send('That\'s not a valid save.') embed = discord.Embed() embed.colour = random.randint(0, 0xffffff) args = await scripting.parse_snippets(args, ctx) args = argparse(args) adv = args.adv(boolwise=True) b = args.join('b', '+') phrase = args.join('phrase', '\n') iterations = min(args.last('rr', 1, int), 25) dc = args.last('dc', type_=int) num_successes = 0 formatted_d20 = save.d20(base_adv=adv) if b: roll_str = f"{formatted_d20}+{b}" else: roll_str = formatted_d20 if not args.last('h', type_=bool): default_title = f'{monster_name} makes {a_or_an(save_name)}!' else: default_title = f"An unknown creature makes {a_or_an(save_name)}!" embed.title = args.last('title', '') \ .replace('[name]', monster_name) \ .replace('[sname]', save_name) \ or default_title if iterations > 1: embed.description = (f"**DC {dc}**\n" if dc else '') + ( '*' + phrase + '*' if phrase is not None else '') for i in range(iterations): result = roll(roll_str, inline=True) if dc and result.total >= dc: num_successes += 1 embed.add_field(name=f"Check {i + 1}", value=result.skeleton) if dc: embed.set_footer( text= f"{num_successes} Successes | {iterations - num_successes} Failues" ) else: result = roll(roll_str, inline=True) if dc: embed.set_footer( text="Success!" if result.total >= dc else "Failure!") embed.description = ( f"**DC {dc}**\n" if dc else '') + result.skeleton + ( '\n*' + phrase + '*' if phrase is not None else '') embeds.add_fields_from_args(embed, args.get('f')) if args.last('image') is not None: embed.set_thumbnail(url=args.last('image')) elif not args.last('h', type_=bool): embed.set_thumbnail(url=monster.get_image_url()) if monster.source == 'homebrew': embeds.add_homebrew_footer(embed) await ctx.send(embed=embed) try: await ctx.message.delete() except: pass
async def monster_check(self, ctx, monster_name, check, *args): """Rolls a check for a monster. __Valid Arguments__ adv/dis -b [conditional bonus] -phrase [flavor text] -title [title] *note: [name] and [cname] will be replaced automatically* -dc [dc] -rr [iterations] str/dex/con/int/wis/cha (different skill base; e.g. Strength (Intimidation)) -h (hides name and image of monster)""" monster: Monster = await select_monster_full(ctx, monster_name) monster_name = monster.get_title_name() skill_key = await search_and_select(ctx, SKILL_NAMES, check, lambda s: s) skill_name = camel_to_title(skill_key) embed = discord.Embed() embed.colour = random.randint(0, 0xffffff) args = await scripting.parse_snippets(args, ctx) args = argparse(args) adv = args.adv(boolwise=True) b = args.join('b', '+') phrase = args.join('phrase', '\n') iterations = min(args.last('rr', 1, int), 25) dc = args.last('dc', type_=int) num_successes = 0 skill = monster.skills[skill_key] mod = skill.value formatted_d20 = skill.d20(base_adv=adv, base_only=True) if 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 = mod - monster.get_mod( SKILL_MAP[skill_key]) + monster.get_mod(base) skill_name = f"{verbose_stat(base)} ({skill_name})" skill_name = skill_name.title() if not args.last('h', type_=bool): default_title = '{} makes {} check!'.format( monster_name, a_or_an(skill_name)) else: default_title = f"An unknown creature makes {a_or_an(skill_name)} check!" if b is not None: roll_str = formatted_d20 + '{:+}'.format(mod) + '+' + b else: roll_str = formatted_d20 + '{:+}'.format(mod) embed.title = args.last('title', '') \ .replace('[name]', monster_name) \ .replace('[cname]', skill_name) \ or default_title if iterations > 1: embed.description = (f"**DC {dc}**\n" if dc else '') + ( '*' + phrase + '*' if phrase is not None else '') for i in range(iterations): result = roll(roll_str, inline=True) if dc and result.total >= dc: num_successes += 1 embed.add_field(name=f"Check {i + 1}", value=result.skeleton) if dc: embed.set_footer( text= f"{num_successes} Successes | {iterations - num_successes} Failues" ) else: result = roll(roll_str, inline=True) if dc: embed.set_footer( text="Success!" if result.total >= dc else "Failure!") embed.description = ( f"**DC {dc}**\n" if dc else '') + result.skeleton + ( '\n*' + phrase + '*' if phrase is not None else '') embeds.add_fields_from_args(embed, args.get('f')) if args.last('image') is not None: embed.set_thumbnail(url=args.last('image')) elif not args.last('h', type_=bool): embed.set_thumbnail(url=monster.get_image_url()) if monster.source == 'homebrew': embeds.add_homebrew_footer(embed) await ctx.send(embed=embed) try: await ctx.message.delete() except: pass
async def monster_atk(self, ctx, monster_name, atk_name=None, *, args=''): """Rolls a monster's attack. __Valid Arguments__ adv/dis -ac [target ac] -b [to hit bonus] -d [damage bonus] -d# [applies damage to the first # hits] -rr [times to reroll] -t [target] -phrase [flavor text] crit (automatically crit) -h (hides monster name, image, and attack details)""" if atk_name is None or atk_name == 'list': return await ctx.invoke(self.monster_atk_list, monster_name) try: await ctx.message.delete() except: pass monster = await select_monster_full(ctx, monster_name) attacks = monster.attacks monster_name = monster.get_title_name() attack = await search_and_select(ctx, attacks, atk_name, lambda a: a['name']) args = await scripting.parse_snippets(args, ctx) args = argparse(args) if not args.last('h', type_=bool): name = monster_name image = args.get('image') or monster.get_image_url() else: name = "An unknown creature" image = None attack = Attack.from_old(attack) embed = discord.Embed() if args.last('title') is not None: embed.title = args.last('title') \ .replace('[name]', name) \ .replace('[aname]', attack.name) else: embed.title = '{} attacks with {}!'.format(name, a_or_an(attack.name)) if image: embed.set_thumbnail(url=image) caster, targets, combat = await targetutils.maybe_combat( ctx, monster, args.get('t')) await Automation.from_attack(attack).run(ctx, embed, caster, targets, args, combat=combat, title=embed.title) if combat: await combat.final() _fields = args.get('f') embeds.add_fields_from_args(embed, _fields) embed.colour = random.randint(0, 0xffffff) if monster.source == 'homebrew': embeds.add_homebrew_footer(embed) await ctx.send(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 __Valid Arguments__ -h - Shows the obfuscated stat block, even if you can see the full stat block.""" guild_settings = await self.get_settings(ctx.guild) pm = guild_settings.get("pm_result", False) pm_dm = guild_settings.get("pm_dm", False) req_dm_monster = guild_settings.get("req_dm_monster", True) visible_roles = {'gm', 'game master', 'dm', 'dungeon master'} if req_dm_monster and ctx.guild: visible = True if visible_roles.intersection( set(str(r).lower() for r in ctx.author.roles)) else False else: visible = True # #817 -h arg for monster lookup if name.endswith(' -h'): name = name[:-3] visible = False choices = await get_monster_choices(ctx, filter_by_license=False) monster = await self._lookup_search3(ctx, {'monster': choices}, name) embed_queue = [EmbedWithAuthor(ctx)] color = embed_queue[-1].colour embed_queue[-1].title = monster.name embed_queue[-1].url = monster.url def safe_append(title, desc): if len(desc) < 1024: embed_queue[-1].add_field(name=title, value=desc, inline=False) 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 = '\n\n'.join(f"**{a.name}:** {a.desc}" for a in monster.traits) if trait: safe_append("Special Abilities", trait) if monster.actions: action = '\n\n'.join(f"**{a.name}:** {a.desc}" for a in monster.actions) if action: safe_append("Actions", action) if monster.reactions: reaction = '\n\n'.join(f"**{a.name}:** {a.desc}" for a in monster.reactions) if reaction: safe_append("Reactions", reaction) if monster.legactions: proper_name = f'The {monster.name}' if not monster.proper else monster.name legendary = [ f"{proper_name} can take {monster.la_per_round} legendary actions, choosing from " f"the options below. Only one legendary action can be used at a time and only at the " f"end of another creature's turn. {proper_name} regains spent legendary actions at " f"the start of its turn." ] for a in monster.legactions: if a.name: legendary.append(f"**{a.name}:** {a.desc}") else: legendary.append(a.desc) if legendary: safe_append("Legendary Actions", '\n\n'.join(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))) embed_queue[-1].set_footer(text=f"Creature | {monster.source_str()}") if monster.homebrew: add_homebrew_footer(embed_queue[-1]) embed_queue[0].set_thumbnail(url=monster.get_image_url()) await Stats.increase_stat(ctx, "monsters_looked_up_life") for embed in embed_queue: if pm or (visible and pm_dm and req_dm_monster): await ctx.author.send(embed=embed) else: await ctx.send(embed=embed)
async def monster_atk(self, ctx, monster_name, atk_name=None, *, args=''): """Rolls a monster's attack. __Valid Arguments__ -t "<target>" - Sets targets for the attack. You can pass as many as needed. Will target combatants if channel is in initiative. -t "<target>|<args>" - Sets a target, and also allows for specific args to apply to them. (e.g, -t "OR1|hit" to force the attack against OR1 to hit) *adv/dis* - Advantage or Disadvantage *ea* - Elven Accuracy double advantage -ac <target ac> - overrides target AC *-b* <to hit bonus> - adds a bonus to hit -criton <num> - a number to crit on if rolled on or above *-d* <damage bonus> - adds a bonus to damage *-c* <damage bonus on crit> - adds a bonus to crit damage -rr <times> - number of times to roll the attack against each target *-mi <value>* - minimum value of each die on the damage roll *-resist* <damage resistance> *-immune* <damage immunity> *-vuln* <damage vulnerability> *-neutral* <damage type> - ignores this damage type in resistance calculations -dtype <damage type> - replaces all damage types with this damage type -dtype <old>new> - replaces all of one damage type with another (e.g. `-dtype fire>cold`) *hit* - automatically hits *miss* - automatically misses *crit* - automatically crits if hit *max* - deals max damage *magical* - makes the damage type magical -h - hides name, rolled values, and monster details -phrase <text> - adds flavour text -title <title> - changes the result title *note: `[name]` and `[aname]` will be replaced automatically* -thumb <url> - adds flavour image -f "Field Title|Field Text" - see `!help embed` <user snippet> - see `!help snippet` An italicized argument means the argument supports ephemeral arguments - e.g. `-d1` applies damage to the first hit, `-b1` applies a bonus to one attack, and so on. """ if atk_name is None or atk_name == 'list': return await self.monster_atk_list(ctx, monster_name) await try_delete(ctx.message) monster = await select_monster_full(ctx, monster_name) attacks = monster.attacks attack = await search_and_select(ctx, attacks, atk_name, lambda a: a.name) args = await helpers.parse_snippets(args, ctx) args = argparse(args) embed = discord.Embed() if not args.last('h', type_=bool): embed.set_thumbnail(url=monster.get_image_url()) caster, targets, combat = await targetutils.maybe_combat(ctx, monster, args) await attackutils.run_attack(ctx, embed, args, caster, attack, targets, combat) embed.colour = random.randint(0, 0xffffff) if monster.homebrew: embeds.add_homebrew_footer(embed) await ctx.send(embed=embed)