async def spellbook(self, ctx): """Commands to display a character's known spells and metadata.""" character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.description = f"{character.name} knows {len(character.spellbook.spells)} spells." embed.add_field(name="DC", value=str(character.spellbook.dc)) embed.add_field(name="Spell Attack Bonus", value=str(character.spellbook.sab)) embed.add_field(name="Spell Slots", value=character.spellbook.slots_str() or "None") # dynamic help flags flag_show_multiple_source_help = False flag_show_homebrew_help = False spells_known = collections.defaultdict(lambda: []) choices = await get_spell_choices(ctx) for spell_ in character.spellbook.spells: results, strict = search(choices, spell_.name, lambda sp: sp.name, strict=True) if not strict: if len(results) > 1: spells_known['unknown'].append( f"*{spell_.name} ({'*' * len(results)})*") flag_show_multiple_source_help = True else: spells_known['unknown'].append(f"*{spell_.name}*") flag_show_homebrew_help = True else: spell = results if spell.source == 'homebrew': formatted = f"*{spell.name}*" flag_show_homebrew_help = True else: formatted = spell.name spells_known[str(spell.level)].append(formatted) level_name = { '0': 'Cantrips', '1': '1st Level', '2': '2nd Level', '3': '3rd Level', '4': '4th Level', '5': '5th Level', '6': '6th Level', '7': '7th Level', '8': '8th Level', '9': '9th Level' } for level, spells in sorted(list(spells_known.items()), key=lambda k: k[0]): if spells: spells.sort() embed.add_field(name=level_name.get(level, "Unknown"), value=', '.join(spells), inline=False) # dynamic help footer_out = [] if flag_show_homebrew_help: footer_out.append( "An italicized spell indicates that the spell is homebrew.") if flag_show_multiple_source_help: footer_out.append( "Asterisks after a spell indicates that the spell is being provided by multiple sources." ) if footer_out: embed.set_footer(text=' '.join(footer_out)) await ctx.send(embed=embed)
async def customcounter(self, ctx, name=None, *, modifier=None): """Commands to implement custom counters. When called on its own, if modifier is supplied, increases the counter *name* by *modifier*. If modifier is not supplied, prints the value and metadata of the counter *name*. """ if name is None: return await self.customcounter_summary(ctx) character: Character = await Character.from_ctx(ctx) counter = await character.select_consumable(ctx, name) cc_embed_title = counter.title if counter.title is not None else counter.name # replace [name] in title cc_embed_title = cc_embed_title.replace('[name]', character.name) if modifier is None: # display value counter_display_embed = EmbedWithCharacter(character) counter_display_embed.add_field(name=counter.name, value=counter.full_str()) if counter.desc: counter_display_embed.add_field(name='Description', value=counter.desc, inline=False) return await ctx.send(embed=counter_display_embed) operator = None if ' ' in modifier: m = modifier.split(' ') operator = m[0] modifier = m[-1] change = '' old_value = counter.value try: modifier = int(modifier) except ValueError: return await ctx.send( f"Could not modify counter: {modifier} is not a number") result_embed = EmbedWithCharacter(character) if not operator or operator == 'mod': new_value = counter.value + modifier elif operator == 'set': new_value = modifier else: return await ctx.send("Invalid operator. Use mod or set.") counter.set(new_value) await character.commit(ctx) delta = f"({counter.value - old_value:+})" if new_value - counter.value: # we overflowed somewhere out = f"{str(counter)} {delta}\n({abs(new_value - counter.value)} overflow)" else: out = f"{str(counter)} {delta}" result_embed.add_field(name=cc_embed_title, value=out) if counter.desc: result_embed.add_field(name='Description', value=counter.desc, inline=False) await try_delete(ctx.message) await ctx.send(embed=result_embed)
async def _rest(self, ctx, rest_type, *args): """ Runs a rest. :param ctx: The Context. :param character: The Character. :param rest_type: "long", "short", "all" :param args: a list of args. """ character: Character = await Character.from_ctx(ctx) old_hp = character.hp old_slots = { lvl: character.spellbook.get_slots(lvl) for lvl in range(1, 10) } embed = EmbedWithCharacter(character, name=False) if rest_type == 'long': reset_counters = character.long_rest() embed.title = f"{character.name} took a Long Rest!" elif rest_type == 'short': reset_counters = character.short_rest() embed.title = f"{character.name} took a Short Rest!" elif rest_type == 'all': reset_counters = character.reset_all_consumables() embed.title = f"{character.name} reset all counters!" else: raise ValueError(f"Invalid rest type: {rest_type}") if '-h' in args: values = ', '.join( set(ctr.name for ctr, _ in reset_counters) | {"Hit Points", "Death Saves", "Spell Slots"}) embed.add_field(name="Reset Values", value=values) else: # hp hp_delta = character.hp - old_hp hp_delta_str = "" if hp_delta: hp_delta_str = f" ({hp_delta:+})" embed.add_field(name="Hit Points", value=f"{character.hp_str()}{hp_delta_str}") # slots slots_out = [] slots_delta = { lvl: character.spellbook.get_slots(lvl) - old_slots[lvl] for lvl in range(1, 10) } for lvl in range(1, 10): if character.spellbook.get_max_slots(lvl): if slots_delta[lvl]: slots_out.append( f"{character.spellbook.slots_str(lvl)} ({slots_delta[lvl]:+})" ) else: slots_out.append(character.spellbook.slots_str(lvl)) if slots_out: embed.add_field(name="Spell Slots", value='\n'.join(slots_out)) # ccs counters_out = [] for counter, result in reset_counters: if result.new_value != result.old_value: counters_out.append( f"{counter.name}: {str(counter)} ({result.delta})") else: counters_out.append(f"{counter.name}: {str(counter)}") if counters_out: embed.add_field(name="Reset Counters", value='\n'.join(counters_out)) await character.commit(ctx) await ctx.send(embed=embed)
async def spellbook(self, ctx): """Commands to display a character's known spells and metadata.""" character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.description = f"{character.name} knows {len(character.spellbook.spells)} spells." embed.add_field(name="DC", value=str(character.spellbook.dc)) embed.add_field(name="Spell Attack Bonus", value=str(character.spellbook.sab)) embed.add_field(name="Spell Slots", value=character.get_remaining_slots_str() or "None") spells_known = collections.defaultdict(lambda: []) choices = await get_spell_choices(ctx) for spell_ in character.spellbook.spells: spell = await get_castable_spell(ctx, spell_.name, choices, strict=True) if spell is None: spells_known['unknown'].append(f"*{spell_.name}*") else: if spell.source == 'homebrew': formatted = f"*{spell.name}*" else: formatted = spell.name spells_known[str(spell.level)].append(formatted) level_name = {'0': 'Cantrips', '1': '1st Level', '2': '2nd Level', '3': '3rd Level', '4': '4th Level', '5': '5th Level', '6': '6th Level', '7': '7th Level', '8': '8th Level', '9': '9th Level'} for level, spells in sorted(list(spells_known.items()), key=lambda k: k[0]): if spells: spells.sort() embed.add_field(name=level_name.get(level, "Unknown"), value=', '.join(spells)) await ctx.send(embed=embed)
async def join(self, ctx, *, args: str = ''): """Adds the current active character to combat. A character must be loaded through the SheetManager module first. Args: adv/dis -b [conditional bonus] -phrase [flavor text] -p [init value] -h (same as !init add) --group (same as !init add)""" char = await Character.from_ctx(ctx) character = char.character # if char.get_combat_id(): # return await ctx.send(f"This character is already in a combat. " # f"Please leave combat in <#{char.get_combat_id()}> first.\n" # f"If this seems like an error, please `!update` your character sheet.") # we just ignore this for now. # I'll figure out a better solution when I actually need it skills = character.get('skills') if skills is None: return await ctx.send('You must update your character sheet first.' ) skill = 'initiative' embed = EmbedWithCharacter(char, False) embed.colour = char.get_color() skill_effects = character.get('skill_effects', {}) args += ' ' + skill_effects.get(skill, '') # dicecloud v7 - autoadv args = shlex.split(args) args = argparse(args) adv = args.adv() b = args.join('b', '+') or None p = args.last('p', type_=int) phrase = args.join('phrase', '\n') or None if p is None: if b: bonus = '{:+}'.format(skills[skill]) + '+' + b check_roll = roll('1d20' + bonus, adv=adv, inline=True) else: bonus = '{:+}'.format(skills[skill]) check_roll = roll('1d20' + bonus, adv=adv, inline=True) embed.title = '{} makes an Initiative check!'.format( char.get_name()) embed.description = check_roll.skeleton + ( '\n*' + phrase + '*' if phrase is not None else '') init = check_roll.total else: init = p bonus = 0 embed.title = "{} already rolled initiative!".format( char.get_name()) embed.description = "Placed at initiative `{}`.".format(init) group = args.last('group') controller = str(ctx.author.id) private = args.last('h', type_=bool) bonus = roll(bonus).total combat = await Combat.from_ctx(ctx) me = await PlayerCombatant.from_character(char.get_name(), controller, init, bonus, char.get_ac(), private, char.get_resists(), ctx, combat, char.id, str(ctx.author.id), char) if combat.get_combatant(char.get_name()) is not None: await ctx.send("Combatant already exists.") return if group is None: combat.add_combatant(me) embed.set_footer(text="Added to combat!") else: grp = combat.get_group(group, create=init) grp.add_combatant(me) embed.set_footer(text=f"Joined group {grp.name}!") await combat.final() await ctx.send(embed=embed) char.join_combat(str(ctx.channel.id)) await char.commit(ctx)
async def check(self, ctx, check, *args): """Rolls a check for your current active character. __Valid Arguments__ adv/dis -b [conditional bonus] -mc [minimum roll] -phrase [flavor text] -title [title] *note: [charname] and [cname] will be replaced automatically* -dc [dc] -rr [iterations] str/dex/con/int/wis/cha (different skill base; e.g. Strength (Intimidation)) """ char: Character = await Character.from_ctx(ctx) skill_key = await search_and_select(ctx, SKILL_NAMES, check, lambda s: s) skill_name = camel_to_title(skill_key) embed = EmbedWithCharacter(char, False) skill = char.skills[skill_key] args = await self.new_arg_stuff(args, ctx, char) # advantage adv = args.adv(boolwise=True) # roll bonus b = args.join('b', '+') # phrase phrase = args.join('phrase', '\n') # num rolls iterations = min(args.last('rr', 1, int), 25) # dc dc = args.last('dc', type_=int) # reliable talent (#654) rt = char.get_setting('talent', 0) and skill.prof >= 1 mc = args.last('mc') or 10 * rt # halfling luck ro = char.get_setting('reroll') num_successes = 0 mod = skill.value formatted_d20 = skill.d20(base_adv=adv, reroll=ro, min_val=mc, base_only=True) if 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 = mod - char.get_mod(SKILL_MAP[skill_key]) + char.get_mod(base) skill_name = f"{verbose_stat(base)} ({skill_name})" if b is not None: roll_str = f"{formatted_d20}{mod:+}+{b}" else: roll_str = f"{formatted_d20}{mod:+}" if args.last('title'): embed.title = args.last('title', '') \ .replace('[charname]', char.name) \ .replace('[cname]', skill_name) else: embed.title = f'{char.name} makes {a_or_an(skill_name)} check!' 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} Failures") 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')) await ctx.send(embed=embed) try: await ctx.message.delete() except: pass
async def attack(self, ctx, atk_name=None, *, args: str = ''): """Rolls an attack for the current active character. __Valid Arguments__ adv/dis adv#/dis# (applies adv to the first # attacks) ea (Elven Accuracy double advantage) -ac [target ac] -t [target] -b [to hit bonus] -criton [a number to crit on if rolled on or above] -d [damage bonus] -d# [applies damage to the first # hits] -c [damage bonus on crit] -rr [times to reroll] -mi [minimum weapon dice roll] -resist [damage resistance] -immune [damage immunity] -vuln [damage vulnerability] -neutral [damage non-resistance] hit (automatically hits) miss (automatically misses) crit (automatically crit) max (deals max damage) -phrase [flavor text] -title [title] *note: [charname], [aname], and [target] will be replaced automatically* -f "Field Title|Field Text" (see !embed) -h (hides attack details) [user snippet]""" if atk_name is None: return await ctx.invoke(self.attack_list) char: Character = await Character.from_ctx(ctx) attack = await search_and_select(ctx, char.attacks, atk_name, lambda a: a.name) args = await self.new_arg_stuff(args, ctx, char) args['name'] = char.name args['criton'] = args.last('criton') or char.get_setting('criton', 20) args['reroll'] = char.get_setting('reroll', 0) args['critdice'] = char.get_setting('critdice', 0) args['crittype'] = char.get_setting('crittype', 'default') result = sheet_attack(attack.to_old(), args, EmbedWithCharacter(char, name=False)) embed = result['embed'] if args.last('h', type_=bool): try: await ctx.author.send(embed=result['full_embed']) except: pass _fields = args.get('f') embeds.add_fields_from_args(embed, _fields) await ctx.send(embed=embed) try: await ctx.message.delete() except: pass
async def customcounter(self, ctx, name=None, *, modifier=None): """Commands to implement custom counters. If a modifier is not supplied, prints the value and metadata of the counter *name*. Otherwise, changes the counter *name* by *modifier*. Supports dice. The following can be put after the counter *name* to change how the *modifier* is applied: `mod` - Add *modifier* counter value `set` - Sets the counter value to *modifier* *Ex:* `!cc Test 1` `!cc Test -2*2d4` `!cc Test set 1d4` """ if name is None: return await self.customcounter_summary(ctx) character: Character = await Character.from_ctx(ctx) counter = await character.select_consumable(ctx, name) cc_embed_title = counter.title if counter.title is not None else counter.name # replace [name] in title cc_embed_title = cc_embed_title.replace('[name]', character.name) if modifier is None: # display value counter_display_embed = EmbedWithCharacter(character) counter_display_embed.add_field(name=counter.name, value=counter.full_str()) if counter.desc: counter_display_embed.add_field(name='Description', value=counter.desc, inline=False) return await ctx.send(embed=counter_display_embed) operator = None if ' ' in modifier: m = modifier.split(' ') operator = m[0] modifier = m[-1] roll_text = '' try: result = int(modifier) except ValueError: try: # if we're not a number, are we dice roll_result = d20.roll(str(modifier)) result = roll_result.total roll_text = f"\nRoll: {roll_result}" except d20.RollSyntaxError: raise InvalidArgument( f"Could not modify counter: {modifier} cannot be interpreted as a number or dice string.") old_value = counter.value result_embed = EmbedWithCharacter(character) if not operator or operator == 'mod': new_value = counter.value + result elif operator == 'set': new_value = result else: return await ctx.send("Invalid operator. Use mod or set.") counter.set(new_value) await character.commit(ctx) delta = f"({counter.value - old_value:+})" out = f"{str(counter)} {delta}{roll_text}" if new_value - counter.value: # we overflowed somewhere out += f"\n({abs(new_value - counter.value)} overflow)" result_embed.add_field(name=cc_embed_title, value=out) if counter.desc: result_embed.add_field(name='Description', value=counter.desc, inline=False) await try_delete(ctx.message) await ctx.send(embed=result_embed)
async def save(self, ctx, skill, *args): """Rolls a save for your current active character. __Valid Arguments__ adv/dis -b [conditional bonus] -phrase [flavor text] -title [title] *note: [charname] and [sname] will be replaced automatically* -image [image URL] -dc [dc] (does not apply to Death Saves) -rr [iterations] (does not apply to Death Saves)""" if skill == 'death': ds_cmd = self.bot.get_command('game deathsave') if ds_cmd is None: return await ctx.send("Error: GameTrack cog not loaded.") return await ctx.invoke(ds_cmd, *args) char: Character = await Character.from_ctx(ctx) try: save = char.saves.get(skill) except ValueError: return await ctx.send('That\'s not a valid save.') embed = EmbedWithCharacter(char, name=False) args = await self.new_arg_stuff(args, ctx, char) 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, reroll=char.get_setting('reroll')) if b: roll_str = f"{formatted_d20}+{b}" else: roll_str = formatted_d20 save_name = f"{verbose_stat(skill[:3]).title()} Save" if args.last('title'): embed.title = args.last('title', '') \ .replace('[charname]', char.name) \ .replace('[sname]', save_name) else: embed.title = f'{char.name} makes {a_or_an(save_name)}!' 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"Save {i + 1}", value=result.skeleton) if dc: embed.set_footer(text=f"{num_successes} Successes | {iterations - num_successes} Failures") 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')) await ctx.send(embed=embed) try: await ctx.message.delete() except: pass
async def _old_cast(self, ctx, spell_name, args): spell = getSpell(spell_name) self.bot.rdb.incr('spells_looked_up_life') if spell is None: return await self.bot.say("Spell not found.", delete_after=15) if spell.get('source') == "UAMystic": return await self.bot.say("Mystic talents are not supported.") char = await Character.from_ctx(ctx) args = await scripting.parse_snippets(args, ctx) args = await char.parse_cvars(args, ctx) args = shlex.split(args) args = argparse(args) can_cast = True spell_level = int(spell.get('level', 0)) cast_level = args.last('l', spell_level, int) if not spell_level <= cast_level <= 9: return await self.bot.say("Invalid spell level.") # make sure we can cast it if not char.get_remaining_slots( cast_level) > 0 and spell_name in char.get_spell_list(): can_cast = False if args.last('i', type_=bool): can_cast = True if not can_cast: embed = EmbedWithCharacter(char) embed.title = "Cannot cast spell!" embed.description = "Not enough spell slots remaining, or spell not in known spell list!\n" \ "Use `!game longrest` to restore all spell slots, or pass `-i` to ignore restrictions." if cast_level > 0: embed.add_field(name="Spell Slots", value=char.get_remaining_slots_str(cast_level)) return await self.bot.say(embed=embed) if len(args) == 0: rolls = spell.get('roll', None) if isinstance(rolls, list): rolls = '\n'.join(rolls) \ .replace('SPELL', str(char.get_spell_ab() - char.get_prof_bonus())) \ .replace('PROF', str(char.get_prof_bonus())) rolls = rolls.split('\n') out = "**{} casts {}:** ".format( char.get_name(), spell['name']) + '\n'.join( roll(r, inline=True).skeleton for r in rolls) elif rolls is not None: rolls = rolls \ .replace('SPELL', str(char.get_spell_ab() - char.get_prof_bonus())) \ .replace('PROF', str(char.get_prof_bonus())) out = "**{} casts {}:** ".format( char.get_name(), spell['name']) + roll( rolls, inline=True).skeleton else: out = "**{} casts {}!** ".format(char.get_name(), spell['name']) else: rolls = args.get('r') roll_results = "" for r in rolls: res = roll(r, inline=True) if res.total is not None: roll_results += res.result + '\n' else: roll_results += "**Effect:** " + r out = "**{} casts {}:**\n".format(char.get_name(), spell['name']) + roll_results if not args.last('i', type_=bool): char.use_slot(cast_level) if cast_level > 0: out += f"\n**Remaining Spell Slots**: {char.get_remaining_slots_str(cast_level)}" out = "Spell not supported by new cast, falling back to old cast.\n" + out await char.commit(ctx) # make sure we save changes await self.bot.say(out) spell_cmd = self.bot.get_command('spell') if spell_cmd is None: return await self.bot.say("Lookup cog not loaded.") await ctx.invoke(spell_cmd, name=spell['name'])
async def spellbook(self, ctx, *args): """ Commands to display a character's known spells and metadata. __Valid Arguments__ all - Display all of a character's known spells, including unprepared ones. """ await ctx.trigger_typing() character: Character = await Character.from_ctx(ctx) ep = embeds.EmbedPaginator(EmbedWithCharacter(character)) ep.add_field(name="DC", value=str(character.spellbook.dc), inline=True) ep.add_field(name="Spell Attack Bonus", value=str(character.spellbook.sab), inline=True) ep.add_field(name="Spell Slots", value=character.spellbook.slots_str() or "None", inline=True) show_unprepared = 'all' in args known_count = len(character.spellbook.spells) prepared_count = sum(1 for spell in character.spellbook.spells if spell.prepared) if known_count == prepared_count: ep.add_description(f"{character.name} knows {known_count} spells.") else: ep.add_description(f"{character.name} has {prepared_count} spells prepared and knows {known_count} spells.") # dynamic help flags flag_show_multiple_source_help = False flag_show_homebrew_help = False flag_show_prepared_help = False flag_show_prepared_underline_help = False spells_known = collections.defaultdict(lambda: []) choices = await get_spell_choices(ctx) for sb_spell in character.spellbook.spells: if not (sb_spell.prepared or show_unprepared): flag_show_prepared_help = True continue # homebrew / multisource formatting results, strict = search(choices, sb_spell.name, lambda sp: sp.name, strict=True) if not strict: known_level = 'unknown' if len(results) > 1: formatted = f"*{sb_spell.name} ({'*' * len(results)})*" flag_show_multiple_source_help = True else: formatted = f"*{sb_spell.name}*" flag_show_homebrew_help = True else: spell = results known_level = str(spell.level) if spell.homebrew: formatted = f"*{spell.name}*" flag_show_homebrew_help = True else: formatted = spell.name # prepared formatting if show_unprepared and sb_spell.prepared: formatted = f"__{formatted}__" flag_show_prepared_underline_help = True spells_known[known_level].append(formatted) level_name = { '0': 'Cantrips', '1': '1st Level', '2': '2nd Level', '3': '3rd Level', '4': '4th Level', '5': '5th Level', '6': '6th Level', '7': '7th Level', '8': '8th Level', '9': '9th Level' } for level, spells in sorted(list(spells_known.items()), key=lambda k: k[0]): if spells: spells.sort(key=lambda s: s.lstrip('*_')) ep.add_field(name=level_name.get(level, "Unknown"), value=', '.join(spells), inline=False) # dynamic help footer_out = [] if flag_show_homebrew_help: footer_out.append("An italicized spell indicates that the spell is homebrew.") if flag_show_multiple_source_help: footer_out.append("Asterisks after a spell indicates that the spell is being provided by multiple sources.") if flag_show_prepared_help: footer_out.append(f"Unprepared spells were not shown. Use \"{ctx.prefix}spellbook all\" to view them!") if flag_show_prepared_underline_help: footer_out.append("Prepared spells are marked with an underline.") if footer_out: ep.set_footer(value=' '.join(footer_out)) await ep.send_to(ctx)
async def cast(self, ctx, spell_name, *, args=''): """Casts a spell. __Valid Arguments:__ -i - Ignores Spellbook restrictions, for demonstrations or rituals. -l [level] - Specifies the level to cast the spell at. **__Save Spells__** -dc [Save DC] - Default: Pulls a cvar called `dc`. -save [Save type] - Default: The spell's default save. -d [damage] - adds additional damage. **__Attack Spells__** See `!a`. **__All Spells__** -phrase [phrase] - adds flavor text.""" try: await self.bot.delete_message(ctx.message) except: pass char = None if not '-i' in args: char = await Character.from_ctx(ctx) spell_name = await searchCharacterSpellName(spell_name, ctx, char) else: spell_name = await searchSpellNameFull(spell_name, ctx) if spell_name is None: return spell = strict_search(c.autospells, 'name', spell_name) if spell is None: return await self._old_cast(ctx, spell_name, args) # fall back to old cast if not char: char = await Character.from_ctx(ctx) args = await scripting.parse_snippets(args, ctx) args = await char.parse_cvars(args, ctx) args = shlex.split(args) args = argparse(args) can_cast = True spell_level = int(spell.get('level', 0)) cast_level = args.last('l', spell_level, int) if not spell_level <= cast_level <= 9: return await self.bot.say("Invalid spell level.") # make sure we can cast it if not char.get_remaining_slots( cast_level) > 0 and spell_name in char.get_spell_list(): can_cast = False if args.last('i', type_=bool): can_cast = True if not can_cast: embed = EmbedWithCharacter(char) embed.title = "Cannot cast spell!" embed.description = "Not enough spell slots remaining, or spell not in known spell list!\n" \ "Use `!game longrest` to restore all spell slots, or pass `-i` to ignore restrictions." if cast_level > 0: embed.add_field(name="Spell Slots", value=char.get_remaining_slots_str(cast_level)) return await self.bot.say(embed=embed) args['l'] = [cast_level] args['name'] = [char.get_name()] args['dc'] = [args.get('dc', [char.get_save_dc()])[-1]] args['casterlevel'] = [char.get_level()] args['crittype'] = [char.get_setting('crittype', 'default')] args['ab'] = [char.get_spell_ab()] args['SPELL'] = [ str( char.evaluate_cvar("SPELL") or (char.get_spell_ab() - char.get_prof_bonus())) ] result = sheet_cast(spell, args, EmbedWithCharacter(char, name=False)) embed = result['embed'] _fields = args.get('f') if type(_fields) == list: for f in _fields: title = f.split('|')[0] if '|' in f else '\u200b' value = "|".join(f.split('|')[1:]) if '|' in f else f embed.add_field(name=title, value=value) if not args.last('i', type_=bool): char.use_slot(cast_level) if cast_level > 0: embed.add_field(name="Spell Slots", value=char.get_remaining_slots_str(cast_level)) await char.commit(ctx) # make sure we save changes await self.bot.say(embed=embed)
async def customcounter(self, ctx, name=None, *, modifier=None): """Commands to implement custom counters. When called on its own, if modifier is supplied, increases the counter *name* by *modifier*. If modifier is not supplied, prints the value and metadata of the counter *name*.""" if name is None: return await ctx.invoke(self.bot.get_command("customcounter list")) character = await Character.from_ctx(ctx) sel = await character.select_consumable(ctx, name) if sel is None: return await self.bot.say("Selection timed out or was cancelled.") name = sel[0] counter = sel[1] assert character is not None assert counter is not None if modifier is None: # display value counterDisplayEmbed = EmbedWithCharacter(character) val = self._get_cc_value(character, counter) counterDisplayEmbed.add_field(name=name, value=val) return await self.bot.say(embed=counterDisplayEmbed) operator = None if ' ' in modifier: m = modifier.split(' ') operator = m[0] modifier = m[-1] try: modifier = int(modifier) except ValueError: return await self.bot.say( f"Could not modify counter: {modifier} is not a number") resultEmbed = EmbedWithCharacter(character) if not operator or operator == 'mod': consValue = int(counter.get('value', 0)) newValue = consValue + modifier elif operator == 'set': newValue = modifier else: return await self.bot.say("Invalid operator. Use mod or set.") try: character.set_consumable(name, newValue) await character.commit(ctx) _max = self._get_cc_max(character, counter) actualValue = int(character.get_consumable(name).get('value', 0)) if counter.get('type') == 'bubble': assert _max not in ('N/A', None) numEmpty = _max - counter.get('value', 0) filled = '\u25c9' * counter.get('value', 0) empty = '\u3007' * numEmpty out = f"{filled}{empty}" else: out = f"{counter.get('value', 0)}" if (not _max in (None, 'N/A')) and not counter.get('type') == 'bubble': resultEmbed.description = f"**__{name}__**\n{out}/{_max}" else: resultEmbed.description = f"**__{name}__**\n{out}" if newValue - actualValue: resultEmbed.description += f"\n({abs(newValue - actualValue)} overflow)" except CounterOutOfBounds: resultEmbed.description = f"Could not modify counter: new value out of bounds" try: await self.bot.delete_message(ctx.message) except: pass await self.bot.say(embed=resultEmbed)
async def spellbook(self, ctx): """Commands to display a character's known spells and metadata.""" character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.description = f"{character.get_name()} knows {len(character.get_spell_list())} spells." embed.add_field(name="DC", value=str(character.get_save_dc())) embed.add_field(name="Spell Attack Bonus", value=str(character.get_spell_ab())) embed.add_field(name="Spell Slots", value=character.get_remaining_slots_str() or "None") spells_known = {} for spell_name in character.get_spell_list(): spell = strict_search(c.spells, 'name', spell_name) if spell is None: continue spells_known[spell['level']] = spells_known.get( spell['level'], []) + [spell_name] level_name = { '0': 'Cantrips', '1': '1st Level', '2': '2nd Level', '3': '3rd Level', '4': '4th Level', '5': '5th Level', '6': '6th Level', '7': '7th Level', '8': '8th Level', '9': '9th Level' } for level, spells in sorted(list(spells_known.items()), key=lambda k: k[0]): if spells: embed.add_field(name=level_name.get(level, "Unknown Level"), value=', '.join(spells)) await self.bot.say(embed=embed)
async def spellbook(self, ctx): """Commands to display a character's known spells and metadata.""" character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.description = f"{character.get_name()} knows {len(character.get_spell_list())} spells." embed.add_field(name="DC", value=str(character.get_save_dc())) embed.add_field(name="Spell Attack Bonus", value=str(character.get_spell_ab())) embed.add_field(name="Spell Slots", value=character.get_remaining_slots_str() or "None") spells_known = {} choices = await get_spell_choices(ctx) for spell_ in character.get_raw_spells(): if isinstance(spell_, str): spell, strict = search(c.spells, spell_, lambda sp: sp.name) if spell is None or not strict: continue spells_known[str(spell.level)] = spells_known.get( str(spell.level), []) + [spell.name] else: spellname = spell_['name'] strict = spell_['strict'] spell = await get_castable_spell(ctx, spellname, choices) if spell is None and strict: continue elif spell is None: spells_known['unknown'] = spells_known.get( 'unknown', []) + [f"*{spellname}*"] else: if spell.source == 'homebrew': formatted = f"*{spell.name}*" else: formatted = spell.name spells_known[str(spell.level)] = spells_known.get( str(spell.level), []) + [formatted] level_name = { '0': 'Cantrips', '1': '1st Level', '2': '2nd Level', '3': '3rd Level', '4': '4th Level', '5': '5th Level', '6': '6th Level', '7': '7th Level', '8': '8th Level', '9': '9th Level' } for level, spells in sorted(list(spells_known.items()), key=lambda k: k[0]): if spells: spells.sort() embed.add_field(name=level_name.get(level, "Unknown"), value=', '.join(spells)) await ctx.send(embed=embed)
async def join(self, ctx, *, args: str = ''): """Adds the current active character to combat. A character must be loaded through the SheetManager module first. Args: adv/dis -b [conditional bonus] -phrase [flavor text] -p [init value] -h (same as !init add) --group (same as !init add)""" char: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(char, False) embed.colour = char.get_color() args = shlex.split(args) args = argparse(args) adv = args.adv(boolwise=True) b = args.join('b', '+') or None p = args.last('p', type_=int) phrase = args.join('phrase', '\n') or None group = args.last('group') if p is None: roll_str = char.skills.initiative.d20(base_adv=adv) if b: roll_str = f"{roll_str}+{b}" check_roll = roll(roll_str, inline=True) embed.title = '{} makes an Initiative check!'.format(char.name) embed.description = check_roll.skeleton + ( '\n*' + phrase + '*' if phrase is not None else '') init = check_roll.total else: init = p embed.title = "{} already rolled initiative!".format(char.name) embed.description = "Placed at initiative `{}`.".format(init) controller = str(ctx.author.id) private = args.last('h', type_=bool) bonus = char.skills.initiative.value combat = await Combat.from_ctx(ctx) me = await PlayerCombatant.from_character(char.name, controller, init, bonus, char.ac, private, char.get_resists(), ctx, combat, char.upstream, str(ctx.author.id), char) if combat.get_combatant(char.name) is not None: await ctx.send("Combatant already exists.") return if group is None: combat.add_combatant(me) embed.set_footer(text="Added to combat!") else: grp = combat.get_group(group, create=init) grp.add_combatant(me) embed.set_footer(text=f"Joined group {grp.name}!") await combat.final() await ctx.send(embed=embed) await char.commit(ctx)