new statmod calculation + health calculation
This commit is contained in:
247
generate.py
247
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)
|
||||
|
||||
Reference in New Issue
Block a user