From 4d165fa2c9dc77c5d6bf2183dbb032e1824c7e14 Mon Sep 17 00:00:00 2001 From: Dmitry Chumak Date: Sat, 10 Jun 2023 10:36:18 +0300 Subject: [PATCH] new statmod calculation + health calculation --- generate.py | 247 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 224 insertions(+), 23 deletions(-) diff --git a/generate.py b/generate.py index e754c3a..f54554a 100644 --- a/generate.py +++ b/generate.py @@ -1,8 +1,10 @@ +import warnings + from PIL import Image, ImageDraw, ImageFont import requests, os from time import sleep -DNDB_BASE_URL = "https://www.dndbeyond.com/character/" +DNDB_BASE_URL = "https://character-service.dndbeyond.com/character/v5/character" DNDB_CHARS = [ { "characterId": 69760590, @@ -54,6 +56,14 @@ DNDB_CHARS = [ } ] + +class AbilityScoreCalculator: + @staticmethod + def calculate_modifier(stat): + import math + return math.floor((stat - 10) / 2) + + class DataModifierService: def getModifiersByType(self, groupedModifiers: dict, type: str, subType: str = "") -> list: matching = [] @@ -66,17 +76,206 @@ class DataModifierService: matching += filtered + # print(matching) return matching def getTotalModifierValue(self, groupedModifiers: dict, type: str, subType: str) -> int: modifiers = self.getModifiersByType(groupedModifiers, type, subType) modifier_values = [modifier['value'] for modifier in modifiers] + # print(modifier_values) return sum(modifier_values) -def get_stat_modifier(stat): - import math - return math.floor((stat - 10) / 2) +class CharacterService: + + def __init__(self): + self.ability_score_calculator = AbilityScoreCalculator() + self.data_modifier = DataModifierService() + # self.item_ac_calculator = item_ac_calculator + self.stat_map = { + 1: 'strength', + 2: 'dexterity', + 3: 'constitution', + 4: 'intelligence', + 5: 'wisdom', + 6: 'charisma', + } + +# def calculate_ac(self, character: dict) -> int: + +# dex_mod = self.get_stat_mod(character, 'dex') +# character_ac = 10 + dex_mod +# armor_ac = 0 +# shield_ac = 0 + +# item_ac_calculator = self.item_ac_calculator + +# equipped_items = list(filter(lambda item: not item['equipped'] and item['definition'], character['inventory'])) + +# equipped_shields = list(filter(lambda item: item['definition']['armorTypeId'] == item_ac_calculator.ARMOR_TYPE_SHIELD, equipped_items)) + +# equipped_armor = list(filter(lambda item: item_ac_calculator.isArmorItem(item['definition']) and item['definition']['armorTypeId'] != item_ac_calculator.ARMOR_TYPE_SHIELD, equipped_items)) + +# if equipped_shields: +# shield_ac = max(map(lambda item: self.item_ac_calculator.calculateItemAc(item['definition'], dex_mod, False), equipped_shields)) or 0 + +# if equipped_armor: +# armor_ac = max(map(lambda item: self.item_ac_calculator.calculateItemAc(item['definition'], dex_mod, False), equipped_armor)) or 0 +# else: +# unarmored_modifiers = self.data_modifier.getModifiersByType(character['modifiers'], 'set', 'unarmored-armor-class') or [] +# armor_ac = max(map(lambda modifier: character_ac + self.get_stat_mod(character, int(modifier['statId'])) + modifier['value'] if modifier.get('statId') else character_ac + modifier['value'], unarmored_modifiers)) or 0 + +# bonus_ac = self.data_modifier.getTotalModifierValue(character['modifiers'], 'bonus', 'armor-class') + +# return max(character_ac, armor_ac) + shield_ac + bonus_ac + + def get_stat_mod(self, character, stat_name): + if isinstance(stat_name, int): + stat_name = self.stat_map.get(stat_name, '') + stat_name = stat_name.lower() + + stat_id = False + + for dndbeyond_id, allowed_stat in self.stat_map.items(): + if stat_name == allowed_stat or allowed_stat.startswith(stat_name[:3]): + stat_id = dndbeyond_id + break + + if stat_id is False: + return 0 + + stats = {stat['id']: stat.get('value') for stat in character.get('overrideStats', [])} + if stat_id not in stats or stats[stat_id] == None: + stats = {stat['id']: stat.get('value') or 0 for stat in character.get('stats', [])} + + if stat_id not in stats: + return 0 + + bonus_stat_name = self.stat_map[stat_id] + '-score' + bonus_stats = character.get('bonusStats', {}) + bonus_value = sum([bonus_stat['value'] for bonus_stat in bonus_stats if bonus_stat['name'] == bonus_stat_name]) + + # print(f"getTotalModifierValue({character.get('modifiers', [])}, 'bonus', {bonus_stat_name})") + total_bonus = self.data_modifier.getTotalModifierValue(character.get('modifiers', {}), 'bonus', bonus_stat_name) + + all_stats_value = stats[stat_id] + bonus_value + total_bonus + limited_stats_value = all_stats_value if all_stats_value <= 20 else 20 + # print(f".calculate_modifier({stats[stat_id]} + {bonus_value} + {total_bonus})") + + return self.ability_score_calculator.calculate_modifier(limited_stats_value) + + def get_max_hp(self, character): + import math + + if character.get('overrideHitPoints', False): + return character['overrideHitPoints'] + + max_hp = 0 + # if 'hitPointType' in character['preferences']: + # bonus_per_level_hp = self.data_modifier.getTotalModifierValue(character['modifiers'], 'bonus', 'hit-points-per-level') + + # con = self.get_stat_mod(character, 'con') + + # for class_ in character['classes']: + # hit_die = class_['definition']['hitDice'] + # adjusted_level = class_['level'] + + # if 'isStartingClass' in class_ and class_['isStartingClass']: + # max_hp += hit_die + con + # adjusted_level -= 1 + + # print(f"(({hit_die} / 2) + 1) + {con}) * {adjusted_level} + {bonus_per_level_hp} * {class_['level']}") + + # max_hp += (math.ceil((hit_die / 2) + 1) + con) * adjusted_level + bonus_per_level_hp * class_['level'] + hp_type = character['preferences'].get('hitPointType', False) + if hp_type == 2: + max_hp = character.get('baseHitPoints', 0) + const_bonus = self.get_stat_mod(character, 'con') + max_hp += const_bonus*10 + else: + max_hp = character.get('baseHitPoints', 0) + + max_hp += character.get('bonusHitPoints', 0) or 0 + + return max_hp + + # def get_passive_score(self, character, proficiency_name): + # proficiency_name = proficiency_name.lower() + + # if proficiency_name in ['insight', 'perception']: + # stat_mod = self.get_stat_mod(character, 'wis') + # elif proficiency_name == 'investigation': + # stat_mod = self.get_stat_mod(character, 'int') + # else: + # stat_mod = 0 + + # skill_mod = 0 + + # active_bonuses = self.data_modifier.getModifiersByType(character['modifiers'], 'proficiency', proficiency_name) + # if active_bonuses: + # skill_mod = self.get_proficiency_bonus(character) + + # passive_bonuses = self.data_modifier.getModifiersByType(character['modifiers'], 'bonus', f"passive-{proficiency_name}") + # skill_mod += sum([bonus['value'] for bonus in passive_bonuses]) + + # return 10 + stat_mod + skill_mod + + # def get_xp_needed(self, character): + # xp_per_level = [ + # 0, + # 300, + # 900, + # 2700, + # 6500, + # 14000, + # 23000, + # 34000, + # 48000, + # 64000, + # 85000, + # 100000, + # 120000, + # 140000, + # 165000, + # 195000, + # 225000, + # 265000, + # 305000, + # 355000, + # ] + + # level = sum([class_['level'] for class_ in character['classes']]) + + # return xp_per_level[min(level, len(xp_per_level) - 1)] + + # def get_proficiency_bonus(self, character): + # skill_proficiency_by_level = [ + # 2, + # 2, + # 2, + # 2, + # 3, + # 3, + # 3, + # 3, + # 4, + # 4, + # 4, + # 4, + # 5, + # 5, + # 5, + # 5, + # 6, + # 6, + # 6, + # 6, + # ] + + # level = sum([class_['level'] for class_ in character['classes']]) + # level = max(1, min(level, 20)) + + # return skill_proficiency_by_level[level - 1] def calculate_coordinates(object_width, object_height, center_point): """ @@ -95,15 +294,17 @@ def calculate_coordinates(object_width, object_height, center_point): return (x, y) def get_correct_text_sizes(draw, text: str, font: ImageFont.FreeTypeFont): - label_width, label_height = draw.textsize(text, font=font) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + label_width, label_height = draw.textsize(text, font=font) - # to fix text placing we need to get offsets - # https://stackoverflow.com/questions/59008322/pillow-imagedraw-text-coordinates-to-center - offset_x, offset_y = font.getoffset(text) - label_width += offset_x - label_height += offset_y + # to fix text placing we need to get offsets + # https://stackoverflow.com/questions/59008322/pillow-imagedraw-text-coordinates-to-center + offset_x, offset_y = font.getoffset(text) + label_width += offset_x + label_height += offset_y - return (label_width, label_height) + return (label_width, label_height) def generate_main(char: dict): STATS_DICT = { @@ -140,10 +341,9 @@ def generate_main(char: dict): } for i in range(3): - response = requests.get(f"{DNDB_BASE_URL}/{char['characterId']}/json", headers=headers) + response = requests.get(f"{DNDB_BASE_URL}/{char['characterId']}?includeCustomItems=true", headers=headers) if response.status_code == 200: - data = response.json() - print(data["stats"]) + data = response.json()['data'] break sleep(1) @@ -151,6 +351,11 @@ def generate_main(char: dict): print("Error: Failed to retrieve data from URL.") return False + ch = CharacterService() + print(f"Chars name: {char['characterName']}") + max_hp = ch.get_max_hp(data) + print(f"CHARS HEALTH: {max_hp-int(data['removedHitPoints'])}/{max_hp}") + # Load the background image background = Image.open("parts/background-main.png") @@ -165,13 +370,8 @@ def generate_main(char: dict): draw_width, _ = draw.im.size # pyright: ignore[reportGeneralTypeIssues] - for stat in data["stats"]: - modifiers = data.get('modifiers', {}) - - modifier_service = DataModifierService() - total_modifier_value = modifier_service.getTotalModifierValue(modifiers, 'bonus', STATS_DICT[stat['id']]['name'] + '-score') - stat["value"] += total_modifier_value - final_stat = get_stat_modifier(stat["value"]) + for stat in ch.stat_map.keys(): + final_stat = ch.get_stat_mod(data, stat) # Define the font and font size for the text labels stats_font = ImageFont.truetype("fonts/NotoSerif-Regular.ttf", 30) @@ -179,10 +379,10 @@ def generate_main(char: dict): # Add text labels on top of the image stats_label = f"+{final_stat}" if final_stat > 0 else f"{final_stat}" - print(f"{STATS_DICT[stat['id']]['name']}: {stats_label}") + print(f"{STATS_DICT[stat]['name']}: {stats_label}") label_width, label_height = get_correct_text_sizes(draw, stats_label, stats_font) - draw.text(calculate_coordinates(label_width, label_height, STATS_DICT[stat["id"]]["coords"]), stats_label, font=stats_font, fill=(255, 255, 255)) + draw.text(calculate_coordinates(label_width, label_height, STATS_DICT[stat]["coords"]), stats_label, font=stats_font, fill=(255, 255, 255)) name_font = ImageFont.truetype("fonts/alaruss.ttf", 96) name_label = char['characterName'].upper() @@ -211,5 +411,6 @@ def generate_main(char: dict): empty_image = Image.new("RGBA", (100, 100), (0, 0, 0, 0)) empty_image.save("results/transparent.png") +# generate_main(DNDB_CHARS[1]) for char in DNDB_CHARS: generate_main(char)