from math import ceil from typing import Iterable from discord_webhook import DiscordEmbed, AsyncDiscordWebhook from enums import CantMoveEmoji, DoneMovingEmoji, NotMovedEmoji, TurnsLeftEmoji, get_indicator_key from model import Character def simple_character_field(c: Character) -> tuple[str, str]: header, statuses = character_header(c) if c.visibility.show_stats: body = [ f"**HP** {c.hp}/{c.max_hp}", f"**MP** {c.mp}/{c.max_mp}", ] else: hp_caption, _ = hp_text(c.hp, c.max_hp) mp_caption, _ = mp_text(c.mp, c.max_mp) body = [ f"**HP** {hp_caption}", f"**MP** {mp_caption}", ] body.extend(character_details(c, statuses)) return "".join(header), " / ".join(body) def detailed_character_field(c: Character) -> tuple[str, str]: header, statuses = character_header(c) if c.visibility.show_stats: body = [ f'**HP** {bar_string(c.hp, c.max_hp, length=80)} {c.hp}/{c.max_hp}', f'**MP** {bar_string(c.mp, c.max_mp, length=80)} {c.mp}/{c.max_mp}', ] else: hp_caption, hp_value = hp_text(c.hp, c.max_hp) mp_caption, mp_value = mp_text(c.mp, c.max_mp) body = [ f'**HP** {bar_string(hp_value, 5, length=80)} {hp_caption}', f'**MP** {bar_string(mp_value, 5, length=80)} {mp_caption}', ] details = character_details(c, statuses) if len(details) > 0: body.append(" / ".join(details)) return "".join(header), "\n".join(body) def detailed_character_embed(c: Character) -> DiscordEmbed: header, statuses = character_header(c) if c.visibility.show_stats: fields = [ ( f"**HP** {c.hp}/{c.max_hp}", bar_string(c.hp, c.max_hp, length=80), False, ), ( f"**MP** {c.mp}/{c.max_mp}", bar_string(c.mp, c.max_mp, length=80), False, ), ] else: hp_caption, _ = hp_text(c.hp, c.max_hp) mp_caption, _ = mp_text(c.mp, c.max_mp) fields = [ ( f"**HP**", hp_caption, True, ), ( f"**MP**", mp_caption, True, ), ] if c.max_ip > 0: fields.append((f"**IP**", f"{c.ip}/{c.max_ip}", True)) if c.sp is not None: fields.append((f"**{c.character_type.sp_name_abbr}**", str(c.sp), True)) if len(statuses) > 0: fields.append(("**Status**", f'_{", ".join(statuses)}_', True)) result = DiscordEmbed( title="".join(header), ) if c.player_name is not None: result.set_footer(text=f'Player: {c.player_name}') if c.color is not None: result.set_color(c.color) if c.thumbnail is not None: result.set_thumbnail(c.thumbnail) for name, value, inline in fields: result.add_embed_field(name, value, inline) return result def character_header(c: Character) -> tuple[list[str], list[str]]: header = [ turn_icon(c), get_indicator_key(c.access_key), " ", f'**{c.name}**' if c.visibility.show_name else '***???***' ] statuses = list(sorted(str(s) for s in c.statuses)) hp = hp_marker(c.hp, c.max_hp) if hp is not None: header.append(" ") icon, status = hp statuses.insert(0, status), header.append(icon) return header, statuses def character_details(c: Character, statuses: list[str]): result = [] if c.max_ip > 0: result.append(f"**IP** {c.ip}/{c.max_ip}") if c.sp is not None: result.append(f"**{c.character_type.sp_name_abbr}** {c.sp}") if len(statuses) > 0: result.append(f'_{", ".join(statuses)}_') return result def bar_string(val: int, mx: int, delta: int | None = None, length: int | None = None) -> str: effective_delta = delta if delta is not None else 0 length = length if length is not None else mx if effective_delta >= 0: filled_size = ceil(length * val / mx) prev_size = ceil(length * (val + effective_delta) / mx) delta_size = filled_size - prev_size bar = '\\|' if prev_size % 2 == 1 else "" bar += (prev_size // 2) * "\\||" if delta_size > 0: bar += f'**{"⏐" * delta_size}**' bar += (length - filled_size) * "·" else: filled_size = ceil(length * val / mx) prev_size = ceil(length * (val - effective_delta) / mx) delta_size = prev_size - filled_size bar = '\\|' if filled_size % 2 == 1 else "" bar += (filled_size // 2) * "\\||" if delta_size > 0: bar += f'~~{"·" * delta_size}~~' bar += (length - prev_size) * "·" return bar def turn_icon(c: Character) -> str: if c.max_turns <= 0 or c.hp == 0: return CantMoveEmoji elif c.turns_left <= 0: return DoneMovingEmoji elif c.max_turns == 1: return NotMovedEmoji else: return TurnsLeftEmoji[-1] if c.turns_left >= len(TurnsLeftEmoji) else TurnsLeftEmoji[c.turns_left] def hp_marker(hp: int, max_hp: int) -> tuple[str, str] | None: if hp == 0: return "🏳️", "**Surrendered**" elif hp * 2 < max_hp: return "⚠️", "**Crisis**" else: return None def hp_text(hp: int, max_hp: int) -> tuple[str, int]: if hp == 0: return "Defeated", 0 elif hp * 4 < max_hp: return "Peril", 1 elif hp * 2 < max_hp: return "Crisis", 2 elif hp * 4 < 3 * max_hp: return "Wounded", 3 elif hp < max_hp: return "Scratched", 4 else: return "Full", 5 def mp_text(mp: int, max_mp: int) -> tuple[str, int]: if mp == 0: return "Empty", 0 elif mp * 4 < max_mp: return "Exhausted", 1 elif mp * 2 < max_mp: return "Tired", 2 elif mp * 4 < 3 * max_mp: return "Flagging", 3 elif mp < max_mp: return "Energetic", 4 else: return "Full", 5 class WebhookExecutor(object): __slots__ = ["url"] def __init__(self, url: str): self.url = url async def run(self, embeds: Iterable[DiscordEmbed]): webhook = AsyncDiscordWebhook( url=self.url, username="Party Status", avatar_url="https://media.discordapp.net/attachments/989315969920929862/" + "1084018762606444634/heartmonitor.png", embeds=embeds, ) await webhook.execute(False)