diff --git a/src/warchron/constants.py b/src/warchron/constants.py index d4f69e9..7c28c79 100644 --- a/src/warchron/constants.py +++ b/src/warchron/constants.py @@ -4,7 +4,7 @@ from pathlib import Path from typing import Dict from PyQt6.QtCore import Qt -from PyQt6.QtGui import QIcon +from PyQt6.QtGui import QIcon, QPixmap, QPainter # Paths @@ -17,37 +17,44 @@ ROLE_ID = Qt.ItemDataRole.UserRole + 1 class IconName(str, Enum): - UNDO = ("undo",) - REDO = ("redo",) - PAIRING = ("pairing",) - DRAW = ("draw",) - DELETE = ("delete",) - SAVE_AS = ("save_as",) - SAVE = ("save",) - NEW = ("new",) - EXIT = ("exit",) - END = ("end",) - OPEN = ("load",) - ONGOING = ("ongoing",) - EXPORT = ("export",) - EDIT = ("edit",) - ADD = ("add",) - ABOUT = ("about",) - WARS = ("wars",) - DONE = ("done",) - WIN = ("win",) - PLAYERS = ("players",) + UNDO = "undo" + REDO = "redo" + PAIRING = "pairing" + DRAW = "draw" + TIEBREAK = "tie-break" + DELETE = "delete" + SAVE_AS = "save_as" + SAVE = "save" + NEW = "new" + EXIT = "exit" + END = "end" + OPEN = "load" + ONGOING = "ongoing" + EXPORT = "export" + EDIT = "edit" + ADD = "add" + ABOUT = "about" + WARS = "wars" + DONE = "done" + WIN = "win" + PLAYERS = "players" WARCHRON = "warchron" + TOKEN = "token" + TOKENS = "tokens" + TIEBREAK_TOKEN = auto() class Icons: - _cache: Dict[str, QIcon] = {} + _icon_cache: Dict[IconName, QIcon] = {} + + _pixmap_cache: Dict[IconName, QPixmap] = {} _paths = { IconName.UNDO: "arrow-curve-180-left", IconName.REDO: "arrow-curve", IconName.PAIRING: "arrow-switch", IconName.DRAW: "balance.png", + IconName.TIEBREAK: "balance-unbalance.png", IconName.DELETE: "cross.png", IconName.SAVE_AS: "disk--pencil.png", IconName.SAVE: "disk.png", @@ -65,14 +72,43 @@ class Icons: IconName.WIN: "trophy.png", IconName.PLAYERS: "users.png", IconName.WARCHRON: "warchron_logo.png", + IconName.TOKEN: "point.png", + IconName.TOKENS: "points.png", } @classmethod def get(cls, name: IconName) -> QIcon: - if name not in cls._cache: + if name not in cls._icon_cache: path = RESOURCES_DIR / cls._paths[name] - cls._cache[name] = QIcon(str(path)) - return cls._cache[name] + cls._icon_cache[name] = QIcon(str(path)) + return cls._icon_cache[name] + + @classmethod + def get_pixmap(cls, name: IconName) -> QPixmap: + if name in cls._pixmap_cache: + return cls._pixmap_cache[name] + if name == IconName.TIEBREAK_TOKEN: + pix = cls._compose( + cls.get_pixmap(IconName.TIEBREAK), + cls.get_pixmap(IconName.TOKEN), + ) + else: + path = RESOURCES_DIR / cls._paths[name] + pix = QPixmap(path.as_posix()) + cls._pixmap_cache[name] = pix + return pix + + @staticmethod + def _compose(left: QPixmap, right: QPixmap) -> QPixmap: + w = left.width() + right.width() + h = max(left.height(), right.height()) + result = QPixmap(w, h) + result.fill(Qt.GlobalColor.transparent) + painter = QPainter(result) + painter.drawPixmap(0, (h - left.height()) // 2, left) + painter.drawPixmap(left.width(), (h - right.height()) // 2, right) + painter.end() + return result class ItemType(StrEnum): diff --git a/src/warchron/controller/dtos.py b/src/warchron/controller/dtos.py index 454aa61..5222b03 100644 --- a/src/warchron/controller/dtos.py +++ b/src/warchron/controller/dtos.py @@ -105,6 +105,8 @@ class BattleDTO: state_icon: QIcon | None player1_icon: QIcon | None player2_icon: QIcon | None + player1_tooltip: str | None = None + player2_tooltip: str | None = None @dataclass diff --git a/src/warchron/controller/round_controller.py b/src/warchron/controller/round_controller.py index 429d5ec..d0c8601 100644 --- a/src/warchron/controller/round_controller.py +++ b/src/warchron/controller/round_controller.py @@ -2,9 +2,11 @@ from typing import List, Dict, TYPE_CHECKING from PyQt6.QtWidgets import QDialog from PyQt6.QtWidgets import QMessageBox +from PyQt6.QtGui import QIcon -from warchron.constants import ItemType, RefreshScope, Icons, IconName +from warchron.constants import ItemType, RefreshScope, Icons, IconName, ContextType from warchron.model.exception import ForbiddenOperation, DomainError +from warchron.model.tie_manager import TieResolver from warchron.model.round import Round from warchron.model.war import War @@ -31,6 +33,7 @@ class RoundController: def _fill_round_details(self, round_id: str) -> None: rnd = self.app.model.get_round(round_id) camp = self.app.model.get_campaign_by_round(round_id) + war = self.app.model.get_war_by_round(round_id) self.app.view.show_round_details(index=camp.get_round_index(round_id)) participants = self.app.model.get_round_participants(round_id) sectors = camp.get_sectors_in_round(round_id) @@ -97,9 +100,29 @@ class RoundController: winner_name = "" p1_icon = None p2_icon = None + p1_tooltip = None + p2_tooltip = None if battle.is_draw(): p1_icon = Icons.get(IconName.DRAW) p2_icon = Icons.get(IconName.DRAW) + if TieResolver.was_tie_broken_by_tokens( + war, ContextType.BATTLE, battle.sector_id + ): + effective_winner = TieResolver.get_effective_winner_id( + war, ContextType.BATTLE, battle.sector_id, None + ) + p1_war = None + if battle.player_1_id is not None: + p1_war = camp.participants[ + battle.player_1_id + ].war_participant_id + pixmap = Icons.get_pixmap(IconName.TIEBREAK_TOKEN) + if effective_winner == p1_war: + p1_icon = QIcon(pixmap) + p1_tooltip = "Won by tie-break" + else: + p2_icon = QIcon(pixmap) + p2_tooltip = "Won by tie-break" elif battle.winner_id: if battle.winner_id == battle.player_1_id: p1_icon = Icons.get(IconName.WIN) @@ -118,6 +141,8 @@ class RoundController: state_icon=state_icon, player1_icon=p1_icon, player2_icon=p2_icon, + player1_tooltip=p1_tooltip, + player2_tooltip=p2_tooltip, ) ) self.app.view.display_round_battles(battles_for_display) @@ -168,6 +193,7 @@ class RoundController: parent=self.app.view, players=players, counters=counters, + context_type=ContextType.BATTLE, context_id=ctx.context_id, ) if not dialog.exec(): diff --git a/src/warchron/model/tie_manager.py b/src/warchron/model/tie_manager.py index da031f9..54cef74 100644 --- a/src/warchron/model/tie_manager.py +++ b/src/warchron/model/tie_manager.py @@ -106,3 +106,18 @@ class TieResolver: return ev.participant_id # None if confirmed draw return None + + @staticmethod + def was_tie_broken_by_tokens( + war: War, + context_type: ContextType, + context_id: str, + ) -> bool: + for ev in reversed(war.events): + if ( + isinstance(ev, TieResolved) + and ev.context_type == context_type + and ev.context_id == context_id + ): + return ev.participant_id is not None + return False diff --git a/src/warchron/view/resources/balance-unbalance.png b/src/warchron/view/resources/balance-unbalance.png new file mode 100644 index 0000000..9e52d62 Binary files /dev/null and b/src/warchron/view/resources/balance-unbalance.png differ diff --git a/src/warchron/view/resources/point.png b/src/warchron/view/resources/point.png new file mode 100644 index 0000000..1776e06 Binary files /dev/null and b/src/warchron/view/resources/point.png differ diff --git a/src/warchron/view/resources/points.png b/src/warchron/view/resources/points.png new file mode 100644 index 0000000..d517811 Binary files /dev/null and b/src/warchron/view/resources/points.png differ diff --git a/src/warchron/view/tie_dialog.py b/src/warchron/view/tie_dialog.py index 495033c..201ea32 100644 --- a/src/warchron/view/tie_dialog.py +++ b/src/warchron/view/tie_dialog.py @@ -1,8 +1,9 @@ from typing import List, Dict from PyQt6.QtWidgets import QWidget, QDialog +from PyQt6.QtCore import Qt -from warchron.constants import Icons, IconName +from warchron.constants import Icons, IconName, ContextType, RESOURCES_DIR from warchron.controller.dtos import ParticipantOption from warchron.view.ui.ui_tie_dialog import Ui_tieDialog @@ -14,6 +15,7 @@ class TieDialog(QDialog): *, players: List[ParticipantOption], counters: List[int], + context_type: ContextType, context_id: str, ) -> None: super().__init__(parent) @@ -22,7 +24,13 @@ class TieDialog(QDialog): self._p2_id = players[1].id self.ui: Ui_tieDialog = Ui_tieDialog() self.ui.setupUi(self) # type: ignore - self.ui.tieContext.setText("Battle tie") # Change with context + self.ui.tieContext.setText(self._get_context_title(context_type)) + icon_path = (RESOURCES_DIR / Icons._paths[IconName.TOKENS]).as_posix() + html = f' Remaining token(s)' + self.ui.label_2.setText(html) + self.ui.label_2.setTextFormat(Qt.TextFormat.RichText) + self.ui.label_3.setText(html) + self.ui.label_3.setTextFormat(Qt.TextFormat.RichText) self.ui.groupBox_1.setTitle(players[0].name) self.ui.groupBox_2.setTitle(players[1].name) self.ui.tokenCount_1.setText(str(counters[0])) @@ -38,3 +46,13 @@ class TieDialog(QDialog): self._p1_id: self.ui.tokenSpend_1.isChecked(), self._p2_id: self.ui.tokenSpend_2.isChecked(), } + + @staticmethod + def _get_context_title(context_type: ContextType) -> str: + titles = { + ContextType.BATTLE: "Battle tie", + ContextType.CAMPAIGN: "Campaign tie", + ContextType.WAR: "War tie", + ContextType.CHOICE: "Choice tie", + } + return titles.get(context_type, "Tie") diff --git a/src/warchron/view/view.py b/src/warchron/view/view.py index 15e1e29..5082df5 100644 --- a/src/warchron/view/view.py +++ b/src/warchron/view/view.py @@ -4,7 +4,7 @@ from pathlib import Path import calendar from PyQt6 import QtWidgets -from PyQt6.QtCore import Qt, QPoint +from PyQt6.QtCore import Qt, QPoint, QSize from PyQt6.QtWidgets import QWidget, QFileDialog, QTreeWidgetItem, QMenu from PyQt6.QtGui import QCloseEvent @@ -540,6 +540,7 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): table = self.battlesTable table.clearContents() table.setRowCount(len(sectors)) + self.battlesTable.setIconSize(QSize(32, 16)) for row, battle in enumerate(sectors): sector_item = QtWidgets.QTableWidgetItem(battle.sector_name) if battle.state_icon: