From 883080e275aafcd61d5eccce27f8200163b4d811 Mon Sep 17 00:00:00 2001 From: hided62 Date: Tue, 28 Jul 2026 16:28:13 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20100=EA=B8=B0=20NPC=20=EB=8F=99=EC=A1=B0?= =?UTF-8?q?=20=EB=B0=8F=20=ED=9B=84=EB=B3=B4=20=EC=B6=94=EC=B2=A8=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0=EA=B3=BC=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=20=EB=8A=A5=EB=A0=A5=EC=B9=98=20=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hwe/css/select_general_from_pool.css | 18 ++- hwe/j_get_select_pool.php | 9 +- hwe/j_select_picked_general.php | 55 +++++-- hwe/j_update_picked_general.php | 122 +++++++++++----- hwe/sammo/CentennialAllStarGrowthService.php | 145 ++++++++++++++++++- hwe/select_general_from_pool.php | 119 +++++++++++---- hwe/ts/select_general_from_pool.ts | 70 +++++---- tests/CentennialAllStarGrowthTest.php | 74 ++++++++++ 8 files changed, 499 insertions(+), 113 deletions(-) diff --git a/hwe/css/select_general_from_pool.css b/hwe/css/select_general_from_pool.css index 3af9c79e..c8eb0847 100644 --- a/hwe/css/select_general_from_pool.css +++ b/hwe/css/select_general_from_pool.css @@ -51,6 +51,22 @@ display: none; } +.picture_choice { + margin: 8px auto; +} + +.picture_choice label, +.event_picture label { + display: inline-flex; + align-items: center; + gap: 4px; + margin: 4px 8px; +} + +.picture_choice img { + object-fit: cover; +} + .custom_picture { display: none; } @@ -61,4 +77,4 @@ .custom_stat { display: none; -} \ No newline at end of file +} diff --git a/hwe/j_get_select_pool.php b/hwe/j_get_select_pool.php index 3696f142..3d0d4a79 100644 --- a/hwe/j_get_select_pool.php +++ b/hwe/j_get_select_pool.php @@ -11,6 +11,13 @@ function sortTokens(&$tokens){ } function putInfoText(&$info){ + if (($info['event100Growth'] ?? false) === true) { + $initialStats = CentennialAllStarGrowthService::calculateUserInitialStats($info); + $info['initialLeadership'] = $initialStats['leadership']; + $info['initialStrength'] = $initialStats['strength']; + $info['initialIntel'] = $initialStats['intel']; + } + if(key_exists('specialDomestic', $info)){ $class = buildGeneralSpecialDomesticClass($info['specialDomestic']); $info['specialDomesticName'] = $class->getName(); @@ -91,4 +98,4 @@ Json::die([ 'result'=>true, 'pick'=>$pick, 'validUntil'=>$valid_until -]); \ No newline at end of file +]); diff --git a/hwe/j_select_picked_general.php b/hwe/j_select_picked_general.php index db4d4cac..81bea0cf 100644 --- a/hwe/j_select_picked_general.php +++ b/hwe/j_select_picked_general.php @@ -21,6 +21,7 @@ $intel = Util::getPost( ); $personal = Util::getPost('personal', 'string', null); $use_own_picture = Util::getPost('use_own_picture', 'bool', false); +$pictureSource = Util::getPost('picture_source', 'string', 'selected'); if(!$pick){ @@ -69,16 +70,39 @@ if(!$selectInfo){ } $selectInfo = Json::decode($selectInfo); -$ownerInfo = RootDB::db()->queryFirstRow('SELECT `name`,`picture`,`imgsvr` FROM member WHERE `NO`=%i',$userID); -if(!$ownerInfo){ +$ownerInfo = RootDB::db()->queryFirstRow( + 'SELECT `name`,`picture`,`imgsvr`,`grade` FROM member WHERE `NO`=%i', + $userID +); +if(!$ownerInfo){ Json::die([ 'result'=>false, 'reason'=>'멤버 정보를 가져오지 못했습니다.' - ]); -} - - -$gencount = $db->queryFirstField('SELECT count(`no`) FROM general WHERE npc<2'); + ]); +} +if ($isCentennialAllStar) { + if (!in_array($pictureSource, ['selected', 'own'], true)) { + Json::die([ + 'result' => false, + 'reason' => '올바르지 않은 전콘 선택입니다.', + ]); + } + if ($pictureSource === 'own') { + $canUseOwnPicture = in_array('picture', GameConst::$generalPoolAllowOption, true) + && $env['show_img_level'] >= 1 + && $ownerInfo['grade'] >= 1 + && $ownerInfo['picture'] !== ''; + if (!$canUseOwnPicture) { + Json::die([ + 'result' => false, + 'reason' => '사용할 수 있는 내 전콘이 없습니다.', + ]); + } + } +} + + +$gencount = $db->queryFirstField('SELECT count(`no`) FROM general WHERE npc<2'); if ($gencount >= $maxgeneral) { Json::die([ @@ -102,9 +126,12 @@ if ($isCentennialAllStar) { } $builder = $pickedGeneral->getGeneralBuilder(); +if ($isCentennialAllStar) { + CentennialAllStarGrowthService::prepareInitialUser($builder, $selectInfo); +} foreach(GameConst::$generalPoolAllowOption as $allowOption){ - if($allowOption == 'stat'){ + if($allowOption == 'stat' && !$isCentennialAllStar){ $leadership = Util::valueFit($leadership, GameConst::$defaultStatMin, GameConst::$defaultStatMax); $strength = Util::valueFit($strength, GameConst::$defaultStatMin, GameConst::$defaultStatMax); $intel = Util::valueFit($intel, GameConst::$defaultStatMin, GameConst::$defaultStatMax); @@ -117,8 +144,14 @@ foreach(GameConst::$generalPoolAllowOption as $allowOption){ } $builder->setStat($leadership, $strength, $intel); } - else if($allowOption == 'picture' && $use_own_picture){ - $builder->setPicture($ownerInfo['imgsvr'], $ownerInfo['picture']); + else if( + $allowOption == 'picture' + && ( + (!$isCentennialAllStar && $use_own_picture) + || ($isCentennialAllStar && $pictureSource === 'own') + ) + ){ + $builder->setPicture($ownerInfo['imgsvr'], $ownerInfo['picture']); } else if($allowOption == 'ego'){ if(!$personal || $personal == 'Random'){ @@ -135,7 +168,7 @@ foreach(GameConst::$generalPoolAllowOption as $allowOption){ } $builder->setEgo($personal); } -} +} $userNick = $ownerInfo['name']; diff --git a/hwe/j_update_picked_general.php b/hwe/j_update_picked_general.php index a91c7af2..dc6d1ece 100644 --- a/hwe/j_update_picked_general.php +++ b/hwe/j_update_picked_general.php @@ -7,6 +7,7 @@ include "func.php"; WebUtil::requireAJAX(); $pick = Util::getPost('pick'); +$pictureSource = Util::getPost('picture_source', 'string', 'selected'); if(!$pick){ Json::die([ @@ -33,13 +34,22 @@ if(!$generalID){ } list( - $year, - $month, - $startYear, - $maxgeneral, - $npcmode, - $turnterm -) = $gameStor->getValuesAsArray(['year', 'month', 'startyear', 'maxgeneral', 'npcmode', 'turnterm']); + $year, + $month, + $startYear, + $maxgeneral, + $npcmode, + $turnterm, + $showImgLevel +) = $gameStor->getValuesAsArray([ + 'year', + 'month', + 'startyear', + 'maxgeneral', + 'npcmode', + 'turnterm', + 'show_img_level', +]); if($npcmode!=2){ Json::die([ @@ -56,7 +66,10 @@ if(!$info){ ]); } -$ownerInfo = RootDB::db()->queryFirstRow('SELECT `name`,`picture`,`imgsvr` FROM member WHERE `NO`=%i',$userID); +$ownerInfo = RootDB::db()->queryFirstRow( + 'SELECT `name`,`picture`,`imgsvr`,`grade` FROM member WHERE `NO`=%i', + $userID +); if(!$ownerInfo){ Json::die([ 'result'=>false, @@ -65,6 +78,27 @@ if(!$ownerInfo){ } $info = Json::decode($info); +$isCentennialAllStar = CentennialAllStarGrowthService::isActive(); +if ($isCentennialAllStar) { + if (!in_array($pictureSource, ['current', 'own', 'selected'], true)) { + Json::die([ + 'result' => false, + 'reason' => '올바르지 않은 전콘 선택입니다.', + ]); + } + if ($pictureSource === 'own') { + $canUseOwnPicture = in_array('picture', GameConst::$generalPoolAllowOption, true) + && $showImgLevel >= 1 + && $ownerInfo['grade'] >= 1 + && $ownerInfo['picture'] !== ''; + if (!$canUseOwnPicture) { + Json::die([ + 'result' => false, + 'reason' => '사용할 수 있는 내 전콘이 없습니다.', + ]); + } + } +} $generalObj = General::createObjFromDB($generalID); @@ -102,37 +136,45 @@ $db->update('select_pool',[ 'reserved_until'=>null, ], '(owner=%i or reserved_until < %s) AND general_id is NULL', $userID, $now); -$isCentennialAllStar = CentennialAllStarGrowthService::isActive(); -if ($isCentennialAllStar) { - CentennialAllStarGrowthService::applyTarget($generalObj, $info, [ - 'startyear' => $startYear, - 'year' => $year, - 'month' => $month, - ]); -} else { - if(key_exists('leadership', $info)){ - $generalObj->updateVar('leadership', $info['leadership']); - $generalObj->updateVar('strength', $info['strength']); - $generalObj->updateVar('intel', $info['intel']); - } - if(key_exists('dex', $info)){ - $generalObj->updateVar('dex1', $info['dex'][0]); - $generalObj->updateVar('dex2', $info['dex'][1]); - $generalObj->updateVar('dex3', $info['dex'][2]); - $generalObj->updateVar('dex4', $info['dex'][3]); - $generalObj->updateVar('dex5', $info['dex'][4]); - } - if(key_exists('ego', $info)){ - $generalObj->updateVar('personal', $info['ego']); - } - if(key_exists('specialDomestic', $info)){ - $generalObj->updateVar('special', $info['specialDomestic']); - } - if(key_exists('specialWar', $info)){ - $generalObj->updateVar('special2', $info['specialWar']); - } -} -if(key_exists('picture', $info)){ +if ($isCentennialAllStar) { + CentennialAllStarGrowthService::prepareLegacyUserReselection($generalObj); + CentennialAllStarGrowthService::applyTarget($generalObj, $info, [ + 'startyear' => $startYear, + 'year' => $year, + 'month' => $month, + ]); +} else { + if(key_exists('leadership', $info)){ + $generalObj->updateVar('leadership', $info['leadership']); + $generalObj->updateVar('strength', $info['strength']); + $generalObj->updateVar('intel', $info['intel']); + } + if(key_exists('dex', $info)){ + $generalObj->updateVar('dex1', $info['dex'][0]); + $generalObj->updateVar('dex2', $info['dex'][1]); + $generalObj->updateVar('dex3', $info['dex'][2]); + $generalObj->updateVar('dex4', $info['dex'][3]); + $generalObj->updateVar('dex5', $info['dex'][4]); + } + if(key_exists('ego', $info)){ + $generalObj->updateVar('personal', $info['ego']); + } + if(key_exists('specialDomestic', $info)){ + $generalObj->updateVar('special', $info['specialDomestic']); + } + if(key_exists('specialWar', $info)){ + $generalObj->updateVar('special2', $info['specialWar']); + } +} +if ($isCentennialAllStar) { + if ($pictureSource === 'own') { + $generalObj->updateVar('imgsvr', $ownerInfo['imgsvr']); + $generalObj->updateVar('picture', $ownerInfo['picture']); + } elseif ($pictureSource === 'selected' && key_exists('picture', $info)) { + $generalObj->updateVar('imgsvr', $info['imgsvr']); + $generalObj->updateVar('picture', $info['picture']); + } +} elseif(key_exists('picture', $info)){ $generalObj->updateVar('imgsvr', $info['imgsvr']); $generalObj->updateVar('picture', $info['picture']); } @@ -158,4 +200,4 @@ $generalObj->applyDB($db); Json::die([ 'result'=>true, 'reason'=>'success' -]); +]); diff --git a/hwe/sammo/CentennialAllStarGrowthService.php b/hwe/sammo/CentennialAllStarGrowthService.php index fd7f39a2..d2b1e47e 100644 --- a/hwe/sammo/CentennialAllStarGrowthService.php +++ b/hwe/sammo/CentennialAllStarGrowthService.php @@ -19,15 +19,24 @@ final class CentennialAllStarGrowthService return GameConst::$targetGeneralPool === self::POOL_CLASS; } - public static function initialAux(array $targetInfo): array + public static function initialAux(array $targetInfo, ?array $userInitialStats = null): array { + $granted = array_fill_keys(array_merge(self::STAT_KEYS, self::DEX_KEYS), 0); + if ($userInitialStats !== null) { + foreach (self::STAT_KEYS as $key) { + $initial = (int) ($userInitialStats[$key] ?? GameConst::$defaultStatMin); + $granted[$key] = max(0, $initial - min($initial, GameConst::$defaultStatMin)); + } + } + return [ 'targetId' => (string) ($targetInfo['uniqueName'] ?? ''), - 'granted' => array_fill_keys(array_merge(self::STAT_KEYS, self::DEX_KEYS), 0), + 'granted' => $granted, 'progressMonth' => -1, 'milestone' => 0, 'naturalSpecialDomestic' => null, 'eventSpecialDomestic' => null, + 'userInitialStats' => $userInitialStats, ]; } @@ -36,6 +45,130 @@ final class CentennialAllStarGrowthService $builder->setAuxVar(self::AUX_KEY, self::initialAux($targetInfo)); } + /** + * Builds an ordinary-user stat total while preserving the selected + * candidate's relative strengths as closely as integer stats allow. + * + * @return array{leadership:int,strength:int,intel:int} + */ + public static function calculateUserInitialStats(array $targetInfo): array + { + $targets = []; + $bases = []; + foreach (self::STAT_KEYS as $key) { + $target = min( + GameConst::$defaultStatMax, + max(0, (int) ($targetInfo[$key] ?? 0)) + ); + $targets[$key] = $target; + $bases[$key] = min($target, GameConst::$defaultStatMin); + } + + $targetTotal = array_sum($targets); + $desiredTotal = min(GameConst::$defaultStatTotal, $targetTotal); + $baseTotal = array_sum($bases); + $capacityTotal = $targetTotal - $baseTotal; + if ($capacityTotal <= 0 || $desiredTotal <= $baseTotal) { + return $bases; + } + + $ratio = ($desiredTotal - $baseTotal) / $capacityTotal; + $result = []; + $fractions = []; + foreach (self::STAT_KEYS as $idx => $key) { + $raw = $bases[$key] + ($targets[$key] - $bases[$key]) * $ratio; + $result[$key] = (int) floor($raw); + $fractions[] = [ + 'key' => $key, + 'fraction' => $raw - $result[$key], + 'order' => $idx, + ]; + } + + usort($fractions, static function (array $lhs, array $rhs): int { + $fractionOrder = $rhs['fraction'] <=> $lhs['fraction']; + return $fractionOrder !== 0 ? $fractionOrder : $lhs['order'] <=> $rhs['order']; + }); + $remainder = $desiredTotal - array_sum($result); + foreach ($fractions as $fraction) { + if ($remainder <= 0) { + break; + } + $key = $fraction['key']; + if ($result[$key] >= $targets[$key]) { + continue; + } + $result[$key]++; + $remainder--; + } + + return $result; + } + + public static function prepareInitialUser( + GeneralBuilder $builder, + array $targetInfo + ): void { + $initialStats = self::calculateUserInitialStats($targetInfo); + $builder->setStat( + $initialStats['leadership'], + $initialStats['strength'], + $initialStats['intel'] + ); + $builder->setAuxVar( + self::AUX_KEY, + self::initialAux($targetInfo, $initialStats) + ); + } + + /** + * Old 100th-season characters did not distinguish their form-entered + * initial stats from organic growth. Before their first reselection, treat + * the ordinary creation range as the replaceable initial allocation. + */ + public static function prepareLegacyUserReselection(General $general): void + { + $aux = $general->getAuxVar(self::AUX_KEY); + if (!is_array($aux) || is_array($aux['userInitialStats'] ?? null)) { + return; + } + + $granted = is_array($aux['granted'] ?? null) + ? $aux['granted'] + : array_fill_keys(array_merge(self::STAT_KEYS, self::DEX_KEYS), 0); + $legacyInitialStats = []; + foreach (self::STAT_KEYS as $key) { + $current = (int) $general->getVar($key); + $granted[$key] = self::calculateLegacyUserGrant( + $current, + (int) ($granted[$key] ?? 0) + ); + $beforeEventGrant = max( + 0, + $current - max(0, (int) ($aux['granted'][$key] ?? 0)) + ); + $legacyInitialStats[$key] = min( + $beforeEventGrant, + GameConst::$defaultStatMax + ); + } + $aux['granted'] = $granted; + $aux['userInitialStats'] = $legacyInitialStats; + $general->setAuxVar(self::AUX_KEY, $aux); + } + + public static function calculateLegacyUserGrant(int $current, int $eventGrant): int + { + $eventGrant = max(0, $eventGrant); + $beforeEventGrant = max(0, $current - $eventGrant); + $replaceableInitialGrant = max( + 0, + min($beforeEventGrant, GameConst::$defaultStatMax) + - min($beforeEventGrant, GameConst::$defaultStatMin) + ); + return $eventGrant + $replaceableInitialGrant; + } + public static function calculateProgress( int $startYear, int $year, @@ -87,6 +220,10 @@ final class CentennialAllStarGrowthService ? $aux['granted'] : array_fill_keys(array_merge(self::STAT_KEYS, self::DEX_KEYS), 0); $targetChanged = ($aux['targetId'] ?? '') !== $targetId; + $isUserTarget = is_array($aux['userInitialStats'] ?? null); + $nextUserInitialStats = $targetChanged && $isUserTarget + ? self::calculateUserInitialStats($targetInfo) + : ($aux['userInitialStats'] ?? null); $changed = false; foreach (self::STAT_KEYS as $key) { @@ -99,6 +236,9 @@ final class CentennialAllStarGrowthService GameConst::$defaultStatMin, $progress ); + if ($nextUserInitialStats !== null) { + $floor = max($floor, (int) ($nextUserInitialStats[$key] ?? 0)); + } $current = (int) $general->getVar($key); if ($targetChanged) { $result = CentennialAllStarGrowth::replaceTarget( @@ -178,6 +318,7 @@ final class CentennialAllStarGrowthService $aux['granted'] = $granted; $aux['progressMonth'] = max((int) ($aux['progressMonth'] ?? -1), $progressMonth); $aux['milestone'] = max($previousMilestone, $milestone); + $aux['userInitialStats'] = $nextUserInitialStats; $general->setAuxVar(self::AUX_KEY, $aux); return [ diff --git a/hwe/select_general_from_pool.php b/hwe/select_general_from_pool.php index 285336c8..ca7a459c 100644 --- a/hwe/select_general_from_pool.php +++ b/hwe/select_general_from_pool.php @@ -19,8 +19,16 @@ if ($admin['npcmode'] != 2) { $member = RootDB::db()->queryFirstRow("SELECT no,name,picture,imgsvr,grade from member where no= %i", $userID); -$generalID = $db->queryFirstField('SELECT no FROM general WHERE owner = %i', $userID); +$currentGeneral = $db->queryFirstRow( + 'SELECT no,picture,imgsvr FROM general WHERE owner = %i', + $userID +); +$generalID = $currentGeneral['no'] ?? null; $gencount = $db->queryFirstField('SELECT count(`no`) FROM general WHERE npc<2'); +$isCentennialAllStar = CentennialAllStarGrowthService::isActive(); +$canUseOwnPicture = $admin['show_img_level'] >= 1 + && $member['grade'] >= 1 + && $member['picture'] != ""; $nationList = $db->query('SELECT nation,`name`,color,scout FROM nation'); shuffle($nationList); @@ -54,6 +62,7 @@ foreach (getCharacterList(false) as $id => [$name, $info]) {