diff --git a/app/Http/Controllers/Abstracts/AbstractUnitsController.php b/app/Http/Controllers/Abstracts/AbstractUnitsController.php index 520b6f0d..e1909137 100644 --- a/app/Http/Controllers/Abstracts/AbstractUnitsController.php +++ b/app/Http/Controllers/Abstracts/AbstractUnitsController.php @@ -94,7 +94,7 @@ public function index(Request $request, PlayerService $player, ObjectService $ob $amount = $planet->getObjectAmount($object->machine_name); // Check requirements of this building - $requirements_met = $objects->objectRequirementsMet($object->machine_name, $planet, $player); + $requirements_met = $objects->objectRequirementsMet($object->machine_name, $planet, $player, 0, false); // Check if the current planet has enough resources to build this building. $enough_resources = $planet->hasResources($objects->getObjectPrice($object->machine_name, $planet)); diff --git a/app/Services/BuildingQueueService.php b/app/Services/BuildingQueueService.php index 64b328b6..68297853 100644 --- a/app/Services/BuildingQueueService.php +++ b/app/Services/BuildingQueueService.php @@ -56,6 +56,23 @@ public function retrieveFinished(int $planet_id): Collection ->get(); } + /** + * Get building queue items + * + * @return Collection + */ + public function retrieveQueueItems(PlanetService $planet): Collection + { + // Fetch queue items from model + return BuildingQueue::where([ + ['planet_id', $planet->getPlanetId()], + ['processed', 0], + ['canceled', 0], + ]) + ->orderBy('time_start', 'asc') + ->get(); + } + /** * Add a building to the building queue for the current planet. * @@ -67,8 +84,6 @@ public function add(PlanetService $planet, int $building_id): void { $build_queue = $this->retrieveQueue($planet); - $building = $this->objects->getObjectById($building_id); - // Max amount of buildings that can be in the queue in a given time. // TODO: refactor throw exception into a more user-friendly message. if ($build_queue->isQueueFull()) { @@ -76,6 +91,8 @@ public function add(PlanetService $planet, int $building_id): void throw new Exception('Maximum number of items already in queue.'); } + $building = $this->objects->getObjectById($building_id); + // Check if user satisifes requirements to build this object. // TODO: refactor throw exception into a more user-friendly message. $requirements_met = $this->objects->objectRequirementsMet($building->machine_name, $planet, $planet->getPlayer()); @@ -113,14 +130,7 @@ public function add(PlanetService $planet, int $building_id): void */ public function retrieveQueue(PlanetService $planet): BuildingQueueListViewModel { - // Fetch queue items from model - $queue_items = BuildingQueue::where([ - ['planet_id', $planet->getPlanetId()], - ['processed', 0], - ['canceled', 0], - ]) - ->orderBy('time_start', 'asc') - ->get(); + $queue_items = $this->retrieveQueueItems($planet); // Convert to ViewModel array $list = array(); @@ -231,6 +241,14 @@ public function start(PlanetService $planet, int $time_start = 0): void continue; } + // Sanity check: check if the building requirements are still met. If not, + // then cancel build request. + if (!$this->objects->objectRequirementsMet($object->machine_name, $planet, $planet->getPlayer(), $queue_item->object_level_target, false)) { + $this->cancel($planet, $queue_item->id, $queue_item->object_id); + + continue; + } + // All OK, deduct resources and start building process. $planet->deductResources($price); @@ -277,26 +295,6 @@ public function cancel(PlanetService $planet, int $building_queue_id, int $build // If object is found: add canceled flag. if ($queue_item) { - // Gets all building queue records of this target level and all that - // come after it. So e.g. if user cancels build order for metal mine - // level 5 then any other already queued build orders for lvl 6,7,8 etc. - // will also be canceled. - $queue_items_higher_level = BuildingQueue::where([ - ['planet_id', $planet->getPlanetId()], - ['object_id', $building_id], - ['object_level_target', '>', $queue_item->object_level_target], - ['processed', 0], - ['canceled', 0], - ])->get(); - - // Add canceled flag to all entries with a higher level (if any). - foreach ($queue_items_higher_level as $queue_item_higher_level) { - $queue_item_higher_level->building = 0; - $queue_item_higher_level->canceled = 1; - - $queue_item_higher_level->save(); - } - // Give back resources if the current entry was already building. if ($queue_item->building === 1) { $planet->addResources(new Resources($queue_item->metal, $queue_item->crystal, $queue_item->deuterium, 0)); @@ -308,8 +306,62 @@ public function cancel(PlanetService $planet, int $building_queue_id, int $build $queue_item->save(); + // Check if requirements for all other items in the queue are still met. + // So e.g. if user cancels build order for metal mine + // level 5 then any other already queued build orders for lvl 6,7,8 etc. + // will also be canceled. Same applies to building requirements, + // if user cancels build order for robotics factory which is requirement + // for shipyard then shipyard will also be canceled. + // Requirements are checked only for building queue objects as + // unit queue objects cannot be canceled. + $this->cancelItemMissingRequirements($planet); + + $research_queue = resolve('OGame\Services\ResearchQueueService'); + $research_queue->cancelItemMissingRequirements($planet->getPlayer(), $planet); + // Set the next queue item to start (if applicable) $this->start($planet); } } + + /** + * Get is object in building queue + * + * @return bool + */ + public function objectInBuildingQueue(PlanetService $planet, string $machine_name, int $level): bool + { + $queue_items = $this->retrieveQueueItems($planet); + + foreach ($queue_items as $item) { + $object = $this->objects->getObjectById($item->object_id); + + if ($object->machine_name === $machine_name && $item->object_level_target === $level) { + return true; + } + } + + return false; + } + + /** + * Cancel first building queue item missing requirements. + * This function will be called recursively when it cancels the item. + * + * @return void + */ + public function cancelItemMissingRequirements(PlanetService $planet): void + { + $build_queue_items = $this->retrieveQueueItems($planet); + + foreach ($build_queue_items as $build_queue_item) { + $object = $this->objects->getObjectById($build_queue_item->object_id); + + if (!$this->objects->objectRequirementsMet($object->machine_name, $planet, $planet->getPlayer(), $build_queue_item->object_level_target)) { + $this->cancel($planet, $build_queue_item->id, $object->id); + break; + } + + } + } } diff --git a/app/Services/ObjectService.php b/app/Services/ObjectService.php index cbe710ad..0ba6272b 100644 --- a/app/Services/ObjectService.php +++ b/app/Services/ObjectService.php @@ -329,21 +329,41 @@ public function getBuildingObjectsWithStorage(): array * @param string $machine_name * @param PlanetService $planet * @param PlayerService $player + * @param int $level + * @param bool $queued * @return bool */ - public function objectRequirementsMet(string $machine_name, PlanetService $planet, PlayerService $player): bool + public function objectRequirementsMet(string $machine_name, PlanetService $planet, PlayerService $player, int $level = 0, bool $queued = true): bool { try { $object = $this->getObjectByMachineName($machine_name); + + // Check required prior levels + if ($level) { + if (!$this->objectLevelsMet($object, $planet, $player, $level, $queued)) { + return false; + } + } + foreach ($object->requirements as $requirement) { // Load required object and check if requirements are met. $object_required = $this->getObjectByMachineName($requirement->object_machine_name); + $check_queue = $queued; + + // Skip queue check for research lab as it must be present for research objects + if ($object_required->machine_name === 'research_lab') { + $check_queue = false; + } + if ($object_required->type === GameObjectType::Research) { - if ($player->getResearchLevel($object_required->machine_name) < $requirement->level) { + // Check if requirements are met with existing technology or with research items in build queue. + if ($player->getResearchLevel($object_required->machine_name) < $requirement->level && (!$check_queue || !$player->isResearchingTech($requirement->object_machine_name, $requirement->level))) { return false; } } else { - if ($planet->getObjectLevel($object_required->machine_name) < $requirement->level) { + // Check if requirements are met with existing buildings or with buildings in build queue. + // Building queue is checked only for building queue objects, not for unit queue objects. + if ($planet->getObjectLevel($object_required->machine_name) < $requirement->level && (!$check_queue || !$planet->isBuildingObject($requirement->object_machine_name, $requirement->level))) { return false; } } @@ -486,4 +506,46 @@ public function getObjectRawPrice(string $machine_name, int $level = 0): Resourc return new Resources($metal, $crystal, $deuterium, $energy); } + + /** + * Check if object prior level requirements are met (for building it). + * Prior levels can be already built or in queues + * + * @param GameObject $object + * @param PlanetService $planet + * @param PlayerService $player + * @param int $level + * @param bool $queued + * @return bool + */ + private function objectLevelsMet(GameObject $object, PlanetService $planet, PlayerService $player, int $level, bool $queued): bool + { + $current_level = 0; + + if ($object->type === GameObjectType::Research) { + $current_level = $planet->getPlayer()->getResearchLevel($object->machine_name); + } else { + $current_level = $planet->getObjectLevel($object->machine_name); + } + + // Check if target level is next level + if ($current_level + 1 === $level) { + return true; + } + + // Check if items in queues should be included or not + if (!$queued) { + // There are prior levels, but queue should not be included + return false; + } + + // Check prior levels from queues + for ($i = $current_level + 1; $i < $level; $i++) { + if (!$planet->isBuildingObject($object->machine_name, $i) && !$player->isResearchingTech($object->machine_name, $i)) { + return false; + } + } + + return true; + } } diff --git a/app/Services/PlanetService.php b/app/Services/PlanetService.php index ecc9ccea..c6464525 100644 --- a/app/Services/PlanetService.php +++ b/app/Services/PlanetService.php @@ -1459,6 +1459,24 @@ public function isBuilding(): bool return count($build_queue) > 0; } + /** + * Get is the current planet building the object or not + * + * @return bool + */ + public function isBuildingObject(string $machine_name, int $level): bool + { + $object = $this->objects->getObjectByMachineName($machine_name); + + // Check only building queue objects + if ($object->type !== GameObjectType::Building && $object->type !== GameObjectType::Station) { + return false; + } + + $build_queue = resolve(BuildingQueueService::class); + return $build_queue->objectInBuildingQueue($this, $machine_name, $level); + } + /** * Get building count from planet * diff --git a/app/Services/PlayerService.php b/app/Services/PlayerService.php index 1b97ccbf..200b2ef9 100644 --- a/app/Services/PlayerService.php +++ b/app/Services/PlayerService.php @@ -572,4 +572,15 @@ public function delete(): void // Delete the actual user. $this->user->delete(); } + + /** + * Get is the player researching the tech or not + * + * @return bool + */ + public function isResearchingTech(string $machine_name, int $level): bool + { + $research_queue = resolve('OGame\Services\ResearchQueueService'); + return $research_queue->objectInResearchQueue($this, $machine_name, $level); + } } diff --git a/app/Services/ResearchQueueService.php b/app/Services/ResearchQueueService.php index d71c9f52..2261676b 100644 --- a/app/Services/ResearchQueueService.php +++ b/app/Services/ResearchQueueService.php @@ -116,21 +116,29 @@ public function retrieveFinishedForUser(PlayerService $player): \Illuminate\Supp */ public function add(PlayerService $player, PlanetService $planet, int $research_object_id): void { - $build_queue = $this->retrieveQueue($planet); + $research_queue = $this->retrieveQueue($planet); - // Max amount of buildings that can be in the queue at a given time. - if ($build_queue->isQueueFull()) { - // Max amount of build queue items already exist, throw exception. + // Max amount of research items that can be in the queue at a given time. + // TODO: refactor throw exception into a more user-friendly message. + if ($research_queue->isQueueFull()) { + // Max amount of research queue items already exist, throw exception. throw new Exception('Maximum number of items already in queue.'); } $object = $this->objects->getResearchObjectById($research_object_id); + // Check if user satisifes requirements to research this object. + // TODO: refactor throw exception into a more user-friendly message. + $requirements_met = $this->objects->objectRequirementsMet($object->machine_name, $planet, $planet->getPlayer()); + if (!$requirements_met) { + throw new Exception('Requirements not met to build this object.'); + } + // @TODO: add checks that current logged in user is owner of planet - // and is able to add this object to the building queue. + // and is able to add this object to the research queue. $current_level = $player->getResearchLevel($object->machine_name); - // Check to see how many other items of this building there are already + // Check to see how many other items of this technology there are already // in the queue, because if so then the level needs to be higher than that. $amount = $this->activeBuildingQueueItemCount($player, $research_object_id); $next_level = $current_level + $amount + 1; @@ -148,7 +156,7 @@ public function add(PlayerService $player, PlanetService $planet, int $research_ } /** - * Retrieve current building build queue for a planet. + * Retrieve current research queue for a planet. * * @param PlanetService $planet * @return ResearchQueueListViewModel @@ -219,9 +227,9 @@ public function activeBuildingQueueItemCount(PlayerService $player, int $buildin } /** - * Start building the next item in the queue (if available). + * Start researching the next item in the queue (if available). * - * This actually starts the building process and deducts the resources + * This actually starts the research process and deducts the resources * from the planet. If there are not enough resources the build attempt * will fail. * @@ -229,7 +237,7 @@ public function activeBuildingQueueItemCount(PlayerService $player, int $buildin * * @param int $time_start * Optional parameter to indicate when the new item should start, this - * is used for when a few build queue items are finished at the exact + * is used for when a few research queue items are finished at the exact * same time, e.g. when a user closes its session and logs back in * after a while. * @return void @@ -253,17 +261,17 @@ public function start(PlayerService $player, int $time_start = 0): void $planet = $player->planets->childPlanetById($queue_item->planet_id); $object = $this->objects->getResearchObjectById($queue_item->object_id); - // See if the planet has enough resources for this build attempt. + // See if the planet has enough resources for this research attempt. $price = $this->objects->getObjectPrice($object->machine_name, $planet); - $build_time = $player->planets->current()->getTechnologyResearchTime($object->machine_name); + $research_time = $player->planets->current()->getTechnologyResearchTime($object->machine_name); - // Only start the queue item if there are no other queue items building + // Only start the queue item if there are no other queue items researching // for this planet. - $build_queue = $this->retrieveQueue($planet); - $currently_building = $build_queue->getCurrentlyBuildingFromQueue(); + $research_queue = $this->retrieveQueue($planet); + $currently_researching = $research_queue->getCurrentlyBuildingFromQueue(); - if ($currently_building !== null) { - // There already is something else building, don't start a new one. + if ($currently_researching !== null) { + // There already is something else researching, don't start a new one. break; } @@ -272,29 +280,37 @@ public function start(PlayerService $player, int $time_start = 0): void // is wrong. $current_level = $player->getResearchLevel($object->machine_name); if ($queue_item->object_level_target !== ($current_level + 1)) { - // Error, cancel build queue item. + // Error, cancel research queue item. $this->cancel($player, $queue_item->id, $queue_item->object_id); continue; } // Sanity check: check if the planet has enough resources. If not, - // then cancel build request. + // then cancel research request. if (!$planet->hasResources($price)) { - // Error, cancel build queue item. + // Error, cancel research queue item. + $this->cancel($player, $queue_item->id, $queue_item->object_id); + + continue; + } + + // Sanity check: check if the researching requirements are still met. If not, + // then cancel research request. + if (!$this->objects->objectRequirementsMet($object->machine_name, $planet, $player, $queue_item->object_level_target, false)) { $this->cancel($player, $queue_item->id, $queue_item->object_id); continue; } - // All OK, deduct resources and start building process. + // All OK, deduct resources and start researching process. $planet->deductResources($price); if (!$time_start) { $time_start = (int)Carbon::now()->timestamp; } - $queue_item->time_duration = (int)$build_time; + $queue_item->time_duration = (int)$research_time; $queue_item->time_start = $time_start; $queue_item->time_end = $queue_item->time_start + $queue_item->time_duration; $queue_item->building = 1; @@ -305,7 +321,7 @@ public function start(PlayerService $player, int $time_start = 0): void // If the calculated end time is lower than the current time, // we force that the planet is updated again which will grant - // the building immediately without having to wait for a refresh. + // the research immediately without having to wait for a refresh. if ($queue_item->time_end < Carbon::now()->timestamp) { $player->updateResearchQueue(); } @@ -313,22 +329,22 @@ public function start(PlayerService $player, int $time_start = 0): void } /** - * Cancels an active building queue record. + * Cancels an active research queue record. * * @param PlayerService $player - * @param int $building_queue_id - * @param int $building_id + * @param int $research_queue_id + * @param int $research_id * @throws Exception */ - public function cancel(PlayerService $player, int $building_queue_id, int $building_id): void + public function cancel(PlayerService $player, int $research_queue_id, int $research_id): void { $queue_item = $this->model ->join('planets', 'research_queues.planet_id', '=', 'planets.id') ->join('users', 'planets.user_id', '=', 'users.id') ->where([ ['users.id', $player->getId()], - ['research_queues.id', $building_queue_id], - ['object_id', $building_id], + ['research_queues.id', $research_queue_id], + ['object_id', $research_id], ['processed', 0], ['canceled', 0], ]) @@ -339,36 +355,18 @@ public function cancel(PlayerService $player, int $building_queue_id, int $build if ($queue_item) { // Typecast to a new object to avoid issues with the model. $queue_item = $queue_item instanceof ResearchQueue ? $queue_item : new ResearchQueue($queue_item->getAttributes()); - $planetService = $player->planets->childPlanetById($queue_item->planet_id); + $planet = $player->planets->childPlanetById($queue_item->planet_id); - // Gets all building queue records of this target level and all that + // Gets all research queue records of this target level and all that // come after it. So e.g. if user cancels build order for metal mine // level 5 then any other already queued build orders for lvl 6,7,8 etc. - // will also be canceled. - $queue_items_higher_level = $this->model - ->join('planets', 'research_queues.planet_id', '=', 'planets.id') - ->join('users', 'planets.user_id', '=', 'users.id') - ->where([ - ['users.id', $player->getId()], - ['object_id', $building_id], - ['object_level_target', '>', $queue_item->object_level_target], - ['processed', 0], - ['canceled', 0], - ]) - ->select('research_queues.*') - ->get(); - - // Add canceled flag to all entries with a higher level (if any). - foreach ($queue_items_higher_level as $queue_item_higher_level) { - $queue_item_higher_level->building = 0; - $queue_item_higher_level->canceled = 1; - - $queue_item_higher_level->save(); - } + // will also be canceled. Same applies to research requirements, + // if user cancels research order for Energy Technology which is requirement + // for Impulse Drive then Impulse Drive will also be canceled. // Give back resources if the current entry was already building. if ($queue_item->building === 1) { - $planetService->addResources(new Resources($queue_item->metal, $queue_item->crystal, $queue_item->deuterium, 0)); + $planet->addResources(new Resources($queue_item->metal, $queue_item->crystal, $queue_item->deuterium, 0)); } // Add canceled flag to the main entry. @@ -377,8 +375,73 @@ public function cancel(PlayerService $player, int $building_queue_id, int $build $queue_item->save(); + $this->cancelItemMissingRequirements($player, $planet); + + $build_queue = resolve('OGame\Services\BuildingQueueService'); + $build_queue->cancelItemMissingRequirements($planet); + // Set the next queue item to start (if applicable) $this->start($player); } } + + /** + * Get is object in research queue + * + * @return bool + */ + public function objectInResearchQueue(PlayerService $player, string $machine_name, int $level): bool + { + // Fetch queue items from model + $queue_items = $this->model + ->join('planets', 'research_queues.planet_id', '=', 'planets.id') + ->join('users', 'planets.user_id', '=', 'users.id') + ->where([ + ['users.id', $player->getId()], + ['research_queues.canceled', 0], + ]) + ->select('research_queues.*') + ->orderBy('research_queues.time_start', 'asc') + ->get(); + + foreach ($queue_items as $item) { + $object = $this->objects->getObjectById($item->object_id); + + if ($object->machine_name === $machine_name && $item->object_level_target === $level) { + return true; + } + } + + return false; + } + + /** + * Cancel first research queue item missing requirements. + * This function will be called recursively when it cancels the item. + * + * @return void + */ + public function cancelItemMissingRequirements(PlayerService $player, PlanetService $planet): void + { + // Fetch queue items from model + $research_queue_items = $this->model + ->join('planets', 'research_queues.planet_id', '=', 'planets.id') + ->join('users', 'planets.user_id', '=', 'users.id') + ->where([ + ['users.id', $player->getId()], + ['research_queues.canceled', 0], + ]) + ->select('research_queues.*') + ->orderBy('research_queues.time_start', 'asc') + ->get(); + + foreach ($research_queue_items as $research_queue_item) { + $object = $this->objects->getObjectById($research_queue_item->object_id); + + if (!$this->objects->objectRequirementsMet($object->machine_name, $planet, $player, $research_queue_item->object_level_target)) { + $this->cancel($player, $research_queue_item->id, $object->id); + break; + } + } + } } diff --git a/app/Services/UnitQueueService.php b/app/Services/UnitQueueService.php index 7b42f711..03c6d77d 100644 --- a/app/Services/UnitQueueService.php +++ b/app/Services/UnitQueueService.php @@ -34,7 +34,7 @@ class UnitQueueService private UnitQueue $model; /** - * BuildingQueue constructor. + * UnitQueueService constructor. */ public function __construct(ObjectService $objects) { @@ -44,7 +44,7 @@ public function __construct(ObjectService $objects) } /** - * Retrieve current building build queue for a planet. + * Retrieve current unit build queue for a planet. * * @param PlanetService $planet * @return UnitQueueListViewModel @@ -100,7 +100,7 @@ public function retrieveQueue(PlanetService $planet): UnitQueueListViewModel } /** - * Retrieve current building build queue for a planet. + * Retrieve current unit build queue for a planet. * * @param int $planet_id * @return Collection @@ -159,7 +159,7 @@ public function add(PlanetService $planet, int $object_id, int $requested_build_ $object = $this->objects->getUnitObjectById($object_id); // Check if user satisifes requirements to build this object. - $requirements_met = $this->objects->objectRequirementsMet($object->machine_name, $planet, $planet->getPlayer()); + $requirements_met = $this->objects->objectRequirementsMet($object->machine_name, $planet, $planet->getPlayer(), 0, false); // Sanity check: check if the planet has enough resources to build // the amount requested. If not, then adjust the ordered amount. diff --git a/tests/AccountTestCase.php b/tests/AccountTestCase.php index dbc63614..5ecb9ba2 100644 --- a/tests/AccountTestCase.php +++ b/tests/AccountTestCase.php @@ -384,7 +384,7 @@ protected function assertResourcesOnPage(TestResponse $response, Resources $reso } } - protected function assertObjectInQueue(TestResponse $response, string $machine_name, string $error_message = ''): void + protected function assertObjectInQueue(TestResponse $response, string $machine_name, int $level, string $error_message = ''): void { // Get object name from machine name. try { @@ -399,7 +399,7 @@ protected function assertObjectInQueue(TestResponse $response, string $machine_n if (!$responseContent) { $responseContent = ''; } - $condition1 = str_contains($responseContent, 'Cancel production of ' . $object->title); + $condition1 = str_contains($responseContent, 'Cancel production of ' . $object->title . ' level '. $level); $condition2 = str_contains($responseContent, 'do you really want to cancel ' . $object->title); $this->assertTrue($condition1 || $condition2, 'Neither of the expected texts were found in the response.'); } catch (Exception $e) { @@ -432,6 +432,70 @@ protected function assertObjectNotInQueue(TestResponse $response, string $machin } } + protected function assertEmptyBuildingQueue(TestResponse $response, string $error_message = ''): void + { + // Check if "no buildings being built" text is present on page. + try { + $responseContent = $response->getContent(); + if (!$responseContent) { + $responseContent = ''; + } + $condition = str_contains($responseContent, 'no building being built'); + $this->assertTrue($condition, 'expected text was not found in the response.'); + } catch (Exception $e) { + if (!empty($error_message)) { + $this->fail($error_message . '. Error: ' . $e->getMessage()); + } else { + $this->fail('Building queue is not empty. Error: ' . $e->getMessage()); + } + } + } + + protected function assertEmptyResearchQueue(TestResponse $response, string $error_message = ''): void + { + // Check if "no research done" text is present on page. + try { + $responseContent = $response->getContent(); + if (!$responseContent) { + $responseContent = ''; + } + $condition = str_contains($responseContent, 'no research done'); + $this->assertTrue($condition, 'expected text was not found in the response.'); + } catch (Exception $e) { + if (!empty($error_message)) { + $this->fail($error_message . '. Error: ' . $e->getMessage()); + } else { + $this->fail('Research queue is not empty. Error: ' . $e->getMessage()); + } + } + } + + protected function assertRequirementsNotMet(TestResponse $response, string $machine_name, string $error_message = ''): void + { + // Get object name from machine name. + try { + $object = $this->planetService->objects->getObjectByMachineName($machine_name); + } catch (Exception $e) { + $this->fail('Failed to get object by machine name: ' . $machine_name . '. Error: ' . $e->getMessage()); + } + + // Check if "Requirements are not met" text is present on page. + try { + $responseContent = $response->getContent(); + if (!$responseContent) { + $responseContent = ''; + } + $condition = str_contains($responseContent, $object->title.'
Requirements are not met!'); + $this->assertTrue($condition, 'expected text was not found in the response.'); + } catch (Exception $e) { + if (!empty($error_message)) { + $this->fail($error_message . '. Error: ' . $e->getMessage()); + } else { + $this->fail('Requirements are met. Error: ' . $e->getMessage()); + } + } + } + /** * Add a resource build request to the current users current planet. * @param string $machine_name diff --git a/tests/Feature/BuildQueueCancelTest.php b/tests/Feature/BuildQueueCancelTest.php index b23f0be3..0fcb9267 100644 --- a/tests/Feature/BuildQueueCancelTest.php +++ b/tests/Feature/BuildQueueCancelTest.php @@ -35,7 +35,7 @@ public function testBuildQueueCancelMultiple(): void Carbon::setTestNow($testTime); $response = $this->get('/resources'); - $this->assertObjectInQueue($response, 'metal_mine', 'Metal mine is expected in build queue but cannot be found.'); + $this->assertObjectInQueue($response, 'metal_mine', 3, 'Metal mine level 3 is expected in build queue but cannot be found.'); // Extract first and second number on page which looks like this where num1/num2 are ints: // "cancelProduction(num1,num2," @@ -95,7 +95,7 @@ public function testBuildQueueCancelRefundResources(): void $this->addResourceBuildRequest('metal_mine'); $response = $this->get('/resources'); - $this->assertObjectInQueue($response, 'metal_mine', 'Metal mine is not in build queue.'); + $this->assertObjectInQueue($response, 'metal_mine', 1, 'Metal mine level 1 is not in build queue.'); // Extract first and second number on page which looks like this where num1/num2 are ints: // "cancelProduction(num1,num2," @@ -149,7 +149,7 @@ public function testBuildQueueCancelSecondEntry(): void // Extract first and second number on page which looks like this where num1/num2 are ints: // "cancelbuilding(num1,num2," - $this->assertObjectInQueue($response, 'crystal_mine', 'Crystal mine is not in build queue.'); + $this->assertObjectInQueue($response, 'crystal_mine', 1, 'Crystal mine level 1 is not in build queue.'); // Extract the content from the response $pageContent = $response->getContent(); @@ -178,4 +178,51 @@ public function testBuildQueueCancelSecondEntry(): void $this->throwException(new BindingResolutionException('Less than two "cancelProduction" calls found.')); } } + + /** + * Tests building queue item is cancelled if requirements are not met. + */ + public function testCancelObjectMissingRequirements(): void + { + // Assert that build queue is empty + $response = $this->get('/facilities'); + $response->assertStatus(200); + + $this->assertEmptyBuildingQueue($response); + + // Add resource to build required facilities to planet + $this->planetAddResources(new Resources(5000, 5000, 5000, 0)); + + // Add required facilities to building queue + $this->addFacilitiesBuildRequest('robot_factory'); + $this->addFacilitiesBuildRequest('robot_factory'); + $this->addFacilitiesBuildRequest('shipyard'); + + $response = $this->get('/facilities'); + $this->assertObjectInQueue($response, 'shipyard', 1, 'Shipyard level 1 is not in build queue.'); + + // Extract the first and second number from the first cancelbuilding call + $cancelProductionCall = $response->getContent(); + if (empty($cancelProductionCall)) { + $cancelProductionCall = ''; + } + $cancelProductionCall = explode('onclick="cancelbuilding(', $cancelProductionCall); + $cancelProductionCall = explode(',', $cancelProductionCall[1]); + $number1 = (int)$cancelProductionCall[0]; + $number2 = (int)$cancelProductionCall[1]; + + // Check if both numbers are integers. If not, throw an exception. + if (empty($number1) || empty($number2)) { + throw new BindingResolutionException('Could not extract the building queue ID from the page.'); + } + + // Cancel Robotics Factory level 1, this will cancel also Robotics Factory level 2 and Shipyard level 1 + $this->cancelFacilitiesBuildRequest($number1, $number2); + + // Assert that building queue is empty + $response = $this->get('/facilities'); + $response->assertStatus(200); + + $this->assertEmptyBuildingQueue($response); + } } diff --git a/tests/Feature/BuildQueueTest.php b/tests/Feature/BuildQueueTest.php index 1305a7f2..2d59b788 100644 --- a/tests/Feature/BuildQueueTest.php +++ b/tests/Feature/BuildQueueTest.php @@ -221,6 +221,40 @@ public function testBuildQueueFailUnfulfilledRequirements(): void $this->assertObjectLevelOnPage($response, 'fusion_plant', 0, 'Fusion Reactor has been built while player has not satisfied building requirements.'); } + /** + * Verify that shipyard can be queued when robotics factory is in queue. + * @throws Exception + */ + public function testBuildQueueFacilitiesShipyardQueuedRequirements(): void + { + // Set the current time to a specific moment for testing + $testTime = Carbon::create(2024, 1, 1, 12, 0, 0); + Carbon::setTestNow($testTime); + + // Add resource to build required facilities to planet + $this->planetAddResources(new Resources(5000, 5000, 5000, 0)); + + // Assert that building requirements for Shipyard are not met as Robotics Factory is missing + $response = $this->get('/facilities'); + $response->assertStatus(200); + $this->assertRequirementsNotMet($response, 'shipyard', 'Shipyard building requirements not met.'); + + // Add Robotics Factory level 1 and 2 to build queue + $this->addFacilitiesBuildRequest('robot_factory'); + $this->addFacilitiesBuildRequest('robot_factory'); + + // Add Shipyard level 1 to queue + $this->addFacilitiesBuildRequest('shipyard'); + + // Verify the research is finished 10 minute later. + $testTime = Carbon::create(2024, 1, 1, 12, 10, 0); + Carbon::setTestNow($testTime); + + $response = $this->get('/facilities'); + $response->assertStatus(200); + $this->assertObjectLevelOnPage($response, 'shipyard', 1, 'Shipyard is not at level one 10 minutes after build request issued.'); + } + /** * Verify that building construction time is calculated correctly (higher than 0) * @throws Exception diff --git a/tests/Feature/ResearchQueueCancelTest.php b/tests/Feature/ResearchQueueCancelTest.php index 6cb5875c..04da5b1b 100644 --- a/tests/Feature/ResearchQueueCancelTest.php +++ b/tests/Feature/ResearchQueueCancelTest.php @@ -3,6 +3,7 @@ namespace Tests\Feature; use Exception; +use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Support\Carbon; use Illuminate\Testing\TestResponse; use OGame\Models\Resources; @@ -39,7 +40,7 @@ public function testResearchQueueCancelMultiple(): void Carbon::setTestNow($testTime); $response = $this->get('/research'); - $this->assertObjectInQueue($response, 'energy_technology', 'Energy Technology is expected in build queue but cannot be found.'); + $this->assertObjectInQueue($response, 'energy_technology', 3, 'Energy Technology level 3 is expected in build queue but cannot be found.'); $this->pressCancelButtonOnPage($response); @@ -81,7 +82,7 @@ public function testResearchQueueCancelRefundResources(): void $response = $this->get('/research'); $response->assertStatus(200); - $this->assertObjectInQueue($response, 'energy_technology', 'Energy Technology is not in build queue.'); + $this->assertObjectInQueue($response, 'energy_technology', 1, 'Energy Technology level 1 is not in build queue.'); $this->pressCancelButtonOnPage($response); @@ -116,7 +117,7 @@ public function testBuildQueueCancelSecondEntry(): void $response = $this->get('/research'); $response->assertStatus(200); - $this->assertObjectInQueue($response, 'computer_technology', 'Computer Technology is not in build queue.'); + $this->assertObjectInQueue($response, 'computer_technology', 1, 'Computer Technology level 1 is not in build queue.'); // Extract the content from the response $pageContent = $response->getContent(); @@ -208,4 +209,50 @@ private function pressCancelButtonOnPage(TestResponse $response): void // Do POST to cancel build queue item: $this->cancelResearchBuildRequest($number1, $number2); } + + /** + * Tests research queue item is cancelled if requirements are not met. + */ + public function testCancelResearchMissingRequirements(): void + { + // Assert that research queue is empty + $response = $this->get('/research'); + $response->assertStatus(200); + + $this->assertEmptyResearchQueue($response); + + // Set facilities and add resources to planet that test requires. + $this->planetSetObjectLevel('research_lab', 2); + $this->planetAddResources(new Resources(5000, 5000, 5000, 0)); + + $this->addResearchBuildRequest('energy_technology'); + $this->addResearchBuildRequest('impulse_drive'); + + $response = $this->get('/research'); + $this->assertObjectInQueue($response, 'impulse_drive', 1, 'Impulse Drive 1 is not in research queue.'); + + // Extract the first and second number from the first cancelbuilding call + $cancelProductionCall = $response->getContent(); + if (empty($cancelProductionCall)) { + $cancelProductionCall = ''; + } + $cancelProductionCall = explode('onclick="cancelbuilding(', $cancelProductionCall); + $cancelProductionCall = explode(',', $cancelProductionCall[1]); + $number1 = (int)$cancelProductionCall[0]; + $number2 = (int)$cancelProductionCall[1]; + + // Check if both numbers are integers. If not, throw an exception. + if (empty($number1) || empty($number2)) { + throw new BindingResolutionException('Could not extract the building queue ID from the page.'); + } + + // Cancel Energy technology level 1, this will cancel also Impulse Drive level 1 + $this->cancelResearchBuildRequest($number1, $number2); + + // Assert that building queue is empty + $response = $this->get('/research'); + $response->assertStatus(200); + + $this->assertEmptyResearchQueue($response); + } } diff --git a/tests/Feature/ResearchQueueTest.php b/tests/Feature/ResearchQueueTest.php index 4b6ba031..61a866fb 100644 --- a/tests/Feature/ResearchQueueTest.php +++ b/tests/Feature/ResearchQueueTest.php @@ -109,6 +109,7 @@ public function testResearchQueueMultiQueue(): void Carbon::setTestNow($testTime); // Check if one of the research items is finished and is now level 1. + $this->planetService->getPlayer()->updateResearchQueue(); $response = $this->get('/research'); $response->assertStatus(200); $this->assertObjectLevelOnPage($response, 'energy_technology', 1, 'Energy technology is not at level one 15 minutes after build request issued.'); @@ -154,7 +155,7 @@ public function testResearchQueueCancelRefundResources(): void // Assert that resources have been actually deducted $this->assertResourcesOnPage($response, new Resources(0, 500, 0, 0)); // Assert that the research is in the queue - $this->assertObjectInQueue($response, 'energy_technology', 'Energy Technology is not in build queue.'); + $this->assertObjectInQueue($response, 'energy_technology', 1, 'Energy Technology level 1 is not in build queue.'); // Extract first and second number on page which looks like this where num1/num2 are ints: // "cancelProduction(num1,num2," @@ -197,4 +198,45 @@ public function testResearchProductionTime(): void $research_time = $this->planetService->getTechnologyResearchTime('energy_technology'); $this->assertGreaterThan(0, $research_time); } + + /** + * Verify that research lab requirement is working for research objects. + * @throws Exception + */ + public function testResearchLabRequirement(): void + { + // Set the current time to a specific moment for testing + $testTime = Carbon::create(2024, 1, 1, 12, 0, 0); + Carbon::setTestNow($testTime); + + // Add required resources for research to planet + $this->planetAddResources(new Resources(5000, 5000, 5000, 0)); + + // Assert that research requirements for Energy Technology are not met as Research Lab is missing + $response = $this->get('/research'); + $response->assertStatus(200); + $this->assertRequirementsNotMet($response, 'energy_technology', 'Energy Technology research requirements not met.'); + + // Add Research Lab level 1 to build queue + $this->addFacilitiesBuildRequest('research_lab'); + + // Assert that research requirements for Energy Technology are not met as Research Lab is in build queue + $response = $this->get('/research'); + $response->assertStatus(200); + $this->assertRequirementsNotMet($response, 'energy_technology', 'Energy Technology research requirements not met.'); + + // Verify that Energy Technology can be added to research queue 2 minute later. + $testTime = Carbon::create(2024, 1, 1, 12, 2, 0); + Carbon::setTestNow($testTime); + + $this->addResearchBuildRequest('energy_technology'); + + // Verify the research is finished 2 minute later. + $testTime = Carbon::create(2024, 1, 1, 12, 4, 0); + Carbon::setTestNow($testTime); + + $response = $this->get('/research'); + $response->assertStatus(200); + $this->assertObjectLevelOnPage($response, 'energy_technology', 1, 'Energy technology is not at level one 2 minutes after build request issued.'); + } } diff --git a/tests/Feature/UnitQueueTest.php b/tests/Feature/UnitQueueTest.php index dadd5bf4..bdb07bab 100644 --- a/tests/Feature/UnitQueueTest.php +++ b/tests/Feature/UnitQueueTest.php @@ -374,6 +374,115 @@ public function testUnitQueueInsufficientResources(): void $this->assertResourcesOnPage($response, new Resources(15500, 5500, 0, 0)); } + /** + * Verify that shipyard requirement is working for unit objects. + * @throws Exception + */ + public function testUnitQueueShipyardRequirement(): void + { + // Set the current time to a specific moment for testing + $testTime = Carbon::create(2024, 1, 1, 12, 0, 0); + Carbon::setTestNow($testTime); + + // Add required resources to planet + $this->planetAddResources(new Resources(5000, 5000, 5000, 0)); + + // Assert that build requirements for Solar Satellite are not met as Shipyard is missing + $response = $this->get('/shipyard'); + $response->assertStatus(200); + $this->assertRequirementsNotMet($response, 'solar_satellite', 'Solar Satellite build requirements are met.'); + + // Add Shipyard level 1 with requisities to build queue + $this->addFacilitiesBuildRequest('robot_factory'); + $this->addFacilitiesBuildRequest('robot_factory'); + $this->addFacilitiesBuildRequest('shipyard'); + + // Assert that build requirements for Solar Satellite are not met as Shipyard is in build queue + $response = $this->get('/shipyard'); + $response->assertStatus(200); + $this->assertRequirementsNotMet($response, 'solar_satellite', 'Solar Satellite build requirements are met.'); + + // Verify that Solar Satellite can be added to unit queue 10 minute later. + $testTime = Carbon::create(2024, 1, 1, 12, 10, 0); + Carbon::setTestNow($testTime); + + $this->addShipyardBuildRequest('solar_satellite', 1); + + // Verify the building is finished 10 minute later. + $testTime = Carbon::create(2024, 1, 1, 12, 20, 0); + Carbon::setTestNow($testTime); + + $response = $this->get('/shipyard'); + $response->assertStatus(200); + $this->assertObjectLevelOnPage($response, 'solar_satellite', 1, 'Solar Satellite build job is not finished yet 10 minute after build request issued.'); + } + + /** + * Verify that research requirements are working for unit objects. + * @throws Exception + */ + public function testUnitQueueResearchRequirement(): void + { + // Set the current time to a specific moment for testing + $testTime = Carbon::create(2024, 1, 1, 12, 0, 0); + Carbon::setTestNow($testTime); + + // Add required resources to planet + $this->planetAddResources(new Resources(5000, 5000, 5000, 0)); + + // Assert that build requirements for Light Fighter are not met + $response = $this->get('/shipyard'); + $response->assertStatus(200); + $this->assertRequirementsNotMet($response, 'light_fighter', 'Light Fighter build requirements are met.'); + + // Add Shipyard and Research Lab to build queue + $this->addFacilitiesBuildRequest('robot_factory'); + $this->addFacilitiesBuildRequest('robot_factory'); + $this->addFacilitiesBuildRequest('shipyard'); + $this->addFacilitiesBuildRequest('research_lab'); + + // Verify the building is finished 10 minute later. + $testTime = Carbon::create(2024, 1, 1, 12, 10, 0); + Carbon::setTestNow($testTime); + + $response = $this->get('/facilities'); + $response->assertStatus(200); + $this->assertObjectLevelOnPage($response, 'research_lab', 1, 'Research Lab build job is not finished yet 10 minute after build request issued.'); + + // Assert that build requirements for Light Fighter are not met as Combustion Drive technology is missing + $response = $this->get('/shipyard'); + $response->assertStatus(200); + $this->assertRequirementsNotMet($response, 'light_fighter', 'Light Fighter build requirements are met.'); + + // Add required technology to research queue + $this->addResearchBuildRequest('energy_technology'); + $this->addResearchBuildRequest('combustion_drive'); + + // Assert that build requirements for Light Fighter are not met as Combustion Drive technology is in build queue + $response = $this->get('/shipyard'); + $response->assertStatus(200); + $this->assertRequirementsNotMet($response, 'light_fighter', 'Light Fighter build requirements are met.'); + + // Verify the research is finished 10 minute later. + $testTime = Carbon::create(2024, 1, 1, 12, 20, 0); + Carbon::setTestNow($testTime); + + $this->planetService->getPlayer()->updateResearchQueue(); + $response = $this->get('/research'); + $response->assertStatus(200); + $this->assertObjectLevelOnPage($response, 'combustion_drive', 1, 'Combustion Drive is not at level one 10 minutes after build request issued.'); + + $this->addShipyardBuildRequest('light_fighter', 1); + + // Verify the building is finished 10 minute later. + $testTime = Carbon::create(2024, 1, 1, 12, 30, 0); + Carbon::setTestNow($testTime); + + $response = $this->get('/shipyard'); + $response->assertStatus(200); + $this->assertObjectLevelOnPage($response, 'light_fighter', 1, 'Light Fighter build job is not finished yet 10 minute after build request issued.'); + } + /** * Verify that unit construction time is calculated correctly (higher than 0) * @throws BindingResolutionException diff --git a/tests/Unit/ObjectServiceTest.php b/tests/Unit/ObjectServiceTest.php index 63cb69ab..575f6040 100644 --- a/tests/Unit/ObjectServiceTest.php +++ b/tests/Unit/ObjectServiceTest.php @@ -7,26 +7,29 @@ class ObjectServiceTest extends UnitTestCase { + /** + * Tests maximum building amount returns correct value. + */ public function testGetObjectMaxBuildAmount(): void { - $objectService = new ObjectService(); + $object_service = new ObjectService(); $this->createAndSetPlanetModel([]); - // Test with requirement not met - $maxBuildAmount = $objectService->getObjectMaxBuildAmount('plasma_turret', $this->planetService, false); - $this->assertEquals(0, $maxBuildAmount); + // Test with requirements not met + $max_build_amount = $object_service->getObjectMaxBuildAmount('plasma_turret', $this->planetService, false); + $this->assertEquals(0, $max_build_amount); - // Test with object limited to one instance - $maxBuildAmount = $objectService->getObjectMaxBuildAmount('small_shield_dome', $this->planetService, true); - $this->assertEquals(1, $maxBuildAmount); + // Test with object limited to one instance per user + $max_build_amount = $object_service->getObjectMaxBuildAmount('small_shield_dome', $this->planetService, true); + $this->assertEquals(1, $max_build_amount); $this->createAndSetPlanetModel([ 'small_shield_dome' => 1, ]); // Test with object limited to one instance which already exists - $maxBuildAmount = $objectService->getObjectMaxBuildAmount('small_shield_dome', $this->planetService, true); - $this->assertEquals(0, $maxBuildAmount); + $max_build_amount = $object_service->getObjectMaxBuildAmount('small_shield_dome', $this->planetService, true); + $this->assertEquals(0, $max_build_amount); $this->createAndSetPlanetModel([ 'metal' => 24000, @@ -34,7 +37,7 @@ public function testGetObjectMaxBuildAmount(): void ]); // Test it calculates max amount correctly - $maxBuildAmount = $objectService->getObjectMaxBuildAmount('anti_ballistic_missile', $this->planetService, true); - $this->assertEquals(3, $maxBuildAmount); + $max_build_amount = $object_service->getObjectMaxBuildAmount('anti_ballistic_missile', $this->planetService, true); + $this->assertEquals(3, $max_build_amount); } } diff --git a/tests/Unit/PlanetServiceTest.php b/tests/Unit/PlanetServiceTest.php index d6b2691c..5c143fda 100644 --- a/tests/Unit/PlanetServiceTest.php +++ b/tests/Unit/PlanetServiceTest.php @@ -3,6 +3,7 @@ namespace Tests\Unit; use Illuminate\Contracts\Container\BindingResolutionException; +use OGame\Models\BuildingQueue; use OGame\Models\Enums\ResourceType; use OGame\Models\Resources; use Tests\UnitTestCase; @@ -192,4 +193,23 @@ public function testGetPlanetBuildingCount(): void $this->assertEquals(150, $this->planetService->getBuildingCount()); } + + /** + * Tests object building queue status. + */ + public function testIsBuildingObject(): void + { + $this->createAndSetPlanetModel([ + 'id' => 1, + ]); + + // Add level 3 shipyard to building queue + $queue = new BuildingQueue(); + $queue->planet_id = $this->planetService->getPlanetId(); + $queue->object_id = 21; + $queue->object_level_target = 3; + $queue->save(); + + $this->assertTrue($this->planetService->isBuildingObject('shipyard', 3)); + } }