def precreate_checks(name, code): if len(code) > SNIPPET_SIZE_LIMIT: raise InvalidArgument(f"Snippets must be shorter than {SNIPPET_SIZE_LIMIT} characters.") if len(name) < 2: raise InvalidArgument("Snippet names must be at least 2 characters long.") if ' ' in name: raise InvalidArgument("Snippet names cannot contain spaces.")
def run(self, autoctx): super(IEffect, self).run(autoctx) if isinstance(self.duration, str): try: duration = int( autoctx.parse_annostr(self.duration, is_full_expression=True)) 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, 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) 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) effect_result = autoctx.target.target.add_effect(effect) autoctx.queue(f"**Effect**: {str(effect)}") if conc_conflict := effect_result['conc_conflict']: autoctx.queue( f"**Concentration**: dropped {', '.join([e.name for e in conc_conflict])}" )
async def replace(self, expr): """Replaces expressions that start with c: or s: with dice expressions for the character's check/save.""" skill_match = INLINE_SKILL_RE.match(expr) if skill_match is None: return expr, None character = await self._get_character() check_search = skill_match.group(2).lower() if skill_match.group(1) == 'c': skill_key = next((c for c in constants.SKILL_NAMES if c.lower().startswith(check_search)), None) if skill_key is None: raise InvalidArgument( f"`{check_search}` is not a valid skill.") skill = character.skills[skill_key] skill_name = f"{camel_to_title(skill_key)} Check" check_dice = skill.d20( reroll=character.options.reroll, min_val=10 * bool(character.options.talent and skill.prof >= 1)) else: try: skill = character.saves.get(check_search) skill_name = f"{verbose_stat(check_search[:3]).title()} Save" except ValueError: raise InvalidArgument(f"`{check_search}` is not a valid save.") check_dice = skill.d20(reroll=character.options.reroll) rest_of_expr = expr[skill_match.end():] return f"{check_dice}{rest_of_expr}", skill_name
async def update_gvar(ctx, gid, value): value = str(value) gvar = await ctx.bot.mdb.gvars.find_one({"key": gid}) if gvar is None: raise InvalidArgument("Global variable not found.") elif gvar['owner'] != str(ctx.author.id) and not str(ctx.author.id) in gvar.get('editors', []): raise NotAllowed("You are not allowed to edit this variable.") elif len(value) > GVAR_SIZE_LIMIT: raise InvalidArgument(f"Gvars must be shorter than {GVAR_SIZE_LIMIT} characters.") await ctx.bot.mdb.gvars.update_one({"key": gid}, {"$set": {"value": value}})
def set_cvar(character, name, value): value = str(value) if not name.isidentifier(): raise InvalidArgument("Cvar names must be identifiers " "(only contain a-z, A-Z, 0-9, _, and not start with a number).") elif name in character.get_scope_locals(True): raise InvalidArgument(f"The variable `{name}` is already built in.") elif len(value) > CVAR_SIZE_LIMIT: raise InvalidArgument(f"Cvars must be shorter than {CVAR_SIZE_LIMIT} characters.") character.set_cvar(name, value)
async def set_uvar(ctx, name, value): value = str(value) if not name.isidentifier(): raise InvalidArgument("Uvar names must be valid identifiers " "(only contain a-z, A-Z, 0-9, _, and not start with a number).") elif len(value) > UVAR_SIZE_LIMIT: raise InvalidArgument(f"Uvars must be shorter than {UVAR_SIZE_LIMIT} characters.") await ctx.bot.mdb.uvars.update_one( {"owner": str(ctx.author.id), "name": name}, {"$set": {"value": value}}, True)
async def create_servsnippet(ctx, snipname, snippet): snippet = str(snippet) if len(snippet) > SNIPPET_SIZE_LIMIT: raise InvalidArgument(f"Snippets must be shorter than {SNIPPET_SIZE_LIMIT} characters.") elif len(snipname) < 2: raise InvalidArgument("Snippet names must be at least 2 characters long.") elif ' ' in snipname: raise InvalidArgument("Snippet names cannot contain spaces.") await ctx.bot.mdb.servsnippets.update_one({"server": str(ctx.guild.id), "name": snipname}, {"$set": {"snippet": snippet}}, True)
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?") try: race_response = await self.bot.wait_for('message', timeout=90, check=chk) except asyncio.TimeoutError: raise InvalidArgument("Timed out waiting for race.") race = await search_and_select(ctx, c.fancyraces, race_response.content, lambda e: e.name) await ctx.send(author.mention + " What class?") try: class_response = await self.bot.wait_for('message', timeout=90, check=chk) except asyncio.TimeoutError: raise InvalidArgument("Timed out waiting for class.") _class = await search_and_select(ctx, c.classes, class_response.content, lambda e: e['name']) if 'subclasses' in _class: await ctx.send(author.mention + " What subclass?") 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, _class['subclasses'], subclass_response.content, lambda e: e['name']) else: subclass = None await ctx.send(author.mention + " What background?") try: bg_response = await self.bot.wait_for('message', timeout=90, check=chk) except asyncio.TimeoutError: raise InvalidArgument("Timed out waiting for background.") background = await search_and_select(ctx, c.backgrounds, bg_response.content, lambda e: e.name) return race, _class, subclass, background
async def set_svar(ctx, name, value): if ctx.guild is None: raise NotAllowed("You cannot set a svar in a private message.") value = str(value) if not name.isidentifier(): raise InvalidArgument("Svar names must be valid identifiers " "(only contain a-z, A-Z, 0-9, _, and not start with a number).") elif len(value) > SVAR_SIZE_LIMIT: raise InvalidArgument(f"Svars must be shorter than {SVAR_SIZE_LIMIT} characters.") await ctx.bot.mdb.svars.update_one( {"owner": ctx.guild.id, "name": name}, {"$set": {"value": value}}, True)
def add_known_spell(self, spell, dc: int = None, sab: int = None, mod: int = None): """Adds a spell to the character's known spell list.""" if spell.name in self.spellbook: raise InvalidArgument("You already know this spell.") sbs = SpellbookSpell.from_spell(spell, dc, sab, mod) self.spellbook.spells.append(sbs) self.overrides.spells.append(sbs)
def run(self, autoctx): super(IEffect, self).run(autoctx) 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) 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) 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 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)
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
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: list of int """ try: save = caster.saves.get(save_key) save_name = f"{verbose_stat(save_key[:3]).title()} 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') return _run_common(save, args, embed, rr_format="Save {}")
def run(self, autoctx): super(IEffect, self).run(autoctx) if isinstance(self.duration, str): try: self.duration = int(autoctx.parse_annostr(self.duration)) except ValueError: raise InvalidArgument( f"{self.duration} is not an integer (in effect duration)") duration = autoctx.args.last('dur', self.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 parse_stat_choice(args, _): for i, arg in enumerate(args): if arg == 'True': # hack: sadv/sdis on their own should be equivalent to -sadv/sdis all args[i] = arg = 'all' if arg not in STAT_ABBREVIATIONS and arg != 'all': raise InvalidArgument(f"{arg} is not a valid stat") return args
def set_cvar(self, name, val: str): """Sets a cvar to a string value.""" if any(c in name for c in '/()[]\\.^$*+?|{}'): raise InvalidArgument("Cvar contains invalid character.") self.character['cvars'] = self.character.get('cvars', {}) # set value self.character['cvars'][name] = str(val) return self
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 set_uvar(ctx, name, value): if not name.isidentifier(): raise InvalidArgument("Uvar names must be valid identifiers " "(only contain a-z, A-Z, 0-9, _, and not start with a number).") await ctx.bot.mdb.uvars.update_one( {"owner": str(ctx.author.id), "name": name}, {"$set": {"value": value}}, True)
def add_known_spell(self, spell): """Adds a spell to the character's known spell list. :param spell (Spell) - the Spell. :returns self""" if spell.name in self.spellbook: raise InvalidArgument("You already know this spell.") sbs = SpellbookSpell.from_spell(spell) self.spellbook.spells.append(sbs) self.overrides.spells.append(sbs)
def run(self, autoctx): super(Roll, self).run(autoctx) d = autoctx.args.join('d', '+') maxdmg = autoctx.args.last('max', None, bool) # add on combatant damage effects (#224) if autoctx.combatant: effect_d = '+'.join(autoctx.combatant.active_effects('d')) if effect_d: if d: d = f"{d}+{effect_d}" else: d = effect_d dice = self.dice if self.cantripScale: def cantrip_scale(matchobj): level = autoctx.caster.spellcasting.casterLevel if level < 5: levelDice = "1" elif level < 11: levelDice = "2" elif level < 17: levelDice = "3" else: levelDice = "4" return levelDice + 'd' + matchobj.group(2) dice = re.sub(r'(\d+)d(\d+)', cantrip_scale, dice) if self.higher and not autoctx.get_cast_level() == autoctx.spell.level: higher = self.higher.get(str(autoctx.get_cast_level())) if higher: dice = f"{dice}+{higher}" if d and not self.hidden: dice = f"{dice}+{d}" if maxdmg: def maxSub(matchobj): return f"{matchobj.group(1)}d{matchobj.group(2)}mi{matchobj.group(2)}" dice = re.sub(r'(\d+)d(\d+)', maxSub, dice) rolled = roll(dice, rollFor=self.name.title(), inline=True, show_blurbs=False) if not self.hidden: autoctx.meta_queue(rolled.result) if not rolled.raw_dice: raise InvalidArgument( f"Invalid roll in meta roll: {rolled.result}") autoctx.metavars[self.name] = rolled.consolidated()
async def create_gvar(ctx, value): value = str(value) if len(value) > GVAR_SIZE_LIMIT: raise InvalidArgument(f"Gvars must be shorter than {GVAR_SIZE_LIMIT} characters.") name = str(uuid.uuid4()) data = {'key': name, 'owner': str(ctx.author.id), 'owner_name': str(ctx.author), 'value': value, 'editors': []} await ctx.bot.mdb.gvars.insert_one(data) return name
def parse_stat_choice(args, _): for i, arg in enumerate(args): if arg == 'True': # hack: sadv/sdis on their own should be equivalent to -sadv/sdis all args[i] = arg = 'all' else: args[i] = arg = arg[:3].lower( ) # only check first three arg characters against STAT_ABBREVIATIONS if arg not in STAT_ABBREVIATIONS and arg != 'all': raise InvalidArgument(f"{arg} is not a valid stat") return args
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?") 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?") 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?") 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?") 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
def remove_known_spell(self, sb_spell): """ Removes a spell from the character's spellbook override. :param sb_spell: The spell to remove. :type sb_spell SpellbookSpell """ if sb_spell not in self.overrides.spells: raise InvalidArgument("This spell is not in the overrides.") self.overrides.spells.remove(sb_spell) spell_in_book = next(s for s in self.spellbook.spells if s.name == sb_spell.name) self.spellbook.spells.remove(spell_in_book)
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 create_servalias(ctx, alias_name, commands): if len(commands) > ALIAS_SIZE_LIMIT: raise InvalidArgument( f"Aliases must be shorter than {ALIAS_SIZE_LIMIT} characters.") await ctx.bot.mdb.servaliases.update_one( { "server": str(ctx.guild.id), "name": alias_name }, {"$set": { "commands": commands.lstrip('!') }}, True)
def run(self, autoctx): super(Roll, self).run(autoctx) d = autoctx.args.join('d', '+', ephem=True) maxdmg = autoctx.args.last('max', None, bool, ephem=True) mi = autoctx.args.last('mi', None, int) # add on combatant damage effects (#224) if autoctx.combatant: effect_d = '+'.join(autoctx.combatant.active_effects('d')) if effect_d: if d: d = f"{d}+{effect_d}" else: d = effect_d dice = self.dice if autoctx.is_spell: if self.cantripScale: dice = autoctx.cantrip_scale(dice) if self.higher and not autoctx.get_cast_level( ) == autoctx.spell.level: higher = self.higher.get(str(autoctx.get_cast_level())) if higher: dice = f"{dice}+{higher}" if not self.hidden: # -mi # (#527) if mi: dice = re.sub(r'(\d+d\d+)', rf'\1mi{mi}', dice) if d: dice = f"{dice}+{d}" if maxdmg: def maxSub(matchobj): return f"{matchobj.group(1)}d{matchobj.group(2)}mi{matchobj.group(2)}" dice = re.sub(r'(\d+)d(\d+)', maxSub, dice) rolled = roll(dice, rollFor=self.name.title(), inline=True, show_blurbs=False) if not self.hidden: autoctx.meta_queue(rolled.result) if not rolled.raw_dice: raise InvalidArgument( f"Invalid roll in meta roll: {rolled.result}") autoctx.metavars[self.name] = rolled.consolidated()
def evaluate_math(self, varstr): """Evaluates a cvar expression in a MathEvaluator. :param varstr - the expression to evaluate. :returns int - the value of the expression.""" varstr = str(varstr).strip('<>{}') evaluator = aliasing.evaluators.MathEvaluator.with_character(self) try: return int(evaluator.eval(varstr)) except Exception as e: raise InvalidArgument(f"Cannot evaluate {varstr}: {e}")
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 """ if save_key.startswith('death'): save = Skill(0) stat_name = stat = 'Death' save_name = 'Death Save' else: try: save = caster.saves.get(save_key) stat = save_key[:3] stat_name = verbose_stat(stat).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 handling if isinstance(caster, init.Combatant): # -sb args['b'] = args.get('b') + caster.active_effects('sb') # -sadv/sdis sadv_effects = caster.active_effects('sadv') sdis_effects = caster.active_effects('sdis') if 'all' in sadv_effects or stat in sadv_effects: args[ 'adv'] = True # Because adv() only checks last() just forcibly add them if 'all' in sdis_effects or stat in sdis_effects: args['dis'] = True result = _run_common(save, args, embed, rr_format="Save {}") return SaveResult(rolls=result.rolls, skill=save, skill_name=stat_name, skill_roll_result=result)