def _get_skills(self) -> Skills: out = {} def derive_adv(skl): advs = set() for adv in skl['adv']: if not adv['restriction']: advs.add(True) for adv in skl['dis']: if not adv['restriction']: advs.add(False) if len(advs) == 1: return advs.pop() return None for skill_id, skill in self.character_data['skills'].items(): prof_type = {1: 0, 2: 0.5, 3: 1, 4: 2}.get(skill['prof'], 0) adv_type = derive_adv(skill) out[SKILL_MAP[skill_id]] = Skill(skill['modifier'], prof_type, skill['bonus'], adv_type) out['initiative'] = Skill( self.character_data['initiative']['modifier'], adv=self.character_data['initiative']['adv'] or None) for stat_key, skill in zip(constants.STAT_ABBREVIATIONS, constants.STAT_NAMES): out[skill] = Skill( self.character_data['stats'][stat_key]['modifier']) return Skills(out)
def _get_skills(self) -> Skills: out = {} # main skills for skill_id, skill in self.character_data['skills'].items(): prof_type = {1: 0, 2: 0.5, 3: 1, 4: 2}.get(skill['prof'], 0) adv_type = derive_adv(skill['adv'], skill['dis']) out[SKILL_MAP[skill_id]] = Skill(skill['modifier'], prof_type, skill['bonus'], adv_type) # initiative out['initiative'] = Skill( self.character_data['initiative']['modifier'], adv=self.character_data['initiative']['adv'] or None) # ability skills (base strength, dex, etc checks) for stat_key, skill in zip(constants.STAT_ABBREVIATIONS, constants.STAT_NAMES): stat_obj = self.character_data['stats'][stat_key] prof_type = { 1: 0, 2: 0.5, 3: 1, 4: 2 }.get(stat_obj['modifierProficiency'], 0) out[skill] = Skill(stat_obj['modifier'], prof_type) return Skills(out)
def test_alias_skill_comparison(): s1 = AliasSkill(Skill(1)) s2 = AliasSkill(Skill(2)) assert s1 == s1 assert s1 != s2 assert s2 > s1 assert s2 >= s1 assert not s2 < s1 assert not s2 <= s1 assert s1 >= s1 assert s1 <= s1 assert s1 == 1 assert s1 != 2 assert s2 > 1 assert s2 >= 1 assert not s2 < 1 assert not s2 <= 1 assert s1 >= 1 assert s1 <= 1 assert 1 == s1 assert 1 != s2 assert 2 > s1 assert 2 >= s1 assert not 2 < s1 assert not 2 <= s1 assert 1 >= s1 assert 1 <= s1
def get_skills_and_saves(self) -> (Skills, Saves): if self.character_data is None: raise Exception('You must call get_character() first.') character = self.character_data stats = self.get_stats() NAME_SET = set(SKILL_NAMES + SAVE_NAMES) ADV_INT_MAP = {-1: False, 0: None, 1: True} profs = {} effects = collections.defaultdict(lambda: 0) # calculate profs for prof in character.get('proficiencies', []): if prof.get('enabled', False) and not prof.get('removed', False): profs[prof.get('name')] = prof.get('value') \ if prof.get('value') > profs.get(prof.get('name', 'None'), 0) \ else profs[prof.get('name')] # and effects for effect in self.character_data.get('effects', []): if effect.get('stat') in NAME_SET \ and effect.get('enabled', True) \ and not effect.get('removed', False): statname = effect.get('stat') if effect.get('operation') == 'disadvantage': effects[statname] = max(-1, effects[statname] - 1) if effect.get('operation') == 'advantage': effects[statname] = min(1, effects[statname] + 1) # assign skills skills = {} for skill in SKILL_NAMES: prof_mult = profs.get(skill, 0) base_val = floor(stats.get_mod(SKILL_MAP[skill]) + stats.prof_bonus * prof_mult) adv = ADV_INT_MAP.get(effects.get(skill)) if skill not in STAT_NAMES: value = int(self.calculate_stat(skill, base=base_val)) else: value = base_val skills[skill] = Skill( value, prof=prof_mult, adv=adv ) # and saves saves = {} for save in SAVE_NAMES: prof_mult = profs.get(save, 0) base_val = floor(stats.get_mod(SKILL_MAP[save]) + stats.prof_bonus * prof_mult) adv = ADV_INT_MAP.get(effects.get(save)) saves[save] = Skill( int(self.calculate_stat(save, base=base_val)), prof=prof_mult, adv=adv ) return Skills(skills), Saves(saves)
def _get_saves(self) -> Saves: out = {} for stat_key, save_key in zip(constants.STAT_ABBREVIATIONS, constants.SAVE_NAMES): out[save_key] = Skill(self.character_data['stats'][stat_key]['save'], prof=1 if self.character_data['stats'][stat_key]['saveProficiency'] else 0) return Saves(out)
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)
def _get_saves(self) -> Saves: out = {} for stat_key, save_key in zip(constants.STAT_ABBREVIATIONS, constants.SAVE_NAMES): stat_data = self.character_data['stats'][stat_key] adv_type = derive_adv(stat_data['saveAdv'], stat_data['saveDis']) out[save_key] = Skill( stat_data['save'], prof=1 if stat_data['saveProficiency'] else 0, adv=adv_type) return Saves(out)
def get_skills_and_saves(self): if self.character_data is None: raise Exception('You must call get_character() first.') character = self.character_data skills = {} saves = {} is_joat = self.version == 2 and bool(character.cell("AR45").value) for cell, skill, advcell in SKILL_CELL_MAP: if isinstance(cell, int): advcell = f"F{cell}" profcell = f"H{cell}" cell = f"I{cell}" else: profcell = None try: value = int(character.cell(cell).value) except (TypeError, ValueError): raise MissingAttribute(skill) adv = None if self.version == 2 and advcell: advtype = character.cell(advcell).value if advtype in {'a', 'adv', 'advantage'}: adv = True elif advtype in {'d', 'dis', 'disadvantage'}: adv = False prof = 0 if "Save" not in skill and is_joat: prof = 0.5 if profcell: proftype = character.cell(profcell).value_unformatted if proftype == 'e': prof = 2 elif proftype and proftype != '0': prof = 1 skl_obj = Skill(value, prof, adv=adv) if "Save" in skill: saves[skill] = skl_obj else: skills[skill] = skl_obj skills = Skills(skills) saves = Saves(saves) return skills, saves
def get_skills_and_saves(self): if self.character_data is None: raise Exception('You must call get_character() first.') character = self.character_data skills = {} saves = {} is_joat = False all_check_bonus = 0 if self.version == (2, 0): is_joat = bool(character.value("AR45")) all_check_bonus = int(character.value("AQ26") or 0) elif self.version == (2, 1): is_joat = bool(character.value("AQ59")) all_check_bonus = int(character.value("AR58")) joat_bonus = int(is_joat and self.get_stats().prof_bonus // 2) # calculate str, dex, con, etc checks for cell, skill, advcell in BASE_ABILITY_CHECKS: try: # add bonuses manually since the cell does not include them value = int( character.value(cell)) + all_check_bonus + joat_bonus except (TypeError, ValueError): raise MissingAttribute(skill) prof = 0 if is_joat: prof = 0.5 skl_obj = Skill(value, prof) skills[skill] = skl_obj # read the value of the rest of the skills for cell, skill, advcell in SKILL_CELL_MAP: if isinstance(cell, int): advcell = f"F{cell}" profcell = f"H{cell}" cell = f"I{cell}" else: profcell = None try: value = int(character.value(cell)) except (TypeError, ValueError): raise MissingAttribute(skill) adv = None if self.version >= (2, 0) and advcell: advtype = character.unformatted_value(advcell) if advtype in {'a', 'adv', 'advantage'}: adv = True elif advtype in {'d', 'dis', 'disadvantage'}: adv = False prof = 0 if "Save" not in skill and is_joat: prof = 0.5 if profcell: proftype = character.unformatted_value(profcell) if proftype == 'e': prof = 2 elif proftype and proftype != '0': prof = 1 skl_obj = Skill(value, prof, adv=adv) if "Save" in skill: saves[skill] = skl_obj else: skills[skill] = skl_obj skills = Skills(skills) saves = Saves(saves) return skills, saves
def get_skills_and_saves(self): """Returns a dict of all the character's skills.""" if self.character_data is None: raise Exception('You must call get_character() first.') stats = self.get_stats() profBonus = stats.prof_bonus profs = dict() bonuses = dict() advantages = collections.defaultdict(lambda: []) for mod in self.modifiers(): mod['subType'] = mod['subType'].replace("-saving-throws", "Save") if mod['type'] == 'half-proficiency': profs[mod['subType']] = max(profs.get(mod['subType'], 0), 0.5) elif mod['type'] == 'proficiency': profs[mod['subType']] = max(profs.get(mod['subType'], 0), 1) elif mod['type'] == 'expertise': profs[mod['subType']] = 2 elif mod['type'] == 'bonus': if not mod['isGranted']: continue if mod['statId'] is not None: bonuses[mod['subType']] = bonuses.get( mod['subType'], 0) + self.stat_from_id(mod['statId']) else: bonuses[mod['subType']] = bonuses.get( mod['subType'], 0) + (mod['value'] or 0) elif mod['type'] == 'advantage' and not mod[ 'restriction']: # unconditional adv advantages[mod['subType']].append(True) elif mod['type'] == 'disadvantage' and not mod[ 'restriction']: # unconditional dis advantages[mod['subType']].append(False) profs['animalHandling'] = profs.get('animal-handling', 0) profs['sleightOfHand'] = profs.get('sleight-of-hand', 0) advantages['animalHandling'] = advantages['animal-handling'] advantages['sleightOfHand'] = advantages['sleight-of-hand'] def _simplify_adv(adv_list): adv_set = set(adv_list) if len(adv_set) == 1: return adv_set.pop() return None skills = {} for skill in SKILL_NAMES: # add proficiency and bonuses to skills relevantprof = profs.get(skill, 0) relevantbonus = bonuses.get(skill, 0) relevantadv = _simplify_adv(advantages[skill]) if 'ability-checks' in profs and skill != 'initiative': relevantprof = max(relevantprof, profs['ability-checks']) if 'ability-checks' in bonuses and skill != 'initiative': relevantbonus += bonuses['ability-checks'] skills[skill] = Skill(floor( stats.get_mod(SKILL_MAP[skill]) + (profBonus * relevantprof) + relevantbonus), relevantprof, relevantbonus, adv=relevantadv) # saves saves = {} for save in SAVE_NAMES: # add proficiency and bonuses to skills relevantprof = profs.get(save, 0) relevantbonus = bonuses.get(save, 0) relevantadv = _simplify_adv(advantages[save]) if 'saving-throws' in profs: relevantprof = max(relevantprof, profs['saving-throws']) if 'saving-throws' in bonuses: relevantbonus += bonuses['saving-throws'] saves[save] = Skill(floor( stats.get_mod(SKILL_MAP[save]) + (profBonus * relevantprof) + relevantbonus), relevantprof, relevantbonus, adv=relevantadv) # values ignored_ids = set() for charval in self.character_data['characterValues']: if charval['value'] is None: continue if charval['typeId'] == 39: # misc saving throw bonus save_id = SAVE_NAMES[charval['valueId'] - 1] save_bonus = charval['value'] saves[save_id].value += save_bonus saves[save_id].bonus += save_bonus elif charval['valueId'] in HOUSERULE_SKILL_MAP and charval[ 'valueId'] not in ignored_ids: skill_name = HOUSERULE_SKILL_MAP[charval['valueId']] if charval['typeId'] == 23: # override skills[skill_name] = Skill(charval['value']) ignored_ids.add( charval['valueId'] ) # this must be the final value so we stop looking elif charval['typeId'] in { 24, 25 }: # PROBABLY skill magic/misc bonus skills[skill_name].value += charval['value'] skills[skill_name].bonus += charval['value'] elif charval['typeId'] == 26: # proficiency stuff relevantprof = profs.get(skill_name, 0) skills[skill_name].value -= relevantprof * profBonus if charval[ 'value'] == 0: # no prof, don't need to do anything skills[skill_name].prof = 0 elif charval['value'] == 1: # half prof, round down skills[skill_name].value += profBonus // 2 skills[skill_name].prof = 0.5 elif charval['value'] == 2: # half, round up skills[skill_name].value += ceil(profBonus / 2) skills[skill_name].prof = 0.5 elif charval['value'] == 3: # full skills[skill_name].value += profBonus skills[skill_name].prof = 1 elif charval['value'] == 4: # double skills[skill_name].value += profBonus * 2 skills[skill_name].prof = 2 skills = Skills(skills) saves = Saves(saves) return skills, saves
def get_skills_and_saves(self): """Returns a dict of all the character's skills.""" if self.character_data is None: raise Exception('You must call get_character() first.') character = self.character_data stats = self.get_stats() profBonus = stats.prof_bonus profs = dict() bonuses = dict() for modtype in character['modifiers'].values( ): # calculate proficiencies in all skills for mod in modtype: mod['subType'] = mod['subType'].replace( "-saving-throws", "Save") if mod['type'] == 'half-proficiency': profs[mod['subType']] = max(profs.get(mod['subType'], 0), 0.5) elif mod['type'] == 'proficiency': profs[mod['subType']] = max(profs.get(mod['subType'], 0), 1) elif mod['type'] == 'expertise': profs[mod['subType']] = 2 elif mod['type'] == 'bonus': if not mod['isGranted']: continue if mod['statId'] is not None: bonuses[mod['subType']] = bonuses.get( mod['subType'], 0) + self.stat_from_id( mod['statId']) else: bonuses[mod['subType']] = bonuses.get( mod['subType'], 0) + (mod['value'] or 0) profs['animalHandling'] = profs.get('animal-handling', 0) profs['sleightOfHand'] = profs.get('sleight-of-hand', 0) skills = {} for skill in SKILL_NAMES: # add proficiency and bonuses to skills relevantprof = profs.get(skill, 0) relevantbonus = bonuses.get(skill, 0) if 'ability-checks' in profs: relevantprof = max(relevantprof, profs['ability-checks']) if 'ability-checks' in bonuses: relevantbonus += bonuses['ability-checks'] skills[skill] = Skill( floor( stats.get_mod(SKILL_MAP[skill]) + (profBonus * relevantprof) + relevantbonus), relevantprof, relevantbonus) saves = {} for save in SAVE_NAMES: # add proficiency and bonuses to skills relevantprof = profs.get(save, 0) relevantbonus = bonuses.get(save, 0) if 'saving-throws' in profs: relevantprof = max(relevantprof, profs['saving-throws']) if 'saving-throws' in bonuses: relevantbonus += bonuses['saving-throws'] saves[save] = Skill( floor( stats.get_mod(SKILL_MAP[save]) + (profBonus * relevantprof) + relevantbonus), relevantprof, relevantbonus) ignored_ids = set() for charval in self.character_data['characterValues']: if charval['valueId'] in HOUSERULE_SKILL_MAP and charval[ 'valueId'] not in ignored_ids: skill_name = HOUSERULE_SKILL_MAP[charval['valueId']] if charval['typeId'] == 23: # override skills[skill_name] = Skill(charval['value']) ignored_ids.add( charval['valueId'] ) # this must be the final value so we stop looking elif charval['typeId'] in { 24, 25 }: # PROBABLY skill magic/misc bonus skills[skill_name].value += charval['value'] skills[skill_name].bonus += charval['value'] elif charval['typeId'] == 26: # proficiency stuff relevantprof = profs.get(skill_name, 0) skills[skill_name].value -= relevantprof * profBonus if charval[ 'value'] == 0: # no prof, don't need to do anything skills[skill_name].prof = 0 elif charval['value'] == 1: # half prof, round down skills[skill_name].value += profBonus // 2 skills[skill_name].prof = 0.5 elif charval['value'] == 2: # half, round up skills[skill_name].value += ceil(profBonus / 2) skills[skill_name].prof = 0.5 elif charval['value'] == 3: # full skills[skill_name].value += profBonus skills[skill_name].prof = 1 elif charval['value'] == 4: # double skills[skill_name].value += profBonus * 2 skills[skill_name].prof = 2 skills = Skills(skills) saves = Saves(saves) return skills, saves
def init_skill(self): # groups: if all combatants are the same type, return the first one's skill, otherwise +0 if all(isinstance(c, MonsterCombatant) for c in self._combatants) \ and len(set(c.monster_name for c in self._combatants)) == 1: return self._combatants[0].init_skill return Skill(0)
def migrate(character): name = character['stats']['name'] sheet_type = character.get('type') import_version = character.get('version') print(f"Migrating {name} - {sheet_type} v{import_version}") owner = character['owner'] upstream = character['upstream'] active = character['active'] description = character['stats'].get('description', "No description") image = character['stats']['image'] stats = { "prof_bonus": character['stats']['proficiencyBonus'], "strength": character['stats']['strength'], "dexterity": character['stats']['dexterity'], "constitution": character['stats']['constitution'], "intelligence": character['stats']['intelligence'], "wisdom": character['stats']['wisdom'], "charisma": character['stats']['charisma'] } # classes classes = {} for c, l in character['levels'].items(): if c.endswith("Level"): classes[c[:-5]] = l for cls, lvl in list(classes.items())[:]: if any(inv in cls for inv in ".$"): classes.pop(cls) levels = {"total_level": character['levels']['level'], "classes": classes} # attacks attacks = [] for a in character['attacks']: try: bonus = int(a['attackBonus']) bonus_calc = None except (ValueError, TypeError): bonus = None bonus_calc = a['attackBonus'] atk = { "name": a['name'], "bonus": bonus, "damage": a['damage'], "details": a.get('details'), "bonus_calc": bonus_calc } attacks.append(atk) # skills and saves skills = {} for skill_name in SKILL_NAMES: value = character['skills'][skill_name] skefct = character.get('skill_effects', {}).get(skill_name) adv = True if skefct == 'adv' else False if skefct == 'dis' else None skl = Skill(value, 0, 0, adv) skills[skill_name] = skl.to_dict() saves = {} for save_name in SAVE_NAMES: value = character['saves'][save_name] skefct = character.get('skill_effects', {}).get(save_name) adv = True if skefct == 'adv' else False if skefct == 'dis' else None skl = Skill(value, 0, 0, adv) saves[save_name] = skl.to_dict() # combat resistances = { "resist": character.get('resist', []), "immune": character.get('immune', []), "vuln": character.get('vuln', []) } ac = character.get('armor', 10) max_hp = character.get('hp', 4) # you get 4 hp if your character is that old hp = character.get('consumables', {}).get('hp', {}).get('value', max_hp) temp_hp = character.get('consumables', {}).get('temphp', {}).get('value', 0) cvars = character.get('cvars', {}) options = {"options": character.get('settings', {})} # overrides override_attacks = [] for a in character.get('overrides', {}).get('attacks', []): try: bonus = int(a['attackBonus']) bonus_calc = None except (ValueError, TypeError): bonus = None bonus_calc = a['attackBonus'] atk = { "name": a['name'], "bonus": bonus, "damage": a['damage'], "details": a['details'], "bonus_calc": bonus_calc } override_attacks.append(atk) override_spells = [] for old_spell in character.get('overrides', {}).get('spells', []): if isinstance(old_spell, dict): spl = SpellbookSpell(old_spell['name'], old_spell['strict']) else: spl = SpellbookSpell(old_spell, True) override_spells.append(spl.to_dict()) overrides = { "desc": character.get('overrides', {}).get('desc'), "image": character.get('overrides', {}).get('image'), "attacks": override_attacks, "spells": override_spells } # other things consumables = [] for cname, cons in character.get('consumables', {}).get('custom', {}).items(): value = cons['value'] minv = cons.get('min') maxv = cons.get('max') reset = cons.get('reset') display_type = cons.get('type') live_id = cons.get('live') counter = CustomCounter(None, cname, value, minv, maxv, reset, display_type, live_id) consumables.append(counter.to_dict()) death_saves = { "successes": character.get('consumables', {}).get('deathsaves', {}).get('success', {}).get('value', 0), "fails": character.get('consumables', {}).get('deathsaves', {}).get('fail', {}).get('value', 0) } # spellcasting slots = {} max_slots = {} for l in range(1, 10): slots[str(l)] = character.get('consumables', {}).get('spellslots', {}).get(str(l), {}).get('value', 0) max_slots[str(l)] = character.get('spellbook', {}).get('spellslots', {}).get(str(l), 0) spells = [] for old_spell in character.get('spellbook', {}).get('spells', []): if isinstance(old_spell, dict): spl = SpellbookSpell(old_spell['name'], old_spell['strict']) else: spl = SpellbookSpell(old_spell, True) spells.append(spl.to_dict()) spellbook = { "slots": slots, "max_slots": max_slots, "spells": spells, "dc": character.get('spellbook', {}).get('dc'), "sab": character.get('spellbook', {}).get('attackBonus'), "caster_level": character['levels']['level'] } live = 'dicecloud' if character.get('live') else None race = character.get('race') background = character.get('background') char = Character(owner, upstream, active, sheet_type, import_version, name, description, image, stats, levels, attacks, skills, resistances, saves, ac, max_hp, hp, temp_hp, cvars, options, overrides, consumables, death_saves, spellbook, live, race, background) return char