def run(self, autoctx): super().run(autoctx) spell = self.spell if spell is None: autoctx.meta_queue(f"**Error**: Spell {self.id} not found.") return CastSpellResult(success=False) cast_level = self.level if self.level is not None else spell.level if not spell.level <= cast_level <= 9: autoctx.meta_queue( f"**Error**: Unable to cast {spell.name} at level {cast_level} (invalid level)." ) return CastSpellResult(success=False, spell=spell) if autoctx.is_spell: autoctx.meta_queue( f"**Error**: Unable to cast another spell inside a spell.") return CastSpellResult(success=False, spell=spell) if spell.automation and spell.automation.effects: # save old autoctx values old_ab_override = autoctx.ab_override old_dc_override = autoctx.dc_override old_spell_override = autoctx.evaluator.builtins.get('spell') old_level_override = autoctx.spell_level_override # run the spell using the given values if self.attack_bonus is not None: autoctx.ab_override = autoctx.parse_intexpression( self.attack_bonus) if self.dc is not None: autoctx.dc_override = autoctx.parse_intexpression(self.dc) if self.casting_mod is not None: autoctx.evaluator.builtins[ 'spell'] = autoctx.parse_intexpression(self.casting_mod) if self.level is not None: autoctx.spell_level_override = self.level autoctx.spell = spell results = self.run_children(spell.automation.effects, autoctx) # and restore them autoctx.ab_override = old_ab_override autoctx.dc_override = old_dc_override autoctx.evaluator.builtins['spell'] = old_spell_override autoctx.spell_level_override = old_level_override autoctx.spell = None else: # copied from Spell.cast results = [] autoctx.queue(smart_trim(spell.description)) autoctx.push_embed_field(title=spell.name) if cast_level != spell.level and spell.higherlevels: autoctx.queue(smart_trim(spell.higherlevels)) autoctx.push_embed_field(title="At Higher Levels") return CastSpellResult(success=True, spell=spell, children=results)
async def subclass(self, ctx, *, name: str): """Looks up a subclass.""" result: gamedata.Subclass = await self._lookup_search3( ctx, {'class': compendium.subclasses}, name, query_type='subclass' ) embed = EmbedWithAuthor(ctx) embed.url = result.url embed.title = result.name embed.description = smart_trim(result.description, 2048) for level in result.levels: for feature in level: text = smart_trim(feature.text, 1024) embed.add_field(name=feature.name, value=text, inline=False) handle_source_footer( embed, result, f"Use {ctx.prefix}classfeat to look up a feature if it is cut off", add_source_str=True ) await (await self._get_destination(ctx)).send(embed=embed)
def _get_custom_counters(self): out = [] for cons in self.character_data['consumables']: live_id = f"{cons['id']}-{cons['typeId']}" display_type = 'bubble' if cons['max'] < 7 else None reset = RESET_MAP.get(cons['reset'], 'long') name = cons['name'].replace('\u2019', "'").strip() desc = None source_feature_type = cons['sourceFeatureType'] source_feature_id = cons['sourceFeatureId'] if cons['desc'] is not None: desc = smart_trim( html_to_md(cons['desc'].replace('\u2019', "'"))) source_feature = compendium.lookup_entity(source_feature_type, source_feature_id) log.debug( f"Processed counter named {name!r} for feature {source_feature}" ) if source_feature is None: log.warning( f"Could not find source feature ({source_feature_type}, {source_feature_id}) for counter " f"named {name!r}") if cons['max'] and name: # don't make counters with a range of 0 - 0, or blank named counters out.append( CustomCounter(None, name, cons['value'], minv='0', maxv=str(cons['max']), reset=reset, display_type=display_type, live_id=live_id, desc=desc, ddb_source_feature_type=source_feature_type, ddb_source_feature_id=source_feature_id)) return [cc.to_dict() for cc in out]
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)