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