"""MTG-specific deck line extensions.""" from odoo import api, fields, models from odoo.exceptions import UserError MTG_DECK_LEGALITY_SELECTION = [ ("unknown", "Unknown"), ("legal", "Legal"), ("restricted", "Restricted"), ("not_legal", "Not Legal"), ("banned", "Banned"), ] class MvdTcgDeckLine(models.Model): """Expose MTG card metadata directly on deck lines.""" _inherit = "mvd.tcg.deck.line" mtg_set_id = fields.Many2one( related="card_id.mtg_set_id", string="Set", readonly=True, store=True, index=True, ) mtg_collector_number = fields.Char( related="card_id.mtg_collector_number", string="Collector Number", readonly=True, store=True, index=True, ) mtg_mana_cost = fields.Char( related="card_id.mtg_mana_cost", string="Mana Cost", readonly=True, store=True, ) mtg_mana_value = fields.Float( related="card_id.mtg_mana_value", string="Mana Value", readonly=True, store=True, index=True, ) mtg_type_line = fields.Char( related="card_id.mtg_type_line", string="Type Line", readonly=True, ) mtg_primary_card_type_id = fields.Many2one( "mvd.tcg.mtg.card.type", string="Primary Card Type", compute="_compute_mtg_primary_card_type", store=True, readonly=True, index=True, ) mtg_legality_status = fields.Selection( selection=MTG_DECK_LEGALITY_SELECTION, string="Legality Status", compute="_compute_mtg_rule_flags", store=True, readonly=True, index=True, ) mtg_legality_ok = fields.Boolean( string="Legality OK", compute="_compute_mtg_rule_flags", store=True, readonly=True, ) mtg_color_identity_violation = fields.Boolean( string="Color Identity Violation", compute="_compute_mtg_rule_flags", store=True, readonly=True, ) mtg_singleton_violation = fields.Boolean( string="Singleton Violation", compute="_compute_mtg_rule_flags", store=True, readonly=True, ) mtg_issue_count = fields.Integer( string="Issue Count", compute="_compute_mtg_rule_flags", store=True, readonly=True, ) @api.depends( "card_id.mtg_card_type_ids", "card_id.mtg_card_type_ids.sequence", "card_id.mtg_card_type_ids.name", ) def _compute_mtg_primary_card_type(self): """Expose one stable primary card type for grouping and sorting. Returns: None: The compute updates records in place. """ for line in self: card_type = line.card_id.mtg_card_type_ids.sorted( key=lambda current_type: ( current_type.sequence, current_type.name or "", current_type.id, ) )[:1] line.mtg_primary_card_type_id = card_type @api.depends( "board_id.code", "board_id.include_in_total", "deck_id.is_mtg_deck", "deck_id.mtg_format_id", "deck_id.mtg_format_id.code", "deck_id.mtg_color_identity_signature", "deck_id.line_ids.quantity", "deck_id.line_ids.board_id", "deck_id.line_ids.board_id.code", "deck_id.line_ids.board_id.include_in_total", "deck_id.line_ids.card_id", "deck_id.line_ids.card_id.mtg_oracle_id", "deck_id.line_ids.card_id.mtg_type_line", "deck_id.line_ids.card_id.mtg_oracle_text", "card_id.mtg_oracle_id", "card_id.mtg_color_identity_signature", "card_id.mtg_legality_ids.status", "card_id.mtg_legality_ids.format_code", ) def _compute_mtg_rule_flags(self): """Compute line-level MTG rule signals for the current deck context. Returns: None: The compute updates records in place. """ singleton_violation_by_line = {} legality_status_by_line = {} legality_ok_by_line = {} color_violation_by_line = {} mtg_decks = self.mapped("deck_id").filtered("is_mtg_deck") for deck in mtg_decks: format_code = deck.mtg_format_id.code or "" commander_identity = set(deck.mtg_color_identity_signature or "") singleton_groups = [] candidate_lines = deck.line_ids.filtered( lambda line: line.card_id and line.card_id.game_id.code == "mtg" and line.board_id.include_in_total ) candidate_entries = [] for line in candidate_lines: card = line.card_id.with_context(lang="en_US") if self._mtg_is_singleton_exempt(card): continue singleton_aliases = set(card.mtg_get_singleton_key_aliases()) if not singleton_aliases: singleton_aliases = {card.external_ref or str(card.id)} candidate_entries.append( { "line": line, "aliases": singleton_aliases, } ) remaining_entries = list(candidate_entries) while remaining_entries: seed_entry = remaining_entries.pop(0) grouped_lines = [seed_entry["line"]] grouped_aliases = set(seed_entry["aliases"]) did_merge = True while did_merge: did_merge = False unmatched_entries = [] for entry in remaining_entries: if grouped_aliases & entry["aliases"]: grouped_lines.append(entry["line"]) grouped_aliases |= entry["aliases"] did_merge = True else: unmatched_entries.append(entry) remaining_entries = unmatched_entries singleton_groups.append(grouped_lines) for grouped_lines in singleton_groups: total_quantity = sum(line.quantity for line in grouped_lines) has_violation = total_quantity > 1 for line in grouped_lines: singleton_violation_by_line[line.id] = has_violation for line in deck.line_ids: if not line.card_id or line.card_id.game_id.code != "mtg": legality_status_by_line[line.id] = "unknown" legality_ok_by_line[line.id] = True color_violation_by_line[line.id] = False continue legality_status = self._mtg_get_card_legality_status( line.card_id, format_code, ) legality_status_by_line[line.id] = legality_status legality_ok_by_line[line.id] = legality_status not in { "banned", "not_legal", } if legality_status == "restricted" and line.quantity > 1: legality_ok_by_line[line.id] = False line_identity = set(line.card_id.mtg_color_identity_signature or "") color_violation_by_line[line.id] = bool( commander_identity and line.board_id.code != "command_zone" and line_identity - commander_identity ) for line in self: legality_status = legality_status_by_line.get(line.id, "unknown") legality_ok = legality_ok_by_line.get(line.id, True) color_violation = color_violation_by_line.get(line.id, False) singleton_violation = singleton_violation_by_line.get(line.id, False) line.mtg_legality_status = legality_status line.mtg_legality_ok = legality_ok line.mtg_color_identity_violation = color_violation line.mtg_singleton_violation = singleton_violation line.mtg_issue_count = sum( bool(flag) for flag in ( not legality_ok, color_violation, singleton_violation, ) ) @api.model def _mtg_get_card_legality_status(self, card, format_code): """Return the legality status of one card for one MTG format. Args: card: MTG card record. format_code: Canonical MTG format code such as ``commander``. Returns: str: One status from ``MTG_DECK_LEGALITY_SELECTION``. """ if not card or not format_code: return "unknown" legality = card.mtg_legality_ids.filtered( lambda current_legality: current_legality.format_code == format_code )[:1] return legality.status if legality else "unknown" @api.model def _mtg_is_singleton_exempt(self, card): """Return whether a card should bypass singleton checks. Args: card: MTG card record in a stable language context. Returns: bool: ``True`` when the card can legally appear multiple times. """ return bool(card and card.mtg_allows_unlimited_copies()) def _mvd_tcg_validate_move_to_board(self, target_board): """Block duplicate MTG singleton cards during board moves.""" self.ensure_one() result = super()._mvd_tcg_validate_move_to_board(target_board) deck = self.deck_id if ( not deck.is_mtg_deck or not target_board.include_in_total or not deck._mtg_format_enforces_singleton() ): return result english_card = self.card_id.with_context(lang="en_US") conflict_lines = deck._mtg_get_singleton_conflict_lines( english_card, excluding_line=self, ) total_quantity = self.quantity + sum(conflict_lines.mapped("quantity")) if total_quantity <= 1: return result conflicting_names = ", ".join(conflict_lines.mapped("card_id.display_name")[:3]) raise UserError( _( "Commander- and Brawl-style decks can include only one copy of the same card across all styles and printings. " "Moving this line would conflict with: %(cards)s" ) % {"cards": conflicting_names or english_card.display_name} ) def action_move_to_mtg_command_zone(self): """Move the current line to the MTG command zone.""" self.ensure_one() return self._mvd_tcg_move_to_board("command_zone") def action_move_to_mtg_mainboard(self): """Move the current line to the MTG mainboard.""" self.ensure_one() return self._mvd_tcg_move_to_board("mainboard") def action_move_to_mtg_sideboard(self): """Move the current line to the MTG sideboard.""" self.ensure_one() return self._mvd_tcg_move_to_board("sideboard") def action_move_to_mtg_maybeboard(self): """Move the current line to the MTG maybeboard.""" self.ensure_one() return self._mvd_tcg_move_to_board("maybeboard") def _mvd_tcg_get_card_form_view_xmlid(self): """Prefer the MTG-specific card form for MTG deck lines. Returns: str: Stable XMLID for the preferred card form view. """ self.ensure_one() if self.game_id.code == "mtg": return "mvd_tcg_mtg.mvd_tcg_mtg_card_view_form" return super()._mvd_tcg_get_card_form_view_xmlid()