async def game_spellslot(self, ctx, level: int = None, value: str = None): """Views or sets your remaining spell slots.""" if level is not None: try: assert 0 < level < 10 except AssertionError: return await ctx.send("Invalid spell level.") character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.set_footer(text="\u25c9 = Available / \u3007 = Used") if level is None and value is None: # show remaining embed.description = f"__**Remaining Spell Slots**__\n{character.spellbook.slots_str()}" elif value is None: embed.description = f"__**Remaining Level {level} Spell Slots**__\n" \ f"{character.spellbook.slots_str(level)}" else: try: if value.startswith(('+', '-')): value = character.spellbook.get_slots(level) + int(value) else: value = int(value) except ValueError: return await ctx.send(f"{value} is not a valid integer.") try: assert 0 <= value <= character.spellbook.get_max_slots(level) except AssertionError: raise CounterOutOfBounds() character.spellbook.set_slots(level, value) await character.commit(ctx) embed.description = f"__**Remaining Level {level} Spell Slots**__\n" \ f"{character.spellbook.slots_str(level)}" await ctx.send(embed=embed)
async def game_spellslot(self, ctx, level: int = None, value: str = None, *args): """ Views or sets your remaining spell slots. __Valid Arguments__ nopact - Modifies normal spell slots first instead of a Pact Magic slots, if applicable. """ if level is not None: try: assert 0 < level < 10 except AssertionError: return await ctx.send("Invalid spell level.") character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) if level is None and value is None: # show remaining embed.description = f"__**Remaining Spell Slots**__\n{character.spellbook.slots_str()}" elif value is None: embed.description = f"__**Remaining Level {level} Spell Slots**__\n" \ f"{character.spellbook.slots_str(level)}" else: old_slots = character.spellbook.get_slots(level) value = maybe_mod(value, old_slots) character.spellbook.set_slots(level, value, pact='nopact' not in args) await character.commit(ctx) embed.description = f"__**Remaining Level {level} Spell Slots**__\n" \ f"{character.spellbook.slots_str(level)} ({(value - old_slots):+})" # footer - pact vs non pact if character.spellbook.max_pact_slots is not None: embed.set_footer(text=f"{constants.FILLED_BUBBLE} = Available / {constants.EMPTY_BUBBLE} = Used\n" f"{constants.FILLED_BUBBLE_ALT} / {constants.EMPTY_BUBBLE_ALT} = Pact Slot") else: embed.set_footer(text=f"{constants.FILLED_BUBBLE} = Available / {constants.EMPTY_BUBBLE} = Used") await ctx.send(embed=embed)
async def customcounter_summary(self, ctx, page: int = 0): """ Prints a summary of all custom counters. Use `!cc list <page>` to view pages if you have more than 25 counters. """ character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character, title="Custom Counters") # Check that we're not over the field limit total = len(character.consumables) if total > 25: # Discord Field limit page = max(0, page - 1) # Humans count from 1 maxpage = total // 25 start = min(page * 25, total - 25) end = max(start + 25, total) # Build the current page embed.set_footer( text= f"Page [{page + 1}/{maxpage + 1}] | {ctx.prefix}cc list <page>" ) for counter in character.consumables[start:end]: embed.add_field(name=counter.name, value=counter.full_str()) else: for counter in character.consumables: embed.add_field(name=counter.name, value=counter.full_str()) 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)
def get_sheet_embed(self): embed = EmbedWithCharacter(self) desc_details = [] # race/class (e.g. Tiefling Bard/Warlock) classes = '/'.join(f"{cls} {lvl}" for cls, lvl in self.levels) desc_details.append(f"{self.race} {classes}") # prof bonus desc_details.append( f"**Proficiency Bonus**: {self.stats.prof_bonus:+}") # combat details desc_details.append(f"**AC**: {self.ac}") desc_details.append(f"**HP**: {self.get_hp_str()}") desc_details.append( f"**Initiative**: {self.skills.initiative.value:+}") # stats desc_details.append(str(self.stats)) save_profs = str(self.saves) if save_profs: desc_details.append(f"**Save Proficiencies**: {save_profs}") skill_profs = str(self.skills) if skill_profs: desc_details.append(f"**Skill Proficiencies**: {skill_profs}") desc_details.append( f"**Senses**: passive Perception {10 + self.skills.perception.value}" ) # resists resists = str(self.resistances) if resists: desc_details.append(resists) embed.description = '\n'.join(desc_details) # attacks atks = self.attacks atk_str = "" for attack in atks: a = f"{str(attack)}\n" if len(atk_str) + len(a) > 1000: atk_str += "[...]" break atk_str += a atk_str = atk_str.strip() if atk_str: embed.add_field(name="Attacks", value=atk_str) # sheet url? if self._import_version < 15: embed.set_footer( text= f"You are using an old sheet version ({self.sheet_type} v{self._import_version}). " f"Please run !update.") return embed
async def spellbook(self, ctx): """Commands to display a character's known spells and metadata.""" await ctx.trigger_typing() 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.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)
def get_sheet_embed(self): embed = EmbedWithCharacter(self) # noinspection PyListCreation # this could be a list literal, but it's more readable this way desc_details = [] # race/class (e.g. Tiefling Bard/Warlock) desc_details.append(f"{self.race} {str(self.levels)}") # prof bonus desc_details.append( f"**Proficiency Bonus**: {self.stats.prof_bonus:+}") # combat details desc_details.append(f"**AC**: {self.ac}") desc_details.append(f"**HP**: {self.hp_str()}") desc_details.append( f"**Initiative**: {self.skills.initiative.value:+}") # stats desc_details.append(str(self.stats)) save_profs = str(self.saves) if save_profs: desc_details.append(f"**Save Proficiencies**: {save_profs}") skill_profs = str(self.skills) if skill_profs: desc_details.append(f"**Skill Proficiencies**: {skill_profs}") desc_details.append( f"**Senses**: passive Perception {10 + self.skills.perception.value}" ) # resists resists = str(self.resistances) if resists: desc_details.append(resists) embed.description = '\n'.join(desc_details) # attacks atk_str = self.attacks.build_str(self) if len(atk_str) > 1000: atk_str = f"{atk_str[:1000]}\n[...]" if atk_str: embed.add_field(name="Attacks", value=atk_str) # sheet url? if self._import_version < SHEET_VERSION: embed.set_footer( text= f"You are using an old sheet version ({self.sheet_type} v{self._import_version}). " f"Please run !update.") return embed
async def game_deathsave(self, ctx, *args): """Commands to manage character death saves. __Valid Arguments__ See `!help save`.""" character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character, name=False) args = await helpers.parse_snippets(args, ctx) args = await helpers.parse_with_character(ctx, character, args) args = argparse(args) checkutils.update_csetting_args(character, args) caster, _, _ = await targetutils.maybe_combat(ctx, character, args) result = checkutils.run_save('death', caster, args, embed) dc = result.skill_roll_result.dc or 10 death_phrase = '' for save_roll in result.skill_roll_result.rolls: if save_roll.crit == d20.CritType.CRIT: character.hp = 1 elif save_roll.crit == d20.CritType.FAIL: character.death_saves.fail(2) elif save_roll.total >= dc: character.death_saves.succeed() else: character.death_saves.fail() if save_roll.crit == d20.CritType.CRIT: death_phrase = f"{character.name} is UP with 1 HP!" break elif character.death_saves.is_dead(): death_phrase = f"{character.name} is DEAD!" break elif character.death_saves.is_stable(): death_phrase = f"{character.name} is STABLE!" break if death_phrase: embed.set_footer(text=death_phrase) embed.add_field(name="Death Saves", value=str(character.death_saves), inline=False) await character.commit(ctx) await ctx.send(embed=embed) await try_delete(ctx.message) if gamelog := self.bot.get_cog('GameLog'): await gamelog.send_save(ctx, character, result.skill_name, result.rolls)
async def game_deathsave_save(self, ctx): """Adds a successful death save.""" character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.title = f'{character.name} succeeds a Death Save!' character.death_saves.succeed() await character.commit(ctx) if character.death_saves.is_stable(): embed.set_footer(text=f"{character.name} is STABLE!") embed.description = "Added 1 successful death save." embed.add_field(name="Death Saves", value=str(character.death_saves)) await ctx.send(embed=embed)
async def game_deathsave_fail(self, ctx): """Adds a failed death save.""" character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.title = f'{character.name} fails a Death Save!' character.death_saves.fail() await character.commit(ctx) if character.death_saves.is_dead(): embed.set_footer(text=f"{character.name} is DEAD!") embed.description = "Added 1 failed death save." embed.add_field(name="Death Saves", value=str(character.death_saves)) await ctx.send(embed=embed)
async def game_deathsave_fail(self, ctx): """Adds a failed death save.""" character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.title = f'{character.get_name()} fails a Death Save!' death_phrase = '' if character.add_failed_ds(): death_phrase = f"{character.get_name()} is DEAD!" await character.commit(ctx) embed.description = "Added 1 failed death save." if death_phrase: embed.set_footer(text=death_phrase) embed.add_field(name="Death Saves", value=character.get_ds_str()) await ctx.send(embed=embed)
async def game_deathsave_save(self, ctx): """Adds a successful death save.""" character = Character.from_ctx(ctx) embed = EmbedWithCharacter(character) embed.title = f'{character.get_name()} succeeds a Death Save!' death_phrase = '' if character.add_successful_ds(): death_phrase = f"{character.get_name()} is STABLE!" character.commit(ctx) embed.description = "Added 1 successful death save." if death_phrase: embed.set_footer(text=death_phrase) embed.add_field(name="Death Saves", value=character.get_ds_str()) await self.bot.say(embed=embed)
async def send_ddb_ctas(ctx, character): """Sends relevant CTAs after a DDB character is imported. Only show a CTA 1/24h to not spam people.""" ddb_user = await ctx.bot.ddb.get_ddb_user(ctx, ctx.author.id) if ddb_user is not None: ld_dict = ddb_user.to_ld_dict() else: ld_dict = {"key": str(ctx.author.id), "anonymous": True} gamelog_flag = await ctx.bot.ldclient.variation('cog.gamelog.cta.enabled', ld_dict, False) # has the user seen this cta within the last 7d? if await ctx.bot.rdb.get(f"cog.sheetmanager.cta.seen.{ctx.author.id}"): return embed = EmbedWithCharacter(character) embed.title = "Heads up!" embed.description = "There's a couple of things you can do to make your experience even better!" embed.set_footer(text="You won't see this message again this week.") # link ddb user if ddb_user is None: embed.add_field( name="Connect Your D&D Beyond Account", value= "Visit your [Account Settings](https://www.dndbeyond.com/account) page in D&D Beyond to link your " "D&D Beyond and Discord accounts. This lets you use all your D&D Beyond content in Avrae for free!", inline=False) # game log if character.ddb_campaign_id and gamelog_flag: try: await CampaignLink.from_id(ctx.bot.mdb, character.ddb_campaign_id) except NoCampaignLink: embed.add_field( name="Link Your D&D Beyond Campaign", value= f"Sync rolls between a Discord channel and your D&D Beyond character sheet by linking your " f"campaign! Use `{ctx.prefix}campaign https://www.dndbeyond.com/campaigns/" f"{character.ddb_campaign_id}` in the Discord channel you want to link it to.", inline=False) if not embed.fields: return await ctx.send(embed=embed) await ctx.bot.rdb.setex(f"cog.sheetmanager.cta.seen.{ctx.author.id}", str(time.time()), 60 * 60 * 24 * 7)
def embed_for_basic_attack(gctx, action_name, character, to_hit_roll=None, damage_roll=None): """ Creates an embed for a character making an attack where the Avrae action is unknown. Handles inserting the correct fields for to-hit and damage. :type gctx: GameLogEventContext :type action_name: str :type character: cogs5e.models.character.Character :type to_hit_roll: ddb.dice.tree.RollRequestRoll :type damage_roll: ddb.dice.tree.RollRequestRoll """ embed = EmbedWithCharacter(character, name=False) # set title embed.title = f'{character.get_title_name()} attacks with {action_name}!' # add to hit (and damage, either if it is provided or the action expects damage and it is not provided) meta_rolls = [] if to_hit_roll is not None: meta_rolls.append(f"**To Hit**: {str(to_hit_roll.to_d20())}") if damage_roll is not None: if damage_roll.roll_kind == ddb.dice.RollKind.CRITICAL_HIT: meta_rolls.append( f"**Damage (CRIT!)**: {str(damage_roll.to_d20())}") else: meta_rolls.append(f"**Damage**: {str(damage_roll.to_d20())}") else: meta_rolls.append("**Damage**: Waiting for roll...") embed.add_field(name="Meta", value='\n'.join(meta_rolls), inline=False) # set footer embed.set_footer(text=f"Rolled in {gctx.campaign.campaign_name}", icon_url=constants.DDB_LOGO_ICON) return embed
async def customcounter_summary(self, ctx, page: int = 1): """ Prints a summary of all custom counters. Use `!cc list <page>` to view pages if you have more than 25 counters. """ character: Character = await Character.from_ctx(ctx) embed = EmbedWithCharacter(character, title="Custom Counters") if character.consumables: # paginate if > 25 total = len(character.consumables) maxpage = total // 25 + 1 page = max(1, min(page, maxpage)) pages = [character.consumables[i:i + 25] for i in range(0, total, 25)] for counter in pages[page - 1]: embed.add_field(name=counter.name, value=counter.full_str()) if total > 25: embed.set_footer(text=f"Page [{page}/{maxpage}] | {ctx.prefix}cc list <page>") else: embed.add_field(name="No Custom Counters", value=f"Check out `{ctx.prefix}help cc create` to see how to create new ones.") await ctx.send(embed=embed)
async def save(self, ctx, skill, *, args: str = ''): """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, *shlex.split(args)) char = await Character.from_ctx(ctx) saves = char.get_saves() if not saves: return await ctx.send('You must update your character sheet first.') try: save = next(a for a in saves.keys() if skill.lower() == a.lower()) except StopIteration: try: save = next(a for a in saves.keys() if skill.lower() in a.lower()) except StopIteration: return await ctx.send('That\'s not a valid save.') embed = EmbedWithCharacter(char, name=False) skill_effects = char.get_skill_effects() args += ' ' + skill_effects.get(save, '') # dicecloud v11 - autoadv args = await self.new_arg_stuff(args, ctx, char) adv = args.adv() 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 = format_d20(adv, char.get_setting('reroll')) if b is not None: roll_str = formatted_d20 + '{:+}'.format(saves[save]) + '+' + b else: roll_str = formatted_d20 + '{:+}'.format(saves[save]) embed.title = args.last('title', '') \ .replace('[charname]', char.get_name()) \ .replace('[sname]', camel_to_title(save)) \ or '{} makes {}!'.format(char.get_name(), a_or_an(camel_to_title(save))) 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, adv=adv, 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} Failues") else: result = roll(roll_str, adv=adv, 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
def embed_for_action(gctx, action, character, to_hit_roll=None, damage_roll=None): """ Creates an embed for a character performing some action (attack or spell). Handles inserting the correct fields for to-hit and damage based on the action's automation and whether the rolls are present. :type gctx: GameLogEventContext :type action: Attack or gamedata.spell.Spell :type character: cogs5e.models.character.Character :type to_hit_roll: ddb.dice.tree.RollRequestRoll :type damage_roll: ddb.dice.tree.RollRequestRoll """ embed = EmbedWithCharacter(character, name=False) automation = action.automation waiting_for_damage = False # set title if isinstance(action, Attack): attack_name = a_or_an( action.name) if not action.proper else action.name verb = action.verb or "attacks with" embed.title = f'{character.get_title_name()} {verb} {attack_name}!' else: # spell embed.title = f'{character.get_title_name()} casts {action.name}!' # add to hit (and damage, either if it is provided or the action expects damage and it is not provided) meta_rolls = [] if to_hit_roll is not None: meta_rolls.append(f"**To Hit**: {str(to_hit_roll.to_d20())}") if damage_roll is not None: if damage_roll.roll_kind == ddb.dice.RollKind.CRITICAL_HIT: meta_rolls.append( f"**Damage (CRIT!)**: {str(damage_roll.to_d20())}") else: meta_rolls.append(f"**Damage**: {str(damage_roll.to_d20())}") elif automation_has_damage(automation): meta_rolls.append("**Damage**: Waiting for roll...") waiting_for_damage = True # add dcs, texts if automation: for effect in automation_dfg(automation, enter_filter=action_enter_filter): # break if we see a damage and are waiting on a damage roll if effect.type == 'damage' and waiting_for_damage: break # note: while certain fields here are AnnotatedStrings, it should never be annotated directly from the sheet # and GameLog events cannot trigger custom attacks, so this should be fine # save: add the DC if effect.type == 'save': meta_rolls.append( f"**DC**: {effect.dc}\n{effect.stat[:3].upper()} Save") # text: add the text as a field elif effect.type == 'text': embed.add_field(name="Effect", value=effect.text, inline=False) embed.insert_field_at(0, name="Meta", value='\n'.join(meta_rolls), inline=False) # set footer embed.set_footer(text=f"Rolled in {gctx.campaign.campaign_name}", icon_url=constants.DDB_LOGO_ICON) return 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 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 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 STAT_ABBREVIATIONS): base = next(s for s in STAT_ABBREVIATIONS 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 check(self, ctx, check, *, args: str = ''): """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 = await Character.from_ctx(ctx) skills = char.get_skills() if not skills: return await ctx.send('You must update your character sheet first.') try: skill = next(a for a in skills.keys() if check.lower() == a.lower())#this checks for the skill exactly except StopIteration: try: skill = next(a for a in skills.keys() if check.lower() in a.lower())#this checks for the partial name of the skill except StopIteration: try: # Probably will be fairly slow, but whatever skill = next(SKILL_ALIASES[alias] for alias in SKILL_ALIASES.keys() if check.lower() == alias.lower())#go through our alias names except StopIteration: return await ctx.send('That\'s not a valid check.') embed = EmbedWithCharacter(char, False) skill_effects = char.get_skill_effects() args += ' ' + skill_effects.get(skill, '') # dicecloud v7 - autoadv args = await self.new_arg_stuff(args, ctx, char) adv = args.adv() 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 = format_d20(adv, char.get_setting('reroll')) mc = args.last('mc', None) if mc: formatted_d20 = f"{formatted_d20}mi{mc}" mod = skills[skill] skill_name = skill 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]) + char.get_mod(base) skill_name = f"{verbose_stat(base)} ({skill})" skill_name = camel_to_title(skill_name) default_title = '{} makes {} check!'.format(char.get_name(), a_or_an(skill_name)) 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('[charname]', char.get_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, adv=adv, 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, adv=adv, 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