|
|
|
from typing import Callable
|
|
|
|
|
|
|
|
from urwid import Pile, Columns, ListBox, SimpleFocusListWalker, Text, WidgetWrap, SelectableIcon, AttrMap, Padding, \
|
|
|
|
Divider, LineBox, WidgetPlaceholder, Overlay, Widget, Edit, IntEdit, \
|
|
|
|
Button
|
|
|
|
|
|
|
|
from model import Character, Clock
|
|
|
|
from enums import CharacterType, Counter, decode_discord, CombatSide
|
|
|
|
|
|
|
|
|
|
|
|
class SessionStateUI(WidgetWrap):
|
|
|
|
@staticmethod
|
|
|
|
def render_round_timer(rd: int | None, fp_spent: int, up_spent: int):
|
|
|
|
return (
|
|
|
|
([('RoundLabel', 'Round'), ':\u00A0', ('RoundNumber', str(rd)), ' / '] if rd is not None else []) +
|
|
|
|
([('FabulaLabel', 'FP\u00A0Used'), ':\u00A0', ('FabulaNumber', str(fp_spent))]) +
|
|
|
|
([' / ', ('UltimaLabel', 'UP\u00A0Used'), ':\u00A0', ('UltimaNumber', str(up_spent))]
|
|
|
|
if up_spent > 0 else [])
|
|
|
|
)
|
|
|
|
|
|
|
|
def __init__(self, rd: int | None, fp_spent: int, up_spent: int):
|
|
|
|
self.text = Text(self.render_round_timer(rd=rd, fp_spent=fp_spent, up_spent=up_spent))
|
|
|
|
super().__init__(Columns([
|
|
|
|
(4, Padding(
|
|
|
|
w=AttrMap(
|
|
|
|
SelectableIcon(text='<◕>', cursor_position=1),
|
|
|
|
'ClockUnfocused', 'ClockFocused'),
|
|
|
|
align='left',
|
|
|
|
width=3,
|
|
|
|
right=1)),
|
|
|
|
('weight', 1, self.text)]))
|
|
|
|
|
|
|
|
def update_session(self, rd: int | None, fp_spent: int, up_spent: int):
|
|
|
|
self.text.set_text(self.render_round_timer(rd=rd, fp_spent=fp_spent, up_spent=up_spent))
|
|
|
|
|
|
|
|
|
|
|
|
class ClockUI(WidgetWrap):
|
|
|
|
@classmethod
|
|
|
|
def render_clock_text(cls, clock: Clock):
|
|
|
|
return [
|
|
|
|
('ClockName', clock.name),
|
|
|
|
' ',
|
|
|
|
('ClockBarEdge', '❰'),
|
|
|
|
('ClockBarFilled', '❚' * clock.current),
|
|
|
|
('ClockBarEmpty', '·' * (clock.maximum - clock.current)),
|
|
|
|
('ClockBarEdge', '❱'),
|
|
|
|
' ',
|
|
|
|
('ClockCurrent', str(clock.current)),
|
|
|
|
'/',
|
|
|
|
('ClockMax', str(clock.maximum))
|
|
|
|
]
|
|
|
|
|
|
|
|
def __init__(self, clock: Clock):
|
|
|
|
self.text = Text(self.render_clock_text(clock=clock))
|
|
|
|
super().__init__(Columns([
|
|
|
|
(4, Padding(
|
|
|
|
w=AttrMap(
|
|
|
|
SelectableIcon(text='<◕>', cursor_position=1),
|
|
|
|
'ClockUnfocused', 'ClockFocused'),
|
|
|
|
align='left',
|
|
|
|
width=3,
|
|
|
|
right=1)),
|
|
|
|
('weight', 1, self.text)]))
|
|
|
|
|
|
|
|
def update(self, clock):
|
|
|
|
self.text.set_text(
|
|
|
|
self.render_clock_text(clock=clock))
|
|
|
|
|
|
|
|
|
|
|
|
class ClocksUI(WidgetWrap):
|
|
|
|
def __init__(self):
|
|
|
|
self.state = SessionStateUI(rd=0, fp_spent=0, up_spent=0)
|
|
|
|
self.items = SimpleFocusListWalker([self.state])
|
|
|
|
self.list = ListBox(self.items)
|
|
|
|
super().__init__(self.list)
|
|
|
|
|
|
|
|
def update_session(self, rd: int | None, fp_spent: int, up_spent: int):
|
|
|
|
self.state.update_session(rd=rd, fp_spent=fp_spent, up_spent=up_spent)
|
|
|
|
|
|
|
|
def add_clock(self, clock: Clock, index: int = -1) -> int:
|
|
|
|
if index < 0:
|
|
|
|
index = len(self.items) - 1
|
|
|
|
self.items.insert(index + 1, ClockUI(clock=clock))
|
|
|
|
return index
|
|
|
|
|
|
|
|
def update_clock(self, index: int, clock: Clock):
|
|
|
|
if index < 0:
|
|
|
|
return
|
|
|
|
self.items[index + 1].update(clock)
|
|
|
|
|
|
|
|
def reorder_clock(self, old: int, new: int = -1):
|
|
|
|
focused = self.list.focus_position == old + 1
|
|
|
|
item = self.items.pop(old + 1)
|
|
|
|
new = new if new >= 0 else len(self.items)
|
|
|
|
self.items.insert(new + 1, self.items.pop(item))
|
|
|
|
if focused:
|
|
|
|
self.items.set_focus(position=new)
|
|
|
|
|
|
|
|
def remove_clock(self, index: int):
|
|
|
|
self.items.pop(index + 1)
|
|
|
|
|
|
|
|
def clear(self):
|
|
|
|
self.items.clear()
|
|
|
|
self.append(self.state)
|
|
|
|
|
|
|
|
|
|
|
|
class CharacterUI(WidgetWrap):
|
|
|
|
@classmethod
|
|
|
|
def render_character_text(cls, character: Character):
|
|
|
|
if character.side == CombatSide.Heroes:
|
|
|
|
if character.character_type == CharacterType.PlayerCharacter:
|
|
|
|
role_icon = '♡'
|
|
|
|
role_style = 'CharacterPlayer'
|
|
|
|
else:
|
|
|
|
role_icon = '♥'
|
|
|
|
role_style = 'CharacterAlly'
|
|
|
|
else:
|
|
|
|
if character.character_type == CharacterType.NonPlayerCharacter:
|
|
|
|
role_icon = '♤'
|
|
|
|
role_style = 'CharacterEnemy'
|
|
|
|
else:
|
|
|
|
role_icon = '♠'
|
|
|
|
role_style = 'CharacterEnemyPlayer'
|
|
|
|
if character.max_turns <= 0 or character.hp == 0:
|
|
|
|
turn_style = 'TurnDisabled'
|
|
|
|
turn_text = '[✖]'
|
|
|
|
elif character.turns_left > 9:
|
|
|
|
turn_style = 'TurnAvailable'
|
|
|
|
turn_text = f'[#]'
|
|
|
|
elif character.turns_left > 1:
|
|
|
|
turn_style = 'TurnAvailable'
|
|
|
|
turn_text = f'[{character.turns_left}]'
|
|
|
|
elif character.turns_left <= 0:
|
|
|
|
turn_style = 'TurnActed'
|
|
|
|
turn_text = '[✓]'
|
|
|
|
elif character.max_turns > 1:
|
|
|
|
turn_style = 'TurnAvailable'
|
|
|
|
turn_text = '[1]'
|
|
|
|
else:
|
|
|
|
turn_style = 'TurnAvailable'
|
|
|
|
turn_text = '[ ]'
|
|
|
|
if character.hp == character.max_hp:
|
|
|
|
hp_style = 'HPFull'
|
|
|
|
hp_suffix = '★'
|
|
|
|
elif character.hp * 4 > character.max_hp * 3:
|
|
|
|
hp_style = 'HPScratched'
|
|
|
|
hp_suffix = '☆'
|
|
|
|
elif character.hp * 2 > character.max_hp:
|
|
|
|
hp_style = 'HPWounded'
|
|
|
|
hp_suffix = '\u00A0'
|
|
|
|
elif character.hp * 4 > character.max_hp:
|
|
|
|
hp_style = 'HPCrisis'
|
|
|
|
hp_suffix = '!'
|
|
|
|
elif character.hp > 0:
|
|
|
|
hp_style = 'HPPeril'
|
|
|
|
hp_suffix = '‼'
|
|
|
|
else:
|
|
|
|
hp_style = 'HPDown'
|
|
|
|
hp_suffix = '✖'
|
|
|
|
return ([(turn_style, turn_text), ' ', (role_style, role_icon + character.name)],
|
|
|
|
['\u00A0\u00A0\u00A0\u00A0',
|
|
|
|
(hp_style, hp_suffix),
|
|
|
|
('HPLabel', 'HP'),
|
|
|
|
'\u00A0',
|
|
|
|
(hp_style, str(character.hp).rjust(3, '\u00A0')),
|
|
|
|
'/',
|
|
|
|
('HPMax', str(character.max_hp).rjust(3, '\u00A0')),
|
|
|
|
' \u00A0\u00A0\u00A0\u00A0',
|
|
|
|
('MPLabel', '\u00A0MP'),
|
|
|
|
'\u00A0',
|
|
|
|
('MPValue', str(character.mp).rjust(3, '\u00A0')),
|
|
|
|
'/',
|
|
|
|
('MPMax', str(character.max_mp).rjust(3, '\u00A0'))] + ([
|
|
|
|
' \u00A0\u00A0\u00A0\u00A0',
|
|
|
|
('FPLabel' if character.character_type == CharacterType.PlayerCharacter else 'UPLabel',
|
|
|
|
'\u00A0FP' if character.character_type == CharacterType.PlayerCharacter else '\u00A0UP'),
|
|
|
|
'\u00A0',
|
|
|
|
('FPValue' if character.character_type == CharacterType.PlayerCharacter else 'UPValue',
|
|
|
|
str(character.sp).rjust(3, '\u00A0')),
|
|
|
|
'\u00A0' * 4
|
|
|
|
] if character.sp is not None else []) + ([
|
|
|
|
' \u00A0\u00A0\u00A0\u00A0',
|
|
|
|
('IPLabel', '\u00A0IP'),
|
|
|
|
'\u00A0',
|
|
|
|
('IPValue', str(character.ip).rjust(3, '\u00A0')),
|
|
|
|
'/',
|
|
|
|
('IPMax', str(character.max_ip).rjust(3, '\u00A0'))
|
|
|
|
] if character.max_ip > 0 else []) + ([
|
|
|
|
'\n \u00A0\u00A0\u00A0\u00A0',
|
|
|
|
(('StatusKO', "KO") if character.hp == 0 else
|
|
|
|
('StatusCrisis', "Crisis") if character.hp * 2 < character.max_hp else ""),
|
|
|
|
('Statuses',
|
|
|
|
", ".join(
|
|
|
|
([""] if character.hp * 2 < character.max_hp else []) +
|
|
|
|
sorted(list(str(s) for s in character.statuses)))),
|
|
|
|
] if len(character.statuses) > 0 or character.hp * 2 < character.max_hp else []))
|
|
|
|
|
|
|
|
def __init__(self, character: Character):
|
|
|
|
name_text, condition_text = self.render_character_text(character=character)
|
|
|
|
self.nameText = Text(name_text)
|
|
|
|
self.conditionText = Text(condition_text)
|
|
|
|
self.icon = SelectableIcon(
|
|
|
|
text=f'({character.access_key if character.access_key != "" else " "})', cursor_position=1)
|
|
|
|
super().__init__(Columns([
|
|
|
|
(4, Padding(
|
|
|
|
w=AttrMap(
|
|
|
|
self.icon,
|
|
|
|
'CharacterUnfocused', 'CharacterFocused'),
|
|
|
|
align='left',
|
|
|
|
width=3,
|
|
|
|
right=1)),
|
|
|
|
('weight', 1, Pile([self.nameText, self.conditionText]))]))
|
|
|
|
|
|
|
|
|
|
|
|
class CharactersUI(WidgetWrap):
|
|
|
|
def __init__(self):
|
|
|
|
self.items = SimpleFocusListWalker([])
|
|
|
|
self.list = ListBox(self.items)
|
|
|
|
super().__init__(ListBox(self.items))
|
|
|
|
|
|
|
|
def add_character(self, character: Character):
|
|
|
|
self.items.append(CharacterUI(character=character))
|
|
|
|
|
|
|
|
|
|
|
|
class LogMessageUI(WidgetWrap):
|
|
|
|
def __init__(self, text):
|
|
|
|
super().__init__(Columns([
|
|
|
|
(4, Padding(
|
|
|
|
w=AttrMap(
|
|
|
|
SelectableIcon(text='[▶]', cursor_position=1),
|
|
|
|
'LogUnfocused', 'LogFocused'),
|
|
|
|
align='left',
|
|
|
|
width=3,
|
|
|
|
right=1)),
|
|
|
|
('weight', 1, Text(decode_discord(text, 'LogText', 'LogBold')))
|
|
|
|
]))
|
|
|
|
|
|
|
|
|
|
|
|
class LogUI(WidgetWrap):
|
|
|
|
def __init__(self):
|
|
|
|
self.items = SimpleFocusListWalker([])
|
|
|
|
self.divider = Divider('=')
|
|
|
|
self.divider_by_zero = True
|
|
|
|
self.list = ListBox(self.items)
|
|
|
|
super().__init__(self.list)
|
|
|
|
|
|
|
|
def add_message(self, text: str):
|
|
|
|
if self.divider not in self.items and not self.divider_by_zero:
|
|
|
|
if len(self.items) == 0:
|
|
|
|
self.divider_by_zero = True
|
|
|
|
else:
|
|
|
|
self.items.insert(0, self.divider)
|
|
|
|
self.divider_by_zero = False
|
|
|
|
self.items.insert(0, LogMessageUI(text))
|
|
|
|
self.items.set_focus(0)
|
|
|
|
|
|
|
|
def move_divider(self, location: int = -1):
|
|
|
|
if self.divider in self.items:
|
|
|
|
self.items.remove(self.divider)
|
|
|
|
location = location if location > -1 else len(self.items)
|
|
|
|
if 0 < location <= len(self.items):
|
|
|
|
self.items.insert(len(self.items) - location, self.divider)
|
|
|
|
self.divider_by_zero = False
|
|
|
|
else:
|
|
|
|
self.divider_by_zero = True
|
|
|
|
|
|
|
|
def clear(self):
|
|
|
|
self.items.clear()
|
|
|
|
|
|
|
|
|
|
|
|
class MainUI(WidgetWrap):
|
|
|
|
def __init__(self, clocks: ClocksUI, characters: CharactersUI, log: LogUI):
|
|
|
|
super().__init__(Pile([
|
|
|
|
('weight', 1, LineBox(original_widget=clocks, title="Clocks",
|
|
|
|
title_attr="FrameTitle", title_align='left')),
|
|
|
|
('weight', 7, LineBox(original_widget=characters, title="Character Status",
|
|
|
|
title_attr="FrameTitle", title_align='left')),
|
|
|
|
('weight', 2, LineBox(original_widget=log, title="Log",
|
|
|
|
title_attr="FrameTitle", title_align='left')),
|
|
|
|
]))
|
|
|
|
|
|
|
|
|
|
|
|
class AbilityEntryPopup(WidgetWrap):
|
|
|
|
def __init__(self, user: Character, callback: Callable[[str, list[tuple[Counter]]], None]):
|
|
|
|
self.user = user
|
|
|
|
self.index = index
|
|
|
|
self.complete_callback = callback
|
|
|
|
self.ability_name_editor = Edit(f'{user.name} uses ')
|
|
|
|
self.mp_cost_editor = IntEdit(f'Mind Points cost (of {user.mp}) ', 0)
|
|
|
|
self.ip_cost_editor = IntEdit(f'Inventory Points cost (of {user.ip}) ', 0)
|
|
|
|
self.hp_cost_editor = IntEdit(f'Health Points cost (of {user.hp}) ', 0)
|
|
|
|
editors = [self.ability_name_editor, self.mp_cost_editor, self.ip_cost_editor, self.hp_cost_editor]
|
|
|
|
if user.sp is not None:
|
|
|
|
self.sp_cost_editor = IntEdit(f'{user.character_type.sp_name_abbr} cost (of {user.sp}) ', 0)
|
|
|
|
editors.append(self.sp_cost_editor)
|
|
|
|
else:
|
|
|
|
self.sp_cost_editor = None
|
|
|
|
ok_button = Button('Initiate')
|
|
|
|
cancel_button = Button('Abort')
|
|
|
|
contents = Pile([
|
|
|
|
*editors,
|
|
|
|
Padding(Columns((ok_button, cancel_button)), 'right'),
|
|
|
|
])
|
|
|
|
super().__init__(LineBox(
|
|
|
|
original_widget=contents,
|
|
|
|
title='Use Ability',
|
|
|
|
))
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class FabulaUI(WidgetPlaceholder):
|
|
|
|
def __init__(self, main: MainUI):
|
|
|
|
self.main = main
|
|
|
|
super().__init__(main)
|
|
|
|
|
|
|
|
def open_popup(self, popup: Widget):
|
|
|
|
self.original_widget = Overlay(popup, self.original_widget, 'center', ('relative', 50), 'middle', 'min_height')
|
|
|
|
|
|
|
|
def close_popup(self):
|
|
|
|
if isinstance(self.original_widget, Overlay):
|
|
|
|
self.original_widget = self.original_widget.bottom_w
|