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