From 57fbf031512f502233d5bf4ba64f5f2bafa0d105 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 19 Jul 2026 22:15:57 +0000 Subject: [PATCH 1/3] Fix building DM halve showing Halve again after first use (#1331) Mirror shipyard queue behavior: track dm_halved on building queues, show Complete after the first halve, and add completeBuilding endpoint. Co-authored-by: Cursor --- app/Http/Controllers/FacilitiesController.php | 47 +++++++ app/Models/BuildingQueue.php | 2 + app/Services/BuildingQueueService.php | 1 + app/Services/HalvingService.php | 119 ++++++++++++++++++ .../Queue/BuildingQueueViewModel.php | 4 +- ...00001_add_dm_fields_to_building_queues.php | 28 +++++ .../buildqueue/building-active.blade.php | 40 ++++-- routes/web.php | 1 + tests/Unit/HalvingServiceTest.php | 78 ++++++++++++ 9 files changed, 307 insertions(+), 13 deletions(-) create mode 100644 database/migrations/2026_07_19_000001_add_dm_fields_to_building_queues.php diff --git a/app/Http/Controllers/FacilitiesController.php b/app/Http/Controllers/FacilitiesController.php index d6e7e0ed4..1ab868cc6 100644 --- a/app/Http/Controllers/FacilitiesController.php +++ b/app/Http/Controllers/FacilitiesController.php @@ -147,6 +147,53 @@ public function halveBuilding(Request $request, PlayerService $player, HalvingSe } } + /** + * Complete a building queue item instantly using Dark Matter. + * + * @param Request $request + * @param PlayerService $player + * @param HalvingService $halvingService + * @return JsonResponse + */ + public function completeBuilding(Request $request, PlayerService $player, HalvingService $halvingService): JsonResponse + { + try { + $queueItemId = (int)$request->input('queue_item_id'); + + if ($queueItemId <= 0) { + return response()->json([ + 'success' => false, + 'error' => true, + 'message' => 'Invalid queue item ID', + 'newAjaxToken' => csrf_token(), + ]); + } + + $result = $halvingService->completeBuilding( + $player->getUser(), + $queueItemId, + $player->planets->current() + ); + + session()->flash('success', __('You have successfully accelerated the order.')); + + return response()->json([ + 'success' => true, + 'error' => false, + 'newAjaxToken' => csrf_token(), + 'cost' => $result['cost'], + 'new_balance' => $result['new_balance'], + ]); + } catch (Exception $e) { + return response()->json([ + 'success' => false, + 'error' => true, + 'message' => $e->getMessage(), + 'newAjaxToken' => csrf_token(), + ]); + } + } + /** * Start repairs for the wreck field. * diff --git a/app/Models/BuildingQueue.php b/app/Models/BuildingQueue.php index fec45b424..3349731b9 100644 --- a/app/Models/BuildingQueue.php +++ b/app/Models/BuildingQueue.php @@ -22,6 +22,8 @@ * @property float $deuterium * @property int $building * @property int $processed + * @property int $dm_halved + * @property int $dm_completed * @property int $canceled * @property Carbon|null $created_at * @property Carbon|null $updated_at diff --git a/app/Services/BuildingQueueService.php b/app/Services/BuildingQueueService.php index 1b9d9a3a8..00f735803 100644 --- a/app/Services/BuildingQueueService.php +++ b/app/Services/BuildingQueueService.php @@ -282,6 +282,7 @@ public function retrieveQueue(PlanetService $planet): BuildingQueueListViewModel $item['building'], $item['object_level_target'], $is_downgrade, + (bool)($item['dm_halved'] ?? false), ); $list[] = $viewModel; diff --git a/app/Services/HalvingService.php b/app/Services/HalvingService.php index 71134d9eb..015033e83 100644 --- a/app/Services/HalvingService.php +++ b/app/Services/HalvingService.php @@ -170,6 +170,10 @@ public function halveBuilding(User $user, int $queueItemId, PlanetService $plane throw new Exception('Queue item not found or already completed'); } + if ($queueItem->dm_halved) { + throw new Exception('Queue item has already been halved. Use Complete to finish instantly.'); + } + // Calculate new time values // Cost is based on remaining time, reduction is 50% of original time $timeValues = $this->calculateNewTimeValues( @@ -201,6 +205,7 @@ public function halveBuilding(User $user, int $queueItemId, PlanetService $plane // Update queue item time_end $queueItem->time_end = $timeValues['new_time_end']; + $queueItem->dm_halved = 1; $queueItem->save(); return [ @@ -215,6 +220,93 @@ public function halveBuilding(User $user, int $queueItemId, PlanetService $plane return $result; } + /** + * Complete a building queue item instantly using Dark Matter. + * + * This is used when the building has already been halved once. Instead of halving + * again, the queue is completed immediately. + * + * @param User $user The user performing the completion + * @param int $queueItemId The building queue item ID + * @param PlanetService $planet The planet service + * @return array{success: bool, cost: int, new_balance: int} + * @throws Exception If insufficient Dark Matter or invalid queue item + */ + public function completeBuilding(User $user, int $queueItemId, PlanetService $planet): array + { + /** @var array{success: bool, cost: int, new_balance: int} $result */ + $result = DB::transaction(function () use ($user, $queueItemId, $planet) { + // Lock user row for Dark Matter balance + $lockedUser = User::where('id', $user->id)->lockForUpdate()->first(); + if (!$lockedUser) { + throw new Exception('User not found'); + } + + // Lock and retrieve queue item + $queueItem = BuildingQueue::where('id', $queueItemId) + ->where('planet_id', $planet->getPlanetId()) + ->where('processed', 0) + ->where('canceled', 0) + ->where('building', 1) + ->lockForUpdate() + ->first(); + + if (!$queueItem) { + throw new Exception('Queue item not found or already completed'); + } + + // Calculate cost based on remaining time + $currentTime = $this->getCurrentTimestamp(); + $remainingTime = (int)$queueItem->time_end - $currentTime; + + if ($remainingTime <= 0) { + throw new Exception('Queue item already completed'); + } + + $cost = $this->calculateHalvingCost($remainingTime, 'building'); + + // Check balance + if ($lockedUser->dark_matter < $cost) { + throw new Exception("Insufficient Dark Matter. Required: {$cost}, Available: {$lockedUser->dark_matter}"); + } + + // Debit Dark Matter + $lockedUser->dark_matter -= $cost; + $lockedUser->save(); + + // Record transaction + $object = ObjectService::getObjectById($queueItem->object_id); + $description = "Completing building: {$object->title} on planet {$planet->getPlanetName()} (ID: {$planet->getPlanetId()})"; + $this->transactionService->recordTransaction( + $lockedUser, + -$cost, + DarkMatterTransactionType::HALVING->value, + $description, + $lockedUser->dark_matter + ); + + $originalTimeEnd = (int)$queueItem->time_end; + + // Set time_end to now so the building queue processor grants the level immediately. + $queueItem->time_end = $currentTime; + if ((int)$queueItem->time_start > $currentTime) { + $queueItem->time_start = $currentTime; + } + $queueItem->dm_completed = 1; + $queueItem->save(); + + $this->updateFutureBuildingQueueItems($planet->getPlanetId(), $originalTimeEnd, $currentTime); + + return [ + 'success' => true, + 'cost' => $cost, + 'new_balance' => $lockedUser->dark_matter, + ]; + }); + + return $result; + } + /** * Halve a research queue item. * @@ -627,4 +719,31 @@ private function updateFutureQueueItems(int $planetId, int $oldTimeEnd, int $new 'time_end' => DB::raw("time_end - {$timeReduction}"), ]); } + + /** + * Update future building queue items for this planet to advance their time_start and time_end + * equally based on a issued halving/completing time reduction. + * + * @param int $planetId Planet whose building queue should be updated + * @param int $oldTimeEnd Original time_end of the modified item (before the change) + * @param int $newTimeEnd New time_end of the modified item (after the change) + * @return void + */ + private function updateFutureBuildingQueueItems(int $planetId, int $oldTimeEnd, int $newTimeEnd): void + { + if ($newTimeEnd >= $oldTimeEnd) { + return; + } + + $timeReduction = $oldTimeEnd - $newTimeEnd; + + BuildingQueue::where('planet_id', $planetId) + ->where('processed', 0) + ->where('canceled', 0) + ->where('time_start', '>=', $oldTimeEnd) + ->update([ + 'time_start' => DB::raw("time_start - {$timeReduction}"), + 'time_end' => DB::raw("time_end - {$timeReduction}"), + ]); + } } diff --git a/app/ViewModels/Queue/BuildingQueueViewModel.php b/app/ViewModels/Queue/BuildingQueueViewModel.php index fd0197801..449b5dc0b 100644 --- a/app/ViewModels/Queue/BuildingQueueViewModel.php +++ b/app/ViewModels/Queue/BuildingQueueViewModel.php @@ -17,6 +17,7 @@ class BuildingQueueViewModel extends QueueViewModel * @param bool $building * @param int $level_target * @param bool $is_downgrade + * @param bool $dm_halved */ public function __construct( int $id, @@ -25,7 +26,8 @@ public function __construct( int $time_total, public bool $building, public int $level_target, - public bool $is_downgrade = false + public bool $is_downgrade = false, + public bool $dm_halved = false, ) { parent::__construct($id, $object, $time_countdown, $time_total); } diff --git a/database/migrations/2026_07_19_000001_add_dm_fields_to_building_queues.php b/database/migrations/2026_07_19_000001_add_dm_fields_to_building_queues.php new file mode 100644 index 000000000..04f011e58 --- /dev/null +++ b/database/migrations/2026_07_19_000001_add_dm_fields_to_building_queues.php @@ -0,0 +1,28 @@ +tinyInteger('dm_halved')->default(0)->after('processed'); + $table->tinyInteger('dm_completed')->default(0)->after('dm_halved'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('building_queues', function (Blueprint $table) { + $table->dropColumn(['dm_halved', 'dm_completed']); + }); + } +}; diff --git a/resources/views/ingame/shared/buildqueue/building-active.blade.php b/resources/views/ingame/shared/buildqueue/building-active.blade.php index 02a108d55..124f37d0e 100644 --- a/resources/views/ingame/shared/buildqueue/building-active.blade.php +++ b/resources/views/ingame/shared/buildqueue/building-active.blade.php @@ -45,24 +45,40 @@ @php $halvingService = app(\OGame\Services\HalvingService::class); - $halvingCost = $halvingService->calculateHalvingCost($build_active->time_total, 'building'); + $halvingCost = $halvingService->calculateHalvingCost($build_active->time_countdown, 'building'); + $wouldComplete = $build_active->dm_halved; @endphp - -
- Halve time - Costs: {{ number_format($halvingCost) }} DM -
+ @if ($wouldComplete) + +
+ @lang('Complete') + @lang('Costs: :amount DM', ['amount' => number_format($halvingCost)]) +
+ @else + +
+ @lang('Halve time') + @lang('Costs: :amount DM', ['amount' => number_format($halvingCost)]) +
+ @endif