addPsr4('sammo\\', __DIR__ . '/../hwe/sammo', true); require_once __DIR__ . '/../hwe/sammo/ActionLogger.php'; require_once __DIR__ . '/../hwe/sammo/GameConstBase.php'; require_once __DIR__ . '/../hwe/d_setting/GameConst.php'; require_once __DIR__ . '/../hwe/sammo/CentennialAllStarGrowthService.php'; final class CentennialAllStarGrowthTest extends TestCase { public function testProgressMilestones(): void { self::assertSame(0.0, CentennialAllStarGrowth::progress(180, 180, 1)); self::assertEqualsWithDelta(0.2, CentennialAllStarGrowth::progress(180, 183, 1), 0.000001); self::assertEqualsWithDelta(0.4, CentennialAllStarGrowth::progress(180, 186, 1), 0.000001); self::assertSame(1.0, CentennialAllStarGrowth::progress(180, 195, 1)); self::assertSame(1.0, CentennialAllStarGrowth::progress(180, 210, 1)); } public function testMAndGGeneralsAdvanceAtNinetyPercentProgress(): void { self::assertSame( CentennialAllStarGrowthService::NPC_PROGRESS_MULTIPLIER, CentennialAllStarGrowthService::progressMultiplierForNPCType(3) ); self::assertSame( CentennialAllStarGrowthService::NPC_PROGRESS_MULTIPLIER, CentennialAllStarGrowthService::progressMultiplierForNPCType(4) ); self::assertSame(1.0, CentennialAllStarGrowthService::progressMultiplierForNPCType(2)); self::assertEqualsWithDelta( 0.36, CentennialAllStarGrowthService::calculateProgress(180, 186, 1, 0.9), 0.000001 ); self::assertEqualsWithDelta( 0.9, CentennialAllStarGrowthService::calculateProgress(180, 195, 1, 0.9), 0.000001 ); self::assertEqualsWithDelta( 0.9, CentennialAllStarGrowthService::calculateProgress(180, 210, 1, 0.9), 0.000001 ); self::assertSame( 91, CentennialAllStarGrowth::statFloor(100, 15, 0.9) ); self::assertSame( 0.4, CentennialAllStarGrowthService::dexTargetRatioForNPCType(3) ); self::assertSame( 0.4, CentennialAllStarGrowthService::dexTargetRatioForNPCType(4) ); self::assertSame( 1.0, CentennialAllStarGrowthService::dexTargetRatioForNPCType(2) ); } public function testProgressMultiplierMustStayWithinUnitInterval(): void { $this->expectException(InvalidArgumentException::class); CentennialAllStarGrowthService::calculateProgress(180, 195, 1, 1.01); } public function testLowStatGrowsWhileHigherStatStays(): void { $floor = CentennialAllStarGrowth::statFloor(90, 15, 0.6); self::assertSame(60, $floor); self::assertSame( ['value' => 70, 'granted' => 0, 'delta' => 0], CentennialAllStarGrowth::advance(70, 0, $floor) ); self::assertSame( ['value' => 60, 'granted' => 10, 'delta' => 10], CentennialAllStarGrowth::advance(50, 0, $floor) ); } public function testUserInitialStatsPreserveCandidateShapeAtOrdinaryTotal(): void { $initial = CentennialAllStarGrowthService::calculateUserInitialStats([ 'leadership' => 80, 'strength' => 70, 'intel' => 50, ]); self::assertSame([ 'leadership' => 65, 'strength' => 58, 'intel' => 42, ], $initial); self::assertSame(165, array_sum($initial)); self::assertLessThanOrEqual(80, $initial['leadership']); self::assertLessThanOrEqual(70, $initial['strength']); self::assertLessThanOrEqual(50, $initial['intel']); } public function testUserInitialStatsDoNotRaiseCandidateBelowOrdinaryTotal(): void { self::assertSame([ 'leadership' => 60, 'strength' => 45, 'intel' => 30, ], CentennialAllStarGrowthService::calculateUserInitialStats([ 'leadership' => 60, 'strength' => 45, 'intel' => 30, ])); } public function testCurrentReselectionBaselineUsesCurrentYearProgress(): void { $target = [ 'leadership' => 100, 'strength' => 15, 'intel' => 100, ]; self::assertSame([ 'leadership' => 75, 'strength' => 15, 'intel' => 75, ], CentennialAllStarGrowthService::calculateUserCurrentTargetStats( $target, ['startyear' => 180, 'year' => 180, 'month' => 1] )); self::assertSame([ 'leadership' => 83, 'strength' => 15, 'intel' => 83, ], CentennialAllStarGrowthService::calculateUserCurrentTargetStats( $target, ['startyear' => 180, 'year' => 192, 'month' => 1] )); self::assertSame([ 'leadership' => 100, 'strength' => 15, 'intel' => 100, ], CentennialAllStarGrowthService::calculateUserCurrentTargetStats( $target, ['startyear' => 180, 'year' => 195, 'month' => 1] )); } public function testInitialUserGrantMakesInitialAllocationReplaceable(): void { $initial = CentennialAllStarGrowthService::calculateUserInitialStats([ 'uniqueName' => 'A1000001', 'leadership' => 80, 'strength' => 70, 'intel' => 50, ]); $aux = CentennialAllStarGrowthService::initialAux([ 'uniqueName' => 'A1000001', ], $initial); self::assertSame('A1000001', $aux['targetId']); self::assertSame($initial, $aux['userInitialStats']); self::assertSame(1.0, $aux['dexTargetRatio']); self::assertSame(50, $aux['granted']['leadership']); self::assertSame(43, $aux['granted']['strength']); self::assertSame(27, $aux['granted']['intel']); } public function testLegacyInitialGrantKeepsOnlyGrowthBeyondCreationRange(): void { self::assertSame( 35, CentennialAllStarGrowthService::calculateLegacyUserGrant(50, 0) ); self::assertSame( 75, CentennialAllStarGrowthService::calculateLegacyUserGrant(90, 40) ); self::assertSame( 105, CentennialAllStarGrowthService::calculateLegacyUserGrant(140, 40) ); self::assertSame( 35, 140 - CentennialAllStarGrowthService::calculateLegacyUserGrant(140, 40) ); } public function testReselectDropsOldGrantAndKeepsOrganicGrowth(): void { self::assertSame( ['value' => 82, 'granted' => 7, 'organic' => 75], CentennialAllStarGrowth::replaceTarget(86, 11, 82) ); self::assertSame( ['value' => 90, 'granted' => 0, 'organic' => 90], CentennialAllStarGrowth::replaceTarget(101, 11, 82) ); } public function testRepeatedAdvanceIsIdempotent(): void { $first = CentennialAllStarGrowth::advance(50, 0, 60); $second = CentennialAllStarGrowth::advance($first['value'], $first['granted'], 60); self::assertSame(60, $second['value']); self::assertSame(10, $second['granted']); self::assertSame(0, $second['delta']); self::assertSame(50, CentennialAllStarGrowth::recordableValue(60, 10)); } public function testDexUsesSlowEarlyCurve(): void { self::assertSame(0, CentennialAllStarGrowth::dexFloor(900000, 0)); self::assertSame(36000, CentennialAllStarGrowth::dexFloor(900000, 0.2)); self::assertSame(144000, CentennialAllStarGrowth::dexFloor(900000, 0.4)); self::assertSame(900000, CentennialAllStarGrowth::dexFloor(900000, 1)); } public function testNpcDexStopsAtFortyPercentOfHistoricalTarget(): void { $target = 900000; $ratio = GameConst::$centennialNpcDexTargetRatio; self::assertSame(0.4, $ratio); self::assertSame( 57600, CentennialAllStarGrowthService::calculateDexTargetFloor( $target, ['startyear' => 180, 'year' => 186, 'month' => 1], $ratio ) ); self::assertSame( 360000, CentennialAllStarGrowthService::calculateDexTargetFloor( $target, ['startyear' => 180, 'year' => 195, 'month' => 1], $ratio ) ); self::assertSame( 360000, CentennialAllStarGrowthService::calculateDexTargetFloor( $target, ['startyear' => 180, 'year' => 210, 'month' => 1], $ratio ) ); } public function testScenarioConfigKeepsNpcDexTargetRatioAtFortyPercent(): void { $scenario = json_decode( file_get_contents(__DIR__ . '/../hwe/scenario/scenario_915.json'), true, 512, JSON_THROW_ON_ERROR ); self::assertSame( GameConst::$centennialNpcDexTargetRatio, $scenario['map']['centennialNpcDexTargetRatio'] ); } public function testNpcDexRatioChangeRemovesOnlyOldEventGrant(): void { self::assertSame( ['value' => 360000, 'granted' => 360000, 'organic' => 0], CentennialAllStarGrowth::replaceTarget(810000, 810000, 360000) ); self::assertSame( ['value' => 360000, 'granted' => 310000, 'organic' => 50000], CentennialAllStarGrowth::replaceTarget(860000, 810000, 360000) ); self::assertSame( ['value' => 400000, 'granted' => 0, 'organic' => 400000], CentennialAllStarGrowth::replaceTarget(1210000, 810000, 360000) ); } public function testExistingNpcDexGrantIsRebasedWithoutChangingFinalStats(): void { $vars = [ 'leadership' => 91, 'strength' => 91, 'intel' => 91, 'dex1' => 810000, 'dex2' => 810000, 'dex3' => 810000, 'dex4' => 810000, 'dex5' => 810000, 'special' => 'None', ]; $aux = [ 'targetId' => 'A1000001', 'granted' => [ 'leadership' => 76, 'strength' => 76, 'intel' => 76, 'dex1' => 810000, 'dex2' => 810000, 'dex3' => 810000, 'dex4' => 810000, 'dex5' => 810000, ], 'progressMonth' => 162, 'milestone' => 4, 'naturalSpecialDomestic' => null, 'eventSpecialDomestic' => null, 'userInitialStats' => null, ]; $general = $this->getMockBuilder(General::class) ->disableOriginalConstructor() ->onlyMethods(['getAuxVar', 'setAuxVar', 'getVar', 'updateVar']) ->getMock(); $general->method('getAuxVar')->willReturnCallback( static fn(string $key) => $key === CentennialAllStarGrowthService::AUX_KEY ? $aux : null ); $general->method('getVar')->willReturnCallback( static fn(string $key) => $vars[$key] ?? null ); $general->method('updateVar')->willReturnCallback( static function (string $key, $value) use (&$vars): void { $vars[$key] = $value; } ); $general->method('setAuxVar')->willReturnCallback( static function (string $key, $value) use (&$aux): void { if ($key === CentennialAllStarGrowthService::AUX_KEY) { $aux = $value; } } ); $result = CentennialAllStarGrowthService::applyTarget( $general, [ 'uniqueName' => 'A1000001', 'leadership' => 100, 'strength' => 100, 'intel' => 100, 'dex' => [900000, 900000, 900000, 900000, 900000], ], ['startyear' => 180, 'year' => 195, 'month' => 1], CentennialAllStarGrowthService::NPC_PROGRESS_MULTIPLIER, GameConst::$centennialNpcDexTargetRatio ); self::assertTrue($result['changed']); self::assertSame(91, $vars['leadership']); self::assertSame(91, $vars['strength']); self::assertSame(91, $vars['intel']); foreach (['dex1', 'dex2', 'dex3', 'dex4', 'dex5'] as $key) { self::assertSame(360000, $vars[$key]); self::assertSame(360000, $aux['granted'][$key]); } self::assertSame(0.4, $aux['dexTargetRatio']); } public function testUserDexStillReachesFullHistoricalTarget(): void { self::assertSame( 900000, CentennialAllStarGrowthService::calculateDexTargetFloor( 900000, ['startyear' => 180, 'year' => 195, 'month' => 1] ) ); } public function testUntrackedGeneralKeepsFullHallValue(): void { self::assertSame( 123456, CentennialAllStarGrowthService::recordableRawValue( ['dex1' => 123456, 'aux' => '{}'], 'dex1' ) ); } }