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/ui.py

321 lines
12 KiB

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