|
|
""" |
|
|
DungeonMaster AI - Output Formatters |
|
|
|
|
|
Utilities for formatting game data for display in chat and UI components. |
|
|
""" |
|
|
|
|
|
from typing import Any |
|
|
|
|
|
|
|
|
def format_dice_roll( |
|
|
notation: str, |
|
|
individual_rolls: list[int], |
|
|
modifier: int, |
|
|
total: int, |
|
|
is_check: bool = False, |
|
|
dc: int | None = None, |
|
|
success: bool | None = None, |
|
|
) -> str: |
|
|
""" |
|
|
Format a dice roll result for display. |
|
|
|
|
|
Args: |
|
|
notation: The dice notation (e.g., "2d6+3") |
|
|
individual_rolls: List of individual die results |
|
|
modifier: The modifier applied to the roll |
|
|
total: The final total |
|
|
is_check: Whether this was an ability check/save |
|
|
dc: Difficulty class if this was a check |
|
|
success: Whether the check succeeded (if applicable) |
|
|
|
|
|
Returns: |
|
|
Formatted string for display |
|
|
""" |
|
|
|
|
|
dice_str = ", ".join(str(r) for r in individual_rolls) |
|
|
|
|
|
|
|
|
if modifier > 0: |
|
|
breakdown = f"[{dice_str}] + {modifier}" |
|
|
elif modifier < 0: |
|
|
breakdown = f"[{dice_str}] - {abs(modifier)}" |
|
|
else: |
|
|
breakdown = f"[{dice_str}]" |
|
|
|
|
|
|
|
|
is_critical = False |
|
|
is_fumble = False |
|
|
if len(individual_rolls) == 1 and individual_rolls[0] == 20: |
|
|
is_critical = "d20" in notation.lower() |
|
|
if len(individual_rolls) == 1 and individual_rolls[0] == 1: |
|
|
is_fumble = "d20" in notation.lower() |
|
|
|
|
|
|
|
|
result_parts = [f"{notation} = {breakdown} = **{total}**"] |
|
|
|
|
|
if is_critical: |
|
|
result_parts.append("**CRITICAL!**") |
|
|
elif is_fumble: |
|
|
result_parts.append("*Critical Failure!*") |
|
|
|
|
|
if is_check and dc is not None: |
|
|
if success: |
|
|
result_parts.append(f"vs DC {dc}: **Success!**") |
|
|
else: |
|
|
result_parts.append(f"vs DC {dc}: *Failure*") |
|
|
|
|
|
return " ".join(result_parts) |
|
|
|
|
|
|
|
|
def format_hp_change( |
|
|
character_name: str, |
|
|
previous_hp: int, |
|
|
current_hp: int, |
|
|
max_hp: int, |
|
|
is_damage: bool, |
|
|
) -> str: |
|
|
""" |
|
|
Format an HP change for display. |
|
|
|
|
|
Args: |
|
|
character_name: Name of the character |
|
|
previous_hp: HP before the change |
|
|
current_hp: HP after the change |
|
|
max_hp: Maximum HP |
|
|
is_damage: True if damage, False if healing |
|
|
|
|
|
Returns: |
|
|
Formatted string for display |
|
|
""" |
|
|
change = abs(current_hp - previous_hp) |
|
|
|
|
|
if is_damage: |
|
|
if current_hp <= 0: |
|
|
return f"**{character_name}** takes {change} damage and falls unconscious! (0/{max_hp} HP)" |
|
|
elif current_hp <= max_hp // 4: |
|
|
return f"**{character_name}** takes {change} damage and is badly wounded! ({current_hp}/{max_hp} HP)" |
|
|
else: |
|
|
return f"**{character_name}** takes {change} damage. ({current_hp}/{max_hp} HP)" |
|
|
else: |
|
|
if current_hp >= max_hp: |
|
|
return f"**{character_name}** is fully healed! ({max_hp}/{max_hp} HP)" |
|
|
else: |
|
|
return f"**{character_name}** heals {change} HP. ({current_hp}/{max_hp} HP)" |
|
|
|
|
|
|
|
|
def format_combat_turn( |
|
|
combatant_name: str, |
|
|
initiative: int, |
|
|
is_player: bool, |
|
|
round_number: int, |
|
|
) -> str: |
|
|
""" |
|
|
Format a combat turn announcement. |
|
|
|
|
|
Args: |
|
|
combatant_name: Name of the combatant whose turn it is |
|
|
initiative: Their initiative value |
|
|
is_player: Whether this is a player character |
|
|
round_number: Current combat round |
|
|
|
|
|
Returns: |
|
|
Formatted string for display |
|
|
""" |
|
|
if is_player: |
|
|
return f"**Round {round_number}** - It's your turn, **{combatant_name}**! (Initiative: {initiative})" |
|
|
else: |
|
|
return f"**Round {round_number}** - **{combatant_name}** takes their turn. (Initiative: {initiative})" |
|
|
|
|
|
|
|
|
def format_ability_modifier(score: int) -> str: |
|
|
""" |
|
|
Format an ability score modifier for display. |
|
|
|
|
|
Args: |
|
|
score: The ability score (1-30) |
|
|
|
|
|
Returns: |
|
|
Formatted modifier string (e.g., "+2" or "-1") |
|
|
""" |
|
|
modifier = (score - 10) // 2 |
|
|
if modifier >= 0: |
|
|
return f"+{modifier}" |
|
|
return str(modifier) |
|
|
|
|
|
|
|
|
def format_currency(gp: int = 0, sp: int = 0, cp: int = 0) -> str: |
|
|
""" |
|
|
Format currency for display. |
|
|
|
|
|
Args: |
|
|
gp: Gold pieces |
|
|
sp: Silver pieces |
|
|
cp: Copper pieces |
|
|
|
|
|
Returns: |
|
|
Formatted currency string |
|
|
""" |
|
|
parts = [] |
|
|
if gp > 0: |
|
|
parts.append(f"{gp} gp") |
|
|
if sp > 0: |
|
|
parts.append(f"{sp} sp") |
|
|
if cp > 0: |
|
|
parts.append(f"{cp} cp") |
|
|
|
|
|
if not parts: |
|
|
return "0 gp" |
|
|
return ", ".join(parts) |
|
|
|
|
|
|
|
|
def format_condition_list(conditions: list[str]) -> str: |
|
|
""" |
|
|
Format a list of conditions for display. |
|
|
|
|
|
Args: |
|
|
conditions: List of condition names |
|
|
|
|
|
Returns: |
|
|
Formatted string or empty message |
|
|
""" |
|
|
if not conditions: |
|
|
return "None" |
|
|
return ", ".join(conditions) |
|
|
|
|
|
|
|
|
def format_initiative_order( |
|
|
combatants: list[dict[str, Any]], |
|
|
current_turn_index: int, |
|
|
) -> str: |
|
|
""" |
|
|
Format the initiative order for display. |
|
|
|
|
|
Args: |
|
|
combatants: List of combatant dictionaries with name, initiative, hp info |
|
|
current_turn_index: Index of current turn combatant |
|
|
|
|
|
Returns: |
|
|
Formatted initiative order string |
|
|
""" |
|
|
lines = ["**Initiative Order:**"] |
|
|
|
|
|
for i, combatant in enumerate(combatants): |
|
|
marker = ">" if i == current_turn_index else " " |
|
|
name = combatant.get("name", "Unknown") |
|
|
init = combatant.get("initiative", 0) |
|
|
hp_current = combatant.get("hp_current", 0) |
|
|
hp_max = combatant.get("hp_max", 1) |
|
|
|
|
|
hp_percent = (hp_current / hp_max) * 100 if hp_max > 0 else 0 |
|
|
if hp_percent > 50: |
|
|
status = "Healthy" |
|
|
elif hp_percent > 25: |
|
|
status = "Wounded" |
|
|
elif hp_percent > 0: |
|
|
status = "Critical" |
|
|
else: |
|
|
status = "Down" |
|
|
|
|
|
lines.append(f"{marker} {init:2d} | {name} ({status})") |
|
|
|
|
|
return "\n".join(lines) |
|
|
|
|
|
|
|
|
def format_character_summary(character: dict[str, Any]) -> str: |
|
|
""" |
|
|
Format a character summary for context. |
|
|
|
|
|
Args: |
|
|
character: Character data dictionary |
|
|
|
|
|
Returns: |
|
|
Formatted character summary |
|
|
""" |
|
|
name = character.get("name", "Unknown") |
|
|
race = character.get("race", "Unknown") |
|
|
char_class = character.get("class", "Unknown") |
|
|
level = character.get("level", 1) |
|
|
|
|
|
hp = character.get("hit_points", {}) |
|
|
hp_current = hp.get("current", 0) |
|
|
hp_max = hp.get("maximum", 1) |
|
|
|
|
|
ac = character.get("armor_class", 10) |
|
|
conditions = character.get("conditions", []) |
|
|
|
|
|
summary = f"**{name}** - Level {level} {race} {char_class}\n" |
|
|
summary += f"HP: {hp_current}/{hp_max} | AC: {ac}\n" |
|
|
|
|
|
if conditions: |
|
|
summary += f"Conditions: {format_condition_list(conditions)}" |
|
|
|
|
|
return summary |
|
|
|
|
|
|
|
|
def format_adventure_intro(adventure_data: dict[str, Any]) -> str: |
|
|
""" |
|
|
Format an adventure introduction for the opening narration. |
|
|
|
|
|
Args: |
|
|
adventure_data: Adventure JSON data |
|
|
|
|
|
Returns: |
|
|
Formatted introduction text |
|
|
""" |
|
|
metadata = adventure_data.get("metadata", {}) |
|
|
starting_scene = adventure_data.get("starting_scene", {}) |
|
|
|
|
|
name = metadata.get("name", "Unnamed Adventure") |
|
|
description = metadata.get("description", "") |
|
|
narrative = starting_scene.get("narrative", "Your adventure begins...") |
|
|
|
|
|
intro = f"# {name}\n\n" |
|
|
if description: |
|
|
intro += f"*{description}*\n\n" |
|
|
intro += "---\n\n" |
|
|
intro += str(narrative) |
|
|
|
|
|
return intro |
|
|
|