🎉 Initialize module repository

This commit is contained in:
Marc Wempe
2026-04-03 23:08:57 +02:00
commit bf9156b973
190 changed files with 4090 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
__pycache__/
*.py[cod]
.DS_Store
.pytest_cache/
.ruff_cache/
*.log
*.swp
*~

1
__init__.py Normal file
View File

@@ -0,0 +1 @@
from . import models

39
__manifest__.py Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "MVD TCG MTG",
"summary": "Magic: The Gathering reference adapter for the MVD TCG suite",
"version": "19.0.9.5.1",
"description": """
Magic: The Gathering adapter built on top of MVD TCG Base.
This module adds MTG-specific reference data and presentation:
- sets, colors, rarities, finishes, formats, card types, and keywords
- MTG card fields such as mana cost, oracle text, collector number, and faces
- MTG-focused search, views, and symbol rendering in the Odoo backend
It does not import data on its own. External sources such as Scryfall are
handled by dedicated connector modules.
""",
"category": "Tools",
"author": "Mantjeverse Digital",
"license": "LGPL-3",
"depends": ["mvd_tcg_base", "web"],
"data": [
"security/ir.model.access.csv",
"data/mvd_tcg_game_data.xml",
"data/mvd_tcg_mtg_taxonomy_data.xml",
"views/mvd_tcg_mtg_set_views.xml",
"views/mvd_tcg_mtg_taxonomy_views.xml",
"views/mvd_tcg_mtg_card_views.xml",
"views/menu_views.xml",
],
"assets": {
"web.assets_backend": [
"mvd_tcg_mtg/static/src/js/fields/mtg_symbol_catalog.js",
"mvd_tcg_mtg/static/src/js/fields/mtg_symbols_field.js",
"mvd_tcg_mtg/static/src/xml/mtg_symbols_field.xml",
"mvd_tcg_mtg/static/src/scss/mtg_symbols_field.scss",
],
},
"application": False,
"installable": True,
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="mvd_tcg_game_mtg" model="mvd.tcg.game">
<field name="name">Magic: The Gathering</field>
<field name="code">mtg</field>
<field name="sequence">10</field>
<field name="description">Magic: The Gathering reference root for MTG-specific adapters.</field>
</record>
</odoo>

View File

@@ -0,0 +1,248 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="mvd_tcg_mtg_rarity_common" model="mvd.tcg.mtg.rarity">
<field name="name">Common</field>
<field name="code">common</field>
<field name="sequence">10</field>
</record>
<record id="mvd_tcg_mtg_rarity_uncommon" model="mvd.tcg.mtg.rarity">
<field name="name">Uncommon</field>
<field name="code">uncommon</field>
<field name="sequence">20</field>
</record>
<record id="mvd_tcg_mtg_rarity_rare" model="mvd.tcg.mtg.rarity">
<field name="name">Rare</field>
<field name="code">rare</field>
<field name="sequence">30</field>
</record>
<record id="mvd_tcg_mtg_rarity_mythic" model="mvd.tcg.mtg.rarity">
<field name="name">Mythic Rare</field>
<field name="code">mythic</field>
<field name="sequence">40</field>
</record>
<record id="mvd_tcg_mtg_rarity_special" model="mvd.tcg.mtg.rarity">
<field name="name">Special</field>
<field name="code">special</field>
<field name="sequence">50</field>
</record>
<record id="mvd_tcg_mtg_rarity_bonus" model="mvd.tcg.mtg.rarity">
<field name="name">Bonus</field>
<field name="code">bonus</field>
<field name="sequence">60</field>
</record>
<record id="mvd_tcg_mtg_color_white" model="mvd.tcg.mtg.color">
<field name="name">White</field>
<field name="code">W</field>
<field name="sequence">10</field>
</record>
<record id="mvd_tcg_mtg_color_blue" model="mvd.tcg.mtg.color">
<field name="name">Blue</field>
<field name="code">U</field>
<field name="sequence">20</field>
</record>
<record id="mvd_tcg_mtg_color_black" model="mvd.tcg.mtg.color">
<field name="name">Black</field>
<field name="code">B</field>
<field name="sequence">30</field>
</record>
<record id="mvd_tcg_mtg_color_red" model="mvd.tcg.mtg.color">
<field name="name">Red</field>
<field name="code">R</field>
<field name="sequence">40</field>
</record>
<record id="mvd_tcg_mtg_color_green" model="mvd.tcg.mtg.color">
<field name="name">Green</field>
<field name="code">G</field>
<field name="sequence">50</field>
</record>
<record id="mvd_tcg_mtg_color_colorless" model="mvd.tcg.mtg.color">
<field name="name">Colorless</field>
<field name="code">C</field>
<field name="sequence">60</field>
</record>
<record id="mvd_tcg_mtg_card_type_artifact" model="mvd.tcg.mtg.card.type">
<field name="name">Artifact</field>
<field name="code">artifact</field>
<field name="sequence">10</field>
</record>
<record id="mvd_tcg_mtg_card_type_battle" model="mvd.tcg.mtg.card.type">
<field name="name">Battle</field>
<field name="code">battle</field>
<field name="sequence">20</field>
</record>
<record id="mvd_tcg_mtg_card_type_creature" model="mvd.tcg.mtg.card.type">
<field name="name">Creature</field>
<field name="code">creature</field>
<field name="sequence">30</field>
</record>
<record id="mvd_tcg_mtg_card_type_enchantment" model="mvd.tcg.mtg.card.type">
<field name="name">Enchantment</field>
<field name="code">enchantment</field>
<field name="sequence">40</field>
</record>
<record id="mvd_tcg_mtg_card_type_instant" model="mvd.tcg.mtg.card.type">
<field name="name">Instant</field>
<field name="code">instant</field>
<field name="sequence">50</field>
</record>
<record id="mvd_tcg_mtg_card_type_kindred" model="mvd.tcg.mtg.card.type">
<field name="name">Kindred</field>
<field name="code">kindred</field>
<field name="sequence">60</field>
</record>
<record id="mvd_tcg_mtg_card_type_land" model="mvd.tcg.mtg.card.type">
<field name="name">Land</field>
<field name="code">land</field>
<field name="sequence">70</field>
</record>
<record id="mvd_tcg_mtg_card_type_planeswalker" model="mvd.tcg.mtg.card.type">
<field name="name">Planeswalker</field>
<field name="code">planeswalker</field>
<field name="sequence">80</field>
</record>
<record id="mvd_tcg_mtg_card_type_sorcery" model="mvd.tcg.mtg.card.type">
<field name="name">Sorcery</field>
<field name="code">sorcery</field>
<field name="sequence">90</field>
</record>
<record id="mvd_tcg_mtg_finish_nonfoil" model="mvd.tcg.mtg.finish">
<field name="name">Nonfoil</field>
<field name="code">nonfoil</field>
<field name="sequence">10</field>
</record>
<record id="mvd_tcg_mtg_finish_foil" model="mvd.tcg.mtg.finish">
<field name="name">Foil</field>
<field name="code">foil</field>
<field name="sequence">20</field>
</record>
<record id="mvd_tcg_mtg_finish_etched" model="mvd.tcg.mtg.finish">
<field name="name">Etched</field>
<field name="code">etched</field>
<field name="sequence">30</field>
</record>
<record id="mvd_tcg_mtg_platform_paper" model="mvd.tcg.mtg.platform">
<field name="name">Paper</field>
<field name="code">paper</field>
<field name="sequence">10</field>
</record>
<record id="mvd_tcg_mtg_platform_arena" model="mvd.tcg.mtg.platform">
<field name="name">Arena</field>
<field name="code">arena</field>
<field name="sequence">20</field>
</record>
<record id="mvd_tcg_mtg_platform_mtgo" model="mvd.tcg.mtg.platform">
<field name="name">MTGO</field>
<field name="code">mtgo</field>
<field name="sequence">30</field>
</record>
<record id="mvd_tcg_mtg_format_standard" model="mvd.tcg.mtg.format">
<field name="name">Standard</field>
<field name="code">standard</field>
<field name="sequence">10</field>
</record>
<record id="mvd_tcg_mtg_format_future" model="mvd.tcg.mtg.format">
<field name="name">Future</field>
<field name="code">future</field>
<field name="sequence">20</field>
</record>
<record id="mvd_tcg_mtg_format_historic" model="mvd.tcg.mtg.format">
<field name="name">Historic</field>
<field name="code">historic</field>
<field name="sequence">30</field>
</record>
<record id="mvd_tcg_mtg_format_timeless" model="mvd.tcg.mtg.format">
<field name="name">Timeless</field>
<field name="code">timeless</field>
<field name="sequence">40</field>
</record>
<record id="mvd_tcg_mtg_format_gladiator" model="mvd.tcg.mtg.format">
<field name="name">Gladiator</field>
<field name="code">gladiator</field>
<field name="sequence">50</field>
</record>
<record id="mvd_tcg_mtg_format_pioneer" model="mvd.tcg.mtg.format">
<field name="name">Pioneer</field>
<field name="code">pioneer</field>
<field name="sequence">60</field>
</record>
<record id="mvd_tcg_mtg_format_modern" model="mvd.tcg.mtg.format">
<field name="name">Modern</field>
<field name="code">modern</field>
<field name="sequence">70</field>
</record>
<record id="mvd_tcg_mtg_format_legacy" model="mvd.tcg.mtg.format">
<field name="name">Legacy</field>
<field name="code">legacy</field>
<field name="sequence">80</field>
</record>
<record id="mvd_tcg_mtg_format_pauper" model="mvd.tcg.mtg.format">
<field name="name">Pauper</field>
<field name="code">pauper</field>
<field name="sequence">90</field>
</record>
<record id="mvd_tcg_mtg_format_vintage" model="mvd.tcg.mtg.format">
<field name="name">Vintage</field>
<field name="code">vintage</field>
<field name="sequence">100</field>
</record>
<record id="mvd_tcg_mtg_format_penny" model="mvd.tcg.mtg.format">
<field name="name">Penny</field>
<field name="code">penny</field>
<field name="sequence">110</field>
</record>
<record id="mvd_tcg_mtg_format_commander" model="mvd.tcg.mtg.format">
<field name="name">Commander</field>
<field name="code">commander</field>
<field name="sequence">120</field>
</record>
<record id="mvd_tcg_mtg_format_oathbreaker" model="mvd.tcg.mtg.format">
<field name="name">Oathbreaker</field>
<field name="code">oathbreaker</field>
<field name="sequence">130</field>
</record>
<record id="mvd_tcg_mtg_format_standardbrawl" model="mvd.tcg.mtg.format">
<field name="name">Standard Brawl</field>
<field name="code">standardbrawl</field>
<field name="sequence">140</field>
</record>
<record id="mvd_tcg_mtg_format_brawl" model="mvd.tcg.mtg.format">
<field name="name">Brawl</field>
<field name="code">brawl</field>
<field name="sequence">150</field>
</record>
<record id="mvd_tcg_mtg_format_alchemy" model="mvd.tcg.mtg.format">
<field name="name">Alchemy</field>
<field name="code">alchemy</field>
<field name="sequence">160</field>
</record>
<record id="mvd_tcg_mtg_format_paupercommander" model="mvd.tcg.mtg.format">
<field name="name">Pauper Commander</field>
<field name="code">paupercommander</field>
<field name="sequence">170</field>
</record>
<record id="mvd_tcg_mtg_format_duel" model="mvd.tcg.mtg.format">
<field name="name">Duel Commander</field>
<field name="code">duel</field>
<field name="sequence">180</field>
</record>
<record id="mvd_tcg_mtg_format_oldschool" model="mvd.tcg.mtg.format">
<field name="name">Old School</field>
<field name="code">oldschool</field>
<field name="sequence">190</field>
</record>
<record id="mvd_tcg_mtg_format_premodern" model="mvd.tcg.mtg.format">
<field name="name">Premodern</field>
<field name="code">premodern</field>
<field name="sequence">200</field>
</record>
<record id="mvd_tcg_mtg_format_predh" model="mvd.tcg.mtg.format">
<field name="name">PreDH</field>
<field name="code">predh</field>
<field name="sequence">210</field>
</record>
</odoo>

1144
i18n/de.po Normal file

File diff suppressed because it is too large Load Diff

6
models/__init__.py Normal file
View File

@@ -0,0 +1,6 @@
from . import mvd_tcg_game
from . import mvd_tcg_card
from . import mvd_tcg_mtg_card_face
from . import mvd_tcg_mtg_card_legality
from . import mvd_tcg_mtg_set
from . import mvd_tcg_mtg_taxonomy

499
models/mvd_tcg_card.py Normal file
View File

@@ -0,0 +1,499 @@
"""Magic: The Gathering card extensions for the TCG suite."""
import re
from odoo import api, fields, models
COLLECTOR_NUMBER_PATTERN = re.compile(r"^(\d+)(.*)$")
class MvdTcgCard(models.Model):
"""Extend neutral card references with MTG-specific metadata."""
_inherit = "mvd.tcg.card"
is_mtg_game = fields.Boolean(compute="_compute_is_mtg_game")
mtg_set_id = fields.Many2one(
"mvd.tcg.mtg.set",
string="Set",
index=True,
ondelete="restrict",
)
mtg_set_code = fields.Char(
related="mtg_set_id.code",
string="Set Code",
store=True,
index=True,
)
mtg_rarity_id = fields.Many2one(
"mvd.tcg.mtg.rarity",
string="Rarity",
index=True,
ondelete="restrict",
)
mtg_rarity_code = fields.Char(
related="mtg_rarity_id.code",
string="Rarity Code",
store=True,
index=True,
)
mtg_collector_number = fields.Char(string="Collector Number", index=True)
mtg_collector_sort_key = fields.Char(
string="Collector Sort Key",
compute="_compute_mtg_collector_sort_key",
store=True,
index=True,
)
mtg_oracle_id = fields.Char(string="Oracle ID", index=True)
mtg_layout = fields.Char(string="Layout", index=True)
mtg_mana_cost = fields.Char(string="Mana Cost")
mtg_mana_value = fields.Float(string="Mana Value")
mtg_type_line = fields.Char(string="Type Line", translate=True)
mtg_oracle_text = fields.Text(string="Oracle Text", translate=True)
mtg_flavor_text = fields.Text(string="Flavor Text", translate=True)
mtg_color_ids = fields.Many2many(
"mvd.tcg.mtg.color",
"mvd_tcg_card_mtg_color_rel",
"card_id",
"color_id",
string="Colors",
)
mtg_color_identity_ids = fields.Many2many(
"mvd.tcg.mtg.color",
"mvd_tcg_card_mtg_color_identity_rel",
"card_id",
"color_id",
string="Color Identity",
)
mtg_color_identity_signature = fields.Char(
string="Color Identity Signature",
compute="_compute_mtg_color_identity_signature",
store=True,
index=True,
help="Ordered MTG color identity signature, for example W, UB or WUBRG.",
)
mtg_card_type_ids = fields.Many2many(
"mvd.tcg.mtg.card.type",
"mvd_tcg_card_mtg_card_type_rel",
"card_id",
"type_id",
string="Card Types",
)
mtg_keyword_ids = fields.Many2many(
"mvd.tcg.mtg.keyword",
"mvd_tcg_card_mtg_keyword_rel",
"card_id",
"keyword_id",
string="Keywords",
)
mtg_game_platform_ids = fields.Many2many(
"mvd.tcg.mtg.platform",
"mvd_tcg_card_mtg_platform_rel",
"card_id",
"platform_id",
string="Games",
)
mtg_finish_ids = fields.Many2many(
"mvd.tcg.mtg.finish",
"mvd_tcg_card_mtg_finish_rel",
"card_id",
"finish_id",
string="Finishes",
)
mtg_power = fields.Char(string="Power")
mtg_toughness = fields.Char(string="Toughness")
mtg_loyalty = fields.Char(string="Loyalty")
mtg_artist = fields.Char(string="Artist", index="trigram")
mtg_color_signature = fields.Char(
string="Color Signature",
compute="_compute_mtg_color_signature",
store=True,
index=True,
help="Ordered MTG color signature, for example W, UB or WUBRG.",
)
mtg_face_ids = fields.One2many("mvd.tcg.mtg.card.face", "card_id", string="Faces")
mtg_face_count = fields.Integer(
string="Face Count",
compute="_compute_mtg_face_count",
store=True,
)
mtg_legality_ids = fields.One2many(
"mvd.tcg.mtg.card.legality",
"card_id",
string="Legalities",
)
mtg_is_token = fields.Boolean(string="Token")
mtg_is_reprint = fields.Boolean(string="Reprint")
mtg_is_promo = fields.Boolean(string="Promo")
mtg_is_digital = fields.Boolean(string="Digital")
mtg_is_full_art = fields.Boolean(string="Full Art")
mtg_is_textless = fields.Boolean(string="Textless")
mtg_set_released_on = fields.Date(
related="mtg_set_id.released_on",
string="Released On",
store=True,
index=True,
)
mtg_set_type = fields.Char(
related="mtg_set_id.set_type",
string="Set Type",
store=True,
index=True,
)
_mtg_set_collector_unique = models.Constraint(
"UNIQUE (mtg_set_id, mtg_collector_number)",
"The collector number must be unique inside an MTG set.",
)
@api.depends("game_id.code")
def _compute_is_mtg_game(self):
"""Flag whether the current card belongs to the MTG adapter."""
for card in self:
card.is_mtg_game = card.game_id.code == "mtg"
@api.depends("mtg_collector_number")
def _compute_mtg_collector_sort_key(self):
"""Build a stable natural-sort key for MTG collector numbers.
Returns:
None: The compute updates records in place.
"""
for card in self:
collector_number = (card.mtg_collector_number or "").strip().lower()
if not collector_number:
card.mtg_collector_sort_key = "99999999:"
continue
match = COLLECTOR_NUMBER_PATTERN.match(collector_number)
if match:
numeric_part, suffix = match.groups()
card.mtg_collector_sort_key = (
f"{int(numeric_part):08d}:{suffix.strip()}"
)
continue
card.mtg_collector_sort_key = f"99999999:{collector_number}"
@api.depends("mtg_color_ids", "mtg_color_ids.sequence", "mtg_color_ids.code")
def _compute_mtg_color_signature(self):
"""Build the canonical MTG color signature from selected colors.
Returns:
None: The compute updates records in place.
"""
for card in self:
colors = card.mtg_color_ids.sorted(
key=lambda color: (color.sequence, color.code or "", color.id)
)
card.mtg_color_signature = "".join(
(color.code or "").strip().upper() for color in colors
) or False
@api.depends(
"mtg_color_identity_ids",
"mtg_color_identity_ids.sequence",
"mtg_color_identity_ids.code",
)
def _compute_mtg_color_identity_signature(self):
"""Build the canonical MTG color identity signature.
Returns:
None: The compute updates records in place.
"""
for card in self:
colors = card.mtg_color_identity_ids.sorted(
key=lambda color: (color.sequence, color.code or "", color.id)
)
card.mtg_color_identity_signature = "".join(
(color.code or "").strip().upper() for color in colors
) or False
@api.depends("mtg_face_ids")
def _compute_mtg_face_count(self):
"""Compute how many explicit faces are linked to each card.
Returns:
None: The compute updates records in place.
"""
face_data = self.env["mvd.tcg.mtg.card.face"]._read_group(
[("card_id", "in", self.ids)],
["card_id"],
["__count"],
)
counts_by_card = {record.id: count for record, count in face_data}
for card in self:
card.mtg_face_count = counts_by_card.get(card.id, 0)
def action_open_mtg_set(self):
"""Open the linked MTG set in form view.
Returns:
dict: Window action for the linked MTG set.
"""
self.ensure_one()
if not self.mtg_set_id:
return False
return {
"type": "ir.actions.act_window",
"name": self.mtg_set_id.display_name,
"res_model": "mvd.tcg.mtg.set",
"view_mode": "form",
"res_id": self.mtg_set_id.id,
"target": "current",
}
def mtg_get_rules_sections(self):
"""Return one face-aware MTG rules structure for the current card.
Returns:
list[dict[str, object]]: Ordered rules sections. Multi-face cards
return one section per face, while single-face cards return one
fallback section from the card header fields.
"""
self.ensure_one()
face_sections = []
for face in self.mtg_face_ids.sorted(lambda current_face: (current_face.sequence, current_face.id)):
section = {
"sequence": face.sequence,
"name": face.name or False,
"mana_cost": face.mana_cost or False,
"type_line": face.type_line or False,
"oracle_text": face.oracle_text or False,
"flavor_text": face.flavor_text or False,
"power": face.power or False,
"toughness": face.toughness or False,
"loyalty": face.loyalty or False,
}
if any(section.values()):
face_sections.append(section)
if face_sections:
return face_sections
return [
{
"sequence": 10,
"name": self.name or False,
"mana_cost": self.mtg_mana_cost or False,
"type_line": self.mtg_type_line or False,
"oracle_text": self.mtg_oracle_text or False,
"flavor_text": self.mtg_flavor_text or False,
"power": self.mtg_power or False,
"toughness": self.mtg_toughness or False,
"loyalty": self.mtg_loyalty or False,
}
]
def mtg_get_rules_summary(self):
"""Return one analysis-friendly MTG rules summary.
Returns:
str | bool: Single-face cards return their oracle text directly.
Multi-face cards return a combined face-aware rules summary.
"""
self.ensure_one()
rule_sections = self.mtg_get_rules_sections()
if len(rule_sections) <= 1:
return rule_sections and rule_sections[0]["oracle_text"] or False
rendered_sections = []
for section in rule_sections:
section_lines = []
heading_parts = [section["name"]]
if section["mana_cost"]:
heading_parts.append(section["mana_cost"])
heading = " ".join(part for part in heading_parts if part)
if heading:
section_lines.append(heading)
if section["type_line"]:
section_lines.append(section["type_line"])
if section["oracle_text"]:
section_lines.append(section["oracle_text"])
if section_lines:
rendered_sections.append("\n".join(section_lines))
return "\n\n//\n\n".join(rendered_sections) or False
def mtg_get_singleton_key_aliases(self):
"""Return stable MTG identity aliases across printings and styles.
Returns:
tuple[str, ...]: Ordered identity aliases. Cards with a missing
Oracle ID still expose rules-based fallbacks that can match
styled variants of the same card.
"""
self.ensure_one()
english_card = self.with_context(lang="en_US")
aliases = []
oracle_id = (english_card.mtg_oracle_id or "").strip()
if oracle_id:
aliases.append(f"oracle:{oracle_id}")
rules_summary = re.sub(
r"\s+",
" ",
(english_card.mtg_get_rules_summary() or "").strip(),
).strip()
if rules_summary:
aliases.append(f"rules:{rules_summary}")
deduplicated_oracle_chunks = []
for section in english_card.mtg_get_rules_sections():
oracle_text = re.sub(
r"\s+",
" ",
(section["oracle_text"] or "").strip(),
).strip()
if oracle_text and oracle_text not in deduplicated_oracle_chunks:
deduplicated_oracle_chunks.append(oracle_text)
if deduplicated_oracle_chunks:
aliases.append(f"oracletext:{' // '.join(deduplicated_oracle_chunks)}")
face_names = " // ".join(
section["name"]
for section in english_card.mtg_get_rules_sections()
if section["name"]
)
fallback_name = face_names or (english_card.name or "").strip()
fallback_type = (english_card.mtg_type_line or "").strip()
fallback_cost = (english_card.mtg_mana_cost or "").strip()
if any((fallback_name, fallback_type, fallback_cost)):
aliases.append(f"fallback:{fallback_name}|{fallback_type}|{fallback_cost}")
deduplicated_aliases = []
for alias in aliases:
if alias and alias not in deduplicated_aliases:
deduplicated_aliases.append(alias)
return tuple(deduplicated_aliases)
def mtg_get_singleton_key(self):
"""Return the primary MTG singleton key for the current card.
Returns:
str | bool: First stable singleton alias, if any.
"""
self.ensure_one()
aliases = self.mtg_get_singleton_key_aliases()
return aliases[0] if aliases else False
def mtg_allows_unlimited_copies(self):
"""Return whether one MTG card bypasses singleton restrictions.
Returns:
bool: ``True`` for basic lands and cards with explicit unlimited-
copy rules text.
"""
self.ensure_one()
english_card = self.with_context(lang="en_US")
combined_type_line = " ".join(
section["type_line"]
for section in english_card.mtg_get_rules_sections()
if section["type_line"]
).lower()
rules_summary = (english_card.mtg_get_rules_summary() or "").lower()
is_basic_land = "land" in combined_type_line and "basic" in combined_type_line
allows_unlimited_copies = (
"a deck can have any number of cards named" in rules_summary
)
return is_basic_land or allows_unlimited_copies
@api.model
def _mtg_exact_color_signature_from_ids(self, color_ids):
"""Build the canonical color signature for selected color records.
Args:
color_ids: MTG color record ids from a filter domain.
Returns:
str | bool: Canonical signature like ``WB`` or ``False`` when invalid.
"""
normalized_ids = [int(color_id) for color_id in color_ids or [] if color_id]
if not normalized_ids:
return False
colors = self.env["mvd.tcg.mtg.color"].browse(normalized_ids).exists().sorted(
key=lambda color: (color.sequence, color.code or "", color.id)
)
if len(colors) != len(set(normalized_ids)):
return False
return "".join((color.code or "").strip().upper() for color in colors)
@api.model
def _mtg_transform_exact_color_domain(self, domain):
"""Rewrite MTG color filters into exact color-signature filters.
Args:
domain: Original ORM domain.
Returns:
list: Domain with exact MTG color matching where applicable.
"""
domain_object = fields.Domain(domain or [])
selected_color_ids = []
for condition in domain_object.iter_conditions():
if condition.field_expr != "mtg_color_ids":
continue
if condition.operator == "=" and condition.value:
color_id = int(condition.value)
if color_id not in selected_color_ids:
selected_color_ids.append(color_id)
continue
if condition.operator == "in" and isinstance(condition.value, (list, tuple)):
for color_id in condition.value:
normalized_id = int(color_id)
if normalized_id not in selected_color_ids:
selected_color_ids.append(normalized_id)
if not selected_color_ids:
return domain
exact_signature = self._mtg_exact_color_signature_from_ids(selected_color_ids)
if not exact_signature:
return list(fields.Domain.FALSE)
remaining_domain = domain_object.map_conditions(
lambda condition: (
fields.Domain.TRUE
if condition.field_expr == "mtg_color_ids"
else fields.Domain(condition)
)
).optimize(self)
exact_domain = fields.Domain(
[("mtg_color_signature", "=", exact_signature)]
)
return list(fields.Domain.AND([remaining_domain, exact_domain]).optimize(self))
@api.model
def _search(
self,
domain,
offset=0,
limit=None,
order=None,
*,
active_test=True,
bypass_access=False,
):
"""Apply exact MTG color matching for the Magic card action.
Args:
domain: ORM domain for the current search.
offset: Search offset.
limit: Optional maximal number of records.
order: SQL order clause.
active_test: Whether active records should be filtered implicitly.
bypass_access: Whether access rules should be bypassed.
Returns:
Query: Matching search query.
"""
if self.env.context.get("mvd_mtg_exact_color_filter"):
domain = self._mtg_transform_exact_color_domain(domain)
return super()._search(
domain,
offset=offset,
limit=limit,
order=order,
active_test=active_test,
bypass_access=bypass_access,
)

14
models/mvd_tcg_game.py Normal file
View File

@@ -0,0 +1,14 @@
"""MTG-specific helpers on the neutral game model."""
from odoo import api, models
class MvdTcgGame(models.Model):
"""Provide stable access to the seeded MTG game record."""
_inherit = "mvd.tcg.game"
@api.model
def _mvd_tcg_get_mtg_game(self):
"""Return the seeded MTG game record."""
return self.env.ref("mvd_tcg_mtg.mvd_tcg_game_mtg")

View File

@@ -0,0 +1,33 @@
"""Magic: The Gathering face-level reference models."""
from odoo import fields, models
class MvdTcgMtgCardFace(models.Model):
"""Represent one ordered printed face of an MTG card reference."""
_name = "mvd.tcg.mtg.card.face"
_description = "MTG Card Face"
_order = "card_id, sequence, id"
card_id = fields.Many2one(
"mvd.tcg.card",
required=True,
index=True,
ondelete="cascade",
)
sequence = fields.Integer(default=10, index=True)
name = fields.Char(required=True, translate=True, index="trigram")
mana_cost = fields.Char()
type_line = fields.Char(translate=True)
oracle_text = fields.Text(translate=True)
flavor_text = fields.Text(translate=True)
power = fields.Char()
toughness = fields.Char()
loyalty = fields.Char()
artist = fields.Char(index="trigram")
_card_sequence_unique = models.Constraint(
"UNIQUE (card_id, sequence)",
"The MTG face sequence must be unique per card.",
)

View File

@@ -0,0 +1,52 @@
"""Magic: The Gathering legality models."""
from odoo import fields, models
LEGALITY_SELECTION = [
("legal", "Legal"),
("not_legal", "Not Legal"),
("restricted", "Restricted"),
("banned", "Banned"),
]
class MvdTcgMtgCardLegality(models.Model):
"""Store one legality status per MTG card and constructed format."""
_name = "mvd.tcg.mtg.card.legality"
_description = "MTG Card Legality"
_order = "format_sequence, id"
card_id = fields.Many2one(
"mvd.tcg.card",
required=True,
index=True,
ondelete="cascade",
)
format_id = fields.Many2one(
"mvd.tcg.mtg.format",
required=True,
index=True,
ondelete="restrict",
)
format_code = fields.Char(
related="format_id.code",
store=True,
index=True,
)
format_sequence = fields.Integer(
related="format_id.sequence",
store=True,
index=True,
)
status = fields.Selection(
selection=LEGALITY_SELECTION,
required=True,
default="not_legal",
index=True,
)
_card_format_unique = models.Constraint(
"UNIQUE (card_id, format_id)",
"The MTG legality format must be unique per card.",
)

72
models/mvd_tcg_mtg_set.py Normal file
View File

@@ -0,0 +1,72 @@
"""Magic: The Gathering set models for the TCG suite."""
from odoo import api, fields, models
class MvdTcgMtgSet(models.Model):
"""Represent a Magic: The Gathering set within the MTG adapter."""
_name = "mvd.tcg.mtg.set"
_description = "MTG Set"
_order = "released_on desc, code, id"
name = fields.Char(required=True, translate=True, index="trigram")
code = fields.Char(required=True, index=True)
active = fields.Boolean(default=True)
sequence = fields.Integer(default=10)
game_id = fields.Many2one(
"mvd.tcg.game",
required=True,
index=True,
default=lambda self: self._default_game_id(),
ondelete="restrict",
)
released_on = fields.Date(index=True)
set_type = fields.Char(index=True)
official_card_count = fields.Integer()
icon_svg_uri = fields.Char()
note = fields.Text(translate=True)
mtg_card_ids = fields.One2many(
"mvd.tcg.card",
"mtg_set_id",
string="Cards",
)
mtg_card_count = fields.Integer(
string="Card Count",
compute="_compute_mtg_card_count",
)
_code_unique = models.Constraint(
"UNIQUE (game_id, code)",
"The MTG set code must be unique per game.",
)
@api.model
def _default_game_id(self):
"""Return the seeded MTG game record."""
return self.env["mvd.tcg.game"]._mvd_tcg_get_mtg_game().id
@api.depends("mtg_card_ids")
def _compute_mtg_card_count(self):
"""Compute how many MTG cards are currently linked to each set."""
card_data = self.env["mvd.tcg.card"]._read_group(
[("mtg_set_id", "in", self.ids)],
["mtg_set_id"],
["__count"],
)
counts_by_set = {record.id: count for record, count in card_data}
for mtg_set in self:
mtg_set.mtg_card_count = counts_by_set.get(mtg_set.id, 0)
def action_open_cards(self):
"""Open the MTG cards catalog filtered on the selected set."""
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id(
"mvd_tcg_mtg.mvd_tcg_mtg_card_action"
)
action["domain"] = [("mtg_set_id", "=", self.id)]
action["context"] = {
"default_game_id": self.game_id.id,
"default_mtg_set_id": self.id,
}
return action

View File

@@ -0,0 +1,230 @@
"""Magic: The Gathering taxonomy models for cards and faceting."""
import re
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class MvdTcgMtgTaxonomyMixin(models.AbstractModel):
"""Share technical code handling across MTG taxonomy models."""
_name = "mvd.tcg.mtg.taxonomy.mixin"
_description = "MTG Taxonomy Technical Code Mixin"
@api.model
def _mvd_tcg_generate_code(self, name):
"""Return a slug-like technical code for one taxonomy name."""
return re.sub(r"[^a-z0-9]+", "_", (name or "").strip().lower()).strip("_") or "item"
@api.model
def _mvd_tcg_get_unique_code(self, name):
"""Return a unique technical code for one taxonomy record name."""
base_code = self._mvd_tcg_generate_code(name)
existing_codes = set(self.search([]).mapped("code"))
if base_code not in existing_codes:
return base_code
suffix = 2
while f"{base_code}_{suffix}" in existing_codes:
suffix += 1
return f"{base_code}_{suffix}"
def _mvd_tcg_can_edit_code(self):
"""Return whether the current user may edit taxonomy codes."""
return self.env.is_superuser() or any(
self.env.user.has_group(xmlid)
for xmlid in (
"mvd_tcg_base.mvd_tcg_base_group_administrator",
"base.group_system",
)
)
@api.model_create_multi
def create(self, vals_list):
"""Generate missing technical codes for manually created taxonomy records."""
prepared_vals_list = []
for vals in vals_list:
prepared_vals = dict(vals)
if not prepared_vals.get("code"):
prepared_vals["code"] = self._mvd_tcg_get_unique_code(
prepared_vals.get("name")
)
prepared_vals_list.append(prepared_vals)
return super().create(prepared_vals_list)
def write(self, vals):
"""Protect taxonomy codes from normal business edits."""
if "code" in vals and not self.env.context.get("mvd_tcg_bypass_taxonomy_code_write"):
if not self._mvd_tcg_can_edit_code():
raise UserError(
_(
"MTG taxonomy codes are technical identifiers and can only be "
"changed by TCG administrators."
)
)
return super().write(vals)
class MvdTcgMtgRarity(models.Model):
"""Store normalized MTG rarity records."""
_name = "mvd.tcg.mtg.rarity"
_inherit = "mvd.tcg.mtg.taxonomy.mixin"
_description = "MTG Rarity"
_order = "sequence, name, id"
name = fields.Char(required=True, translate=True, index="trigram")
code = fields.Char(required=True, index=True)
active = fields.Boolean(default=True)
sequence = fields.Integer(default=10)
card_ids = fields.One2many("mvd.tcg.card", "mtg_rarity_id")
_code_unique = models.Constraint(
"UNIQUE (code)",
"The MTG rarity code must be unique.",
)
class MvdTcgMtgColor(models.Model):
"""Store normalized MTG colors for faceting and card metadata."""
_name = "mvd.tcg.mtg.color"
_inherit = "mvd.tcg.mtg.taxonomy.mixin"
_description = "MTG Color"
_order = "sequence, name, id"
name = fields.Char(required=True, translate=True, index="trigram")
code = fields.Char(required=True, index=True)
active = fields.Boolean(default=True)
sequence = fields.Integer(default=10)
card_ids = fields.Many2many(
"mvd.tcg.card",
"mvd_tcg_card_mtg_color_rel",
"color_id",
"card_id",
)
_code_unique = models.Constraint(
"UNIQUE (code)",
"The MTG color code must be unique.",
)
class MvdTcgMtgCardType(models.Model):
"""Store normalized MTG card types for faceting and grouping."""
_name = "mvd.tcg.mtg.card.type"
_inherit = "mvd.tcg.mtg.taxonomy.mixin"
_description = "MTG Card Type"
_order = "sequence, name, id"
name = fields.Char(required=True, translate=True, index="trigram")
code = fields.Char(required=True, index=True)
active = fields.Boolean(default=True)
sequence = fields.Integer(default=10)
card_ids = fields.Many2many(
"mvd.tcg.card",
"mvd_tcg_card_mtg_card_type_rel",
"type_id",
"card_id",
)
_code_unique = models.Constraint(
"UNIQUE (code)",
"The MTG card type code must be unique.",
)
class MvdTcgMtgKeyword(models.Model):
"""Store normalized MTG keywords imported from Scryfall."""
_name = "mvd.tcg.mtg.keyword"
_inherit = "mvd.tcg.mtg.taxonomy.mixin"
_description = "MTG Keyword"
_order = "sequence, name, id"
name = fields.Char(required=True, translate=True, index="trigram")
code = fields.Char(required=True, index=True)
active = fields.Boolean(default=True)
sequence = fields.Integer(default=10)
card_ids = fields.Many2many(
"mvd.tcg.card",
"mvd_tcg_card_mtg_keyword_rel",
"keyword_id",
"card_id",
)
_code_unique = models.Constraint(
"UNIQUE (code)",
"The MTG keyword code must be unique.",
)
class MvdTcgMtgFormat(models.Model):
"""Store normalized MTG formats for legality information."""
_name = "mvd.tcg.mtg.format"
_inherit = "mvd.tcg.mtg.taxonomy.mixin"
_description = "MTG Format"
_order = "sequence, name, id"
name = fields.Char(required=True, translate=True, index="trigram")
code = fields.Char(required=True, index=True)
active = fields.Boolean(default=True)
sequence = fields.Integer(default=10)
legality_ids = fields.One2many("mvd.tcg.mtg.card.legality", "format_id")
_code_unique = models.Constraint(
"UNIQUE (code)",
"The MTG format code must be unique.",
)
class MvdTcgMtgFinish(models.Model):
"""Store normalized MTG finishes such as foil and etched."""
_name = "mvd.tcg.mtg.finish"
_inherit = "mvd.tcg.mtg.taxonomy.mixin"
_description = "MTG Finish"
_order = "sequence, name, id"
name = fields.Char(required=True, translate=True, index="trigram")
code = fields.Char(required=True, index=True)
active = fields.Boolean(default=True)
sequence = fields.Integer(default=10)
card_ids = fields.Many2many(
"mvd.tcg.card",
"mvd_tcg_card_mtg_finish_rel",
"finish_id",
"card_id",
)
_code_unique = models.Constraint(
"UNIQUE (code)",
"The MTG finish code must be unique.",
)
class MvdTcgMtgPlatform(models.Model):
"""Store normalized MTG game platforms such as paper and Arena."""
_name = "mvd.tcg.mtg.platform"
_inherit = "mvd.tcg.mtg.taxonomy.mixin"
_description = "MTG Platform"
_order = "sequence, name, id"
name = fields.Char(required=True, translate=True, index="trigram")
code = fields.Char(required=True, index=True)
active = fields.Boolean(default=True)
sequence = fields.Integer(default=10)
card_ids = fields.Many2many(
"mvd.tcg.card",
"mvd_tcg_card_mtg_platform_rel",
"platform_id",
"card_id",
)
_code_unique = models.Constraint(
"UNIQUE (code)",
"The MTG platform code must be unique.",
)

View File

@@ -0,0 +1,39 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mvd_tcg_mtg_set_user,mvd.tcg.mtg.set.user,model_mvd_tcg_mtg_set,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
access_mvd_tcg_mtg_set_operator,mvd.tcg.mtg.set.operator,model_mvd_tcg_mtg_set,mvd_tcg_base.mvd_tcg_base_group_operator,1,0,0,0
access_mvd_tcg_mtg_set_manager,mvd.tcg.mtg.set.manager,model_mvd_tcg_mtg_set,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
access_mvd_tcg_mtg_set_system,mvd.tcg.mtg.set.system,model_mvd_tcg_mtg_set,base.group_system,1,1,1,1
access_mvd_tcg_mtg_rarity_user,mvd.tcg.mtg.rarity.user,model_mvd_tcg_mtg_rarity,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
access_mvd_tcg_mtg_rarity_operator,mvd.tcg.mtg.rarity.operator,model_mvd_tcg_mtg_rarity,mvd_tcg_base.mvd_tcg_base_group_operator,1,0,0,0
access_mvd_tcg_mtg_rarity_manager,mvd.tcg.mtg.rarity.manager,model_mvd_tcg_mtg_rarity,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
access_mvd_tcg_mtg_rarity_system,mvd.tcg.mtg.rarity.system,model_mvd_tcg_mtg_rarity,base.group_system,1,1,1,1
access_mvd_tcg_mtg_color_user,mvd.tcg.mtg.color.user,model_mvd_tcg_mtg_color,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
access_mvd_tcg_mtg_color_operator,mvd.tcg.mtg.color.operator,model_mvd_tcg_mtg_color,mvd_tcg_base.mvd_tcg_base_group_operator,1,0,0,0
access_mvd_tcg_mtg_color_manager,mvd.tcg.mtg.color.manager,model_mvd_tcg_mtg_color,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
access_mvd_tcg_mtg_color_system,mvd.tcg.mtg.color.system,model_mvd_tcg_mtg_color,base.group_system,1,1,1,1
access_mvd_tcg_mtg_card_type_user,mvd.tcg.mtg.card.type.user,model_mvd_tcg_mtg_card_type,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
access_mvd_tcg_mtg_card_type_operator,mvd.tcg.mtg.card.type.operator,model_mvd_tcg_mtg_card_type,mvd_tcg_base.mvd_tcg_base_group_operator,1,0,0,0
access_mvd_tcg_mtg_card_type_manager,mvd.tcg.mtg.card.type.manager,model_mvd_tcg_mtg_card_type,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
access_mvd_tcg_mtg_card_type_system,mvd.tcg.mtg.card.type.system,model_mvd_tcg_mtg_card_type,base.group_system,1,1,1,1
access_mvd_tcg_mtg_keyword_user,mvd.tcg.mtg.keyword.user,model_mvd_tcg_mtg_keyword,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
access_mvd_tcg_mtg_keyword_operator,mvd.tcg.mtg.keyword.operator,model_mvd_tcg_mtg_keyword,mvd_tcg_base.mvd_tcg_base_group_operator,1,0,0,0
access_mvd_tcg_mtg_keyword_manager,mvd.tcg.mtg.keyword.manager,model_mvd_tcg_mtg_keyword,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
access_mvd_tcg_mtg_keyword_system,mvd.tcg.mtg.keyword.system,model_mvd_tcg_mtg_keyword,base.group_system,1,1,1,1
access_mvd_tcg_mtg_format_user,mvd.tcg.mtg.format.user,model_mvd_tcg_mtg_format,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
access_mvd_tcg_mtg_format_operator,mvd.tcg.mtg.format.operator,model_mvd_tcg_mtg_format,mvd_tcg_base.mvd_tcg_base_group_operator,1,0,0,0
access_mvd_tcg_mtg_format_manager,mvd.tcg.mtg.format.manager,model_mvd_tcg_mtg_format,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
access_mvd_tcg_mtg_format_system,mvd.tcg.mtg.format.system,model_mvd_tcg_mtg_format,base.group_system,1,1,1,1
access_mvd_tcg_mtg_finish_user,mvd.tcg.mtg.finish.user,model_mvd_tcg_mtg_finish,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
access_mvd_tcg_mtg_finish_operator,mvd.tcg.mtg.finish.operator,model_mvd_tcg_mtg_finish,mvd_tcg_base.mvd_tcg_base_group_operator,1,0,0,0
access_mvd_tcg_mtg_finish_manager,mvd.tcg.mtg.finish.manager,model_mvd_tcg_mtg_finish,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
access_mvd_tcg_mtg_finish_system,mvd.tcg.mtg.finish.system,model_mvd_tcg_mtg_finish,base.group_system,1,1,1,1
access_mvd_tcg_mtg_platform_user,mvd.tcg.mtg.platform.user,model_mvd_tcg_mtg_platform,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
access_mvd_tcg_mtg_platform_operator,mvd.tcg.mtg.platform.operator,model_mvd_tcg_mtg_platform,mvd_tcg_base.mvd_tcg_base_group_operator,1,0,0,0
access_mvd_tcg_mtg_platform_manager,mvd.tcg.mtg.platform.manager,model_mvd_tcg_mtg_platform,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
access_mvd_tcg_mtg_platform_system,mvd.tcg.mtg.platform.system,model_mvd_tcg_mtg_platform,base.group_system,1,1,1,1
access_mvd_tcg_mtg_card_face_user,mvd.tcg.mtg.card.face.user,model_mvd_tcg_mtg_card_face,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
access_mvd_tcg_mtg_card_face_manager,mvd.tcg.mtg.card.face.manager,model_mvd_tcg_mtg_card_face,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
access_mvd_tcg_mtg_card_face_system,mvd.tcg.mtg.card.face.system,model_mvd_tcg_mtg_card_face,base.group_system,1,1,1,1
access_mvd_tcg_mtg_card_legality_user,mvd.tcg.mtg.card.legality.user,model_mvd_tcg_mtg_card_legality,mvd_tcg_base.mvd_tcg_base_group_user,1,0,0,0
access_mvd_tcg_mtg_card_legality_manager,mvd.tcg.mtg.card.legality.manager,model_mvd_tcg_mtg_card_legality,mvd_tcg_base.mvd_tcg_base_group_manager,1,1,1,1
access_mvd_tcg_mtg_card_legality_system,mvd.tcg.mtg.card.legality.system,model_mvd_tcg_mtg_card_legality,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mvd_tcg_mtg_set_user mvd.tcg.mtg.set.user model_mvd_tcg_mtg_set mvd_tcg_base.mvd_tcg_base_group_user 1 0 0 0
3 access_mvd_tcg_mtg_set_operator mvd.tcg.mtg.set.operator model_mvd_tcg_mtg_set mvd_tcg_base.mvd_tcg_base_group_operator 1 0 0 0
4 access_mvd_tcg_mtg_set_manager mvd.tcg.mtg.set.manager model_mvd_tcg_mtg_set mvd_tcg_base.mvd_tcg_base_group_manager 1 1 1 1
5 access_mvd_tcg_mtg_set_system mvd.tcg.mtg.set.system model_mvd_tcg_mtg_set base.group_system 1 1 1 1
6 access_mvd_tcg_mtg_rarity_user mvd.tcg.mtg.rarity.user model_mvd_tcg_mtg_rarity mvd_tcg_base.mvd_tcg_base_group_user 1 0 0 0
7 access_mvd_tcg_mtg_rarity_operator mvd.tcg.mtg.rarity.operator model_mvd_tcg_mtg_rarity mvd_tcg_base.mvd_tcg_base_group_operator 1 0 0 0
8 access_mvd_tcg_mtg_rarity_manager mvd.tcg.mtg.rarity.manager model_mvd_tcg_mtg_rarity mvd_tcg_base.mvd_tcg_base_group_manager 1 1 1 1
9 access_mvd_tcg_mtg_rarity_system mvd.tcg.mtg.rarity.system model_mvd_tcg_mtg_rarity base.group_system 1 1 1 1
10 access_mvd_tcg_mtg_color_user mvd.tcg.mtg.color.user model_mvd_tcg_mtg_color mvd_tcg_base.mvd_tcg_base_group_user 1 0 0 0
11 access_mvd_tcg_mtg_color_operator mvd.tcg.mtg.color.operator model_mvd_tcg_mtg_color mvd_tcg_base.mvd_tcg_base_group_operator 1 0 0 0
12 access_mvd_tcg_mtg_color_manager mvd.tcg.mtg.color.manager model_mvd_tcg_mtg_color mvd_tcg_base.mvd_tcg_base_group_manager 1 1 1 1
13 access_mvd_tcg_mtg_color_system mvd.tcg.mtg.color.system model_mvd_tcg_mtg_color base.group_system 1 1 1 1
14 access_mvd_tcg_mtg_card_type_user mvd.tcg.mtg.card.type.user model_mvd_tcg_mtg_card_type mvd_tcg_base.mvd_tcg_base_group_user 1 0 0 0
15 access_mvd_tcg_mtg_card_type_operator mvd.tcg.mtg.card.type.operator model_mvd_tcg_mtg_card_type mvd_tcg_base.mvd_tcg_base_group_operator 1 0 0 0
16 access_mvd_tcg_mtg_card_type_manager mvd.tcg.mtg.card.type.manager model_mvd_tcg_mtg_card_type mvd_tcg_base.mvd_tcg_base_group_manager 1 1 1 1
17 access_mvd_tcg_mtg_card_type_system mvd.tcg.mtg.card.type.system model_mvd_tcg_mtg_card_type base.group_system 1 1 1 1
18 access_mvd_tcg_mtg_keyword_user mvd.tcg.mtg.keyword.user model_mvd_tcg_mtg_keyword mvd_tcg_base.mvd_tcg_base_group_user 1 0 0 0
19 access_mvd_tcg_mtg_keyword_operator mvd.tcg.mtg.keyword.operator model_mvd_tcg_mtg_keyword mvd_tcg_base.mvd_tcg_base_group_operator 1 0 0 0
20 access_mvd_tcg_mtg_keyword_manager mvd.tcg.mtg.keyword.manager model_mvd_tcg_mtg_keyword mvd_tcg_base.mvd_tcg_base_group_manager 1 1 1 1
21 access_mvd_tcg_mtg_keyword_system mvd.tcg.mtg.keyword.system model_mvd_tcg_mtg_keyword base.group_system 1 1 1 1
22 access_mvd_tcg_mtg_format_user mvd.tcg.mtg.format.user model_mvd_tcg_mtg_format mvd_tcg_base.mvd_tcg_base_group_user 1 0 0 0
23 access_mvd_tcg_mtg_format_operator mvd.tcg.mtg.format.operator model_mvd_tcg_mtg_format mvd_tcg_base.mvd_tcg_base_group_operator 1 0 0 0
24 access_mvd_tcg_mtg_format_manager mvd.tcg.mtg.format.manager model_mvd_tcg_mtg_format mvd_tcg_base.mvd_tcg_base_group_manager 1 1 1 1
25 access_mvd_tcg_mtg_format_system mvd.tcg.mtg.format.system model_mvd_tcg_mtg_format base.group_system 1 1 1 1
26 access_mvd_tcg_mtg_finish_user mvd.tcg.mtg.finish.user model_mvd_tcg_mtg_finish mvd_tcg_base.mvd_tcg_base_group_user 1 0 0 0
27 access_mvd_tcg_mtg_finish_operator mvd.tcg.mtg.finish.operator model_mvd_tcg_mtg_finish mvd_tcg_base.mvd_tcg_base_group_operator 1 0 0 0
28 access_mvd_tcg_mtg_finish_manager mvd.tcg.mtg.finish.manager model_mvd_tcg_mtg_finish mvd_tcg_base.mvd_tcg_base_group_manager 1 1 1 1
29 access_mvd_tcg_mtg_finish_system mvd.tcg.mtg.finish.system model_mvd_tcg_mtg_finish base.group_system 1 1 1 1
30 access_mvd_tcg_mtg_platform_user mvd.tcg.mtg.platform.user model_mvd_tcg_mtg_platform mvd_tcg_base.mvd_tcg_base_group_user 1 0 0 0
31 access_mvd_tcg_mtg_platform_operator mvd.tcg.mtg.platform.operator model_mvd_tcg_mtg_platform mvd_tcg_base.mvd_tcg_base_group_operator 1 0 0 0
32 access_mvd_tcg_mtg_platform_manager mvd.tcg.mtg.platform.manager model_mvd_tcg_mtg_platform mvd_tcg_base.mvd_tcg_base_group_manager 1 1 1 1
33 access_mvd_tcg_mtg_platform_system mvd.tcg.mtg.platform.system model_mvd_tcg_mtg_platform base.group_system 1 1 1 1
34 access_mvd_tcg_mtg_card_face_user mvd.tcg.mtg.card.face.user model_mvd_tcg_mtg_card_face mvd_tcg_base.mvd_tcg_base_group_user 1 0 0 0
35 access_mvd_tcg_mtg_card_face_manager mvd.tcg.mtg.card.face.manager model_mvd_tcg_mtg_card_face mvd_tcg_base.mvd_tcg_base_group_manager 1 1 1 1
36 access_mvd_tcg_mtg_card_face_system mvd.tcg.mtg.card.face.system model_mvd_tcg_mtg_card_face base.group_system 1 1 1 1
37 access_mvd_tcg_mtg_card_legality_user mvd.tcg.mtg.card.legality.user model_mvd_tcg_mtg_card_legality mvd_tcg_base.mvd_tcg_base_group_user 1 0 0 0
38 access_mvd_tcg_mtg_card_legality_manager mvd.tcg.mtg.card.legality.manager model_mvd_tcg_mtg_card_legality mvd_tcg_base.mvd_tcg_base_group_manager 1 1 1 1
39 access_mvd_tcg_mtg_card_legality_system mvd.tcg.mtg.card.legality.system model_mvd_tcg_mtg_card_legality base.group_system 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -0,0 +1 @@
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><g fill='none'><circle fill='#CAC5C0' cx='50' cy='50' r='50'/><path d='M23 52.428c0-9.787 2.061-18.813 6.191-27.072 5.117-10.236 12.123-15.355 21.012-15.355 8.797 0 15.666 4.359 20.605 13.064 4.127 7.186 6.191 15.537 6.191 25.051 0 9.881-2.064 18.814-6.191 26.803-5.031 10.058-12.035 15.081-21.011 15.081-8.531 0-15.305-4.307-20.336-12.926-4.308-7.361-6.461-15.576-6.461-24.646zm11.309-4.444c0 12.926 1.93 22.984 5.795 30.168 2.691 5.025 6.146 7.541 10.367 7.541 10.146 0 15.221-10.506 15.221-31.518 0-9.244-.809-17.059-2.422-23.434-2.785-10.504-7.498-15.76-14.145-15.76-9.879 0-14.816 10.15-14.816 30.443v2.56z' fill='#0D0F0F'/></g></svg>

After

Width:  |  Height:  |  Size: 702 B

View File

@@ -0,0 +1 @@
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><g transform='translate(0 -1)' fill='none'><circle fill='#CAC5C0' cx='50' cy='50.998' r='50'/><path d='M55.685 11.001v64.108c0 7.671 3.226 11.504 9.687 11.504h1.684v4.388h-34.111v-4.388h2.141c6.247 0 9.369-3.833 9.369-11.504v-42.054c0-7.758-2.697-11.643-8.081-11.643h-3.429v-4.247h1.237c6.66 0 12.691-2.057 18.08-6.165l3.423.001z' fill='#0D0F0F'/></g></svg>

After

Width:  |  Height:  |  Size: 420 B

Some files were not shown because too many files have changed in this diff Show More