From f53e0a48675bdddc4e7396c238eb73ff314a7521 Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Sun, 13 Dec 2020 13:13:05 -0800 Subject: [PATCH] Adds page for restricted lists. --- assets/css/style.css | 48 +++++ src/Command/ImportRestrictionsCommand.php | 8 + src/Controller/DefaultController.php | 192 ++++++++++++++++-- src/Controller/RestrictionsController.php | 167 +++++++++++++++ src/Entity/Restriction.php | 72 +++++++ src/Entity/RestrictionInterface.php | 35 ++++ templates/Restrictions/cardlist.html.twig | 15 ++ templates/Restrictions/index.html.twig | 50 +++++ .../Restrictions/poddescription.html.twig | 9 + templates/Restrictions/podlist.html.twig | 29 +++ .../restricteddescription.html.twig | 8 + .../Restrictions/restrictedlist.html.twig | 47 +++++ translations/messages.en.yml | 1 + 13 files changed, 667 insertions(+), 14 deletions(-) create mode 100644 src/Controller/RestrictionsController.php create mode 100644 templates/Restrictions/cardlist.html.twig create mode 100644 templates/Restrictions/index.html.twig create mode 100644 templates/Restrictions/poddescription.html.twig create mode 100644 templates/Restrictions/podlist.html.twig create mode 100644 templates/Restrictions/restricteddescription.html.twig create mode 100644 templates/Restrictions/restrictedlist.html.twig diff --git a/assets/css/style.css b/assets/css/style.css index 9867e807..90532d7c 100755 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -532,3 +532,51 @@ abbr.legality.banned { #rules div.example { padding-left: 3em; } + +/**** Restricted Lists ****/ +.restrictions-container .active-restrictions { + border-left: 3px solid green; + padding: 1rem; +} + +.restrictions-container .inactive-restrictions { + border-left: 3px solid red; + padding: 1rem; + +} + +.restrictions-container .restricted-list { + border-bottom: 2px dotted grey; + padding-bottom: 2rem; +} + +.restrictions-container .restricted-list h2 a { + display: none; +} + +.restrictions-container .restricted-list h2:hover a { + display: inline; +} + +.restrictions-container .pod { + border: 1px solid grey; + border-radius: .5rem; + margin-bottom: 1rem; +} + +.restrictions-container .pod .pod-card { + padding: .5rem; +} + +.restrictions-container .pod .restricted-card { + font-weight: bold; +} + +.restrictions-container .pod .title { + background-color: lightblue; + border-bottom: 1px solid black; + font-weight: bold; + padding: .5rem; +} + + diff --git a/src/Command/ImportRestrictionsCommand.php b/src/Command/ImportRestrictionsCommand.php index 6e5e0fb5..e10f5b96 100644 --- a/src/Command/ImportRestrictionsCommand.php +++ b/src/Command/ImportRestrictionsCommand.php @@ -31,6 +31,10 @@ class ImportRestrictionsCommand extends Command const ISSUER_FFG_SHORTNAME = 'FFG'; + const FORMAT_JOUST = 'joust'; + + const FORMAT_MELEE = 'melee'; + protected array $faqIssuers = [ self::ISSUER_FFG, self::ISSUER_CONCLAVE, @@ -183,6 +187,10 @@ protected function buildContents(array $data): array $rhett[$name]['banned'] = $banned; if (array_key_exists('pods', $format)) { $rhett[$name]['restricted_pods'] = $format['pods']; + for ($i = 0, $n = count($rhett[$name]['restricted_pods']); $i < $n; $i++) { + $rhett[$name]['restricted_pods'][$i]['title'] + = (self::FORMAT_JOUST === $name ? 'P' : 'MP') . ($i + 1); + } } } diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php index 74a626e0..743d976f 100644 --- a/src/Controller/DefaultController.php +++ b/src/Controller/DefaultController.php @@ -2,11 +2,16 @@ namespace App\Controller; +use App\Entity\Card; +use App\Entity\CardInterface; use App\Entity\DecklistInterface; use App\Entity\Faction; +use App\Entity\Restriction; +use App\Entity\RestrictionInterface; use App\Entity\Type; use App\Entity\TypeInterface; use App\Services\AgendaHelper; +use App\Services\CardsData; use App\Services\DecklistManager; use Exception; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -70,11 +75,11 @@ public function indexAction( $counts = []; foreach ($countByType as $code => $qty) { $typeName = $typeNames[$code]; - $counts[] = $qty . " " . $typeName . "s"; + $counts[] = $qty." ".$typeName."s"; } $array['count_by_type'] = join(' • ', $counts); - $factions = [ $faction->getName() ]; + $factions = [$faction->getName()]; foreach ($decklist->getSlots()->getAgendas() as $agenda) { $minor_faction = $agendaHelper->getMinorFaction($agenda->getCard()); if ($minor_faction) { @@ -89,13 +94,17 @@ public function indexAction( } } - return $this->render('Default/index.html.twig', [ - 'pagetitle' => "${gameName} Deckbuilder", - 'pagedescription' => "Build your deck for ${gameName} by ${publisherName}." - . " Browse the cards and the thousand of decklists submitted by the community." - . " Publish your own decks and get feedback.", - 'decklists_by_faction' => $decklists_by_faction - ], $response); + return $this->render( + 'Default/index.html.twig', + [ + 'pagetitle' => "${gameName} Deckbuilder", + 'pagedescription' => "Build your deck for ${gameName} by ${publisherName}." + ." Browse the cards and the thousand of decklists submitted by the community." + ." Publish your own decks and get feedback.", + 'decklists_by_faction' => $decklists_by_faction, + ], + $response + ); } /** @@ -115,6 +124,7 @@ public function rulesreferenceAction(int $cacheExpiration, TranslatorInterface $ array("pagetitle" => $translator->trans("nav.rules"), "pagedescription" => "Rules Reference") ); $response->setContent($page); + return $response; } @@ -135,6 +145,151 @@ public function faqAction(int $cacheExpiration, TranslatorInterface $translator) array("pagetitle" => $translator->trans("nav.rules"), "pagedescription" => "F.A.Q") ); $response->setContent($page); + + return $response; + } + + /** + * @Route("/restrictions", name="restrictions", methods={"GET"}) + * @param int $cacheExpiration + * @param TranslatorInterface $translator + * @return Response + */ + public function restrictionsAction( + int $cacheExpiration, + TranslatorInterface $translator, + CardsData $cardsData + ): Response { + $response = new Response(); + $response->setPublic(); + $response->setMaxAge($cacheExpiration); + + $restrictionsRepo = $this->getDoctrine()->getRepository(Restriction::class); + $cardsRepo = $this->getDoctrine()->getRepository(Card::class); + $restrictions = $restrictionsRepo->findBy([], ['effectiveOn' => 'DESC']); + + // get all card codes from all RLs + $allCardCodes = []; + foreach ($restrictions as $restriction) { + $allCardCodes = array_merge($allCardCodes, $restriction->getReferencedCards()); + } + + // preload all cards data for those so we don't have to take multiple trips to the database. + $cards = $cardsRepo->findBy( + ['code' => array_unique($allCardCodes)], + ['faction' => 'ASC', 'type' => 'ASC', 'code' => 'ASC'] + ); + + // create a lookup map of cards by their code + $cardsMap = []; + /* @var CardInterface $card */ + foreach ($cards as $card) { + $cardsMap[$card->getCode()] = $cardsData->getCardInfo($card, false, null); + } + + $extractAndSortList = function (array $cardCodes, array $cardsMap): array { + $rhett = array_values(array_intersect_key($cardsMap, array_fill_keys($cardCodes, null))); + usort( + $rhett, + function (array $a, array $b): int { + $factions = [ + 'neutral', + 'baratheon', + 'greyjoy', + 'lannister', + 'martell', + 'thenightswatch', + 'stark', + 'tyrell', + 'targaryen', + ]; + $types = [ + 'agenda', + 'plot', + 'character', + 'attachment', + 'location', + 'event', + ]; + + return array_search($a['faction_code'], $factions) <=> array_search($b['faction_code'], $factions) + ?: array_search($a['type_code'], $types) <=> array_search($b['type_code'], $types); + } + ); + + return $rhett; + }; + + $extractAndSortPods = function (array $pods, array $cardsMap, $extractAndSortList): array { + return array_map(function (array $pod) use ($cardsMap, $extractAndSortList) { + $rhett = [ + 'title' => $pod['title'], + ]; + if (array_key_exists('restricted', $pod) && $pod['restricted']) { + $rhett['restricted'] = $cardsMap[$pod['restricted']]; + } + $rhett['cards'] = $extractAndSortList($pod['cards'], $cardsMap); + return $rhett; + }, $pods); + }; + + // transmogrify restricted lists for output + $restrictions = array_map( + function (RestrictionInterface $restriction) use ($extractAndSortList, $extractAndSortPods, $cardsMap) { + $rhett = [ + 'code' => $restriction->getcode(), + 'cardSet' => $restriction->getCardSet(), + 'title' => $restriction->getTitle(), + 'effectiveOn' => $restriction->getEffectiveOn(), + 'active' => $restriction->isActive(), + 'issuer' => $restriction->getIssuer(), + ]; + $rhett['joustRestrictedList'] = $extractAndSortList($restriction->getJoustRestrictedList(), $cardsMap); + $rhett['joustRestrictedPods'] = $extractAndSortPods( + $restriction->getJoustRestrictedPods(), + $cardsMap, + $extractAndSortList + ); + $rhett['joustBannedList'] = $extractAndSortList($restriction->getJoustBannedList(), $cardsMap); + $rhett['meleeRestrictedList'] = $extractAndSortList($restriction->getMeleeRestrictedList(), $cardsMap); + $rhett['meleeRestrictedPods'] = $extractAndSortPods( + $restriction->getMeleeRestrictedPods(), + $cardsMap, + $extractAndSortList + ); + $rhett['meleeBannedList'] = $extractAndSortList($restriction->getMeleeBannedList(), $cardsMap); + + return $rhett; + }, + $restrictions + ); + + + // split RLs into active and inactive + // and populate them with full card info + $activeRestrictions = []; + $inactiveRestrictions = []; + + foreach ($restrictions as $restriction) { + if ($restriction['active']) { + $activeRestrictions[] = $restriction; + } else { + $inactiveRestrictions[] = $restriction; + } + } + + + $page = $this->renderView( + 'Default/restrictions.html.twig', + [ + "pagetitle" => $translator->trans("nav.restrictions"), + "pagedescription" => "Restricted and Banned Cards", + "inactive_lists" => $inactiveRestrictions, + "active_lists" => $activeRestrictions, + ] + ); + $response->setContent($page); + return $response; } @@ -154,10 +309,11 @@ public function tournamentregulationsAction(int $cacheExpiration, TranslatorInte 'Default/tournamentregulations.html.twig', array( "pagetitle" => $translator->trans("nav.rules"), - "pagedescription" => "Tournament Regulations" + "pagedescription" => "Tournament Regulations", ) ); $response->setContent($page); + return $response; } @@ -174,10 +330,14 @@ public function aboutAction(Request $request, int $cacheExpiration, string $game $response->setPublic(); $response->setMaxAge($cacheExpiration); - return $this->render($this->getLocaleSpecificViewPath('about', $request->getLocale()), array( + return $this->render( + $this->getLocaleSpecificViewPath('about', $request->getLocale()), + array( "pagetitle" => "About", "game_name" => $gameName, - ), $response); + ), + $response + ); } /** @@ -193,10 +353,14 @@ public function apiIntroAction(int $cacheExpiration, string $gameName, string $p $response->setPublic(); $response->setMaxAge($cacheExpiration); - return $this->render('Default/apiIntro.html.twig', array( + return $this->render( + 'Default/apiIntro.html.twig', + array( "pagetitle" => "API", "game_name" => $gameName, "publisher_name" => $publisherName, - ), $response); + ), + $response + ); } } diff --git a/src/Controller/RestrictionsController.php b/src/Controller/RestrictionsController.php new file mode 100644 index 00000000..35743934 --- /dev/null +++ b/src/Controller/RestrictionsController.php @@ -0,0 +1,167 @@ +setPublic(); + $response->setMaxAge($cacheExpiration); + + $restrictionsRepo = $this->getDoctrine()->getRepository(Restriction::class); + $cardsRepo = $this->getDoctrine()->getRepository(Card::class); + $restrictions = $restrictionsRepo->findBy([], ['effectiveOn' => 'DESC']); + + // get all card codes from all RLs + $allCardCodes = []; + foreach ($restrictions as $restriction) { + $allCardCodes = array_merge($allCardCodes, $restriction->getReferencedCards()); + } + + // preload all cards data for those so we don't have to take multiple trips to the database. + $cards = $cardsRepo->findBy( + ['code' => array_unique($allCardCodes)], + ['faction' => 'ASC', 'type' => 'ASC', 'code' => 'ASC'] + ); + + // create a lookup map of cards by their code + $cardsMap = []; + /* @var CardInterface $card */ + foreach ($cards as $card) { + $cardsMap[$card->getCode()] = $cardsData->getCardInfo($card, false, null); + } + + $extractAndSortList = function (array $cardCodes, array $cardsMap): array { + $rhett = array_values(array_intersect_key($cardsMap, array_fill_keys($cardCodes, null))); + usort( + $rhett, + function (array $a, array $b): int { + $factions = [ + 'neutral', + 'baratheon', + 'greyjoy', + 'lannister', + 'martell', + 'thenightswatch', + 'stark', + 'tyrell', + 'targaryen', + ]; + $types = [ + 'agenda', + 'plot', + 'character', + 'attachment', + 'location', + 'event', + ]; + + return array_search($a['faction_code'], $factions) <=> array_search($b['faction_code'], $factions) + ?: array_search($a['type_code'], $types) <=> array_search($b['type_code'], $types); + } + ); + + return $rhett; + }; + + $extractAndSortPods = function (array $pods, array $cardsMap, $extractAndSortList): array { + return array_map(function (array $pod) use ($cardsMap, $extractAndSortList) { + $rhett = [ + 'title' => $pod['title'], + ]; + if (array_key_exists('restricted', $pod) && $pod['restricted']) { + $rhett['restricted'] = $cardsMap[$pod['restricted']]; + } + $rhett['cards'] = $extractAndSortList($pod['cards'], $cardsMap); + return $rhett; + }, $pods); + }; + + // transmogrify restricted lists for output + $restrictions = array_map( + function (RestrictionInterface $restriction) use ($extractAndSortList, $extractAndSortPods, $cardsMap) { + $rhett = [ + 'code' => $restriction->getcode(), + 'cardSet' => $restriction->getCardSet(), + 'title' => $restriction->getTitle(), + 'effectiveOn' => $restriction->getEffectiveOn(), + 'active' => $restriction->isActive(), + 'issuer' => $restriction->getIssuer(), + ]; + $rhett['joustRestrictedList'] = $extractAndSortList($restriction->getJoustRestrictedList(), $cardsMap); + $rhett['joustRestrictedPods'] = $extractAndSortPods( + $restriction->getJoustRestrictedPods(), + $cardsMap, + $extractAndSortList + ); + $rhett['joustBannedList'] = $extractAndSortList($restriction->getJoustBannedList(), $cardsMap); + $rhett['meleeRestrictedList'] = $extractAndSortList($restriction->getMeleeRestrictedList(), $cardsMap); + $rhett['meleeRestrictedPods'] = $extractAndSortPods( + $restriction->getMeleeRestrictedPods(), + $cardsMap, + $extractAndSortList + ); + $rhett['meleeBannedList'] = $extractAndSortList($restriction->getMeleeBannedList(), $cardsMap); + + return $rhett; + }, + $restrictions + ); + + + // split RLs into active and inactive + // and populate them with full card info + $activeRestrictions = []; + $inactiveRestrictions = []; + + foreach ($restrictions as $restriction) { + if ($restriction['active']) { + $activeRestrictions[] = $restriction; + } else { + $inactiveRestrictions[] = $restriction; + } + } + + + $page = $this->renderView( + 'Restrictions/index.html.twig', + [ + "pagetitle" => $translator->trans("nav.restrictions"), + "pagedescription" => "Restricted and Banned Cards", + "inactive_lists" => $inactiveRestrictions, + "active_lists" => $activeRestrictions, + ] + ); + $response->setContent($page); + + return $response; + } +} diff --git a/src/Entity/Restriction.php b/src/Entity/Restriction.php index b58fb950..12834d6c 100644 --- a/src/Entity/Restriction.php +++ b/src/Entity/Restriction.php @@ -222,4 +222,76 @@ public function getVersion(): string { return $this->version; } + + /** + * @inheritdoc + */ + public function getJoustRestrictedList(): array + { + return $this->getContents()['joust']['restricted']; + } + + /** + * @inheritdoc + */ + public function getJoustBannedList(): array + { + return $this->getContents()['joust']['banned']; + } + + /** + * @inheritdoc + */ + public function getJoustRestrictedPods(): array + { + return $this->getContents()['joust']['restricted_pods']; + } + + /** + * @inheritdoc + */ + public function getMeleeRestrictedList(): array + { + return $this->getContents()['melee']['restricted']; + } + + /** + * @inheritdoc + */ + public function getMeleeBannedList(): array + { + return $this->getContents()['melee']['banned']; + } + + /** + * @inheritdoc + */ + public function getMeleeRestrictedPods(): array + { + return $this->getContents()['melee']['restricted_pods']; + } + + /** + * @inheritdoc + */ + public function getReferencedCards() : array + { + $cardsInPods = array_map(function (array $pod) { + $rhett = []; + if (array_key_exists('restricted', $pod) && $pod['restricted']) { + $rhett[] = $pod['restricted']; + } + return array_merge($rhett, $pod['cards']); + }, + array_merge($this->getJoustRestrictedPods(), $this->getMeleeRestrictedPods())); + return array_unique( + array_merge( + $this->getJoustRestrictedList(), + $this->getJoustBannedList(), + $this->getMeleeRestrictedList(), + $this->getMeleeBannedList(), + $cardsInPods + ) + ); + } } diff --git a/src/Entity/RestrictionInterface.php b/src/Entity/RestrictionInterface.php index 7b2dcac8..b15bedfd 100644 --- a/src/Entity/RestrictionInterface.php +++ b/src/Entity/RestrictionInterface.php @@ -89,4 +89,39 @@ public function setVersion(string $version): void; * @return string */ public function getVersion(): string; + + /** + * @return array + */ + public function getJoustRestrictedList(): array; + + /** + * @return array + */ + public function getJoustBannedList(): array; + + /** + * @return array + */ + public function getJoustRestrictedPods(): array; + + /** + * @return array + */ + public function getMeleeRestrictedList(): array; + + /** + * @return array + */ + public function getMeleeBannedList(): array; + + /** + * @return array + */ + public function getMeleeRestrictedPods(): array; + + /** + * @return array + */ + public function getReferencedCards(): array; } diff --git a/templates/Restrictions/cardlist.html.twig b/templates/Restrictions/cardlist.html.twig new file mode 100644 index 00000000..ef433854 --- /dev/null +++ b/templates/Restrictions/cardlist.html.twig @@ -0,0 +1,15 @@ +{# + @file + Partial template for rendering a list of cards. + + Available variables: + - list: a list of cards +#} +{% for card in list %} +
+ + {{ card.name }} ({{ card.pack_code }}) +
+{% endfor %} +
diff --git a/templates/Restrictions/index.html.twig b/templates/Restrictions/index.html.twig new file mode 100644 index 00000000..c0e23598 --- /dev/null +++ b/templates/Restrictions/index.html.twig @@ -0,0 +1,50 @@ +{# + @file + Page template for displaying all restricted lists (RL). + + Available variables: + - active_lists: a list of active RLs + - inactive_lists: a list of inactive RLs + - pagetitle: The page title + - pagedescription: The page description +#} +{% extends 'base.html.twig' %} +{% block body %} +
+ + {% include 'alerts.html.twig' %} + + +
+
+ {% if active_lists|length %} +
+

Active Restricted Lists

+

The following lists of card restrictions are supported in the deck builder.

+ {% for list in active_lists %} + {% include 'Restrictions/restrictedlist.html.twig' with {'list': list} %} + {% endfor %} +
+ {% endif %} + {% if inactive_lists|length %} +
+

Inactive Restricted Lists

+

The following lists of card restrictions are not supported in + the deck builder and are listed here for reference only.

+ {% for list in inactive_lists %} + {% include 'Restrictions/restrictedlist.html.twig' with {'list': list } %} + {% endfor %} +
+ {% endif %} +
+
+
+{% endblock %} + + + + diff --git a/templates/Restrictions/poddescription.html.twig b/templates/Restrictions/poddescription.html.twig new file mode 100644 index 00000000..60594fb9 --- /dev/null +++ b/templates/Restrictions/poddescription.html.twig @@ -0,0 +1,9 @@ +{# + @file + Partial template for rendering a description for pods. +#} +

Some restricted cards are so powerful that their inclusion in a deck comes with + additional deckbuilding restrictions. + If a player selects one of these cards as their restricted card + (shown in bold text below), they may not include any of the + cards listed following them.

diff --git a/templates/Restrictions/podlist.html.twig b/templates/Restrictions/podlist.html.twig new file mode 100644 index 00000000..b8689515 --- /dev/null +++ b/templates/Restrictions/podlist.html.twig @@ -0,0 +1,29 @@ +{# + @file + Partial template for rendering a list of pods. + + Available variables: + - pods: a list of pods +#} +{% for pod in list.joustRestrictedPods %} +
+
Pod {{ loop.index }}
+
+ {% if pod.restricted %} + + {% endif %} + {% for card in pod.cards %} + + {% endfor %} +
+
+
+{% endfor %} diff --git a/templates/Restrictions/restricteddescription.html.twig b/templates/Restrictions/restricteddescription.html.twig new file mode 100644 index 00000000..a3403f09 --- /dev/null +++ b/templates/Restrictions/restricteddescription.html.twig @@ -0,0 +1,8 @@ +{# + @file + Partial template for rendering a description for restricted lists. +#} +

A player may select one card from the restricted list for any given deck, + and cannot then include any other restricted cards in the same deck. + A player may run as many copies of his or her chosen restricted card in a deck as + the regular game rules (or card text) allow.

diff --git a/templates/Restrictions/restrictedlist.html.twig b/templates/Restrictions/restrictedlist.html.twig new file mode 100644 index 00000000..e36befe0 --- /dev/null +++ b/templates/Restrictions/restrictedlist.html.twig @@ -0,0 +1,47 @@ +{# +@file + Partial template for rendering a restricted list. + + Available variables: + - list: a restricted list object +#} +
+

{{ list.title }} + + + +

+

+ Issued by {{ list.issuer }}, effective on {{ list.effectiveOn | date("m/d/Y")}}. +

+ {%if list.joustRestrictedList | length %} +

Joust Restricted List

+ {% include 'Restrictions/restricteddescription.html.twig' %} + {% include 'Restrictions/cardlist.html.twig' with {'list': list.joustRestrictedList} %} + {% endif %} + {%if list.joustRestrictedPods | length %} +

Joust Restricted Pods

+ {% include 'Restrictions/poddescription.html.twig' %} + {% include 'Restrictions/podlist.html.twig' with {'pods': list.joustRestrictedPods} %} + {% endif %} + {%if list.joustBannedList | length %} +

Joust Banned List

+

The following cards are not legal for competitive play in Joust.

+ {% include 'Restrictions/cardlist.html.twig' with {'list': list.joustBannedList} %} + {% endif %} + {%if list.meleeRestrictedList | length %} +

Melee Restricted List

+ {% include 'Restrictions/restricteddescription.html.twig' %} + {% include 'Restrictions/cardlist.html.twig' with {'list': list.meleeRestrictedList} %} + {% endif %} + {%if list.meleeRestrictedPods | length %} +

Melee Restricted Pods

+ {% include 'Restrictions/poddescription.html.twig' %} + {% include 'Restrictions/podlist.html.twig' with {'pods': list.meleeRestrictedPods} %} + {% endif %} + {%if list.meleeBannedList | length %} +

Melee Banned List

+

The following cards are not legal for competitive play in Melee.

+ {% include 'Restrictions/cardlist.html.twig' with {'list': list.meleeBannedList} %} + {% endif %} +
diff --git a/translations/messages.en.yml b/translations/messages.en.yml index c9e8c6cd..3a0a4548 100644 --- a/translations/messages.en.yml +++ b/translations/messages.en.yml @@ -16,6 +16,7 @@ nav: rules: Rules rulesreference: Rules Reference faq: F.A.Q. + restrictions: Restricted and Banned Cards tournamentregulations: Tournament Regulations advancedsearch: Advanced Search cardsearch: Card Search