Files
core/hwe/ts/PageFront.vue
T

645 lines
19 KiB
Vue

<template>
<div id="outBlock">
<div id="outBlock2">
<nav class="navbar navbar-expand navbar-dark bg-dark d-sm-block p-0">
<div class="container-fluid px-0">
<div class="collapse navbar-collapse">
<ul class="navbar-nav me-auto mx-auto">
<li class="nav-item">테스트</li>
<li class="nav-item">테스트</li>
<li class="nav-item">테스트</li>
</ul>
</div>
</div>
</nav>
<!-- 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>
<div class="scenarioName">시나리오</div>
<div v-if="frontInfo" class="gameInfo">
<div class="subScenarioName">{{ globalInfo.scenarioText }}</div>
<div class="subNPCType">
NPC , 상성: {{ globalInfo.extendedGeneral ? "확장" : "표준" }}
{{ globalInfo.isFiction ? "가상" : "사실" }}
</div>
<div class="subNPCMode">NPC선택: {{ ["불가능", "가능", "선택 생성"][globalInfo.npcMode] }}</div>
<div class="subTournamentMode">토너먼트: 경기당 {{ calcTournamentTerm(globalInfo.turnterm) }}</div>
<div class="subOtherSetting">
기타 설정:
<AutorunInfo :autorunMode="globalInfo.autorunUser" />
</div>
<div class="subYearMonth">
현재: {{ globalInfo.year }} {{ globalInfo.month }} ({{ globalInfo.turnterm }} 서버)
</div>
<div class="subOnlineUserCnt">전체 접속자 : {{ globalInfo.onlineUserCnt.toLocaleString() }}</div>
<div class="subAPILimit">턴당 갱신횟수: {{ globalInfo.apiLimit.toLocaleString() }}</div>
<div class="subGeneralCnt">
등록 장수: 유저
{{ globalInfo.genCount.toLocaleString() }} / {{ globalInfo.generalCntLimit.toLocaleString() }}
+ NPC TODO:NPC Cnt
</div>
<div class="subTournamentState">TODO: tournament Text</div>
<div class="subLastExecuted" :style="{ color: serverLocked ? 'magenta' : 'cyan' }">
동작 시각: {{ formatTime(lastExecuted).substring(5) }}
</div>
<div v-if="globalInfo.auctionCount" class="subAuctionState" style="color: cyan">
{{ globalInfo.auctionCount.toLocaleString() }} 거래 진행중
</div>
<div v-else class="subAcutionState" style="color: magenta">진행중인 거래 없음</div>
<div v-if="globalInfo.lastVote" class="subVoteState">
<span style="color: cyan">설문 진행 : </span><span>{{ globalInfo.lastVote.title }}</span>
</div>
<div v-else class="subVoteState" style="color: magenta">진행중인 설문 없음</div>
</div>
<div class="onlineNations">접속중인 국가: {{ globalInfo.onlineNations }}</div>
<div class="onlineUsers">접속자</div>
<div class="nationNotice">국가 방침
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-if="nationInfo" class="nationNoticeBody" v-html="nationInfo.nationNotice.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="scrollHardTo('reservedCommandPanel')">
명령으로
</button>
</div>
<div class="col-5 d-grid">
<button type="button" class="btn btn-sammo-base2 refreshPage"> </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="PublicRecord">
<div class="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">
<div class="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">
<div class="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 class="commonToolbar">툴바?</div>
<MessagePanel
v-if="generalInfo"
:generalID="generalInfo.no"
:generalName="generalInfo.name"
:nationID="generalInfo.nation"
:permissionLevel="generalInfo.permission"
ref="msgPanel"
/>
<div class="commonToolbar">
<GlobalMenu
v-if="globalMenu && globalInfo"
:globalInfo="globalInfo"
:modelValue="globalMenu"
variant="sammo-base2"
/>
</div>
</main>
</BContainer>
<div v-else>서버 갱신 중입니다.</div>
</div>
<nav id="mobileBottomBar">
<BButton>하단 </BButton>
<BButton>하단 바2</BButton>
</nav>
</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, type InvalidResponse } from "./SammoAPI";
import { parseTime } from "./util/parseTime";
import { unwrap } from "./util/unwrap";
import Denque from "denque";
import { formatLog } from "@/utilGame/formatLog";
import type { ExecuteResponse, 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 { NationInfoFull } from "./defs/API/Nation";
import type { MapResult, NationStaticItem } from "./defs";
import { formatTime } from "./util/formatTime";
import { calcTournamentTerm } from "./utilGame";
import MapViewer, { type CityPositionMap, type MapCityParsed, type MapCityParsedRaw } from "./components/MapViewer.vue";
import PartialReservedCommand from "./PartialReservedCommand.vue";
import AutorunInfo from "./components/AutorunInfo.vue";
import { scrollHardTo } from "./util/scrollHardTo";
import MainControlBar from "./components/MainControlBar.vue";
import GlobalMenu from "./components/GlobalMenu.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("../");
}
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 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 recordLock = false;
watch(refreshCounter, async () => {
if (recordLock) {
return;
}
try {
recordLock = true;
const response = await SammoAPI.Global.GetRecentRecord({
lastGeneralRecordID: lastGeneralRecordID.value,
lastWorldHistoryID: lastWorldHistoryID.value,
});
recordLock = false;
if (response.flushGeneral) {
generalRecords.value = new Denque<[number, string]>();
}
if (response.flushGlobal) {
globalRecords.value = new Denque<[number, string]>();
}
if (response.flushHistory) {
worldHistory.value = new Denque<[number, string]>();
}
if (response.general.length) {
lastGeneralRecordID.value = Math.max(lastGeneralRecordID.value, response.general[0][0]);
}
if (response.global.length) {
lastGeneralRecordID.value = Math.max(lastGeneralRecordID.value, response.global[0][0]);
}
if (response.history.length) {
lastWorldHistoryID.value = Math.max(lastWorldHistoryID.value, response.history[0][0]);
}
while (response.general.length) {
const [id, record] = unwrap(response.general.pop());
if (!generalRecords.value.isEmpty() && id <= unwrap(generalRecords.value.get(0))[0]) {
continue;
}
if (generalRecords.value.length >= 15) {
generalRecords.value.pop();
}
generalRecords.value.unshift([id, record]);
}
while (response.global.length) {
const [id, record] = unwrap(response.global.pop());
if (!globalRecords.value.isEmpty() && id <= unwrap(globalRecords.value.get(0))[0]) {
continue;
}
if (globalRecords.value.length >= 15) {
globalRecords.value.pop();
}
globalRecords.value.unshift([id, record]);
}
while (response.history.length) {
const [id, record] = unwrap(response.history.pop());
if (!worldHistory.value.isEmpty() && id <= unwrap(worldHistory.value.get(0))[0]) {
continue;
}
if (worldHistory.value.length >= 15) {
worldHistory.value.pop();
}
worldHistory.value.unshift([id, record]);
}
} catch (e) {
recordLock = false;
console.error(e);
toasts.danger({
title: "최근 기록 갱신 실패",
body: `${e}`,
});
}
});
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<NationInfoFull>();
let generalInfoLock = false;
watch(refreshCounter, async () => {
if (generalInfoLock) {
return;
}
try {
generalInfoLock = true;
const response = await SammoAPI.General.GetFrontInfo();
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;
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,
};
} catch (e) {
generalInfoLock = false;
console.error(e);
toasts.danger({
title: "최근 정보 갱신 실패",
body: `${e}`,
});
}
});
watch(refreshCounter, async () => {
try {
const response = await SammoAPI.Nation.GetNationInfo({full: true});
console.log('nation', response);
if(!response.isFull){
throw 'NationInfo 반환값 에러';
}
nationInfo.value = response;
} catch (e) {
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";
#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;
> main > div {
min-height: 100px;
/* 테스트 */
}
}
#ingameBoard {
grid-template-columns: 1fr;
}
#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>