feat: -60 turnterm 가변턴

This commit is contained in:
2025-10-29 17:24:25 +00:00
parent d5b2d4e1f2
commit b7c7d6ffce
37 changed files with 424 additions and 61 deletions
+24 -4
View File
@@ -1130,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;
@@ -1177,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) {
@@ -1199,6 +1200,12 @@ function checkDelay()
// 1턴이상 갱신 없었으면 서버 지연
$term = $gameStor->turnterm;
if($term < 0){
//가변 턴에는 수행 안함!
return;
}
if ($term >= 20) {
$threshold = 1;
} else if ($term >= 10) {
@@ -1278,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;
@@ -2232,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자리 소수
@@ -2247,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(
+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'],
+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;
+14 -5
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);
+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
@@ -1,4 +1,5 @@
import { clamp } from 'lodash-es';
export function calcTournamentTerm(turnTerm: number): number{
turnTerm = Math.abs(turnTerm);
return clamp(turnTerm, 5, 120);
}
+260
View File
@@ -0,0 +1,260 @@
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);
}
private static addSecondsUTC(base: Date, sec: number): Date {
return addSeconds(base, sec);
}
private static addDaysUTC(base: Date, days: number): Date {
return addDays(base, days);
}
// ----- 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();
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 = VarTurn60.addSecondsUTC(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(VarTurn60.addSecondsUTC(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 = VarTurn60.addSecondsUTC(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 : VarTurn60.addDaysUTC(this.baseDayUTC, dayDiff);
return new VarTurn60(nextBaseDayUTC, nextTurnIdx, nextSecOffset);
}
}
+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'];