From 424184a51a655121752ec42077e9f402b7300b08 Mon Sep 17 00:00:00 2001 From: hided62 Date: Wed, 29 Jul 2026 07:29:43 +0000 Subject: [PATCH] fix: define mixed S100 dex conversion ownership --- hwe/sammo/CentennialAllStarGrowthService.php | 22 +- tests/CentennialAllStarGrowthTest.php | 219 +++++++++++++++++++ 2 files changed, 234 insertions(+), 7 deletions(-) diff --git a/hwe/sammo/CentennialAllStarGrowthService.php b/hwe/sammo/CentennialAllStarGrowthService.php index b90578d2..26228888 100644 --- a/hwe/sammo/CentennialAllStarGrowthService.php +++ b/hwe/sammo/CentennialAllStarGrowthService.php @@ -506,20 +506,28 @@ final class CentennialAllStarGrowthService max(0, $sourceBefore), max(0, (int) ($granted[$sourceKey] ?? 0)) ); - $sourceOrganicBefore = max(0, $sourceBefore - $sourceGrantedBefore); - $sourceGrantedAfter = max( - 0, - $sourceAfter - min($sourceAfter, $sourceOrganicBefore) - ); - $eventGrantRemoved = max(0, $sourceGrantedBefore - $sourceGrantedAfter); + /* + * A dex value can contain both organic and event-backed points. Split + * the actual command deltas by their share of the source total instead + * of consuming either bucket first. This keeps the combined ownership + * stable when conversion and target reselection are interleaved. + */ + $eventGrantRemoved = $sourceBefore > 0 + ? intdiv($sourceDecrease * $sourceGrantedBefore, $sourceBefore) + : 0; + $sourceGrantedAfter = max(0, $sourceGrantedBefore - $eventGrantRemoved); $destinationGrantedBefore = min( max(0, $destinationBefore), max(0, (int) ($granted[$destinationKey] ?? 0)) ); + $eventGrantTransferred = $sourceBefore > 0 + ? intdiv($destinationIncrease * $sourceGrantedBefore, $sourceBefore) + : 0; $eventGrantTransferred = min( $destinationIncrease, - (int) floor($eventGrantRemoved * $convertCoeff) + $eventGrantRemoved, + $eventGrantTransferred ); $granted[$sourceKey] = $sourceGrantedAfter; $granted[$destinationKey] = min( diff --git a/tests/CentennialAllStarGrowthTest.php b/tests/CentennialAllStarGrowthTest.php index 4720efa5..65b9d4c4 100644 --- a/tests/CentennialAllStarGrowthTest.php +++ b/tests/CentennialAllStarGrowthTest.php @@ -516,6 +516,151 @@ final class CentennialAllStarGrowthTest extends TestCase self::assertSame(129600, CentennialAllStarGrowthService::recordableValue($general, 'dex2')); } + public function testMixedDexConversionSplitsEventAndNaturalPointsByTotalShare(): void + { + $vars = [ + 'dex1' => 360000, + 'dex2' => 600000, + ]; + $aux = [ + 'targetId' => 'archer', + 'granted' => [ + 'dex1' => 0, + 'dex2' => 900000, + ], + 'dexConsumed' => [ + 'dex1' => 0, + 'dex2' => 0, + ], + 'dexFloor' => [ + 'dex1' => 0, + 'dex2' => 900000, + ], + ]; + $general = $this->createStateGeneralMock($vars, $aux); + + CentennialAllStarGrowthService::reconcileDexConversion( + $general, + 'dex2', + 'dex1', + 1000000, + 600000, + 0, + 360000, + 0.9 + ); + + self::assertSame(540000, $aux['granted']['dex2']); + self::assertSame(324000, $aux['granted']['dex1']); + self::assertSame(60_000, CentennialAllStarGrowthService::recordableValue($general, 'dex2')); + self::assertSame(36_000, CentennialAllStarGrowthService::recordableValue($general, 'dex1')); + self::assertSame(864000, $aux['granted']['dex1'] + $aux['granted']['dex2']); + self::assertSame( + 96000, + CentennialAllStarGrowthService::recordableValue($general, 'dex1') + + CentennialAllStarGrowthService::recordableValue($general, 'dex2') + ); + } + + public function testArcherCavalryGhostConversionChainKeepsOnlyCurrentTargetBudget(): void + { + $vars = $this->emptyGeneralVars(); + $aux = CentennialAllStarGrowthService::initialAux( + $this->singleDexTarget('archer', 1), + [] + ); + $general = $this->createStateGeneralMock($vars, $aux); + $env = ['startyear' => 180, 'year' => 195, 'month' => 1]; + + CentennialAllStarGrowthService::applyTarget( + $general, + $this->singleDexTarget('archer', 1), + $env + ); + $this->convertDex($general, $vars, 'dex2', 'dex1'); + self::assertSame([324000, 540000, 0, 0, 0], $this->dexValues($vars)); + + CentennialAllStarGrowthService::applyTarget( + $general, + $this->singleDexTarget('cavalry', 2), + $env + ); + self::assertSame([0, 0, 900000, 0, 0], $this->dexValues($vars)); + $this->convertDex($general, $vars, 'dex3', 'dex1'); + self::assertSame([324000, 0, 540000, 0, 0], $this->dexValues($vars)); + + CentennialAllStarGrowthService::applyTarget( + $general, + $this->singleDexTarget('ghost', 3), + $env + ); + self::assertSame([0, 0, 0, 900000, 0], $this->dexValues($vars)); + $this->convertDex($general, $vars, 'dex4', 'dex1'); + + self::assertSame([324000, 0, 0, 540000, 0], $this->dexValues($vars)); + self::assertSame(864000, array_sum($this->dexValues($vars))); + foreach (['dex1', 'dex2', 'dex3', 'dex4', 'dex5'] as $key) { + self::assertSame(0, CentennialAllStarGrowthService::recordableValue($general, $key)); + } + } + + public function testSelectingInfantryAfterConversionReplacesOldEventBudget(): void + { + $vars = $this->emptyGeneralVars(); + $aux = CentennialAllStarGrowthService::initialAux( + $this->singleDexTarget('archer', 1), + [] + ); + $general = $this->createStateGeneralMock($vars, $aux); + $env = ['startyear' => 180, 'year' => 195, 'month' => 1]; + + CentennialAllStarGrowthService::applyTarget( + $general, + $this->singleDexTarget('archer', 1), + $env + ); + $this->convertDex($general, $vars, 'dex2', 'dex1'); + CentennialAllStarGrowthService::applyTarget( + $general, + $this->singleDexTarget('infantry', 0), + $env + ); + + self::assertSame([900000, 0, 0, 0, 0], $this->dexValues($vars)); + self::assertSame(900000, array_sum($aux['granted'])); + self::assertSame(0, CentennialAllStarGrowthService::recordableValue($general, 'dex1')); + } + + public function testMixedConversionThenReselectionPreservesOnlyPostLossNaturalTotal(): void + { + $vars = $this->emptyGeneralVars(); + $aux = CentennialAllStarGrowthService::initialAux( + $this->singleDexTarget('archer', 1), + [] + ); + $general = $this->createStateGeneralMock($vars, $aux); + $env = ['startyear' => 180, 'year' => 195, 'month' => 1]; + + CentennialAllStarGrowthService::applyTarget( + $general, + $this->singleDexTarget('archer', 1), + $env + ); + $vars['dex2'] += 100000; + $this->convertDex($general, $vars, 'dex2', 'dex1'); + CentennialAllStarGrowthService::applyTarget( + $general, + $this->singleDexTarget('infantry', 0), + $env + ); + + self::assertSame([900000, 60000, 0, 0, 0], $this->dexValues($vars)); + self::assertSame(864000, $aux['granted']['dex1']); + self::assertSame(0, $aux['granted']['dex2']); + self::assertSame(96000, array_sum($this->recordableDexValues($general))); + self::assertSame(960000, array_sum($this->dexValues($vars))); + } + public function testReselectionResetsConsumedDexFloorAndTransferredGrant(): void { $vars = [ @@ -638,4 +783,78 @@ final class CentennialAllStarGrowthTest extends TestCase ); return $general; } + + private function emptyGeneralVars(): array + { + return [ + 'leadership' => 100, + 'strength' => 100, + 'intel' => 100, + 'dex1' => 0, + 'dex2' => 0, + 'dex3' => 0, + 'dex4' => 0, + 'dex5' => 0, + 'special' => 'None', + ]; + } + + private function singleDexTarget(string $id, int $dexIndex): array + { + $dex = [0, 0, 0, 0, 0]; + $dex[$dexIndex] = 900000; + return [ + 'uniqueName' => $id, + 'leadership' => 100, + 'strength' => 100, + 'intel' => 100, + 'dex' => $dex, + ]; + } + + private function convertDex( + General $general, + array &$vars, + string $sourceKey, + string $destinationKey + ): void { + $sourceBefore = $vars[$sourceKey]; + $destinationBefore = $vars[$destinationKey]; + $cut = (int) ($sourceBefore * 0.4); + $add = (int) ($cut * 0.9); + $vars[$sourceKey] -= $cut; + $vars[$destinationKey] += $add; + CentennialAllStarGrowthService::reconcileDexConversion( + $general, + $sourceKey, + $destinationKey, + $sourceBefore, + $vars[$sourceKey], + $destinationBefore, + $vars[$destinationKey], + 0.9 + ); + } + + private function dexValues(array $vars): array + { + return [ + $vars['dex1'], + $vars['dex2'], + $vars['dex3'], + $vars['dex4'], + $vars['dex5'], + ]; + } + + private function recordableDexValues(General $general): array + { + return array_map( + static fn(string $key): int => CentennialAllStarGrowthService::recordableValue( + $general, + $key + ), + ['dex1', 'dex2', 'dex3', 'dex4', 'dex5'] + ); + } }