Example #1
0
    def run(self, autoctx):
        super(IEffect, self).run(autoctx)
        if isinstance(self.duration, str):
            try:
                duration = int(autoctx.parse_annostr(self.duration))
            except ValueError:
                raise InvalidArgument(f"{self.duration} is not an integer (in effect duration)")
        else:
            duration = self.duration

        duration = autoctx.args.last('dur', duration, int)
        if isinstance(autoctx.target.target, Combatant):
            effect = initiative.Effect.new(autoctx.target.target.combat, autoctx.target.target, self.name,
                                           duration, autoctx.parse_annostr(self.effects), tick_on_end=self.tick_on_end)
            if autoctx.conc_effect:
                effect.set_parent(autoctx.conc_effect)
            autoctx.target.target.add_effect(effect)
        else:
            effect = initiative.Effect.new(None, None, self.name, duration, autoctx.parse_annostr(self.effects),
                                           tick_on_end=self.tick_on_end)
        autoctx.queue(f"**Effect**: {str(effect)}")
Example #2
0
 def new(cls, combat, combatant, name, duration, effect_args, concentration: bool = False, character=None,
         tick_on_end=False):
     if isinstance(effect_args, str):
         if (combatant and isinstance(combatant, PlayerCombatant)) or character:
             effect_args = argparse(effect_args, combatant.character or character)
         else:
             effect_args = argparse(effect_args)
     effect_dict = {}
     for arg in effect_args:
         if arg in cls.SPECIAL_ARGS:
             effect_dict[arg] = cls.SPECIAL_ARGS[arg][0](effect_args.last(arg), name)
         elif arg in cls.LIST_ARGS:
             effect_dict[arg] = effect_args.get(arg, [])
         elif arg in cls.VALID_ARGS:
             effect_dict[arg] = effect_args.last(arg)
     try:
         duration = int(duration)
     except (ValueError, TypeError):
         raise InvalidArgument("Effect duration must be an integer.")
     return cls(combat, combatant, name, duration, duration, effect_dict, concentration=concentration,
                tonend=tick_on_end)
Example #3
0
async def _snippet_before_edit(ctx, name=None, delete=False):
    if delete:
        return
    confirmation = None
    # special arg checking
    if not name:
        return
    name = name.lower()
    if name in SPECIAL_ARGS or name.startswith('-'):
        confirmation = f"**Warning:** Creating a snippet named `{name}` will prevent you from using the built-in `{name}` argument in Avrae commands.\nAre you sure you want to create this snippet? (Reply with yes/no)"
    # roll string checking
    try:
        d20.parse(name)
    except d20.RollSyntaxError:
        pass
    else:
        confirmation = f"**Warning:** Creating a snippet named `{name}` might cause hidden problems if you try to use the same roll in other commands.\nAre you sure you want to create this snippet? (Reply with yes/no)"

    if confirmation is not None:
        if not await confirm(ctx, confirmation):
            raise InvalidArgument('Ok, cancelling.')
Example #4
0
def run_save(save_key, caster, args, embed):
    """
    Runs a caster's saving throw, building on an existing embed and handling most arguments.

    Also handles save bonuses from ieffects if caster is a combatant.

    :type save_key: str
    :type caster: cogs5e.models.sheet.statblock.StatBlock
    :type args: utils.argparser.ParsedArguments
    :type embed: discord.Embed
    :return: The total of each save.
    :rtype: SaveResult
    """
    try:
        save = caster.saves.get(save_key)
        stat_name = verbose_stat(save_key[:3]).title()
        save_name = f"{stat_name} Save"
    except ValueError:
        raise InvalidArgument('That\'s not a valid save.')

    # -title
    if args.last('title'):
        embed.title = args.last('title', '') \
            .replace('[name]', caster.get_title_name()) \
            .replace('[sname]', save_name)
    elif args.last('h'):
        embed.title = f"An unknown creature makes {a_or_an(save_name)}!"
    else:
        embed.title = f'{caster.get_title_name()} makes {a_or_an(save_name)}!'

    # ieffect -sb
    if isinstance(caster, init.Combatant):
        args['b'] = args.get('b') + caster.active_effects('sb')

    result = _run_common(save, args, embed, rr_format="Save {}")
    return SaveResult(rolls=result.rolls,
                      skill=save,
                      skill_name=stat_name,
                      skill_roll_result=result)
Example #5
0
    def get(self, arg, default=None, type_=str, ephem=False):
        """
        Gets a list of all values of an argument.

        :param str arg: The name of the arg to get.
        :param default: The default value to return if the arg is not found. Not cast to type.
        :param type_: The type that each value in the list should be returned as.
        :param bool ephem: Whether to add applicable ephemeral arguments to the returned list.
        :return: The relevant argument list.
        :rtype: list
        """
        if default is None:
            default = []
        parsed = self._get_values(arg, ephem=ephem)
        if not parsed:
            return default
        try:
            return [type_(v) for v in parsed]
        except (ValueError, TypeError):
            raise InvalidArgument(
                f"One or more arguments cannot be cast to {type_.__name__} (in `{arg}`)"
            )
Example #6
0
    def new(cls, character, name, minv=None, maxv=None, reset=None, display_type=None, live_id=None):
        if reset not in ('short', 'long', 'none', None):
            raise InvalidArgument("Invalid reset.")
        if any(c in name for c in ".$"):
            raise InvalidArgument("Invalid character in CC name.")
        if minv is not None and maxv is not None:
            max_value = character.evaluate_cvar(maxv)
            if max_value < character.evaluate_cvar(minv):
                raise InvalidArgument("Max value is less than min value.")
            if max_value == 0:
                raise InvalidArgument("Max value cannot be 0.")
        if reset and maxv is None:
            raise InvalidArgument("Reset passed but no maximum passed.")
        if display_type == 'bubble' and (maxv is None or minv is None):
            raise InvalidArgument("Bubble display requires a max and min value.")

        value = character.evaluate_cvar(maxv) or 0
        return cls(character, name.strip(), value, minv, maxv, reset, display_type, live_id)
    def create_consumable(self, name, **kwargs):
        """Creates a custom consumable, returning the character object."""
        self._initialize_custom_counters()
        _max = kwargs.get('maxValue')
        _min = kwargs.get('minValue')
        _reset = kwargs.get('reset')
        _type = kwargs.get('displayType')
        _live_id = kwargs.get('live')
        if not (_reset in ('short', 'long', 'none') or _reset is None):
            raise InvalidArgument("Invalid reset.")
        if any(c in name for c in ".$"):
            raise InvalidArgument("Invalid character in CC name.")
        if _max is not None and _min is not None:
            maxV = self.evaluate_cvar(_max)
            try:
                assert maxV >= self.evaluate_cvar(_min)
            except AssertionError:
                raise InvalidArgument("Max value is less than min value.")
            if maxV == 0:
                raise InvalidArgument("Max value cannot be 0.")
        if _reset and _max is None:
            raise InvalidArgument("Reset passed but no maximum passed.")
        if _type == 'bubble' and (_max is None or _min is None):
            raise InvalidArgument(
                "Bubble display requires a max and min value.")
        newCounter = {'value': self.evaluate_cvar(_max) or 0}
        if _max is not None: newCounter['max'] = _max
        if _min is not None: newCounter['min'] = _min
        if _reset and _max is not None: newCounter['reset'] = _reset
        newCounter['type'] = _type
        newCounter['live'] = _live_id
        log.debug(f"Creating new counter {newCounter}")

        self.character['consumables']['custom'][name] = newCounter

        return self
Example #8
0
    def run(self, autoctx):
        super().run(autoctx)
        if autoctx.target is None:
            raise TargetException(
                "Tried to add an effect without a target! Make sure all IEffect effects are inside "
                "of a Target effect.")

        if isinstance(self.duration, str):
            try:
                duration = autoctx.parse_intexpression(self.duration)
            except Exception:
                raise AutomationException(
                    f"{self.duration} is not an integer (in effect duration)")
        else:
            duration = self.duration

        if self.desc:
            desc = autoctx.parse_annostr(self.desc)
            if len(desc) > 500:
                desc = f"{desc[:500]}..."
        else:
            desc = None

        duration = autoctx.args.last('dur', duration, int)
        conc_conflict = []
        if isinstance(autoctx.target.target, init.Combatant):
            effect = init.Effect.new(autoctx.target.target.combat,
                                     autoctx.target.target,
                                     self.name,
                                     duration,
                                     autoctx.parse_annostr(self.effects),
                                     tick_on_end=self.tick_on_end,
                                     concentration=self.concentration,
                                     desc=desc)
            conc_parent = None
            stack_parent = None

            # concentration spells
            if autoctx.conc_effect:
                if autoctx.conc_effect.combatant is autoctx.target.target and self.concentration:
                    raise InvalidArgument(
                        "Concentration spells cannot add concentration effects to the caster."
                    )
                conc_parent = autoctx.conc_effect

            # stacking
            if self.stacking and (stack_parent :=
                                  autoctx.target.target.get_effect(
                                      effect.name, strict=True)):
                count = 2
                effect.desc = None
                effect.duration = effect.remaining = -1
                effect.concentration = False
                original_name = effect.name
                effect.name = f"{original_name} x{count}"
                while autoctx.target.target.get_effect(effect.name,
                                                       strict=True):
                    count += 1
                    effect.name = f"{original_name} x{count}"

            # parenting
            explicit_parent = None
            if self.parent is not None and (parent_ref := autoctx.metavars.get(
                    self.parent, None)) is not None:
                if not isinstance(parent_ref, IEffectMetaVar):
                    raise InvalidArgument(
                        f"Could not set IEffect parent: The variable `{self.parent}` is not an IEffectMetaVar "
                        f"(got `{type(parent_ref).__name__}`).")
                # noinspection PyProtectedMember
                explicit_parent = parent_ref._effect
Example #9
0
 def precreate_checks(name, code):
     if len(code) > ALIAS_SIZE_LIMIT:
         raise InvalidArgument(
             f"Aliases must be shorter than {ALIAS_SIZE_LIMIT} characters.")
     if ' ' in name:
         raise InvalidArgument("Alias names cannot contain spaces.")
Example #10
0
    def new(cls,
            character,
            name,
            minv=None,
            maxv=None,
            reset=None,
            display_type=None,
            live_id=None,
            reset_to=None,
            reset_by=None,
            title=None,
            desc=None):
        if reset not in ('short', 'long', 'none', None):
            raise InvalidArgument("Invalid reset.")
        if any(c in name for c in ".$"):
            raise InvalidArgument("Invalid character in CC name.")
        if display_type == 'bubble' and (maxv is None or minv is None):
            raise InvalidArgument(
                "Bubble display requires a max and min value.")

        # sanity checks
        if reset not in ('none', None) and (maxv is None and reset_to is None
                                            and reset_by is None):
            raise InvalidArgument(
                "Reset passed but no valid reset value (`max`, `resetto`, `resetby`) passed."
            )
        if reset_to is not None and reset_by is not None:
            raise InvalidArgument(
                "Both `resetto` and `resetby` arguments found.")
        if not name.strip():
            raise InvalidArgument("The name of the counter can not be empty.")

        min_value = None
        if minv is not None:
            min_value = character.evaluate_math(minv)

        max_value = None
        if maxv is not None:
            max_value = character.evaluate_math(maxv)
            if min_value is not None and max_value < min_value:
                raise InvalidArgument("Max value is less than min value.")
            if max_value == 0:
                raise InvalidArgument("Max value cannot be 0.")

        reset_to_value = None
        if reset_to is not None:
            reset_to_value = character.evaluate_math(reset_to)
            if min_value is not None and reset_to_value < min_value:
                raise InvalidArgument("Reset to value is less than min value.")
            if max_value is not None and reset_to_value > max_value:
                raise InvalidArgument(
                    "Reset to value is greater than max value.")

        if reset_by is not None:
            try:
                d20.parse(str(reset_by))
            except d20.RollSyntaxError:
                raise InvalidArgument(
                    f"{reset_by} (`resetby`) cannot be interpreted as a number or dice string."
                )

        # set initial value
        initial_value = max(0, min_value or 0)
        if reset_to_value is not None:
            initial_value = reset_to_value
        elif max_value is not None:
            initial_value = max_value

        # length checks
        if desc and len(desc) > 1024:
            raise InvalidArgument(
                'Description must be less than 1024 characters.')

        if title and len(title) >= 256:
            raise InvalidArgument('Title must be less than 256 characters.')

        if len(name) > 256:
            raise InvalidArgument('Name must be less than 256 characters.')

        return cls(character, name.strip(), initial_value, minv, maxv, reset,
                   display_type, live_id, reset_to, reset_by, title, desc)
Example #11
0
def parse_attack_arg(arg, name):
    data = arg.split('|')
    if not len(data) == 3:
        raise InvalidArgument("`attack` arg must be formatted `HIT|DAMAGE|TEXT`")
    return {'name': name, 'attackBonus': data[0] or None, 'damage': data[1] or None, 'details': data[2] or None}
Example #12
0
async def _alias_before_edit(ctx, name=None, delete=False):
    if name and name in ctx.bot.all_commands:
        raise InvalidArgument(
            f"`{name}` is already a builtin command. Try another name.")
Example #13
0
    async def select_details(self, ctx):
        author = ctx.author
        channel = ctx.channel

        def chk(m):
            return m.author == author and m.channel == channel

        await ctx.send(
            author.mention + " What race?",
            allowed_mentions=discord.AllowedMentions(users=[ctx.author]))
        try:
            race_response = await self.bot.wait_for('message',
                                                    timeout=90,
                                                    check=chk)
        except asyncio.TimeoutError:
            raise InvalidArgument("Timed out waiting for race.")
        race_choices = await get_race_choices(ctx)
        race = await search_and_select(ctx, race_choices,
                                       race_response.content, lambda e: e.name)

        await ctx.send(
            author.mention + " What class?",
            allowed_mentions=discord.AllowedMentions(users=[ctx.author]))
        try:
            class_response = await self.bot.wait_for('message',
                                                     timeout=90,
                                                     check=chk)
        except asyncio.TimeoutError:
            raise InvalidArgument("Timed out waiting for class.")
        class_choices = await available(ctx, compendium.classes, 'class')
        _class = await search_and_select(ctx, class_choices,
                                         class_response.content,
                                         lambda e: e.name)

        subclass_choices = await available(ctx, _class.subclasses, 'class')
        if subclass_choices:
            await ctx.send(
                author.mention + " What subclass?",
                allowed_mentions=discord.AllowedMentions(users=[ctx.author]))
            try:
                subclass_response = await self.bot.wait_for('message',
                                                            timeout=90,
                                                            check=chk)
            except asyncio.TimeoutError:
                raise InvalidArgument("Timed out waiting for subclass.")
            subclass = await search_and_select(ctx, subclass_choices,
                                               subclass_response.content,
                                               lambda e: e.name)
        else:
            subclass = None

        await ctx.send(
            author.mention + " What background?",
            allowed_mentions=discord.AllowedMentions(users=[ctx.author]))
        try:
            bg_response = await self.bot.wait_for('message',
                                                  timeout=90,
                                                  check=chk)
        except asyncio.TimeoutError:
            raise InvalidArgument("Timed out waiting for background.")
        background_choices = await available(ctx, compendium.backgrounds,
                                             'background')
        background = await search_and_select(ctx, background_choices,
                                             bg_response.content,
                                             lambda e: e.name)
        return race, _class, subclass, background
Example #14
0
File: spell.py Project: avrae/avrae
    async def cast(self, ctx, caster, targets, args, combat=None):
        """
        Casts this spell.

        :param ctx: The context of the casting.
        :param caster: The caster of this spell.
        :type caster: :class:`~cogs5e.models.sheet.statblock.StatBlock`
        :param targets: A list of targets
        :type targets: list of :class:`~cogs5e.models.sheet.statblock.StatBlock`
        :param args: Args
        :type args: :class:`~utils.argparser.ParsedArguments`
        :param combat: The combat the spell was cast in, if applicable.
        :rtype: CastResult
        """

        # generic args
        l = args.last('l', self.level, int)
        i = args.last('i', type_=bool)
        title = args.last('title')
        nopact = args.last('nopact', type_=bool)

        # meta checks
        if not self.level <= l <= 9:
            raise SpellException("Invalid spell level.")

        # caster spell-specific overrides
        dc_override = None
        ab_override = None
        spell_override = None
        is_prepared = True
        spellbook_spell = caster.spellbook.get_spell(self)
        if spellbook_spell is not None:
            dc_override = spellbook_spell.dc
            ab_override = spellbook_spell.sab
            spell_override = spellbook_spell.mod
            is_prepared = spellbook_spell.prepared

        if not i:
            # if I'm a warlock, and I didn't have any slots of this level anyway (#655)
            # automatically scale up to our pact slot level (or the next available level s.t. max > 0)
            if l > 0 \
                    and l == self.level \
                    and not caster.spellbook.get_max_slots(l) \
                    and not caster.spellbook.can_cast(self, l):
                if caster.spellbook.pact_slot_level is not None:
                    l = caster.spellbook.pact_slot_level
                else:
                    l = next((sl for sl in range(l, 6)
                              if caster.spellbook.get_max_slots(sl)),
                             l)  # only scale up to l5
                args['l'] = l

            # can I cast this spell?
            if not caster.spellbook.can_cast(self, l):
                embed = EmbedWithAuthor(ctx)
                embed.title = "Cannot cast spell!"
                if not caster.spellbook.get_slots(l):
                    # out of spell slots
                    err = (
                        f"You don't have enough level {l} slots left! Use `-l <level>` to cast at a different "
                        f"level, `{ctx.prefix}g lr` to take a long rest, or `-i` to ignore spell slots!"
                    )
                elif self.name not in caster.spellbook:
                    # don't know spell
                    err = (
                        f"You don't know this spell! Use `{ctx.prefix}sb add {self.name}` to add it to your "
                        f"spellbook, or pass `-i` to ignore restrictions.")
                else:
                    # ?
                    err = (
                        "Not enough spell slots remaining, or spell not in known spell list!\n"
                        f"Use `{ctx.prefix}game longrest` to restore all spell slots if this is a character, "
                        f"or pass `-i` to ignore restrictions.")
                embed.description = err
                if l > 0:
                    embed.add_field(name="Spell Slots",
                                    value=caster.spellbook.remaining_casts_of(
                                        self, l))
                return CastResult(embed=embed,
                                  success=False,
                                  automation_result=None)

            # #1000: is this spell prepared (soft check)?
            if not is_prepared:
                skip_prep_conf = await confirm(
                    ctx,
                    f"{self.name} is not prepared. Do you want to cast it anyway? (Reply with yes/no)",
                    delete_msgs=True)
                if not skip_prep_conf:
                    embed = EmbedWithAuthor(
                        ctx,
                        title=f"Cannot cast spell!",
                        description=
                        f"{self.name} is not prepared! Prepare it on your character sheet and use "
                        f"`{ctx.prefix}update` to mark it as prepared, or use `-i` to ignore restrictions."
                    )
                    return CastResult(embed=embed,
                                      success=False,
                                      automation_result=None)

            # use resource
            caster.spellbook.cast(self, l, pact=not nopact)

        # base stat stuff
        mod_arg = args.last("mod", type_=int)
        with_arg = args.last("with")
        stat_override = ''
        if mod_arg is not None:
            mod = mod_arg
            prof_bonus = caster.stats.prof_bonus
            dc_override = 8 + mod + prof_bonus
            ab_override = mod + prof_bonus
            spell_override = mod
        elif with_arg is not None:
            if with_arg not in STAT_ABBREVIATIONS:
                raise InvalidArgument(
                    f"{with_arg} is not a valid stat to cast with.")
            mod = caster.stats.get_mod(with_arg)
            dc_override = 8 + mod + caster.stats.prof_bonus
            ab_override = mod + caster.stats.prof_bonus
            spell_override = mod
            stat_override = f" with {verbose_stat(with_arg)}"

        # begin setup
        embed = discord.Embed()
        if title:
            embed.title = title.replace('[name]', caster.name) \
                .replace('[aname]', self.name) \
                .replace('[sname]', self.name) \
                .replace('[verb]', 'casts')  # #1514, [aname] is action name now, #1587, add verb to action/cast
        else:
            embed.title = f"{caster.get_title_name()} casts {self.name}{stat_override}!"
        if targets is None:
            targets = [None]

        # concentration
        noconc = args.last("noconc", type_=bool)
        conc_conflict = None
        conc_effect = None
        if all((self.concentration, isinstance(caster, BaseCombatant), combat,
                not noconc)):
            duration = args.last('dur', self.get_combat_duration(), int)
            conc_effect = Effect.new(combat, caster, self.name, duration, "",
                                     True)
            effect_result = caster.add_effect(conc_effect)
            conc_conflict = effect_result['conc_conflict']

        # run
        automation_result = None
        if self.automation and self.automation.effects:
            title = f"{caster.name} cast {self.name}!"
            automation_result = await self.automation.run(
                ctx,
                embed,
                caster,
                targets,
                args,
                combat,
                self,
                conc_effect=conc_effect,
                ab_override=ab_override,
                dc_override=dc_override,
                spell_override=spell_override,
                title=title)
        else:  # no automation, display spell description
            phrase = args.join('phrase', '\n')
            if phrase:
                embed.description = f"*{phrase}*"
            embed.add_field(name="Description",
                            value=smart_trim(self.description),
                            inline=False)
            embed.set_footer(text="No spell automation found.")

        if l != self.level and self.higherlevels:
            embed.add_field(name="At Higher Levels",
                            value=smart_trim(self.higherlevels),
                            inline=False)

        if l > 0 and not i:
            embed.add_field(name="Spell Slots",
                            value=caster.spellbook.remaining_casts_of(self, l))

        if conc_conflict:
            conflicts = ', '.join(e.name for e in conc_conflict)
            embed.add_field(name="Concentration",
                            value=f"Dropped {conflicts} due to concentration.")

        if 'thumb' in args:
            embed.set_thumbnail(url=maybe_http_url(args.last('thumb', '')))
        elif self.image:
            embed.set_thumbnail(url=self.image)

        add_fields_from_args(embed, args.get('f'))
        gamedata.lookuputils.handle_source_footer(embed,
                                                  self,
                                                  add_source_str=False)

        return CastResult(embed=embed,
                          success=True,
                          automation_result=automation_result)
Example #15
0
    def new(cls,
            character,
            name,
            minv=None,
            maxv=None,
            reset=None,
            display_type=None,
            live_id=None,
            reset_to=None,
            reset_by=None):
        if reset not in ('short', 'long', 'none', None):
            raise InvalidArgument("Invalid reset.")
        if any(c in name for c in ".$"):
            raise InvalidArgument("Invalid character in CC name.")
        if display_type == 'bubble' and (maxv is None or minv is None):
            raise InvalidArgument(
                "Bubble display requires a max and min value.")

        # sanity checks
        if maxv is None and reset not in ('none', None):
            raise InvalidArgument("Reset passed but no maximum passed.")
        if reset_to is not None and reset_by is not None:
            raise InvalidArgument(
                "Both `resetto` and `resetby` arguments found.")

        min_value = None
        if minv is not None:
            min_value = character.evaluate_math(minv)

        max_value = None
        if maxv is not None:
            max_value = character.evaluate_math(maxv)
            if min_value is not None and max_value < min_value:
                raise InvalidArgument("Max value is less than min value.")
            if max_value == 0:
                raise InvalidArgument("Max value cannot be 0.")

        reset_to_value = None
        if reset_to is not None:
            reset_to_value = character.evaluate_math(reset_to)
            if min_value is not None and reset_to_value < min_value:
                raise InvalidArgument("Reset to value is less than min value.")
            if max_value is not None and reset_to_value > max_value:
                raise InvalidArgument(
                    "Reset to value is greater than max value.")

        if reset_by is not None:
            try:
                d20.parse(reset_by)
            except d20.RollSyntaxError:
                raise InvalidArgument(
                    f"{reset_by} (`resetby`) cannot be interpreted as a number or dice string."
                )

        # set initial value
        initial_value = max(0, min_value or 0)
        if reset_to_value is not None:
            initial_value = reset_to_value
        elif max_value is not None:
            initial_value = max_value

        return cls(character, name.strip(), initial_value, minv, maxv, reset,
                   display_type, live_id, reset_to, reset_by)
Example #16
0
    async def customcounter(self, ctx, name=None, *, modifier=None):
        """Commands to implement custom counters.
        If a modifier is not supplied, prints the value and metadata of the counter *name*.
        Otherwise, changes the counter *name* by *modifier*. Supports dice.

        The following can be put after the counter *name* to change how the *modifier* is applied:
        `mod` - Add *modifier* counter value
        `set` - Sets the counter value to *modifier*

        *Ex:*
        `!cc Test 1`
        `!cc Test -2*2d4`
        `!cc Test set 1d4`

        """
        if name is None:
            return await self.customcounter_summary(ctx)
        character: Character = await Character.from_ctx(ctx)
        counter = await character.select_consumable(ctx, name)

        cc_embed_title = counter.title if counter.title is not None else counter.name

        # replace [name] in title
        cc_embed_title = cc_embed_title.replace('[name]', character.name)

        if modifier is None:  # display value
            counter_display_embed = EmbedWithCharacter(character)
            counter_display_embed.add_field(name=counter.name, value=counter.full_str())
            if counter.desc:
                counter_display_embed.add_field(name='Description', value=counter.desc, inline=False)
            return await ctx.send(embed=counter_display_embed)

        operator = None
        if ' ' in modifier:
            m = modifier.split(' ')
            operator = m[0]
            modifier = m[-1]

        roll_text = ''
        try:
            result = int(modifier)
        except ValueError:
            try:  # if we're not a number, are we dice
                roll_result = d20.roll(str(modifier))
                result = roll_result.total
                roll_text = f"\nRoll: {roll_result}"
            except d20.RollSyntaxError:
                raise InvalidArgument(
                    f"Could not modify counter: {modifier} cannot be interpreted as a number or dice string.")

        old_value = counter.value
        result_embed = EmbedWithCharacter(character)
        if not operator or operator == 'mod':
            new_value = counter.value + result
        elif operator == 'set':
            new_value = result
        else:
            return await ctx.send("Invalid operator. Use mod or set.")

        counter.set(new_value)
        await character.commit(ctx)

        delta = f"({counter.value - old_value:+})"
        out = f"{str(counter)} {delta}{roll_text}"

        if new_value - counter.value:  # we overflowed somewhere
            out += f"\n({abs(new_value - counter.value)} overflow)"

        result_embed.add_field(name=cc_embed_title, value=out)

        if counter.desc:
            result_embed.add_field(name='Description', value=counter.desc, inline=False)

        await try_delete(ctx.message)
        await ctx.send(embed=result_embed)
Example #17
0
    def run(self, autoctx):
        super().run(autoctx)
        if autoctx.target is None:
            raise TargetException(
                "Tried to add an effect without a target! Make sure all IEffect effects are inside "
                "of a Target effect.")

        if isinstance(self.duration, str):
            try:
                duration = autoctx.parse_intexpression(self.duration)
            except Exception:
                raise AutomationException(
                    f"{self.duration} is not an integer (in effect duration)")
        else:
            duration = self.duration

        if self.desc:
            desc = autoctx.parse_annostr(self.desc)
            if len(desc) > 500:
                desc = f"{desc[:500]}..."
        else:
            desc = None

        duration = autoctx.args.last('dur', duration, int)
        conc_conflict = []
        if isinstance(autoctx.target.target, init.Combatant):
            effect = init.Effect.new(autoctx.target.target.combat,
                                     autoctx.target.target,
                                     self.name,
                                     duration,
                                     autoctx.parse_annostr(self.effects),
                                     tick_on_end=self.tick_on_end,
                                     concentration=self.concentration,
                                     desc=desc)

            # concentration spells
            if autoctx.conc_effect:
                if autoctx.conc_effect.combatant is autoctx.target.target and self.concentration:
                    raise InvalidArgument(
                        "Concentration spells cannot add concentration effects to the caster."
                    )
                effect.set_parent(autoctx.conc_effect)

            # stacking
            if self.stacking and (stack_parent :=
                                  autoctx.target.target.get_effect(
                                      effect.name, strict=True)):
                count = 2
                effect.desc = None
                effect.duration = effect.remaining = -1
                effect.concentration = False
                effect.set_parent(stack_parent)
                original_name = effect.name
                effect.name = f"{original_name} x{count}"
                while autoctx.target.target.get_effect(effect.name,
                                                       strict=True):
                    count += 1
                    effect.name = f"{original_name} x{count}"

            # add
            effect_result = autoctx.target.target.add_effect(effect)
            autoctx.queue(f"**Effect**: {effect.get_str(description=False)}")
            if conc_conflict := effect_result['conc_conflict']:
                autoctx.queue(
                    f"**Concentration**: dropped {', '.join([e.name for e in conc_conflict])}"
                )
Example #18
0
 def set_cvar(self, name: str, val: str):
     """Sets a cvar to a string value."""
     if not name.isidentifier():
         raise InvalidArgument("Cvar name must be a valid identifier "
                               "(contains only a-z, A-Z, 0-9, and _, and not start with a number).")
     self.cvars[name] = str(val)
Example #19
0
async def create_alias(ctx, alias_name, commands):
    commands = str(commands)
    if len(commands) > ALIAS_SIZE_LIMIT:
        raise InvalidArgument(f"Aliases must be shorter than {ALIAS_SIZE_LIMIT} characters.")
    await ctx.bot.mdb.aliases.update_one({"owner": str(ctx.author.id), "name": alias_name},
                                         {"$set": {"commands": commands}}, True)
Example #20
0
 def set_uvar(self, name, val: str):
     if any(c in name for c in '/()[]\\.^$*+?|{}'):
         raise InvalidArgument("Cvar contains invalid character.")
     self._cache['uvars'][name] = str(val)
     self.names[name] = str(val)
     self.uvars_changed.add(name)
Example #21
0
    async def cast(self, ctx, caster, targets, args, combat=None):
        """
        Casts this spell.

        :param ctx: The context of the casting.
        :param caster: The caster of this spell.
        :type caster: :class:`~cogs5e.models.sheet.statblock.StatBlock`
        :param targets: A list of targets
        :type targets: list of :class:`~cogs5e.models.sheet.statblock.StatBlock`
        :param args: Args
        :type args: :class:`~utils.argparser.ParsedArguments`
        :param combat: The combat the spell was cast in, if applicable.
        :return: {embed: Embed}
        """

        # generic args
        l = args.last('l', self.level, int)
        i = args.last('i', type_=bool)
        title = args.last('title')

        # meta checks
        if not self.level <= l <= 9:
            raise SpellException("Invalid spell level.")

        # caster spell-specific overrides
        dc_override = None
        ab_override = None
        spell_override = None
        spellbook_spell = caster.spellbook.get_spell(self)
        if spellbook_spell is not None:
            dc_override = spellbook_spell.dc
            ab_override = spellbook_spell.sab
            spell_override = spellbook_spell.mod

        if not i:
            # if I'm a warlock, and I didn't have any slots of this level anyway (#655)
            # automatically scale up to the next level s.t. our slots are not 0
            if l > 0 \
                    and l == self.level \
                    and not caster.spellbook.get_max_slots(l) \
                    and not caster.spellbook.can_cast(self, l):
                l = next((sl for sl in range(l, 6)
                          if caster.spellbook.get_max_slots(sl)),
                         l)  # only scale up to l5
                args['l'] = l

            # can I cast this spell?
            if not caster.spellbook.can_cast(self, l):
                embed = EmbedWithAuthor(ctx)
                embed.title = "Cannot cast spell!"
                if not caster.spellbook.get_slots(l):
                    # out of spell slots
                    err = f"You don't have enough level {l} slots left! Use `-l <level>` to cast at a different level, " \
                          f"`{ctx.prefix}g lr` to take a long rest, or `-i` to ignore spell slots!"
                elif self.name not in caster.spellbook:
                    # don't know spell
                    err = f"You don't know this spell! Use `{ctx.prefix}sb add {self.name}` to add it to your spellbook, " \
                          f"or pass `-i` to ignore restrictions."
                else:
                    # ?
                    err = "Not enough spell slots remaining, or spell not in known spell list!\n" \
                          f"Use `{ctx.prefix}game longrest` to restore all spell slots if this is a character, " \
                          f"or pass `-i` to ignore restrictions."
                embed.description = err
                if l > 0:
                    embed.add_field(name="Spell Slots",
                                    value=caster.spellbook.remaining_casts_of(
                                        self, l))
                return {"embed": embed}

            # use resource
            caster.spellbook.cast(self, l)

        # base stat stuff
        mod_arg = args.last("mod", type_=int)
        with_arg = args.last("with")
        stat_override = ''
        if mod_arg is not None:
            mod = mod_arg
            prof_bonus = caster.stats.prof_bonus
            dc_override = 8 + mod + prof_bonus
            ab_override = mod + prof_bonus
            spell_override = mod
        elif with_arg is not None:
            if with_arg not in STAT_ABBREVIATIONS:
                raise InvalidArgument(
                    f"{with_arg} is not a valid stat to cast with.")
            mod = caster.stats.get_mod(with_arg)
            dc_override = 8 + mod + caster.stats.prof_bonus
            ab_override = mod + caster.stats.prof_bonus
            spell_override = mod
            stat_override = f" with {verbose_stat(with_arg)}"

        # begin setup
        embed = discord.Embed()
        if title:
            embed.title = title.replace('[sname]', self.name)
        else:
            embed.title = f"{caster.get_title_name()} casts {self.name}{stat_override}!"
        if targets is None:
            targets = [None]

        # concentration
        noconc = args.last("noconc", type_=bool)
        conc_conflict = None
        conc_effect = None
        if all((self.concentration, isinstance(caster, init.Combatant), combat,
                not noconc)):
            duration = args.last('dur', self.get_combat_duration(), int)
            conc_effect = initiative.Effect.new(combat, caster, self.name,
                                                duration, "", True)
            effect_result = caster.add_effect(conc_effect)
            conc_conflict = effect_result['conc_conflict']

        if self.automation and self.automation.effects:
            title = f"{caster.name} cast {self.name}!"
            await self.automation.run(ctx,
                                      embed,
                                      caster,
                                      targets,
                                      args,
                                      combat,
                                      self,
                                      conc_effect=conc_effect,
                                      ab_override=ab_override,
                                      dc_override=dc_override,
                                      spell_override=spell_override,
                                      title=title)
        else:
            phrase = args.join('phrase', '\n')
            if phrase:
                embed.description = f"*{phrase}*"

            text = trim_str(self.description, 1024)
            embed.add_field(name="Description", value=text, inline=False)
            if l != self.level and self.higherlevels:
                embed.add_field(name="At Higher Levels",
                                value=trim_str(self.higherlevels, 1024),
                                inline=False)
            embed.set_footer(text="No spell automation found.")

        if l > 0 and not i:
            embed.add_field(name="Spell Slots",
                            value=caster.spellbook.remaining_casts_of(self, l))

        if conc_conflict:
            conflicts = ', '.join(e.name for e in conc_conflict)
            embed.add_field(name="Concentration",
                            value=f"Dropped {conflicts} due to concentration.")

        if 'thumb' in args:
            embed.set_thumbnail(url=args.last('thumb'))
        elif self.image:
            embed.set_thumbnail(url=self.image)

        add_fields_from_args(embed, args.get('f'))
        gamedata.lookuputils.handle_source_footer(embed,
                                                  self,
                                                  add_source_str=False)

        return {"embed": embed}