You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
fabula/embedgen.py

283 lines
7.3 KiB

from math import ceil
from typing import Iterable
from discord_webhook import DiscordEmbed, AsyncDiscordWebhook
from model import Character
CantMoveEmoji = ""
DoneMovingEmoji = ""
NotMovedEmoji = "🟦"
TurnsLeftEmoji = [
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"🔟",
"#",
]
IndicatorKeys = {
None: "",
"0": "0",
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5",
"6": "6",
"7": "7",
"8": "8",
"9": "9",
"A": "🇦",
"B": "🇧",
"C": "🇨",
"D": "🇩",
"E": "🇪",
"F": "🇫",
"G": "🇬",
"H": "🇭",
"I": "🇮",
"J": "🇯",
"K": "🇰",
"L": "🇱",
"M": "🇲",
"N": "🇳",
"O": "🇴",
"P": "🇵",
"Q": "🇶",
"R": "🇷",
"S": "🇸",
"T": "🇹",
"U": "🇺",
"V": "🇻",
"W": "🇼",
"X": "🇽",
"Y": "🇾",
"Z": "🇿",
}
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):
header = [
turn_icon(c),
IndicatorKeys.get(c.access_key, IndicatorKeys[None]),
" ",
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, statuses):
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.role.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)