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 class SessionStateUI(WidgetWrap): @classmethod def render_round_timer(cls, rd: int, fp_spent: int, up_spent: int): return ([('RoundLabel', 'Round'), ': ', ('RoundNumber', str(rd)), ' / '] if rd > 0 else []) + [ ('FabulaLabel', 'FP Used'), ': ', ('FabulaNumber', str(fp_spent)), ] + ([' / ', ('UltimaLabel', 'UP Used'), ': ', ('UltimaNumber', str(up_spent))] if up_spent > 0 else []) def __init__(self, rd: int, 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, 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.list = ListBox(self.items) super().__init__(Pile([('pack', self.state), ('weight', 1, self.list)])) def update_session(self, rd: int, 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 == -1: index = len(self.items) self.items.insert(index, ClockUI(clock=clock)) return index def update_clock(self, index: int, clock: Clock): self.items[index].update(clock) def reorder_clock(self, old: int, new: int = -1): focused = self.list.focus_position == old item = self.items.pop(old) new = new if new != -1 else len(self.items) self.items.insert(new, 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() class CharacterUI(WidgetWrap): @classmethod def render_character_text(cls, character: Character): if character.role == CharacterType.Player: role_icon = '♥' role_style = 'CharacterPlayer' elif character.role == CharacterType.Ally: role_icon = '♡' role_style = 'CharacterAlly' elif character.role == CharacterType.Enemy: role_icon = '♤' role_style = 'CharacterEnemy' elif character.role == CharacterType.EnemyPlayer: role_icon = '♠' role_style = 'CharacterEnemyPlayer' else: role_icon = '*' role_style = 'CharacterUnknown' if character.max_turns == 0 or character.hp == 0: turn_style = 'TurnDisabled' turn_text = '[✗]' 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), ], [('HPLabel', hp_suffix + 'HP'), '\u00A0', (hp_style, str(character.hp).rjust(3, '\u00A0')), '/', ('HPMax', str(character.max_hp).rjust(3, '\u00A0')), '\u00A0 ', ('MPLabel', '\u00A0MP'), '\u00A0', ('MPValue', str(character.mp).rjust(3, '\u00A0')), '/', ('MPMax', str(character.max_mp).rjust(3, '\u00A0'))] + ( ['\u00A0 ', ('FPLabel' if character.role in (CharacterType.Player, CharacterType.EnemyPlayer) else 'UPLabel', '\u00A0FP' if character.role in (CharacterType.Player, CharacterType.EnemyPlayer) else '\u00A0UP'), '\u00A0', ('FPValue' if character.role in (CharacterType.Player, CharacterType.EnemyPlayer) else 'UPValue', str(character.sp).rjust(3, '\u00A0')), '\u00A0' * 4] if character.sp is not None else []) + ( ['\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 ', (('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([ ('LogBold', v) if index % 2 != 0 else ('LogText', v) for index, v in enumerate(text.split("**")) if v != ""]))])) 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', 5, Columns([ ('weight', 5, LineBox(original_widget=characters, title="Character Status", title_attr="FrameTitle", title_align='left')), ('weight', 3, LineBox(original_widget=clocks, title="Clocks", title_attr="FrameTitle", title_align='left')), ])), ('weight', 3, LineBox(original_widget=log, title="Combat 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.role.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