def new(cls, combat, combatant, name, duration, effect_args, concentration: bool = False, character=None, tick_on_end=False, desc: str = None): if isinstance(effect_args, str): if (combatant and combatant.type == CombatantType.PLAYER) or character: effect_args = argparse(effect_args, combatant.character or character) else: effect_args = argparse(effect_args) effect_dict = {} for arg in effect_args: arg_arg = None if arg in LIST_ARGS: arg_arg = effect_args.get(arg, []) elif arg in VALID_ARGS: arg_arg = effect_args.last(arg) if arg in SPECIAL_ARGS: effect_dict[arg] = SPECIAL_ARGS[arg][0](arg_arg, name) elif arg_arg is not None: effect_dict[arg] = arg_arg try: duration = int(duration) except (ValueError, TypeError): raise InvalidArgument("Effect duration must be an integer.") id = create_effect_id() return cls(combat, combatant, id, name, duration, duration, effect_dict, concentration=concentration, tonend=tick_on_end, desc=desc)
def test_argparse_custom_adv(): args = argparse('custom_adv') custom_adv = { 'adv': 'custom_adv', } assert args.adv(custom=custom_adv) == 1 assert args.adv() == 0 custom_dis = { 'dis': 'custom_dis' } assert args.adv(custom=custom_dis) == 0 args = argparse('custom_dis') assert args.adv(custom=custom_dis) == -1 assert args.adv() == 0 custom_ea = { 'ea': 'custom_ea' } args = argparse('custom_ea') assert args.adv(ea=True, custom=custom_ea) == 2 assert args.adv() == 0
def test_contextual_argparse(): args = argparse("-d 5") args.add_context("foo", argparse('-d 1 -phrase "I am foo"')) args.add_context("bar", argparse('-d 2 -phrase "I am bar"')) args.set_context('foo') assert args.last("d") == '1' assert args.get("d") == ['5', '1'] assert args.last("phrase") == "I am foo" assert args.get("phrase") == ["I am foo"] args.set_context('bar') assert args.last("d") == '2' assert args.get("d") == ['5', '2'] assert args.last("phrase") == "I am bar" assert args.get("phrase") == ["I am bar"] args.set_context('bletch') assert args.last("d") == '5' assert args.get("d") == ['5'] assert args.last("phrase") is None assert args.get("phrase") == [] args.set_context(None) assert args.last("d") == '5' assert args.get("d") == ['5'] assert args.last("phrase") is None assert args.get("phrase") == []
async def effect(self, ctx, name: str, effect_name: str, *, args: str = ''): """Attaches a status effect to a combatant. [args] is a set of args that affects a combatant in combat. __**Valid Arguments**__ -dur [duration] conc (makes effect require conc) end (makes effect tick on end of turn) __Attacks__ -b [bonus] (see !a) -d [damage bonus] (see !a) -attack "[hit]|[damage]|[description]" (Adds an attack to the combatant) __Resists__ -resist [resist] (gives the combatant resistance) -immune [immune] (gives the combatant immunity) -vuln [vulnability] (gives the combatant vulnerability) -neutral [neutral] (removes immune/resist/vuln) __General__ -ac [ac] (modifies ac temporarily; adds if starts with +/- or sets otherwise) -sb [save bonus] (Adds a bonus to saving throws)""" combat = await Combat.from_ctx(ctx) combatant = await combat.select_combatant(name) if combatant is None: await ctx.send("Combatant not found.") return if effect_name.lower() in (e.name.lower() for e in combatant.get_effects()): return await ctx.send("Effect already exists.", delete_after=10) if isinstance(combatant, PlayerCombatant): args = argparse(args, combatant.character) else: args = argparse(args) duration = args.last('dur', -1, int) conc = args.last('conc', False, bool) end = args.last('end', False, bool) effectObj = Effect.new(combat, combatant, duration=duration, name=effect_name, effect_args=args, concentration=conc, tick_on_end=end) result = combatant.add_effect(effectObj) out = "Added effect {} to {}.".format(effect_name, combatant.name) if result['conc_conflict']: conflicts = [e.name for e in result['conc_conflict']] out += f"\nRemoved {', '.join(conflicts)} due to concentration conflict!" await ctx.send(out, delete_after=10) await combat.final()
def test_argparse(): args = argparse("""-phrase "hello world" -h argument -t or1 -t or2""") assert args.last('phrase') == 'hello world' assert args.get('t') == ['or1', 'or2'] assert args.adv() == 0 assert args.last('t') == 'or2' assert args.last('h', type_=bool) is True assert 'argument' in args assert args.last('notin', default=5) == 5 args = argparse("""adv""") assert args.adv() == 1 args = argparse("""adv dis adv""") assert args.adv() == 0
async def cast(self, ctx, spell_name, *, args=''): await try_delete(ctx.message) char: Character = await Character.from_ctx(ctx) args = await helpers.parse_snippets(args, ctx, character=char) args = argparse(args) if not args.last('i', type_=bool): try: spell = await select_spell_full(ctx, spell_name, list_filter=lambda s: s.name in char.spellbook) except NoSelectionElements: return await ctx.send( f"No matching spells found. Make sure this spell is in your " f"`{ctx.prefix}spellbook`, or cast with the `-i` argument to ignore restrictions!") else: spell = await select_spell_full(ctx, spell_name) caster, targets, combat = await targetutils.maybe_combat(ctx, char, args) result = await spell.cast(ctx, caster, targets, args, combat=combat) embed = result.embed embed.colour = char.get_color() if 'thumb' not in args: embed.set_thumbnail(url=char.image) # save changes: combat state, spell slot usage await char.commit(ctx) if combat: await combat.final() await ctx.send(embed=embed) if (gamelog := self.bot.get_cog('GameLog')) and result.automation_result: await gamelog.send_automation(ctx, char, spell.name, result.automation_result)
async def lookup_settings(self, ctx, *args): """This command has been replaced by `!servsettings`. If you're used to it, it still works like before!""" guild_settings = await ctx.get_server_settings() if not args: settings_ui = ui.ServerSettingsUI.new(ctx.bot, owner=ctx.author, settings=guild_settings, guild=ctx.guild) await settings_ui.send_to(ctx) return # old deprecated CLI behaviour args = argparse(args) out = [] if 'req_dm_monster' in args: setting = get_positivity(args.last('req_dm_monster', True)) guild_settings.lookup_dm_required = setting out.append(f'req_dm_monster set to {setting}!') if 'pm_dm' in args: setting = get_positivity(args.last('pm_dm', True)) guild_settings.lookup_pm_dm = setting out.append(f'pm_dm set to {setting}!') if 'pm_result' in args: setting = get_positivity(args.last('pm_result', True)) guild_settings.lookup_pm_result = setting out.append(f'pm_result set to {setting}!') if out: await guild_settings.commit(ctx.bot.mdb) await ctx.send("Lookup settings set:\n" + '\n'.join(out)) else: await ctx.send(f"No settings found. Try using `{ctx.prefix}lookup_settings` to open an interactive menu.")
async def definitely_combat(combat, args, allow_groups=True): target_args = args.get('t') targets = [] for i, t in enumerate(target_args): contextargs = None if '|' in t: t, contextargs = t.split('|', 1) contextargs = argparse(contextargs) try: target = await combat.select_combatant(t, f"Select target #{i + 1}.", select_group=allow_groups) except SelectionException: raise InvalidArgument(f"Target {t} not found.") if isinstance(target, CombatantGroup): for combatant in target.get_combatants(): if contextargs: args.add_context(combatant, contextargs) targets.append(combatant) else: if contextargs: args.add_context(target, contextargs) targets.append(target) return targets
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_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 maybe_combat(ctx, caster, args, allow_groups=True): """ If channel not in combat: returns caster, target_list, None unmodified. If channel in combat but caster not: returns caster, list of combatants, combat. If channel in combat and caster in combat: returns caster as combatant, list of combatants, combat. """ target_args = args.get('t') targets = [] try: combat = await Combat.from_ctx(ctx) except CombatNotFound: for i, target in enumerate(target_args): if '|' in target: target, contextargs = target.split('|', 1) args.add_context(target, argparse(contextargs)) targets.append(target) return caster, targets, None # get targets as Combatants targets = await definitely_combat(combat, args, allow_groups) # get caster as Combatant if caster in combat if isinstance(caster, Character): caster = next( (c for c in combat.get_combatants() if getattr(c, 'character_id', None) == caster.upstream and getattr(c, 'character_owner', None) == caster.owner), caster) return caster, targets, combat
async def begin(self, ctx, *args): """Begins combat in the channel the command is invoked. Usage: !init begin <ARGS (opt)> __Valid Arguments__ dyn (dynamic init; rerolls all initiatives at the start of a round) turnnotif (notifies the next player) -name <NAME> (names the combat)""" await Combat.ensure_unique_chan(ctx) options = {} args = argparse(args) if args.last('dyn', False, bool): # rerolls all inits at the start of each round options['dynamic'] = True if 'name' in args: options['name'] = args.last('name') if args.last('turnnotif', False, bool): options['turnnotif'] = True temp_summary_msg = await ctx.send("```Awaiting combatants...```") Combat.message_cache[temp_summary_msg.id] = temp_summary_msg # add to cache combat = Combat.new(str(ctx.channel.id), temp_summary_msg.id, str(ctx.author.id), options, ctx) await combat.final() try: await temp_summary_msg.pin() except: pass await ctx.send( f"Everyone roll for initiative!\n" f"If you have a character set up with SheetManager: `{ctx.prefix}init join`\n" f"If it's a 5e monster: `{ctx.prefix}init madd [monster name]`\n" f"Otherwise: `{ctx.prefix}init add [modifier] [name]`")
async def monster_cast(self, ctx, monster_name, spell_name, *args): await try_delete(ctx.message) monster: Monster = await select_monster_full(ctx, monster_name) args = await helpers.parse_snippets(args, ctx, statblock=monster) args = argparse(args) if not args.last('i', type_=bool): try: spell = await select_spell_full( ctx, spell_name, list_filter=lambda s: s.name in monster.spellbook) except NoSelectionElements: return await ctx.send( f"No matching spells found in the creature's spellbook. Cast again " f"with the `-i` argument to ignore restrictions!") 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()) handle_source_footer(embed, monster, add_source_str=False) # save changes: combat state if combat: await combat.final() await ctx.send(embed=embed)
async def attack_add(self, ctx, name, *args): """ Adds an attack to the active character. __Arguments__ -d [damage]: How much damage the attack should do. -b [to-hit]: The to-hit bonus of the attack. -desc [description]: A description of the attack. """ character: Character = await Character.from_ctx(ctx) parsed = argparse(args) attack = Attack.new(character, name, bonus_calc=parsed.join('b', '+'), damage=parsed.join('d', '+'), details=parsed.join('desc', '\n')) conflict = next((a for a in character.overrides.attacks if a.name.lower() == attack.name.lower()), None) if conflict: character.overrides.attacks.remove(conflict) character.overrides.attacks.append(attack) await character.commit(ctx) out = f"Created attack {attack.name}!" if conflict: out += f" Removed a duplicate attack." await ctx.send(out)
async def playertoken(self, ctx, *args): """ Generates and sends a token for use on VTTs. __Valid Arguments__ -border <gold|plain|none> - Chooses the token border. """ char: Character = await Character.from_ctx(ctx) if not char.image: return await ctx.send("This character has no image.") token_args = argparse(args) ddb_user = await self.bot.ddb.get_ddb_user(ctx, ctx.author.id) is_subscriber = ddb_user and ddb_user.is_subscriber try: processed = await img.generate_token(char.image, is_subscriber, token_args) except Exception as e: return await ctx.send(f"Error generating token: {e}") file = discord.File(processed, filename="image.png") embed = embeds.EmbedWithCharacter(char, image=False) embed.set_image(url="attachment://image.png") await ctx.send(file=file, embed=embed) processed.close()
async def maybe_combat(ctx, caster, args, allow_groups=True): """ If channel not in combat: returns caster, target_list, None unmodified. If channel in combat but caster not: returns caster, list of combatants, combat. If channel in combat and caster in combat: returns caster as combatant, list of combatants, combat. """ target_args = args.get('t') targets = [] try: combat = await ctx.get_combat() except CombatNotFound: for i, target in enumerate(target_args): if '|' in target: target, contextargs = target.split('|', 1) args.add_context(target, argparse(contextargs)) targets.append(target) return caster, targets, None # get targets as Combatants targets = await definitely_combat(combat, args, allow_groups) # get caster as Combatant if caster in combat caster = await maybe_combat_caster(ctx, caster, combat=combat) return caster, targets, combat
async def customcounter_create(self, ctx, name, *args): """Creates a new custom counter. __Valid Arguments__ `-reset <short|long|none>` - Counter will reset to max on a short/long rest, or not ever when "none". Default - will reset on a call of `!cc reset`. `-max <max value>` - The maximum value of the counter. `-min <min value>` - The minimum value of the counter. `-type <bubble|default>` - Whether the counter displays bubbles to show remaining uses or numbers. Default - numbers.""" character: Character = await Character.from_ctx(ctx) conflict = next((c for c in character.consumables if c.name.lower() == name.lower()), None) if conflict: if await confirm(ctx, "Warning: This will overwrite an existing consumable. Continue?"): character.consumables.remove(conflict) else: return await ctx.send("Overwrite unconfirmed. Aborting.") args = argparse(args) _reset = args.last('reset') _max = args.last('max') _min = args.last('min') _type = args.last('type') try: new_counter = CustomCounter.new(character, name, maxv=_max, minv=_min, reset=_reset, display_type=_type) character.consumables.append(new_counter) await character.commit(ctx) except InvalidArgument as e: return await ctx.send(f"Failed to create counter: {e}") else: await ctx.send(f"Custom counter created.")
async def attack_add(self, ctx, name, *, args=""): """ Adds an attack to the active character. __Arguments__ -d [damage]: How much damage the attack should do. -b [to-hit]: The to-hit bonus of the attack. -desc [description]: A description of the attack. """ parsed = argparse(args) attack = { "name": name, "attackBonus": parsed.join('b', '+'), "damage": parsed.join('d', '+'), "details": parsed.join('desc', '\n') } character = await Character.from_ctx(ctx) attack_overrides = character.get_override("attacks", []) duplicate = next((a for a in attack_overrides if a['name'].lower() == attack['name'].lower()), None) if duplicate: attack_overrides.remove(duplicate) attack_overrides.append(attack) character.set_override("attacks", attack_overrides) await character.commit(ctx) out = f"Created attack {attack['name']}!" if duplicate: out += f" Removed a duplicate attack." await ctx.send(out)
async def monster_atk(self, ctx, monster_name, atk_name=None, *, args=''): 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, statblock=monster) 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 actionutils.run_attack(ctx, embed, args, caster, attack, targets, combat) embed.colour = random.randint(0, 0xffffff) handle_source_footer(embed, monster, add_source_str=False) 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)
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. 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) """ try: await ctx.message.delete() except: pass char: Character = await Character.from_ctx(ctx) args = await helpers.parse_snippets(args, ctx) args = await char.parse_cvars(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 char.spellbook) else: spell = await select_spell_full(ctx, spell_name) caster, targets, combat = await targetutils.maybe_combat( ctx, char, args) result = await spell.cast(ctx, caster, targets, args, combat=combat) embed = result['embed'] embed.colour = char.get_color() embed.set_thumbnail(url=char.image) add_fields_from_args(embed, args.get('f')) if 'thumb' in args: embed.set_thumbnail(url=args.last('thumb')) # save changes: combat state, spell slot usage await char.commit(ctx) if combat: await combat.final() await ctx.send(embed=embed)
async def update(self, ctx, *args): """ Updates the current character sheet, preserving all settings. __Valid Arguments__ `-v` - Shows character sheet after update is complete. `-nocc` - Do not automatically create or update custom counters for class resources and features. `-noprep` - Import all known spells as prepared. """ old_character: Character = await Character.from_ctx(ctx) url = old_character.upstream args = argparse(args) prefixes = 'dicecloud-', 'google-', 'beyond-' _id = url[:] for p in prefixes: if url.startswith(p): _id = url[len(p):] break sheet_type = old_character.sheet_type if sheet_type == 'dicecloud': parser = DicecloudParser(_id) loading = await ctx.send('Updating character data from Dicecloud...') elif sheet_type == 'google': parser = GoogleSheet(_id) loading = await ctx.send('Updating character data from Google...') elif sheet_type == 'beyond': parser = BeyondSheetParser(_id) loading = await ctx.send('Updating character data from Beyond...') else: return await ctx.send(f"Error: Unknown sheet type {sheet_type}.") try: character = await parser.load_character(ctx, args) except ExternalImportError as eep: return await loading.edit(content=f"Error loading character: {eep}") except Exception as eep: log.warning(f"Error importing character {old_character.upstream}") log.warning(traceback.format_exc()) return await loading.edit(content=f"Error loading character: {eep}") character.update(old_character) # keeps an old check if the old character was active on the current server was_server_active = old_character.is_active_server(ctx) await character.commit(ctx) # overwrites the old_character's server active state # since character._active_guilds is old_character._active_guilds here if old_character.is_active_global(): await character.set_active(ctx) if was_server_active: await character.set_server_active(ctx) await loading.edit(content=f"Updated and saved data for {character.name}!") if args.last('v'): await ctx.send(embed=character.get_sheet_embed()) if sheet_type == 'beyond': await send_ddb_ctas(ctx, character)
def new(cls, name, duration, effect_args): if isinstance(effect_args, str): effect_args = argparse(effect_args) effect_dict = {} for arg in cls.VALID_ARGS: if arg in effect_args: effect_dict[arg] = effect_args.last(arg) return cls(name, duration, duration, effect_dict)
def test_argparse_adv(): """ 16 cases: (adv, dis, ea, ea arg in .adv()) a d e ea | out =========+==== 0 0 0 0 | 0 0 0 0 1 | 0 0 0 1 0 | 0 0 0 1 1 | 2 0 1 0 0 | -1 0 1 0 1 | -1 0 1 1 0 | -1 0 1 1 1 | 0 1 0 0 0 | 1 1 0 0 1 | 1 1 0 1 0 | 1 1 0 1 1 | 2 1 1 0 0 | 0 1 1 0 1 | 0 1 1 1 0 | 0 1 1 1 1 | 0 """ args = argparse('') assert args.adv() == 0 assert args.adv(ea=True) == 0 args = argparse('ea') assert args.adv() == 0 assert args.adv(ea=True) == 2 args = argparse('dis') assert args.adv() == -1 assert args.adv(ea=True) == -1 args = argparse('dis ea') assert args.adv() == -1 assert args.adv(ea=True) == 0 args = argparse('adv') assert args.adv() == 1 assert args.adv(ea=True) == 1 args = argparse('adv ea') assert args.adv() == 1 assert args.adv(ea=True) == 2 args = argparse('adv dis') assert args.adv() == 0 assert args.adv(ea=True) == 0 args = argparse('adv dis ea') assert args.adv() == 0 assert args.adv(ea=True) == 0
async def _cast(self, ctx, combatant_name, spell_name, args): args = await scripting.parse_snippets(args, ctx) combat = await Combat.from_ctx(ctx) if combatant_name is None: combatant = combat.current_combatant if combatant is None: return await ctx.send( f"You must start combat with `{ctx.prefix}init next` first." ) else: try: combatant = await combat.select_combatant( combatant_name, "Select the caster.") if combatant is None: return await ctx.send("Combatant not found.") except SelectionException: return await ctx.send("Combatant not found.") if isinstance(combatant, CombatantGroup): return await ctx.send("Groups cannot cast spells.") is_character = isinstance(combatant, PlayerCombatant) if is_character and combatant.character_owner == str(ctx.author.id): args = await combatant.character.parse_cvars(args, ctx) args = shlex.split(args) args = argparse(args) if not args.last('i', type_=bool): spell = await select_spell_full( ctx, spell_name, list_filter=lambda s: s.name.lower( ) in combatant.spellcasting.lower_spells) else: spell = await select_spell_full(ctx, spell_name) targets = [] for i, t in enumerate(args.get('t')): target = await combat.select_combatant(t, f"Select target #{i + 1}.", select_group=True) if isinstance(target, CombatantGroup): targets.extend(target.get_combatants()) else: targets.append(target) result = await spell.cast(ctx, combatant, targets, args, combat=combat) embed = result['embed'] embed.colour = random.randint( 0, 0xffffff) if not is_character else combatant.character.get_color() add_fields_from_args(embed, args.get('f')) await ctx.send(embed=embed) await combat.final()
def test_contextual_ephemeral_argparse(): args = argparse("-d3 5") args.add_context("foo", argparse('-d 3 -d1 1 -phrase "I am foo"')) args.add_context("bar", argparse('-d1 2 -phrase "I am bar"')) args.set_context('foo') assert args.get("d", ephem=True) == ['3', '5', '1'] assert args.get("d", ephem=True) == ['3', '5'] args.set_context('bar') assert args.get("d", ephem=True) == ['5', '2'] assert args.get("d", ephem=True) == [] args.set_context(None) assert args.get("d", ephem=True) == [] args.set_context('foo') assert args.get("d", ephem=True) == ['3']
def run(self, autoctx): super(Save, self).run(autoctx) save = autoctx.args.last('save') or self.stat adv = autoctx.args.adv(False) dc_override = None if self.dc: try: dc_override = autoctx.evaluator.parse(self.dc, autoctx.metavars) dc_override = int(dc_override) except (TypeError, ValueError): raise AutomationException( f"{dc_override} cannot be interpreted as a DC.") dc = autoctx.args.last( 'dc', type_=int ) or dc_override or autoctx.dc_override or autoctx.caster.spellcasting.dc if not dc: raise NoSpellDC() try: save_skill = next(s for s in ('strengthSave', 'dexteritySave', 'constitutionSave', 'intelligenceSave', 'wisdomSave', 'charismaSave') if save.lower() in s.lower()) except StopIteration: raise InvalidSaveType() autoctx.meta_queue(f"**DC**: {dc}") if autoctx.target.target: # character save effects (#408) if autoctx.target.character: save_args = autoctx.target.character.get_skill_effects().get( save_skill) if save_args: adv = argparse(save_args).adv() + adv adv = max(-1, min(1, adv)) # bound, cancel out double dis/adv saveroll = autoctx.target.get_save_dice(save_skill) save_roll = roll(saveroll, adv=adv, rollFor='{} Save'.format(save_skill[:3].upper()), inline=True, show_blurbs=False) is_success = save_roll.total >= dc autoctx.queue(save_roll.result + ("; Success!" if is_success else "; Failure!")) else: autoctx.meta_queue('{} Save'.format(save_skill[:3].upper())) is_success = False if is_success: self.on_success(autoctx) else: self.on_fail(autoctx)
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 effect(self, ctx, name: str, effect_name: str, *args): """Attaches a status effect to a combatant. [args] is a set of args that affects a combatant in combat. __**Valid Arguments**__ -dur [duration] - sets the duration of the effect, in rounds conc - makes effect require conc end - makes effect tick on end of turn -t [target] - specifies more combatants to add this effect to __Attacks__ -b [bonus] (see !a) -d [damage bonus] (see !a) -attack "[hit]|[damage]|[description]" - Adds an attack to the combatant __Resists__ -resist [resist] - gives the combatant resistance -immune [immune] - gives the combatant immunity -vuln [vulnability] - gives the combatant vulnerability -neutral [neutral] - removes immune/resist/vuln __General__ -ac [ac] - modifies ac temporarily; adds if starts with +/- or sets otherwise -sb [save bonus] - Adds a bonus to saving throws""" combat = await Combat.from_ctx(ctx) args = argparse(args) targets = [] first_target = await combat.select_combatant(name) if first_target is None: await ctx.send("Combatant not found.") return targets.append(first_target) for i, t in enumerate(args.get('t')): target = await combat.select_combatant(t, f"Select target #{i + 1}.", select_group=True) if isinstance(target, CombatantGroup): targets.extend(target.get_combatants()) else: targets.append(target) duration = args.last('dur', -1, int) conc = args.last('conc', False, bool) end = args.last('end', False, bool) embed = EmbedWithAuthor(ctx) for combatant in targets: if effect_name.lower() in (e.name.lower() for e in combatant.get_effects()): out = "Effect already exists." else: effectObj = Effect.new(combat, combatant, duration=duration, name=effect_name, effect_args=args, concentration=conc, tick_on_end=end) result = combatant.add_effect(effectObj) out = "Added effect {} to {}.".format(effect_name, combatant.name) if result['conc_conflict']: conflicts = [e.name for e in result['conc_conflict']] out += f"\nRemoved {', '.join(conflicts)} due to concentration conflict!" embed.add_field(name=combatant.name, value=out) await ctx.send(embed=embed, delete_after=10 * len(targets)) await combat.final()
async def monster_atk(self, ctx, monster_name, atk_name='list', *, args=''): """Rolls a monster's attack. Attack name can be "list" for a list of all of the monster's attacks. 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)""" try: await self.bot.delete_message(ctx.message) except: pass monster = await select_monster_full(ctx, monster_name) self.bot.rdb.incr('monsters_looked_up_life') attacks = monster.attacks monster_name = monster.get_title_name() if atk_name == 'list': attacks_string = '\n'.join( "**{0}:** +{1} To Hit, {2} damage.".format( a['name'], a['attackBonus'], a['damage'] or 'no') for a in attacks) return await self.bot.say("{}'s attacks:\n{}".format( monster_name, attacks_string)) attack = fuzzy_search(attacks, 'name', atk_name) if attack is None: return await self.bot.say("No attack with that name found.", delete_after=15) args = shlex.split(args) args = argparse(args) args['name'] = [monster_name] args['image'] = args.get('image') or [monster.get_image_url()] attack['details'] = attack.get('desc') or attack.get('details') result = sheet_attack(attack, args) embed = result['embed'] embed.colour = random.randint(0, 0xffffff) embeds.add_fields_from_args(embed, args.get('f')) if monster.source == 'homebrew': embed.set_footer(text="Homebrew content.", icon_url="https://avrae.io/static/homebrew.png") await self.bot.say(embed=embed)