From e3615fa6bbd2e09652c08530121f8af05b92105c Mon Sep 17 00:00:00 2001 From: hided62 Date: Wed, 29 Jul 2026 05:20:53 +0000 Subject: [PATCH] fix: cap S100 NPC dex growth at forty percent --- hwe/sammo/CentennialAllStarGrowthService.php | 48 ++++- .../Event/Action/AdvanceCentennialAllStar.php | 5 +- hwe/sammo/GameConstBase.php | 2 + hwe/scenario/scenario_915.json | 3 +- tests/CentennialAllStarGrowthTest.php | 177 ++++++++++++++++++ 5 files changed, 227 insertions(+), 8 deletions(-) diff --git a/hwe/sammo/CentennialAllStarGrowthService.php b/hwe/sammo/CentennialAllStarGrowthService.php index 6f031ec3..4d224808 100644 --- a/hwe/sammo/CentennialAllStarGrowthService.php +++ b/hwe/sammo/CentennialAllStarGrowthService.php @@ -37,6 +37,7 @@ final class CentennialAllStarGrowthService 'naturalSpecialDomestic' => null, 'eventSpecialDomestic' => null, 'userInitialStats' => $userInitialStats, + 'dexTargetRatio' => 1.0, ]; } @@ -220,6 +221,24 @@ final class CentennialAllStarGrowthService ); } + public static function calculateDexTargetFloor( + int $target, + array $env, + float $targetRatio = 1.0 + ): int { + if ($targetRatio < 0 || $targetRatio > 1) { + throw new \InvalidArgumentException('dex target ratio must be between 0 and 1'); + } + $target = min(GameConst::$dexLimit, max(0, $target)); + $scaledTarget = (int) floor($target * $targetRatio); + $progress = self::calculateProgress( + (int) $env['startyear'], + (int) $env['year'], + (int) $env['month'] + ); + return CentennialAllStarGrowth::dexFloor($scaledTarget, $progress); + } + /** * Mutates the General object but leaves persistence to the caller. * @@ -229,7 +248,8 @@ final class CentennialAllStarGrowthService General $general, array $targetInfo, array $env, - float $progressMultiplier = 1.0 + float $progressMultiplier = 1.0, + float $dexTargetRatio = 1.0 ): array { $startYear = (int) $env['startyear']; @@ -259,10 +279,14 @@ final class CentennialAllStarGrowthService $nextUserInitialStats = $targetChanged && $isUserTarget ? self::calculateUserInitialStats($targetInfo) : ($aux['userInitialStats'] ?? null); + $previousDexTargetRatio = is_numeric($aux['dexTargetRatio'] ?? null) + ? (float) $aux['dexTargetRatio'] + : 1.0; + $dexTargetRatioChanged = $previousDexTargetRatio !== $dexTargetRatio; $userCurrentTargetStats = $isUserTarget ? self::calculateUserCurrentTargetStats($targetInfo, $env) : null; - $changed = false; + $changed = $dexTargetRatioChanged; foreach (self::STAT_KEYS as $key) { if (!array_key_exists($key, $targetInfo)) { @@ -303,10 +327,13 @@ final class CentennialAllStarGrowthService if (!array_key_exists($idx, $targetDex)) { continue; } - $target = min(GameConst::$dexLimit, max(0, (int) $targetDex[$idx])); - $floor = CentennialAllStarGrowth::dexFloor($target, $progress); + $floor = self::calculateDexTargetFloor( + (int) $targetDex[$idx], + $env, + $dexTargetRatio + ); $current = (int) $general->getVar($key); - if ($targetChanged) { + if ($targetChanged || $dexTargetRatioChanged) { $result = CentennialAllStarGrowth::replaceTarget( $current, (int) ($granted[$key] ?? 0), @@ -357,6 +384,7 @@ final class CentennialAllStarGrowthService $aux['progressMonth'] = max((int) ($aux['progressMonth'] ?? -1), $progressMonth); $aux['milestone'] = max($previousMilestone, $milestone); $aux['userInitialStats'] = $nextUserInitialStats; + $aux['dexTargetRatio'] = $dexTargetRatio; $general->setAuxVar(self::AUX_KEY, $aux); return [ @@ -380,6 +408,13 @@ final class CentennialAllStarGrowthService : 1.0; } + public static function dexTargetRatioForNPCType(int $npcType): float + { + return in_array($npcType, [3, 4], true) + ? GameConst::$centennialNpcDexTargetRatio + : 1.0; + } + public static function applyCurrentTargetToBuiltNPC( \MeekroDB $db, GeneralBuilder $builder, @@ -398,7 +433,8 @@ final class CentennialAllStarGrowthService $general, $targetInfo, $env, - self::NPC_PROGRESS_MULTIPLIER + self::NPC_PROGRESS_MULTIPLIER, + GameConst::$centennialNpcDexTargetRatio ); $general->applyDB($db); return $result; diff --git a/hwe/sammo/Event/Action/AdvanceCentennialAllStar.php b/hwe/sammo/Event/Action/AdvanceCentennialAllStar.php index 200a2f1c..5ac86d16 100644 --- a/hwe/sammo/Event/Action/AdvanceCentennialAllStar.php +++ b/hwe/sammo/Event/Action/AdvanceCentennialAllStar.php @@ -29,7 +29,10 @@ class AdvanceCentennialAllStar extends \sammo\Event\Action $general, $targetInfo, $env, - CentennialAllStarGrowthService::progressMultiplierFor($general) + CentennialAllStarGrowthService::progressMultiplierFor($general), + CentennialAllStarGrowthService::dexTargetRatioForNPCType( + $general->getNPCType() + ) ); if ($result['milestone'] > $result['previousMilestone']) { diff --git a/hwe/sammo/GameConstBase.php b/hwe/sammo/GameConstBase.php index 6fff5e0e..240a17f8 100644 --- a/hwe/sammo/GameConstBase.php +++ b/hwe/sammo/GameConstBase.php @@ -420,6 +420,8 @@ class GameConstBase public static $retirementYear = 80; public static $targetGeneralPool = 'RandomNameGeneral'; + /** @var float 100기 올스타 NPC의 원본 목표 대비 최종 숙련 비율 */ + public static $centennialNpcDexTargetRatio = 0.4; public static $generalPoolAllowOption = ['stat', 'ego', 'picture']; public static $randGenFirstName = [ diff --git a/hwe/scenario/scenario_915.json b/hwe/scenario/scenario_915.json index 184b9743..a4ef096b 100644 --- a/hwe/scenario/scenario_915.json +++ b/hwe/scenario/scenario_915.json @@ -4,7 +4,8 @@ "map": { "mapName": "miniche", "targetGeneralPool": "SPoolUnderU100", - "generalPoolAllowOption": ["stat", "ego", "picture"] + "generalPoolAllowOption": ["stat", "ego", "picture"], + "centennialNpcDexTargetRatio": 0.4 }, "history": [ "●180년 1월:【100기 이벤트】 역대 장수들이 평범한 능력으로 다시 모여, 지난 전성기의 힘과 서서히 동조하기 시작했다!" diff --git a/tests/CentennialAllStarGrowthTest.php b/tests/CentennialAllStarGrowthTest.php index 41cbb508..5a50822e 100644 --- a/tests/CentennialAllStarGrowthTest.php +++ b/tests/CentennialAllStarGrowthTest.php @@ -3,6 +3,11 @@ use PHPUnit\Framework\TestCase; use sammo\CentennialAllStarGrowth; use sammo\CentennialAllStarGrowthService; +use sammo\General; +use sammo\GameConst; + +$loader = require __DIR__ . '/../vendor/autoload.php'; +$loader->addPsr4('sammo\\', __DIR__ . '/../hwe/sammo', true); require_once __DIR__ . '/../hwe/sammo/ActionLogger.php'; require_once __DIR__ . '/../hwe/sammo/GameConstBase.php'; @@ -46,6 +51,22 @@ final class CentennialAllStarGrowthTest extends TestCase 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 @@ -148,6 +169,7 @@ final class CentennialAllStarGrowthTest extends TestCase 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']); @@ -203,6 +225,161 @@ final class CentennialAllStarGrowthTest extends TestCase 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(