def _transform_attack(attack) -> Attack: desc = html_to_md(attack['desc']) if attack['saveDc'] is not None and attack['saveStat'] is not None: stat = constants.STAT_ABBREVIATIONS[attack['saveStat'] - 1] for_half = desc and 'half' in desc the_attack = automation.Save(stat, fail=[automation.Damage("{damage}")], success=[] if not for_half else [automation.Damage("{damage}/2")], dc=str(attack['saveDc'])) # attack, then save if attack['toHit'] is not None: the_attack = automation.Attack(hit=[the_attack], attackBonus=str( attack['toHit']), miss=[]) # target and damage meta target = automation.Target('each', [the_attack]) damage_roll = automation.Roll(attack['damage'] or '0', 'damage') effects = [damage_roll, target] # description text if desc: effects.append(automation.Text(desc)) return Attack(attack['name'], automation.Automation(effects)) else: return Attack.new(attack['name'], attack['toHit'], attack['damage'] or '0', desc)
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(name, bonus_calc=parsed.join('b', '+'), damage_calc=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)
async def attack_add(self, ctx, name, *args): """ Adds an attack to the active character. __Valid 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. -verb <verb> - The verb to use for this attack. (e.g. "Padellis <verb> a dagger!") proper - This attack's name is a proper noun. -criton <#> - This attack crits on a number other than a natural 20. -phrase <text> - Some flavor text to add to each attack with this attack. -thumb <image url> - The attack's image. -c <extra crit damage> - How much extra damage (beyond doubling dice) this attack does on a crit. """ character: Character = await Character.from_ctx(ctx) parsed = argparse(args) attack = Attack.new(name, bonus_calc=parsed.join('b', '+'), damage_calc=parsed.join('d', '+'), details=parsed.join('desc', '\n'), verb=parsed.last('verb'), proper=parsed.last('proper', False, bool), criton=parsed.last('criton', type_=int), phrase=parsed.join('phrase', '\n'), thumb=parsed.last('thumb'), extra_crit_damage=parsed.last('c')) conflict = next((a for a in character.overrides.attacks if a.name.lower() == attack.name.lower()), None) if conflict: if await confirm( ctx, "This will overwrite an attack with the same name. Continue? (Reply with yes/no)" ): character.overrides.attacks.remove(conflict) else: return await ctx.send("Okay, aborting.") 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.value(name_index) damage = wksht.value(damage_index) bonus = wksht.value(bonus_index) 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() if bonus: try: bonus = int(bonus) except (TypeError, ValueError): bonus = None else: bonus = None attack = Attack.new(name, bonus, damage, details) 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_calc = atk_dict.get('damage', '') damage = re.sub(r'{(.*?)}', damage_sub, damage_calc) 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.new(name, bonus, damage, details) 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 = [] 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'] atk_bonus = None dmgBonus = None dice_size = max(monk_scale(), atkIn['dice']['diceValue']) base_dice = f"{atkIn['dice']['diceCount']}d{dice_size}" if atkIn["abilityModifierStatId"]: atk_bonus = self.stat_from_id(atkIn['abilityModifierStatId']) dmgBonus = self.stat_from_id(atkIn['abilityModifierStatId']) if atkIn["isMartialArts"] and self.get_levels().get("Monk"): atk_bonus = max(atk_bonus, self.stat_from_id(2)) # allow using dex dmgBonus = max(dmgBonus, self.stat_from_id(2)) if isProf and atk_bonus is not None: atk_bonus += prof if dmgBonus: damage = f"{base_dice}+{dmgBonus}[{parse_dmg_type(atkIn)}]" else: damage = f"{base_dice}[{parse_dmg_type(atkIn)}]" attack = Attack.new(atkIn['name'], atk_bonus, damage, atkIn['snippet']) out.append(attack) elif atkType == 'customAction': isProf = atkIn['isProficient'] dmgBonus = (atkIn['fixedValue'] or 0) + (atkIn['damageBonus'] or 0) atk_bonus = None if atkIn['statId']: atk_bonus = 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 atk_bonus is not None: atk_bonus += 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.new(atkIn['name'], atk_bonus, damage, atkIn['snippet']) out.append(attack) elif atkType == 'item': itemdef = atkIn['definition'] character_item_bonuses = self.get_specific_item_bonuses( atkIn['id']) item_specific_bonuses = self._item_modifiers(itemdef) item_properties = itemdef['properties'] + [ collections.defaultdict(lambda: None, name=n) for n in item_specific_bonuses['extraProperties'] ] isProf = self.get_prof( itemdef['type']) or character_item_bonuses['isPact'] mod_bonus = self.get_relevant_atkmod(itemdef, item_properties) if character_item_bonuses['isHex']: mod_bonus = max(mod_bonus, self.stat_from_id(6)) if itemdef['magic'] and self.get_levels().get( 'Artificer') and "Battle Ready" in self._all_features: mod_bonus = max(mod_bonus, self.stat_from_id(4)) magic_bonus = item_specific_bonuses['magicBonus'] item_dmg_bonus = self.get_stat(f"{itemdef['type'].lower()}-damage") dmgBonus = mod_bonus + magic_bonus + character_item_bonuses[ 'damage'] + item_dmg_bonus toHitBonus = ( prof if isProf else 0) + magic_bonus + character_item_bonuses['attackBonus'] is_melee = not 'Range' in [p['name'] for p in item_properties] is_one_handed = not 'Two-Handed' in [ p['name'] for p in item_properties ] is_weapon = itemdef['filterType'] == 'Weapon' has_gwf = "Great Weapon Fighting" in self._all_features 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 character_item_bonuses[ '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}" damage_type = (item_specific_bonuses['replaceDamageType'] or itemdef['damageType'] or 'unknown').lower() if base_dice and is_melee and has_gwf and not is_one_handed: base_dice += "ro<3" if base_dice: damage = f"{base_dice}+{dmgBonus}[{damage_type}" \ f"{'^' if itemdef['magic'] or character_item_bonuses['isPact'] else ''}]" else: damage = None atk_bonus = character_item_bonuses[ 'attackBonusOverride'] or mod_bonus + toHitBonus details = character_item_bonuses['note'] or html2text.html2text( itemdef['description'], bodywidth=0).strip() name = character_item_bonuses['name'] or itemdef['name'] attack = Attack.new(name, atk_bonus, damage, details) out.append(attack) if 'Versatile' in [p['name'] for p in item_properties]: versDmg = next(p['notes'] for p in item_properties if p['name'] == 'Versatile') if has_gwf: versDmg += "ro<3" damage = f"{versDmg}+{dmgBonus}[{damage_type}" \ f"{'^' if itemdef['magic'] or character_item_bonuses['isPact'] else ''}]" attack = Attack.new(f"2-Handed {name}", atk_bonus, 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)) character_item_bonuses = self.get_specific_item_bonuses( 1) # magic number: Unarmed Strike ID if dice_size: dmg = f"1d{dice_size}+{ability_mod}" else: dmg = 1 + ability_mod atk_bonus = character_item_bonuses['attackBonusOverride'] or \ (prof + self.get_stat('natural-attacks') + character_item_bonuses['attackBonus']) dmg_bonus = self.get_stat( 'natural-attacks-damage') + character_item_bonuses['damage'] if dmg_bonus: dmg = f"{dmg}+{dmg_bonus}" details = character_item_bonuses['note'] or None name = character_item_bonuses['name'] or "Unarmed Strike" attack = Attack.new(name, ability_mod + atk_bonus, f"{dmg}[bludgeoning]", details) out.append(attack) return out
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.new(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.new(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.new(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.new(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.new("Unarmed Strike", ability_mod + atkBonus, f"{dmg}[bludgeoning]") out.append(attack) return out