diff --git a/src/warchron/controller/controller.py b/src/warchron/controller/controller.py index 8d92385..2cbd73c 100644 --- a/src/warchron/controller/controller.py +++ b/src/warchron/controller/controller.py @@ -4,6 +4,11 @@ from pathlib import Path from PyQt6.QtWidgets import QMessageBox, QDialog from warchron.model.model import Model +from warchron.model.exception import ( + DeletionForbidden, + DeletionRequiresConfirmation, + UpdateRequiresConfirmation, +) from warchron.view.view import View from warchron.constants import ItemType, RefreshScope from warchron.controller.dtos import ( @@ -19,17 +24,15 @@ from warchron.controller.dtos import ( ChoiceDTO, BattleDTO, ) -from warchron.view.view import ( - PlayerDialog, - WarDialog, - CampaignDialog, - ObjectiveDialog, - WarParticipantDialog, - CampaignParticipantDialog, - SectorDialog, - ChoicesDialog, - BattlesDialog, -) +from warchron.view.player_dialog import PlayerDialog +from warchron.view.war_dialog import WarDialog +from warchron.view.campaign_dialog import CampaignDialog +from warchron.view.objective_dialog import ObjectiveDialog +from warchron.view.war_participant_dialog import WarParticipantDialog +from warchron.view.campaign_participant_dialog import CampaignParticipantDialog +from warchron.view.sector_dialog import SectorDialog +from warchron.view.choices_dialog import ChoicesDialog +from warchron.view.battles_dialog import BattlesDialog class Controller: @@ -364,134 +367,49 @@ class Controller: self.view.select_tree_item(item_type=item_type, item_id=item_id) def edit_item(self, item_type: str, item_id: str) -> None: - if item_type == ItemType.PLAYER: - play = self.model.get_player(item_id) - player_dialog = PlayerDialog(self.view, default_name=play.name) - if player_dialog.exec() == QDialog.DialogCode.Accepted: - name = player_dialog.get_player_name() - if not self._validate_player_inputs(name): - return - self.model.update_player(item_id, name=name) + try: + if item_type == ItemType.PLAYER: + self.edit_player(item_id) self.refresh(RefreshScope.PLAYERS_LIST) - elif item_type == ItemType.WAR: - war = self.model.get_war(item_id) - war_dialog = WarDialog( - self.view, default_name=war.name, default_year=war.year - ) - if war_dialog.exec() == QDialog.DialogCode.Accepted: - name = war_dialog.get_war_name() - year = war_dialog.get_war_year() - if not self._validate_war_inputs(name, year): - return - self.model.update_war(item_id, name=name, year=year) + elif item_type == ItemType.WAR: + self.edit_war(item_id) self.refresh_and_select( - RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war.id + RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=item_id ) - elif item_type == ItemType.CAMPAIGN: - camp = self.model.get_campaign(item_id) - camp_dialog = CampaignDialog( - self.view, default_name=camp.name, default_month=camp.month - ) - if camp_dialog.exec() == QDialog.DialogCode.Accepted: - name = camp_dialog.get_campaign_name() - month = camp_dialog.get_campaign_month() - if not self._validate_campaign_inputs(name, month): - return - self.model.update_campaign(item_id, name=name, month=month) + elif item_type == ItemType.CAMPAIGN: + self.edit_campaign(item_id) self.refresh_and_select( - RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp.id + RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=item_id ) - elif item_type == ItemType.OBJECTIVE: - obj = self.model.get_objective(item_id) - obj_dialog = ObjectiveDialog( - self.view, default_name=obj.name, default_description=obj.description - ) - if obj_dialog.exec() == QDialog.DialogCode.Accepted: - name = obj_dialog.get_objective_name() - description = obj_dialog.get_objective_description() - if not self._validate_objective_inputs(name, description): - return - self.model.update_objective(item_id, name=name, description=description) + elif item_type == ItemType.OBJECTIVE: + self.edit_objective(item_id) self.refresh(RefreshScope.WAR_DETAILS) - elif item_type == ItemType.WAR_PARTICIPANT: - war_part = self.model.get_war_participant(item_id) - player = self.model.get_player(war_part.player_id) - play_opt = ParticipantOption(id=player.id, name=player.name) - war_part_dialog = WarParticipantDialog( - self.view, - players=[play_opt], - default_player_id=war_part.id, - default_faction=war_part.faction, - editable_player=False, - ) - if war_part_dialog.exec() == QDialog.DialogCode.Accepted: - faction = war_part_dialog.get_participant_faction() - self.model.update_war_participant(item_id, faction=faction) + elif item_type == ItemType.WAR_PARTICIPANT: + self.edit_war_participant(item_id) self.refresh(RefreshScope.WAR_DETAILS) - elif item_type == ItemType.SECTOR: - sect = self.model.get_sector(item_id) - camp = self.model.get_campaign_by_sector(item_id) - war = self.model.get_war_by_campaign(camp.id) - rounds = camp.get_all_rounds() - rnd_dto: List[RoundDTO] = [ - RoundDTO(id=rnd.id, index=i) for i, rnd in enumerate(rounds, start=1) - ] - objectives = war.get_all_objectives() - obj_dto: List[ObjectiveDTO] = [ - ObjectiveDTO(id=obj.id, name=obj.name, description=obj.description) - for obj in objectives - ] - sect_dialog = SectorDialog( - self.view, - default_name=sect.name, - rounds=rnd_dto, - default_round_id=sect.round_id, - objectives=obj_dto, - default_major_id=sect.major_objective_id, - default_minor_id=sect.minor_objective_id, - default_influence_id=sect.influence_objective_id, - ) - if sect_dialog.exec() == QDialog.DialogCode.Accepted: - name = sect_dialog.get_sector_name() - round_id = sect_dialog.get_round_id() - major_id = sect_dialog.get_major_id() - minor_id = sect_dialog.get_minor_id() - influence_id = sect_dialog.get_influence_id() - self.model.update_sector( - item_id, - name=name, - round_id=round_id, - major_id=major_id, - minor_id=minor_id, - influence_id=influence_id, - ) + elif item_type == ItemType.SECTOR: + self.edit_sector(item_id) self.refresh(RefreshScope.CAMPAIGN_DETAILS) - elif item_type == ItemType.CAMPAIGN_PARTICIPANT: - camp_part = self.model.get_campaign_participant(item_id) - war_part = self.model.get_war_participant(camp_part.war_participant_id) - player = self.model.get_player(war_part.player_id) - part_opt = [ParticipantOption(id=player.id, name=player.name)] - camp_part_dialog = CampaignParticipantDialog( - self.view, - participants=part_opt, - default_participant_id=camp_part.id, - default_leader=camp_part.leader, - default_theme=camp_part.theme, - editable_player=False, - ) - if camp_part_dialog.exec() == QDialog.DialogCode.Accepted: - leader = camp_part_dialog.get_participant_leader() - theme = camp_part_dialog.get_participant_theme() - self.model.update_campaign_participant( - item_id, leader=leader, theme=theme - ) + elif item_type == ItemType.CAMPAIGN_PARTICIPANT: + self.edit_campaign_participant(item_id) + self.refresh(RefreshScope.CAMPAIGN_DETAILS) + elif item_type == ItemType.CHOICE: + self.edit_round_choice(item_id) + self.refresh(RefreshScope.ROUND_DETAILS) + elif item_type == ItemType.BATTLE: + self.edit_round_battle(item_id) + self.refresh(RefreshScope.ROUND_DETAILS) + self.is_dirty = True + except UpdateRequiresConfirmation as e: + reply = QMessageBox.question( + self.view, + "Confirm update", + e.message, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + ) + if reply == QMessageBox.StandardButton.Yes: + e.apply_update() self.refresh(RefreshScope.CAMPAIGN_DETAILS) - elif item_type == ItemType.CHOICE: - self.edit_round_choice(item_id) - self.refresh(RefreshScope.ROUND_DETAILS) - elif item_type == ItemType.BATTLE: - self.edit_round_battle(item_id) - self.refresh(RefreshScope.ROUND_DETAILS) def delete_item(self, item_type: str, item_id: str) -> None: reply = QMessageBox.question( @@ -502,39 +420,56 @@ class Controller: ) if reply != QMessageBox.StandardButton.Yes: return - if item_type == ItemType.PLAYER: - self.model.remove_player(item_id) - self.refresh(RefreshScope.PLAYERS_LIST) - elif item_type == ItemType.WAR: - self.model.remove_war(item_id) - self.refresh(RefreshScope.WARS_TREE) - elif item_type == ItemType.CAMPAIGN: - war = self.model.get_war_by_campaign(item_id) - war_id = war.id - self.model.remove_campaign(item_id) - self.refresh_and_select( - RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id + try: + if item_type == ItemType.PLAYER: + self.model.remove_player(item_id) + self.refresh(RefreshScope.PLAYERS_LIST) + elif item_type == ItemType.WAR: + self.model.remove_war(item_id) + self.refresh(RefreshScope.WARS_TREE) + elif item_type == ItemType.CAMPAIGN: + war = self.model.get_war_by_campaign(item_id) + war_id = war.id + self.model.remove_campaign(item_id) + self.refresh_and_select( + RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id + ) + elif item_type == ItemType.OBJECTIVE: + self.model.remove_objective(item_id) + self.refresh(RefreshScope.WAR_DETAILS) + elif item_type == ItemType.WAR_PARTICIPANT: + self.model.remove_war_participant(item_id) + self.refresh(RefreshScope.WAR_DETAILS) + elif item_type == ItemType.SECTOR: + self.model.remove_sector(item_id) + self.refresh(RefreshScope.CAMPAIGN_DETAILS) + elif item_type == ItemType.CAMPAIGN_PARTICIPANT: + self.model.remove_campaign_participant(item_id) + self.refresh(RefreshScope.CAMPAIGN_DETAILS) + elif item_type == ItemType.ROUND: + camp = self.model.get_campaign_by_round(item_id) + camp_id = camp.id + self.model.remove_round(item_id) + self.refresh_and_select( + RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp_id + ) + self.is_dirty = True + except DeletionForbidden as e: + QMessageBox.warning( + self.view, + "Deletion forbidden", + e.reason, ) - elif item_type == ItemType.OBJECTIVE: - self.model.remove_objective(item_id) - self.refresh(RefreshScope.WAR_DETAILS) - elif item_type == ItemType.WAR_PARTICIPANT: - self.model.remove_war_participant(item_id) - self.refresh(RefreshScope.WAR_DETAILS) - elif item_type == ItemType.SECTOR: - self.model.remove_sector(item_id) - self.refresh(RefreshScope.CAMPAIGN_DETAILS) - elif item_type == ItemType.CAMPAIGN_PARTICIPANT: - self.model.remove_campaign_participant(item_id) - self.refresh(RefreshScope.CAMPAIGN_DETAILS) - elif item_type == ItemType.ROUND: - camp = self.model.get_campaign_by_round(item_id) - camp_id = camp.id - self.model.remove_round(item_id) - self.refresh_and_select( - RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp_id + except DeletionRequiresConfirmation as e: + reply = QMessageBox.question( + self.view, + "Confirm deletion", + e.message, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) - self.is_dirty = True + if reply == QMessageBox.StandardButton.Yes: + e.cleanup_action() + self.refresh(RefreshScope.CAMPAIGN_DETAILS) # Player methods @@ -557,6 +492,15 @@ class Controller: self.is_dirty = True self.refresh(RefreshScope.PLAYERS_LIST) + def edit_player(self, player_id: str) -> None: + play = self.model.get_player(player_id) + player_dialog = PlayerDialog(self.view, default_name=play.name) + if player_dialog.exec() == QDialog.DialogCode.Accepted: + name = player_dialog.get_player_name() + if not self._validate_player_inputs(name): + return + self.model.update_player(player_id, name=name) + # War methods def _validate_war_inputs(self, name: str, year: int) -> bool: @@ -586,6 +530,16 @@ class Controller: RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war.id ) + def edit_war(self, war_id: str) -> None: + war = self.model.get_war(war_id) + war_dialog = WarDialog(self.view, default_name=war.name, default_year=war.year) + if war_dialog.exec() == QDialog.DialogCode.Accepted: + name = war_dialog.get_war_name() + year = war_dialog.get_war_year() + if not self._validate_war_inputs(name, year): + return + self.model.update_war(war_id, name=name, year=year) + # Objective methods def _validate_objective_inputs(self, name: str, description: str) -> bool: @@ -610,6 +564,20 @@ class Controller: self.is_dirty = True self.refresh(RefreshScope.WAR_DETAILS) + def edit_objective(self, objective_id: str) -> None: + obj = self.model.get_objective(objective_id) + obj_dialog = ObjectiveDialog( + self.view, default_name=obj.name, default_description=obj.description + ) + if obj_dialog.exec() == QDialog.DialogCode.Accepted: + name = obj_dialog.get_objective_name() + description = obj_dialog.get_objective_description() + if not self._validate_objective_inputs(name, description): + return + self.model.update_objective( + objective_id, name=name, description=description + ) + # War participant methods def add_war_participant(self) -> None: @@ -630,6 +598,21 @@ class Controller: self.is_dirty = True self.refresh(RefreshScope.WAR_DETAILS) + def edit_war_participant(self, participant_id: str) -> None: + war_part = self.model.get_war_participant(participant_id) + player = self.model.get_player(war_part.player_id) + play_opt = ParticipantOption(id=player.id, name=player.name) + war_part_dialog = WarParticipantDialog( + self.view, + players=[play_opt], + default_player_id=war_part.id, + default_faction=war_part.faction, + editable_player=False, + ) + if war_part_dialog.exec() == QDialog.DialogCode.Accepted: + faction = war_part_dialog.get_participant_faction() + self.model.update_war_participant(participant_id, faction=faction) + # Campaign methods def _validate_campaign_inputs(self, name: str, month: int) -> bool: @@ -666,6 +649,18 @@ class Controller: RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=camp.id ) + def edit_campaign(self, campaign_id: str) -> None: + camp = self.model.get_campaign(campaign_id) + camp_dialog = CampaignDialog( + self.view, default_name=camp.name, default_month=camp.month + ) + if camp_dialog.exec() == QDialog.DialogCode.Accepted: + name = camp_dialog.get_campaign_name() + month = camp_dialog.get_campaign_month() + if not self._validate_campaign_inputs(name, month): + return + self.model.update_campaign(campaign_id, name=name, month=month) + # Campaign participant methods def add_campaign_participant(self) -> None: @@ -692,6 +687,26 @@ class Controller: self.is_dirty = True self.refresh(RefreshScope.CAMPAIGN_DETAILS) + def edit_campaign_participant(self, participant_id: str) -> None: + camp_part = self.model.get_campaign_participant(participant_id) + war_part = self.model.get_war_participant(camp_part.war_participant_id) + player = self.model.get_player(war_part.player_id) + part_opt = [ParticipantOption(id=player.id, name=player.name)] + camp_part_dialog = CampaignParticipantDialog( + self.view, + participants=part_opt, + default_participant_id=camp_part.id, + default_leader=camp_part.leader, + default_theme=camp_part.theme, + editable_player=False, + ) + if camp_part_dialog.exec() == QDialog.DialogCode.Accepted: + leader = camp_part_dialog.get_participant_leader() + theme = camp_part_dialog.get_participant_theme() + self.model.update_campaign_participant( + participant_id, leader=leader, theme=theme + ) + # Sector methods def _validate_sector_inputs( @@ -740,6 +755,44 @@ class Controller: self.is_dirty = True self.refresh(RefreshScope.CAMPAIGN_DETAILS) + def edit_sector(self, sector_id: str) -> None: + sect = self.model.get_sector(sector_id) + camp = self.model.get_campaign_by_sector(sector_id) + war = self.model.get_war_by_campaign(camp.id) + rounds = camp.get_all_rounds() + rnd_dto: List[RoundDTO] = [ + RoundDTO(id=rnd.id, index=i) for i, rnd in enumerate(rounds, start=1) + ] + objectives = war.get_all_objectives() + obj_dto: List[ObjectiveDTO] = [ + ObjectiveDTO(id=obj.id, name=obj.name, description=obj.description) + for obj in objectives + ] + sect_dialog = SectorDialog( + self.view, + default_name=sect.name, + rounds=rnd_dto, + default_round_id=sect.round_id, + objectives=obj_dto, + default_major_id=sect.major_objective_id, + default_minor_id=sect.minor_objective_id, + default_influence_id=sect.influence_objective_id, + ) + if sect_dialog.exec() == QDialog.DialogCode.Accepted: + name = sect_dialog.get_sector_name() + round_id = sect_dialog.get_round_id() + major_id = sect_dialog.get_major_id() + minor_id = sect_dialog.get_minor_id() + influence_id = sect_dialog.get_influence_id() + self.model.update_sector( + sector_id, + name=name, + round_id=round_id, + major_id=major_id, + minor_id=minor_id, + influence_id=influence_id, + ) + # Round methods def add_round(self) -> None: diff --git a/src/warchron/model/battle.py b/src/warchron/model/battle.py new file mode 100644 index 0000000..04a24c7 --- /dev/null +++ b/src/warchron/model/battle.py @@ -0,0 +1,35 @@ +class Battle: + def __init__( + self, + sector_id: str, + player_1_id: str | None = None, + player_2_id: str | None = None, + ): + self.sector_id: str = sector_id # ref to Campaign.sector + self.player_1_id: str | None = player_1_id # ref to Campaign.participants + self.player_2_id: str | None = player_2_id # ref to Campaign.participants + self.winner_id: str | None = None + self.score: str | None = None + self.victory_condition: str | None = None + self.comment: str | None = None + + def set_id(self, new_id: str) -> None: + self.sector_id = new_id + + def set_player_1(self, new_player_id: str | None) -> None: + self.player_1_id = new_player_id + + def set_player_2(self, new_player_id: str | None) -> None: + self.player_2_id = new_player_id + + def set_winner(self, new_player_id: str | None) -> None: + self.winner_id = new_player_id + + def set_score(self, new_score: str | None) -> None: + self.score = new_score + + def set_victory_condition(self, new_victory_condition: str | None) -> None: + self.victory_condition = new_victory_condition + + def set_comment(self, new_comment: str | None) -> None: + self.comment = new_comment diff --git a/src/warchron/model/campaign.py b/src/warchron/model/campaign.py index bf3431e..7adc7f2 100644 --- a/src/warchron/model/campaign.py +++ b/src/warchron/model/campaign.py @@ -2,7 +2,15 @@ from __future__ import annotations from uuid import uuid4 from typing import Any, Dict, List -from warchron.model.round import Round, Choice, Battle +from warchron.model.exception import ( + DeletionRequiresConfirmation, + UpdateRequiresConfirmation, +) +from warchron.model.campaign_participant import CampaignParticipant +from warchron.model.sector import Sector +from warchron.model.round import Round +from warchron.model.choice import Choice +from warchron.model.battle import Battle class Campaign: @@ -90,12 +98,47 @@ class Campaign: part.set_theme(theme) def remove_campaign_participant(self, participant_id: str) -> None: - # TODO manage choices referring to it - # TODO manage battles referring to it - del self.participants[participant_id] + rounds_blocking: list[Round] = [] + for rnd in self.rounds: + if rnd.has_choice_with_participant( + participant_id + ) or rnd.has_battle_with_participant(participant_id): + rounds_blocking.append(rnd) + if not rounds_blocking: + del self.participants[participant_id] + return + + def cleanup() -> None: + for rnd in rounds_blocking: + rnd.clear_participant_references(participant_id) + rnd.remove_choice(participant_id) + del self.participants[participant_id] + + rounds_str = ", ".join( + str(self.get_round_index(rnd.id)) for rnd in rounds_blocking + ) + raise DeletionRequiresConfirmation( + message=( + f"This participant is used in round(s): {rounds_str}.\n" + "Related choices and battles will be cleared.\n" + "Do you want to continue?" + ), + cleanup_action=cleanup, + ) # Sector methods + def has_sector(self, sector_id: str) -> bool: + return sector_id in self.sectors + + def has_sector_with_objective(self, objective_id: str) -> bool: + return any( + sect.major_objective_id == objective_id + or sect.minor_objective_id == objective_id + or sect.influence_objective_id == objective_id + for sect in self.sectors.values() + ) + def add_sector( self, name: str, round_id: str, major_id: str, minor_id: str, influence_id: str ) -> Sector: @@ -126,16 +169,75 @@ class Campaign: influence_id: str, ) -> None: sect = self.get_sector(sector_id) - sect.set_name(name) - sect.set_round(round_id) - sect.set_major(major_id) - sect.set_minor(minor_id) - sect.set_influence(influence_id) + old_round_id = sect.round_id + + def apply_update() -> None: + sect.set_name(name) + sect.set_round(round_id) + sect.set_major(major_id) + sect.set_minor(minor_id) + sect.set_influence(influence_id) + + if old_round_id == round_id: + apply_update() + return + affected_rounds: list[Round] = [] + for rnd in self.rounds: + if rnd.id == old_round_id and ( + rnd.has_choice_with_sector(sector_id) + or rnd.has_battle_with_sector(sector_id) + ): + affected_rounds.append(rnd) + if not affected_rounds: + apply_update() + return + + def cleanup_and_update() -> None: + for rnd in affected_rounds: + rnd.clear_sector_references(sector_id) + rnd.remove_battle(sector_id) + apply_update() + + rounds_str = ", ".join( + str(self.get_round_index(rnd.id)) for rnd in affected_rounds + ) + raise UpdateRequiresConfirmation( + message=( + f"Changing the round of this sector will affect round(s): {rounds_str}." + "\nRelated battles and choices will be cleared.\n" + "Do you want to continue?" + ), + apply_update=cleanup_and_update, + ) def remove_sector(self, sector_id: str) -> None: - # TODO manage choices referring to it - # TODO manage battles referring to it - del self.sectors[sector_id] + rounds_blocking: list[Round] = [] + for rnd in self.rounds: + if rnd.has_battle_with_sector(sector_id) or rnd.has_choice_with_sector( + sector_id + ): + rounds_blocking.append(rnd) + if not rounds_blocking: + del self.sectors[sector_id] + return + + def cleanup() -> None: + for rnd in rounds_blocking: + rnd.clear_sector_references(sector_id) + rnd.remove_battle(sector_id) + del self.sectors[sector_id] + + rounds_str = ", ".join( + str(self.get_round_index(rnd.id)) for rnd in rounds_blocking + ) + raise DeletionRequiresConfirmation( + message=( + f"This sector is used in round(s): {rounds_str}.\n" + "Battles and choices using this sector will be cleared.\n" + "Do you want to continue?" + ), + cleanup_action=cleanup, + ) def get_sectors_in_round(self, round_id: str) -> List[Sector]: sectors = [s for s in self.sectors.values() if s.round_id == round_id] @@ -227,62 +329,3 @@ class Campaign: def remove_battle(self, round_id: str, sector_id: str) -> None: rnd = self.get_round(round_id) rnd.remove_battle(sector_id) - - -class CampaignParticipant: - def __init__( - self, *, war_participant_id: str, leader: str | None, theme: str | None - ): - self.id: str = str(uuid4()) - self.war_participant_id: str = war_participant_id # ref to War.participants - self.leader: str | None = leader - self.theme: str | None = theme - - def set_id(self, new_id: str) -> None: - self.id = new_id - - def set_war_participant(self, new_participant: str) -> None: - self.war_participant_id = new_participant - - def set_leader(self, new_faction: str) -> None: - self.leader = new_faction - - def set_theme(self, new_theme: str) -> None: - self.theme = new_theme - - -class Sector: - def __init__( - self, - name: str, - round_id: str, - major_id: str | None, - minor_id: str | None, - influence_id: str | None, - ): - self.id: str = str(uuid4()) - self.name: str = name - self.round_id: str = round_id - self.major_objective_id: str | None = major_id # ref to War.objectives - self.minor_objective_id: str | None = minor_id # ref to War.objectives - self.influence_objective_id: str | None = influence_id # ref to War.objectives - self.mission: str | None = None - self.description: str | None = None - - def set_id(self, new_id: str) -> None: - self.id = new_id - - def set_name(self, new_name: str) -> None: - self.name = new_name - - def set_round(self, new_round_id: str) -> None: - self.round_id = new_round_id - - def set_major(self, new_major_id: str) -> None: - self.major_objective_id = new_major_id - - def set_minor(self, new_minor_id: str) -> None: - self.minor_objective_id = new_minor_id - - def set_influence(self, new_influence_id: str) -> None: - self.influence_objective_id = new_influence_id diff --git a/src/warchron/model/campaign_participant.py b/src/warchron/model/campaign_participant.py new file mode 100644 index 0000000..aa98528 --- /dev/null +++ b/src/warchron/model/campaign_participant.py @@ -0,0 +1,24 @@ +from __future__ import annotations +from uuid import uuid4 + + +class CampaignParticipant: + def __init__( + self, *, war_participant_id: str, leader: str | None, theme: str | None + ): + self.id: str = str(uuid4()) + self.war_participant_id: str = war_participant_id # ref to War.participants + self.leader: str | None = leader + self.theme: str | None = theme + + def set_id(self, new_id: str) -> None: + self.id = new_id + + def set_war_participant(self, new_participant: str) -> None: + self.war_participant_id = new_participant + + def set_leader(self, new_faction: str) -> None: + self.leader = new_faction + + def set_theme(self, new_theme: str) -> None: + self.theme = new_theme diff --git a/src/warchron/model/choice.py b/src/warchron/model/choice.py new file mode 100644 index 0000000..d49ec66 --- /dev/null +++ b/src/warchron/model/choice.py @@ -0,0 +1,27 @@ +class Choice: + def __init__( + self, + participant_id: str, + priority_sector_id: str | None = None, + secondary_sector_id: str | None = None, + ): + self.participant_id: str = participant_id # ref to Campaign.participants + self.priority_sector_id: str | None = ( + priority_sector_id # ref to Campaign.sectors + ) + self.secondary_sector_id: str | None = ( + secondary_sector_id # ref to Campaign.sectors + ) + self.comment: str | None = None + + def set_id(self, new_id: str) -> None: + self.participant_id = new_id + + def set_priority(self, new_priority_id: str | None) -> None: + self.priority_sector_id = new_priority_id + + def set_secondary(self, new_secondary_id: str | None) -> None: + self.secondary_sector_id = new_secondary_id + + def set_comment(self, new_comment: str | None) -> None: + self.comment = new_comment diff --git a/src/warchron/model/exception.py b/src/warchron/model/exception.py new file mode 100644 index 0000000..be14e13 --- /dev/null +++ b/src/warchron/model/exception.py @@ -0,0 +1,31 @@ +from typing import Callable + + +class DeletionForbidden(Exception): + def __init__(self, reason: str): + self.reason = reason + super().__init__(reason) + + +class DeletionRequiresConfirmation(Exception): + def __init__( + self, + message: str, + *, + cleanup_action: Callable[[], None], + ): + self.message = message + self.cleanup_action = cleanup_action + super().__init__(message) + + +class UpdateRequiresConfirmation(Exception): + def __init__( + self, + message: str, + *, + apply_update: Callable[[], None], + ): + self.message = message + self.apply_update = apply_update + super().__init__(message) diff --git a/src/warchron/model/model.py b/src/warchron/model/model.py index 1a17563..1f448b3 100644 --- a/src/warchron/model/model.py +++ b/src/warchron/model/model.py @@ -4,10 +4,17 @@ import json import shutil from datetime import datetime +from warchron.model.exception import DeletionForbidden from warchron.model.player import Player -from warchron.model.war import War, Objective, WarParticipant -from warchron.model.campaign import Campaign, Sector, CampaignParticipant -from warchron.model.round import Round, Choice, Battle +from warchron.model.war import War +from warchron.model.war_participant import WarParticipant +from warchron.model.objective import Objective +from warchron.model.campaign import Campaign +from warchron.model.campaign_participant import CampaignParticipant +from warchron.model.sector import Sector +from warchron.model.round import Round +from warchron.model.choice import Choice +from warchron.model.battle import Battle class Model: @@ -78,7 +85,16 @@ class Model: return list(self.players.values()) def remove_player(self, player_id: str) -> None: - # TODO manage war_participants referring to it + wars_using_player: List[str] = [] + for war in self.wars.values(): + if war.has_player(player_id): + wars_using_player.append(war.name) + if wars_using_player: + wars_str = ", ".join(wars_using_player) + raise DeletionForbidden( + f"This player is participating in war(s): {wars_str}.\n" + "Remove it from participants first." + ) del self.players[player_id] # War methods @@ -94,6 +110,7 @@ class Model: def get_war(self, id: str) -> War: return self.wars[id] + # TODO replace multiloops by internal has_* method def get_war_by_campaign(self, campaign_id: str) -> War: for war in self.wars.values(): for camp in war.campaigns: @@ -101,6 +118,7 @@ class Model: return war raise KeyError(f"Campaign {campaign_id} not found in any War") + # TODO replace multiloops by internal has_* method def get_war_by_sector(self, sector_id: str) -> War: for war in self.wars.values(): for camp in war.campaigns: @@ -109,6 +127,7 @@ class Model: return war raise KeyError(f"Sector {sector_id} not found in any War") + # TODO replace multiloops by internal has_* method def get_war_by_round(self, round_id: str) -> War: for war in self.wars.values(): for camp in war.campaigns: @@ -117,6 +136,7 @@ class Model: return war raise KeyError(f"Round {round_id} not found in any War") + # TODO replace multiloops by internal has_* method def get_war_by_objective(self, objective_id: str) -> War: for war in self.wars.values(): for obj in war.objectives.values(): @@ -154,6 +174,7 @@ class Model: war = self.get_war(war_id) return war.add_objective(name, description) + # TODO replace multiloops by internal has_* method def get_objective(self, objective_id: str) -> Objective: for war in self.wars.values(): for obj in war.objectives.values(): @@ -185,6 +206,7 @@ class Model: war = self.get_war(war_id) return war.add_war_participant(player_id, faction) + # TODO replace multiloops by internal has_* method def get_war_participant(self, participant_id: str) -> WarParticipant: for war in self.wars.values(): for part in war.participants.values(): @@ -213,6 +235,7 @@ class Model: war = self.get_war(war_id) return war.add_campaign(name, month) + # TODO replace multiloops by internal has_* method def get_campaign(self, campaign_id: str) -> Campaign: for war in self.wars.values(): for campaign in war.campaigns: @@ -263,6 +286,7 @@ class Model: camp = self.get_campaign(campaign_id) return camp.add_sector(name, round_id, major_id, minor_id, influence_id) + # TODO replace multiloops by internal has_* method def get_sector(self, sector_id: str) -> Sector: for war in self.wars.values(): for camp in war.campaigns: @@ -312,6 +336,7 @@ class Model: war_part = war.get_war_participant(participant_id) return self.players[war_part.player_id].name + # TODO replace multiloops by internal has_* method def get_campaign_participant(self, participant_id: str) -> CampaignParticipant: for war in self.wars.values(): for camp in war.campaigns: @@ -343,9 +368,10 @@ class Model: # Round methods def add_round(self, campaign_id: str) -> Round: - camp = self.get_campaign(campaign_id) - return camp.add_round() + war = self.get_war_by_campaign(campaign_id) + return war.add_round(campaign_id) + # TODO replace multiloops by internal has_* method def get_round(self, round_id: str) -> Round: for war in self.wars.values(): for camp in war.campaigns: diff --git a/src/warchron/model/objective.py b/src/warchron/model/objective.py index e69de29..d3e5d76 100644 --- a/src/warchron/model/objective.py +++ b/src/warchron/model/objective.py @@ -0,0 +1,18 @@ +from __future__ import annotations +from uuid import uuid4 + + +class Objective: + def __init__(self, name: str, description: str): + self.id: str = str(uuid4()) + self.name: str = name + self.description: str = description + + def set_id(self, new_id: str) -> None: + self.id = new_id + + def set_name(self, new_name: str) -> None: + self.name = new_name + + def set_description(self, new_description: str) -> None: + self.description = new_description diff --git a/src/warchron/model/round.py b/src/warchron/model/round.py index 420eea8..2460d4c 100644 --- a/src/warchron/model/round.py +++ b/src/warchron/model/round.py @@ -2,6 +2,9 @@ from __future__ import annotations from uuid import uuid4 from typing import Any, Dict +from warchron.model.choice import Choice +from warchron.model.battle import Battle + class Round: def __init__(self) -> None: @@ -40,6 +43,18 @@ class Round: def get_choice(self, participant_id: str) -> Choice | None: return self.choices.get(participant_id) + def has_choice_with_sector(self, sector_id: str) -> bool: + return any( + choice.priority_sector_id == sector_id + or choice.secondary_sector_id == sector_id + for choice in self.choices.values() + ) + + def has_choice_with_participant(self, participant_id: str) -> bool: + return any( + choice.participant_id == participant_id for choice in self.choices.values() + ) + def create_choice(self, participant_id: str) -> Choice: if participant_id not in self.choices: choice = Choice( @@ -63,6 +78,13 @@ class Round: choice.set_secondary(secondary_sector_id) choice.set_comment(comment) + def clear_sector_references(self, sector_id: str) -> None: + for choice in self.choices.values(): + if choice.priority_sector_id == sector_id: + choice.priority_sector_id = None + if choice.secondary_sector_id == sector_id: + choice.secondary_sector_id = None + def remove_choice(self, participant_id: str) -> None: del self.choices[participant_id] @@ -71,6 +93,15 @@ class Round: def get_battle(self, sector_id: str) -> Battle | None: return self.battles.get(sector_id) + def has_battle_with_sector(self, sector_id: str) -> bool: + return any(bat.sector_id == sector_id for bat in self.battles.values()) + + def has_battle_with_participant(self, participant_id: str) -> bool: + return any( + bat.player_1_id == participant_id or bat.player_2_id == participant_id + for bat in self.battles.values() + ) + def create_battle(self, sector_id: str) -> Battle: if sector_id not in self.battles: battle = Battle(sector_id=sector_id, player_1_id=None, player_2_id=None) @@ -96,71 +127,14 @@ class Round: bat.set_victory_condition(victory_condition) bat.set_comment(comment) + def clear_participant_references(self, participant_id: str) -> None: + for battle in self.battles.values(): + if battle.player_1_id == participant_id: + battle.player_1_id = None + if battle.player_2_id == participant_id: + battle.player_2_id = None + if battle.winner_id == participant_id: + battle.winner_id = None + def remove_battle(self, sector_id: str) -> None: del self.battles[sector_id] - - -class Choice: - def __init__( - self, - participant_id: str, - priority_sector_id: str | None = None, - secondary_sector_id: str | None = None, - ): - self.participant_id: str = participant_id # ref to Campaign.participants - self.priority_sector_id: str | None = ( - priority_sector_id # ref to Campaign.sectors - ) - self.secondary_sector_id: str | None = ( - secondary_sector_id # ref to Campaign.sectors - ) - self.comment: str | None = None - - def set_id(self, new_id: str) -> None: - self.participant_id = new_id - - def set_priority(self, new_priority_id: str | None) -> None: - self.priority_sector_id = new_priority_id - - def set_secondary(self, new_secondary_id: str | None) -> None: - self.secondary_sector_id = new_secondary_id - - def set_comment(self, new_comment: str | None) -> None: - self.comment = new_comment - - -class Battle: - def __init__( - self, - sector_id: str, - player_1_id: str | None = None, - player_2_id: str | None = None, - ): - self.sector_id: str = sector_id # ref to Campaign.sector - self.player_1_id: str | None = player_1_id # ref to Campaign.participants - self.player_2_id: str | None = player_2_id # ref to Campaign.participants - self.winner_id: str | None = None - self.score: str | None = None - self.victory_condition: str | None = None - self.comment: str | None = None - - def set_id(self, new_id: str) -> None: - self.sector_id = new_id - - def set_player_1(self, new_player_id: str | None) -> None: - self.player_1_id = new_player_id - - def set_player_2(self, new_player_id: str | None) -> None: - self.player_2_id = new_player_id - - def set_winner(self, new_player_id: str | None) -> None: - self.winner_id = new_player_id - - def set_score(self, new_score: str | None) -> None: - self.score = new_score - - def set_victory_condition(self, new_victory_condition: str | None) -> None: - self.victory_condition = new_victory_condition - - def set_comment(self, new_comment: str | None) -> None: - self.comment = new_comment diff --git a/src/warchron/model/sector.py b/src/warchron/model/sector.py new file mode 100644 index 0000000..47cb4a8 --- /dev/null +++ b/src/warchron/model/sector.py @@ -0,0 +1,39 @@ +from __future__ import annotations +from uuid import uuid4 + + +class Sector: + def __init__( + self, + name: str, + round_id: str, + major_id: str | None, + minor_id: str | None, + influence_id: str | None, + ): + self.id: str = str(uuid4()) + self.name: str = name + self.round_id: str = round_id + self.major_objective_id: str | None = major_id # ref to War.objectives + self.minor_objective_id: str | None = minor_id # ref to War.objectives + self.influence_objective_id: str | None = influence_id # ref to War.objectives + self.mission: str | None = None + self.description: str | None = None + + def set_id(self, new_id: str) -> None: + self.id = new_id + + def set_name(self, new_name: str) -> None: + self.name = new_name + + def set_round(self, new_round_id: str) -> None: + self.round_id = new_round_id + + def set_major(self, new_major_id: str) -> None: + self.major_objective_id = new_major_id + + def set_minor(self, new_minor_id: str) -> None: + self.minor_objective_id = new_minor_id + + def set_influence(self, new_influence_id: str) -> None: + self.influence_objective_id = new_influence_id diff --git a/src/warchron/model/war.py b/src/warchron/model/war.py index 70cf6f4..892edeb 100644 --- a/src/warchron/model/war.py +++ b/src/warchron/model/war.py @@ -3,8 +3,15 @@ from uuid import uuid4 from datetime import datetime from typing import Any, Dict, List -from warchron.model.campaign import Campaign, Sector, CampaignParticipant -from warchron.model.round import Round, Choice, Battle +from warchron.model.exception import DeletionForbidden +from warchron.model.war_participant import WarParticipant +from warchron.model.objective import Objective +from warchron.model.campaign_participant import CampaignParticipant +from warchron.model.sector import Sector +from warchron.model.campaign import Campaign +from warchron.model.round import Round +from warchron.model.choice import Choice +from warchron.model.battle import Battle class War: @@ -76,7 +83,16 @@ class War: obj.set_description(description) def remove_objective(self, objective_id: str) -> None: - # TODO manage sectors referring to it + camp_using_obj: List[str] = [] + for camp in self.campaigns: + if camp.has_sector_with_objective(objective_id): + camp_using_obj.append(camp.name) + if camp_using_obj: + camps_str = ", ".join(camp_using_obj) + raise DeletionForbidden( + f"This objective is used in campaign(s) sector(s): {camps_str}.\n" + "Remove it from campaign(s) first." + ) del self.objectives[objective_id] # War participant methods @@ -103,14 +119,23 @@ class War: def get_all_war_participants(self) -> List[WarParticipant]: return list(self.participants.values()) - def update_war_participant(self, player_id: str, *, faction: str) -> None: - part = self.get_war_participant(player_id) + def update_war_participant(self, participant_id: str, *, faction: str) -> None: + part = self.get_war_participant(participant_id) # Can't change referred Model.players part.set_faction(faction) - def remove_war_participant(self, player_id: str) -> None: - # TODO manage campaign_participants referring to it - del self.participants[player_id] + def remove_war_participant(self, participant_id: str) -> None: + camp_using_part: List[str] = [] + for camp in self.campaigns: + if camp.has_war_participant(participant_id): + camp_using_part.append(camp.name) + if camp_using_part: + camps_str = ", ".join(camp_using_part) + raise DeletionForbidden( + f"This war participant is used in campaign(s): {camps_str}.\n" + "Remove it from campaign(s) first." + ) + del self.participants[participant_id] # Campaign methods @@ -133,13 +158,15 @@ class War: return camp raise KeyError(f"Campaign {campaign_id} not found in War {self.id}") - def get_campaign_by_round(self, round_id: str) -> Campaign: + # TODO replace multiloops by internal has_* method + def get_campaign_by_round(self, round_id: str) -> Campaign | None: for camp in self.campaigns: for rnd in camp.rounds: if rnd.id == round_id: return camp - raise KeyError(f"Round {round_id} not found in any Campaign") + return None + # TODO replace multiloops by internal has_* method def get_campaign_by_sector(self, sector_id: str) -> Campaign: for camp in self.campaigns: for sect in camp.sectors.values(): @@ -179,6 +206,7 @@ class War: camp = self.get_campaign(campaign_id) return camp.add_sector(name, round_id, major_id, minor_id, influence_id) + # TODO replace multiloops by internal has_* method def get_sector(self, sector_id: str) -> Sector: for camp in self.campaigns: for sect in camp.sectors.values(): @@ -226,6 +254,7 @@ class War: camp = self.get_campaign(campaign_id) return camp.add_campaign_participant(participant_id, leader, theme) + # TODO replace multiloops by internal has_* method def get_campaign_participant(self, participant_id: str) -> CampaignParticipant: for camp in self.campaigns: for part in camp.participants.values(): @@ -249,19 +278,18 @@ class War: camp = self.get_campaign(campaign_id) return camp.add_round() - def add_battle(self, campaign_id: str) -> Round: - camp = self.get_campaign(campaign_id) - return camp.add_round() - def remove_round(self, round_id: str) -> None: camp = self.get_campaign_by_round(round_id) - camp.remove_round(round_id) + if camp is not None: + camp.remove_round(round_id) # Choice methods def create_choice(self, round_id: str, participant_id: str) -> Choice: camp = self.get_campaign_by_round(round_id) - return camp.create_choice(round_id, participant_id) + if camp is not None: + return camp.create_choice(round_id, participant_id) + raise KeyError("Campaign with round {round_id} doesn't exist") def update_choice( self, @@ -272,19 +300,27 @@ class War: comment: str | None, ) -> None: camp = self.get_campaign_by_round(round_id) - camp.update_choice( - round_id, participant_id, priority_sector_id, secondary_sector_id, comment - ) + if camp is not None: + camp.update_choice( + round_id, + participant_id, + priority_sector_id, + secondary_sector_id, + comment, + ) def remove_choice(self, round_id: str, participant_id: str) -> None: camp = self.get_campaign_by_round(round_id) - camp.remove_choice(round_id, participant_id) + if camp is not None: + camp.remove_choice(round_id, participant_id) # Battle methods def create_battle(self, round_id: str, sector_id: str) -> Battle: camp = self.get_campaign_by_round(round_id) - return camp.create_battle(round_id, sector_id) + if camp is not None: + return camp.create_battle(round_id, sector_id) + raise KeyError("Campaign with round {round_id} doesn't exist") def update_battle( self, @@ -298,49 +334,19 @@ class War: comment: str | None, ) -> None: camp = self.get_campaign_by_round(round_id) - camp.update_battle( - round_id, - sector_id, - player_1_id, - player_2_id, - winner_id, - score, - victory_condition, - comment, - ) + if camp is not None: + camp.update_battle( + round_id, + sector_id, + player_1_id, + player_2_id, + winner_id, + score, + victory_condition, + comment, + ) def remove_battle(self, round_id: str, sector_id: str) -> None: camp = self.get_campaign_by_round(round_id) - camp.remove_battle(round_id, sector_id) - - -class Objective: - def __init__(self, name: str, description: str): - self.id: str = str(uuid4()) - self.name: str = name - self.description: str = description - - def set_id(self, new_id: str) -> None: - self.id = new_id - - def set_name(self, new_name: str) -> None: - self.name = new_name - - def set_description(self, new_description: str) -> None: - self.description = new_description - - -class WarParticipant: - def __init__(self, *, player_id: str, faction: str): - self.id: str = str(uuid4()) - self.player_id: str = player_id # ref to WarModel.players - self.faction: str = faction - - def set_id(self, new_id: str) -> None: - self.id = new_id - - def set_player(self, new_player: str) -> None: - self.player_id = new_player - - def set_faction(self, new_faction: str) -> None: - self.faction = new_faction + if camp is not None: + camp.remove_battle(round_id, sector_id) diff --git a/src/warchron/model/war_participant.py b/src/warchron/model/war_participant.py index e69de29..4b405f0 100644 --- a/src/warchron/model/war_participant.py +++ b/src/warchron/model/war_participant.py @@ -0,0 +1,18 @@ +from __future__ import annotations +from uuid import uuid4 + + +class WarParticipant: + def __init__(self, *, player_id: str, faction: str): + self.id: str = str(uuid4()) + self.player_id: str = player_id # ref to WarModel.players + self.faction: str = faction + + def set_id(self, new_id: str) -> None: + self.id = new_id + + def set_player(self, new_player: str) -> None: + self.player_id = new_player + + def set_faction(self, new_faction: str) -> None: + self.faction = new_faction diff --git a/src/warchron/view/battles_dialog.py b/src/warchron/view/battles_dialog.py new file mode 100644 index 0000000..2ef86a0 --- /dev/null +++ b/src/warchron/view/battles_dialog.py @@ -0,0 +1,68 @@ +from typing import cast, List + +from PyQt6.QtWidgets import QWidget, QDialog + +from warchron.controller.dtos import ParticipantOption, SectorDTO +from warchron.view.helpers import select_if_exists +from warchron.view.ui.ui_battle_result_dialog import Ui_battleResultDialog + + +class BattlesDialog(QDialog): + def __init__( + self, + parent: QWidget | None = None, + *, + sectors: List[SectorDTO], + default_sector_id: str | None = None, + players: List[ParticipantOption], + default_player_1_id: str | None = None, + default_player_2_id: str | None = None, + default_winner_id: str | None = None, + default_score: str | None = None, + default_victory_condition: str | None = None, + default_comment: str | None = None, + ) -> None: + super().__init__(parent) + self.ui: Ui_battleResultDialog = Ui_battleResultDialog() + self.ui.setupUi(self) # type: ignore + for sect in sectors: + self.ui.sectorComboBox.addItem(sect.name, sect.id) + select_if_exists(self.ui.sectorComboBox, default_sector_id) + self.ui.sectorComboBox.setEnabled(False) + + self.ui.player1ComboBox.addItem("(none)", None) + self.ui.player2ComboBox.addItem("(none)", None) + for play in players: + self.ui.player1ComboBox.addItem(play.name, play.id) + self.ui.player2ComboBox.addItem(play.name, play.id) + select_if_exists(self.ui.player1ComboBox, default_player_1_id) + select_if_exists(self.ui.player2ComboBox, default_player_2_id) + self.ui.winnerComboBox.addItem("(none)", None) + for play in players: + if play.id in (default_player_1_id, default_player_2_id): + self.ui.winnerComboBox.addItem(play.name, play.id) + select_if_exists(self.ui.winnerComboBox, default_winner_id) + self.ui.score.setText(default_score) + self.ui.victoryCondition.setText(default_victory_condition) + self.ui.battleComment.setPlainText(default_comment) + + def get_sector_id(self) -> str: + return cast(str, self.ui.sectorComboBox.currentData()) + + def get_player_1_id(self) -> str: + return cast(str, self.ui.player1ComboBox.currentData()) + + def get_player_2_id(self) -> str: + return cast(str, self.ui.player2ComboBox.currentData()) + + def get_winner_id(self) -> str: + return cast(str, self.ui.winnerComboBox.currentData()) + + def get_score(self) -> str: + return self.ui.score.text().strip() + + def get_victory_condition(self) -> str: + return self.ui.victoryCondition.text().strip() + + def get_comment(self) -> str: + return self.ui.battleComment.toPlainText().strip() diff --git a/src/warchron/view/campaign_dialog.py b/src/warchron/view/campaign_dialog.py new file mode 100644 index 0000000..185203f --- /dev/null +++ b/src/warchron/view/campaign_dialog.py @@ -0,0 +1,25 @@ +from PyQt6.QtWidgets import QDialog + +from warchron.view.ui.ui_campaign_dialog import Ui_campaignDialog +from PyQt6.QtWidgets import QWidget + + +class CampaignDialog(QDialog): + def __init__( + self, + parent: QWidget | None = None, + default_name: str = "", + default_month: int | None = None, + ) -> None: + super().__init__(parent) + self.ui: Ui_campaignDialog = Ui_campaignDialog() + self.ui.setupUi(self) # type: ignore + self.ui.campaignName.setText(default_name) + if default_month is not None: + self.ui.campaignMonth.setValue(default_month) + + def get_campaign_name(self) -> str: + return self.ui.campaignName.text().strip() + + def get_campaign_month(self) -> int: + return int(self.ui.campaignMonth.value()) diff --git a/src/warchron/view/campaign_participant_dialog.py b/src/warchron/view/campaign_participant_dialog.py new file mode 100644 index 0000000..ae0fd86 --- /dev/null +++ b/src/warchron/view/campaign_participant_dialog.py @@ -0,0 +1,40 @@ +from typing import cast, List + +from PyQt6.QtWidgets import QWidget, QDialog + +from warchron.controller.dtos import ParticipantOption +from warchron.view.helpers import select_if_exists +from warchron.view.ui.ui_campaign_participant_dialog import ( + Ui_campaignParticipantDialog, +) + + +class CampaignParticipantDialog(QDialog): + def __init__( + self, + parent: QWidget | None = None, + *, + participants: List[ParticipantOption], + default_participant_id: str | None = None, + default_leader: str | None = "", + default_theme: str | None = "", + editable_player: bool = True, + ) -> None: + super().__init__(parent) + self.ui: Ui_campaignParticipantDialog = Ui_campaignParticipantDialog() + self.ui.setupUi(self) # type: ignore + for part in participants: + self.ui.playerComboBox.addItem(part.name, part.id) + select_if_exists(self.ui.playerComboBox, default_participant_id) + self.ui.playerComboBox.setEnabled(editable_player) + self.ui.leader.setText(default_leader) + self.ui.theme.setText(default_theme) + + def get_player_id(self) -> str: + return cast(str, self.ui.playerComboBox.currentData()) + + def get_participant_leader(self) -> str: + return self.ui.leader.text().strip() + + def get_participant_theme(self) -> str: + return self.ui.theme.text().strip() diff --git a/src/warchron/view/choices_dialog.py b/src/warchron/view/choices_dialog.py new file mode 100644 index 0000000..afaa79a --- /dev/null +++ b/src/warchron/view/choices_dialog.py @@ -0,0 +1,48 @@ +from typing import cast, List + +from PyQt6.QtWidgets import QWidget, QDialog + +from warchron.controller.dtos import ParticipantOption, SectorDTO +from warchron.view.helpers import select_if_exists +from warchron.view.ui.ui_choices_dialog import Ui_choicesDialog + + +class ChoicesDialog(QDialog): + def __init__( + self, + parent: QWidget | None = None, + *, + participants: List[ParticipantOption], + default_participant_id: str | None = None, + sectors: List[SectorDTO], + default_priority_id: str | None = None, + default_secondary_id: str | None = None, + default_comment: str | None = None, + ) -> None: + super().__init__(parent) + self.ui: Ui_choicesDialog = Ui_choicesDialog() + self.ui.setupUi(self) # type: ignore + for part in participants: + self.ui.playerComboBox.addItem(part.name, part.id) + select_if_exists(self.ui.playerComboBox, default_participant_id) + self.ui.playerComboBox.setEnabled(False) + self.ui.priorityComboBox.addItem("(none)", None) + self.ui.secondaryComboBox.addItem("(none)", None) + for sect in sectors: + self.ui.priorityComboBox.addItem(sect.name, sect.id) + self.ui.secondaryComboBox.addItem(sect.name, sect.id) + select_if_exists(self.ui.priorityComboBox, default_priority_id) + select_if_exists(self.ui.secondaryComboBox, default_secondary_id) + self.ui.choiceComment.setPlainText(default_comment) + + def get_participant_id(self) -> str: + return cast(str, self.ui.playerComboBox.currentData()) + + def get_priority_id(self) -> str: + return cast(str, self.ui.priorityComboBox.currentData()) + + def get_secondary_id(self) -> str: + return cast(str, self.ui.secondaryComboBox.currentData()) + + def get_comment(self) -> str: + return self.ui.choiceComment.toPlainText().strip() diff --git a/src/warchron/view/helpers.py b/src/warchron/view/helpers.py new file mode 100644 index 0000000..3e2f585 --- /dev/null +++ b/src/warchron/view/helpers.py @@ -0,0 +1,27 @@ +import calendar + +from PyQt6.QtWidgets import QComboBox + +from warchron.controller.dtos import WarDTO, CampaignDTO + + +def select_if_exists(combo: QComboBox, value: str | None) -> None: + if value is None: + return + idx = combo.findData(value) + if idx != -1: + combo.setCurrentIndex(idx) + + +def format_war_label(war: WarDTO) -> str: + return f"{war.name} ({war.year})" + + +def format_campaign_label(camp: CampaignDTO) -> str: + return f"{camp.name} ({calendar.month_name[camp.month]})" + + +def format_round_label(index: int) -> str: + if index is None: + return "" + return f"Round {index}" diff --git a/src/warchron/view/objective_dialog.py b/src/warchron/view/objective_dialog.py new file mode 100644 index 0000000..a414c8b --- /dev/null +++ b/src/warchron/view/objective_dialog.py @@ -0,0 +1,24 @@ +from PyQt6.QtWidgets import QWidget, QDialog + +from warchron.view.ui.ui_objective_dialog import Ui_objectiveDialog + + +class ObjectiveDialog(QDialog): + def __init__( + self, + parent: QWidget | None = None, + *, + default_name: str = "", + default_description: str | None = "", + ) -> None: + super().__init__(parent) + self.ui: Ui_objectiveDialog = Ui_objectiveDialog() + self.ui.setupUi(self) # type: ignore + self.ui.objectiveName.setText(default_name) + self.ui.objectiveDescription.setPlainText(default_description) + + def get_objective_name(self) -> str: + return self.ui.objectiveName.text().strip() + + def get_objective_description(self) -> str: + return self.ui.objectiveDescription.toPlainText().strip() diff --git a/src/warchron/view/player_dialog.py b/src/warchron/view/player_dialog.py new file mode 100644 index 0000000..af039e6 --- /dev/null +++ b/src/warchron/view/player_dialog.py @@ -0,0 +1,16 @@ +from PyQt6.QtWidgets import QWidget, QDialog + +from warchron.view.ui.ui_player_dialog import Ui_playerDialog + + +class PlayerDialog(QDialog): + def __init__( + self, parent: QWidget | None = None, *, default_name: str = "" + ) -> None: + super().__init__(parent) + self.ui: Ui_playerDialog = Ui_playerDialog() + self.ui.setupUi(self) # type: ignore + self.ui.playerName.setText(default_name) + + def get_player_name(self) -> str: + return self.ui.playerName.text().strip() diff --git a/src/warchron/view/sector_dialog.py b/src/warchron/view/sector_dialog.py new file mode 100644 index 0000000..74312dd --- /dev/null +++ b/src/warchron/view/sector_dialog.py @@ -0,0 +1,54 @@ +from typing import cast, List + +from PyQt6.QtWidgets import QWidget, QDialog + +from warchron.controller.dtos import ObjectiveDTO, RoundDTO +from warchron.view.helpers import select_if_exists, format_round_label +from warchron.view.ui.ui_sector_dialog import Ui_sectorDialog + + +class SectorDialog(QDialog): + def __init__( + self, + parent: QWidget | None = None, + *, + default_name: str = "", + rounds: List[RoundDTO], + default_round_id: str | None = None, + objectives: List[ObjectiveDTO], + default_major_id: str | None = None, + default_minor_id: str | None = None, + default_influence_id: str | None = None, + ) -> None: + super().__init__(parent) + self.ui: Ui_sectorDialog = Ui_sectorDialog() + self.ui.setupUi(self) # type: ignore + self.ui.majorComboBox.addItem("(none)", None) + self.ui.minorComboBox.addItem("(none)", None) + self.ui.influenceComboBox.addItem("(none)", None) + self.ui.sectorName.setText(default_name) + for index, rnd in enumerate(rounds, start=1): + self.ui.roundComboBox.addItem(format_round_label(index), rnd.id) + select_if_exists(self.ui.roundComboBox, default_round_id) + for obj in objectives: + self.ui.majorComboBox.addItem(obj.name, obj.id) + self.ui.minorComboBox.addItem(obj.name, obj.id) + self.ui.influenceComboBox.addItem(obj.name, obj.id) + select_if_exists(self.ui.majorComboBox, default_major_id) + select_if_exists(self.ui.minorComboBox, default_minor_id) + select_if_exists(self.ui.influenceComboBox, default_influence_id) + + def get_sector_name(self) -> str: + return self.ui.sectorName.text().strip() + + def get_round_id(self) -> str: + return cast(str, self.ui.roundComboBox.currentData()) + + def get_major_id(self) -> str: + return cast(str, self.ui.majorComboBox.currentData()) + + def get_minor_id(self) -> str: + return cast(str, self.ui.minorComboBox.currentData()) + + def get_influence_id(self) -> str: + return cast(str, self.ui.influenceComboBox.currentData()) diff --git a/src/warchron/view/view.py b/src/warchron/view/view.py index 3b34fcf..8a86271 100644 --- a/src/warchron/view/view.py +++ b/src/warchron/view/view.py @@ -1,17 +1,10 @@ -from typing import cast, Callable, List +from typing import Callable, List from pathlib import Path import calendar from PyQt6 import QtWidgets from PyQt6.QtCore import Qt, QPoint -from PyQt6.QtWidgets import ( - QWidget, - QDialog, - QFileDialog, - QTreeWidgetItem, - QMenu, - QComboBox, -) +from PyQt6.QtWidgets import QWidget, QFileDialog, QTreeWidgetItem, QMenu from PyQt6.QtGui import QCloseEvent from warchron.constants import ROLE_TYPE, ROLE_ID, ItemType @@ -21,49 +14,17 @@ from warchron.controller.dtos import ( WarDTO, WarParticipantDTO, ObjectiveDTO, - CampaignDTO, CampaignParticipantDTO, SectorDTO, - RoundDTO, ChoiceDTO, BattleDTO, ) -from warchron.view.ui.ui_main_window import Ui_MainWindow -from warchron.view.ui.ui_player_dialog import Ui_playerDialog -from warchron.view.ui.ui_war_dialog import Ui_warDialog -from warchron.view.ui.ui_campaign_dialog import Ui_campaignDialog -from warchron.view.ui.ui_objective_dialog import Ui_objectiveDialog -from warchron.view.ui.ui_war_participant_dialog import Ui_warParticipantDialog -from warchron.view.ui.ui_campaign_participant_dialog import ( - Ui_campaignParticipantDialog, +from warchron.view.helpers import ( + format_campaign_label, + format_round_label, + format_war_label, ) -from warchron.view.ui.ui_sector_dialog import Ui_sectorDialog -from warchron.view.ui.ui_choices_dialog import Ui_choicesDialog -from warchron.view.ui.ui_battle_result_dialog import Ui_battleResultDialog - -# utils... - - -def select_if_exists(combo: QComboBox, value: str | None) -> None: - if value is None: - return - idx = combo.findData(value) - if idx != -1: - combo.setCurrentIndex(idx) - - -def format_war_label(war: WarDTO) -> str: - return f"{war.name} ({war.year})" - - -def format_campaign_label(camp: CampaignDTO) -> str: - return f"{camp.name} ({calendar.month_name[camp.month]})" - - -def format_round_label(index: int) -> str: - if index is None: - return "" - return f"Round {index}" +from warchron.view.ui.ui_main_window import Ui_MainWindow class View(QtWidgets.QMainWindow, Ui_MainWindow): @@ -517,285 +478,3 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): table.setItem(row, 1, player_1_item) table.setItem(row, 2, player_2_item) table.resizeColumnsToContents() - - -class PlayerDialog(QDialog): - def __init__( - self, parent: QWidget | None = None, *, default_name: str = "" - ) -> None: - super().__init__(parent) - self.ui: Ui_playerDialog = Ui_playerDialog() - self.ui.setupUi(self) # type: ignore - self.ui.playerName.setText(default_name) - - def get_player_name(self) -> str: - return self.ui.playerName.text().strip() - - -class WarDialog(QDialog): - def __init__( - self, - parent: QWidget | None = None, - default_name: str = "", - default_year: int | None = None, - ) -> None: - super().__init__(parent) - self.ui: Ui_warDialog = Ui_warDialog() - self.ui.setupUi(self) # type: ignore - self.ui.warName.setText(default_name) - if default_year is not None: - self.ui.warYear.setValue(default_year) - - def get_war_name(self) -> str: - return self.ui.warName.text().strip() - - def get_war_year(self) -> int: - return int(self.ui.warYear.value()) - - -class CampaignDialog(QDialog): - def __init__( - self, - parent: QWidget | None = None, - default_name: str = "", - default_month: int | None = None, - ) -> None: - super().__init__(parent) - self.ui: Ui_campaignDialog = Ui_campaignDialog() - self.ui.setupUi(self) # type: ignore - self.ui.campaignName.setText(default_name) - if default_month is not None: - self.ui.campaignMonth.setValue(default_month) - - def get_campaign_name(self) -> str: - return self.ui.campaignName.text().strip() - - def get_campaign_month(self) -> int: - return int(self.ui.campaignMonth.value()) - - -class ObjectiveDialog(QDialog): - def __init__( - self, - parent: QWidget | None = None, - *, - default_name: str = "", - default_description: str | None = "", - ) -> None: - super().__init__(parent) - self.ui: Ui_objectiveDialog = Ui_objectiveDialog() - self.ui.setupUi(self) # type: ignore - self.ui.objectiveName.setText(default_name) - self.ui.objectiveDescription.setPlainText(default_description) - - def get_objective_name(self) -> str: - return self.ui.objectiveName.text().strip() - - def get_objective_description(self) -> str: - return self.ui.objectiveDescription.toPlainText().strip() - - -class WarParticipantDialog(QDialog): - def __init__( - self, - parent: QWidget | None = None, - *, - players: List[ParticipantOption], - default_player_id: str | None = None, - default_faction: str | None = "", - editable_player: bool = True, - ): - super().__init__(parent) - self.ui: Ui_warParticipantDialog = Ui_warParticipantDialog() - self.ui.setupUi(self) # type: ignore - for player in players: - self.ui.playerComboBox.addItem(player.name, player.id) - select_if_exists(self.ui.playerComboBox, default_player_id) - self.ui.playerComboBox.setEnabled(editable_player) - self.ui.faction.setText(default_faction) - - def get_player_id(self) -> str: - return cast(str, self.ui.playerComboBox.currentData()) - - def get_participant_faction(self) -> str: - return self.ui.faction.text().strip() - - -class CampaignParticipantDialog(QDialog): - def __init__( - self, - parent: QWidget | None = None, - *, - participants: List[ParticipantOption], - default_participant_id: str | None = None, - default_leader: str | None = "", - default_theme: str | None = "", - editable_player: bool = True, - ) -> None: - super().__init__(parent) - self.ui: Ui_campaignParticipantDialog = Ui_campaignParticipantDialog() - self.ui.setupUi(self) # type: ignore - for part in participants: - self.ui.playerComboBox.addItem(part.name, part.id) - select_if_exists(self.ui.playerComboBox, default_participant_id) - self.ui.playerComboBox.setEnabled(editable_player) - self.ui.leader.setText(default_leader) - self.ui.theme.setText(default_theme) - - def get_player_id(self) -> str: - return cast(str, self.ui.playerComboBox.currentData()) - - def get_participant_leader(self) -> str: - return self.ui.leader.text().strip() - - def get_participant_theme(self) -> str: - return self.ui.theme.text().strip() - - -class SectorDialog(QDialog): - def __init__( - self, - parent: QWidget | None = None, - *, - default_name: str = "", - rounds: List[RoundDTO], - default_round_id: str | None = None, - objectives: List[ObjectiveDTO], - default_major_id: str | None = None, - default_minor_id: str | None = None, - default_influence_id: str | None = None, - ) -> None: - super().__init__(parent) - self.ui: Ui_sectorDialog = Ui_sectorDialog() - self.ui.setupUi(self) # type: ignore - self.ui.majorComboBox.addItem("(none)", None) - self.ui.minorComboBox.addItem("(none)", None) - self.ui.influenceComboBox.addItem("(none)", None) - self.ui.sectorName.setText(default_name) - for index, rnd in enumerate(rounds, start=1): - self.ui.roundComboBox.addItem(format_round_label(index), rnd.id) - select_if_exists(self.ui.roundComboBox, default_round_id) - for obj in objectives: - self.ui.majorComboBox.addItem(obj.name, obj.id) - self.ui.minorComboBox.addItem(obj.name, obj.id) - self.ui.influenceComboBox.addItem(obj.name, obj.id) - select_if_exists(self.ui.majorComboBox, default_major_id) - select_if_exists(self.ui.minorComboBox, default_minor_id) - select_if_exists(self.ui.influenceComboBox, default_influence_id) - - def get_sector_name(self) -> str: - return self.ui.sectorName.text().strip() - - def get_round_id(self) -> str: - return cast(str, self.ui.roundComboBox.currentData()) - - def get_major_id(self) -> str: - return cast(str, self.ui.majorComboBox.currentData()) - - def get_minor_id(self) -> str: - return cast(str, self.ui.minorComboBox.currentData()) - - def get_influence_id(self) -> str: - return cast(str, self.ui.influenceComboBox.currentData()) - - -class ChoicesDialog(QDialog): - def __init__( - self, - parent: QWidget | None = None, - *, - participants: List[ParticipantOption], - default_participant_id: str | None = None, - sectors: List[SectorDTO], - default_priority_id: str | None = None, - default_secondary_id: str | None = None, - default_comment: str | None = None, - ) -> None: - super().__init__(parent) - self.ui: Ui_choicesDialog = Ui_choicesDialog() - self.ui.setupUi(self) # type: ignore - for part in participants: - self.ui.playerComboBox.addItem(part.name, part.id) - select_if_exists(self.ui.playerComboBox, default_participant_id) - self.ui.playerComboBox.setEnabled(False) - self.ui.priorityComboBox.addItem("(none)", None) - self.ui.secondaryComboBox.addItem("(none)", None) - for sect in sectors: - self.ui.priorityComboBox.addItem(sect.name, sect.id) - self.ui.secondaryComboBox.addItem(sect.name, sect.id) - select_if_exists(self.ui.priorityComboBox, default_priority_id) - select_if_exists(self.ui.secondaryComboBox, default_secondary_id) - self.ui.choiceComment.setPlainText(default_comment) - - def get_participant_id(self) -> str: - return cast(str, self.ui.playerComboBox.currentData()) - - def get_priority_id(self) -> str: - return cast(str, self.ui.priorityComboBox.currentData()) - - def get_secondary_id(self) -> str: - return cast(str, self.ui.secondaryComboBox.currentData()) - - def get_comment(self) -> str: - return self.ui.choiceComment.toPlainText().strip() - - -class BattlesDialog(QDialog): - def __init__( - self, - parent: QWidget | None = None, - *, - sectors: List[SectorDTO], - default_sector_id: str | None = None, - players: List[ParticipantOption], - default_player_1_id: str | None = None, - default_player_2_id: str | None = None, - default_winner_id: str | None = None, - default_score: str | None = None, - default_victory_condition: str | None = None, - default_comment: str | None = None, - ) -> None: - super().__init__(parent) - self.ui: Ui_battleResultDialog = Ui_battleResultDialog() - self.ui.setupUi(self) # type: ignore - for sect in sectors: - self.ui.sectorComboBox.addItem(sect.name, sect.id) - select_if_exists(self.ui.sectorComboBox, default_sector_id) - self.ui.sectorComboBox.setEnabled(False) - - self.ui.player1ComboBox.addItem("(none)", None) - self.ui.player2ComboBox.addItem("(none)", None) - for play in players: - self.ui.player1ComboBox.addItem(play.name, play.id) - self.ui.player2ComboBox.addItem(play.name, play.id) - select_if_exists(self.ui.player1ComboBox, default_player_1_id) - select_if_exists(self.ui.player2ComboBox, default_player_2_id) - self.ui.winnerComboBox.addItem("(none)", None) - for play in players: - if play.id in (default_player_1_id, default_player_2_id): - self.ui.winnerComboBox.addItem(play.name, play.id) - select_if_exists(self.ui.winnerComboBox, default_winner_id) - self.ui.score.setText(default_score) - self.ui.victoryCondition.setText(default_victory_condition) - self.ui.battleComment.setPlainText(default_comment) - - def get_sector_id(self) -> str: - return cast(str, self.ui.sectorComboBox.currentData()) - - def get_player_1_id(self) -> str: - return cast(str, self.ui.player1ComboBox.currentData()) - - def get_player_2_id(self) -> str: - return cast(str, self.ui.player2ComboBox.currentData()) - - def get_winner_id(self) -> str: - return cast(str, self.ui.winnerComboBox.currentData()) - - def get_score(self) -> str: - return self.ui.score.text().strip() - - def get_victory_condition(self) -> str: - return self.ui.victoryCondition.text().strip() - - def get_comment(self) -> str: - return self.ui.battleComment.toPlainText().strip() diff --git a/src/warchron/view/war_dialog.py b/src/warchron/view/war_dialog.py new file mode 100644 index 0000000..3d17a3f --- /dev/null +++ b/src/warchron/view/war_dialog.py @@ -0,0 +1,24 @@ +from PyQt6.QtWidgets import QWidget, QDialog + +from warchron.view.ui.ui_war_dialog import Ui_warDialog + + +class WarDialog(QDialog): + def __init__( + self, + parent: QWidget | None = None, + default_name: str = "", + default_year: int | None = None, + ) -> None: + super().__init__(parent) + self.ui: Ui_warDialog = Ui_warDialog() + self.ui.setupUi(self) # type: ignore + self.ui.warName.setText(default_name) + if default_year is not None: + self.ui.warYear.setValue(default_year) + + def get_war_name(self) -> str: + return self.ui.warName.text().strip() + + def get_war_year(self) -> int: + return int(self.ui.warYear.value()) diff --git a/src/warchron/view/war_participant_dialog.py b/src/warchron/view/war_participant_dialog.py new file mode 100644 index 0000000..9718602 --- /dev/null +++ b/src/warchron/view/war_participant_dialog.py @@ -0,0 +1,33 @@ +from typing import cast, List + +from PyQt6.QtWidgets import QWidget, QDialog + +from warchron.controller.dtos import ParticipantOption +from warchron.view.helpers import select_if_exists +from warchron.view.ui.ui_war_participant_dialog import Ui_warParticipantDialog + + +class WarParticipantDialog(QDialog): + def __init__( + self, + parent: QWidget | None = None, + *, + players: List[ParticipantOption], + default_player_id: str | None = None, + default_faction: str | None = "", + editable_player: bool = True, + ): + super().__init__(parent) + self.ui: Ui_warParticipantDialog = Ui_warParticipantDialog() + self.ui.setupUi(self) # type: ignore + for player in players: + self.ui.playerComboBox.addItem(player.name, player.id) + select_if_exists(self.ui.playerComboBox, default_player_id) + self.ui.playerComboBox.setEnabled(editable_player) + self.ui.faction.setText(default_faction) + + def get_player_id(self) -> str: + return cast(str, self.ui.playerComboBox.currentData()) + + def get_participant_faction(self) -> str: + return self.ui.faction.text().strip()