new statmod calculation + health calculation

This commit is contained in:
2023-06-10 10:36:18 +03:00
parent 9e1438433f
commit 4d165fa2c9

View File

@@ -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)