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)}")
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)
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.')
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)
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}`)" )
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
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
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.")
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)
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}
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.")
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
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)
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)
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)
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])}" )
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)
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)
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)
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}