Compare commits

..
13 Commits
49 changed files with 1012 additions and 88 deletions
+48 -4
View File
@@ -923,6 +923,13 @@ function banner()
function addTurn($date, int $turnterm, int $turn = 1, bool $withFraction = true)
{
if($turnterm < 0){
if($turnterm == -60){
return VarTurn60::fromDatetime(new \DateTimeImmutable(($date)))->addTurn($turn )->toDateStr($withFraction);
}
throw new \Exception("InvalidTurnTerm".$turnterm);
}
$date = new \DateTime($date);
$target = $turnterm * $turn;
$date->add(new \DateInterval("PT{$target}M"));
@@ -934,6 +941,13 @@ function addTurn($date, int $turnterm, int $turn = 1, bool $withFraction = true)
function subTurn($date, int $turnterm, int $turn = 1, bool $withFraction = true)
{
if($turnterm < 0){
if($turnterm == -60){
return VarTurn60::fromDatetime(new \DateTimeImmutable(($date)))->addTurn($turn * -1)->toDateStr($withFraction);
}
throw new \Exception("InvalidTurnTerm".$turnterm);
}
$date = new \DateTime($date);
$target = $turnterm * $turn;
$date->sub(new \DateInterval("PT{$target}M"));
@@ -945,6 +959,14 @@ function subTurn($date, int $turnterm, int $turn = 1, bool $withFraction = true)
function cutTurn($date, int $turnterm, bool $withFraction = true)
{
if($turnterm < 0){
if($turnterm == -60){
[$baseDate, ] = VarTurn60::fromDatetime(new \DateTimeImmutable(($date)))->cutTurn($withFraction);
return $baseDate;
}
throw new \Exception("InvalidTurnTerm".$turnterm);
}
$date = new \DateTime($date);
$baseDate = new \DateTime($date->format('Y-m-d'));
@@ -963,6 +985,8 @@ function cutTurn($date, int $turnterm, bool $withFraction = true)
function cutDay($date, int $turnterm, bool $withFraction = true)
{
assert($turnterm > 0);
$date = new \DateTime($date);
$baseDate = new \DateTime($date->format('Y-m-d'));
@@ -1106,6 +1130,7 @@ function CheckOverhead()
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
[$turnterm, $refreshLimit] = $gameStor->getValuesAsArray(['turnterm', 'refreshLimit']);
$turnterm = abs($turnterm);
$nextRefreshLimit = Util::round(pow($turnterm, 0.6) * 3) * GameConst::$refreshLimitCoef;
@@ -1153,7 +1178,7 @@ function timeover(): bool
list($turnterm, $turntime) = $gameStor->getValuesAsArray(['turnterm', 'turntime']);
$diff = (new \DateTime())->getTimestamp() - (new \DateTime($turntime))->getTimestamp();
$t = min($turnterm, 5);
$t = min(abs($turnterm), 5);
$term = $diff;
if ($term >= $t || $term < 0) {
@@ -1175,6 +1200,12 @@ function checkDelay()
// 1턴이상 갱신 없었으면 서버 지연
$term = $gameStor->turnterm;
if($term < 0){
//가변 턴에는 수행 안함!
return;
}
if ($term >= 20) {
$threshold = 1;
} else if ($term >= 10) {
@@ -1254,10 +1285,14 @@ function turnDate($curtime)
$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);
if($term == -60){
$num = VarTurn60::calcTurnDiff(new \DateTimeImmutable($turn), new \DateTimeImmutable($curtime));
}
else{
$curturn = cutTurn($curtime, $admin['turnterm']);
$num = intdiv((strtotime($curturn) - strtotime($turn)), $term * 60);
}
$date = $admin['startyear'] * 12;
$date += $num;
@@ -2208,6 +2243,10 @@ function getRandTurn(RandUtil $rng, $term, ?\DateTimeInterface $baseDateTime = n
throw new MustNotBeReachedException();
}
if($term == -60){
[, $term] = VarTurn60::fromDatetime($baseDateTime)->cutTurn();
}
$randSecond = $rng->nextRangeInt(0, 60 * $term - 1);
$randFraction = $rng->nextRangeInt(0, 999999) / 1000000; //6자리 소수
@@ -2223,6 +2262,11 @@ function getRandTurn2(RandUtil $rng, $term, ?\DateTimeInterface $baseDateTime =
} else {
throw new MustNotBeReachedException();
}
if($term == -60){
[, $term] = VarTurn60::fromDatetime($baseDateTime)->cutTurn();
}
$randSecond = $rng->nextRangeInt(0, 60 * $term - 1);
$randFraction = $rng->nextRangeInt(0, 999999) / 1000000; //6자리 소수
+6 -1
View File
@@ -181,7 +181,12 @@ function info($type = 0)
$admin = $gameStor->getValues(['year', 'month', 'turnterm', 'maxgeneral']);
$termtype = "{$admin['turnterm']}분 턴";
$turnTermText = $admin['turnterm'].'분';
if($admin['turnterm'] < 0){
$turnTermText = "가변 {$admin['turnterm']}분";
}
$termtype = "{$turnTermText}분 턴";
$gencount = $db->queryFirstField('SELECT count(no) FROM general WHERE npc < 2');
$npccount = $db->queryFirstField('SELECT count(no) FROM general WHERE npc >= 2');
+3 -1
View File
@@ -10,6 +10,7 @@ use sammo\Enums\InheritanceKey;
* @return int 토너먼트 초 단위
*/
function calcTournamentTerm(int $turnTerm): int{
$turnTerm = abs($turnTerm);
return Util::valueFit($turnTerm, 5, 120);
}
@@ -138,6 +139,7 @@ function processTournament()
function getTournamentTermText(int $turnTerm)
{
$turnTerm = abs($turnTerm);
$term = calcTournamentTerm($turnTerm);
if ($term % 60 === 0) {
@@ -342,7 +344,7 @@ function startBetting($type)
{
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
[$year, $month, $startyear, $turnterm] = $gameStor->getValuesAsArray(['year', 'month', 'startyear', 'turnterm']);
[$year, $month, $startyear] = $gameStor->getValuesAsArray(['year', 'month', 'startyear']);
pushGlobalHistoryLog([
"<S>◆</>{$year}{$month}월:<B><b>【대회】</b></>우승자를 예상하는 <C>내기</>가 진행중입니다! 호사가의 참여를 기다립니다!"
], $year, $month);
+1
View File
@@ -54,6 +54,7 @@ if ($session->userGrade < 5 && !$allowReset) {
<label for="turnterm" class="col-sm-3 col-form-label">턴 시간(분)</label>
<div class="col-sm-9">
<div id="turnterm" class="btn-group btn-group-toggle" data-bs-toggle="buttons">
<input type="radio" class="btn-check" name="turnterm" id="turnterm_var60" value="-60" checked><label for="turnterm_var60" class="btn btn-secondary">가변60</label>
<input type="radio" class="btn-check" name="turnterm" id="turnterm_120" value="120"><label for="turnterm_120" class="btn btn-secondary">120</label>
<input type="radio" class="btn-check" name="turnterm" id="turnterm_60" value="60" checked><label for="turnterm_60" class="btn btn-secondary">60</label>
<input type="radio" class="btn-check" name="turnterm" id="turnterm_30" value="30"><label for="turnterm_30" class="btn btn-secondary">30</label>
+1
View File
@@ -36,6 +36,7 @@ list(
$turnterm,
$npcmode
) = $gameStor->getValuesAsArray(['maxgeneral', 'turnterm', 'npcmode']);
$turnterm = abs($turnterm);
if($npcmode!=1){
Json::die([
+1 -1
View File
@@ -121,7 +121,7 @@ $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', TimeUtil::nowAddMinutes(12 * abs($env['turnterm'])));
$builder->fillRemainSpecAsZero($env);
$builder->build($env);
$generalID = $builder->getGeneralID();
+1 -1
View File
@@ -60,7 +60,7 @@ $me->setAuxVar('use_auto_nation_turn', $use_auto_nation_turn);
$me->setVar('tnmt', $tnmt);
if ($me->getNPCType() == 1 && $detachNPC) {
$turnterm = $gameStor->turnterm;
$turnterm = abs($gameStor->turnterm);
if ($turnterm < 10) {
$targetKillTurn = 30 / $turnterm;
+1
View File
@@ -39,6 +39,7 @@ list(
$npcmode,
$turnterm
) = $gameStor->getValuesAsArray(['year', 'month', 'maxgeneral', 'npcmode', 'turnterm']);
$turnterm = abs($turnterm);
if($npcmode!=2){
Json::die([
+13 -4
View File
@@ -28,6 +28,7 @@ use sammo\UniqueConst;
use sammo\UserLogger;
use sammo\Util;
use sammo\Validator;
use sammo\VarTurn60;
use sammo\WebUtil;
use function sammo\addTurn;
@@ -355,20 +356,28 @@ class Join extends \sammo\BaseAPI
}
if ($inheritTurntimeZone !== null) {
$inheritTurntime = $inheritTurntimeZone * $admin['turnterm'];
$inheritTurntime += $rng->nextRangeInt(0, Util::clamp($admin['turnterm'] - 1, 0));
$turnterm = abs($admin['turnterm']);
$inheritTurntime = $inheritTurntimeZone * $turnterm;
$inheritTurntime += $rng->nextRangeInt(0, Util::clamp($turnterm - 1, 0));
$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 = new \DateTimeImmutable(cutTurn($admin['turntime'], $turnterm));
$turntime = $turntime->add(TimeUtil::secondsToDateInterval($inheritTurntime));
$turntime = TimeUtil::format($turntime, true);
} else {
$turntime = getRandTurn($rng, $admin['turnterm'], new \DateTimeImmutable($admin['turntime']));
$turntime = getRandTurn($rng, abs($admin['turnterm']), new \DateTimeImmutable($admin['turntime']));
}
if ($admin['turnterm'] == -60){
$baseObj = VarTurn60::fromDatetime(new \DateTimeImmutable($admin['turntime']));
$userTurnObj = VarTurn60::fromDatetime(new \DateTimeImmutable($turntime));
if($baseObj->turnIdx != $userTurnObj->turnIdx){
$turntime = $userTurnObj->addTurn(-1)->toDateStr();
}
}
$now = TimeUtil::now(true);
if ($now >= $turntime) {
@@ -62,7 +62,7 @@ class ResetTurnTime extends \sammo\BaseAPI
}
$gameStor = KVStorage::getStorage($db, 'game_env');
$turnTerm = $gameStor->getValue('turnterm');
$turnTerm = abs($gameStor->getValue('turnterm'));
$currTurnTime = new DateTimeImmutable($general->getTurnTime());
+1 -1
View File
@@ -46,7 +46,7 @@ abstract class AbsFromUserPool extends AbsGeneralPool{
$gameStor = KVStorage::getStorage($db, 'game_env');
$result = [];
$validUntil = TimeUtil::nowAddMinutes(2 * $gameStor->turnterm);
$validUntil = TimeUtil::nowAddMinutes(2 * abs($gameStor->turnterm));
while(count($result) < $pickCnt){
$cand = $rng->choiceUsingWeightPair($pool);
$poolID = $cand['id'];
+4 -4
View File
@@ -164,7 +164,7 @@ abstract class Auction
if ($date === null) {
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
$turnTerm = $gameStor->getValue('turnterm');
$turnTerm = abs($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
));
@@ -324,7 +324,7 @@ abstract class Auction
}
$gameStor = KVStorage::getStorage($db, 'game_env');
$turnTerm = $gameStor->getValue('turnterm');
$turnTerm = abs($gameStor->getValue('turnterm'));
if ($this->info->detail->availableLatestBidCloseDate !== null) {
$extendedCloseDate = $now->add(TimeUtil::secondsToDateInterval(
@@ -437,7 +437,7 @@ abstract class Auction
$general->increaseVar($resType->value, -$morePoint);
$gameStor = KVStorage::getStorage($db, 'game_env');
$turnTerm = $gameStor->getValue('turnterm');
$turnTerm = abs($gameStor->getValue('turnterm'));
$extendedCloseDate = $now->add(TimeUtil::secondsToDateInterval(
max(static::MIN_EXTENSION_MINUTES_PER_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_PER_BID) * 60
));
@@ -471,7 +471,7 @@ abstract class Auction
if ($highestBid->aux->tryExtendCloseDate) {
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
$turnTerm = $gameStor->getValue('turnterm');
$turnTerm = abs($gameStor->getValue('turnterm'));
//연장 요청이 있었다.
$extendedCloseDate = $this->info->closeDate->add(TimeUtil::secondsToDateInterval(
+8 -2
View File
@@ -58,7 +58,7 @@ abstract class AuctionBasicResource extends Auction
$now = new \DateTimeImmutable();
$gameStor = KVStorage::getStorage($db, 'game_env');
$turnTerm = $gameStor->getValue('turnterm');
$turnTerm = abs($gameStor->getValue('turnterm'));
$closeDate = $now->add(TimeUtil::secondsToDateInterval($closeTurnCnt * $turnTerm * 60));
$openResult = static::openAuction(new AuctionInfo(
@@ -246,7 +246,13 @@ 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));
if($turnTerm == -60){
$date = VarTurn60::fromDatetime(new \DateTimeImmutable())->addTurn(1)->toDateStr();
}
else{
$date = (new DateTimeImmutable())->add(TimeUtil::secondsToDateInterval($turnTerm * 60));
}
$this->shrinkCloseDate($date);
}
+3 -2
View File
@@ -75,6 +75,7 @@ class AuctionUniqueItem extends Auction
$now = new DateTimeImmutable();
[$turnTerm, $year, $month] = $gameStor->getValuesAsArray(['turnterm', 'year', 'month']);
$turnTerm = abs($turnTerm);
$closeDate = $now->add(TimeUtil::secondsToDateInterval(
max(static::MIN_AUCTION_CLOSE_MINUTES, $turnTerm * static::COEFF_AUCTION_CLOSE_MINUTES) * 60
@@ -266,7 +267,7 @@ class AuctionUniqueItem extends Auction
}
if ($availableEquipUniqueCnt <= 0) {
$turnTerm = $gameStor->getValue('turnterm');
$turnTerm = abs($gameStor->getValue('turnterm'));
//제한에 걸렸다면 자동 연장
$extendedCloseDate = $this->info->closeDate->add(TimeUtil::secondsToDateInterval(
max(static::MIN_EXTENSION_MINUTES_BY_EXTENSION_QUERY, $turnTerm * static::COEFF_EXTENSION_MINUTES_LIMIT_UNIQUE_CNT) * 60
@@ -314,7 +315,7 @@ class AuctionUniqueItem extends Auction
if (!$availableItemTypes) {
if ($isExtendCloseDateRequired) {
$turnTerm = $gameStor->getValue('turnterm');
$turnTerm = abs($gameStor->getValue('turnterm'));
//동일 부위 제한에 걸렸다면 자동 연장
$extendedCloseDate = $this->info->closeDate->add(TimeUtil::secondsToDateInterval(
max(static::MIN_EXTENSION_MINUTES_LIMIT_BY_BID, $turnTerm * static::COEFF_EXTENSION_MINUTES_LIMIT_BY_BID) * 60
@@ -200,7 +200,7 @@ class che_불가침제의 extends Command\NationCommand
$now = new \DateTime($date);
$validUntil = new \DateTime($date);
$validMinutes = max(30, $env['turnterm'] * 3);
$validMinutes = max(30, abs($env['turnterm']) * 3);
$validUntil->add(new \DateInterval("PT{$validMinutes}M"));
$josaWa = JosaUtil::pick($nationName, '와');
@@ -149,7 +149,7 @@ class che_불가침파기제의 extends Command\NationCommand{
$now = new \DateTime($date);
$validUntil = new \DateTime($date);
$validMinutes = max(30, $env['turnterm']*3);
$validMinutes = max(30, abs($env['turnterm'])*3);
$validUntil->add(new \DateInterval("PT{$validMinutes}M"));
$msg = new DiplomaticMessage(
@@ -147,7 +147,7 @@ class che_종전제의 extends Command\NationCommand{
$now = new \DateTime($date);
$validUntil = new \DateTime($date);
$validMinutes = max(30, $env['turnterm']*3);
$validMinutes = max(30, abs($env['turnterm'])*3);
$validUntil->add(new \DateInterval("PT{$validMinutes}M"));
$msg = new DiplomaticMessage(
@@ -0,0 +1,204 @@
<?php
namespace sammo\Command\Nation;
use \sammo\DB;
use \sammo\Util;
use \sammo\JosaUtil;
use \sammo\General;
use \sammo\DummyGeneral;
use \sammo\ActionLogger;
use \sammo\GameConst;
use \sammo\LastTurn;
use \sammo\GameUnitConst;
use \sammo\Command;
use \sammo\Constraint\Constraint;
use \sammo\Constraint\ConstraintHelper;
use sammo\Enums\GeneralQueryMode;
use sammo\StaticEventHandler;
use function sammo\pullGeneralCommand;
use function sammo\pushGeneralCommand;
class che_행동지시 extends Command\NationCommand
{
static protected $actionName = '행동 지시';
static public $reqArg = true;
protected function argTest(): bool
{
if ($this->arg === null) {
return false;
}
if (!key_exists('isPull', $this->arg)) {
return false;
}
if (!key_exists('amount', $this->arg)) {
return false;
}
if (!key_exists('destGeneralID', $this->arg)) {
return false;
}
$isPull = $this->arg['isPull'];
$amount = $this->arg['amount'];
$destGeneralID = $this->arg['destGeneralID'];
if (!is_numeric($amount)) {
return false;
}
if ($amount <= 0) {
return false;
}
if (!is_bool($isPull)) {
return false;
}
if (!is_int($destGeneralID)) {
return false;
}
if ($destGeneralID <= 0) {
return false;
}
$amount = Util::clamp($amount, 1, 5);
$this->arg = [
'isPull' => $isPull,
'amount' => $amount,
'destGeneralID' => $destGeneralID
];
return true;
}
protected function init()
{
$general = $this->generalObj;
$this->setCity();
$this->setNation();
$this->minConditionConstraints = [
ConstraintHelper::NotBeNeutral(),
ConstraintHelper::OccupiedCity(),
ConstraintHelper::BeChief(),
ConstraintHelper::SuppliedCity(),
];
}
protected function initWithArg()
{
$destGeneral = General::createObjFromDB($this->arg['destGeneralID']);
$this->setDestGeneral($destGeneral);
if($this->arg['destGeneralID'] == $this->getGeneral()->getID()){
$this->fullConditionConstraints=[
ConstraintHelper::AlwaysFail('본인입니다')
];
return;
}
$this->fullConditionConstraints = [
ConstraintHelper::NotBeNeutral(),
ConstraintHelper::OccupiedCity(),
ConstraintHelper::BeChief(),
ConstraintHelper::SuppliedCity(),
ConstraintHelper::ExistsDestGeneral(),
ConstraintHelper::FriendlyDestGeneral()
];
}
public function getCost(): array
{
return [0, 0];
}
public function getPreReqTurn(): int
{
return 0;
}
public function getPostReqTurn(): int
{
return 0;
}
public function getBrief(): string
{
$isPull = $this->arg['isPull'];
$amount = $this->arg['amount'];
$actSpecificName = $isPull ? '당기기' : '미루기';
$destGeneral = $this->destGeneralObj;
return "{$destGeneral->getName()}{$amount}{$actSpecificName}";
}
public function run(\Sammo\RandUtil $rng): bool
{
if (!$this->hasFullConditionMet()) {
throw new \RuntimeException('불가능한 커맨드를 강제로 실행 시도');
}
$db = DB::db();
$general = $this->generalObj;
$date = $general->getTurnTime($general::TURNTIME_HM);
$isPull = $this->arg['isPull'];
$amount = $this->arg['amount'];
$destGeneral = $this->destGeneralObj;
if($isPull){
pullGeneralCommand($destGeneral->getID(), $amount);
}
else{
pushGeneralCommand($destGeneral->getID(), $amount);
}
$logger = $general->getLogger();
$actDestText = $isPull ? '당겼습니다.' : '미루었습니다.';
$actText = $isPull ? '당기도록' : '미루도록';
$destGeneral->getLogger()->pushGeneralActionLog("<Y>{$general->getName()}</>의 지시로 <C>{$amount}</>턴을 {$actDestText}", ActionLogger::PLAIN);
$logger->pushGeneralActionLog("<Y>{$destGeneral->getName()}</>에게 <C>{$amount}</>턴을 {$actText} 지시했습니다. <1>$date</>");
$this->setResultTurn(new LastTurn(static::getName(), $this->arg));
StaticEventHandler::handleEvent($this->generalObj, $this->destGeneralObj, $this::class, $this->env, $this->arg ?? []);
$general->applyDB($db);
$destGeneral->applyDB($db);
return true;
}
public function exportJSVars(): array
{
$db = DB::db();
$nationID = $this->getNationID();
$troops = Util::convertArrayToDict($db->query('SELECT * FROM troop WHERE nation=%i', $nationID), 'troop_leader');
$destRawGenerals = Util::convertArrayToDict($db->queryAllLists('SELECT no,name,officer_level,npc,gold,rice,leadership,strength,intel,city,crew,train,atmos,troop FROM general WHERE nation = %i ORDER BY npc,binary(name)', $nationID), 0);
if($destRawGenerals){
foreach ($db->queryAllLists(
'SELECT general_id, brief FROM general_turn WHERE general_id IN %li AND turn_idx = 0',
array_keys($destRawGenerals)
) as [$generalID, $brief]) {
if (!key_exists($generalID, $destRawGenerals)) {
continue;
}
$destRawGenerals[$generalID][] = $brief;
}
}
return [
'procRes' => [
'troops' => $troops,
'generals' => array_values($destRawGenerals),
'generalsKey' => ['no', 'name', 'officerLevel', 'npc', 'gold', 'rice', 'leadership', 'strength', 'intel', 'cityID', 'crew', 'train', 'atmos', 'troopID', 'turn0Brief'],
'cities' => \sammo\JSOptionsForCities(),
'amountGuide' => [1, 2, 3, 4, 5],
'minAmount' => 1,
'maxAmount' => 5,
]
];
}
}
+1 -1
View File
@@ -120,7 +120,7 @@ class RaiseInvader extends \sammo\Event\Action
$gameStor = KVStorage::getStorage($db, 'game_env');
$gameStor->setValue('isunited', 1);
$turnterm = $gameStor->turnterm;
$turnterm = abs($gameStor->turnterm);
$generalCnt = $db->queryFirstField('SELECT count(*) FROM general');
if ($npcEachCount < 0) {
+1 -1
View File
@@ -134,7 +134,7 @@ class UpdateNationLevel extends \sammo\Event\Action
if ($levelDiff) {
//유니크 아이템 하나 돌리자
$targetKillTurn = $env['killturn'];
$targetKillTurn -= 24 * 60 / $env['turnterm'];
$targetKillTurn -= 24 * 60 / abs($env['turnterm']);
$nationGenIDList = $db->queryFirstColumn(
'SELECT no FROM general WHERE nation = %i AND killturn >= %i AND npc < 2',
$nation['nation'],
+1
View File
@@ -384,6 +384,7 @@ class GameConstBase
'che_포상',
'che_몰수',
'che_부대탈퇴지시',
'che_행동지시',
],
'외교' => [
'che_물자원조',
+7
View File
@@ -283,6 +283,7 @@ class General extends GeneralBase implements iAction
}
$recwar = new \DateTimeImmutable($this->getVar('recent_war'));
$turnNow = new \DateTimeImmutable($this->getVar('turntime'));
$secDiff = TimeUtil::DateIntervalToSeconds($recwar->diff($turnNow));
if ($secDiff <= 0) {
@@ -290,6 +291,12 @@ class General extends GeneralBase implements iAction
return 0;
}
if($turnTerm == -60){
$turnDiff = VarTurn60::calcTurnDiff($recwar, $turnNow);
$this->calcCache[$cacheKey] = $turnDiff;
return $turnDiff;
}
$result = intdiv(Util::toInt($secDiff), 60 * $turnTerm);
$this->calcCache[$cacheKey] = $result;
return $result;
+3 -3
View File
@@ -1980,7 +1980,7 @@ class GeneralAI
$db = DB::db();
$nationStor = KVStorage::getStorage($db, $general->getNationID(), 'nation_env');
$turnTerm = $this->env['turnterm'];
$turnTerm = abs($this->env['turnterm']);
//천도를 한턴 넣었다면 계속 넣는다.
if ($lastTurn->getCommand() === '천도' && $lastTurn->getArg()['destCityID'] != $this->nation['capital']) {
@@ -3715,7 +3715,7 @@ class GeneralAI
$this->updateInstance();
//특별 메세지 있는 경우 출력
$term = $this->env['turnterm'];
$term = abs($this->env['turnterm']);
if ($general->getVar('npcmsg') && $this->rng->nextBool(GameConst::$npcMessageFreqByDay * $term / (60 * 24))) {
$src = new MessageTarget(
$general->getID(),
@@ -3985,7 +3985,7 @@ class GeneralAI
$userChiefCnt = 0;
$minUserKillturn = $this->env['killturn'] - Util::toInt(240 / $this->env['turnterm']);
$minUserKillturn = $this->env['killturn'] - Util::toInt(240 / abs($this->env['turnterm']));
$minNPCKillturn = 36;
foreach (Util::range($minChiefLevel, 12) as $chiefLevel) {
+11 -2
View File
@@ -245,7 +245,16 @@ class ResetHelper{
true
);
if($sync == 0) {
if($turnterm == -60){
[$starttime, $yearPulled, $month] = VarTurn60::fromDatetime(new \DateTimeImmutable($turntime))->cutDay();
if($yearPulled){
$year = $startyear-1;
}
else{
$year = $startyear;
}
}
else if($sync == 0) {
// 현재 시간을 1월로 맞춤
$starttime = cutTurn($turntime, $turnterm);
$month = 1;
@@ -261,7 +270,7 @@ class ResetHelper{
}
}
$killturn = 4800 / $turnterm;
$killturn = 4800 / abs($turnterm);
if($npcmode == 1) { $killturn = intdiv($killturn, 3); }
$develcost = ($year - $startyear + 10) * 2;
+12 -10
View File
@@ -14,12 +14,23 @@ final class ServerTool
if ((120 % $turnterm) != 0) {
return 'invalid minute';
}
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
$admin = $gameStor->getValues(['turntime', 'turnterm', 'year', 'startyear', 'month', 'isunited']);
$oldunit = $admin['turnterm'] * 60;
$unit = $turnterm * 60;
if($unit == $oldunit){
return null;
}
if($oldunit < 0 || $unit < 0){
return 'variable_turnterm';
}
$reqGameLock = $admin['isunited'] != 2 && !$ignoreLock;
$locked = false;
@@ -40,15 +51,6 @@ final class ServerTool
$locked = tryLock();
}
$oldunit = $admin['turnterm'] * 60;
$unit = $turnterm * 60;
if($unit == $oldunit){
if($locked){
unlock();
}
return null;
}
$unitDiff = $unit / $oldunit;
+15 -6
View File
@@ -219,11 +219,20 @@ class TurnExecutionHelper
$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);
$general->setAuxVar('nextTurnTimeBase', null);
if($gameStor->turnterm == -60){
[$turntime, $nextTurnterm] = VarTurn60::fromDatetime(new \DateTimeImmutable($turntime))->cutTurn();
$turntimeObj = new \DateTimeImmutable($turntime);
$turntimeObj = $turntimeObj->add(TimeUtil::secondsToDateInterval($nextTurnTimeBase * $nextTurnterm / 60));
$turntime = TimeUtil::format($turntimeObj, true);
$general->setAuxVar('nextTurnTimeBase', null);
}
else {
$turntime = cutTurn($turntime, $gameStor->turnterm);
$turntimeObj = new \DateTimeImmutable($turntime);
$turntimeObj = $turntimeObj->add(TimeUtil::secondsToDateInterval($nextTurnTimeBase));
$turntime = TimeUtil::format($turntimeObj, true);
$general->setAuxVar('nextTurnTimeBase', null);
}
}
$general->setVar('turntime', $turntime);
@@ -355,7 +364,7 @@ class TurnExecutionHelper
if (($autorun_user['limit_minutes'] ?? false) && $general->getNPCType() < 2 && $hasReservedTurn) {
$autorun_limit = Util::joinYearMonth($year, $month);
$autorun_limit += intdiv($autorun_user['limit_minutes'], $turnterm);
$autorun_limit += intdiv($autorun_user['limit_minutes'], abs($turnterm));
$general->setAuxVar('autorun_limit', $autorun_limit);
}
+191
View File
@@ -0,0 +1,191 @@
<?php
namespace sammo;
class VarTurn60
{
const MonthAdjustOffset = 2;
static array $min30ToTurn = [
[0, 0, 120], //00:00 2
[0, 30, 120], //00:30
[0, 60, 120], //01:00
[0, 90, 120], //01:30
[1, 0, 120], //02:00 3
[1, 30, 120], //02:30
[1, 60, 120], //03:00
[1, 90, 120], //03:30
[2, 0, 120], //04:00 4
[2, 30, 120], //04:30
[2, 60, 120], //05:00
[2, 90, 120], //05:30
[3, 0, 60], //06:00 5
[3, 30, 60], //06:30
[4, 0, 60], //07:00 6
[4, 30, 60], //07:30
[5, 0, 60], //08:00 7
[5, 30, 60], //08:30
[6, 0, 60], //09:00 8
[6, 30, 60], //09:30
[7, 0, 60], //10:00 9
[7, 30, 60], //10:30
[8, 0, 60], //11:00 10
[8, 30, 60], //11:30
[9, 0, 60], //12:00 11
[9, 30, 60], //12:30
[10, 0, 60], //13:00 0(12)
[10, 30, 60], //13:30
[11, 0, 60], //14:00 1(13)
[11, 30, 60], //14:30
[12, 0, 60], //15:00 2(14)
[12, 30, 60], //15:30
[13, 0, 60], //16:00 3(15)
[13, 30, 60], //16:30
[14, 0, 60], //17:00 4(16)
[14, 30, 60], //17:30
[15, 0, 60], //18:00 5(17)
[15, 30, 60], //18:30
[16, 0, 30], //19:00 6(18)
[17, 0, 30], //19:30 7(19)
[18, 0, 30], //20:00 8(20)
[19, 0, 30], //20:30 9(21)
[20, 0, 30], //21:00 10(22)
[21, 0, 30], //21:30 11(23)
[22, 0, 60], //22:00 0
[22, 30, 60], //22:30
[23, 0, 60], //23:00 1
[23, 30, 60], //23:30
//[24, 0, 60], //24:00 2
];
static array $turnToHM = [
[0, 0, 120], //0
[2, 0, 120], //1
[4, 0, 120], //2
[6, 0, 60], //3
[7, 0, 60], //4
[8, 0, 60], //5
[9, 0, 60], //6
[10, 0, 60], //7
[11, 0, 60], //8
[12, 0, 60], //9
[13, 0, 60], //10
[14, 0, 60], //11
[15, 0, 60], //12
[16, 0, 60], //13
[17, 0, 60], //14
[18, 0, 60], //15
[19, 0, 30], //16
[19, 30, 30],//17
[20, 0, 30], //18
[20, 30, 30],//19
[21, 0, 30], //20
[21, 30, 30],//21
[22, 0, 60], //22
[23, 0, 60], //23
//[24, 0, 60], //24(?)
];
function __construct(
public \DateTimeImmutable $baseDay,
public int $turnIdx, //0~23
public float $secOffset, ///turnTerm에 따라 최대치가 30*60, 60*60, 120*60 가변
)
{
}
static function fromDatetime(\DateTimeInterface $date): self {
$baseDay = new \DateTimeImmutable($date->format('Y-m-d'));
$totalSec = $date->getTimestamp() - $baseDay->getTimestamp();
$min30 = intdiv($totalSec, 60 * 30);
assert($min30 < 48);
[$turnIdx, $minOffset, ] = static::$min30ToTurn[$min30];
$secOffset = $totalSec - 60 * 30 * $min30 + $minOffset * 60;
return new static($baseDay, $turnIdx, $secOffset);
}
/// $b - $a 의 턴차
static function calcTurnDiff(\DateTimeInterface $a, \DateTimeInterface $b): int{
$aObj = static::fromDatetime($a);
$bObj = static::fromDatetime($b);
$resDays = $aObj->baseDay->diff($bObj->baseDay)->days; //놀랍게도 절대값
if($aObj->baseDay > $bObj->baseDay){
$resDays *= -1;
}
$res = $resDays * 24 + $bObj->turnIdx - $aObj->turnIdx;
return $res;
}
function cutTurn($withFraction = true): array{
[$hour, $minOffset, $turnTerm] = static::$turnToHM[$this->turnIdx];
$date = $this->baseDay->add(TimeUtil::secondsToDateInterval(($hour * 60 + $minOffset) * 60));
return [TimeUtil::format($date, $withFraction), $turnTerm];
}
function toDateStr($withFraction = true): string{
[$hour, $minOffset, ] = static::$turnToHM[$this->turnIdx];
$date = $this->baseDay->add(TimeUtil::secondsToDateInterval(($hour * 60 + $minOffset) * 60 + $this->secOffset));
return TimeUtil::format($date, $withFraction);
}
function cutDay($withFraction = true): array{
//고정 시간 기준 cutDay()의 이식
//상수 테이블이 이미 13:00에 1월이 되도록 맞춰져 있음.
//다만 20:00인 경우 8월이 아니라 9월이 됨 ^^;
$newMonth = ($this->turnIdx + static::MonthAdjustOffset) % 12 + 1;
$yearPulled = $newMonth > 3;
$obj = $yearPulled ? $this->addTurn(12): $this;
[$date, ] = $obj->addTurn(-($newMonth - 1))->cutTurn($withFraction);
return [$date, $yearPulled, $newMonth];
}
function addTurn(int $moreTurn): self{
$dayDiff = intdiv($moreTurn, 24);
$moreTurn %= 24;
$nextTurnIdx = $this->turnIdx + $moreTurn;
if($nextTurnIdx < 0){
$dayDiff -= 1;
$nextTurnIdx += 24;
}
else if($nextTurnIdx >= 24){
$dayDiff += 1;
$nextTurnIdx -= 24;
}
[, , $oldTurnTerm] = static::$turnToHM[$this->turnIdx];
[, , $nextTurnTerm] = static::$turnToHM[$nextTurnIdx];
$nextSecOffset = $this->secOffset;
if($oldTurnTerm != $nextTurnTerm){
$nextSecOffset *= $nextTurnTerm;
$nextSecOffset /= $oldTurnTerm;
}
if($dayDiff == 0){
return new static($this->baseDay, $nextTurnIdx, $nextSecOffset);
}
if($dayDiff > 0){
$nextBaseDay = $this->baseDay->add(new \DateInterval("P{$dayDiff}D"));
}
else{
$dayAbsDiff = abs($dayDiff);
$nextBaseDay = $this->baseDay->sub(new \DateInterval("P{$dayAbsDiff}D"));
}
return new static($nextBaseDay, $nextTurnIdx, $nextSecOffset);
}
}
+7
View File
@@ -81,6 +81,13 @@
["RaiseNPCNation"],
["DeleteEvent"]
],
[
"month", 1000,
true,
["ChangeCity", "all", {
"trade":100
}]
],
[
"month", 999,
["Date", "==", 181, 1],
+17 -5
View File
@@ -68,7 +68,7 @@ import { formatTime } from "@/util/formatTime";
import { mb_strwidth } from "@/util/mb_strwidth";
import { parseTime } from "@/util/parseTime";
import type { StoredActionsHelper } from "@/util/StoredActionsHelper";
import addMinutes from "date-fns/esm/addMinutes/index";
import { addMinutes } from "date-fns/addMinutes";
import { range } from "lodash-es";
import { inject, onMounted, ref, type PropType } from "vue";
import VueTypes from "vue-types";
@@ -76,6 +76,7 @@ import DragSelect from "@/components/DragSelect.vue";
import { BButton } from "bootstrap-vue-next";
import { QueryActionHelper } from "@/util/QueryActionHelper";
import type { ChiefResponse } from "@/defs/API/NationCommand";
import { VarTurn60 } from "@/varTurn60";
const props = defineProps({
style: VueTypes.object.isRequired,
@@ -198,10 +199,21 @@ if (!props.officer || !props.officer.turnTime) {
}
} else {
const baseTurnTime = parseTime(props.officer.turnTime);
for (const idx of range(props.officer.turn.length)) {
turnTimes.value.push(
formatTime(addMinutes(baseTurnTime, idx * props.turnTerm), props.turnTerm >= 5 ? "HH:mm" : "mm:ss")
);
if(props.turnTerm == -60){
const baseTurnTimeObj = VarTurn60.fromDatetime(baseTurnTime);
for (const idx of range(props.officer.turn.length)) {
turnTimes.value.push(
formatTime(baseTurnTimeObj.addTurn(idx).toDate(), "HH:mm")
);
}
}
else{
for (const idx of range(props.officer.turn.length)) {
turnTimes.value.push(
formatTime(addMinutes(baseTurnTime, idx * props.turnTerm), props.turnTerm >= 5 ? "HH:mm" : "mm:ss")
);
}
}
}
</script>
+2 -3
View File
@@ -310,14 +310,13 @@ declare const staticValues: {
import { reactive, ref, watch } from "vue";
import "@scss/game_bg.scss";
import TopBackBar from "@/components/TopBackBar.vue";
import _ from "lodash-es";
import { sum } from "lodash-es";
import NumberInputWithInfo from "@/components/NumberInputWithInfo.vue";
import { SammoAPI } from "./SammoAPI";
import { InheritResetStat, type inheritBuffType, type InheritPointLogItem } from "./defs/API/InheritAction";
import * as JosaUtil from "@/util/JosaUtil";
import { BButton } from "bootstrap-vue-next";
import { unwrap } from "./util/unwrap";
import { add as dateAdd } from 'date-fns';
const inheritanceViewText: Record<InheritanceViewType, { title: string; info: string }> = {
sum: {
@@ -428,7 +427,7 @@ const title = "유산 관리";
const items = ref(
(() => {
const totalPoint = Math.floor(_.sum(Object.values(staticValues.items)));
const totalPoint = Math.floor(sum(Object.values(staticValues.items)));
const previousPoint = Math.floor(staticValues.items["previous"]);
const newPoint = Math.floor(totalPoint - previousPoint);
const result: Record<InheritanceViewType, number> = {
+1 -1
View File
@@ -485,7 +485,7 @@ watch(inheritCity, (newValue: undefined | number) => {
const inheritTurnTimeZone = ref<number>();
const turnTimeZoneList: string[] = (()=>{
const result: string[] = [];
const zoneSec = turnterm; // * 60 / 60
const zoneSec = Math.abs(turnterm); // * 60 / 60
let zoneCur = 0;
for(const idx of range(60)){
const zoneNext = zoneCur + zoneSec;
+9 -2
View File
@@ -276,7 +276,7 @@ declare const staticValues: {
</script>
<script lang="ts" setup>
import addMinutes from "date-fns/esm/addMinutes";
import { addMinutes } from "date-fns/addMinutes";
import { isString, range, trim } from "lodash-es";
import queryString from "query-string";
import { onMounted, ref, watch } from "vue";
@@ -297,6 +297,7 @@ import { QueryActionHelper } from "./util/QueryActionHelper";
import SimpleClock from "./components/SimpleClock.vue";
import type { ReservedCommandResponse } from "./defs/API/Command";
import { unwrap } from "./util/unwrap";
import { VarTurn60 } from "./varTurn60";
defineExpose({
updateCommandTable,
@@ -544,7 +545,13 @@ async function reloadCommandList() {
});
yearMonth += 1;
nextTurnTime = addMinutes(nextTurnTime, result.turnTerm);
if(result.turnTerm == -60){
nextTurnTime = VarTurn60.fromDatetime(nextTurnTime).addTurn(1).toDate();
}
else{
nextTurnTime = addMinutes(nextTurnTime, result.turnTerm);
}
}
serverNow.value = parseTime(result.date);
+10 -3
View File
@@ -256,7 +256,7 @@
</template>
<script lang="ts" setup>
import addMinutes from "date-fns/esm/addMinutes";
import { addMinutes } from "date-fns/addMinutes";
import queryString from "query-string";
import { onMounted, ref, watch, type PropType, inject, type Ref } from "vue";
import { formatTime } from "@util/formatTime";
@@ -282,6 +282,7 @@ import { unwrap_err } from "@/util/unwrap_err";
import type { GameConstStore } from "@/GameConstStore";
import { postFilterNationCommandGen } from "@/utilGame/postFilterNationCommandGen";
import { unwrap } from "@/util/unwrap";
import { VarTurn60 } from "@/varTurn60";
const toasts = unwrap(useToast());
@@ -536,13 +537,19 @@ function updateCommandList() {
...obj,
year,
month,
time: formatTime(nextTurnTime, props.turnTerm >= 5 ? "HH:mm" : "mm:ss"),
time: formatTime(nextTurnTime, Math.abs(props.turnTerm) >= 5 ? "HH:mm" : "mm:ss"),
tooltip: tooltip.length == 0 ? undefined : tooltip.join("\n"),
style,
});
yearMonth += 1;
nextTurnTime = addMinutes(nextTurnTime, props.turnTerm);
if(props.turnTerm == -60){
nextTurnTime = VarTurn60.fromDatetime(nextTurnTime).addTurn(1).toDate();
}
else{
nextTurnTime = addMinutes(nextTurnTime, props.turnTerm);
}
}
reservedCommandList.value = _reservedCommandList;
updated.value = true;
+1 -1
View File
@@ -23,7 +23,7 @@
<AutorunInfo :autorunMode="globalInfo.autorunUser" />
</div>
<div class="s-border-t col py-2 col-8 col-lg-4 subYearMonth">
현재: {{ globalInfo.year }} {{ globalInfo.month }} ({{ globalInfo.turnterm }} 서버)
현재: {{ globalInfo.year }} {{ globalInfo.month }} ({{ globalInfo.turnterm == -60 ? '가변 60' : globalInfo.turnterm }} 서버)
</div>
<div class="s-border-t col py-2 col-4 col-lg-2 subOnlineUserCnt">
전체 접속자 : {{ (globalInfo.onlineUserCnt ?? 0).toLocaleString() }}
+8 -2
View File
@@ -175,8 +175,9 @@ import { clamp } from "lodash-es";
import { formatCityName } from "@/utilGame/formatCityName";
import { isValidObjKey } from "@/utilGame/isValidObjKey";
import { calcInjury } from "@/utilGame/calcInjury";
import { addMinutes } from "date-fns/esm";
import { addMinutes } from "date-fns/addMinutes";
import type { GameIActionInfo } from "@/defs/GameObj";
import { VarTurn60 } from "@/varTurn60";
const imagePath = window.pathConfig.gameImage;
const gameConstStore = unwrap(inject<Ref<GameConstStore>>("gameConstStore"));
const props = defineProps<{
@@ -278,7 +279,12 @@ watch(
() => {
let turnTime = parseTime(general.value.turntime);
if (turnTime.getTime() < props.lastExecuted.getTime()) {
turnTime = addMinutes(turnTime, props.turnTerm);
if(props.turnTerm == -60){
turnTime = VarTurn60.fromDatetime(turnTime).addTurn(1).toDate();
}
else {
turnTime = addMinutes(turnTime, props.turnTerm);
}
}
nextExecuteMinute.value = Math.floor(clamp((turnTime.getTime() - props.lastExecuted.getTime()) / 60000, 0, 999));
},
+1 -1
View File
@@ -127,7 +127,7 @@
<script setup lang="ts">
import type { MsgItem, MsgTarget, MsgType } from "@/defs/API/Message";
import { parseTime } from "@/util/parseTime";
import { differenceInMilliseconds, addMinutes } from "date-fns/esm";
import { addMinutes, differenceInMilliseconds } from "date-fns";
import { computed, onMounted, ref, toRef, watch, type ComputedRef, type Ref } from "vue";
import linkifyStr from "linkify-string";
import { SammoAPI } from "@/SammoAPI";
+1 -1
View File
@@ -93,7 +93,7 @@ $(function () {
const runAnalysis = async function () {
let realKillturn = killturn;
if(autorun_user && autorun_user.limit_minutes){
realKillturn -= autorun_user.limit_minutes / turnterm;
realKillturn -= autorun_user.limit_minutes / Math.abs(turnterm);
}
const $content = $('#on_mover .content');
try {
+1 -1
View File
@@ -147,7 +147,7 @@ const descriptor: NamedRules<InstallFormType> = {
turnterm: {
required: true,
type: "enum",
enum: [1, 2, 5, 10, 20, 30, 60, 120],
enum: [1, 2, 5, 10, 20, 30, 60, 120, -60],
transform: parseInt,
},
sync: {
+3 -3
View File
@@ -6,7 +6,7 @@ import axios from 'axios';
import { convertFormData } from '@util/convertFormData';
import { isBrightColor } from "@util/isBrightColor";
import { unwrap } from '@util/unwrap';
import _, { isError, isString } from 'lodash-es';
import { isError, isString, last, trim } from 'lodash-es';
import { addMinutes } from 'date-fns';
import { parseTime } from '@util/parseTime';
import { formatTime } from '@util/formatTime';
@@ -234,7 +234,7 @@ function redrawMsg(msgResponse: MsgResponse, addFront: boolean): MsgResponse {
if (!msgList || msgList.length == 0) {
continue;
}
const lastMsg = unwrap(_.last(msgList));
const lastMsg = unwrap(last(msgList));
minMsgSeq[msgType] = Math.min(minMsgSeq[msgType], lastMsg.id);
}
return obj;
@@ -605,7 +605,7 @@ function activateMessageForm() {
$msgSubmit.on('click', async function () {
const text = _.trim(unwrap_any<string>($msgInput.val()));
const text = trim(unwrap_any<string>($msgInput.val()));
$msgInput.val('').trigger('focus');
const targetMailbox = unwrap_any<string>($mailboxList.val());
-1
View File
@@ -10,7 +10,6 @@ import { default as che_장수대상임관 } from "./che_장수대상임관.vue"
import { default as che_징병 } from "./che_징병.vue";
import { default as che_헌납 } from "./che_헌납.vue";
import { default as cr_건국 } from "./cr_건국.vue";
import { default as ProcessCity } from "../ProcessCity.vue";
import { default as ProcessGeneralAmount } from "../ProcessGeneralAmount.vue";
@@ -0,0 +1,118 @@
<template>
<TopBackBar v-model:searchable="searchable" :title="commandName" :type="procEntryMode" />
<div class="bg0">
<div>장수에게 턴을 당기거라 미루라고 지시합니다.</div>
<div class="row">
<div class="col-12 col-lg-5">
장수 :
<SelectGeneral
v-model="selectedGeneralID"
:cities="citiesMap"
:generals="generalList"
:textHelper="textHelpGeneral"
:searchable="searchable"
/>
</div>
<div class="col-2 col-lg-2">
:
<b-button-group>
<b-button :pressed="isPull" @click="isPull = true"> 당기기 </b-button>
<b-button :pressed="!isPull" @click="isPull = false"> 미루기 </b-button>
</b-button-group>
</div>
<div class="col-7 col-lg-3">
<SelectAmount v-model="amount" :amountGuide="amountGuide" :maxAmount="maxAmount" :minAmount="minAmount" />
</div>
<div class="col-3 col-lg-2 d-grid">
<b-button variant="primary" @click="submit">
{{ commandName }}
</b-button>
</div>
</div>
</div>
<BottomBar :title="commandName" :type="procEntryMode" />
</template>
<script lang="ts">
declare const procRes: {
distanceList: Record<number, number[]>;
cities: [number, string][];
generals: procGeneralRawItemList;
generalsKey: procGeneralKey[];
minAmount: number;
maxAmount: number;
amountGuide: number[];
};
declare const staticValues: {
commandName: string;
entryInfo: ["General" | "Nation", unknown];
};
</script>
<script setup lang="ts">
import SelectGeneral from "@/processing/SelectGeneral.vue";
import SelectAmount from "@/processing/SelectAmount.vue";
import { ref } from "vue";
import { unwrap } from "@/util/unwrap";
import type { Args } from "@/processing/args";
import TopBackBar from "@/components/TopBackBar.vue";
import BottomBar from "@/components/BottomBar.vue";
import {
convertGeneralList,
getProcSearchable,
type procGeneralItem,
type procGeneralKey,
type procGeneralRawItemList,
} from "@/processing/processingRes";
import { getNPCColor } from "@/utilGame";
const citiesMap = ref(
new Map<
number,
{
name: string;
info?: string;
}
>()
);
for (const [id, name] of procRes.cities) {
citiesMap.value.set(id, { name });
}
const generalList = convertGeneralList(procRes.generalsKey, procRes.generals);
const amount = ref(1);
const isPull = ref(true);
const selectedGeneralID = ref(generalList[0].no);
type procGeneralItemWithTurn0Brief = procGeneralItem & {
turn0Brief: string;
};
function textHelpGeneral(_gen: procGeneralItem): string {
const gen = _gen as procGeneralItemWithTurn0Brief;
const nameColor = getNPCColor(gen.npc);
const name = nameColor ? `<span style="color:${nameColor}">${gen.name}</span>` : gen.name;
return `${name} [${citiesMap.value.get(unwrap(gen.cityID))?.name}] (${gen.leadership}/${gen.strength}/${
gen.intel
}) (${gen.turn0Brief})`;
}
async function submit(e: Event) {
const event = new CustomEvent<Args>("customSubmit", {
detail: {
amount: amount.value,
isPull: isPull.value,
destGeneralID: selectedGeneralID.value,
},
});
unwrap(e.target).dispatchEvent(event);
}
const { commandName, entryInfo } = staticValues;
const searchable = getProcSearchable();
const procEntryMode: "chief" | "normal" = entryInfo[0] == "Nation" ? "chief" : "normal";
const { minAmount, maxAmount, amountGuide } = procRes;
</script>
+2
View File
@@ -3,6 +3,7 @@ import { default as che_국호변경 } from "./che_국호변경.vue";
import { default as che_물자원조 } from "./che_물자원조.vue";
import { default as che_불가침제의 } from "./che_불가침제의.vue";
import { default as che_피장파장 } from "./che_피장파장.vue";
import { default as che_행동지시 } from "./che_행동지시.vue";
import { default as cr_인구이동 } from "./cr_인구이동.vue";
@@ -33,6 +34,7 @@ export const commandMap: Record<string, typeof ProcessNation | typeof ProcessCit
che_피장파장,
che_허보: ProcessCity,
cr_인구이동,
che_행동지시,
}
/*
+1 -1
View File
@@ -13,7 +13,7 @@ const intArgs = [
] as const;
const booleanArgs = [
'isGold', 'buyRice',
'isGold', 'buyRice', 'isPull',
] as const;
const integerArrayArgs = [
+7 -1
View File
@@ -1,5 +1,11 @@
import {parseISO} from 'date-fns';
export function parseTime(dateString: string): Date{
return parseISO(dateString);
if(dateString === null){
console.warn('why null');
console.trace();
return new Date();
}
const tmp = parseISO(dateString);
return tmp;
}
+1
View File
@@ -1,4 +1,5 @@
import { clamp } from 'lodash-es';
export function calcTournamentTerm(turnTerm: number): number{
turnTerm = Math.abs(turnTerm);
return clamp(turnTerm, 5, 120);
}
+252
View File
@@ -0,0 +1,252 @@
import {
addSeconds,
addDays,
differenceInCalendarDays,
} from 'date-fns';
import {
toZonedTime,
fromZonedTime,
} from 'date-fns-tz';
import { formatTime } from './util/formatTime';
import { parse as parseDF, isValid } from 'date-fns';
export const SEOUL_TZ = 'Asia/Seoul';
// 문자열이 타임존/오프셋을 포함하는지 단순 판별 (ISO 형태 위주)
const TZ_OFFSET_RE = /([zZ]|[+-]\d{2}:?\d{2})$/;
function tryParseWithFormats(s: string): Date | null {
const fmts = [
"yyyy-MM-dd'T'HH:mm:ss", // 2025-10-31T09:30:00
'yyyy-MM-dd HH:mm:ss', // 2025-10-31 09:30:00
"yyyy-MM-dd'T'HH:mm", // 2025-10-31T09:30
'yyyy-MM-dd HH:mm', // 2025-10-31 09:30
'yyyy-MM-dd', // 2025-10-31
];
for (const fmt of fmts) {
const d = parseDF(s, fmt, new Date(0));
if (isValid(d)) return d;
}
return null;
}
/** 문자열/Date/number 입력을 UTC Date로 정규화
* - 문자열에 오프셋/타임존이 없으면 Asia/Seoul 기준 “벽시계 시간”으로 간주
* - number는 epoch ms로 간주 (epoch s면 직접 *1000 해서 넘겨줘)
*/
export function toUTC(input: string | Date | number): Date {
if (input instanceof Date) {
return new Date(input.getTime()); // 이미 절대시간
}
if (typeof input === 'number') {
return new Date(input); // epoch ms
}
const s = input.trim();
// 오프셋/타임존이 명시된 ISO라면 기본 파서로 (절대시간 유지)
if (TZ_OFFSET_RE.test(s)) {
const d = new Date(s);
if (isValid(d)) return d;
}
// 오프셋이 없는 문자열 → 서울 벽시계로 파싱해서 UTC로 변환
const parsedLocal = tryParseWithFormats(s) ?? new Date(s);
if (!isValid(parsedLocal)) {
throw new Error(`Unparsable date string: "${input}"`);
}
// parsedLocal는 "그 문자열 그대로의 Y/M/D h:m:s"를 들고 있음(타임존 無)
// 이를 Asia/Seoul 로컬로 간주해 UTC로 변환
const utc = fromZonedTime(parsedLocal, SEOUL_TZ);
return utc;
}
export class VarTurn60 {
static readonly TZ = 'Asia/Seoul';
static readonly MonthAdjustOffset = 2;
// [turnIdx, minOffset, turnTermMinutes]
static readonly min30ToTurn: Array<[number, number, number]> = [
[0, 0, 120],
[0, 30, 120],
[0, 60, 120],
[0, 90, 120],
[1, 0, 120],
[1, 30, 120],
[1, 60, 120],
[1, 90, 120],
[2, 0, 120],
[2, 30, 120],
[2, 60, 120],
[2, 90, 120],
[3, 0, 60],
[3, 30, 60],
[4, 0, 60],
[4, 30, 60],
[5, 0, 60],
[5, 30, 60],
[6, 0, 60],
[6, 30, 60],
[7, 0, 60],
[7, 30, 60],
[8, 0, 60],
[8, 30, 60],
[9, 0, 60],
[9, 30, 60],
[10, 0, 60],
[10, 30, 60],
[11, 0, 60],
[11, 30, 60],
[12, 0, 60],
[12, 30, 60],
[13, 0, 60],
[13, 30, 60],
[14, 0, 60],
[14, 30, 60],
[15, 0, 60],
[15, 30, 60],
[16, 0, 30],
[17, 0, 30],
[18, 0, 30],
[19, 0, 30],
[20, 0, 30],
[21, 0, 30],
[22, 0, 60],
[22, 30, 60],
[23, 0, 60],
[23, 30, 60],
];
// [hour, minOffset, turnTermMinutes] for turnIdx 0..23
static readonly turnToHM: Array<[number, number, number]> = [
[0, 0, 120],
[2, 0, 120],
[4, 0, 120],
[6, 0, 60],
[7, 0, 60],
[8, 0, 60],
[9, 0, 60],
[10, 0, 60],
[11, 0, 60],
[12, 0, 60],
[13, 0, 60],
[14, 0, 60],
[15, 0, 60],
[16, 0, 60],
[17, 0, 60],
[18, 0, 60],
[19, 0, 30],
[19, 30, 30],
[20, 0, 30],
[20, 30, 30],
[21, 0, 30],
[21, 30, 30],
[22, 0, 60],
[23, 0, 60],
];
constructor(
public readonly baseDayUTC: Date, // UTC 기준 하루의 00:00 (Seoul 기준)
public readonly turnIdx: number, // 0..23
public readonly secOffset: number // seconds within the turn
) { }
// ----- Utilities -----
/** date → 서울 자정 (UTC 기준 Date) */
private static toSeoulMidnightUTC(dateUTC: Date): Date {
const zoned = toZonedTime(dateUTC, this.TZ);
const midnightInSeoul = new Date(
zoned.getFullYear(), zoned.getMonth(), zoned.getDate(), 0, 0, 0, 0
);
return fromZonedTime(midnightInSeoul, this.TZ);
}
private static format(dateUTC: Date, withFraction = true): string {
const zoned = toZonedTime(dateUTC, this.TZ);
return formatTime(zoned, withFraction);
}
// ----- Core Logic -----
static fromDatetime(input: string | Date | number): VarTurn60 {
const utc = toUTC(input);
const baseDayUTC = this.toSeoulMidnightUTC(utc);
const zoned = toZonedTime(utc, this.TZ);
const totalSec = zoned.getHours() * 3600 + zoned.getMinutes() * 60 + zoned.getSeconds() + zoned.getMilliseconds() / 1000;
const min30 = Math.floor(totalSec / 1800);
if (min30 < 0 || min30 >= 48) throw new RangeError('half-hour index out of range');
const [turnIdx, minOffset] = this.min30ToTurn[min30];
const secOffset = totalSec - 1800 * min30 + minOffset * 60;
return new VarTurn60(baseDayUTC, turnIdx, secOffset);
}
static calcTurnDiff(a: string | Date | number, b: string | Date | number): number {
const aObj = this.fromDatetime(a);
const bObj = this.fromDatetime(b);
const dayDiff = differenceInCalendarDays(bObj.baseDayUTC, aObj.baseDayUTC);
return dayDiff * 24 + (bObj.turnIdx - aObj.turnIdx);
}
cutTurn(withFraction = true): [string, number] {
const [hour, minOffset, turnTerm] = VarTurn60.turnToHM[this.turnIdx];
const sec = (hour * 60 + minOffset) * 60;
const date = addSeconds(this.baseDayUTC, sec);
return [VarTurn60.format(date, withFraction), turnTerm];
}
toDate(): Date {
const [hour, minOffset] = VarTurn60.turnToHM[this.turnIdx];
const sec = (hour * 60 + minOffset) * 60 + this.secOffset;
const date = toZonedTime(addSeconds(this.baseDayUTC, sec), SEOUL_TZ);
return date;
}
toDateStr(withFraction = true): string {
const [hour, minOffset] = VarTurn60.turnToHM[this.turnIdx];
const sec = (hour * 60 + minOffset) * 60 + this.secOffset;
const date = addSeconds(this.baseDayUTC, sec);
return VarTurn60.format(date, withFraction);
}
cutDay(withFraction = true): [string, boolean, number] {
const newMonth = ((this.turnIdx + VarTurn60.MonthAdjustOffset) % 12) + 1;
const moved = this.addTurn(-(newMonth - 1));
const [dateStr] = moved.cutTurn(withFraction);
const yearPulled = newMonth > 3;
return [dateStr, yearPulled, newMonth];
}
addTurn(moreTurn: number): VarTurn60 {
let dayDiff = Math.trunc(moreTurn / 24);
moreTurn %= 24;
let nextTurnIdx = this.turnIdx + moreTurn;
if (nextTurnIdx < 0) {
dayDiff -= 1;
nextTurnIdx += 24;
} else if (nextTurnIdx >= 24) {
dayDiff += 1;
nextTurnIdx -= 24;
}
const [, , oldTurnTerm] = VarTurn60.turnToHM[this.turnIdx];
const [, , nextTurnTerm] = VarTurn60.turnToHM[nextTurnIdx];
let nextSecOffset = this.secOffset;
if (oldTurnTerm !== nextTurnTerm) {
nextSecOffset = (nextSecOffset * nextTurnTerm) / oldTurnTerm;
const cap = nextTurnTerm * 60 - 1;
if (nextSecOffset < 0) nextSecOffset = 0;
if (nextSecOffset > cap) nextSecOffset = cap;
}
const nextBaseDayUTC =
dayDiff === 0 ? this.baseDayUTC : addDays(this.baseDayUTC, dayDiff);
return new VarTurn60(nextBaseDayUTC, nextTurnIdx, nextSecOffset);
}
}
+26 -12
View File
@@ -52,7 +52,8 @@
"css-color-names": "^1.0.1",
"css-loader": "^6.7.3",
"cssnano": "^5.1.15",
"date-fns": "^2.29.3",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"denque": "^2.1.0",
"detect-it": "^4.0.1",
"downloadjs": "^1.4.7",
@@ -3535,15 +3536,22 @@
"peer": true
},
"node_modules/date-fns": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
"engines": {
"node": ">=0.11"
},
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/date-fns-tz": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
"integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
"license": "MIT",
"peerDependencies": {
"date-fns": "^3.0.0 || ^4.0.0"
}
},
"node_modules/debug": {
@@ -12089,9 +12097,15 @@
"peer": true
},
"date-fns": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="
},
"date-fns-tz": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
"integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
"requires": {}
},
"debug": {
"version": "4.3.4",
+2 -1
View File
@@ -65,7 +65,8 @@
"css-color-names": "^1.0.1",
"css-loader": "^6.7.3",
"cssnano": "^5.1.15",
"date-fns": "^2.29.3",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"denque": "^2.1.0",
"detect-it": "^4.0.1",
"downloadjs": "^1.4.7",
+1 -1
View File
@@ -272,7 +272,7 @@ class Session
return $this;
}
$turnterm = $gameStor->turnterm;
$turnterm = abs($gameStor->turnterm);
$isUnited = $gameStor->isunited != 0;
$generalID = $general['no'];