Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed3c84aa8e | ||
|
|
f4a1e2258d | ||
|
|
4ffd7b5bc0 | ||
|
|
e7c773709d | ||
|
|
1e30659bdd | ||
|
|
162f198f08 | ||
|
|
4aa09a294e | ||
|
|
191abf83c0 | ||
|
|
edfeae5f0f | ||
|
|
424184a51a | ||
|
|
03a321f95d | ||
|
|
e3615fa6bb | ||
|
|
32991f736d | ||
|
|
8b2748d3d4 | ||
|
|
883080e275 | ||
|
|
ce807e2564 | ||
|
|
9a2ac82373 | ||
|
|
3c35539c16 | ||
|
|
5ab08403c6 | ||
|
|
c1ce478d22 |
+5
-2
@@ -13,6 +13,9 @@ $db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
|
||||
[$turntime, $tnmt_time] = $gameStor->getValuesAsArray(['turntime','tnmt_time']);
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$turntimeDisplay = $clock->formatTick(Util::toInt($turntime), true);
|
||||
$tnmtTimeDisplay = $tnmt_time === null ? '-' : $clock->formatTick(Util::toInt($tnmt_time), true);
|
||||
|
||||
$plock = $db->queryFirstField('SELECT plock FROM plock WHERE `type` ="GAME"');
|
||||
?>
|
||||
@@ -30,8 +33,8 @@ $plock = $db->queryFirstField('SELECT plock FROM plock WHERE `type` ="GAME"');
|
||||
</head>
|
||||
<body>
|
||||
<form action=_119_b.php method=post>
|
||||
시간조정 : <input type=text size=3 name=minute><input type=submit name=btn value='분당김'><input type=submit name=btn value='분지연'> 최종갱신 : <?=$turntime?><br>
|
||||
시간조정 : <input type=text size=3 name=minute2><input type=submit name=btn value='토너분당김'><input type=submit name=btn value='토너분지연'> 토너먼트 : <?=$tnmt_time?><br>
|
||||
시간조정 : <input type=text size=3 name=minute><input type=submit name=btn value='분당김'><input type=submit name=btn value='분지연'> 최종갱신 : <?=$turntimeDisplay?> (tick <?=$turntime?>)<br>
|
||||
시간조정 : <input type=text size=3 name=minute2><input type=submit name=btn value='토너분당김'><input type=submit name=btn value='토너분지연'> 토너먼트 : <?=$tnmtTimeDisplay?> (tick <?=$tnmt_time?>)<br>
|
||||
봉급지급 : <input type=submit name=btn value='금지급'><input type=submit name=btn value='쌀지급'><br>
|
||||
락 풀 기 : <input type=submit name=btn value='락걸기'><input type=submit name=btn value='락풀기'> 현재 : <?=$plock>0?"동결중":"가동중"?><br>
|
||||
</form>
|
||||
|
||||
+9
-36
@@ -38,21 +38,9 @@ switch ($btn) {
|
||||
usleep(500000);
|
||||
}
|
||||
|
||||
$gameStor->cacheValues(['turntime', 'starttime', 'tnmt_time']);
|
||||
$turntime = (new \DateTimeImmutable($gameStor->turntime))->sub(new \DateInterval("PT{$minute}M"));
|
||||
$starttime = (new \DateTimeImmutable($gameStor->starttime))->sub(new \DateInterval("PT{$minute}M"));
|
||||
$tnmt_time = (new \DateTimeImmutable($gameStor->tnmt_time))->sub(new \DateInterval("PT{$minute}M"));
|
||||
|
||||
$gameStor->turntime = $turntime->format('Y-m-d H:i:s.u');
|
||||
$gameStor->starttime = $starttime->format('Y-m-d H:i:s');
|
||||
$gameStor->tnmt_time = $tnmt_time->format('Y-m-d H:i:s');
|
||||
|
||||
$db->update('general', [
|
||||
'turntime' => $db->sqleval('DATE_SUB(turntime, INTERVAL %i MINUTE)', $minute)
|
||||
], true);
|
||||
$db->update('ng_auction', [
|
||||
'close_date' => $db->sqleval('DATE_SUB(close_date, INTERVAL %i MINUTE)', $minute)
|
||||
], 'finished = 0');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
// 스케줄 전체를 벽시계에서 빼지 않고 논리 현재 tick만 앞으로 이동합니다.
|
||||
$clock->advance($gameStor, $clock->ticksFromMinutes($minute));
|
||||
if ($locked) {
|
||||
unlock();
|
||||
}
|
||||
@@ -66,34 +54,19 @@ switch ($btn) {
|
||||
}
|
||||
usleep(500000);
|
||||
}
|
||||
$gameStor->cacheValues(['turntime', 'starttime', 'tnmt_time']);
|
||||
$turntime = (new \DateTimeImmutable($gameStor->turntime))->add(new \DateInterval("PT{$minute}M"));
|
||||
$starttime = (new \DateTimeImmutable($gameStor->starttime))->add(new \DateInterval("PT{$minute}M"));
|
||||
$tnmt_time = (new \DateTimeImmutable($gameStor->tnmt_time))->add(new \DateInterval("PT{$minute}M"));
|
||||
|
||||
$gameStor->turntime = $turntime->format('Y-m-d H:i:s.u');
|
||||
$gameStor->starttime = $starttime->format('Y-m-d H:i:s');
|
||||
$gameStor->tnmt_time = $tnmt_time->format('Y-m-d H:i:s');
|
||||
|
||||
$db->update('general', [
|
||||
'turntime' => $db->sqleval('DATE_ADD(turntime, INTERVAL %i MINUTE)', $minute)
|
||||
], true);
|
||||
$db->update('ng_auction', [
|
||||
'close_date' => $db->sqleval('DATE_ADD(close_date, INTERVAL %i MINUTE)', $minute)
|
||||
], 'finished = 0');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$clock->advance($gameStor, -$clock->ticksFromMinutes($minute));
|
||||
if ($locked) {
|
||||
unlock();
|
||||
}
|
||||
break;
|
||||
case "토너분당김":
|
||||
$tnmt_time = new \DateTime($gameStor->tnmt_time);
|
||||
$tnmt_time->sub(new \DateInterval("PT{$minute2}M"));
|
||||
$gameStor->tnmt_time = $tnmt_time->format('Y-m-d H:i:s');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$gameStor->tnmt_time = Util::toInt($gameStor->tnmt_time) - $clock->ticksFromMinutes($minute2);
|
||||
break;
|
||||
case "토너분지연":
|
||||
$tnmt_time = new \DateTimeImmutable($gameStor->tnmt_time);
|
||||
$tnmt_time->add(new \DateInterval("PT{$minute2}M"));
|
||||
$gameStor->tnmt_time = $tnmt_time->format('Y-m-d H:i:s');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$gameStor->tnmt_time = Util::toInt($gameStor->tnmt_time) + $clock->ticksFromMinutes($minute2);
|
||||
break;
|
||||
case "금지급":
|
||||
processGoldIncome();
|
||||
|
||||
+2
-2
@@ -40,7 +40,7 @@ $admin = getAdmin();
|
||||
</tr>
|
||||
<tr>
|
||||
<td width=110 align=right>시작시간변경</td>
|
||||
<td width=285><input type=text size=20 maxlength=20 style=color:white;background-color:black;text-align:right; name=starttime value='<?=substr($admin['starttime'], 0, 19)?>'><input type=submit name=btn value=변경1></td>
|
||||
<td width=285><input type=text size=20 maxlength=20 style=color:white;background-color:black;text-align:right; name=starttime value='<?=substr($admin['starttime_display'], 0, 19)?>'><input type=submit name=btn value=변경1></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width=110 align=right>최대 장수</td>
|
||||
@@ -52,7 +52,7 @@ $admin = getAdmin();
|
||||
<td width=110 align=right>시작 년도</td>
|
||||
<td width=285><input type=text size=3 maxlength=3 style=color:white;background-color:black;text-align:right; name=startyear value='<?=$admin['startyear']?>'><input type=submit name=btn value=변경4></td>
|
||||
<td width=110 align=right>최근 갱신 시간</td>
|
||||
<td width=285> <?=$admin['turntime']?></td>
|
||||
<td width=285> <?=$admin['turntime_display']?> (tick <?=$admin['turntime']?>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width=110 align=right>턴시간</td>
|
||||
|
||||
@@ -43,7 +43,12 @@ switch ($btn) {
|
||||
pushGlobalHistoryLog(["<R>★</><S>{$log}</>"]);
|
||||
break;
|
||||
case "변경1":
|
||||
$gameStor->starttime = (new \DateTime($starttime))->format('Y-m-d H:i:s');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$gameStor->clock_base_time = TimeUtil::format(GameClock::baseTimeForProjection(
|
||||
new \DateTimeImmutable($starttime),
|
||||
Util::toInt($gameStor->starttime),
|
||||
$clock->getTurnTermMinutes(),
|
||||
), true);
|
||||
break;
|
||||
case "변경2":
|
||||
$gameStor->maxgeneral = $maxgeneral;
|
||||
@@ -78,4 +83,4 @@ switch ($btn) {
|
||||
break;
|
||||
}
|
||||
|
||||
header('location:_admin1.php');
|
||||
header('location:_admin1.php');
|
||||
|
||||
@@ -99,7 +99,7 @@ switch ($btn) {
|
||||
], '`no` IN %li', $genlist);
|
||||
break;
|
||||
case "강제 사망":
|
||||
$date = TimeUtil::now(true);
|
||||
$date = GameClock::fromStorage($gameStor)->nowTick();
|
||||
$db->update('general', [
|
||||
'killturn' => 0,
|
||||
'turntime' => $date,
|
||||
|
||||
+3
-2
@@ -42,6 +42,7 @@ if ($session->userGrade < 6) {
|
||||
}
|
||||
|
||||
$db = DB::db();
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
|
||||
|
||||
if ($btn == '정렬하기') {
|
||||
$gen = 0;
|
||||
@@ -111,7 +112,7 @@ $generalObj = General::createObjFromDB($gen, null, GeneralQueryMode::FullWithAcc
|
||||
대상장수 :
|
||||
<select name=gen size=1>
|
||||
<?php foreach ($generalBasicList as $general) : ?>
|
||||
<option <?= $gen == $general['no'] ? 'selected' : '' ?> value='<?= $general['no'] ?>'><?= $general['name'] ?> (<?= substr($general['turntime'], 14, 5) ?>)</option>
|
||||
<option <?= $gen == $general['no'] ? 'selected' : '' ?> value='<?= $general['no'] ?>'><?= $general['name'] ?> (<?= substr($clock->formatTick(Util::toInt($general['turntime'])), 14, 5) ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<input type=submit name=btn value='조회하기'>
|
||||
@@ -179,4 +180,4 @@ $generalObj = General::createObjFromDB($gen, null, GeneralQueryMode::FullWithAcc
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -16,7 +16,7 @@ $db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
increaseRefresh("현재도시", 1);
|
||||
|
||||
$lastExecute = substr($gameStor->turntime, 5, 14);
|
||||
$lastExecute = substr(GameClock::fromStorage($gameStor)->formatTick(Util::toInt($gameStor->turntime)), 5, 14);
|
||||
|
||||
$me = $db->queryFirstRow('SELECT no,nation,officer_level,city from general where owner=%i', $userID);
|
||||
$myNation = $db->queryFirstRow('SELECT nation,level,spy FROM nation WHERE nation=%i', $me['nation']) ?? [
|
||||
@@ -537,4 +537,4 @@ $templates = new \League\Plates\Engine('templates');
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
+3
-1
@@ -129,6 +129,7 @@ $templates = new \League\Plates\Engine('templates');
|
||||
);
|
||||
|
||||
$generalTurnList = [];
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
|
||||
foreach ($db->queryAllLists(
|
||||
'SELECT general_id, turn_idx, brief FROM general_turn WHERE general_id IN %li AND turn_idx < 5 ORDER BY general_id ASC, turn_idx ASC',
|
||||
@@ -142,6 +143,7 @@ $templates = new \League\Plates\Engine('templates');
|
||||
|
||||
$genCntEff = 0;
|
||||
foreach ($generals as &$general) {
|
||||
$general['turntime'] = $clock->formatTick(Util::toInt($general['turntime']), true);
|
||||
$general['cityText'] = CityConst::byID($general['city'])->name;
|
||||
$general['troopText'] = $troopName[$general['troop']] ?? '-';
|
||||
|
||||
@@ -284,4 +286,4 @@ $templates = new \League\Plates\Engine('templates');
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
+30
-12
@@ -19,10 +19,10 @@ $userID = Session::getUserID();
|
||||
$generalID = $session->generalID;
|
||||
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$gameStor->cacheValues(['turntime', 'opentime', 'autorun_user', 'npcmode']);
|
||||
|
||||
increaseRefresh("내정보", 1);
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$gameStor->cacheValues(['turntime', 'opentime', 'autorun_user', 'npcmode']);
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$nowTick = $clock->nowTick();
|
||||
|
||||
$me = General::createObjFromDB($generalID, null, GeneralQueryMode::FullWithAccessLog);
|
||||
|
||||
@@ -41,12 +41,30 @@ $lastRefresh = $db->queryFirstField(
|
||||
$generalID
|
||||
);
|
||||
|
||||
$targetTime = addTurn($lastRefresh, $gameStor->turnterm, GameConst::$minTurnDieOnPrestart);
|
||||
if ($gameStor->turntime <= $gameStor->opentime) {
|
||||
//서버 가오픈시 할 수 있는 행동
|
||||
if ($me->getNPCType() == 0 && $me->getNationID() == 0) {
|
||||
$nextChange = $me->getAuxVar('next_change');
|
||||
if (!is_int($nextChange)) {
|
||||
$nextChange = null;
|
||||
}
|
||||
$nextChangeDisplay = $nextChange === null ? null : $clock->formatTick($nextChange);
|
||||
|
||||
increaseRefresh("내정보", 1);
|
||||
if ($gameStor->turntime <= $gameStor->opentime) {
|
||||
$targetTime = $me->getAuxVar('prestart_delete_after');
|
||||
if (!is_int($targetTime)) {
|
||||
$targetTime = addTurn(
|
||||
$lastRefresh === null ? $nowTick : Util::toInt($lastRefresh),
|
||||
$gameStor->turnterm,
|
||||
GameConst::$minTurnDieOnPrestart
|
||||
);
|
||||
$me->setAuxVar('prestart_delete_after', $targetTime);
|
||||
$me->applyDB($db);
|
||||
}
|
||||
$targetTimeDisplay = $clock->formatTick($targetTime);
|
||||
|
||||
//서버 가오픈시 할 수 있는 행동
|
||||
if ($me->getNPCType() == 0 && $me->getNationID() == 0) {
|
||||
$showDieOnPrestartBtn = true;
|
||||
if ($targetTime <= TimeUtil::now()) {
|
||||
if ($targetTime <= $nowTick) {
|
||||
$availableDieOnPrestart = true;
|
||||
}
|
||||
}
|
||||
@@ -159,7 +177,7 @@ $changeDefence999Atmos = $me->onCalcDomestic('changeDefenceTrain', "atmos999", $
|
||||
<a href="b_myPage.php?detachNPC=1"><button type="button" style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px;>빙의 해체 요청</button></a>-->
|
||||
|
||||
<?php if ($showDieOnPrestartBtn) : ?>
|
||||
가오픈 기간 내 장수 삭제 (<?= substr($targetTime, 0, 19) ?> 부터)<br>
|
||||
가오픈 기간 내 장수 삭제 (<?= $targetTimeDisplay ?> 부터)<br>
|
||||
<button type="button" id='dieOnPrestart' style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px;>장수 삭제</button><br><br>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -174,7 +192,7 @@ $changeDefence999Atmos = $me->onCalcDomestic('changeDefenceTrain', "atmos999", $
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($gameStor->npcmode == 2 && $me->getNPCType() == 0) : ?>
|
||||
다른 장수 선택 (<?= substr($me->getAuxVar('next_change') ?? TimeUtil::now(), 0, 19) ?> 부터)<br>
|
||||
다른 장수 선택 (<?= $nextChangeDisplay ?? '지금' ?>부터)<br>
|
||||
<a href="select_general_from_pool.php" id='select_general_from_pool'><button type="button" style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px;>다른 장수 선택</button></a><br><br>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -272,4 +290,4 @@ $changeDefence999Atmos = $me->onCalcDomestic('changeDefenceTrain', "atmos999", $
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -228,10 +228,10 @@ if($btn == "자동개최설정") {
|
||||
$phase = 0;
|
||||
startBetting($admin['tnmt_type'], 720);
|
||||
} elseif($btn == "베팅마감") {
|
||||
$dt = date("Y-m-d H:i:s", time() + 60);
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$gameStor->tournament=7;
|
||||
$gameStor->phase=0;
|
||||
$gameStor->tnmt_time = $dt;
|
||||
$gameStor->tnmt_time = $clock->nowTick() + $clock->ticksFromSeconds(60);
|
||||
} elseif($btn == "16강") {
|
||||
finalFight($admin['tnmt_type'], $admin['tournament'], $admin['phase'], 16);
|
||||
} elseif($btn == "8강") {
|
||||
@@ -248,4 +248,4 @@ if($btn == "자동개최설정") {
|
||||
$gameStor->tnmt_msg = $msg;
|
||||
}
|
||||
|
||||
header('location:b_tournament.php');
|
||||
header('location:b_tournament.php');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+102
-110
@@ -630,7 +630,11 @@ function generalInfo(General $generalObj)
|
||||
$injury = "건강";
|
||||
}
|
||||
|
||||
$remaining = (new \DateTimeImmutable($generalObj->getTurnTime()))->diff(new \DateTimeImmutable())->i;
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env'));
|
||||
$remaining = max(0, intdiv(
|
||||
$generalObj->getTurnTick() - $clock->nowTick(),
|
||||
$clock->ticksFromMinutes(1),
|
||||
));
|
||||
|
||||
if ($nation['color'] == "") {
|
||||
$nation['color'] = "#000000";
|
||||
@@ -921,45 +925,37 @@ function banner()
|
||||
);
|
||||
}
|
||||
|
||||
function addTurn($date, int $turnterm, int $turn = 1, bool $withFraction = true)
|
||||
{
|
||||
$date = new \DateTime($date);
|
||||
$target = $turnterm * $turn;
|
||||
$date->add(new \DateInterval("PT{$target}M"));
|
||||
if ($withFraction) {
|
||||
return $date->format('Y-m-d H:i:s.u');
|
||||
}
|
||||
return $date->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
function subTurn($date, int $turnterm, int $turn = 1, bool $withFraction = true)
|
||||
{
|
||||
$date = new \DateTime($date);
|
||||
$target = $turnterm * $turn;
|
||||
$date->sub(new \DateInterval("PT{$target}M"));
|
||||
if ($withFraction) {
|
||||
return $date->format('Y-m-d H:i:s.u');
|
||||
}
|
||||
return $date->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
function cutTurn($date, int $turnterm, bool $withFraction = true)
|
||||
{
|
||||
$date = new \DateTime($date);
|
||||
|
||||
$baseDate = new \DateTime($date->format('Y-m-d'));
|
||||
$baseDate->sub(new \DateInterval("P1D"));
|
||||
$baseDate->add(new \DateInterval("PT1H"));
|
||||
|
||||
$diffMin = intdiv($date->getTimeStamp() - $baseDate->getTimeStamp(), 60);
|
||||
$diffMin -= $diffMin % $turnterm;
|
||||
|
||||
$baseDate->add(new \DateInterval("PT{$diffMin}M"));
|
||||
if ($withFraction) {
|
||||
return $baseDate->format('Y-m-d H:i:s.u');
|
||||
}
|
||||
return $baseDate->format('Y-m-d H:i:s');
|
||||
}
|
||||
function addTurn(int $tick, int $turnterm, int $turn = 1, bool $withFraction = true): int
|
||||
{
|
||||
return $tick + GameClock::TICKS_PER_TURN * $turn;
|
||||
}
|
||||
|
||||
function subTurn(int $tick, int $turnterm, int $turn = 1, bool $withFraction = true): int
|
||||
{
|
||||
return $tick - GameClock::TICKS_PER_TURN * $turn;
|
||||
}
|
||||
|
||||
function cutTurn(int $tick, int $turnterm, bool $withFraction = true): int
|
||||
{
|
||||
$remainder = $tick % GameClock::TICKS_PER_TURN;
|
||||
if ($remainder < 0) {
|
||||
$remainder += GameClock::TICKS_PER_TURN;
|
||||
}
|
||||
return $tick - $remainder;
|
||||
}
|
||||
|
||||
/** 시나리오 초기화 입력인 벽시계를 기존 01:00 기준 월 경계로 정렬합니다. */
|
||||
function cutTurnDateTime(string $date, int $turnterm, bool $withFraction = true): string
|
||||
{
|
||||
$dateObj = new \DateTime($date);
|
||||
$baseDate = new \DateTime($dateObj->format('Y-m-d'));
|
||||
$baseDate->sub(new \DateInterval('P1D'));
|
||||
$baseDate->add(new \DateInterval('PT1H'));
|
||||
$diffMin = intdiv($dateObj->getTimestamp() - $baseDate->getTimestamp(), 60);
|
||||
$diffMin -= $diffMin % $turnterm;
|
||||
$baseDate->add(new \DateInterval("PT{$diffMin}M"));
|
||||
return $baseDate->format($withFraction ? 'Y-m-d H:i:s.u' : 'Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
function cutDay($date, int $turnterm, bool $withFraction = true)
|
||||
{
|
||||
@@ -1001,11 +997,9 @@ function increaseRefresh($type = "", $cnt = 1)
|
||||
$generalID = $session->generalID;
|
||||
$userGrade = $session->userGrade;
|
||||
|
||||
$dateObj = new \DateTimeImmutable();
|
||||
$date = TimeUtil::format($dateObj, false);
|
||||
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$date = GameClock::fromStorage($gameStor)->nowTick();
|
||||
$isunited = $gameStor->isunited;
|
||||
$opentime = $gameStor->opentime;
|
||||
|
||||
@@ -1145,18 +1139,19 @@ function unlock(): bool
|
||||
return $db->affectedRows() > 0;
|
||||
}
|
||||
|
||||
function timeover(): bool
|
||||
function timeover(): bool
|
||||
{
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
|
||||
list($turnterm, $turntime) = $gameStor->getValuesAsArray(['turnterm', 'turntime']);
|
||||
$diff = (new \DateTime())->getTimestamp() - (new \DateTime($turntime))->getTimestamp();
|
||||
list($turnterm, $turntime) = $gameStor->getValuesAsArray(['turnterm', 'turntime']);
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$diff = $clock->nowTick() - Util::toInt($turntime);
|
||||
|
||||
$t = min($turnterm, 5);
|
||||
|
||||
$term = $diff;
|
||||
if ($term >= $t || $term < 0) {
|
||||
$term = $clock->ticksFromSeconds($t);
|
||||
if ($diff >= $term || $diff < 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@@ -1169,9 +1164,8 @@ function checkDelay()
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
|
||||
//서버정보
|
||||
$now = new \DateTimeImmutable();
|
||||
$turntime = new \DateTimeImmutable($gameStor->turntime);
|
||||
$timeMinDiff = intdiv($now->getTimestamp() - $turntime->getTimestamp(), 60);
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$timeMinDiff = intdiv($clock->nowTick() - Util::toInt($gameStor->turntime), $clock->ticksFromMinutes(1));
|
||||
|
||||
// 1턴이상 갱신 없었으면 서버 지연
|
||||
$term = $gameStor->turnterm;
|
||||
@@ -1185,20 +1179,17 @@ function checkDelay()
|
||||
//지연 해야할 밀린 턴 횟수
|
||||
$iter = intdiv($timeMinDiff, $term);
|
||||
if ($iter > $threshold) {
|
||||
$minute = $iter * $term;
|
||||
$newTurntime = $turntime->add(new \DateInterval("PT{$minute}M"));
|
||||
$newNextTurntime = $turntime->add(new \DateInterval("PT{$term}M"));
|
||||
$gameStor->turntime = $newTurntime->format('Y-m-d H:i:s');
|
||||
$gameStor->starttime = (new \DateTimeImmutable($gameStor->starttime))
|
||||
->add(new \DateInterval("PT{$minute}M"))
|
||||
->format('Y-m-d H:i:s');
|
||||
|
||||
$db->update('general', [
|
||||
'turntime' => $db->sqleval('DATE_ADD(turntime, INTERVAL %i MINUTE)', $minute)
|
||||
], 'turntime<=DATE_ADD(turntime, INTERVAL %i MINUTE)', $term);
|
||||
$db->update('ng_auction', [
|
||||
'close_date' => $db->sqleval('DATE_ADD(close_date, INTERVAL %i MINUTE)', $minute)
|
||||
], 'finished = 0');
|
||||
$minute = $iter * $term;
|
||||
$delayTick = $clock->ticksFromMinutes($minute);
|
||||
$gameStor->turntime = Util::toInt($gameStor->turntime) + $delayTick;
|
||||
$gameStor->starttime = Util::toInt($gameStor->starttime) + $delayTick;
|
||||
|
||||
$db->update('general', [
|
||||
'turntime' => $db->sqleval('turntime + %i', $delayTick)
|
||||
], true);
|
||||
$db->update('ng_auction', [
|
||||
'close_tick' => $db->sqleval('close_tick + %i', $delayTick)
|
||||
], 'finished = 0');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1247,17 +1238,15 @@ function updateOnline()
|
||||
$gameStor->online_nation = join(', ', $onlineNation);
|
||||
}
|
||||
|
||||
function turnDate($curtime)
|
||||
function turnDate($curtime)
|
||||
{
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$admin = $gameStor->getValues(['startyear', 'starttime', 'turnterm', 'year', 'month']);
|
||||
|
||||
$turn = $admin['starttime'];
|
||||
$curturn = cutTurn($curtime, $admin['turnterm']);
|
||||
$term = $admin['turnterm'];
|
||||
|
||||
$num = intdiv((strtotime($curturn) - strtotime($turn)), $term * 60);
|
||||
$curturn = cutTurn(Util::toInt($curtime), $admin['turnterm']);
|
||||
$num = intdiv($curturn - Util::toInt($turn), GameClock::TICKS_PER_TURN);
|
||||
|
||||
$date = $admin['startyear'] * 12;
|
||||
$date += $num;
|
||||
@@ -1705,12 +1694,21 @@ function tryUniqueItemLottery(RandUtil $rng, General $general, string $acquireTy
|
||||
return giveRandomUniqueItem($rng, $general, $acquireType);
|
||||
}
|
||||
|
||||
function getAdmin()
|
||||
{
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
return $gameStor->getAll();
|
||||
}
|
||||
function getAdmin()
|
||||
{
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$admin = $gameStor->getAll();
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
foreach (['turntime', 'starttime', 'opentime', 'tnmt_time'] as $key) {
|
||||
if (($admin[$key] ?? null) !== null) {
|
||||
$admin["{$key}_display"] = $clock->formatTick(Util::toInt($admin[$key]), true);
|
||||
}
|
||||
}
|
||||
$admin['clock_now_tick'] = $clock->nowTick();
|
||||
$admin['clock_now_display'] = $clock->formatTick($admin['clock_now_tick'], true);
|
||||
return $admin;
|
||||
}
|
||||
|
||||
/** @return General[] */
|
||||
function deleteNation(General $lord, bool $applyDB): array
|
||||
@@ -2199,35 +2197,29 @@ function SabotageInjury(RandUtil $rng, array $cityGeneralList, string $reason):
|
||||
return $injuryCount;
|
||||
}
|
||||
|
||||
function getRandTurn(RandUtil $rng, $term, ?\DateTimeInterface $baseDateTime = null)
|
||||
{
|
||||
if ($baseDateTime === null) {
|
||||
$baseDateTime = new \DateTimeImmutable();
|
||||
} else if ($baseDateTime instanceof \DateTime) {
|
||||
$baseDateTime = \DateTimeImmutable::createFromMutable($baseDateTime);
|
||||
} else if ($baseDateTime instanceof \DateTimeImmutable) {
|
||||
//do Nothing
|
||||
} else {
|
||||
throw new MustNotBeReachedException();
|
||||
}
|
||||
|
||||
$randSecond = $rng->nextRangeInt(0, 60 * $term - 1);
|
||||
$randFraction = $rng->nextRangeInt(0, 999999) / 1000000; //6자리 소수
|
||||
|
||||
return TimeUtil::format($baseDateTime->add(TimeUtil::secondsToDateInterval($randSecond + $randFraction)), true);
|
||||
}
|
||||
|
||||
function getRandTurn2(RandUtil $rng, $term, ?\DateTimeInterface $baseDateTime = null)
|
||||
{
|
||||
if ($baseDateTime === null) {
|
||||
$baseDateTime = new \DateTimeImmutable();
|
||||
} else if ($baseDateTime instanceof \DateTime) {
|
||||
$baseDateTime = \DateTimeImmutable::createFromMutable($baseDateTime);
|
||||
} else {
|
||||
throw new MustNotBeReachedException();
|
||||
}
|
||||
$randSecond = $rng->nextRangeInt(0, 60 * $term - 1);
|
||||
$randFraction = $rng->nextRangeInt(0, 999999) / 1000000; //6자리 소수
|
||||
|
||||
return $baseDateTime->sub(TimeUtil::secondsToDateInterval($randSecond + $randFraction))->format('Y-m-d H:i:s.u');
|
||||
}
|
||||
function getRandTurn(RandUtil $rng, int $term, ?int $baseTick = null): int
|
||||
{
|
||||
$db = DB::db();
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
|
||||
$baseTick ??= $clock->nowTick();
|
||||
|
||||
$randSecond = $rng->nextRangeInt(0, 60 * $term - 1);
|
||||
$randMicrosecond = $rng->nextRangeInt(0, 999999); // 레거시 RNG 소비 6자리 유지
|
||||
|
||||
return $baseTick
|
||||
+ $clock->ticksFromSeconds($randSecond)
|
||||
+ intdiv($randMicrosecond * $clock->ticksPerSecond(), 1_000_000);
|
||||
}
|
||||
|
||||
function getRandTurn2(RandUtil $rng, int $term, ?int $baseTick = null): int
|
||||
{
|
||||
$db = DB::db();
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
|
||||
$baseTick ??= $clock->nowTick();
|
||||
$randSecond = $rng->nextRangeInt(0, 60 * $term - 1);
|
||||
$randMicrosecond = $rng->nextRangeInt(0, 999999); // 레거시 RNG 소비 6자리 유지
|
||||
|
||||
return $baseTick
|
||||
- $clock->ticksFromSeconds($randSecond)
|
||||
- intdiv($randMicrosecond * $clock->ticksPerSecond(), 1_000_000);
|
||||
}
|
||||
|
||||
@@ -62,11 +62,12 @@ function processAuction()
|
||||
{
|
||||
$db = DB::db();
|
||||
|
||||
$now = TimeUtil::now();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$nowTick = GameClock::fromStorage($gameStor)->nowTick();
|
||||
|
||||
$auctionList = $db->queryAllLists(
|
||||
'SELECT id, `type` FROM ng_auction WHERE `close_date` <= %s AND finished = 0',
|
||||
$now
|
||||
'SELECT id, `type` FROM ng_auction WHERE `close_tick` <= %i AND finished = 0',
|
||||
$nowTick
|
||||
);
|
||||
|
||||
if (!$auctionList) {
|
||||
|
||||
+65
-37
@@ -42,15 +42,21 @@ function pushGeneralCommand(int $generalID, int $turnCnt=1){
|
||||
|
||||
$db = DB::db();
|
||||
|
||||
$db->update('general_turn', [
|
||||
'turn_idx'=>$db->sqleval('turn_idx + %i', $turnCnt)
|
||||
], 'general_id=%i ORDER BY turn_idx DESC', $generalID);
|
||||
$db->update('general_turn', [
|
||||
'turn_idx'=>$db->sqleval('turn_idx - %i', GameConst::$maxTurn),
|
||||
'action'=>'휴식',
|
||||
'arg'=>'{}',
|
||||
'brief'=>'휴식'
|
||||
], 'general_id=%i AND turn_idx >= %i', $generalID, GameConst::$maxTurn);
|
||||
$db->query(
|
||||
'UPDATE general_turn AS dst
|
||||
LEFT JOIN general_turn AS src
|
||||
ON src.general_id = dst.general_id
|
||||
AND src.turn_idx = dst.turn_idx - %i
|
||||
SET dst.action = IF(src.id IS NULL, %s, src.action),
|
||||
dst.arg = IF(src.id IS NULL, %s, src.arg),
|
||||
dst.brief = IF(src.id IS NULL, %s, src.brief)
|
||||
WHERE dst.general_id = %i',
|
||||
$turnCnt,
|
||||
'휴식',
|
||||
'{}',
|
||||
'휴식',
|
||||
$generalID
|
||||
);
|
||||
}
|
||||
|
||||
function pullGeneralCommand(int $generalID, int $turnCnt=1){
|
||||
@@ -67,15 +73,21 @@ function pullGeneralCommand(int $generalID, int $turnCnt=1){
|
||||
|
||||
$db = DB::db();
|
||||
|
||||
$db->update('general_turn', [
|
||||
'turn_idx'=>$db->sqleval('turn_idx + %i', GameConst::$maxTurn),
|
||||
'action'=>'휴식',
|
||||
'arg'=>'{}',
|
||||
'brief'=>'휴식'
|
||||
], 'general_id=%i AND turn_idx < %i', $generalID, $turnCnt);
|
||||
$db->update('general_turn', [
|
||||
'turn_idx'=>$db->sqleval('turn_idx - %i', $turnCnt)
|
||||
], 'general_id=%i ORDER BY turn_idx ASC', $generalID);
|
||||
$db->query(
|
||||
'UPDATE general_turn AS dst
|
||||
LEFT JOIN general_turn AS src
|
||||
ON src.general_id = dst.general_id
|
||||
AND src.turn_idx = dst.turn_idx + %i
|
||||
SET dst.action = IF(src.id IS NULL, %s, src.action),
|
||||
dst.arg = IF(src.id IS NULL, %s, src.arg),
|
||||
dst.brief = IF(src.id IS NULL, %s, src.brief)
|
||||
WHERE dst.general_id = %i',
|
||||
$turnCnt,
|
||||
'휴식',
|
||||
'{}',
|
||||
'휴식',
|
||||
$generalID
|
||||
);
|
||||
}
|
||||
|
||||
function repeatGeneralCommand(int $generalId, int $turnCnt){
|
||||
@@ -126,15 +138,23 @@ function pushNationCommand(int $nationID, int $officerLevel, int $turnCnt=1){
|
||||
|
||||
$db = DB::db();
|
||||
|
||||
$db->update('nation_turn', [
|
||||
'turn_idx'=>$db->sqleval('turn_idx + %i', $turnCnt)
|
||||
], 'nation_id=%i AND officer_level=%i ORDER BY turn_idx DESC', $nationID, $officerLevel);
|
||||
$db->update('nation_turn', [
|
||||
'turn_idx'=>$db->sqleval('turn_idx - %i', GameConst::$maxChiefTurn),
|
||||
'action'=>'휴식',
|
||||
'arg'=>'{}',
|
||||
'brief'=>'휴식'
|
||||
], 'nation_id=%i AND officer_level=%i AND turn_idx >= %i ORDER BY turn_idx ASC', $nationID, $officerLevel, GameConst::$maxChiefTurn);
|
||||
$db->query(
|
||||
'UPDATE nation_turn AS dst
|
||||
LEFT JOIN nation_turn AS src
|
||||
ON src.nation_id = dst.nation_id
|
||||
AND src.officer_level = dst.officer_level
|
||||
AND src.turn_idx = dst.turn_idx - %i
|
||||
SET dst.action = IF(src.id IS NULL, %s, src.action),
|
||||
dst.arg = IF(src.id IS NULL, %s, src.arg),
|
||||
dst.brief = IF(src.id IS NULL, %s, src.brief)
|
||||
WHERE dst.nation_id = %i AND dst.officer_level = %i',
|
||||
$turnCnt,
|
||||
'휴식',
|
||||
'{}',
|
||||
'휴식',
|
||||
$nationID,
|
||||
$officerLevel
|
||||
);
|
||||
}
|
||||
|
||||
function pullNationCommand(int $nationID, int $officerLevel, int $turnCnt=1){
|
||||
@@ -157,15 +177,23 @@ function pullNationCommand(int $nationID, int $officerLevel, int $turnCnt=1){
|
||||
|
||||
$db = DB::db();
|
||||
|
||||
$db->update('nation_turn', [
|
||||
'turn_idx'=>$db->sqleval('turn_idx + %i', GameConst::$maxChiefTurn),
|
||||
'action'=>'휴식',
|
||||
'arg'=>'{}',
|
||||
'brief'=>'휴식',
|
||||
], 'nation_id=%i AND officer_level=%i AND turn_idx < %i', $nationID, $officerLevel, $turnCnt);
|
||||
$db->update('nation_turn', [
|
||||
'turn_idx'=>$db->sqleval('turn_idx - %i', $turnCnt)
|
||||
], 'nation_id=%i AND officer_level=%i ORDER BY turn_idx ASC', $nationID, $officerLevel);
|
||||
$db->query(
|
||||
'UPDATE nation_turn AS dst
|
||||
LEFT JOIN nation_turn AS src
|
||||
ON src.nation_id = dst.nation_id
|
||||
AND src.officer_level = dst.officer_level
|
||||
AND src.turn_idx = dst.turn_idx + %i
|
||||
SET dst.action = IF(src.id IS NULL, %s, src.action),
|
||||
dst.arg = IF(src.id IS NULL, %s, src.arg),
|
||||
dst.brief = IF(src.id IS NULL, %s, src.brief)
|
||||
WHERE dst.nation_id = %i AND dst.officer_level = %i',
|
||||
$turnCnt,
|
||||
'휴식',
|
||||
'{}',
|
||||
'휴식',
|
||||
$nationID,
|
||||
$officerLevel
|
||||
);
|
||||
}
|
||||
|
||||
function repeatNationCommand(int $nationID, int $officerLevel, int $turnCnt){
|
||||
@@ -494,4 +522,4 @@ function setNationCommand(int $generalID, array $turnList, string $command, ?arr
|
||||
'arg_test'=>true,
|
||||
'reason'=>'success'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+14
-6
@@ -651,16 +651,24 @@ function checkStatistic()
|
||||
}
|
||||
|
||||
|
||||
function convForOldGeneral(array $general, int $year, int $month)
|
||||
{
|
||||
$general['history'] = getGeneralHistoryLogAll($general['no']);
|
||||
return [
|
||||
function convForOldGeneral(array $general, int $year, int $month)
|
||||
{
|
||||
$general['history'] = getGeneralHistoryLogAll($general['no']);
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env'));
|
||||
$turnTimeDisplay = $clock->formatTick(Util::toInt($general['turntime']), true);
|
||||
$general['turntime_tick'] = Util::toInt($general['turntime']);
|
||||
$general['turntime'] = $turnTimeDisplay;
|
||||
if ($general['recent_war'] !== null) {
|
||||
$general['recent_war_tick'] = Util::toInt($general['recent_war']);
|
||||
$general['recent_war'] = $clock->formatTick(Util::toInt($general['recent_war']), true);
|
||||
}
|
||||
return [
|
||||
'server_id' => UniqueConst::$serverID,
|
||||
'general_no' => $general['no'],
|
||||
'owner' => $general['owner'],
|
||||
'name' => $general['name'],
|
||||
'last_yearmonth' => $year * 100 + $month,
|
||||
'turntime' => $general['turntime'],
|
||||
'turntime' => $turnTimeDisplay,
|
||||
'data' => Json::encode($general)
|
||||
];
|
||||
}
|
||||
@@ -734,7 +742,7 @@ function checkEmperior()
|
||||
|
||||
/** @var int[] */
|
||||
$auctionList = $db->queryFirstColumn(
|
||||
'SELECT `id` FROM `ng_auction` WHERE `type` = %s AND `finished` = 0 ORDER BY `close_date` ASC',
|
||||
'SELECT `id` FROM `ng_auction` WHERE `type` = %s AND `finished` = 0 ORDER BY `close_tick` ASC',
|
||||
AuctionType::UniqueItem->value
|
||||
);
|
||||
foreach($auctionList as $auctionID){
|
||||
|
||||
@@ -4,6 +4,10 @@ namespace sammo;
|
||||
|
||||
function printLimitMsg($turntime)
|
||||
{
|
||||
if (is_int($turntime) || (is_string($turntime) && ctype_digit(ltrim($turntime, '-')))) {
|
||||
$gameStor = KVStorage::getStorage(DB::db(), 'game_env');
|
||||
$turntime = GameClock::fromStorage($gameStor)->formatTick(Util::toInt($turntime), true);
|
||||
}
|
||||
//FIXME: template로 이동.
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
|
||||
@@ -61,8 +61,12 @@ function chiefTurnTable()
|
||||
";
|
||||
}
|
||||
|
||||
function templateLimitMsg(string $turntime): string
|
||||
{
|
||||
function templateLimitMsg(string $turntime): string
|
||||
{
|
||||
if (ctype_digit(ltrim($turntime, '-'))) {
|
||||
$gameStor = KVStorage::getStorage(DB::db(), 'game_env');
|
||||
$turntime = GameClock::fromStorage($gameStor)->formatTick(Util::toInt($turntime), true);
|
||||
}
|
||||
return "이미 너무 많은 접속을 하셨습니다. 다음 턴에 다시 시도해주세요. (턴시간: {$turntime})";
|
||||
}
|
||||
|
||||
|
||||
+11
-8
@@ -17,10 +17,10 @@ function processTournament()
|
||||
{
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
|
||||
$admin = $gameStor->getValues(['tournament', 'phase', 'tnmt_type', 'tnmt_auto', 'tnmt_time', 'turnterm', 'last_tournament_betting_id']);
|
||||
$now = new \DateTime();
|
||||
$offset = $now->getTimestamp() - (new \DateTime($admin['tnmt_time']))->getTimestamp();
|
||||
$offset = intdiv($clock->nowTick() - Util::toInt($admin['tnmt_time']), $clock->ticksPerSecond());
|
||||
|
||||
//수동일땐 무시
|
||||
if (!$admin['tnmt_auto']) {
|
||||
@@ -122,10 +122,10 @@ function processTournament()
|
||||
if ($tnmt == 6) {
|
||||
$betTerm = Util::valueFit($unit * 60, null, 3600);
|
||||
//처리 초 더한 날짜
|
||||
$dt = date("Y-m-d H:i:s", strtotime($admin['tnmt_time']) + $unit * $i + $betTerm);
|
||||
$gameStor->tournament = $tnmt;
|
||||
$gameStor->phase = $phase;
|
||||
$gameStor->tnmt_time = $dt;
|
||||
$gameStor->tnmt_time = Util::toInt($admin['tnmt_time'])
|
||||
+ $clock->ticksFromSeconds($unit * $i + $betTerm);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,7 @@ function processTournament()
|
||||
$second = $unit * $iter;
|
||||
$gameStor->tournament = $tnmt;
|
||||
$gameStor->phase = $phase;
|
||||
$gameStor->tnmt_time = (new \DateTimeImmutable($admin['tnmt_time']))->add(new \DateInterval("PT{$second}S"))->format('Y-m-d H:i:s');
|
||||
$gameStor->tnmt_time = Util::toInt($admin['tnmt_time']) + $clock->ticksFromSeconds($second);
|
||||
}
|
||||
|
||||
function getTournamentTermText(int $turnTerm)
|
||||
@@ -160,7 +160,8 @@ function getTournamentTime()
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
|
||||
list($tnmt, $tnmt_time) = $gameStor->getValuesAsArray(['tournament', 'tnmt_time']);
|
||||
$dt = substr($tnmt_time, 11, 5);
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$dt = substr($clock->formatTick(Util::toInt($tnmt_time)), 11, 5);
|
||||
switch ($tnmt) {
|
||||
case 1:
|
||||
$tnmt = "개막시간 {$dt}";
|
||||
@@ -284,9 +285,11 @@ function startTournament($type)
|
||||
$admin = $gameStor->getValues(['year', 'month', 'turnterm']);
|
||||
$turnTerm = $admin['turnterm'];
|
||||
$unit = calcTournamentTerm($turnTerm);
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
|
||||
$gameStor->tnmt_auto = true;
|
||||
$gameStor->tnmt_time = (new \DateTimeImmutable())->add(new \DateInterval("PT{$unit}M"))->format('Y-m-d H:i:s');
|
||||
// 기존 startTournament은 unit을 분으로 더하므로 그 계약을 유지합니다.
|
||||
$gameStor->tnmt_time = $clock->nowTick() + $clock->ticksFromMinutes($unit);
|
||||
$gameStor->tournament = 1;
|
||||
$gameStor->tnmt_type = $type;
|
||||
$gameStor->last_tournament_betting_id = 0;
|
||||
@@ -1171,7 +1174,7 @@ function fight($tnmt_type, $tnmt, $phs, $group, $g1, $g2, $type)
|
||||
}
|
||||
$damage1 *= $factor1;
|
||||
$damage2 *= $factor2;
|
||||
|
||||
|
||||
//1합 승부
|
||||
if ($phase == 1) {
|
||||
|
||||
|
||||
@@ -16,12 +16,10 @@ $session = Session::requireLogin([])->setReadOnly();
|
||||
$userID = Session::getUserID();
|
||||
|
||||
|
||||
$oNow = new \DateTimeImmutable();
|
||||
|
||||
$now = $oNow->format('Y-m-d H:i:s');
|
||||
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$now = $clock->nowTick();
|
||||
|
||||
$oldGeneral = $db->queryFirstField('SELECT `no` FROM general WHERE `owner`=%i', $userID);
|
||||
if($oldGeneral !== null){
|
||||
@@ -44,15 +42,14 @@ if($npcmode!=1){
|
||||
]);
|
||||
}
|
||||
|
||||
$token = $db->queryFirstRow('SELECT * FROM select_npc_token WHERE `owner`=%i AND `valid_until`>=%s', $userID, $now);
|
||||
$token = $db->queryFirstRow('SELECT * FROM select_npc_token WHERE `owner`=%i AND `valid_until`>=%i', $userID, $now);
|
||||
$pickResult = [];
|
||||
|
||||
|
||||
if($token && $refresh){
|
||||
$pickMoreFrom = (new \DateTime($token['pick_more_from']))->getTimestamp();
|
||||
$nowT = $oNow->getTimestamp();
|
||||
$pickMoreFrom = Util::toInt($token['pick_more_from']);
|
||||
|
||||
if($nowT >= $pickMoreFrom){
|
||||
if($now >= $pickMoreFrom){
|
||||
$oldPickResult = Json::decode($token['pick_result']);
|
||||
|
||||
foreach($keepResult as $keepId){
|
||||
@@ -75,15 +72,14 @@ if($token && $refresh){
|
||||
}
|
||||
|
||||
if($token && !$refresh){
|
||||
$pickMoreFrom = (new \DateTime($token['pick_more_from']))->getTimestamp();
|
||||
$nowT = $oNow->getTimestamp();
|
||||
$pickMoreFrom = Util::toInt($token['pick_more_from']);
|
||||
|
||||
Json::die([
|
||||
'result'=>true,
|
||||
'pick'=>Json::decode($token['pick_result']),
|
||||
'pickMoreFrom'=>$token['pick_more_from'],
|
||||
'pickMoreSeconds'=>$pickMoreFrom-$nowT,
|
||||
'validUntil'=>$token['valid_until']
|
||||
'pickMoreFrom'=>$clock->formatTick($pickMoreFrom),
|
||||
'pickMoreSeconds'=>intdiv($pickMoreFrom - $now, $clock->ticksPerSecond()),
|
||||
'validUntil'=>$clock->formatTick(Util::toInt($token['valid_until']))
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -100,7 +96,7 @@ foreach($db->query('SELECT `no`, `name`, leadership, strength, intel, nation, im
|
||||
$weight[$general['no']] = pow($allStat, 1.5);
|
||||
}
|
||||
|
||||
foreach($db->queryFirstColumn('SELECT pick_result FROM select_npc_token WHERE `owner`!=%i AND valid_until >=%s', $userID, $now) as $reserved){
|
||||
foreach($db->queryFirstColumn('SELECT pick_result FROM select_npc_token WHERE `owner`!=%i AND valid_until >=%i', $userID, $now) as $reserved){
|
||||
$reserved = Json::decode($reserved);
|
||||
foreach(array_keys($reserved) as $reservedNPC){
|
||||
if(key_exists($reservedNPC, $weight)){
|
||||
@@ -131,8 +127,8 @@ $newNonce = random_int(0, 0xfffffff);
|
||||
$validSecond = max(VALID_SECOND, $turnterm*40);
|
||||
$pickMoreSecond = max(PICK_MORE_SECOND, Util::round(pow($turnterm, 0.672)*8));
|
||||
|
||||
$validUntil = $oNow->add(new \DateInterval(sprintf('PT%dS', $validSecond)));
|
||||
$pickMoreFrom = $oNow->add(new \DateInterval(sprintf('PT%dS', $pickMoreSecond)));
|
||||
$validUntil = $now + $clock->ticksFromSeconds($validSecond);
|
||||
$pickMoreFrom = $now + $clock->ticksFromSeconds($pickMoreSecond);
|
||||
|
||||
$db->delete('select_npc_token', 'valid_until < %s', $now);
|
||||
|
||||
@@ -140,8 +136,8 @@ $inserted = 0;
|
||||
|
||||
if($token){
|
||||
$db->update('select_npc_token', [
|
||||
'valid_until'=>$validUntil->format('Y-m-d H:i:s'),
|
||||
'pick_more_from'=>$pickMoreFrom->format('Y-m-d H:i:s'),
|
||||
'valid_until'=>$validUntil,
|
||||
'pick_more_from'=>$pickMoreFrom,
|
||||
'pick_result'=>Json::encode($pickResult),
|
||||
'nonce'=>$newNonce
|
||||
], 'owner = %i AND nonce = %i', $userID, $token['nonce']);
|
||||
@@ -152,8 +148,8 @@ if($token){
|
||||
else{
|
||||
$db->insertIgnore('select_npc_token', [
|
||||
'owner'=>$userID,
|
||||
'valid_until'=>$validUntil->format('Y-m-d H:i:s'),
|
||||
'pick_more_from'=>'2000-01-01 01:00:00',
|
||||
'valid_until'=>$validUntil,
|
||||
'pick_more_from'=>$now,
|
||||
'pick_result'=>Json::encode($pickResult),
|
||||
'nonce'=>$newNonce
|
||||
]);
|
||||
@@ -173,7 +169,7 @@ if($inserted === 0){
|
||||
Json::die([
|
||||
'result'=>true,
|
||||
'pick'=>$pickResult,
|
||||
'pickMoreFrom'=>($inserted===-1)?$pickMoreFrom->format('Y-m-d H:i:s'):'2000-01-01 01:00:00',
|
||||
'pickMoreFrom'=>$clock->formatTick(($inserted===-1)?$pickMoreFrom:$now),
|
||||
'pickMoreSeconds'=>($inserted===-1)?$pickMoreSecond:0,
|
||||
'validUntil'=>$validUntil->format('Y-m-d H:i:s')
|
||||
]);
|
||||
'validUntil'=>$clock->formatTick($validUntil)
|
||||
]);
|
||||
|
||||
+30
-14
@@ -10,7 +10,23 @@ function sortTokens(&$tokens){
|
||||
});
|
||||
}
|
||||
|
||||
function putInfoText(&$info){
|
||||
function putInfoText(&$info, ?array $currentTargetEnv){
|
||||
if (($info['event100Growth'] ?? false) === true) {
|
||||
if ($currentTargetEnv === null) {
|
||||
$displayStats = CentennialAllStarGrowthService::calculateUserInitialStats($info);
|
||||
$info['selectionStatLabel'] = '시작 능력치';
|
||||
} else {
|
||||
$displayStats = CentennialAllStarGrowthService::calculateUserCurrentTargetStats(
|
||||
$info,
|
||||
$currentTargetEnv
|
||||
);
|
||||
$info['selectionStatLabel'] = '현재 변경 기준 능력치';
|
||||
}
|
||||
$info['selectionLeadership'] = $displayStats['leadership'];
|
||||
$info['selectionStrength'] = $displayStats['strength'];
|
||||
$info['selectionIntel'] = $displayStats['intel'];
|
||||
}
|
||||
|
||||
if(key_exists('specialDomestic', $info)){
|
||||
$class = buildGeneralSpecialDomesticClass($info['specialDomestic']);
|
||||
$info['specialDomesticName'] = $class->getName();
|
||||
@@ -27,14 +43,13 @@ function putInfoText(&$info){
|
||||
$session = Session::requireLogin([])->setReadOnly();
|
||||
$userID = Session::getUserID();
|
||||
|
||||
$oNow = new \DateTimeImmutable();
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$now = $clock->nowTick();
|
||||
|
||||
$now = $oNow->format('Y-m-d H:i:s');
|
||||
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
|
||||
$npcmode = $gameStor->getValue('npcmode');
|
||||
$eventEnv = $gameStor->getValues(['npcmode', 'startyear', 'year', 'month']);
|
||||
$npcmode = $eventEnv['npcmode'];
|
||||
if($npcmode!=2){
|
||||
Json::die([
|
||||
'result'=>false,
|
||||
@@ -42,7 +57,8 @@ if($npcmode!=2){
|
||||
]);
|
||||
}
|
||||
|
||||
$rawGeneral = $db->queryFirstRow('SELECT no, aux FROM general WHERE `owner` = %i', $userID);
|
||||
$rawGeneral = $db->queryFirstRow('SELECT no, aux FROM general WHERE `owner` = %i', $userID);
|
||||
$currentTargetEnv = $rawGeneral ? $eventEnv : null;
|
||||
if($rawGeneral){
|
||||
$generalAux = Json::decode($rawGeneral['aux']);
|
||||
if(key_exists('next_change', $generalAux)&& $generalAux['next_change'] > $now){
|
||||
@@ -62,7 +78,7 @@ if($tokens){
|
||||
foreach($tokens as $token){
|
||||
$valid_until = $token['reserved_until'];
|
||||
$info = Json::decode($token['info']);
|
||||
putInfoText($info);
|
||||
putInfoText($info, $currentTargetEnv);
|
||||
$info['uniqueName'] = $token['unique_name'];
|
||||
$pick[] = $info;
|
||||
}
|
||||
@@ -70,7 +86,7 @@ if($tokens){
|
||||
Json::die([
|
||||
'result'=>true,
|
||||
'pick'=>$pick,
|
||||
'validUntil'=>$valid_until
|
||||
'validUntil'=>$clock->formatTick(Util::toInt($valid_until))
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -83,12 +99,12 @@ $valid_until = null;
|
||||
foreach(pickGeneralFromPool($db, $rng, $userID, 14) as $pickObj){
|
||||
$valid_until = $pickObj->getValidUntil();
|
||||
$info = $pickObj->getInfo();
|
||||
putInfoText($info);
|
||||
putInfoText($info, $currentTargetEnv);
|
||||
$pick[] = $info;
|
||||
}
|
||||
sortTokens($pick);//좀 무식하지만..
|
||||
Json::die([
|
||||
'result'=>true,
|
||||
'pick'=>$pick,
|
||||
'validUntil'=>$valid_until
|
||||
]);
|
||||
'validUntil'=>$valid_until === null ? null : $clock->formatTick(Util::toInt($valid_until))
|
||||
]);
|
||||
|
||||
+2
-2
@@ -211,7 +211,7 @@ try{
|
||||
$show_img_level,
|
||||
!!$tournament_trig,
|
||||
$join_mode,
|
||||
TimeUtil::now(),
|
||||
TimeUtil::format(GameClock::readWallTime(), false),
|
||||
$autorun_user
|
||||
));
|
||||
}
|
||||
@@ -220,4 +220,4 @@ catch(\Exception $e){
|
||||
'result'=>false,
|
||||
'reason'=>$e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ $db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$rootDB = RootDB::db();
|
||||
|
||||
$oNow = new \DateTimeImmutable();
|
||||
$now = $oNow->format('Y-m-d H:i:s');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$now = $clock->nowTick();
|
||||
|
||||
$member = $rootDB->queryFirstRow('SELECT `name`, `penalty` FROM member WHERE `NO`=%i', $userID);
|
||||
if(!$member){
|
||||
@@ -139,4 +139,4 @@ $rootDB->insert('member_log', [
|
||||
Json::die([
|
||||
'result'=>true,
|
||||
'reason'=>'success'
|
||||
]);
|
||||
]);
|
||||
|
||||
@@ -6,21 +6,22 @@ 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);
|
||||
$pictureSource = Util::getPost('picture_source', 'string', 'selected');
|
||||
|
||||
|
||||
if(!$pick){
|
||||
@@ -36,8 +37,8 @@ $db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
|
||||
$rootDB = RootDB::db();
|
||||
$oNow = new \DateTimeImmutable();
|
||||
$now = $oNow->format('Y-m-d H:i:s');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$now = $clock->nowTick();
|
||||
|
||||
$hasGeneralID = $db->queryFirstField('SELECT no FROM general WHERE owner = %i', $userID);
|
||||
if($hasGeneralID){
|
||||
@@ -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([
|
||||
@@ -87,24 +111,27 @@ 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();
|
||||
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,17 +144,23 @@ 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'){
|
||||
$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'=>'올바르지 않은 성격입니다.'
|
||||
@@ -135,7 +168,7 @@ foreach(GameConst::$generalPoolAllowOption as $allowOption){
|
||||
}
|
||||
$builder->setEgo($personal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$userNick = $ownerInfo['name'];
|
||||
|
||||
@@ -143,8 +176,29 @@ $builder->setOwner($userID);
|
||||
$builder->setOwnerName($userNick);
|
||||
$builder->setKillturn(5);
|
||||
$builder->setNPCType(0);
|
||||
$builder->setAuxVar('next_change', TimeUtil::nowAddMinutes(12 * $env['turnterm']));
|
||||
$builder->setAuxVar('next_change', $now + GameClock::TICKS_PER_TURN * 12);
|
||||
$builder->setAuxVar(
|
||||
'prestart_delete_after',
|
||||
addTurn($now, $env['turnterm'], GameConst::$minTurnDieOnPrestart)
|
||||
);
|
||||
$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 +243,4 @@ $rootDB->insert('member_log', [
|
||||
Json::die([
|
||||
'result'=>true,
|
||||
'reason'=>'success'
|
||||
]);
|
||||
]);
|
||||
|
||||
@@ -19,6 +19,7 @@ if(!class_exists('\\sammo\\DB')){
|
||||
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
|
||||
if(file_exists(__DIR__.'/.htaccess')){
|
||||
$reserved = $db->queryFirstRow(
|
||||
@@ -75,8 +76,8 @@ $admin['maxUserCnt'] = $admin['maxgeneral'];
|
||||
$admin['npcMode'] = $admin['npcmode'];
|
||||
$admin['turnTerm'] = $admin['turnterm'];
|
||||
$admin['isUnited'] = $admin['isunited'];
|
||||
$admin['starttime'] = substr($admin['opentime'], 5, 11);
|
||||
$admin['turntime'] = substr($admin['turntime'], 5, 11);
|
||||
$admin['starttime'] = substr($clock->formatTick(Util::toInt($admin['opentime'])), 5, 11);
|
||||
$admin['turntime'] = substr($clock->formatTick(Util::toInt($admin['turntime'])), 5, 11);
|
||||
unset($admin['npcmode']);
|
||||
unset($admin['maxgeneral']);
|
||||
unset($admin['turnterm']);
|
||||
@@ -131,4 +132,4 @@ if($general){
|
||||
Json::die([
|
||||
'game'=>$admin,
|
||||
'me'=>$me?:null
|
||||
]);
|
||||
]);
|
||||
|
||||
@@ -77,7 +77,8 @@ $month = $query['month'];
|
||||
$repeatCnt = $query['repeatCnt'];
|
||||
|
||||
$rawAttacker = $query['attackerGeneral'];
|
||||
$rawAttacker['turntime'] = TimeUtil::now();
|
||||
$battleClock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env'));
|
||||
$rawAttacker['turntime'] = $battleClock->formatTick($battleClock->nowTick());
|
||||
$rawAttackerCity = $query['attackerCity'];
|
||||
$rawAttackerNation = $query['attackerNation'];
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ include "func.php";
|
||||
WebUtil::requireAJAX();
|
||||
|
||||
$pick = Util::getPost('pick');
|
||||
$pictureSource = Util::getPost('picture_source', 'string', 'selected');
|
||||
|
||||
if(!$pick){
|
||||
Json::die([
|
||||
@@ -21,8 +22,8 @@ $db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
|
||||
$rootDB = RootDB::db();
|
||||
$oNow = new \DateTimeImmutable();
|
||||
$now = $oNow->format('Y-m-d H:i:s');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$now = $clock->nowTick();
|
||||
|
||||
$generalID = $db->queryFirstField('SELECT no FROM general WHERE owner = %i', $userID);
|
||||
if(!$generalID){
|
||||
@@ -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,44 +136,52 @@ $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']);
|
||||
}
|
||||
if(key_exists('generalName', $info)){
|
||||
$generalObj->updateVar('name', $info['generalName']);
|
||||
}
|
||||
$generalObj->setAuxVar('next_change', TimeUtil::nowAddMinutes(12 * $turnterm));
|
||||
$generalObj->setAuxVar('next_change', $now + GameClock::TICKS_PER_TURN * 12);
|
||||
|
||||
$userNick = $ownerInfo['name'];
|
||||
$generalObj->setVar('owner_name', $userNick);
|
||||
@@ -158,4 +200,4 @@ $generalObj->applyDB($db);
|
||||
Json::die([
|
||||
'result'=>true,
|
||||
'reason'=>'success'
|
||||
]);
|
||||
]);
|
||||
|
||||
+4
-2
@@ -11,9 +11,11 @@ $db = DB::db();
|
||||
$updated = false;
|
||||
$locked = false;
|
||||
$lastExecuted = TurnExecutionHelper::executeAllCommand($updated, $locked);
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
|
||||
Json::die([
|
||||
'result' => true,
|
||||
'updated' => $updated,
|
||||
'locked' => $locked,
|
||||
'lastExecuted' => $lastExecuted,
|
||||
]);
|
||||
'lastExecutedTick' => $lastExecuted,
|
||||
'lastExecuted' => $clock->formatTick($lastExecuted, true),
|
||||
]);
|
||||
|
||||
@@ -21,7 +21,7 @@ class BidUniqueAuction extends \sammo\BaseAPI
|
||||
])
|
||||
->rule('int', 'amount')
|
||||
->rule('int', 'auctionID')
|
||||
->rule('boolean', 'extendCloseDate');
|
||||
->rule('boolean', 'extendCloseTick');
|
||||
|
||||
if (!$v->validate()) {
|
||||
return $v->errorStr();
|
||||
@@ -38,7 +38,7 @@ class BidUniqueAuction extends \sammo\BaseAPI
|
||||
{
|
||||
$auctionID = $this->args['auctionID'];
|
||||
$amount = $this->args['amount'];
|
||||
$tryExtendCloseDate = $this->args['extendCloseDate'] ?? false;
|
||||
$tryExtendCloseDate = $this->args['extendCloseTick'] ?? false;
|
||||
|
||||
$generalID = $session->generalID;
|
||||
$general = General::createObjFromDB($generalID);
|
||||
|
||||
@@ -15,6 +15,8 @@ use sammo\General;
|
||||
use sammo\Json;
|
||||
use sammo\TimeUtil;
|
||||
use sammo\Util;
|
||||
use sammo\GameClock;
|
||||
use sammo\KVStorage;
|
||||
|
||||
use function sammo\getAuctionLogRecent;
|
||||
|
||||
@@ -33,12 +35,13 @@ class GetActiveResourceAuctionList extends \sammo\BaseAPI
|
||||
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null | string | array | APIRecoveryType
|
||||
{
|
||||
$db = DB::db();
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
|
||||
|
||||
$buyRiceList = [];
|
||||
$sellRiceList = [];
|
||||
/** @var AuctionInfo[] */
|
||||
$auctions = array_map(fn ($raw) => AuctionInfo::fromArray($raw), $db->query(
|
||||
'SELECT * FROM `ng_auction` WHERE `type` IN %ls AND `finished` = 0 ORDER BY `close_date` ASC',
|
||||
'SELECT * FROM `ng_auction` WHERE `type` IN %ls AND `finished` = 0 ORDER BY `close_tick` ASC',
|
||||
[
|
||||
AuctionType::BuyRice->value,
|
||||
AuctionType::SellRice->value,
|
||||
@@ -87,8 +90,8 @@ class GetActiveResourceAuctionList extends \sammo\BaseAPI
|
||||
'type' => $auction->type->value,
|
||||
'hostGeneralID' => $auction->hostGeneralID,
|
||||
'hostName' => $auction->detail->hostName,
|
||||
'openDate' => TimeUtil::format($auction->openDate, false),
|
||||
'closeDate' => TimeUtil::format($auction->closeDate, false),
|
||||
'openDate' => $clock->formatTick($auction->openTick),
|
||||
'closeDate' => $clock->formatTick($auction->closeTick),
|
||||
'amount' => $auction->detail->amount,
|
||||
'startBidAmount' => $auction->detail->startBidAmount,
|
||||
'finishBidAmount' => $auction->detail->finishBidAmount,
|
||||
|
||||
@@ -14,6 +14,8 @@ use sammo\Enums\GeneralQueryMode;
|
||||
use sammo\Enums\InheritanceKey;
|
||||
use sammo\InheritancePointManager;
|
||||
use sammo\TimeUtil;
|
||||
use sammo\GameClock;
|
||||
use sammo\KVStorage;
|
||||
use sammo\Validator;
|
||||
use sammo\General;
|
||||
|
||||
@@ -42,6 +44,7 @@ class GetUniqueItemAuctionDetail extends \sammo\BaseAPI
|
||||
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null | string | array | APIRecoveryType
|
||||
{
|
||||
$db = DB::db();
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
|
||||
|
||||
$generalID = $session->generalID;
|
||||
$auctionID = $this->args['auctionID'];
|
||||
@@ -92,9 +95,11 @@ class GetUniqueItemAuctionDetail extends \sammo\BaseAPI
|
||||
'target' => $auction->target,
|
||||
'isCallerHost' => $auction->hostGeneralID === $generalID,
|
||||
'hostName' => $auction->detail->hostName,
|
||||
'closeDate' => TimeUtil::format($auction->closeDate, false),
|
||||
'closeDate' => $clock->formatTick($auction->closeTick),
|
||||
'remainCloseDateExtensionCnt' => $auction->detail->remainCloseDateExtensionCnt,
|
||||
'availableLatestBidCloseDate' => TimeUtil::format($auction->detail->availableLatestBidCloseDate, false),
|
||||
'availableLatestBidCloseDate' => $auction->detail->availableLatestBidCloseTick === null
|
||||
? null
|
||||
: $clock->formatTick($auction->detail->availableLatestBidCloseTick),
|
||||
],
|
||||
'bidList' => $responseBid,
|
||||
'obfuscatedName' => $obfuscatedName,
|
||||
|
||||
@@ -12,6 +12,8 @@ use sammo\Enums\APIRecoveryType;
|
||||
use sammo\Enums\AuctionType;
|
||||
use sammo\TimeUtil;
|
||||
use sammo\Util;
|
||||
use sammo\GameClock;
|
||||
use sammo\KVStorage;
|
||||
|
||||
class GetUniqueItemAuctionList extends \sammo\BaseAPI
|
||||
{
|
||||
@@ -28,12 +30,13 @@ class GetUniqueItemAuctionList extends \sammo\BaseAPI
|
||||
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag): null | string | array | APIRecoveryType
|
||||
{
|
||||
$db = DB::db();
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
|
||||
|
||||
$generalID = $session->generalID;
|
||||
|
||||
/** @var AuctionInfo[] */
|
||||
$auctions = array_map(fn($raw)=>AuctionInfo::fromArray($raw), $db->query(
|
||||
'SELECT * FROM `ng_auction` WHERE `type` = %s ORDER BY `close_date` ASC',
|
||||
'SELECT * FROM `ng_auction` WHERE `type` = %s ORDER BY `close_tick` ASC',
|
||||
AuctionType::UniqueItem->value
|
||||
) ?? []);
|
||||
|
||||
@@ -85,9 +88,11 @@ class GetUniqueItemAuctionList extends \sammo\BaseAPI
|
||||
'target' => $auction->target,
|
||||
'isCallerHost' => $auction->hostGeneralID === $generalID,
|
||||
'hostName' => $auction->detail->hostName,
|
||||
'closeDate' => TimeUtil::format($auction->closeDate, false),
|
||||
'closeDate' => $clock->formatTick($auction->closeTick),
|
||||
'remainCloseDateExtensionCnt' => $auction->detail->remainCloseDateExtensionCnt,
|
||||
'availableLatestBidCloseDate' => TimeUtil::format($auction->detail->availableLatestBidCloseDate, false),
|
||||
'availableLatestBidCloseDate' => $auction->detail->availableLatestBidCloseTick === null
|
||||
? null
|
||||
: $clock->formatTick($auction->detail->availableLatestBidCloseTick),
|
||||
'highestBid' => [
|
||||
'generalName' => $highestBid->aux->generalName,
|
||||
'amount' => $highestBid->amount,
|
||||
|
||||
@@ -7,9 +7,11 @@ use DateTimeInterface;
|
||||
use sammo\DB;
|
||||
use sammo\Enums\APIRecoveryType;
|
||||
use sammo\GameConst;
|
||||
use sammo\GameClock;
|
||||
use sammo\Json;
|
||||
use sammo\KVStorage;
|
||||
use sammo\TimeUtil;
|
||||
use sammo\Util;
|
||||
|
||||
use function sammo\cutTurn;
|
||||
|
||||
@@ -82,11 +84,12 @@ class GetReservedCommand extends \sammo\BaseAPI
|
||||
|
||||
return [
|
||||
'result' => true,
|
||||
'turnTime' => $turnTime,
|
||||
'turnTimeTick' => Util::toInt($turnTime),
|
||||
'turnTime' => GameClock::fromStorage($gameStor)->formatTick(Util::toInt($turnTime)),
|
||||
'turnTerm' => $turnTerm,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'date' => TimeUtil::now(true),
|
||||
'date' => GameClock::fromStorage($gameStor)->formatTick(GameClock::fromStorage($gameStor)->nowTick(), true),
|
||||
'turn' => $commandList,
|
||||
'autorun_limit' => $generalAux['autorun_limit'] ?? null,
|
||||
];
|
||||
|
||||
@@ -11,7 +11,8 @@ use sammo\Session;
|
||||
use sammo\General;
|
||||
use sammo\JosaUtil;
|
||||
use sammo\KVStorage;
|
||||
use sammo\TimeUtil;
|
||||
use sammo\GameClock;
|
||||
use sammo\Util;
|
||||
|
||||
use function sammo\addTurn;
|
||||
use function sammo\increaseRefresh;
|
||||
@@ -37,8 +38,14 @@ class DieOnPrestart extends \sammo\BaseAPI
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$gameStor->cacheValues(['turnterm', 'opentime', 'turntime', 'year', 'month']);
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$nowTick = $clock->nowTick();
|
||||
|
||||
$general = $db->queryFirstRow('SELECT no,name,nation,owner_name,npc FROM general WHERE owner=%i AND npc = 0', $userID);
|
||||
if (!$general) {
|
||||
return '장수가 없습니다';
|
||||
}
|
||||
|
||||
$lastRefresh = $db->queryFirstField(
|
||||
'SELECT %b FROM general_access_log WHERE %b = %i',
|
||||
GeneralAccessLogColumn::lastRefresh->value,
|
||||
@@ -46,8 +53,9 @@ class DieOnPrestart extends \sammo\BaseAPI
|
||||
$general['no']
|
||||
);
|
||||
|
||||
if (!$general) {
|
||||
return '장수가 없습니다';
|
||||
$generalObj = General::createObjFromDB($general['no']);
|
||||
if ($generalObj instanceof DummyGeneral) {
|
||||
trigger_error("올바르지 않은 삭제 프로세스 $userID", E_USER_WARNING);
|
||||
}
|
||||
|
||||
increaseRefresh("장수 삭제", 1);
|
||||
@@ -61,16 +69,21 @@ class DieOnPrestart extends \sammo\BaseAPI
|
||||
return '이미 국가에 소속되어있습니다.';
|
||||
}
|
||||
|
||||
//서버 가오픈시 할 수 있는 행동
|
||||
$targetTime = addTurn($lastRefresh, $gameStor->turnterm, GameConst::$minTurnDieOnPrestart);
|
||||
if ($targetTime > TimeUtil::now()) {
|
||||
$targetTimeShort = substr($targetTime, 0, 19);
|
||||
return "아직 삭제할 수 없습니다. {$targetTimeShort} 부터 가능합니다.";
|
||||
$targetTime = $generalObj->getAuxVar('prestart_delete_after');
|
||||
if (!is_int($targetTime)) {
|
||||
$targetTime = addTurn(
|
||||
$lastRefresh === null ? $nowTick : Util::toInt($lastRefresh),
|
||||
$gameStor->turnterm,
|
||||
GameConst::$minTurnDieOnPrestart
|
||||
);
|
||||
$generalObj->setAuxVar('prestart_delete_after', $targetTime);
|
||||
$generalObj->applyDB($db);
|
||||
}
|
||||
|
||||
$generalObj = General::createObjFromDB($general['no']);
|
||||
if ($generalObj instanceof DummyGeneral) {
|
||||
trigger_error("올바르지 않은 삭제 프로세스 $userID", E_USER_WARNING);
|
||||
//서버 가오픈시 할 수 있는 행동
|
||||
if ($targetTime > $nowTick) {
|
||||
$targetTimeShort = $clock->formatTick($targetTime);
|
||||
return "아직 삭제할 수 없습니다. {$targetTimeShort} 부터 가능합니다.";
|
||||
}
|
||||
|
||||
$generalName = $generalObj->getName();
|
||||
|
||||
@@ -14,6 +14,7 @@ use sammo\Enums\GeneralColumn;
|
||||
use sammo\Enums\GeneralQueryMode;
|
||||
use sammo\Enums\RankColumn;
|
||||
use sammo\GameConst;
|
||||
use sammo\GameClock;
|
||||
use sammo\General;
|
||||
use sammo\KVStorage;
|
||||
use sammo\LastTurn;
|
||||
@@ -105,6 +106,7 @@ class GetFrontInfo extends \sammo\BaseAPI
|
||||
$db = DB::db();
|
||||
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$gameStor->cacheValues(['isunited', 'opentime', 'refresh']);
|
||||
|
||||
$lastHistoryID = $this->args['lastWorldHistoryID'];
|
||||
@@ -158,6 +160,7 @@ class GetFrontInfo extends \sammo\BaseAPI
|
||||
private function generateGlobalInfo(MeekroDB $db): array
|
||||
{
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
|
||||
[
|
||||
$scenarioText, $extendedGeneral, $isFiction, $npcMode,
|
||||
@@ -210,7 +213,8 @@ class GetFrontInfo extends \sammo\BaseAPI
|
||||
'month' => $month,
|
||||
'autorunUser' => $autorunUser,
|
||||
'turnterm' => $turnterm,
|
||||
'lastExecuted' => $lastExecuted,
|
||||
'lastExecutedTick' => $lastExecuted,
|
||||
'lastExecuted' => $clock->formatTick(Util::toInt($lastExecuted), true),
|
||||
'lastVoteID' => $lastVoteID,
|
||||
'develCost' => $develCost,
|
||||
'noticeMsg' => $noticeMsg,
|
||||
@@ -224,7 +228,8 @@ class GetFrontInfo extends \sammo\BaseAPI
|
||||
'isLocked' => $isLocked,
|
||||
'tournamentType' => $tournamentType,
|
||||
'tournamentState' => $tournamentState,
|
||||
'tournamentTime' => $tournamentTime,
|
||||
'tournamentTimeTick' => $tournamentTime,
|
||||
'tournamentTime' => $tournamentTime === null ? null : $clock->formatTick(Util::toInt($tournamentTime)),
|
||||
'genCount' => $globalGenCount,
|
||||
'generalCntLimit' => $generalCntLimit,
|
||||
'serverCnt' => $serverCnt,
|
||||
@@ -361,6 +366,7 @@ class GetFrontInfo extends \sammo\BaseAPI
|
||||
|
||||
public function generateGeneralInfo(MeekroDB $db, General $general, array $rawNation): array
|
||||
{
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
|
||||
|
||||
$permission = checkSecretPermission($general->getRaw());
|
||||
|
||||
@@ -425,8 +431,12 @@ class GetFrontInfo extends \sammo\BaseAPI
|
||||
'crew' => $general->getVar(GeneralColumn::crew), // number;
|
||||
'train' => $general->getVar(GeneralColumn::train), // number;
|
||||
'atmos' => $general->getVar(GeneralColumn::atmos), // number;
|
||||
'turntime' => $general->getVar(GeneralColumn::turntime), // string;
|
||||
'recent_war' => $general->getVar(GeneralColumn::recent_war), // string;
|
||||
'turntimeTick' => $general->getTurnTick(), // number;
|
||||
'turntime' => $general->getTurnTime(), // string;
|
||||
'recent_war_tick' => $general->getVar(GeneralColumn::recent_war), // number|null;
|
||||
'recent_war' => $general->getVar(GeneralColumn::recent_war) === null
|
||||
? null
|
||||
: $clock->formatTick(Util::toInt($general->getVar(GeneralColumn::recent_war)), true), // string|null;
|
||||
'horse' => $general->getVar(GeneralColumn::horse), // GameObjClassKey;
|
||||
'weapon' => $general->getVar(GeneralColumn::weapon), // GameObjClassKey;
|
||||
'book' => $general->getVar(GeneralColumn::book), // GameObjClassKey;
|
||||
@@ -544,7 +554,7 @@ class GetFrontInfo extends \sammo\BaseAPI
|
||||
'result' => false,
|
||||
'reason' => '접속 제한중입니다.',
|
||||
'recovery' => APIRecoveryType::GameQuota,
|
||||
'recovery_arg' => $general->getVar('turntime'),
|
||||
'recovery_arg' => $general->getTurnTime(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use sammo\Enums\APIRecoveryType;
|
||||
use sammo\Enums\GeneralAccessLogColumn;
|
||||
use sammo\Enums\RankColumn;
|
||||
use sammo\GameConst;
|
||||
use sammo\GameClock;
|
||||
use sammo\GameUnitConst;
|
||||
use sammo\General;
|
||||
use sammo\InheritancePointManager;
|
||||
@@ -165,6 +166,7 @@ class Join extends \sammo\BaseAPI
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$gameStor->cacheValues(['year', 'month', 'maxgeneral', 'scenario', 'show_img_level', 'block_general_create', 'turnterm', 'turntime', 'genius', 'npcmode']);
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
########## 동일 정보 존재여부 확인. ##########
|
||||
|
||||
$block_general_create = $gameStor->getValue('block_general_create');
|
||||
@@ -222,7 +224,7 @@ class Join extends \sammo\BaseAPI
|
||||
|
||||
$userLogger = new UserLogger($userID, $admin['year'], $admin['month'], false);
|
||||
|
||||
$now = TimeUtil::now(false);
|
||||
$now = $clock->nowTick();
|
||||
$rng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
|
||||
UniqueConst::$hiddenSeed,
|
||||
'MakeGeneral',
|
||||
@@ -360,17 +362,14 @@ class Join extends \sammo\BaseAPI
|
||||
|
||||
$userLogger->push(sprintf("턴 시간 %02d:%02d 로 지정", intdiv($inheritTurntime, 60), $inheritTurntime % 60), "inheritPoint");
|
||||
|
||||
$inheritTurntime += $rng->nextRangeInt(0, 999999) / 1000000;
|
||||
|
||||
$turntime = new \DateTimeImmutable(cutTurn($admin['turntime'], $admin['turnterm']));
|
||||
$turntime = $turntime->add(TimeUtil::secondsToDateInterval($inheritTurntime));
|
||||
$turntime = TimeUtil::format($turntime, true);
|
||||
$inheritTurnMicrosecond = $rng->nextRangeInt(0, 999999);
|
||||
$turntime = cutTurn(Util::toInt($admin['turntime']), $admin['turnterm'])
|
||||
+ $clock->ticksFromSeconds($inheritTurntime)
|
||||
+ intdiv($inheritTurnMicrosecond * $clock->ticksPerSecond(), 1_000_000);
|
||||
} else {
|
||||
$turntime = getRandTurn($rng, $admin['turnterm'], new \DateTimeImmutable($admin['turntime']));
|
||||
$turntime = getRandTurn($rng, $admin['turnterm'], Util::toInt($admin['turntime']));
|
||||
}
|
||||
|
||||
|
||||
$now = TimeUtil::now(true);
|
||||
if ($now >= $turntime) {
|
||||
$turntime = addTurn($turntime, $admin['turnterm']);
|
||||
}
|
||||
@@ -435,6 +434,13 @@ class Join extends \sammo\BaseAPI
|
||||
'specage2' => $specage2,
|
||||
'special2' => $special2,
|
||||
'penalty' => Json::encode($penalty),
|
||||
'aux' => Json::encode([
|
||||
'prestart_delete_after' => addTurn(
|
||||
$now,
|
||||
$admin['turnterm'],
|
||||
GameConst::$minTurnDieOnPrestart
|
||||
),
|
||||
]),
|
||||
]);
|
||||
$generalID = $db->insertId();
|
||||
$db->insert('general_access_log', [
|
||||
|
||||
@@ -8,6 +8,8 @@ use DateTimeInterface;
|
||||
use sammo\Enums\APIRecoveryType;
|
||||
use sammo\TurnExecutionHelper;
|
||||
use sammo\UniqueConst;
|
||||
use sammo\GameClock;
|
||||
use sammo\KVStorage;
|
||||
|
||||
class ExecuteEngine extends \sammo\BaseAPI
|
||||
{
|
||||
@@ -35,11 +37,13 @@ class ExecuteEngine extends \sammo\BaseAPI
|
||||
$updated = false;
|
||||
$locked = false;
|
||||
$lastExecuted = TurnExecutionHelper::executeAllCommand($updated, $locked);
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env'));
|
||||
return [
|
||||
'result' => true,
|
||||
'updated' => $updated,
|
||||
'locked' => $locked,
|
||||
'lastExecuted' => $lastExecuted,
|
||||
'lastExecutedTick' => $lastExecuted,
|
||||
'lastExecuted' => $clock->formatTick($lastExecuted, true),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace sammo\API\Global;
|
||||
|
||||
use sammo\DB;
|
||||
use sammo\GameClock;
|
||||
use sammo\Enums\APIRecoveryType;
|
||||
use sammo\Json;
|
||||
use sammo\KVStorage;
|
||||
@@ -145,7 +146,8 @@ class GeneralList extends \sammo\BaseAPI
|
||||
|
||||
|
||||
if (static::$withToken) {
|
||||
$now = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$now = GameClock::fromStorage($gameStor)->nowTick();
|
||||
$tokens = [];
|
||||
foreach ($db->query('SELECT * FROM select_npc_token WHERE `valid_until`>=%s', $now) as $token) {
|
||||
$validUntil = $token['valid_until'];
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace sammo\API\InheritAction;
|
||||
|
||||
use sammo\Session;
|
||||
use DateTimeInterface;
|
||||
use sammo\CentennialAllStarGrowthService;
|
||||
use sammo\DB;
|
||||
use sammo\Enums\APIRecoveryType;
|
||||
use sammo\Enums\RankColumn;
|
||||
@@ -98,6 +99,10 @@ class ResetStat extends \sammo\BaseAPI
|
||||
return 'NPC는 능력치 초기화를 할 수 없습니다.';
|
||||
}
|
||||
|
||||
if (!CentennialAllStarGrowthService::isStatResetAllowed()) {
|
||||
return '100기 올스타 장수는 능력치 초기화를 사용할 수 없습니다.';
|
||||
}
|
||||
|
||||
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
|
||||
@@ -64,13 +64,11 @@ class ResetTurnTime extends \sammo\BaseAPI
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$turnTerm = $gameStor->getValue('turnterm');
|
||||
|
||||
$currTurnTime = new DateTimeImmutable($general->getTurnTime());
|
||||
|
||||
$rng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
|
||||
UniqueConst::$hiddenSeed,
|
||||
'ResetTurnTime',
|
||||
$userID,
|
||||
$general->getAuxVar('nextTurnTimeBase') ?? $general->getTurnTime()
|
||||
$general->getAuxVar('nextTurnTimeBase') ?? $general->getTurnTick()
|
||||
)));
|
||||
|
||||
$afterTurn = $rng->nextFloat1() * $turnTerm * 60;
|
||||
|
||||
@@ -8,6 +8,7 @@ use sammo\Enums\APIRecoveryType;
|
||||
use sammo\Enums\GeneralLiteQueryMode;
|
||||
use sammo\Enums\GeneralQueryMode;
|
||||
use sammo\General;
|
||||
use sammo\GameClock;
|
||||
use sammo\GeneralLite;
|
||||
use sammo\Session;
|
||||
use sammo\Util;
|
||||
@@ -264,8 +265,11 @@ class GeneralList extends \sammo\BaseAPI
|
||||
'honorText' => fn ($rawGeneral) => getHonor($rawGeneral['experience']),
|
||||
'dedLevelText' => fn ($rawGeneral) => getDedLevelText($rawGeneral['dedlevel']),
|
||||
//'0000-00-00 11:23';
|
||||
'turntime' => fn ($rawGeneral) => substr($rawGeneral['turntime'], 0, 19),
|
||||
'recent_war' => fn ($rawGeneral) => substr($rawGeneral['recent_war'], 0, 19),
|
||||
'turntime' => fn ($rawGeneral) => GameClock::fromStorage($gameStor)
|
||||
->formatTick(Util::toInt($rawGeneral['turntime'])),
|
||||
'recent_war' => fn ($rawGeneral) => $rawGeneral['recent_war'] === null
|
||||
? null
|
||||
: GameClock::fromStorage($gameStor)->formatTick(Util::toInt($rawGeneral['recent_war'])),
|
||||
'bill' => fn ($rawGeneral) => getBillByLevel($rawGeneral['dedlevel']),
|
||||
'reservedCommand' => fn ($rawGeneral) => $reservedCommand[$rawGeneral['no']] ?? null,
|
||||
'autorun_limit' => fn ($rawGeneral) => ($rawGeneral['aux'] ?? [])['autorun_limit'] ?? 0,
|
||||
|
||||
@@ -8,6 +8,7 @@ use sammo\DB;
|
||||
use sammo\Enums\APIRecoveryType;
|
||||
use sammo\Enums\GeneralQueryMode;
|
||||
use sammo\GameConst;
|
||||
use sammo\GameClock;
|
||||
use sammo\General;
|
||||
use sammo\Json;
|
||||
use sammo\KVStorage;
|
||||
@@ -51,7 +52,8 @@ class GetReservedCommand extends \sammo\BaseAPI
|
||||
$nationID = $me['nation'];
|
||||
$limitState = checkLimit($me['refresh_score']);
|
||||
if ($limitState >= 2) {
|
||||
return "접속 제한중입니다. 1턴 이내에 너무 많은 갱신을 하셨습니다. (다음 갱신 가능 시각 : {$me['turntime']})";
|
||||
$limitTime = GameClock::fromStorage($gameStor)->formatTick(Util::toInt($me['turntime']), true);
|
||||
return "접속 제한중입니다. 1턴 이내에 너무 많은 갱신을 하셨습니다. (다음 갱신 가능 시각 : {$limitTime})";
|
||||
}
|
||||
|
||||
$permission = checkSecretPermission($me);
|
||||
|
||||
@@ -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();
|
||||
@@ -25,8 +30,9 @@ abstract class AbsFromUserPool extends AbsGeneralPool{
|
||||
}
|
||||
|
||||
static public function pickGeneralFromPool(\MeekroDB $db, RandUtil $rng, int $owner, int $pickCnt, ?string $prefix=null):array{
|
||||
$oNow = new \DateTimeImmutable();
|
||||
$now = $oNow->format('Y-m-d H:i:s');
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$now = $clock->nowTick();
|
||||
|
||||
$db->update('select_pool', [
|
||||
'reserved_until'=>null,
|
||||
@@ -36,17 +42,15 @@ 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){
|
||||
throw new \RuntimeException('pool 부족');
|
||||
}
|
||||
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$result = [];
|
||||
$validUntil = TimeUtil::nowAddMinutes(2 * $gameStor->turnterm);
|
||||
$result = [];
|
||||
$validUntil = $now + GameClock::TICKS_PER_TURN * 2;
|
||||
while(count($result) < $pickCnt){
|
||||
$cand = $rng->choiceUsingWeightPair($pool);
|
||||
$poolID = $cand['id'];
|
||||
@@ -69,4 +73,4 @@ abstract class AbsFromUserPool extends AbsGeneralPool{
|
||||
|
||||
return array_values($result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ abstract class AbsGeneralPool{
|
||||
* specialWar
|
||||
*/
|
||||
|
||||
public function __construct(\MeekroDB $db, RandUtil $rng, array $info, string $validUntil)
|
||||
public function __construct(\MeekroDB $db, RandUtil $rng, array $info, int $validUntil)
|
||||
{
|
||||
$this->db = $db;
|
||||
$this->info = $info;
|
||||
@@ -92,7 +92,7 @@ abstract class AbsGeneralPool{
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
public function getValidUntil():string{
|
||||
public function getValidUntil():int{
|
||||
return $this->validUntil;
|
||||
}
|
||||
|
||||
@@ -109,4 +109,4 @@ abstract class AbsGeneralPool{
|
||||
|
||||
abstract public static function getPoolName():string;
|
||||
abstract public static function initPool(\MeekroDB $db);
|
||||
}
|
||||
}
|
||||
|
||||
+55
-54
@@ -146,40 +146,35 @@ abstract class Auction
|
||||
return $this->info;
|
||||
}
|
||||
|
||||
public function shrinkCloseDate(?DateTimeInterface $date): ?string
|
||||
public function shrinkCloseTick(?int $tick): ?string
|
||||
{
|
||||
if ($date === null) {
|
||||
$date = new DateTimeImmutable();
|
||||
}
|
||||
|
||||
$this->info->closeDate = $date;
|
||||
$db = DB::db();
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
|
||||
$this->info->closeTick = $tick ?? $clock->nowTick();
|
||||
$db->update('ng_auction', $this->info->toArray('id'), 'id = %i', $this->info->id);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function extendLatestBidCloseDate(?DateTimeInterface $date): ?string
|
||||
public function extendLatestBidCloseTick(?int $tick): ?string
|
||||
{
|
||||
if ($date === null) {
|
||||
if ($tick === null) {
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$turnTerm = $gameStor->getValue('turnterm');
|
||||
$date = $this->info->closeDate->add(TimeUtil::secondsToDateInterval(
|
||||
max(static::MIN_EXTENSION_MINUTES_PER_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_PER_BID) * 60
|
||||
));
|
||||
$tick = $this->info->closeTick + $clock->ticksFromMinutes(
|
||||
max(static::MIN_EXTENSION_MINUTES_PER_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_PER_BID)
|
||||
);
|
||||
}
|
||||
else{
|
||||
$date = DateTimeImmutable::createFromInterface($date);
|
||||
}
|
||||
if ($this->info->detail->availableLatestBidCloseDate !== null && $date < $this->info->detail->availableLatestBidCloseDate) {
|
||||
if ($this->info->detail->availableLatestBidCloseTick !== null && $tick < $this->info->detail->availableLatestBidCloseTick) {
|
||||
return '기간보다 짧습니다.';
|
||||
}
|
||||
$this->info->detail->availableLatestBidCloseDate = $date;
|
||||
$this->info->detail->availableLatestBidCloseTick = $tick;
|
||||
return null;
|
||||
}
|
||||
|
||||
public function extendCloseDate(DateTimeInterface $date, bool $force = false): ?string
|
||||
public function extendCloseTick(int $tick, bool $force = false): ?string
|
||||
{
|
||||
if (!$force) {
|
||||
if ($this->info->detail->remainCloseDateExtensionCnt === null) {
|
||||
@@ -193,12 +188,11 @@ abstract class Auction
|
||||
}
|
||||
}
|
||||
|
||||
if ($date < $this->info->closeDate) {
|
||||
if ($tick < $this->info->closeTick) {
|
||||
return '종료 기간보다 짧습니다.';
|
||||
}
|
||||
|
||||
$closeDate = DateTimeImmutable::createFromInterface($date);
|
||||
$this->info->closeDate = $closeDate;
|
||||
$this->info->closeTick = $tick;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -245,12 +239,13 @@ abstract class Auction
|
||||
|
||||
//TODO: 전역 알림이 나타나야한다. 일반 메시지보다는 중요하고, 메시지보단 약하게..
|
||||
//TODO: 바로가기를 제공하는 편이 좋을 것 같다.
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
|
||||
$msg = new Message(
|
||||
MessageType::private,
|
||||
$src,
|
||||
$dest,
|
||||
$reason,
|
||||
new DateTime(),
|
||||
DateTime::createFromImmutable($clock->tickToDateTime($clock->nowTick())),
|
||||
new DateTime('9999-12-31'),
|
||||
[]
|
||||
);
|
||||
@@ -275,7 +270,12 @@ abstract class Auction
|
||||
$db->update('ng_auction', $this->info->toArray('id'), 'id = %i', $this->info->id);
|
||||
}
|
||||
|
||||
private function bidInheritPoint(int $amount, \DateTimeImmutable $now, bool $tryExtendCloseDate): ?string
|
||||
private function bidInheritPoint(
|
||||
int $amount,
|
||||
int $nowTick,
|
||||
\DateTimeImmutable $nowDate,
|
||||
bool $tryExtendCloseDate,
|
||||
): ?string
|
||||
{
|
||||
$db = DB::db();
|
||||
|
||||
@@ -311,7 +311,7 @@ abstract class Auction
|
||||
$general->getVar('owner'),
|
||||
$general->getID(),
|
||||
$amount,
|
||||
$now,
|
||||
$nowDate,
|
||||
new AuctionBidItemData(
|
||||
$general->getVar('owner_name'),
|
||||
$obfuscatedName,
|
||||
@@ -324,15 +324,16 @@ abstract class Auction
|
||||
}
|
||||
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$turnTerm = $gameStor->getValue('turnterm');
|
||||
|
||||
if ($this->info->detail->availableLatestBidCloseDate !== null) {
|
||||
$extendedCloseDate = $now->add(TimeUtil::secondsToDateInterval(
|
||||
max(static::MIN_EXTENSION_MINUTES_PER_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_PER_BID) * 60
|
||||
));
|
||||
if ($this->info->detail->availableLatestBidCloseTick !== null) {
|
||||
$extendedCloseTick = $nowTick + $clock->ticksFromMinutes(
|
||||
max(static::MIN_EXTENSION_MINUTES_PER_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_PER_BID)
|
||||
);
|
||||
|
||||
if ($extendedCloseDate > $this->info->closeDate && $this->info->closeDate < $this->info->detail->availableLatestBidCloseDate) {
|
||||
$this->extendCloseDate(min($extendedCloseDate, $this->info->detail->availableLatestBidCloseDate), true);
|
||||
if ($extendedCloseTick > $this->info->closeTick && $this->info->closeTick < $this->info->detail->availableLatestBidCloseTick) {
|
||||
$this->extendCloseTick(min($extendedCloseTick, $this->info->detail->availableLatestBidCloseTick), true);
|
||||
$this->applyDB();
|
||||
}
|
||||
}
|
||||
@@ -356,12 +357,16 @@ abstract class Auction
|
||||
return '경매가 이미 끝났습니다.';
|
||||
}
|
||||
|
||||
$now = new \DateTimeImmutable();
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$nowTick = $clock->nowTick();
|
||||
$nowDate = $clock->tickToDateTime($nowTick);
|
||||
|
||||
if ($auctionInfo->closeDate < $now) {
|
||||
if ($auctionInfo->closeTick < $nowTick) {
|
||||
return '경매가 이미 끝났습니다.';
|
||||
}
|
||||
if ($auctionInfo->openDate > $now) {
|
||||
if ($auctionInfo->openTick > $nowTick) {
|
||||
return '경매가 아직 시작되지 않았습니다.';
|
||||
}
|
||||
|
||||
@@ -377,13 +382,11 @@ abstract class Auction
|
||||
|
||||
|
||||
if ($auctionInfo->reqResource === ResourceType::inheritancePoint) {
|
||||
return $this->bidInheritPoint($amount, $now, $tryExtendCloseDate);
|
||||
return $this->bidInheritPoint($amount, $nowTick, $nowDate, $tryExtendCloseDate);
|
||||
}
|
||||
|
||||
//reqResource는 말 그대로 '구매자가 내야하는 자원'이다.
|
||||
|
||||
$db = DB::db();
|
||||
|
||||
$highestBid = $this->getHighestBid();
|
||||
if (!$auctionInfo->detail->isReverse) {
|
||||
if ($highestBid !== null && $amount <= $highestBid->amount) {
|
||||
@@ -421,7 +424,7 @@ abstract class Auction
|
||||
$general->getVar('owner'),
|
||||
$general->getID(),
|
||||
$amount,
|
||||
$now,
|
||||
$nowDate,
|
||||
new AuctionBidItemData(
|
||||
$general->getVar('owner_name'),
|
||||
$general->getName(),
|
||||
@@ -436,14 +439,13 @@ abstract class Auction
|
||||
|
||||
$general->increaseVar($resType->value, -$morePoint);
|
||||
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$turnTerm = $gameStor->getValue('turnterm');
|
||||
$extendedCloseDate = $now->add(TimeUtil::secondsToDateInterval(
|
||||
max(static::MIN_EXTENSION_MINUTES_PER_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_PER_BID) * 60
|
||||
));
|
||||
$extendedCloseTick = $nowTick + $clock->ticksFromMinutes(
|
||||
max(static::MIN_EXTENSION_MINUTES_PER_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_PER_BID)
|
||||
);
|
||||
|
||||
if ($extendedCloseDate > $this->info->closeDate) {
|
||||
$this->extendCloseDate($extendedCloseDate, true);
|
||||
if ($extendedCloseTick > $this->info->closeTick) {
|
||||
$this->extendCloseTick($extendedCloseTick, true);
|
||||
$this->applyDB();
|
||||
}
|
||||
|
||||
@@ -456,10 +458,10 @@ abstract class Auction
|
||||
|
||||
public function tryFinish(): ?bool
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
if ($now < $this->info->closeDate) {
|
||||
return null;
|
||||
}
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
if ($clock->nowTick() < $this->info->closeTick) return null;
|
||||
|
||||
//경매를 닫아야한다.
|
||||
$highestBid = $this->getHighestBid();
|
||||
@@ -469,17 +471,15 @@ abstract class Auction
|
||||
}
|
||||
|
||||
if ($highestBid->aux->tryExtendCloseDate) {
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$turnTerm = $gameStor->getValue('turnterm');
|
||||
|
||||
//연장 요청이 있었다.
|
||||
$extendedCloseDate = $this->info->closeDate->add(TimeUtil::secondsToDateInterval(
|
||||
max(static::MIN_EXTENSION_MINUTES_BY_EXTENSION_QUERY, $turnTerm * static::COEFF_EXTENSION_MINUTES_BY_EXTENSION_QUERY) * 60
|
||||
));
|
||||
$extendedCloseTick = $this->info->closeTick + $clock->ticksFromMinutes(
|
||||
max(static::MIN_EXTENSION_MINUTES_BY_EXTENSION_QUERY, $turnTerm * static::COEFF_EXTENSION_MINUTES_BY_EXTENSION_QUERY)
|
||||
);
|
||||
|
||||
if ($this->extendCloseDate($extendedCloseDate) === null) {
|
||||
$this->extendLatestBidCloseDate(null);
|
||||
if ($this->extendCloseTick($extendedCloseTick) === null) {
|
||||
$this->extendLatestBidCloseTick(null);
|
||||
$this->applyDB();
|
||||
return false;
|
||||
}
|
||||
@@ -509,12 +509,13 @@ abstract class Auction
|
||||
|
||||
//TODO: 전역 알림이 나타나야한다. 일반 메시지보다는 중요하고, 메시지보단 약하게..
|
||||
//TODO: 바로가기를 제공하는 편이 좋을 것 같다.
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
|
||||
$msg = new Message(
|
||||
MessageType::private,
|
||||
$src,
|
||||
$dest,
|
||||
$failReason,
|
||||
new \DateTime(),
|
||||
DateTime::createFromImmutable($clock->tickToDateTime($clock->nowTick())),
|
||||
new \DateTime('9999-12-31'),
|
||||
[]
|
||||
);
|
||||
|
||||
@@ -56,10 +56,11 @@ abstract class AuctionBasicResource extends Auction
|
||||
}
|
||||
|
||||
|
||||
$now = new \DateTimeImmutable();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$nowTick = $clock->nowTick();
|
||||
$turnTerm = $gameStor->getValue('turnterm');
|
||||
$closeDate = $now->add(TimeUtil::secondsToDateInterval($closeTurnCnt * $turnTerm * 60));
|
||||
$closeTick = $nowTick + GameClock::TICKS_PER_TURN * $closeTurnCnt;
|
||||
|
||||
$openResult = static::openAuction(new AuctionInfo(
|
||||
null,
|
||||
@@ -68,8 +69,8 @@ abstract class AuctionBasicResource extends Auction
|
||||
"$amount",
|
||||
$general->getId(),
|
||||
$bidderRes,
|
||||
$now,
|
||||
$closeDate,
|
||||
$nowTick,
|
||||
$closeTick,
|
||||
new AuctionInfoDetail(
|
||||
"{$hostResName} {$amount} 경매",
|
||||
$general->getName(),
|
||||
@@ -145,12 +146,13 @@ abstract class AuctionBasicResource extends Auction
|
||||
|
||||
//TODO: 전역 알림이 나타나야한다. 일반 메시지보다는 중요하고, 메시지보단 약하게..
|
||||
//TODO: 바로가기를 제공하는 편이 좋을 것 같다.
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env'));
|
||||
$msg = new Message(
|
||||
MessageType::private,
|
||||
$src,
|
||||
$dest,
|
||||
"{$this->auctionID}번 {$hostResName} 경매에 입찰이 없어 취소되었습니다.",
|
||||
new \DateTime(),
|
||||
\DateTime::createFromImmutable($clock->tickToDateTime($clock->nowTick())),
|
||||
new \DateTime('9999-12-31'),
|
||||
[]
|
||||
);
|
||||
@@ -246,8 +248,8 @@ abstract class AuctionBasicResource extends Auction
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$turnTerm = $gameStor->getValue('turnterm');
|
||||
$date = (new DateTimeImmutable())->add(TimeUtil::secondsToDateInterval($turnTerm * 60));
|
||||
$this->shrinkCloseDate($date);
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$this->shrinkCloseTick($clock->nowTick() + GameClock::TICKS_PER_TURN);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -72,16 +72,15 @@ class AuctionUniqueItem extends Auction
|
||||
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
|
||||
$now = new DateTimeImmutable();
|
||||
|
||||
[$turnTerm, $year, $month] = $gameStor->getValuesAsArray(['turnterm', 'year', 'month']);
|
||||
|
||||
$closeDate = $now->add(TimeUtil::secondsToDateInterval(
|
||||
max(static::MIN_AUCTION_CLOSE_MINUTES, $turnTerm * static::COEFF_AUCTION_CLOSE_MINUTES) * 60
|
||||
));
|
||||
$availableLatestBidCloseDate = $closeDate->add(TimeUtil::secondsToDateInterval(
|
||||
max(static::MIN_EXTENSION_MINUTES_LIMIT_BY_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_LIMIT_BY_BID) * 60
|
||||
));
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$nowTick = $clock->nowTick();
|
||||
$closeTick = $nowTick + $clock->ticksFromMinutes(
|
||||
max(static::MIN_AUCTION_CLOSE_MINUTES, $turnTerm * static::COEFF_AUCTION_CLOSE_MINUTES)
|
||||
);
|
||||
$availableLatestBidCloseTick = $closeTick + $clock->ticksFromMinutes(
|
||||
max(static::MIN_EXTENSION_MINUTES_LIMIT_BY_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_LIMIT_BY_BID)
|
||||
);
|
||||
|
||||
$info = new AuctionInfo(
|
||||
null,
|
||||
@@ -90,8 +89,8 @@ class AuctionUniqueItem extends Auction
|
||||
$itemKey,
|
||||
$general->getID(),
|
||||
ResourceType::inheritancePoint,
|
||||
$now,
|
||||
$closeDate,
|
||||
$nowTick,
|
||||
$closeTick,
|
||||
new AuctionInfoDetail(
|
||||
"{$item->getName()} 경매",
|
||||
static::genObfuscatedName($general->getID()),
|
||||
@@ -100,7 +99,7 @@ class AuctionUniqueItem extends Auction
|
||||
$startAmount,
|
||||
null,
|
||||
1,
|
||||
$availableLatestBidCloseDate,
|
||||
$availableLatestBidCloseTick,
|
||||
)
|
||||
);
|
||||
|
||||
@@ -267,21 +266,22 @@ class AuctionUniqueItem extends Auction
|
||||
|
||||
if ($availableEquipUniqueCnt <= 0) {
|
||||
$turnTerm = $gameStor->getValue('turnterm');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
//제한에 걸렸다면 자동 연장
|
||||
$extendedCloseDate = $this->info->closeDate->add(TimeUtil::secondsToDateInterval(
|
||||
max(static::MIN_EXTENSION_MINUTES_BY_EXTENSION_QUERY, $turnTerm * static::COEFF_EXTENSION_MINUTES_LIMIT_UNIQUE_CNT) * 60
|
||||
));
|
||||
$extendedCloseTick = $this->info->closeTick + $clock->ticksFromMinutes(
|
||||
max(static::MIN_EXTENSION_MINUTES_BY_EXTENSION_QUERY, $turnTerm * static::COEFF_EXTENSION_MINUTES_LIMIT_UNIQUE_CNT)
|
||||
);
|
||||
|
||||
if($bidder->getID() != $this->info->hostGeneralID){
|
||||
$this->setHostAsNeutral();
|
||||
}
|
||||
$this->extendCloseDate($extendedCloseDate, true);
|
||||
$this->extendLatestBidCloseDate(null);
|
||||
$this->extendCloseTick($extendedCloseTick, true);
|
||||
$this->extendLatestBidCloseTick(null);
|
||||
$this->applyDB();
|
||||
return '유니크 아이템 소유 제한 상태입니다. 종료 시간이 연장됩니다.';
|
||||
}
|
||||
|
||||
$isExtendCloseDateRequired = false;
|
||||
$isExtendCloseTickRequired = false;
|
||||
foreach (GameConst::$allItems as $itemType => $itemList) {
|
||||
//아직은 그런 경우는 없지만 동일 유니크를 여러 부위에 장착할 수 있을지도 모름
|
||||
if (!key_exists($itemKey, $itemList)) {
|
||||
@@ -291,13 +291,13 @@ class AuctionUniqueItem extends Auction
|
||||
$ownItem = $general->getItem($itemType);
|
||||
if ($ownItem->getRawClassName() == $itemKey) {
|
||||
//FIXME: 이 경우에는 환불이 되던가 해야함.
|
||||
$isExtendCloseDateRequired = true;
|
||||
$isExtendCloseTickRequired = true;
|
||||
$reasons[] = '이미 그 유니크를 가지고 있습니다.';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$ownItem->isBuyable()) {
|
||||
$isExtendCloseDateRequired = true;
|
||||
$isExtendCloseTickRequired = true;
|
||||
$reasons[] = '이미 다른 유니크를 가지고 있습니다.';
|
||||
continue;
|
||||
}
|
||||
@@ -313,18 +313,19 @@ class AuctionUniqueItem extends Auction
|
||||
}
|
||||
|
||||
if (!$availableItemTypes) {
|
||||
if ($isExtendCloseDateRequired) {
|
||||
if ($isExtendCloseTickRequired) {
|
||||
$turnTerm = $gameStor->getValue('turnterm');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
//동일 부위 제한에 걸렸다면 자동 연장
|
||||
$extendedCloseDate = $this->info->closeDate->add(TimeUtil::secondsToDateInterval(
|
||||
max(static::MIN_EXTENSION_MINUTES_LIMIT_BY_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_LIMIT_BY_BID) * 60
|
||||
));
|
||||
$extendedCloseTick = $this->info->closeTick + $clock->ticksFromMinutes(
|
||||
max(static::MIN_EXTENSION_MINUTES_LIMIT_BY_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_LIMIT_BY_BID)
|
||||
);
|
||||
|
||||
if($bidder->getID() != $this->info->hostGeneralID){
|
||||
$this->setHostAsNeutral();
|
||||
}
|
||||
$this->extendCloseDate($extendedCloseDate, true);
|
||||
$this->extendLatestBidCloseDate(null);
|
||||
$this->extendCloseTick($extendedCloseTick, true);
|
||||
$this->extendLatestBidCloseTick(null);
|
||||
$this->applyDB();
|
||||
}
|
||||
return join(' ', $reasons);
|
||||
|
||||
@@ -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'];
|
||||
@@ -18,15 +19,32 @@ final class CentennialAllStarGrowthService
|
||||
return GameConst::$targetGeneralPool === self::POOL_CLASS;
|
||||
}
|
||||
|
||||
public static function initialAux(array $targetInfo): array
|
||||
public static function isStatResetAllowed(): bool
|
||||
{
|
||||
return !self::isActive();
|
||||
}
|
||||
|
||||
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,
|
||||
'dexConsumed' => array_fill_keys(self::DEX_KEYS, 0),
|
||||
'dexFloor' => array_fill_keys(self::DEX_KEYS, 0),
|
||||
'progressMonth' => -1,
|
||||
'milestone' => 0,
|
||||
'naturalSpecialDomestic' => null,
|
||||
'eventSpecialDomestic' => null,
|
||||
'userInitialStats' => $userInitialStats,
|
||||
'dexTargetRatio' => 1.0,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -35,18 +53,225 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the event stat baseline that a newly selected target receives at
|
||||
* the supplied game date. Organic growth can still leave the actual stat
|
||||
* above this baseline.
|
||||
*
|
||||
* @return array{leadership:int,strength:int,intel:int}
|
||||
*/
|
||||
public static function calculateUserCurrentTargetStats(
|
||||
array $targetInfo,
|
||||
array $env
|
||||
): array {
|
||||
$initialStats = self::calculateUserInitialStats($targetInfo);
|
||||
$progress = self::calculateProgress(
|
||||
(int) $env['startyear'],
|
||||
(int) $env['year'],
|
||||
(int) $env['month']
|
||||
);
|
||||
$result = [];
|
||||
foreach (self::STAT_KEYS as $key) {
|
||||
$target = min(
|
||||
GameConst::$maxLevel,
|
||||
max(0, (int) ($targetInfo[$key] ?? 0))
|
||||
);
|
||||
$result[$key] = max(
|
||||
$initialStats[$key],
|
||||
CentennialAllStarGrowth::statFloor(
|
||||
$target,
|
||||
GameConst::$defaultStatMin,
|
||||
$progress
|
||||
)
|
||||
);
|
||||
}
|
||||
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,
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* @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,
|
||||
float $dexTargetRatio = 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);
|
||||
@@ -57,7 +282,27 @@ final class CentennialAllStarGrowthService
|
||||
? $aux['granted']
|
||||
: array_fill_keys(array_merge(self::STAT_KEYS, self::DEX_KEYS), 0);
|
||||
$targetChanged = ($aux['targetId'] ?? '') !== $targetId;
|
||||
$changed = false;
|
||||
$dexConsumed = is_array($aux['dexConsumed'] ?? null)
|
||||
? $aux['dexConsumed']
|
||||
: array_fill_keys(self::DEX_KEYS, 0);
|
||||
if ($targetChanged) {
|
||||
$dexConsumed = array_fill_keys(self::DEX_KEYS, 0);
|
||||
}
|
||||
$dexFloor = is_array($aux['dexFloor'] ?? null)
|
||||
? $aux['dexFloor']
|
||||
: array_fill_keys(self::DEX_KEYS, 0);
|
||||
$isUserTarget = is_array($aux['userInitialStats'] ?? null);
|
||||
$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 = $dexTargetRatioChanged;
|
||||
|
||||
foreach (self::STAT_KEYS as $key) {
|
||||
if (!array_key_exists($key, $targetInfo)) {
|
||||
@@ -69,6 +314,9 @@ final class CentennialAllStarGrowthService
|
||||
GameConst::$defaultStatMin,
|
||||
$progress
|
||||
);
|
||||
if ($userCurrentTargetStats !== null) {
|
||||
$floor = $userCurrentTargetStats[$key];
|
||||
}
|
||||
$current = (int) $general->getVar($key);
|
||||
if ($targetChanged) {
|
||||
$result = CentennialAllStarGrowth::replaceTarget(
|
||||
@@ -95,10 +343,17 @@ 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 = max(
|
||||
0,
|
||||
self::calculateDexTargetFloor(
|
||||
(int) $targetDex[$idx],
|
||||
$env,
|
||||
$dexTargetRatio
|
||||
) - max(0, (int) ($dexConsumed[$key] ?? 0))
|
||||
);
|
||||
$dexFloor[$key] = $floor;
|
||||
$current = (int) $general->getVar($key);
|
||||
if ($targetChanged) {
|
||||
if ($targetChanged || $dexTargetRatioChanged) {
|
||||
$result = CentennialAllStarGrowth::replaceTarget(
|
||||
$current,
|
||||
(int) ($granted[$key] ?? 0),
|
||||
@@ -146,8 +401,12 @@ final class CentennialAllStarGrowthService
|
||||
$milestone = min(5, (int) floor($progress * 5 + 0.0000001));
|
||||
$aux['targetId'] = $targetId;
|
||||
$aux['granted'] = $granted;
|
||||
$aux['dexConsumed'] = $dexConsumed;
|
||||
$aux['dexFloor'] = $dexFloor;
|
||||
$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 [
|
||||
@@ -159,6 +418,207 @@ 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 dexTargetRatioForNPCType(int $npcType): float
|
||||
{
|
||||
return in_array($npcType, [3, 4], true)
|
||||
? GameConst::$centennialNpcDexTargetRatio
|
||||
: 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;
|
||||
}
|
||||
self::initializeGeneratedNPC($general, $targetInfo);
|
||||
$result = self::applyTarget(
|
||||
$general,
|
||||
$targetInfo,
|
||||
$env,
|
||||
self::NPC_PROGRESS_MULTIPLIER,
|
||||
GameConst::$centennialNpcDexTargetRatio
|
||||
);
|
||||
$general->applyDB($db);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the ordinary M/G-general stat total while aligning its strong,
|
||||
* middle, and weak stats with the selected all-star target. The target's
|
||||
* creation-date growth floor is applied immediately afterwards.
|
||||
*/
|
||||
public static function initializeGeneratedNPC(
|
||||
General $general,
|
||||
array $targetInfo
|
||||
): void {
|
||||
$generatedStats = [];
|
||||
foreach (self::STAT_KEYS as $key) {
|
||||
$generatedStats[$key] = (int) $general->getVar($key);
|
||||
}
|
||||
$initialStats = self::calculateGeneratedNPCInitialStats(
|
||||
$targetInfo,
|
||||
$generatedStats
|
||||
);
|
||||
foreach (self::STAT_KEYS as $key) {
|
||||
$general->updateVar($key, $initialStats[$key]);
|
||||
}
|
||||
foreach (self::DEX_KEYS as $key) {
|
||||
$general->updateVar($key, 0);
|
||||
}
|
||||
$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
|
||||
* value and consumes any guaranteed floor crossed by the source value.
|
||||
* This keeps the monthly floor from refilling points already converted.
|
||||
*/
|
||||
public static function reconcileDexConversion(
|
||||
General $general,
|
||||
string $sourceKey,
|
||||
string $destinationKey,
|
||||
int $sourceBefore,
|
||||
int $sourceAfter,
|
||||
int $destinationBefore,
|
||||
int $destinationAfter,
|
||||
float $convertCoeff
|
||||
): void {
|
||||
if (!in_array($sourceKey, self::DEX_KEYS, true)
|
||||
|| !in_array($destinationKey, self::DEX_KEYS, true)
|
||||
|| $sourceKey === $destinationKey
|
||||
) {
|
||||
throw new \InvalidArgumentException('invalid dex conversion keys');
|
||||
}
|
||||
if ($convertCoeff < 0 || $convertCoeff > 1) {
|
||||
throw new \InvalidArgumentException('dex conversion coefficient must be between 0 and 1');
|
||||
}
|
||||
|
||||
$sourceDecrease = max(0, $sourceBefore - $sourceAfter);
|
||||
$destinationIncrease = max(0, $destinationAfter - $destinationBefore);
|
||||
if ($sourceDecrease === 0 && $destinationIncrease === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$aux = $general->getAuxVar(self::AUX_KEY);
|
||||
if (!is_array($aux)) {
|
||||
return;
|
||||
}
|
||||
$granted = is_array($aux['granted'] ?? null)
|
||||
? $aux['granted']
|
||||
: array_fill_keys(array_merge(self::STAT_KEYS, self::DEX_KEYS), 0);
|
||||
$dexConsumed = is_array($aux['dexConsumed'] ?? null)
|
||||
? $aux['dexConsumed']
|
||||
: array_fill_keys(self::DEX_KEYS, 0);
|
||||
$dexFloor = is_array($aux['dexFloor'] ?? null)
|
||||
? $aux['dexFloor']
|
||||
: [];
|
||||
|
||||
$sourceGrantedBefore = min(
|
||||
max(0, $sourceBefore),
|
||||
max(0, (int) ($granted[$sourceKey] ?? 0))
|
||||
);
|
||||
/*
|
||||
* A dex value can contain both organic and event-backed points. Split
|
||||
* the actual command deltas by their share of the source total instead
|
||||
* of consuming either bucket first. This keeps the combined ownership
|
||||
* stable when conversion and target reselection are interleaved.
|
||||
*/
|
||||
$eventGrantRemoved = $sourceBefore > 0
|
||||
? intdiv($sourceDecrease * $sourceGrantedBefore, $sourceBefore)
|
||||
: 0;
|
||||
$sourceGrantedAfter = max(0, $sourceGrantedBefore - $eventGrantRemoved);
|
||||
|
||||
$destinationGrantedBefore = min(
|
||||
max(0, $destinationBefore),
|
||||
max(0, (int) ($granted[$destinationKey] ?? 0))
|
||||
);
|
||||
$eventGrantTransferred = $sourceBefore > 0
|
||||
? intdiv($destinationIncrease * $sourceGrantedBefore, $sourceBefore)
|
||||
: 0;
|
||||
$eventGrantTransferred = min(
|
||||
$destinationIncrease,
|
||||
$eventGrantRemoved,
|
||||
$eventGrantTransferred
|
||||
);
|
||||
$granted[$sourceKey] = $sourceGrantedAfter;
|
||||
$granted[$destinationKey] = min(
|
||||
max(0, $destinationAfter),
|
||||
$destinationGrantedBefore + $eventGrantTransferred
|
||||
);
|
||||
|
||||
$sourceFloor = max(
|
||||
0,
|
||||
(int) ($dexFloor[$sourceKey] ?? $sourceBefore)
|
||||
);
|
||||
$gapBefore = max(0, $sourceFloor - $sourceBefore);
|
||||
$gapAfter = max(0, $sourceFloor - $sourceAfter);
|
||||
$dexConsumed[$sourceKey] = max(
|
||||
0,
|
||||
(int) ($dexConsumed[$sourceKey] ?? 0)
|
||||
+ max(0, $gapAfter - $gapBefore)
|
||||
);
|
||||
|
||||
$aux['granted'] = $granted;
|
||||
$aux['dexConsumed'] = $dexConsumed;
|
||||
$general->setAuxVar(self::AUX_KEY, $aux);
|
||||
}
|
||||
|
||||
public static function recordableValue(General $general, string $key): int
|
||||
{
|
||||
$aux = $general->getAuxVar(self::AUX_KEY);
|
||||
|
||||
@@ -14,6 +14,7 @@ use function \sammo\tryUniqueItemLottery;
|
||||
|
||||
use \sammo\Constraint\ConstraintHelper;
|
||||
use sammo\StaticEventHandler;
|
||||
use sammo\CentennialAllStarGrowthService;
|
||||
|
||||
class che_숙련전환 extends Command\GeneralCommand
|
||||
{
|
||||
@@ -157,6 +158,7 @@ class che_숙련전환 extends Command\GeneralCommand
|
||||
$logger = $general->getLogger();
|
||||
|
||||
$srcDex = $general->getVar('dex' . $this->srcArmType);
|
||||
$destDex = $general->getVar('dex' . $this->destArmType);
|
||||
$cutDex = Util::toInt($srcDex * static::$decreaseCoeff);
|
||||
$cutDexText = number_format($cutDex);
|
||||
$addDex = Util::toInt($cutDex * static::$convertCoeff);
|
||||
@@ -164,6 +166,19 @@ class che_숙련전환 extends Command\GeneralCommand
|
||||
|
||||
$general->increaseVar('dex' . $this->srcArmType, -$cutDex);
|
||||
$general->increaseVar('dex' . $this->destArmType, $addDex);
|
||||
// 100기 이벤트 지급분을 목적 숙련으로 옮기고 소비한 성장 하한은 다시 채우지 않는다.
|
||||
if (CentennialAllStarGrowthService::isActive()) {
|
||||
CentennialAllStarGrowthService::reconcileDexConversion(
|
||||
$general,
|
||||
'dex' . $this->srcArmType,
|
||||
'dex' . $this->destArmType,
|
||||
$srcDex,
|
||||
$general->getVar('dex' . $this->srcArmType),
|
||||
$destDex,
|
||||
$general->getVar('dex' . $this->destArmType),
|
||||
static::$convertCoeff
|
||||
);
|
||||
}
|
||||
|
||||
$josaUl = JosaUtil::pick($cutDex, '을');
|
||||
$josaRo = JosaUtil::pick($addDex, '로');
|
||||
|
||||
@@ -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, '라');
|
||||
|
||||
@@ -159,7 +159,7 @@ class che_발령 extends Command\NationCommand
|
||||
$destGeneral->getLogger()->pushGeneralActionLog("<Y>{$generalName}</>에 의해 <G><b>{$destCityName}</b></>{$josaRo} 발령됐습니다. <1>$date</>");
|
||||
|
||||
$yearMonth = Util::joinYearMonth($this->env['year'], $this->env['month']);
|
||||
if (cutTurn($general->getTurnTime(), $this->env['turnterm']) != cutTurn($destGeneral->getTurnTime(), $this->env['turnterm'])) {
|
||||
if (cutTurn($general->getTurnTick(), $this->env['turnterm']) != cutTurn($destGeneral->getTurnTick(), $this->env['turnterm'])) {
|
||||
$yearMonth += 1;
|
||||
}
|
||||
$destGeneral->setAuxVar('last발령', $yearMonth);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ class che_천도 extends Command\NationCommand
|
||||
$nationID = $general->getNationID();
|
||||
$nationStor = \sammo\KVStorage::getStorage(DB::db(), $nationID, 'nation_env');
|
||||
|
||||
$nationStor->last천도Trial = [$general->getVar('officer_level'), $general->getTurnTime()];
|
||||
$nationStor->last천도Trial = [$general->getVar('officer_level'), $general->getTurnTick()];
|
||||
|
||||
if ($lastTurn->getCommand() != $commandName || $lastTurn->getArg() !== $this->arg) {
|
||||
$this->setResultTurn(new LastTurn(
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
namespace sammo\DTO;
|
||||
|
||||
use LDTO\Attr\Convert;
|
||||
use LDTO\Attr\JsonString;
|
||||
use LDTO\Attr\NullIsUndefined;
|
||||
use LDTO\Attr\RawName;
|
||||
use LDTO\Converter\DateTimeConverter;
|
||||
use sammo\Enums\AuctionType;
|
||||
use sammo\Enums\ResourceType;
|
||||
|
||||
@@ -23,12 +21,10 @@ class AuctionInfo extends \LDTO\DTO
|
||||
#[RawName('req_resource')]
|
||||
public ResourceType $reqResource,
|
||||
|
||||
#[RawName('open_date')]
|
||||
#[Convert(DateTimeConverter::class)]
|
||||
public \DateTimeImmutable $openDate,
|
||||
#[RawName('close_date')]
|
||||
#[Convert(DateTimeConverter::class)]
|
||||
public \DateTimeImmutable $closeDate,
|
||||
#[RawName('open_tick')]
|
||||
public int $openTick,
|
||||
#[RawName('close_tick')]
|
||||
public int $closeTick,
|
||||
|
||||
#[JsonString]
|
||||
public AuctionInfoDetail $detail,
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
namespace sammo\DTO;
|
||||
|
||||
use LDTO\Attr\Convert;
|
||||
use LDTO\Attr\NullIsUndefined;
|
||||
use LDTO\Converter\DateTimeConverter;
|
||||
|
||||
class AuctionInfoDetail extends \LDTO\DTO
|
||||
{
|
||||
@@ -21,8 +19,7 @@ class AuctionInfoDetail extends \LDTO\DTO
|
||||
#[NullIsUndefined]
|
||||
public ?int $remainCloseDateExtensionCnt,
|
||||
#[NullIsUndefined]
|
||||
#[Convert(DateTimeConverter::class)]
|
||||
public ?\DateTimeImmutable $availableLatestBidCloseDate,
|
||||
public ?int $availableLatestBidCloseTick,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
namespace sammo\DTO;
|
||||
|
||||
use LDTO\Attr\Convert;
|
||||
use LDTO\Attr\NullIsUndefined;
|
||||
use LDTO\Attr\RawName;
|
||||
use LDTO\Converter\DateTimeConverter;
|
||||
|
||||
class GeneralAccessLog extends \LDTO\DTO
|
||||
{
|
||||
@@ -20,8 +18,7 @@ class GeneralAccessLog extends \LDTO\DTO
|
||||
public ?int $userID,
|
||||
|
||||
#[RawName('last_refresh')]
|
||||
#[Convert(DateTimeConverter::class)]
|
||||
public \DateTimeImmutable $lastRefresh,
|
||||
public ?int $lastRefresh,
|
||||
|
||||
public int $refresh,
|
||||
|
||||
@@ -35,4 +32,4 @@ class GeneralAccessLog extends \LDTO\DTO
|
||||
public int $refreshScoreTotal,
|
||||
) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class DiplomaticMessage extends Message{
|
||||
$this->validDiplomacy = false;
|
||||
}
|
||||
|
||||
if($this->validUntil < (new \DateTime())){
|
||||
if($this->validUntil < $this->date){
|
||||
$this->validDiplomacy = false;
|
||||
}
|
||||
}
|
||||
@@ -281,4 +281,4 @@ class DiplomaticMessage extends Message{
|
||||
return self::DECLINED;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,15 @@ 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),
|
||||
CentennialAllStarGrowthService::dexTargetRatioForNPCType(
|
||||
$general->getNPCType()
|
||||
)
|
||||
);
|
||||
|
||||
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()
|
||||
|
||||
@@ -184,6 +184,10 @@ class GameConstBase
|
||||
/** @var array 선택할 수 없으나 게임 내에 유효한 장수 내정 특기 */
|
||||
public static $optionalSpecialDomestic = [
|
||||
'None',
|
||||
'che_event_귀병', 'che_event_신산', 'che_event_환술', 'che_event_집중', 'che_event_신중',
|
||||
'che_event_반계', 'che_event_보병', 'che_event_궁병', 'che_event_기병', 'che_event_공성',
|
||||
'che_event_돌격', 'che_event_무쌍', 'che_event_견고', 'che_event_위압', 'che_event_저격',
|
||||
'che_event_필살', 'che_event_징병', 'che_event_의술', 'che_event_격노', 'che_event_척사',
|
||||
];
|
||||
|
||||
/** @var string 기본 전투 특기 */
|
||||
@@ -416,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 = [
|
||||
|
||||
+70
-26
@@ -281,16 +281,13 @@ class General extends GeneralBase implements iAction
|
||||
$this->calcCache[$cacheKey] = $result;
|
||||
return $result;
|
||||
}
|
||||
$recwar = new \DateTimeImmutable($this->getVar('recent_war'));
|
||||
$turnNow = new \DateTimeImmutable($this->getVar('turntime'));
|
||||
$secDiff = TimeUtil::DateIntervalToSeconds($recwar->diff($turnNow));
|
||||
|
||||
if ($secDiff <= 0) {
|
||||
$this->calcCache[$cacheKey] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
$result = intdiv(Util::toInt($secDiff), 60 * $turnTerm);
|
||||
$tickDiff = Util::toInt($this->getVar('turntime')) - Util::toInt($this->getVar('recent_war'));
|
||||
if ($tickDiff <= 0) {
|
||||
$this->calcCache[$cacheKey] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
$result = intdiv($tickDiff, GameClock::TICKS_PER_TURN);
|
||||
$this->calcCache[$cacheKey] = $result;
|
||||
return $result;
|
||||
}
|
||||
@@ -1045,18 +1042,64 @@ class General extends GeneralBase implements iAction
|
||||
|
||||
/** @var Map<GeneralAccessLog,int>|null */
|
||||
$rawAccessLog = null;
|
||||
$rankColumnValues = array_map(fn (\BackedEnum $e) => $e->value, $rankColumn);
|
||||
|
||||
if (!$accessLogColumn) {
|
||||
$rawGeneral = $db->queryFirstRow('SELECT %l FROM general WHERE no = %i', Util::formatListOfBackticks($column), $generalID);
|
||||
if ($rankColumn) {
|
||||
$rawGeneral = $db->queryFirstRow(
|
||||
'SELECT %l, (
|
||||
SELECT GROUP_CONCAT(
|
||||
CONCAT(`type`, CHAR(61), `value`)
|
||||
ORDER BY `type`
|
||||
)
|
||||
FROM rank_data
|
||||
WHERE rank_data.general_id = general.no
|
||||
AND `type` IN %ls
|
||||
) AS `_rank_values`
|
||||
FROM general WHERE no = %i',
|
||||
Util::formatListOfBackticks($column),
|
||||
$rankColumnValues,
|
||||
$generalID
|
||||
);
|
||||
} else {
|
||||
$rawGeneral = $db->queryFirstRow(
|
||||
'SELECT %l FROM general WHERE no = %i',
|
||||
Util::formatListOfBackticks($column),
|
||||
$generalID
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$rawGeneral = $db->queryFirstRow(
|
||||
'SELECT %l, %l FROM `general` LEFT JOIN general_access_log
|
||||
ON general.no = general_access_log.general_id WHERE no = %i',
|
||||
Util::formatListOfBackticks($column),
|
||||
Util::formatListOfBackticks($accessLogColumn),
|
||||
$generalID
|
||||
);
|
||||
if ($rankColumn) {
|
||||
$rawGeneral = $db->queryFirstRow(
|
||||
'SELECT %l, %l, (
|
||||
SELECT GROUP_CONCAT(
|
||||
CONCAT(`type`, CHAR(61), `value`)
|
||||
ORDER BY `type`
|
||||
)
|
||||
FROM rank_data
|
||||
WHERE rank_data.general_id = general.no
|
||||
AND `type` IN %ls
|
||||
) AS `_rank_values`
|
||||
FROM `general` LEFT JOIN general_access_log
|
||||
ON general.no = general_access_log.general_id
|
||||
WHERE no = %i',
|
||||
Util::formatListOfBackticks($column),
|
||||
Util::formatListOfBackticks($accessLogColumn),
|
||||
$rankColumnValues,
|
||||
$generalID
|
||||
);
|
||||
} else {
|
||||
$rawGeneral = $db->queryFirstRow(
|
||||
'SELECT %l, %l FROM `general` LEFT JOIN general_access_log
|
||||
ON general.no = general_access_log.general_id WHERE no = %i',
|
||||
Util::formatListOfBackticks($column),
|
||||
Util::formatListOfBackticks($accessLogColumn),
|
||||
$generalID
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($accessLogColumn) {
|
||||
$rawAccessLog = new Map();
|
||||
foreach ($accessLogColumn as $accessLogKey) {
|
||||
if (!key_exists($accessLogKey->value, $rawGeneral)) {
|
||||
@@ -1076,15 +1119,16 @@ class General extends GeneralBase implements iAction
|
||||
|
||||
$rawRankValues = new Map();
|
||||
if ($rankColumn) {
|
||||
$rawValue = $db->queryAllLists(
|
||||
'SELECT `type`, `value` FROM rank_data WHERE general_id = %i AND `type` IN %ls',
|
||||
$generalID,
|
||||
array_map(fn (\BackedEnum $e) => $e->value, $rankColumn)
|
||||
);
|
||||
foreach ($rawValue as [$rawRankType, $rankValue]) {
|
||||
$rankType = RankColumn::tryFrom($rawRankType);
|
||||
$rawRankValues->put($rankType, $rankValue);
|
||||
$rawRankPairs = $rawGeneral['_rank_values'];
|
||||
foreach ($rawRankPairs === null || $rawRankPairs === ''
|
||||
? []
|
||||
: explode(',', $rawRankPairs) as $rawRankPair
|
||||
) {
|
||||
[$rawRankType, $rankValue] = explode('=', $rawRankPair, 2);
|
||||
$rankType = RankColumn::from($rawRankType);
|
||||
$rawRankValues->put($rankType, (int) $rankValue);
|
||||
}
|
||||
unset($rawGeneral['_rank_values']);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+12
-15
@@ -302,7 +302,7 @@ class GeneralAI
|
||||
$this->calcWarRoute();
|
||||
$troopCandidate = [];
|
||||
|
||||
$chiefTurn = cutTurn($this->general->getTurnTime(), $this->env['turnterm']);
|
||||
$chiefTurn = cutTurn($this->general->getTurnTick(), $this->env['turnterm']);
|
||||
$yearMonth = Util::joinYearMonth($this->env['year'], $this->env['month']);
|
||||
|
||||
foreach ($this->troopLeaders as $troopLeader) {
|
||||
@@ -319,7 +319,7 @@ class GeneralAI
|
||||
|
||||
$last발령 = $troopLeader->getAuxVar('last발령');
|
||||
if ($last발령) {
|
||||
$leaderTurn = cutTurn($troopLeader->getTurnTime(), $this->env['turnterm']);
|
||||
$leaderTurn = cutTurn($troopLeader->getTurnTick(), $this->env['turnterm']);
|
||||
$compYearMonth = $yearMonth;
|
||||
if ($chiefTurn < $leaderTurn) {
|
||||
$compYearMonth += 1;
|
||||
@@ -405,7 +405,7 @@ class GeneralAI
|
||||
return null;
|
||||
}
|
||||
|
||||
$chiefTurn = cutTurn($this->general->getTurnTime(), $this->env['turnterm']);
|
||||
$chiefTurn = cutTurn($this->general->getTurnTick(), $this->env['turnterm']);
|
||||
$yearMonth = Util::joinYearMonth($this->env['year'], $this->env['month']);
|
||||
|
||||
$troopCandidate = [];
|
||||
@@ -428,7 +428,7 @@ class GeneralAI
|
||||
|
||||
$last발령 = $troopLeader->getAuxVar('last발령');
|
||||
if ($last발령) {
|
||||
$leaderTurn = cutTurn($troopLeader->getTurnTime(), $this->env['turnterm']);
|
||||
$leaderTurn = cutTurn($troopLeader->getTurnTick(), $this->env['turnterm']);
|
||||
$compYearMonth = $yearMonth;
|
||||
if ($chiefTurn < $leaderTurn) {
|
||||
$compYearMonth += 1;
|
||||
@@ -586,8 +586,8 @@ class GeneralAI
|
||||
continue;
|
||||
}
|
||||
|
||||
$generalTurnTime = $userGeneral->getTurnTime();
|
||||
$troopTurnTime = $troopLeader->getTurnTime();
|
||||
$generalTurnTime = $userGeneral->getTurnTick();
|
||||
$troopTurnTime = $troopLeader->getTurnTick();
|
||||
|
||||
if ($generalTurnTime < $troopTurnTime) { //NOTE: 어차피 수뇌 턴이 제일 빠르다
|
||||
$generalCadidates[$generalID] = $userGeneral;
|
||||
@@ -782,7 +782,7 @@ class GeneralAI
|
||||
|
||||
if (
|
||||
key_exists($troopLeader->getCityID(), $this->supplyCities) &&
|
||||
$this->troopLeaders[$troopID]->getTurnTime() < $lostGeneral->getTurnTime()
|
||||
$this->troopLeaders[$troopID]->getTurnTick() < $lostGeneral->getTurnTick()
|
||||
) {
|
||||
//이미 탈출 가능한 부대를 탔다
|
||||
continue;
|
||||
@@ -1986,7 +1986,7 @@ class GeneralAI
|
||||
if ($lastTurn->getCommand() === '천도' && $lastTurn->getArg()['destCityID'] != $this->nation['capital']) {
|
||||
$cmd = buildNationCommandClass('che_천도', $this->general, $this->env, $lastTurn, $lastTurn->getArg());
|
||||
if ($cmd->hasFullConditionMet()) {
|
||||
$nationStor->last천도Trial = [$general->getVar('officer_level'), $general->getTurnTime()];
|
||||
$nationStor->last천도Trial = [$general->getVar('officer_level'), $general->getTurnTick()];
|
||||
$this->reqUpdateInstance = true;
|
||||
return $cmd;
|
||||
}
|
||||
@@ -1995,12 +1995,9 @@ class GeneralAI
|
||||
$lastTrial = $nationStor->last천도Trial;
|
||||
if ($lastTrial) {
|
||||
[$lastTrialLevel, $lastTrialTurnTime] = $lastTrial;
|
||||
$timeDiffSeconds = TimeUtil::DateIntervalToSeconds(
|
||||
date_create_immutable($lastTrialTurnTime)->diff(
|
||||
date_create_immutable($general->getTurnTime())
|
||||
)
|
||||
);
|
||||
if ($timeDiffSeconds < $turnTerm * 30 && $lastTrialLevel !== $general->getVar('officer_level')) { //0.5Turn
|
||||
$timeDiffTick = abs($general->getTurnTick() - Util::toInt($lastTrialTurnTime));
|
||||
if ($timeDiffTick < intdiv(GameClock::TICKS_PER_TURN, 2)
|
||||
&& $lastTrialLevel !== $general->getVar('officer_level')) { //0.5Turn
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -2109,7 +2106,7 @@ class GeneralAI
|
||||
}
|
||||
|
||||
|
||||
$nationStor->last천도Trial = [$general->getVar('officer_level'), $general->getTurnTime()];
|
||||
$nationStor->last천도Trial = [$general->getVar('officer_level'), $general->getTurnTick()];
|
||||
$this->reqUpdateInstance = true;
|
||||
return $cmd;
|
||||
}
|
||||
|
||||
+33
-10
@@ -54,14 +54,24 @@ abstract class GeneralBase
|
||||
);
|
||||
}
|
||||
|
||||
function getTurnTime(int $short = self::TURNTIME_FULL_MS): ?string
|
||||
{
|
||||
if(!key_exists('turntime', $this->raw)){
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
self::TURNTIME_FULL_MS => function ($turntime) {
|
||||
function getTurnTime(int $short = self::TURNTIME_FULL_MS): ?string
|
||||
{
|
||||
if(!key_exists('turntime', $this->raw)){
|
||||
return null;
|
||||
}
|
||||
|
||||
$rawTurnTime = $this->getVar('turntime');
|
||||
// 비교용 Dummy와 과거 fixture는 문자열을 잠시 허용하되 제품 DB는 tick만 사용합니다.
|
||||
if (is_string($rawTurnTime) && !ctype_digit(ltrim($rawTurnTime, '-'))) {
|
||||
$formattedTurnTime = $rawTurnTime;
|
||||
} else {
|
||||
$db = DB::db();
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
|
||||
$formattedTurnTime = $clock->formatTick(Util::toInt($rawTurnTime), true);
|
||||
}
|
||||
|
||||
return [
|
||||
self::TURNTIME_FULL_MS => function ($turntime) {
|
||||
return $turntime;
|
||||
},
|
||||
self::TURNTIME_FULL => function ($turntime) {
|
||||
@@ -73,8 +83,21 @@ abstract class GeneralBase
|
||||
self::TURNTIME_HM => function ($turntime) {
|
||||
return substr($turntime, 11, 5);
|
||||
},
|
||||
][$short]($this->getVar('turntime'));
|
||||
}
|
||||
][$short]($formattedTurnTime);
|
||||
}
|
||||
|
||||
function getTurnTick(): ?int
|
||||
{
|
||||
if (!key_exists('turntime', $this->raw)) {
|
||||
return null;
|
||||
}
|
||||
$rawTurnTime = $this->getVar('turntime');
|
||||
if (is_string($rawTurnTime) && !ctype_digit(ltrim($rawTurnTime, '-'))) {
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env'));
|
||||
return $clock->dateTimeToTick(new \DateTimeImmutable($rawTurnTime));
|
||||
}
|
||||
return Util::toInt($rawTurnTime);
|
||||
}
|
||||
|
||||
function getNPCType(): int
|
||||
{
|
||||
|
||||
+4703
-31594
File diff suppressed because it is too large
Load Diff
@@ -59,7 +59,7 @@ class RandomNameGeneral extends AbsGeneralPool{
|
||||
'generalName'=>$generalName,
|
||||
'imgsvr'=>0,
|
||||
'picture'=>null
|
||||
], '9999-12-31 12:00:00');
|
||||
], PHP_INT_MAX);
|
||||
}
|
||||
|
||||
static public function pickGeneralFromPool(MeekroDB $db, RandUtil $rng, int $owner, int $pickCnt, ?string $prefix = null): array
|
||||
@@ -68,26 +68,25 @@ class RandomNameGeneral extends AbsGeneralPool{
|
||||
$result = [];
|
||||
$dbInsert = [];
|
||||
|
||||
$oNow = new \DateTimeImmutable();
|
||||
|
||||
|
||||
for($i=0;$i<$pickCnt;$i++){
|
||||
$result[] = static::pickGeneral1FromPool($db, $rng, $owner, $prefix);
|
||||
}
|
||||
|
||||
if($owner){
|
||||
$now = $oNow->format('Y-m-d H:i:s');
|
||||
$gameStor = \sammo\KVStorage::getStorage($db, 'game_env');
|
||||
$clock = \sammo\GameClock::fromStorage($gameStor);
|
||||
$now = $clock->nowTick();
|
||||
$db->delete('select_pool', [
|
||||
'reserved_until'=>null,
|
||||
'owner'=>null,
|
||||
],'(reserved_until < %s OR reserved_until IS NULL) AND general_id IS null', $now);
|
||||
$validUntil = $oNow->add(new \DateInterval(sprintf('PT%dS', 30)));
|
||||
$validUntil = $now + $clock->ticksFromSeconds(30);
|
||||
foreach($result as $pickedGeneral){
|
||||
$dbInsert[] = [
|
||||
'owner'=>$owner,
|
||||
'uniqueName'=>$pickedGeneral->getUniqueName(),
|
||||
'info'=>$pickedGeneral->getInfo(),
|
||||
'reserved_until'=>$validUntil->format(('Y-m-d H:i:s'))
|
||||
'reserved_until'=>$validUntil
|
||||
];
|
||||
}
|
||||
$db->insert('select_pool', $dbInsert);
|
||||
@@ -99,4 +98,4 @@ class RandomNameGeneral extends AbsGeneralPool{
|
||||
public static function initPool(\MeekroDB $db){
|
||||
//do Nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,12 @@ use sammo\RandUtil;
|
||||
|
||||
class SPoolUnderU100 extends AbsFromUserPool
|
||||
{
|
||||
public function __construct(\MeekroDB $db, RandUtil $rng, array $info, string $validUntil)
|
||||
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, int $validUntil)
|
||||
{
|
||||
$targetInfo = $info;
|
||||
$initialInfo = $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'));
|
||||
|
||||
+32
-15
@@ -104,6 +104,7 @@ class Message
|
||||
|
||||
public static function buildFromArray(array $row) : Message
|
||||
{
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env'));
|
||||
$dbMessage = Json::decode($row['message']);
|
||||
|
||||
$msgType = MessageType::from($row['type']);
|
||||
@@ -116,8 +117,8 @@ class Message
|
||||
$src,
|
||||
$dest,
|
||||
$dbMessage['text'],
|
||||
new \DateTime($row['time']),
|
||||
new \DateTime($row['valid_until']),
|
||||
\DateTime::createFromImmutable($clock->tickToDateTime(Util::toInt($row['time']))),
|
||||
\DateTime::createFromImmutable($clock->tickToDateTime(Util::toInt($row['valid_until']))),
|
||||
$option
|
||||
];
|
||||
|
||||
@@ -151,9 +152,12 @@ class Message
|
||||
public static function getMessageByID(int $messageID) : ?Message
|
||||
{
|
||||
$db = DB::db();
|
||||
$now = new \DateTime();
|
||||
$row = $db->queryFirstRow('SELECT * FROM `message` WHERE `id` = %i AND valid_until', $messageID);
|
||||
//FIXME: $now가 들어가야 하는데 안 들어가있는데?
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
|
||||
$row = $db->queryFirstRow(
|
||||
'SELECT * FROM `message` WHERE `id` = %i AND valid_until > %i',
|
||||
$messageID,
|
||||
$clock->nowTick(),
|
||||
);
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
@@ -171,12 +175,12 @@ class Message
|
||||
{
|
||||
$db = DB::db();
|
||||
|
||||
$date = (new \DateTime())->format('Y-m-d H:i:s');
|
||||
$date = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))->nowTick();
|
||||
|
||||
$where = new \WhereClause('and');
|
||||
$where->add('mailbox = %i', $mailbox);
|
||||
$where->add('type = %s', $msgType->value);
|
||||
$where->add('valid_until > %s', $date);
|
||||
$where->add('valid_until > %i', $date);
|
||||
if ($fromSeq > 0) {
|
||||
$where->add('id >= %i', $fromSeq);
|
||||
}
|
||||
@@ -203,12 +207,12 @@ class Message
|
||||
{
|
||||
$db = DB::db();
|
||||
|
||||
$date = (new \DateTime())->format('Y-m-d H:i:s');
|
||||
$date = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))->nowTick();
|
||||
|
||||
$where = new \WhereClause('and');
|
||||
$where->add('mailbox = %i', $mailbox);
|
||||
$where->add('type = %s', $msgType->value);
|
||||
$where->add('valid_until > %s', $date);
|
||||
$where->add('valid_until > %i', $date);
|
||||
$where->add('id < %i', $toSeq);
|
||||
|
||||
if ($limit > 0) {
|
||||
@@ -236,7 +240,8 @@ class Message
|
||||
return '시스템 외교 메시지는 삭제할 수 없습니다.';
|
||||
}
|
||||
|
||||
$prev5min = new \DateTime();
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage(DB::db(), 'game_env'));
|
||||
$prev5min = \DateTime::createFromImmutable($clock->tickToDateTime($clock->nowTick()));
|
||||
$prev5min->sub(new \DateInterval('PT5M'));
|
||||
|
||||
if($msgObj->date < $prev5min){
|
||||
@@ -265,14 +270,15 @@ class Message
|
||||
|
||||
}
|
||||
|
||||
$in1min = new \DateTime();
|
||||
$now = \DateTime::createFromImmutable($clock->tickToDateTime($clock->nowTick()));
|
||||
$in1min = clone $now;
|
||||
$in1min->add(new \DateInterval('PT1M'));
|
||||
$newMsg = new Message(
|
||||
$msgObj->msgType,
|
||||
$msgObj->src,
|
||||
$msgObj->dest,
|
||||
"req_del_msg",
|
||||
new \DateTime(),
|
||||
$now,
|
||||
$in1min,
|
||||
$msgOption
|
||||
);
|
||||
@@ -300,13 +306,23 @@ class Message
|
||||
|
||||
|
||||
$db = DB::db();
|
||||
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
|
||||
$timeTick = $clock->nowTick();
|
||||
if (Util::toInt($this->validUntil->format('Y')) >= 9000) {
|
||||
$validUntilTick = GameClock::MAX_SAFE_TICK;
|
||||
} else {
|
||||
$validitySeconds = $this->validUntil->getTimestamp() - $this->date->getTimestamp();
|
||||
$validUntilTick = $timeTick + $clock->ticksFromSeconds($validitySeconds);
|
||||
}
|
||||
$this->date = \DateTime::createFromImmutable($clock->tickToDateTime($timeTick));
|
||||
$this->validUntil = \DateTime::createFromImmutable($clock->tickToDateTime($validUntilTick));
|
||||
$db->insert('message', [
|
||||
'mailbox' => $mailbox,
|
||||
'type' => $this->msgType->value,
|
||||
'src' => $src_id,
|
||||
'dest' => $dest_id,
|
||||
'time' => $this->date->format('Y-m-d H:i:s'),
|
||||
'valid_until' => $this->validUntil->format('Y-m-d H:i:s'),
|
||||
'time' => $timeTick,
|
||||
'valid_until' => $validUntilTick,
|
||||
'message' => Json::encode([
|
||||
'src'=>($this->src)?($this->src->toArray()):[],
|
||||
'dest'=>($this->dest)?($this->dest->toArray()):[],
|
||||
@@ -489,7 +505,8 @@ class Message
|
||||
'text' => $this->msg,
|
||||
'option' => $this->msgOption
|
||||
]),
|
||||
'valid_until'=>$this->validUntil->format('Y-m-d H:i:s'),
|
||||
'valid_until'=>GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))
|
||||
->dateTimeToTick($this->validUntil),
|
||||
], 'id=%i', $this->id);
|
||||
|
||||
}
|
||||
|
||||
+34
-16
@@ -245,21 +245,35 @@ class ResetHelper{
|
||||
true
|
||||
);
|
||||
|
||||
if($sync == 0) {
|
||||
// 현재 시간을 1월로 맞춤
|
||||
$starttime = cutTurn($turntime, $turnterm);
|
||||
$month = 1;
|
||||
$year = $startyear;
|
||||
} else {
|
||||
// 현재 시간과 동기화
|
||||
[$starttime, $yearPulled, $month] = cutDay($turntime, $turnterm);
|
||||
$requestedTime = new \DateTimeImmutable($turntime);
|
||||
if($sync == 0) {
|
||||
// 현재 시간을 1월로 맞춤
|
||||
$baseTime = new \DateTimeImmutable(cutTurnDateTime($turntime, $turnterm));
|
||||
$month = 1;
|
||||
$year = $startyear;
|
||||
} else {
|
||||
// 현재 시간과 동기화
|
||||
[$baseTimeString, $yearPulled, $month] = cutDay($turntime, $turnterm);
|
||||
$baseTime = new \DateTimeImmutable($baseTimeString);
|
||||
if($yearPulled){
|
||||
$year = $startyear-1;
|
||||
}
|
||||
else{
|
||||
$year = $startyear;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$wallNow = GameClock::readWallTime();
|
||||
$initialClock = new GameClock(
|
||||
$baseTime,
|
||||
$turnterm,
|
||||
0,
|
||||
GameClock::MODE_REALTIME,
|
||||
$wallNow,
|
||||
fn (): \DateTimeImmutable => $wallNow,
|
||||
);
|
||||
$requestedTick = $initialClock->dateTimeToTick($requestedTime);
|
||||
$currentTick = $initialClock->dateTimeToTick($wallNow);
|
||||
|
||||
$killturn = 4800 / $turnterm;
|
||||
if($npcmode == 1) { $killturn = intdiv($killturn, 3); }
|
||||
@@ -283,10 +297,14 @@ class ResetHelper{
|
||||
'maxnation'=>GameConst::$defaultMaxNation,
|
||||
'refreshLimit'=>30000,
|
||||
'develcost'=>$develcost,
|
||||
'turntime'=>$turntime,
|
||||
'starttime'=>$starttime,
|
||||
'opentime'=>$turntime,
|
||||
'turnterm'=>$turnterm,
|
||||
'turntime'=>$requestedTick,
|
||||
'starttime'=>0,
|
||||
'opentime'=>$requestedTick,
|
||||
'turnterm'=>$turnterm,
|
||||
'clock_base_time'=>TimeUtil::format($baseTime, true),
|
||||
'clock_tick'=>$currentTick,
|
||||
'clock_mode'=>GameClock::MODE_REALTIME,
|
||||
'clock_wall_anchor'=>TimeUtil::format($wallNow, true),
|
||||
'killturn'=>$killturn,
|
||||
'genius'=>GameConst::$defaultMaxGenius,
|
||||
'show_img_level'=>$show_img_level,
|
||||
@@ -308,7 +326,7 @@ class ResetHelper{
|
||||
'name'=>$admin['name'],
|
||||
'picture'=>$admin['picture'],
|
||||
'imgsvr'=>$admin['imgsvr'],
|
||||
'turntime'=>$turntime,
|
||||
'turntime'=>$requestedTick,
|
||||
'killturn'=>9999,
|
||||
'crewtype'=>GameUnitConst::DEFAULT_CREWTYPE
|
||||
]);
|
||||
@@ -344,7 +362,7 @@ class ResetHelper{
|
||||
|
||||
$db->insert('ng_games', [
|
||||
'server_id'=>$serverID,
|
||||
'date'=>$turntime,
|
||||
'date'=>TimeUtil::format($requestedTime, false),
|
||||
'winner_nation'=>null,
|
||||
'map'=>$scenarioObj->getMapTheme(),
|
||||
'season'=>$seasonIdx,
|
||||
@@ -368,4 +386,4 @@ class ResetHelper{
|
||||
'result'=>true
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,7 +653,7 @@ class GeneralBuilder{
|
||||
$officerLevel = $nationID?1:0;
|
||||
}
|
||||
|
||||
$turntime = \sammo\getRandTurn($this->rng, $env['turnterm'], new \DateTimeImmutable($env['turntime']));
|
||||
$turntime = \sammo\getRandTurn($this->rng, $env['turnterm'], Util::toInt($env['turntime']));
|
||||
|
||||
if($this->killturn){
|
||||
$killturn = $this->killturn;
|
||||
@@ -735,4 +735,4 @@ class GeneralBuilder{
|
||||
|
||||
return true; //생성되었다.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class ScoutMessage extends Message
|
||||
$this->validScout = false;
|
||||
}
|
||||
|
||||
if ($this->validUntil <= new \DateTime()) {
|
||||
if ($this->validUntil <= $this->date) {
|
||||
$this->validScout = false;
|
||||
}
|
||||
}
|
||||
@@ -122,11 +122,11 @@ class ScoutMessage extends Message
|
||||
public static function invalidateAll(int $generalID, ?int $exceptMsgID = null)
|
||||
{
|
||||
$db = DB::db();
|
||||
$now = TimeUtil::now();
|
||||
$now = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'))->nowTick();
|
||||
//XXX: 뭔가 기존 쿼리가 애매하다. invalid 관련해서 다른 옵션이 가능한가?
|
||||
$rawMsgList = Util::convertArrayToDict($db->query(
|
||||
'SELECT * FROM `message` WHERE
|
||||
`mailbox` = %i AND `type` = "private" AND `dest` = `mailbox` AND `valid_until` > %s AND
|
||||
`mailbox` = %i AND `type` = "private" AND `dest` = `mailbox` AND `valid_until` > %i AND
|
||||
JSON_VALUE(message, "$.option.action") = %s',
|
||||
$generalID,
|
||||
$now,
|
||||
|
||||
@@ -40,34 +40,22 @@ final class ServerTool
|
||||
$locked = tryLock();
|
||||
}
|
||||
|
||||
$oldunit = $admin['turnterm'] * 60;
|
||||
$unit = $turnterm * 60;
|
||||
|
||||
if($unit == $oldunit){
|
||||
if($turnterm == $admin['turnterm']){
|
||||
if($locked){
|
||||
unlock();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$unitDiff = $unit / $oldunit;
|
||||
|
||||
$servTurnTime = new \DateTimeImmutable($admin['turntime']);
|
||||
foreach ($db->query('SELECT no,turntime FROM general') as $gen) {
|
||||
$genTurnTime = new \DateTimeImmutable($gen['turntime']);
|
||||
$timeDiff = TimeUtil::DateIntervalToSeconds($genTurnTime->diff($servTurnTime));
|
||||
$timeDiff *= $unitDiff;
|
||||
$newGenTurnTime = $servTurnTime->add(TimeUtil::secondsToDateInterval($timeDiff));
|
||||
|
||||
$db->update('general', [
|
||||
'turntime' => $newGenTurnTime->format('Y-m-d H:i:s.u')
|
||||
], 'no=%i', $gen['no']);
|
||||
}
|
||||
$turn = ($admin['year'] - $admin['startyear']) * 12 + $admin['month'] - 1;
|
||||
$starttime = $servTurnTime->sub(TimeUtil::secondsToDateInterval($turn * $unit))->format('Y-m-d H:i:s');
|
||||
$starttime = cutTurn($starttime, $turnterm, false);
|
||||
$oldClock = GameClock::fromStorage($gameStor);
|
||||
$currentTick = $oldClock->nowTick();
|
||||
$currentDisplay = $oldClock->tickToDateTime($currentTick);
|
||||
$oldClock->persistTick($gameStor, $currentTick);
|
||||
$gameStor->turnterm = $turnterm;
|
||||
$gameStor->starttime = $starttime;
|
||||
$gameStor->clock_base_time = TimeUtil::format(
|
||||
GameClock::baseTimeForProjection($currentDisplay, $currentTick, $turnterm),
|
||||
true,
|
||||
);
|
||||
pushGlobalHistoryLog(["<R>★</>턴시간이 <C>{$turnterm}분</>으로 변경됩니다."]);
|
||||
|
||||
if($locked){
|
||||
|
||||
@@ -215,14 +215,13 @@ class TurnExecutionHelper
|
||||
$general->rebirth();
|
||||
}
|
||||
|
||||
$turntime = addTurn($general->getTurnTime(), $gameStor->turnterm);
|
||||
$turntime = addTurn($general->getTurnTick(), $gameStor->turnterm);
|
||||
|
||||
$nextTurnTimeBase = $general->getAuxVar('nextTurnTimeBase');
|
||||
if($nextTurnTimeBase !== null){
|
||||
$turntime = cutTurn($turntime, $gameStor->turnterm);
|
||||
$turntimeObj = new \DateTimeImmutable($turntime);
|
||||
$turntimeObj = $turntimeObj->add(TimeUtil::secondsToDateInterval($nextTurnTimeBase));
|
||||
$turntime = TimeUtil::format($turntimeObj, true);
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$turntime += $clock->ticksFromSeconds($nextTurnTimeBase);
|
||||
$general->setAuxVar('nextTurnTimeBase', null);
|
||||
}
|
||||
|
||||
@@ -230,7 +229,7 @@ class TurnExecutionHelper
|
||||
}
|
||||
|
||||
|
||||
static public function executeGeneralCommandUntil(string $date, \DateTimeInterface $limitActionTime, int $year, int $month)
|
||||
static public function executeGeneralCommandUntil(int $date, \DateTimeInterface $limitActionTime, int $year, int $month)
|
||||
{
|
||||
$db = DB::db();
|
||||
$generalsTodo = $db->query(
|
||||
@@ -244,7 +243,7 @@ class TurnExecutionHelper
|
||||
$autorun_user = $gameStor->autorun_user;
|
||||
|
||||
foreach ($generalsTodo as $rawGeneral) {
|
||||
$currActionTime = new \DateTimeImmutable();
|
||||
$currActionTime = GameClock::readWallTime();
|
||||
if ($currActionTime > $limitActionTime) {
|
||||
return [true, $currentTurn];
|
||||
}
|
||||
@@ -350,7 +349,7 @@ class TurnExecutionHelper
|
||||
pullNationCommand($general->getVar('nation'), $general->getVar('officer_level'));
|
||||
pullGeneralCommand($general->getID());
|
||||
|
||||
$currentTurn = $general->getTurnTime();
|
||||
$currentTurn = $general->getTurnTick();
|
||||
$general->increaseVarWithLimit('myset', GameConst::$incDefSettingChange, null, GameConst::$maxDefSettingChange);
|
||||
|
||||
if (($autorun_user['limit_minutes'] ?? false) && $general->getNPCType() < 2 && $hasReservedTurn) {
|
||||
@@ -390,13 +389,14 @@ class TurnExecutionHelper
|
||||
return true;
|
||||
}
|
||||
|
||||
static public function executeAllCommand(&$executed = false, &$locked = false): string
|
||||
static public function executeAllCommand(&$executed = false, &$locked = false): int
|
||||
{
|
||||
$db = DB::db();
|
||||
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
|
||||
if (TimeUtil::now(true) < $gameStor->turntime) {
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
if ($clock->nowTick() < $gameStor->turntime) {
|
||||
//턴 시각 이전이면 아무것도 하지 않음
|
||||
return $gameStor->turntime;
|
||||
}
|
||||
@@ -420,7 +420,7 @@ class TurnExecutionHelper
|
||||
//접속자 수 따라서 갱신제한 변경
|
||||
CheckOverhead();
|
||||
|
||||
$date = TimeUtil::now(true);
|
||||
$date = $clock->nowTick();
|
||||
// 최종 처리 월턴의 다음 월턴시간 구함
|
||||
//$lastExecuted = $gameStor->turntime;
|
||||
$prevTurn = cutTurn($gameStor->turntime, $gameStor->turnterm);
|
||||
@@ -433,7 +433,7 @@ class TurnExecutionHelper
|
||||
$maxActionTime = max($maxActionTime * 2 / 3, $maxActionTime - 10);
|
||||
}
|
||||
|
||||
$limitActionTime = (new \DateTimeImmutable())->add(TimeUtil::secondsToDateInterval($maxActionTime));
|
||||
$limitActionTime = GameClock::readWallTime()->add(TimeUtil::secondsToDateInterval($maxActionTime));
|
||||
|
||||
// 현재 턴 이전 월턴까지 모두처리.
|
||||
//최종 처리 이후 다음 월턴이 현재 시간보다 전이라면
|
||||
|
||||
@@ -57,16 +57,16 @@ class WarUnitGeneral extends WarUnit
|
||||
$this->general->increaseRankVar(RankColumn::warnum, 1);
|
||||
|
||||
if ($this->isAttacker) {
|
||||
$semiTurn = $general->getTurnTime();
|
||||
$semiTurn = $general->getTurnTick();
|
||||
} else if ($oppose !== null) {
|
||||
$semiTurn = $oppose->getGeneral()->getTurnTime();
|
||||
$semiTurn = $oppose->getGeneral()->getTurnTick();
|
||||
} else {
|
||||
LogText("WarUnitGeneral::setOppose", "defender인데 oppose가 null {$general->getID()}, {$general->getTurnTime()}");
|
||||
$semiTurn = $general->getTurnTime();
|
||||
$semiTurn = $general->getTurnTick();
|
||||
}
|
||||
$phase = $this->getRealPhase();
|
||||
$semiTurn = substr($semiTurn, 0, strlen($semiTurn) - 2);
|
||||
$semiTurn .= sprintf("%02d", Util::valueFit($phase, 0, 99));
|
||||
$semiTurn -= $semiTurn % 100;
|
||||
$semiTurn += Util::valueFit($phase, 0, 99);
|
||||
$general->setVar('recent_war', $semiTurn);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,16 @@
|
||||
"map": {
|
||||
"mapName": "miniche",
|
||||
"targetGeneralPool": "SPoolUnderU100",
|
||||
"generalPoolAllowOption": ["stat", "ego", "picture"]
|
||||
"generalPoolAllowOption": ["stat", "ego", "picture"],
|
||||
"centennialNpcDexTargetRatio": 0.4
|
||||
},
|
||||
"history": [
|
||||
"<C>●</>180년 1월:<L><b>【100기 이벤트】</b></> 역대 장수들이 평범한 능력으로 다시 모여, 지난 전성기의 힘과 서서히 동조하기 시작했다!"
|
||||
],
|
||||
"const": {
|
||||
"npcBanMessageProb": 1
|
||||
"npcBanMessageProb":0.005,
|
||||
"defaultMaxGeneral": 800,
|
||||
"uniqueTrialCoef": 2
|
||||
},
|
||||
"events": [
|
||||
[
|
||||
@@ -26,12 +29,34 @@
|
||||
],
|
||||
[
|
||||
"month", 1000,
|
||||
["Date", "==", 181, 12],
|
||||
["ChangeCity", "occupied", {
|
||||
"pop": "+60000",
|
||||
"agri": "+1200",
|
||||
"comm": "+1200"
|
||||
}]
|
||||
["Date", "==", 181, 1],
|
||||
["RaiseNPCNation"],
|
||||
["DeleteEvent"]
|
||||
],
|
||||
[
|
||||
"destroy_nation", 1000,
|
||||
["and",
|
||||
["Date", ">=", 183, 1],
|
||||
["RemainNation", "==", 1]
|
||||
],
|
||||
["BlockScoutAction"],
|
||||
["DeleteEvent"]
|
||||
],
|
||||
[
|
||||
"month", 999,
|
||||
["Date", "==", 181, 1],
|
||||
["OpenNationBetting", 4, 5000],
|
||||
["OpenNationBetting", 1, 2000],
|
||||
["DeleteEvent"]
|
||||
],
|
||||
[
|
||||
"month", 999,
|
||||
["and",
|
||||
["Date", ">=", 183, 1],
|
||||
["RemainNation", "<=", 8]
|
||||
],
|
||||
["OpenNationBetting", 1, 1000],
|
||||
["DeleteEvent"]
|
||||
],
|
||||
[
|
||||
"destroy_nation", 1000,
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
<script>
|
||||
var hasGeneralID = <?= $generalID === null ? 'false' : 'true' ?>;
|
||||
var isCentennialAllStar = <?= $isCentennialAllStar ? 'true' : 'false' ?>;
|
||||
var defaultStatTotal = <?= GameConst::$defaultStatTotal ?>;
|
||||
var defaultStatMin = <?= GameConst::$defaultStatMin ?>;
|
||||
var defaultStatMax = <?= GameConst::$defaultStatMax ?>;
|
||||
@@ -104,6 +113,27 @@ if ($gencount >= $admin['maxgeneral']) {
|
||||
<small id="valid_until">(<span id="valid_until_text"></span>까지 유효)</small><small id="outdate_token">- 만료 -</small><br>
|
||||
<form class="card_holder">
|
||||
</form>
|
||||
<?php if ($isCentennialAllStar && $generalID !== null) : ?>
|
||||
<div id="reselect_picture_plate" class="picture_choice">
|
||||
<strong>변경 후 전콘</strong>
|
||||
<label>
|
||||
<input type="radio" name="reselect_picture_source" value="selected" checked>
|
||||
새로 선택할 장수 전콘
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="reselect_picture_source" value="current">
|
||||
<img width="32" height="32" src="<?= GetImageURL($currentGeneral['imgsvr']) ?>/<?= $currentGeneral['picture'] ?>" border="0">
|
||||
현재 장수 전콘
|
||||
</label>
|
||||
<?php if ($canUseOwnPicture) : ?>
|
||||
<label>
|
||||
<input type="radio" name="reselect_picture_source" value="own">
|
||||
<img width="32" height="32" src="<?= GetImageURL($member['imgsvr']) ?>/<?= $member['picture'] ?>" border="0">
|
||||
내 원래 전콘
|
||||
</label>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -117,7 +147,28 @@ if ($gencount >= $admin['maxgeneral']) {
|
||||
<form id='custom_form'>
|
||||
<table class='tb_layout' style='width:100%;text-align:left;'>
|
||||
<?php
|
||||
if ($admin['show_img_level'] >= 1 && $member['grade'] >= 1 && $member['picture'] != "") {
|
||||
if ($isCentennialAllStar) {
|
||||
echo "
|
||||
<tr class='event_picture'>
|
||||
<td align=right class='bg1'>전콘 선택</td>
|
||||
<td colspan=2>
|
||||
<label><input type=radio name=picture_source value=selected checked> 선택한 장수 전콘</label>
|
||||
";
|
||||
if ($canUseOwnPicture) {
|
||||
$imageTemp = GetImageURL($member['imgsvr']);
|
||||
echo "
|
||||
<label>
|
||||
<input type=radio name=picture_source value=own>
|
||||
<img width='64' height='64' src='{$imageTemp}/{$member['picture']}' border='0'>
|
||||
내 전콘
|
||||
</label>
|
||||
";
|
||||
}
|
||||
echo "
|
||||
</td>
|
||||
</tr>
|
||||
";
|
||||
} elseif ($canUseOwnPicture) {
|
||||
$imageTemp = GetImageURL($member['imgsvr']);
|
||||
echo "
|
||||
<tr class='custom_picture'>
|
||||
@@ -144,36 +195,42 @@ if ($gencount >= $admin['maxgeneral']) {
|
||||
</select> <span id="charInfoText"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class='custom_stat'>
|
||||
<td align=right class='bg1'>통솔</td>
|
||||
<td colspan=2><input type="number" name="leadership" id="leadership" value="50"></td>
|
||||
</tr>
|
||||
<tr class='custom_stat'>
|
||||
<td align=right class='bg1'>무력</td>
|
||||
<td colspan=2><input type="number" name="strength" id="strength" value="50"></td>
|
||||
</tr>
|
||||
<tr class='custom_stat'>
|
||||
<td align=right class='bg1'>지력</td>
|
||||
<td colspan=2><input type="number" name="intel" id="intel" value="50"></td>
|
||||
</tr>
|
||||
<tr class='custom_stat'>
|
||||
<td align=right class='bg1'>능력치 조정</td>
|
||||
<td colspan=2>
|
||||
<input type=button value=랜덤형 onclick=abilityRand()>
|
||||
<input type=button value=통솔무력형 onclick=abilityLeadpow()>
|
||||
<input type=button value=통솔지력형 onclick=abilityLeadint()>
|
||||
<input type=button value=무력지력형 onclick=abilityPowint()>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class='custom_stat'>
|
||||
<td align=center colspan=3>
|
||||
<font color=orange>모든 능력치는 ( <?= GameConst::$defaultStatMin ?> <= 능력치 <=<?= GameConst::$defaultStatMax ?> ) 사이로 잡으셔야 합니다.<br>
|
||||
그 외의 능력치는 가입되지 않습니다.</font>
|
||||
</td>
|
||||
</tr>
|
||||
<?php if (!$isCentennialAllStar) : ?>
|
||||
<tr class='custom_stat'>
|
||||
<td align=right class='bg1'>통솔</td>
|
||||
<td colspan=2><input type="number" name="leadership" id="leadership" value="50"></td>
|
||||
</tr>
|
||||
<tr class='custom_stat'>
|
||||
<td align=right class='bg1'>무력</td>
|
||||
<td colspan=2><input type="number" name="strength" id="strength" value="50"></td>
|
||||
</tr>
|
||||
<tr class='custom_stat'>
|
||||
<td align=right class='bg1'>지력</td>
|
||||
<td colspan=2><input type="number" name="intel" id="intel" value="50"></td>
|
||||
</tr>
|
||||
<tr class='custom_stat'>
|
||||
<td align=right class='bg1'>능력치 조정</td>
|
||||
<td colspan=2>
|
||||
<input type=button value=랜덤형 onclick=abilityRand()>
|
||||
<input type=button value=통솔무력형 onclick=abilityLeadpow()>
|
||||
<input type=button value=통솔지력형 onclick=abilityLeadint()>
|
||||
<input type=button value=무력지력형 onclick=abilityPowint()>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class='custom_stat'>
|
||||
<td align=center colspan=3>
|
||||
<font color=orange>모든 능력치는 ( <?= GameConst::$defaultStatMin ?> <= 능력치 <=<?= GameConst::$defaultStatMax ?> ) 사이로 잡으셔야 합니다.<br>
|
||||
그 외의 능력치는 가입되지 않습니다.</font>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<tr>
|
||||
<td align=center colspan=3>
|
||||
<span class='custom_stat'>능력치의 총합은 <?= GameConst::$defaultStatTotal ?>입니다. 가입후 0~10의 능력치 보너스를 받게 됩니다.<br></span>
|
||||
<?php if ($isCentennialAllStar) : ?>
|
||||
선택한 장수의 최종 능력치 비율을 반영한 약화 능력치로 시작합니다.<br>
|
||||
<?php else : ?>
|
||||
<span class='custom_stat'>능력치의 총합은 <?= GameConst::$defaultStatTotal ?>입니다. 가입후 0~10의 능력치 보너스를 받게 됩니다.<br></span>
|
||||
<?php endif; ?>
|
||||
임의의 도시에서 재야로 시작하며 건국과 임관은 게임 내에서 실행합니다.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -194,4 +251,4 @@ if ($gencount >= $admin['maxgeneral']) {
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
+12
-12
@@ -45,8 +45,8 @@ CREATE TABLE `general` (
|
||||
`book` VARCHAR(20) NOT NULL DEFAULT 'None',
|
||||
`horse` VARCHAR(20) NOT NULL DEFAULT 'None',
|
||||
`item` VARCHAR(20) NOT NULL DEFAULT 'None',
|
||||
`turntime` DATETIME(6) NOT NULL,
|
||||
`recent_war` DATETIME(6) NULL DEFAULT NULL,
|
||||
`turntime` BIGINT NOT NULL,
|
||||
`recent_war` BIGINT NULL DEFAULT NULL,
|
||||
`makelimit` INT(2) NULL DEFAULT '0',
|
||||
`killturn` INT(3) NULL DEFAULT NULL,
|
||||
`block` INT(1) NULL DEFAULT '0',
|
||||
@@ -97,7 +97,7 @@ CREATE TABLE `general_access_log` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`general_id` INT(11) NOT NULL,
|
||||
`user_id` INT(11) NULL DEFAULT NULL,
|
||||
`last_refresh` DATETIME NULL DEFAULT NULL,
|
||||
`last_refresh` BIGINT NULL DEFAULT NULL,
|
||||
`refresh` INT(11) NOT NULL DEFAULT '0',
|
||||
`refresh_total` INT(11) NOT NULL DEFAULT '0',
|
||||
`refresh_score` INT(11) NOT NULL DEFAULT '0',
|
||||
@@ -244,8 +244,8 @@ CREATE TABLE `message` (
|
||||
`type` ENUM('private', 'national', 'public', 'diplomacy') NOT NULL,
|
||||
`src` INT(11) NOT NULL,
|
||||
`dest` INT(11) NOT NULL,
|
||||
`time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`valid_until` DATETIME NOT NULL DEFAULT '9999-12-31 23:59:59',
|
||||
`time` BIGINT NOT NULL,
|
||||
`valid_until` BIGINT NOT NULL,
|
||||
`message` TEXT NOT NULL COLLATE 'utf8mb4_bin',
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `by_mailbox` (`mailbox`, `type`, `id`),
|
||||
@@ -536,8 +536,8 @@ CREATE TABLE IF NOT EXISTS `reserved_open` (
|
||||
CREATE TABLE `select_npc_token` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`owner` INT(11) NOT NULL,
|
||||
`valid_until` DATETIME NOT NULL,
|
||||
`pick_more_from` DATETIME NOT NULL,
|
||||
`valid_until` BIGINT NOT NULL,
|
||||
`pick_more_from` BIGINT NOT NULL,
|
||||
`pick_result` TEXT NOT NULL COLLATE 'utf8mb4_bin',
|
||||
`nonce` INT(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
@@ -553,7 +553,7 @@ CREATE TABLE `select_pool` (
|
||||
`unique_name` VARCHAR(20) NOT NULL,
|
||||
`owner` INT(11) NULL DEFAULT NULL,
|
||||
`general_id` INT(11) NULL DEFAULT NULL,
|
||||
`reserved_until` DATETIME NULL DEFAULT NULL,
|
||||
`reserved_until` BIGINT NULL DEFAULT NULL,
|
||||
`info` TEXT NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE INDEX `unique_name` (`unique_name`),
|
||||
@@ -694,11 +694,11 @@ CREATE TABLE `ng_auction` (
|
||||
`target` VARCHAR(50) NULL DEFAULT NULL COLLATE 'utf8mb4_bin',
|
||||
`host_general_id` INT(11) NOT NULL,
|
||||
`req_resource` ENUM('gold','rice','inheritPoint') NOT NULL COLLATE 'utf8mb4_bin',
|
||||
`open_date` DATETIME NOT NULL,
|
||||
`close_date` DATETIME NOT NULL,
|
||||
`open_tick` BIGINT NOT NULL,
|
||||
`close_tick` BIGINT NOT NULL,
|
||||
`detail` LONGTEXT NOT NULL COLLATE 'utf8mb4_bin',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
INDEX `by_close` (`finished`, `type`, `close_date`) USING BTREE,
|
||||
INDEX `by_close` (`finished`, `type`, `close_tick`) USING BTREE,
|
||||
INDEX `by_general_id` (`host_general_id`, `type`, `finished`) USING BTREE,
|
||||
CONSTRAINT `detail` CHECK (json_valid(`detail`))
|
||||
)
|
||||
@@ -721,4 +721,4 @@ CREATE TABLE `ng_auction_bid` (
|
||||
CONSTRAINT `aux` CHECK (json_valid(`aux`))
|
||||
)
|
||||
COLLATE='utf8mb4_general_ci'
|
||||
ENGINE = Aria;
|
||||
ENGINE = Aria;
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
<BButton class="col-6 offset-6" variant="primary" @click="checkOwner"> 소유자 찾기 </BButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-lg-4 col-sm-6 col-12 py-2">
|
||||
<div v-if="canResetStat" class="col col-lg-4 col-sm-6 col-12 py-2">
|
||||
<div class="row px-4">
|
||||
<div class="a-right col-6 align-self-center">능력치 초기화</div>
|
||||
<div class="col-6">
|
||||
@@ -222,6 +222,17 @@
|
||||
<BButton class="col-6 offset-6" variant="primary" @click="resetStat"> 능력치 초기화</BButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="col col-lg-4 col-sm-6 col-12 py-2">
|
||||
<div class="row px-4">
|
||||
<div class="a-right col-6 align-self-center">능력치 초기화</div>
|
||||
<div class="col-6 align-self-center">사용 불가</div>
|
||||
</div>
|
||||
<div class="a-right">
|
||||
<small class="form-text text-muted">
|
||||
100기 올스타 장수는 장수 전환 시 능력치 성장 기록을 보존하기 위해 능력치 초기화를 사용할 수 없습니다.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
@@ -297,6 +308,7 @@ declare const staticValues: {
|
||||
}
|
||||
>;
|
||||
availableTargetGeneral: Record<number, string>;
|
||||
canResetStat: boolean;
|
||||
currentStat: {
|
||||
leadership: number;
|
||||
strength: number;
|
||||
@@ -447,6 +459,7 @@ const {
|
||||
availableSpecialWar,
|
||||
availableUnique,
|
||||
availableTargetGeneral,
|
||||
canResetStat,
|
||||
currentStat
|
||||
} = staticValues;
|
||||
|
||||
@@ -739,4 +752,4 @@ async function getMoreLog(): Promise<void> {
|
||||
.tnum {
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -32,6 +32,10 @@ type CardItem = {
|
||||
leadership?: number,
|
||||
strength?: number,
|
||||
intel?: number,
|
||||
selectionStatLabel?: string,
|
||||
selectionLeadership?: number,
|
||||
selectionStrength?: number,
|
||||
selectionIntel?: number,
|
||||
dex?: number[],
|
||||
}
|
||||
|
||||
@@ -42,7 +46,8 @@ type GeneralPoolResponse = {
|
||||
}
|
||||
|
||||
declare const characterInfo: Record<string, { name: string, info: string }>;
|
||||
declare const hasGeneralID: number;
|
||||
declare const hasGeneralID: number;
|
||||
declare const isCentennialAllStar: boolean;
|
||||
declare let currentGeneralInfo: CardItem | undefined;
|
||||
declare const cards: Record<string, CardItem>;
|
||||
declare const validCustomOption: string[];
|
||||
@@ -51,9 +56,13 @@ const templateGeneralCard = '<div class="general_card">\
|
||||
<h4 class="bg1 with_border"><%generalName%></h4>\
|
||||
<h4><img src="<%iconPath%>" height=64 width=64></h4><p>\
|
||||
<%if(event100Growth){%><b>195년 최종 동조 목표</b><br><%}%>\
|
||||
<%if(leadership){%>\
|
||||
<%leadership%> / <%strength%> / <%intel%><br>\
|
||||
<%}%>\
|
||||
<%if(leadership){%>\
|
||||
<%leadership%> / <%strength%> / <%intel%><br>\
|
||||
<%}%>\
|
||||
<%if(selectionStatLabel){%>\
|
||||
<b><%selectionStatLabel%></b><br>\
|
||||
<%selectionLeadership%> / <%selectionStrength%> / <%selectionIntel%><br>\
|
||||
<%}%>\
|
||||
<%if(personalText){%><%personalText%><br><%}%>\
|
||||
<%if(specialDomesticText||specialWarText){%>\
|
||||
<%specialDomesticText%> / <%specialWarText%><br>\
|
||||
@@ -107,9 +116,12 @@ async function pickGeneral(this: HTMLElement, e: JQuery.Event) {
|
||||
url: 'j_update_picked_general.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
pick: unwrap_any<string>($btn.val())
|
||||
})
|
||||
data: convertFormData({
|
||||
pick: unwrap_any<string>($btn.val()),
|
||||
picture_source: unwrap_any<string>(
|
||||
$('input[name="reselect_picture_source"]:checked').val() ?? 'selected'
|
||||
)
|
||||
})
|
||||
});
|
||||
result = response.data;
|
||||
if (!result.result) {
|
||||
@@ -138,23 +150,25 @@ async function buildGeneral(e: JQuery.Event) {
|
||||
|
||||
let result: InvalidResponse;
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_select_picked_general.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
pick: unwrap(currentGeneralInfo).uniqueName,
|
||||
use_own_picture: $('#use_own_picture').is(':checked'),
|
||||
leadership: parseInt(unwrap_any<string>($('#leadership').val())),
|
||||
strength: parseInt(unwrap_any<string>(
|
||||
$(unwrap(currentGeneralInfo).event100Growth ? '#strength' : '#leadership').val()
|
||||
)),
|
||||
intel: parseInt(unwrap_any<string>(
|
||||
$(unwrap(currentGeneralInfo).event100Growth ? '#intel' : '#leadership').val()
|
||||
)),
|
||||
personal: unwrap_any<string>($('#selChar').val())
|
||||
})
|
||||
})
|
||||
const formData: Record<string, string|number|boolean> = {
|
||||
pick: unwrap(currentGeneralInfo).uniqueName,
|
||||
use_own_picture: $('#use_own_picture').is(':checked'),
|
||||
picture_source: unwrap_any<string>(
|
||||
$('input[name="picture_source"]:checked').val() ?? 'selected'
|
||||
),
|
||||
personal: unwrap_any<string>($('#selChar').val())
|
||||
};
|
||||
if (!isCentennialAllStar) {
|
||||
formData.leadership = parseInt(unwrap_any<string>($('#leadership').val()));
|
||||
formData.strength = parseInt(unwrap_any<string>($('#leadership').val()));
|
||||
formData.intel = parseInt(unwrap_any<string>($('#leadership').val()));
|
||||
}
|
||||
const response = await axios({
|
||||
url: 'j_select_picked_general.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData(formData)
|
||||
})
|
||||
result = response.data;
|
||||
if (!result.result) {
|
||||
throw result.reason;
|
||||
@@ -200,9 +214,13 @@ function printGenerals(value: GeneralPoolResponse) {
|
||||
|
||||
const emptyCard = {
|
||||
'leadership': null,
|
||||
'strength': null,
|
||||
'intel': null,
|
||||
'personalText': null,
|
||||
'strength': null,
|
||||
'intel': null,
|
||||
'selectionStatLabel': null,
|
||||
'selectionLeadership': null,
|
||||
'selectionStrength': null,
|
||||
'selectionIntel': null,
|
||||
'personalText': null,
|
||||
'specialDomesticText': null,
|
||||
'specialWarText': null,
|
||||
'dex': null
|
||||
|
||||
@@ -105,6 +105,7 @@ $lastInheritPointLogs = $db->query('SELECT id, server_id, year, month, date, tex
|
||||
'availableUnique' => $availableUnique,
|
||||
'lastInheritPointLogs' => $lastInheritPointLogs,
|
||||
'availableTargetGeneral' => $availableTargetGeneral,
|
||||
'canResetStat' => CentennialAllStarGrowthService::isStatResetAllowed(),
|
||||
'currentStat' => [
|
||||
'leadership' => Util::clamp($me->getVar('leadership'), GameConst::$defaultStatMin, GameConst::$defaultStatMax),
|
||||
'strength' => Util::clamp($me->getVar('strength'), GameConst::$defaultStatMin, GameConst::$defaultStatMax),
|
||||
@@ -120,4 +121,4 @@ $lastInheritPointLogs = $db->query('SELECT id, server_id, year, month, date, tex
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# 논리 game clock 운영
|
||||
|
||||
게임 진행 시각은 `36,000,000 tick × 완료 턴 + 현재 세부 tick`으로 저장합니다.
|
||||
표시 시각만 `game_env.clock_base_time`을 기준으로 달력 시각으로 투영합니다.
|
||||
|
||||
## 기존 DB migration
|
||||
|
||||
웹과 턴 daemon을 먼저 중지하고 HWE DB SQL dump를 만든 다음 상태를 확인합니다.
|
||||
|
||||
```bash
|
||||
php scripts/migrate-game-clock.php --status
|
||||
php scripts/migrate-game-clock.php --apply --backup=/absolute/path/to/hwe-before-clock.sql
|
||||
```
|
||||
|
||||
적용 명령은 비어 있지 않은 절대경로 backup과 `GAME` lock을 요구합니다. Aria
|
||||
DDL은 transaction rollback이 되지 않으므로 기존 날짜 컬럼과 `game_env` 값은
|
||||
`*_wall_backup`으로 남깁니다. 새 코드 검증이 끝나기 전에는 이 값을 제거하지
|
||||
마세요. 복구할 때는 PHP/daemon을 중지하고 명령에 지정했던 전체 SQL dump를
|
||||
복원하는 것이 기준 절차입니다.
|
||||
|
||||
## 시계 조회와 전진
|
||||
|
||||
```bash
|
||||
php scripts/game-clock.php --status
|
||||
php scripts/game-clock.php --advance-turns=12 --apply
|
||||
php scripts/game-clock.php --advance-ticks=36000000 --apply
|
||||
php scripts/game-clock.php --mode=realtime --apply
|
||||
```
|
||||
|
||||
명시적으로 전진하면 시계는 `manual` 모드가 됩니다. 이 모드의 엔진은 실제
|
||||
시각을 읽지 않습니다. `realtime`으로 전환할 때 현재 논리 tick을 새 벽시계
|
||||
anchor에 고정하므로 표시 시각이 튀지 않습니다.
|
||||
|
||||
격리 DB에서 manual clock과 엔진 진행을 함께 확인할 수 있습니다.
|
||||
|
||||
```bash
|
||||
php scripts/verify-game-clock-engine.php --apply --engine-calls=2
|
||||
```
|
||||
|
||||
이 검증기는 manual mode만 허용하고, 엔진 호출 전후 clock tick이 벽시계 때문에
|
||||
변하지 않았는지와 마지막 처리 tick이 현재 tick을 넘지 않았는지 검사합니다.
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use sammo\DB;
|
||||
use sammo\GameClock;
|
||||
use sammo\KVStorage;
|
||||
use sammo\Util;
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$_SERVER['REMOTE_ADDR'] ??= '127.0.0.1';
|
||||
$_SERVER['REQUEST_URI'] ??= '/cli/game-clock';
|
||||
|
||||
require dirname(__DIR__) . '/hwe/lib.php';
|
||||
require dirname(__DIR__) . '/hwe/func.php';
|
||||
|
||||
/** @return never */
|
||||
function gameClockUsage(int $exitCode = 0): void
|
||||
{
|
||||
$stream = $exitCode === 0 ? STDOUT : STDERR;
|
||||
fwrite($stream, <<<'TEXT'
|
||||
Usage:
|
||||
php scripts/game-clock.php --status
|
||||
php scripts/game-clock.php --mode=manual|realtime --apply
|
||||
php scripts/game-clock.php --advance-turns=N --apply
|
||||
php scripts/game-clock.php --advance-ticks=N --apply
|
||||
|
||||
Mutation commands require --apply and the GAME lock. Explicit advancement
|
||||
switches the clock to manual mode, so subsequent engine runs never consult the
|
||||
wall clock. Use --mode=realtime to resume wall-clock-paced progression.
|
||||
TEXT);
|
||||
exit($exitCode);
|
||||
}
|
||||
|
||||
/** @return int */
|
||||
function signedIntegerOption(array $options, string $name): int
|
||||
{
|
||||
$value = $options[$name] ?? null;
|
||||
if (!is_string($value) || preg_match('/^-?[0-9]+$/', $value) !== 1) {
|
||||
throw new InvalidArgumentException("--{$name}에는 정수를 지정해야 합니다.");
|
||||
}
|
||||
$result = filter_var($value, FILTER_VALIDATE_INT);
|
||||
if ($result === false) {
|
||||
throw new InvalidArgumentException("--{$name} 값이 INT64 범위를 벗어났습니다.");
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
function printGameClockState(KVStorage $gameStor): void
|
||||
{
|
||||
$gameStor->resetCache();
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$nowTick = $clock->nowTick();
|
||||
$state = $gameStor->getValues(['year', 'month', 'turntime', 'turnterm']);
|
||||
printf(
|
||||
"mode=%s game=%d-%02d now_tick=%d now=%s last_tick=%d last=%s turnterm=%dm ticks_per_turn=%d ticks_per_second=%d\n",
|
||||
$clock->getMode(),
|
||||
Util::toInt($state['year']),
|
||||
Util::toInt($state['month']),
|
||||
$nowTick,
|
||||
$clock->formatTick($nowTick, true),
|
||||
Util::toInt($state['turntime']),
|
||||
$clock->formatTick(Util::toInt($state['turntime']), true),
|
||||
Util::toInt($state['turnterm']),
|
||||
GameClock::TICKS_PER_TURN,
|
||||
$clock->ticksPerSecond(),
|
||||
);
|
||||
}
|
||||
|
||||
$options = getopt('', ['help', 'status', 'mode:', 'advance-turns:', 'advance-ticks:', 'apply']);
|
||||
if (isset($options['help'])) {
|
||||
gameClockUsage();
|
||||
}
|
||||
|
||||
$commands = array_filter([
|
||||
'status' => isset($options['status']),
|
||||
'mode' => array_key_exists('mode', $options),
|
||||
'advance-turns' => array_key_exists('advance-turns', $options),
|
||||
'advance-ticks' => array_key_exists('advance-ticks', $options),
|
||||
]);
|
||||
if (count($commands) !== 1) {
|
||||
gameClockUsage(2);
|
||||
}
|
||||
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
|
||||
if (isset($commands['status'])) {
|
||||
printGameClockState($gameStor);
|
||||
exit(0);
|
||||
}
|
||||
if (!isset($options['apply'])) {
|
||||
fwrite(STDERR, "변경 명령에는 --apply가 필요합니다.\n");
|
||||
exit(2);
|
||||
}
|
||||
if (!\sammo\tryLock()) {
|
||||
fwrite(STDERR, "GAME lock을 획득하지 못했습니다.\n");
|
||||
exit(3);
|
||||
}
|
||||
|
||||
try {
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
$currentTick = $clock->nowTick();
|
||||
|
||||
if (isset($commands['mode'])) {
|
||||
$mode = (string)$options['mode'];
|
||||
if (!in_array($mode, [GameClock::MODE_MANUAL, GameClock::MODE_REALTIME], true)) {
|
||||
throw new InvalidArgumentException('--mode는 manual 또는 realtime이어야 합니다.');
|
||||
}
|
||||
$clock->persistTick($gameStor, $currentTick, $mode);
|
||||
} else {
|
||||
if (isset($commands['advance-turns'])) {
|
||||
$nextTick = $clock->addTurns($currentTick, signedIntegerOption($options, 'advance-turns'));
|
||||
} else {
|
||||
$nextTick = GameClock::addTicks($currentTick, signedIntegerOption($options, 'advance-ticks'));
|
||||
}
|
||||
$clock->persistTick($gameStor, $nextTick, GameClock::MODE_MANUAL);
|
||||
}
|
||||
} finally {
|
||||
$gameStor->resetCache();
|
||||
\sammo\unlock();
|
||||
}
|
||||
|
||||
printGameClockState($gameStor);
|
||||
@@ -0,0 +1,340 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use sammo\DB;
|
||||
use sammo\GameClock;
|
||||
use sammo\Json;
|
||||
use sammo\KVStorage;
|
||||
use sammo\Util;
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$_SERVER['REMOTE_ADDR'] ??= '127.0.0.1';
|
||||
$_SERVER['REQUEST_URI'] ??= '/cli/migrate-game-clock';
|
||||
|
||||
require dirname(__DIR__) . '/hwe/lib.php';
|
||||
require dirname(__DIR__) . '/hwe/func.php';
|
||||
|
||||
/** @return never */
|
||||
function migrationUsage(int $exitCode = 0): void
|
||||
{
|
||||
$stream = $exitCode === 0 ? STDOUT : STDERR;
|
||||
fwrite($stream, <<<'TEXT'
|
||||
Usage:
|
||||
php scripts/migrate-game-clock.php --status
|
||||
php scripts/migrate-game-clock.php --apply --backup=/absolute/path/to/pre-migration.sql
|
||||
|
||||
--status is read-only. --apply requires a pre-existing, non-empty SQL backup and
|
||||
the GAME lock. Original DATETIME columns and game_env values are retained with
|
||||
the *_wall_backup suffix so recovery does not depend only on reverse arithmetic.
|
||||
Stop web/daemon traffic before applying; Aria DDL is not transactional.
|
||||
TEXT);
|
||||
exit($exitCode);
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
function tableColumnTypes(\MeekroDB $db, string $table): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($db->query("SHOW COLUMNS FROM %b", $table) as $column) {
|
||||
$result[(string)$column['Field']] = strtolower((string)$column['Type']);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
function isDateColumn(?string $type): bool
|
||||
{
|
||||
return $type !== null && str_starts_with($type, 'datetime');
|
||||
}
|
||||
|
||||
function isBigIntColumn(?string $type): bool
|
||||
{
|
||||
return $type !== null && str_starts_with($type, 'bigint');
|
||||
}
|
||||
|
||||
/** @return array{state:string,details:array<string,array<string,string>>} */
|
||||
function inspectMigration(\MeekroDB $db): array
|
||||
{
|
||||
$details = [];
|
||||
foreach (['general', 'general_access_log', 'message', 'select_npc_token', 'select_pool', 'ng_auction'] as $table) {
|
||||
$details[$table] = tableColumnTypes($db, $table);
|
||||
}
|
||||
|
||||
$old = isDateColumn($details['general']['turntime'] ?? null)
|
||||
&& isDateColumn($details['general']['recent_war'] ?? null)
|
||||
&& isDateColumn($details['general_access_log']['last_refresh'] ?? null)
|
||||
&& isDateColumn($details['message']['time'] ?? null)
|
||||
&& isDateColumn($details['message']['valid_until'] ?? null)
|
||||
&& isDateColumn($details['select_npc_token']['valid_until'] ?? null)
|
||||
&& isDateColumn($details['select_npc_token']['pick_more_from'] ?? null)
|
||||
&& isDateColumn($details['select_pool']['reserved_until'] ?? null)
|
||||
&& isDateColumn($details['ng_auction']['open_date'] ?? null)
|
||||
&& isDateColumn($details['ng_auction']['close_date'] ?? null);
|
||||
$new = isBigIntColumn($details['general']['turntime'] ?? null)
|
||||
&& isBigIntColumn($details['general']['recent_war'] ?? null)
|
||||
&& isBigIntColumn($details['general_access_log']['last_refresh'] ?? null)
|
||||
&& isBigIntColumn($details['message']['time'] ?? null)
|
||||
&& isBigIntColumn($details['message']['valid_until'] ?? null)
|
||||
&& isBigIntColumn($details['select_npc_token']['valid_until'] ?? null)
|
||||
&& isBigIntColumn($details['select_npc_token']['pick_more_from'] ?? null)
|
||||
&& isBigIntColumn($details['select_pool']['reserved_until'] ?? null)
|
||||
&& isBigIntColumn($details['ng_auction']['open_tick'] ?? null)
|
||||
&& isBigIntColumn($details['ng_auction']['close_tick'] ?? null);
|
||||
|
||||
return [
|
||||
'state' => $old ? 'legacy' : ($new ? 'tick' : 'partial-or-unknown'),
|
||||
'details' => $details,
|
||||
];
|
||||
}
|
||||
|
||||
function printMigrationStatus(\MeekroDB $db): string
|
||||
{
|
||||
$inspection = inspectMigration($db);
|
||||
printf("schema_state=%s\n", $inspection['state']);
|
||||
foreach ($inspection['details'] as $table => $columns) {
|
||||
$interesting = array_filter(
|
||||
$columns,
|
||||
static fn (string $name): bool => preg_match(
|
||||
'/^(turntime|recent_war|last_refresh|time|valid_until|pick_more_from|reserved_until|open_date|close_date|open_tick|close_tick|.*_wall_backup)$/',
|
||||
$name,
|
||||
) === 1,
|
||||
ARRAY_FILTER_USE_KEY,
|
||||
);
|
||||
printf("%s %s\n", $table, Json::encode($interesting));
|
||||
}
|
||||
return $inspection['state'];
|
||||
}
|
||||
|
||||
/** Convert a DATETIME column into a staged BIGINT tick column. */
|
||||
function fillTickColumn(
|
||||
\MeekroDB $db,
|
||||
string $table,
|
||||
string $dateColumn,
|
||||
string $tickColumn,
|
||||
string $baseTime,
|
||||
int $ticksPerSecond,
|
||||
bool $nullable,
|
||||
): void {
|
||||
$nullSql = $nullable ? ' NULL DEFAULT NULL' : ' NOT NULL';
|
||||
$db->query("ALTER TABLE %b ADD COLUMN %b BIGINT{$nullSql}", $table, $tickColumn);
|
||||
$db->query(
|
||||
"UPDATE %b SET %b = (TIMESTAMPDIFF(MICROSECOND, %s, %b) * %i) DIV 1000000",
|
||||
$table,
|
||||
$tickColumn,
|
||||
$baseTime,
|
||||
$dateColumn,
|
||||
$ticksPerSecond,
|
||||
);
|
||||
$missing = Util::toInt($db->queryFirstField(
|
||||
"SELECT COUNT(*) FROM %b WHERE %b IS NOT NULL AND %b IS NULL",
|
||||
$table,
|
||||
$dateColumn,
|
||||
$tickColumn,
|
||||
));
|
||||
if ($missing !== 0) {
|
||||
throw new RuntimeException("{$table}.{$tickColumn} 변환 누락: {$missing}");
|
||||
}
|
||||
}
|
||||
|
||||
function assertSafeTickColumn(\MeekroDB $db, string $table, string $column): void
|
||||
{
|
||||
$unsafe = Util::toInt($db->queryFirstField(
|
||||
'SELECT COUNT(*) FROM %b WHERE %b > %i OR %b < %i',
|
||||
$table,
|
||||
$column,
|
||||
GameClock::MAX_SAFE_TICK,
|
||||
$column,
|
||||
-GameClock::MAX_SAFE_TICK,
|
||||
));
|
||||
if ($unsafe !== 0) {
|
||||
throw new RuntimeException("{$table}.{$column} JavaScript safe integer 초과: {$unsafe}");
|
||||
}
|
||||
}
|
||||
|
||||
$options = getopt('', ['help', 'status', 'apply', 'backup:']);
|
||||
if (isset($options['help'])) {
|
||||
migrationUsage();
|
||||
}
|
||||
if (isset($options['status']) === isset($options['apply'])) {
|
||||
migrationUsage(2);
|
||||
}
|
||||
|
||||
$db = DB::db();
|
||||
if (isset($options['status'])) {
|
||||
exit(printMigrationStatus($db) === 'partial-or-unknown' ? 2 : 0);
|
||||
}
|
||||
|
||||
$backup = $options['backup'] ?? null;
|
||||
if (!is_string($backup) || $backup === '' || $backup[0] !== '/' || !is_file($backup) || filesize($backup) === 0) {
|
||||
fwrite(STDERR, "--backup에는 적용 직전에 만든 비어 있지 않은 절대경로 SQL 백업을 지정해야 합니다.\n");
|
||||
exit(2);
|
||||
}
|
||||
if (inspectMigration($db)['state'] !== 'legacy') {
|
||||
fwrite(STDERR, "legacy 스키마가 아니므로 적용하지 않습니다. --status 결과를 확인해 주세요.\n");
|
||||
exit(2);
|
||||
}
|
||||
if (!\sammo\tryLock()) {
|
||||
fwrite(STDERR, "GAME lock을 획득하지 못했습니다.\n");
|
||||
exit(3);
|
||||
}
|
||||
|
||||
try {
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$legacy = $gameStor->getValues(['starttime', 'turntime', 'opentime', 'tnmt_time', 'turnterm']);
|
||||
$turnTerm = Util::toInt($legacy['turnterm']);
|
||||
$baseTime = new DateTimeImmutable((string)$legacy['starttime']);
|
||||
$wallNow = GameClock::readWallTime();
|
||||
$conversionClock = new GameClock(
|
||||
$baseTime,
|
||||
$turnTerm,
|
||||
0,
|
||||
GameClock::MODE_REALTIME,
|
||||
$wallNow,
|
||||
static fn (): DateTimeImmutable => $wallNow,
|
||||
);
|
||||
$baseTimeString = $baseTime->format('Y-m-d H:i:s.u');
|
||||
$ticksPerSecond = $conversionClock->ticksPerSecond();
|
||||
|
||||
fillTickColumn($db, 'general', 'turntime', 'turntime_game_tick', $baseTimeString, $ticksPerSecond, false);
|
||||
fillTickColumn($db, 'general', 'recent_war', 'recent_war_game_tick', $baseTimeString, $ticksPerSecond, true);
|
||||
fillTickColumn($db, 'general_access_log', 'last_refresh', 'last_refresh_game_tick', $baseTimeString, $ticksPerSecond, true);
|
||||
fillTickColumn($db, 'message', 'time', 'time_game_tick', $baseTimeString, $ticksPerSecond, false);
|
||||
fillTickColumn($db, 'message', 'valid_until', 'valid_until_game_tick', $baseTimeString, $ticksPerSecond, false);
|
||||
$db->query(
|
||||
'UPDATE message SET valid_until_game_tick = %i WHERE YEAR(valid_until) >= 9000',
|
||||
GameClock::MAX_SAFE_TICK,
|
||||
);
|
||||
fillTickColumn($db, 'select_npc_token', 'valid_until', 'valid_until_game_tick', $baseTimeString, $ticksPerSecond, false);
|
||||
fillTickColumn($db, 'select_npc_token', 'pick_more_from', 'pick_more_from_game_tick', $baseTimeString, $ticksPerSecond, false);
|
||||
fillTickColumn($db, 'select_pool', 'reserved_until', 'reserved_until_game_tick', $baseTimeString, $ticksPerSecond, true);
|
||||
fillTickColumn($db, 'ng_auction', 'open_date', 'open_game_tick', $baseTimeString, $ticksPerSecond, false);
|
||||
fillTickColumn($db, 'ng_auction', 'close_date', 'close_game_tick', $baseTimeString, $ticksPerSecond, false);
|
||||
foreach ([
|
||||
['general', 'turntime_game_tick'],
|
||||
['general', 'recent_war_game_tick'],
|
||||
['general_access_log', 'last_refresh_game_tick'],
|
||||
['message', 'time_game_tick'],
|
||||
['message', 'valid_until_game_tick'],
|
||||
['select_npc_token', 'valid_until_game_tick'],
|
||||
['select_npc_token', 'pick_more_from_game_tick'],
|
||||
['select_pool', 'reserved_until_game_tick'],
|
||||
['ng_auction', 'open_game_tick'],
|
||||
['ng_auction', 'close_game_tick'],
|
||||
] as [$table, $column]) {
|
||||
assertSafeTickColumn($db, $table, $column);
|
||||
}
|
||||
|
||||
foreach ($db->query('SELECT no, aux FROM general') as $row) {
|
||||
$aux = Json::decode((string)$row['aux']);
|
||||
$nextChange = $aux['next_change'] ?? null;
|
||||
if (is_string($nextChange) && !ctype_digit(ltrim($nextChange, '-'))) {
|
||||
$aux['next_change_wall_backup'] = $nextChange;
|
||||
$aux['next_change'] = $conversionClock->dateTimeToTick(new DateTimeImmutable($nextChange));
|
||||
$db->update('general', ['aux' => Json::encode($aux)], 'no = %i', $row['no']);
|
||||
}
|
||||
}
|
||||
foreach ($db->query('SELECT id, detail FROM ng_auction') as $row) {
|
||||
$detail = Json::decode((string)$row['detail']);
|
||||
$legacyLimit = $detail['availableLatestBidCloseDate'] ?? null;
|
||||
if (is_string($legacyLimit) && $legacyLimit !== '') {
|
||||
$detail['availableLatestBidCloseDateWallBackup'] = $legacyLimit;
|
||||
$detail['availableLatestBidCloseTick'] = $conversionClock->dateTimeToTick(new DateTimeImmutable($legacyLimit));
|
||||
} else {
|
||||
$detail['availableLatestBidCloseTick'] = null;
|
||||
}
|
||||
unset($detail['availableLatestBidCloseDate']);
|
||||
$db->update('ng_auction', ['detail' => Json::encode($detail)], 'id = %i', $row['id']);
|
||||
}
|
||||
foreach ($db->query('SELECT namespace, value FROM nation_env WHERE `key` = %s', 'last천도Trial') as $row) {
|
||||
$trial = Json::decode((string)$row['value']);
|
||||
if (!is_array($trial) || !isset($trial[1]) || !is_string($trial[1]) || ctype_digit(ltrim($trial[1], '-'))) {
|
||||
continue;
|
||||
}
|
||||
$db->insertUpdate('nation_env', [
|
||||
'namespace' => $row['namespace'],
|
||||
'key' => 'last천도Trial_wall_backup',
|
||||
'value' => Json::encode($trial),
|
||||
]);
|
||||
$trial[1] = $conversionClock->dateTimeToTick(new DateTimeImmutable($trial[1]));
|
||||
$db->update(
|
||||
'nation_env',
|
||||
['value' => Json::encode($trial)],
|
||||
'namespace = %i AND `key` = %s',
|
||||
$row['namespace'],
|
||||
'last천도Trial',
|
||||
);
|
||||
}
|
||||
|
||||
$db->query(
|
||||
'ALTER TABLE general '
|
||||
. 'DROP INDEX turntime, DROP INDEX troop, '
|
||||
. 'CHANGE turntime turntime_wall_backup DATETIME(6) NULL DEFAULT NULL, '
|
||||
. 'CHANGE recent_war recent_war_wall_backup DATETIME(6) NULL DEFAULT NULL, '
|
||||
. 'CHANGE turntime_game_tick turntime BIGINT NOT NULL, '
|
||||
. 'CHANGE recent_war_game_tick recent_war BIGINT NULL DEFAULT NULL, '
|
||||
. 'ADD INDEX turntime (turntime, no), ADD INDEX troop (troop, turntime)'
|
||||
);
|
||||
$db->query(
|
||||
'ALTER TABLE general_access_log '
|
||||
. 'CHANGE last_refresh last_refresh_wall_backup DATETIME NULL DEFAULT NULL, '
|
||||
. 'CHANGE last_refresh_game_tick last_refresh BIGINT NULL DEFAULT NULL'
|
||||
);
|
||||
$db->query(
|
||||
'ALTER TABLE message '
|
||||
. 'CHANGE time time_wall_backup DATETIME NULL DEFAULT NULL, '
|
||||
. 'CHANGE valid_until valid_until_wall_backup DATETIME NULL DEFAULT NULL, '
|
||||
. 'CHANGE time_game_tick time BIGINT NOT NULL, '
|
||||
. 'CHANGE valid_until_game_tick valid_until BIGINT NOT NULL'
|
||||
);
|
||||
$db->query(
|
||||
'ALTER TABLE select_npc_token DROP INDEX valid_until, '
|
||||
. 'CHANGE valid_until valid_until_wall_backup DATETIME NULL DEFAULT NULL, '
|
||||
. 'CHANGE pick_more_from pick_more_from_wall_backup DATETIME NULL DEFAULT NULL, '
|
||||
. 'CHANGE valid_until_game_tick valid_until BIGINT NOT NULL, '
|
||||
. 'CHANGE pick_more_from_game_tick pick_more_from BIGINT NOT NULL, '
|
||||
. 'ADD INDEX valid_until (valid_until)'
|
||||
);
|
||||
$db->query(
|
||||
'ALTER TABLE select_pool DROP INDEX reserved_until, '
|
||||
. 'CHANGE reserved_until reserved_until_wall_backup DATETIME NULL DEFAULT NULL, '
|
||||
. 'CHANGE reserved_until_game_tick reserved_until BIGINT NULL DEFAULT NULL, '
|
||||
. 'ADD INDEX reserved_until (reserved_until, general_id)'
|
||||
);
|
||||
$db->query(
|
||||
'ALTER TABLE ng_auction DROP INDEX by_close, '
|
||||
. 'CHANGE open_date open_date_wall_backup DATETIME NULL DEFAULT NULL, '
|
||||
. 'CHANGE close_date close_date_wall_backup DATETIME NULL DEFAULT NULL, '
|
||||
. 'CHANGE open_game_tick open_tick BIGINT NOT NULL, '
|
||||
. 'CHANGE close_game_tick close_tick BIGINT NOT NULL, '
|
||||
. 'ADD INDEX by_close (finished, type, close_tick)'
|
||||
);
|
||||
|
||||
foreach (['starttime', 'turntime', 'opentime', 'tnmt_time'] as $key) {
|
||||
$value = $legacy[$key] ?? null;
|
||||
$gameStor->{"{$key}_wall_backup"} = $value;
|
||||
$gameStor->{$key} = $value === null || $value === ''
|
||||
? null
|
||||
: $conversionClock->dateTimeToTick(new DateTimeImmutable((string)$value));
|
||||
}
|
||||
GameClock::initializeStorage(
|
||||
$gameStor,
|
||||
$baseTime,
|
||||
$turnTerm,
|
||||
$conversionClock->dateTimeToTick($wallNow),
|
||||
GameClock::MODE_REALTIME,
|
||||
$wallNow,
|
||||
);
|
||||
} finally {
|
||||
\sammo\unlock();
|
||||
}
|
||||
|
||||
if (printMigrationStatus($db) !== 'tick') {
|
||||
fwrite(STDERR, "마이그레이션 후 스키마 검증에 실패했습니다. 백업과 *_wall_backup을 이용해 복구해 주세요.\n");
|
||||
exit(4);
|
||||
}
|
||||
fwrite(STDOUT, "게임 시계 migration 완료. *_wall_backup은 별도 검증 후에만 수동 제거해 주세요.\n");
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use sammo\DB;
|
||||
use sammo\GameClock;
|
||||
use sammo\KVStorage;
|
||||
use sammo\TurnExecutionHelper;
|
||||
use sammo\Util;
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$_SERVER['REMOTE_ADDR'] ??= '127.0.0.1';
|
||||
$_SERVER['REQUEST_URI'] ??= '/cli/verify-game-clock-engine';
|
||||
|
||||
require dirname(__DIR__) . '/hwe/lib.php';
|
||||
require dirname(__DIR__) . '/hwe/func.php';
|
||||
|
||||
$options = getopt('', ['apply', 'engine-calls:']);
|
||||
if (!isset($options['apply'])) {
|
||||
fwrite(STDERR, "Usage: php scripts/verify-game-clock-engine.php --apply [--engine-calls=N]\n");
|
||||
exit(2);
|
||||
}
|
||||
$engineCalls = filter_var($options['engine-calls'] ?? '1', FILTER_VALIDATE_INT);
|
||||
if ($engineCalls === false || $engineCalls < 1 || $engineCalls > 1000) {
|
||||
throw new InvalidArgumentException('--engine-calls는 1..1000이어야 합니다.');
|
||||
}
|
||||
|
||||
$db = DB::db();
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
if ($clock->getMode() !== GameClock::MODE_MANUAL) {
|
||||
throw new RuntimeException('재현 검증은 manual clock에서만 실행할 수 있습니다.');
|
||||
}
|
||||
|
||||
$fixedNowTick = $clock->nowTick();
|
||||
$before = $gameStor->getValues(['year', 'month', 'turntime']);
|
||||
$executedCount = 0;
|
||||
for ($call = 0; $call < $engineCalls; $call++) {
|
||||
$executed = false;
|
||||
$locked = false;
|
||||
TurnExecutionHelper::executeAllCommand($executed, $locked);
|
||||
if ($locked) {
|
||||
throw new RuntimeException('엔진이 GAME lock 또는 동결 상태로 진행하지 못했습니다.');
|
||||
}
|
||||
if ($executed) {
|
||||
$executedCount++;
|
||||
}
|
||||
$gameStor->resetCache();
|
||||
$clock = GameClock::fromStorage($gameStor);
|
||||
if ($clock->nowTick() !== $fixedNowTick) {
|
||||
throw new RuntimeException('엔진 실행 중 manual clock tick이 실제 시간에 의해 변했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
$after = $gameStor->getValues(['year', 'month', 'turntime']);
|
||||
if (Util::toInt($after['turntime']) > $fixedNowTick) {
|
||||
throw new RuntimeException('마지막 실행 tick이 현재 game tick을 넘어갔습니다.');
|
||||
}
|
||||
printf(
|
||||
"clock_tick=%d clock=%s calls=%d executed_calls=%d before=%d-%02d/%d after=%d-%02d/%d\n",
|
||||
$fixedNowTick,
|
||||
$clock->formatTick($fixedNowTick, true),
|
||||
$engineCalls,
|
||||
$executedCount,
|
||||
Util::toInt($before['year']),
|
||||
Util::toInt($before['month']),
|
||||
Util::toInt($before['turntime']),
|
||||
Util::toInt($after['year']),
|
||||
Util::toInt($after['month']),
|
||||
Util::toInt($after['turntime']),
|
||||
);
|
||||
@@ -26,6 +26,11 @@ const OUTPUT_COLUMNS = [
|
||||
'selectionReasons',
|
||||
];
|
||||
|
||||
const EXCLUDED_EVENT_PHASES = [
|
||||
5, 10, 15, 20, 25, 30, 35, 40, 45, 50,
|
||||
55, 60, 65, 70, 75, 80, 85, 90, 95,
|
||||
];
|
||||
|
||||
const LEGACY_SPECIAL_WAR_MAP = [
|
||||
40 => 'che_event_귀병',
|
||||
41 => 'che_event_신산',
|
||||
@@ -110,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}");
|
||||
}
|
||||
@@ -158,6 +163,7 @@ $rows = [];
|
||||
$seenSources = [];
|
||||
$nameIndexes = [];
|
||||
$phaseCounts = [];
|
||||
$chiefCounts = [];
|
||||
$reasonCounts = [];
|
||||
$lineNo = 0;
|
||||
|
||||
@@ -182,6 +188,9 @@ while (($line = fgets(STDIN)) !== false) {
|
||||
foreach ($row[11] as $reason) {
|
||||
$reasonGroup = str_starts_with($reason, 'chief:') ? 'chief' : 'hall';
|
||||
$reasonCounts[$reasonGroup] = ($reasonCounts[$reasonGroup] ?? 0) + 1;
|
||||
if ($reasonGroup === 'chief') {
|
||||
$chiefCounts[$row[8]] = ($chiefCounts[$row[8]] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
$rows[] = $row;
|
||||
}
|
||||
@@ -201,11 +210,18 @@ foreach ($nameIndexes as $generalName => $indexes) {
|
||||
}
|
||||
}
|
||||
ksort($phaseCounts, SORT_NUMERIC);
|
||||
if (array_keys($phaseCounts) !== range(1, 99)) {
|
||||
fail('input does not cover every phase from 1 through 99');
|
||||
$expectedPhases = array_values(array_diff(range(1, 99), EXCLUDED_EVENT_PHASES));
|
||||
if (array_keys($phaseCounts) !== $expectedPhases) {
|
||||
fail('input does not cover every non-event phase from 1 through 99');
|
||||
}
|
||||
foreach ($expectedPhases as $phase) {
|
||||
if (($chiefCounts[$phase] ?? 0) !== 8) {
|
||||
fail("phase {$phase}: expected 8 unification chiefs including the ruler");
|
||||
}
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'excludedEventPhases' => EXCLUDED_EVENT_PHASES,
|
||||
'columns' => OUTPUT_COLUMNS,
|
||||
'data' => $rows,
|
||||
];
|
||||
|
||||
@@ -23,6 +23,7 @@ phases AS (
|
||||
FROM emperior e
|
||||
LEFT JOIN ng_games g ON g.server_id = e.server_id
|
||||
WHERE e.no BETWEEN 1 AND 99
|
||||
AND MOD(e.no, 5) <> 0
|
||||
),
|
||||
hall_eligible AS (
|
||||
SELECT
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
namespace sammo;
|
||||
|
||||
/**
|
||||
* 게임 진행 시각과 표시용 달력 시각 사이의 유일한 변환 경계입니다.
|
||||
*
|
||||
* 게임 로직과 DB에는 tick만 저장합니다. 벽시계는 realtime mode의 anchor를
|
||||
* 진행시키는 입력으로만 사용하며, manual mode에서는 전혀 참조하지 않습니다.
|
||||
*/
|
||||
final class GameClock
|
||||
{
|
||||
public const TICKS_PER_TURN = 36_000_000;
|
||||
public const MAX_SAFE_TICK = 9_007_199_254_740_991;
|
||||
public const MODE_REALTIME = 'realtime';
|
||||
public const MODE_MANUAL = 'manual';
|
||||
|
||||
/** @var null|callable():\DateTimeImmutable */
|
||||
private $wallNowProvider;
|
||||
|
||||
public function __construct(
|
||||
private readonly \DateTimeImmutable $baseTime,
|
||||
private readonly int $turnTermMinutes,
|
||||
private readonly int $anchorTick,
|
||||
private readonly string $mode,
|
||||
private readonly \DateTimeImmutable $wallAnchor,
|
||||
?callable $wallNowProvider = null,
|
||||
) {
|
||||
if ($turnTermMinutes <= 0) {
|
||||
throw new \InvalidArgumentException('turnterm은 양수여야 합니다.');
|
||||
}
|
||||
if (self::TICKS_PER_TURN % ($turnTermMinutes * 60) !== 0) {
|
||||
throw new \InvalidArgumentException(
|
||||
"turnterm {$turnTermMinutes}분은 정수 tick/초로 표현할 수 없습니다."
|
||||
);
|
||||
}
|
||||
if (!in_array($mode, [self::MODE_REALTIME, self::MODE_MANUAL], true)) {
|
||||
throw new \InvalidArgumentException("알 수 없는 game clock mode: {$mode}");
|
||||
}
|
||||
self::requireSafeTick($anchorTick);
|
||||
$this->wallNowProvider = $wallNowProvider;
|
||||
}
|
||||
|
||||
public static function fromStorage(KVStorage $gameStor, ?callable $wallNowProvider = null): self
|
||||
{
|
||||
$values = $gameStor->getValues([
|
||||
'clock_base_time',
|
||||
'clock_tick',
|
||||
'clock_mode',
|
||||
'clock_wall_anchor',
|
||||
'turnterm',
|
||||
]);
|
||||
|
||||
$baseTime = new \DateTimeImmutable((string)$values['clock_base_time']);
|
||||
$wallAnchor = new \DateTimeImmutable((string)$values['clock_wall_anchor']);
|
||||
|
||||
return new self(
|
||||
$baseTime,
|
||||
Util::toInt($values['turnterm']),
|
||||
Util::toInt($values['clock_tick']),
|
||||
(string)$values['clock_mode'],
|
||||
$wallAnchor,
|
||||
$wallNowProvider,
|
||||
);
|
||||
}
|
||||
|
||||
public static function initializeStorage(
|
||||
KVStorage $gameStor,
|
||||
\DateTimeInterface $baseTime,
|
||||
int $turnTermMinutes,
|
||||
int $currentTick,
|
||||
string $mode = self::MODE_REALTIME,
|
||||
?\DateTimeInterface $wallAnchor = null,
|
||||
): void {
|
||||
$wallAnchor ??= self::readWallTime();
|
||||
// 생성자 validation을 초기화 경로에도 동일하게 적용합니다.
|
||||
new self(
|
||||
\DateTimeImmutable::createFromInterface($baseTime),
|
||||
$turnTermMinutes,
|
||||
$currentTick,
|
||||
$mode,
|
||||
\DateTimeImmutable::createFromInterface($wallAnchor),
|
||||
);
|
||||
|
||||
$gameStor->clock_base_time = TimeUtil::format($baseTime, true);
|
||||
$gameStor->clock_tick = $currentTick;
|
||||
$gameStor->clock_mode = $mode;
|
||||
$gameStor->clock_wall_anchor = TimeUtil::format($wallAnchor, true);
|
||||
}
|
||||
|
||||
public function getBaseTime(): \DateTimeImmutable
|
||||
{
|
||||
return $this->baseTime;
|
||||
}
|
||||
|
||||
public function getTurnTermMinutes(): int
|
||||
{
|
||||
return $this->turnTermMinutes;
|
||||
}
|
||||
|
||||
public function getMode(): string
|
||||
{
|
||||
return $this->mode;
|
||||
}
|
||||
|
||||
public function ticksPerSecond(): int
|
||||
{
|
||||
return intdiv(self::TICKS_PER_TURN, $this->turnTermMinutes * 60);
|
||||
}
|
||||
|
||||
public function nowTick(): int
|
||||
{
|
||||
if ($this->mode === self::MODE_MANUAL) {
|
||||
return $this->anchorTick;
|
||||
}
|
||||
|
||||
return self::addTicks($this->anchorTick, $this->ticksBetween($this->wallAnchor, $this->wallNow()));
|
||||
}
|
||||
|
||||
public function ticksFromSeconds(int|float $seconds): int
|
||||
{
|
||||
if (is_int($seconds)) {
|
||||
return $seconds * $this->ticksPerSecond();
|
||||
}
|
||||
return (int)round($seconds * $this->ticksPerSecond());
|
||||
}
|
||||
|
||||
public function ticksFromMinutes(int|float $minutes): int
|
||||
{
|
||||
return $this->ticksFromSeconds($minutes * 60);
|
||||
}
|
||||
|
||||
public function addTurns(int $tick, int $turns = 1): int
|
||||
{
|
||||
if (abs($turns) > intdiv(self::MAX_SAFE_TICK, self::TICKS_PER_TURN)) {
|
||||
throw new \OverflowException('turn 수가 JavaScript safe integer tick 범위를 벗어났습니다.');
|
||||
}
|
||||
return self::addTicks($tick, self::TICKS_PER_TURN * $turns);
|
||||
}
|
||||
|
||||
public static function addTicks(int $tick, int $deltaTick): int
|
||||
{
|
||||
self::requireSafeTick($tick);
|
||||
if ($deltaTick > self::MAX_SAFE_TICK || $deltaTick < -self::MAX_SAFE_TICK) {
|
||||
throw new \OverflowException('delta tick이 JavaScript safe integer 범위를 벗어났습니다.');
|
||||
}
|
||||
if (($deltaTick > 0 && $tick > self::MAX_SAFE_TICK - $deltaTick)
|
||||
|| ($deltaTick < 0 && $tick < -self::MAX_SAFE_TICK - $deltaTick)) {
|
||||
throw new \OverflowException('JavaScript safe integer tick 범위를 벗어났습니다.');
|
||||
}
|
||||
return self::requireSafeTick($tick + $deltaTick);
|
||||
}
|
||||
|
||||
public static function requireSafeTick(int $tick): int
|
||||
{
|
||||
if (abs($tick) > self::MAX_SAFE_TICK) {
|
||||
throw new \OverflowException("tick {$tick}은 JavaScript safe integer 범위를 벗어났습니다.");
|
||||
}
|
||||
return $tick;
|
||||
}
|
||||
|
||||
public function floorTurn(int $tick): int
|
||||
{
|
||||
$remainder = $tick % self::TICKS_PER_TURN;
|
||||
if ($remainder < 0) {
|
||||
$remainder += self::TICKS_PER_TURN;
|
||||
}
|
||||
return $tick - $remainder;
|
||||
}
|
||||
|
||||
/** @return array{turn:int, subTick:int} */
|
||||
public function splitTick(int $tick): array
|
||||
{
|
||||
$turnStart = $this->floorTurn($tick);
|
||||
return [
|
||||
'turn' => intdiv($turnStart, self::TICKS_PER_TURN),
|
||||
'subTick' => $tick - $turnStart,
|
||||
];
|
||||
}
|
||||
|
||||
public function dateTimeToTick(\DateTimeInterface $dateTime): int
|
||||
{
|
||||
return self::requireSafeTick($this->ticksBetween($this->baseTime, $dateTime));
|
||||
}
|
||||
|
||||
public function tickToDateTime(int $tick): \DateTimeImmutable
|
||||
{
|
||||
self::requireSafeTick($tick);
|
||||
$ticksPerSecond = $this->ticksPerSecond();
|
||||
$seconds = intdiv($tick, $ticksPerSecond);
|
||||
$remainingTicks = $tick % $ticksPerSecond;
|
||||
if ($remainingTicks < 0) {
|
||||
$seconds--;
|
||||
$remainingTicks += $ticksPerSecond;
|
||||
}
|
||||
$microseconds = intdiv($remainingTicks * 1_000_000, $ticksPerSecond);
|
||||
|
||||
$result = $this->baseTime->modify("{$seconds} seconds");
|
||||
if ($microseconds !== 0) {
|
||||
$result = $result->modify("{$microseconds} microseconds");
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function formatTick(int $tick, bool $withFraction = false): string
|
||||
{
|
||||
return TimeUtil::format($this->tickToDateTime($tick), $withFraction);
|
||||
}
|
||||
|
||||
public static function baseTimeForProjection(
|
||||
\DateTimeInterface $projectedTime,
|
||||
int $tick,
|
||||
int $turnTermMinutes,
|
||||
): \DateTimeImmutable {
|
||||
$ticksPerSecond = intdiv(self::TICKS_PER_TURN, $turnTermMinutes * 60);
|
||||
if ($ticksPerSecond <= 0 || self::TICKS_PER_TURN % ($turnTermMinutes * 60) !== 0) {
|
||||
throw new \InvalidArgumentException('정수 tick/초로 표현할 수 없는 turnterm입니다.');
|
||||
}
|
||||
$seconds = intdiv($tick, $ticksPerSecond);
|
||||
$remainingTicks = $tick % $ticksPerSecond;
|
||||
if ($remainingTicks < 0) {
|
||||
$seconds--;
|
||||
$remainingTicks += $ticksPerSecond;
|
||||
}
|
||||
$microseconds = intdiv($remainingTicks * 1_000_000, $ticksPerSecond);
|
||||
$negativeSeconds = -$seconds;
|
||||
$result = \DateTimeImmutable::createFromInterface($projectedTime)->modify("{$negativeSeconds} seconds");
|
||||
if ($microseconds !== 0) {
|
||||
$negativeMicroseconds = -$microseconds;
|
||||
$result = $result->modify("{$negativeMicroseconds} microseconds");
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function persistTick(KVStorage $gameStor, int $tick, ?string $mode = null): void
|
||||
{
|
||||
self::requireSafeTick($tick);
|
||||
$mode ??= $this->mode;
|
||||
if (!in_array($mode, [self::MODE_REALTIME, self::MODE_MANUAL], true)) {
|
||||
throw new \InvalidArgumentException("알 수 없는 game clock mode: {$mode}");
|
||||
}
|
||||
$gameStor->clock_tick = $tick;
|
||||
$gameStor->clock_mode = $mode;
|
||||
// 수동 시계의 전진은 실제 시간에 의존하지 않아야 합니다. realtime으로
|
||||
// 전환하는 순간에만 새 벽시계 anchor를 읽습니다.
|
||||
$wallAnchor = $mode === self::MODE_REALTIME ? $this->wallNow() : $this->wallAnchor;
|
||||
$gameStor->clock_wall_anchor = TimeUtil::format($wallAnchor, true);
|
||||
}
|
||||
|
||||
public function advance(KVStorage $gameStor, int $deltaTick): int
|
||||
{
|
||||
$nextTick = $this->nowTick() + $deltaTick;
|
||||
$this->persistTick($gameStor, $nextTick);
|
||||
return $nextTick;
|
||||
}
|
||||
|
||||
private function ticksBetween(\DateTimeInterface $from, \DateTimeInterface $to): int
|
||||
{
|
||||
$secondDiff = $to->getTimestamp() - $from->getTimestamp();
|
||||
$microsecondDiff = Util::toInt($to->format('u')) - Util::toInt($from->format('u'));
|
||||
$ticksPerSecond = $this->ticksPerSecond();
|
||||
|
||||
return $secondDiff * $ticksPerSecond
|
||||
+ intdiv($microsecondDiff * $ticksPerSecond, 1_000_000);
|
||||
}
|
||||
|
||||
private function wallNow(): \DateTimeImmutable
|
||||
{
|
||||
if ($this->wallNowProvider !== null) {
|
||||
$now = ($this->wallNowProvider)();
|
||||
if (!$now instanceof \DateTimeImmutable) {
|
||||
throw new \UnexpectedValueException('wallNowProvider는 DateTimeImmutable을 반환해야 합니다.');
|
||||
}
|
||||
return $now;
|
||||
}
|
||||
return self::readWallTime();
|
||||
}
|
||||
|
||||
/** 운영 anchor와 실행 budget에서만 사용하는 실제 벽시계 입력입니다. */
|
||||
public static function readWallTime(): \DateTimeImmutable
|
||||
{
|
||||
return new \DateTimeImmutable();
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,15 @@
|
||||
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';
|
||||
require_once __DIR__ . '/../hwe/d_setting/GameConst.php';
|
||||
require_once __DIR__ . '/../hwe/sammo/CentennialAllStarGrowthService.php';
|
||||
|
||||
final class CentennialAllStarGrowthTest extends TestCase
|
||||
@@ -17,6 +25,202 @@ final class CentennialAllStarGrowthTest extends TestCase
|
||||
self::assertSame(1.0, CentennialAllStarGrowth::progress(180, 210, 1));
|
||||
}
|
||||
|
||||
public function testStatResetIsDisabledOnlyForCentennialAllStarPool(): void
|
||||
{
|
||||
$previousPool = GameConst::$targetGeneralPool;
|
||||
try {
|
||||
GameConst::$targetGeneralPool = CentennialAllStarGrowthService::POOL_CLASS;
|
||||
self::assertFalse(CentennialAllStarGrowthService::isStatResetAllowed());
|
||||
|
||||
GameConst::$targetGeneralPool = 'RandomNameGeneral';
|
||||
self::assertTrue(CentennialAllStarGrowthService::isStatResetAllowed());
|
||||
} finally {
|
||||
GameConst::$targetGeneralPool = $previousPool;
|
||||
}
|
||||
}
|
||||
|
||||
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 testGeneratedNpcKeepsOrdinaryTotalAndUsesCreationDateTargetFloor(): void
|
||||
{
|
||||
$vars = [
|
||||
'leadership' => 72,
|
||||
'strength' => 66,
|
||||
'intel' => 12,
|
||||
'dex1' => 120000,
|
||||
'dex2' => 240000,
|
||||
'dex3' => 360000,
|
||||
'dex4' => 480000,
|
||||
'dex5' => 600000,
|
||||
'special' => 'None',
|
||||
];
|
||||
$target = [
|
||||
'uniqueName' => 'A1000001',
|
||||
'leadership' => 100,
|
||||
'strength' => 80,
|
||||
'intel' => 10,
|
||||
'dex' => [900000, 800000, 700000, 600000, 500000],
|
||||
];
|
||||
$aux = CentennialAllStarGrowthService::initialAux($target);
|
||||
$general = $this->createStateGeneralMock($vars, $aux);
|
||||
|
||||
CentennialAllStarGrowthService::initializeGeneratedNPC($general, $target);
|
||||
CentennialAllStarGrowthService::applyTarget(
|
||||
$general,
|
||||
$target,
|
||||
['startyear' => 180, 'year' => 195, 'month' => 1],
|
||||
CentennialAllStarGrowthService::NPC_PROGRESS_MULTIPLIER,
|
||||
GameConst::$centennialNpcDexTargetRatio
|
||||
);
|
||||
|
||||
self::assertSame(91, $vars['leadership']);
|
||||
self::assertSame(73, $vars['strength']);
|
||||
self::assertSame(12, $vars['intel']);
|
||||
self::assertSame(176, $vars['leadership'] + $vars['strength'] + $vars['intel']);
|
||||
self::assertSame(
|
||||
[360000, 320000, 280000, 240000, 200000],
|
||||
$this->dexValues($vars)
|
||||
);
|
||||
self::assertSame(19, $aux['granted']['leadership']);
|
||||
self::assertSame(7, $aux['granted']['strength']);
|
||||
self::assertSame(0, $aux['granted']['intel']);
|
||||
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
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
CentennialAllStarGrowthService::calculateProgress(180, 195, 1, 1.01);
|
||||
}
|
||||
|
||||
public function testLowStatGrowsWhileHigherStatStays(): void
|
||||
{
|
||||
$floor = CentennialAllStarGrowth::statFloor(90, 15, 0.6);
|
||||
@@ -31,6 +235,112 @@ final class CentennialAllStarGrowthTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -61,6 +371,521 @@ 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 testDexConversionConsumesEventFloorWithoutMonthlyRefill(): void
|
||||
{
|
||||
$vars = [
|
||||
'leadership' => 100,
|
||||
'strength' => 100,
|
||||
'intel' => 100,
|
||||
'dex1' => 216000,
|
||||
'dex2' => 489600,
|
||||
'dex3' => 360000,
|
||||
'dex4' => 360000,
|
||||
'dex5' => 360000,
|
||||
'special' => 'None',
|
||||
];
|
||||
$aux = [
|
||||
'targetId' => 'A1000001',
|
||||
'granted' => [
|
||||
'leadership' => 85,
|
||||
'strength' => 85,
|
||||
'intel' => 85,
|
||||
'dex1' => 360000,
|
||||
'dex2' => 360000,
|
||||
'dex3' => 360000,
|
||||
'dex4' => 360000,
|
||||
'dex5' => 360000,
|
||||
],
|
||||
'dexConsumed' => [
|
||||
'dex1' => 0,
|
||||
'dex2' => 0,
|
||||
'dex3' => 0,
|
||||
'dex4' => 0,
|
||||
'dex5' => 0,
|
||||
],
|
||||
'progressMonth' => 180,
|
||||
'milestone' => 5,
|
||||
'naturalSpecialDomestic' => null,
|
||||
'eventSpecialDomestic' => null,
|
||||
'userInitialStats' => [],
|
||||
'dexTargetRatio' => 1.0,
|
||||
];
|
||||
$general = $this->createStateGeneralMock($vars, $aux);
|
||||
|
||||
CentennialAllStarGrowthService::reconcileDexConversion(
|
||||
$general,
|
||||
'dex1',
|
||||
'dex2',
|
||||
360000,
|
||||
216000,
|
||||
360000,
|
||||
489600,
|
||||
0.9
|
||||
);
|
||||
|
||||
self::assertSame(216000, $aux['granted']['dex1']);
|
||||
self::assertSame(489600, $aux['granted']['dex2']);
|
||||
self::assertSame(144000, $aux['dexConsumed']['dex1']);
|
||||
|
||||
CentennialAllStarGrowthService::applyTarget(
|
||||
$general,
|
||||
[
|
||||
'uniqueName' => 'A1000001',
|
||||
'leadership' => 100,
|
||||
'strength' => 100,
|
||||
'intel' => 100,
|
||||
'dex' => [360000, 360000, 360000, 360000, 360000],
|
||||
],
|
||||
['startyear' => 180, 'year' => 195, 'month' => 1]
|
||||
);
|
||||
|
||||
self::assertSame(216000, $vars['dex1']);
|
||||
self::assertSame(489600, $vars['dex2']);
|
||||
self::assertSame(216000, $aux['dexFloor']['dex1']);
|
||||
self::assertSame(0, CentennialAllStarGrowthService::recordableValue($general, 'dex1'));
|
||||
self::assertSame(0, CentennialAllStarGrowthService::recordableValue($general, 'dex2'));
|
||||
|
||||
$vars['dex1'] = 129600;
|
||||
$vars['dex2'] = 567360;
|
||||
CentennialAllStarGrowthService::reconcileDexConversion(
|
||||
$general,
|
||||
'dex1',
|
||||
'dex2',
|
||||
216000,
|
||||
129600,
|
||||
489600,
|
||||
567360,
|
||||
0.9
|
||||
);
|
||||
CentennialAllStarGrowthService::applyTarget(
|
||||
$general,
|
||||
[
|
||||
'uniqueName' => 'A1000001',
|
||||
'leadership' => 100,
|
||||
'strength' => 100,
|
||||
'intel' => 100,
|
||||
'dex' => [360000, 360000, 360000, 360000, 360000],
|
||||
],
|
||||
['startyear' => 180, 'year' => 195, 'month' => 1]
|
||||
);
|
||||
|
||||
self::assertSame(129600, $vars['dex1']);
|
||||
self::assertSame(567360, $vars['dex2']);
|
||||
self::assertSame(230400, $aux['dexConsumed']['dex1']);
|
||||
self::assertSame(129600, $aux['dexFloor']['dex1']);
|
||||
self::assertSame(0, CentennialAllStarGrowthService::recordableValue($general, 'dex1'));
|
||||
self::assertSame(0, CentennialAllStarGrowthService::recordableValue($general, 'dex2'));
|
||||
}
|
||||
|
||||
public function testNaturalDexConversionDoesNotBecomeEventGrant(): void
|
||||
{
|
||||
$vars = [
|
||||
'dex1' => 216000,
|
||||
'dex2' => 129600,
|
||||
];
|
||||
$aux = [
|
||||
'targetId' => 'A1000001',
|
||||
'granted' => [
|
||||
'dex1' => 0,
|
||||
'dex2' => 0,
|
||||
],
|
||||
'dexConsumed' => [
|
||||
'dex1' => 0,
|
||||
'dex2' => 0,
|
||||
],
|
||||
'dexFloor' => [
|
||||
'dex1' => 360000,
|
||||
'dex2' => 0,
|
||||
],
|
||||
];
|
||||
$general = $this->createStateGeneralMock($vars, $aux);
|
||||
|
||||
CentennialAllStarGrowthService::reconcileDexConversion(
|
||||
$general,
|
||||
'dex1',
|
||||
'dex2',
|
||||
360000,
|
||||
216000,
|
||||
0,
|
||||
129600,
|
||||
0.9
|
||||
);
|
||||
|
||||
self::assertSame(0, $aux['granted']['dex1']);
|
||||
self::assertSame(0, $aux['granted']['dex2']);
|
||||
self::assertSame(144000, $aux['dexConsumed']['dex1']);
|
||||
self::assertSame(216000, CentennialAllStarGrowthService::recordableValue($general, 'dex1'));
|
||||
self::assertSame(129600, CentennialAllStarGrowthService::recordableValue($general, 'dex2'));
|
||||
}
|
||||
|
||||
public function testMixedDexConversionSplitsEventAndNaturalPointsByTotalShare(): void
|
||||
{
|
||||
$vars = [
|
||||
'dex1' => 360000,
|
||||
'dex2' => 600000,
|
||||
];
|
||||
$aux = [
|
||||
'targetId' => 'archer',
|
||||
'granted' => [
|
||||
'dex1' => 0,
|
||||
'dex2' => 900000,
|
||||
],
|
||||
'dexConsumed' => [
|
||||
'dex1' => 0,
|
||||
'dex2' => 0,
|
||||
],
|
||||
'dexFloor' => [
|
||||
'dex1' => 0,
|
||||
'dex2' => 900000,
|
||||
],
|
||||
];
|
||||
$general = $this->createStateGeneralMock($vars, $aux);
|
||||
|
||||
CentennialAllStarGrowthService::reconcileDexConversion(
|
||||
$general,
|
||||
'dex2',
|
||||
'dex1',
|
||||
1000000,
|
||||
600000,
|
||||
0,
|
||||
360000,
|
||||
0.9
|
||||
);
|
||||
|
||||
self::assertSame(540000, $aux['granted']['dex2']);
|
||||
self::assertSame(324000, $aux['granted']['dex1']);
|
||||
self::assertSame(60_000, CentennialAllStarGrowthService::recordableValue($general, 'dex2'));
|
||||
self::assertSame(36_000, CentennialAllStarGrowthService::recordableValue($general, 'dex1'));
|
||||
self::assertSame(864000, $aux['granted']['dex1'] + $aux['granted']['dex2']);
|
||||
self::assertSame(
|
||||
96000,
|
||||
CentennialAllStarGrowthService::recordableValue($general, 'dex1')
|
||||
+ CentennialAllStarGrowthService::recordableValue($general, 'dex2')
|
||||
);
|
||||
}
|
||||
|
||||
public function testArcherCavalryGhostConversionChainKeepsOnlyCurrentTargetBudget(): void
|
||||
{
|
||||
$vars = $this->emptyGeneralVars();
|
||||
$aux = CentennialAllStarGrowthService::initialAux(
|
||||
$this->singleDexTarget('archer', 1),
|
||||
[]
|
||||
);
|
||||
$general = $this->createStateGeneralMock($vars, $aux);
|
||||
$env = ['startyear' => 180, 'year' => 195, 'month' => 1];
|
||||
|
||||
CentennialAllStarGrowthService::applyTarget(
|
||||
$general,
|
||||
$this->singleDexTarget('archer', 1),
|
||||
$env
|
||||
);
|
||||
$this->convertDex($general, $vars, 'dex2', 'dex1');
|
||||
self::assertSame([324000, 540000, 0, 0, 0], $this->dexValues($vars));
|
||||
|
||||
CentennialAllStarGrowthService::applyTarget(
|
||||
$general,
|
||||
$this->singleDexTarget('cavalry', 2),
|
||||
$env
|
||||
);
|
||||
self::assertSame([0, 0, 900000, 0, 0], $this->dexValues($vars));
|
||||
$this->convertDex($general, $vars, 'dex3', 'dex1');
|
||||
self::assertSame([324000, 0, 540000, 0, 0], $this->dexValues($vars));
|
||||
|
||||
CentennialAllStarGrowthService::applyTarget(
|
||||
$general,
|
||||
$this->singleDexTarget('ghost', 3),
|
||||
$env
|
||||
);
|
||||
self::assertSame([0, 0, 0, 900000, 0], $this->dexValues($vars));
|
||||
$this->convertDex($general, $vars, 'dex4', 'dex1');
|
||||
|
||||
self::assertSame([324000, 0, 0, 540000, 0], $this->dexValues($vars));
|
||||
self::assertSame(864000, array_sum($this->dexValues($vars)));
|
||||
foreach (['dex1', 'dex2', 'dex3', 'dex4', 'dex5'] as $key) {
|
||||
self::assertSame(0, CentennialAllStarGrowthService::recordableValue($general, $key));
|
||||
}
|
||||
}
|
||||
|
||||
public function testSelectingInfantryAfterConversionReplacesOldEventBudget(): void
|
||||
{
|
||||
$vars = $this->emptyGeneralVars();
|
||||
$aux = CentennialAllStarGrowthService::initialAux(
|
||||
$this->singleDexTarget('archer', 1),
|
||||
[]
|
||||
);
|
||||
$general = $this->createStateGeneralMock($vars, $aux);
|
||||
$env = ['startyear' => 180, 'year' => 195, 'month' => 1];
|
||||
|
||||
CentennialAllStarGrowthService::applyTarget(
|
||||
$general,
|
||||
$this->singleDexTarget('archer', 1),
|
||||
$env
|
||||
);
|
||||
$this->convertDex($general, $vars, 'dex2', 'dex1');
|
||||
CentennialAllStarGrowthService::applyTarget(
|
||||
$general,
|
||||
$this->singleDexTarget('infantry', 0),
|
||||
$env
|
||||
);
|
||||
|
||||
self::assertSame([900000, 0, 0, 0, 0], $this->dexValues($vars));
|
||||
self::assertSame(900000, array_sum($aux['granted']));
|
||||
self::assertSame(0, CentennialAllStarGrowthService::recordableValue($general, 'dex1'));
|
||||
}
|
||||
|
||||
public function testMixedConversionThenReselectionPreservesOnlyPostLossNaturalTotal(): void
|
||||
{
|
||||
$vars = $this->emptyGeneralVars();
|
||||
$aux = CentennialAllStarGrowthService::initialAux(
|
||||
$this->singleDexTarget('archer', 1),
|
||||
[]
|
||||
);
|
||||
$general = $this->createStateGeneralMock($vars, $aux);
|
||||
$env = ['startyear' => 180, 'year' => 195, 'month' => 1];
|
||||
|
||||
CentennialAllStarGrowthService::applyTarget(
|
||||
$general,
|
||||
$this->singleDexTarget('archer', 1),
|
||||
$env
|
||||
);
|
||||
$vars['dex2'] += 100000;
|
||||
$this->convertDex($general, $vars, 'dex2', 'dex1');
|
||||
CentennialAllStarGrowthService::applyTarget(
|
||||
$general,
|
||||
$this->singleDexTarget('infantry', 0),
|
||||
$env
|
||||
);
|
||||
|
||||
self::assertSame([900000, 60000, 0, 0, 0], $this->dexValues($vars));
|
||||
self::assertSame(864000, $aux['granted']['dex1']);
|
||||
self::assertSame(0, $aux['granted']['dex2']);
|
||||
self::assertSame(96000, array_sum($this->recordableDexValues($general)));
|
||||
self::assertSame(960000, array_sum($this->dexValues($vars)));
|
||||
}
|
||||
|
||||
public function testReselectionResetsConsumedDexFloorAndTransferredGrant(): void
|
||||
{
|
||||
$vars = [
|
||||
'leadership' => 100,
|
||||
'strength' => 100,
|
||||
'intel' => 100,
|
||||
'dex1' => 216000,
|
||||
'dex2' => 489600,
|
||||
'dex3' => 360000,
|
||||
'dex4' => 360000,
|
||||
'dex5' => 360000,
|
||||
'special' => 'None',
|
||||
];
|
||||
$aux = [
|
||||
'targetId' => 'A1000001',
|
||||
'granted' => [
|
||||
'leadership' => 85,
|
||||
'strength' => 85,
|
||||
'intel' => 85,
|
||||
'dex1' => 216000,
|
||||
'dex2' => 489600,
|
||||
'dex3' => 360000,
|
||||
'dex4' => 360000,
|
||||
'dex5' => 360000,
|
||||
],
|
||||
'dexConsumed' => [
|
||||
'dex1' => 144000,
|
||||
'dex2' => 0,
|
||||
'dex3' => 0,
|
||||
'dex4' => 0,
|
||||
'dex5' => 0,
|
||||
],
|
||||
'dexFloor' => [
|
||||
'dex1' => 216000,
|
||||
'dex2' => 360000,
|
||||
'dex3' => 360000,
|
||||
'dex4' => 360000,
|
||||
'dex5' => 360000,
|
||||
],
|
||||
'progressMonth' => 180,
|
||||
'milestone' => 5,
|
||||
'naturalSpecialDomestic' => null,
|
||||
'eventSpecialDomestic' => null,
|
||||
'userInitialStats' => [],
|
||||
'dexTargetRatio' => 1.0,
|
||||
];
|
||||
$general = $this->createStateGeneralMock($vars, $aux);
|
||||
|
||||
CentennialAllStarGrowthService::applyTarget(
|
||||
$general,
|
||||
[
|
||||
'uniqueName' => 'A1000002',
|
||||
'leadership' => 100,
|
||||
'strength' => 100,
|
||||
'intel' => 100,
|
||||
'dex' => [400000, 400000, 400000, 400000, 400000],
|
||||
],
|
||||
['startyear' => 180, 'year' => 195, 'month' => 1]
|
||||
);
|
||||
|
||||
self::assertSame(400000, $vars['dex1']);
|
||||
self::assertSame(400000, $vars['dex2']);
|
||||
self::assertSame(0, $aux['dexConsumed']['dex1']);
|
||||
self::assertSame(400000, $aux['dexFloor']['dex1']);
|
||||
self::assertSame(400000, $aux['granted']['dex1']);
|
||||
self::assertSame(400000, $aux['granted']['dex2']);
|
||||
}
|
||||
|
||||
public function testUserDexStillReachesFullHistoricalTarget(): void
|
||||
{
|
||||
self::assertSame(
|
||||
900000,
|
||||
CentennialAllStarGrowthService::calculateDexTargetFloor(
|
||||
900000,
|
||||
['startyear' => 180, 'year' => 195, 'month' => 1]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function testUntrackedGeneralKeepsFullHallValue(): void
|
||||
{
|
||||
self::assertSame(
|
||||
@@ -71,4 +896,111 @@ final class CentennialAllStarGrowthTest extends TestCase
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private function createStateGeneralMock(array &$vars, array &$aux): General
|
||||
{
|
||||
$general = $this->getMockBuilder(General::class)
|
||||
->disableOriginalConstructor()
|
||||
->onlyMethods(['getAuxVar', 'setAuxVar', 'getVar', 'updateVar'])
|
||||
->getMock();
|
||||
$general->method('getAuxVar')->willReturnCallback(
|
||||
static function (string $key) use (&$aux) {
|
||||
return $key === CentennialAllStarGrowthService::AUX_KEY
|
||||
? $aux
|
||||
: null;
|
||||
}
|
||||
);
|
||||
$general->method('getVar')->willReturnCallback(
|
||||
static function (string $key) use (&$vars) {
|
||||
return $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;
|
||||
}
|
||||
}
|
||||
);
|
||||
return $general;
|
||||
}
|
||||
|
||||
private function emptyGeneralVars(): array
|
||||
{
|
||||
return [
|
||||
'leadership' => 100,
|
||||
'strength' => 100,
|
||||
'intel' => 100,
|
||||
'dex1' => 0,
|
||||
'dex2' => 0,
|
||||
'dex3' => 0,
|
||||
'dex4' => 0,
|
||||
'dex5' => 0,
|
||||
'special' => 'None',
|
||||
];
|
||||
}
|
||||
|
||||
private function singleDexTarget(string $id, int $dexIndex): array
|
||||
{
|
||||
$dex = [0, 0, 0, 0, 0];
|
||||
$dex[$dexIndex] = 900000;
|
||||
return [
|
||||
'uniqueName' => $id,
|
||||
'leadership' => 100,
|
||||
'strength' => 100,
|
||||
'intel' => 100,
|
||||
'dex' => $dex,
|
||||
];
|
||||
}
|
||||
|
||||
private function convertDex(
|
||||
General $general,
|
||||
array &$vars,
|
||||
string $sourceKey,
|
||||
string $destinationKey
|
||||
): void {
|
||||
$sourceBefore = $vars[$sourceKey];
|
||||
$destinationBefore = $vars[$destinationKey];
|
||||
$cut = (int) ($sourceBefore * 0.4);
|
||||
$add = (int) ($cut * 0.9);
|
||||
$vars[$sourceKey] -= $cut;
|
||||
$vars[$destinationKey] += $add;
|
||||
CentennialAllStarGrowthService::reconcileDexConversion(
|
||||
$general,
|
||||
$sourceKey,
|
||||
$destinationKey,
|
||||
$sourceBefore,
|
||||
$vars[$sourceKey],
|
||||
$destinationBefore,
|
||||
$vars[$destinationKey],
|
||||
0.9
|
||||
);
|
||||
}
|
||||
|
||||
private function dexValues(array $vars): array
|
||||
{
|
||||
return [
|
||||
$vars['dex1'],
|
||||
$vars['dex2'],
|
||||
$vars['dex3'],
|
||||
$vars['dex4'],
|
||||
$vars['dex5'],
|
||||
];
|
||||
}
|
||||
|
||||
private function recordableDexValues(General $general): array
|
||||
{
|
||||
return array_map(
|
||||
static fn(string $key): int => CentennialAllStarGrowthService::recordableValue(
|
||||
$general,
|
||||
$key
|
||||
),
|
||||
['dex1', 'dex2', 'dex3', 'dex4', 'dex5']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use sammo\GameConst;
|
||||
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';
|
||||
require_once __DIR__ . '/../hwe/sammo/ActionLogger.php';
|
||||
require_once __DIR__ . '/../hwe/sammo/GameConstBase.php';
|
||||
require_once __DIR__ . '/../hwe/d_setting/GameConst.php';
|
||||
|
||||
final class CentennialAllStarPoolTest extends TestCase
|
||||
{
|
||||
private const POOL_PATH = __DIR__ . '/../hwe/sammo/GeneralPool/Pool/UnderS100.json';
|
||||
|
||||
public function testPoolCoversEveryCompletedPhase(): void
|
||||
public function testPoolCoversEveryCompletedNonEventPhase(): void
|
||||
{
|
||||
$pool = json_decode(
|
||||
file_get_contents(self::POOL_PATH),
|
||||
@@ -31,10 +40,18 @@ final class CentennialAllStarPoolTest extends TestCase
|
||||
],
|
||||
$pool['columns']
|
||||
);
|
||||
self::assertCount(5757, $pool['data']);
|
||||
self::assertCount(4682, $pool['data']);
|
||||
self::assertSame(
|
||||
[
|
||||
5, 10, 15, 20, 25, 30, 35, 40, 45, 50,
|
||||
55, 60, 65, 70, 75, 80, 85, 90, 95,
|
||||
],
|
||||
$pool['excludedEventPhases']
|
||||
);
|
||||
|
||||
$column = array_flip($pool['columns']);
|
||||
$phases = [];
|
||||
$chiefCounts = [];
|
||||
$sourceKeys = [];
|
||||
$generalNames = [];
|
||||
foreach ($pool['data'] as $row) {
|
||||
@@ -49,6 +66,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']]);
|
||||
@@ -57,11 +81,48 @@ final class CentennialAllStarPoolTest extends TestCase
|
||||
'/^(hall:[a-z0-9_]+|chief:(?:5|6|7|8|9|10|11|12))$/',
|
||||
$reason
|
||||
);
|
||||
if (str_starts_with($reason, 'chief:')) {
|
||||
$chiefCounts[$phase] = ($chiefCounts[$phase] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ksort($phases, SORT_NUMERIC);
|
||||
self::assertSame(range(1, 99), array_keys($phases));
|
||||
$expectedPhases = array_values(array_diff(
|
||||
range(1, 99),
|
||||
$pool['excludedEventPhases']
|
||||
));
|
||||
self::assertSame($expectedPhases, array_keys($phases));
|
||||
foreach ($expectedPhases as $phase) {
|
||||
self::assertSame(
|
||||
8,
|
||||
$chiefCounts[$phase] ?? 0,
|
||||
"phase {$phase} must include the ruler and seven chiefs"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -84,4 +145,31 @@ final class CentennialAllStarPoolTest extends TestCase
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function testEveryHistoricalEventSpecialIsExposedToTheFrontend(): void
|
||||
{
|
||||
$pool = json_decode(
|
||||
file_get_contents(self::POOL_PATH),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR
|
||||
);
|
||||
$column = array_flip($pool['columns']);
|
||||
$registeredSpecials = array_flip(array_merge(
|
||||
GameConst::$availableSpecialDomestic,
|
||||
GameConst::$optionalSpecialDomestic
|
||||
));
|
||||
|
||||
foreach ($pool['data'] as $row) {
|
||||
$special = $row[$column['specialDomestic']];
|
||||
if ($special === null || $special === '') {
|
||||
continue;
|
||||
}
|
||||
self::assertArrayHasKey(
|
||||
$special,
|
||||
$registeredSpecials,
|
||||
"{$special} must be included in GetConst iActionInfo.specialDomestic"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace sammo;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class GameClockBoundaryTest extends TestCase
|
||||
{
|
||||
/** @dataProvider gameSchedulingFiles */
|
||||
public function testGameSchedulingCodeDoesNotReadWallOrDatabaseClock(string $relativePath): void
|
||||
{
|
||||
$source = file_get_contents(__DIR__ . '/../' . $relativePath);
|
||||
self::assertIsString($source);
|
||||
|
||||
foreach ([
|
||||
'/TimeUtil::now(?:DateTimeImmutable)?\s*\(/',
|
||||
'/new\s+\\\\?DateTime(?:Immutable)?\s*\(\s*\)/',
|
||||
'/\bNOW\s*\(/i',
|
||||
'/\bCURRENT_TIMESTAMP\b/i',
|
||||
'/\bCURDATE\s*\(/i',
|
||||
] as $pattern) {
|
||||
self::assertDoesNotMatchRegularExpression($pattern, $source, $relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
public static function gameSchedulingFiles(): array
|
||||
{
|
||||
return array_map(static fn (string $path): array => [$path], [
|
||||
'hwe/sammo/TurnExecutionHelper.php',
|
||||
'hwe/sammo/Auction.php',
|
||||
'hwe/sammo/AuctionBasicResource.php',
|
||||
'hwe/sammo/AuctionUniqueItem.php',
|
||||
'hwe/func_auction.php',
|
||||
'hwe/func_tournament.php',
|
||||
'hwe/c_tournament.php',
|
||||
'hwe/sammo/AbsFromUserPool.php',
|
||||
'hwe/sammo/GeneralPool/RandomNameGeneral.php',
|
||||
'hwe/sammo/API/General/DieOnPrestart.php',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testTickSchemaDoesNotUseDatabaseDefaultsForGameSchedules(): void
|
||||
{
|
||||
$schema = file_get_contents(__DIR__ . '/../hwe/sql/schema.sql');
|
||||
self::assertIsString($schema);
|
||||
foreach ([
|
||||
'`turntime` BIGINT',
|
||||
'`recent_war` BIGINT',
|
||||
'`last_refresh` BIGINT',
|
||||
'`time` BIGINT',
|
||||
'`valid_until` BIGINT',
|
||||
'`reserved_until` BIGINT',
|
||||
'`open_tick` BIGINT',
|
||||
'`close_tick` BIGINT',
|
||||
] as $expected) {
|
||||
self::assertStringContainsString($expected, $schema);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace sammo;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
require_once __DIR__ . '/../src/sammo/GameClock.php';
|
||||
|
||||
final class GameClockTest extends TestCase
|
||||
{
|
||||
/** @dataProvider supportedTurnTerms */
|
||||
public function testEachSupportedTurnTermHasIntegerTicksPerSecond(int $turnTerm, int $ticksPerSecond): void
|
||||
{
|
||||
$base = new \DateTimeImmutable('2026-08-03 00:00:00.000000');
|
||||
$clock = new GameClock($base, $turnTerm, 0, GameClock::MODE_MANUAL, $base);
|
||||
|
||||
self::assertSame($ticksPerSecond, $clock->ticksPerSecond());
|
||||
self::assertSame(GameClock::TICKS_PER_TURN, $clock->ticksFromMinutes($turnTerm));
|
||||
}
|
||||
|
||||
public static function supportedTurnTerms(): array
|
||||
{
|
||||
return [
|
||||
'1 minute' => [1, 600_000],
|
||||
'2 minutes' => [2, 300_000],
|
||||
'5 minutes' => [5, 120_000],
|
||||
'10 minutes' => [10, 60_000],
|
||||
'60 minutes' => [60, 10_000],
|
||||
'120 minutes' => [120, 5_000],
|
||||
];
|
||||
}
|
||||
|
||||
public function testTickFormulaAndDisplayProjectionRoundTrip(): void
|
||||
{
|
||||
$base = new \DateTimeImmutable('2026-08-03 12:34:56.000000');
|
||||
$clock = new GameClock($base, 60, 0, GameClock::MODE_MANUAL, $base);
|
||||
$tick = GameClock::TICKS_PER_TURN * 7 + 12_345;
|
||||
|
||||
self::assertSame(['turn' => 7, 'subTick' => 12_345], $clock->splitTick($tick));
|
||||
self::assertSame($tick, $clock->dateTimeToTick($clock->tickToDateTime($tick)));
|
||||
self::assertSame('2026-08-03 19:34:57.234500', $clock->formatTick($tick, true));
|
||||
}
|
||||
|
||||
public function testManualModeDoesNotReadWallClock(): void
|
||||
{
|
||||
$base = new \DateTimeImmutable('2026-08-03 00:00:00.000000');
|
||||
$wallRead = false;
|
||||
$clock = new GameClock(
|
||||
$base,
|
||||
10,
|
||||
123_456,
|
||||
GameClock::MODE_MANUAL,
|
||||
$base,
|
||||
function () use (&$wallRead): \DateTimeImmutable {
|
||||
$wallRead = true;
|
||||
return new \DateTimeImmutable('2099-01-01 00:00:00.000000');
|
||||
},
|
||||
);
|
||||
|
||||
self::assertSame(123_456, $clock->nowTick());
|
||||
self::assertFalse($wallRead);
|
||||
}
|
||||
|
||||
public function testNegativeTickProjectionAndBaseRecalculation(): void
|
||||
{
|
||||
$projected = new \DateTimeImmutable('2026-08-03 12:00:00.123400');
|
||||
$tick = -36_001_234;
|
||||
$base = GameClock::baseTimeForProjection($projected, $tick, 60);
|
||||
$clock = new GameClock($base, 60, $tick, GameClock::MODE_MANUAL, $projected);
|
||||
|
||||
self::assertSame($tick, $clock->dateTimeToTick($clock->tickToDateTime($tick)));
|
||||
self::assertSame('2026-08-03 12:00:00.123400', $clock->formatTick($tick, true));
|
||||
self::assertSame(['turn' => -2, 'subTick' => 35_998_766], $clock->splitTick($tick));
|
||||
}
|
||||
|
||||
public function testRealtimeModeAdvancesFromAnchorWithoutDatabaseNow(): void
|
||||
{
|
||||
$base = new \DateTimeImmutable('2026-08-03 00:00:00.000000');
|
||||
$wallAnchor = new \DateTimeImmutable('2026-08-03 10:00:00.000000');
|
||||
$clock = new GameClock(
|
||||
$base,
|
||||
120,
|
||||
100,
|
||||
GameClock::MODE_REALTIME,
|
||||
$wallAnchor,
|
||||
fn (): \DateTimeImmutable => new \DateTimeImmutable('2026-08-03 10:00:01.500000'),
|
||||
);
|
||||
|
||||
self::assertSame(7_600, $clock->nowTick());
|
||||
}
|
||||
|
||||
public function testTickArithmeticRejectsValuesThatJavaScriptCannotRepresentExactly(): void
|
||||
{
|
||||
self::assertSame(GameClock::MAX_SAFE_TICK, GameClock::addTicks(GameClock::MAX_SAFE_TICK - 1, 1));
|
||||
|
||||
$this->expectException(\OverflowException::class);
|
||||
GameClock::addTicks(GameClock::MAX_SAFE_TICK, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import fs from 'node:fs';
|
||||
import {createHash} from 'node:crypto';
|
||||
import {chromium} from 'playwright';
|
||||
|
||||
const baseURL = process.env.REF_BROWSER_URL;
|
||||
const screenshotPath = process.env.REF_SCREENSHOT_PATH;
|
||||
const resultPath = process.env.REF_RESULT_PATH;
|
||||
const username = process.env.REF_USER_ID ?? 's100user01';
|
||||
const password = fs.readFileSync('/run/secrets/test_user_password', 'utf8').trim();
|
||||
const expectedReason = '100기 올스타 장수는 능력치 초기화를 사용할 수 없습니다.';
|
||||
const expectedWarning = '100기 올스타 장수는 장수 전환 시 능력치 성장 기록을 보존하기 위해 능력치 초기화를 사용할 수 없습니다.';
|
||||
|
||||
if (!baseURL || !screenshotPath || !resultPath) {
|
||||
throw new Error('REF_BROWSER_URL, REF_SCREENSHOT_PATH, and REF_RESULT_PATH are required');
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({headless: true});
|
||||
const context = await browser.newContext({
|
||||
viewport: {width: 1280, height: 960},
|
||||
deviceScaleFactor: 1,
|
||||
});
|
||||
const browserMessages = [];
|
||||
const attachPageListeners = targetPage => {
|
||||
targetPage.on('console', message => {
|
||||
browserMessages.push({type: message.type(), text: message.text()});
|
||||
});
|
||||
targetPage.on('pageerror', error => {
|
||||
browserMessages.push({type: 'pageerror', text: error.message});
|
||||
});
|
||||
};
|
||||
let page = await context.newPage();
|
||||
attachPageListeners(page);
|
||||
|
||||
await page.goto(baseURL, {waitUntil: 'domcontentloaded', timeout: 60_000});
|
||||
const salt = await page.locator('#global_salt').inputValue();
|
||||
const passwordHash = createHash('sha512')
|
||||
.update(salt + password + salt)
|
||||
.digest('hex');
|
||||
const loginResponse = await page.request.post(
|
||||
new URL('api.php?path=Login/LoginByID', baseURL).href,
|
||||
{data: {username, password: passwordHash}},
|
||||
);
|
||||
const loginResult = await loginResponse.json();
|
||||
if (!loginResponse.ok() || loginResult.result !== true) {
|
||||
throw new Error(`login failed: ${String(loginResult.reason ?? loginResponse.status())}`);
|
||||
}
|
||||
|
||||
const gameLoginResponse = await page.request.get(
|
||||
new URL('hwe/api.php?path=Global/GetConst', baseURL).href,
|
||||
);
|
||||
const gameLoginResult = await gameLoginResponse.json();
|
||||
if (!gameLoginResponse.ok() || gameLoginResult.result !== true) {
|
||||
throw new Error(`game login failed: ${JSON.stringify(gameLoginResult)}`);
|
||||
}
|
||||
|
||||
await page.close();
|
||||
browserMessages.length = 0;
|
||||
page = await context.newPage();
|
||||
attachPageListeners(page);
|
||||
await page.goto(
|
||||
new URL('hwe/v_inheritPoint.php', baseURL).href,
|
||||
{waitUntil: 'networkidle', timeout: 60_000},
|
||||
);
|
||||
const bodyText = await page.locator('body').innerText();
|
||||
if (bodyText.includes('#0 /var/www/html/')) {
|
||||
throw new Error('PHP stack trace was rendered in the inheritance page');
|
||||
}
|
||||
if (!bodyText.includes(expectedWarning)) {
|
||||
await page.screenshot({path: screenshotPath, fullPage: true});
|
||||
fs.writeFileSync(
|
||||
resultPath,
|
||||
`${JSON.stringify({
|
||||
username,
|
||||
url: page.url(),
|
||||
bodyText: bodyText.slice(0, 4000),
|
||||
browserMessages,
|
||||
}, null, 2)}\n`,
|
||||
{mode: 0o600},
|
||||
);
|
||||
throw new Error('S100 stat-reset restriction was not rendered');
|
||||
}
|
||||
if (await page.getByRole('button', {name: '능력치 초기화'}).count() !== 0) {
|
||||
throw new Error('S100 stat-reset button must not be rendered');
|
||||
}
|
||||
|
||||
const warning = page.getByText(expectedWarning, {exact: true});
|
||||
await warning.waitFor({state: 'visible', timeout: 60_000});
|
||||
const warningGeometry = await warning.evaluate(element => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const style = getComputedStyle(element);
|
||||
return {
|
||||
rect: {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
},
|
||||
style: {
|
||||
color: style.color,
|
||||
fontFamily: style.fontFamily,
|
||||
fontSize: style.fontSize,
|
||||
lineHeight: style.lineHeight,
|
||||
},
|
||||
};
|
||||
});
|
||||
const beforeState = await page.evaluate(() => ({
|
||||
currentStat: staticValues.currentStat,
|
||||
previousPoint: staticValues.items.previous,
|
||||
}));
|
||||
|
||||
const resetResponse = await page.request.put(
|
||||
new URL('hwe/api.php?path=InheritAction/ResetStat', baseURL).href,
|
||||
{
|
||||
data: {
|
||||
leadership: 55,
|
||||
strength: 55,
|
||||
intel: 55,
|
||||
},
|
||||
},
|
||||
);
|
||||
const resetResult = await resetResponse.json();
|
||||
if (!resetResponse.ok()
|
||||
|| resetResult.result !== false
|
||||
|| resetResult.reason !== expectedReason
|
||||
) {
|
||||
throw new Error(`unexpected ResetStat response: ${JSON.stringify(resetResult)}`);
|
||||
}
|
||||
|
||||
await page.reload({waitUntil: 'networkidle', timeout: 60_000});
|
||||
const afterState = await page.evaluate(() => ({
|
||||
currentStat: staticValues.currentStat,
|
||||
previousPoint: staticValues.items.previous,
|
||||
}));
|
||||
if (JSON.stringify(afterState) !== JSON.stringify(beforeState)) {
|
||||
throw new Error(`ResetStat rejection changed state: ${JSON.stringify({beforeState, afterState})}`);
|
||||
}
|
||||
|
||||
await page.screenshot({path: screenshotPath, fullPage: true});
|
||||
fs.writeFileSync(
|
||||
resultPath,
|
||||
`${JSON.stringify({
|
||||
username,
|
||||
url: page.url(),
|
||||
viewport: {width: 1280, height: 960, deviceScaleFactor: 1},
|
||||
warningGeometry,
|
||||
resetResult,
|
||||
beforeState,
|
||||
afterState,
|
||||
browserMessages,
|
||||
}, null, 2)}\n`,
|
||||
{mode: 0o600},
|
||||
);
|
||||
|
||||
await browser.close();
|
||||
console.log(`S100 inheritance stat-reset guard verified: ${screenshotPath}`);
|
||||
@@ -0,0 +1,76 @@
|
||||
import fs from 'node:fs';
|
||||
import {createHash} from 'node:crypto';
|
||||
import {chromium} from 'playwright';
|
||||
|
||||
const baseURL = process.env.REF_BROWSER_URL;
|
||||
const screenshotPath = process.env.REF_SCREENSHOT_PATH;
|
||||
const resultPath = process.env.REF_RESULT_PATH;
|
||||
const username = process.env.REF_USER_ID ?? 's100user01';
|
||||
const password = fs.readFileSync('/run/secrets/test_user_password', 'utf8').trim();
|
||||
|
||||
if (!baseURL || !screenshotPath || !resultPath) {
|
||||
throw new Error('REF_BROWSER_URL, REF_SCREENSHOT_PATH, and REF_RESULT_PATH are required');
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({headless: true});
|
||||
const context = await browser.newContext({
|
||||
viewport: {width: 1280, height: 960},
|
||||
deviceScaleFactor: 1,
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto(baseURL, {waitUntil: 'domcontentloaded', timeout: 60_000});
|
||||
const salt = await page.locator('#global_salt').inputValue();
|
||||
const passwordHash = createHash('sha512')
|
||||
.update(salt + password + salt)
|
||||
.digest('hex');
|
||||
const loginResponse = await page.request.post(
|
||||
new URL('api.php?path=Login/LoginByID', baseURL).href,
|
||||
{data: {username, password: passwordHash}},
|
||||
);
|
||||
const loginResult = await loginResponse.json();
|
||||
if (!loginResponse.ok() || loginResult.result !== true) {
|
||||
throw new Error(`login failed: ${String(loginResult.reason ?? loginResponse.status())}`);
|
||||
}
|
||||
|
||||
const myPageURL = new URL('hwe/b_myPage.php', baseURL).href;
|
||||
const readCooldownText = async () => {
|
||||
await page.goto(myPageURL, {waitUntil: 'networkidle', timeout: 60_000});
|
||||
const bodyText = await page.locator('body').innerText();
|
||||
const lines = bodyText.split('\n').map(line => line.trim());
|
||||
const reselect = lines.find(line => line.startsWith('다른 장수 선택 ('));
|
||||
const deletion = lines.find(line => line.startsWith('가오픈 기간 내 장수 삭제 ('));
|
||||
if (!reselect || !deletion) {
|
||||
throw new Error(`cooldown text missing: ${bodyText.slice(0, 4000)}`);
|
||||
}
|
||||
return {reselect, deletion};
|
||||
};
|
||||
|
||||
const first = await readCooldownText();
|
||||
await page.waitForTimeout(1100);
|
||||
const second = await readCooldownText();
|
||||
await page.waitForTimeout(1100);
|
||||
const third = await readCooldownText();
|
||||
|
||||
if (JSON.stringify(first) !== JSON.stringify(second)
|
||||
|| JSON.stringify(second) !== JSON.stringify(third)
|
||||
) {
|
||||
throw new Error(`cooldown moved after refresh: ${JSON.stringify({first, second, third})}`);
|
||||
}
|
||||
|
||||
await page.screenshot({path: screenshotPath, fullPage: true});
|
||||
fs.writeFileSync(
|
||||
resultPath,
|
||||
`${JSON.stringify({
|
||||
username,
|
||||
url: page.url(),
|
||||
viewport: {width: 1280, height: 960, deviceScaleFactor: 1},
|
||||
first,
|
||||
second,
|
||||
third,
|
||||
}, null, 2)}\n`,
|
||||
{mode: 0o600},
|
||||
);
|
||||
|
||||
await browser.close();
|
||||
console.log(`S100 my-page cooldown refresh guard verified: ${screenshotPath}`);
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use sammo\CentennialAllStarGrowthService;
|
||||
use sammo\DB;
|
||||
use sammo\Event\Action\AdvanceCentennialAllStar;
|
||||
use sammo\GameConst;
|
||||
use sammo\General;
|
||||
use sammo\Json;
|
||||
use sammo\KVStorage;
|
||||
use sammo\RootDB;
|
||||
|
||||
const APP_ROOT = '/var/www/html';
|
||||
const TEST_ENV = ['startyear' => 180, 'year' => 195, 'month' => 1];
|
||||
const STAT_KEYS = ['leadership', 'strength', 'intel'];
|
||||
const DEX_KEYS = ['dex1', 'dex2', 'dex3', 'dex4', 'dex5'];
|
||||
|
||||
$_SERVER['REMOTE_ADDR'] = '127.0.0.1';
|
||||
$_SERVER['REQUEST_URI'] = '/s100-reselection-final-growth-check';
|
||||
|
||||
require APP_ROOT . '/hwe/lib.php';
|
||||
require APP_ROOT . '/hwe/func.php';
|
||||
|
||||
$mode = $argv[1] ?? '';
|
||||
$username = $argv[2] ?? 's100user01';
|
||||
$snapshotPath = $argv[3] ?? '/tmp/s100-reselection-final-growth.json';
|
||||
if (!in_array($mode, ['prepare', 'verify'], true)) {
|
||||
throw new InvalidArgumentException('Mode must be prepare or verify');
|
||||
}
|
||||
if (preg_match('/^s100user[0-9]{2}$/', $username) !== 1) {
|
||||
throw new InvalidArgumentException('Username must match s100userNN');
|
||||
}
|
||||
|
||||
$db = DB::db();
|
||||
$owner = RootDB::db()->queryFirstField(
|
||||
'SELECT no FROM member WHERE id=%s',
|
||||
$username
|
||||
);
|
||||
if ($owner === null) {
|
||||
throw new RuntimeException("No member for {$username}");
|
||||
}
|
||||
$generalID = $db->queryFirstField('SELECT no FROM general WHERE owner=%i', $owner);
|
||||
if ($generalID === null) {
|
||||
throw new RuntimeException("No general for owner {$owner}");
|
||||
}
|
||||
|
||||
if ($mode === 'prepare') {
|
||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||
$gameStor->setValue('year', TEST_ENV['year']);
|
||||
$gameStor->setValue('month', TEST_ENV['month']);
|
||||
|
||||
$eventResult = (new AdvanceCentennialAllStar())->run(TEST_ENV);
|
||||
$general = General::createObjFromDB((int) $generalID);
|
||||
$beforeGrowth = readGeneralState($general);
|
||||
|
||||
$statGrowthKey = null;
|
||||
foreach (STAT_KEYS as $key) {
|
||||
if ((int) $general->getVar($key) < GameConst::$maxLevel) {
|
||||
$statGrowthKey = $key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($statGrowthKey === null) {
|
||||
throw new RuntimeException('No stat can receive an organic level-up');
|
||||
}
|
||||
$general->increaseVar(
|
||||
"{$statGrowthKey}_exp",
|
||||
GameConst::$upgradeLimit
|
||||
);
|
||||
if (!$general->checkStatChange()) {
|
||||
throw new RuntimeException('Organic stat level-up did not occur');
|
||||
}
|
||||
|
||||
$general->addDex($general->getCrewTypeObj(), 12345, false);
|
||||
$general->setAuxVar('next_change', '2000-01-01 00:00:00');
|
||||
$general->applyDB($db);
|
||||
|
||||
$afterGrowth = readGeneralState($general);
|
||||
$snapshot = [
|
||||
'username' => $username,
|
||||
'generalID' => (int) $generalID,
|
||||
'eventResult' => $eventResult,
|
||||
'statGrowthKey' => $statGrowthKey,
|
||||
'beforeGrowth' => $beforeGrowth,
|
||||
'afterGrowth' => $afterGrowth,
|
||||
];
|
||||
if (file_put_contents(
|
||||
$snapshotPath,
|
||||
Json::encode($snapshot, Json::PRETTY) . PHP_EOL
|
||||
) === false) {
|
||||
throw new RuntimeException("Could not write {$snapshotPath}");
|
||||
}
|
||||
chmod($snapshotPath, 0600);
|
||||
printf(
|
||||
"Prepared final-growth reselection: user=%s target=%s stat=%s dexDelta=%d\n",
|
||||
$username,
|
||||
$afterGrowth['targetId'],
|
||||
$statGrowthKey,
|
||||
array_sum($afterGrowth['dex']) - array_sum($beforeGrowth['dex'])
|
||||
);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$snapshot = Json::decode((string) file_get_contents($snapshotPath));
|
||||
$general = General::createObjFromDB((int) $generalID);
|
||||
$actual = readGeneralState($general);
|
||||
$targetInfoRaw = $db->queryFirstField(
|
||||
'SELECT info FROM select_pool WHERE general_id=%i',
|
||||
$generalID
|
||||
);
|
||||
if ($targetInfoRaw === null) {
|
||||
throw new RuntimeException('Reselected target is not assigned to the general');
|
||||
}
|
||||
$targetInfo = Json::decode($targetInfoRaw);
|
||||
$oldTargetId = $snapshot['afterGrowth']['targetId'];
|
||||
$newTargetId = (string) ($targetInfo['uniqueName'] ?? '');
|
||||
if ($newTargetId === '' || $newTargetId === $oldTargetId) {
|
||||
throw new RuntimeException('Chromium reselection did not change the target');
|
||||
}
|
||||
if ($actual['targetId'] !== $newTargetId) {
|
||||
throw new RuntimeException('General aux target does not match the assigned pool target');
|
||||
}
|
||||
|
||||
$expectedStats = CentennialAllStarGrowthService::calculateUserCurrentTargetStats(
|
||||
$targetInfo,
|
||||
TEST_ENV
|
||||
);
|
||||
$expectedDex = [];
|
||||
foreach (STAT_KEYS as $key) {
|
||||
$organic = max(
|
||||
0,
|
||||
(int) $snapshot['afterGrowth']['stats'][$key]
|
||||
- (int) $snapshot['afterGrowth']['granted'][$key]
|
||||
);
|
||||
$expectedStats[$key] = max($organic, $expectedStats[$key]);
|
||||
assertSameValue($expectedStats[$key], $actual['stats'][$key], $key);
|
||||
assertSameValue(
|
||||
$actual['stats'][$key] - $organic,
|
||||
$actual['granted'][$key],
|
||||
"{$key} event grant"
|
||||
);
|
||||
}
|
||||
foreach (DEX_KEYS as $idx => $key) {
|
||||
$organic = max(
|
||||
0,
|
||||
(int) $snapshot['afterGrowth']['dex'][$key]
|
||||
- (int) $snapshot['afterGrowth']['granted'][$key]
|
||||
);
|
||||
$targetFloor = CentennialAllStarGrowthService::calculateDexTargetFloor(
|
||||
(int) ($targetInfo['dex'][$idx] ?? 0),
|
||||
TEST_ENV
|
||||
);
|
||||
$expectedDex[$key] = max($organic, $targetFloor);
|
||||
assertSameValue($expectedDex[$key], $actual['dex'][$key], $key);
|
||||
assertSameValue($targetFloor, $actual['dexFloor'][$key], "{$key} floor");
|
||||
assertSameValue(
|
||||
$actual['dex'][$key] - $organic,
|
||||
$actual['granted'][$key],
|
||||
"{$key} event grant"
|
||||
);
|
||||
}
|
||||
|
||||
printf(
|
||||
"Verified final-growth reselection: user=%s old=%s new=%s stats=%s dex=%s\n",
|
||||
$username,
|
||||
$oldTargetId,
|
||||
$newTargetId,
|
||||
Json::encode($actual['stats']),
|
||||
Json::encode($actual['dex'])
|
||||
);
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* targetId:string,
|
||||
* stats:array<string,int>,
|
||||
* dex:array<string,int>,
|
||||
* granted:array<string,int>,
|
||||
* dexFloor:array<string,int>
|
||||
* }
|
||||
*/
|
||||
function readGeneralState(General $general): array
|
||||
{
|
||||
$aux = $general->getAuxVar(CentennialAllStarGrowthService::AUX_KEY);
|
||||
if (!is_array($aux)) {
|
||||
throw new RuntimeException('Centennial growth ledger is missing');
|
||||
}
|
||||
$stats = [];
|
||||
foreach (STAT_KEYS as $key) {
|
||||
$stats[$key] = (int) $general->getVar($key);
|
||||
}
|
||||
$dex = [];
|
||||
foreach (DEX_KEYS as $key) {
|
||||
$dex[$key] = (int) $general->getVar($key);
|
||||
}
|
||||
return [
|
||||
'targetId' => (string) ($aux['targetId'] ?? ''),
|
||||
'stats' => $stats,
|
||||
'dex' => $dex,
|
||||
'granted' => $aux['granted'] ?? [],
|
||||
'dexFloor' => $aux['dexFloor'] ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
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