Example #1
0
    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)
Example #2
0
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") == []
Example #4
0
    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()
Example #5
0
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
Example #6
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)
Example #7
0
    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.")
Example #8
0
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
Example #9
0
    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)
Example #10
0
    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)
Example #11
0
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
Example #12
0
    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]`")
Example #13
0
    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)
Example #14
0
    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)
Example #15
0
    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()
Example #16
0
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
Example #17
0
    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)
Example #19
0
    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)
Example #20
0
    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)
Example #21
0
    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)
Example #22
0
    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)
Example #24
0
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
Example #25
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']
Example #27
0
    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)
Example #28
0
    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)
Example #29
0
    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()
Example #30
0
    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)