""" 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 """ # Format individual dice dice_str = ", ".join(str(r) for r in individual_rolls) # Build the breakdown if modifier > 0: breakdown = f"[{dice_str}] + {modifier}" elif modifier < 0: breakdown = f"[{dice_str}] - {abs(modifier)}" else: breakdown = f"[{dice_str}]" # Check for critical (d20 rolls) 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() # Build result string 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