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

312 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
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