Files
mvd_tcg_mtg_deck/models/mvd_tcg_deck_line.py
2026-04-03 23:08:58 +02:00

330 lines
12 KiB
Python

"""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()