def __init__(self, owner: str, upstream: str, active: bool, sheet_type: str, import_version: int, name: str, description: str, image: str, stats: dict, levels: dict, attacks: list, skills: dict, resistances: dict, saves: dict, ac: int, max_hp: int, hp: int, temp_hp: int, cvars: dict, options: dict, overrides: dict, consumables: list, death_saves: dict, spellbook: dict, live, race: str, background: str, **kwargs): if kwargs: log.warning(f"Unused kwargs: {kwargs}") # sheet metadata self._owner = owner self._upstream = upstream self._active = active self._sheet_type = sheet_type self._import_version = import_version # main character info self.name = name self._description = description self._image = image self.stats = BaseStats.from_dict(stats) self.levels = Levels.from_dict(levels) self._attacks = [Attack.from_dict(atk) for atk in attacks] self.skills = Skills.from_dict(skills) self.resistances = Resistances.from_dict(resistances) self.saves = Saves.from_dict(saves) # hp/ac self.ac = ac self.max_hp = max_hp self._hp = hp self._temp_hp = temp_hp # customization self.cvars = cvars self.options = CharOptions.from_dict(options) self.overrides = ManualOverrides.from_dict(overrides) # ccs self.consumables = [ CustomCounter.from_dict(self, cons) for cons in consumables ] self.death_saves = DeathSaves.from_dict(death_saves) # spellbook spellbook = Spellbook.from_dict(spellbook) super(Character, self).__init__(spellbook) # live sheet integrations self._live = live integration = INTEGRATION_MAP.get(live) if integration: self._live_integration = integration(self) else: self._live_integration = None # misc research things self.race = race self.background = background
async def attack_add(self, ctx, name, *args): """ Adds an attack to the active character. __Arguments__ -d [damage]: How much damage the attack should do. -b [to-hit]: The to-hit bonus of the attack. -desc [description]: A description of the attack. """ character: Character = await Character.from_ctx(ctx) parsed = argparse(args) attack = Attack.new(character, name, bonus_calc=parsed.join('b', '+'), damage=parsed.join('d', '+'), details=parsed.join('desc', '\n')) conflict = next((a for a in character.overrides.attacks if a.name.lower() == attack.name.lower()), None) if conflict: character.overrides.attacks.remove(conflict) character.overrides.attacks.append(attack) await character.commit(ctx) out = f"Created attack {attack.name}!" if conflict: out += f" Removed a duplicate attack." await ctx.send(out)
def parse_attack(self, name_index, bonus_index, damage_index, sheet=None): """Calculates and returns a dict.""" if self.character_data is None: raise Exception('You must call get_character() first.') wksht = sheet or self.character_data name = wksht.cell(name_index).value damage = wksht.cell(damage_index).value bonus = wksht.cell(bonus_index).value details = None if not name: return None if not damage: damage = None else: details = None if '|' in damage: damage, details = damage.split('|', 1) dice, comment = get_roll_comment(damage) if details: details = details.strip() if any(d in comment.lower() for d in DAMAGE_TYPES): damage = "{}[{}]".format(dice, comment) else: damage = dice if comment.strip() and not details: damage = comment.strip() bonus_calc = None if bonus: try: bonus = int(bonus) except (TypeError, ValueError): bonus_calc = bonus bonus = None else: bonus = None attack = Attack(name, bonus, damage, details, bonus_calc) return attack
def parse_attack(self, atk_dict) -> Attack: """Calculates and returns a dict.""" if self.character_data is None: raise Exception('You must call get_character() first.') log.debug(f"Processing attack {atk_dict.get('name')}") # setup temporary local vars temp_names = {} if atk_dict.get('parent', {}).get('collection') == 'Spells': spellParentID = atk_dict.get('parent', {}).get('id') try: spellObj = next(s for s in self.character_data.get('spells', []) if s.get('_id') == spellParentID) except StopIteration: pass else: spellListParentID = spellObj.get('parent', {}).get('id') try: spellListObj = next( s for s in self.character_data.get('spellLists', []) if s.get('_id') == spellListParentID) except StopIteration: pass else: try: temp_names['attackBonus'] = int( self.evaluator.eval( spellListObj.get('attackBonus'))) temp_names['DC'] = int( self.evaluator.eval(spellListObj.get('saveDC'))) except Exception as e: log.debug(f"Exception parsing spellvars: {e}") temp_names['rageDamage'] = self.calculate_stat('rageDamage') old_names = self.evaluator.names.copy() self.evaluator.names.update(temp_names) log.debug(f"evaluator tempnames: {temp_names}") # attack bonus bonus_calc = atk_dict.get('attackBonus', '').replace('{', '').replace('}', '') if not bonus_calc: bonus = None else: try: bonus = int(self.evaluator.eval(bonus_calc)) except: bonus = None # damage def damage_sub(match): out = match.group(1) try: log.debug(f"damage_sub: evaluating {out}") return str(self.evaluator.eval(out)) except Exception as ex: log.debug(f"exception in damage_sub: {ex}") return match.group(0) damage = re.sub(r'{(.*?)}', damage_sub, atk_dict.get('damage', '')) damage = damage.replace('{', '').replace('}', '') if not damage: damage = None else: damage += ' [{}]'.format(atk_dict.get('damageType')) # details details = atk_dict.get('details', None) if details: details = re.sub(r'{([^{}]*)}', damage_sub, details) # build attack name = atk_dict['name'] attack = Attack(name, bonus, damage, details, bonus_calc) self.evaluator.names = old_names return attack
def parse_attack(self, atkIn, atkType): """Calculates and returns a list of dicts.""" if self.character_data is None: raise Exception('You must call get_character() first.') prof = self.get_stats().prof_bonus out = [] if atkType == 'action': if atkIn['dice'] is None: return [] # thanks DDB isProf = atkIn['isProficient'] atkBonus = None dmgBonus = "" if atkIn["abilityModifierStatId"]: atkBonus = self.stat_from_id( atkIn['abilityModifierStatId']) + (prof if isProf else 0) dmgBonus = f"+{self.stat_from_id(atkIn['abilityModifierStatId'])}" attack = Attack( atkIn['name'], atkBonus, f"{atkIn['dice']['diceString']}{dmgBonus}[{parse_dmg_type(atkIn)}]", atkIn['snippet']) out.append(attack) elif atkType == 'customAction': isProf = atkIn['isProficient'] dmgBonus = (atkIn['fixedValue'] or 0) + (atkIn['damageBonus'] or 0) atkBonus = None if atkIn['statId']: atkBonus = self.stat_from_id(atkIn['statId']) + ( prof if isProf else 0) + (atkIn['toHitBonus'] or 0) dmgBonus = (atkIn['fixedValue'] or 0) + self.stat_from_id( atkIn['statId']) + (atkIn['damageBonus'] or 0) if atkIn['attackSubtype'] == 3: # natural weapons if atkBonus is not None: atkBonus += self.get_stat('natural-attacks') dmgBonus += self.get_stat('natural-attacks-damage') damage = f"{atkIn['diceCount']}d{atkIn['diceType']}+{dmgBonus}[{parse_dmg_type(atkIn)}]" attack = Attack(atkIn['name'], atkBonus, damage, atkIn['snippet']) out.append(attack) elif atkType == 'item': itemdef = atkIn['definition'] weirdBonuses = self.get_specific_item_bonuses(atkIn['id']) isProf = self.get_prof(itemdef['type']) or weirdBonuses['isPact'] magicBonus = sum( m['value'] for m in itemdef['grantedModifiers'] if m['type'] == 'bonus' and m['subType'] == 'magic') modBonus = self.get_relevant_atkmod( itemdef) if not weirdBonuses['isHex'] else self.stat_from_id(6) dmgBonus = modBonus + magicBonus + weirdBonuses['damage'] toHitBonus = (prof if isProf else 0) + magicBonus + weirdBonuses['attackBonus'] is_melee = not 'Range' in [ p['name'] for p in itemdef['properties'] ] is_one_handed = not 'Two-Handed' in [ p['name'] for p in itemdef['properties'] ] is_weapon = itemdef['filterType'] == 'Weapon' if is_melee and is_one_handed: dmgBonus += self.get_stat('one-handed-melee-attacks-damage') if not is_melee and is_weapon: toHitBonus += self.get_stat('ranged-weapon-attacks') damage = None if itemdef['fixedDamage'] or itemdef['damage']: damage = f"{itemdef['fixedDamage'] or itemdef['damage']['diceString']}+{dmgBonus}" \ f"[{itemdef['damageType'].lower()}" \ f"{'^' if itemdef['magic'] or weirdBonuses['isPact'] else ''}]" atkBonus = weirdBonuses[ 'attackBonusOverride'] or modBonus + toHitBonus details = html2text.html2text(itemdef['description'], bodywidth=0).strip() attack = Attack(itemdef['name'], atkBonus, damage, details) out.append(attack) if 'Versatile' in [p['name'] for p in itemdef['properties']]: versDmg = next(p['notes'] for p in itemdef['properties'] if p['name'] == 'Versatile') damage = f"{versDmg}+{dmgBonus}[{itemdef['damageType'].lower()}" \ f"{'^' if itemdef['magic'] or weirdBonuses['isPact'] else ''}]" attack = Attack(f"2-Handed {itemdef['name']}", atkBonus, damage, details) out.append(attack) elif atkType == 'unarmed': monk_level = self.get_levels().get('Monk') ability_mod = self.stat_from_id(1) if not monk_level else max( self.stat_from_id(1), self.stat_from_id(2)) if not monk_level: dmg = 1 + ability_mod elif monk_level < 5: dmg = f"1d4+{ability_mod}" elif monk_level < 11: dmg = f"1d6+{ability_mod}" elif monk_level < 17: dmg = f"1d8+{ability_mod}" else: dmg = f"1d10+{ability_mod}" atkBonus = self.get_stats().prof_bonus atkBonus += self.get_stat('natural-attacks') natural_bonus = self.get_stat('natural-attacks-damage') if natural_bonus: dmg = f"{dmg}+{natural_bonus}" attack = Attack("Unarmed Strike", ability_mod + atkBonus, f"{dmg}[bludgeoning]") out.append(attack) return [a.to_dict() for a in out]
async def monster_atk(self, ctx, monster_name, atk_name=None, *, args=''): """Rolls a monster's attack. __Valid Arguments__ adv/dis -ac [target ac] -b [to hit bonus] -d [damage bonus] -d# [applies damage to the first # hits] -rr [times to reroll] -t [target] -phrase [flavor text] crit (automatically crit) -h (hides monster name, image, and attack details)""" if atk_name is None or atk_name == 'list': return await ctx.invoke(self.monster_atk_list, monster_name) try: await ctx.message.delete() except: pass monster = await select_monster_full(ctx, monster_name) attacks = monster.attacks monster_name = monster.get_title_name() attack = await search_and_select(ctx, attacks, atk_name, lambda a: a['name']) args = await scripting.parse_snippets(args, ctx) args = argparse(args) if not args.last('h', type_=bool): name = monster_name image = args.get('image') or monster.get_image_url() else: name = "An unknown creature" image = None attack = Attack.from_old(attack) embed = discord.Embed() if args.last('title') is not None: embed.title = args.last('title') \ .replace('[name]', name) \ .replace('[aname]', attack.name) else: embed.title = '{} attacks with {}!'.format(name, a_or_an(attack.name)) if image: embed.set_thumbnail(url=image) caster, targets, combat = await targetutils.maybe_combat( ctx, monster, args.get('t')) await Automation.from_attack(attack).run(ctx, embed, caster, targets, args, combat=combat, title=embed.title) if combat: await combat.final() _fields = args.get('f') embeds.add_fields_from_args(embed, _fields) embed.colour = random.randint(0, 0xffffff) if monster.source == 'homebrew': embeds.add_homebrew_footer(embed) await ctx.send(embed=embed)
def parse_attack(self, atkIn, atkType): """Calculates and returns a list of dicts.""" if self.character_data is None: raise Exception('You must call get_character() first.') prof = self.get_stats().prof_bonus out = [] def monk_scale(): monk_level = self.get_levels().get('Monk') if not monk_level: monk_dice_size = 0 elif monk_level < 5: monk_dice_size = 4 elif monk_level < 11: monk_dice_size = 6 elif monk_level < 17: monk_dice_size = 8 else: monk_dice_size = 10 return monk_dice_size if atkType == 'action': if atkIn['dice'] is None: return [] # thanks DDB isProf = atkIn['isProficient'] atkBonus = None dmgBonus = None dice_size = max(monk_scale(), atkIn['dice']['diceValue']) base_dice = f"{atkIn['dice']['diceCount']}d{dice_size}" if atkIn["abilityModifierStatId"]: atkBonus = self.stat_from_id(atkIn['abilityModifierStatId']) dmgBonus = self.stat_from_id(atkIn['abilityModifierStatId']) if atkIn["isMartialArts"] and self.get_levels().get("Monk"): atkBonus = max(atkBonus, self.stat_from_id(2)) # allow using dex dmgBonus = max(dmgBonus, self.stat_from_id(2)) if isProf: atkBonus += prof if dmgBonus: damage = f"{base_dice}+{dmgBonus}[{parse_dmg_type(atkIn)}]" else: damage = f"{base_dice}[{parse_dmg_type(atkIn)}]" attack = Attack( atkIn['name'], atkBonus, damage, atkIn['snippet'] ) out.append(attack) elif atkType == 'customAction': isProf = atkIn['isProficient'] dmgBonus = (atkIn['fixedValue'] or 0) + (atkIn['damageBonus'] or 0) atkBonus = None if atkIn['statId']: atkBonus = self.stat_from_id(atkIn['statId']) + (prof if isProf else 0) + (atkIn['toHitBonus'] or 0) dmgBonus = (atkIn['fixedValue'] or 0) + self.stat_from_id(atkIn['statId']) + (atkIn['damageBonus'] or 0) if atkIn['attackSubtype'] == 3: # natural weapons if atkBonus is not None: atkBonus += self.get_stat('natural-attacks') dmgBonus += self.get_stat('natural-attacks-damage') damage = f"{atkIn['diceCount']}d{atkIn['diceType']}+{dmgBonus}[{parse_dmg_type(atkIn)}]" attack = Attack( atkIn['name'], atkBonus, damage, atkIn['snippet'] ) out.append(attack) elif atkType == 'item': itemdef = atkIn['definition'] weirdBonuses = self.get_specific_item_bonuses(atkIn['id']) isProf = self.get_prof(itemdef['type']) or weirdBonuses['isPact'] magicBonus = self._item_magic_bonus(itemdef) modBonus = self.get_relevant_atkmod(itemdef) if not weirdBonuses['isHex'] else self.stat_from_id(6) item_dmg_bonus = self.get_stat(f"{itemdef['type'].lower()}-damage") dmgBonus = modBonus + magicBonus + weirdBonuses['damage'] + item_dmg_bonus toHitBonus = (prof if isProf else 0) + magicBonus + weirdBonuses['attackBonus'] is_melee = not 'Range' in [p['name'] for p in itemdef['properties']] is_one_handed = not 'Two-Handed' in [p['name'] for p in itemdef['properties']] is_weapon = itemdef['filterType'] == 'Weapon' if is_melee and is_one_handed: dmgBonus += self.get_stat('one-handed-melee-attacks-damage') if not is_melee and is_weapon: toHitBonus += self.get_stat('ranged-weapon-attacks') if weirdBonuses['isPact'] and self._improved_pact_weapon_applies(itemdef): dmgBonus += 1 toHitBonus += 1 base_dice = None if itemdef['fixedDamage']: base_dice = itemdef['fixedDamage'] elif itemdef['damage']: if not itemdef['isMonkWeapon']: base_dice = f"{itemdef['damage']['diceCount']}d{itemdef['damage']['diceValue']}" else: dice_size = max(monk_scale(), itemdef['damage']['diceValue']) base_dice = f"{itemdef['damage']['diceCount']}d{dice_size}" if base_dice: damage = f"{base_dice}+{dmgBonus}" \ f"[{itemdef['damageType'].lower()}" \ f"{'^' if itemdef['magic'] or weirdBonuses['isPact'] else ''}]" else: damage = None atkBonus = weirdBonuses['attackBonusOverride'] or modBonus + toHitBonus details = html2text.html2text(itemdef['description'], bodywidth=0).strip() attack = Attack( itemdef['name'], atkBonus, damage, details ) out.append(attack) if 'Versatile' in [p['name'] for p in itemdef['properties']]: versDmg = next(p['notes'] for p in itemdef['properties'] if p['name'] == 'Versatile') damage = f"{versDmg}+{dmgBonus}[{itemdef['damageType'].lower()}" \ f"{'^' if itemdef['magic'] or weirdBonuses['isPact'] else ''}]" attack = Attack( f"2-Handed {itemdef['name']}", atkBonus, damage, details ) out.append(attack) elif atkType == 'unarmed': dice_size = monk_scale() ability_mod = self.stat_from_id(1) if not self.get_levels().get('Monk') else max(self.stat_from_id(1), self.stat_from_id(2)) atkBonus = prof if dice_size: dmg = f"1d{dice_size}+{ability_mod}" else: dmg = 1 + ability_mod atkBonus += self.get_stat('natural-attacks') natural_bonus = self.get_stat('natural-attacks-damage') if natural_bonus: dmg = f"{dmg}+{natural_bonus}" attack = Attack( "Unarmed Strike", ability_mod + atkBonus, f"{dmg}[bludgeoning]" ) out.append(attack) return [a.to_dict() for a in out]