Merge branch 'devel' into compare/scenario2601-monthly-seed-parity-20260802
This commit is contained in:
@@ -464,28 +464,24 @@ final class CentennialAllStarGrowthService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discards the builder's generic random stat/dex values before applying
|
* Keeps the ordinary M/G-general stat total while aligning its strong,
|
||||||
* the selected all-star target. A newly generated NPC has no organic
|
* middle, and weak stats with the selected all-star target. The target's
|
||||||
* growth yet, so those temporary values must not mask the creation-date
|
* creation-date growth floor is applied immediately afterwards.
|
||||||
* event baseline.
|
|
||||||
*/
|
*/
|
||||||
public static function initializeGeneratedNPC(
|
public static function initializeGeneratedNPC(
|
||||||
General $general,
|
General $general,
|
||||||
array $targetInfo
|
array $targetInfo
|
||||||
): void {
|
): void {
|
||||||
|
$generatedStats = [];
|
||||||
foreach (self::STAT_KEYS as $key) {
|
foreach (self::STAT_KEYS as $key) {
|
||||||
$target = min(
|
$generatedStats[$key] = (int) $general->getVar($key);
|
||||||
GameConst::$maxLevel,
|
}
|
||||||
max(0, (int) ($targetInfo[$key] ?? 0))
|
$initialStats = self::calculateGeneratedNPCInitialStats(
|
||||||
);
|
$targetInfo,
|
||||||
$general->updateVar(
|
$generatedStats
|
||||||
$key,
|
);
|
||||||
CentennialAllStarGrowth::statFloor(
|
foreach (self::STAT_KEYS as $key) {
|
||||||
$target,
|
$general->updateVar($key, $initialStats[$key]);
|
||||||
GameConst::$defaultStatMin,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
foreach (self::DEX_KEYS as $key) {
|
foreach (self::DEX_KEYS as $key) {
|
||||||
$general->updateVar($key, 0);
|
$general->updateVar($key, 0);
|
||||||
@@ -493,6 +489,41 @@ final class CentennialAllStarGrowthService
|
|||||||
$general->setAuxVar(self::AUX_KEY, self::initialAux($targetInfo));
|
$general->setAuxVar(self::AUX_KEY, self::initialAux($targetInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $targetInfo
|
||||||
|
* @param array{leadership:int,strength:int,intel:int} $generatedStats
|
||||||
|
* @return array{leadership:int,strength:int,intel:int}
|
||||||
|
*/
|
||||||
|
public static function calculateGeneratedNPCInitialStats(
|
||||||
|
array $targetInfo,
|
||||||
|
array $generatedStats
|
||||||
|
): array {
|
||||||
|
$targetOrder = self::STAT_KEYS;
|
||||||
|
$keyOrder = array_flip(self::STAT_KEYS);
|
||||||
|
usort(
|
||||||
|
$targetOrder,
|
||||||
|
static function (string $lhs, string $rhs) use ($targetInfo, $keyOrder): int {
|
||||||
|
$targetCompare = (int) ($targetInfo[$rhs] ?? 0)
|
||||||
|
<=> (int) ($targetInfo[$lhs] ?? 0);
|
||||||
|
return $targetCompare !== 0
|
||||||
|
? $targetCompare
|
||||||
|
: $keyOrder[$lhs] <=> $keyOrder[$rhs];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$generatedValues = array_map(
|
||||||
|
static fn (string $key): int => (int) ($generatedStats[$key] ?? 0),
|
||||||
|
self::STAT_KEYS
|
||||||
|
);
|
||||||
|
rsort($generatedValues, SORT_NUMERIC);
|
||||||
|
|
||||||
|
$result = array_fill_keys(self::STAT_KEYS, 0);
|
||||||
|
foreach ($targetOrder as $idx => $key) {
|
||||||
|
$result[$key] = $generatedValues[$idx];
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves the event-backed part of a dex conversion with the converted
|
* Moves the event-backed part of a dex conversion with the converted
|
||||||
* value and consumes any guaranteed floor crossed by the source value.
|
* value and consumes any guaranteed floor crossed by the source value.
|
||||||
|
|||||||
@@ -83,12 +83,12 @@ final class CentennialAllStarGrowthTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testGeneratedNpcUsesExactCreationDateTargetInsteadOfRandomBase(): void
|
public function testGeneratedNpcKeepsOrdinaryTotalAndUsesCreationDateTargetFloor(): void
|
||||||
{
|
{
|
||||||
$vars = [
|
$vars = [
|
||||||
'leadership' => 72,
|
'leadership' => 72,
|
||||||
'strength' => 61,
|
'strength' => 66,
|
||||||
'intel' => 32,
|
'intel' => 12,
|
||||||
'dex1' => 120000,
|
'dex1' => 120000,
|
||||||
'dex2' => 240000,
|
'dex2' => 240000,
|
||||||
'dex3' => 360000,
|
'dex3' => 360000,
|
||||||
@@ -117,17 +117,104 @@ final class CentennialAllStarGrowthTest extends TestCase
|
|||||||
|
|
||||||
self::assertSame(91, $vars['leadership']);
|
self::assertSame(91, $vars['leadership']);
|
||||||
self::assertSame(73, $vars['strength']);
|
self::assertSame(73, $vars['strength']);
|
||||||
self::assertSame(10, $vars['intel']);
|
self::assertSame(12, $vars['intel']);
|
||||||
|
self::assertSame(176, $vars['leadership'] + $vars['strength'] + $vars['intel']);
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
[360000, 320000, 280000, 240000, 200000],
|
[360000, 320000, 280000, 240000, 200000],
|
||||||
$this->dexValues($vars)
|
$this->dexValues($vars)
|
||||||
);
|
);
|
||||||
self::assertSame(76, $aux['granted']['leadership']);
|
self::assertSame(19, $aux['granted']['leadership']);
|
||||||
self::assertSame(58, $aux['granted']['strength']);
|
self::assertSame(7, $aux['granted']['strength']);
|
||||||
self::assertSame(0, $aux['granted']['intel']);
|
self::assertSame(0, $aux['granted']['intel']);
|
||||||
self::assertSame(360000, $aux['granted']['dex1']);
|
self::assertSame(360000, $aux['granted']['dex1']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testFernandoAndUmaMusumeStartAtOrdinaryNpcTotal(): void
|
||||||
|
{
|
||||||
|
$pool = json_decode(
|
||||||
|
file_get_contents(__DIR__ . '/../hwe/sammo/GeneralPool/Pool/UnderS100.json'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
$columns = array_flip($pool['columns']);
|
||||||
|
$targets = [];
|
||||||
|
foreach ($pool['data'] as $row) {
|
||||||
|
$name = $row[$columns['generalName']];
|
||||||
|
if (!in_array($name, ['43·페르난도', '47·우마무스메'], true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$targets[$name] = [
|
||||||
|
'leadership' => $row[$columns['leadership']],
|
||||||
|
'strength' => $row[$columns['strength']],
|
||||||
|
'intel' => $row[$columns['intel']],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
self::assertCount(2, $targets);
|
||||||
|
|
||||||
|
$generated = [
|
||||||
|
'leadership' => 73,
|
||||||
|
'strength' => 10,
|
||||||
|
'intel' => 67,
|
||||||
|
];
|
||||||
|
$fernando = CentennialAllStarGrowthService::calculateGeneratedNPCInitialStats(
|
||||||
|
$targets['43·페르난도'],
|
||||||
|
$generated
|
||||||
|
);
|
||||||
|
$umaMusume = CentennialAllStarGrowthService::calculateGeneratedNPCInitialStats(
|
||||||
|
$targets['47·우마무스메'],
|
||||||
|
$generated
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame([
|
||||||
|
'leadership' => 67,
|
||||||
|
'strength' => 73,
|
||||||
|
'intel' => 10,
|
||||||
|
], $fernando);
|
||||||
|
self::assertSame([
|
||||||
|
'leadership' => 10,
|
||||||
|
'strength' => 67,
|
||||||
|
'intel' => 73,
|
||||||
|
], $umaMusume);
|
||||||
|
self::assertSame(GameConst::$defaultStatNPCTotal, array_sum($fernando));
|
||||||
|
self::assertSame(GameConst::$defaultStatNPCTotal, array_sum($umaMusume));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEveryCandidateKeepsTheGeneratedNpcStatTotal(): void
|
||||||
|
{
|
||||||
|
$pool = json_decode(
|
||||||
|
file_get_contents(__DIR__ . '/../hwe/sammo/GeneralPool/Pool/UnderS100.json'),
|
||||||
|
true,
|
||||||
|
512,
|
||||||
|
JSON_THROW_ON_ERROR
|
||||||
|
);
|
||||||
|
$columns = array_flip($pool['columns']);
|
||||||
|
$generated = [
|
||||||
|
'leadership' => 73,
|
||||||
|
'strength' => 67,
|
||||||
|
'intel' => 10,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($pool['data'] as $row) {
|
||||||
|
$target = [
|
||||||
|
'leadership' => $row[$columns['leadership']],
|
||||||
|
'strength' => $row[$columns['strength']],
|
||||||
|
'intel' => $row[$columns['intel']],
|
||||||
|
];
|
||||||
|
$initial = CentennialAllStarGrowthService::calculateGeneratedNPCInitialStats(
|
||||||
|
$target,
|
||||||
|
$generated
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
GameConst::$defaultStatNPCTotal,
|
||||||
|
array_sum($initial),
|
||||||
|
$row[$columns['generalName']]
|
||||||
|
);
|
||||||
|
sort($initial, SORT_NUMERIC);
|
||||||
|
self::assertSame([10, 67, 73], array_values($initial));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function testProgressMultiplierMustStayWithinUnitInterval(): void
|
public function testProgressMultiplierMustStayWithinUnitInterval(): void
|
||||||
{
|
{
|
||||||
$this->expectException(InvalidArgumentException::class);
|
$this->expectException(InvalidArgumentException::class);
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use sammo\CentennialAllStarGrowthService;
|
||||||
|
use sammo\DB;
|
||||||
|
use sammo\GameConst;
|
||||||
|
use sammo\General;
|
||||||
|
use sammo\GeneralPool\SPoolUnderU100;
|
||||||
|
use sammo\Json;
|
||||||
|
use sammo\LiteHashDRBG;
|
||||||
|
use sammo\RandUtil;
|
||||||
|
use sammo\Util;
|
||||||
|
|
||||||
|
const APP_ROOT = '/var/www/html';
|
||||||
|
const TARGET_NAMES = ['43·페르난도', '47·우마무스메'];
|
||||||
|
|
||||||
|
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
|
||||||
|
$_SERVER['REQUEST_URI'] = '/s100-generated-npc-stat-floor-check';
|
||||||
|
|
||||||
|
require APP_ROOT . '/hwe/lib.php';
|
||||||
|
require APP_ROOT . '/hwe/func.php';
|
||||||
|
|
||||||
|
$env = [
|
||||||
|
'startyear' => 180,
|
||||||
|
'year' => 180,
|
||||||
|
'month' => 12,
|
||||||
|
'fiction' => [1],
|
||||||
|
'show_img_level' => 3,
|
||||||
|
];
|
||||||
|
$pool = Json::decode((string) file_get_contents(
|
||||||
|
APP_ROOT . '/hwe/sammo/GeneralPool/Pool/UnderS100.json'
|
||||||
|
));
|
||||||
|
$columns = array_flip($pool['columns']);
|
||||||
|
$targets = [];
|
||||||
|
foreach ($pool['data'] as $idx => $row) {
|
||||||
|
$name = $row[$columns['generalName']];
|
||||||
|
if (!in_array($name, TARGET_NAMES, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$info = array_combine($pool['columns'], $row);
|
||||||
|
$info['uniqueName'] = sprintf('A100%04d', $idx + 1);
|
||||||
|
$info['event100Growth'] = true;
|
||||||
|
$targets[$name] = $info;
|
||||||
|
}
|
||||||
|
if (count($targets) !== count(TARGET_NAMES)) {
|
||||||
|
throw new RuntimeException('Required S100 candidates are missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = DB::db();
|
||||||
|
$results = [];
|
||||||
|
foreach (TARGET_NAMES as $name) {
|
||||||
|
$target = $targets[$name];
|
||||||
|
$rng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
|
||||||
|
's100-generated-npc-stat-floor-check',
|
||||||
|
$target['uniqueName']
|
||||||
|
)));
|
||||||
|
$poolGeneral = new SPoolUnderU100(
|
||||||
|
$db,
|
||||||
|
$rng,
|
||||||
|
$target,
|
||||||
|
'2999-01-01 00:00:00'
|
||||||
|
);
|
||||||
|
$builder = $poolGeneral->getGeneralBuilder();
|
||||||
|
$builder->setNationID(0)
|
||||||
|
->setNPCType(3)
|
||||||
|
->setMoney(1000, 1000)
|
||||||
|
->setExpDed(0, 0)
|
||||||
|
->setLifeSpan(160, 230);
|
||||||
|
$builder->fillRandomStat(['무' => 0.333, '지' => 0.333, '무지' => 0.334]);
|
||||||
|
$generatedStats = $builder->getStat();
|
||||||
|
assertSameValue(
|
||||||
|
GameConst::$defaultStatNPCTotal,
|
||||||
|
array_sum($generatedStats),
|
||||||
|
"{$name} generic total"
|
||||||
|
);
|
||||||
|
$builder->fillRemainSpecAsZero($env);
|
||||||
|
$builder->build($env);
|
||||||
|
CentennialAllStarGrowthService::applyCurrentTargetToBuiltNPC(
|
||||||
|
$db,
|
||||||
|
$builder,
|
||||||
|
$target,
|
||||||
|
$env
|
||||||
|
);
|
||||||
|
|
||||||
|
$general = General::createObjFromDB($builder->getGeneralID());
|
||||||
|
$actualStats = [
|
||||||
|
'leadership' => (int) $general->getVar('leadership'),
|
||||||
|
'strength' => (int) $general->getVar('strength'),
|
||||||
|
'intel' => (int) $general->getVar('intel'),
|
||||||
|
];
|
||||||
|
if (array_sum($actualStats) < GameConst::$defaultStatNPCTotal) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"{$name} total fell below " . GameConst::$defaultStatNPCTotal
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetOrder = ['leadership', 'strength', 'intel'];
|
||||||
|
usort(
|
||||||
|
$targetOrder,
|
||||||
|
static fn (string $lhs, string $rhs): int =>
|
||||||
|
(int) $target[$rhs] <=> (int) $target[$lhs]
|
||||||
|
);
|
||||||
|
if ($actualStats[$targetOrder[0]] < $actualStats[$targetOrder[1]]
|
||||||
|
|| $actualStats[$targetOrder[1]] < $actualStats[$targetOrder[2]]
|
||||||
|
) {
|
||||||
|
throw new RuntimeException("{$name} target stat order was not preserved");
|
||||||
|
}
|
||||||
|
|
||||||
|
$results[] = [
|
||||||
|
'name' => $name,
|
||||||
|
'target' => array_intersect_key($target, $actualStats),
|
||||||
|
'generic' => array_combine(
|
||||||
|
['leadership', 'strength', 'intel'],
|
||||||
|
$generatedStats
|
||||||
|
),
|
||||||
|
'actual' => $actualStats,
|
||||||
|
'total' => array_sum($actualStats),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo Json::encode($results, Json::PRETTY), PHP_EOL;
|
||||||
|
|
||||||
|
function assertSameValue(int $expected, int $actual, string $label): void
|
||||||
|
{
|
||||||
|
if ($actual !== $expected) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"{$label}: expected {$expected}, got {$actual}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user