feat: Vue3로 새롭게 작성한 메인 페이지 (#229)
- v_front.php - API 추가 - General/GetFrontInfo - Global/GetGlobalMenu - DTO 추가 - MenuItem, MenuMulti, MenuSplit - 글로벌 메뉴 출력 방식 변경 - d_setting/GlobalMenu.php Reviewed-on: https://storage.hided.net/gitea/devsam/core/pulls/229
This commit was merged in pull request #229.
This commit is contained in:
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace sammo;
|
||||||
|
|
||||||
|
use sammo\DTO\MenuItem;
|
||||||
|
use sammo\DTO\MenuMulti;
|
||||||
|
use sammo\DTO\MenuSplit;
|
||||||
|
|
||||||
|
//변경을 원한다면 파일을 복제 후 수정
|
||||||
|
|
||||||
|
class GlobalMenu {
|
||||||
|
/** @var (MenuItem|MenuMulti|MenuSplit)[] */
|
||||||
|
|
||||||
|
const version = 3;
|
||||||
|
static ?array $menu = null;
|
||||||
|
public static function getMenu(): array{
|
||||||
|
if(static::$menu !== null){
|
||||||
|
return static::$menu;
|
||||||
|
}
|
||||||
|
static::$menu = [
|
||||||
|
new MenuItem('천통국 베팅', 'v_nationBetting.php', condHighlightVar: 'nationBetting'),
|
||||||
|
new MenuMulti('게임정보', [
|
||||||
|
new MenuItem('세력일람', 'a_kingdomList.php', newTab: true),
|
||||||
|
new MenuItem('장수일람', 'a_genList.php', newTab: true),
|
||||||
|
new MenuItem('명장일람', 'a_bestGeneral.php', newTab: true),
|
||||||
|
new MenuItem('명예의전당', 'a_hallOfFame.php', newTab: true),
|
||||||
|
new MenuItem('왕조일람', 'a_emperior.php', newTab: true),
|
||||||
|
]),
|
||||||
|
new MenuItem('연감', 'v_history.php', newTab: true),
|
||||||
|
new MenuSplit(
|
||||||
|
new MenuItem('게시판', '/board/community', newTab: true),
|
||||||
|
[
|
||||||
|
new MenuItem('건의/제안', '/board/request', newTab: true),
|
||||||
|
new MenuItem('팁/강좌', '/board/tip', newTab: true),
|
||||||
|
new MenuItem('패치 내역', '/board/patch', newTab: true),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
new MenuSplit(
|
||||||
|
new MenuItem('공식 오픈 톡', 'https://open.kakao.com/o/', newTab: true),
|
||||||
|
[
|
||||||
|
new MenuItem('잡담 오픈 톡', 'https://open.kakao.com/o/', newTab: true)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
new MenuItem('전투 시뮬레이터', 'battle_simulator.php', newTab: true),
|
||||||
|
new MenuMulti('기타 정보', [
|
||||||
|
new MenuItem('접속량정보', 'a_traffic.php', newTab: true),
|
||||||
|
new MenuItem('빙의일람', 'a_npcList', newTab: true, condShowVar: 'npcMode'),
|
||||||
|
]),
|
||||||
|
new MenuItem('설문조사', 'v_vote.php', newTab: true, condHighlightVar: 'vote'),
|
||||||
|
];
|
||||||
|
return static::$menu;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,574 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace sammo\API\General;
|
||||||
|
|
||||||
|
use DateTimeInterface;
|
||||||
|
use MeekroDB;
|
||||||
|
use sammo\CityConst;
|
||||||
|
use sammo\DB;
|
||||||
|
use sammo\DTO\VoteInfo;
|
||||||
|
use sammo\Enums\APIRecoveryType;
|
||||||
|
use sammo\Enums\CityColumn;
|
||||||
|
use sammo\Enums\GeneralColumn;
|
||||||
|
use sammo\Enums\RankColumn;
|
||||||
|
use sammo\GameConst;
|
||||||
|
use sammo\General;
|
||||||
|
use sammo\KVStorage;
|
||||||
|
use sammo\LastTurn;
|
||||||
|
use sammo\Validator;
|
||||||
|
|
||||||
|
use sammo\Session;
|
||||||
|
use sammo\TimeUtil;
|
||||||
|
use sammo\Util;
|
||||||
|
|
||||||
|
use function sammo\buildNationCommandClass;
|
||||||
|
use function sammo\checkLimit;
|
||||||
|
use function sammo\getTournamentTermText;
|
||||||
|
use function sammo\buildNationTypeClass;
|
||||||
|
use function sammo\calcLeadershipBonus;
|
||||||
|
use function sammo\checkSecretPermission;
|
||||||
|
use function sammo\getBillByLevel;
|
||||||
|
use function sammo\getDed;
|
||||||
|
use function sammo\getHonor;
|
||||||
|
use function sammo\getNationStaticInfo;
|
||||||
|
use function sammo\getOfficerLevelText;
|
||||||
|
use function sammo\increaseRefresh;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 통째로 메인페이지의 주요 정보를 반환하는 날로먹는 API
|
||||||
|
*/
|
||||||
|
class GetFrontInfo extends \sammo\BaseAPI
|
||||||
|
{
|
||||||
|
const ROW_LIMIT = 15;
|
||||||
|
|
||||||
|
public function getRequiredSessionMode(): int
|
||||||
|
{
|
||||||
|
return static::REQ_GAME_LOGIN | static::REQ_READ_ONLY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateArgs(): ?string
|
||||||
|
{
|
||||||
|
$v = new Validator($this->args);
|
||||||
|
$v->rule('lengthMin', 'lastNationNoticeDate', 19)
|
||||||
|
->rule('integer', 'lastGeneralRecordID')
|
||||||
|
->rule('integer', 'lastWorldHistoryID');
|
||||||
|
if (!$v->validate()) {
|
||||||
|
return $v->errorStr();
|
||||||
|
}
|
||||||
|
$this->args['lastGeneralRecordID'] = (int)($this->args['lastGeneralRecordID'] ?? 0);
|
||||||
|
$this->args['lastWorldHistoryID'] = (int)($this->args['lastWorldHistoryID'] ?? 0);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getGlobalRecord(int $lastRecordID): array
|
||||||
|
{
|
||||||
|
$db = DB::db();
|
||||||
|
|
||||||
|
$globalRecord = $db->queryAllLists(
|
||||||
|
'SELECT id, `text` FROM general_record WHERE `general_id` = 0 AND log_type = %s AND id >= %i ORDER BY `id` DESC LIMIT %i',
|
||||||
|
'history',
|
||||||
|
$lastRecordID,
|
||||||
|
static::ROW_LIMIT + 1,
|
||||||
|
);
|
||||||
|
return $globalRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getGeneralRecord(int $generalID, int $lastRecordID): array
|
||||||
|
{
|
||||||
|
$db = DB::db();
|
||||||
|
|
||||||
|
$generalRecord = $db->queryAllLists(
|
||||||
|
'SELECT id, `text` FROM general_record WHERE `general_id` = %i AND log_type = %s AND id >= %i ORDER BY `id` DESC LIMIT %i',
|
||||||
|
$generalID,
|
||||||
|
'action',
|
||||||
|
$lastRecordID,
|
||||||
|
static::ROW_LIMIT + 1,
|
||||||
|
);
|
||||||
|
return $generalRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getHistory(int $lastHistoryID): array
|
||||||
|
{
|
||||||
|
$db = DB::db();
|
||||||
|
|
||||||
|
$history = $db->queryAllLists(
|
||||||
|
'SELECT id, `text` FROM world_history WHERE nation_id = 0 AND id >= %i ORDER BY `id` DESC LIMIT %i',
|
||||||
|
$lastHistoryID,
|
||||||
|
static::ROW_LIMIT + 1,
|
||||||
|
);
|
||||||
|
return $history;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateRecentRecord(int $generalID){
|
||||||
|
$db = DB::db();
|
||||||
|
|
||||||
|
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||||
|
$gameStor->cacheValues(['isunited', 'opentime', 'refresh']);
|
||||||
|
|
||||||
|
$lastHistoryID = $this->args['lastWorldHistoryID'];
|
||||||
|
$lastRecordID = $this->args['lastGeneralRecordID'];
|
||||||
|
|
||||||
|
$history = $this->getHistory($lastHistoryID);
|
||||||
|
$globalRecord = $this->getGlobalRecord($lastRecordID);
|
||||||
|
$generalRecord = $this->getGeneralRecord($generalID, $lastRecordID);
|
||||||
|
|
||||||
|
$flushHistory = false;
|
||||||
|
$flushGlobalRecord = false;
|
||||||
|
$flushGeneralRecord = false;
|
||||||
|
|
||||||
|
if (!$history) {
|
||||||
|
$flushHistory = false;
|
||||||
|
} else if (Util::array_last($history)[0] <= $lastHistoryID) {
|
||||||
|
$flushHistory = false;
|
||||||
|
array_pop($history);
|
||||||
|
} else if (count($history) > static::ROW_LIMIT) {
|
||||||
|
array_pop($history);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$globalRecord) {
|
||||||
|
$flushGlobalRecord = false;
|
||||||
|
} else if (Util::array_last($globalRecord)[0] == $lastRecordID) {
|
||||||
|
$flushGlobalRecord = false;
|
||||||
|
array_pop($globalRecord);
|
||||||
|
} else if (count($globalRecord) > static::ROW_LIMIT) {
|
||||||
|
array_pop($globalRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$generalRecord) {
|
||||||
|
$flushGeneralRecord = false;
|
||||||
|
} else if (Util::array_last($generalRecord)[0] == $lastRecordID) {
|
||||||
|
$flushGeneralRecord = false;
|
||||||
|
array_pop($generalRecord);
|
||||||
|
} else if (count($generalRecord) > static::ROW_LIMIT) {
|
||||||
|
array_pop($generalRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'history' => $history,
|
||||||
|
'global' => $globalRecord,
|
||||||
|
'general' => $generalRecord,
|
||||||
|
'flushHistory' => $flushHistory ? 1 : 0,
|
||||||
|
'flushGlobal' => $flushGlobalRecord ? 1 : 0,
|
||||||
|
'flushGeneral' => $flushGeneralRecord ? 1 : 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateGlobalInfo(MeekroDB $db): array
|
||||||
|
{
|
||||||
|
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||||
|
|
||||||
|
[
|
||||||
|
$scenarioText, $extendedGeneral, $isFiction, $npcMode,
|
||||||
|
$joinMode, $autorunUser, $turnterm, $lastExecuted,
|
||||||
|
$lastVoteID, $develCost, $noticeMsg,
|
||||||
|
$onlineNations, $onlineUserCnt,
|
||||||
|
$year, $month, $startYear,
|
||||||
|
$generalCntLimit,
|
||||||
|
$apiLimit,
|
||||||
|
$serverCnt,
|
||||||
|
] = $gameStor->getValuesAsArray([
|
||||||
|
'scenario_text', 'extended_general', 'fiction', 'npcmode',
|
||||||
|
'join_mode', 'autorun_user', 'turnterm', 'turntime',
|
||||||
|
'lastVote', 'develcost', 'msg',
|
||||||
|
'online_nation', 'online_user_cnt',
|
||||||
|
'year', 'month', 'startyear',
|
||||||
|
'maxgeneral',
|
||||||
|
'conlimit',
|
||||||
|
'server_cnt',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$lastVote = null;
|
||||||
|
if ($lastVoteID) {
|
||||||
|
$voteStor = KVStorage::getStorage($db, 'vote');
|
||||||
|
$lastVote = VoteInfo::fromArray($voteStor->getValue("vote_{$lastVoteID}"));
|
||||||
|
if ($lastVote->endDate && $lastVote->endDate < TimeUtil::now()) {
|
||||||
|
$lastVote = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$auctionCount = $db->queryFirstField('SELECT count(*) FROM ng_auction WHERE finished = 0');
|
||||||
|
$isTournamentActive = $gameStor->tournament > 0;
|
||||||
|
$isTournamentApplicationOpen = $gameStor->tournament == 1;
|
||||||
|
$isBettingActive = $gameStor->tournament == 6;
|
||||||
|
$tournamentType = $gameStor->tnmt_type;
|
||||||
|
$tournamentState = $gameStor->tournament;
|
||||||
|
$tournamentTime = $gameStor->tnmt_time;
|
||||||
|
$isLocked = boolval($db->queryFirstField('SELECT plock FROM plock WHERE `type`="GAME" LIMIT 1'));
|
||||||
|
|
||||||
|
$globalGenCount = $db->queryAllLists('SELECT npc, count(no) FROM general GROUP BY npc');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'scenarioText' => $scenarioText,
|
||||||
|
'extendedGeneral' => $extendedGeneral,
|
||||||
|
'isFiction' => $isFiction,
|
||||||
|
'npcMode' => $npcMode,
|
||||||
|
'joinMode' => $joinMode,
|
||||||
|
'startyear' => $startYear,
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month,
|
||||||
|
'autorunUser' => $autorunUser,
|
||||||
|
'turnterm' => $turnterm,
|
||||||
|
'lastExecuted' => $lastExecuted,
|
||||||
|
'lastVoteID' => $lastVoteID,
|
||||||
|
'develCost' => $develCost,
|
||||||
|
'noticeMsg' => $noticeMsg,
|
||||||
|
'onlineNations' => $onlineNations,
|
||||||
|
'onlineUserCnt' => $onlineUserCnt,
|
||||||
|
'apiLimit' => $apiLimit,
|
||||||
|
'auctionCount' => $auctionCount,
|
||||||
|
'isTournamentActive' => $isTournamentActive,
|
||||||
|
'isTournamentApplicationOpen' => $isTournamentApplicationOpen,
|
||||||
|
'isBettingActive' => $isBettingActive,
|
||||||
|
'isLocked' => $isLocked,
|
||||||
|
'tournamentType' => $tournamentType,
|
||||||
|
'tournamentState' => $tournamentState,
|
||||||
|
'tournamentTime' => $tournamentTime,
|
||||||
|
'genCount' => $globalGenCount,
|
||||||
|
'generalCntLimit' => $generalCntLimit,
|
||||||
|
'serverCnt' => $serverCnt,
|
||||||
|
'lastVote' => $lastVote === null ? null : $lastVote->toArray(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateNationInfo(MeekroDB $db, General $general, array $rawNation): array
|
||||||
|
{
|
||||||
|
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||||
|
$admin = $gameStor->getAll(true);
|
||||||
|
|
||||||
|
$lastNationNoticeDate = $this->args['lastNationNoticeDate'] ?? '2022-08-19 00:00:00';
|
||||||
|
|
||||||
|
$nationID = $general->getNationID();
|
||||||
|
|
||||||
|
//XXX: 매번 더하는가?
|
||||||
|
$nationPopulation = $db->queryFirstRow(
|
||||||
|
'SELECT COUNT(*) as cityCnt, CAST(SUM(pop) AS INTEGER) as `now`, CAST(SUM(pop_max) AS INTEGER) as `max` from city where nation=%i',
|
||||||
|
$nationID
|
||||||
|
);
|
||||||
|
|
||||||
|
//XXX: 매번 더하는가?
|
||||||
|
$nationCrew = $db->queryFirstRow(
|
||||||
|
'SELECT COUNT(*) as generalCnt, CAST(SUM(crew) AS INTEGER) as `now`,CAST(SUM(leadership)*100 AS INTEGER) as `max` from general where nation=%i AND npc != 5',
|
||||||
|
$nationID
|
||||||
|
);
|
||||||
|
|
||||||
|
$nationStor = KVStorage::getStorage($db, $nationID, 'nation_env');
|
||||||
|
[$nationNotice, $onlineGen] = $nationStor->getValuesAsArray(['nationNotice', 'online_genenerals']);
|
||||||
|
if (!$nationNotice || ($nationNotice['date'] ?? '') <= $lastNationNoticeDate) {
|
||||||
|
$nationNotice = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$topChiefs = Util::convertArrayToDict($db->query(
|
||||||
|
'SELECT officer_level, no, name, npc FROM general WHERE nation = %i AND officer_level >= 11',
|
||||||
|
$nationID
|
||||||
|
), 'officer_level');
|
||||||
|
|
||||||
|
$impossibleStrategicCommandLists = [];
|
||||||
|
$strategicCommandLists = GameConst::$availableChiefCommand['전략'];
|
||||||
|
$yearMonth = Util::joinYearMonth($admin['year'], $admin['month']);
|
||||||
|
foreach ($strategicCommandLists as $command) {
|
||||||
|
$cmd = buildNationCommandClass($command, $general, $admin, new LastTurn());
|
||||||
|
$nextAvailableTurn = $cmd->getNextAvailableTurn();
|
||||||
|
if ($nextAvailableTurn > $yearMonth) {
|
||||||
|
$impossibleStrategicCommandLists[] = [$cmd->getName(), $nextAvailableTurn - $yearMonth];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$nationClass = buildNationTypeClass($rawNation['type']);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $nationID,
|
||||||
|
'full' => true,
|
||||||
|
'name' => $rawNation['name'],
|
||||||
|
'population' => $nationPopulation,
|
||||||
|
'crew' => $nationCrew,
|
||||||
|
'type' => [
|
||||||
|
'raw' => $rawNation['type'],
|
||||||
|
'name' => $nationClass->getName(),
|
||||||
|
'pros' => $nationClass::$pros,
|
||||||
|
'cons' => $nationClass::$cons,
|
||||||
|
],
|
||||||
|
'color' => $rawNation['color'],
|
||||||
|
'level' => $rawNation['level'],
|
||||||
|
'capital' => $rawNation['capital'],
|
||||||
|
'gold' => $rawNation['gold'],
|
||||||
|
'rice' => $rawNation['rice'],
|
||||||
|
'tech' => $rawNation['tech'],
|
||||||
|
'gennum' => $rawNation['gennum'],
|
||||||
|
'power' => $rawNation['power'],
|
||||||
|
|
||||||
|
'bill' => $rawNation['bill'],
|
||||||
|
'taxRate' => $rawNation['rate'],
|
||||||
|
'onlineGen' => $onlineGen,
|
||||||
|
'notice' => $nationNotice,
|
||||||
|
'topChiefs' => $topChiefs,
|
||||||
|
|
||||||
|
'diplomaticLimit' => $rawNation['surlimit'],
|
||||||
|
'strategicCmdLimit' => $rawNation['strategic_cmd_limit'],
|
||||||
|
'impossibleStrategicCommand' => $impossibleStrategicCommandLists,
|
||||||
|
|
||||||
|
'prohibitScout' => $rawNation['scout'],
|
||||||
|
'prohibitWar' => $rawNation['war'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateDummyNationInfo(MeekroDB $db, General $general, array $rawNation): array
|
||||||
|
{
|
||||||
|
$staticInfo = getNationStaticInfo(0);
|
||||||
|
return [
|
||||||
|
'id' => 0,
|
||||||
|
'full' => false,
|
||||||
|
'name' => $staticInfo['name'],
|
||||||
|
'population' => [
|
||||||
|
'cityCnt' => 0,
|
||||||
|
'now' => 0,
|
||||||
|
'max' => 0,
|
||||||
|
],
|
||||||
|
'crew' => [
|
||||||
|
'generalCnt' => 0,
|
||||||
|
'now' => 0,
|
||||||
|
'max' => 0,
|
||||||
|
],
|
||||||
|
'type' => [
|
||||||
|
'raw' => 'None',
|
||||||
|
'name' => '-',
|
||||||
|
'pros' => '',
|
||||||
|
'cons' => '',
|
||||||
|
],
|
||||||
|
'color' => $staticInfo['color'],
|
||||||
|
'level' => $staticInfo['level'],
|
||||||
|
'capital' => $staticInfo['capital'],
|
||||||
|
'gold' => $staticInfo['gold'],
|
||||||
|
'rice' => $staticInfo['rice'],
|
||||||
|
'tech' => $staticInfo['tech'],
|
||||||
|
'gennum' => $staticInfo['gennum'],
|
||||||
|
'power' => $staticInfo['power'],
|
||||||
|
'onlineGen' => '', //TODO 접속자
|
||||||
|
'notice' => '',
|
||||||
|
'topChiefs' => [],
|
||||||
|
'impossibleStrategicCommand' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateGeneralInfo(MeekroDB $db, General $general, array $rawNation): array
|
||||||
|
{
|
||||||
|
|
||||||
|
$permission = checkSecretPermission($general->getRaw());
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'no' => $general->getID(), // number;
|
||||||
|
'name' => $general->getName(), // string;
|
||||||
|
'nation' => $general->getNationID(), // number;
|
||||||
|
'npc' => $general->getNPCType(), // number;
|
||||||
|
'injury' => $general->getVar(GeneralColumn::injury), // number;
|
||||||
|
'leadership' => $general->getVar(GeneralColumn::leadership), // number;
|
||||||
|
'strength' => $general->getVar('strength'), // number;
|
||||||
|
'intel' => $general->getVar('intel'), // number;
|
||||||
|
'explevel' => $general->getVar('explevel'), // number;
|
||||||
|
'dedlevel' => $general->getVar('dedlevel'), // number;
|
||||||
|
'gold' => $general->getVar('gold'), // number;
|
||||||
|
'rice' => $general->getVar('rice'), // number;
|
||||||
|
'killturn' => $general->getVar(GeneralColumn::killturn), // number;
|
||||||
|
'picture' => $general->getVar(GeneralColumn::picture), // string;
|
||||||
|
'imgsvr' => $general->getVar(GeneralColumn::imgsvr), // 0 | 1;
|
||||||
|
'age' => $general->getVar(GeneralColumn::age), // number;
|
||||||
|
'specialDomestic' => $general->getVar(GeneralColumn::special), // GameObjClassKey;
|
||||||
|
'specialWar' => $general->getVar(GeneralColumn::special2), // GameObjClassKey;
|
||||||
|
'personal' => $general->getVar(GeneralColumn::personal), // GameObjClassKey;
|
||||||
|
'belong' => $general->getVar(GeneralColumn::belong), // number;
|
||||||
|
'connect' => $general->getVar(GeneralColumn::connect), // number;
|
||||||
|
|
||||||
|
'officerLevel' => $general->getVar(GeneralColumn::officer_level), // number;
|
||||||
|
'officerLevelText' => getOfficerLevelText($general->getVar(GeneralColumn::officer_level), $rawNation['level']), // string;
|
||||||
|
'lbonus' => calcLeadershipBonus($general->getVar(GeneralColumn::officer_level), $rawNation['level']), // number;
|
||||||
|
'ownerName' => $general->getVar(GeneralColumn::owner_name), // string | null;
|
||||||
|
'honorText' => getHonor($general->getVar(GeneralColumn::experience)), // string;
|
||||||
|
'dedLevelText' => getDed($general->getVar(GeneralColumn::dedication)), // string;
|
||||||
|
'bill' => getBillByLevel($general->getVar(GeneralColumn::dedlevel)), // number;
|
||||||
|
'reservedCommand' => null, // TurnObj[] | null;
|
||||||
|
|
||||||
|
'autorun_limit' => $general->getAuxVar('autorun_limit') ?? 0, // number;
|
||||||
|
|
||||||
|
'city' => $general->getVar(GeneralColumn::city), // number;
|
||||||
|
'troop' => $general->getVar(GeneralColumn::troop), // number;
|
||||||
|
//P0 End
|
||||||
|
|
||||||
|
'con' => $general->getVar(GeneralColumn::con), // number;
|
||||||
|
'specage' => $general->getVar(GeneralColumn::specage), // number;
|
||||||
|
'specage2' => $general->getVar(GeneralColumn::specage2), // number;
|
||||||
|
'leadership_exp' => $general->getVar(GeneralColumn::leadership_exp), // number;
|
||||||
|
'strength_exp' => $general->getVar(GeneralColumn::strength_exp), // number;
|
||||||
|
'intel_exp' => $general->getVar(GeneralColumn::intel_exp), // number;
|
||||||
|
|
||||||
|
'dex1' => $general->getVar(GeneralColumn::dex1), // number;
|
||||||
|
'dex2' => $general->getVar(GeneralColumn::dex2), // number;
|
||||||
|
'dex3' => $general->getVar(GeneralColumn::dex3), // number;
|
||||||
|
'dex4' => $general->getVar(GeneralColumn::dex4), // number;
|
||||||
|
'dex5' => $general->getVar(GeneralColumn::dex5), // number;
|
||||||
|
|
||||||
|
'experience' => $general->getVar(GeneralColumn::experience), // number;
|
||||||
|
'dedication' => $general->getVar(GeneralColumn::dedication), // number;
|
||||||
|
'officer_level' => $general->getVar(GeneralColumn::officer_level), // number;
|
||||||
|
'officer_city' => $general->getVar(GeneralColumn::officer_city), // number;
|
||||||
|
'defence_train' => $general->getVar(GeneralColumn::defence_train), // number;
|
||||||
|
'crewtype' => $general->getVar(GeneralColumn::crewtype), // GameObjClassKey;
|
||||||
|
'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;
|
||||||
|
'horse' => $general->getVar(GeneralColumn::horse), // GameObjClassKey;
|
||||||
|
'weapon' => $general->getVar(GeneralColumn::weapon), // GameObjClassKey;
|
||||||
|
'book' => $general->getVar(GeneralColumn::book), // GameObjClassKey;
|
||||||
|
'item' => $general->getVar(GeneralColumn::item), // GameObjClassKey;
|
||||||
|
|
||||||
|
'warnum' => $general->getRankVar(RankColumn::warnum), // number;
|
||||||
|
'killnum' => $general->getRankVar(RankColumn::killnum), // number;
|
||||||
|
'deathnum' => $general->getRankVar(RankColumn::deathnum), // number;
|
||||||
|
'killcrew' => $general->getRankVar(RankColumn::killcrew), // number;
|
||||||
|
'deathcrew' => $general->getRankVar(RankColumn::deathcrew), // number;
|
||||||
|
'firenum' => $general->getRankVar(RankColumn::firenum), // number;
|
||||||
|
//P1 End
|
||||||
|
|
||||||
|
'permission' => $permission,
|
||||||
|
];
|
||||||
|
|
||||||
|
$nationID = $general->getNationID();
|
||||||
|
$troopID = $general->getVar(GeneralColumn::troop);
|
||||||
|
if (!$troopID) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$troopName = $db->queryFirstField(
|
||||||
|
'SELECT `name` FROM troop WHERE nation = %i AND troop_leader = %i',
|
||||||
|
$nationID,
|
||||||
|
$troopID
|
||||||
|
);
|
||||||
|
if (!$troopName) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$troopCityID = $db->queryFirstField(
|
||||||
|
'SELECT city FROM general WHERE nation = %i AND `no` = %i',
|
||||||
|
$nationID,
|
||||||
|
$troopID
|
||||||
|
);
|
||||||
|
if (!$troopCityID) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$troopReservedCommand = $db->query(
|
||||||
|
'SELECT action, arg, brief FROM general_turn WHERE general_id = %i AND turn_idx < 5 ORDER BY turn_idx asc',
|
||||||
|
$troopID
|
||||||
|
);
|
||||||
|
if ($troopReservedCommand) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result['troopInfo'] = [
|
||||||
|
'leader' => [
|
||||||
|
'city' => $troopCityID,
|
||||||
|
'reservedCommand' => $troopReservedCommand,
|
||||||
|
],
|
||||||
|
'name' => $troopName,
|
||||||
|
];
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateCityInfo(MeekroDB $db, General $general, array $rawNation): array
|
||||||
|
{
|
||||||
|
$rawCity = $general->getRawCity() ?? [];
|
||||||
|
$cityID = $rawCity[CityColumn::city->value];
|
||||||
|
|
||||||
|
$nationID = $rawCity[CityColumn::nation->value];
|
||||||
|
if ($nationID == $general->getNationID()) {
|
||||||
|
$nationName = $rawNation['name'];
|
||||||
|
$nationColor = $rawNation['color'];
|
||||||
|
} else {
|
||||||
|
$staticNation = getNationStaticInfo($nationID);
|
||||||
|
$nationName = $staticNation['name'];
|
||||||
|
$nationColor = $staticNation['color'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rawOfficerList = $db->query('SELECT officer_level, name, npc FROM general WHERE officer_city = %i AND officer_level IN (4,3,2)', $cityID);
|
||||||
|
$officerList = [4 => null, 3 => null, 2 => null];
|
||||||
|
foreach ($rawOfficerList as $officer) {
|
||||||
|
$officerLevel = $officer['officer_level'];
|
||||||
|
$officerList[$officerLevel] = $officer;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'id' => $rawCity[CityColumn::city->value],
|
||||||
|
'name' => $rawCity[CityColumn::name->value],
|
||||||
|
'nationInfo' => [
|
||||||
|
'id' => $nationID,
|
||||||
|
'name' => $nationName,
|
||||||
|
'color' => $nationColor,
|
||||||
|
],
|
||||||
|
'level' => $rawCity[CityColumn::level->value],
|
||||||
|
'trust' => $rawCity[CityColumn::trust->value],
|
||||||
|
'pop' => [$rawCity[CityColumn::pop->value], $rawCity[CityColumn::pop_max->value]],
|
||||||
|
'agri' => [$rawCity[CityColumn::agri->value], $rawCity[CityColumn::agri_max->value]],
|
||||||
|
'comm' => [$rawCity[CityColumn::comm->value], $rawCity[CityColumn::comm_max->value]],
|
||||||
|
'secu' => [$rawCity[CityColumn::secu->value], $rawCity[CityColumn::secu_max->value]],
|
||||||
|
'def' => [$rawCity[CityColumn::def->value], $rawCity[CityColumn::def_max->value]],
|
||||||
|
'wall' => [$rawCity[CityColumn::wall->value], $rawCity[CityColumn::wall_max->value]],
|
||||||
|
'trade' => $rawCity[CityColumn::trade->value],
|
||||||
|
'officerList' => $officerList,
|
||||||
|
];
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function launch(Session $session, ?DateTimeInterface $modifiedSince, ?string $reqEtag)
|
||||||
|
{
|
||||||
|
$generalID = $session->generalID;
|
||||||
|
//NOTE: 이 경우 staticNation 정보를 조회한다.
|
||||||
|
$general = General::createGeneralObjFromDB($generalID);
|
||||||
|
$nationID = $general->getNationID();
|
||||||
|
$cityID = $general->getCityID();
|
||||||
|
|
||||||
|
$con = checkLimit($general->getVar('con'));
|
||||||
|
if ($con >= 2) {
|
||||||
|
return [
|
||||||
|
'result' => false,
|
||||||
|
'reason' => '접속 제한중입니다.',
|
||||||
|
'recovery' => APIRecoveryType::GameQuota,
|
||||||
|
'recovery_arg' => $general->getVar('turntime'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = DB::db();
|
||||||
|
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||||
|
$gameStor->cacheAll(true);
|
||||||
|
|
||||||
|
increaseRefresh("General/GetFrontInfo", 1);
|
||||||
|
|
||||||
|
$rawCity = $db->queryFirstRow('SELECT * FROM city WHERE city = %i', $cityID);
|
||||||
|
$general->setRawCity($rawCity);
|
||||||
|
|
||||||
|
if ($nationID == 0) {
|
||||||
|
$rawNation = getNationStaticInfo(0);
|
||||||
|
} else {
|
||||||
|
$rawNation = $db->queryFirstRow('SELECT * FROM nation WHERE nation = %i', $nationID);
|
||||||
|
}
|
||||||
|
|
||||||
|
$globalInfo = $this->generateGlobalInfo($db);
|
||||||
|
$nationInfo = $nationID != 0 ?
|
||||||
|
$this->generateNationInfo($db, $general, $rawNation) :
|
||||||
|
$this->generateDummyNationInfo($db, $general, $rawNation);
|
||||||
|
$generalInfo = $this->generateGeneralInfo($db, $general, $rawNation);
|
||||||
|
$cityInfo = $this->generateCityInfo($db, $general, $rawNation);
|
||||||
|
|
||||||
|
//TODO: 마지막 투표, 토너먼트, 베팅을 했는지 정보를 별도로 가져와야 함. aux?
|
||||||
|
|
||||||
|
|
||||||
|
return [
|
||||||
|
'result' => true,
|
||||||
|
'recentRecord' => $this->generateRecentRecord($generalID),
|
||||||
|
'global' => $globalInfo,
|
||||||
|
'nation' => $nationInfo,
|
||||||
|
'general' => $generalInfo,
|
||||||
|
'city' => $cityInfo,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace sammo\API\Global;
|
||||||
|
|
||||||
|
use LDTO\DTO;
|
||||||
|
use sammo\APICacheResult;
|
||||||
|
use sammo\BaseAPI;
|
||||||
|
use sammo\GlobalMenu;
|
||||||
|
use sammo\Session;
|
||||||
|
|
||||||
|
class GetGlobalMenu extends BaseAPI
|
||||||
|
{
|
||||||
|
public function getRequiredSessionMode(): int
|
||||||
|
{
|
||||||
|
return self::NO_SESSION;
|
||||||
|
}
|
||||||
|
function validateArgs(): ?string
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function launch(Session $session, ?\DateTimeInterface $modifiedSince, ?string $reqEtag)
|
||||||
|
{
|
||||||
|
if ($reqEtag !== null) {
|
||||||
|
$version = GlobalMenu::version;
|
||||||
|
if ($reqEtag == "v{$version}") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'result' => true,
|
||||||
|
'menu' => array_map(function (DTO $dto) {
|
||||||
|
return $dto->toArray();
|
||||||
|
}, GlobalMenu::getMenu()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
public function tryCache(): ?APICacheResult
|
||||||
|
{
|
||||||
|
$version = GlobalMenu::version;
|
||||||
|
return new APICacheResult(
|
||||||
|
null,
|
||||||
|
"v{$version}",
|
||||||
|
60 * 60 * 6,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,24 +5,26 @@ namespace sammo\API\Global;
|
|||||||
use sammo\Session;
|
use sammo\Session;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use sammo\DB;
|
use sammo\DB;
|
||||||
|
use sammo\KVStorage;
|
||||||
use sammo\Util;
|
use sammo\Util;
|
||||||
use sammo\Validator;
|
use sammo\Validator;
|
||||||
|
|
||||||
class GetRecentRecord extends \sammo\BaseAPI
|
class GetRecentRecord extends \sammo\BaseAPI
|
||||||
{
|
{
|
||||||
|
static bool $allowExternalAPI = false;
|
||||||
|
|
||||||
const ROW_LIMIT = 15;
|
const ROW_LIMIT = 15;
|
||||||
|
|
||||||
public function validateArgs(): ?string
|
public function validateArgs(): ?string
|
||||||
{
|
{
|
||||||
$v = new Validator($this->args);
|
$v = new Validator($this->args);
|
||||||
$v->rule('int', 'lastGeneralRecordID')
|
$v->rule('integer', 'lastGeneralRecordID')
|
||||||
->rule('int', 'lastWorldHistoryID');
|
->rule('integer', 'lastWorldHistoryID');
|
||||||
if (!$v->validate()) {
|
if (!$v->validate()) {
|
||||||
return $v->errorStr();
|
return $v->errorStr();
|
||||||
}
|
}
|
||||||
$this->args['lastGeneralRecordID'] = (int)($this->args['lastGeneralRecordID']??0);
|
$this->args['lastGeneralRecordID'] = (int)($this->args['lastGeneralRecordID'] ?? 0);
|
||||||
$this->args['lastWorldHistoryID'] = (int)($this->args['lastWorldHistoryID']??0);
|
$this->args['lastWorldHistoryID'] = (int)($this->args['lastWorldHistoryID'] ?? 0);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +76,9 @@ class GetRecentRecord extends \sammo\BaseAPI
|
|||||||
{
|
{
|
||||||
$db = DB::db();
|
$db = DB::db();
|
||||||
|
|
||||||
|
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||||
|
$gameStor->cacheValues(['isunited', 'opentime', 'refresh']);
|
||||||
|
|
||||||
$lastHistoryID = $this->args['lastWorldHistoryID'];
|
$lastHistoryID = $this->args['lastWorldHistoryID'];
|
||||||
$lastRecordID = $this->args['lastGeneralRecordID'];
|
$lastRecordID = $this->args['lastGeneralRecordID'];
|
||||||
|
|
||||||
@@ -81,38 +86,35 @@ class GetRecentRecord extends \sammo\BaseAPI
|
|||||||
$globalRecord = $this->getGlobalRecord($lastRecordID);
|
$globalRecord = $this->getGlobalRecord($lastRecordID);
|
||||||
$generalRecord = $this->getGeneralRecord($session->generalID, $lastRecordID);
|
$generalRecord = $this->getGeneralRecord($session->generalID, $lastRecordID);
|
||||||
|
|
||||||
$flushHistory = true;
|
$flushHistory = false;
|
||||||
$flushGlobalRecord = true;
|
$flushGlobalRecord = false;
|
||||||
$flushGeneralRecord = true;
|
$flushGeneralRecord = false;
|
||||||
|
|
||||||
if($history){
|
if (!$history) {
|
||||||
if(Util::array_last($history)[0] == $lastHistoryID){
|
$flushHistory = false;
|
||||||
$flushHistory = false;
|
} else if (Util::array_last($history)[0] <= $lastHistoryID) {
|
||||||
array_pop($history);
|
$flushHistory = false;
|
||||||
}
|
array_pop($history);
|
||||||
else if(count($history) > static::ROW_LIMIT){
|
} else if (count($history) > static::ROW_LIMIT) {
|
||||||
array_pop($history);
|
array_pop($history);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if($globalRecord){
|
if (!$globalRecord) {
|
||||||
if(Util::array_last($globalRecord)[0] == $lastRecordID){
|
$flushGlobalRecord = false;
|
||||||
$flushGlobalRecord = false;
|
} else if (Util::array_last($globalRecord)[0] == $lastRecordID) {
|
||||||
array_pop($globalRecord);
|
$flushGlobalRecord = false;
|
||||||
}
|
array_pop($globalRecord);
|
||||||
else if(count($globalRecord) > static::ROW_LIMIT){
|
} else if (count($globalRecord) > static::ROW_LIMIT) {
|
||||||
array_pop($globalRecord);
|
array_pop($globalRecord);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if($generalRecord){
|
if (!$generalRecord) {
|
||||||
if(Util::array_last($generalRecord)[0] == $lastRecordID){
|
$flushGeneralRecord = false;
|
||||||
$flushGeneralRecord = false;
|
} else if (Util::array_last($generalRecord)[0] == $lastRecordID) {
|
||||||
array_pop($generalRecord);
|
$flushGeneralRecord = false;
|
||||||
}
|
array_pop($generalRecord);
|
||||||
else if(count($generalRecord) > static::ROW_LIMIT){
|
} else if (count($generalRecord) > static::ROW_LIMIT) {
|
||||||
array_pop($generalRecord);
|
array_pop($generalRecord);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -120,9 +122,9 @@ class GetRecentRecord extends \sammo\BaseAPI
|
|||||||
'history' => $history,
|
'history' => $history,
|
||||||
'global' => $globalRecord,
|
'global' => $globalRecord,
|
||||||
'general' => $generalRecord,
|
'general' => $generalRecord,
|
||||||
'flushHistory' => $flushHistory?1:0,
|
'flushHistory' => $flushHistory ? 1 : 0,
|
||||||
'flushGlobal' => $flushGlobalRecord?1:0,
|
'flushGlobal' => $flushGlobalRecord ? 1 : 0,
|
||||||
'flushGeneral' => $flushGeneralRecord?1:0,
|
'flushGeneral' => $flushGeneralRecord ? 1 : 0,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ class SendMessage extends \sammo\BaseAPI
|
|||||||
if ($mailbox === Message::MAILBOX_PUBLIC) {
|
if ($mailbox === Message::MAILBOX_PUBLIC) {
|
||||||
$msgID = $this->genPublicMessage($src, $text)->send();
|
$msgID = $this->genPublicMessage($src, $text)->send();
|
||||||
return [
|
return [
|
||||||
|
'msgType' => 'public',
|
||||||
'msgID' => $msgID
|
'msgID' => $msgID
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -201,6 +202,7 @@ class SendMessage extends \sammo\BaseAPI
|
|||||||
if ($destNationID === $nationID) {
|
if ($destNationID === $nationID) {
|
||||||
$msgID = $this->genNationalMessage($src, $text)->send();
|
$msgID = $this->genNationalMessage($src, $text)->send();
|
||||||
return [
|
return [
|
||||||
|
'msgType' => 'national',
|
||||||
'msgID' => $msgID
|
'msgID' => $msgID
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -211,6 +213,7 @@ class SendMessage extends \sammo\BaseAPI
|
|||||||
}
|
}
|
||||||
$msgID = $msgObjOrError->send();
|
$msgID = $msgObjOrError->send();
|
||||||
return [
|
return [
|
||||||
|
'msgType' => 'diplomacy',
|
||||||
'msgID' => $msgID
|
'msgID' => $msgID
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -231,6 +234,7 @@ class SendMessage extends \sammo\BaseAPI
|
|||||||
$msgID = $msgObjOrError->send();
|
$msgID = $msgObjOrError->send();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
'msgType' => 'private',
|
||||||
'msgID' => $msgID
|
'msgID' => $msgID
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class GetNationInfo extends \sammo\BaseAPI
|
|||||||
public function validateArgs(): ?string
|
public function validateArgs(): ?string
|
||||||
{
|
{
|
||||||
$v = new Validator($this->args);
|
$v = new Validator($this->args);
|
||||||
$v->rule('boolean', 'full');
|
$v->rule('in', 'full', ['true', 'false']);
|
||||||
|
|
||||||
if (!$v->validate()) {
|
if (!$v->validate()) {
|
||||||
return $v->errorStr();
|
return $v->errorStr();
|
||||||
@@ -70,7 +70,7 @@ class GetNationInfo extends \sammo\BaseAPI
|
|||||||
$gameEnv = $gameStor->getValues(['year', 'month', 'startyear']);
|
$gameEnv = $gameStor->getValues(['year', 'month', 'startyear']);
|
||||||
|
|
||||||
$nation = $db->queryFirstRow(
|
$nation = $db->queryFirstRow(
|
||||||
'SELECT nation, `name`, color, capital, gennum, gold, rice, bill, rate, secretlimit, chief_set, scout, war, strategic_cmd_limit, surlimit, tech, `power`, `level`, `type` FROM nation WHERE id = %i',
|
'SELECT nation, `name`, color, capital, gennum, gold, rice, bill, rate, secretlimit, chief_set, scout, war, strategic_cmd_limit, surlimit, tech, `power`, `level`, `type` FROM nation WHERE nation = %i',
|
||||||
$nationID
|
$nationID
|
||||||
);
|
);
|
||||||
$nationStor = \sammo\KVStorage::getStorage($db, $nationID, 'nation_env');
|
$nationStor = \sammo\KVStorage::getStorage($db, $nationID, 'nation_env');
|
||||||
@@ -94,11 +94,14 @@ class GetNationInfo extends \sammo\BaseAPI
|
|||||||
$troopName[$troopID] = $tName;
|
$troopName[$troopID] = $tName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
$result = [
|
||||||
'result' => true,
|
'result' => true,
|
||||||
|
'isFull' => $isFull,
|
||||||
'nation' => $nation,
|
'nation' => $nation,
|
||||||
'impossibleStrategicCommandLists' => $impossibleStrategicCommandLists,
|
'impossibleStrategicCommandLists' => $impossibleStrategicCommandLists,
|
||||||
'troops' => $troopName,
|
'troops' => $troopName,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace sammo\DTO;
|
||||||
|
|
||||||
|
use LDTO\Attr\NullIsUndefined;
|
||||||
|
use LDTO\DTO;
|
||||||
|
|
||||||
|
class MenuItem extends DTO{
|
||||||
|
public readonly string $type;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $name,
|
||||||
|
public readonly string $url,
|
||||||
|
|
||||||
|
#[NullIsUndefined]
|
||||||
|
public ?string $icon = null,
|
||||||
|
#[NullIsUndefined]
|
||||||
|
public ?bool $newTab = false,
|
||||||
|
#[NullIsUndefined]
|
||||||
|
public ?string $condHighlightVar = null,
|
||||||
|
#[NullIsUndefined]
|
||||||
|
public ?string $condShowVar = null,
|
||||||
|
){
|
||||||
|
$this->type = 'item';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace sammo\DTO;
|
||||||
|
|
||||||
|
use LDTO\Attr\Convert;
|
||||||
|
use LDTO\DTO;
|
||||||
|
use LDTO\Converter\ArrayConverter;
|
||||||
|
|
||||||
|
class MenuMulti extends DTO{
|
||||||
|
public readonly string $type;
|
||||||
|
|
||||||
|
/** @param MenuItem[] $subMenu */
|
||||||
|
public function __construct(
|
||||||
|
public string $name,
|
||||||
|
#[Convert(ArrayConverter::class, [DTO::class])]
|
||||||
|
public array $subMenu,
|
||||||
|
){
|
||||||
|
$this->type = 'multi';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace sammo\DTO;
|
||||||
|
|
||||||
|
use LDTO\Attr\Convert;
|
||||||
|
use LDTO\Converter\ArrayConverter;
|
||||||
|
use LDTO\DTO;
|
||||||
|
|
||||||
|
class MenuSplit extends DTO{
|
||||||
|
public readonly string $type;
|
||||||
|
|
||||||
|
/** @param MenuItem[] $subMenu */
|
||||||
|
public function __construct(
|
||||||
|
public MenuItem $main,
|
||||||
|
#[Convert(ArrayConverter::class, [DTO::class])]
|
||||||
|
public array $subMenu,
|
||||||
|
){
|
||||||
|
$this->type = 'split';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1082,7 +1082,7 @@ class General implements iAction
|
|||||||
'personal', 'special', 'special2', 'defence_train', 'tnmt', 'npc', 'npc_org', 'deadyear', 'npcmsg',
|
'personal', 'special', 'special2', 'defence_train', 'tnmt', 'npc', 'npc_org', 'deadyear', 'npcmsg',
|
||||||
'dex1', 'dex2', 'dex3', 'dex4', 'dex5', 'betray',
|
'dex1', 'dex2', 'dex3', 'dex4', 'dex5', 'betray',
|
||||||
'recent_war', 'last_turn', 'myset',
|
'recent_war', 'last_turn', 'myset',
|
||||||
'specage', 'specage2', 'con', 'connect', 'aux', 'lastrefresh',
|
'specage', 'specage2', 'con', 'connect', 'aux', 'lastrefresh', 'permission', 'penalty',
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($reqColumns === null) {
|
if ($reqColumns === null) {
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace sammo;
|
||||||
|
|
||||||
|
use sammo\DTO\MenuItem;
|
||||||
|
use sammo\DTO\MenuMulti;
|
||||||
|
use sammo\DTO\MenuSplit;
|
||||||
|
|
||||||
|
//변경을 원한다면 d_setting/GlobalMenu.orig.php 파일을 복제 후 수정
|
||||||
|
|
||||||
|
class GlobalMenu {
|
||||||
|
/** @var (MenuItem|MenuMulti|MenuSplit)[] */
|
||||||
|
|
||||||
|
const version = 1;
|
||||||
|
static ?array $menu = null;
|
||||||
|
public static function getMenu(): array{
|
||||||
|
if(static::$menu !== null){
|
||||||
|
return static::$menu;
|
||||||
|
}
|
||||||
|
static::$menu = [
|
||||||
|
new MenuItem('천통국 베팅', 'v_nationBetting.php', condHighlightVar: 'nationBetting'),
|
||||||
|
new MenuMulti('게임정보', [
|
||||||
|
new MenuItem('세력일람', 'a_kingdomList.php', newTab: true),
|
||||||
|
new MenuItem('장수일람', 'a_genList.php', newTab: true),
|
||||||
|
new MenuItem('명장일람', 'a_bestGeneral.php', newTab: true),
|
||||||
|
new MenuItem('명예의전당', 'a_hallOfFame.php', newTab: true),
|
||||||
|
new MenuItem('왕조일람', 'a_emperior.php', newTab: true),
|
||||||
|
]),
|
||||||
|
new MenuItem('연감', 'v_history.php', newTab: true),
|
||||||
|
new MenuSplit(
|
||||||
|
new MenuItem('게시판', '/board/community', newTab: true),
|
||||||
|
[
|
||||||
|
new MenuItem('건의/제안', '/board/request', newTab: true),
|
||||||
|
new MenuItem('팁/강좌', '/board/tip', newTab: true),
|
||||||
|
new MenuItem('패치 내역', '/board/patch', newTab: true),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
new MenuSplit(
|
||||||
|
new MenuItem('공식 오픈 톡', 'https://open.kakao.com/o/', newTab: true),
|
||||||
|
[
|
||||||
|
new MenuItem('잡담 오픈 톡', 'https://open.kakao.com/o/', newTab: true)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
new MenuItem('전투 시뮬레이터', 'battle_simulator.php', newTab: true),
|
||||||
|
new MenuMulti('기타 정보', [
|
||||||
|
new MenuItem('접속량정보', 'a_traffic.php', newTab: true),
|
||||||
|
new MenuItem('빙의일람', 'a_npcList', newTab: true, condShowVar: 'npcMode'),
|
||||||
|
]),
|
||||||
|
new MenuItem('설문조사', 'v_vote.php', newTab: true, condHighlightVar: 'vote'),
|
||||||
|
];
|
||||||
|
return static::$menu;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
.ev_warning {
|
||||||
|
color: orangered;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ev_notice {
|
||||||
|
color: orangered;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ev_succes {
|
||||||
|
color: lightgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ev_sell {
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ev_failed {
|
||||||
|
color: orangered;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ev_highlight {
|
||||||
|
color: orangered;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden_but_copyable {
|
||||||
|
color: rgba(0, 0, 0, 0);
|
||||||
|
font-size: 0;
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@charset 'utf-8';
|
||||||
|
|
||||||
.map_title_tooltiptext .tooltip-inner {
|
.map_title_tooltiptext .tooltip-inner {
|
||||||
max-width: 220px;
|
max-width: 220px;
|
||||||
width: 220px;
|
width: 220px;
|
||||||
|
|||||||
@@ -0,0 +1,621 @@
|
|||||||
|
<template>
|
||||||
|
<div id="outBlock" :class="`sam-color-${nationStaticInfo?.color.substring(1, 7) ?? '000000'}`">
|
||||||
|
<div id="outBlock2">
|
||||||
|
<!-- eslint-disable-next-line vue/max-attributes-per-line -->
|
||||||
|
<BContainer v-if="asyncReady" id="container" :position="'position-relative'" :toast="{ root: true }" class="bg0">
|
||||||
|
<main class="gx-0">
|
||||||
|
<div class="commonToolbar">
|
||||||
|
<GlobalMenu
|
||||||
|
v-if="globalMenu && globalInfo"
|
||||||
|
:globalInfo="globalInfo"
|
||||||
|
:modelValue="globalMenu"
|
||||||
|
variant="sammo-base2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<GameInfo
|
||||||
|
v-if="frontInfo"
|
||||||
|
:frontInfo="frontInfo"
|
||||||
|
:serverLocked="serverLocked"
|
||||||
|
:serverName="serverName"
|
||||||
|
:lastExecuted="lastExecuted"
|
||||||
|
/>
|
||||||
|
<div class="s-border-t px-2 py-2 onlineNations">접속중인 국가: {{ globalInfo.onlineNations }}</div>
|
||||||
|
<div class="s-border-t px-2 py-2 onlineUsers">【 접속자 】 {{ nationInfo?.onlineGen }}</div>
|
||||||
|
<div class="s-border-t py-2 nationNotice">
|
||||||
|
<div class="px-2">【 국가방침 】</div>
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<div v-if="nationInfo" class="nationNoticeBody" v-html="nationInfo.notice.msg" />
|
||||||
|
</div>
|
||||||
|
<div id="ingameBoard">
|
||||||
|
<!-- TODO: 운영자 툴바는 어디에?-->
|
||||||
|
<div class="mapView">
|
||||||
|
<MapViewer
|
||||||
|
v-if="map"
|
||||||
|
:serverNick="serverNick"
|
||||||
|
:serverID="serverID"
|
||||||
|
:mapName="unwrap(gameConstStore?.gameConst.mapName)"
|
||||||
|
:isDetailMap="true"
|
||||||
|
:cityPosition="cityPosition"
|
||||||
|
:imagePath="imagePath"
|
||||||
|
:formatCityInfo="formatCityInfoText"
|
||||||
|
:mapData="map"
|
||||||
|
:disallowClick="false"
|
||||||
|
:genHref="genMapCityHref"
|
||||||
|
@city-click="onCityClick"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="reservedCommandZone">
|
||||||
|
<PartialReservedCommand id="reservedCommandPanel" ref="reservedCommandPanel" />
|
||||||
|
</div>
|
||||||
|
<div id="actionMiniPlate" class="gx-0 row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="gx-1 row">
|
||||||
|
<div class="col-8 d-grid">
|
||||||
|
<button type="button" class="btn btn-sammo-base2" @click="tryRefresh">갱 신</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 d-grid">
|
||||||
|
<button type="button" class="btn btn-sammo-base2" @click="moveLobby">로비로</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="frontInfo" class="cityInfo">
|
||||||
|
<CityBasicCard :city="frontInfo.city" />
|
||||||
|
</div>
|
||||||
|
<div v-if="frontInfo" class="nationInfo">
|
||||||
|
<NationBasicCard :nation="frontInfo.nation" :global="globalInfo" />
|
||||||
|
</div>
|
||||||
|
<div v-if="frontInfo && generalInfo && nationStaticInfo" class="generalInfo">
|
||||||
|
<GeneralBasicCard
|
||||||
|
:general="generalInfo"
|
||||||
|
:nation="nationStaticInfo"
|
||||||
|
:troopInfo="frontInfo.general.troopInfo"
|
||||||
|
:turnTerm="globalInfo.turnterm"
|
||||||
|
:lastExecuted="lastExecuted"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="generalCommandToolbar">
|
||||||
|
<MainControlBar
|
||||||
|
v-if="generalInfo"
|
||||||
|
:permission="generalInfo.permission"
|
||||||
|
:showSecret="showSecret"
|
||||||
|
:myLevel="generalInfo.officerLevel"
|
||||||
|
:nationLevel="nationStaticInfo?.level ?? 0"
|
||||||
|
:nationColor="nationStaticInfo?.color.substring(1, 7) ?? '000000'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div id="actionMiniPlateSub" class="gx-0 row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="gx-1 row">
|
||||||
|
<div class="col-3 d-grid">
|
||||||
|
<button type="button" class="btn btn-dark" @click="scrollToSelector('#reservedCommandPanel')">
|
||||||
|
명령으로
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-5 d-grid">
|
||||||
|
<button type="button" class="btn btn-sammo-base2" @click="tryRefresh">갱 신</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 d-grid">
|
||||||
|
<button type="button" class="btn btn-sammo-base2" @click="moveLobby">로비로</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="RecordZone row gx-0">
|
||||||
|
<div class="PublicRecord col col-12 col-md-6">
|
||||||
|
<div class="bg1 center s-border-tb title">장수 동향</div>
|
||||||
|
<template v-for="[idx, rawText] of globalRecords.toArray()" :key="idx">
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<div :v-data-idx="idx" v-html="formatLog(rawText)" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="GeneralLog col col-12 col-md-6">
|
||||||
|
<div class="bg1 center s-border-tb title">개인 기록</div>
|
||||||
|
<template v-for="[idx, rawText] of generalRecords.toArray()" :key="idx">
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<div :v-data-idx="idx" v-html="formatLog(rawText)" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="WorldHistory col col-12">
|
||||||
|
<div class="bg1 center s-border-tb title">중원 정세</div>
|
||||||
|
<template v-for="[idx, rawText] of worldHistory.toArray()" :key="idx">
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<div :v-data-idx="idx" v-html="formatLog(rawText)" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="commonToolbar">
|
||||||
|
<GlobalMenu
|
||||||
|
v-if="globalMenu && globalInfo"
|
||||||
|
:globalInfo="globalInfo"
|
||||||
|
:modelValue="globalMenu"
|
||||||
|
variant="sammo-base2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<MessagePanel
|
||||||
|
v-if="generalInfo"
|
||||||
|
ref="msgPanel"
|
||||||
|
:generalID="generalInfo.no"
|
||||||
|
:generalName="generalInfo.name"
|
||||||
|
:nationID="generalInfo.nation"
|
||||||
|
:permissionLevel="generalInfo.permission"
|
||||||
|
/>
|
||||||
|
<div class="commonToolbar">
|
||||||
|
<GlobalMenu
|
||||||
|
v-if="globalMenu && globalInfo"
|
||||||
|
:globalInfo="globalInfo"
|
||||||
|
:modelValue="globalMenu"
|
||||||
|
variant="sammo-base2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</BContainer>
|
||||||
|
<div v-else>서버 갱신 중입니다.</div>
|
||||||
|
</div>
|
||||||
|
<GameBottomBar
|
||||||
|
v-if="frontInfo && globalMenu"
|
||||||
|
id="mobileBottomBar"
|
||||||
|
:frontInfo="frontInfo"
|
||||||
|
:globalMenu="globalMenu"
|
||||||
|
@refresh="tryRefresh"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
declare const staticValues: {
|
||||||
|
serverName: string;
|
||||||
|
serverNick: string;
|
||||||
|
serverID: string;
|
||||||
|
mapName: string;
|
||||||
|
unitSet: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare const getCityPosition: () => CityPositionMap;
|
||||||
|
declare const formatCityInfo: (city: MapCityParsedRaw) => MapCityParsed;
|
||||||
|
</script>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { BContainer, BButton, useToast } from "bootstrap-vue-3";
|
||||||
|
import { isString } from "lodash";
|
||||||
|
import { computed, onMounted, provide, ref, watch } from "vue";
|
||||||
|
import { GameConstStore, getGameConstStore } from "./GameConstStore";
|
||||||
|
import { SammoAPI } from "./SammoAPI";
|
||||||
|
import { parseTime } from "./util/parseTime";
|
||||||
|
import { unwrap } from "./util/unwrap";
|
||||||
|
import Denque from "denque";
|
||||||
|
import { formatLog } from "@/utilGame/formatLog";
|
||||||
|
import type { GetFrontInfoResponse, GetMenuResponse } from "./defs/API/Global";
|
||||||
|
import { delay } from "./util/delay";
|
||||||
|
import CityBasicCard from "./components/CityBasicCard.vue";
|
||||||
|
import NationBasicCard from "./components/NationBasicCard.vue";
|
||||||
|
import GeneralBasicCard from "./components/GeneralBasicCard.vue";
|
||||||
|
import MessagePanel from "./components/MessagePanel.vue";
|
||||||
|
import type { MapResult, NationStaticItem } from "./defs";
|
||||||
|
import MapViewer, { type CityPositionMap, type MapCityParsed, type MapCityParsedRaw } from "./components/MapViewer.vue";
|
||||||
|
import PartialReservedCommand from "./PartialReservedCommand.vue";
|
||||||
|
import { scrollToSelector } from "./util/scrollToSelector";
|
||||||
|
import MainControlBar from "./components/MainControlBar.vue";
|
||||||
|
import GlobalMenu from "./components/GlobalMenu.vue";
|
||||||
|
import GameInfo from "./components/GameInfo.vue";
|
||||||
|
import GameBottomBar from "./components/GameBottomBar.vue";
|
||||||
|
|
||||||
|
const { serverName, serverNick, serverID } = staticValues;
|
||||||
|
|
||||||
|
const asyncReady = ref(false);
|
||||||
|
const gameConstStore = ref<GameConstStore>();
|
||||||
|
|
||||||
|
const toasts = unwrap(useToast());
|
||||||
|
|
||||||
|
provide("gameConstStore", gameConstStore);
|
||||||
|
|
||||||
|
const lastExecuted = ref<Date>(parseTime("2022-08-15 00:00:00"));
|
||||||
|
const serverLocked = ref(true);
|
||||||
|
const refreshCounter = ref(0);
|
||||||
|
|
||||||
|
const storeP = getGameConstStore().then((store) => {
|
||||||
|
gameConstStore.value = store;
|
||||||
|
});
|
||||||
|
|
||||||
|
const showSecret = computed(() => {
|
||||||
|
if (!frontInfo.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (frontInfo.value.general.permission >= 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (frontInfo.value.general.officerLevel >= 2) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
let responseLock = false;
|
||||||
|
|
||||||
|
const msgPanel = ref<InstanceType<typeof MessagePanel>>();
|
||||||
|
|
||||||
|
function moveLobby() {
|
||||||
|
location.replace("../");
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastVoteState = (() => {
|
||||||
|
const key = `state.${serverID}.lastVote`;
|
||||||
|
const value = parseInt(localStorage.getItem(key) ?? "0");
|
||||||
|
const obj = ref<number>(value);
|
||||||
|
watch(obj, (newValue, oldValue) => {
|
||||||
|
if (newValue == oldValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localStorage.setItem(key, newValue.toString());
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
})();
|
||||||
|
|
||||||
|
async function tryRefresh() {
|
||||||
|
msgPanel.value?.tryRefresh();
|
||||||
|
|
||||||
|
if (responseLock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
responseLock = true;
|
||||||
|
const responseP = SammoAPI.Global.ExecuteEngine({ serverID }, true).then((response) => {
|
||||||
|
if (response.result) {
|
||||||
|
lastExecuted.value = parseTime(response.lastExecuted);
|
||||||
|
serverLocked.value = response.locked;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
|
//TODO: 갱신 알림 띄우기
|
||||||
|
const response = await Promise.race([delay(3000), responseP]);
|
||||||
|
responseLock = false;
|
||||||
|
|
||||||
|
if (response === undefined) {
|
||||||
|
//timeout이지만 일단 갱신한다.
|
||||||
|
refreshCounter.value += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.result) {
|
||||||
|
if (response.reqRefresh) {
|
||||||
|
alert(`갱신이 필요합니다: ${response.reason}`);
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(response.reason);
|
||||||
|
if (!asyncReady.value) {
|
||||||
|
throw response.reason;
|
||||||
|
}
|
||||||
|
toasts.danger({
|
||||||
|
title: "갱신 실패",
|
||||||
|
body: response.reason,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshCounter.value += 1;
|
||||||
|
|
||||||
|
//TODO: 서버와 클라이언트 버전이 다르다면 갱신 필요
|
||||||
|
} catch (e) {
|
||||||
|
responseLock = false;
|
||||||
|
//매우 심각한 버그
|
||||||
|
console.error(e);
|
||||||
|
alert(`서버 갱신 실패: ${e}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Promise.all([storeP, tryRefresh()]).then(() => {
|
||||||
|
asyncReady.value = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const globalMenu = ref<GetMenuResponse["menu"]>();
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const response = await SammoAPI.Global.GetGlobalMenu();
|
||||||
|
globalMenu.value = response.menu;
|
||||||
|
} catch (e) {
|
||||||
|
if (isString(e)) {
|
||||||
|
toasts.danger({
|
||||||
|
title: "메뉴 갱신 실패",
|
||||||
|
body: `${e}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const frontInfo = ref<GetFrontInfoResponse>();
|
||||||
|
const globalInfo = ref<GetFrontInfoResponse["global"]>({} as GetFrontInfoResponse["global"]);
|
||||||
|
const generalInfo = ref<GetFrontInfoResponse["general"]>();
|
||||||
|
const nationStaticInfo = ref<NationStaticItem>();
|
||||||
|
const nationInfo = ref<GetFrontInfoResponse["nation"]>();
|
||||||
|
|
||||||
|
const lastGeneralRecordID = ref(0);
|
||||||
|
const lastWorldHistoryID = ref(0);
|
||||||
|
|
||||||
|
const generalRecords = ref(new Denque<[number, string]>());
|
||||||
|
const globalRecords = ref(new Denque<[number, string]>());
|
||||||
|
const worldHistory = ref(new Denque<[number, string]>());
|
||||||
|
|
||||||
|
let generalInfoLock = false;
|
||||||
|
|
||||||
|
watch(refreshCounter, async () => {
|
||||||
|
if (generalInfoLock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
generalInfoLock = true;
|
||||||
|
const response = await SammoAPI.General.GetFrontInfo({
|
||||||
|
lastGeneralRecordID: lastGeneralRecordID.value,
|
||||||
|
lastWorldHistoryID: lastWorldHistoryID.value,
|
||||||
|
});
|
||||||
|
generalInfoLock = false;
|
||||||
|
|
||||||
|
frontInfo.value = response;
|
||||||
|
globalInfo.value = response.global;
|
||||||
|
generalInfo.value = response.general;
|
||||||
|
|
||||||
|
const newLastExecuted = parseTime(response.global.lastExecuted);
|
||||||
|
if (newLastExecuted.getTime() > lastExecuted.value.getTime()) {
|
||||||
|
lastExecuted.value = newLastExecuted;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawNation = response.nation;
|
||||||
|
nationInfo.value = rawNation;
|
||||||
|
nationStaticInfo.value = {
|
||||||
|
nation: rawNation.id,
|
||||||
|
name: rawNation.name,
|
||||||
|
color: rawNation.color,
|
||||||
|
type: rawNation.type.raw,
|
||||||
|
level: rawNation.level,
|
||||||
|
capital: rawNation.capital,
|
||||||
|
gennum: rawNation.gennum,
|
||||||
|
power: rawNation.power,
|
||||||
|
};
|
||||||
|
|
||||||
|
const recentRecord = response.recentRecord;
|
||||||
|
const recordIter = [
|
||||||
|
["flushGeneral", generalRecords, "general", lastGeneralRecordID],
|
||||||
|
["flushGlobal", globalRecords, "global", lastGeneralRecordID],
|
||||||
|
["flushHistory", worldHistory, "history", lastWorldHistoryID],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
let haveNewRecord = false;
|
||||||
|
|
||||||
|
for (const [flushKey, recordRef, recordKey, lastRecordID] of recordIter) {
|
||||||
|
if (recentRecord[flushKey]) {
|
||||||
|
recordRef.value = new Denque<[number, string]>();
|
||||||
|
haveNewRecord = true;
|
||||||
|
}
|
||||||
|
const subRecord = recentRecord[recordKey];
|
||||||
|
if (subRecord.length) {
|
||||||
|
lastRecordID.value = Math.max(lastRecordID.value, subRecord[0][0]);
|
||||||
|
haveNewRecord = true;
|
||||||
|
}
|
||||||
|
while (subRecord.length) {
|
||||||
|
const [id, record] = unwrap(subRecord.pop());
|
||||||
|
if (!recordRef.value.isEmpty() && id <= unwrap(recordRef.value.get(0))[0]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (recordRef.value.length >= 15) {
|
||||||
|
recordRef.value.pop();
|
||||||
|
}
|
||||||
|
recordRef.value.unshift([id, formatLog(record)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (haveNewRecord) {
|
||||||
|
toasts.info(
|
||||||
|
{
|
||||||
|
title: "갱신 완료",
|
||||||
|
body: `동향 변경이 있습니다.`,
|
||||||
|
},
|
||||||
|
{ delay: 1000 * 3 }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toasts.success(
|
||||||
|
{
|
||||||
|
title: "갱신 완료",
|
||||||
|
},
|
||||||
|
{ delay: 1000 * 3 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastVoteID = response.global.lastVoteID;
|
||||||
|
if (lastVoteID > lastVoteState.value) {
|
||||||
|
lastVoteState.value = lastVoteID;
|
||||||
|
toasts.warning(
|
||||||
|
{
|
||||||
|
title: "설문조사 안내",
|
||||||
|
body: `새로운 설문조사가 있습니다.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
delay: 1000 * 60,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
generalInfoLock = false;
|
||||||
|
console.error(e);
|
||||||
|
toasts.danger({
|
||||||
|
title: "최근 정보 갱신 실패",
|
||||||
|
body: `${e}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const map = ref<MapResult>();
|
||||||
|
|
||||||
|
const cityPosition = getCityPosition();
|
||||||
|
const formatCityInfoText = formatCityInfo;
|
||||||
|
const imagePath = window.pathConfig.gameImage;
|
||||||
|
|
||||||
|
function genMapCityHref(cityID: number) {
|
||||||
|
return `b_currentCity.php?citylist=${cityID}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCityClick(city: MapCityParsed, $event: MouseEvent | TouchEvent): void {
|
||||||
|
if (city.id === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($event.ctrlKey) {
|
||||||
|
window.open(genMapCityHref(city.id), "_blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.href = genMapCityHref(city.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(refreshCounter, async () => {
|
||||||
|
try {
|
||||||
|
map.value = await SammoAPI.Global.GetMap({
|
||||||
|
neutralView: 0,
|
||||||
|
showMe: 1,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
toasts.danger({
|
||||||
|
title: "지도 갱신 실패",
|
||||||
|
body: `${e}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const reservedCommandPanel = ref<InstanceType<typeof PartialReservedCommand> | null>(null);
|
||||||
|
watch(refreshCounter, async () => {
|
||||||
|
reservedCommandPanel.value?.reloadCommandList();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@scss/common/break_500px.scss";
|
||||||
|
:deep() {
|
||||||
|
@import "@scss/gameEvent.scss";
|
||||||
|
@import "@scss/battleLog.scss";
|
||||||
|
}
|
||||||
|
|
||||||
|
#mobileBottomBar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#outBlock {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generalInfo {
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ingameBoard {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nationNoticeBody {
|
||||||
|
:deep(p) {
|
||||||
|
min-height: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-500px {
|
||||||
|
#outBlock {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
#mobileBottomBar {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#outBlock2 {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ingameBoard {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reservedCommandZone {
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generalCommandToolbar {
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nationInfo {
|
||||||
|
grid-row: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generalInfo {
|
||||||
|
grid-row: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cityInfo {
|
||||||
|
grid-row: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#actionMiniPlate {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-1000px {
|
||||||
|
#container {
|
||||||
|
width: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ingameBoard {
|
||||||
|
grid-template-columns: 500px 200px 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mapView {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reservedCommandZone {
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 1 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cityInfo {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
grid-row: 2 / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
#actionMiniPlate {
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 3;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generalCommandToolbar {
|
||||||
|
grid-column: 1 / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nationInfo {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generalInfo {
|
||||||
|
grid-column: 2 / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
#actionMiniPlateSub {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+12
-4
@@ -22,11 +22,13 @@ import type { SetBlockWarResponse, GeneralListResponse as NationGeneralListRespo
|
|||||||
import type { UploadImageResponse } from "./defs/API/Misc";
|
import type { UploadImageResponse } from "./defs/API/Misc";
|
||||||
import type { GeneralLogType, GetGeneralLogResponse, JoinArgs } from "./defs/API/General";
|
import type { GeneralLogType, GetGeneralLogResponse, JoinArgs } from "./defs/API/General";
|
||||||
import type {
|
import type {
|
||||||
ExecuteResponse,
|
ExecuteResponse,
|
||||||
GetConstResponse,
|
GetConstResponse,
|
||||||
GetCurrentHistoryResponse,
|
GetCurrentHistoryResponse,
|
||||||
GetDiplomacyResponse,
|
GetDiplomacyResponse,
|
||||||
|
GetFrontInfoResponse,
|
||||||
GetHistoryResponse,
|
GetHistoryResponse,
|
||||||
|
GetMenuResponse,
|
||||||
GetRecentRecordResponse,
|
GetRecentRecordResponse,
|
||||||
} from "./defs/API/Global";
|
} from "./defs/API/Global";
|
||||||
import type { CachedMapResult, CommandTableResponse, GeneralListResponse, ItemTypeKey, MapResult } from "./defs";
|
import type { CachedMapResult, CommandTableResponse, GeneralListResponse, ItemTypeKey, MapResult } from "./defs";
|
||||||
@@ -131,6 +133,11 @@ const apiRealPath = {
|
|||||||
DieOnPrestart: POST as APICallT<undefined>,
|
DieOnPrestart: POST as APICallT<undefined>,
|
||||||
BuildNationCandidate: POST as APICallT<undefined>,
|
BuildNationCandidate: POST as APICallT<undefined>,
|
||||||
GetCommandTable: GET as APICallT<undefined, CommandTableResponse>,
|
GetCommandTable: GET as APICallT<undefined, CommandTableResponse>,
|
||||||
|
GetFrontInfo: GET as APICallT<{
|
||||||
|
lastNationNoticeDate?: string,
|
||||||
|
lastGeneralRecordID?: number,
|
||||||
|
lastWorldHistoryID?: number,
|
||||||
|
}, GetFrontInfoResponse>,
|
||||||
},
|
},
|
||||||
Global: {
|
Global: {
|
||||||
GeneralList: GET as APICallT<undefined, GeneralListResponse>,
|
GeneralList: GET as APICallT<undefined, GeneralListResponse>,
|
||||||
@@ -152,10 +159,11 @@ const apiRealPath = {
|
|||||||
}, ExecuteResponse, InvalidResponse & {
|
}, ExecuteResponse, InvalidResponse & {
|
||||||
reqRefresh?: boolean
|
reqRefresh?: boolean
|
||||||
}>,
|
}>,
|
||||||
GetRecentRecord: GET as APICallT<{
|
/*GetRecentRecord: GET as APICallT<{
|
||||||
lastGeneralRecordID: number;
|
lastGeneralRecordID: number;
|
||||||
lastWorldHistoryID: number;
|
lastWorldHistoryID: number;
|
||||||
} | undefined, GetRecentRecordResponse>,
|
} | undefined, GetRecentRecordResponse>,*/
|
||||||
|
GetGlobalMenu: GET as APICallT<undefined, GetMenuResponse>,
|
||||||
},
|
},
|
||||||
InheritAction: {
|
InheritAction: {
|
||||||
BuyHiddenBuff: PUT as APICallT<{
|
BuyHiddenBuff: PUT as APICallT<{
|
||||||
@@ -191,7 +199,7 @@ const apiRealPath = {
|
|||||||
SendMessage: POST as APICallT<{
|
SendMessage: POST as APICallT<{
|
||||||
mailbox: number;
|
mailbox: number;
|
||||||
text: string;
|
text: string;
|
||||||
}, ValidResponse & {msgID: number}>
|
}, ValidResponse & {msgID: number, msgType: MsgType}>
|
||||||
},
|
},
|
||||||
Misc: {
|
Misc: {
|
||||||
UploadImage: POST as APICallT<
|
UploadImage: POST as APICallT<
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"v_nationGeneral": "v_nationGeneral.ts",
|
"v_nationGeneral": "v_nationGeneral.ts",
|
||||||
"v_globalDiplomacy": "v_globalDiplomacy",
|
"v_globalDiplomacy": "v_globalDiplomacy",
|
||||||
"v_troop": "v_troop.ts",
|
"v_troop": "v_troop.ts",
|
||||||
"v_vote": "v_vote.ts"
|
"v_vote": "v_vote.ts",
|
||||||
|
"v_front": "v_front.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<template>
|
||||||
|
<span v-if="autorunMode.limit_minutes > 0" v-b-tooltip.hover :title="tooltipText" style="text-decoration: underline;">자율행동</span>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { AutorunUserMode } from '@/defs/API/Global';
|
||||||
|
import type { Entries } from '@/util/Entries';
|
||||||
|
import { ref, toRef, watch } from 'vue';
|
||||||
|
|
||||||
|
type AutorunMode = {
|
||||||
|
limit_minutes: number,
|
||||||
|
options: Record<AutorunUserMode, number>,
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
autorunMode: AutorunMode
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const tooltipText = ref("_");
|
||||||
|
const autorunMode = toRef(props, 'autorunMode');
|
||||||
|
|
||||||
|
function updateTooltipText(autorunMode: AutorunMode){
|
||||||
|
const {options, limit_minutes} = autorunMode;
|
||||||
|
const optionMap: Record<AutorunUserMode, string> = {
|
||||||
|
'battle': '출병',
|
||||||
|
'warp': '순간이동',
|
||||||
|
'recruit': '징병',
|
||||||
|
'recruit_high': '모병',
|
||||||
|
'train': '훈련/사기진작',
|
||||||
|
'chief': '사령턴',
|
||||||
|
'develop': '내정',
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = new Map<AutorunUserMode | 'limit_minutes', string>();
|
||||||
|
for(const [option, value] of Object.entries(options) as Entries<typeof options>){
|
||||||
|
if(value > 0){
|
||||||
|
response.set(option, optionMap[option]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(response.has('recruit_high')){
|
||||||
|
response.delete('recruit');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(limit_minutes >= 43200){
|
||||||
|
response.set('limit_minutes', '항상 유효');
|
||||||
|
}
|
||||||
|
else if(limit_minutes % 60 == 0){
|
||||||
|
response.set('limit_minutes', `${limit_minutes / 60}시간 유효`);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
response.set('limit_minutes', `${limit_minutes}분 유효`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = Array.from(response.values()).join(', ');
|
||||||
|
tooltipText.value = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTooltipText(autorunMode.value);
|
||||||
|
watch(autorunMode, (newAutorunMode) => {
|
||||||
|
updateTooltipText(newAutorunMode);
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
<template>
|
||||||
|
<div class="city-card-basic bg2">
|
||||||
|
<div
|
||||||
|
class="cityNamePanel"
|
||||||
|
:style="{
|
||||||
|
color: isBrightColor(city.nationInfo.color) ? 'black' : 'white',
|
||||||
|
backgroundColor: city.nationInfo.color,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div>【{{ cityRegionText }} | {{ cityLevelText }}】 {{ city.name }}</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="nationNamePanel"
|
||||||
|
:style="{
|
||||||
|
color: isBrightColor(city.nationInfo.color) ? 'black' : 'white',
|
||||||
|
backgroundColor: city.nationInfo.color,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ city.nationInfo.id ? `지배 국가 【 ${city.nationInfo.name} 】` : "공 백 지" }}
|
||||||
|
</div>
|
||||||
|
<div class="gPanel popPanel">
|
||||||
|
<div class="gHead bg1">주민</div>
|
||||||
|
<div class="gBody">
|
||||||
|
<SammoBar :height="7" :percent="(city.pop[0] / city.pop[1]) * 100" />
|
||||||
|
<div class="cellText">{{ city.pop[0].toLocaleString() }} / {{ city.pop[1].toLocaleString() }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gPanel trustPanel">
|
||||||
|
<div class="gHead bg1">민심</div>
|
||||||
|
<div class="gBody">
|
||||||
|
<SammoBar :height="7" :percent="city.trust" />
|
||||||
|
<div class="cellText">{{ city.trust.toLocaleString() }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gPanel agriPanel">
|
||||||
|
<div class="gHead bg1">농업</div>
|
||||||
|
<div class="gBody">
|
||||||
|
<SammoBar :height="7" :percent="(city.agri[0] / city.agri[1]) * 100" />
|
||||||
|
<div class="cellText">
|
||||||
|
{{ city.agri[0].toLocaleString() }}
|
||||||
|
/
|
||||||
|
{{ city.agri[1].toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gPanel commPanel">
|
||||||
|
<div class="gHead bg1">상업</div>
|
||||||
|
<div class="gBody">
|
||||||
|
<SammoBar :height="7" :percent="(city.comm[0] / city.comm[1]) * 100" />
|
||||||
|
<div class="cellText">
|
||||||
|
{{ city.comm[0].toLocaleString() }}
|
||||||
|
/
|
||||||
|
{{ city.comm[1].toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gPanel secuPanel">
|
||||||
|
<div class="gHead bg1">치안</div>
|
||||||
|
<div class="gBody">
|
||||||
|
<SammoBar :height="7" :percent="(city.secu[0] / city.secu[1]) * 100" />
|
||||||
|
<div class="cellText">
|
||||||
|
{{ city.secu[0].toLocaleString() }}
|
||||||
|
/
|
||||||
|
{{ city.secu[1].toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gPanel defPanel">
|
||||||
|
<div class="gHead bg1">수비</div>
|
||||||
|
<div class="gBody">
|
||||||
|
<SammoBar :height="7" :percent="(city.def[0] / city.def[1]) * 100" />
|
||||||
|
<div class="cellText">
|
||||||
|
{{ city.def[0].toLocaleString() }}
|
||||||
|
/
|
||||||
|
{{ city.def[1].toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gPanel wallPanel">
|
||||||
|
<div class="gHead bg1">성벽</div>
|
||||||
|
<div class="gBody">
|
||||||
|
<SammoBar :height="7" :percent="(city.wall[0] / city.wall[1]) * 100" />
|
||||||
|
<div class="cellText">
|
||||||
|
{{ city.wall[0].toLocaleString() }}
|
||||||
|
/
|
||||||
|
{{ city.wall[1].toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gPanel tradePanel">
|
||||||
|
<div class="gHead bg1">시세</div>
|
||||||
|
<div class="gBody">
|
||||||
|
<SammoBar :height="7" :percent="city.trade ?? 100" />
|
||||||
|
<div class="cellText">{{ city.trade ? `${city.trade}%` : "상인 없음" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gPanel officer4Panel">
|
||||||
|
<div class="gHead bg1">태수</div>
|
||||||
|
<div class="gBody cellTextOnly" :style="{ color: getNPCColor(city.officerList[4]?.npc ?? 0) }">
|
||||||
|
{{ city.officerList[4]?.name ?? "-" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gPanel officer3Panel">
|
||||||
|
<div class="gHead bg1">군사</div>
|
||||||
|
<div class="gBody cellTextOnly" :style="{ color: getNPCColor(city.officerList[3]?.npc ?? 0) }">
|
||||||
|
{{ city.officerList[3]?.name ?? "-" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gPanel officer2Panel">
|
||||||
|
<div class="gHead bg1">종사</div>
|
||||||
|
<div class="gBody cellTextOnly" :style="{ color: getNPCColor(city.officerList[2]?.npc ?? 0) }">
|
||||||
|
{{ city.officerList[2]?.name ?? "-" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { unwrap } from "@/util/unwrap";
|
||||||
|
import { isBrightColor } from "@/util/isBrightColor";
|
||||||
|
import { getNPCColor } from "@/utilGame";
|
||||||
|
import type { GameConstStore } from "@/GameConstStore";
|
||||||
|
import { inject, ref, toRef, watch, type Ref } from "vue";
|
||||||
|
import type { GetFrontInfoResponse } from "@/defs/API/Global";
|
||||||
|
import SammoBar from "@/components/SammoBar.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
city: GetFrontInfoResponse["city"];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const gameConstStore = unwrap(inject<Ref<GameConstStore>>("gameConstStore"));
|
||||||
|
|
||||||
|
const city = toRef(props, "city");
|
||||||
|
|
||||||
|
const cityRegionText = ref("");
|
||||||
|
const cityLevelText = ref("");
|
||||||
|
watch(
|
||||||
|
city,
|
||||||
|
(city) => {
|
||||||
|
const cityInfo = gameConstStore.value.cityConst[city.id];
|
||||||
|
cityRegionText.value = gameConstStore.value.cityConstMap.region[cityInfo.region] as string;
|
||||||
|
cityLevelText.value = gameConstStore.value.cityConstMap.level[city.level] as string;
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@scss/common/break_500px.scss";
|
||||||
|
|
||||||
|
.city-card-basic {
|
||||||
|
display: grid;
|
||||||
|
border-right: solid 1px gray;
|
||||||
|
border-bottom: solid 1px gray;
|
||||||
|
|
||||||
|
.cellText {
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cellTextOnly {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.gPanel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 2fr;
|
||||||
|
border-top: solid 1px gray;
|
||||||
|
border-left: solid 1px gray;
|
||||||
|
|
||||||
|
.gHead {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cityNamePanel,
|
||||||
|
.nationNamePanel {
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
border-top: solid 1px gray;
|
||||||
|
border-left: solid 1px gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popPanel {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
grid-template-columns: 1fr 5fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-1000px {
|
||||||
|
.city-card-basic {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
|
|
||||||
|
.cityNamePanel,
|
||||||
|
.nationNamePanel {
|
||||||
|
grid-column: 1 / 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.officer4Panel {
|
||||||
|
grid-column: 4 / 5;
|
||||||
|
grid-row: 3 / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.officer3Panel {
|
||||||
|
grid-column: 4 / 5;
|
||||||
|
grid-row: 4 / 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.officer2Panel {
|
||||||
|
grid-column: 4 / 5;
|
||||||
|
grid-row: 5 / 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-500px {
|
||||||
|
.city-card-basic {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
|
||||||
|
.cityNamePanel,
|
||||||
|
.nationNamePanel {
|
||||||
|
grid-column: 1 / 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
<template>
|
||||||
|
<nav class="gameBottomBar navbar-expand navbar-dark bg-dark d-sm-block d-md-none p-0">
|
||||||
|
<div class="collapse navbar-collapse">
|
||||||
|
<ul class="navbar-nav me-auto mx-auto">
|
||||||
|
<li class="nav-item dropup">
|
||||||
|
<div
|
||||||
|
id="navbarGlobal"
|
||||||
|
class="dropdown-toggle text-white btn btn-sammo-base2"
|
||||||
|
role="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
외부 메뉴
|
||||||
|
</div>
|
||||||
|
<GlobalMenuBar
|
||||||
|
id="navbarGlobalItems"
|
||||||
|
aria-labelledby="navbarGlobal"
|
||||||
|
:globalInfo="globalInfo"
|
||||||
|
:modelValue="globalMenu"
|
||||||
|
:columns="3"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropup">
|
||||||
|
<div
|
||||||
|
id="navbarNation"
|
||||||
|
class="dropdown-toggle btn btn-sammo-nation controlBar"
|
||||||
|
role="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
국가 메뉴
|
||||||
|
</div>
|
||||||
|
<MainControlDropdown
|
||||||
|
id="navbarNationItems"
|
||||||
|
aria-labelledby="navbarNation"
|
||||||
|
:showSecret="showSecret"
|
||||||
|
:permission="frontInfo.general.permission"
|
||||||
|
:myLevel="frontInfo.general.officerLevel"
|
||||||
|
:nationLevel="nationInfo.level"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropup">
|
||||||
|
<div
|
||||||
|
id="navbarQuick"
|
||||||
|
class="dropdown-toggle text-white btn btn-dark"
|
||||||
|
role="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
빠른 이동
|
||||||
|
</div>
|
||||||
|
<ul id="navbarQuickItems" class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
|
||||||
|
<li><a class="dropdown-item disabled">국가 정보</a></li>
|
||||||
|
<hr class="dropdown-divider" />
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item" @click="scrollToSelector('.nationNotice')">방침</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item" @click="scrollToSelector('#reservedCommandPanel')">명령</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item" @click="scrollToSelector('.nationInfo')">국가</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item" @click="scrollToSelector('.generalInfo')">장수</button>
|
||||||
|
</li>
|
||||||
|
<li><button type="button" class="dropdown-item" @click="scrollToSelector('.cityInfo')">도시</button></li>
|
||||||
|
<li><a class="dropdown-item disabled">동향 정보</a></li>
|
||||||
|
<hr class="dropdown-divider" />
|
||||||
|
<li><button type="button" class="dropdown-item" @click="scrollToSelector('.mapView')">지도</button></li>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item" @click="scrollToSelector('.PublicRecord')">
|
||||||
|
동향
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item" @click="scrollToSelector('.GeneralLog')">개인</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item" @click="scrollToSelector('.WorldHistory')">정세</button>
|
||||||
|
</li>
|
||||||
|
<li><span> </span></li>
|
||||||
|
<li><a class="dropdown-item disabled">메시지</a></li>
|
||||||
|
<hr class="dropdown-divider" />
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item" @click="scrollToSelector('.PublicTalk > .stickyAnchor')">전체</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item" @click="scrollToSelector('.NationalTalk > .stickyAnchor')">국가</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item" @click="scrollToSelector('.PrivateTalk > .stickyAnchor')">개인</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="dropdown-item" @click="scrollToSelector('.DiplomacyTalk > .stickyAnchor')">외교</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button type="button" class="btn btn-sammo-base2" @click="moveLobby">로비로</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="refreshPage btn btn-sammo-base2 text-white" role="button" @click="emit('refresh')">갱신</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { GetFrontInfoResponse, GetMenuResponse } from "@/defs/API/Global";
|
||||||
|
import { scrollToSelector } from "@/util/scrollToSelector";
|
||||||
|
import { toRefs, watch, ref, computed } from "vue";
|
||||||
|
import GlobalMenuBar from "./GlobalMenuDropdown.vue";
|
||||||
|
import MainControlDropdown from "./MainControlDropdown.vue";
|
||||||
|
|
||||||
|
//FIXME: scrollToSelector
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
frontInfo: GetFrontInfoResponse;
|
||||||
|
globalMenu: GetMenuResponse["menu"];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "refresh"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { frontInfo, globalMenu } = toRefs(props);
|
||||||
|
|
||||||
|
const globalInfo = ref(frontInfo.value.global);
|
||||||
|
watch(frontInfo, (frontInfo) => {
|
||||||
|
globalInfo.value = frontInfo.global;
|
||||||
|
});
|
||||||
|
|
||||||
|
const nationInfo = ref(frontInfo.value.nation);
|
||||||
|
watch(frontInfo, (frontInfo) => {
|
||||||
|
nationInfo.value = frontInfo.nation;
|
||||||
|
});
|
||||||
|
|
||||||
|
function moveLobby() {
|
||||||
|
location.replace("../");
|
||||||
|
}
|
||||||
|
|
||||||
|
const showSecret = computed(() => {
|
||||||
|
if (!frontInfo.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (frontInfo.value.general.permission >= 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (frontInfo.value.general.officerLevel >= 2) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@scss/common/break_500px.scss";
|
||||||
|
@import "@scss/common/base.scss";
|
||||||
|
|
||||||
|
@include media-500px {
|
||||||
|
.gameBottomBar {
|
||||||
|
.nav-item ul.dropdown-menu {
|
||||||
|
max-height: calc(100vh - 50px);
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item > .btn {
|
||||||
|
text-align: center;
|
||||||
|
width: 125px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
box-shadow: 0 -1px 0 $dark;
|
||||||
|
border-left: none !important;
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navbarNationItems {
|
||||||
|
columns: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navbarQuickItems {
|
||||||
|
columns: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<h3 class="scenarioName center">
|
||||||
|
{{ gameConstStore?.gameConst.title }} {{ serverName }}{{ frontInfo?.global.serverCnt }}기
|
||||||
|
<span class="avoid-wrap" style="color: cyan">{{ frontInfo?.global.scenarioText }}</span>
|
||||||
|
</h3>
|
||||||
|
<div v-if="frontInfo" class="gameInfo row gx-0">
|
||||||
|
<div class="s-border-t col py-2 col-8 col-md-4 subScenarioName" style="color: cyan">
|
||||||
|
{{ globalInfo.scenarioText }}
|
||||||
|
</div>
|
||||||
|
<div class="s-border-t col py-2 col-4 col-md-2 subNPCType" style="color: cyan">
|
||||||
|
NPC 수, 상성:
|
||||||
|
{{ globalInfo.extendedGeneral ? "확장" : "표준" }}
|
||||||
|
{{ globalInfo.isFiction ? "가상" : "사실" }}
|
||||||
|
</div>
|
||||||
|
<div class="s-border-t col py-2 col-4 col-md-2 subNPCMode" style="color: cyan">
|
||||||
|
NPC선택: {{ ["불가능", "가능", "선택 생성"][globalInfo.npcMode] }}
|
||||||
|
</div>
|
||||||
|
<div class="s-border-t col py-2 col-4 col-md-2 subTournamentMode" style="color: cyan">
|
||||||
|
토너먼트: 경기당 {{ calcTournamentTerm(globalInfo.turnterm) }}분
|
||||||
|
</div>
|
||||||
|
<div class="s-border-t col py-2 col-4 col-md-2 subOtherSetting" style="color: cyan">
|
||||||
|
기타 설정:
|
||||||
|
<AutorunInfo :autorunMode="globalInfo.autorunUser" />
|
||||||
|
</div>
|
||||||
|
<div class="s-border-t col py-2 col-8 col-md-4 subYearMonth">
|
||||||
|
현재: {{ globalInfo.year }}年 {{ globalInfo.month }}月 ({{ globalInfo.turnterm }}분 턴 서버)
|
||||||
|
</div>
|
||||||
|
<div class="s-border-t col py-2 col-4 col-md-2 subOnlineUserCnt">
|
||||||
|
전체 접속자 수: {{ globalInfo.onlineUserCnt.toLocaleString() }}명
|
||||||
|
</div>
|
||||||
|
<div class="s-border-t col py-2 col-4 col-md-2 subAPILimit">
|
||||||
|
턴당 갱신횟수: {{ globalInfo.apiLimit.toLocaleString() }}회
|
||||||
|
</div>
|
||||||
|
<div class="s-border-t col py-2 col-8 col-md-4 subGeneralCnt">
|
||||||
|
등록 장수: 유저
|
||||||
|
{{ createdUserCnt.toLocaleString() }} / {{ globalInfo.generalCntLimit.toLocaleString() }} +
|
||||||
|
<span style="color: cyan">NPC {{ createdNPCCnt.toLocaleString() }} 명</span>
|
||||||
|
</div>
|
||||||
|
<div class="s-border-t py-2 col col-6 col-md-4 subTournamentState">
|
||||||
|
<span v-if="frontInfo.global.tournamentType">
|
||||||
|
<a v-if="tournamentStep.availableJoin" href="b_tournament.php" target="_blank">
|
||||||
|
↑<span style="color: cyan"
|
||||||
|
>{{ formatTournamentType(frontInfo.global.tournamentType) }}
|
||||||
|
<span style="color: orange">{{ tournamentStep.state }}</span> {{ tournamentStep.nextText }}
|
||||||
|
{{ formatTime(tournamentTime).substring(11, 16) }}</span
|
||||||
|
>↑
|
||||||
|
</a>
|
||||||
|
<span v-else>
|
||||||
|
↑<span style="color: cyan"
|
||||||
|
>{{ formatTournamentType(frontInfo.global.tournamentType) }}
|
||||||
|
<span style="color: magenta">{{ tournamentStep.state }}</span> {{ tournamentStep.nextText }}
|
||||||
|
{{ formatTime(tournamentTime).substring(11, 16) }}</span
|
||||||
|
>↑
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span v-else style="color: magenta"> 현재 토너먼트 경기 없음 </span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="s-border-t py-2 col col-6 col-md-2 subLastExecuted"
|
||||||
|
:style="{ color: serverLocked ? 'magenta' : 'cyan' }"
|
||||||
|
>
|
||||||
|
동작 시각: {{ formatTime(lastExecuted).substring(5) }}
|
||||||
|
</div>
|
||||||
|
<div class="s-border-t py-2 col col-6 col-md-2 subAuctionState">
|
||||||
|
<a v-if="globalInfo.auctionCount" href="v_auction.php" target="_blank" style="color: cyan">
|
||||||
|
{{ globalInfo.auctionCount.toLocaleString() }}건 거래 진행중
|
||||||
|
</a>
|
||||||
|
<span v-else style="color: magenta">진행중인 거래 없음</span>
|
||||||
|
</div>
|
||||||
|
<div class="s-border-t py-2 col col-6 col-md-4 subVoteState">
|
||||||
|
<a v-if="globalInfo.lastVote" href="v_vote.php" target="_blank">
|
||||||
|
<span style="color: cyan">설문 진행 중: </span><span>{{ globalInfo.lastVote.title }}</span>
|
||||||
|
</a>
|
||||||
|
<span v-else style="color: magenta">진행중인 설문 없음</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { GetFrontInfoResponse } from "@/defs/API/Global";
|
||||||
|
import type { GameConstStore } from "@/GameConstStore";
|
||||||
|
import { unwrap } from "@/util/unwrap";
|
||||||
|
import { inject, toRefs, ref, watch } from "vue";
|
||||||
|
import { formatTime } from "@/util/formatTime";
|
||||||
|
import { calcTournamentTerm } from "@/utilGame";
|
||||||
|
import { formatTournamentStep, type TournamentStepType, formatTournamentType } from "@/utilGame/formatTournament";
|
||||||
|
import { parseTime } from "@/util/parseTime";
|
||||||
|
import AutorunInfo from "./AutorunInfo.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
frontInfo: GetFrontInfoResponse;
|
||||||
|
serverName: string;
|
||||||
|
serverLocked: boolean;
|
||||||
|
lastExecuted: Date;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { frontInfo, serverName, serverLocked, lastExecuted } = toRefs(props);
|
||||||
|
|
||||||
|
const globalInfo = ref(frontInfo.value.global);
|
||||||
|
|
||||||
|
const gameConstStore = unwrap(inject<GameConstStore>("gameConstStore"));
|
||||||
|
|
||||||
|
const generalCnt = ref(new Map<number, number>());
|
||||||
|
const createdUserCnt = ref(0);
|
||||||
|
const createdNPCCnt = ref(0);
|
||||||
|
|
||||||
|
const tournamentStep = ref<TournamentStepType>({
|
||||||
|
availableJoin: false,
|
||||||
|
state: "초기화 중",
|
||||||
|
nextText: "",
|
||||||
|
});
|
||||||
|
const tournamentTime = ref<Date>(new Date());
|
||||||
|
|
||||||
|
function updateFrontInfo(frontInfo: GetFrontInfoResponse){
|
||||||
|
const global = frontInfo.global;
|
||||||
|
globalInfo.value = global;
|
||||||
|
|
||||||
|
const value = new Map<number, number>();
|
||||||
|
let userCnt = 0;
|
||||||
|
let npcCnt = 0;
|
||||||
|
for (const [npcType, cnt] of global.genCount) {
|
||||||
|
value.set(npcType, cnt);
|
||||||
|
if (npcType < 2) {
|
||||||
|
userCnt += cnt;
|
||||||
|
} else {
|
||||||
|
npcCnt += cnt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
generalCnt.value = value;
|
||||||
|
createdUserCnt.value = userCnt;
|
||||||
|
createdNPCCnt.value = npcCnt;
|
||||||
|
|
||||||
|
tournamentStep.value = formatTournamentStep(global.tournamentState);
|
||||||
|
tournamentTime.value = parseTime(global.tournamentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(frontInfo, updateFrontInfo, {immediate: true});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@scss/common/break_500px.scss";
|
||||||
|
|
||||||
|
.gameInfo {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subVoteState,
|
||||||
|
.subAuctionState,
|
||||||
|
.subTournamentState {
|
||||||
|
a {
|
||||||
|
text-decoration: gray underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<template>
|
||||||
|
<div class="global-menu">
|
||||||
|
<template v-for="(item, idx) in filteredMenu" :key="idx">
|
||||||
|
<BButton
|
||||||
|
v-if="item.type === 'item'"
|
||||||
|
class="col"
|
||||||
|
:variant="variant"
|
||||||
|
:href="item.url"
|
||||||
|
:target="item.newTab ? '_blank' : undefined"
|
||||||
|
>{{ item.name }}</BButton
|
||||||
|
>
|
||||||
|
<template v-else-if="item.type === 'multi'">
|
||||||
|
<BDropdown :variant="variant" :text="item.name" class="col">
|
||||||
|
<BDropdownItem
|
||||||
|
v-for="(subItem, subIdx) in item.subMenu"
|
||||||
|
:key="subIdx"
|
||||||
|
:variant="variant"
|
||||||
|
:href="subItem.url"
|
||||||
|
:target="subItem.newTab ? '_blank' : undefined"
|
||||||
|
>{{ subItem.name }}</BDropdownItem
|
||||||
|
>
|
||||||
|
</BDropdown>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.type === 'split'">
|
||||||
|
<BDropdown
|
||||||
|
split
|
||||||
|
class="col"
|
||||||
|
:variant="variant"
|
||||||
|
:text="item.main.name"
|
||||||
|
:splitHref="item.main.url"
|
||||||
|
@click="splitClick(item.main)($event)"
|
||||||
|
>
|
||||||
|
<BDropdownItem
|
||||||
|
v-for="(subItem, subIdx) in item.subMenu"
|
||||||
|
:key="subIdx"
|
||||||
|
:href="subItem.url"
|
||||||
|
:target="subItem.newTab ? '_blank' : undefined"
|
||||||
|
>{{ subItem.name }}</BDropdownItem
|
||||||
|
>
|
||||||
|
</BDropdown>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { GetFrontInfoResponse, GetMenuResponse, MenuItem, MenuMulti, MenuSplit } from "@/defs/API/Global";
|
||||||
|
import { BButton, BDropdown, BDropdownItem, type ButtonVariant } from "bootstrap-vue-3";
|
||||||
|
import { isArray } from "lodash";
|
||||||
|
import { computed, toRef } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: GetMenuResponse["menu"];
|
||||||
|
globalInfo: GetFrontInfoResponse["global"];
|
||||||
|
variant: ButtonVariant | 'sammo-base2',
|
||||||
|
mobileRowSize?: number,
|
||||||
|
desktopRowSize?: number,
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const mobileRowSize = computed(() => {
|
||||||
|
return props.mobileRowSize || 4;
|
||||||
|
});
|
||||||
|
|
||||||
|
const desktopRowSize = computed(() => {
|
||||||
|
return props.desktopRowSize || 8;
|
||||||
|
});
|
||||||
|
|
||||||
|
const variant = computed(() => {
|
||||||
|
return props.variant as ButtonVariant;
|
||||||
|
});
|
||||||
|
|
||||||
|
const modelValue = toRef(props, "modelValue");
|
||||||
|
const globalInfo = toRef(props, "globalInfo");
|
||||||
|
|
||||||
|
type MenuVariant = MenuItem | MenuSplit | MenuMulti;
|
||||||
|
|
||||||
|
function filterMenu(menu: MenuVariant | MenuVariant[]): MenuVariant | MenuVariant[] | undefined {
|
||||||
|
if (isArray(menu)) {
|
||||||
|
return menu.filter(filterMenu) as MenuVariant[];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menu.type === "item") {
|
||||||
|
if (!menu.condShowVar) {
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
const cond = menu.condShowVar;
|
||||||
|
if (cond in globalInfo.value) {
|
||||||
|
if (cond.startsWith("!")) {
|
||||||
|
if (!globalInfo.value[cond.slice(1) as keyof GetFrontInfoResponse["global"]]) {
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
} else if (globalInfo.value[cond as keyof GetFrontInfoResponse["global"]]) {
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menu.type === "multi") {
|
||||||
|
const filtered = menu.subMenu.filter(filterMenu) as MenuVariant[];
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (filtered.length === 1) {
|
||||||
|
return filtered[0];
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menu.type === "split") {
|
||||||
|
const filterMain = filterMenu(menu.main);
|
||||||
|
if (!filterMain) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const filtered = menu.subMenu.filter(filterMenu) as MenuVariant[];
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
return filterMain;
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredMenu = computed(() => filterMenu(modelValue.value) as GetMenuResponse["menu"]);
|
||||||
|
|
||||||
|
function splitClick(menu: MenuItem) {
|
||||||
|
return (e: Event) => {
|
||||||
|
if (!menu.newTab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
window.open(menu.url);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@scss/common/break_500px.scss";
|
||||||
|
.global-menu {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-1000px {
|
||||||
|
.global-menu {
|
||||||
|
grid-template-columns: repeat(v-bind(desktopRowSize), 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@include media-500px {
|
||||||
|
.global-menu {
|
||||||
|
grid-template-columns: repeat(v-bind(mobileRowSize), 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-start">
|
||||||
|
<template v-for="(item, idx) in filteredMenu" :key="idx">
|
||||||
|
<li v-if="item.type === 'item'">
|
||||||
|
<a class="dropdown-item" :href="item.url" :target="item.newTab ? '_blank' : undefined">
|
||||||
|
{{ item.name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<template v-else-if="item.type === 'multi'">
|
||||||
|
<li :style="{ orphans: item.subMenu.length + 1 }">
|
||||||
|
<span class="dropdown-item disabled">{{ item.name }}</span>
|
||||||
|
</li>
|
||||||
|
<li v-for="(subItem, subIdx) in item.subMenu" :key="subIdx">
|
||||||
|
<a class="dropdown-item subItem" :href="subItem.url" :target="subItem.newTab ? '_blank' : undefined">{{
|
||||||
|
subItem.name
|
||||||
|
}}</a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="item.type === 'split'">
|
||||||
|
<li :style="{ orphans: item.subMenu.length + 1 }">
|
||||||
|
<a class="dropdown-item" :href="item.main.url" :target="item.main.newTab ? '_blank' : undefined">{{
|
||||||
|
item.main.name
|
||||||
|
}}</a>
|
||||||
|
</li>
|
||||||
|
<li v-for="(subItem, subIdx) in item.subMenu" :key="subIdx">
|
||||||
|
<a class="dropdown-item subItem" :href="subItem.url" :target="subItem.newTab ? '_blank' : undefined">{{
|
||||||
|
subItem.name
|
||||||
|
}}</a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { GetFrontInfoResponse, GetMenuResponse, MenuItem, MenuMulti, MenuSplit } from "@/defs/API/Global";
|
||||||
|
import { isArray } from "lodash";
|
||||||
|
import { computed, toRef } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: GetMenuResponse["menu"];
|
||||||
|
globalInfo: GetFrontInfoResponse["global"];
|
||||||
|
mobileRowSize?: number;
|
||||||
|
desktopRowSize?: number;
|
||||||
|
columns?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const columns = computed(() => {
|
||||||
|
return props.columns ?? 3;
|
||||||
|
});
|
||||||
|
|
||||||
|
const modelValue = toRef(props, "modelValue");
|
||||||
|
const globalInfo = toRef(props, "globalInfo");
|
||||||
|
|
||||||
|
type MenuVariant = MenuItem | MenuSplit | MenuMulti;
|
||||||
|
|
||||||
|
function filterMenu(menu: MenuVariant | MenuVariant[]): MenuVariant | MenuVariant[] | undefined {
|
||||||
|
if (isArray(menu)) {
|
||||||
|
return menu.filter(filterMenu) as MenuVariant[];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menu.type === "item") {
|
||||||
|
if (!menu.condShowVar) {
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
const cond = menu.condShowVar;
|
||||||
|
if (cond in globalInfo.value) {
|
||||||
|
if (cond.startsWith("!")) {
|
||||||
|
if (!globalInfo.value[cond.slice(1) as keyof GetFrontInfoResponse["global"]]) {
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
} else if (globalInfo.value[cond as keyof GetFrontInfoResponse["global"]]) {
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menu.type === "multi") {
|
||||||
|
const filtered = menu.subMenu.filter(filterMenu) as MenuVariant[];
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (filtered.length === 1) {
|
||||||
|
return filtered[0];
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menu.type === "split") {
|
||||||
|
const filterMain = filterMenu(menu.main);
|
||||||
|
if (!filterMain) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const filtered = menu.subMenu.filter(filterMenu) as MenuVariant[];
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
return filterMain;
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredMenu = computed(() => filterMenu(modelValue.value) as GetMenuResponse["menu"]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@scss/common/break_500px.scss";
|
||||||
|
|
||||||
|
.subItem {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
columns: v-bind(columns);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<div class="controlBar">
|
||||||
|
<a href="v_board.php" :class="`commandButton btn btn-sammo-nation ${myLevel >= 1 ? '' : 'disabled'}`">회 의 실</a>
|
||||||
|
<a
|
||||||
|
href="v_board.php?isSecret=true"
|
||||||
|
:class="`commandButton ${permission >= 2 ? '' : 'disabled'} btn btn-sammo-nation`"
|
||||||
|
>기 밀 실</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="v_troop.php"
|
||||||
|
:class="`commandButton ${myLevel >= 1 && nationLevel >= 1 ? '' : 'disabled'} btn btn-sammo-nation`"
|
||||||
|
>부대 편성</a
|
||||||
|
>
|
||||||
|
<a href="t_diplomacy.php" :class="`commandButton ${showSecret ? '' : 'disabled'} btn btn-sammo-nation`">외 교 부</a>
|
||||||
|
<a href="b_myBossInfo.php" :class="`commandButton ${myLevel >= 1 ? '' : 'disabled'} btn btn-sammo-nation`"
|
||||||
|
>인 사 부</a
|
||||||
|
>
|
||||||
|
<a href="v_nationStratFinan.php" :class="`commandButton ${showSecret ? '' : 'disabled'} btn btn-sammo-nation`"
|
||||||
|
>내 무 부</a
|
||||||
|
>
|
||||||
|
<a href="v_chiefCenter.php" :class="`commandButton ${showSecret ? '' : 'disabled'} btn btn-sammo-nation`"
|
||||||
|
>사 령 부</a
|
||||||
|
>
|
||||||
|
<a href="v_NPCControl.php" :class="`commandButton ${showSecret ? '' : 'disabled'} btn btn-sammo-nation`"
|
||||||
|
>NPC 정책</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="b_genList.php"
|
||||||
|
target="_blank"
|
||||||
|
:class="`open-window commandButton btn btn-sammo-nation ${showSecret ? '' : 'disabled'}`"
|
||||||
|
>암 행 부</a
|
||||||
|
>
|
||||||
|
<a href="b_tournament.php" target="_blank" class="commandButton btn btn-sammo-nation">토 너 먼 트</a>
|
||||||
|
<a href="b_myKingdomInfo.php" :class="`commandButton btn btn-sammo-nation ${myLevel >= 1 ? '' : 'disabled'}`"
|
||||||
|
>세력 정보</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="b_myCityInfo.php"
|
||||||
|
:class="`commandButton btn btn-sammo-nation ${myLevel >= 1 && nationLevel >= 1 ? '' : 'disabled'}`"
|
||||||
|
>세력 도시</a
|
||||||
|
>
|
||||||
|
<a href="v_nationGeneral.php" :class="`commandButton btn btn-sammo-nation ${myLevel >= 1 ? '' : 'disabled'}`"
|
||||||
|
>세력 장수</a
|
||||||
|
>
|
||||||
|
<a href="v_globalDiplomacy.php" class="commandButton btn btn-sammo-nation">중원 정보</a>
|
||||||
|
<a href="b_currentCity.php" class="commandButton btn btn-sammo-nation">현재 도시</a>
|
||||||
|
<a
|
||||||
|
href="v_battleCenter.php"
|
||||||
|
target="_blank"
|
||||||
|
:class="`open-window commandButton btn btn-sammo-nation ${showSecret ? '' : 'disabled'}`"
|
||||||
|
>감 찰 부</a
|
||||||
|
>
|
||||||
|
<a href="v_inheritPoint.php" class="commandButton btn btn-sammo-nation">유산 관리</a>
|
||||||
|
<a href="b_myPage.php" class="commandButton btn btn-sammo-nation">내 정보&설정</a>
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="v_auction.php" target="_blank" class="open-window commandButton btn btn-sammo-nation">경 매 장</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-sammo-nation dropdown-toggle dropdown-toggle-split"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="visually-hidden">Toggle Dropdown</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li>
|
||||||
|
<a href="v_auction.php" target="_blank" class="open-window commandButton dropdown-item">금/쌀 경매장</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="v_auction.php?type=unique" target="_blank" class="open-window commandButton dropdown-item"
|
||||||
|
>유니크 경매장</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<a href="b_betting.php" target="_blank" class="commandButton btn btn-sammo-nation">베 팅 장</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { toRefs } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
showSecret: boolean;
|
||||||
|
permission: number;
|
||||||
|
myLevel: number;
|
||||||
|
nationLevel: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { showSecret, permission, myLevel, nationLevel } = toRefs(props);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@scss/common/break_500px.scss";
|
||||||
|
|
||||||
|
@include media-500px {
|
||||||
|
.controlBar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-1000px {
|
||||||
|
.controlBar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(10, 1fr);
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<ul class="dropdown-menu dromdown-menu-start">
|
||||||
|
<li><a href="v_board.php" :class="`dropdown-item ${myLevel >= 1 ? '' : 'disabled'}`">회의실</a></li>
|
||||||
|
<li>
|
||||||
|
<a href="v_board.php?isSecret=true" :class="`dropdown-item ${permission >= 2 ? '' : 'disabled'} `">기밀실</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="v_troop.php" :class="`dropdown-item ${myLevel >= 1 && nationLevel >= 1 ? '' : 'disabled'} `">부대 편성</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="t_diplomacy.php" :class="`dropdown-item ${showSecret ? '' : 'disabled'} `">외교부</a></li>
|
||||||
|
<li><a href="b_myBossInfo.php" :class="`dropdown-item ${myLevel >= 1 ? '' : 'disabled'} `">인사부</a></li>
|
||||||
|
<li><a href="v_nationStratFinan.php" :class="`dropdown-item ${showSecret ? '' : 'disabled'} `">내무부</a></li>
|
||||||
|
<li><a href="v_chiefCenter.php" :class="`dropdown-item ${showSecret ? '' : 'disabled'} `">사령부</a></li>
|
||||||
|
<li><a href="v_NPCControl.php" :class="`dropdown-item ${showSecret ? '' : 'disabled'} `">NPC 정책</a></li>
|
||||||
|
<li>
|
||||||
|
<a href="b_genList.php" target="_blank" :class="`dropdown-item open-window ${showSecret ? '' : 'disabled'}`"
|
||||||
|
>암행부</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li><a href="b_tournament.php" class="dropdown-item" target="_blank">토너먼트</a></li>
|
||||||
|
<li><a href="b_myKingdomInfo.php" :class="`dropdown-item ${myLevel >= 1 ? '' : 'disabled'}`">세력 정보</a></li>
|
||||||
|
<li>
|
||||||
|
<a href="b_myCityInfo.php" :class="`dropdown-item ${myLevel >= 1 && nationLevel >= 1 ? '' : 'disabled'}`"
|
||||||
|
>세력도시</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li><a href="v_nationGeneral.php" :class="`dropdown-item ${myLevel >= 1 ? '' : 'disabled'}`">세력 장수</a></li>
|
||||||
|
<li><a href="v_globalDiplomacy.php" class="dropdown-item">중원 정보</a></li>
|
||||||
|
<li><a href="b_currentCity.php" class="dropdown-item">현재 도시</a></li>
|
||||||
|
<li>
|
||||||
|
<a href="v_battleCenter.php" target="_blank" :class="`dropdown-item open-window ${showSecret ? '' : 'disabled'}`"
|
||||||
|
>감찰부</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li><a href="v_inheritPoint.php" class="dropdown-item">유산 관리</a></li>
|
||||||
|
<li><a href="b_myPage.php" class="dropdown-item">내 정보&설정</a></li>
|
||||||
|
<li style="orphans: 2">
|
||||||
|
<a href="v_auction.php" target="_blank" class="dropdown-item open-window">금/쌀 경매장</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="v_auction.php?type=unique" target="_blank" class="dropdown-item open-window">유니크 경매장</a></li>
|
||||||
|
<li><a href="b_betting.php" class="dropdown-item" target="_blank">베팅장</a></li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, toRefs } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
showSecret: boolean;
|
||||||
|
permission: number;
|
||||||
|
myLevel: number;
|
||||||
|
nationLevel: number;
|
||||||
|
columns?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const columns = computed(() => {
|
||||||
|
return props.columns ?? 4;
|
||||||
|
});
|
||||||
|
const { showSecret, permission, myLevel, nationLevel } = toRefs(props);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.subItem {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
columns: v-bind(columns);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,755 @@
|
|||||||
|
<template>
|
||||||
|
<div class="MessagePanel">
|
||||||
|
<div class="MessageInputForm bg0 row gx-0">
|
||||||
|
<div id="mailbox_list-col" class="col-6 col-md-2 d-grid">
|
||||||
|
<BFormSelect v-model="targetMailbox" class="bg-dark text-white">
|
||||||
|
<optgroup
|
||||||
|
v-for="group of mailboxList"
|
||||||
|
:key="group.label"
|
||||||
|
:label="group.label"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: group.color,
|
||||||
|
color: !group.color ? undefined : isBrightColor(group.color) ? '#000000' : '#ffffff',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<BFormSelectOption
|
||||||
|
v-for="target of group.options"
|
||||||
|
:key="target.value"
|
||||||
|
:disabled="target.disabled"
|
||||||
|
:value="target.value"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: target.color ?? '#000000',
|
||||||
|
color: isBrightColor(target.color ?? '#000000') ? '#000000' : '#ffffff',
|
||||||
|
}"
|
||||||
|
>{{ target.text }}
|
||||||
|
</BFormSelectOption>
|
||||||
|
</optgroup>
|
||||||
|
</BFormSelect>
|
||||||
|
</div>
|
||||||
|
<div id="msg_input-col" class="col-12 col-md-8 d-grid">
|
||||||
|
<input v-model="newMessageText" type="text" maxlength="99" class="form-control" @keydown.enter="sendMessage" />
|
||||||
|
</div>
|
||||||
|
<div id="msg_submit-col" class="col-6 col-md-2 d-grid">
|
||||||
|
<BButton variant="primary" @click="sendMessage">서신전달&갱신</BButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="PublicTalk">
|
||||||
|
<div class="stickyAnchor"></div>
|
||||||
|
<div class="BoardHeader bg0">전체 메시지</div>
|
||||||
|
<template v-if="messagePublic.length == 0">
|
||||||
|
<div>메시지가 없습니다.</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="MessageList">
|
||||||
|
<MessagePlate
|
||||||
|
v-for="msg of messagePublic"
|
||||||
|
:key="msg.id"
|
||||||
|
:modelValue="msg"
|
||||||
|
:generalID="generalID"
|
||||||
|
:generalName="generalName"
|
||||||
|
:nationID="nationID"
|
||||||
|
:permissionLevel="permissionLevel"
|
||||||
|
></MessagePlate>
|
||||||
|
<div class="d-grid Actions">
|
||||||
|
<button type="button" class="btn btn-dark only-mobile" @click="foldMessage($event, 'public')">접기</button>
|
||||||
|
<button type="button" class="btn btn-secondary" @click="loadOldMessage($event, 'public')">
|
||||||
|
이전 메시지 불러오기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="NationalTalk">
|
||||||
|
<div class="stickyAnchor"></div>
|
||||||
|
<div class="BoardHeader bg0">국가 메시지</div>
|
||||||
|
<template v-if="messageNational.length == 0">
|
||||||
|
<div>메시지가 없습니다.</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="MessageList">
|
||||||
|
<MessagePlate
|
||||||
|
v-for="msg of messageNational"
|
||||||
|
:key="msg.id"
|
||||||
|
:modelValue="msg"
|
||||||
|
:generalID="generalID"
|
||||||
|
:generalName="generalName"
|
||||||
|
:nationID="nationID"
|
||||||
|
:permissionLevel="permissionLevel"
|
||||||
|
></MessagePlate>
|
||||||
|
<div class="d-grid Actions">
|
||||||
|
<button type="button" class="btn btn-dark only-mobile" @click="foldMessage($event, 'national')">접기</button>
|
||||||
|
<button type="button" class="btn btn-secondary" @click="loadOldMessage($event, 'national')">
|
||||||
|
이전 메시지 불러오기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="PrivateTalk">
|
||||||
|
<div class="stickyAnchor"></div>
|
||||||
|
<div class="BoardHeader bg0 d-flex">
|
||||||
|
<div class="flex-grow-1 align-self-center">개인 메시지</div>
|
||||||
|
<div>
|
||||||
|
<BButton
|
||||||
|
v-if="messagePrivate.length > 0 && latestPrivateMsgToastInfo[2] > latestPrivateMsgToastInfo[1]"
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
@click="readLatestMsg('private')"
|
||||||
|
>모두 읽음</BButton
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="messagePrivate.length == 0">
|
||||||
|
<div>메시지가 없습니다.</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="MessageList">
|
||||||
|
<MessagePlate
|
||||||
|
v-for="msg of messagePrivate"
|
||||||
|
:key="msg.id"
|
||||||
|
:modelValue="msg"
|
||||||
|
:generalID="generalID"
|
||||||
|
:generalName="generalName"
|
||||||
|
:nationID="nationID"
|
||||||
|
:permissionLevel="permissionLevel"
|
||||||
|
></MessagePlate>
|
||||||
|
<div class="d-grid Actions">
|
||||||
|
<button type="button" class="btn btn-dark only-mobile" @click="foldMessage($event, 'private')">접기</button>
|
||||||
|
<button type="button" class="btn btn-secondary" @click="loadOldMessage($event, 'private')">
|
||||||
|
이전 메시지 불러오기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="DiplomacyTalk">
|
||||||
|
<div class="stickyAnchor"></div>
|
||||||
|
<div class="BoardHeader bg0 d-flex">
|
||||||
|
<div class="flex-grow-1 align-self-center">외교 메시지</div>
|
||||||
|
<div>
|
||||||
|
<BButton
|
||||||
|
v-if="messageDiplomacy.length > 0 && latestDiplomacyMsgToastInfo[2] > latestDiplomacyMsgToastInfo[1]"
|
||||||
|
ize="sm"
|
||||||
|
variant="secondary"
|
||||||
|
@click="readLatestMsg('diplomacy')"
|
||||||
|
>모두 읽음</BButton
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="messageDiplomacy.length == 0">
|
||||||
|
<div>메시지가 없습니다.</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="MessageList">
|
||||||
|
<MessagePlate
|
||||||
|
v-for="msg of messageDiplomacy"
|
||||||
|
:key="msg.id"
|
||||||
|
:modelValue="msg"
|
||||||
|
:generalID="generalID"
|
||||||
|
:generalName="generalName"
|
||||||
|
:nationID="nationID"
|
||||||
|
:permissionLevel="permissionLevel"
|
||||||
|
></MessagePlate>
|
||||||
|
<div class="d-grid Actions">
|
||||||
|
<button type="button" class="btn btn-dark only-mobile" @click="foldMessage($event, 'diplomacy')">접기</button>
|
||||||
|
<button type="button" class="btn btn-secondary" @click="loadOldMessage($event, 'diplomacy')">
|
||||||
|
이전 메시지 불러오기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
declare const staticValues: {
|
||||||
|
serverName: string;
|
||||||
|
serverNick: string;
|
||||||
|
serverID: string;
|
||||||
|
mapName: string;
|
||||||
|
unitSet: string;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref, toRef, watch, type Ref } from "vue";
|
||||||
|
import { delay } from "@/util/delay";
|
||||||
|
import { SammoAPI } from "@/SammoAPI";
|
||||||
|
import { isString } from "lodash";
|
||||||
|
import type { MabilboxListResponse, MsgItem, MsgResponse, MsgType } from "@/defs/API/Message";
|
||||||
|
import MessagePlate from "@/components/MessagePlate.vue";
|
||||||
|
import { useToast, BFormSelect } from "bootstrap-vue-3";
|
||||||
|
import { unwrap } from "@/util/unwrap";
|
||||||
|
import { isBrightColor } from "@/util/isBrightColor";
|
||||||
|
|
||||||
|
const serverID = staticValues.serverID;
|
||||||
|
const toasts = unwrap(useToast());
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "request-refresh"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
generalID: number;
|
||||||
|
generalName: string;
|
||||||
|
nationID: number;
|
||||||
|
permissionLevel: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const generalID = toRef(props, "generalID");
|
||||||
|
const generalName = toRef(props, "generalName");
|
||||||
|
const nationID = toRef(props, "nationID");
|
||||||
|
const permissionLevel = toRef(props, "permissionLevel");
|
||||||
|
|
||||||
|
let nationMailbox = nationID.value + 9000;
|
||||||
|
watch(nationID, (newVal) => {
|
||||||
|
nationMailbox = newVal + 9000;
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastSequence = ref(-1);
|
||||||
|
|
||||||
|
const initRefreshLimit = 20;
|
||||||
|
let refreshLimit = initRefreshLimit;
|
||||||
|
const refreshP: Set<Promise<boolean>> = new Set();
|
||||||
|
let lastRefreshDone = 0;
|
||||||
|
let refreshTimer: null | number = null;
|
||||||
|
|
||||||
|
const messageStorage = new Map<number, MsgItem>();
|
||||||
|
const messagePublic = ref<MsgItem[]>([]);
|
||||||
|
const messageNational = ref<MsgItem[]>([]);
|
||||||
|
const messagePrivate = ref<MsgItem[]>([]);
|
||||||
|
const messageDiplomacy = ref<MsgItem[]>([]);
|
||||||
|
|
||||||
|
const messageIndexedList: Record<MsgType, Ref<MsgItem[]>> = {
|
||||||
|
public: messagePublic,
|
||||||
|
national: messageNational,
|
||||||
|
private: messagePrivate,
|
||||||
|
diplomacy: messageDiplomacy,
|
||||||
|
};
|
||||||
|
|
||||||
|
function generateLatestMsgState(msgType: MsgType) {
|
||||||
|
const storageKey = `state.${serverID}.latestReadMsg.${msgType}`;
|
||||||
|
const latestReadMsgID = parseInt(localStorage.getItem(storageKey) ?? "0");
|
||||||
|
const obj = ref<[string | undefined, number, number]>([undefined, latestReadMsgID, latestReadMsgID]);
|
||||||
|
watch(obj, ([, newMsgID], [, oldMsgID]) => {
|
||||||
|
if (newMsgID == oldMsgID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localStorage.setItem(storageKey, newMsgID.toString());
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestPrivateMsgToastInfo = generateLatestMsgState("private");
|
||||||
|
const latestDiplomacyMsgToastInfo = generateLatestMsgState("diplomacy");
|
||||||
|
|
||||||
|
function readLatestMsg(msgType: MsgType) {
|
||||||
|
const targetMap = {
|
||||||
|
private: latestPrivateMsgToastInfo,
|
||||||
|
diplomacy: latestDiplomacyMsgToastInfo,
|
||||||
|
public: undefined,
|
||||||
|
national: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const target = targetMap[msgType];
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [toastID, , lastestReceivedID] = target.value;
|
||||||
|
if (toastID) {
|
||||||
|
toasts.remove(toastID);
|
||||||
|
}
|
||||||
|
target.value = [undefined, lastestReceivedID, lastestReceivedID];
|
||||||
|
}
|
||||||
|
|
||||||
|
function _updateLatestMsg(msg: MsgItem) {
|
||||||
|
const msgType = msg.msgType;
|
||||||
|
|
||||||
|
//TODO: 메시지함으로 바로 이동하는 기능이 나중에 필요할 것
|
||||||
|
if (msgType == "private") {
|
||||||
|
const [toastID, latestMsgID] = latestPrivateMsgToastInfo.value;
|
||||||
|
if (msg.id <= latestMsgID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toastID) {
|
||||||
|
toasts.remove(toastID);
|
||||||
|
}
|
||||||
|
const newToastID = toasts.show(
|
||||||
|
{
|
||||||
|
title: "새로운 개인 메시지",
|
||||||
|
body: "새로운 개인 메시지가 도착했습니다.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
delay: 1000 * 60 * 10,
|
||||||
|
variant: 'warning'
|
||||||
|
}
|
||||||
|
).options.id;
|
||||||
|
latestPrivateMsgToastInfo.value = [newToastID, latestMsgID, msg.id];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgType == "diplomacy") {
|
||||||
|
const [toastID, latestMsgID] = latestDiplomacyMsgToastInfo.value;
|
||||||
|
if (msg.id <= latestMsgID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toastID) {
|
||||||
|
toasts.remove(toastID);
|
||||||
|
}
|
||||||
|
const newToastID = toasts.show(
|
||||||
|
{
|
||||||
|
title: "새로운 외교 메시지",
|
||||||
|
body: "새로운 외교 메시지가 도착했습니다.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
delay: 1000 * 60 * 10,
|
||||||
|
variant: 'warning'
|
||||||
|
}
|
||||||
|
).options.id;
|
||||||
|
latestDiplomacyMsgToastInfo.value = [newToastID, latestMsgID, msg.id];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLatestMsg(msg: MsgItem[]) {
|
||||||
|
for (const msgItem of msg) {
|
||||||
|
if (msgItem.src.id == generalID.value) continue;
|
||||||
|
_updateLatestMsg(msgItem);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processMsg(msg: MsgItem) {
|
||||||
|
if (msg.option.delete) {
|
||||||
|
(() => {
|
||||||
|
const targetID = msg.option.delete;
|
||||||
|
const targetMsg = messageStorage.get(targetID);
|
||||||
|
if (!targetMsg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
targetMsg.option.invalid = true;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMsgResponse(response: MsgResponse) {
|
||||||
|
if (!response.keepRecent) {
|
||||||
|
messageStorage.clear();
|
||||||
|
for (const msgList of Object.values(messageIndexedList)) {
|
||||||
|
msgList.value.length = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.generalName != generalName.value) {
|
||||||
|
emit("request-refresh");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.nationID != nationID.value) {
|
||||||
|
emit("request-refresh");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSequence.value = Math.max(lastSequence.value, response.sequence);
|
||||||
|
|
||||||
|
for (const msgType of Object.keys(messageIndexedList) as (keyof typeof messageIndexedList)[]) {
|
||||||
|
const msgList = messageIndexedList[msgType];
|
||||||
|
|
||||||
|
const newMsgList = response[msgType];
|
||||||
|
if (newMsgList.length == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//순서가 어떤 순서인지 모르니, 여기서 내림차순으로 맞춘다.
|
||||||
|
newMsgList.sort((a, b) => b.id - a.id);
|
||||||
|
|
||||||
|
if (msgList.value.length == 0) {
|
||||||
|
msgList.value = newMsgList;
|
||||||
|
updateLatestMsg(newMsgList);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//head test
|
||||||
|
const filteredMsgList: MsgItem[] = [];
|
||||||
|
|
||||||
|
for (const msg of newMsgList) {
|
||||||
|
const oldMsg = messageStorage.get(msg.id);
|
||||||
|
if (oldMsg !== undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
processMsg(msg);
|
||||||
|
messageStorage.set(msg.id, msg);
|
||||||
|
filteredMsgList.push(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filteredMsgList.length == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgHeadID = msgList.value[0].id;
|
||||||
|
const newMsgTailID = filteredMsgList[filteredMsgList.length - 1].id;
|
||||||
|
if (newMsgTailID > msgHeadID) {
|
||||||
|
msgList.value = [...filteredMsgList, ...msgList.value];
|
||||||
|
updateLatestMsg(filteredMsgList);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgTailID = msgList.value[msgList.value.length - 1].id;
|
||||||
|
const newMsgHeadID = filteredMsgList[0].id;
|
||||||
|
|
||||||
|
if (msgTailID > newMsgHeadID) {
|
||||||
|
msgList.value.push(...filteredMsgList);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//중간에 삽입되는 경우는 에러이다.
|
||||||
|
console.error("중간 삽입 있음", msgType, newMsgList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginRefreshTimer() {
|
||||||
|
if (refreshTimer) {
|
||||||
|
clearInterval(refreshTimer);
|
||||||
|
refreshTimer = null;
|
||||||
|
}
|
||||||
|
refreshTimer = window.setInterval(function () {
|
||||||
|
const now = Date.now();
|
||||||
|
if (lastRefreshDone + 5000 < now) {
|
||||||
|
//만약 서버 응답이 없다면?
|
||||||
|
|
||||||
|
if (refreshP.size > refreshLimit && refreshTimer) {
|
||||||
|
clearInterval(refreshTimer);
|
||||||
|
refreshTimer = null;
|
||||||
|
toasts.danger(
|
||||||
|
{
|
||||||
|
title: "메시지 자동 갱신 실패",
|
||||||
|
body: "서버 응답이 없습니다. 새로고침을 해주세요.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
delay: 1000 * 3600,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tryRefresh();
|
||||||
|
}
|
||||||
|
}, 2500);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryRefresh() {
|
||||||
|
if (refreshP.size > 0) {
|
||||||
|
await Promise.race([...refreshP, delay(500)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const waiterP = (async () => {
|
||||||
|
let response: MsgResponse | undefined = undefined;
|
||||||
|
try {
|
||||||
|
response = await SammoAPI.Message.GetRecentMessage({
|
||||||
|
sequence: lastSequence.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (lastRefreshDone < now) {
|
||||||
|
lastRefreshDone = now;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (isString(e)) {
|
||||||
|
toasts.warning({
|
||||||
|
title: "갱신 실패",
|
||||||
|
body: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMsgResponse(response);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
})();
|
||||||
|
void waiterP.then((result) => {
|
||||||
|
if (!result) {
|
||||||
|
console.error("?! result");
|
||||||
|
refreshLimit--;
|
||||||
|
} else {
|
||||||
|
refreshLimit = initRefreshLimit;
|
||||||
|
}
|
||||||
|
refreshP.delete(waiterP);
|
||||||
|
});
|
||||||
|
refreshP.add(waiterP);
|
||||||
|
|
||||||
|
return waiterP;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetMailbox = ref(nationID.value + 9000);
|
||||||
|
type MailboxGroup = {
|
||||||
|
label: string;
|
||||||
|
color?: string;
|
||||||
|
options: MailboxTarget[];
|
||||||
|
};
|
||||||
|
type MailboxTarget = {
|
||||||
|
value: number;
|
||||||
|
text: string;
|
||||||
|
nationID: number;
|
||||||
|
disabled?: true;
|
||||||
|
color?: string;
|
||||||
|
};
|
||||||
|
const mailboxList = ref<MailboxGroup[]>([]);
|
||||||
|
const newMessageText = ref<string>("");
|
||||||
|
|
||||||
|
function refreshMailboxList(obj: MabilboxListResponse) {
|
||||||
|
let myNationColor = "#000000";
|
||||||
|
const diplomacyMailboxList: MailboxGroup = {
|
||||||
|
label: "외교메시지",
|
||||||
|
color: "#000000",
|
||||||
|
options: [],
|
||||||
|
};
|
||||||
|
const nationMailboxList: MailboxGroup[] = [];
|
||||||
|
obj.nation.sort(function (lhs, rhs) {
|
||||||
|
if (lhs.mailbox == nationMailbox) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (rhs.mailbox == nationMailbox) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return lhs.mailbox - rhs.mailbox;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const nation of obj.nation) {
|
||||||
|
if (nationMailbox == nation.mailbox) {
|
||||||
|
myNationColor = nation.color;
|
||||||
|
//nationColor저장하는 코드가 있었음
|
||||||
|
}
|
||||||
|
|
||||||
|
const nationBox: MailboxGroup = {
|
||||||
|
label: nation.name,
|
||||||
|
color: nation.color,
|
||||||
|
options: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
nation.general.sort(function (lhs, rhs) {
|
||||||
|
if (lhs[1] < rhs[1]) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (lhs[1] > rhs[1]) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [destGeneralID, destGeneralName, destGeneralFlag] of nation.general) {
|
||||||
|
const isRuler = !!(destGeneralFlag & 0x1);
|
||||||
|
const isAmbassador = !!(destGeneralFlag & 0x4);
|
||||||
|
|
||||||
|
if (destGeneralID == generalID.value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let textName = destGeneralName;
|
||||||
|
if (isRuler) {
|
||||||
|
textName = `*${textName}*`;
|
||||||
|
} else if (isAmbassador) {
|
||||||
|
textName = `#${textName}#`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target: MailboxTarget = {
|
||||||
|
value: destGeneralID,
|
||||||
|
text: textName,
|
||||||
|
nationID: nation.nationID,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (permissionLevel.value == 4 && isAmbassador && nationMailbox != nation.mailbox) {
|
||||||
|
target.disabled = true;
|
||||||
|
}
|
||||||
|
nationBox.options.push(target);
|
||||||
|
}
|
||||||
|
nationMailboxList.push(nationBox);
|
||||||
|
|
||||||
|
if (permissionLevel.value < 4 || nationMailbox == nation.mailbox) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
diplomacyMailboxList.options.push({
|
||||||
|
value: nation.mailbox,
|
||||||
|
text: nation.name,
|
||||||
|
nationID: nation.nationID,
|
||||||
|
color: nation.color,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const favoriteBox: MailboxGroup = {
|
||||||
|
label: "즐겨찾기",
|
||||||
|
color: "#000000",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: nationMailbox,
|
||||||
|
text: "【 아국 메세지 】",
|
||||||
|
nationID: nationID.value,
|
||||||
|
color: myNationColor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 9999,
|
||||||
|
text: "【 전체 메세지 】",
|
||||||
|
nationID: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
mailboxList.value = [favoriteBox, diplomacyMailboxList, ...nationMailboxList];
|
||||||
|
}
|
||||||
|
|
||||||
|
function foldMessage($event: MouseEvent, type: MsgType) {
|
||||||
|
const target = messageIndexedList[type].value;
|
||||||
|
if (target.length < 10) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const remain = target.slice(10);
|
||||||
|
target.length = 10;
|
||||||
|
|
||||||
|
for (const msg of remain) {
|
||||||
|
messageStorage.delete(msg.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadOldMessage($event: MouseEvent, type: MsgType) {
|
||||||
|
const target = messageIndexedList[type].value;
|
||||||
|
if (target.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const last = target[target.length - 1].id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await SammoAPI.Message.GetOldMessage({
|
||||||
|
to: last,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
updateMsgResponse(response);
|
||||||
|
} catch (e) {
|
||||||
|
if (isString(e)) {
|
||||||
|
toasts.warning({
|
||||||
|
title: "이전 메시지 불러오기 실패",
|
||||||
|
body: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendMessage() {
|
||||||
|
const text = newMessageText.value;
|
||||||
|
if (!text) {
|
||||||
|
return tryRefresh();
|
||||||
|
}
|
||||||
|
const mailbox = targetMailbox.value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const waiter = SammoAPI.Message.SendMessage({
|
||||||
|
mailbox,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
newMessageText.value = "";
|
||||||
|
await waiter;
|
||||||
|
await tryRefresh();
|
||||||
|
} catch (e) {
|
||||||
|
if (isString(e)) {
|
||||||
|
toasts.warning({
|
||||||
|
title: "메시지 전송 실패",
|
||||||
|
body: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryFullRefresh() {
|
||||||
|
try {
|
||||||
|
const refreshP = tryRefresh();
|
||||||
|
const response = await SammoAPI.Message.GetContactList();
|
||||||
|
refreshMailboxList(response);
|
||||||
|
await refreshP;
|
||||||
|
} catch (e) {
|
||||||
|
if (isString(e)) {
|
||||||
|
toasts.warning({
|
||||||
|
title: " 실패했습니다.",
|
||||||
|
body: e,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.error(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
beginRefreshTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
tryRefresh,
|
||||||
|
tryFullRefresh,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await tryFullRefresh();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "@scss/common/break_500px.scss";
|
||||||
|
|
||||||
|
.BoardHeader {
|
||||||
|
color: white;
|
||||||
|
outline-style: solid;
|
||||||
|
outline-width: 1px;
|
||||||
|
outline-color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-1000px {
|
||||||
|
.MessagePanel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MessageInputForm {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PublicTalk {
|
||||||
|
border-right: 1px solid gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PrivateTalk {
|
||||||
|
border-right: 1px solid gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.only-mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MessageList {
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
height: 650px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-500px {
|
||||||
|
#msg_submit-col {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
#msg_input-col {
|
||||||
|
order: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.MessageInputForm {
|
||||||
|
position: sticky;
|
||||||
|
top: 0px;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stickyAnchor {
|
||||||
|
position: relative;
|
||||||
|
top: -68px;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-grid.Actions {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:id="`msg_${msg.id}`"
|
||||||
|
:class="['msg_plate', `msg_plate_${msg.msgType}`, `msg_plate_${nationType}`]"
|
||||||
|
:data-id="msg.id"
|
||||||
|
>
|
||||||
|
<div class="msg_icon">
|
||||||
|
<img v-if="src.icon" class="generalIcon" width="64" height="64" :src="encodeURI(src.icon)" />
|
||||||
|
<img v-else class="generalIcon" width="64" height="64" :src="encodeURI(defaultIcon)" />
|
||||||
|
</div>
|
||||||
|
<div class="msg_body">
|
||||||
|
<div class="msg_header">
|
||||||
|
<template v-if="deletable">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn btn-outline-warning btn-sm btn-delete-msg"
|
||||||
|
style="float: right"
|
||||||
|
@click="tryDelete"
|
||||||
|
>
|
||||||
|
❌
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="msg.msgType == 'private'">
|
||||||
|
<template v-if="src.name == generalName">
|
||||||
|
<span :class="`msg_target msg_${srcColorType}`" :style="{ backgroundColor: src.color }">나</span
|
||||||
|
><span class="msg_from_to">▶</span
|
||||||
|
><span :class="`msg_target msg_${destColorType}`" :style="{ backgroundColor: dest.color }"
|
||||||
|
>{{ dest.name }}:{{ dest.nation }}</span
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span :class="`msg_target msg_${srcColorType}`" :style="{ backgroundColor: src.color }"
|
||||||
|
>{{ src.name }}:{{ src.nation }}</span
|
||||||
|
><span class="msg_from_to">▶</span
|
||||||
|
><span :class="`msg_target msg_${destColorType}`" :style="{ backgroundColor: dest.color }">나</span>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="msg.msgType == 'national' && src.nation_id === dest.nation_id">
|
||||||
|
<span :class="`msg_target msg_${srcColorType}`" :style="{ backgroundColor: src.color }">{{ src.name }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="msg.msgType == 'national' || msg.msgType == 'diplomacy'">
|
||||||
|
<template v-if="src.nation_id == nationID">
|
||||||
|
<span :class="`msg_target msg_${srcColorType}`" :style="{ backgroundColor: src.color }">{{ src.name }}</span
|
||||||
|
><span class="msg_from_to">▶</span
|
||||||
|
><span :class="`msg_target msg_${destColorType}`" :style="{ backgroundColor: dest.color }">{{
|
||||||
|
dest.nation
|
||||||
|
}}</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span :class="`msg_target msg_${srcColorType}`" :style="{ backgroundColor: src.color }"
|
||||||
|
>{{ src.name }}:{{ src.nation }}</span
|
||||||
|
><span class="msg_from_to"></span>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span :class="`msg_target msg_${srcColorType}`" :style="{ backgroundColor: dest.color }"
|
||||||
|
>{{ src.name }}:{{ src.nation }}</span
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
<span class="msg_time"><{{ msg.time }}></span>
|
||||||
|
</div>
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html vue/max-attributes-per-line -->
|
||||||
|
<div :class="['msg_content', isValidMsg ? 'msg_valid' : 'msg_invalid']" v-html="isValidMsg ? linkifyStr(msg.text) : '삭제된 메시지입니다'"
|
||||||
|
></div>
|
||||||
|
<div v-if="msg.option.action" class="msg_prompt">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="prompt_yes btn_prompt"
|
||||||
|
:disabled="allowButton ? undefined : true"
|
||||||
|
@click="tryAccept"
|
||||||
|
>
|
||||||
|
수락
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="prompt_no btn_prompt"
|
||||||
|
:disabled="allowButton ? undefined : true"
|
||||||
|
@click="tryDecline"
|
||||||
|
>
|
||||||
|
거절
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { MsgItem, MsgTarget } from "@/defs/API/Message";
|
||||||
|
import { parseTime } from "@/util/parseTime";
|
||||||
|
import { differenceInMilliseconds, addMinutes } from "date-fns/esm";
|
||||||
|
import { computed, ref, toRef, watch, type ComputedRef, type Ref } from "vue";
|
||||||
|
import linkifyStr from "linkifyjs/string";
|
||||||
|
import { SammoAPI } from "@/SammoAPI";
|
||||||
|
import { isError, isString } from "lodash";
|
||||||
|
import { isBrightColor } from "@/util/isBrightColor";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: MsgItem;
|
||||||
|
generalID: number;
|
||||||
|
generalName: string;
|
||||||
|
nationID: number;
|
||||||
|
permissionLevel: number;
|
||||||
|
deleted?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "request-refresh"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const src: Ref<MsgTarget> = ref(props.modelValue.src);
|
||||||
|
const dest: Ref<MsgTarget> = ref(props.modelValue.dest ?? props.modelValue.src);
|
||||||
|
const srcColorType = computed(() => isBrightColor(src.value.color) ? "bright" : "dark");
|
||||||
|
const destColorType = computed(() => isBrightColor(dest.value.color) ? "bright" : "dark");
|
||||||
|
|
||||||
|
const msg = toRef(props, "modelValue");
|
||||||
|
const defaultIcon = `${window.pathConfig.sharedIcon}/default.jpg`;
|
||||||
|
|
||||||
|
const isValidMsg = ref(true);
|
||||||
|
const deletable = ref(false);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.deleted,
|
||||||
|
() => {
|
||||||
|
isValidMsg.value = testValidMsg(msg.value);
|
||||||
|
deletable.value = testDeletable(msg.value);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
msg,
|
||||||
|
(msg) => {
|
||||||
|
isValidMsg.value = testValidMsg(msg);
|
||||||
|
deletable.value = testDeletable(msg);
|
||||||
|
|
||||||
|
src.value = msg.src;
|
||||||
|
dest.value = msg.dest ?? {
|
||||||
|
id: 0,
|
||||||
|
name: "",
|
||||||
|
nation: "재야",
|
||||||
|
nation_id: 0,
|
||||||
|
color: "#000000",
|
||||||
|
icon: defaultIcon,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const deletableTimer: Ref<number | undefined> = ref();
|
||||||
|
const allowButton = computed(() => {
|
||||||
|
if (msg.value.msgType != "diplomacy") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (props.permissionLevel >= 4) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
function testDeletable(msg: MsgItem): boolean {
|
||||||
|
if (deletableTimer.value) {
|
||||||
|
clearTimeout(deletableTimer.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.deleted) return false;
|
||||||
|
if (msg.option.action) return false;
|
||||||
|
if (msg.src.id != props.generalID) return false;
|
||||||
|
if (msg.option.invalid) return false;
|
||||||
|
if (!msg.option.deletable) return false;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const last5min = addMinutes(parseTime(msg.time), 5);
|
||||||
|
|
||||||
|
const timeDiff = differenceInMilliseconds(last5min, now);
|
||||||
|
|
||||||
|
if (timeDiff <= 0) return false;
|
||||||
|
|
||||||
|
deletableTimer.value = window.setTimeout(() => {
|
||||||
|
deletable.value = testDeletable(msg);
|
||||||
|
}, timeDiff);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nationType: ComputedRef<"local" | "src" | "dest"> = computed(() => {
|
||||||
|
if (msg.value.src.nation_id === msg.value.dest?.nation_id) {
|
||||||
|
return "local";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.value.src.nation_id === props.nationID) {
|
||||||
|
return "src";
|
||||||
|
}
|
||||||
|
return "dest";
|
||||||
|
});
|
||||||
|
|
||||||
|
function testValidMsg(msg: MsgItem): boolean {
|
||||||
|
if (props.deleted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (msg.option.invalid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryDelete() {
|
||||||
|
if (!confirm("삭제하시겠습니까?")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await SammoAPI.Message.DeleteMessage({ msgID: msg.value.id });
|
||||||
|
} catch (e) {
|
||||||
|
if (isString(e)) {
|
||||||
|
alert(e);
|
||||||
|
}
|
||||||
|
if (isError(e)) {
|
||||||
|
alert(e.message);
|
||||||
|
}
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
emit("request-refresh");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryAccept() {
|
||||||
|
if (!confirm("수락하시겠습니까?")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await SammoAPI.Message.DecideMessageResponse({ msgID: msg.value.id, response: true });
|
||||||
|
} catch (e) {
|
||||||
|
if (isString(e)) {
|
||||||
|
alert(e);
|
||||||
|
}
|
||||||
|
if (isError(e)) {
|
||||||
|
alert(e.message);
|
||||||
|
}
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
emit("request-refresh");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryDecline() {
|
||||||
|
if (!confirm("거절하시겠습니까?")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await SammoAPI.Message.DecideMessageResponse({ msgID: msg.value.id, response: false });
|
||||||
|
} catch (e) {
|
||||||
|
if (isString(e)) {
|
||||||
|
alert(e);
|
||||||
|
}
|
||||||
|
if (isError(e)) {
|
||||||
|
alert(e.message);
|
||||||
|
}
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
emit("request-refresh");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.msg_plate {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 64px 1fr;
|
||||||
|
border-bottom: solid 1px gray;
|
||||||
|
min-height: 64px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
word-break: break-all;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg_plate_private {
|
||||||
|
background-color: #5d1e1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg_plate_private.msg_plate_dest {
|
||||||
|
background-color: #5d461a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg_plate_public {
|
||||||
|
background-color: #141c65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg_plate_national,
|
||||||
|
.msg_plate_diplomacy {
|
||||||
|
background-color: #00582c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg_plate_national.msg_plate_dest,
|
||||||
|
.msg_plate_diplomacy.msg_plate_dest {
|
||||||
|
background-color: #704615;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg_plate_national.msg_plate_src,
|
||||||
|
.msg_plate_diplomacy.msg_plate_src {
|
||||||
|
background-color: #70153b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg_icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-right: solid 1px gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg_time {
|
||||||
|
font-size: 0.75em;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg_header {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg_invalid {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg_content {
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg_target {
|
||||||
|
margin: 2px 2px 0 2px;
|
||||||
|
padding: 2px 3px;
|
||||||
|
display: inline-block;
|
||||||
|
box-shadow: 2px 2px black;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.msg_target.msg_bright {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg_target.msg_dark {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg_from_to {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg_prompt {
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
.btn-delete-msg {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
margin: 2px 2px 0 2px;
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
<template>
|
||||||
|
<div class="nation-card-basic bg2">
|
||||||
|
<div
|
||||||
|
class="name tb-title"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: nation.color,
|
||||||
|
color: isBrightColor(nation.color) ? 'black' : 'white',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ nation.name }}
|
||||||
|
</div>
|
||||||
|
<div class="type-head tb-head bg1">성향</div>
|
||||||
|
<div class="type-body tb-body">
|
||||||
|
{{ nation.type.name }} (<span style="color: cyan">{{ nation.type.pros }}</span>
|
||||||
|
<span style="color: magenta">{{ nation.type.cons }}</span
|
||||||
|
>)
|
||||||
|
</div>
|
||||||
|
<div class="c12-head tb-head bg1">{{ formatOfficerLevelText(12, nation.level) }}</div>
|
||||||
|
<div class="c12-body tb-body" :style="{ color: getNPCColor(nation.topChiefs[12]?.npc ?? 1) }">
|
||||||
|
{{ nation.topChiefs[12]?.name ?? "-" }}
|
||||||
|
</div>
|
||||||
|
<div class="c11-head tb-head bg1">{{ formatOfficerLevelText(11, nation.level) }}</div>
|
||||||
|
<div class="c11-body tb-body" :style="{ color: getNPCColor(nation.topChiefs[11]?.npc ?? 1) }">
|
||||||
|
{{ nation.topChiefs[11]?.name ?? "-" }}
|
||||||
|
</div>
|
||||||
|
<div class="pop-head tb-head bg1">총 주민</div>
|
||||||
|
<div v-if="!nation.id" class="pop-body tb-body">해당 없음</div>
|
||||||
|
<div v-else class="pop-body tb-body">
|
||||||
|
{{ nation.population.now.toLocaleString() }} / {{ nation.population.max.toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
<div class="crew-head tb-head bg1">총 병사</div>
|
||||||
|
<div v-if="!nation.id" class="crew-body tb-body">해당 없음</div>
|
||||||
|
<div v-else class="crew-body tb-body">
|
||||||
|
{{ nation.crew.now.toLocaleString() }} / {{ nation.crew.max.toLocaleString() }}
|
||||||
|
</div>
|
||||||
|
<div class="gold-head tb-head bg1">국고</div>
|
||||||
|
<div v-if="!nation.id" class="gold-body tb-body">해당 없음</div>
|
||||||
|
<div v-else class="gold-body tb-body">{{ nation.gold.toLocaleString() }}</div>
|
||||||
|
<div class="rice-head tb-head bg1">병량</div>
|
||||||
|
<div v-if="!nation.id" class="rice-body tb-body">해당 없음</div>
|
||||||
|
<div v-else class="rice-body tb-body">{{ nation.rice.toLocaleString() }}</div>
|
||||||
|
<div class="bill-head tb-head bg1">지급률</div>
|
||||||
|
<div v-if="!nation.id" class="bill-body tb-body">해당 없음</div>
|
||||||
|
<div v-else class="bill-body tb-body">{{ nation.bill }}%</div>
|
||||||
|
<div class="taxRate-head tb-head bg1">세율</div>
|
||||||
|
<div v-if="!nation.id" class="taxRate-body tb-body">해당 없음</div>
|
||||||
|
<div v-else class="taxRate-body tb-body">{{ nation.taxRate }}%</div>
|
||||||
|
<div class="cityCnt-head tb-head bg1">속령</div>
|
||||||
|
<div v-if="!nation.id" class="cityCnt-body tb-body">해당 없음</div>
|
||||||
|
<div v-else class="cityCnt-body tb-body">{{ nation.population.cityCnt.toLocaleString() }}</div>
|
||||||
|
<div class="genCnt-head tb-head bg1">장수</div>
|
||||||
|
<div v-if="!nation.id" class="genCnt-body tb-body">해당 없음</div>
|
||||||
|
<div v-else class="genCnt-body tb-body">{{ nation.crew.generalCnt.toLocaleString() }}</div>
|
||||||
|
<div class="power-head tb-head bg1">국력</div>
|
||||||
|
<div v-if="!nation.id" class="power-body tb-body">해당 없음</div>
|
||||||
|
<div v-else class="power-body tb-body">{{ nation.power.toLocaleString() }}</div>
|
||||||
|
<div class="tech-head tb-head bg1">기술력</div>
|
||||||
|
<div v-if="!nation.id" class="tech-body tb-body">해당 없음</div>
|
||||||
|
<div v-else class="tech-body tb-body">
|
||||||
|
{{ currentTechLevel }}등급 /
|
||||||
|
<span :style="{ color: onTechLimit ? 'magenta' : 'limegreen' }">{{
|
||||||
|
Math.floor(nation.tech).toLocaleString()
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="strategicCmd-head tb-head bg1">전략</div>
|
||||||
|
<div v-if="!nation.id" class="strategicCmd-body tb-body">해당 없음</div>
|
||||||
|
<div
|
||||||
|
v-else-if="impossibleStrategicCommandText"
|
||||||
|
v-b-tooltip.hover
|
||||||
|
class="strategicCmd-body tb-body"
|
||||||
|
:title="impossibleStrategicCommandText"
|
||||||
|
style="text-decoration: underline dashed red"
|
||||||
|
>
|
||||||
|
<span v-if="nation.strategicCmdLimit" style="color: red">{{ nation.strategicCmdLimit.toLocaleString() }}턴</span>
|
||||||
|
<span v-else style="color: yellow">가능</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="strategicCmd-body tb-body">
|
||||||
|
<span v-if="nation.strategicCmdLimit" style="color: red">{{ nation.strategicCmdLimit.toLocaleString() }}턴</span>
|
||||||
|
<span v-else style="color: limegreen">가능</span>
|
||||||
|
</div>
|
||||||
|
<div class="diplomaticCmd-head tb-head bg1">외교</div>
|
||||||
|
<div v-if="!nation.id" class="diplomaticCmd-body tb-body">해당 없음</div>
|
||||||
|
<div v-else class="diplomaticCmd-body tb-body">
|
||||||
|
<span v-if="nation.diplomaticLimit" style="color: red">{{ nation.diplomaticLimit.toLocaleString() }}턴</span>
|
||||||
|
<span v-else style="color: limegreen">가능</span>
|
||||||
|
</div>
|
||||||
|
<div class="prohibitScout-head tb-head bg1">임관</div>
|
||||||
|
<div v-if="!nation.id" class="prohibitScout-body tb-body">해당 없음</div>
|
||||||
|
<div v-else class="prohibitScout-body tb-body">
|
||||||
|
<span v-if="nation.prohibitScout" style="color: red">금지</span>
|
||||||
|
<span v-else style="color: limegreen">허가</span>
|
||||||
|
</div>
|
||||||
|
<div class="prohibitWar-head tb-head bg1">전쟁</div>
|
||||||
|
<div v-if="!nation.id" class="prohibitWar-body tb-body">해당 없음</div>
|
||||||
|
<div v-else class="prohibitWar-body tb-body">
|
||||||
|
<span v-if="nation.prohibitWar" style="color: red">금지</span>
|
||||||
|
<span v-else style="color: limegreen">허가</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { GetFrontInfoResponse } from "@/defs/API/Global";
|
||||||
|
import type { GameConstStore } from "@/GameConstStore";
|
||||||
|
import { joinYearMonth } from "@/util/joinYearMonth";
|
||||||
|
import { parseYearMonth } from "@/util/parseYearMonth";
|
||||||
|
import { unwrap } from "@/util/unwrap";
|
||||||
|
import { isBrightColor } from "@/util/isBrightColor";
|
||||||
|
import { formatOfficerLevelText, getNPCColor, isTechLimited, convTechLevel, getMaxRelativeTechLevel } from "@/utilGame";
|
||||||
|
import { inject, ref, toRef, watch, type Ref } from "vue";
|
||||||
|
const props = defineProps<{
|
||||||
|
nation: GetFrontInfoResponse["nation"];
|
||||||
|
global: GetFrontInfoResponse["global"];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const gameConstStore = unwrap(inject<Ref<GameConstStore>>("gameConstStore"));
|
||||||
|
const nation = toRef(props, "nation");
|
||||||
|
const global = toRef(props, "global");
|
||||||
|
|
||||||
|
const currentTechLevel = ref(0);
|
||||||
|
const maxTechLevel = ref(0);
|
||||||
|
const onTechLimit = ref(false);
|
||||||
|
watch(
|
||||||
|
nation,
|
||||||
|
(nation) => {
|
||||||
|
const { startyear, year } = global.value;
|
||||||
|
console.log(gameConstStore);
|
||||||
|
maxTechLevel.value = getMaxRelativeTechLevel(startyear, year, gameConstStore.value.gameConst.maxTechLevel);
|
||||||
|
currentTechLevel.value = convTechLevel(nation.tech, maxTechLevel.value);
|
||||||
|
onTechLimit.value = isTechLimited(startyear, year, nation.tech, gameConstStore.value.gameConst.maxTechLevel);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const impossibleStrategicCommandText = ref<string>("");
|
||||||
|
watch(
|
||||||
|
nation,
|
||||||
|
(nation) => {
|
||||||
|
if (nation.impossibleStrategicCommand.length == 0) {
|
||||||
|
impossibleStrategicCommandText.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearMonth = joinYearMonth(global.value.year, global.value.month);
|
||||||
|
const texts = [];
|
||||||
|
for (const [cmdName, turnCnt] of nation.impossibleStrategicCommand) {
|
||||||
|
const [year, month] = parseYearMonth(yearMonth + turnCnt);
|
||||||
|
texts.push(`${cmdName}: ${turnCnt.toLocaleString()}턴 뒤(${year}년 ${month}월부터)`);
|
||||||
|
}
|
||||||
|
impossibleStrategicCommandText.value = texts.join("<br>\n");
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.nation-card-basic {
|
||||||
|
width: 500px;
|
||||||
|
height: 193px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 7fr 18fr 7fr 18fr;
|
||||||
|
grid-template-rows: repeat(10, calc(192px / 10));
|
||||||
|
|
||||||
|
border-bottom: solid 1px gray;
|
||||||
|
border-right: solid 1px gray;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
grid-column: 1 / span 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-body {
|
||||||
|
grid-column: 2 / span 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-title {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px;
|
||||||
|
line-height: calc(193px / 10);
|
||||||
|
|
||||||
|
border-left: solid 1px gray;
|
||||||
|
border-top: solid 1px gray;
|
||||||
|
}
|
||||||
|
.tb-head {
|
||||||
|
border-left: solid 1px gray;
|
||||||
|
border-top: solid 1px gray;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
padding: 0px;
|
||||||
|
line-height: calc(193px / 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-body {
|
||||||
|
border-top: solid 1px gray;
|
||||||
|
padding: 0px;
|
||||||
|
line-height: calc(193px / 10);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+232
-78
@@ -1,96 +1,250 @@
|
|||||||
import type { CityID, CrewTypeID, GameCityDefault, GameConstType, GameUnitType, GameIActionKey, GameIActionCategory, GameIActionInfo } from "@/defs/GameObj";
|
import type {
|
||||||
import type { diplomacyState, MapResult, SimpleNationObj } from "@/defs";
|
CityID,
|
||||||
|
CrewTypeID,
|
||||||
|
GameCityDefault,
|
||||||
|
GameConstType,
|
||||||
|
GameUnitType,
|
||||||
|
GameIActionKey,
|
||||||
|
GameIActionCategory,
|
||||||
|
GameIActionInfo,
|
||||||
|
} from "@/defs/GameObj";
|
||||||
|
import type { diplomacyState, MapResult, NationLevel, SimpleNationObj } from "@/defs";
|
||||||
|
import type { GeneralListItemP0, GeneralListItemP1, NationNotice } from "./Nation";
|
||||||
|
import type { VoteInfo } from "./Vote";
|
||||||
|
|
||||||
export interface GetConstResponse {
|
export interface GetConstResponse {
|
||||||
result: true;
|
result: true;
|
||||||
cacheKey: string;
|
cacheKey: string;
|
||||||
data: {
|
data: {
|
||||||
gameConst: GameConstType;
|
gameConst: GameConstType;
|
||||||
gameUnitConst: Record<CrewTypeID, GameUnitType>;
|
gameUnitConst: Record<CrewTypeID, GameUnitType>;
|
||||||
cityConst: Record<CityID, GameCityDefault>;
|
cityConst: Record<CityID, GameCityDefault>;
|
||||||
cityConstMap: {
|
cityConstMap: {
|
||||||
region: Record<number | string, string | number>;
|
region: Record<number | string, string | number>;
|
||||||
level: Record<number | string, string | number>; //defs.CityLevelText
|
level: Record<number | string, string | number>; //defs.CityLevelText
|
||||||
};
|
|
||||||
iActionInfo: Record<
|
|
||||||
GameIActionCategory,
|
|
||||||
Record<
|
|
||||||
GameIActionKey,
|
|
||||||
GameIActionInfo
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
iActionKeyMap: {
|
|
||||||
availableNationType: "nationType";
|
|
||||||
neutralNationType: "nationType";
|
|
||||||
defaultSpecialDomestic: "specialDomestic";
|
|
||||||
availableSpecialDomestic: "specialDomestic";
|
|
||||||
optionalSpecialDomestic: "specialDomestic";
|
|
||||||
defaultSpecialWar: "specialWar";
|
|
||||||
availableSpecialWar: "specialWar";
|
|
||||||
optionalSpecialWar: "specialWar";
|
|
||||||
neutralPersonality: "personality";
|
|
||||||
availablePersonality: "personality";
|
|
||||||
optionalPersonality: "personality";
|
|
||||||
allItems: "item";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
iActionInfo: Record<GameIActionCategory, Record<GameIActionKey, GameIActionInfo>>;
|
||||||
|
iActionKeyMap: {
|
||||||
|
availableNationType: "nationType";
|
||||||
|
neutralNationType: "nationType";
|
||||||
|
defaultSpecialDomestic: "specialDomestic";
|
||||||
|
availableSpecialDomestic: "specialDomestic";
|
||||||
|
optionalSpecialDomestic: "specialDomestic";
|
||||||
|
defaultSpecialWar: "specialWar";
|
||||||
|
availableSpecialWar: "specialWar";
|
||||||
|
optionalSpecialWar: "specialWar";
|
||||||
|
neutralPersonality: "personality";
|
||||||
|
availablePersonality: "personality";
|
||||||
|
optionalPersonality: "personality";
|
||||||
|
allItems: "item";
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HistoryObj = {
|
export type HistoryObj = {
|
||||||
server_id: string,
|
server_id: string;
|
||||||
year: number;
|
year: number;
|
||||||
month: number;
|
month: number;
|
||||||
map: MapResult,
|
map: MapResult;
|
||||||
global_history: string[],
|
global_history: string[];
|
||||||
global_action: string[],
|
global_action: string[];
|
||||||
nations: {
|
nations: {
|
||||||
capital: number,
|
capital: number;
|
||||||
cities: string[],
|
cities: string[];
|
||||||
color: string,
|
color: string;
|
||||||
gennum: number,
|
gennum: number;
|
||||||
level: number,
|
level: number;
|
||||||
name: string,
|
name: string;
|
||||||
nation: number,
|
nation: number;
|
||||||
power: number,
|
power: number;
|
||||||
type: GameIActionKey
|
type: GameIActionKey;
|
||||||
}[],
|
}[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export type GetHistoryResponse = {
|
export type GetHistoryResponse = {
|
||||||
result: true;
|
result: true;
|
||||||
data: HistoryObj & {
|
data: HistoryObj & {
|
||||||
hash: string;
|
hash: string;
|
||||||
//no: number,
|
//no: number,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export type GetCurrentHistoryResponse = {
|
export type GetCurrentHistoryResponse = {
|
||||||
result: true;
|
result: true;
|
||||||
data: HistoryObj;
|
data: HistoryObj;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
export type GetDiplomacyResponse = {
|
export type GetDiplomacyResponse = {
|
||||||
result: true;
|
result: true;
|
||||||
nations: SimpleNationObj[];
|
nations: SimpleNationObj[];
|
||||||
conflict: [number, Record<number, number>][];
|
conflict: [number, Record<number, number>][];
|
||||||
diplomacyList: Record<number, Record<number, diplomacyState>>;
|
diplomacyList: Record<number, Record<number, diplomacyState>>;
|
||||||
myNationID: number;
|
myNationID: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ExecuteResponse = {
|
export type ExecuteResponse = {
|
||||||
result: true;
|
result: true;
|
||||||
updated: boolean;
|
updated: boolean;
|
||||||
locked: boolean;
|
locked: boolean;
|
||||||
lastExecuted: string;
|
lastExecuted: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetRecentRecordResponse = {
|
export type GetRecentRecordResponse = {
|
||||||
result: true;
|
result: true;
|
||||||
history: [number, string][];
|
history: [number, string][];
|
||||||
global: [number, string][];
|
global: [number, string][];
|
||||||
general: [number, string][];
|
general: [number, string][];
|
||||||
flushHistory: 1 | 0;
|
flushHistory: 1 | 0;
|
||||||
flushGlobal: 1 | 0;
|
flushGlobal: 1 | 0;
|
||||||
flushGeneral: 1 | 0;
|
flushGeneral: 1 | 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AutorunUserMode = "develop" | "warp" | "recruit" | "recruit_high" | "train" | "battle" | "chief";
|
||||||
|
|
||||||
|
export type GetFrontInfoResponse = {
|
||||||
|
result: true;
|
||||||
|
recentRecord: Omit<GetRecentRecordResponse, "result">;
|
||||||
|
global: {
|
||||||
|
scenarioText: string;
|
||||||
|
extendedGeneral: 1 | 0;
|
||||||
|
isFiction: 1 | 0;
|
||||||
|
npcMode: 2 | 1 | 0;
|
||||||
|
joinMode: "onlyRandom" | "full";
|
||||||
|
startyear: number;
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
autorunUser: {
|
||||||
|
limit_minutes: number,
|
||||||
|
options: Record<AutorunUserMode, number>,
|
||||||
|
};
|
||||||
|
turnterm: number;
|
||||||
|
lastExecuted: string;
|
||||||
|
lastVoteID: number;
|
||||||
|
develCost: number;
|
||||||
|
noticeMsg: number;
|
||||||
|
onlineNations: string; //TODO: string[]으로 변경
|
||||||
|
onlineUserCnt: number;
|
||||||
|
apiLimit: number;
|
||||||
|
auctionCount: number;
|
||||||
|
isTournamentActive: boolean;
|
||||||
|
isTournamentApplicationOpen: boolean;
|
||||||
|
isBettingActive: boolean;
|
||||||
|
isLocked: boolean;
|
||||||
|
tournamentType: null | 0 | 1 | 2 | 3;
|
||||||
|
tournamentState: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
|
||||||
|
tournamentTime: string;
|
||||||
|
genCount: [number, number][];
|
||||||
|
generalCntLimit: number;
|
||||||
|
serverCnt: number;
|
||||||
|
lastVote: VoteInfo | null;
|
||||||
|
};
|
||||||
|
general: GeneralListItemP1 & {
|
||||||
|
permission: number;
|
||||||
|
troopInfo?: {
|
||||||
|
leader: {
|
||||||
|
city: number;
|
||||||
|
reservedCommand: GeneralListItemP1["reservedCommand"];
|
||||||
|
};
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
nation: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
population: {
|
||||||
|
cityCnt: number;
|
||||||
|
now: number;
|
||||||
|
max: number;
|
||||||
|
};
|
||||||
|
crew: {
|
||||||
|
generalCnt: number;
|
||||||
|
now: number;
|
||||||
|
max: number;
|
||||||
|
};
|
||||||
|
type: {
|
||||||
|
raw: GameIActionKey;
|
||||||
|
name: string;
|
||||||
|
pros: string;
|
||||||
|
cons: string;
|
||||||
|
};
|
||||||
|
color: string;
|
||||||
|
level: NationLevel
|
||||||
|
capital: number;
|
||||||
|
gold: number;
|
||||||
|
rice: number;
|
||||||
|
tech: number;
|
||||||
|
gennum: number;
|
||||||
|
power: number;
|
||||||
|
bill: number;
|
||||||
|
taxRate: number;
|
||||||
|
onlineGen: string;
|
||||||
|
notice: NationNotice;
|
||||||
|
topChiefs: Record<
|
||||||
|
11 | 12,
|
||||||
|
{
|
||||||
|
officer_level: number;
|
||||||
|
no: number;
|
||||||
|
name: string;
|
||||||
|
npc: GeneralListItemP0["npc"];
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
diplomaticLimit: number;
|
||||||
|
strategicCmdLimit: number;
|
||||||
|
impossibleStrategicCommand: [string, number][];
|
||||||
|
prohibitScout: 1 | 0;
|
||||||
|
prohibitWar: 1 | 0;
|
||||||
|
};
|
||||||
|
city: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
nationInfo: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
level: number;
|
||||||
|
trust: number;
|
||||||
|
pop: [number, number];
|
||||||
|
agri: [number, number];
|
||||||
|
comm: [number, number];
|
||||||
|
secu: [number, number];
|
||||||
|
def: [number, number];
|
||||||
|
wall: [number, number];
|
||||||
|
trade: null | number;
|
||||||
|
officerList: Record<
|
||||||
|
2 | 3 | 4,
|
||||||
|
{
|
||||||
|
officer_level: 2 | 3 | 4;
|
||||||
|
name: string;
|
||||||
|
npc: GeneralListItemP0["npc"];
|
||||||
|
} | null
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MenuItem = {
|
||||||
|
type: 'item';
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
icon?: string;
|
||||||
|
newTab?: boolean;
|
||||||
|
condHightlightVar?: string;
|
||||||
|
condShowVar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MenuSplit = {
|
||||||
|
type: 'split';
|
||||||
|
main: MenuItem;
|
||||||
|
subMenu: MenuItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MenuMulti = {
|
||||||
|
type: 'multi';
|
||||||
|
name: string;
|
||||||
|
subMenu: MenuItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetMenuResponse = {
|
||||||
|
result: true;
|
||||||
|
menu: (MenuItem | MenuSplit | MenuMulti)[];
|
||||||
}
|
}
|
||||||
@@ -150,12 +150,23 @@ export type LiteNationInfoResponse = ValidResponse & {
|
|||||||
nation: NationStaticItem;
|
nation: NationStaticItem;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type NationInfoFull = ValidResponse & {
|
||||||
|
nation: NationItem;
|
||||||
|
isFull: true,
|
||||||
|
impossibleStrategicCommandLists: [string, number][];
|
||||||
|
troops: Record<number, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NationNotice = {
|
||||||
|
date: string,
|
||||||
|
msg: string,
|
||||||
|
author: string,
|
||||||
|
authorID: number,
|
||||||
|
}
|
||||||
|
|
||||||
export type NationInfoResponse =
|
export type NationInfoResponse =
|
||||||
| (ValidResponse & {
|
| (ValidResponse & {
|
||||||
nation: NationStaticItem;
|
nation: NationStaticItem;
|
||||||
|
isFull?: false;
|
||||||
})
|
})
|
||||||
| (ValidResponse & {
|
| NationInfoFull;
|
||||||
nation: NationItem;
|
|
||||||
impossibleStrategicCommandLists: [string, number][];
|
|
||||||
troops: Record<number, string>;
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export type Entries<T> = {
|
||||||
|
[K in keyof T]: [K, T[K]];
|
||||||
|
}[keyof T][];
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
const typeMap = ["전력전", "통솔전", "일기토", "설전"];
|
||||||
|
|
||||||
|
export function formatTournamentType(type: null | undefined | 0 | 1 | 2 | 3): string {
|
||||||
|
if (type === null || type === undefined) {
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
return typeMap[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TournamentStepType = {
|
||||||
|
availableJoin: boolean;
|
||||||
|
state: string;
|
||||||
|
nextText: string;
|
||||||
|
};
|
||||||
|
const stepMap: TournamentStepType[] = [
|
||||||
|
{ availableJoin: false, state: "경기 없음", nextText:"" },
|
||||||
|
{ availableJoin: true, state: "참가 모집중", nextText:"개막시간" },
|
||||||
|
{ availableJoin: false, state: "예선 진행중", nextText:"다음경기" },
|
||||||
|
{ availableJoin: false, state: "본선 추첨중", nextText:"다음추첨" },
|
||||||
|
{ availableJoin: false, state: "본선 진행중", nextText:"다음경기" },
|
||||||
|
{ availableJoin: false, state: "16강 배정중", nextText:"16강배정" },
|
||||||
|
{ availableJoin: true, state: "베팅 진행중", nextText:"베팅마감" },
|
||||||
|
{ availableJoin: false, state: "16강 진행중", nextText:"다음경기" },
|
||||||
|
{ availableJoin: false, state: "8강 진행중", nextText:"다음경기" },
|
||||||
|
{ availableJoin: false, state: "4강 진행중", nextText:"다음경기" },
|
||||||
|
{ availableJoin: false, state: "결승 진행중", nextText:"다음경기" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function formatTournamentStep(step: null | undefined | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10): TournamentStepType {
|
||||||
|
if (step === null || step === undefined) {
|
||||||
|
return stepMap[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return stepMap[step];
|
||||||
|
}
|
||||||
@@ -13,4 +13,5 @@ export { getNPCColor } from "./getNPCColor";
|
|||||||
export { isValidObjKey } from "./isValidObjKey";
|
export { isValidObjKey } from "./isValidObjKey";
|
||||||
export { nextExpLevelRemain } from "./nextExpLevelRemain";
|
export { nextExpLevelRemain } from "./nextExpLevelRemain";
|
||||||
export { postFilterNationCommandGen } from "./postFilterNationCommandGen";
|
export { postFilterNationCommandGen } from "./postFilterNationCommandGen";
|
||||||
export { isTechLimited, convTechLevel, getMaxRelativeTechLevel } from "./techLevel";
|
export { isTechLimited, convTechLevel, getMaxRelativeTechLevel } from "./techLevel";
|
||||||
|
export { calcTournamentTerm } from "./tournament";
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { clamp } from 'lodash-es';
|
||||||
|
export function calcTournamentTerm(turnTerm: number): number{
|
||||||
|
return clamp(turnTerm, 5, 120);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { BootstrapVue3, BToastPlugin } from 'bootstrap-vue-3'
|
||||||
|
import { auto500px } from './util/auto500px';
|
||||||
|
import { htmlReady } from './util/htmlReady';
|
||||||
|
import { insertCustomCSS } from './util/customCSS';
|
||||||
|
import PageFront from './PageFront.vue';
|
||||||
|
import Multiselect from 'vue-multiselect';
|
||||||
|
auto500px();
|
||||||
|
|
||||||
|
htmlReady(() => {
|
||||||
|
insertCustomCSS();
|
||||||
|
});
|
||||||
|
createApp(PageFront).use(BootstrapVue3).use(BToastPlugin).component('v-multiselect', Multiselect).mount('#app');
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace sammo;
|
||||||
|
|
||||||
|
include "lib.php";
|
||||||
|
include "func.php";
|
||||||
|
DummySession::getInstance();
|
||||||
|
|
||||||
|
$mapName = GameConst::$mapName;
|
||||||
|
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=500" />
|
||||||
|
<title><?= UniqueConst::$serverName ?>: 메인</title>
|
||||||
|
<?= WebUtil::printStaticValues([
|
||||||
|
'staticValues' => [
|
||||||
|
'serverName' => UniqueConst::$serverName,
|
||||||
|
'serverNick' => DB::prefix(),
|
||||||
|
'serverID' => UniqueConst::$serverID,
|
||||||
|
'mapName' => GameConst::$mapName,
|
||||||
|
'unitSet' => GameConst::$unitSet,
|
||||||
|
|
||||||
|
'maxTurn' => GameConst::$maxTurn,
|
||||||
|
'maxPushTurn' => 12,
|
||||||
|
'serverNow' => TimeUtil::now(false),
|
||||||
|
]
|
||||||
|
], false) ?>
|
||||||
|
<?= WebUtil::printJS('../d_shared/common_path.js') ?>
|
||||||
|
<?= WebUtil::printJS("js/map/theme_{$mapName}.js") ?>
|
||||||
|
<?= WebUtil::printCSS('../d_shared/common.css') ?>
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css" />
|
||||||
|
<?= WebUtil::printDist('vue', 'v_front', true) ?>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user