diff --git a/.gitignore b/.gitignore index b0eb966..ede8f80 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,9 @@ -/.idea +# Vagrant files /.vagrant /*-cloudimg-console.log -# Data Repo -/data +# IDE Files +/.idea ###> symfony/framework-bundle ### .env @@ -13,4 +13,4 @@ ###< symfony/framework-bundle ### ###> lexik/jwt-authentication-bundle ### /config/jwt/*.pem -###< lexik/jwt-authentication-bundle ### +###< lexik/jwt-authentication-bundle ### \ No newline at end of file diff --git a/Vagrantfile b/Vagrantfile index 2321f77..4e61a47 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -62,6 +62,9 @@ Vagrant.configure("2") do |config| echo '[Assertion]' > /etc/php/7.3/cli/conf.d/99-assert.ini echo 'zend.assertions = 1' >> /etc/php/7.3/cli/conf.d/99-assert.ini echo 'assert.exception = On' >> /etc/php/7.3/cli/conf.d/99-assert.ini + + wget https://get.symfony.com/cli/installer -O /root/installer.sh && bash /root/installer.sh + mv /root/.symfony/bin/symfony /usr/local/bin/symfony SHELL config.vm.provision "install", type: "shell", privileged: false, inline: <<-SHELL diff --git a/config/services/contrib.yaml b/config/services/contrib.yaml index 0c572a8..70ff0a6 100644 --- a/config/services/contrib.yaml +++ b/config/services/contrib.yaml @@ -18,10 +18,12 @@ services: App\Contrib\Transformers\ArmorSetBonusTransformer: ~ App\Contrib\Transformers\CharmTransformer: ~ App\Contrib\Transformers\DecorationTransformer: ~ + App\Contrib\Transformers\EndemicLifeTransformer: ~ App\Contrib\Transformers\ItemTransformer: ~ App\Contrib\Transformers\LocationTransformer: ~ App\Contrib\Transformers\MonsterTransformer: ~ App\Contrib\Transformers\MotionValueTransformer: ~ + App\Contrib\Transformers\QuestTransformer: ~ App\Contrib\Transformers\SkillTransformer: ~ App\Contrib\Transformers\UserTransformer: ~ App\Contrib\Transformers\WeaponTransformer: ~ diff --git a/src/Command/WorldEventSyncCommand.php b/src/Command/WorldEventSyncCommand.php index 5540bb2..3feba45 100644 --- a/src/Command/WorldEventSyncCommand.php +++ b/src/Command/WorldEventSyncCommand.php @@ -42,8 +42,7 @@ public function __construct(WorldEventReader $eventReader, EntityManagerInterfac protected function configure(): void { $this ->addArgument('platform', InputArgument::REQUIRED) - ->addArgument('expansion', InputArgument::OPTIONAL, '', Expansion::BASE) - ->addOption('sleep', null, InputOption::VALUE_REQUIRED, '', 5); + ->addArgument('expansion', InputArgument::OPTIONAL, '', Expansion::BASE); } /** @@ -55,8 +54,7 @@ protected function configure(): void { protected function execute(InputInterface $input, OutputInterface $output): int { $events = $this->eventReader->read( $input->getArgument('platform'), - $input->getArgument('expansion'), - (int)$input->getOption('sleep') + $input->getArgument('expansion') ); $added = 0; diff --git a/src/Contrib/Transformers/EndemicLifeTransformer.php b/src/Contrib/Transformers/EndemicLifeTransformer.php new file mode 100644 index 0000000..e190537 --- /dev/null +++ b/src/Contrib/Transformers/EndemicLifeTransformer.php @@ -0,0 +1,76 @@ +type, $data->researchPointValue); + } + + /** + * {@inheritdoc} + */ + public function doUpdate(EntityInterface $entity, object $data): void { + assert($entity instanceof EndemicLife); + + if (ObjectUtil::isset($data, 'name')) + $this->getStrings($entity)->setName($data->name); + + if (ObjectUtil::isset($data, 'description')) + $this->getStrings($entity)->setDescription($data->description); + + if (ObjectUtil::isset($data, 'type')) + $entity->setType($data->type); + + if (ObjectUtil::isset($data, 'researchPointValue')) + $entity->setResearchPointValue($data->researchPointValue); + + if (ObjectUtil::isset($data, 'spawnConditions')) + $entity->setSpawnConditions($data->spawnConditions); + + if (ObjectUtil::isset($data, 'locations')) + $this->populateFromIdArray('locations', $entity->getLocations(), Location::class, $data->locations); + } + + /** + * {@inheritdoc} + */ + public function doDelete(EntityInterface $entity): void { + // TODO: Implement doDelete() method. + } + + /** + * @param EndemicLife $endemicLife + * + * @return EndemicLifeStrings + */ + protected function getStrings(EndemicLife $endemicLife): EndemicLifeStrings { + $strings = L10nUtil::findOrCreateStrings($this->getCurrentLocale(), $endemicLife); + assert($strings instanceof EndemicLifeStrings); + + return $strings; + } + } \ No newline at end of file diff --git a/src/Contrib/Transformers/QuestTransformer.php b/src/Contrib/Transformers/QuestTransformer.php new file mode 100644 index 0000000..d2fdeed --- /dev/null +++ b/src/Contrib/Transformers/QuestTransformer.php @@ -0,0 +1,309 @@ +entityManager->find(Location::class, $data->location); + + if (!$location) + throw IntegrityException::missingReference('location', 'Location'); + + $quest = new Quest($location, $data->objective, $data->type, $data->rank, $data->stars); + + // Additional validation, based on the provided objective type. Also gives us a chance to set values that + // should never be changed during a call to doUpdate(). + switch ($data->objective) { + case QuestObjective::GATHER: + $missing = ObjectUtil::getMissingProperties( + $data, + [ + 'item', + 'amount', + ] + ); + + if ($missing) + throw ValidationException::missingFields($missing); + + break; + + case QuestObjective::DELIVER: + $type = $data->deliveryType ?? null; + $required = [ + 'deliveryType', + 'amount', + ]; + + if ($type === DeliveryType::ENDEMIC_LIFE) + $required[] = 'endemicLife'; + else if ($type === DeliveryType::OBJECT) + $required[] = 'objectName'; + else + throw ValidationException::invalidFieldType('deliveryType', 'quest delivery type'); + + $missing = ObjectUtil::getMissingProperties($data, $required); + + if ($missing) + throw ValidationException::missingFields($missing); + + $quest->setDeliveryType($data->deliveryType); + + break; + + case QuestObjective::HUNT: + case QuestObjective::CAPTURE: + case QuestObjective::SLAY: + if (!isset($data->targets)) + throw ValidationException::missingFields(['targets']); + + break; + + default: + throw ValidationException::invalidFieldType('objective', 'quest objective type'); + } + + return $quest; + } + + /** + * {@inheritdoc} + */ + public function doUpdate(EntityInterface $entity, object $data): void { + assert($entity instanceof Quest); + + if (isset($data->name)) + $this->getStrings($entity)->setName($data->name); + + if (isset($data->description)) + $this->getStrings($entity)->setDescription($data->description); + + if (isset($data->type)) + $entity->setType($data->type); + + if (isset($data->rank)) + $entity->setRank($data->rank); + + if (isset($data->stars)) + $entity->setStars($data->stars); + + if (isset($data->timeLimit)) + $entity->setTimeLimit($data->timeLimit); + + if (isset($data->maxHunters)) + $entity->setMaxHunters($data->maxHunters); + + if (isset($data->maxFaints)) + $entity->setMaxFaints($data->maxFaints); + + if (isset($data->location)) { + /** @var Location|null $location */ + $location = $this->entityManager->find(Location::class, $data->location); + + if (!$location) + throw IntegrityException::missingReference('location', 'Location'); + + $entity->setLocation($location); + } + + if (isset($data->rewards)) { + /** @var int[] $itemIds */ + $itemIds = []; + + foreach ($data->rewards as $rewardIndex => $rewardDefinition) { + $missing = ObjectUtil::getMissingProperties( + $rewardDefinition, + [ + 'item', + 'conditions', + ] + ); + + if ($missing) + throw ValidationException::missingNestedFields('rewards', $rewardIndex, $missing); + + /** @var Item|null $item */ + $item = $this->entityManager->find(Item::class, $rewardDefinition->item); + + if (!$item) + throw IntegrityException::missingReference('rewards[' . $rewardIndex . '].item', 'Item'); + + $itemIds[] = $item->getId(); + $reward = $entity->getRewardForItem($item); + + if (!$reward) + $entity->getRewards()->add($reward = new QuestReward($entity, $item)); + + $reward->getConditions()->clear(); + + foreach ($rewardDefinition->conditions as $index => $definition) { + $missing = ObjectUtil::getMissingProperties( + $definition, + [ + 'type', + 'rank', + 'quantity', + 'chance', + ] + ); + + if ($missing) { + throw ValidationException::missingNestedFields( + 'rewards[' . $rewardIndex . '].conditions', + $index, + $missing + ); + } + + $reward->getConditions()->add( + $condition = new RewardCondition( + $definition->type, + $definition->rank, + $definition->quantity, + $definition->chance + ) + ); + + if (ObjectUtil::isset($definition, 'subtype')) { + $subtype = $definition->subtype; + + if (is_string($subtype)) + $subtype = strtolower($subtype); + + $condition->setSubtype($subtype); + } + } + + foreach ($entity->getRewards() as $reward) { + if (!in_array($reward->getItem()->getId(), $itemIds)) + $entity->getRewards()->removeElement($reward); + } + } + } + + switch ($entity->getObjective()) { + case QuestObjective::GATHER: + if (isset($data->amount)) + $entity->setAmount($data->amount); + + if (isset($data->item)) { + /** @var Item|null $item */ + $item = $this->entityManager->find(Item::class, $data->item); + + if (!$item) + throw IntegrityException::missingReference('item', 'Item'); + + $entity->setItem($item); + } + + break; + + case QuestObjective::DELIVER: + if (isset($data->amount)) + $entity->setAmount($data->amount); + + if ($entity->getDeliveryType() === DeliveryType::ENDEMIC_LIFE && isset($data->endemicLife)) { + /** @var EndemicLife|null $endemicLife */ + $endemicLife = $this->entityManager->find(EndemicLife::class, $data->endemicLife); + + if (!$endemicLife) + throw IntegrityException::missingReference('endemicLife', 'Endemic Life'); + + $entity->setEndemicLife($endemicLife); + } else if ($entity->getDeliveryType() === DeliveryType::OBJECT && isset($data->objectName)) + $this->getStrings($entity)->setObjectName(strtolower($data->objectName)); + + break; + + case QuestObjective::HUNT: + case QuestObjective::CAPTURE: + case QuestObjective::SLAY: + if (isset($data->targets)) { + $entity->getTargets()->clear(); + + foreach ($data->targets as $rewardIndex => $definition) { + $missing = ObjectUtil::getMissingProperties( + $definition, + [ + 'amount', + 'monster', + ] + ); + + if ($missing) + throw ValidationException::missingNestedFields('targets', $rewardIndex, $missing); + + /** @var Monster|null $monster */ + $monster = $this->entityManager->find(Monster::class, $definition->monster); + + if (!$monster) { + throw IntegrityException::missingReference( + 'targets[' . $rewardIndex . '].monster', + 'Monster' + ); + } + + $entity->getTargets()->add(new MonsterQuestTarget($entity, $monster, $definition->amount)); + } + } + + break; + } + } + + /** + * {@inheritdoc} + */ + public function doDelete(EntityInterface $entity): void { + assert($entity instanceof Quest); + + // TODO Remove events related to deleted quest + } + + /** + * @param Quest $quest + * + * @return QuestStrings + */ + protected function getStrings(Quest $quest): QuestStrings { + $strings = L10nUtil::findOrCreateStrings($this->getCurrentLocale(), $quest); + assert($strings instanceof QuestStrings); + + return $strings; + } + } \ No newline at end of file diff --git a/src/Controller/AbstractController.php b/src/Controller/AbstractController.php index fd75e37..4a581af 100644 --- a/src/Controller/AbstractController.php +++ b/src/Controller/AbstractController.php @@ -1,10 +1,39 @@ requestStack->getCurrentRequest()->getLocale(), $entity) ); } + + /** + * @param Projection $projection + * @param string $prefix + * @param Ailment $ailment + * + * @return array + */ + protected function normalizeAilment(Projection $projection, string $prefix, Ailment $ailment): array { + $output = [ + 'id' => $ailment->getId(), + ]; + + if ($this->isAnyAllowed($projection, $prefix, 'name', 'description')) { + /** @var AilmentStrings $strings */ + $strings = $this->getStrings($ailment); + + $output += [ + 'name' => $strings->getName(), + 'description' => $strings->getDescription(), + ]; + } + + if ($this->isAllowed($projection, $prefix, 'recovery')) { + $recovery = $ailment->getRecovery(); + + $output['recovery'] = [ + 'actions' => $recovery->getActions(), + ]; + + if ($this->isAllowed($projection, $prefix, 'recovery.items')) { + $output['recovery']['items'] = $recovery->getItems()->map( + function(Item $item) use ($projection, $prefix) { + return $this->normalizeItem( + $projection, + $this->mergePaths($prefix, 'recovery.items'), + $item + ); + } + ); + } + } + + if ($this->isAllowed($projection, $prefix, 'protection')) { + $protection = []; + + if ($this->isAllowed($projection, $prefix, 'protection.skills')) { + $protection['skills'] = $ailment->getProtection()->getSkills()->map( + function(Skill $skill) use ($projection, $prefix) { + return $this->normalizeSkill( + $projection, + $this->mergePaths($prefix, 'protection.skills'), + $skill + ); + } + )->toArray(); + } + + if ($this->isAllowed($projection, $prefix, 'protection.items')) { + $protection['items'] = $ailment->getProtection()->getItems()->map( + function(Item $item) use ($projection, $prefix) { + return $this->normalizeItem( + $projection, + $this->mergePaths($prefix, 'protection.items'), + $item + ); + } + )->toArray(); + } + } + + return $output; + } + + /** + * @param Projection $projection + * @param string $prefix + * @param Skill $skill + * + * @return array + */ + protected function normalizeSkill(Projection $projection, string $prefix, Skill $skill): array { + $output = [ + 'id' => $skill->getId(), + ]; + + if ($this->isAnyAllowed($projection, $prefix, 'name', 'description')) { + /** @var SkillStrings $skillStrings */ + $skillStrings = $this->getStrings($skill); + + $output += [ + 'name' => $skillStrings->getName(), + 'description' => $skillStrings->getDescription(), + ]; + } else + $skillStrings = null; // ensure always defined so we can `use` it later + + if ($this->isAllowed($projection, $prefix, 'ranks')) { + $output['ranks'] = $skill->getRanks()->map( + function(SkillRank $rank) use ($projection, $prefix, $skill, $skillStrings) { + $output = [ + 'level' => $rank->getLevel(), + 'modifiers' => $rank->getModifiers() ?: new \stdClass(), + ]; + + if ($this->isAllowed($projection, $prefix, 'ranks.skillName')) + $output['skillName'] = ($skillStrings ?? $this->getStrings($skill))->getName(); + + if ($this->isAllowed($projection, $prefix, 'ranks.description')) { + /** @var SkillRankStrings $strings */ + $strings = $this->getStrings($rank); + + $output['description'] = $strings->getDescription(); + } + } + )->toArray(); + } + + return $output; + } + + /** + * @param Projection $projection + * @param string $prefix + * @param Item $item + * + * @return array + */ + protected function normalizeItem(Projection $projection, string $prefix, Item $item): array { + $output = [ + 'id' => $item->getId(), + 'rarity' => $item->getRarity(), + 'value' => $item->getValue(), + 'carryLimit' => $item->getCarryLimit(), + 'buyPrice' => $item->getBuyPrice(), + 'sellPrice' => $item->getSellPrice(), + ]; + + if ($this->isAnyAllowed($projection, $prefix, 'name', 'description')) { + /** @var ItemStrings $strings */ + $strings = $this->getStrings($item); + + $output += [ + 'name' => $strings->getName(), + 'description' => $strings->getDescription(), + ]; + } + + return $output; + } + + /** + * @param RewardCondition $condition + * + * @return array + */ + protected function normalizeRewardCondition(RewardCondition $condition): array { + return [ + 'type' => $condition->getType(), + 'rank' => $condition->getRank(), + 'quantity' => $condition->getQuantity(), + 'chance' => $condition->getChance(), + 'subtype' => $condition->getSubtype(), + ]; + } + + /** + * @param Projection $projection + * @param string $prefix + * @param WorldEvent $event + * + * @return array + */ + protected function normalizeWorldEvent(Projection $projection, string $prefix, WorldEvent $event): array { + return [ + 'id' => $event->getId(), + 'platform' => $event->getPlatform(), + 'exclusive' => $event->getExclusive(), + 'type' => $event->getType(), + 'expansion' => $event->getExpansion(), + 'startTimestamp' => $event->getStartTimestamp()->format(\DateTime::ISO8601), + 'endTimestamp' => $event->getEndTimestamp()->format(\DateTime::ISO8601), + ]; + } + + /** + * @param Projection $projection + * @param string $prefix + * @param Quest $quest + * + * @return array + */ + protected function normalizeQuest(Projection $projection, string $prefix, Quest $quest): array { + $output = [ + 'id' => $quest->getId(), + 'objective' => $quest->getObjective(), + 'type' => $quest->getType(), + 'rank' => $quest->getRank(), + 'stars' => $quest->getStars(), + 'timeLimit' => $quest->getTimeLimit(), + 'maxHunters' => $quest->getMaxHunters(), + 'maxFaints' => $quest->getMaxFaints(), + ]; + + if ($this->isAnyAllowed($projection, $prefix, 'name', 'description')) { + /** @var QuestStrings $strings */ + $strings = $this->getStrings($quest); + + $output += [ + 'name' => $strings->getName(), + 'description' => $strings->getDescription(), + ]; + } + + if ($this->isAllowed($projection, $prefix, 'rewards')) { + $output['rewards'] = $quest->getRewards()->map( + function(QuestReward $reward) use ($projection, $prefix) { + $output = []; + + if ($this->isAllowed($projection, $prefix, 'rewards.item')) { + $output['item'] = $this->normalizeItem( + $projection, + $this->mergePaths($prefix, 'rewards.item'), + $reward->getItem() + ); + } + + if ($this->isAllowed($projection, $prefix, 'rewards.conditions')) { + $output['conditions'] = $reward->getConditions()->map( + function(RewardCondition $condition) use ($projection, $prefix) { + return $this->normalizeRewardCondition($condition); + } + )->toArray(); + } + + return $output; + } + )->toArray(); + } + + switch ($quest->getObjective()) { + case QuestObjective::GATHER: + $output['amount'] = $quest->getAmount(); + + if ($this->isAllowed($projection, $prefix, 'item')) { + $output['item'] = $this->normalizeItem( + $projection, + $this->mergePaths($prefix, 'item'), + $quest->getItem() + ); + } + + break; + + case QuestObjective::DELIVER: + $output += [ + 'deliveryType' => $quest->getDeliveryType(), + 'amount' => $quest->getAmount(), + ]; + + if ( + $quest->getDeliveryType() === DeliveryType::ENDEMIC_LIFE && + $this->isAllowed($projection, $prefix, 'endemicLife') + ) { + $output['endemicLife'] = $this->normalizeEndemicLife( + $projection, + $this->mergePaths($prefix, 'endemicLife'), + $quest->getEndemicLife() + ); + } else if ( + $quest->getDeliveryType() === DeliveryType::OBJECT && + $this->isAllowed($projection, $prefix, 'objectName') + ) + $output['objectName'] = ($strings ?? $this->getStrings($quest))->getObjectName(); + + break; + + case QuestObjective::CAPTURE: + case QuestObjective::HUNT: + case QuestObjective::SLAY: + if ($this->isAllowed($projection, $prefix, 'targets')) { + $output['targets'] = $quest->getTargets()->map( + function(MonsterQuestTarget $target) use ($projection, $prefix) { + $output = [ + 'amount' => $target->getAmount(), + ]; + + if ($this->isAllowed($projection, $prefix, 'targets.monster')) { + $output['monster'] = $this->normalizeMonster( + $projection, + $this->mergePaths($prefix, 'targets.monster'), + $target->getMonster() + ); + } + + return $output; + } + )->toArray(); + } + + break; + } + + return $output; + } + + /** + * @param Projection $projection + * @param string $prefix + * @param Monster $monster + * + * @return array + */ + protected function normalizeMonster(Projection $projection, string $prefix, Monster $monster): array { + $output = [ + 'id' => $monster->getId(), + 'type' => $monster->getType(), + 'species' => $monster->getSpecies(), + 'elements' => $monster->getElements(), + ]; + + if ($this->isAnyAllowed($projection, $prefix, 'name', 'description')) { + /** @var MonsterStrings $strings */ + $strings = $this->getStrings($monster); + + $output += [ + 'name' => $strings->getName(), + 'description' => $strings->getDescription(), + ]; + } + + if ($this->isAllowed($projection, $prefix, 'ailments')) { + $output['ailments'] = $monster->getAilments()->map( + function(Ailment $ailment) use ($projection, $prefix) { + return $this->normalizeAilment( + $projection, + $this->mergePaths($prefix, 'ailments'), + $ailment + ); + } + )->toArray(); + } + + if ($this->isAllowed($projection, $prefix, 'locations')) { + $output['locations'] = $monster->getLocations()->map( + function(Location $location) use ($projection, $prefix) { + return $this->normalizeLocation( + $projection, + $this->mergePaths($prefix, 'locations'), + $location + ); + } + )->toArray(); + } + + if ($this->isAllowed($projection, $prefix, 'resistances')) { + $output['resistances'] = $monster->getResistances()->map( + function(MonsterResistance $resistance) { + return [ + 'element' => $resistance->getElement(), + 'condition' => $resistance->getCondition(), + ]; + } + )->toArray(); + } + + if ($this->isAllowed($projection, $prefix, 'weaknesses')) { + $output['weaknesses'] = $monster->getWeaknesses()->map( + function(MonsterWeakness $weakness) use ($projection, $prefix) { + $output = [ + 'element' => $weakness->getElement(), + 'stars' => $weakness->getStars(), + ]; + + if ($this->isAllowed($projection, $prefix, 'weaknesses.condition')) { + /** @var MonsterWeaknessStrings $strings */ + $strings = $this->getStrings($weakness); + + $output['condition'] = $strings->getCondition(); + } + + return $output; + } + )->toArray(); + } + + if ($this->isAllowed($projection, $prefix, 'rewards')) { + $monster['rewards'] = $monster->getRewards()->map( + function(MonsterReward $reward) use ($projection, $prefix) { + $output = []; + + if ($this->isAllowed($projection, $prefix, 'rewards.item')) { + $output['item'] = $this->normalizeItem( + $projection, + $this->mergePaths($prefix, 'rewards.item'), + $reward->getItem() + ); + } + + if ($this->isAllowed($projection, $prefix, 'rewards.conditions')) { + $output['conditions'] = $reward->getConditions()->map( + function(RewardCondition $condition) use ($projection, $prefix) { + return $this->normalizeRewardCondition($condition); + } + )->toArray(); + } + } + )->toArray(); + } + + return $output; + } + + /** + * @param Projection $projection + * @param string $prefix + * @param EndemicLife $endemicLife + * + * @return array + */ + protected function normalizeEndemicLife( + Projection $projection, + string $prefix, + EndemicLife $endemicLife + ): array { + $output = [ + 'id' => $endemicLife->getId(), + 'type' => $endemicLife->getType(), + 'researchPointValue' => $endemicLife->getResearchPointValue(), + 'spawnConditions' => $endemicLife->getSpawnConditions(), + ]; + + if ($this->isAnyAllowed($projection, $prefix, 'name', 'description')) { + /** @var EndemicLifeStrings $strings */ + $strings = $this->getStrings($endemicLife); + + $output += [ + 'name' => $strings->getName(), + 'description' => $strings->getDescription(), + ]; + } + + if ($this->isAllowed($projection, $prefix, 'locations')) { + $output['locations'] = $endemicLife->getLocations() + ->map( + function(Location $location) use ($projection, $prefix) { + return $this->normalizeLocation( + $projection, + $this->mergePaths($prefix, 'locations'), + $location + ); + } + ) + ->toArray(); + } + + return $output; + } + + /** + * @param Projection $projection + * @param string $prefix + * @param Location $location + * + * @return array + */ + protected function normalizeLocation(Projection $projection, string $prefix, Location $location): array { + $output = [ + 'id' => $location->getId(), + 'zoneCount' => $location->getZoneCount(), + ]; + + if ($this->isAllowed($projection, $prefix, 'name')) { + /** @var LocationStrings $strings */ + $strings = $this->getStrings($location); + + $output['name'] = $strings->getName(); + } + + if ($this->isAllowed($projection, $prefix, 'camps')) { + $output['camps'] = $location->getCamps()->map( + function(Camp $camp) use ($projection, $prefix) { + $output = [ + 'zone' => $camp->getZone(), + ]; + + if ($this->isAllowed($projection, $prefix, 'camps.name')) { + /** @var CampStrings $strings */ + $strings = $this->getStrings($camp); + + $output['name'] = $strings->getName(); + } + + return $output; + } + )->toArray(); + } + + return $output; + } + + /** + * @param Projection $projection + * @param string $prefix + * @param string $path + * + * @return bool + */ + protected function isAllowed(Projection $projection, string $prefix, string $path): bool { + return $projection->isAllowed($this->mergePaths($prefix, $path)); + } + + /** + * @param Projection $projection + * @param string $prefix + * @param string[] $paths + * + * @return bool + */ + protected function isAnyAllowed(Projection $projection, string $prefix, string ...$paths): bool { + foreach ($paths as $path) { + if ($this->isAllowed($projection, $prefix, $path)) + return true; + } + + return false; + } + + /** + * @param string $prefix + * @param string $path + * + * @return string + */ + protected function mergePaths(string $prefix, string $path): string { + return $prefix . ($prefix ? '.' : '') . $path; + } } \ No newline at end of file diff --git a/src/Controller/EndemicLifeController.php b/src/Controller/EndemicLifeController.php new file mode 100644 index 0000000..68b3fe2 --- /dev/null +++ b/src/Controller/EndemicLifeController.php @@ -0,0 +1,157 @@ +doList($request); + } + + /** + * @Route(path="/endemic-life", methods={"PUT"}, name="endemic-life.create") + * + * @param Request $request + * @param EndemicLifeTransformer $transformer + * + * @return Response + */ + public function create(Request $request, EndemicLifeTransformer $transformer): Response { + return $this->doCreate($transformer, $request); + } + + /** + * @Route(path="/endemic-life/{endemicLife<\d+>}", methods={"GET"}, name="endemic-life.read") + * + * @param Request $request + * @param EndemicLife $endemicLife + * + * @return Response + */ + public function read(Request $request, EndemicLife $endemicLife): Response { + return $this->respond($request, $endemicLife); + } + + /** + * @Route(path="/endemic-life/{endemicLife<\d+>}", methods={"PATCH"}, name="endemic-life.update") + * + * @param Request $request + * @param EndemicLife $endemicLife + * @param EndemicLifeTransformer $transformer + * + * @return Response + */ + public function update( + Request $request, + EndemicLife $endemicLife, + EndemicLifeTransformer $transformer + ): Response { + return $this->doUpdate($transformer, $endemicLife, $request); + } + + /** + * @Route(path="/endemic-life/{endemicLife<\d+>}", methods={"DELETE"}, name="endemic-life.delete") + * + * @param EndemicLife $endemicLife + * @param EndemicLifeTransformer $transformer + * + * @return Response + */ + public function delete(EndemicLife $endemicLife, EndemicLifeTransformer $transformer): Response { + return $this->doDelete($transformer, $endemicLife); + } + + /** + * {@inheritdoc} + */ + protected function normalizeOne(EntityInterface $entity, Projection $projection): array { + assert($entity instanceof EndemicLife); + + $output = [ + 'id' => $entity->getId(), + 'type' => $entity->getType(), + 'researchPointValue' => $entity->getResearchPointValue(), + 'spawnConditions' => $entity->getSpawnConditions(), + ]; + + if ($projection->isAllowed('name') || $projection->isAllowed('description')) { + /** @var EndemicLifeStrings $strings */ + $strings = $this->getStrings($entity); + + $output += [ + 'name' => $strings->getName(), + 'description' => $strings->getDescription(), + ]; + } + + if ($projection->isAllowed('locations')) { + $output['locations'] = $entity->getLocations()->map( + function(Location $location) use ($projection) { + $output = [ + 'id' => $location->getId(), + 'zoneCount' => $location->getZoneCount(), + ]; + + if ($projection->isAllowed('locations.name')) { + /** @var LocationStrings $strings */ + $strings = $this->getStrings($location); + + $output['name'] = $strings->getName(); + } + + if ($projection->isAllowed('locations.camps')) { + $output['camps'] = $location->getCamps()->map( + function(Camp $camp) use ($projection) { + $output = [ + 'id' => $camp->getId(), + 'zone' => $camp->getZone(), + ]; + + if ($projection->isAllowed('locations.camps.name')) { + /** @var CampStrings $strings */ + $strings = $this->getStrings($camp); + + $output['name'] = $strings->getName(); + } + + return $output; + } + )->toArray(); + } + + return $output; + } + )->toArray(); + } + + return $output; + } + } \ No newline at end of file diff --git a/src/Controller/EventController.php b/src/Controller/EventController.php index fc15876..a56852f 100644 --- a/src/Controller/EventController.php +++ b/src/Controller/EventController.php @@ -1,10 +1,6 @@ $entity->getId(), - 'platform' => $entity->getPlatform(), - 'exclusive' => $entity->getExclusive(), - 'type' => $entity->getType(), - 'expansion' => $entity->getExpansion(), - 'questRank' => $entity->getQuestRank(), - 'masterRank' => $entity->isMasterRank(), - 'startTimestamp' => $entity->getStartTimestamp()->format(\DateTime::ISO8601), - 'endTimestamp' => $entity->getEndTimestamp()->format(\DateTime::ISO8601), - ]; + $output = $this->normalizeWorldEvent($projection, '', $entity); - if ( - $projection->isAllowed('name') || - $projection->isAllowed('description') || - $projection->isAllowed('requirements') || - $projection->isAllowed('successConditions') - ) { - /** @var WorldEventStrings $strings */ - $strings = $this->getStrings($entity); - - $output += [ - 'name' => $strings->getName(), - 'description' => $strings->getDescription(), - 'requirements' => $strings->getRequirements(), - 'successConditions' => $strings->getSuccessConditions(), - ]; - } - - if ($projection->isAllowed('location')) { - $location = $entity->getLocation(); - - $output['location'] = [ - 'id' => $location->getId(), - 'zoneCount' => $location->getZoneCount(), - ]; - - if ($projection->isAllowed('location.name')) { - /** @var LocationStrings $strings */ - $strings = $this->getStrings($location); - - $output['location']['name'] = $strings->getName(); - } - - if ($projection->isAllowed('location.camps')) { - $output['location']['camps'] = $location->getCamps()->map( - function(Camp $camp) use ($projection): array { - $output = [ - 'id' => $camp->getId(), - 'zone' => $camp->getZone(), - ]; - - if ($projection->isAllowed('location.camps.name')) { - /** @var CampStrings $strings */ - $strings = $this->getStrings($camp); - - $output['name'] = $strings->getName(); - } - - return $output; - } - )->toArray(); - } - } + if ($projection->isAllowed('quest')) + $output['quest'] = $this->normalizeQuest($projection, 'quest', $entity->getQuest()); return $output; } diff --git a/src/Controller/QuestController.php b/src/Controller/QuestController.php new file mode 100644 index 0000000..f05acdc --- /dev/null +++ b/src/Controller/QuestController.php @@ -0,0 +1,105 @@ +doList($request); + } + + /** + * @Route(path="/quests", methods={"PUT"}, name="quests.create") + * + * @param Request $request + * @param QuestTransformer $transformer + * + * @return Response + */ + public function create(Request $request, QuestTransformer $transformer): Response { + return $this->doCreate($transformer, $request); + } + + /** + * @Route(path="/quests/{quest<\d+>}", methods={"GET"}, name="quests.read") + * + * @param Request $request + * @param Quest $quest + * + * @return Response + */ + public function read(Request $request, Quest $quest): Response { + return $this->respond($request, $quest); + } + + /** + * @Route(path="/quests/{quest<\d+>}", methods={"PATCH"}, name="quests.update") + * + * @param Request $request + * @param Quest $quest + * @param QuestTransformer $transformer + * + * @return Response + */ + public function update(Request $request, Quest $quest, QuestTransformer $transformer): Response { + return $this->doUpdate($transformer, $quest, $request); + } + + /** + * @Route(path="/quests/{quest<\d+>}", methods={"DELETE"}, name="quests.delete") + * + * @param Quest $quest + * @param QuestTransformer $transformer + * + * @return Response + */ + public function delete(Quest $quest, QuestTransformer $transformer): Response { + return $this->doDelete($transformer, $quest); + } + + /** + * @param EntityInterface $entity + * @param Projection $projection + * + * @return array + */ + protected function normalizeOne(EntityInterface $entity, Projection $projection): array { + assert($entity instanceof Quest); + + $output = $this->normalizeQuest($projection, '', $entity); + + if ($projection->isAllowed('events')) { + $output['events'] = $entity->getEvents()->map( + function(WorldEvent $event) use ($projection) { + return $this->normalizeWorldEvent($projection, 'events', $event); + } + )->toArray(); + } + + return $output; + } + } \ No newline at end of file diff --git a/src/Entity/Armor.php b/src/Entity/Armor.php index 2ce60fe..7099161 100644 --- a/src/Entity/Armor.php +++ b/src/Entity/Armor.php @@ -42,7 +42,7 @@ class Armor implements EntityInterface, TranslatableEntityInterface { /** * @Assert\NotBlank() - * @Assert\Choice(callback={"App\Game\Rank", "all"}) + * @Assert\Choice(callback={"App\Game\Rank", "values"}) * * @ORM\Column(type="string", length=16) * diff --git a/src/Entity/ArmorSet.php b/src/Entity/ArmorSet.php index c19017c..f2feb8f 100644 --- a/src/Entity/ArmorSet.php +++ b/src/Entity/ArmorSet.php @@ -23,7 +23,7 @@ class ArmorSet implements EntityInterface, TranslatableEntityInterface { use EntityTrait; /** - * @Assert\Choice(callback={"App\Game\Rank", "all"}) + * @Assert\Choice(callback={"App\Game\Rank", "values"}) * * @ORM\Column(type="string", length=16) * diff --git a/src/Entity/EndemicLife.php b/src/Entity/EndemicLife.php new file mode 100644 index 0000000..7ffd14d --- /dev/null +++ b/src/Entity/EndemicLife.php @@ -0,0 +1,169 @@ +type = $type; + $this->researchPointValue = $researchPointValue; + + $this->strings = new ArrayCollection(); + $this->locations = new ArrayCollection(); + } + + /** + * @return string + */ + public function getType(): string { + return $this->type; + } + + /** + * @param string $type + * + * @return $this + */ + public function setType(string $type) { + $this->type = $type; + + return $this; + } + + /** + * @return int + */ + public function getResearchPointValue(): int { + return $this->researchPointValue; + } + + /** + * @param int $researchPointValue + * + * @return $this + */ + public function setResearchPointValue(int $researchPointValue) { + $this->researchPointValue = $researchPointValue; + + return $this; + } + + /** + * @return string[] + */ + public function getSpawnConditions(): array { + return $this->spawnConditions; + } + + /** + * @param string[] $spawnConditions + * + * @return $this + */ + public function setSpawnConditions(array $spawnConditions) { + $this->spawnConditions = $spawnConditions; + + return $this; + } + + /** + * @return Collection|Selectable|EndemicLifeStrings[] + */ + public function getStrings(): Collection { + return $this->strings; + } + + /** + * @param string $language + * + * @return EndemicLifeStrings + */ + public function addStrings(string $language): EntityInterface { + $this->strings->add($strings = new EndemicLifeStrings($this, $language)); + + return $strings; + } + + /** + * @return Location[]|Collection|Selectable + */ + public function getLocations(): Collection { + return $this->locations; + } + } \ No newline at end of file diff --git a/src/Entity/MonsterQuestTarget.php b/src/Entity/MonsterQuestTarget.php new file mode 100644 index 0000000..b91fad6 --- /dev/null +++ b/src/Entity/MonsterQuestTarget.php @@ -0,0 +1,66 @@ +quest = $quest; + $this->monster = $monster; + $this->amount = $amount; + } + + /** + * @return Monster + */ + public function getMonster(): Monster { + return $this->monster; + } + + /** + * @return int + */ + public function getAmount(): int { + return $this->amount; + } + } \ No newline at end of file diff --git a/src/Entity/Quest.php b/src/Entity/Quest.php new file mode 100644 index 0000000..7c9a3b5 --- /dev/null +++ b/src/Entity/Quest.php @@ -0,0 +1,473 @@ +location = $location; + $this->objective = $objective; + $this->type = $type; + $this->rank = $rank; + $this->stars = $stars; + + $this->strings = new ArrayCollection(); + $this->targets = new ArrayCollection(); + $this->rewards = new ArrayCollection(); + $this->events = new ArrayCollection(); + } + + /** + * @return Location + */ + public function getLocation(): Location { + return $this->location; + } + + /** + * @param Location $location + * + * @return $this + */ + public function setLocation(Location $location) { + $this->location = $location; + + return $this; + } + + /** + * @return string + */ + public function getObjective(): string { + return $this->objective; + } + + /** + * @return string + */ + public function getType(): string { + return $this->type; + } + + /** + * @param string $type + * + * @return $this + */ + public function setType(string $type) { + $this->type = $type; + + return $this; + } + + /** + * @return string + */ + public function getRank(): string { + return $this->rank; + } + + /** + * @param string $rank + * + * @return $this + */ + public function setRank(string $rank) { + $this->rank = $rank; + + return $this; + } + + /** + * @return int + */ + public function getStars(): int { + return $this->stars; + } + + /** + * @param int $stars + * + * @return $this + */ + public function setStars(int $stars) { + $this->stars = $stars; + + return $this; + } + + /** + * @return int + */ + public function getTimeLimit(): int { + return $this->timeLimit; + } + + /** + * @param int $timeLimit + * + * @return $this + */ + public function setTimeLimit(int $timeLimit) { + $this->timeLimit = $timeLimit; + + return $this; + } + + /** + * @return int + */ + public function getMaxHunters(): int { + return $this->maxHunters; + } + + /** + * @param int $maxHunters + * + * @return $this + */ + public function setMaxHunters(int $maxHunters) { + $this->maxHunters = $maxHunters; + + return $this; + } + + /** + * @return int + */ + public function getMaxFaints(): int { + return $this->maxFaints; + } + + /** + * @param int $maxFaints + * + * @return $this + */ + public function setMaxFaints(int $maxFaints) { + $this->maxFaints = $maxFaints; + + return $this; + } + + /** + * @return MonsterQuestTarget[]|Collection|Selectable + */ + public function getTargets() { + return $this->targets; + } + + /** + * @return string|null + */ + public function getDeliveryType(): ?string { + return $this->deliveryType; + } + + /** + * @param string|null $deliveryType + * + * @return $this + */ + public function setDeliveryType(?string $deliveryType) { + $this->deliveryType = $deliveryType; + + return $this; + } + + /** + * @return int|null + */ + public function getAmount(): ?int { + return $this->amount; + } + + /** + * @param int|null $amount + * + * @return $this + */ + public function setAmount(?int $amount) { + $this->amount = $amount; + + return $this; + } + + /** + * @return EndemicLife|null + */ + public function getEndemicLife(): ?EndemicLife { + return $this->endemicLife; + } + + /** + * @param EndemicLife|null $endemicLife + * + * @return $this + */ + public function setEndemicLife(?EndemicLife $endemicLife) { + $this->endemicLife = $endemicLife; + + return $this; + } + + /** + * @return Item|null + */ + public function getItem(): ?Item { + return $this->item; + } + + /** + * @param Item|null $item + * + * @return $this + */ + public function setItem(?Item $item) { + $this->item = $item; + + return $this; + } + + /** + * @return QuestReward[]|Collection|Selectable + */ + public function getRewards(): Collection { + return $this->rewards; + } + + /** + * @param Item $item + * + * @return QuestReward|null + */ + public function getRewardForItem(Item $item): ?QuestReward { + return $this->getRewards()->matching( + Criteria::create() + ->where(Criteria::expr()->eq('item', $item)) + ->setMaxResults(1) + )->first() ?: null; + } + + /** + * @return Collection|Selectable|WorldEvent[] + */ + public function getEvents(): Collection { + return $this->events; + } + + /** + * @return Collection|Selectable|QuestStrings[] + */ + public function getStrings(): Collection { + return $this->strings; + } + + /** + * @param string $language + * + * @return QuestStrings + */ + public function addStrings(string $language): EntityInterface { + $this->getStrings()->add($strings = new QuestStrings($this, $language)); + + return $strings; + } + } \ No newline at end of file diff --git a/src/Entity/QuestReward.php b/src/Entity/QuestReward.php new file mode 100644 index 0000000..bf4de2b --- /dev/null +++ b/src/Entity/QuestReward.php @@ -0,0 +1,76 @@ +quest = $quest; + $this->item = $item; + + $this->conditions = new ArrayCollection(); + } + + /** + * @return Item + */ + public function getItem(): Item { + return $this->item; + } + + /** + * @return RewardCondition[]|Collection|Selectable + */ + public function getConditions() { + return $this->conditions; + } + } \ No newline at end of file diff --git a/src/Entity/RewardCondition.php b/src/Entity/RewardCondition.php index 744366f..66ff8c3 100644 --- a/src/Entity/RewardCondition.php +++ b/src/Entity/RewardCondition.php @@ -29,7 +29,7 @@ class RewardCondition implements EntityInterface { /** * @Assert\NotBlank() - * @Assert\Choice(callback={"App\Game\Rank", "all"}) + * @Assert\Choice(callback={"App\Game\Rank", "values"}) * * @ORM\Column(type="string", length=16) * diff --git a/src/Entity/SluggableInterface.php b/src/Entity/SluggableInterface.php deleted file mode 100644 index 32a5b83..0000000 --- a/src/Entity/SluggableInterface.php +++ /dev/null @@ -1,14 +0,0 @@ -slug; - } - - /** - * @param string $slug - * - * @return $this - * @deprecated - */ - public function setSlug(string $slug) { - $this->slug = StringUtil::toSlug($slug); - - return $this; - } - } \ No newline at end of file diff --git a/src/Entity/Strings/EndemicLifeStrings.php b/src/Entity/Strings/EndemicLifeStrings.php new file mode 100644 index 0000000..d94f0a7 --- /dev/null +++ b/src/Entity/Strings/EndemicLifeStrings.php @@ -0,0 +1,88 @@ +endemicLife = $endemicLife; + $this->language = $language; + } + + /** + * @return string|null + */ + public function getName(): ?string { + return $this->name; + } + + /** + * @param string|null $name + * + * @return $this + */ + public function setName(?string $name) { + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getDescription(): string { + return $this->description; + } + + /** + * @param string $description + * + * @return $this + */ + public function setDescription(string $description) { + $this->description = $description; + + return $this; + } + } \ No newline at end of file diff --git a/src/Entity/Strings/QuestStrings.php b/src/Entity/Strings/QuestStrings.php new file mode 100644 index 0000000..6f621ad --- /dev/null +++ b/src/Entity/Strings/QuestStrings.php @@ -0,0 +1,118 @@ +quest = $quest; + $this->language = $language; + } + + /** + * @return string + */ + public function getName(): string { + return $this->name; + } + + /** + * @param string $name + * + * @return $this + */ + public function setName(string $name) { + $this->name = $name; + + return $this; + } + + /** + * @return string + */ + public function getDescription(): string { + return $this->description; + } + + /** + * @param string $description + * + * @return $this + */ + public function setDescription(string $description) { + $this->description = $description; + + return $this; + } + + /** + * @return string|null + */ + public function getObjectName(): ?string { + return $this->objectName; + } + + /** + * @param string|null $objectName + * + * @return $this + */ + public function setObjectName(?string $objectName) { + $this->objectName = $objectName; + + return $this; + } + } \ No newline at end of file diff --git a/src/Entity/Strings/WorldEventStrings.php b/src/Entity/Strings/WorldEventStrings.php deleted file mode 100644 index c288300..0000000 --- a/src/Entity/Strings/WorldEventStrings.php +++ /dev/null @@ -1,141 +0,0 @@ -event = $event; - $this->language = $language; - } - - /** - * @return string - */ - public function getName(): string { - return $this->name; - } - - /** - * @param string $name - * - * @return $this - */ - public function setName(string $name) { - $this->name = $name; - - return $this; - } - - /** - * @return string|null - */ - public function getDescription(): ?string { - return $this->description; - } - - /** - * @param string|null $description - * - * @return $this - */ - public function setDescription(?string $description) { - $this->description = $description; - - return $this; - } - - /** - * @return string|null - */ - public function getRequirements(): ?string { - return $this->requirements; - } - - /** - * @param string|null $requirements - * - * @return $this - */ - public function setRequirements(?string $requirements) { - $this->requirements = $requirements; - - return $this; - } - - /** - * @return string|null - */ - public function getSuccessConditions(): ?string { - return $this->successConditions; - } - - /** - * @param string|null $successConditions - * - * @return $this - */ - public function setSuccessConditions(?string $successConditions) { - $this->successConditions = $successConditions; - - return $this; - } - } \ No newline at end of file diff --git a/src/Entity/WorldEvent.php b/src/Entity/WorldEvent.php index 38ae831..6a20177 100644 --- a/src/Entity/WorldEvent.php +++ b/src/Entity/WorldEvent.php @@ -1,29 +1,32 @@ quest = $quest; $this->type = $type; $this->expansion = $expansion; $this->platform = $platform; $this->startTimestamp = $startTimestamp; $this->endTimestamp = $endTimestamp; - $this->location = $location; - $this->questRank = $questRank; + } - $this->strings = new ArrayCollection(); + /** + * @return Quest + */ + public function getQuest(): Quest { + return $this->quest; } /** @@ -185,20 +150,6 @@ public function getEndTimestamp(): \DateTimeImmutable { return $this->endTimestamp; } - /** - * @return Location - */ - public function getLocation(): Location { - return $this->location; - } - - /** - * @return int - */ - public function getQuestRank(): int { - return $this->questRank; - } - /** * @return string|null */ @@ -216,40 +167,4 @@ public function setExclusive(?string $exclusive) { return $this; } - - /** - * @return bool - */ - public function isMasterRank(): bool { - return $this->masterRank; - } - - /** - * @param bool $masterRank - * - * @return $this - */ - public function setMasterRank(bool $masterRank) { - $this->masterRank = $masterRank; - - return $this; - } - - /** - * @return Collection|Selectable|WorldEventStrings[] - */ - public function getStrings(): Collection { - return $this->strings; - } - - /** - * @param string $language - * - * @return WorldEventStrings - */ - public function addStrings(string $language): EntityInterface { - $this->getStrings()->add($strings = new WorldEventStrings($this, $language)); - - return $strings; - } } \ No newline at end of file diff --git a/src/Game/EndemicLifeSpawnCondition.php b/src/Game/EndemicLifeSpawnCondition.php new file mode 100644 index 0000000..87292f1 --- /dev/null +++ b/src/Game/EndemicLifeSpawnCondition.php @@ -0,0 +1,14 @@ +getConstants(); - - return self::$types; - } } \ No newline at end of file diff --git a/src/Game/Quest/DeliveryType.php b/src/Game/Quest/DeliveryType.php new file mode 100644 index 0000000..f2ce4a0 --- /dev/null +++ b/src/Game/Quest/DeliveryType.php @@ -0,0 +1,14 @@ +getConstants()); - - return self::$ranks; - } } \ No newline at end of file diff --git a/src/Game/WorldEventType.php b/src/Game/WorldEventType.php index af3c694..569e972 100644 --- a/src/Game/WorldEventType.php +++ b/src/Game/WorldEventType.php @@ -1,30 +1,13 @@ getConstants(); - - return self::$types; - } } \ No newline at end of file diff --git a/src/Migrations/Version20200214023728.php b/src/Migrations/Version20200214023728.php new file mode 100644 index 0000000..c4607c9 --- /dev/null +++ b/src/Migrations/Version20200214023728.php @@ -0,0 +1,60 @@ +abortIf( + $this->connection->getDatabasePlatform()->getName() !== 'mysql', + 'Migration can only be executed safely on \'mysql\'.' + ); + + $this->addSql('CREATE TABLE quests (id INT UNSIGNED AUTO_INCREMENT NOT NULL, location_id INT UNSIGNED NOT NULL, endemic_life_id INT UNSIGNED DEFAULT NULL, item_id INT UNSIGNED DEFAULT NULL, objective VARCHAR(18) NOT NULL, type VARCHAR(18) NOT NULL, rank VARCHAR(6) NOT NULL, stars SMALLINT UNSIGNED NOT NULL, time_limit SMALLINT UNSIGNED NOT NULL, max_hunters SMALLINT UNSIGNED NOT NULL, max_faints SMALLINT UNSIGNED NOT NULL, delivery_type VARCHAR(12) DEFAULT NULL, amount SMALLINT UNSIGNED DEFAULT NULL, INDEX IDX_989E5D3464D218E (location_id), INDEX IDX_989E5D3420AAC085 (endemic_life_id), INDEX IDX_989E5D34126F525E (item_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE quest_monster_targets (id INT UNSIGNED AUTO_INCREMENT NOT NULL, quest_id INT UNSIGNED NOT NULL, monster_id INT UNSIGNED NOT NULL, amount SMALLINT UNSIGNED NOT NULL, INDEX IDX_EF4D0372209E9EF4 (quest_id), INDEX IDX_EF4D0372C5FF1223 (monster_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE quest_strings (id INT UNSIGNED AUTO_INCREMENT NOT NULL, quest_id INT UNSIGNED NOT NULL, name VARCHAR(128) NOT NULL, description LONGTEXT NOT NULL, object_name VARCHAR(64) DEFAULT NULL, language VARCHAR(7) NOT NULL, UNIQUE INDEX UNIQ_E0BDD7BE5E237E06 (name), INDEX IDX_E0BDD7BE209E9EF4 (quest_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE endemic_life_strings (id INT UNSIGNED AUTO_INCREMENT NOT NULL, endemic_life_id INT UNSIGNED NOT NULL, name VARCHAR(64) NOT NULL, description LONGTEXT DEFAULT NULL, language VARCHAR(7) NOT NULL, INDEX IDX_2CBF2B1C20AAC085 (endemic_life_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE endemic_life (id INT UNSIGNED AUTO_INCREMENT NOT NULL, type VARCHAR(12) NOT NULL, research_point_value SMALLINT UNSIGNED NOT NULL, spawn_conditions LONGTEXT NOT NULL COMMENT \'(DC2Type:json)\', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE endemic_life_locations (endemic_life_id INT UNSIGNED NOT NULL, location_id INT UNSIGNED NOT NULL, INDEX IDX_9981EB3C20AAC085 (endemic_life_id), INDEX IDX_9981EB3C64D218E (location_id), PRIMARY KEY(endemic_life_id, location_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE quests ADD CONSTRAINT FK_989E5D3464D218E FOREIGN KEY (location_id) REFERENCES locations (id)'); + $this->addSql('ALTER TABLE quests ADD CONSTRAINT FK_989E5D3420AAC085 FOREIGN KEY (endemic_life_id) REFERENCES endemic_life (id)'); + $this->addSql('ALTER TABLE quests ADD CONSTRAINT FK_989E5D34126F525E FOREIGN KEY (item_id) REFERENCES items (id)'); + $this->addSql('ALTER TABLE quest_monster_targets ADD CONSTRAINT FK_EF4D0372209E9EF4 FOREIGN KEY (quest_id) REFERENCES quests (id)'); + $this->addSql('ALTER TABLE quest_monster_targets ADD CONSTRAINT FK_EF4D0372C5FF1223 FOREIGN KEY (monster_id) REFERENCES monsters (id)'); + $this->addSql('ALTER TABLE quest_strings ADD CONSTRAINT FK_E0BDD7BE209E9EF4 FOREIGN KEY (quest_id) REFERENCES quests (id)'); + $this->addSql('ALTER TABLE endemic_life_strings ADD CONSTRAINT FK_2CBF2B1C20AAC085 FOREIGN KEY (endemic_life_id) REFERENCES endemic_life (id)'); + $this->addSql('ALTER TABLE endemic_life_locations ADD CONSTRAINT FK_9981EB3C20AAC085 FOREIGN KEY (endemic_life_id) REFERENCES endemic_life (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE endemic_life_locations ADD CONSTRAINT FK_9981EB3C64D218E FOREIGN KEY (location_id) REFERENCES locations (id) ON DELETE CASCADE'); + } + + public function down(Schema $schema): void { + // this down() migration is auto-generated, please modify it to your needs + $this->abortIf( + $this->connection->getDatabasePlatform()->getName() !== 'mysql', + 'Migration can only be executed safely on \'mysql\'.' + ); + + $this->addSql('ALTER TABLE quest_monster_targets DROP FOREIGN KEY FK_EF4D0372209E9EF4'); + $this->addSql('ALTER TABLE quest_strings DROP FOREIGN KEY FK_E0BDD7BE209E9EF4'); + $this->addSql('ALTER TABLE quests DROP FOREIGN KEY FK_989E5D3420AAC085'); + $this->addSql('ALTER TABLE endemic_life_strings DROP FOREIGN KEY FK_2CBF2B1C20AAC085'); + $this->addSql('ALTER TABLE endemic_life_locations DROP FOREIGN KEY FK_9981EB3C20AAC085'); + $this->addSql('DROP TABLE quests'); + $this->addSql('DROP TABLE quest_monster_targets'); + $this->addSql('DROP TABLE quest_strings'); + $this->addSql('DROP TABLE endemic_life_strings'); + $this->addSql('DROP TABLE endemic_life'); + $this->addSql('DROP TABLE endemic_life_locations'); + } + } diff --git a/src/Migrations/Version20200409195903.php b/src/Migrations/Version20200409195903.php new file mode 100644 index 0000000..7da6ab1 --- /dev/null +++ b/src/Migrations/Version20200409195903.php @@ -0,0 +1,45 @@ +abortIf( + $this->connection->getDatabasePlatform()->getName() !== 'mysql', + 'Migration can only be executed safely on \'mysql\'.' + ); + + $this->addSql('CREATE TABLE quest_rewards (id INT UNSIGNED AUTO_INCREMENT NOT NULL, quest_id INT UNSIGNED NOT NULL, item_id INT UNSIGNED NOT NULL, INDEX IDX_BD05A37C209E9EF4 (quest_id), INDEX IDX_BD05A37C126F525E (item_id), UNIQUE INDEX UNIQ_BD05A37C209E9EF4126F525E (quest_id, item_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE quest_reward_conditions (quest_reward_id INT UNSIGNED NOT NULL, reward_condition_id INT UNSIGNED NOT NULL, INDEX IDX_40FEEF82423BA179 (quest_reward_id), INDEX IDX_40FEEF82E0BFB5A3 (reward_condition_id), PRIMARY KEY(quest_reward_id, reward_condition_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE quest_rewards ADD CONSTRAINT FK_BD05A37C209E9EF4 FOREIGN KEY (quest_id) REFERENCES quests (id)'); + $this->addSql('ALTER TABLE quest_rewards ADD CONSTRAINT FK_BD05A37C126F525E FOREIGN KEY (item_id) REFERENCES items (id)'); + $this->addSql('ALTER TABLE quest_reward_conditions ADD CONSTRAINT FK_40FEEF82423BA179 FOREIGN KEY (quest_reward_id) REFERENCES quest_rewards (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE quest_reward_conditions ADD CONSTRAINT FK_40FEEF82E0BFB5A3 FOREIGN KEY (reward_condition_id) REFERENCES reward_conditions (id) ON DELETE CASCADE'); + $this->addSql('DROP TABLE world_event_strings'); + + // Delete all events to prevent `null` values in the `quest_id` column + $this->addSql('DELETE FROM world_events'); + + $this->addSql('ALTER TABLE world_events DROP FOREIGN KEY FK_C3B92A0964D218E'); + $this->addSql('DROP INDEX IDX_C3B92A0964D218E ON world_events'); + $this->addSql('ALTER TABLE world_events DROP quest_rank, DROP master_rank, DROP location_id, ADD quest_id INT UNSIGNED NOT NULL'); + $this->addSql('ALTER TABLE world_events ADD CONSTRAINT FK_C3B92A09209E9EF4 FOREIGN KEY (quest_id) REFERENCES quests (id)'); + $this->addSql('CREATE INDEX IDX_C3B92A09209E9EF4 ON world_events (quest_id)'); + } + + public function down(Schema $schema): void { + $this->throwIrreversibleMigrationException(); + } + } diff --git a/src/Repository/QuestRepository.php b/src/Repository/QuestRepository.php new file mode 100644 index 0000000..c92e2f4 --- /dev/null +++ b/src/Repository/QuestRepository.php @@ -0,0 +1,27 @@ +getEntityManager()->createQueryBuilder() + ->from(Quest::class, 'quest') + ->leftJoin('quest.strings', 'strings') + ->select('quest') + ->where('strings.language = :language') + ->andWhere('strings.name = :name') + ->setMaxResults(1) + ->setParameter('language', $language) + ->setParameter('name', $name) + ->getQuery() + ->getOneOrNullResult(); + } + } \ No newline at end of file diff --git a/src/Repository/WorldEventRepository.php b/src/Repository/WorldEventRepository.php deleted file mode 100644 index de91be1..0000000 --- a/src/Repository/WorldEventRepository.php +++ /dev/null @@ -1,38 +0,0 @@ -getEntityManager()->createQueryBuilder() - ->from(WorldEvent::class, 'e') - ->leftJoin('e.strings', 's') - ->select('e') - ->where('s.language = :language') - ->andWhere('s.name = :name') - ->andWhere('e.platform = :platform') - ->andWhere('e.startTimestamp = :start') - ->setMaxResults(1) - ->setParameter('language', $language) - ->setParameter('name', $name) - ->setParameter('platform', $platform) - ->setParameter('start', $startTimestamp) - ->getQuery() - ->getOneOrNullResult(); - } - } \ No newline at end of file diff --git a/src/WorldEvent/WorldEventReader.php b/src/WorldEvent/WorldEventReader.php index ec9f403..50f947d 100644 --- a/src/WorldEvent/WorldEventReader.php +++ b/src/WorldEvent/WorldEventReader.php @@ -1,40 +1,28 @@ [ - Expansion::BASE => 'http://game.capcom.com/world/%s/schedule.html', - Expansion::ICEBORNE => 'http://game.capcom.com/world/%s/schedule-master.html', + Expansion::BASE => 'http://game.capcom.com/world/us/schedule.html', + Expansion::ICEBORNE => 'http://game.capcom.com/world/us/schedule-master.html', ], PlatformType::PC => [ - Expansion::BASE => 'http://game.capcom.com/world/steam/%s/schedule.html', - Expansion::ICEBORNE => 'http://game.capcom.com/world/steam/%s/schedule-master.html', + Expansion::BASE => 'http://game.capcom.com/world/steam/us/schedule.html', + Expansion::ICEBORNE => 'http://game.capcom.com/world/steam/us/schedule-master.html', ], ]; - public const LANGUAGE_TAG_MAP = [ - LanguageTag::ENGLISH => 'us', - LanguageTag::FRENCH => 'fr', - LanguageTag::GERMAN => 'de', - LanguageTag::CHINESE_SIMPLIFIED => 'cn', - LanguageTag::CHINESE_TRADITIONAL => 'hk', - ]; - public const TABLE_TYPE_MAP = [ 'table1' => WorldEventType::KULVE_TAROTH, 'table2' => WorldEventType::EVENT_QUEST, @@ -46,11 +34,6 @@ class WorldEventReader { */ protected $entityManager; - /** - * @var HttpMethodsClient - */ - protected $client; - /** * WorldEventReader constructor. * @@ -58,220 +41,136 @@ class WorldEventReader { */ public function __construct(EntityManagerInterface $entityManager) { $this->entityManager = $entityManager; - $this->client = new HttpMethodsClient( - HttpClientDiscovery::find(), - MessageFactoryDiscovery::find() - ); } /** * @param string $platform * @param string $expansion - * @param int $sleepDuration * * @return \Generator|WorldEvent[] */ - public function read(string $platform, string $expansion, int $sleepDuration = 5): \Generator { - /** - * Holds new events, in the order they are parsed out of the initial page. Used to add non-English strings - * to events during parsing. - * - * @var WorldEvent[] $events - */ - $events = []; - - /** - * {@see LanguageTag::ENGLISH} is always loaded first in order to give us a stable event name to match - * against, so that languages added in future releases don't mess with event parsing. - */ - $languages = array_merge( - [LanguageTag::ENGLISH], - array_filter( - LanguageTag::values(), - function(string $language) { - return $language !== LanguageTag::ENGLISH; - } - ) - ); - + public function read(string $platform, string $expansion): \Generator { // Used to infer years for event terms during parsing. $currentTimestamp = new \DateTimeImmutable(); - foreach ($languages as $languageIndex => $language) { - $url = static::PLATFORM_TYPE_MAP[$platform][$expansion] ?? null; + $url = static::PLATFORM_TYPE_MAP[$platform][$expansion] ?? null; - if (!$url) { - throw new \InvalidArgumentException( - sprintf('No URL found for platform and expansion: %s, %s', $platform, $expansion ?? 'NULL') - ); - } - - $crawler = new Crawler(file_get_contents(sprintf($url, static::LANGUAGE_TAG_MAP[$language]))); - $timezoneOffsetNode = $crawler->filter('label[for=zoneSelect]'); - - // Pre-Iceborne events page contained a typo in the `for` attribute of the timezone selector. Until the PC - // event page is updated to the Iceborne layout, we'll need to fall back on the old `for` value. - if ($timezoneOffsetNode->count() === 0) - $timezoneOffsetNode = $crawler->filter('label[for=zoonSelect]'); - - $timezoneOffset = (int)$timezoneOffsetNode->attr('data-zone'); - - $offsetInterval = new \DateInterval( - 'PT' . abs($timezoneOffset) . 'H' + if (!$url) { + throw new \InvalidArgumentException( + sprintf('No URL found for platform and expansion: %s, %s', $platform, $expansion ?? 'NULL') ); + } - if ($timezoneOffset < 0) - $offsetInterval->invert = true; - - // Used to infer years for event terms during parsing. - $currentTimestamp = new \DateTimeImmutable(); + $crawler = new Crawler(file_get_contents($url)); - $tables = $crawler->filter('#schedule .tableArea > table'); - $eventIndex = 0; + $timezoneOffset = (int)$crawler->filter('label[for=zoneSelect]')->attr('data-zone'); + $offsetInterval = new \DateInterval( + 'PT' . abs($timezoneOffset) . 'H' + ); - for ($tableIndex = 0, $tablesLength = $tables->count(); $tableIndex < $tablesLength; $tableIndex++) { - $rows = $tables->eq($tableIndex)->filter('tbody > tr'); + if ($timezoneOffset < 0) + $offsetInterval->invert = true; - for ($i = 0, $ii = $rows->count(); $i < $ii; $i++) { - $row = $rows->eq($i); + $tables = $crawler->filter('#schedule .tableArea > table'); - $questInfo = $row->filter('td.quest'); - $popupItems = $questInfo->filter('.pop li > span'); + for ($tableIndex = 0, $tablesLength = $tables->count(); $tableIndex < $tablesLength; $tableIndex++) { + $rows = $tables->eq($tableIndex)->filter('tbody > tr'); - $name = trim($questInfo->filter('.title > span')->text()); + for ($i = 0, $ii = $rows->count(); $i < $ii; $i++) { + $row = $rows->eq($i); - $termStrings = preg_split( - '/(\d{2}-\d{2} \d{2}:\d{2} 〜 \d{2}-\d{2} \d{2}:\d{2})/', - mb_strtolower($questInfo->filter('.terms')->text()), - null, - PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE - ); + $questInfo = $row->filter('td.quest'); + $name = trim($questInfo->filter('.title > span')->text()); - /** @var \DateTimeImmutable[][] $terms */ - $terms = []; + $quest = $this->entityManager->getRepository(Quest::class) + ->findOneByName(LanguageTag::ENGLISH, $name); - foreach ($termStrings as $item) { - // Fix for duplicated values in terms node - if (isset($terms[$item])) - continue; + // TODO Need better handling for this, preferably via Discord notifications (or similar) + if (!$quest) + throw new \RuntimeException('Missing quest ' . $name); - $text = trim($item); + $termStrings = preg_split( + '/(\d{2}-\d{2} \d{2}:\d{2} 〜 \d{2}-\d{2} \d{2}:\d{2})/', + mb_strtolower($questInfo->filter('.terms')->text()), + null, + PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE + ); - if (!$text || !is_numeric($text[0])) - continue; + /** @var \DateTimeImmutable[][] $terms */ + $terms = []; - $text = str_replace('-', '/', $text); + foreach ($termStrings as $item) { + // Fix for duplicated values in terms node + if (isset($terms[$item])) + continue; - $start = (new \DateTime(substr($text, 0, 10), new \DateTimeZone('UTC'))) - ->sub($offsetInterval); + $text = trim($item); - if ($start->diff($currentTimestamp)->days >= 90) { - $start->setDate( - (int)$start->format('Y') - 1, - (int)$start->format('m'), - (int)$start->format('d') - ); - } + if (!$text || !is_numeric($text[0])) + continue; - $end = (new \DateTime(substr($text, 15), new \DateTimeZone('UTC'))) - ->sub($offsetInterval); + $text = str_replace('-', '/', $text); - if ($end->diff($currentTimestamp)->days >= 90) { - $end->setDate( - (int)$end->format('Y') + 1, - (int)$end->format('m'), - (int)$end->format('d') - ); - } + $start = (new \DateTime(substr($text, 0, 10), new \DateTimeZone('UTC'))) + ->sub($offsetInterval); - $terms[$item] = [ - \DateTimeImmutable::createFromMutable($start), - \DateTimeImmutable::createFromMutable($end), - ]; + if ($start->diff($currentTimestamp)->d >= 90) { + $start->setDate( + (int)$start->format('Y') - 1, + (int)$start->format('m'), + (int)$start->format('d') + ); } - foreach ($terms as $term) { - if (!isset($events[$eventIndex])) { - $event = $this->entityManager->getRepository(WorldEvent::class)->search( - $language, - $name, - $platform, - $term[0] - ); - - if (!$event) { - /** @var Location|null $location */ - $location = $this->entityManager->getRepository(Location::class)->findOneByName( - $language, - $locName = trim($popupItems->eq(0)->text()) - ); - - if (!$location) - throw new \Exception('Unrecognized location: ' . $locName); - - $rank = (int)preg_replace('/\D/', '', $row->filter('.level')->text()); - $exclusive = null; - - if ($row->filter('.image > .ps4')->count() > 0) - $exclusive = PlatformExclusivityType::PS4; - - $type = static::TABLE_TYPE_MAP[$tables->eq($tableIndex)->attr('class')]; - - if ($expansion === Expansion::ICEBORNE && $type === WorldEventType::KULVE_TAROTH) - $type = WorldEventType::SAFI_JIIVA; - - $event = new WorldEvent( - $type, - $expansion, - $platform, - $term[0], - $term[1], - $location, - $rank - ); - - $event->setMasterRank($expansion === Expansion::ICEBORNE); - - if ($exclusive) - $event->setExclusive($exclusive); + $end = (new \DateTime(substr($text, 15), new \DateTimeZone('UTC'))) + ->sub($offsetInterval); - yield $event; - } - - $events[$eventIndex] = $event; - } - - $event = $events[$eventIndex++]; - - if (!$event) - throw new \RuntimeException('No event found for index ' . $eventIndex); + if ($end->diff($currentTimestamp)->days >= 90) { + $end->setDate( + (int)$end->format('Y') + 1, + (int)$end->format('m'), + (int)$end->format('d') + ); + } - if (L10nUtil::findStrings($language, $event)) - continue; + $terms[$item] = [ + \DateTimeImmutable::createFromMutable($start), + \DateTimeImmutable::createFromMutable($end), + ]; + } - $strings = $event->addStrings($language); - $strings->setName($name); + foreach ($terms as $term) { + $event = $this->entityManager->getRepository(WorldEvent::class)->findOneBy( + [ + 'platform' => $platform, + 'startTimestamp' => $term[0], + 'quest' => $quest, + ] + ); - $description = str_replace("\r\n", "\n", trim($questInfo->filter('.txt')->text())); + if ($event) + continue; - if ($description) - $strings->setDescription($description); + $type = static::TABLE_TYPE_MAP[$tables->eq($tableIndex)->attr('class')]; - $successConditions = trim($popupItems->eq(2)->text()); + if ($expansion === Expansion::ICEBORNE && $type === WorldEventType::KULVE_TAROTH) + $type = WorldEventType::SAFI_JIIVA; - if ($successConditions) - $strings->setSuccessConditions($successConditions); + $event = new WorldEvent( + $quest, + $type, + $expansion, + $platform, + $term[0], + $term[1] + ); - $requirements = trim($popupItems->eq(1)->text()); + if ($row->filter('.image > .ps4')->count() > 0) + $event->setExclusive(PlatformExclusivityType::PS4); - if (strtolower($requirements) !== 'none') - $strings->setRequirements($requirements); - } + yield $event; } } - - if ($sleepDuration > 0 && $languageIndex + 1 < sizeof($languages)) - sleep($sleepDuration); } } }