feat: 100기 NPC 동조와 후보 추첨 보완
This commit is contained in:
@@ -6,19 +6,19 @@ include "func.php";
|
||||
|
||||
WebUtil::requireAJAX();
|
||||
|
||||
$pick = Util::getPost('pick');
|
||||
$leadership = Util::getPost('leadership', 'int', GameConst::$defaultStatMin);
|
||||
$isCentennialAllStar = CentennialAllStarGrowthService::isActive();
|
||||
$strength = Util::getPost(
|
||||
$isCentennialAllStar ? 'strength' : 'leadership',
|
||||
'int',
|
||||
GameConst::$defaultStatMin
|
||||
);
|
||||
$intel = Util::getPost(
|
||||
$isCentennialAllStar ? 'intel' : 'leadership',
|
||||
'int',
|
||||
GameConst::$defaultStatMin
|
||||
);
|
||||
$pick = Util::getPost('pick');
|
||||
$leadership = Util::getPost('leadership', 'int', GameConst::$defaultStatMin);
|
||||
$isCentennialAllStar = CentennialAllStarGrowthService::isActive();
|
||||
$strength = Util::getPost(
|
||||
$isCentennialAllStar ? 'strength' : 'leadership',
|
||||
'int',
|
||||
GameConst::$defaultStatMin
|
||||
);
|
||||
$intel = Util::getPost(
|
||||
$isCentennialAllStar ? 'intel' : 'leadership',
|
||||
'int',
|
||||
GameConst::$defaultStatMin
|
||||
);
|
||||
$personal = Util::getPost('personal', 'string', null);
|
||||
$use_own_picture = Util::getPost('use_own_picture', 'bool', false);
|
||||
|
||||
@@ -87,19 +87,19 @@ if ($gencount >= $maxgeneral) {
|
||||
]);
|
||||
}
|
||||
|
||||
$poolClass = getGeneralPoolClass(GameConst::$targetGeneralPool);
|
||||
/** @var AbsGeneralPool */
|
||||
if ($isCentennialAllStar) {
|
||||
$rng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
|
||||
UniqueConst::$hiddenSeed,
|
||||
'selectPickedGeneral',
|
||||
$userID,
|
||||
$pick
|
||||
)));
|
||||
$pickedGeneral = new $poolClass($db, $rng, $selectInfo, $now);
|
||||
} else {
|
||||
$pickedGeneral = new $poolClass($db, $selectInfo, $now);
|
||||
}
|
||||
$poolClass = getGeneralPoolClass(GameConst::$targetGeneralPool);
|
||||
/** @var AbsGeneralPool */
|
||||
if ($isCentennialAllStar) {
|
||||
$rng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
|
||||
UniqueConst::$hiddenSeed,
|
||||
'selectPickedGeneral',
|
||||
$userID,
|
||||
$pick
|
||||
)));
|
||||
$pickedGeneral = new $poolClass($db, $rng, $selectInfo, $now);
|
||||
} else {
|
||||
$pickedGeneral = new $poolClass($db, $selectInfo, $now);
|
||||
}
|
||||
|
||||
$builder = $pickedGeneral->getGeneralBuilder();
|
||||
|
||||
@@ -124,10 +124,10 @@ foreach(GameConst::$generalPoolAllowOption as $allowOption){
|
||||
if(!$personal || $personal == 'Random'){
|
||||
$personal = Util::choiceRandom(GameConst::$availablePersonality);
|
||||
}
|
||||
$invalidPersonal = $isCentennialAllStar
|
||||
? !in_array($personal, GameConst::$availablePersonality, true)
|
||||
: !array_search($personal, GameConst::$availablePersonality);
|
||||
if($invalidPersonal){
|
||||
$invalidPersonal = $isCentennialAllStar
|
||||
? !in_array($personal, GameConst::$availablePersonality, true)
|
||||
: !array_search($personal, GameConst::$availablePersonality);
|
||||
if($invalidPersonal){
|
||||
Json::die([
|
||||
'result'=>false,
|
||||
'reason'=>'올바르지 않은 성격입니다.'
|
||||
@@ -145,6 +145,23 @@ $builder->setKillturn(5);
|
||||
$builder->setNPCType(0);
|
||||
$builder->setAuxVar('next_change', TimeUtil::nowAddMinutes(12 * $env['turnterm']));
|
||||
$builder->fillRemainSpecAsZero($env);
|
||||
if ($isCentennialAllStar) {
|
||||
$candidateCities = $db->queryFirstColumn(
|
||||
'SELECT city FROM city WHERE level >= 5 AND level <= 6 AND nation = 0'
|
||||
);
|
||||
if (!$candidateCities) {
|
||||
$candidateCities = $db->queryFirstColumn(
|
||||
'SELECT city FROM city WHERE level >= 5 AND level <= 6'
|
||||
);
|
||||
}
|
||||
if (!$candidateCities) {
|
||||
Json::die([
|
||||
'result' => false,
|
||||
'reason' => '장수를 생성할 소·중성이 없습니다.',
|
||||
]);
|
||||
}
|
||||
$builder->setCityID($rng->choice($candidateCities));
|
||||
}
|
||||
$builder->build($env);
|
||||
$generalID = $builder->getGeneralID();
|
||||
if(!$generalID){
|
||||
@@ -189,4 +206,4 @@ $rootDB->insert('member_log', [
|
||||
Json::die([
|
||||
'result'=>true,
|
||||
'reason'=>'success'
|
||||
]);
|
||||
]);
|
||||
|
||||
@@ -8,6 +8,11 @@ use sammo\Util;
|
||||
|
||||
abstract class AbsFromUserPool extends AbsGeneralPool{
|
||||
|
||||
protected static function getCandidateWeight(array $info, int $owner): int|float
|
||||
{
|
||||
return array_sum($info['dex'] ?? []);
|
||||
}
|
||||
|
||||
public function occupyGeneralName(): bool
|
||||
{
|
||||
$generalID = $this->getGeneralBuilder()->getGeneralID();
|
||||
@@ -36,8 +41,7 @@ abstract class AbsFromUserPool extends AbsGeneralPool{
|
||||
$pool = [];
|
||||
foreach($db->query('SELECT id, unique_name, info FROM select_pool WHERE reserved_until IS NULL AND general_id IS NULL', $pickCnt) as $cand){
|
||||
$cand['info'] = Json::decode($cand['info']);
|
||||
$dexTotal = array_sum($cand['info']['dex']);
|
||||
$pool[] = [$cand, $dexTotal];
|
||||
$pool[] = [$cand, static::getCandidateWeight($cand['info'], $owner)];
|
||||
}
|
||||
|
||||
if(count($pool) < $pickCnt){
|
||||
@@ -69,4 +73,4 @@ abstract class AbsFromUserPool extends AbsGeneralPool{
|
||||
|
||||
return array_values($result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ final class CentennialAllStarGrowthService
|
||||
public const POOL_CLASS = 'SPoolUnderU100';
|
||||
public const AUX_KEY = 'event100_allstar';
|
||||
public const TRAIT_UNLOCK_PROGRESS = 0.4;
|
||||
public const NPC_PROGRESS_MULTIPLIER = 0.9;
|
||||
|
||||
private const STAT_KEYS = ['leadership', 'strength', 'intel'];
|
||||
private const DEX_KEYS = ['dex1', 'dex2', 'dex3', 'dex4', 'dex5'];
|
||||
@@ -35,18 +36,47 @@ final class CentennialAllStarGrowthService
|
||||
$builder->setAuxVar(self::AUX_KEY, self::initialAux($targetInfo));
|
||||
}
|
||||
|
||||
public static function calculateProgress(
|
||||
int $startYear,
|
||||
int $year,
|
||||
int $month,
|
||||
float $progressMultiplier = 1.0
|
||||
): float {
|
||||
if ($progressMultiplier < 0 || $progressMultiplier > 1) {
|
||||
throw new \InvalidArgumentException('progress multiplier must be between 0 and 1');
|
||||
}
|
||||
return min(
|
||||
1,
|
||||
CentennialAllStarGrowth::progress($startYear, $year, $month)
|
||||
* $progressMultiplier
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutates the General object but leaves persistence to the caller.
|
||||
*
|
||||
* @return array{progress:float,milestone:int,previousMilestone:int,targetChanged:bool,changed:bool}
|
||||
*/
|
||||
public static function applyTarget(General $general, array $targetInfo, array $env): array
|
||||
public static function applyTarget(
|
||||
General $general,
|
||||
array $targetInfo,
|
||||
array $env,
|
||||
float $progressMultiplier = 1.0
|
||||
): array
|
||||
{
|
||||
$startYear = (int) $env['startyear'];
|
||||
$year = (int) $env['year'];
|
||||
$month = (int) $env['month'];
|
||||
$progress = CentennialAllStarGrowth::progress($startYear, $year, $month);
|
||||
$progressMonth = max(0, ($year - $startYear) * 12 + $month - 1);
|
||||
$progress = self::calculateProgress(
|
||||
$startYear,
|
||||
$year,
|
||||
$month,
|
||||
$progressMultiplier
|
||||
);
|
||||
$progressMonth = (int) floor(
|
||||
max(0, ($year - $startYear) * 12 + $month - 1)
|
||||
* $progressMultiplier
|
||||
);
|
||||
$targetId = (string) ($targetInfo['uniqueName'] ?? '');
|
||||
|
||||
$aux = $general->getAuxVar(self::AUX_KEY);
|
||||
@@ -159,6 +189,42 @@ final class CentennialAllStarGrowthService
|
||||
];
|
||||
}
|
||||
|
||||
public static function progressMultiplierFor(General $general): float
|
||||
{
|
||||
return self::progressMultiplierForNPCType($general->getNPCType());
|
||||
}
|
||||
|
||||
public static function progressMultiplierForNPCType(int $npcType): float
|
||||
{
|
||||
return in_array($npcType, [3, 4], true)
|
||||
? self::NPC_PROGRESS_MULTIPLIER
|
||||
: 1.0;
|
||||
}
|
||||
|
||||
public static function applyCurrentTargetToBuiltNPC(
|
||||
\MeekroDB $db,
|
||||
GeneralBuilder $builder,
|
||||
array $targetInfo,
|
||||
array $env
|
||||
): ?array {
|
||||
if (!self::isActive()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$general = General::createObjFromDB($builder->getGeneralID());
|
||||
if (!in_array($general->getNPCType(), [3, 4], true)) {
|
||||
return null;
|
||||
}
|
||||
$result = self::applyTarget(
|
||||
$general,
|
||||
$targetInfo,
|
||||
$env,
|
||||
self::NPC_PROGRESS_MULTIPLIER
|
||||
);
|
||||
$general->applyDB($db);
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function recordableValue(General $general, string $key): int
|
||||
{
|
||||
$aux = $general->getAuxVar(self::AUX_KEY);
|
||||
|
||||
@@ -7,6 +7,7 @@ use \sammo\Util;
|
||||
use \sammo\JosaUtil;
|
||||
use \sammo\General;
|
||||
use \sammo\ActionLogger;
|
||||
use \sammo\CentennialAllStarGrowthService;
|
||||
use \sammo\GameConst;
|
||||
use \sammo\LastTurn;
|
||||
use \sammo\GameUnitConst;
|
||||
@@ -190,6 +191,12 @@ class che_인재탐색 extends Command\GeneralCommand
|
||||
$newNPC->fillRemainSpecAsRandom($pickTypeList, $avgGen, $env);
|
||||
|
||||
$newNPC->build($this->env);
|
||||
CentennialAllStarGrowthService::applyCurrentTargetToBuiltNPC(
|
||||
$db,
|
||||
$newNPC,
|
||||
$pickedNPC->getInfo(),
|
||||
$this->env
|
||||
);
|
||||
$pickedNPC->occupyGeneralName();
|
||||
$npcName = $newNPC->getGeneralName();
|
||||
$josaRa = JosaUtil::pick($npcName, '라');
|
||||
|
||||
@@ -7,6 +7,7 @@ use \sammo\Util;
|
||||
use \sammo\JosaUtil;
|
||||
use \sammo\General;
|
||||
use \sammo\ActionLogger;
|
||||
use \sammo\CentennialAllStarGrowthService;
|
||||
use \sammo\GameConst;
|
||||
use \sammo\LastTurn;
|
||||
use \sammo\GameUnitConst;
|
||||
@@ -160,6 +161,12 @@ class che_의병모집 extends Command\NationCommand
|
||||
$newNPC->fillRemainSpecAsRandom($pickTypeList, $avgGen, $env);
|
||||
|
||||
$newNPC->build($this->env);
|
||||
CentennialAllStarGrowthService::applyCurrentTargetToBuiltNPC(
|
||||
$db,
|
||||
$newNPC,
|
||||
$pickedNPC->getInfo(),
|
||||
$this->env
|
||||
);
|
||||
$pickedNPC->occupyGeneralName();
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,12 @@ class AdvanceCentennialAllStar extends \sammo\Event\Action
|
||||
) as $row) {
|
||||
$general = General::createObjFromDB((int) $row['no']);
|
||||
$targetInfo = Json::decode($row['info']);
|
||||
$result = CentennialAllStarGrowthService::applyTarget($general, $targetInfo, $env);
|
||||
$result = CentennialAllStarGrowthService::applyTarget(
|
||||
$general,
|
||||
$targetInfo,
|
||||
$env,
|
||||
CentennialAllStarGrowthService::progressMultiplierFor($general)
|
||||
);
|
||||
|
||||
if ($result['milestone'] > $result['previousMilestone']) {
|
||||
$percent = $result['milestone'] * 20;
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace sammo\Event\Action;
|
||||
use \sammo\GameConst;
|
||||
use \sammo\Util;
|
||||
use \sammo\DB;
|
||||
use sammo\CentennialAllStarGrowthService;
|
||||
use sammo\LiteHashDRBG;
|
||||
use sammo\RandUtil;
|
||||
use sammo\UniqueConst;
|
||||
@@ -35,7 +36,8 @@ class CreateManyNPC extends \sammo\Event\Action
|
||||
)));
|
||||
|
||||
$result = [];
|
||||
foreach (pickGeneralFromPool(DB::db(), $rng, 0, $cnt) as $pickedNPC) {
|
||||
$db = DB::db();
|
||||
foreach (pickGeneralFromPool($db, $rng, 0, $cnt) as $pickedNPC) {
|
||||
$age = $rng->nextRangeInt(20, 25);
|
||||
$birthYear = $env['year'] - $age;
|
||||
$deathYear = $env['year'] + $rng->nextRangeInt(10, 50);
|
||||
@@ -50,6 +52,12 @@ class CreateManyNPC extends \sammo\Event\Action
|
||||
}
|
||||
$newNPC->fillRemainSpecAsZero($env);
|
||||
$newNPC->build($env);
|
||||
CentennialAllStarGrowthService::applyCurrentTargetToBuiltNPC(
|
||||
$db,
|
||||
$newNPC,
|
||||
$pickedNPC->getInfo(),
|
||||
$env
|
||||
);
|
||||
$pickedNPC->occupyGeneralName();
|
||||
$result[] = [
|
||||
$newNPC->getGeneralName(), $newNPC->getGeneralID()
|
||||
|
||||
+4682
-4682
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,11 @@ use sammo\RandUtil;
|
||||
|
||||
class SPoolUnderU100 extends AbsFromUserPool
|
||||
{
|
||||
private const MIN_DEX_WEIGHT = 100000;
|
||||
private const STAT_BONUS_MIN_TOTAL = 160;
|
||||
private const STAT_BONUS_MAX_TOTAL = 190;
|
||||
private const STAT_BONUS_MAX_MULTIPLIER = 1.5;
|
||||
|
||||
public function __construct(\MeekroDB $db, RandUtil $rng, array $info, string $validUntil)
|
||||
{
|
||||
$targetInfo = $info;
|
||||
@@ -36,6 +41,32 @@ class SPoolUnderU100 extends AbsFromUserPool
|
||||
return '100기 올스타 클래식';
|
||||
}
|
||||
|
||||
protected static function getCandidateWeight(array $info, int $owner): int|float
|
||||
{
|
||||
$dexWeight = max(
|
||||
self::MIN_DEX_WEIGHT,
|
||||
array_sum($info['dex'] ?? [])
|
||||
);
|
||||
if ($owner <= 0) {
|
||||
return $dexWeight;
|
||||
}
|
||||
|
||||
$statTotal = array_sum([
|
||||
(int) ($info['leadership'] ?? 0),
|
||||
(int) ($info['strength'] ?? 0),
|
||||
(int) ($info['intel'] ?? 0),
|
||||
]);
|
||||
$normalizedStat = min(1, max(
|
||||
0,
|
||||
($statTotal - self::STAT_BONUS_MIN_TOTAL)
|
||||
/ (self::STAT_BONUS_MAX_TOTAL - self::STAT_BONUS_MIN_TOTAL)
|
||||
));
|
||||
$statMultiplier = 1
|
||||
+ (self::STAT_BONUS_MAX_MULTIPLIER - 1) * $normalizedStat;
|
||||
|
||||
return $dexWeight * $statMultiplier;
|
||||
}
|
||||
|
||||
public static function initPool(\MeekroDB $db)
|
||||
{
|
||||
$jsonData = Json::decode(file_get_contents(__DIR__ . '/Pool/UnderS100.json'));
|
||||
|
||||
@@ -115,7 +115,7 @@ function decodeSourceRow(string $line, int $lineNo): array
|
||||
if ($sourceName === '') {
|
||||
fail("line {$lineNo}: empty general name");
|
||||
}
|
||||
$generalName = sprintf('【%d기】%s', $phaseNo, $sourceName);
|
||||
$generalName = sprintf('%d·%s', $phaseNo, $sourceName);
|
||||
if (mb_strlen($generalName) > 32) {
|
||||
fail("line {$lineNo}: generated name exceeds 32 characters: {$generalName}");
|
||||
}
|
||||
|
||||
@@ -17,6 +17,40 @@ final class CentennialAllStarGrowthTest extends TestCase
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use sammo\GeneralPool\SPoolUnderU100;
|
||||
|
||||
require_once __DIR__ . '/../hwe/sammo/AbsGeneralPool.php';
|
||||
require_once __DIR__ . '/../hwe/sammo/AbsFromUserPool.php';
|
||||
require_once __DIR__ . '/../hwe/sammo/GeneralPool/SPoolUnderU100.php';
|
||||
|
||||
final class CentennialAllStarPoolTest extends TestCase
|
||||
{
|
||||
@@ -57,6 +62,13 @@ final class CentennialAllStarPoolTest extends TestCase
|
||||
$generalName = $row[$column['generalName']];
|
||||
self::assertArrayNotHasKey($generalName, $generalNames);
|
||||
$generalNames[$generalName] = true;
|
||||
self::assertMatchesRegularExpression('/^(\d{1,2})·.+$/u', $generalName);
|
||||
self::assertSame(
|
||||
1,
|
||||
preg_match('/^(\d{1,2})·/u', $generalName, $nameMatch)
|
||||
);
|
||||
self::assertSame($phase, (int) $nameMatch[1]);
|
||||
self::assertLessThanOrEqual(32, mb_strlen($generalName));
|
||||
|
||||
self::assertCount(5, $row[$column['dex']]);
|
||||
self::assertNotEmpty($row[$column['selectionReasons']]);
|
||||
@@ -86,6 +98,29 @@ final class CentennialAllStarPoolTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function testCentennialSelectionWeightFavorsUserStatsAndKeepsZeroDexEligible(): void
|
||||
{
|
||||
$method = new ReflectionMethod(SPoolUnderU100::class, 'getCandidateWeight');
|
||||
$method->setAccessible(true);
|
||||
$low = [
|
||||
'leadership' => 80,
|
||||
'strength' => 70,
|
||||
'intel' => 10,
|
||||
'dex' => [0, 0, 0, 0, 0],
|
||||
];
|
||||
$high = [
|
||||
'leadership' => 95,
|
||||
'strength' => 85,
|
||||
'intel' => 10,
|
||||
'dex' => [0, 0, 0, 0, 0],
|
||||
];
|
||||
|
||||
self::assertSame(100000, $method->invoke(null, $low, 0));
|
||||
self::assertSame(100000.0, $method->invoke(null, $low, 1));
|
||||
self::assertSame(150000.0, $method->invoke(null, $high, 1));
|
||||
self::assertSame(100000, $method->invoke(null, $high, 0));
|
||||
}
|
||||
|
||||
public function testEveryHistoricalEventSpecialHasAnImplementation(): void
|
||||
{
|
||||
$pool = json_decode(
|
||||
|
||||
Reference in New Issue
Block a user