파일 이식(0.31.1)
This commit is contained in:
+64
@@ -14,6 +14,9 @@ dist-ssr
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
@@ -22,3 +25,64 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
|
||||
# 체섭 ignore
|
||||
*.log
|
||||
|
||||
logs/*.txt
|
||||
d_log/*.txt
|
||||
d_log/*.zip
|
||||
sess_*
|
||||
*/logs/*.txt
|
||||
*/logs/*/*.txt
|
||||
*/logs/preserved
|
||||
err.txt
|
||||
|
||||
hwe/.htaccess
|
||||
|
||||
d_shared
|
||||
|
||||
/d_pic
|
||||
d_pic/*.jpg
|
||||
d_pic/*.gif
|
||||
d_pic/*.png
|
||||
d_pic/*.webp
|
||||
d_pic/uploaded_image
|
||||
|
||||
/dist_js
|
||||
*/dist_misc/*
|
||||
*/dist_js/*
|
||||
*/dist_css/*
|
||||
/dist_misc/*
|
||||
/dist_js/*
|
||||
/dist_css/*
|
||||
|
||||
d_setting/*.php
|
||||
*/d_setting/*.php
|
||||
*/d_setting/*.json
|
||||
*/d_setting/templates/*.php
|
||||
!*.orig.php
|
||||
|
||||
**/data/file_cache
|
||||
|
||||
**/old/*
|
||||
test.*
|
||||
|
||||
/che
|
||||
/kwe
|
||||
/pwe
|
||||
/nya
|
||||
/pya
|
||||
/twe
|
||||
|
||||
/hwe/data
|
||||
|
||||
/vendor
|
||||
phpinfo.php
|
||||
*.sqlite3
|
||||
|
||||
ingame/js/*
|
||||
ingame/css/*
|
||||
gateway/js/*
|
||||
gateway/css/*
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Deny from all
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {}
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
declare module 'vue-multiselect' {
|
||||
// Type definitions for Vue-Multislect 2.1.0
|
||||
// Definitions by: Akshay Jat https://github.com/akki-jat
|
||||
|
||||
import { VueConstructor } from 'vue';
|
||||
|
||||
|
||||
declare class Multiselect extends VueConstructor { }
|
||||
declare class multiselectMixin extends VueConstructor { }
|
||||
declare class pointerMixin extends VueConstructor { }
|
||||
|
||||
export default Multiselect;
|
||||
export { Multiselect, multiselectMixin, pointerMixin };
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div
|
||||
v-for="(officer, _idx) in [chiefList[chiefLevel]]"
|
||||
:key="_idx"
|
||||
:class="[`chiefBox${chiefLevel}`, 'subRows']"
|
||||
:style="style"
|
||||
@click="$emit('click', this)"
|
||||
>
|
||||
<div
|
||||
class="bg1 nameHeader"
|
||||
:style="{
|
||||
color: getNpcColor(officer?.npcType ?? 0),
|
||||
textDecoration: isMe ? 'underline' : undefined,
|
||||
}"
|
||||
>
|
||||
{{ officer ? officer?.name ?? "-" : "" }}
|
||||
</div>
|
||||
<div class="bg1 center row gx-0">
|
||||
<div class="col">
|
||||
{{ officer?.officerLevelText }}
|
||||
</div>
|
||||
<div class="col">
|
||||
{{ officer ? (officer?.turnTime ?? " - ").slice(-5) : "" }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(turn, idx) in officer?.turn ?? []"
|
||||
:key="idx"
|
||||
class="tableCell align-self-center turn_pad"
|
||||
:style="{
|
||||
fontSize: mb_strwidth(turn.brief) > 28 ? `${28 / mb_strwidth(turn.brief)}em` : undefined,
|
||||
}"
|
||||
>
|
||||
{{ turn.brief }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { getNpcColor } from "@/common_legacy";
|
||||
import { mb_strwidth } from "@/util/mb_strwidth";
|
||||
import { defineComponent } from "vue";
|
||||
import VueTypes from "vue-types";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
chiefLevel: VueTypes.integer.isRequired,
|
||||
style: VueTypes.object.isRequired,
|
||||
chiefList: VueTypes.object.isRequired,
|
||||
isMe: VueTypes.bool.isRequired,
|
||||
},
|
||||
emits: ["click"],
|
||||
methods: {
|
||||
mb_strwidth,
|
||||
getNpcColor,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<div style="position: relative">
|
||||
<DragSelect
|
||||
v-slot="{ selected }"
|
||||
:class="['subRows', 'chiefCommand']"
|
||||
:style="style"
|
||||
:disabled="!props.officer || !isEditMode"
|
||||
attribute="turnIdx"
|
||||
@dragStart="dragStart()"
|
||||
@dragDone="dragDone(...$event)"
|
||||
>
|
||||
<div class="bg1 center row gx-0" style="font-size: 1.2em">
|
||||
<div class="col-5 align-self-center text-end">
|
||||
{{ officer ? `${officer.officerLevelText} : ` : "" }}
|
||||
</div>
|
||||
<div
|
||||
class="col-7 align-self-center"
|
||||
:style="{
|
||||
color: getNpcColor(officer?.npcType ?? 0),
|
||||
}"
|
||||
>
|
||||
{{ officer?.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="vidx in maxTurn" :key="vidx" :turnIdx="vidx" class="row c-bg2 gx-0">
|
||||
<div
|
||||
:class="[
|
||||
'col-2',
|
||||
'time_pad',
|
||||
'f_tnum',
|
||||
(isDragToggle || isCopyButtonShown) && selected.has(vidx.toString()) ? 'inverted' : undefined,
|
||||
]"
|
||||
>
|
||||
{{ turnTimes[vidx - 1] }}
|
||||
</div>
|
||||
<div v-if="!officer || !officer.turn || !(vidx - 1 in officer.turn)" class="center" />
|
||||
<div
|
||||
v-else
|
||||
class="tableCell align-self-center col-10 center turn_pad"
|
||||
:style="{
|
||||
fontSize:
|
||||
mb_strwidth(officer.turn[vidx - 1].brief) > 28
|
||||
? `${28 / mb_strwidth(officer.turn[vidx - 1].brief)}em`
|
||||
: undefined,
|
||||
}"
|
||||
>
|
||||
{{ officer.turn[vidx - 1].brief }}
|
||||
</div>
|
||||
</div>
|
||||
</DragSelect>
|
||||
<BButton
|
||||
ref="btnCopy"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
display: isCopyButtonShown ? 'block' : 'none',
|
||||
top: `${btnPos * 30 + 25}px`,
|
||||
right: '10px',
|
||||
}"
|
||||
@blur="isCopyButtonShown = false"
|
||||
@click="tryCopy()"
|
||||
>
|
||||
복사하기
|
||||
</BButton>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { getNpcColor } from "@/common_legacy";
|
||||
import { formatTime } from "@/util/formatTime";
|
||||
import { mb_strwidth } from "@/util/mb_strwidth";
|
||||
import { parseTime } from "@/util/parseTime";
|
||||
import type { StoredActionsHelper } from "@/util/StoredActionsHelper";
|
||||
import addMinutes from "date-fns/esm/addMinutes/index";
|
||||
import { range } from "lodash";
|
||||
import { inject, onMounted, ref, type PropType } from "vue";
|
||||
import VueTypes from "vue-types";
|
||||
import DragSelect from "@/components/DragSelect.vue";
|
||||
import { BButton } from "bootstrap-vue-3";
|
||||
import { QueryActionHelper } from "@/util/QueryActionHelper";
|
||||
import type { ChiefResponse } from "@/defs/API/NationCommand";
|
||||
|
||||
const props = defineProps({
|
||||
style: VueTypes.object.isRequired,
|
||||
officer: {
|
||||
type: Object as PropType<ChiefResponse["chiefList"][0]>,
|
||||
default: undefined,
|
||||
},
|
||||
turnTerm: VueTypes.integer.isRequired,
|
||||
maxTurn: VueTypes.integer.isRequired,
|
||||
});
|
||||
|
||||
const btnPos = ref(0);
|
||||
const btnCopy = ref<InstanceType<typeof BButton> | null>(null);
|
||||
|
||||
const storedActionsHelper = inject<StoredActionsHelper>("storedNationActionsHelper");
|
||||
const isEditMode = storedActionsHelper?.isEditMode ?? ref(false);
|
||||
const isDragToggle = ref(false);
|
||||
|
||||
const isCopyButtonShown = ref(false);
|
||||
|
||||
const queryActionHelper = new QueryActionHelper(props.maxTurn);
|
||||
const selectedTurnList = queryActionHelper.selectedTurnList;
|
||||
|
||||
onMounted(() => {
|
||||
if (props.officer === undefined) {
|
||||
return;
|
||||
}
|
||||
queryActionHelper.reservedCommandList.value = props.officer.turn.map((rawTurn) => {
|
||||
return {
|
||||
...rawTurn,
|
||||
time: "",
|
||||
};
|
||||
});
|
||||
console.log(queryActionHelper.reservedCommandList.value);
|
||||
});
|
||||
|
||||
function dragStart() {
|
||||
isDragToggle.value = true;
|
||||
}
|
||||
|
||||
function eqSet<T>(as: Set<T>, bs: Set<T>) {
|
||||
if (as.size !== bs.size) return false;
|
||||
for (var a of as) if (!bs.has(a)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function dragDone(...rawSelectedTurn: string[]) {
|
||||
if (rawSelectedTurn.length === 0) {
|
||||
return;
|
||||
}
|
||||
const newSelectedTurnList = new Set<number>();
|
||||
|
||||
let maxPos = -1;
|
||||
for (const rawIdx of rawSelectedTurn) {
|
||||
const idx = parseInt(rawIdx) - 1;
|
||||
newSelectedTurnList.add(idx);
|
||||
maxPos = Math.max(maxPos, idx);
|
||||
}
|
||||
|
||||
btnPos.value = maxPos;
|
||||
|
||||
isDragToggle.value = false;
|
||||
if (newSelectedTurnList.size == 1 && eqSet(selectedTurnList.value, newSelectedTurnList)) {
|
||||
isCopyButtonShown.value = false;
|
||||
return;
|
||||
}
|
||||
selectedTurnList.value = newSelectedTurnList;
|
||||
isCopyButtonShown.value = true;
|
||||
setTimeout(() => {
|
||||
(btnCopy.value?.$el as HTMLButtonElement).focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function tryCopy() {
|
||||
const actions = queryActionHelper.extractQueryActions();
|
||||
isCopyButtonShown.value = false;
|
||||
|
||||
if (!storedActionsHelper) {
|
||||
return;
|
||||
}
|
||||
storedActionsHelper.clipboard.value = [...actions];
|
||||
}
|
||||
|
||||
const turnTimes = ref<string[]>([]);
|
||||
|
||||
if (!props.officer || !props.officer.turnTime) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const _ of range(props.maxTurn)) {
|
||||
turnTimes.value.push("\xa0");
|
||||
}
|
||||
} else {
|
||||
const baseTurnTime = parseTime(props.officer.turnTime);
|
||||
for (const idx of range(props.officer.turn.length)) {
|
||||
turnTimes.value.push(
|
||||
formatTime(addMinutes(baseTurnTime, idx * props.turnTerm), props.turnTerm >= 5 ? "HH:mm" : "mm:ss")
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,49 @@
|
||||
import { SammoAPI } from './SammoAPI';
|
||||
import type { GetConstResponse } from './defs/API/Global';
|
||||
import type { CityID, CrewTypeID, GameCityDefault, GameConstType, GameUnitType, GameIActionCategory, GameIActionKey, GameIActionInfo } from './defs/GameObj';
|
||||
|
||||
export class GameConstStore {
|
||||
public readonly gameConst: GameConstType;
|
||||
public readonly gameUnitConst: Record<CrewTypeID, GameUnitType>;
|
||||
public readonly cityConst: Record<CityID, GameCityDefault>;
|
||||
public readonly cityConstMap: {
|
||||
region: Record<number | string, string | number>;
|
||||
level: Record<number | string, string | number>; //defs.CityLevelText
|
||||
};
|
||||
public readonly iActionInfo: Record<
|
||||
GameIActionCategory,
|
||||
Record<
|
||||
GameIActionKey,
|
||||
GameIActionInfo
|
||||
>
|
||||
>;
|
||||
public readonly iActionKeyMap: Record<string, GameIActionCategory>;
|
||||
|
||||
constructor(response: GetConstResponse) {
|
||||
const data = response.data;
|
||||
this.gameConst = Object.freeze(data.gameConst);
|
||||
this.gameUnitConst = Object.freeze(data.gameUnitConst);
|
||||
this.cityConst = Object.freeze(data.cityConst);
|
||||
this.cityConstMap = Object.freeze(data.cityConstMap);
|
||||
this.iActionInfo = Object.freeze(data.iActionInfo);
|
||||
this.iActionKeyMap = Object.freeze(data.iActionKeyMap);
|
||||
}
|
||||
}
|
||||
|
||||
let gameConstStore: GameConstStore | undefined = undefined;
|
||||
|
||||
export async function getGameConstStore(): Promise<GameConstStore> {
|
||||
//TODO: LocalStorage Cache 조합도 생각해보기.
|
||||
if (gameConstStore !== undefined) {
|
||||
return gameConstStore;
|
||||
}
|
||||
try {
|
||||
const result = await SammoAPI.Global.GetConst();
|
||||
gameConstStore = new GameConstStore(result);
|
||||
}
|
||||
catch (e: unknown) {
|
||||
console.error(`FATAL!: GameConst를 가져오지 못함: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
return gameConstStore;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<BContainer id="container" :toast="{ root: true }" class="bg0">
|
||||
<TopBackBar type="close" :title="isResAuction ? '경매장' : '유니크 경매장'" :reloadable="true" @reload="tryReload">
|
||||
<BButton @click="isResAuction = true">금/쌀</BButton>
|
||||
<BButton @click="isResAuction = false">유니크</BButton>
|
||||
</TopBackBar>
|
||||
<AuctionResource v-if="isResAuction" ref="auctionResource"></AuctionResource>
|
||||
<AuctionUniqueItem v-else ref="auctionUniqueItem"></AuctionUniqueItem>
|
||||
<BottomBar type="close"></BottomBar>
|
||||
</BContainer>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
/*declare const staticValues: {
|
||||
serverID: string,
|
||||
turnterm: number,
|
||||
serverNick: string,
|
||||
};*/
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { BButton, BContainer } from "bootstrap-vue-3";
|
||||
import TopBackBar from "@/components/TopBackBar.vue";
|
||||
import BottomBar from "./components/BottomBar.vue";
|
||||
import AuctionResource from "@/components/AuctionResource.vue";
|
||||
import AuctionUniqueItem from "@/components/AuctionUniqueItem.vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
isResAuction: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const auctionResource = ref<InstanceType<typeof AuctionResource> | null>(null);
|
||||
const auctionUniqueItem = ref<InstanceType<typeof AuctionUniqueItem> | null>(null);
|
||||
|
||||
const isResAuction = ref(props.isResAuction);
|
||||
|
||||
async function tryReload() {
|
||||
console.log(auctionResource.value);
|
||||
console.log(auctionUniqueItem.value);
|
||||
if(isResAuction.value && auctionResource.value){
|
||||
await auctionResource.value.refresh();
|
||||
}
|
||||
if(!isResAuction.value && auctionUniqueItem.value){
|
||||
await auctionUniqueItem.value.refresh();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div id="container">
|
||||
<TopBackBar :title="title" />
|
||||
|
||||
<div id="newArticle" class="bg0">
|
||||
<div class="newArticleHeader bg2 center">새 게시물 작성</div>
|
||||
<div class="row gx-0">
|
||||
<div class="col-2 col-md-1 articleTitle bg1 center">제목</div>
|
||||
<div class="col-10 col-md-11">
|
||||
<!-- eslint-disable-next-line vue/max-attributes-per-line -->
|
||||
<input v-model="newArticle.title" class="titleInput" type="text" maxlength="250" placeholder="제목" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row gx-0">
|
||||
<div class="col-2 col-md-1 bg1 center">내용</div>
|
||||
<div class="col-10 col-md-11">
|
||||
<textarea
|
||||
ref="newArticleTextForm"
|
||||
v-model="newArticle.text"
|
||||
class="contentInput autosize"
|
||||
placeholder="내용"
|
||||
@input="autoResizeTextarea"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-8 col-md-10" />
|
||||
<div class="col-4 col-md-2 d-grid">
|
||||
<b-button id="submitArticle" @click="submitArticle"> 등록 </b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="board">
|
||||
<template v-if="articles && articles.length">
|
||||
<board-article
|
||||
v-for="article in articles"
|
||||
:key="article.no"
|
||||
:article="article"
|
||||
@submitComment="reloadArticles"
|
||||
/>
|
||||
</template>
|
||||
<template v-else> 게시물이 없습니다. </template>
|
||||
</div>
|
||||
|
||||
<BottomBar />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, reactive, ref } from "vue";
|
||||
import TopBackBar from "@/components/TopBackBar.vue";
|
||||
import BottomBar from "@/components/BottomBar.vue";
|
||||
import BoardArticle from "@/components/BoardArticle.vue";
|
||||
import { convertFormData } from "@util/convertFormData";
|
||||
import axios from "axios";
|
||||
import type { InvalidResponse } from "@/defs";
|
||||
import { autoResizeTextarea } from "@util/autoResizeTextarea";
|
||||
import { unwrap } from "@util/unwrap";
|
||||
export type BoardResponse = {
|
||||
result: true;
|
||||
articles: Record<number, BoardArticleItem>;
|
||||
};
|
||||
|
||||
export type BoardArticleItem = {
|
||||
no: number;
|
||||
nation_no: number;
|
||||
is_secret?: boolean;
|
||||
date: string;
|
||||
general_no: number;
|
||||
author: string;
|
||||
author_icon: string;
|
||||
title: string;
|
||||
text: string;
|
||||
comment: BoardCommentItem[];
|
||||
};
|
||||
|
||||
export type BoardCommentItem = {
|
||||
no: number;
|
||||
nation_no: number;
|
||||
is_secret?: boolean;
|
||||
date: string;
|
||||
document_no: number;
|
||||
general_no: number;
|
||||
author: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: "PageBoard",
|
||||
components: {
|
||||
TopBackBar,
|
||||
BottomBar,
|
||||
BoardArticle,
|
||||
},
|
||||
props: {
|
||||
isSecretBoard: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const newArticleTextForm = ref<HTMLInputElement>();
|
||||
const articles = reactive<BoardArticleItem[]>([]);
|
||||
|
||||
const reloadArticles = async () => {
|
||||
let boardResponse: BoardResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: "j_board_get_articles.php",
|
||||
responseType: "json",
|
||||
method: "post",
|
||||
data: convertFormData({
|
||||
isSecret: props.isSecretBoard,
|
||||
}),
|
||||
});
|
||||
const result: InvalidResponse | BoardResponse = response.data;
|
||||
if (!result.result) {
|
||||
throw result.reason;
|
||||
}
|
||||
boardResponse = result;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`에러: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
articles.length = 0;
|
||||
articles.push(...Object.values(boardResponse.articles));
|
||||
articles.reverse();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await reloadArticles();
|
||||
});
|
||||
|
||||
return {
|
||||
newArticleTextForm,
|
||||
articles,
|
||||
reloadArticles,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: this.isSecretBoard ? "기밀실" : "회의실",
|
||||
newArticle: {
|
||||
title: "",
|
||||
text: "",
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
autoResizeTextarea,
|
||||
async submitArticle() {
|
||||
const { title, text } = this.newArticle;
|
||||
if (!title && !text) {
|
||||
return;
|
||||
}
|
||||
|
||||
let result: InvalidResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: "j_board_article_add.php",
|
||||
method: "post",
|
||||
responseType: "json",
|
||||
data: convertFormData({
|
||||
isSecret: this.isSecretBoard,
|
||||
title,
|
||||
text,
|
||||
}),
|
||||
});
|
||||
result = response.data;
|
||||
if (!result.result) {
|
||||
throw result.reason;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다. :${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.newArticle = { title: "", text: "" };
|
||||
const newArticleTextForm = unwrap(this.newArticleTextForm);
|
||||
newArticleTextForm.style.height = "auto";
|
||||
|
||||
await this.reloadArticles();
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<BContainer v-if="asyncReady" id="container" :toast="{ root: true }">
|
||||
<div class="card">
|
||||
<h3 class="card-header">{{ serverName }} 현황</h3>
|
||||
<MapViewer
|
||||
v-if="cachedMap"
|
||||
:server-nick="serverNick"
|
||||
:serverID="serverID"
|
||||
:map-name="unwrap(gameConstStore?.gameConst.mapName)"
|
||||
:map-data="cachedMap"
|
||||
:is-detail-map="true"
|
||||
:city-position="cityPosition"
|
||||
:format-city-info="formatCityInfoText"
|
||||
:image-path="imagePath"
|
||||
:disallow-click="true"
|
||||
/>
|
||||
<div v-if="cachedMap" class="card-body">
|
||||
<template v-for="(item, idx) in cachedMap.history" :key="idx">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-html="formatLog(item)" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
declare const staticValues: {
|
||||
serverName: string;
|
||||
serverNick: string;
|
||||
serverID: string;
|
||||
};
|
||||
|
||||
declare const getCityPosition: () => CityPositionMap;
|
||||
declare const formatCityInfo: (city: MapCityParsedRaw) => MapCityParsed;
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, provide, ref } from "vue";
|
||||
import { BContainer } from "bootstrap-vue-3";
|
||||
import { SammoAPI } from "./SammoAPI";
|
||||
import { formatLog } from "./utilGame/formatLog";
|
||||
import MapViewer, { type CityPositionMap, type MapCityParsedRaw, type MapCityParsed } from "./components/MapViewer.vue";
|
||||
import { getGameConstStore, type GameConstStore } from "./GameConstStore";
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
import type { CachedMapResult } from "./defs";
|
||||
|
||||
const serverName = staticValues.serverName;
|
||||
const serverNick = staticValues.serverNick;
|
||||
const serverID = staticValues.serverID;
|
||||
|
||||
const asyncReady = ref<boolean>(false);
|
||||
const gameConstStore = ref<GameConstStore>();
|
||||
provide("gameConstStore", gameConstStore);
|
||||
const storeP = getGameConstStore().then((store) => {
|
||||
gameConstStore.value = store;
|
||||
});
|
||||
|
||||
void Promise.all([storeP]).then(() => {
|
||||
asyncReady.value = true;
|
||||
});
|
||||
|
||||
const cityPosition = getCityPosition();
|
||||
const formatCityInfoText = formatCityInfo;
|
||||
const imagePath = window.pathConfig.gameImage;
|
||||
|
||||
const cachedMap = ref<CachedMapResult>();
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
cachedMap.value = await SammoAPI.Global.GetCachedMap();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/../scss/common/bootstrap5.scss";
|
||||
@include media-1000px {
|
||||
#container {
|
||||
width: 700px;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-500px {
|
||||
#container {
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<div id="container" class="pageChiefCenter">
|
||||
<TopBackBar title="사령부" reloadable @reload="reloadTable" />
|
||||
|
||||
<div v-if="chiefList !== undefined" id="mainTable" :class="`${targetIsMe ? 'targetIsMe' : 'targetIsNotMe'}`">
|
||||
<template v-for="(chiefLevel, vidx) in [12, 10, 8, 6, 11, 9, 7, 5]" :key="chiefLevel">
|
||||
<div v-if="vidx % 4 == 0" :class="['turnIdx', vidx == 0 && !targetIsMe ? undefined : 'only1000px']">
|
||||
<div :class="['subRows', 'bg0']" :style="mainTableGridRows">
|
||||
<div class="bg1"> </div>
|
||||
<div v-for="idx in maxChiefTurn" :key="idx" :class="[`turnIdxLeft`, 'align-self-center', 'center']">
|
||||
{{ idx }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(officer, idx) in [chiefList[chiefLevel]]"
|
||||
:key="idx"
|
||||
:class="[`${viewTarget == chiefLevel ? '' : 'only1000px'}`]"
|
||||
>
|
||||
<TopItem
|
||||
v-if="officerLevel != chiefLevel"
|
||||
:style="mainTableGridRows"
|
||||
:officer="officer"
|
||||
:maxTurn="maxChiefTurn"
|
||||
:turnTerm="turnTerm"
|
||||
/>
|
||||
<ChiefReservedCommand
|
||||
v-else
|
||||
:key="idx"
|
||||
:targetIsMe="targetIsMe"
|
||||
:year="year"
|
||||
:month="month"
|
||||
:turn="officer.turn"
|
||||
:turnTerm="turnTerm"
|
||||
:commandList="unwrap(commandList)"
|
||||
:turnTime="officer.turnTime"
|
||||
:maxTurn="maxChiefTurn"
|
||||
:maxPushTurn="Math.floor(maxChiefTurn / 2)"
|
||||
:date="date"
|
||||
:officer="officer"
|
||||
@raiseReload="reloadTable()"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="vidx % 4 == 3" :class="['turnIdx', vidx == 7 && !targetIsMe ? undefined : 'only1000px']">
|
||||
<div :class="['subRows', 'bg0']" :style="mainTableGridRows">
|
||||
<div class="bg1"> </div>
|
||||
<div v-for="idx in maxChiefTurn" :key="idx" :class="[`turnIdxRight`, 'align-self-center', 'center']">
|
||||
{{ idx }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="chiefList" id="bottomChiefBox">
|
||||
<div id="bottomChiefList" class="c-bg2">
|
||||
<template v-for="(chiefLevel, vidx) in [12, 10, 8, 6, 11, 9, 7, 5]" :key="chiefLevel">
|
||||
<div v-if="vidx % 4 == 0" class="turnIdx subRows bg0" :style="subTableGridRows">
|
||||
<div class="bg1" style="grid-row: 1/3" />
|
||||
<div v-for="idx in maxChiefTurn" :key="idx" :class="[`turnIdxLeft`]">
|
||||
{{ idx }}
|
||||
</div>
|
||||
</div>
|
||||
<BottomItem
|
||||
:chiefLevel="chiefLevel"
|
||||
:chiefList="chiefList"
|
||||
:style="subTableGridRows"
|
||||
:isMe="chiefLevel == officerLevel"
|
||||
@click="viewTarget = chiefLevel"
|
||||
/>
|
||||
<div v-if="vidx % 4 == 3" class="turnIdx subRows bg0" :style="subTableGridRows">
|
||||
<div class="bg1" style="grid-row: 1/3" />
|
||||
<div v-for="idx in maxChiefTurn" :key="idx" :class="`turnIdxRight`">
|
||||
{{ idx }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div id="bottomBar">
|
||||
<BottomBar />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
declare const staticValues: {
|
||||
serverNick: string;
|
||||
mapName: string;
|
||||
unitSet: string;
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import "@scss/common/bootstrap5.scss";
|
||||
import "@scss/game_bg.scss";
|
||||
import "../../css/config.css";
|
||||
|
||||
import { computed, provide, reactive, ref, toRefs, watch } from "vue";
|
||||
import ChiefReservedCommand from "@/components/ChiefReservedCommand.vue";
|
||||
import TopBackBar from "@/components/TopBackBar.vue";
|
||||
import BottomBar from "@/components/BottomBar.vue";
|
||||
import VueTypes from "vue-types";
|
||||
import { isString } from "lodash";
|
||||
import { entriesWithType } from "./util/entriesWithType";
|
||||
import TopItem from "@/ChiefCenter/TopItem.vue";
|
||||
import BottomItem from "@/ChiefCenter/BottomItem.vue";
|
||||
import type { OptionalFull } from "./defs";
|
||||
import { SammoAPI } from "./SammoAPI";
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
import { StoredActionsHelper } from "./util/StoredActionsHelper";
|
||||
import type { ChiefResponse } from "./defs/API/NationCommand";
|
||||
|
||||
const props = defineProps({
|
||||
maxChiefTurn: VueTypes.number.isRequired,
|
||||
});
|
||||
|
||||
const tableObj = reactive<Omit<OptionalFull<ChiefResponse>, "result">>({
|
||||
lastExecute: undefined,
|
||||
year: undefined,
|
||||
month: undefined,
|
||||
turnTerm: undefined,
|
||||
date: undefined,
|
||||
chiefList: undefined,
|
||||
isChief: undefined,
|
||||
autorun_limit: undefined,
|
||||
officerLevel: undefined,
|
||||
commandList: undefined,
|
||||
mapName: undefined,
|
||||
unitSet: undefined,
|
||||
});
|
||||
|
||||
const { year, month, turnTerm, date, chiefList, officerLevel, commandList } = toRefs(tableObj);
|
||||
|
||||
const viewTarget = ref<number | undefined>();
|
||||
|
||||
const targetIsMe = ref<boolean>(false);
|
||||
watch(viewTarget, (val) => {
|
||||
console.log("targetChange!", val, targetIsMe);
|
||||
if (val === undefined) {
|
||||
targetIsMe.value = false;
|
||||
return;
|
||||
}
|
||||
if (tableObj.officerLevel === undefined) {
|
||||
targetIsMe.value = false;
|
||||
}
|
||||
targetIsMe.value = val === tableObj.officerLevel;
|
||||
});
|
||||
|
||||
async function reloadTable(): Promise<void> {
|
||||
try {
|
||||
const response = await SammoAPI.NationCommand.GetReservedCommand();
|
||||
console.log(response);
|
||||
for (const [key, value] of entriesWithType(response)) {
|
||||
if (key === "result") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === "officerLevel") {
|
||||
if (value < 5) {
|
||||
tableObj.officerLevel = undefined;
|
||||
} else {
|
||||
tableObj.officerLevel = value as number;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
//HACK:
|
||||
tableObj[key as unknown as "year"] = value as unknown as undefined;
|
||||
}
|
||||
|
||||
if (viewTarget.value === undefined) {
|
||||
if (!tableObj.officerLevel) {
|
||||
viewTarget.value = 12;
|
||||
} else {
|
||||
viewTarget.value = tableObj.officerLevel;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
alert(e);
|
||||
}
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const mainTableGridRows = computed(() => {
|
||||
return {
|
||||
gridTemplateRows: `24px repeat(${props.maxChiefTurn}, 30px)`,
|
||||
};
|
||||
});
|
||||
|
||||
const subTableGridRows = computed(() => {
|
||||
return {
|
||||
gridTemplateRows: `36px repeat(${props.maxChiefTurn + 1}, 1fr)`,
|
||||
};
|
||||
});
|
||||
|
||||
void reloadTable();
|
||||
|
||||
const storedActionsHelper = new StoredActionsHelper(
|
||||
staticValues.serverNick,
|
||||
"nation",
|
||||
staticValues.mapName,
|
||||
staticValues.unitSet
|
||||
);
|
||||
provide("storedNationActionsHelper", storedActionsHelper);
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "@scss/chiefCenter.scss";
|
||||
</style>
|
||||
@@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<BContainer v-if="asyncReady" id="container" :toast="{ root: true }" class="pageGlobalDiplomacy">
|
||||
<TopBackBar title="중원 정보"></TopBackBar>
|
||||
<div class="diplomacy bg0">
|
||||
<div class="s-border-tb center tb-title" style="background-color: blue">외교 현황</div>
|
||||
<div class="diplomacy_area">
|
||||
<table v-if="diplomacy" class="center" style="margin: auto; min-width: 400px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th
|
||||
v-for="nation of diplomacy.nations"
|
||||
:key="nation.nation"
|
||||
class="thead-nation"
|
||||
:style="{
|
||||
color: isBrightColor(nation.color) ? '#000' : '#fff',
|
||||
backgroundColor: nation.color,
|
||||
}"
|
||||
>
|
||||
{{ nation.name }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="me of diplomacy.nations" :key="me.nation">
|
||||
<th
|
||||
class="tbody-nation"
|
||||
:style="{
|
||||
color: isBrightColor(me.color) ? '#000' : '#fff',
|
||||
backgroundColor: me.color,
|
||||
}"
|
||||
>
|
||||
{{ me.name }}
|
||||
</th>
|
||||
<template v-for="you of diplomacy.nations" :key="you.nation">
|
||||
<td v-if="me.nation == you.nation" class="tbody-cell">\</td>
|
||||
<td
|
||||
v-else-if="me.nation == diplomacy.myNationID || you.nation == diplomacy.myNationID"
|
||||
class="tbody-cell"
|
||||
style="background-color: #660000"
|
||||
v-html="infomativeStateCharMap[diplomacy.diplomacyList[me.nation][you.nation]]"
|
||||
/>
|
||||
<td
|
||||
v-else
|
||||
class="tbody-cell"
|
||||
v-html="neutralStateCharMap[diplomacy.diplomacyList[me.nation][you.nation]]"
|
||||
/>
|
||||
</template>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td :colspan="Object.keys(diplomacy.nations).length + 1" class="center">
|
||||
불가침 :
|
||||
<span style="color: limegreen">@</span>, 통상 : ㆍ, 선포 : <span style="color: magenta">▲</span>, 교전 :
|
||||
<span style="color: red">★</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="diplomacy && diplomacy.conflict.length > 0" class="conflict-area gx-0 bg0">
|
||||
<div class="s-border-tb center tb-title" style="background-color: magenta">분쟁 현황</div>
|
||||
<div v-for="[cityID, conflictNations] of diplomacy.conflict" :key="cityID" class="row gx-0">
|
||||
<div class="conflictCityName">{{ gameConstStore?.cityConst[cityID].name }}</div>
|
||||
<div class="conflictNation col">
|
||||
<div
|
||||
v-for="[nation, percent] of Object.entries(conflictNations).map(
|
||||
([nationID, percent])=>[nationMap.get(parseInt(nationID)),percent] as [SimpleNationObj, number]
|
||||
)"
|
||||
:key="nation.nation"
|
||||
class="row gx-0"
|
||||
>
|
||||
<div
|
||||
class="conflictNationName"
|
||||
:style="{
|
||||
color: isBrightColor(nation.color) ? '#000' : '#fff',
|
||||
backgroundColor: nation.color,
|
||||
flexBasis: '16ch',
|
||||
paddingLeft: '1ch',
|
||||
}"
|
||||
>
|
||||
{{ nation.name }}
|
||||
</div>
|
||||
<div
|
||||
class="conflictNationPercent"
|
||||
:style="{
|
||||
flexBasis: '6ch',
|
||||
textAlign: 'right',
|
||||
paddingRight: '0.5ch',
|
||||
}"
|
||||
>
|
||||
{{ percent.toLocaleString(undefined, { minimumFractionDigits: 1 }) }}%
|
||||
</div>
|
||||
<div class="col align-self-center">
|
||||
<div
|
||||
class="progress"
|
||||
:style="{
|
||||
width: `${percent}%`,
|
||||
marginLeft: 0,
|
||||
height: '1.2em',
|
||||
backgroundColor: nation.color,
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="map_area mx-0 bg0">
|
||||
<div class="s-border-tb center tb-title" style="background-color: green">중원 지도</div>
|
||||
<div class="row g-0">
|
||||
<div class="map_position">
|
||||
<MapViewer
|
||||
v-if="map"
|
||||
:server-nick="serverNick"
|
||||
:serverID="serverID"
|
||||
:map-name="unwrap(gameConstStore?.gameConst.mapName)"
|
||||
:map-data="map"
|
||||
:is-detail-map="true"
|
||||
:city-position="cityPosition"
|
||||
:format-city-info="formatCityInfoText"
|
||||
:image-path="imagePath"
|
||||
:disallow-click="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="nation_position"><SimpleNationList v-if="diplomacy" :nations="diplomacy.nations" /></div>
|
||||
</div>
|
||||
</div>
|
||||
<BottomBar></BottomBar>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
declare const staticValues: {
|
||||
serverNick: string;
|
||||
serverID: string;
|
||||
};
|
||||
|
||||
declare const getCityPosition: () => CityPositionMap;
|
||||
declare const formatCityInfo: (city: MapCityParsedRaw) => MapCityParsed;
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, provide, ref, watch } from "vue";
|
||||
import { BContainer, useToast } from "bootstrap-vue-3";
|
||||
import TopBackBar from "@/components/TopBackBar.vue";
|
||||
import BottomBar from "@/components/BottomBar.vue";
|
||||
import { SammoAPI } from "./SammoAPI";
|
||||
import SimpleNationList from "./components/SimpleNationList.vue";
|
||||
import MapViewer, { type CityPositionMap, type MapCityParsedRaw, type MapCityParsed } from "./components/MapViewer.vue";
|
||||
import { getGameConstStore, type GameConstStore } from "./GameConstStore";
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
import type { SimpleNationObj, diplomacyState, MapResult } from "./defs";
|
||||
import type { GetDiplomacyResponse } from "./defs/API/Global";
|
||||
import { isString } from "lodash";
|
||||
import { isBrightColor } from "@/util/isBrightColor";
|
||||
|
||||
const serverID = staticValues.serverID;
|
||||
const serverNick = staticValues.serverNick;
|
||||
|
||||
const asyncReady = ref<boolean>(false);
|
||||
const gameConstStore = ref<GameConstStore>();
|
||||
provide("gameConstStore", gameConstStore);
|
||||
const storeP = getGameConstStore().then((store) => {
|
||||
gameConstStore.value = store;
|
||||
});
|
||||
|
||||
void Promise.all([storeP]).then(() => {
|
||||
asyncReady.value = true;
|
||||
});
|
||||
|
||||
const infomativeStateCharMap: Record<diplomacyState, string> = {
|
||||
0: '<span style="color:red;">★</span>',
|
||||
1: '<span style="color:magenta;">▲</span>',
|
||||
2: "ㆍ",
|
||||
7: '<span style="color:green;">@</span>',
|
||||
};
|
||||
|
||||
const neutralStateCharMap: Record<diplomacyState, string> = {
|
||||
0: '<span style="color:red;">★</span>',
|
||||
1: '<span style="color:magenta;">▲</span>',
|
||||
2: "",
|
||||
7: "에러",
|
||||
};
|
||||
|
||||
const cityPosition = getCityPosition();
|
||||
const formatCityInfoText = formatCityInfo;
|
||||
const imagePath = window.pathConfig.gameImage;
|
||||
|
||||
const map = ref<MapResult>();
|
||||
const diplomacy = ref<GetDiplomacyResponse>();
|
||||
|
||||
const nationMap = ref(new Map<number, SimpleNationObj>());
|
||||
|
||||
const toasts = unwrap(useToast());
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
map.value = await SammoAPI.Global.GetMap({
|
||||
neutralView: 0,
|
||||
showMe: 1,
|
||||
});
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
watch(diplomacy, (diplomacy) => {
|
||||
if (diplomacy === undefined) {
|
||||
return;
|
||||
}
|
||||
const map = new Map<number, SimpleNationObj>();
|
||||
for (const nation of diplomacy.nations) {
|
||||
map.set(nation.nation, nation);
|
||||
}
|
||||
nationMap.value = map;
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
diplomacy.value = await SammoAPI.Global.GetDiplomacy();
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
* {
|
||||
font-size: 14px;
|
||||
}
|
||||
.diplomacy_area {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.thead-nation {
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: mixed;
|
||||
text-align: end;
|
||||
padding-bottom: 1ch;
|
||||
padding-top: 1ch;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
min-width: 1ch;
|
||||
max-width: 3ch;
|
||||
font-weight: normal;
|
||||
}
|
||||
.tbody-nation {
|
||||
text-align: end;
|
||||
padding-right: 1ch;
|
||||
padding-left: 1ch;
|
||||
min-width: 10ch;
|
||||
font-weight: normal;
|
||||
}
|
||||
.diplomacy {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
.conflict-area {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
.map_area {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
.tb-title {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.tbody-cell {
|
||||
border-left: solid 1px gray;
|
||||
border-top: solid 1px gray;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.conflictCityName {
|
||||
flex-basis: 16ch;
|
||||
text-align: right;
|
||||
padding-right: 1ch;
|
||||
align-self: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<BContainer v-if="asyncReady" id="container" :toast="{ root: true }" class="bg0 pageHistory">
|
||||
<TopBackBar title="연감" type="close">
|
||||
<div> </div>
|
||||
<!-- HACK: variant에 정상적인 값을 넣어야 해서...-->
|
||||
<BDropdown class="optionMenu" right :variant="('sammo-base2' as 'primary')">
|
||||
<template #button-content><i class="bi bi-gear-fill"></i> 설정</template>
|
||||
<BDropdownItem @click="isNationRankingBottom = !isNationRankingBottom"
|
||||
>국가 순서 위치 변경(모바일 전용)</BDropdownItem
|
||||
>
|
||||
</BDropdown>
|
||||
</TopBackBar>
|
||||
<div class="center row mx-0 s-border-tb">
|
||||
<div class="col-md-1 col-2 year-selector text-end align-self-center">연월 선택:</div>
|
||||
<BButton class="col-md-1 col-2" @click="queryYearMonth = unwrap(queryYearMonth) - 1">◀ 이전달</BButton>
|
||||
<div class="col-md-3 col-5 d-grid">
|
||||
<BFormSelect v-model="queryYearMonth" :options="generateYearMonthList()" />
|
||||
</div>
|
||||
<BButton class="col-md-1 col-2" @click="queryYearMonth = unwrap(queryYearMonth) + 1">다음달 ▶</BButton>
|
||||
</div>
|
||||
<div v-if="history" id="map_holder" :class="['row', 'gx-0', isNationRankingBottom ? 'isNationRankingBottom' : '']">
|
||||
<div class="map_position">
|
||||
<MapViewer
|
||||
:server-nick="serverNick"
|
||||
:serverID="queryServerID"
|
||||
:map-name="mapName"
|
||||
:map-data="history.map"
|
||||
:is-detail-map="true"
|
||||
:city-position="cityPosition"
|
||||
:format-city-info="formatCityInfoText"
|
||||
:image-path="imagePath"
|
||||
:disallow-click="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="nation_position"><SimpleNationList :nations="history.nations" /></div>
|
||||
<div class="world_history col-12">
|
||||
<div class="bg1 center s-border-tb"><b>중원 정세</b></div>
|
||||
<div class="content">
|
||||
<template v-for="(item, idx) in history.global_history" :key="idx">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-html="formatLog(item)" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="general_public_record col-12">
|
||||
<div class="bg1 center s-border-tb"><b>장수 동향</b></div>
|
||||
<div class="content">
|
||||
<template v-for="(item, idx) in history.global_action" :key="idx">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-html="formatLog(item)" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BottomBar type="close"></BottomBar>
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
declare const staticValues: {
|
||||
firstYearMonth: number;
|
||||
lastYearMonth: number;
|
||||
currentYearMonth: number;
|
||||
serverNick: string;
|
||||
serverID: string;
|
||||
mapName: string;
|
||||
};
|
||||
declare const query: {
|
||||
yearMonth: number | null;
|
||||
serverID: string;
|
||||
};
|
||||
|
||||
declare const getCityPosition: () => CityPositionMap;
|
||||
declare const formatCityInfo: (city: MapCityParsedRaw) => MapCityParsed;
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, provide, ref, watch } from "vue";
|
||||
import { BContainer, BButton, BFormSelect, BDropdown, BDropdownItem } from "bootstrap-vue-3";
|
||||
import TopBackBar from "@/components/TopBackBar.vue";
|
||||
import BottomBar from "@/components/BottomBar.vue";
|
||||
import type { HistoryObj } from "./defs/API/Global";
|
||||
import { SammoAPI } from "./SammoAPI";
|
||||
import { joinYearMonth } from "./util/joinYearMonth";
|
||||
import { parseYearMonth } from "./util/parseYearMonth";
|
||||
import { formatLog } from "./utilGame/formatLog";
|
||||
import SimpleNationList from "./components/SimpleNationList.vue";
|
||||
import MapViewer, { type CityPositionMap, type MapCityParsedRaw, type MapCityParsed } from "./components/MapViewer.vue";
|
||||
import { getGameConstStore, type GameConstStore } from "./GameConstStore";
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
|
||||
const queryYearMonth = ref<number>();
|
||||
const queryServerID = query.serverID;
|
||||
const serverNick = staticValues.serverNick;
|
||||
|
||||
const lastYearMonth = ref(staticValues.lastYearMonth);
|
||||
const firstYearMonth = ref(staticValues.firstYearMonth);
|
||||
const currentYearMonth = ref(staticValues.currentYearMonth);
|
||||
|
||||
const asyncReady = ref<boolean>(false);
|
||||
const gameConstStore = ref<GameConstStore>();
|
||||
provide("gameConstStore", gameConstStore);
|
||||
const storeP = getGameConstStore().then((store) => {
|
||||
gameConstStore.value = store;
|
||||
});
|
||||
|
||||
void Promise.all([storeP]).then(() => {
|
||||
asyncReady.value = true;
|
||||
});
|
||||
|
||||
const isNationRankingBottom = useLocalStorage(`${serverNick}_isNationRankingBottom`, false);
|
||||
|
||||
const mapName = staticValues.mapName;
|
||||
const cityPosition = getCityPosition();
|
||||
const formatCityInfoText = formatCityInfo;
|
||||
const imagePath = window.pathConfig.gameImage;
|
||||
|
||||
const history = ref<HistoryObj>();
|
||||
|
||||
function generateYearMonthList(): { text: string; value: number }[] {
|
||||
const result: { text: string; value: number }[] = [];
|
||||
let yearMonth = firstYearMonth.value;
|
||||
while (yearMonth <= lastYearMonth.value) {
|
||||
const [year, month] = parseYearMonth(yearMonth);
|
||||
const info: string[] = [];
|
||||
if (queryYearMonth.value === yearMonth) {
|
||||
info.push("선택");
|
||||
}
|
||||
result.push({ text: `${year}년 ${month}월 ${info.length > 0 ? `(${info.join(", ")})` : ""}`, value: yearMonth });
|
||||
yearMonth += 1;
|
||||
}
|
||||
const [year, month] = parseYearMonth(yearMonth);
|
||||
const info: string[] = [];
|
||||
if (queryYearMonth.value === yearMonth) {
|
||||
info.push("선택");
|
||||
}
|
||||
|
||||
if (staticValues.serverID === query.serverID) {
|
||||
info.push("현재");
|
||||
result.push({ text: `${year}년 ${month}월 ${info.length > 0 ? `(${info.join(", ")})` : ""}`, value: yearMonth });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
watch(queryYearMonth, async (yearMonth) => {
|
||||
if (yearMonth === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (yearMonth < firstYearMonth.value) {
|
||||
queryYearMonth.value = firstYearMonth.value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (staticValues.serverID === query.serverID) {
|
||||
if (yearMonth > lastYearMonth.value + 1) {
|
||||
queryYearMonth.value = lastYearMonth.value + 1;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (yearMonth > lastYearMonth.value) {
|
||||
queryYearMonth.value = lastYearMonth.value;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (yearMonth > lastYearMonth.value && staticValues.serverID === query.serverID) {
|
||||
try {
|
||||
const result = await SammoAPI.Global.GetCurrentHistory();
|
||||
history.value = result.data;
|
||||
console.log(result);
|
||||
const newLastYearMonth = joinYearMonth(result.data.year, result.data.month) - 1;
|
||||
if (newLastYearMonth > lastYearMonth.value) {
|
||||
lastYearMonth.value = newLastYearMonth;
|
||||
currentYearMonth.value = newLastYearMonth + 1;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const [year, month] = parseYearMonth(yearMonth);
|
||||
try {
|
||||
const result = await SammoAPI.Global.GetHistory[queryServerID][year][month]();
|
||||
history.value = result.data;
|
||||
console.log(result);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
queryYearMonth.value = (() => {
|
||||
if (query.yearMonth === null) {
|
||||
return staticValues.currentYearMonth;
|
||||
}
|
||||
if (query.yearMonth > staticValues.lastYearMonth + 1) {
|
||||
return staticValues.currentYearMonth;
|
||||
}
|
||||
if (query.yearMonth < staticValues.firstYearMonth) {
|
||||
return staticValues.currentYearMonth;
|
||||
}
|
||||
return query.yearMonth;
|
||||
})();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@scss/common/break_500px.scss";
|
||||
|
||||
@include media-500px {
|
||||
.optionMenu::v-deep .dropdown-toggle {
|
||||
height: 32px;
|
||||
}
|
||||
.isNationRankingBottom {
|
||||
.nation_position {
|
||||
order: 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
.hidden_but_copyable {
|
||||
color: rgba(0, 0, 0, 0);
|
||||
font-size: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,590 @@
|
||||
<template>
|
||||
<TopBackBar :title="title" />
|
||||
<div
|
||||
id="container"
|
||||
class="bg0 px-2"
|
||||
style="max-width: 1000px; margin: auto; border: solid 1px #888888; overflow: hidden"
|
||||
>
|
||||
<div id="inheritance_list" class="row">
|
||||
<template v-for="(text, key) in inheritanceViewText" :key="key">
|
||||
<div :id="`inherit_${key}`" class="col col-sm-4 col-12 inherit_item inherit_template_item">
|
||||
<div class="row">
|
||||
<label :id="`inherit_${key}_head`" class="inherit_head col col-md-6 col-sm-7 col-6 col-form-label">{{
|
||||
text.title
|
||||
}}</label>
|
||||
<div class="col col-md-6 col-sm-5 col-6">
|
||||
<input
|
||||
:id="`inherit_${key}_value`"
|
||||
type="text"
|
||||
class="form-control inherit_value f_tnum"
|
||||
readonly
|
||||
:value="Math.floor(items[key]).toLocaleString()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: right">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<small class="form-text text-muted" v-html="text.info" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="key == 'new'" style="width: 100%; padding: 0 10px">
|
||||
<hr :style="{ opacity: 0.5 }" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div id="inheritance_store">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="bg1 a-center">유산 포인트 상점</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col offset-md-4 col-md-4 col-sm-6 col-12 py-2">
|
||||
<div class="row px-4">
|
||||
<div class="a-right col-6 align-self-center">다음 전투 특기 선택</div>
|
||||
<div class="col-6">
|
||||
<select v-model="nextSpecialWar" class="form-select col-6">
|
||||
<option v-for="(info, key) in availableSpecialWar" :key="key" :value="key">
|
||||
{{ info.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="a-right">
|
||||
<small class="form-text text-muted"
|
||||
><!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span style="color: white" v-html="availableSpecialWar[nextSpecialWar].info" /><br />다음에 얻을 전투
|
||||
특기를 정합니다.<br /><span style="color: white"
|
||||
>필요 포인트: {{ inheritActionCost.nextSpecial }}</span
|
||||
></small
|
||||
>
|
||||
</div>
|
||||
<div class="row px-4">
|
||||
<BButton class="col-6 offset-6" variant="primary" @click="setNextSpecialWar"> 구입 </BButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-4 col-sm-6 col-12 py-2">
|
||||
<div class="row px-4">
|
||||
<div class="a-right col-6 align-self-center">유니크 경매</div>
|
||||
<div class="col-6">
|
||||
<select v-model="specificUnique" class="form-select col-6">
|
||||
<option disabled selected :value="null">유니크 선택</option>
|
||||
<option v-for="(info, key) in availableUnique" :key="key" :value="key">
|
||||
{{ info.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row px-4">
|
||||
<div class="col f_tnum">
|
||||
<NumberInputWithInfo
|
||||
v-model="specificUniqueAmount"
|
||||
title="입찰 포인트"
|
||||
:min="inheritActionCost.minSpecificUnique"
|
||||
:max="items.previous"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="a-right">
|
||||
<small class="form-text text-muted"
|
||||
>얻고자 하는 유니크 아이템으로 경매를 시작합니다. 24턴 동안 진행됩니다.<br />
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span style="color: white" v-html="specificUnique == null ? '' : availableUnique[specificUnique].info" />
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="row px-4">
|
||||
<BButton class="col-6 offset-6" variant="primary" @click="openUniqueItemAuction"> 경매 시작 </BButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 100%; padding: 0 10px">
|
||||
<hr :style="{ opacity: 0.5 }" />
|
||||
</div>
|
||||
<div class="row py-sm-2">
|
||||
<div class="col col-md-4 col-sm-6 col-12 py-2">
|
||||
<div class="row px-4">
|
||||
<div class="a-right col-6 align-self-center">랜덤 턴 초기화</div>
|
||||
<BButton class="col-6" variant="primary" @click="buySimple('ResetTurnTime')"> 구입 </BButton>
|
||||
</div>
|
||||
<div class="a-right">
|
||||
<small class="form-text text-muted"
|
||||
>다음 턴이 랜덤으로 바뀝니다. (필요 포인트가 피보나치식으로 증가합니다)<br /><span style="color: white"
|
||||
>필요 포인트: {{ inheritActionCost.resetTurnTime }}</span
|
||||
></small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-4 col-sm-6 col-12 py-2">
|
||||
<div class="row px-4">
|
||||
<div class="a-right col-6 align-self-center">랜덤 유니크 획득</div>
|
||||
<BButton class="col-6" variant="primary" @click="buySimple('BuyRandomUnique')"> 구입 </BButton>
|
||||
</div>
|
||||
<div class="a-right">
|
||||
<small class="form-text text-muted"
|
||||
>다음 턴에 랜덤 유니크를 얻습니다.<br /><span style="color: white"
|
||||
>필요 포인트: {{ inheritActionCost.randomUnique }}</span
|
||||
></small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-md-4 col-sm-6 col-12 py-2">
|
||||
<div class="row px-4">
|
||||
<div class="a-right col-6 align-self-center">즉시 전투 특기 초기화</div>
|
||||
<BButton class="col-6" variant="primary" @click="buySimple('ResetSpecialWar')"> 구입 </BButton>
|
||||
</div>
|
||||
<div class="a-right">
|
||||
<small class="form-text text-muted"
|
||||
>즉시 전투 특기를 초기화합니다. (필요 포인트가 피보나치식으로 증가합니다)<br /><span style="color: white"
|
||||
>필요 포인트: {{ inheritActionCost.resetSpecialWar }}</span
|
||||
></small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 100%; padding: 0 10px">
|
||||
<hr :style="{ opacity: 0.5 }" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div v-for="(info, buffKey) in inheritBuffHelpText" :key="buffKey" class="col col-md-4 col-sm-6 col-12">
|
||||
<div class="row">
|
||||
<label class="col col-sm-6 col-form-label" :for="`buff-${buffKey}`">{{ info.title }}</label>
|
||||
<div class="col col-sm-6 f_tnum">
|
||||
<b-form-input
|
||||
:id="`buff-${buffKey}`"
|
||||
v-model.number="inheritBuff[buffKey]"
|
||||
type="number"
|
||||
:min="prevInheritBuff[buffKey] ?? 0"
|
||||
:max="maxInheritBuff"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: right">
|
||||
<small class="form-text text-muted f_tnum"
|
||||
>{{ info.info }}<br /><span style="color: white"
|
||||
>필요 포인트:
|
||||
{{
|
||||
inheritActionCost.buff[inheritBuff[buffKey]] - inheritActionCost.buff[prevInheritBuff[buffKey] ?? 0]
|
||||
}}</span
|
||||
></small
|
||||
>
|
||||
</div>
|
||||
<div class="row px-4" style="margin-bottom: 1em">
|
||||
<BButton
|
||||
variant="secondary"
|
||||
class="col col-md-6 col-4 offset-md-0 offset-4"
|
||||
@click="inheritBuff[buffKey] = prevInheritBuff[buffKey] ?? 0"
|
||||
>
|
||||
리셋 </BButton
|
||||
><BButton variant="primary" class="col col-md-6 col-4" @click="buyInheritBuff(buffKey)"> 구입 </BButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="bg1 a-center">유산 포인트 변경 내역</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="[idx, log] of inheritPointLogs" :key="idx" class="row">
|
||||
<div class="col a-right" style="max-width: 20ch">
|
||||
<small class="text-muted tnum">[{{ log.date }}]</small>
|
||||
</div>
|
||||
<div class="col a-left">
|
||||
{{ log.text }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid"><BButton @click="getMoreLog()">더 가져오기</BButton></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
type InheritanceType =
|
||||
| "previous"
|
||||
| "lived_month"
|
||||
| "max_belong"
|
||||
| "max_domestic_critical"
|
||||
| "active_action"
|
||||
// | "snipe_combat"
|
||||
| "combat"
|
||||
| "sabotage"
|
||||
| "unifier"
|
||||
| "dex"
|
||||
| "tournament"
|
||||
| "betting";
|
||||
|
||||
type InheritanceViewType = InheritanceType | "sum" | "new";
|
||||
|
||||
declare const staticValues: {
|
||||
lastInheritPointLogs: InheritPointLogItem[];
|
||||
items: Record<InheritanceType, number>;
|
||||
currentInheritBuff: {
|
||||
[v in inheritBuffType]: number | undefined;
|
||||
};
|
||||
maxInheritBuff: number;
|
||||
inheritActionCost: {
|
||||
buff: number[];
|
||||
resetTurnTime: number;
|
||||
resetSpecialWar: number;
|
||||
randomUnique: number;
|
||||
nextSpecial: number;
|
||||
minSpecificUnique: number;
|
||||
};
|
||||
resetTurnTimeLevel: number;
|
||||
resetSpecialWarLevel: number;
|
||||
|
||||
availableSpecialWar: Record<
|
||||
string,
|
||||
{
|
||||
title: string;
|
||||
info: string;
|
||||
}
|
||||
>;
|
||||
availableUnique: Record<
|
||||
string,
|
||||
{
|
||||
title: string;
|
||||
rawName: string;
|
||||
info: string;
|
||||
}
|
||||
>;
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from "vue";
|
||||
import "@scss/common/bootstrap5.scss";
|
||||
import "@scss/game_bg.scss";
|
||||
import TopBackBar from "@/components/TopBackBar.vue";
|
||||
import _ from "lodash";
|
||||
import NumberInputWithInfo from "@/components/NumberInputWithInfo.vue";
|
||||
import { SammoAPI } from "./SammoAPI";
|
||||
import type { inheritBuffType, InheritPointLogItem } from "./defs/API/InheritAction";
|
||||
import * as JosaUtil from "@/util/JosaUtil";
|
||||
import { BButton } from "bootstrap-vue-3";
|
||||
import { unwrap } from "./util/unwrap";
|
||||
|
||||
const inheritanceViewText: Record<InheritanceViewType, { title: string; info: string }> = {
|
||||
sum: {
|
||||
title: "총 포인트",
|
||||
info: "다음 플레이에서 사용할 수 있는 총 포인트입니다.",
|
||||
},
|
||||
previous: {
|
||||
title: "기존 포인트",
|
||||
info: "이전에 물려받은 포인트입니다.",
|
||||
},
|
||||
new: {
|
||||
title: "신규 포인트",
|
||||
info: "이번 플레이에서 얻은 총 포인트입니다.",
|
||||
},
|
||||
lived_month: {
|
||||
title: "생존",
|
||||
info: "살아남은 기간입니다. (1개월 단위)",
|
||||
},
|
||||
max_belong: {
|
||||
title: "최대 임관년 수",
|
||||
info: "가장 오래 임관했던 국가의 연도입니다.",
|
||||
},
|
||||
max_domestic_critical: {
|
||||
title: "최대 연속 내정 성공",
|
||||
info: "성공한 내정 중 최대 연속값입니다.",
|
||||
},
|
||||
active_action: {
|
||||
title: "능동 행동 수",
|
||||
info: "장수 동향에 본인의 이름이 직접 나타난 수입니다.<br>일부 사령턴은 제외됩니다.",
|
||||
},
|
||||
/* snipe_combat: {
|
||||
title: "병종 상성 우위 횟수",
|
||||
info: "유리한 상성을 가지고 전투했습니다.",
|
||||
},*/
|
||||
combat: {
|
||||
title: "전투 횟수",
|
||||
info: "전투 횟수입니다.",
|
||||
},
|
||||
sabotage: {
|
||||
title: "계략 성공 횟수",
|
||||
info: "계략 성공 횟수입니다.",
|
||||
},
|
||||
unifier: {
|
||||
title: "천통 기여",
|
||||
info: "천통에 기여한 포인트입니다. <br>각 국의 군주, 천통 수뇌, 천통 군주가 받습니다.",
|
||||
},
|
||||
dex: {
|
||||
title: "숙련도",
|
||||
info: "총 숙련도합입니다. <br>최대 숙련 이후에는 상승량이 1/3로 감소합니다.",
|
||||
},
|
||||
tournament: {
|
||||
title: "토너먼트",
|
||||
info: "토너먼트 입상 포인트입니다.",
|
||||
},
|
||||
betting: {
|
||||
title: "베팅 당첨",
|
||||
info: "성공적인 베팅을 했습니다. <br>수익율과 베팅 성공 횟수를 따릅니다.",
|
||||
},
|
||||
};
|
||||
|
||||
const inheritBuffHelpText: Record<
|
||||
inheritBuffType,
|
||||
{
|
||||
title: string;
|
||||
info: string;
|
||||
}
|
||||
> = {
|
||||
warAvoidRatio: {
|
||||
title: "회피 확률 증가",
|
||||
info: "전투 시 회피 확률이 1%p ~ 5%p 증가합니다.",
|
||||
},
|
||||
warCriticalRatio: {
|
||||
title: "필살 확률 증가",
|
||||
info: "전투 시 필살 확률이 1%p ~ 5%p 증가합니다.",
|
||||
},
|
||||
warMagicTrialProb: {
|
||||
title: "계략 시도 확률 증가",
|
||||
info: "전투 시 계략을 시도할 확률이 1%p ~ 5%p 증가합니다. 무장도 계략을 시도합니다.",
|
||||
},
|
||||
warAvoidRatioOppose: {
|
||||
title: "상대 회피 확률 감소",
|
||||
info: "전투 시 상대의 회피 확률이 1%p ~ 5%p 감소합니다.",
|
||||
},
|
||||
warCriticalRatioOppose: {
|
||||
title: "상대 필살 확률 감소",
|
||||
info: "전투 시 상대의 필살 확률이 1%p ~ 5%p 감소합니다.",
|
||||
},
|
||||
warMagicTrialProbOppose: {
|
||||
title: "상대 계략 시도 확률 감소",
|
||||
info: "전투 시 상대의 계략 시도 확률이 1%p ~ 5%p 감소합니다.",
|
||||
},
|
||||
domesticSuccessProb: {
|
||||
title: "내정 성공 확률 증가",
|
||||
info: "민심, 인구, 농업, 상업, 치안, 수비, 성벽, 기술 내정의 성공 확률이 1%p ~ 5%p 증가합니다.",
|
||||
},
|
||||
domesticFailProb: {
|
||||
title: "내정 실패 확률 감소",
|
||||
info: "민심, 인구, 농업, 상업, 치안, 수비, 성벽, 기술 내정의 실패 확률이 1%p ~ 5%p 감소합니다.",
|
||||
},
|
||||
};
|
||||
|
||||
const inheritBuff = reactive({} as Record<inheritBuffType, number>);
|
||||
for (const buffKey of Object.keys(inheritBuffHelpText) as inheritBuffType[]) {
|
||||
inheritBuff[buffKey] = staticValues.currentInheritBuff[buffKey] ?? 0;
|
||||
}
|
||||
|
||||
const title = "유산 관리";
|
||||
|
||||
const items = ref(
|
||||
(() => {
|
||||
const totalPoint = Math.floor(_.sum(Object.values(staticValues.items)));
|
||||
const previousPoint = Math.floor(staticValues.items["previous"]);
|
||||
const newPoint = Math.floor(totalPoint - previousPoint);
|
||||
const result: Record<InheritanceViewType, number> = {
|
||||
...staticValues.items,
|
||||
sum: totalPoint,
|
||||
new: newPoint,
|
||||
};
|
||||
return result;
|
||||
})()
|
||||
);
|
||||
|
||||
const {
|
||||
maxInheritBuff,
|
||||
inheritActionCost,
|
||||
currentInheritBuff: prevInheritBuff,
|
||||
availableSpecialWar,
|
||||
availableUnique,
|
||||
} = staticValues;
|
||||
|
||||
const nextSpecialWar = ref(Object.keys(availableSpecialWar)[0]);
|
||||
const specificUnique = ref<string | null>(null);
|
||||
const specificUniqueAmount = ref(inheritActionCost.minSpecificUnique);
|
||||
|
||||
const lastLogID = ref(Math.min(...staticValues.lastInheritPointLogs.map((v) => v.id)));
|
||||
const inheritPointLogs = ref(
|
||||
(() => {
|
||||
const logs = new Map<number, InheritPointLogItem>();
|
||||
for (const log of staticValues.lastInheritPointLogs) {
|
||||
logs.set(log.id, log);
|
||||
}
|
||||
return logs;
|
||||
})()
|
||||
);
|
||||
|
||||
staticValues.lastInheritPointLogs;
|
||||
async function buyInheritBuff(buffKey: inheritBuffType) {
|
||||
const level = Math.floor(unwrap(inheritBuff[buffKey]));
|
||||
const prevLevel = prevInheritBuff[buffKey] ?? 0;
|
||||
if (level == prevLevel) {
|
||||
return;
|
||||
}
|
||||
if (level < prevLevel) {
|
||||
alert("낮출 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
const cost = inheritActionCost.buff[level] - inheritActionCost.buff[prevLevel];
|
||||
if (items.value.previous < cost) {
|
||||
alert("유산 포인트가 부족합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const name = inheritBuffHelpText[buffKey].title;
|
||||
const josaUl = JosaUtil.pick(name, "을");
|
||||
if (!confirm(`${name}${josaUl} ${level}등급으로 올릴까요? ${cost} 포인트가 소모됩니다.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await SammoAPI.InheritAction.BuyHiddenBuff({
|
||||
type: buffKey,
|
||||
level,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
alert("성공했습니다.");
|
||||
//TODO: 페이지 새로고침 필요없이 하도록
|
||||
location.reload();
|
||||
}
|
||||
async function buySimple(type: "ResetTurnTime" | "BuyRandomUnique" | "ResetSpecialWar") {
|
||||
const costMap: Record<typeof type, number> = {
|
||||
ResetTurnTime: inheritActionCost.resetTurnTime,
|
||||
ResetSpecialWar: inheritActionCost.resetSpecialWar,
|
||||
BuyRandomUnique: inheritActionCost.randomUnique,
|
||||
};
|
||||
|
||||
const cost = costMap[type];
|
||||
if (cost === undefined) {
|
||||
alert(`올바르지 않은 타입:${type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const messageMap: Record<typeof type, string> = {
|
||||
ResetTurnTime: `${cost} 포인트로 턴을 초기화 하시겠습니까?`,
|
||||
ResetSpecialWar: `${cost} 포인트로 전투 특기를 초기화 하시겠습니까?`,
|
||||
BuyRandomUnique: `${cost} 포인트로 랜덤 유니크를 구입하시겠습니까?`,
|
||||
};
|
||||
if (items.value.previous < cost) {
|
||||
alert("유산 포인트가 부족합니다.");
|
||||
return;
|
||||
}
|
||||
if (!confirm(messageMap[type])) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await SammoAPI.InheritAction[type]();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
alert("성공했습니다.");
|
||||
//TODO: 페이지 새로고침 필요없이 하도록
|
||||
location.reload();
|
||||
}
|
||||
async function setNextSpecialWar() {
|
||||
const specialWarName = availableSpecialWar[nextSpecialWar.value].title ?? undefined;
|
||||
if (specialWarName === undefined) {
|
||||
alert(`잘못된 타입: ${nextSpecialWar.value}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const cost = inheritActionCost.nextSpecial;
|
||||
if (items.value.previous < cost) {
|
||||
alert("유산 포인트가 부족합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const josaRo = JosaUtil.pick(specialWarName, "로");
|
||||
if (!confirm(`${cost} 포인트로 다음 전특을 ${specialWarName}${josaRo} 고정하겠습니까?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await SammoAPI.InheritAction.SetNextSpecialWar({
|
||||
type: nextSpecialWar.value,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
alert("성공했습니다.");
|
||||
//TODO: 페이지 새로고침 필요없이 하도록
|
||||
location.reload();
|
||||
}
|
||||
async function openUniqueItemAuction() {
|
||||
if (specificUnique.value === null) {
|
||||
alert("유니크를 선택해주세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
const uniqueName = availableUnique[specificUnique.value].title ?? undefined;
|
||||
if (uniqueName === undefined) {
|
||||
alert(`잘못된 타입: ${specificUnique.value}`);
|
||||
return;
|
||||
}
|
||||
const uniqueRawName = availableUnique[specificUnique.value].rawName ?? undefined;
|
||||
|
||||
const amount = specificUniqueAmount.value;
|
||||
if (items.value.previous < amount) {
|
||||
alert("유산 포인트가 부족합니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
const josaUl = JosaUtil.pick(uniqueRawName, "을");
|
||||
if (!confirm(`${amount} 포인트로 ${uniqueName}${josaUl} 입찰하겠습니까?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await SammoAPI.Auction.OpenUniqueAuction({
|
||||
itemID: specificUnique.value,
|
||||
amount,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
alert("성공했습니다. 경매장을 확인해주세요.");
|
||||
//TODO: 페이지 새로고침 필요없이 하도록
|
||||
location.reload();
|
||||
}
|
||||
|
||||
async function getMoreLog(): Promise<void>{
|
||||
try{
|
||||
const result = await SammoAPI.InheritAction.GetMoreLog({
|
||||
lastID: lastLogID.value
|
||||
});
|
||||
for(const log of result.log){
|
||||
inheritPointLogs.value.set(log.id, log);
|
||||
lastLogID.value = Math.min(lastLogID.value, log.id);
|
||||
}
|
||||
}catch(e){
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.col-form-label {
|
||||
text-align: right;
|
||||
padding-right: 2ch;
|
||||
}
|
||||
|
||||
.inherit_value {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.tnum {
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,636 @@
|
||||
<template>
|
||||
<TopBackBar title="장수 생성" />
|
||||
|
||||
<div v-if="gameConstStore && args" id="container" class="bg0">
|
||||
<div class="nation-list">
|
||||
<div class="nation-header nation-row bg1 center">
|
||||
<div>국가명</div>
|
||||
<div>임관권유문</div>
|
||||
<div class="display-toggle d-grid">
|
||||
<b-button
|
||||
v-model="displayTable"
|
||||
:pressed="displayTable"
|
||||
:variant="displayTable ? 'info' : 'secondary'"
|
||||
@click="displayTable = !displayTable"
|
||||
>
|
||||
{{ displayTable ? "숨기기" : "보이기" }}
|
||||
</b-button>
|
||||
</div>
|
||||
<div class="zoom-toggle d-grid">
|
||||
<b-button
|
||||
v-model="toggleZoom"
|
||||
:pressed="toggleZoom"
|
||||
:variant="toggleZoom ? 'info' : 'secondary'"
|
||||
:disabled="!displayTable"
|
||||
@click="toggleZoom = !toggleZoom"
|
||||
>
|
||||
{{ toggleZoom ? "작게 보기" : "크게 보기" }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="displayTable">
|
||||
<div
|
||||
v-for="nation in nationList"
|
||||
:key="nation.nation"
|
||||
:class="['nation-row', 's-border-b', toggleZoom ? 'on-zoom' : 'on-fit']"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
backgroundColor: nation.color,
|
||||
color: isBrightColor(nation.color) ? 'black' : 'white',
|
||||
fontSize: '1.3em',
|
||||
}"
|
||||
class="d-grid"
|
||||
>
|
||||
<div class="align-self-center center">
|
||||
{{ nation.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="nation-scout-plate align-self-center">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div class="nation-scout-msg" v-html="nation.scoutmsg ?? '-'" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 국가 설명 -->
|
||||
<div class="row bg1">
|
||||
<div class="col center">장수 생성</div>
|
||||
</div>
|
||||
<div class="forms">
|
||||
<div class="row">
|
||||
<div class="col col-md-4 col-3 a-right align-self-center">장수명</div>
|
||||
<div class="col col-md-3 col-9 align-self-center">
|
||||
<input v-model="args.name" class="form-control" />
|
||||
</div>
|
||||
<div class="col col-md-1 col-3 a-right align-self-center">전콘 사용</div>
|
||||
<div class="col col-md-4 col-9 align-self-center">
|
||||
<img style="height: 64px; width: 64px" :src="iconPath" />
|
||||
<label> <input v-model="args.pic" type="checkbox" /> 사용 </label>
|
||||
</div>
|
||||
<div class="col col-md-4 col-3 align-self-center a-right">성격</div>
|
||||
|
||||
<div class="col col-md-8 col-9 align-self-center">
|
||||
<div class="row">
|
||||
<div class="col col-md-3 col-4 align-self-center">
|
||||
<select v-model="args.character" class="form-select form-inline" style="max-width: 20ch">
|
||||
<option v-for="(personalityObj, key) in availablePersonality" :key="key" :value="key">
|
||||
{{ personalityObj.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col col-md-9 col-8 align-self-center">
|
||||
<small class="text-muted">
|
||||
{{ availablePersonality[args.character].info }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--<div class="row">
|
||||
<div class="col">
|
||||
계정관리에서 자신만을 표현할 수 있는 아이콘을 업로드 해보세요!
|
||||
</div>
|
||||
</div>-->
|
||||
<div class="row" style="margin-top: 1em">
|
||||
<div class="col col-md-4 col-3 a-right align-self-center">
|
||||
능력치
|
||||
<br />
|
||||
<small class="text-muted">통/무/지</small>
|
||||
</div>
|
||||
<div class="col col-md-2 col-3 align-self-center">
|
||||
<input v-model.number="args.leadership" type="number" class="form-control" />
|
||||
</div>
|
||||
<div class="col col-md-2 col-3 align-self-center">
|
||||
<input v-model.number="args.strength" type="number" class="form-control" />
|
||||
</div>
|
||||
<div class="col col-md-2 col-3 align-self-center">
|
||||
<input v-model.number="args.intel" type="number" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-top: 1em">
|
||||
<div class="col col-md-4 col-3 a-right align-self-center">능력치 조절</div>
|
||||
<div class="col col-md-8 col-9">
|
||||
<b-button variant="secondary" class="stat-btn" @click="randStatRandom"> 랜덤형 </b-button>
|
||||
<b-button variant="secondary" class="stat-btn" @click="randStatLeadPow"> 통솔무력형 </b-button>
|
||||
<b-button variant="secondary" class="stat-btn" @click="randStatLeadInt"> 통솔지력형 </b-button>
|
||||
<b-button variant="secondary" class="stat-btn" @click="randStatPowInt"> 무력지력형 </b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="border-top: solid 1px #aaa; margin-top: 0.5em">
|
||||
<div class="col a-center" style="color: orange">
|
||||
모든 능력치는 ( {{ stats.min }} <= 능력치 <= {{ stats.max }} ) 사이로 잡으셔야 합니다. <br />그 외의
|
||||
능력치는 가입되지 않습니다.
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col a-center">
|
||||
능력치의 총합은 {{ stats.total }} 입니다. 가입후 {{ stats.bonusMin }} ~ {{ stats.bonusMax }} 의 능력치 보너스를
|
||||
받게 됩니다. <br />임의의 도시에서 재야로 시작하며 건국과 임관은 게임 내에서 실행합니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row bg1">
|
||||
<div class="col col-md-11 col-9 center align-self-center">유산 포인트 사용</div>
|
||||
<div class="col col-md-1 col-3">
|
||||
<label>
|
||||
<input v-model="displayInherit" type="checkbox" />
|
||||
{{ displayInherit ? "숨기기" : "보이기" }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="displayInherit" class="inherit-block">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<NumberInputWithInfo v-model.number="inheritTotalPoint" title="보유한 유산 포인트" :readonly="true" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo v-model.number="inheritRequiredPoint" title="필요 유산 포인트" :readonly="true" />
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col col-md-6 col-sm-6 col-12 p-2 align-self-center">
|
||||
<div class="row">
|
||||
<div class="col col-6 a-right align-self-center">천재로 생성</div>
|
||||
<div class="col col-6 align-self-center">
|
||||
<select v-model="args.inheritSpecial" class="form-select form-inline" style="max-width: 20ch">
|
||||
<option :value="undefined">사용안함</option>
|
||||
<option v-for="(inheritSpecial, key) in availableInheritSpecial" :key="key" :value="key">
|
||||
{{ inheritSpecial.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col align-self-center">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<small class="text-muted" v-html="availableInheritSpecial[args.inheritSpecial ?? '']?.info" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col col-md-6 col-sm-6 col-12 p-2 align-self-center">
|
||||
<div class="row">
|
||||
<div class="col col-6 a-right align-self-center">도시</div>
|
||||
<div class="col col-6 align-self-center">
|
||||
<select v-model.number="inheritCity" class="form-select form-inline" style="max-width: 20ch">
|
||||
<option :value="undefined">사용안함</option>
|
||||
<option v-for="city in availableInheritCity" :key="city.id" :value="city.id">
|
||||
{{ `[${city.region}] ${city.name}` }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col col-md-6 col-sm-6 col-12 p-2 align-self-center">
|
||||
<div class="a-center">
|
||||
<label> <input v-model="inheritTurnTimeSet" type="checkbox" />턴 시간 고정 </label>
|
||||
</div>
|
||||
<div class="row turn_time_pad">
|
||||
<div class="col col-md-4 offset-md-3 col-4 offset-3">
|
||||
<NumberInputWithInfo
|
||||
v-model="inheritTurnTimeMinute"
|
||||
:readonly="!inheritTurnTimeSet"
|
||||
:min="0"
|
||||
:max="1 - turnterm"
|
||||
:right="true"
|
||||
title="분"
|
||||
/>
|
||||
</div>
|
||||
<div class="col col-md-4 col-4">
|
||||
<NumberInputWithInfo
|
||||
v-model="inheritTurnTimeSecond"
|
||||
:readonly="!inheritTurnTimeSet"
|
||||
:min="0"
|
||||
:max="60"
|
||||
:right="true"
|
||||
title="초"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col col-md-6 col-12 p-2">
|
||||
<div class="a-center">추가 능력치 고정</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<NumberInputWithInfo
|
||||
v-model="(args.inheritBonusStat ?? [0, 0, 0])[0]"
|
||||
:max="stats.bonusMax"
|
||||
title="통솔"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo
|
||||
v-model="(args.inheritBonusStat ?? [0, 0, 0])[1]"
|
||||
:max="stats.bonusMax"
|
||||
title="무력"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo
|
||||
v-model="(args.inheritBonusStat ?? [0, 0, 0])[2]"
|
||||
:max="stats.bonusMax"
|
||||
title="지력"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="border-top: solid 1px #aaa">
|
||||
<div class="col a-center" style="margin: 0.5em">
|
||||
<b-button color="primary" @click="submitForm"> 장수 생성 </b-button>
|
||||
<b-button color="secondary" @click="resetArgs"> 다시 입력 </b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
declare const staticValues: {
|
||||
nationList: {
|
||||
nation: number;
|
||||
name: string;
|
||||
color: string;
|
||||
scout: number;
|
||||
scoutmsg?: string;
|
||||
}[];
|
||||
turnterm: number;
|
||||
|
||||
member: {
|
||||
name: string;
|
||||
grade: number;
|
||||
picture: string;
|
||||
imgsvr: 0 | 1;
|
||||
};
|
||||
|
||||
serverID: string;
|
||||
inheritTotalPoint: number;
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import "@scss/common/bootstrap5.scss";
|
||||
import "@scss/game_bg.scss";
|
||||
|
||||
import TopBackBar from "@/components/TopBackBar.vue";
|
||||
import { getIconPath } from "@util/getIconPath";
|
||||
import { isBrightColor } from "@util/isBrightColor";
|
||||
import { abilityLeadint, abilityLeadpow, abilityPowint, abilityRand } from "@util/generalStats";
|
||||
import { shuffle, sum } from "lodash";
|
||||
import NumberInputWithInfo from "@/components/NumberInputWithInfo.vue";
|
||||
import { SammoAPI } from "./SammoAPI";
|
||||
import type { JoinArgs } from "./defs/API/General";
|
||||
import { GameConstStore, getGameConstStore } from "@/GameConstStore";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import type { CityID, GameIActionInfo } from "./defs/GameObj";
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
|
||||
const gameConstStore = ref<GameConstStore>();
|
||||
|
||||
const { serverID, member, turnterm } = staticValues;
|
||||
|
||||
const nationList = ref(shuffle(staticValues.nationList));
|
||||
const args = ref<JoinArgs>();
|
||||
|
||||
const stats = ref({
|
||||
min: 50,
|
||||
max: 50,
|
||||
total: 50,
|
||||
bonusMin: 1,
|
||||
bonusMax: 5,
|
||||
});
|
||||
|
||||
const inheritTotalPoint = ref(staticValues.inheritTotalPoint);
|
||||
const availableInheritCity = ref<{ id: CityID; name: string; region: string }[]>([]);
|
||||
const availableInheritSpecial = ref<Record<string, GameIActionInfo>>({});
|
||||
const availablePersonality = ref<Record<string, GameIActionInfo>>({});
|
||||
|
||||
watch(gameConstStore, (gameConst) => {
|
||||
if (gameConst === undefined) {
|
||||
console.error();
|
||||
return;
|
||||
}
|
||||
|
||||
availableInheritCity.value = (() => {
|
||||
const cityList: { id: CityID; name: string; region: string }[] = [];
|
||||
for (const city of Object.values(gameConst.cityConst)) {
|
||||
cityList.push({
|
||||
id: city.id,
|
||||
name: city.name,
|
||||
region: gameConst.cityConstMap.region[city.region] as string,
|
||||
});
|
||||
}
|
||||
cityList.sort((lhs, rhs) => {
|
||||
const rC = lhs.region.localeCompare(rhs.region);
|
||||
if (rC != 0) return rC;
|
||||
return lhs.name.localeCompare(rhs.name);
|
||||
});
|
||||
return cityList;
|
||||
})();
|
||||
|
||||
availableInheritSpecial.value = (() => {
|
||||
const specialList: typeof availableInheritSpecial.value = {};
|
||||
for (const specialWarID of Object.values(gameConst.gameConst.availableSpecialWar)) {
|
||||
specialList[specialWarID] = unwrap(gameConst.iActionInfo.specialWar[specialWarID]);
|
||||
}
|
||||
return specialList;
|
||||
})();
|
||||
|
||||
availablePersonality.value = (() => {
|
||||
const personalityList: typeof availablePersonality.value = {};
|
||||
for (const personalityID of Object.values(gameConst.gameConst.availablePersonality)) {
|
||||
personalityList[personalityID] = unwrap(gameConst.iActionInfo.personality[personalityID]);
|
||||
}
|
||||
personalityList["Random"] = {
|
||||
value: "Random",
|
||||
name: "???",
|
||||
info: "무작위 성격을 선택합니다.",
|
||||
};
|
||||
return personalityList;
|
||||
})();
|
||||
|
||||
stats.value = {
|
||||
min: gameConst.gameConst.defaultStatMin,
|
||||
max: gameConst.gameConst.defaultStatMax,
|
||||
total: gameConst.gameConst.defaultStatTotal,
|
||||
bonusMin: gameConst.gameConst.defaultStatMin,
|
||||
bonusMax: gameConst.gameConst.defaultStatMax,
|
||||
};
|
||||
|
||||
args.value = {
|
||||
name: member.name,
|
||||
leadership: stats.value.total - 2 * Math.floor(stats.value.total / 3),
|
||||
strength: Math.floor(stats.value.total / 3),
|
||||
intel: Math.floor(stats.value.total / 3),
|
||||
pic: true,
|
||||
character: "Random",
|
||||
|
||||
inheritCity: undefined,
|
||||
inheritBonusStat: [0, 0, 0],
|
||||
inheritSpecial: undefined,
|
||||
inheritTurntime: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
gameConstStore.value = await getGameConstStore();
|
||||
});
|
||||
|
||||
function randStatRandom() {
|
||||
if (args.value === undefined) throw "nyc";
|
||||
[args.value.leadership, args.value.strength, args.value.intel] = abilityRand(stats.value);
|
||||
}
|
||||
function randStatLeadPow() {
|
||||
if (args.value === undefined) throw "nyc";
|
||||
[args.value.leadership, args.value.strength, args.value.intel] = abilityLeadpow(stats.value);
|
||||
}
|
||||
function randStatLeadInt() {
|
||||
if (args.value === undefined) throw "nyc";
|
||||
[args.value.leadership, args.value.strength, args.value.intel] = abilityLeadint(stats.value);
|
||||
}
|
||||
function randStatPowInt() {
|
||||
if (args.value === undefined) throw "nyc";
|
||||
[args.value.leadership, args.value.strength, args.value.intel] = abilityPowint(stats.value);
|
||||
}
|
||||
function resetArgs() {
|
||||
if (!args.value) throw "nyc";
|
||||
const defaultStatTotal = stats.value.total;
|
||||
args.value.name = member.name;
|
||||
args.value.pic = true;
|
||||
args.value.character = "Random";
|
||||
args.value.leadership = defaultStatTotal - 2 * Math.floor(defaultStatTotal / 3);
|
||||
args.value.strength = Math.floor(defaultStatTotal / 3);
|
||||
args.value.intel = Math.floor(defaultStatTotal / 3);
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (!args.value) throw "nyc";
|
||||
//검증은 언제 되어야 하는가?
|
||||
const subbmitArgs = args.value;
|
||||
const totalStat = subbmitArgs.leadership + subbmitArgs.strength + subbmitArgs.intel;
|
||||
const defaultStatTotal = stats.value.total;
|
||||
|
||||
if (totalStat < defaultStatTotal) {
|
||||
if (
|
||||
!confirm(
|
||||
`설정한 능력치가 ${totalStat}으로, 실제 최대치인 ${defaultStatTotal}보다 적습니다.\r\n그래도 진행할까요?`
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
await SammoAPI.General.Join(subbmitArgs);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
alert("정상적으로 생성되었습니다. \n위키와 팁/강좌 게시판을 꼭 읽어보세요!");
|
||||
location.href = "./";
|
||||
}
|
||||
|
||||
const iconPath = computed(() => {
|
||||
if (!args.value) throw "nyc";
|
||||
|
||||
if (args.value.pic) {
|
||||
return getIconPath(member.imgsvr, member.picture);
|
||||
}
|
||||
return getIconPath(0, "default.jpg");
|
||||
});
|
||||
|
||||
const inheritRequiredPoint = computed(() => {
|
||||
if (!args.value || !gameConstStore.value) throw "nyc";
|
||||
|
||||
let inheritRequiredPoint = 0;
|
||||
const gameConst = gameConstStore.value.gameConst;
|
||||
|
||||
if (args.value.inheritCity !== undefined) {
|
||||
inheritRequiredPoint += gameConst.inheritBornCityPoint;
|
||||
}
|
||||
if (args.value.inheritSpecial !== undefined) {
|
||||
inheritRequiredPoint += gameConst.inheritBornSpecialPoint;
|
||||
}
|
||||
if (args.value.inheritTurntime !== undefined) {
|
||||
inheritRequiredPoint += gameConst.inheritBornTurntimePoint;
|
||||
}
|
||||
if (args.value.inheritBonusStat !== undefined && sum(args.value.inheritBonusStat) != 0) {
|
||||
inheritRequiredPoint += gameConst.inheritBornStatPoint;
|
||||
}
|
||||
return inheritRequiredPoint;
|
||||
});
|
||||
|
||||
const toggleZoom = ref(true);
|
||||
|
||||
const displayTable = ref<boolean>(JSON.parse(localStorage.getItem(`conf.${serverID}.join.displayTable`) ?? "true"));
|
||||
watch(displayTable, (newValue: boolean) => {
|
||||
localStorage.setItem(`conf.${serverID}.join.displayTable`, JSON.stringify(newValue));
|
||||
});
|
||||
|
||||
const displayInherit = ref<boolean>(JSON.parse(localStorage.getItem(`conf.${serverID}.join.displayInherit`) ?? "true"));
|
||||
watch(displayInherit, (newValue: boolean) => {
|
||||
localStorage.setItem(`conf.${serverID}.join.displayInherit`, JSON.stringify(newValue));
|
||||
});
|
||||
|
||||
const inheritCity = ref<number>();
|
||||
watch(inheritCity, (newValue: undefined | number) => {
|
||||
if (!args.value) throw "nyc";
|
||||
|
||||
if (!newValue) {
|
||||
args.value.inheritCity = undefined;
|
||||
return;
|
||||
}
|
||||
args.value.inheritCity = inheritCity.value;
|
||||
});
|
||||
|
||||
const inheritTurnTimeSet = ref(false);
|
||||
watch(inheritTurnTimeSet, (newValue: boolean) => {
|
||||
if (!args.value) throw "nyc";
|
||||
|
||||
if (!newValue) {
|
||||
args.value.inheritTurntime = undefined;
|
||||
return;
|
||||
}
|
||||
args.value.inheritTurntime = inheritTurnTimeMinute.value * 60 + inheritTurnTimeSecond.value;
|
||||
});
|
||||
|
||||
watch(
|
||||
[inheritCity, inheritTurnTimeSet],
|
||||
([newInheritCity, newInheritTurnTimeSet], [oldInheritCity, oldInheritTurnTimeSet]) => {
|
||||
if (newInheritCity === undefined || newInheritTurnTimeSet === false) {
|
||||
return;
|
||||
}
|
||||
alert("도시와 턴 시간을 동시에 설정할 수 없습니다.");
|
||||
|
||||
if (newInheritCity !== oldInheritCity) {
|
||||
inheritCity.value = undefined;
|
||||
}
|
||||
if (newInheritTurnTimeSet !== oldInheritTurnTimeSet) {
|
||||
inheritTurnTimeSet.value = false;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const inheritTurnTimeMinute = ref(0);
|
||||
watch(inheritTurnTimeMinute, (newValue: number) => {
|
||||
if (!args.value) throw "nyc";
|
||||
if (!inheritTurnTimeSet.value) {
|
||||
args.value.inheritTurntime = undefined;
|
||||
return;
|
||||
}
|
||||
args.value.inheritTurntime = newValue * 60 + inheritTurnTimeSecond.value;
|
||||
});
|
||||
|
||||
const inheritTurnTimeSecond = ref(0);
|
||||
watch(inheritTurnTimeSecond, (newValue: number) => {
|
||||
if (!args.value) throw "nyc";
|
||||
|
||||
if (!inheritTurnTimeSet.value) {
|
||||
args.value.inheritTurntime = undefined;
|
||||
return;
|
||||
}
|
||||
args.value.inheritTurntime = inheritTurnTimeMinute.value * 60 + newValue;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "@scss/common/bootstrap5.scss";
|
||||
@import "@scss/editor_component.scss";
|
||||
|
||||
#container {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
margin: auto;
|
||||
border: solid 1px #888888;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.forms {
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.stat-btn {
|
||||
width: 8em;
|
||||
margin-right: 1px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.col-form-label {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.turn_time_pad .col-form-label {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@scss/common/break_500px.scss";
|
||||
|
||||
@include media-1000px {
|
||||
.nation-list .nation-row {
|
||||
display: grid;
|
||||
grid-template-columns: 130px 870px;
|
||||
}
|
||||
|
||||
.zoom-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.zoom-toggle > * {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-500px {
|
||||
.nation-list .nation-row.nation-header {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
|
||||
.display-toggle {
|
||||
grid-column: 2/3;
|
||||
grid-row: 1/3;
|
||||
}
|
||||
.zoom-toggle {
|
||||
grid-column: 3/4;
|
||||
grid-row: 1/3;
|
||||
}
|
||||
}
|
||||
|
||||
.nation-list .nation-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr minmax(1fr, calc(200px * 500 / 870));
|
||||
}
|
||||
|
||||
.on-fit {
|
||||
.nation-scout-plate {
|
||||
max-height: calc(200px * 500 / 870);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nation-scout-msg {
|
||||
width: 870px;
|
||||
transform-origin: 0px 0px;
|
||||
transform: scale(calc(500 / 870));
|
||||
}
|
||||
}
|
||||
|
||||
.on-zoom {
|
||||
.nation-scout-plate {
|
||||
max-height: 200px;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.nation-scout-msg {
|
||||
max-width: 870px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,719 @@
|
||||
<template>
|
||||
<top-back-bar :title="title" />
|
||||
<BContainer
|
||||
id="container"
|
||||
class="tb_layout bg0"
|
||||
:toast="{ root: true }"
|
||||
:style="{ maxWidth: '1000px', margin: 'auto', border: 'solid 1px #888888', padding: '0' }"
|
||||
>
|
||||
<div class="bg1 section_bar">국가 정책</div>
|
||||
<div class="px-3" style="text-align: right">
|
||||
<small class="form-text text-muted">
|
||||
최근 설정:
|
||||
{{ lastSetters.policy.setter ?? "-없음-" }}
|
||||
({{ lastSetters.policy.date ?? "설정 기록 없음" }})
|
||||
</small>
|
||||
</div>
|
||||
<div class="form_list row row-cols-md-2 row-cols-1">
|
||||
<div class="col">
|
||||
<NumberInputWithInfo v-model="nationPolicy.reqNationGold" :step="100" title="국가 권장 금">
|
||||
이보다 많으면 포상, 적으면 몰수/헌납합니다.(긴급포상 제외)
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo v-model="nationPolicy.reqNationRice" :step="100" title="국가 권장 쌀">
|
||||
이보다 많으면 포상, 적으면 몰수/헌납합니다.(긴급포상 제외)
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo v-model="nationPolicy.reqHumanWarUrgentGold" :step="100" title="유저전투장 긴급포상 금">
|
||||
유저장긴급포상시 이보다 금이 적은 장수에게 포상합니다.
|
||||
<br />
|
||||
0이면 보병 6회 징병({{ (defaultStatMax * 100).toLocaleString() }} * 6) 가능한 금을 기준으로 하며, 그 수치는
|
||||
현재 {{ zeroPolicy.reqHumanWarUrgentGold.toLocaleString() }}입니다.
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo v-model="nationPolicy.reqHumanWarUrgentRice" :step="100" title="유저전투장 긴급포상 쌀">
|
||||
유저장긴급포상시 이보다 쌀이 적은 장수에게 포상합니다.
|
||||
<br />
|
||||
0이면 기본 병종으로 {{ (defaultStatMax * 100).toLocaleString() }} * 6명 사살 가능한 쌀을 기준으로 하며, 그
|
||||
수치는 현재 {{ zeroPolicy.reqHumanWarUrgentRice.toLocaleString() }}입니다.
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo v-model="nationPolicy.reqHumanWarRecommandGold" :step="100" title="유저전투장 권장 금">
|
||||
유저전투장에게 주는 금입니다. 이보다 적으면 포상합니다.
|
||||
<br />
|
||||
0이면 유저전투장 긴급포상 금의 2배를 기준으로 하며, 그 수치는 현재
|
||||
{{ (calcPolicyValue("reqHumanWarUrgentGold") * 2).toLocaleString() }}입니다.
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo v-model="nationPolicy.reqHumanWarRecommandRice" :step="100" title="유저전투장 권장 쌀">
|
||||
유저전투장에게 주는 쌀입니다. 이보다 적으면 포상합니다.
|
||||
<br />
|
||||
0이면 유저전투장 긴급포상 쌀의 2배를 기준으로 하며, 그 수치는 현재
|
||||
{{ (calcPolicyValue("reqHumanWarUrgentRice") * 2).toLocaleString() }}입니다.
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo v-model="nationPolicy.reqHumanDevelGold" :step="100" title="유저내정장 권장 금">
|
||||
유저내정장에게 주는 금입니다. 이보다 적으면 포상합니다.
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo v-model="nationPolicy.reqHumanDevelRice" :step="100" title="유저내정장 권장 쌀">
|
||||
유저내정장에게 주는 쌀입니다. 이보다 적으면 포상합니다.
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo v-model="nationPolicy.reqNPCWarGold" :step="100" title="NPC전투장 권장 금">
|
||||
NPC전투장에게 주는 금입니다. 이보다 적으면 포상합니다.
|
||||
<br />
|
||||
0이면 기본 병종 4회({{ (defaultStatNPCMax * 100).toLocaleString() }}
|
||||
* 4) 징병비를 기준으로 하며, 그 수치는 현재
|
||||
{{ zeroPolicy.reqNPCWarGold.toLocaleString() }}입니다.
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo v-model="nationPolicy.reqNPCWarRice" :step="100" title="NPC전투장 권장 쌀">
|
||||
NPC전투장에게 주는 쌀입니다. 이보다 적으면 포상합니다.
|
||||
<br />
|
||||
0이면 기본 병종으로
|
||||
{{ (defaultStatNPCMax * 100).toLocaleString() }} * 4명 사살 가능한 쌀을 기준으로 하며, 그 수치는 현재
|
||||
{{ zeroPolicy.reqNPCWarRice.toLocaleString() }}입니다.
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo v-model="nationPolicy.reqNPCDevelGold" :step="100" title="NPC내정장 권장 금">
|
||||
NPC내정장에게 주는 금입니다. 이보다 5배 더 많다면 헌납합니다.
|
||||
<br />
|
||||
0이면 30턴 내정 가능한 금을 기준으로 하며, 그 수치는 현재
|
||||
{{ zeroPolicy.reqNPCDevelGold }}입니다.
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo v-model="nationPolicy.reqNPCDevelRice" :step="100" title="NPC내정장 권장 쌀">
|
||||
NPC내정장에게 주는 쌀입니다. 이보다 5배 더 많다면 헌납합니다.
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo
|
||||
v-model="nationPolicy.minimumResourceActionAmount"
|
||||
:step="100"
|
||||
:min="100"
|
||||
title="포상/몰수/헌납/삼/팜 최소 단위"
|
||||
>
|
||||
연산결과가 이 단위보다 적다면 수행하지 않습니다.
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo
|
||||
v-model="nationPolicy.maximumResourceActionAmount"
|
||||
:step="100"
|
||||
:min="100"
|
||||
title="포상/몰수/헌납/삼/팜 최대 단위"
|
||||
>
|
||||
연산결과가 이 단위보다 크다면, 이 값에 맞춥니다.
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo v-model="nationPolicy.minWarCrew" :step="50" title="최소 전투 가능 병력 수">
|
||||
이보다 적을 때에는 징병을 시도합니다.
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo
|
||||
v-model="nationPolicy.minNPCRecruitCityPopulation"
|
||||
:step="100"
|
||||
title="NPC 최소 징병 가능 인구 수"
|
||||
>
|
||||
도시의 인구가 이보다 낮으면 NPC는 도시에서 징병하지 않고 후방 워프합니다.
|
||||
<br />NPC의 최대 병력수보다 낮게 설정하면 제자리에서 정착장려를 합니다.
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo
|
||||
:modelValue="nationPolicy.safeRecruitCityPopulationRatio * 100"
|
||||
:step="0.5"
|
||||
:int="false"
|
||||
:min="0"
|
||||
:max="100"
|
||||
title="제자리 징병 허용 인구율(%)"
|
||||
@update:modelValue="nationPolicy.safeRecruitCityPopulationRatio = $event / 100"
|
||||
>
|
||||
전쟁 시 후방 발령, 후방 워프의 기준 인구입니다. 이보다 많다면 '충분하다'고 판단합니다.
|
||||
<br />NPC의 최대 병력수보다 낮게 설정하면 제자리에서 정착장려를 합니다.
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo v-model="nationPolicy.minNPCWarLeadership" :step="5" title="NPC 전투 참여 통솔 기준">
|
||||
이 수치보다 같거나 높으면 NPC전투장으로 분류됩니다.
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col">
|
||||
<NumberInputWithInfo
|
||||
v-model="nationPolicy.properWarTrainAtmos"
|
||||
:step="5"
|
||||
:min="20"
|
||||
:max="100"
|
||||
title="훈련/사기진작 목표치"
|
||||
>
|
||||
훈련/사기진작 기준치입니다. 이보다 같거나 높으면 출병합니다.
|
||||
</NumberInputWithInfo>
|
||||
</div>
|
||||
<!--allowNpcAttackCity는 현재 게임 내 비활성-->
|
||||
</div>
|
||||
<div style="padding: 0 8pt">
|
||||
<div class="alert alert-secondary">
|
||||
전투 부대는 작업중입니다(json양식: {부대번호:[시작도시번호(아국),도착도시번호(적군)],...})
|
||||
<br />후방 징병 부대는 작업중입니다(json양식: [부대번호,...]) <br />내정 부대는 작업중입니다(json양식:
|
||||
[부대번호,...])
|
||||
<input id="CombatForce" type="hidden" :value="JSON.stringify(nationPolicy.CombatForce)" data-type="json" />
|
||||
<input id="SupportForce" type="hidden" :value="JSON.stringify(nationPolicy.SupportForce)" data-type="json" />
|
||||
<input id="DevelopForce" type="hidden" :value="JSON.stringify(nationPolicy.DevelopForce)" data-type="json" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="control_bar" data-type="nationPolicy">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-dark reset_btn" @click="resetPolicy">초기값으로</button>
|
||||
<button type="button" class="btn btn-secondary revert_btn" @click="rollbackPolicy">이전값으로</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary submit_btn" @click="submitPolicy">설정</button>
|
||||
</div>
|
||||
<div class="row row-cols-md-2 row-cols-1 g-0">
|
||||
<div class="col half_section_left">
|
||||
<div class="bg1 section_bar">NPC 사령턴 우선순위</div>
|
||||
<div class="float-right px-3">
|
||||
<small class="form-text text-muted">
|
||||
최근 설정:
|
||||
{{ lastSetters.nation.setter ?? "-없음-" }}
|
||||
({{ lastSetters.nation.date ?? "설정 기록 없음" }})
|
||||
</small>
|
||||
</div>
|
||||
<div class="text-left px-2">
|
||||
<small class="text-muted">
|
||||
예턴이 없거나, 지정되어 있더라도 실패하면
|
||||
<br />아래 순위에 따라 사령턴을 시도합니다.
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
<div class="bg2 sub_bar">비활성</div>
|
||||
<draggable
|
||||
:list="chiefActionUnused"
|
||||
group="chiefAction"
|
||||
class="list-group col priority-list"
|
||||
itemKey="id"
|
||||
>
|
||||
<template #header>
|
||||
<div class="list-group-item list-group-item-dark"><비활성화 항목들></div>
|
||||
</template>
|
||||
<template #item="{ element }">
|
||||
<div class="list-group-item">
|
||||
<i class="bi bi-list" />
|
||||
{{ element.id }}
|
||||
<button
|
||||
v-b-tooltip.hover
|
||||
class="btn btn-sm float-right btn-secondary py-0 px-1"
|
||||
:title="actionHelpText[element.id]"
|
||||
>
|
||||
<i class="bi bi-question-lg" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="bg2 sub_bar">활성</div>
|
||||
<draggable
|
||||
:list="chiefActionPriority"
|
||||
group="chiefAction"
|
||||
class="list-group col priority-list"
|
||||
itemKey="id"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<div class="list-group-item">
|
||||
<i class="bi bi-list" />
|
||||
{{ element.id }}
|
||||
<button
|
||||
v-b-tooltip.hover
|
||||
class="btn btn-sm float-right btn-secondary py-0 px-1"
|
||||
:title="actionHelpText[element.id]"
|
||||
>
|
||||
<i class="bi bi-question-lg" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control_bar" data-type="nationPriority">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-dark reset_btn" @click="resetNationPriority">초기값으로</button>
|
||||
<button type="button" class="btn btn-secondary revert_btn" @click="rollbackNationPriority">
|
||||
이전값으로
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary submit_btn" @click="submitNationPriority">설정</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col half_section_right">
|
||||
<div class="bg1 section_bar">NPC 일반턴 우선순위</div>
|
||||
<div class="float-right px-3">
|
||||
<small class="form-text text-muted">
|
||||
최근 설정:
|
||||
{{ lastSetters.general.setter ?? "-없음-" }}
|
||||
({{ lastSetters.general.date ?? "설정 기록 없음" }})
|
||||
</small>
|
||||
</div>
|
||||
<div class="text-left px-2">
|
||||
<small class="text-muted">
|
||||
순위가 높은 것부터 시도합니다.
|
||||
<br />아무것도 실행할 수 없으면 물자조달이나 인재탐색을 합니다.
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<div class="row g-0">
|
||||
<div class="col">
|
||||
<div class="bg2 sub_bar">비활성</div>
|
||||
<draggable
|
||||
:list="generalActionUnused"
|
||||
group="generalAction"
|
||||
class="list-group col priority-list"
|
||||
itemKey="id"
|
||||
>
|
||||
<template #header>
|
||||
<div class="list-group-item list-group-item-dark"><비활성화 항목들></div>
|
||||
</template>
|
||||
<template #item="{ element }">
|
||||
<div class="list-group-item">
|
||||
<i class="bi bi-list" />
|
||||
{{ element.id }}
|
||||
<button
|
||||
v-b-tooltip.hover
|
||||
class="btn btn-sm float-right btn-secondary py-0 px-1"
|
||||
:title="actionHelpText[element.id]"
|
||||
>
|
||||
<i class="bi bi-question-lg" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="bg2 sub_bar">활성</div>
|
||||
<draggable
|
||||
:list="generalActionPriority"
|
||||
group="generalAction"
|
||||
class="list-group col priority-list"
|
||||
itemKey="id"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<div class="list-group-item">
|
||||
<i class="bi bi-list" />
|
||||
{{ element.id }}
|
||||
<button
|
||||
v-b-tooltip.hover
|
||||
class="btn btn-sm float-right btn-secondary py-0 px-1"
|
||||
:title="actionHelpText[element.id]"
|
||||
>
|
||||
<i class="bi bi-question-lg" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control_bar" data-type="generalPriority">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-dark reset_btn" @click="resetGeneralPriority">초기값으로</button>
|
||||
<button type="button" class="btn btn-secondary revert_btn" @click="rollbackGeneralPriority">
|
||||
이전값으로
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary submit_btn" @click="submitGeneralPriority">설정</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BContainer>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
type SetterInfo = {
|
||||
setter: string | null;
|
||||
date: string | null;
|
||||
};
|
||||
|
||||
declare const staticValues: {
|
||||
nationID: number;
|
||||
|
||||
defaultNationPolicy: NationPolicy;
|
||||
currentNationPolicy: NationPolicy;
|
||||
|
||||
zeroPolicy: NationPolicy;
|
||||
currentNationPriority: NPCChiefActions[];
|
||||
availableNationPriorityItems: NPCChiefActions[];
|
||||
defaultNationPriority: NPCChiefActions[];
|
||||
|
||||
currentGeneralActionPriority: NPCGeneralActions[];
|
||||
availableGeneralActionPriorityItems: NPCGeneralActions[];
|
||||
defaultGeneralActionPriority: NPCGeneralActions[];
|
||||
|
||||
defaultStatNPCMax: number;
|
||||
defaultStatMax: number;
|
||||
|
||||
lastSetters: {
|
||||
policy: SetterInfo;
|
||||
nation: SetterInfo;
|
||||
general: SetterInfo;
|
||||
};
|
||||
};
|
||||
</script>
|
||||
<script setup lang="ts">
|
||||
import "@scss/common/bootstrap5.scss";
|
||||
import "@scss/game_bg.scss";
|
||||
|
||||
import { ref } from "vue";
|
||||
import type { IDItem, InvalidResponse, NationPolicy, NPCChiefActions, NPCGeneralActions } from "@/defs";
|
||||
import NumberInputWithInfo from "@/components/NumberInputWithInfo.vue";
|
||||
import { cloneDeep, isEqual, isNumber, last } from "lodash";
|
||||
import { unwrap } from "@util/unwrap";
|
||||
import { convertFormData } from "@util/convertFormData";
|
||||
import axios from "axios";
|
||||
import { NPCPriorityBtnHelpMessage } from "@/helpTexts";
|
||||
import draggable from "vuedraggable";
|
||||
import TopBackBar from "@/components/TopBackBar.vue";
|
||||
import { convertIDArray } from "@util/convertIDArray";
|
||||
import { useToast, BContainer } from "bootstrap-vue-3";
|
||||
|
||||
const chiefActionPriority = ref<IDItem<NPCChiefActions>[]>([]);
|
||||
const chiefActionKeys = ref(new Set(staticValues.availableNationPriorityItems));
|
||||
for (const id of staticValues.currentNationPriority) {
|
||||
chiefActionPriority.value.push({ id });
|
||||
chiefActionKeys.value.delete(id);
|
||||
}
|
||||
const chiefActionUnused = ref<IDItem<NPCChiefActions>[]>(convertIDArray(chiefActionKeys.value));
|
||||
const chiefActionStack = cloneDeep([chiefActionPriority.value]);
|
||||
|
||||
const generalActionPriority = ref<IDItem<NPCGeneralActions>[]>([]);
|
||||
const generalActionKeys = ref(new Set(staticValues.availableGeneralActionPriorityItems));
|
||||
for (const id of staticValues.currentGeneralActionPriority) {
|
||||
generalActionPriority.value.push({ id });
|
||||
generalActionKeys.value.delete(id);
|
||||
}
|
||||
const generalActionUnused = ref<IDItem<NPCGeneralActions>[]>(convertIDArray(generalActionKeys.value));
|
||||
const generalActionStack = cloneDeep([generalActionPriority.value]);
|
||||
|
||||
const title = "NPC 정책";
|
||||
const lastSetters = ref(staticValues.lastSetters);
|
||||
const defaultStatMax = ref(staticValues.defaultStatMax);
|
||||
const defaultStatNPCMax = ref(staticValues.defaultStatNPCMax);
|
||||
const nationPolicy = ref(cloneDeep(staticValues.currentNationPolicy));
|
||||
const nationPolicyStack = [staticValues.currentNationPolicy];
|
||||
const zeroPolicy = ref(staticValues.zeroPolicy);
|
||||
const actionHelpText = ref(NPCPriorityBtnHelpMessage);
|
||||
|
||||
const toasts = unwrap(useToast());
|
||||
|
||||
function resetPolicy() {
|
||||
if (!confirm("초기 설정으로 되돌릴까요?")) {
|
||||
return;
|
||||
}
|
||||
nationPolicy.value = cloneDeep(staticValues.defaultNationPolicy);
|
||||
toasts.info({
|
||||
title: "초기화 완료",
|
||||
body: "서버 초기값을 적용했습니다.설정 버튼을 누르면 반영됩니다.",
|
||||
});
|
||||
}
|
||||
|
||||
function rollbackPolicy() {
|
||||
if (!confirm("이전 설정으로 되돌릴까요?")) {
|
||||
return;
|
||||
}
|
||||
let lastPolicy = unwrap(last(nationPolicyStack));
|
||||
while (nationPolicyStack.length > 1) {
|
||||
if (!isEqual(lastPolicy, nationPolicy.value)) {
|
||||
break;
|
||||
}
|
||||
nationPolicyStack.pop();
|
||||
lastPolicy = unwrap(last(nationPolicyStack));
|
||||
}
|
||||
nationPolicy.value = cloneDeep(lastPolicy);
|
||||
toasts.info({
|
||||
title: "되돌리기 완료",
|
||||
body: "이전 설정으로 되돌렸습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
async function submitPolicy() {
|
||||
if (!confirm("저장할까요?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: "j_set_npc_control.php",
|
||||
responseType: "json",
|
||||
method: "post",
|
||||
data: convertFormData({
|
||||
type: "nationPolicy",
|
||||
data: JSON.stringify(nationPolicy.value),
|
||||
}),
|
||||
});
|
||||
const result: InvalidResponse = response.data;
|
||||
if (!result.result) {
|
||||
throw result.reason;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: `설정하지 못했습니다: ${e}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
toasts.success({
|
||||
title: "적용 완료",
|
||||
body: "NPC 정책이 반영되었습니다.",
|
||||
});
|
||||
|
||||
const lastPolicy = unwrap(last(nationPolicyStack));
|
||||
if (!isEqual(lastPolicy, nationPolicy.value)) {
|
||||
nationPolicyStack.push(cloneDeep(nationPolicy.value));
|
||||
}
|
||||
}
|
||||
|
||||
function calcPolicyValue(title: keyof NationPolicy): number {
|
||||
if (!(title in nationPolicy.value)) {
|
||||
throw `${title}이 NationPolicy key가 아님`;
|
||||
}
|
||||
const policyValue = nationPolicy.value[title];
|
||||
if (!isNumber(policyValue)) {
|
||||
throw `${title}에 해당하는 값이 number가 아님`;
|
||||
}
|
||||
if (policyValue == 0) {
|
||||
return zeroPolicy.value[title] as number;
|
||||
}
|
||||
return policyValue;
|
||||
}
|
||||
|
||||
function resetNationPriority() {
|
||||
if (!confirm("초기 설정으로 되돌릴까요?")) {
|
||||
return;
|
||||
}
|
||||
chiefActionUnused.value = [];
|
||||
chiefActionPriority.value = staticValues.defaultNationPriority.map((id) => {
|
||||
return { id };
|
||||
});
|
||||
toasts.info({
|
||||
title: "초기화 완료",
|
||||
body: "서버 초기값을 적용했습니다.설정 버튼을 누르면 반영됩니다.",
|
||||
});
|
||||
}
|
||||
|
||||
function rollbackNationPriority() {
|
||||
if (!confirm("이전 설정으로 되돌릴까요?")) {
|
||||
return;
|
||||
}
|
||||
let lastActions = unwrap(last(chiefActionStack));
|
||||
while (chiefActionStack.length > 1) {
|
||||
if (!isEqual(lastActions, chiefActionPriority.value)) {
|
||||
break;
|
||||
}
|
||||
chiefActionStack.pop();
|
||||
lastActions = unwrap(last(chiefActionStack));
|
||||
}
|
||||
const chiefActionKeys = new Set(staticValues.availableNationPriorityItems);
|
||||
for (const { id } of lastActions) {
|
||||
if (chiefActionKeys.has(id)) {
|
||||
chiefActionKeys.delete(id);
|
||||
}
|
||||
}
|
||||
chiefActionPriority.value = cloneDeep(lastActions);
|
||||
chiefActionUnused.value = convertIDArray(chiefActionKeys);
|
||||
|
||||
toasts.info({
|
||||
title: "되돌리기 완료",
|
||||
body: "이전 설정으로 되돌렸습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
async function submitNationPriority() {
|
||||
if (!confirm("저장할까요?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: "j_set_npc_control.php",
|
||||
responseType: "json",
|
||||
method: "post",
|
||||
data: convertFormData({
|
||||
type: "nationPriority",
|
||||
data: JSON.stringify(chiefActionPriority.value.map(({ id }) => id)),
|
||||
}),
|
||||
});
|
||||
const result: InvalidResponse = response.data;
|
||||
if (!result.result) {
|
||||
throw result.reason;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: `설정하지 못했습니다: ${e}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
toasts.success({
|
||||
title: "적용 완료",
|
||||
body: "NPC 정책이 반영되었습니다.",
|
||||
});
|
||||
|
||||
const lastActions = unwrap(last(chiefActionStack));
|
||||
if (!isEqual(lastActions, chiefActionPriority.value)) {
|
||||
chiefActionStack.push(cloneDeep(chiefActionPriority.value));
|
||||
}
|
||||
}
|
||||
|
||||
function resetGeneralPriority() {
|
||||
if (!confirm("초기 설정으로 되돌릴까요?")) {
|
||||
return;
|
||||
}
|
||||
generalActionUnused.value = [];
|
||||
generalActionPriority.value = staticValues.defaultGeneralActionPriority.map((id) => {
|
||||
return { id };
|
||||
});
|
||||
toasts.info({
|
||||
title: "초기화 완료",
|
||||
body: "서버 초기값을 적용했습니다.설정 버튼을 누르면 반영됩니다.",
|
||||
});
|
||||
}
|
||||
|
||||
function rollbackGeneralPriority() {
|
||||
if (!confirm("이전 설정으로 되돌릴까요?")) {
|
||||
return;
|
||||
}
|
||||
let lastActions = unwrap(last(generalActionStack));
|
||||
while (generalActionStack.length > 1) {
|
||||
if (!isEqual(lastActions, generalActionPriority.value)) {
|
||||
break;
|
||||
}
|
||||
generalActionStack.pop();
|
||||
lastActions = unwrap(last(generalActionStack));
|
||||
}
|
||||
const generalActionKeys = new Set(staticValues.availableGeneralActionPriorityItems);
|
||||
for (const { id } of lastActions) {
|
||||
if (generalActionKeys.has(id)) {
|
||||
generalActionKeys.delete(id);
|
||||
}
|
||||
}
|
||||
generalActionPriority.value = cloneDeep(lastActions);
|
||||
generalActionUnused.value = convertIDArray(generalActionKeys);
|
||||
|
||||
toasts.info({
|
||||
title: "되돌리기 완료",
|
||||
body: "이전 설정으로 되돌렸습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
async function submitGeneralPriority() {
|
||||
if (!confirm("저장할까요?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: "j_set_npc_control.php",
|
||||
responseType: "json",
|
||||
method: "post",
|
||||
data: convertFormData({
|
||||
type: "generalPriority",
|
||||
data: JSON.stringify(generalActionPriority.value.map(({ id }) => id)),
|
||||
}),
|
||||
});
|
||||
const result: InvalidResponse = response.data;
|
||||
if (!result.result) {
|
||||
throw result.reason;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: `설정하지 못했습니다: ${e}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
toasts.success({
|
||||
title: "적용 완료",
|
||||
body: "NPC 정책이 반영되었습니다.",
|
||||
});
|
||||
|
||||
const lastActions = unwrap(last(generalActionStack));
|
||||
if (!isEqual(lastActions, generalActionPriority.value)) {
|
||||
generalActionStack.push(cloneDeep(generalActionPriority.value));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tooltip > .tooltip-inner {
|
||||
max-width: 350px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.form_list {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.sub_bar {
|
||||
text-align: center;
|
||||
border: 0.5px solid #aaa;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.priority-list {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.control_bar {
|
||||
padding: 0 8pt 8pt 8pt;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.control_bar .btn {
|
||||
margin-top: 8pt;
|
||||
}
|
||||
|
||||
.reset_btn {
|
||||
width: 15ch;
|
||||
}
|
||||
|
||||
.revert_btn {
|
||||
width: 15ch;
|
||||
}
|
||||
|
||||
.submit_btn {
|
||||
margin-left: 1em;
|
||||
width: 15ch;
|
||||
}
|
||||
|
||||
.half_section_left {
|
||||
border-right: 0.5px solid #aaa;
|
||||
}
|
||||
|
||||
.section_bar {
|
||||
text-align: center;
|
||||
border: 0.5px solid #aaa;
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<BContainer id="container" :toast="{ root: true }" class="pageNationBetting bg0">
|
||||
<TopBackBar :title="title" />
|
||||
<BettingDetail v-if="targetBettingID !== undefined" :bettingID="targetBettingID" @reqToast="addToast" />
|
||||
<div v-if="bettingList === undefined">로딩 중...</div>
|
||||
<div v-else class="bettingList">
|
||||
<div class="bg2">베팅 목록</div>
|
||||
<div
|
||||
v-for="info of Object.values(bettingList).reverse()"
|
||||
:key="info.id"
|
||||
class="bettingItem"
|
||||
@click="targetBettingID = info.id"
|
||||
>
|
||||
[{{ parseYearMonth(info.openYearMonth)[0] }}년 {{ parseYearMonth(info.openYearMonth)[1] }}월] {{ info.name }}
|
||||
<span v-if="info.finished">(종료)</span>
|
||||
<span v-else-if="(yearMonth ?? 0) <= info.closeYearMonth"
|
||||
>({{ parseYearMonth(info.closeYearMonth)[0] }}년 {{ parseYearMonth(info.closeYearMonth)[1] }}월까지)</span
|
||||
>
|
||||
<span v-else>(베팅 마감)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BottomBar />
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import TopBackBar from "@/components/TopBackBar.vue";
|
||||
import BottomBar from "@/components/BottomBar.vue";
|
||||
import type { ToastType } from "@/defs";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { SammoAPI } from "./SammoAPI";
|
||||
import { isString } from "lodash";
|
||||
import { parseYearMonth } from "@/util/parseYearMonth";
|
||||
import { joinYearMonth } from "./util/joinYearMonth";
|
||||
import BettingDetail from "@/components/BettingDetail.vue";
|
||||
import { BContainer, useToast } from "bootstrap-vue-3";
|
||||
import { unwrap } from "./util/unwrap";
|
||||
import type { BettingListResponse } from "./defs/API/Betting";
|
||||
|
||||
|
||||
|
||||
const toasts = unwrap(useToast());
|
||||
const year = ref<number>();
|
||||
const month = ref<number>();
|
||||
const yearMonth = ref<number>();
|
||||
const bettingList = ref<BettingListResponse["bettingList"]>();
|
||||
|
||||
const targetBettingID = ref<number>();
|
||||
|
||||
function addToast(msg: ToastType) {
|
||||
toasts.show(msg.content, msg.options);
|
||||
}
|
||||
|
||||
console.log("시작!");
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const result = await SammoAPI.Betting.GetBettingList({
|
||||
req: 'bettingNation'
|
||||
});
|
||||
year.value = result.year;
|
||||
month.value = result.month;
|
||||
yearMonth.value = joinYearMonth(result.year, result.month);
|
||||
bettingList.value = result.bettingList;
|
||||
console.log(result);
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
const title = "국가 베팅장";
|
||||
</script>
|
||||
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<BContainer id="container" :toast="{ root: true }" class="pageNationGeneral bg0">
|
||||
<TopBackBar :title="title" :reloadable="true" :teleport-zone="toolbarID" @reload="reload" />
|
||||
<!-- eslint-disable-next-line vue/max-attributes-per-line -->
|
||||
<GeneralList
|
||||
v-if="asyncReady"
|
||||
:list="generalList"
|
||||
:troops="troopList"
|
||||
:env="envVal"
|
||||
:toolbarID="toolbarID"
|
||||
role="pageNationGeneral"
|
||||
:height="'fill'"
|
||||
:availableGeneralClick="true"
|
||||
@generalClick="openBattleCenter"
|
||||
/>
|
||||
<!--<BottomBar />-->
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/*declare const staticValues: {
|
||||
serverNick: string,
|
||||
mapName: string,
|
||||
unitSet: string,
|
||||
};*/
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import TopBackBar from "@/components/TopBackBar.vue";
|
||||
//import BottomBar from "@/components/BottomBar.vue";
|
||||
import { BContainer } from "bootstrap-vue-3";
|
||||
import { onMounted, provide, ref } from "vue";
|
||||
import { SammoAPI } from "./SammoAPI";
|
||||
import { merge2DArrToObjectArr } from "./util/merge2DArrToObjectArr";
|
||||
import type { GeneralListItem, GeneralListResponse } from "./defs/API/Nation";
|
||||
import GeneralList from "./components/GeneralList.vue";
|
||||
import { type GameConstStore, getGameConstStore } from "./GameConstStore";
|
||||
|
||||
const generalList = ref<GeneralListItem[]>([]);
|
||||
const troopList = ref<Record<number, string>>({});
|
||||
const envVal = ref<GeneralListResponse["env"]>({
|
||||
year: 1,
|
||||
month: 1,
|
||||
turnterm: 1,
|
||||
turntime: "2022-02-22 22:22:22.22222",
|
||||
killturn: 80,
|
||||
});
|
||||
|
||||
const toolbarID = "toolbar-id";
|
||||
|
||||
const gameConstStore = ref<GameConstStore>();
|
||||
provide("gameConstStore", gameConstStore);
|
||||
|
||||
const asyncReady = ref(false);
|
||||
|
||||
const title = "세력 장수";
|
||||
|
||||
const storeP = getGameConstStore().then((store) => {
|
||||
gameConstStore.value = store;
|
||||
});
|
||||
|
||||
void Promise.all([storeP]).then(() => {
|
||||
asyncReady.value = true;
|
||||
});
|
||||
|
||||
async function reload() {
|
||||
try {
|
||||
const { column, list, permission, troops, env } = await SammoAPI.Nation.GeneralList();
|
||||
troopList.value = {};
|
||||
//XXX: 로직상 똑같은데....
|
||||
if (permission == 0) {
|
||||
const rawGeneralList = merge2DArrToObjectArr(column, list);
|
||||
generalList.value = rawGeneralList.map((v) => {
|
||||
return { permission, st0: true, st1: false, st2: false, ...v };
|
||||
});
|
||||
} else if (permission == 1) {
|
||||
const rawGeneralList = merge2DArrToObjectArr(column, list);
|
||||
generalList.value = rawGeneralList.map((v) => {
|
||||
return { permission, st0: true, st1: true, st2: false, ...v };
|
||||
});
|
||||
} else if ([2, 3, 4].includes(permission)) {
|
||||
const rawGeneralList = merge2DArrToObjectArr(column, list);
|
||||
generalList.value = rawGeneralList.map((v) => {
|
||||
return { permission, st0: true, st1: true, st2: true, ...v };
|
||||
});
|
||||
|
||||
for (const [troopLeader, troopName] of troops) {
|
||||
troopList.value[troopLeader] = troopName;
|
||||
}
|
||||
} else {
|
||||
throw `?? ${permission}`;
|
||||
}
|
||||
envVal.value = env;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function openBattleCenter(generalID: number){
|
||||
window.open(`b_battleCenter.php?gen=${generalID}`)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await reload();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pageNationGeneral {
|
||||
display: grid;
|
||||
//grid-template-rows: auto 1fr auto;
|
||||
grid-template-rows: auto 1fr;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,576 @@
|
||||
<template>
|
||||
<BContainer id="container" :toast="{ root: true }" class="pageNationStratFinan bg0">
|
||||
<TopBackBar title="내무부" />
|
||||
<div class="diplomacyTitle">외교관계</div>
|
||||
<div class="diplomacyTable">
|
||||
<div class="diplomacyHeader tRow bg1">
|
||||
<div>국가명</div>
|
||||
<div>국력</div>
|
||||
<div>장수</div>
|
||||
<div>속령</div>
|
||||
<div>상태</div>
|
||||
<div>기간</div>
|
||||
<div>종료 시점</div>
|
||||
</div>
|
||||
<div v-for="nation in nationsList" :key="nation.nation" :class="['diplomacyItem', 'tRow', 's-border-b']">
|
||||
<div :class="`sam-nation-bg-${nation.color.slice(1)}`">
|
||||
{{ nation.name }}
|
||||
</div>
|
||||
<div>{{ nation.power.toLocaleString() }}</div>
|
||||
<div>{{ nation.gennum.toLocaleString() }}</div>
|
||||
<div>{{ nation.cityCnt.toLocaleString() }}</div>
|
||||
<template v-if="nation.nation == nationID">
|
||||
<div>-</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
:style="{
|
||||
color: diplomacyStateInfo[nation.diplomacy.state].color ?? undefined,
|
||||
}"
|
||||
>
|
||||
{{ diplomacyStateInfo[nation.diplomacy.state].name }}
|
||||
</div>
|
||||
<div>
|
||||
{{ nation.diplomacy.term == 0 ? "-" : `${nation.diplomacy.term}개월` }}
|
||||
</div>
|
||||
<div
|
||||
v-for="([endYear, endMonth], _idx) in [
|
||||
parseYearMonth(joinYearMonth(year, month) + (nation.diplomacy.term ?? 0)),
|
||||
]"
|
||||
:key="_idx"
|
||||
>
|
||||
{{ nation.diplomacy.term == 0 ? "-" : `${endYear}년 ${endMonth}월` }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="noticeTitle">국가 방침 & 임관 권유 메시지</div>
|
||||
<div id="noticeForm">
|
||||
<div class="bg1" style="display: flex; justify-content: space-around">
|
||||
<div style="flex: 1 1 auto">국가 방침</div>
|
||||
<div>
|
||||
<b-button v-if="editable && !inEditNationMsg" @click="enableEditNationMsg"> 국가방침 수정 </b-button>
|
||||
<b-button v-if="editable && inEditNationMsg" @click="saveNationMsg"> 저장 </b-button>
|
||||
<b-button v-if="editable && inEditNationMsg" @click="rollbackNationMsg"> 취소 </b-button>
|
||||
</div>
|
||||
</div>
|
||||
<TipTap
|
||||
v-model="nationMsg"
|
||||
:editable="inEditNationMsg"
|
||||
@ready="trackNationMsgHeight"
|
||||
@update:modelValue="trackNationMsgHeight"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="scoutMsgForm">
|
||||
<div class="bg1" style="display: flex; justify-content: space-around">
|
||||
<div style="flex: 1 1 auto">임관 권유</div>
|
||||
<div>
|
||||
<b-button v-if="editable && !inEditScoutMsg" @click="enableEditScoutMsg"> 임관 권유문 수정 </b-button>
|
||||
<b-button v-if="editable && inEditScoutMsg" @click="saveScoutMsg"> 저장 </b-button>
|
||||
<b-button v-if="editable && inEditScoutMsg" @click="rollbackScoutMsg"> 취소 </b-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border-bottom: solid gray 0.5px">870px x 200px를 넘어서는 내용은 표시되지 않습니다.</div>
|
||||
<div style="width: 870px; margin-left: auto">
|
||||
<TipTap
|
||||
v-model="scoutMsg"
|
||||
:editable="inEditScoutMsg"
|
||||
@ready="trackScoutMsgHeight"
|
||||
@update:modelValue="trackScoutMsgHeight"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="financeTitle">예산&정책</div>
|
||||
<div class="row gx-0 center">
|
||||
<div class="col-6">
|
||||
<div class="row gx-0">
|
||||
<div class="col-12 bg2">자금 예산</div>
|
||||
<div class="col-4 bg1">현 재</div>
|
||||
<div class="col-8">
|
||||
{{ gold.toLocaleString() }}
|
||||
</div>
|
||||
<div class="col-4 bg1">단기수입</div>
|
||||
<div class="col-8">
|
||||
{{ income.gold.war.toLocaleString() }}
|
||||
</div>
|
||||
<div class="col-4 bg1">세 금</div>
|
||||
<div class="col-8">
|
||||
{{ Math.floor(incomeGoldCity).toLocaleString() }}
|
||||
</div>
|
||||
<div class="col-4 bg1">수입/지출</div>
|
||||
<div class="col-8">
|
||||
+{{ Math.floor(incomeGold).toLocaleString() }} /
|
||||
{{ Math.floor(-outcomeByBill).toLocaleString() }}
|
||||
</div>
|
||||
<div class="col-4 bg1">국고 예산</div>
|
||||
<div class="col-8">
|
||||
{{ Math.floor(gold + incomeGold - outcomeByBill).toLocaleString() }}
|
||||
({{ incomeGold >= outcomeByBill ? "+" : "" }}{{ Math.floor(incomeGold - outcomeByBill).toLocaleString() }})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="row gx-0">
|
||||
<div class="col-12 bg2">군량 예산</div>
|
||||
<div class="col-4 bg1">현 재</div>
|
||||
<div class="col-8">
|
||||
{{ rice.toLocaleString() }}
|
||||
</div>
|
||||
<div class="col-4 bg1">둔전수입</div>
|
||||
<div class="col-8">
|
||||
{{ Math.floor(incomeRiceWall).toLocaleString() }}
|
||||
</div>
|
||||
<div class="col-4 bg1">세 금</div>
|
||||
<div class="col-8">
|
||||
{{ Math.floor(incomeRiceCity).toLocaleString() }}
|
||||
</div>
|
||||
<div class="col-4 bg1">수입/지출</div>
|
||||
<div class="col-8">
|
||||
+{{ Math.floor(incomeRice).toLocaleString() }} /
|
||||
{{ Math.floor(-outcomeByBill).toLocaleString() }}
|
||||
</div>
|
||||
<div class="col-4 bg1">국고 예산</div>
|
||||
<div class="col-8">
|
||||
{{ Math.floor(rice + incomeRice - outcomeByBill).toLocaleString() }}
|
||||
({{ incomeRice >= outcomeByBill ? "+" : "" }}{{ Math.floor(incomeRice - outcomeByBill).toLocaleString() }})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="row gx-0">
|
||||
<div class="col-4 bg1 d-grid">
|
||||
<div class="align-self-center">세율 <span class="avoid-wrap">(5 ~ 30%)</span></div>
|
||||
</div>
|
||||
<div class="col-8 row gx-0">
|
||||
<div class="col-md-6 offset-md-3 align-self-center">
|
||||
<div class="input-group my-0">
|
||||
<input
|
||||
v-model="policy.rate"
|
||||
type="number"
|
||||
class="form-control py-1 f_tnum px-0 text-end"
|
||||
min="5"
|
||||
max="30"
|
||||
/><span class="input-group-text py-1 f_tnum">%</span
|
||||
><b-button v-if="editable" variant="primary" size="sm" @click="setRate"> 변경 </b-button
|
||||
><b-button v-if="editable" size="sm" @click="rollbackRate"> 취소 </b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="row gx-0">
|
||||
<div class="col-4 bg1 d-grid">
|
||||
<div class="align-self-center">지급률 <span class="avoid-wrap">(20 ~ 200%)</span></div>
|
||||
</div>
|
||||
<div class="col-8 row gx-0">
|
||||
<div class="col-md-6 offset-md-3 align-self-center">
|
||||
<div class="input-group my-0">
|
||||
<input
|
||||
v-model="policy.bill"
|
||||
type="number"
|
||||
class="form-control py-1 f_tnum px-0 text-end"
|
||||
min="20"
|
||||
max="200"
|
||||
/><span class="input-group-text py-1 f_tnum">%</span
|
||||
><b-button v-if="editable" variant="primary" size="sm" @click="setBill"> 변경 </b-button
|
||||
><b-button v-if="editable" size="sm" @click="rollbackBill"> 취소 </b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="row gx-0">
|
||||
<div class="col-4 bg1 d-grid">
|
||||
<div class="align-self-center">기밀 권한 <span class="avoid-wrap">(1 ~ 99년)</span></div>
|
||||
</div>
|
||||
<div class="col-8 row gx-0">
|
||||
<div class="col-md-6 offset-md-3 align-self-center">
|
||||
<div class="input-group my-0">
|
||||
<input
|
||||
v-model="policy.secretLimit"
|
||||
type="number"
|
||||
class="form-control py-1 f_tnum px-0 text-end"
|
||||
min="1"
|
||||
max="99"
|
||||
/><span class="input-group-text py-1 f_tnum">년</span
|
||||
><b-button v-if="editable" variant="primary" size="sm" @click="setSecretLimit"> 변경 </b-button
|
||||
><b-button v-if="editable" size="sm" @click="rollbackSecretLimit"> 취소 </b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 d-grid">
|
||||
<div class="row gx-0">
|
||||
<div class="col-4 bg1 d-grid">
|
||||
<div class="align-self-center">전쟁 금지 설정</div>
|
||||
</div>
|
||||
<div class="col-8 d-grid">
|
||||
<div class="align-self-center">
|
||||
{{ warSettingCnt.remain }} 회(월 +{{ warSettingCnt.inc }}회, 최대{{ warSettingCnt.max }}회)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3 col-md-4" />
|
||||
<div class="col-3 col-md-2 row gx-0">
|
||||
<div class="col-9 col-md-8 text-end p-2">전쟁 금지</div>
|
||||
<div class="col-3 col-md-4 py-2">
|
||||
<b-form-checkbox v-model="policy.blockWar" switch @change="setBlockWar" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3 col-md-2 row gx-0">
|
||||
<div class="col-9 col-md-8 text-end p-2">임관 금지</div>
|
||||
<div class="col-3 col-md-4 py-2">
|
||||
<b-form-checkbox v-model="policy.blockScout" switch @change="setBlockScout" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>추가 설정</div>
|
||||
<BottomBar title="내무부" />
|
||||
</BContainer>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import TipTap from "./components/TipTap.vue";
|
||||
import TopBackBar from "@/components/TopBackBar.vue";
|
||||
import BottomBar from "@/components/BottomBar.vue";
|
||||
import { computed, defineComponent, reactive, ref, toRefs } from "vue";
|
||||
import { isString } from "lodash";
|
||||
import { type diplomacyState, diplomacyStateInfo, type NationStaticItem } from "./defs";
|
||||
import { SammoAPI } from "./SammoAPI";
|
||||
import { joinYearMonth } from "@/util/joinYearMonth";
|
||||
import { parseYearMonth } from "@/util/parseYearMonth";
|
||||
import { useToast, BContainer } from "bootstrap-vue-3";
|
||||
import { unwrap } from "./util/unwrap";
|
||||
|
||||
type NationItem = NationStaticItem & {
|
||||
cityCnt: number;
|
||||
diplomacy: {
|
||||
state: diplomacyState;
|
||||
term: number | null;
|
||||
};
|
||||
};
|
||||
|
||||
declare const staticValues: {
|
||||
editable: boolean;
|
||||
nationMsg: string;
|
||||
scoutMsg: string;
|
||||
nationID: number;
|
||||
officerLevel: number;
|
||||
year: number;
|
||||
month: number;
|
||||
nationsList: Record<number, NationItem>;
|
||||
|
||||
gold: number;
|
||||
rice: number;
|
||||
income: {
|
||||
gold: {
|
||||
city: number;
|
||||
war: number;
|
||||
};
|
||||
rice: {
|
||||
city: number;
|
||||
wall: number;
|
||||
};
|
||||
};
|
||||
outcome: number;
|
||||
|
||||
policy: {
|
||||
rate: number;
|
||||
bill: number;
|
||||
secretLimit: number;
|
||||
blockScout: boolean;
|
||||
blockWar: boolean;
|
||||
};
|
||||
warSettingCnt: {
|
||||
remain: number;
|
||||
inc: number;
|
||||
max: number;
|
||||
};
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
TopBackBar,
|
||||
BottomBar,
|
||||
TipTap,
|
||||
BContainer,
|
||||
},
|
||||
setup() {
|
||||
const toasts = unwrap(useToast());
|
||||
const self = reactive(staticValues);
|
||||
|
||||
let oldNationMsg = staticValues.nationMsg;
|
||||
const inEditNationMsg = ref(false);
|
||||
|
||||
function enableEditNationMsg() {
|
||||
inEditNationMsg.value = true;
|
||||
}
|
||||
|
||||
function rollbackNationMsg() {
|
||||
inEditNationMsg.value = false;
|
||||
self.nationMsg = oldNationMsg;
|
||||
}
|
||||
|
||||
async function saveNationMsg() {
|
||||
const msg = self.nationMsg;
|
||||
try {
|
||||
await SammoAPI.Nation.SetNotice({
|
||||
msg,
|
||||
});
|
||||
oldNationMsg = msg;
|
||||
inEditNationMsg.value = false;
|
||||
toasts.info({
|
||||
title: "변경",
|
||||
body: "국가 방침을 변경했습니다.",
|
||||
});
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
let oldScoutMsg = staticValues.scoutMsg;
|
||||
const inEditScoutMsg = ref(false);
|
||||
|
||||
function enableEditScoutMsg() {
|
||||
inEditScoutMsg.value = true;
|
||||
}
|
||||
function rollbackScoutMsg() {
|
||||
inEditScoutMsg.value = false;
|
||||
self.scoutMsg = oldScoutMsg;
|
||||
}
|
||||
async function saveScoutMsg() {
|
||||
const msg = self.scoutMsg;
|
||||
try {
|
||||
await SammoAPI.Nation.SetScoutMsg({
|
||||
msg,
|
||||
});
|
||||
oldScoutMsg = msg;
|
||||
inEditScoutMsg.value = false;
|
||||
toasts.info({
|
||||
title: "변경",
|
||||
body: "임관 권유문을 변경했습니다.",
|
||||
});
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
const trackTiptapFormHeight = (target: string) => {
|
||||
let form: HTMLElement | null = null;
|
||||
let outerForm: HTMLElement | null = null;
|
||||
function handler() {
|
||||
if (!form) {
|
||||
form = document.querySelector(`${target} .ProseMirror`);
|
||||
}
|
||||
if (!outerForm) {
|
||||
outerForm = document.querySelector(`${target} .tiptap-editor`);
|
||||
}
|
||||
if (!form || !outerForm) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { height: clientHeight } = form.getBoundingClientRect();
|
||||
const { height: parentHeight } = outerForm.getBoundingClientRect();
|
||||
|
||||
if (parentHeight != clientHeight) {
|
||||
outerForm.style.height = `${clientHeight}px`;
|
||||
}
|
||||
}
|
||||
window.addEventListener("orientationchange", handler, true);
|
||||
|
||||
return handler;
|
||||
};
|
||||
|
||||
const incomeGoldCity = computed(() => {
|
||||
return (self.income.gold.city * self.policy.rate) / 100;
|
||||
});
|
||||
|
||||
const incomeGold = computed(() => {
|
||||
return incomeGoldCity.value + self.income.gold.war;
|
||||
});
|
||||
|
||||
const incomeRiceCity = computed(() => {
|
||||
return (self.income.rice.city * self.policy.rate) / 100;
|
||||
});
|
||||
|
||||
const incomeRiceWall = computed(() => {
|
||||
return (self.income.rice.wall * self.policy.rate) / 100;
|
||||
});
|
||||
|
||||
const incomeRice = computed(() => {
|
||||
return incomeRiceCity.value + incomeRiceWall.value;
|
||||
});
|
||||
|
||||
const outcomeByBill = computed(() => {
|
||||
return (self.outcome * self.policy.bill) / 100;
|
||||
});
|
||||
|
||||
let oldRate = staticValues.policy.rate;
|
||||
async function setRate() {
|
||||
const rate = self.policy.rate;
|
||||
try {
|
||||
await SammoAPI.Nation.SetRate({ amount: rate });
|
||||
oldRate = rate;
|
||||
toasts.info({
|
||||
title: "변경",
|
||||
body: "세율을 변경했습니다.",
|
||||
});
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
function rollbackRate() {
|
||||
self.policy.rate = oldRate;
|
||||
}
|
||||
|
||||
let oldBill = staticValues.policy.bill;
|
||||
async function setBill() {
|
||||
const bill = self.policy.bill;
|
||||
try {
|
||||
await SammoAPI.Nation.SetBill({ amount: bill });
|
||||
oldBill = bill;
|
||||
toasts.info({
|
||||
title: "변경",
|
||||
body: "지급률을 변경했습니다.",
|
||||
});
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
function rollbackBill() {
|
||||
self.policy.bill = oldBill;
|
||||
}
|
||||
|
||||
let oldSecretLimit = staticValues.policy.secretLimit;
|
||||
async function setSecretLimit() {
|
||||
const secretLimit = self.policy.secretLimit;
|
||||
try {
|
||||
await SammoAPI.Nation.SetSecretLimit({ amount: secretLimit });
|
||||
oldSecretLimit = secretLimit;
|
||||
toasts.info({
|
||||
title: "변경",
|
||||
body: "기밀 권한을 변경했습니다.",
|
||||
});
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
self.policy.secretLimit = oldSecretLimit;
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
function rollbackSecretLimit() {
|
||||
self.policy.secretLimit = oldSecretLimit;
|
||||
}
|
||||
|
||||
async function setBlockWar() {
|
||||
try {
|
||||
const result = await SammoAPI.Nation.SetBlockWar({ value: self.policy.blockWar });
|
||||
self.warSettingCnt.remain = result.availableCnt;
|
||||
toasts.info({
|
||||
title: "변경",
|
||||
body: "전쟁 금지 설정을 변경했습니다.",
|
||||
});
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
self.policy.blockWar = !self.policy.blockWar;
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function setBlockScout() {
|
||||
try {
|
||||
await SammoAPI.Nation.SetBlockScout({ value: self.policy.blockScout });
|
||||
toasts.info({
|
||||
title: "변경",
|
||||
body: "임관 설정을 변경했습니다.",
|
||||
});
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
self.policy.blockScout = !self.policy.blockScout;
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
toasts,
|
||||
|
||||
...toRefs(self),
|
||||
inEditNationMsg,
|
||||
inEditScoutMsg,
|
||||
enableEditNationMsg,
|
||||
rollbackNationMsg,
|
||||
saveNationMsg,
|
||||
enableEditScoutMsg,
|
||||
rollbackScoutMsg,
|
||||
saveScoutMsg,
|
||||
diplomacyStateInfo,
|
||||
joinYearMonth,
|
||||
parseYearMonth,
|
||||
|
||||
trackNationMsgHeight: trackTiptapFormHeight("#noticeForm"),
|
||||
trackScoutMsgHeight: trackTiptapFormHeight("#scoutMsgForm"),
|
||||
|
||||
incomeGoldCity,
|
||||
incomeGold,
|
||||
incomeRiceCity,
|
||||
incomeRiceWall,
|
||||
incomeRice,
|
||||
outcomeByBill,
|
||||
|
||||
setRate,
|
||||
rollbackRate,
|
||||
setBill,
|
||||
rollbackBill,
|
||||
setSecretLimit,
|
||||
rollbackSecretLimit,
|
||||
|
||||
setBlockWar,
|
||||
setBlockScout,
|
||||
};
|
||||
},
|
||||
methods: {},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,531 @@
|
||||
<template>
|
||||
<BContainer id="container" :toast="{ root: true }" class="pageVote bg0">
|
||||
<TopBackBar type="close" :reloadable="true" title="" @reload="reloadVote" />
|
||||
<div id="vote-title" class="bg2">설문 조사({{ voteReward }}금과 추첨으로 유니크템 증정!)</div>
|
||||
<table v-if="currentVote" id="vote-result">
|
||||
<colgroup>
|
||||
<col class="vote-idx" />
|
||||
<col class="vote-count" />
|
||||
<col class="vote-percent" />
|
||||
<col class="vote-option" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="3" class="text-end bg1">설문 제목</th>
|
||||
<th id="vote-detail-title">
|
||||
{{ currentVote.voteInfo.title }}
|
||||
<template v-if="currentVote.voteInfo.multipleOptions !== 1"
|
||||
>(
|
||||
{{
|
||||
currentVote.voteInfo.multipleOptions == 0
|
||||
? currentVote.voteInfo.options.length
|
||||
: currentVote.voteInfo.multipleOptions
|
||||
}}개 선택 가능 )</template
|
||||
>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(option, idx) in currentVote.voteInfo.options" :key="idx">
|
||||
<template v-if="canVote">
|
||||
<td v-if="currentVote.voteInfo.multipleOptions == 1" class="text-center">
|
||||
<input :id="`v-vote-${idx}`" v-model="mySinglePick" class="form-check-input" type="radio" :value="idx" />
|
||||
</td>
|
||||
<td v-else class="text-center">
|
||||
<input
|
||||
:id="`v-vote-${idx}`"
|
||||
v-model="myMultiPick"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
:value="idx"
|
||||
/>
|
||||
</td>
|
||||
</template>
|
||||
<td
|
||||
v-else
|
||||
class="text-end f_tnum"
|
||||
:style="{
|
||||
backgroundColor: formatVoteColor(idx),
|
||||
color: isBrightColor(formatVoteColor(idx)) ? '#000' : '#fff',
|
||||
}"
|
||||
>
|
||||
{{ idx + 1 }}.
|
||||
</td>
|
||||
|
||||
<td class="text-end f_tnum vote-count">
|
||||
<label :for="`v-vote-${idx}`">{{ voteDistribution[idx] }}명</label>
|
||||
</td>
|
||||
<td class="text-end f_tnum vote-percent">
|
||||
<label :for="`v-vote-${idx}`"
|
||||
>({{ ((voteDistribution[idx] / Math.max(1, voteTotal)) * 100).toFixed(1) }}%)</label
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<label :for="`v-vote-${idx}`">{{ option }}</label>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<template v-if="canVote">
|
||||
<td class="text-center">투표</td>
|
||||
<td colspan="2">
|
||||
<div class="d-grid"><BButton @click="submitVote">투표</BButton></div>
|
||||
</td>
|
||||
</template>
|
||||
<td v-else colspan="3" class="text-center">결산</td>
|
||||
<td>
|
||||
투표율: {{ voteTotal }} / {{ currentVote.userCnt }} (
|
||||
{{ ((voteTotal / Math.max(1, currentVote.userCnt)) * 100).toFixed(1) }}%)
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<form @submit.prevent="submitComment">
|
||||
<table v-if="currentVote" id="vote-comment">
|
||||
<colgroup>
|
||||
<col class="comment-idx" />
|
||||
<col class="comment-name" />
|
||||
<col class="comment-text" />
|
||||
<col class="comment-date" />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr class="bg1 text-center">
|
||||
<th>#</th>
|
||||
<th>
|
||||
<div class="row gx-0">
|
||||
<div class="col-12 col-md-6">국가명</div>
|
||||
<div class="col-12 col-md-6">장수명</div>
|
||||
</div>
|
||||
</th>
|
||||
<th>댓글</th>
|
||||
<th>일시</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(comment, idx) in currentVote.comments" :key="idx">
|
||||
<td class="comment-idx f_tnum">{{ idx + 1 }}.</td>
|
||||
<td class="comment-name">
|
||||
<div class="row gx-0">
|
||||
<div class="col-12 col-md-6">{{ comment.nationName }}</div>
|
||||
<div class="col-12 col-md-6">{{ comment.generalName }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ comment.text }}</td>
|
||||
<td class="comment-date f_tnum">{{ comment.date.substring(5, 5 + 5 + 1 + 5) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<div class="offset-md-6 d-grid"><BButton type="submit">댓글 달기</BButton></div>
|
||||
</td>
|
||||
<td colspan="2"><BFormInput v-model="myComment" /></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</form>
|
||||
<div id="vote-old-title" class="bg2">이전 설문 조사</div>
|
||||
<div id="vote-old-list">
|
||||
<div v-for="[voteID, voteInfo] of voteList" :key="voteID" class="vote-old-item">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href="#" @click.prevent="currentVoteID = voteID">{{ voteInfo.title }}</a> ({{ voteInfo.startDate }})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isVoteAdmin" id="vote-new-panel">
|
||||
<div class="row"><a href="#" @click.prevent="showNewVote = !showNewVote">새 설문 조사 열기</a></div>
|
||||
<template v-if="showNewVote">
|
||||
<div class="row gx-0">
|
||||
<div class="col-md-3">설문 제목</div>
|
||||
<div class="col-md-9"><BFormInput v-model="newVoteInfo.title" type="text" /></div>
|
||||
</div>
|
||||
<div class="row gx-0">
|
||||
<div class="col-md-3">설문 대상(엔터로 구분) ({{ newVoteInfo.options.length }}건)</div>
|
||||
<div class="col-md-9">
|
||||
<textarea v-model="newVoteOptionsText" class="form-control" :rows="newVoteOptionsLength + 1"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row gx-0">
|
||||
<div class="col-md-3">동시 응답 수(0=모두)</div>
|
||||
<div class="col-md-9">
|
||||
<BFormInput
|
||||
v-model.number="newVoteInfo.multipleOptions"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="newVoteInfo.options.length"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row gx-0">
|
||||
<div class="offset-8 col-4 offset-md-10 col-md-2 d-grid">
|
||||
<BButton @click="submitNewVote">제출</BButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<BottomBar type="close" />
|
||||
</BContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
declare const staticValues: {
|
||||
serverNick: string;
|
||||
serverID: string;
|
||||
isGameLoggedIn: boolean;
|
||||
isVoteAdmin: boolean;
|
||||
voteReward: number;
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import TopBackBar from "@/components/TopBackBar.vue";
|
||||
import BottomBar from "@/components/BottomBar.vue";
|
||||
import { BContainer, useToast, BButton, BFormInput } from "bootstrap-vue-3";
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
import { onMounted, reactive, ref, watch, computed } from "vue";
|
||||
import type { VoteInfo, VoteDetailResult } from "@/defs/API/Vote";
|
||||
import { SammoAPI } from "@/SammoAPI";
|
||||
import { isString, range, sum } from "lodash";
|
||||
import { formatTime } from "@/util/formatTime";
|
||||
import { isBrightColor } from "@/util/isBrightColor";
|
||||
import { formatVoteColor } from "@/utilGame/formatVoteColor";
|
||||
|
||||
const { isVoteAdmin, isGameLoggedIn, voteReward } = staticValues;
|
||||
const voteList = ref(new Map<number, VoteInfo>());
|
||||
|
||||
const toasts = unwrap(useToast());
|
||||
|
||||
const showNewVote = ref(false);
|
||||
|
||||
const voteTotal = ref(0);
|
||||
const voteDistribution = ref<Record<number, number>>({});
|
||||
|
||||
const currentVote = ref<VoteDetailResult>();
|
||||
watch(currentVote, (voteResult) => {
|
||||
if (!voteResult) {
|
||||
return;
|
||||
}
|
||||
const voteDist: Record<number, number> = {};
|
||||
for (const idx of range(voteResult.voteInfo.options.length)) {
|
||||
voteDist[idx] = 0;
|
||||
}
|
||||
voteTotal.value = sum(voteResult.votes.map(([, count]) => count));
|
||||
|
||||
for (const [selection, count] of voteResult.votes) {
|
||||
for (const idx of selection) {
|
||||
voteDist[idx] += count;
|
||||
}
|
||||
}
|
||||
|
||||
voteDistribution.value = voteDist;
|
||||
});
|
||||
|
||||
const canVote = computed(() => {
|
||||
if (!isGameLoggedIn) {
|
||||
return false;
|
||||
}
|
||||
if (!currentVote.value) {
|
||||
return false;
|
||||
}
|
||||
if (currentVote.value.myVote) {
|
||||
return false;
|
||||
}
|
||||
const endDate = currentVote.value.voteInfo.endDate;
|
||||
if (endDate) {
|
||||
const now = formatTime(new Date());
|
||||
if (now > endDate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const currentVoteID = ref<number>();
|
||||
watch(currentVoteID, async (voteID) => {
|
||||
if (voteID === undefined) {
|
||||
return;
|
||||
}
|
||||
await reloadVoteDetail(voteID);
|
||||
});
|
||||
|
||||
const mySinglePick = ref(0);
|
||||
const myMultiPick = ref<number[]>([]);
|
||||
watch(myMultiPick, (newPick, oldPick) => {
|
||||
if (currentVote.value === undefined) {
|
||||
return;
|
||||
}
|
||||
const multipleOptions = currentVote.value.voteInfo.multipleOptions;
|
||||
if (multipleOptions == 0) {
|
||||
return;
|
||||
}
|
||||
if (newPick.length <= multipleOptions) {
|
||||
return;
|
||||
}
|
||||
toasts.warning({
|
||||
title: "오류",
|
||||
body: `${multipleOptions}개까지만 선택할 수 있습니다.`,
|
||||
});
|
||||
myMultiPick.value = oldPick;
|
||||
});
|
||||
|
||||
async function submitVote() {
|
||||
if (currentVote.value === undefined) {
|
||||
return;
|
||||
}
|
||||
const selection = currentVote.value.voteInfo.multipleOptions !== 1 ? myMultiPick.value : [mySinglePick.value];
|
||||
|
||||
if (selection.length === 0) {
|
||||
toasts.danger({
|
||||
title: "오류",
|
||||
body: "선택한 항목이 없습니다.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const voteID = currentVote.value.voteInfo.id;
|
||||
|
||||
try {
|
||||
const result = await SammoAPI.Vote.Vote({
|
||||
voteID,
|
||||
selection,
|
||||
});
|
||||
|
||||
toasts.success({
|
||||
title: "성공",
|
||||
body: "설문을 마쳤습니다.",
|
||||
});
|
||||
if (result.wonLottery) {
|
||||
toasts.info({
|
||||
title: "성공",
|
||||
body: "특별한 설문 보상이 제공되었습니다!",
|
||||
});
|
||||
}
|
||||
void reloadVoteDetail(voteID);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const myComment = ref("");
|
||||
async function submitComment() {
|
||||
if (currentVote.value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const voteID = currentVote.value.voteInfo.id;
|
||||
const text = myComment.value;
|
||||
|
||||
try {
|
||||
await SammoAPI.Vote.AddComment({
|
||||
voteID,
|
||||
text,
|
||||
});
|
||||
|
||||
toasts.success({
|
||||
title: "성공",
|
||||
body: "댓글을 달았습니다.",
|
||||
});
|
||||
myComment.value = "";
|
||||
void reloadVoteDetail(voteID);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newVoteInfo = reactive<Omit<VoteInfo, "id" | "startDate">>({
|
||||
title: "",
|
||||
multipleOptions: 1,
|
||||
endDate: undefined,
|
||||
options: [],
|
||||
});
|
||||
|
||||
const newVoteOptionsLength = ref(0);
|
||||
const newVoteOptionsText = ref("");
|
||||
|
||||
watch(newVoteOptionsText, (newValue) => {
|
||||
const lines = newValue.split("\n");
|
||||
newVoteInfo.options = lines.filter((v) => v.length > 0);
|
||||
newVoteOptionsLength.value = lines.length;
|
||||
});
|
||||
|
||||
async function reloadVoteDetail(voteID: number) {
|
||||
try {
|
||||
const result = await SammoAPI.Vote.GetVoteDetail({ voteID });
|
||||
currentVote.value = result;
|
||||
|
||||
if (voteID !== currentVoteID.value) {
|
||||
myMultiPick.value = [];
|
||||
mySinglePick.value = 0;
|
||||
myComment.value = "";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadVote() {
|
||||
try {
|
||||
const result = await SammoAPI.Vote.GetVoteList();
|
||||
const newVoteList = new Map<number, VoteInfo>();
|
||||
for (const [rawVoteID, info] of Object.entries(result.votes).reverse()) {
|
||||
newVoteList.set(parseInt(rawVoteID), info);
|
||||
}
|
||||
voteList.value = newVoteList;
|
||||
if (newVoteList.size > 0 && currentVoteID.value === undefined) {
|
||||
currentVoteID.value = newVoteList.keys().next().value;
|
||||
} else if (currentVoteID.value !== undefined) {
|
||||
void reloadVoteDetail(currentVoteID.value);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(function () {
|
||||
void reloadVote();
|
||||
});
|
||||
|
||||
async function submitNewVote() {
|
||||
try {
|
||||
await SammoAPI.Vote.NewVote(newVoteInfo);
|
||||
toasts.success({
|
||||
title: "성공",
|
||||
body: "설문 조사가 생성되었습니다.",
|
||||
});
|
||||
void reloadVote();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@scss/common/break_500px.scss";
|
||||
|
||||
#vote-title {
|
||||
font-size: 1.8em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#vote-old-title {
|
||||
font-size: 1.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#vote-result {
|
||||
width: 100%;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding-left: 1ch;
|
||||
padding-right: 1ch;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.vote-idx {
|
||||
width: 5ch;
|
||||
}
|
||||
|
||||
.vote-count {
|
||||
width: 55px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.vote-percent {
|
||||
width: 70px;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#vote-comment {
|
||||
width: 100%;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding-left: 1ch;
|
||||
padding-right: 1ch;
|
||||
}
|
||||
|
||||
.comment-idx {
|
||||
width: 5ch;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.comment-name {
|
||||
width: 110px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
border-top: solid gray 1px;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-1000px {
|
||||
#vote-comment {
|
||||
.comment-name {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
width: 98px;
|
||||
padding-left: 0.5ch;
|
||||
padding-right: 0.5ch;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-500px {
|
||||
#vote-comment {
|
||||
.comment-name {
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.comment-date {
|
||||
width: 50px;
|
||||
padding-left: 0.5ch;
|
||||
padding-right: 0.5ch;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,895 @@
|
||||
<template>
|
||||
<div class="commandPad">
|
||||
<div class="col alert alert-dark m-0 p-1 center">
|
||||
<h4 class="m-0">명령 목록</h4>
|
||||
</div>
|
||||
<div class="row gx-1">
|
||||
<div class="col d-grid">
|
||||
<BButton variant="secondary" @click="isEditMode = !isEditMode">
|
||||
{{ isEditMode ? "일반 모드로" : "고급 모드로" }}
|
||||
</BButton>
|
||||
</div>
|
||||
<div
|
||||
class="col alert alert-primary m-0 p-0"
|
||||
style="text-align: center; display: flex; justify-content: center; align-items: center"
|
||||
>
|
||||
<SimpleClock :serverTime="serverNow" />
|
||||
</div>
|
||||
<div class="col d-grid">
|
||||
<BDropdown right text="반복">
|
||||
<BDropdownItem v-for="turnIdx in maxPushTurn" :key="turnIdx" @click="repeatGeneralCommand(turnIdx)">
|
||||
{{ turnIdx }}턴
|
||||
</BDropdownItem>
|
||||
</BDropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isEditMode" class="row gx-1">
|
||||
<div class="col-4 d-grid">
|
||||
<BDropdown left text="범위">
|
||||
<BDropdownItem @click="queryActionHelper.selectTurn()"> 해제 </BDropdownItem>
|
||||
<BDropdownItem @click="queryActionHelper.selectAll()"> 모든턴 </BDropdownItem>
|
||||
<BDropdownItem @click="queryActionHelper.selectStep(0, 2)"> 홀수턴 </BDropdownItem>
|
||||
<BDropdownItem @click="queryActionHelper.selectStep(1, 2)"> 짝수턴 </BDropdownItem>
|
||||
<BDropdownDivider />
|
||||
|
||||
<BDropdownText v-for="spanIdx in [3, 4, 5, 6, 7]" :key="spanIdx">
|
||||
{{ spanIdx }}턴 간격
|
||||
<br />
|
||||
<BButtonGroup>
|
||||
<BButton
|
||||
v-for="beginIdx in spanIdx"
|
||||
:key="beginIdx"
|
||||
class="ignoreMe"
|
||||
@click="queryActionHelper.selectStep(beginIdx - 1, spanIdx)"
|
||||
>
|
||||
{{ beginIdx }}
|
||||
</BButton>
|
||||
</BButtonGroup>
|
||||
</BDropdownText>
|
||||
</BDropdown>
|
||||
</div>
|
||||
|
||||
<div class="col-4 d-grid">
|
||||
<BDropdown left text="보관함">
|
||||
<BDropdownItem
|
||||
v-for="[actionKey, actions] of storedActions"
|
||||
:key="actionKey"
|
||||
@click.self="useStoredAction(actions)"
|
||||
>
|
||||
{{ actionKey }}
|
||||
<BButton size="sm" @click.prevent="deleteStoredActions(actionKey)"> 삭제 </BButton>
|
||||
</BDropdownItem>
|
||||
</BDropdown>
|
||||
</div>
|
||||
|
||||
<div class="col-4 d-grid">
|
||||
<BDropdown right text="최근 실행">
|
||||
<BDropdownItem
|
||||
v-for="(action, idx) of Array.from(recentActions.values()).reverse()"
|
||||
:key="idx"
|
||||
@click="void reserveCommandDirect([[queryActionHelper.getSelectedTurnList(), action]])"
|
||||
>
|
||||
{{ action.brief }}
|
||||
</BDropdownItem>
|
||||
</BDropdown>
|
||||
</div>
|
||||
|
||||
<div class="col-5 d-grid">
|
||||
<BDropdown left variant="light" :style="{ color: 'black' }" text="선택한 턴을">
|
||||
<BDropdownItem @click="clipboardCut"> <i class="bi bi-scissors" /> 잘라내기 </BDropdownItem>
|
||||
<BDropdownItem @click="clipboardCopy"> <i class="bi bi-files" /> 복사하기 </BDropdownItem>
|
||||
<BDropdownItem @click="clipboardPaste"> <i class="bi bi-clipboard-fill" /> 붙여넣기 </BDropdownItem>
|
||||
<BDropdownDivider />
|
||||
<BDropdownItem @click="setStoredActions">
|
||||
<i class="bi bi-bookmark-plus-fill" /> 보관하기
|
||||
</BDropdownItem>
|
||||
<BDropdownItem @click="subRepeatCommand"> <i class="bi bi-arrow-repeat" /> 반복하기 </BDropdownItem>
|
||||
<BDropdownDivider />
|
||||
<BDropdownItem @click="eraseSelectedTurnList"> <i class="bi bi-eraser" /> 비우기 </BDropdownItem>
|
||||
<BDropdownItem @click="eraseAndPullCommand">
|
||||
<i class="bi bi-arrow-bar-up" /> 지우고 당기기
|
||||
</BDropdownItem>
|
||||
<BDropdownItem @click="pushEmptyCommand"> <i class="bi bi-arrow-bar-down" /> 뒤로 밀기 </BDropdownItem>
|
||||
<!-- 최근에 실행한 10턴 -->
|
||||
</BDropdown>
|
||||
</div>
|
||||
|
||||
<div class="col-7 d-grid">
|
||||
<BButton variant="info" @click="toggleForm($event)"> 명령 선택 ▾ </BButton>
|
||||
</div>
|
||||
</div>
|
||||
<CommandSelectForm
|
||||
ref="commandSelectForm"
|
||||
v-model:activatedCategory="activatedCategory"
|
||||
:commandList="commandList"
|
||||
@onClose="chooseCommand($event)"
|
||||
/>
|
||||
|
||||
<div :style="{ position: 'relative' }">
|
||||
<div
|
||||
class="bg-dark"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
top: `${basicModeRowHeight * currentQuickReserveTarget + 30}px`,
|
||||
width: '100%',
|
||||
zIndex: 9,
|
||||
}"
|
||||
>
|
||||
<CommandSelectForm
|
||||
ref="commandQuickReserveForm"
|
||||
v-model:activatedCategory="activatedCategory"
|
||||
:commandList="commandList"
|
||||
:hideClose="false"
|
||||
@onClose="chooseQuickReserveCommand($event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:class="{
|
||||
commandTable: true,
|
||||
isEditMode,
|
||||
}"
|
||||
>
|
||||
<DragSelect
|
||||
v-slot="{ selected }"
|
||||
:style="rowGridStyle"
|
||||
:disabled="!isEditMode"
|
||||
attribute="turnIdx"
|
||||
@dragStart="isDragToggle = true"
|
||||
@dragDone="
|
||||
isDragToggle = false;
|
||||
queryActionHelper.toggleTurn(...$event);
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="(turnObj, turnIdx) in reservedCommandList.slice(0, viewMaxTurn)"
|
||||
:key="turnIdx"
|
||||
:turnIdx="turnIdx"
|
||||
class="idx_pad center d-grid"
|
||||
>
|
||||
<BButton
|
||||
v-if="isEditMode"
|
||||
size="sm"
|
||||
:variant="
|
||||
isDragToggle && selected.has(`${turnIdx}`)
|
||||
? 'light'
|
||||
: selectedTurnList.has(turnIdx)
|
||||
? 'info'
|
||||
: selectedTurnList.size == 0 && prevSelectedTurnList.has(turnIdx)
|
||||
? 'success'
|
||||
: 'primary'
|
||||
"
|
||||
>
|
||||
{{ turnIdx + 1 }}
|
||||
</BButton>
|
||||
<div v-else class="plain-center">
|
||||
{{ turnIdx + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
</DragSelect>
|
||||
<DragSelect
|
||||
v-slot="{ selected }"
|
||||
:style="rowGridStyle"
|
||||
attribute="turnIdx"
|
||||
:disabled="!isEditMode"
|
||||
@dragStart="isDragSingle = true"
|
||||
@dragDone="
|
||||
isDragSingle = false;
|
||||
queryActionHelper.selectTurn(...$event);
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="(turnObj, turnIdx) in reservedCommandList.slice(0, viewMaxTurn)"
|
||||
:key="turnIdx"
|
||||
height="24"
|
||||
class="month_pad center"
|
||||
:turnIdx="turnIdx"
|
||||
:style="{
|
||||
'white-space': 'nowrap',
|
||||
'font-size': `${Math.min(14, (75 / (`${turnObj.year ?? 1}`.length + 8)) * 1.8)}px`,
|
||||
overflow: 'hidden',
|
||||
color: isDragSingle && selected.has(`${turnIdx}`) ? 'cyan' : undefined,
|
||||
}"
|
||||
>
|
||||
{{ turnObj.year ? `${turnObj.year}年` : "" }}
|
||||
{{ turnObj.month ? `${turnObj.month}月` : "" }}
|
||||
</div>
|
||||
</DragSelect>
|
||||
<div :style="rowGridStyle">
|
||||
<div
|
||||
v-for="(turnObj, turnIdx) in reservedCommandList.slice(0, viewMaxTurn)"
|
||||
:key="turnIdx"
|
||||
class="time_pad center"
|
||||
:style="{
|
||||
backgroundColor: 'black',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
}"
|
||||
>
|
||||
{{ turnObj.time }}
|
||||
</div>
|
||||
</div>
|
||||
<div :style="rowGridStyle">
|
||||
<div
|
||||
v-for="(turnObj, turnIdx) in reservedCommandList.slice(0, viewMaxTurn)"
|
||||
:key="turnIdx"
|
||||
class="turn_pad center"
|
||||
>
|
||||
<span v-b-tooltip.hover class="turn_text" :style="turnObj.style" :title="turnObj.tooltip">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-html="turnObj.brief" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isEditMode" :style="rowGridStyle">
|
||||
<div
|
||||
v-for="(turnObj, turnIdx) in reservedCommandList.slice(0, viewMaxTurn)"
|
||||
:key="turnIdx"
|
||||
class="action_pad d-grid"
|
||||
>
|
||||
<BButton
|
||||
:variant="turnIdx % 2 == 0 ? 'secondary' : 'dark'"
|
||||
size="sm"
|
||||
class="simple_action_btn bi bi-pencil"
|
||||
@click="toggleQuickReserveForm(turnIdx)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row gx-1">
|
||||
<div class="col d-grid">
|
||||
<BDropdown :split="isEditMode" text="당기기" @click="pullGeneralCommandSingle">
|
||||
<BDropdownItem v-for="turnIdx in maxPushTurn" :key="turnIdx" @click="pushGeneralCommand(-turnIdx)">
|
||||
{{ turnIdx }}턴
|
||||
</BDropdownItem>
|
||||
</BDropdown>
|
||||
</div>
|
||||
<div class="col d-grid">
|
||||
<BDropdown :split="isEditMode" text="미루기" @click="pushGeneralCommandSingle">
|
||||
<BDropdownItem v-for="turnIdx in maxPushTurn" :key="turnIdx" @click="pushGeneralCommand(turnIdx)">
|
||||
{{ turnIdx }}턴
|
||||
</BDropdownItem>
|
||||
</BDropdown>
|
||||
</div>
|
||||
<div class="col d-grid">
|
||||
<BButton @click="toggleViewMaxTurn">
|
||||
{{ flippedMaxTurn == viewMaxTurn ? "펼치기" : "접기" }}
|
||||
</BButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
declare const staticValues: {
|
||||
maxTurn: number;
|
||||
maxPushTurn: number;
|
||||
commandList: {
|
||||
category: string;
|
||||
values: CommandItem[];
|
||||
}[];
|
||||
serverNow: string;
|
||||
serverNick: string;
|
||||
mapName: string;
|
||||
unitSet: string;
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import addMinutes from "date-fns/esm/addMinutes";
|
||||
import { range, trim } from "lodash";
|
||||
import { stringifyUrl } from "query-string";
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import { formatTime } from "@util/formatTime";
|
||||
import { joinYearMonth } from "@util/joinYearMonth";
|
||||
import { mb_strwidth } from "@util/mb_strwidth";
|
||||
import { parseTime } from "@util/parseTime";
|
||||
import { parseYearMonth } from "@util/parseYearMonth";
|
||||
import DragSelect from "@/components/DragSelect.vue";
|
||||
import { SammoAPI } from "./SammoAPI";
|
||||
import type { CommandItem } from "@/defs";
|
||||
import CommandSelectForm from "@/components/CommandSelectForm.vue";
|
||||
import { BButton, BButtonGroup, BDropdownItem, BDropdown, BDropdownText, BDropdownDivider } from "bootstrap-vue-3";
|
||||
import { StoredActionsHelper } from "./util/StoredActionsHelper";
|
||||
import type { TurnObj } from "@/defs";
|
||||
import type { Args } from "./processing/args";
|
||||
import { QueryActionHelper } from "./util/QueryActionHelper";
|
||||
import SimpleClock from "./components/SimpleClock.vue";
|
||||
import type { ReservedCommandResponse } from "./defs/API/Command";
|
||||
|
||||
const { maxTurn, maxPushTurn, commandList } = staticValues;
|
||||
|
||||
const listReqArgCommand = new Set<string>();
|
||||
const selectedCommand = ref(staticValues.commandList[0].values[0]);
|
||||
const commandSelectForm = ref<InstanceType<typeof CommandSelectForm> | null>(null);
|
||||
|
||||
for (const commandCategories of commandList) {
|
||||
if (!commandCategories.values) {
|
||||
continue;
|
||||
}
|
||||
for (const commandObj of commandCategories.values) {
|
||||
if (!commandObj.reqArg) {
|
||||
continue;
|
||||
}
|
||||
listReqArgCommand.add(commandObj.value);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleForm($event: Event): void {
|
||||
$event.preventDefault();
|
||||
|
||||
const form = commandSelectForm.value;
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
form.toggle();
|
||||
}
|
||||
|
||||
function isDropdownChildren(e?: Event): boolean {
|
||||
if (!e) {
|
||||
return false;
|
||||
}
|
||||
if (!e.target) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
(e.target as HTMLElement).classList.contains("dropdown-item") ||
|
||||
(e.target as HTMLElement).classList.contains("dropdown-toggle-split") ||
|
||||
(e.target as HTMLElement).classList.contains("ignoreMe")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const queryActionHelper = new QueryActionHelper(staticValues.maxTurn);
|
||||
const storedActionsHelper = new StoredActionsHelper(
|
||||
staticValues.serverNick,
|
||||
"general",
|
||||
staticValues.mapName,
|
||||
staticValues.unitSet
|
||||
);
|
||||
|
||||
const reservedCommandList = queryActionHelper.reservedCommandList;
|
||||
const prevSelectedTurnList = queryActionHelper.prevSelectedTurnList;
|
||||
const selectedTurnList = queryActionHelper.selectedTurnList;
|
||||
|
||||
const isEditMode = storedActionsHelper.isEditMode;
|
||||
|
||||
const flippedMaxTurn = 14;
|
||||
|
||||
const editModeRowHeight = 29.35;
|
||||
const basicModeRowHeight = 34.4;
|
||||
const viewMaxTurn = ref(flippedMaxTurn);
|
||||
const rowGridStyle = ref({
|
||||
display: "grid",
|
||||
gridTemplateRows: `repeat(${viewMaxTurn.value}, ${isEditMode.value ? editModeRowHeight : basicModeRowHeight}px)`,
|
||||
});
|
||||
|
||||
watch([isEditMode, viewMaxTurn], ([isEditMode, maxTurn]) => {
|
||||
rowGridStyle.value.gridTemplateRows = `repeat(${maxTurn}, ${isEditMode ? editModeRowHeight : basicModeRowHeight}px)`;
|
||||
});
|
||||
|
||||
const isDragSingle = ref(false);
|
||||
const isDragToggle = ref(false);
|
||||
|
||||
const invCommandMap: Record<string, CommandItem> = {};
|
||||
for (const category of commandList) {
|
||||
for (const command of category.values) {
|
||||
invCommandMap[command.value] = command;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleViewMaxTurn() {
|
||||
if (viewMaxTurn.value == flippedMaxTurn) {
|
||||
viewMaxTurn.value = maxTurn;
|
||||
} else {
|
||||
viewMaxTurn.value = flippedMaxTurn;
|
||||
}
|
||||
}
|
||||
|
||||
async function repeatGeneralCommand(amount: number) {
|
||||
try {
|
||||
await SammoAPI.Command.RepeatCommand({ amount });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
await reloadCommandList();
|
||||
}
|
||||
|
||||
async function pushGeneralCommand(amount: number) {
|
||||
try {
|
||||
await SammoAPI.Command.PushCommand({ amount });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
await reloadCommandList();
|
||||
}
|
||||
|
||||
const serverNow = ref(new Date());
|
||||
|
||||
function pushGeneralCommandSingle(e: Event) {
|
||||
//NOTE: split 구현에 버그가 있어서, 수동으로 구분해야함
|
||||
if (isDropdownChildren(e)) {
|
||||
return;
|
||||
}
|
||||
if (!isEditMode.value) {
|
||||
return;
|
||||
}
|
||||
void pushGeneralCommand(1);
|
||||
}
|
||||
|
||||
function pullGeneralCommandSingle(e: Event) {
|
||||
//NOTE: split 구현에 버그가 있어서, 수동으로 구분해야함
|
||||
if (isDropdownChildren(e)) {
|
||||
return;
|
||||
}
|
||||
if (!isEditMode.value) {
|
||||
return;
|
||||
}
|
||||
void pushGeneralCommand(-1);
|
||||
}
|
||||
|
||||
async function reloadCommandList() {
|
||||
let result: ReservedCommandResponse;
|
||||
try {
|
||||
result = await SammoAPI.Command.GetReservedCommand();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let yearMonth = joinYearMonth(result.year, result.month);
|
||||
|
||||
const turnTime = parseTime(result.turnTime);
|
||||
let nextTurnTime = new Date(turnTime);
|
||||
|
||||
const autorunLimitYearMonth = result.autorun_limit ?? yearMonth - 1;
|
||||
const [autorunLimitYear, autorunLimitMonth] = parseYearMonth(autorunLimitYearMonth);
|
||||
|
||||
reservedCommandList.value = [];
|
||||
for (const obj of result.turn) {
|
||||
const [year, month] = parseYearMonth(yearMonth);
|
||||
let tooltip: string[] = [];
|
||||
let style: Record<string, unknown> = {};
|
||||
|
||||
const brief = obj.brief;
|
||||
|
||||
if (yearMonth <= autorunLimitYearMonth) {
|
||||
if (obj.brief == "휴식") {
|
||||
obj.brief = "휴식<small>(자율 행동)</small>";
|
||||
}
|
||||
style.color = "#aaffff";
|
||||
|
||||
tooltip.push(`자율 행동 기간: ${autorunLimitYear}년 ${autorunLimitMonth}월까지`);
|
||||
}
|
||||
|
||||
if (mb_strwidth(brief) > 22) {
|
||||
tooltip.push(brief);
|
||||
}
|
||||
|
||||
reservedCommandList.value.push({
|
||||
...obj,
|
||||
year,
|
||||
month,
|
||||
time: formatTime(nextTurnTime, "HH:mm"),
|
||||
tooltip: tooltip.length == 0 ? undefined : tooltip.join("\n"),
|
||||
style,
|
||||
});
|
||||
|
||||
yearMonth += 1;
|
||||
nextTurnTime = addMinutes(nextTurnTime, result.turnTerm);
|
||||
}
|
||||
|
||||
serverNow.value = parseTime(result.date);
|
||||
}
|
||||
|
||||
async function reserveCommandDirect(args: [number[], TurnObj][], reload = true): Promise<boolean> {
|
||||
const query: {
|
||||
turnList: number[];
|
||||
action: string;
|
||||
arg: Args;
|
||||
}[] = [];
|
||||
for (const [turnList, { action, arg }] of args) {
|
||||
query.push({
|
||||
turnList,
|
||||
action,
|
||||
arg,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await SammoAPI.Command.ReserveBulkCommand(query);
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reload) {
|
||||
await reloadCommandList();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function reserveCommand() {
|
||||
let reqTurnList: number[] = queryActionHelper.getSelectedTurnList();
|
||||
|
||||
const commandName = selectedCommand.value.value;
|
||||
|
||||
if (listReqArgCommand.has(commandName)) {
|
||||
document.location.href = stringifyUrl({
|
||||
url: "v_processing.php",
|
||||
query: {
|
||||
command: commandName,
|
||||
turnList: reqTurnList.join("_"),
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await SammoAPI.Command.ReserveCommand({
|
||||
turnList: reqTurnList,
|
||||
action: commandName,
|
||||
});
|
||||
|
||||
storedActionsHelper.pushRecentActions({
|
||||
action: commandName,
|
||||
brief: result.brief,
|
||||
arg: {},
|
||||
});
|
||||
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
await reloadCommandList();
|
||||
}
|
||||
|
||||
function chooseCommand(val?: string) {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
selectedCommand.value = invCommandMap[val];
|
||||
void reserveCommand();
|
||||
}
|
||||
|
||||
const commandQuickReserveForm = ref<InstanceType<typeof CommandSelectForm> | null>(null);
|
||||
|
||||
const currentQuickReserveTarget = ref(-1);
|
||||
function chooseQuickReserveCommand(val?: string) {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
selectedCommand.value = invCommandMap[val];
|
||||
selectedTurnList.value.clear();
|
||||
selectedTurnList.value.add(currentQuickReserveTarget.value);
|
||||
void reserveCommand();
|
||||
}
|
||||
function toggleQuickReserveForm(turnIdx: number) {
|
||||
if (turnIdx == currentQuickReserveTarget.value) {
|
||||
commandQuickReserveForm.value?.toggle();
|
||||
return;
|
||||
}
|
||||
currentQuickReserveTarget.value = turnIdx;
|
||||
commandQuickReserveForm.value?.show();
|
||||
}
|
||||
|
||||
watch(isEditMode, (newEditMode) => {
|
||||
if (newEditMode) {
|
||||
commandQuickReserveForm.value?.close();
|
||||
currentQuickReserveTarget.value = -1;
|
||||
} else {
|
||||
commandSelectForm.value?.close();
|
||||
}
|
||||
});
|
||||
|
||||
const emptyTurnObj: TurnObj = { action: "휴식", brief: "휴식", arg: {} };
|
||||
|
||||
const recentActions = storedActionsHelper.recentActions;
|
||||
const storedActions = storedActionsHelper.storedActions;
|
||||
const activatedCategory = storedActionsHelper.activatedCategory;
|
||||
|
||||
async function eraseSelectedTurnList(releaseSelect = true): Promise<boolean> {
|
||||
const result = await reserveCommandDirect([[queryActionHelper.getSelectedTurnList(), emptyTurnObj]]);
|
||||
if (releaseSelect) {
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const clipboard = storedActionsHelper.clipboard;
|
||||
|
||||
async function clipboardCut(releaseSelect = true) {
|
||||
clipboardCopy(false);
|
||||
return eraseSelectedTurnList(releaseSelect);
|
||||
}
|
||||
|
||||
function clipboardCopy(releaseSelect = true) {
|
||||
clipboard.value = queryActionHelper.extractQueryActions();
|
||||
if (releaseSelect) {
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
}
|
||||
}
|
||||
|
||||
async function clipboardPaste(releaseSelect = true) {
|
||||
const rawActions = clipboard.value;
|
||||
if (rawActions === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actions = queryActionHelper.amplifyQueryActions(rawActions, queryActionHelper.getSelectedTurnList());
|
||||
if (actions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await reserveCommandDirect(actions);
|
||||
if (releaseSelect) {
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function subRepeatCommand(releaseSelect = true): Promise<boolean> {
|
||||
const reqTurnList = queryActionHelper.getSelectedTurnList();
|
||||
const selectedMinTurnIdx = reqTurnList[0];
|
||||
const selectedMaxTurnIdx = reqTurnList[reqTurnList.length - 1];
|
||||
const queryLength = selectedMaxTurnIdx - selectedMinTurnIdx + 1;
|
||||
|
||||
const rawActions = queryActionHelper.extractQueryActions();
|
||||
const actions = queryActionHelper.amplifyQueryActions(rawActions, range(selectedMinTurnIdx, maxTurn, queryLength));
|
||||
|
||||
const result = await reserveCommandDirect(actions);
|
||||
if (releaseSelect) {
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function eraseAndPullCommand(releaseSelect = true): Promise<boolean> {
|
||||
const reqTurnList = queryActionHelper.getSelectedTurnList();
|
||||
const selectedMinTurnIdx = reqTurnList[0];
|
||||
const selectedMaxTurnIdx = reqTurnList[reqTurnList.length - 1];
|
||||
const queryLength = selectedMaxTurnIdx - selectedMinTurnIdx + 1;
|
||||
|
||||
if (selectedMinTurnIdx === 0) {
|
||||
await pushGeneralCommand(-queryLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (selectedMinTurnIdx + queryLength == maxTurn) {
|
||||
return eraseSelectedTurnList(releaseSelect);
|
||||
}
|
||||
|
||||
const actions: [number[], TurnObj][] = [];
|
||||
|
||||
const emptyTurnList: number[] = [];
|
||||
|
||||
for (const srcTurnIdx of range(selectedMinTurnIdx + queryLength, maxTurn)) {
|
||||
const rawAction = reservedCommandList.value[srcTurnIdx];
|
||||
if (rawAction.action == emptyTurnObj.action) {
|
||||
emptyTurnList.push(srcTurnIdx - queryLength);
|
||||
continue;
|
||||
}
|
||||
actions.push([
|
||||
[srcTurnIdx - queryLength],
|
||||
{
|
||||
action: rawAction.action,
|
||||
arg: rawAction.arg,
|
||||
brief: rawAction.brief,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
emptyTurnList.push(...range(maxTurn - queryLength, maxTurn));
|
||||
actions.push([emptyTurnList, emptyTurnObj]);
|
||||
|
||||
const result = await reserveCommandDirect(actions);
|
||||
if (releaseSelect) {
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function pushEmptyCommand(releaseSelect = true): Promise<boolean> {
|
||||
const reqTurnList = queryActionHelper.getSelectedTurnList();
|
||||
const selectedMinTurnIdx = reqTurnList[0];
|
||||
const selectedMaxTurnIdx = reqTurnList[reqTurnList.length - 1];
|
||||
const queryLength = selectedMaxTurnIdx - selectedMinTurnIdx + 1;
|
||||
|
||||
if (selectedMinTurnIdx === 0) {
|
||||
await pushGeneralCommand(queryLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (selectedMaxTurnIdx == maxTurn) {
|
||||
return eraseSelectedTurnList(releaseSelect);
|
||||
}
|
||||
|
||||
const actions: [number[], TurnObj][] = [];
|
||||
|
||||
const emptyTurnList: number[] = [];
|
||||
|
||||
for (const srcTurnIdx of range(selectedMinTurnIdx, maxTurn - queryLength)) {
|
||||
const rawAction = reservedCommandList.value[srcTurnIdx];
|
||||
if (rawAction.action == emptyTurnObj.action) {
|
||||
emptyTurnList.push(srcTurnIdx + queryLength);
|
||||
continue;
|
||||
}
|
||||
actions.push([
|
||||
[srcTurnIdx + queryLength],
|
||||
{
|
||||
action: rawAction.action,
|
||||
arg: rawAction.arg,
|
||||
brief: rawAction.brief,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
emptyTurnList.push(...range(selectedMinTurnIdx, selectedMinTurnIdx + queryLength));
|
||||
actions.push([emptyTurnList, emptyTurnObj]);
|
||||
|
||||
const result = await reserveCommandDirect(actions);
|
||||
if (releaseSelect) {
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function setStoredActions() {
|
||||
const actions = queryActionHelper.extractQueryActions();
|
||||
const turnBrief = new Map<number, string>();
|
||||
for (const [subTurnList, action] of actions) {
|
||||
const actionName = action.action.split("_");
|
||||
const actionShortName = actionName.length == 1 ? actionName[0] : actionName[1];
|
||||
for (const turnIdx of subTurnList) {
|
||||
turnBrief.set(turnIdx, actionShortName[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const turnBriefStr = Array.from(turnBrief.entries())
|
||||
.sort(([turnA], [turnB]) => turnA - turnB)
|
||||
.map(([, action]) => action)
|
||||
.join("");
|
||||
|
||||
const nickName = trim(prompt("선택한 턴들의 별명을 지어주세요", turnBriefStr) ?? "");
|
||||
if (nickName == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
storedActionsHelper.setStoredActions(nickName, actions);
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
}
|
||||
|
||||
function deleteStoredActions(actionKey: string) {
|
||||
storedActionsHelper.deleteStoredActions(actionKey);
|
||||
}
|
||||
|
||||
async function useStoredAction(rawActions: [number[], TurnObj][]) {
|
||||
const reqTurnList = queryActionHelper.getSelectedTurnList();
|
||||
const actions = queryActionHelper.amplifyQueryActions(rawActions, reqTurnList);
|
||||
const result = await reserveCommandDirect(actions);
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
return result;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void reloadCommandList();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@use "sass:color";
|
||||
|
||||
@import "@scss/common/break_500px.scss";
|
||||
@import "@scss/common/variables.scss";
|
||||
@import "@scss/common/bootswatch_custom_variables.scss";
|
||||
@import "@scss/game_bg.scss";
|
||||
|
||||
.commandPad {
|
||||
background-color: $gray-900;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.commandTable {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(20px, 0.8fr) minmax(75px, 2.4fr) minmax(40px, 0.9fr)
|
||||
4.8fr minmax(28px, 0.8fr);
|
||||
//30, 70, 37.65, 160
|
||||
}
|
||||
|
||||
.commandTable.isEditMode {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(30px, 1fr) minmax(75px, 2.5fr) minmax(40px, 1fr) 5fr;
|
||||
//30, 70, 37.65, 160
|
||||
}
|
||||
|
||||
@include media-1000px {
|
||||
.commandPad {
|
||||
margin-left: 10px;
|
||||
|
||||
.turn_pad {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.multiselect__content-wrapper {
|
||||
margin-left: calc(-100% / 7 * 2);
|
||||
width: calc(100% / 7 * 12);
|
||||
}
|
||||
|
||||
.multiselect__single {
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-500px {
|
||||
.dropdown-item {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.commandPad {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.btn {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.month_pad,
|
||||
.time_pad,
|
||||
.turn_pad {
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.isEditMode .month_pad:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.plain-center {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.plain-center,
|
||||
.month_pad,
|
||||
.time_pad,
|
||||
.turn_pad {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.turn_pad {
|
||||
white-space: nowrap;
|
||||
background-color: $nbase2color;
|
||||
}
|
||||
|
||||
.turn_pad:nth-child(2n) {
|
||||
background-color: color.adjust($nbase2color, $lightness: -5%);
|
||||
}
|
||||
|
||||
.turn_pad .turn_text {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,272 @@
|
||||
import type { Args } from "./processing/args";
|
||||
import {
|
||||
callSammoAPI,
|
||||
extractHttpMethod,
|
||||
GET,
|
||||
PATCH,
|
||||
POST,
|
||||
PUT,
|
||||
type APITail,
|
||||
type APICallT,
|
||||
type RawArgType,
|
||||
type ValidResponse,
|
||||
type InvalidResponse,
|
||||
} from "./util/callSammoAPI";
|
||||
export type { ValidResponse, InvalidResponse };
|
||||
import { APIPathGen, NumVar, StrVar } from "./util/APIPathGen.js";
|
||||
import type { BettingDetailResponse, BettingListResponse } from "./defs/API/Betting";
|
||||
import type { ReserveBulkCommandResponse, ReserveCommandResponse, ReservedCommandResponse } from "./defs/API/Command";
|
||||
import type { ChiefResponse } from "./defs/API/NationCommand";
|
||||
import type { inheritBuffType, InheritLogResponse } from "./defs/API/InheritAction";
|
||||
import type { SetBlockWarResponse, GeneralListResponse as NationGeneralListResponse } from "./defs/API/Nation";
|
||||
import type { UploadImageResponse } from "./defs/API/Misc";
|
||||
import type { GeneralLogType, GetGeneralLogResponse, JoinArgs } from "./defs/API/General";
|
||||
import type {
|
||||
GetConstResponse,
|
||||
GetCurrentHistoryResponse,
|
||||
GetDiplomacyResponse,
|
||||
GetHistoryResponse,
|
||||
} from "./defs/API/Global";
|
||||
import type { CachedMapResult, GeneralListResponse, ItemTypeKey, MapResult } from "./defs";
|
||||
import type { VoteDetailResult, VoteListResult } from "./defs/API/Vote";
|
||||
import type { ActiveResourceAuctionList, OpenAuctionResponse, UniqueItemAuctionDetail, UniqueItemAuctionList } from "./defs/API/Auction";
|
||||
|
||||
const apiRealPath = {
|
||||
Auction: {
|
||||
BidBuyRiceAuction: PUT as APICallT<{
|
||||
auctionID: number;
|
||||
amount: number;
|
||||
}>,
|
||||
BidSellRiceAuction: PUT as APICallT<{
|
||||
auctionID: number;
|
||||
amount: number;
|
||||
}>,
|
||||
GetActiveResourceAuctionList: GET as APICallT<undefined, ActiveResourceAuctionList>,
|
||||
OpenBuyRiceAuction: POST as APICallT<
|
||||
{
|
||||
amount: number;
|
||||
closeTurnCnt: number;
|
||||
startBidAmount: number;
|
||||
finishBidAmount: number;
|
||||
},
|
||||
OpenAuctionResponse
|
||||
>,
|
||||
OpenSellRiceAuction: POST as APICallT<
|
||||
{
|
||||
amount: number;
|
||||
closeTurnCnt: number;
|
||||
startBidAmount: number;
|
||||
finishBidAmount: number;
|
||||
},
|
||||
OpenAuctionResponse
|
||||
>,
|
||||
|
||||
BidUniqueAuction: PUT as APICallT<{
|
||||
auctionID: number;
|
||||
amount: number;
|
||||
}>,
|
||||
GetUniqueItemAuctionDetail: GET as APICallT<{
|
||||
auctionID: number;
|
||||
}, UniqueItemAuctionDetail>,
|
||||
GetUniqueItemAuctionList: GET as APICallT<undefined, UniqueItemAuctionList>,
|
||||
OpenUniqueAuction: POST as APICallT<{
|
||||
itemID: string,
|
||||
amount: number,
|
||||
}, OpenAuctionResponse>,
|
||||
},
|
||||
Betting: {
|
||||
Bet: PUT as APICallT<{
|
||||
bettingID: number;
|
||||
bettingType: number[];
|
||||
amount: number;
|
||||
}>,
|
||||
GetBettingDetail: NumVar("betting_id", GET as APICallT<undefined, BettingDetailResponse>),
|
||||
GetBettingList: GET as APICallT<
|
||||
{
|
||||
req?: "bettingNation" | "tournament";
|
||||
},
|
||||
BettingListResponse
|
||||
>,
|
||||
},
|
||||
Command: {
|
||||
GetReservedCommand: GET as APICallT<undefined, ReservedCommandResponse>,
|
||||
PushCommand: PUT as APICallT<{
|
||||
amount: number;
|
||||
}>,
|
||||
RepeatCommand: PUT as APICallT<{
|
||||
amount: number;
|
||||
}>,
|
||||
ReserveCommand: PUT as APICallT<
|
||||
{
|
||||
turnList: number[];
|
||||
action: string;
|
||||
arg?: Args;
|
||||
},
|
||||
ReserveCommandResponse
|
||||
>,
|
||||
ReserveBulkCommand: PUT as APICallT<
|
||||
{
|
||||
turnList: number[];
|
||||
action: string;
|
||||
arg?: Args;
|
||||
}[],
|
||||
ReserveBulkCommandResponse
|
||||
>,
|
||||
},
|
||||
General: {
|
||||
Join: POST as APICallT<JoinArgs>,
|
||||
GetGeneralLog: GET as APICallT<
|
||||
{
|
||||
reqType: GeneralLogType;
|
||||
reqTo?: number;
|
||||
},
|
||||
GetGeneralLogResponse
|
||||
>,
|
||||
DropItem: PUT as APICallT<{
|
||||
itemType: ItemTypeKey;
|
||||
}>,
|
||||
DieOnPrestart: POST as APICallT<undefined>,
|
||||
BuildNationCandidate: POST as APICallT<undefined>,
|
||||
},
|
||||
Global: {
|
||||
GeneralList: GET as APICallT<undefined, GeneralListResponse>,
|
||||
GeneralListWithToken: GET as APICallT<undefined, GeneralListResponse>,
|
||||
GetConst: GET as APICallT<undefined, GetConstResponse>,
|
||||
GetHistory: StrVar("serverID")(NumVar("year", NumVar("month", GET as APICallT<undefined, GetHistoryResponse>))),
|
||||
GetCurrentHistory: GET as APICallT<undefined, GetCurrentHistoryResponse>,
|
||||
GetMap: GET as APICallT<
|
||||
{
|
||||
neutralView?: 0 | 1;
|
||||
showMe?: 0 | 1;
|
||||
},
|
||||
MapResult
|
||||
>,
|
||||
GetCachedMap: GET as APICallT<undefined, CachedMapResult>,
|
||||
GetDiplomacy: GET as APICallT<undefined, GetDiplomacyResponse>,
|
||||
ExecuteEngine: POST as APICallT<undefined, ValidResponse & { updated: boolean }>,
|
||||
},
|
||||
InheritAction: {
|
||||
BuyHiddenBuff: PUT as APICallT<{
|
||||
type: inheritBuffType;
|
||||
level: number;
|
||||
}>,
|
||||
BuyRandomUnique: PUT as APICallT<undefined>,
|
||||
ResetSpecialWar: PUT as APICallT<undefined>,
|
||||
ResetTurnTime: PUT as APICallT<undefined>,
|
||||
SetNextSpecialWar: PUT as APICallT<{
|
||||
type: string;
|
||||
}>,
|
||||
GetMoreLog: GET as APICallT<{
|
||||
lastID: number
|
||||
}, InheritLogResponse>
|
||||
},
|
||||
Misc: {
|
||||
UploadImage: POST as APICallT<
|
||||
{
|
||||
imageData: string;
|
||||
},
|
||||
UploadImageResponse
|
||||
>,
|
||||
},
|
||||
NationCommand: {
|
||||
GetReservedCommand: GET as APICallT<undefined, ChiefResponse>,
|
||||
PushCommand: PUT as APICallT<{
|
||||
amount: number;
|
||||
}>,
|
||||
RepeatCommand: PUT as APICallT<{
|
||||
amount: number;
|
||||
}>,
|
||||
ReserveCommand: PUT as APICallT<
|
||||
{
|
||||
turnList: number[];
|
||||
action: string;
|
||||
arg?: Args;
|
||||
},
|
||||
ReserveCommandResponse
|
||||
>,
|
||||
ReserveBulkCommand: PUT as APICallT<
|
||||
{
|
||||
turnList: number[];
|
||||
action: string;
|
||||
arg?: Args;
|
||||
}[],
|
||||
ReserveBulkCommandResponse
|
||||
>,
|
||||
},
|
||||
Nation: {
|
||||
GeneralList: GET as APICallT<undefined, NationGeneralListResponse>,
|
||||
SetNotice: PUT as APICallT<{
|
||||
msg: string;
|
||||
}>,
|
||||
SetScoutMsg: PUT as APICallT<{
|
||||
msg: string;
|
||||
}>,
|
||||
SetBill: PATCH as APICallT<{
|
||||
amount: number;
|
||||
}>,
|
||||
SetRate: PATCH as APICallT<{
|
||||
amount: number;
|
||||
}>,
|
||||
SetSecretLimit: PATCH as APICallT<{
|
||||
amount: number;
|
||||
}>,
|
||||
SetBlockWar: PATCH as APICallT<
|
||||
{
|
||||
value: boolean;
|
||||
},
|
||||
SetBlockWarResponse
|
||||
>,
|
||||
SetBlockScout: PATCH as APICallT<{
|
||||
value: boolean;
|
||||
}>,
|
||||
GetGeneralLog: GET as APICallT<
|
||||
{
|
||||
generalID: number;
|
||||
reqType: GeneralLogType;
|
||||
reqTo?: number;
|
||||
},
|
||||
GetGeneralLogResponse
|
||||
>,
|
||||
SetTroopName: PATCH as APICallT<{
|
||||
troopID: number;
|
||||
troopName: string;
|
||||
}>,
|
||||
},
|
||||
Vote: {
|
||||
AddComment: POST as APICallT<{
|
||||
voteID: number;
|
||||
text: string;
|
||||
}>,
|
||||
GetVoteList: GET as APICallT<undefined, VoteListResult>,
|
||||
GetVoteDetail: GET as APICallT<
|
||||
{
|
||||
voteID: number;
|
||||
},
|
||||
VoteDetailResult
|
||||
>,
|
||||
NewVote: POST as APICallT<{
|
||||
title: string;
|
||||
multipleOptions?: number;
|
||||
endDate?: string;
|
||||
options: string[];
|
||||
keepOldVote?: boolean;
|
||||
}>,
|
||||
Vote: POST as APICallT<
|
||||
{
|
||||
voteID: number;
|
||||
selection: number[];
|
||||
},
|
||||
ValidResponse & { wonLottery: boolean }
|
||||
>,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const SammoAPI = APIPathGen(apiRealPath, (path: string[], tail: APITail, pathParam) => {
|
||||
const method = extractHttpMethod(tail);
|
||||
return (args?: RawArgType, returnError?: boolean) => {
|
||||
if (returnError) {
|
||||
return callSammoAPI(method, path.join("/"), args, pathParam, true);
|
||||
}
|
||||
return callSammoAPI(method, path.join("/"), args, pathParam);
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { AutoLoginFailed, AutoLoginNonceResponse, AutoLoginResponse, LoginFailed, LoginResponse } from "./defs/API/Login";
|
||||
import { APIPathGen } from "./util/APIPathGen";
|
||||
import { callSammoAPI, extractHttpMethod, GET, POST, type APICallT, type APITail, type InvalidResponse, type RawArgType, type ValidResponse } from "./util/callSammoAPI";
|
||||
export type { ValidResponse, InvalidResponse };
|
||||
|
||||
const apiRealPath = {
|
||||
Login: {
|
||||
LoginByID: POST as APICallT<{
|
||||
username: string,
|
||||
password: string,
|
||||
}, LoginResponse, LoginFailed>,
|
||||
LoginByToken: POST as APICallT<{
|
||||
hashedToken: string,
|
||||
token_id: number,
|
||||
}, AutoLoginResponse, AutoLoginFailed>,
|
||||
ReqNonce: GET as APICallT<undefined, AutoLoginNonceResponse, AutoLoginFailed>
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const SammoRootAPI = APIPathGen(apiRealPath, (path: string[], tail: APITail, pathParam) => {
|
||||
const method = extractHttpMethod(tail);
|
||||
return (args?: RawArgType, returnError?: boolean) => {
|
||||
if (returnError) {
|
||||
return callSammoAPI(method, path.join('/'), args, pathParam, true);
|
||||
}
|
||||
return callSammoAPI(method, path.join('/'), args, pathParam);
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import "@scss/battleCenter.scss";
|
||||
|
||||
import { auto500px } from "./util/auto500px";
|
||||
import { insertCustomCSS } from "./util/customCSS";
|
||||
import { htmlReady } from "./util/htmlReady";
|
||||
|
||||
auto500px();
|
||||
htmlReady(() => {
|
||||
insertCustomCSS();
|
||||
});
|
||||
@@ -0,0 +1,1067 @@
|
||||
import $ from 'jquery';
|
||||
import 'bootstrap';
|
||||
import download from 'downloadjs';
|
||||
import { unwrap } from "@util/unwrap";
|
||||
import { isInteger } from 'lodash';
|
||||
import { errUnknown, getNpcColor } from '@/common_legacy';
|
||||
import { combineArray } from "@util/combineArray";
|
||||
import { isBrightColor } from "@util/isBrightColor";
|
||||
import { numberWithCommas } from "@util/numberWithCommas";
|
||||
import { unwrap_any } from '@util/unwrap_any';
|
||||
import type { BasicGeneralListResponse, InvalidResponse } from '@/defs';
|
||||
import { formatTime } from '@util/formatTime';
|
||||
import { Modal } from 'bootstrap';
|
||||
|
||||
type CityAttackerInfo = {
|
||||
level: number,
|
||||
}
|
||||
|
||||
type CityBasicInfo = {
|
||||
nation?: number,
|
||||
level: number,
|
||||
def: number,
|
||||
wall: number,
|
||||
city?: number,
|
||||
}
|
||||
|
||||
type NationBasicInfo = {
|
||||
level: number,
|
||||
type: string,
|
||||
tech: number,
|
||||
capital: number,
|
||||
}
|
||||
|
||||
type GeneralInfo = {
|
||||
no: number,
|
||||
npc?: number
|
||||
name: string,
|
||||
officer_level: number,
|
||||
explevel: number,
|
||||
|
||||
leadership: number,
|
||||
horse: string,
|
||||
strength: number,
|
||||
weapon: string,
|
||||
intel: number,
|
||||
book: string,
|
||||
item: string,
|
||||
|
||||
injury: number,
|
||||
|
||||
rice: number,
|
||||
|
||||
personal: string,
|
||||
special2: string,
|
||||
|
||||
crew: number,
|
||||
crewtype: number,
|
||||
atmos: number,
|
||||
train: number,
|
||||
|
||||
dex1: number,
|
||||
dex2: number,
|
||||
dex3: number,
|
||||
dex4: number,
|
||||
dex5: number,
|
||||
|
||||
defence_train: number,
|
||||
|
||||
warnum: number,
|
||||
killnum: number,
|
||||
killcrew: number,
|
||||
|
||||
officer_city?: number,
|
||||
};
|
||||
|
||||
type BattleInfo = {
|
||||
attackerGeneral: GeneralInfo,
|
||||
attackerCity: CityAttackerInfo | CityBasicInfo,
|
||||
attackerNation: NationBasicInfo,
|
||||
defenderGenerals: GeneralInfo[],
|
||||
defenderCity: CityBasicInfo,
|
||||
defenderNation: NationBasicInfo,
|
||||
year: number,
|
||||
month: number,
|
||||
repeatCnt: number,
|
||||
};
|
||||
|
||||
type ExportedGeneralInfo = {
|
||||
objType: 'general',
|
||||
data: GeneralInfo,
|
||||
}
|
||||
|
||||
type ExportedBattleInfo = {
|
||||
objType: 'battle',
|
||||
data: BattleInfo
|
||||
}
|
||||
type ExportedInfo = ExportedGeneralInfo | ExportedBattleInfo;
|
||||
|
||||
type BattleResult = {
|
||||
result: true,
|
||||
datetime: string,
|
||||
lastWarLog: {
|
||||
generalBattleResultLog: string,
|
||||
generalBattleDetailLog: string,
|
||||
},
|
||||
avgWar: number,
|
||||
phase: number,
|
||||
killed: number,
|
||||
maxKilled: number,
|
||||
minKilled: number,
|
||||
dead: number,
|
||||
maxDead: number,
|
||||
minDead: number,
|
||||
attackerRice: number,
|
||||
defenderRice: number,
|
||||
attackerSkills: Record<string, number>,
|
||||
defendersSkills: Record<string, number>[],
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
nation?: NationBasicInfo,
|
||||
city?: CityBasicInfo,
|
||||
defaultSpecialDomestic: string,
|
||||
}
|
||||
}
|
||||
|
||||
let modalImport: Modal|undefined = undefined;
|
||||
|
||||
$(function ($) {
|
||||
|
||||
|
||||
const $generalForm = $('.form_sample .general_detail');
|
||||
const $defenderHeaderForm = $('.form_sample .card-header');
|
||||
const $defenderColumn = $('.defender-column');
|
||||
|
||||
const defenderNoList: Record<number, JQuery<HTMLElement>> = {};
|
||||
|
||||
const $attackerCard = $('.attacker_form');
|
||||
|
||||
const initBasicEvent = function () {
|
||||
|
||||
if (window.nation && window.city) {
|
||||
$('.form_city_level').val(window.city.level);
|
||||
$('.form_def').val(window.city.def);
|
||||
$('.form_wall').val(window.city.wall);
|
||||
$('.form_nation_type').val(window.nation.type);
|
||||
$('.form_nation_level').val(window.nation.level);
|
||||
$('.form_tech').val(window.nation.tech / 1000);
|
||||
if (window.nation.capital == window.city.city) {
|
||||
$('.attacker_nation .form_is_capital:first').trigger('click');
|
||||
$('.defender_nation .form_is_capital:first').trigger('click');
|
||||
} else {
|
||||
$('.attacker_nation .form_is_capital:last').trigger('click');
|
||||
$('.defender_nation .form_is_capital:last').trigger('click');
|
||||
}
|
||||
} else {
|
||||
$('.attacker_nation .form_is_capital:last').trigger('click');
|
||||
$('.defender_nation .form_is_capital:last').trigger('click');
|
||||
}
|
||||
|
||||
$('.form_injury').on('change', function () {
|
||||
const $this = $(this) as JQuery<HTMLInputElement>;
|
||||
const $general = getGeneralDetail($this);
|
||||
const $helptext = $general.find('.injury_helptext');
|
||||
|
||||
const injury = parseInt($this.val() as string);
|
||||
//FIXME: PHP 코드와 항상 일치하도록 변경
|
||||
let text = '건강';
|
||||
let color = 'white';
|
||||
if (injury > 60) {
|
||||
text = '위독';
|
||||
color = 'red';
|
||||
} else if (injury > 40) {
|
||||
text = '심각';
|
||||
color = 'magenta';
|
||||
} else if (injury > 20) {
|
||||
text = '중상';
|
||||
color = 'orange';
|
||||
} else if (injury > 0) {
|
||||
text = '경상';
|
||||
color = 'yellow';
|
||||
}
|
||||
$helptext.html(text).css('color', color);
|
||||
});
|
||||
|
||||
$('.export_general').on('click', function () {
|
||||
const $btn = $(this);
|
||||
const $general = getGeneralDetail($btn);
|
||||
|
||||
const values = exportGeneralInfo($general);
|
||||
console.log(values);
|
||||
});
|
||||
$('.delete-defender').on('click', function () {
|
||||
const $card = getGeneralFrame($(this));
|
||||
deleteDefender($card);
|
||||
});
|
||||
$('.copy-defender').on('click', function () {
|
||||
const $card = getGeneralFrame($(this));
|
||||
copyDefender($card);
|
||||
});
|
||||
|
||||
$('.add-defender').on('click', function () {
|
||||
addDefender();
|
||||
});
|
||||
|
||||
$('.btn-general-load').on('click', function () {
|
||||
const $file = $(this).prev();
|
||||
$file[0].click();
|
||||
})
|
||||
|
||||
$<HTMLInputElement>('.form_load_general_file').on('change', function (e) {
|
||||
e.preventDefault();
|
||||
const $this = $(this);
|
||||
const $card = getGeneralFrame($this);
|
||||
|
||||
const files = unwrap(e.target.files);
|
||||
|
||||
importGeneralInfoByFile(files, $card);
|
||||
return false;
|
||||
});
|
||||
|
||||
$('.btn-general-import-server').on('click', function () {
|
||||
const $this = $(this);
|
||||
const $card = getGeneralFrame($this);
|
||||
|
||||
const $modal = $('#importModal');
|
||||
$modal.data('target', $card);
|
||||
if(!modalImport){
|
||||
modalImport = new Modal($modal[0]);
|
||||
}
|
||||
modalImport.show();
|
||||
});
|
||||
|
||||
$('.btn-general-save').on('click', function () {
|
||||
const $this = $(this);
|
||||
const $general = getGeneralDetail($this);
|
||||
const generalData = exportGeneralInfo($general);
|
||||
|
||||
const filename = `general_${generalData.name}.json`;
|
||||
const saveData = JSON.stringify({
|
||||
objType: 'general',
|
||||
data: generalData
|
||||
}, null, 4);
|
||||
|
||||
download(saveData, filename, 'application/json');
|
||||
});
|
||||
|
||||
$('.btn-battle-load').on('click', function () {
|
||||
const $file = $(this).prev();
|
||||
$file[0].click();
|
||||
})
|
||||
|
||||
$<HTMLInputElement>('.form_load_battle_file').on('change', function (e) {
|
||||
e.preventDefault();
|
||||
const files = unwrap(e.target.files);
|
||||
|
||||
importBattleInfoByFile(files);
|
||||
return false;
|
||||
});
|
||||
|
||||
$('.btn-battle-save').on('click', function () {
|
||||
const battleData = exportAllData();
|
||||
const dateText = formatTime(new Date(), 'yyyyMMdd_HHmmss');
|
||||
const filename = `battle_${dateText}.json`;
|
||||
const saveData = JSON.stringify({
|
||||
objType: 'battle',
|
||||
data: battleData
|
||||
}, null, 4);
|
||||
|
||||
download(saveData, filename, 'application/json');
|
||||
})
|
||||
|
||||
const $generals = $('.general_detail');
|
||||
$generals.on('dragover dragleave', function (e) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
})
|
||||
$generals.on('drop', function (e) {
|
||||
e.preventDefault();
|
||||
const $this = $(this);
|
||||
const $card = getGeneralFrame($this);
|
||||
|
||||
const files = unwrap((unwrap(e.originalEvent) as DragEvent).dataTransfer).files;
|
||||
|
||||
importGeneralInfoByFile(files, $card);
|
||||
return false;
|
||||
});
|
||||
|
||||
const $battlePad = $('.dragpad_battle');
|
||||
$battlePad.on('dragover dragleave', function (e) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
})
|
||||
$battlePad.on('drop', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const files = unwrap((unwrap(e.originalEvent) as DragEvent).dataTransfer).files;
|
||||
|
||||
importBattleInfoByFile(files);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
const importGeneralInfo = function ($general: JQuery<HTMLElement>, data: GeneralInfo) {
|
||||
const setVal = function (query: string, val: string | number) {
|
||||
$general.find(query).val(val).trigger('change');
|
||||
}
|
||||
|
||||
setVal('.form_general_name', data.name);
|
||||
|
||||
setVal('.form_officer_level', data.officer_level);
|
||||
setVal('.form_exp_level', data.explevel);
|
||||
|
||||
setVal('.form_leadership', data.leadership);
|
||||
setVal('.form_general_horse', data.horse);
|
||||
setVal('.form_strength', data.strength);
|
||||
setVal('.form_general_weap', data.weapon);
|
||||
setVal('.form_intel', data.intel);
|
||||
setVal('.form_general_book', data.book);
|
||||
setVal('.form_general_item', data.item);
|
||||
|
||||
setVal('.form_injury', data.injury);
|
||||
|
||||
setVal('.form_rice', data.rice);
|
||||
|
||||
setVal('.form_general_character', data.personal);
|
||||
setVal('.form_general_special_war', data.special2);
|
||||
|
||||
setVal('.form_crew', data.crew);
|
||||
setVal('.form_crewtype', data.crewtype);
|
||||
setVal('.form_atmos', data.atmos);
|
||||
setVal('.form_train', data.train);
|
||||
|
||||
setVal('.form_dex1', data.dex1);
|
||||
setVal('.form_dex2', data.dex2);
|
||||
setVal('.form_dex3', data.dex3);
|
||||
setVal('.form_dex4', data.dex4);
|
||||
setVal('.form_dex5', data.dex5);
|
||||
setVal('.form_defence_train', data.defence_train);
|
||||
|
||||
setVal('.form_warnum', data.warnum);
|
||||
setVal('.form_killnum', data.killnum);
|
||||
setVal('.form_killcrew', data.killcrew);
|
||||
|
||||
if (!setGeneralNo($general, data.no)) {
|
||||
setGeneralNo($general, generateNewGeneralNo());
|
||||
}
|
||||
}
|
||||
|
||||
const exportGeneralInfo = function ($general: JQuery<HTMLElement>): GeneralInfo {
|
||||
const getInt = function (query: string): number {
|
||||
return parseInt(unwrap_any<string>($general.find(query).val()));
|
||||
}
|
||||
|
||||
const getVal = function (query: string): string {
|
||||
return unwrap_any<string>($general.find(query).val());
|
||||
}
|
||||
|
||||
return {
|
||||
no: getGeneralNo($general),
|
||||
name: getVal('.form_general_name'),
|
||||
officer_level: getInt('.form_officer_level'),
|
||||
explevel: getInt('.form_exp_level'),
|
||||
|
||||
leadership: getInt('.form_leadership'),
|
||||
horse: getVal('.form_general_horse'),
|
||||
strength: getInt('.form_strength'),
|
||||
weapon: getVal('.form_general_weap'),
|
||||
intel: getInt('.form_intel'),
|
||||
book: getVal('.form_general_book'),
|
||||
item: getVal('.form_general_item'),
|
||||
|
||||
injury: getInt('.form_injury'),
|
||||
|
||||
rice: getInt('.form_rice'),
|
||||
|
||||
personal: getVal('.form_general_character'),
|
||||
special2: getVal('.form_general_special_war'),
|
||||
|
||||
crew: getInt('.form_crew'),
|
||||
crewtype: getInt('.form_crewtype'),
|
||||
atmos: getInt('.form_atmos'),
|
||||
train: getInt('.form_train'),
|
||||
|
||||
dex1: getInt('.form_dex1'),
|
||||
dex2: getInt('.form_dex2'),
|
||||
dex3: getInt('.form_dex3'),
|
||||
dex4: getInt('.form_dex4'),
|
||||
dex5: getInt('.form_dex5'),
|
||||
|
||||
defence_train: getInt('.form_defence_train'),
|
||||
|
||||
warnum: getInt('.form_warnum'),
|
||||
killnum: getInt('.form_killnum'),
|
||||
killcrew: getInt('.form_killcrew'),
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
const importBattleInfoByFile = function (files: FileList) {
|
||||
if (files === null) {
|
||||
alert('파일 에러!');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (files.length < 1) {
|
||||
alert("파일 에러!");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const file = files[0];
|
||||
if (file.size > 1024 * 1024) {
|
||||
alert('파일이 너무 큽니다!');
|
||||
return false;
|
||||
}
|
||||
if (file.type === '') {
|
||||
alert('폴더를 업로드할 수 없습니다!');
|
||||
return false;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function () {
|
||||
let battleData: ExportedInfo;
|
||||
try {
|
||||
battleData = JSON.parse(unwrap_any<string>(reader.result));
|
||||
} catch (e) {
|
||||
alert('올바르지 않은 파일 형식입니다');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!('objType' in battleData)) {
|
||||
alert('파일 형식을 확인할 수 없습니다');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (battleData.objType != 'battle') {
|
||||
alert('전투 데이터가 아닙니다');
|
||||
return false;
|
||||
}
|
||||
|
||||
importBattleInfo(battleData.data);
|
||||
return true;
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
reader.readAsText(file);
|
||||
} catch (e) {
|
||||
alert('파일을 읽는데 실패했습니다.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const importGeneralInfoByFile = function (files: FileList, $card: JQuery<HTMLElement>) {
|
||||
if ($card === undefined) {
|
||||
$card = addDefender();
|
||||
}
|
||||
|
||||
if (files === null) {
|
||||
alert('파일 에러!');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (files.length < 1) {
|
||||
alert("파일 에러!");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const file = files[0];
|
||||
if (file.size > 1024 * 1024) {
|
||||
alert('파일이 너무 큽니다!');
|
||||
return false;
|
||||
}
|
||||
if (file.type === '') {
|
||||
alert('폴더를 업로드할 수 없습니다!');
|
||||
return false;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function () {
|
||||
let generalData: ExportedInfo;
|
||||
try {
|
||||
generalData = JSON.parse(unwrap_any<string>(reader.result));
|
||||
} catch (e) {
|
||||
alert('올바르지 않은 파일 형식입니다');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!('objType' in generalData)) {
|
||||
alert('파일 형식을 확인할 수 없습니다');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (generalData.objType == 'battle') {
|
||||
importBattleInfo(generalData.data);
|
||||
return true;
|
||||
}
|
||||
if (generalData.objType != 'general') {
|
||||
alert('장수 데이터가 아닙니다');
|
||||
return false;
|
||||
}
|
||||
|
||||
$card.find('.form_load_general_file').val('');
|
||||
|
||||
importGeneralInfo($card, generalData.data);
|
||||
return true;
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
reader.readAsText(file);
|
||||
} catch (e) {
|
||||
alert('파일을 읽는데 실패했습니다.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const extendGeneralInfoForDB = function (generalData: GeneralInfo) {
|
||||
|
||||
const dbVal = {
|
||||
nation: (generalData.no) <= 1 ? 1 : 2,
|
||||
city: (generalData.no) <= 1 ? 1 : 3,
|
||||
turntime: '2018-08-26 12:00',
|
||||
special: window.defaultSpecialDomestic,
|
||||
leadership_exp: 0,
|
||||
strength_exp: 0,
|
||||
intel_exp: 0,
|
||||
|
||||
gold: 10000,
|
||||
|
||||
dedication: 0,
|
||||
|
||||
recent_war: '2018-08-26 12:00',
|
||||
experience: Math.pow(generalData.explevel, 2),
|
||||
};
|
||||
|
||||
return $.extend({}, generalData, dbVal);
|
||||
}
|
||||
|
||||
const getGeneralFrame = function ($btn: JQuery<HTMLElement>) {
|
||||
const $card = $btn.closest('.general_form');
|
||||
return $card;
|
||||
}
|
||||
|
||||
const getGeneralDetail = function ($btn: JQuery<HTMLElement>) {
|
||||
const $card = getGeneralFrame($btn);
|
||||
const $general = $card.find('.general_detail');
|
||||
return $general;
|
||||
}
|
||||
|
||||
const getGeneralNo = function ($btn: JQuery<HTMLElement>) {
|
||||
return parseInt(getGeneralFrame($btn).data('general_no'));
|
||||
}
|
||||
|
||||
const setGeneralNo = function ($btn: JQuery<HTMLElement>, value: number) {
|
||||
if (value == 1) {
|
||||
//1번은 무조건 공격자임
|
||||
return false;
|
||||
}
|
||||
if (value in defenderNoList) {
|
||||
return false;
|
||||
}
|
||||
const $card = getGeneralFrame($btn);
|
||||
$card.data('general_no', value);
|
||||
defenderNoList[value] = $card;
|
||||
return true;
|
||||
}
|
||||
|
||||
const generateNewGeneralNo = function () {
|
||||
for (; ;) {
|
||||
const newGeneralNo = Math.floor(Math.random() * (1 << 24)) + 2;
|
||||
if (newGeneralNo in defenderNoList) {
|
||||
continue;
|
||||
}
|
||||
return newGeneralNo;
|
||||
}
|
||||
}
|
||||
|
||||
const deleteGeneralNo = function ($btn: JQuery<HTMLElement>) {
|
||||
const $card = getGeneralFrame($btn);
|
||||
$card.removeData('general_no');
|
||||
const generalNo = getGeneralNo($card);
|
||||
delete defenderNoList[generalNo];
|
||||
}
|
||||
|
||||
const addDefender = function ($cardAfter?: JQuery<HTMLElement>) {
|
||||
const $newCard = $('<div class="card mb-2 defender_form general_form"></div>');
|
||||
|
||||
if ($cardAfter === undefined) {
|
||||
$defenderColumn.append($newCard);
|
||||
} else {
|
||||
$cardAfter.after($newCard);
|
||||
}
|
||||
|
||||
$newCard.append($defenderHeaderForm.clone(true, true));
|
||||
$newCard.append($generalForm.clone(true, true));
|
||||
|
||||
//$newGeneral = getGeneralDetail($newCard);
|
||||
setGeneralNo($newCard, generateNewGeneralNo());
|
||||
|
||||
return $newCard;
|
||||
}
|
||||
|
||||
const deleteDefender = function ($card: JQuery<HTMLElement>) {
|
||||
deleteGeneralNo($card);
|
||||
$card.detach();
|
||||
}
|
||||
|
||||
const copyDefender = function ($card: JQuery<HTMLElement>) {
|
||||
const $general = getGeneralDetail($card);
|
||||
|
||||
const generalData = exportGeneralInfo($general);
|
||||
const $newObj = addDefender($card);
|
||||
importGeneralInfo(getGeneralDetail($newObj), generalData);
|
||||
}
|
||||
|
||||
const importBattleInfo = function (battleData: BattleInfo) {
|
||||
|
||||
$('.form_load_battle_file').val('');
|
||||
console.log(battleData);
|
||||
|
||||
const $attackerNation = $('.attacker_nation');
|
||||
const $defenderNation = $('.defender_nation');
|
||||
|
||||
const attackerGeneral = battleData.attackerGeneral;
|
||||
const attackerCity = battleData.attackerCity;
|
||||
const attackerNation = battleData.attackerNation;
|
||||
|
||||
const defenderGenerals = battleData.defenderGenerals;
|
||||
const defenderCity = battleData.defenderCity;
|
||||
const defenderNation = battleData.defenderNation;
|
||||
|
||||
$('#year').val(battleData.year);
|
||||
$('#month').val(battleData.month);
|
||||
$('#repeat_cnt').val(battleData.repeatCnt);
|
||||
|
||||
$('.delete-defender').trigger('click');
|
||||
|
||||
$attackerNation.find('.form_nation_type').val(attackerNation.type);
|
||||
$attackerNation.find('.form_tech').val(Math.floor(attackerNation.tech / 1000));
|
||||
$attackerNation.find('.form_nation_level').val(attackerNation.level);
|
||||
if (attackerNation.capital == 1) {
|
||||
$attackerNation.find('.form_is_capital:first').trigger('click');
|
||||
} else {
|
||||
$attackerNation.find('.form_is_capital:last').trigger('click');
|
||||
}
|
||||
$attackerNation.find('.form_city_level').val(attackerCity.level);
|
||||
|
||||
importGeneralInfo($('.attacker_form'), attackerGeneral);
|
||||
|
||||
$defenderNation.find('.form_nation_type').val(defenderNation.type);
|
||||
$defenderNation.find('.form_tech').val(Math.floor(defenderNation.tech / 1000));
|
||||
$defenderNation.find('.form_nation_level').val(defenderNation.level);
|
||||
if (defenderNation.capital == 1) {
|
||||
$defenderNation.find('.form_is_capital:first').trigger('click');
|
||||
} else {
|
||||
$defenderNation.find('.form_is_capital:last').trigger('click');
|
||||
}
|
||||
$defenderNation.find('.form_city_level').val(defenderCity.level);
|
||||
$('#city_def').val(defenderCity.def);
|
||||
$('#city_wall').val(defenderCity.wall);
|
||||
|
||||
$.each(defenderGenerals, function (idx, defenderGeneral) {
|
||||
const $card = addDefender();
|
||||
importGeneralInfo($card, defenderGeneral);
|
||||
});
|
||||
}
|
||||
|
||||
const exportAllData = function (): BattleInfo {
|
||||
const $attackerNation = $('.attacker_nation');
|
||||
const $defenderNation = $('.defender_nation');
|
||||
|
||||
const attackerGeneral = exportGeneralInfo($('.attacker_form'));
|
||||
|
||||
const attackerCity = {
|
||||
level: parseInt($attackerNation.find('.form_city_level').val() as string),
|
||||
};
|
||||
|
||||
const attackerNation = {
|
||||
type: unwrap_any<string>($attackerNation.find('.form_nation_type').val()),
|
||||
tech: parseInt($attackerNation.find('.form_tech').val() as string) * 1000,
|
||||
level: parseInt($attackerNation.find('.form_nation_level').val() as string),
|
||||
capital: $attackerNation.find('.form_is_capital:checked').val() == '1' ? 1 : 2,
|
||||
}
|
||||
|
||||
const defenderGenerals = $('.defender_form').map(function () {
|
||||
return exportGeneralInfo($(this));
|
||||
}).toArray();
|
||||
|
||||
const defenderCity = {
|
||||
def: parseInt($('#city_def').val() as string),
|
||||
wall: parseInt($('#city_wall').val() as string),
|
||||
level: parseInt($defenderNation.find('.form_city_level').val() as string),
|
||||
};
|
||||
|
||||
const defenderNation = {
|
||||
type: unwrap_any<string>($defenderNation.find('.form_nation_type').val()),
|
||||
tech: parseInt(unwrap_any<string>($defenderNation.find('.form_tech').val())) * 1000,
|
||||
level: parseInt(unwrap_any<string>($defenderNation.find('.form_nation_level').val())),
|
||||
capital: $defenderNation.find('.form_is_capital:checked').val() == '1' ? 3 : 4,
|
||||
}
|
||||
|
||||
const year = parseInt(unwrap_any<string>($('#year').val()));
|
||||
const month = parseInt(unwrap_any<string>($('#month').val()));
|
||||
const repeatCnt = parseInt(unwrap_any<string>($('#repeat_cnt').val()));
|
||||
|
||||
return {
|
||||
attackerGeneral: attackerGeneral,
|
||||
attackerCity: attackerCity,
|
||||
attackerNation: attackerNation,
|
||||
|
||||
defenderGenerals: defenderGenerals,
|
||||
defenderCity: defenderCity,
|
||||
defenderNation: defenderNation,
|
||||
|
||||
year: year,
|
||||
month: month,
|
||||
repeatCnt: repeatCnt,
|
||||
};
|
||||
}
|
||||
|
||||
const extendAllDataForDB = function (allData: BattleInfo): BattleInfo {
|
||||
const defaultNation = {
|
||||
nation: 0,
|
||||
name: '재야',
|
||||
capital: 0,
|
||||
level: 0,
|
||||
gold: 0,
|
||||
rice: 2000,
|
||||
type: 'None',
|
||||
tech: 0,
|
||||
gennum: 200,
|
||||
};
|
||||
|
||||
const defaultCity = {
|
||||
nation: 0,
|
||||
supply: 1,
|
||||
name: '도시',
|
||||
|
||||
pop: 500000,
|
||||
agri: 10000,
|
||||
comm: 10000,
|
||||
secu: 10000,
|
||||
def: 1000,
|
||||
wall: 1000,
|
||||
|
||||
trust: 100,
|
||||
|
||||
pop_max: 600000,
|
||||
agri_max: 12000,
|
||||
comm_max: 12000,
|
||||
secu_max: 10000,
|
||||
def_max: 12000,
|
||||
wall_max: 12000,
|
||||
|
||||
dead: 0,
|
||||
|
||||
state: 0,
|
||||
|
||||
conflict: '{}',
|
||||
};
|
||||
|
||||
const attackerNation = $.extend({}, defaultNation, allData.attackerNation);
|
||||
attackerNation.nation = 1;
|
||||
attackerNation.name = '출병국';
|
||||
|
||||
const attackerCity = $.extend({}, defaultCity, allData.attackerCity) as CityBasicInfo;
|
||||
attackerCity.nation = 1;
|
||||
attackerCity.city = 1;
|
||||
|
||||
const attackerGeneral = extendGeneralInfoForDB(allData.attackerGeneral);
|
||||
if (2 <= attackerGeneral.officer_level && attackerGeneral.officer_level <= 4) {
|
||||
attackerGeneral.officer_city = 1;
|
||||
} else {
|
||||
attackerGeneral.officer_city = 0;
|
||||
}
|
||||
|
||||
const defenderNation = $.extend({}, defaultNation, allData.defenderNation);
|
||||
defenderNation.nation = 2;
|
||||
defenderNation.name = '수비국';
|
||||
|
||||
const defenderCity = $.extend({}, defaultCity, allData.defenderCity);
|
||||
defenderCity.nation = 2;
|
||||
defenderCity.city = 3;
|
||||
defenderCity.wall_max = Math.floor(defenderCity.wall / 5 * 6);
|
||||
defenderCity.def_max = Math.floor(defenderCity.def / 5 * 6);
|
||||
|
||||
const defenderGenerals: GeneralInfo[] = [];
|
||||
$.each(allData.defenderGenerals, function () {
|
||||
const defenderGeneral = extendGeneralInfoForDB(this);
|
||||
if (2 <= defenderGeneral.officer_level && defenderGeneral.officer_level <= 4) {
|
||||
defenderGeneral.officer_city = 3;
|
||||
} else {
|
||||
defenderGeneral.officer_city = 0;
|
||||
}
|
||||
|
||||
defenderGenerals.push(defenderGeneral);
|
||||
});
|
||||
|
||||
|
||||
return $.extend({}, allData, {
|
||||
attackerGeneral: attackerGeneral,
|
||||
attackerCity: attackerCity,
|
||||
attackerNation: attackerNation,
|
||||
|
||||
defenderGenerals: defenderGenerals,
|
||||
defenderCity: defenderCity,
|
||||
defenderNation: defenderNation,
|
||||
});
|
||||
}
|
||||
|
||||
const parseSkillCount = function (skills: Record<string, number>) {
|
||||
const result: string[] = [];
|
||||
for (const [key, value] of Object.entries(skills)) {
|
||||
result.push(`${key}(${toPretty(value)}회)`);
|
||||
}
|
||||
|
||||
if (result.length == 0) {
|
||||
return '-';
|
||||
}
|
||||
return result.join(', ');
|
||||
}
|
||||
|
||||
const toPretty = function (number: number) {
|
||||
if (isInteger(number)) {
|
||||
number = Math.floor(number);
|
||||
} else {
|
||||
number = parseFloat(number.toFixed(2));
|
||||
}
|
||||
return numberWithCommas(number);
|
||||
}
|
||||
|
||||
const showBattleResult = function (result: BattleResult) {
|
||||
$('#result_datetime').html(result.datetime);
|
||||
$('#result_warcnt').html(toPretty(result.avgWar));
|
||||
$('#result_phase').html(toPretty(result.phase));
|
||||
$('#result_killed').html(toPretty(result.killed));
|
||||
if (result.minKilled != result.maxKilled) {
|
||||
$('#result_maxKilled').html(toPretty(result.maxKilled));
|
||||
$('#result_minKilled').html(toPretty(result.minKilled));
|
||||
$('#result_varKilled').show();
|
||||
} else {
|
||||
$('#result_varKilled').hide();
|
||||
}
|
||||
$('#result_dead').html(toPretty(result.dead));
|
||||
if (result.minDead != result.maxDead) {
|
||||
$('#result_maxDead').html(toPretty(result.maxDead));
|
||||
$('#result_minDead').html(toPretty(result.minDead));
|
||||
$('#result_varDead').show();
|
||||
} else {
|
||||
$('#result_varDead').hide();
|
||||
}
|
||||
|
||||
$('#result_attackerRice').html(toPretty(result.attackerRice));
|
||||
$('#result_defenderRice').html(toPretty(result.defenderRice));
|
||||
$('#result_attackerSkills').html(parseSkillCount(result.attackerSkills));
|
||||
|
||||
$('.result_defenderSkills').detach();
|
||||
|
||||
const $summary = $('#battle_result_summary');
|
||||
|
||||
for (const [idx, defenderSkills] of Object.entries(result.defendersSkills)) {
|
||||
console.log(defenderSkills);
|
||||
const $result = $(`<tr class='result_defenderSkills'><th>수비자${parseInt(idx) + 1} 스킬</th><td></td></tr>`);
|
||||
$result.find('td').html(parseSkillCount(defenderSkills));
|
||||
$summary.append($result);
|
||||
}
|
||||
|
||||
$('#generalBattleResultLog').html(result.lastWarLog.generalBattleResultLog);
|
||||
$('#generalBattleDetailLog').html(result.lastWarLog.generalBattleDetailLog);
|
||||
|
||||
}
|
||||
|
||||
const beginBattle = function () {
|
||||
const data = extendAllDataForDB(exportAllData());
|
||||
console.log(data);
|
||||
$.ajax({
|
||||
type: 'post',
|
||||
url: 'j_simulate_battle.php',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
action: 'battle',
|
||||
query: JSON.stringify(data),
|
||||
}
|
||||
}).then(function (result) {
|
||||
console.log(result);
|
||||
if (!result.result) {
|
||||
alert(result.reason);
|
||||
return;
|
||||
}
|
||||
showBattleResult(result);
|
||||
|
||||
}, function () {
|
||||
alert('전투 개시 실패!');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const reorderDefender = function (defenderOrder: number[]) {
|
||||
for (const generalNo of defenderOrder) {
|
||||
|
||||
if (!(generalNo in defenderNoList)) {
|
||||
//음..?
|
||||
alert(`${generalNo}이 수비자 리스트에 없습니다. 버그인 듯 합니다.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
const $defenderObj = defenderNoList[generalNo];
|
||||
$defenderObj.detach();
|
||||
$defenderColumn.append($defenderObj);
|
||||
}
|
||||
}
|
||||
|
||||
const requestReorderDefender = function () {
|
||||
const data = extendAllDataForDB(exportAllData());
|
||||
console.log(data);
|
||||
$.ajax({
|
||||
type: 'post',
|
||||
url: 'j_simulate_battle.php',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
action: 'reorder',
|
||||
query: JSON.stringify(data),
|
||||
}
|
||||
}).then(function (result) {
|
||||
console.log(result);
|
||||
if (!result.result) {
|
||||
alert(result.reason);
|
||||
return;
|
||||
}
|
||||
reorderDefender(result.order);
|
||||
|
||||
}, function () {
|
||||
alert('재정렬 실패!');
|
||||
});
|
||||
}
|
||||
|
||||
let initGeneralList = false;
|
||||
|
||||
$('#importFromDB').on('click', function () {
|
||||
const generalID = $('#modalSelector').val();
|
||||
console.log(generalID);
|
||||
|
||||
|
||||
$.post({
|
||||
url: 'j_export_simulator_object.php',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
destGeneralID: generalID
|
||||
}
|
||||
}).then(function (data) {
|
||||
|
||||
if (!data.result) {
|
||||
alert(data.reason);
|
||||
return false;
|
||||
}
|
||||
|
||||
const $modal = $('#importModal');
|
||||
const $card = $modal.data('target');
|
||||
importGeneralInfo($card, data.general);
|
||||
|
||||
if(modalImport){
|
||||
modalImport.hide();
|
||||
}
|
||||
}, errUnknown);
|
||||
|
||||
|
||||
});
|
||||
|
||||
unwrap(document.querySelector('#importModal')).addEventListener('show.bs.modal', function () {
|
||||
if (!initGeneralList) {
|
||||
const $list = $('#modalSelector');
|
||||
|
||||
const addNation = function (generalList: GeneralInfo[], nationName: string, color: string) {
|
||||
|
||||
generalList.sort(function (lhs, rhs) {
|
||||
if (lhs.npc != rhs.npc) {
|
||||
return (lhs.npc ?? 0) - (rhs.npc ?? 0);
|
||||
}
|
||||
if (lhs.name < rhs.name) {
|
||||
return -1;
|
||||
}
|
||||
if (lhs.name > rhs.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
const $optGroup = $(`<optgroup label="${nationName}"></optgroup>`);
|
||||
$optGroup.css('background-color', color);
|
||||
$optGroup.css('color', isBrightColor(color) ? 'black' : 'white');
|
||||
|
||||
for (const general of generalList) {
|
||||
const $item = $(`<option value="${general.no}">${general.name}</option>`);
|
||||
if (general.npc) {
|
||||
$item.css('color', unwrap(getNpcColor(general.npc)));
|
||||
} else {
|
||||
$item.css('color', 'white');
|
||||
}
|
||||
$item.css('background-color', 'black');
|
||||
$optGroup.append($item);
|
||||
}
|
||||
$list.append($optGroup);
|
||||
}
|
||||
$.post({
|
||||
url: 'j_get_basic_general_list.php',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
req: 2,
|
||||
}
|
||||
}).then(function (data: BasicGeneralListResponse | InvalidResponse) {
|
||||
if (!data.result) {
|
||||
alert(data.reason);
|
||||
if(modalImport){
|
||||
modalImport.hide();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const nations = data.nation;
|
||||
const myNationID = data.nationID;
|
||||
|
||||
$list.empty();
|
||||
//자국 먼저
|
||||
|
||||
if (0 in data.list) {
|
||||
nations[0] = {
|
||||
nation: 0,
|
||||
name: '재야',
|
||||
color: '#000000'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
addNation(combineArray(data.list[myNationID], data.column) as GeneralInfo[], nations[myNationID].name, nations[myNationID].color);
|
||||
|
||||
for (const nationID of Object.keys(data.list)) {
|
||||
if (parseInt(nationID) == myNationID) {
|
||||
continue;
|
||||
}
|
||||
addNation(combineArray(data.list[nationID], data.column) as GeneralInfo[], nations[nationID].name, nations[nationID].color);
|
||||
}
|
||||
initGeneralList = true;
|
||||
}, errUnknown);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
initBasicEvent();
|
||||
$attackerCard.append($generalForm.clone(true, true));
|
||||
addDefender();
|
||||
|
||||
$('.btn-begin_battle').on('click', function () {
|
||||
beginBattle();
|
||||
});
|
||||
|
||||
$('.btn-reorder_defender').on('click', function () {
|
||||
requestReorderDefender();
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import "@scss/hallOfFame.scss";
|
||||
|
||||
import { auto500px } from "./util/auto500px";
|
||||
import { insertCustomCSS } from "./util/customCSS";
|
||||
import { htmlReady } from "./util/htmlReady";
|
||||
|
||||
auto500px();
|
||||
htmlReady(() => {
|
||||
insertCustomCSS();
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import $ from 'jquery';
|
||||
import { unwrap_any } from '@util/unwrap_any';
|
||||
import { SammoAPI } from './SammoAPI';
|
||||
|
||||
declare const staticValues: {
|
||||
bettingID: number;
|
||||
}
|
||||
|
||||
$(function ($) {
|
||||
$('.submitBtn').on('click', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const $this = $(this);
|
||||
const target = parseInt($this.data('target'));
|
||||
const amount = parseInt(unwrap_any<string>($(`#target_${target}`).val()));
|
||||
|
||||
try {
|
||||
await SammoAPI.Betting.Bet({
|
||||
bettingID: staticValues.bettingID,
|
||||
bettingType: [target],
|
||||
amount: amount,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`베팅을 실패했습니다: ${e}`);
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
location.reload();
|
||||
return;
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,247 @@
|
||||
import $ from 'jquery';
|
||||
import axios from 'axios';
|
||||
import { convertFormData } from '@util/convertFormData';
|
||||
import type { InvalidResponse } from '@/defs';
|
||||
import { unwrap_any } from '@util/unwrap_any';
|
||||
import { setAxiosXMLHttpRequest } from '@util/setAxiosXMLHttpRequest';
|
||||
import * as JosaUtil from '@/util/JosaUtil';
|
||||
import 'bootstrap';
|
||||
import 'select2/dist/js/select2.full.js'
|
||||
|
||||
type GeneralSelectorItem = {
|
||||
id: string|number,
|
||||
text: string,
|
||||
selected: boolean,
|
||||
}
|
||||
|
||||
declare const candidateAmbassadors: GeneralSelectorItem[];
|
||||
declare const candidateAuditors: GeneralSelectorItem[];
|
||||
|
||||
async function changePermission(isAmbassador: boolean, rawGeneralList: GeneralSelectorItem[]) {
|
||||
console.log(isAmbassador);
|
||||
console.log(rawGeneralList);
|
||||
|
||||
const generalList: number[] = [];
|
||||
for (const rawGen of rawGeneralList) {
|
||||
generalList.push(parseInt(rawGen.id as string));
|
||||
}
|
||||
|
||||
let result: InvalidResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_general_set_permission.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
isAmbassador: isAmbassador,
|
||||
genlist: generalList
|
||||
})
|
||||
});
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(`변경하지 못했습니다 : ${result.reason}`);
|
||||
return;
|
||||
}
|
||||
|
||||
alert('변경했습니다.');
|
||||
location.reload();
|
||||
}
|
||||
|
||||
$(function () {
|
||||
setAxiosXMLHttpRequest();
|
||||
|
||||
$('#selectAmbassador').select2({
|
||||
theme: 'bootstrap4',
|
||||
placeholder: "",
|
||||
allowClear: true,
|
||||
language: "ko",
|
||||
width: '300px',
|
||||
maximumSelectionLength: 2,
|
||||
containerCss: {
|
||||
display: "inline-block !important;",
|
||||
color: 'white !important'
|
||||
},
|
||||
data: candidateAmbassadors,
|
||||
//containerCssClass: 'simple-select2-align-center bg-secondary text-secondary',
|
||||
dropdownCssClass: 'simple-select2-align-center bg-secondary text-secondary',
|
||||
});
|
||||
|
||||
$('#selectAuditor').select2({
|
||||
theme: 'bootstrap4',
|
||||
placeholder: "",
|
||||
allowClear: true,
|
||||
language: "ko",
|
||||
width: '300px',
|
||||
maximumSelectionLength: 2,
|
||||
containerCss: {
|
||||
display: "inline-block !important;",
|
||||
color: 'white !important'
|
||||
},
|
||||
data: candidateAuditors,
|
||||
//containerCssClass: 'simple-select2-align-center bg-secondary text-secondary',
|
||||
dropdownCssClass: 'simple-select2-align-center bg-secondary text-secondary',
|
||||
});
|
||||
|
||||
$('#changeAmbassador').on('click', async function (e) {
|
||||
e.preventDefault();
|
||||
if (!confirm('외교권자를 변경할까요?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
await changePermission(true, $('#selectAmbassador').select2('data'));
|
||||
return;
|
||||
});
|
||||
|
||||
$('#changeAuditor').on('click', async function (e) {
|
||||
e.preventDefault();
|
||||
if (!confirm('조언자를 변경할까요?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
await changePermission(false, $('#selectAuditor').select2('data'));
|
||||
return;
|
||||
});
|
||||
|
||||
$('#btn_kick').on('click', async function (e) {
|
||||
e.preventDefault();
|
||||
const $kickSelect = $('#genlist_kick option:selected');
|
||||
const generalID = $kickSelect.val();
|
||||
if (!generalID) {
|
||||
alert('장수를 선택해주세요');
|
||||
return;
|
||||
}
|
||||
const generalName = $kickSelect.data('name');
|
||||
const josaUl = JosaUtil.pick(generalName, '을');
|
||||
if (!confirm(`${generalName}${josaUl} 추방하시겠습니까?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let result: InvalidResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_myBossInfo.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
action: '추방',
|
||||
destGeneralID: generalID
|
||||
})
|
||||
});
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(`추방하지 못했습니다. : ${result.reason}`);
|
||||
return;
|
||||
}
|
||||
alert(`${generalName}${josaUl} 추방했습니다.`);
|
||||
location.reload();
|
||||
});
|
||||
|
||||
$('.btn_appoint').on('click', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const $btn = $(this);
|
||||
const officerLevel = $btn.data('officer_level');
|
||||
const officerLevelText = $btn.data('officer_level_text');
|
||||
let cityID = 0;
|
||||
let cityName = '_';
|
||||
const $generalSelect = $(`#genlist_${officerLevel} option:selected`);
|
||||
const $citySelect = $(`#citylist_${officerLevel} option:selected`);
|
||||
|
||||
const generalID = parseInt(unwrap_any<string>($generalSelect.val()));
|
||||
const generalName = $generalSelect.data('name');
|
||||
const generalOfficerLevel = $generalSelect.data('officer_level');
|
||||
|
||||
|
||||
if (officerLevel >= 5) {
|
||||
if (generalID == 0) {
|
||||
if (!confirm(`${officerLevelText}직을 비우시겠습니까?`)) {
|
||||
return false;
|
||||
}
|
||||
} else if (generalOfficerLevel >= 5) {
|
||||
const josaUl = JosaUtil.pick(generalName, '을');
|
||||
if (!confirm(`이미 수뇌인 ${generalName}${josaUl} ${officerLevelText}직에 임명하시겠습니까?`)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
const josaUl = JosaUtil.pick(generalName, '을');
|
||||
if (!confirm(`${generalName}${josaUl} ${officerLevelText}직에 임명하시겠습니까?`)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cityID = parseInt(unwrap_any<string>($citySelect.val()));
|
||||
if (!cityID) {
|
||||
alert('도시를 선택해주세요');
|
||||
return false;
|
||||
}
|
||||
cityName = $citySelect.find('option:selected .name_field').text();
|
||||
|
||||
if (generalID == 0) {
|
||||
if (!confirm(`${cityName} ${officerLevelText}직을 비우시겠습니까?`)) {
|
||||
return false;
|
||||
}
|
||||
} else if (generalOfficerLevel >= 5) {
|
||||
const josaUl = JosaUtil.pick(generalName, '을');
|
||||
if (!confirm(`수뇌인 ${generalName}${josaUl} ${cityName} ${officerLevelText}직에 임명하시겠습니까?`)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
const josaUl = JosaUtil.pick(generalName, '을');
|
||||
if (!confirm(`${generalName}${josaUl} ${cityName} ${officerLevelText}직에 임명하시겠습니까?`)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result: InvalidResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_myBossInfo.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
action: '임명',
|
||||
destGeneralID: generalID,
|
||||
destCityID: cityID,
|
||||
officerLevel: officerLevel
|
||||
})
|
||||
});
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
if (!result.result) {
|
||||
alert(`임명하지 못했습니다. : ${result.reason}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (generalID) {
|
||||
const josaUl = JosaUtil.pick(generalName, '을');
|
||||
alert(`${generalName}${josaUl} 임명했습니다.`);
|
||||
} else {
|
||||
alert('관직을 비웠습니다.');
|
||||
}
|
||||
|
||||
location.reload();
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"ingame": {
|
||||
"troop": "troop.ts",
|
||||
"map": "map.ts",
|
||||
"install_db": "install_db.ts",
|
||||
"install": "install.ts",
|
||||
"battle_simulator": "battle_simulator.ts",
|
||||
"battleCenter": "battleCenter.ts",
|
||||
"bestGeneral": "bestGeneral.ts",
|
||||
"recent_map": "recent_map.ts",
|
||||
"select_npc": "select_npc.ts",
|
||||
"betting": "betting.ts",
|
||||
"bossInfo": "bossInfo.ts",
|
||||
"myPage": "myPage.ts",
|
||||
"extExpandCity": "extExpandCity.ts",
|
||||
"diplomacy": "diplomacy.ts",
|
||||
"currentCity": "currentCity.ts",
|
||||
"hallOfFame": "hallOfFame.ts",
|
||||
"history": "history.ts",
|
||||
"select_general_from_pool": "select_general_from_pool.ts",
|
||||
"extKingdoms": "extKingdoms.ts",
|
||||
"common": "common_deprecated.ts"
|
||||
},
|
||||
"ingame_vue": {
|
||||
"v_auction": "v_auction.ts",
|
||||
"v_inheritPoint": "v_inheritPoint.ts",
|
||||
"v_board": "v_board.ts",
|
||||
"v_cachedMap": "v_cachedMap",
|
||||
"v_chiefCenter": "v_chiefCenter.ts",
|
||||
"v_NPCControl": "v_NPCControl.ts",
|
||||
"v_join": "v_join.ts",
|
||||
"v_main": "v_main.ts",
|
||||
"v_history": "v_history.ts",
|
||||
"v_nationStratFinan": "v_nationStratFinan.ts",
|
||||
"v_processing": "v_processing.ts",
|
||||
"v_nationBetting": "v_nationBetting.ts",
|
||||
"v_nationGeneral": "v_nationGeneral.ts",
|
||||
"v_globalDiplomacy": "v_globalDiplomacy",
|
||||
"v_vote": "v_vote.ts"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { exportWindow } from "@util/exportWindow";
|
||||
import { errUnknown } from "@/common_legacy";
|
||||
import { initTooltip } from "@/legacy/initTooltip";
|
||||
import { activateFlip } from "@/legacy/activateFlip";
|
||||
import { isBrightColor } from "@util/isBrightColor";
|
||||
import { getIconPath } from "@util/getIconPath";
|
||||
import { mb_strwidth } from "@util/mb_strwidth";
|
||||
import { TemplateEngine } from "@util/TemplateEngine";
|
||||
import { escapeHtml } from "@/legacy/escapeHtml";
|
||||
import { nl2br } from "@util/nl2br";
|
||||
import jQuery from "jquery";
|
||||
|
||||
import "@scss/common_legacy.scss";
|
||||
import { insertCustomCSS } from "./util/customCSS";
|
||||
import { htmlReady } from "./util/htmlReady";
|
||||
|
||||
exportWindow(jQuery, '$');
|
||||
exportWindow(jQuery, 'jQuery');
|
||||
|
||||
htmlReady(function(){
|
||||
initTooltip();
|
||||
activateFlip();
|
||||
insertCustomCSS();
|
||||
})
|
||||
|
||||
/**
|
||||
* {0}, {1}, {2}형태로 포맷해주는 함수
|
||||
*/
|
||||
exportWindow(function(this:string, ...args:(string|number)[]){
|
||||
return this.replace(/{(\d+)}/g, function (match, number) {
|
||||
return (typeof args[number] != 'undefined') ? args[number].toString() : match;
|
||||
});
|
||||
}, 'format', String.prototype);
|
||||
exportWindow(escapeHtml, 'escapeHtml');
|
||||
exportWindow(mb_strwidth, 'mb_strwidth');
|
||||
exportWindow(isBrightColor, 'isBrightColor');
|
||||
exportWindow(TemplateEngine, 'TemplateEngine');
|
||||
exportWindow(getIconPath, 'getIconPath');
|
||||
exportWindow(activateFlip, 'activateFlip');
|
||||
exportWindow(errUnknown, 'errUnknown');
|
||||
exportWindow(nl2br, 'nl2br');
|
||||
exportWindow(initTooltip, 'initTooltip');
|
||||
@@ -0,0 +1,99 @@
|
||||
|
||||
/**
|
||||
* object의 array를 id를 key로 삼는 object로 재 변환
|
||||
*/
|
||||
export function convertDictById<K extends string | number, T extends { id: K }>(arr: ArrayLike<T>): Record<K, T> {
|
||||
const result: Record<string | number, T> = {};
|
||||
for (const v of Object.values(arr)) {
|
||||
result[v.id] = v;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* array를 set 형태의 object로 변환
|
||||
*/
|
||||
export function convertSet<K extends string | number>(arr: ArrayLike<K>): Record<K, true> {
|
||||
const result: Record<string | number, true> = {};
|
||||
for (const v of Object.values(arr)) {
|
||||
result[v] = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function stringFormat(text: string, ...args: (string | number)[]): string {
|
||||
return text.replace(/{(\d+)}/g, function (match, number) {
|
||||
return (typeof args[number] != 'undefined') ? args[number].toString() : match;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 게임내에서 지원하는 color type만 선택할 수 있도록 해주는 함수
|
||||
* @param {string} color #AAAAAA 또는 AAAAAA 형태로 작성된 RGB hex color string
|
||||
* @returns {string}
|
||||
*/
|
||||
export function convColorValue(color: string): string {
|
||||
if (color.charAt(0) == '#') {
|
||||
color = color.substr(1);
|
||||
}
|
||||
color = color.toUpperCase();
|
||||
|
||||
const colorBase = new Set([
|
||||
'000080', '0000FF', '008000', '008080', '00BFFF', '00FF00', '00FFFF', '20B2AA',
|
||||
'2E8B57', '483D8B', '6495ED', '7B68EE', '7CFC00', '7FFFD4', '800000', '800080',
|
||||
'808000', '87CEEB', 'A0522D', 'A9A9A9', 'AFEEEE', 'BA55D3', 'E0FFFF', 'F5F5DC',
|
||||
'FF0000', 'FF00FF', 'FF6347', 'FFA500', 'FFC0CB', 'FFD700', 'FFDAB9', 'FFFF00',
|
||||
'FFFFFF'
|
||||
]);
|
||||
|
||||
if (!colorBase.has(color)) {
|
||||
return '000000';
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
|
||||
//linkify가 불러와 있어야함
|
||||
declare global {
|
||||
interface Window {
|
||||
linkifyStr: (v: string, k?: Record<string, string | number>) => string;
|
||||
}
|
||||
}
|
||||
export function combineObject<K extends string, V>(item: V[], columnList: K[]): Record<K, V> {
|
||||
const newItem: Record<string, V> = {};
|
||||
for (const columnIdx in columnList) {
|
||||
const columnName = columnList[columnIdx];
|
||||
newItem[columnName] = item[columnIdx];
|
||||
}
|
||||
return newItem;
|
||||
}
|
||||
|
||||
export function errUnknown(): void {
|
||||
alert('작업을 실패했습니다.');
|
||||
}
|
||||
|
||||
/*
|
||||
function br2nl (text) {
|
||||
return text.replace(/<\s*\/?br\s*[\/]?>/gi, '\n');
|
||||
}
|
||||
*/
|
||||
|
||||
export function getNpcColor(npcType: number): 'skyblue' | 'cyan' | 'deepskyblue' | 'darkcyan' | 'mediumaquamarine' | undefined {
|
||||
if (npcType == 6){
|
||||
return 'mediumaquamarine';
|
||||
}
|
||||
if (npcType == 5){
|
||||
return 'darkcyan';
|
||||
}
|
||||
if (npcType == 4){
|
||||
return 'deepskyblue';
|
||||
}
|
||||
if (npcType >= 2) {
|
||||
return 'cyan';
|
||||
}
|
||||
if (npcType == 1) {
|
||||
return 'skyblue';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
interface Window {
|
||||
pathConfig: {
|
||||
root: string,
|
||||
sharedIcon: string,
|
||||
gameImage: string,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
<template>
|
||||
<div class="bg0">
|
||||
<div class="bg2">거래장</div>
|
||||
<div style="background-color: orange">쌀 구매</div>
|
||||
<div class="auctionItem gx-0">
|
||||
<div class="idx">번호</div>
|
||||
<div class="host">판매자</div>
|
||||
<div class="amount">수량</div>
|
||||
<div class="highestBidder">입찰자</div>
|
||||
<div class="highestBid">입찰가</div>
|
||||
<div class="bidRatio">단가</div>
|
||||
<div class="finishBid">마감가</div>
|
||||
<div class="closeDate">거래 종료</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="auction of buyRice"
|
||||
:key="auction.id"
|
||||
class="auctionItem gx-0"
|
||||
@click="selectedBuyRiceAuction = auction"
|
||||
>
|
||||
<div class="idx f_tnum">{{ auction.id }}</div>
|
||||
<div class="host">{{ auction.hostName }}</div>
|
||||
<div class="amount f_tnum">쌀 {{ auction.amount.toLocaleString() }}</div>
|
||||
<div class="highestBidder">{{ auction.highestBid?.generalName ?? "-" }}</div>
|
||||
<div :class="['highestBid f_tnum', auction.highestBid ? '' : 'noBid']">
|
||||
금 {{ (auction.highestBid?.amount ?? auction.startBidAmount).toLocaleString() }}
|
||||
</div>
|
||||
<div class="bidRatio f_tnum">
|
||||
{{ auction.highestBid ? (auction.highestBid.amount / auction.amount).toFixed(2) : "-" }}
|
||||
</div>
|
||||
<div class="finishBid f_tnum">금 {{ auction.finishBidAmount.toLocaleString() }}</div>
|
||||
<div class="closeDate f_tnum">{{ cutDateTime(auction.closeDate) }}</div>
|
||||
</div>
|
||||
<div v-if="selectedBuyRiceAuction !== undefined" class="row gx-1">
|
||||
<div class="offset-1 col-4 offset-md-3 col-md-2 align-self-center f_tnum text-end">
|
||||
{{ selectedBuyRiceAuction.id }}번 쌀 {{ selectedBuyRiceAuction.amount }} 경매에 금
|
||||
</div>
|
||||
<div class="col-3 col-md-2">
|
||||
<NumberInputWithInfo
|
||||
v-model="bidAmountBuyRiceAuction"
|
||||
:int="true"
|
||||
:min="selectedBuyRiceAuction.startBidAmount"
|
||||
:max="selectedBuyRiceAuction.finishBidAmount"
|
||||
:step="10"
|
||||
></NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col-2 col-md-1 d-grid"><BButton @click="bidBuyRiceAuction">입찰</BButton></div>
|
||||
</div>
|
||||
|
||||
<div style="background-color: skyblue">쌀 판매</div>
|
||||
<div class="auctionItem gx-0">
|
||||
<div class="idx">번호</div>
|
||||
<div class="host">판매자</div>
|
||||
<div class="amount">수량</div>
|
||||
<div class="highestBidder">입찰자</div>
|
||||
<div class="highestBid">입찰가</div>
|
||||
<div class="bidRatio">단가</div>
|
||||
<div class="finishBid">마감가</div>
|
||||
<div class="closeDate">거래 종료</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="auction of sellRice"
|
||||
:key="auction.id"
|
||||
class="auctionItem gx-0"
|
||||
@click="selectedSellRiceAuction = auction"
|
||||
>
|
||||
<div class="idx f_tnum">{{ auction.id }}</div>
|
||||
<div class="host">{{ auction.hostName }}</div>
|
||||
<div class="amount f_tnum">금{{ auction.amount.toLocaleString() }}</div>
|
||||
<div class="highestBidder">{{ auction.highestBid?.generalName ?? "-" }}</div>
|
||||
<div :class="['highestBid f_tnum', auction.highestBid ? '' : 'noBid']">
|
||||
쌀 {{ (auction.highestBid?.amount ?? auction.startBidAmount).toLocaleString() }}
|
||||
</div>
|
||||
<div class="bidRatio f_tnum">
|
||||
{{ auction.highestBid ? (auction.highestBid.amount / auction.amount).toFixed(2) : "-" }}
|
||||
</div>
|
||||
<div class="finishBid f_tnum">쌀 {{ auction.finishBidAmount.toLocaleString() }}</div>
|
||||
<div class="closeDate f_tnum">{{ cutDateTime(auction.closeDate) }}</div>
|
||||
</div>
|
||||
<div v-if="selectedSellRiceAuction !== undefined" class="row gx-1">
|
||||
<div class="offset-1 col-4 offset-md-3 col-md-2 align-self-center f_tnum text-end">
|
||||
{{ selectedSellRiceAuction.id }}번 금 {{ selectedSellRiceAuction.amount }} 경매에 쌀
|
||||
</div>
|
||||
<div class="col-3 col-md-2">
|
||||
<NumberInputWithInfo
|
||||
v-model="bidAmountSellRiceAuction"
|
||||
:int="true"
|
||||
:min="selectedSellRiceAuction.startBidAmount"
|
||||
:max="selectedSellRiceAuction.finishBidAmount"
|
||||
:step="10"
|
||||
></NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col-2 col-md-1 d-grid"><BButton @click="bidSellRiceAuction">입찰</BButton></div>
|
||||
</div>
|
||||
|
||||
<div>경매 등록</div>
|
||||
<div class="row gx-1">
|
||||
<div class="col-2 offset-md-2 col-md-1">
|
||||
매물<br />
|
||||
<BButtonGroup>
|
||||
<BButton :pressed="openAuctionInfo.type == 'buyRice'" @click="openAuctionInfo.type = 'buyRice'"> 쌀 </BButton>
|
||||
<BButton :pressed="openAuctionInfo.type == 'sellRice'" @click="openAuctionInfo.type = 'sellRice'">
|
||||
금
|
||||
</BButton>
|
||||
</BButtonGroup>
|
||||
</div>
|
||||
|
||||
<div class="col col-md-2">
|
||||
수량 ({{ openAuctionInfo.type == "buyRice" ? "쌀" : "금" }})<br />
|
||||
<NumberInputWithInfo
|
||||
v-model="openAuctionInfo.amount"
|
||||
:int="true"
|
||||
:min="100"
|
||||
:max="10000"
|
||||
:step="10"
|
||||
></NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col-2 col-md-1">
|
||||
기간(턴)
|
||||
<NumberInputWithInfo
|
||||
v-model="openAuctionInfo.closeTurnCnt"
|
||||
:int="true"
|
||||
:min="3"
|
||||
:max="24"
|
||||
:step="1"
|
||||
></NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col col-md-2">
|
||||
시작가 ({{ openAuctionInfo.type == "buyRice" ? "금" : "쌀" }})
|
||||
<NumberInputWithInfo
|
||||
v-model="openAuctionInfo.startBidAmount"
|
||||
:int="true"
|
||||
:min="100"
|
||||
:max="10000"
|
||||
:step="10"
|
||||
></NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col col-md-2">
|
||||
마감가 ({{ openAuctionInfo.type == "buyRice" ? "금" : "쌀" }})
|
||||
<NumberInputWithInfo
|
||||
v-model="openAuctionInfo.finishBidAmount"
|
||||
:int="true"
|
||||
:min="100"
|
||||
:max="10000"
|
||||
:step="10"
|
||||
></NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col-1 d-grid">
|
||||
<BButton @click="openAuction">등록</BButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>이전 경매(최근 20건)</div>
|
||||
<div v-for="(log, idx) in recentLogs" :key="idx">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-html="formatLog(log)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { BasicResourceAuctionInfo } from "@/defs/API/Auction";
|
||||
import { SammoAPI } from "@/SammoAPI";
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
import { useToast, BButtonGroup, BButton } from "bootstrap-vue-3";
|
||||
import { isString } from "lodash";
|
||||
import { onMounted, reactive, ref, watch } from "vue";
|
||||
import NumberInputWithInfo from "@/components/NumberInputWithInfo.vue";
|
||||
import { formatLog } from "@/utilGame/formatLog";
|
||||
const toasts = unwrap(useToast());
|
||||
|
||||
const buyRice = ref<BasicResourceAuctionInfo[]>([]);
|
||||
const sellRice = ref<BasicResourceAuctionInfo[]>([]);
|
||||
const recentLogs = ref<string[]>([]);
|
||||
|
||||
const selectedBuyRiceAuction = ref<BasicResourceAuctionInfo | undefined>(undefined);
|
||||
const bidAmountBuyRiceAuction = ref<number>(0);
|
||||
|
||||
watch(selectedBuyRiceAuction, (auction) => {
|
||||
if (!auction) {
|
||||
return;
|
||||
}
|
||||
bidAmountBuyRiceAuction.value = auction.highestBid ? auction.highestBid.amount : auction.startBidAmount;
|
||||
});
|
||||
|
||||
function cutDateTime(dateTime: string, showSecond = false) {
|
||||
if (showSecond) {
|
||||
return dateTime.substring(5, 19);
|
||||
}
|
||||
return dateTime.substring(5, 16);
|
||||
}
|
||||
|
||||
async function bidBuyRiceAuction() {
|
||||
if (selectedBuyRiceAuction.value === undefined) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await SammoAPI.Auction.BidBuyRiceAuction({
|
||||
auctionID: selectedBuyRiceAuction.value.id,
|
||||
amount: bidAmountBuyRiceAuction.value,
|
||||
});
|
||||
toasts.success({
|
||||
title: "입찰 완료",
|
||||
body: `입찰했습니다.`,
|
||||
});
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selectedSellRiceAuction = ref<BasicResourceAuctionInfo | undefined>(undefined);
|
||||
const bidAmountSellRiceAuction = ref<number>(0);
|
||||
|
||||
watch(selectedSellRiceAuction, (auction) => {
|
||||
if (!auction) {
|
||||
return;
|
||||
}
|
||||
bidAmountSellRiceAuction.value = auction.highestBid ? auction.highestBid.amount : auction.startBidAmount;
|
||||
});
|
||||
|
||||
async function bidSellRiceAuction() {
|
||||
if (selectedSellRiceAuction.value === undefined) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await SammoAPI.Auction.BidSellRiceAuction({
|
||||
auctionID: selectedSellRiceAuction.value.id,
|
||||
amount: bidAmountSellRiceAuction.value,
|
||||
});
|
||||
toasts.success({
|
||||
title: "입찰 완료",
|
||||
body: `입찰했습니다.`,
|
||||
});
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type openAuctionT = {
|
||||
type: "buyRice" | "sellRice";
|
||||
amount: number;
|
||||
startBidAmount: number;
|
||||
finishBidAmount: number;
|
||||
closeTurnCnt: number;
|
||||
};
|
||||
|
||||
const openAuctionInfo = reactive<openAuctionT>({
|
||||
type: "buyRice",
|
||||
amount: 1000,
|
||||
startBidAmount: 500,
|
||||
finishBidAmount: 2000,
|
||||
closeTurnCnt: 24,
|
||||
});
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
const result = await SammoAPI.Auction.GetActiveResourceAuctionList();
|
||||
buyRice.value = result.buyRice;
|
||||
sellRice.value = result.sellRice;
|
||||
recentLogs.value = result.recentLogs;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function openAuction() {
|
||||
const { type, amount, startBidAmount, finishBidAmount, closeTurnCnt } = openAuctionInfo;
|
||||
|
||||
try {
|
||||
const apiCall = type === "buyRice" ? SammoAPI.Auction.OpenBuyRiceAuction : SammoAPI.Auction.OpenSellRiceAuction;
|
||||
const result = await apiCall({
|
||||
amount,
|
||||
startBidAmount,
|
||||
finishBidAmount,
|
||||
closeTurnCnt,
|
||||
});
|
||||
toasts.success({
|
||||
title: "성공",
|
||||
body: `${result.auctionID}번 경매로 등록되었습니다.`,
|
||||
});
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
void refresh();
|
||||
console.log("mounted");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@scss/common/break_500px.scss";
|
||||
|
||||
.auctionItem {
|
||||
display: grid;
|
||||
text-align: center;
|
||||
|
||||
> div {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.noBid {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
border-bottom: solid gray 1px;
|
||||
}
|
||||
|
||||
@include media-500px {
|
||||
.auctionItem {
|
||||
grid-template-columns: 1fr 3fr 3fr 1fr 2fr 2fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
|
||||
.idx {
|
||||
grid-column: 1 / 2;
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
|
||||
.host {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
|
||||
.amount {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 2/ 3;
|
||||
}
|
||||
|
||||
.highestBidder {
|
||||
grid-column: 3 / 4;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
|
||||
.highestBid {
|
||||
grid-column: 3 / 4;
|
||||
grid-row: 2 / 3;
|
||||
}
|
||||
|
||||
.bidRatio {
|
||||
grid-column: 4 / 5;
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
|
||||
.finishBid {
|
||||
grid-column: 5 / 6;
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
|
||||
.closeDate {
|
||||
grid-column: 6 / 7;
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-1000px {
|
||||
.auctionItem {
|
||||
grid-template-columns: 1fr 2fr 2fr 2fr 2fr 1fr 3fr 2fr;
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<div class="bg0">
|
||||
<div>
|
||||
내 가명: <span class="isMe">{{ obfuscatedName }}</span>
|
||||
</div>
|
||||
<template v-if="currentAuction !== undefined">
|
||||
<div class="bg2">경매 {{ currentAuction.auction.id }}번 상세</div>
|
||||
<div class="row gx-0 text-center">
|
||||
<div class="col-2 col-md-1 bg1">경매명</div>
|
||||
<div class="col-4 col-md-2">{{ currentAuction.auction.title }}</div>
|
||||
|
||||
<div class="col-2 col-md-1 bg1">주최자(익명)</div>
|
||||
<div :class="['col-4 col-md-2', currentAuction.auction.isCallerHost ? 'isMe' : '']">
|
||||
{{ currentAuction.auction.hostName }}
|
||||
</div>
|
||||
|
||||
<div class="col-2 col-md-1 bg1">종료일시</div>
|
||||
<div class="col-4 col-md-2 f_tnum">{{ cutDateTime(currentAuction.auction.closeDate, true) }}</div>
|
||||
|
||||
<div class="col-2 col-md-1 bg1">최대지연</div>
|
||||
<div class="col-4 col-md-2 f_tnum">
|
||||
{{ cutDateTime(currentAuction.auction.availableLatestBidCloseDate, true) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg1">입찰자 목록</div>
|
||||
<div
|
||||
class="row gx-0 px-md-5 text-center"
|
||||
:style="{
|
||||
borderBottom: 'solid 1px white',
|
||||
}"
|
||||
>
|
||||
<div class="col-4 offset-md-2 col-md-3">입찰자</div>
|
||||
<div class="col-4 col-md-2 text-end px-5">입찰포인트</div>
|
||||
<div class="col-4 col-md-3">시각</div>
|
||||
</div>
|
||||
<div v-for="bidder of currentAuction.bidList" :key="bidder.amount" class="row gx-0 px-md-5 text-center">
|
||||
<div :class="['col-4 offset-md-2 col-md-3', bidder.isCallerHighestBidder ? 'isMe' : '']">{{ bidder.generalName }}</div>
|
||||
<div class="col-4 col-md-2 text-end px-5 f_tnum">{{ bidder.amount.toLocaleString() }}</div>
|
||||
<div class="col-4 col-md-3 f_tnum">{{ cutDateTime(bidder.date) }}</div>
|
||||
</div>
|
||||
<div class="bg1">입찰하기</div>
|
||||
<div class="row">
|
||||
<label class="col-5 offset-md-3 col-md-3 col-form-label text-center">유산포인트 (잔여: {{ currentAuction.remainPoint.toLocaleString() }}포인트)</label>
|
||||
<div class="col-4 col-md-2">
|
||||
<NumberInputWithInfo
|
||||
v-model="bidAmount"
|
||||
:int="true"
|
||||
:min="currentAuction.bidList[0].amount"
|
||||
:max="currentAuction.remainPoint"
|
||||
title=""
|
||||
:step="1"
|
||||
></NumberInputWithInfo>
|
||||
</div>
|
||||
<div class="col-3 col-md-1 d-grid"><BButton @click="bidAuction">입찰</BButton></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="bg1">진행중인 경매 목록</div>
|
||||
<div
|
||||
class="row gx-0 text-center"
|
||||
:style="{
|
||||
borderBottom: 'solid 1px white',
|
||||
}"
|
||||
>
|
||||
<div class="col-1">번호</div>
|
||||
<div class="col-4">경매명</div>
|
||||
<div class="col-1">주최자</div>
|
||||
<div class="col-2">종료일시</div>
|
||||
<div class="col-1">연장</div>
|
||||
<div class="col-1">1순위</div>
|
||||
<div class="col-2 text-end px-2">포인트</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="[auctionID, auction] of ongoingAuctionList"
|
||||
:key="auctionID"
|
||||
class="row gx-0 text-center clickableRow"
|
||||
@click="currentAuctionID = auctionID"
|
||||
>
|
||||
<div class="col-1">{{ auction.id }}</div>
|
||||
<div class="col-4">{{ auction.title }}</div>
|
||||
<div :class="['col-1', auction.isCallerHost ? 'isMe' : '']">{{ auction.hostName }}</div>
|
||||
<div class="col-2 f_tnum">{{ cutDateTime(auction.closeDate) }}</div>
|
||||
<div class="col-1">{{ auction.remainCloseDateExtensionCnt > 0 ? "남음" : "소진" }}</div>
|
||||
<div :class="['col-1', auction.highestBid.isCallerHighestBidder ? 'isMe' : '']">
|
||||
{{ auction.highestBid.generalName }}
|
||||
</div>
|
||||
<div class="col-2 text-end px-2 f_tnum">{{ auction.highestBid.amount.toLocaleString() }}</div>
|
||||
</div>
|
||||
<div class="bg1">종료된 경매 목록</div>
|
||||
<div
|
||||
class="row gx-0 text-center"
|
||||
:style="{
|
||||
borderBottom: 'solid 1px white',
|
||||
}"
|
||||
>
|
||||
<div class="col-1">번호</div>
|
||||
<div class="col-4">경매명</div>
|
||||
<div class="col-1">주최자</div>
|
||||
<div class="col-2">종료일시</div>
|
||||
<div class="col-1">연장</div>
|
||||
<div class="col-1">1순위</div>
|
||||
<div class="col-2 text-end px-2">포인트</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="[auctionID, auction] of finishedAuctionList"
|
||||
:key="auctionID"
|
||||
class="row gx-0 text-center clickableRow"
|
||||
@click="currentAuctionID = auctionID"
|
||||
>
|
||||
<div class="col-1">{{ auction.id }}</div>
|
||||
<div class="col-4">{{ auction.title }}</div>
|
||||
<div :class="['col-1', auction.isCallerHost ? 'isMe' : '']">{{ auction.hostName }}</div>
|
||||
<div class="col-2 f_tnum">{{ cutDateTime(auction.closeDate) }}</div>
|
||||
<div class="col-1">{{ auction.remainCloseDateExtensionCnt > 0 ? "남음" : "소진" }}</div>
|
||||
<div :class="['col-1', auction.highestBid.isCallerHighestBidder ? 'isMe' : '']">
|
||||
{{ auction.highestBid.generalName }}
|
||||
</div>
|
||||
<div class="col-2 text-end px-2 f_tnum">{{ auction.highestBid.amount.toLocaleString() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { UniqueItemAuctionDetail, UniqueItemAuctionList } from "@/defs/API/Auction";
|
||||
import { SammoAPI } from "@/SammoAPI";
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
import { useToast, BButton } from "bootstrap-vue-3";
|
||||
import { isString } from "lodash";
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import NumberInputWithInfo from "@/components/NumberInputWithInfo.vue";
|
||||
|
||||
type AuctionItemInfo = UniqueItemAuctionList["list"][0];
|
||||
|
||||
const currentAuctionID = ref<number>();
|
||||
const currentAuction = ref<UniqueItemAuctionDetail | undefined>(undefined);
|
||||
const bidAmount = ref<number>(5000);
|
||||
|
||||
async function refreshDetail() {
|
||||
if (currentAuctionID.value === undefined) {
|
||||
return;
|
||||
}
|
||||
const auctionID = currentAuctionID.value;
|
||||
try {
|
||||
currentAuction.value = await SammoAPI.Auction.GetUniqueItemAuctionDetail({ auctionID });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (isString(e)) {
|
||||
unwrap(useToast()).danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
watch(currentAuctionID, () => {
|
||||
void refreshDetail();
|
||||
});
|
||||
const ongoingAuctionList = ref(new Map<number, AuctionItemInfo>());
|
||||
const finishedAuctionList = ref(new Map<number, AuctionItemInfo>());
|
||||
const obfuscatedName = ref("");
|
||||
|
||||
const toasts = unwrap(useToast());
|
||||
|
||||
function cutDateTime(dateTime: string, showSecond = false) {
|
||||
if (showSecond) {
|
||||
return dateTime.substring(5, 19);
|
||||
}
|
||||
return dateTime.substring(5, 16);
|
||||
}
|
||||
|
||||
async function refreshList() {
|
||||
try {
|
||||
const result = await SammoAPI.Auction.GetUniqueItemAuctionList();
|
||||
obfuscatedName.value = result.obfuscatedName;
|
||||
finishedAuctionList.value = new Map(
|
||||
result.list.filter((auction) => auction.finished).map((auction) => [auction.id, auction])
|
||||
);
|
||||
ongoingAuctionList.value = new Map(
|
||||
result.list.filter((auction) => !auction.finished).map((auction) => [auction.id, auction])
|
||||
);
|
||||
if (currentAuctionID.value === undefined && ongoingAuctionList.value.size > 0) {
|
||||
const auctionIterator = ongoingAuctionList.value.values().next();
|
||||
if (!auctionIterator.done) {
|
||||
currentAuctionID.value = auctionIterator.value.id;
|
||||
bidAmount.value = auctionIterator.value.highestBid.amount;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function bidAuction() {
|
||||
if (currentAuction.value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const amount = bidAmount.value;
|
||||
const auctionInfo = currentAuction.value.auction;
|
||||
|
||||
if (confirm(`${auctionInfo.title}에 ${amount}유산포인트를 입찰하시겠습니까?`)) {
|
||||
try {
|
||||
await SammoAPI.Auction.BidUniqueAuction({ auctionID: auctionInfo.id, amount });
|
||||
toasts.success({
|
||||
title: "성공",
|
||||
body: "입찰이 완료되었습니다.",
|
||||
});
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (isString(e)) {
|
||||
toasts.danger({
|
||||
title: "에러",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const waiters = [refreshList(), refreshDetail()];
|
||||
await Promise.all(waiters);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
void refreshList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.isMe {
|
||||
font-weight: bold;
|
||||
color: aquamarine;
|
||||
}
|
||||
|
||||
.clickableRow{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickableRow:hover{
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,460 @@
|
||||
<template>
|
||||
<div v-if="bettingDetailInfo !== undefined && info !== undefined">
|
||||
<div class="bg2">
|
||||
{{ info.name }}
|
||||
<span v-if="info.finished">(종료)</span>
|
||||
<span v-else-if="(yearMonth ?? 0) <= info.closeYearMonth"
|
||||
>({{ parseYearMonth(info.closeYearMonth)[0] }}년 {{ parseYearMonth(info.closeYearMonth)[1] }}월까지)</span
|
||||
>
|
||||
<span v-else>(베팅 마감)</span>
|
||||
(총액: {{ bettingAmount.toLocaleString() }})
|
||||
</div>
|
||||
|
||||
<div class="row bettingCandidates gx-1 gy-1">
|
||||
<div v-for="(candidate, idx) in info.candidates" :key="idx" class="col-4 col-md-2" @click="toggleCandidate(parseInt(idx))">
|
||||
<div
|
||||
:class="[
|
||||
'bettingCandidate',
|
||||
pickedBetType.has(parseInt(idx)) ? 'picked' : undefined,
|
||||
info.finished && winner.has(parseInt(idx)) ? 'picked' : undefined,
|
||||
]"
|
||||
>
|
||||
<div class="title bg1">
|
||||
{{ candidate.title }}
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-if="candidate.isHtml" class="info" v-html="candidate.info" />
|
||||
<div v-else class="info">{{ candidate.info }}</div>
|
||||
<div class="pickRate">선택율: {{ (((partialBet.get(parseInt(idx)) ?? 0) / pureBettingAmount) * 100).toFixed(1) }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!info.finished && (yearMonth ?? 0) <= info.closeYearMonth" class="row gx-0">
|
||||
<div class="col-6 col-md-3 align-self-center">
|
||||
잔여 {{ info.reqInheritancePoint ? "포인트" : "금" }} : {{ bettingDetailInfo.remainPoint.toLocaleString() }}
|
||||
</div>
|
||||
<div class="col-6 col-md-3 align-self-center">
|
||||
사용 포인트: {{ sum(Array.from(myBettings.values())).toLocaleString() }}
|
||||
</div>
|
||||
<div class="col-6 col-md-3 align-self-center">대상: {{ getTypeStr(pickedBetTypeKey) }}</div>
|
||||
<div class="col-4 col-md-2 d-grid">
|
||||
<!-- eslint-disable-next-line vue/max-attributes-per-line -->
|
||||
<b-form-input v-model.number="betPoint" class="d-grid" type="number" :min="10" :max="1000" :step="10" />
|
||||
</div>
|
||||
<div class="col-2 col-md-1 d-grid">
|
||||
<b-button class="d-grid" @click="submitBet"> 베팅 </b-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="bg2">배당 순위</div>
|
||||
<div
|
||||
class="row"
|
||||
:style="{
|
||||
borderBottom: 'gray solid 1px',
|
||||
}"
|
||||
>
|
||||
<div class="col-5 text-center">대상</div>
|
||||
<div class="col-2 text-center">베팅액</div>
|
||||
<div class="col-3 text-center">내 베팅</div>
|
||||
<div class="col-2 text-center">
|
||||
{{ info.finished ? "배율" : "기대 배율" }}
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="info.finished">
|
||||
<div v-for="[betType, amount] of detailBet" :key="betType" class="row">
|
||||
<template v-for="[matchPoint, color] of [calcMatchPointWithColor(betType)]" :key="matchPoint">
|
||||
<div
|
||||
class="col-5"
|
||||
:style="{
|
||||
fontWeight: myBettings.has(betType) ? 'bold' : undefined,
|
||||
color: color,
|
||||
}"
|
||||
>
|
||||
{{ getTypeStr(betType) }}
|
||||
</div>
|
||||
<div class="col-2 text-end">
|
||||
{{ amount.toLocaleString() }}
|
||||
</div>
|
||||
<div v-if="myBettings.has(betType)" class="col-3 text-center">
|
||||
<template v-for="subPoint of [myBettings.get(betType) ?? 0]">
|
||||
({{ subPoint.toLocaleString() }} ->
|
||||
{{
|
||||
calculatedReward[matchPoint] == 0
|
||||
? 0
|
||||
: ((subPoint * calculatedReward[matchPoint]) / (calculatedSubAmount.get(matchPoint) ?? 1))
|
||||
.toFixed(1)
|
||||
.toLocaleString()
|
||||
}})
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="col-3 text-center" />
|
||||
<div class="col-2 text-end">
|
||||
{{
|
||||
(calculatedReward[matchPoint] == 0
|
||||
? 0
|
||||
: calculatedReward[matchPoint] / (calculatedSubAmount.get(matchPoint) ?? 1)
|
||||
)
|
||||
.toFixed(1)
|
||||
.toLocaleString()
|
||||
}}배
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-for="[betType, amount] of detailBet" :key="betType" class="row">
|
||||
<div
|
||||
class="col-5"
|
||||
:style="{
|
||||
fontWeight: myBettings.has(betType) ? 'bold' : undefined,
|
||||
}"
|
||||
>
|
||||
{{ getTypeStr(betType) }}
|
||||
</div>
|
||||
<div class="col-2 text-end">
|
||||
{{ amount.toLocaleString() }}
|
||||
</div>
|
||||
<div v-if="myBettings.has(betType)" class="col-3 text-center">
|
||||
<template v-for="subPoint of [myBettings.get(betType) ?? 0]">
|
||||
({{ subPoint.toLocaleString() }} ->
|
||||
{{ ((subPoint * maxBettingReward) / amount).toFixed(1).toLocaleString() }})
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="col-3 text-center" />
|
||||
<div class="col-2 text-end">{{ (maxBettingReward / amount).toFixed(1).toLocaleString() }}배</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ToastType } from "@/defs";
|
||||
import type { BettingDetailResponse, BettingInfo } from "@/defs/API/Betting";
|
||||
import { SammoAPI } from "@/SammoAPI";
|
||||
import { joinYearMonth } from "@/util/joinYearMonth";
|
||||
import { parseYearMonth } from "@/util/parseYearMonth";
|
||||
import { isString, range, sum } from "lodash";
|
||||
import { ref, type PropType, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
bettingID: {
|
||||
type: Number as PropType<number>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "reqToast", content: ToastType): void;
|
||||
}>();
|
||||
|
||||
const year = ref<number>(0);
|
||||
const month = ref<number>(0);
|
||||
const yearMonth = ref<number>(0);
|
||||
|
||||
const bettingDetailInfo = ref<BettingDetailResponse>();
|
||||
const info = ref<BettingInfo>();
|
||||
|
||||
const bettingAmount = ref<number>(0);
|
||||
const maxBettingReward = ref<number>(1);
|
||||
const pureBettingAmount = ref<number>(0);
|
||||
|
||||
const partialBet = ref(new Map<number, number>());
|
||||
const detailBet = ref<[string, number][]>([]);
|
||||
|
||||
const typeMap = ref(new Map<string, string>());
|
||||
function getTypeStr(type: string): string {
|
||||
const typeResult = typeMap.value.get(type);
|
||||
if (typeResult !== undefined) {
|
||||
return typeResult;
|
||||
}
|
||||
const bettingSubTypes = JSON.parse(type) as number[];
|
||||
if (bettingSubTypes[0] < -1) {
|
||||
return "Invalid";
|
||||
}
|
||||
|
||||
const textBettingType = bettingSubTypes
|
||||
.map((idx) => {
|
||||
return bettingDetailInfo.value?.bettingInfo.candidates[idx].title;
|
||||
})
|
||||
.join(", ");
|
||||
typeMap.value.set(type, textBettingType);
|
||||
return textBettingType;
|
||||
}
|
||||
|
||||
const pickedBetType = ref(new Set<number>());
|
||||
const pickedBetTypeKey = ref("[]");
|
||||
|
||||
const betPoint = ref(0);
|
||||
const myBettings = ref(new Map<string, number>());
|
||||
|
||||
const winner = ref(new Set<number>());
|
||||
const calculatedReward = ref<number[]>([]);
|
||||
const calculatedSubAmount = ref(new Map<number, number>());
|
||||
|
||||
function calcMatchPointWithColor(type: string): [number, "green" | "yellow" | "red" | undefined] {
|
||||
if (!info.value?.finished) {
|
||||
return [0, undefined];
|
||||
}
|
||||
const bettingSubTypes = JSON.parse(type) as number[];
|
||||
if (bettingSubTypes[0] < -1) {
|
||||
return [0, undefined];
|
||||
}
|
||||
|
||||
let matchPoint = 0;
|
||||
for (const subType of bettingSubTypes) {
|
||||
if (winner.value.has(subType)) {
|
||||
matchPoint += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (info.value.isExclusive) {
|
||||
if (matchPoint == info.value.selectCnt) {
|
||||
return [matchPoint, "green"];
|
||||
} else {
|
||||
return [matchPoint, "red"];
|
||||
}
|
||||
}
|
||||
|
||||
let color: "green" | "red" | "yellow" = "green";
|
||||
if (matchPoint == 0) {
|
||||
color = "red";
|
||||
} else if (matchPoint < info.value.selectCnt) {
|
||||
color = "yellow";
|
||||
}
|
||||
return [matchPoint, color];
|
||||
}
|
||||
|
||||
function calcReward() {
|
||||
if (info.value === undefined || bettingDetailInfo.value === undefined) {
|
||||
throw "no info";
|
||||
}
|
||||
const selectCnt = info.value.selectCnt;
|
||||
const rewardAmount = new Array<number>(selectCnt).fill(0);
|
||||
|
||||
const subAmount = new Map<number, number>();
|
||||
for (const [bettingTypeStr, amount] of bettingDetailInfo.value.bettingDetail) {
|
||||
if (amount == 0) {
|
||||
continue;
|
||||
}
|
||||
const [matchPoint] = calcMatchPointWithColor(bettingTypeStr);
|
||||
subAmount.set(matchPoint, (subAmount.get(matchPoint) ?? 0) + amount);
|
||||
}
|
||||
calculatedSubAmount.value = subAmount;
|
||||
|
||||
if (selectCnt == 1) {
|
||||
rewardAmount[selectCnt - 1] = bettingAmount.value;
|
||||
calculatedReward.value = rewardAmount;
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.value.isExclusive) {
|
||||
rewardAmount[selectCnt - 1] = bettingAmount.value;
|
||||
calculatedReward.value = rewardAmount;
|
||||
return;
|
||||
}
|
||||
|
||||
let remainRewardAmount = bettingAmount.value;
|
||||
let accumulatedRewardAmount = 0;
|
||||
let givenRewardAmount = bettingAmount.value;
|
||||
|
||||
for (const matchPoint of range(selectCnt, 0, -1)) {
|
||||
givenRewardAmount /= 2;
|
||||
accumulatedRewardAmount += givenRewardAmount;
|
||||
if (!subAmount.has(matchPoint)) {
|
||||
continue;
|
||||
}
|
||||
rewardAmount[matchPoint] = accumulatedRewardAmount;
|
||||
remainRewardAmount -= accumulatedRewardAmount;
|
||||
accumulatedRewardAmount = 0;
|
||||
}
|
||||
|
||||
//남은 상금은 '당첨자'에게 몰아준다.
|
||||
//당첨자가 아무도 없다면, 0개 맞춘 그룹에게 돌아간다.
|
||||
for (const matchPoint of range(selectCnt, -1, -1)) {
|
||||
if (!subAmount.has(matchPoint)) {
|
||||
continue;
|
||||
}
|
||||
rewardAmount[matchPoint] += remainRewardAmount;
|
||||
break;
|
||||
}
|
||||
|
||||
calculatedReward.value = rewardAmount;
|
||||
}
|
||||
|
||||
async function loadBetting(bettingID: number) {
|
||||
try {
|
||||
const result = await SammoAPI.Betting.GetBettingDetail[bettingID]();
|
||||
year.value = result.year;
|
||||
month.value = result.month;
|
||||
yearMonth.value = joinYearMonth(result.year, result.month);
|
||||
bettingDetailInfo.value = result;
|
||||
info.value = result.bettingInfo;
|
||||
|
||||
typeMap.value.clear();
|
||||
partialBet.value.clear();
|
||||
|
||||
const betSort = new Map<string, number>();
|
||||
|
||||
let _bettingAmount = 0;
|
||||
let adminBettingAmount = 0;
|
||||
for (const [bettingType, amount] of result.bettingDetail) {
|
||||
console.log(amount, typeof amount);
|
||||
let userBet = true;
|
||||
const bettingSubTypes = JSON.parse(bettingType) as number[];
|
||||
for (const bettingSubType of bettingSubTypes) {
|
||||
if (bettingSubType < 0) {
|
||||
userBet = false;
|
||||
continue;
|
||||
}
|
||||
const oldValue = partialBet.value.get(bettingSubType) ?? 0;
|
||||
partialBet.value.set(bettingSubType, oldValue + amount);
|
||||
}
|
||||
|
||||
if (userBet) {
|
||||
const oldValue = betSort.get(bettingType) ?? 0;
|
||||
betSort.set(bettingType, oldValue + amount);
|
||||
}
|
||||
|
||||
_bettingAmount += amount;
|
||||
if (!userBet) {
|
||||
adminBettingAmount += amount;
|
||||
}
|
||||
}
|
||||
console.log(_bettingAmount);
|
||||
bettingAmount.value = _bettingAmount;
|
||||
pureBettingAmount.value = _bettingAmount - adminBettingAmount;
|
||||
if (info.value.isExclusive || info.value.selectCnt == 1) {
|
||||
maxBettingReward.value = _bettingAmount;
|
||||
} else {
|
||||
maxBettingReward.value = _bettingAmount / 2;
|
||||
}
|
||||
|
||||
detailBet.value = Array.from(betSort.entries());
|
||||
detailBet.value.sort(([, lhsVal], [, rhsVal]) => {
|
||||
return rhsVal - lhsVal;
|
||||
});
|
||||
|
||||
pickedBetType.value.clear();
|
||||
pickedBetTypeKey.value = "[]";
|
||||
myBettings.value.clear();
|
||||
|
||||
if (result.bettingInfo.winner) {
|
||||
winner.value = new Set(result.bettingInfo.winner);
|
||||
} else {
|
||||
winner.value.clear();
|
||||
}
|
||||
|
||||
for (const [betType, amount] of result.myBetting) {
|
||||
myBettings.value.set(betType, amount);
|
||||
}
|
||||
calcReward();
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
emit("reqToast", {
|
||||
content: {
|
||||
title: "에러",
|
||||
body: e,
|
||||
},
|
||||
options: {
|
||||
variant: "danger",
|
||||
},
|
||||
});
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
void loadBetting(props.bettingID);
|
||||
watch(
|
||||
() => props.bettingID,
|
||||
(newBettingID) => {
|
||||
void loadBetting(newBettingID);
|
||||
}
|
||||
);
|
||||
|
||||
function toggleCandidate(idx: number) {
|
||||
if (info.value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (bettingDetailInfo.value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (info.value.closeYearMonth < yearMonth.value) {
|
||||
return;
|
||||
}
|
||||
if (info.value.finished) {
|
||||
return;
|
||||
}
|
||||
const selectCnt = bettingDetailInfo.value.bettingInfo.selectCnt;
|
||||
|
||||
if (selectCnt == 1) {
|
||||
pickedBetType.value.clear();
|
||||
pickedBetType.value.add(idx);
|
||||
pickedBetTypeKey.value = JSON.stringify([idx]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pickedBetType.value.has(idx)) {
|
||||
pickedBetType.value.delete(idx);
|
||||
} else if (pickedBetType.value.size < selectCnt) {
|
||||
pickedBetType.value.add(idx);
|
||||
} else {
|
||||
emit("reqToast", {
|
||||
content: {
|
||||
title: "오류",
|
||||
body: `이미 ${selectCnt}개를 선택했습니다.`,
|
||||
},
|
||||
options: {
|
||||
variant: "warning",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const typeArr = Array.from(pickedBetType.value.values());
|
||||
pickedBetTypeKey.value = JSON.stringify(typeArr.sort((lhs, rhs) => lhs - rhs));
|
||||
}
|
||||
|
||||
async function submitBet(): Promise<void> {
|
||||
const bettingInfo = info.value;
|
||||
if (bettingInfo === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bettingID = bettingInfo.id;
|
||||
const bettingType = JSON.parse(pickedBetTypeKey.value) as number[];
|
||||
const amount = betPoint.value;
|
||||
try {
|
||||
await SammoAPI.Betting.Bet({
|
||||
bettingID,
|
||||
bettingType,
|
||||
amount,
|
||||
});
|
||||
emit("reqToast", {
|
||||
content: {
|
||||
title: "완료",
|
||||
body: "베팅했습니다",
|
||||
},
|
||||
options: {
|
||||
variant: "success",
|
||||
},
|
||||
});
|
||||
await loadBetting(bettingInfo.id);
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
emit("reqToast", {
|
||||
content: {
|
||||
title: "에러",
|
||||
body: e,
|
||||
},
|
||||
options: {
|
||||
variant: "danger",
|
||||
},
|
||||
});
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="articleFrame bg0">
|
||||
<div class="bg1 row gx-0">
|
||||
<div class="authorName center">
|
||||
{{ article.author }}
|
||||
</div>
|
||||
<div class="col articleTitle center">
|
||||
{{ article.title }}
|
||||
</div>
|
||||
<div class="col-2 col-md-1 date center">
|
||||
{{ article.date.slice(5, 16) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row gx-0 s-border-b">
|
||||
<div class="col-2 col-md-1 authorIcon center">
|
||||
<img class="generalIcon" width="64" height="64" :src="article.author_icon" />
|
||||
</div>
|
||||
<div class="col text">
|
||||
{{ article.text }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="commentList">
|
||||
<board-comment v-for="comment in article.comment" :key="comment.no" :comment="comment" />
|
||||
</div>
|
||||
<div class="row gx-0">
|
||||
<div class="bg2 inputCommentHeader center d-grid">
|
||||
<div class="align-self-center">댓글 달기</div>
|
||||
</div>
|
||||
<div class="col d-grid">
|
||||
<input
|
||||
v-model.trim="newCommentText"
|
||||
class="commentText"
|
||||
type="text"
|
||||
maxlength="250"
|
||||
placeholder="새 댓글 내용"
|
||||
@keyup.enter="submitComment"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-2 col-md-1 d-grid">
|
||||
<b-button class="submitComment" size="sm" @click="submitComment"> 등록 </b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { BoardArticleItem } from "@/PageBoard.vue";
|
||||
import BoardComment from "@/components/BoardComment.vue";
|
||||
import { ref, type PropType } from "vue";
|
||||
import axios from "axios";
|
||||
import { convertFormData } from "@util/convertFormData";
|
||||
import type { InvalidResponse } from "@/defs";
|
||||
|
||||
const newCommentText = ref("");
|
||||
|
||||
const props = defineProps({
|
||||
article: {
|
||||
type: Object as PropType<BoardArticleItem>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "submit-comment"): void;
|
||||
}>();
|
||||
|
||||
async function submitComment() {
|
||||
const comment = newCommentText.value;
|
||||
if (!comment) {
|
||||
return;
|
||||
}
|
||||
const articleNo = props.article.no;
|
||||
|
||||
let result: InvalidResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: "j_board_comment_add.php",
|
||||
method: "post",
|
||||
responseType: "json",
|
||||
data: convertFormData({
|
||||
articleNo: articleNo,
|
||||
text: comment,
|
||||
}),
|
||||
});
|
||||
result = response.data;
|
||||
if (!result.result) {
|
||||
throw result.reason;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
newCommentText.value = "";
|
||||
|
||||
emit("submit-comment");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
td.text {
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="row gx-0 comment s-border-b">
|
||||
<div class="authorName center d-grid">
|
||||
<div class="align-self-center">
|
||||
{{ comment.author }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col text">
|
||||
{{ comment.text }}
|
||||
</div>
|
||||
<div class="col-2 col-md-1 date center d-grid">
|
||||
<div class="align-self-center">
|
||||
{{ comment.date.slice(5, 16) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { BoardCommentItem } from "@/PageBoard.vue";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "BoardComment",
|
||||
props: {
|
||||
comment: {
|
||||
type: Object as PropType<BoardCommentItem>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="bg0" style="padding-top: 20px">
|
||||
<button type="button" class="btn btn-sammo-base2 back_btn" @click="back">
|
||||
{{ props.type == "close" ? "창 닫기" : "돌아가기" }}
|
||||
</button>
|
||||
<div />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from "vue";
|
||||
import "@scss/game_bg.scss";
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String as PropType<"normal" | "chief" | "close">,
|
||||
default: "normal",
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
function back() {
|
||||
if (props.type === "normal") {
|
||||
location.href = "./";
|
||||
} else if (props.type == "chief") {
|
||||
location.href = "v_chiefCenter.php";
|
||||
} else {
|
||||
//TODO: window.close하려면 부모창이 있어야함!
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,969 @@
|
||||
<template>
|
||||
<div class="commandBox">
|
||||
<div class="only1000px bg1 center row gx-0" style="height: 24px; font-size: 1.2em">
|
||||
<div class="col-5 align-self-center text-end">{{ officer.officerLevelText }} :</div>
|
||||
<div
|
||||
class="col-7 align-self-center"
|
||||
:style="{
|
||||
color: getNpcColor(officer.npcType ?? 0),
|
||||
}"
|
||||
>
|
||||
{{ officer.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div :class="['row', 'controlPad', props.targetIsMe ? 'targetIsMe' : 'targetIsNotMe']">
|
||||
<div class="col-3 col-md-12 order-md-last">
|
||||
<div class="d-grid mb-1 py-1 only500px bg1 center">
|
||||
<div
|
||||
:style="{
|
||||
color: getNpcColor(officer.npcType ?? 0),
|
||||
fontSize: '1.2em',
|
||||
}"
|
||||
>
|
||||
{{ officer.name }}
|
||||
</div>
|
||||
<div>{{ officer.officerLevelText }}</div>
|
||||
</div>
|
||||
<div class="row gx-1 gy-1 py-1">
|
||||
<div class="col-md-4 mx-0 mb-0 mt-1 d-grid">
|
||||
<div class="alert alert-primary mb-0 center" style="padding: 0.5rem 0">
|
||||
<SimpleClock :serverTime="parseTime(props.date)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 d-grid">
|
||||
<BButton variant="secondary" @click="isEditMode = !isEditMode">
|
||||
{{ isEditMode ? "일반 모드" : "고급 모드" }}
|
||||
</BButton>
|
||||
</div>
|
||||
|
||||
<BDropdown class="col-md-4" text="반복">
|
||||
<BDropdownItem v-for="turnIdx in maxPushTurn" :key="turnIdx" @click="repeatNationCommand(turnIdx)">
|
||||
{{ turnIdx }}턴
|
||||
</BDropdownItem>
|
||||
</BDropdown>
|
||||
|
||||
<template v-if="isEditMode">
|
||||
<BDropdown class="col-md-4" left text="범위">
|
||||
<BDropdownItem @click="queryActionHelper.selectTurn()"> 해제 </BDropdownItem>
|
||||
<BDropdownItem @click="queryActionHelper.selectAll()"> 모든턴 </BDropdownItem>
|
||||
<BDropdownItem @click="queryActionHelper.selectStep(0, 2)"> 홀수턴 </BDropdownItem>
|
||||
<BDropdownItem @click="queryActionHelper.selectStep(1, 2)"> 짝수턴 </BDropdownItem>
|
||||
<BDropdownDivider />
|
||||
|
||||
<BDropdownText v-for="spanIdx in [3, 4, 5, 6, 7]" :key="spanIdx">
|
||||
{{ spanIdx }}턴 간격
|
||||
<br />
|
||||
<BButtonGroup>
|
||||
<BButton
|
||||
v-for="beginIdx in spanIdx"
|
||||
:key="beginIdx"
|
||||
class="ignoreMe"
|
||||
@click="queryActionHelper.selectStep(beginIdx - 1, spanIdx)"
|
||||
>
|
||||
{{ beginIdx }}
|
||||
</BButton>
|
||||
</BButtonGroup>
|
||||
</BDropdownText>
|
||||
</BDropdown>
|
||||
|
||||
<BDropdown class="col-md-4" left text="보관함">
|
||||
<BDropdownItem
|
||||
v-for="[actionKey, actions] of storedActions"
|
||||
:key="actionKey"
|
||||
@click.self="useStoredAction(actions)"
|
||||
>
|
||||
{{ actionKey }}
|
||||
<BButton size="sm" @click.prevent="deleteStoredActions(actionKey)"> 삭제 </BButton>
|
||||
</BDropdownItem>
|
||||
</BDropdown>
|
||||
|
||||
<div class="col-md-4 d-grid">
|
||||
<BDropdown right text="최근">
|
||||
<BDropdownItem
|
||||
v-for="(action, idx) in Array.from(recentActions.values()).reverse()"
|
||||
:key="idx"
|
||||
@click="void reserveCommandDirect([[queryActionHelper.getSelectedTurnList(), action]])"
|
||||
>
|
||||
{{ action.brief }}
|
||||
</BDropdownItem>
|
||||
</BDropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<BDropdown class="col-md-6" split text="당기기" @click="pullNationCommandSingle">
|
||||
<BDropdownItem v-for="turnIdx in maxPushTurn" :key="turnIdx" @click="pushNationCommand(-turnIdx)">
|
||||
{{ turnIdx }}턴
|
||||
</BDropdownItem>
|
||||
</BDropdown>
|
||||
<BDropdown class="col-md-6" split text="미루기" @click="pushNationCommandSingle">
|
||||
<BDropdownItem v-for="turnIdx in maxPushTurn" :key="turnIdx" @click="pushNationCommand(turnIdx)">
|
||||
{{ turnIdx }}턴
|
||||
</BDropdownItem>
|
||||
</BDropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div :style="{ position: 'relative' }">
|
||||
<div
|
||||
class="commandQuickReserveFormAnchor bg-dark"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
top: `${basicModeRowHeight * currentQuickReserveTarget + 26}px`,
|
||||
width: '100%',
|
||||
zIndex: 9,
|
||||
}"
|
||||
>
|
||||
<CommandSelectForm
|
||||
ref="commandQuickReserveForm"
|
||||
v-model:activatedCategory="activatedCategory"
|
||||
:commandList="commandList"
|
||||
:hideClose="false"
|
||||
class="bg-dark"
|
||||
style="position: absolute"
|
||||
@onClose="chooseQuickReserveCommand($event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="commandPad chiefReservedCommand">
|
||||
<div :class="['commandTable', isEditMode ? 'editMode' : 'singleMode']">
|
||||
<DragSelect
|
||||
v-slot="{ selected }"
|
||||
:style="rowGridStyle"
|
||||
attribute="turnIdx"
|
||||
:disabled="!isEditMode"
|
||||
@dragStart="isDragSingle = true"
|
||||
@dragDone="
|
||||
isDragSingle = false;
|
||||
queryActionHelper.selectTurn(...$event);
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="(turnObj, turnIdx) in reservedCommandList"
|
||||
:key="turnIdx"
|
||||
:turnIdx="turnIdx"
|
||||
class="time_pad center f_tnum"
|
||||
:style="{
|
||||
backgroundColor: 'black',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
color: isDragSingle && selected.has(`${turnIdx}`) ? 'cyan' : undefined,
|
||||
}"
|
||||
>
|
||||
{{ turnObj.time }}
|
||||
</div>
|
||||
</DragSelect>
|
||||
<DragSelect
|
||||
v-slot="{ selected }"
|
||||
:style="{ ...rowGridStyle, display: isEditMode ? 'grid' : 'none' }"
|
||||
attribute="turnIdx"
|
||||
:disabled="!isEditMode"
|
||||
@dragStart="isDragToggle = true"
|
||||
@dragDone="
|
||||
isDragToggle = false;
|
||||
toggleTurn(...$event);
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="(turnObj, turnIdx) in reservedCommandList"
|
||||
:key="turnIdx"
|
||||
:turnIdx="turnIdx"
|
||||
class="idx_pad center d-grid"
|
||||
>
|
||||
<BButton
|
||||
size="sm"
|
||||
:variant="
|
||||
isDragToggle && selected.has(`${turnIdx}`)
|
||||
? 'light'
|
||||
: selectedTurnList.has(turnIdx)
|
||||
? 'info'
|
||||
: selectedTurnList.size == 0 && prevSelectedTurnList.has(turnIdx)
|
||||
? 'success'
|
||||
: 'primary'
|
||||
"
|
||||
>
|
||||
{{ turnIdx + 1 }}
|
||||
</BButton>
|
||||
</div>
|
||||
</DragSelect>
|
||||
<div :style="rowGridStyle">
|
||||
<div v-for="(turnObj, turnIdx) in reservedCommandList" :key="turnIdx" class="turn_pad center">
|
||||
<span v-b-tooltip.hover class="turn_text" :style="turnObj.style" :title="turnObj.tooltip">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-html="turnObj.brief" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div :style="{ ...rowGridStyle, display: isEditMode ? 'none' : 'grid' }">
|
||||
<div v-for="turnIdx in range(props.maxTurn)" :key="turnIdx" class="action_pad d-grid">
|
||||
<BButton
|
||||
:variant="turnIdx % 2 == 0 ? 'secondary' : 'dark'"
|
||||
size="sm"
|
||||
class="simple_action_btn bi bi-pencil"
|
||||
@click="toggleQuickReserveForm(turnIdx)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position: relative">
|
||||
<CommandSelectForm
|
||||
ref="commandSelectForm"
|
||||
v-model:activatedCategory="activatedCategory"
|
||||
:commandList="commandList"
|
||||
class="bg-dark"
|
||||
:style="{ position: 'absolute', bottom: '0' }"
|
||||
@onClose="chooseCommand($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isEditMode" class="row gx-0">
|
||||
<div class="col-5 col-md-6 d-grid">
|
||||
<BDropdown left variant="light" :style="{ color: 'black' }" text="선택한 턴을">
|
||||
<BDropdownItem @click="clipboardCut"> <i class="bi bi-scissors" /> 잘라내기 </BDropdownItem>
|
||||
<BDropdownItem @click="clipboardCopy"> <i class="bi bi-files" /> 복사하기 </BDropdownItem>
|
||||
<BDropdownItem @click="clipboardPaste">
|
||||
<i class="bi bi-clipboard-fill" /> 붙여넣기
|
||||
</BDropdownItem>
|
||||
<BDropdownDivider />
|
||||
<BDropdownItem @click="setStoredActions">
|
||||
<i class="bi bi-bookmark-plus-fill" /> 보관하기
|
||||
</BDropdownItem>
|
||||
<BDropdownItem @click="subRepeatCommand">
|
||||
<i class="bi bi-arrow-repeat" /> 반복하기
|
||||
</BDropdownItem>
|
||||
<BDropdownDivider />
|
||||
<BDropdownItem @click="eraseSelectedTurnList"> <i class="bi bi-eraser" /> 비우기 </BDropdownItem>
|
||||
<BDropdownItem @click="eraseAndPullCommand">
|
||||
<i class="bi bi-arrow-bar-up" /> 지우고 당기기
|
||||
</BDropdownItem>
|
||||
<BDropdownItem @click="pushEmptyCommand">
|
||||
<i class="bi bi-arrow-bar-down" /> 뒤로 밀기
|
||||
</BDropdownItem>
|
||||
<!-- 최근에 실행한 10턴 -->
|
||||
</BDropdown>
|
||||
</div>
|
||||
|
||||
<div class="col-7 col-md-6 d-grid">
|
||||
<BButton variant="info" @click="toggleForm($event)"> 명령 선택 ▾ </BButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import addMinutes from "date-fns/esm/addMinutes";
|
||||
import { stringifyUrl } from "query-string";
|
||||
import { onMounted, ref, watch, type PropType, inject } from "vue";
|
||||
import { formatTime } from "@util/formatTime";
|
||||
import { joinYearMonth } from "@util/joinYearMonth";
|
||||
import { mb_strwidth } from "@util/mb_strwidth";
|
||||
import { parseTime } from "@util/parseTime";
|
||||
import { parseYearMonth } from "@util/parseYearMonth";
|
||||
import { convertSearch초성 } from "@util/convertSearch초성";
|
||||
import VueTypes from "vue-types";
|
||||
import DragSelect from "@/components/DragSelect.vue";
|
||||
import { isString, range, trim } from "lodash";
|
||||
import { SammoAPI } from "@/SammoAPI";
|
||||
import type { CommandItem, TurnObj } from "@/defs";
|
||||
import { QueryActionHelper } from "@/util/QueryActionHelper";
|
||||
import type { Args } from "@/processing/args";
|
||||
import type { StoredActionsHelper } from "@/util/StoredActionsHelper";
|
||||
import { getNpcColor } from "@/common_legacy";
|
||||
import { BButton, BDropdownItem, BDropdownText, BButtonGroup, BDropdownDivider, BDropdown } from "bootstrap-vue-3";
|
||||
import CommandSelectForm from "@/components/CommandSelectForm.vue";
|
||||
import SimpleClock from "@/components/SimpleClock.vue";
|
||||
import type { ChiefResponse } from "@/defs/API/NationCommand";
|
||||
|
||||
type TurnObjWithTime = TurnObj & {
|
||||
time: string;
|
||||
year?: number;
|
||||
month?: number;
|
||||
tooltip?: string;
|
||||
style?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
maxTurn: VueTypes.integer.isRequired,
|
||||
maxPushTurn: VueTypes.integer.isRequired,
|
||||
date: VueTypes.string.isRequired,
|
||||
year: VueTypes.integer.isRequired,
|
||||
month: VueTypes.integer.isRequired,
|
||||
turnTerm: VueTypes.integer.isRequired,
|
||||
turnTime: VueTypes.string.isRequired,
|
||||
targetIsMe: VueTypes.bool.isRequired,
|
||||
|
||||
selectedTurn: {
|
||||
type: Object as PropType<Set<number>>,
|
||||
required: false,
|
||||
default: () => new Set(),
|
||||
},
|
||||
turn: {
|
||||
type: Array as PropType<TurnObj[]>,
|
||||
required: true,
|
||||
},
|
||||
commandList: {
|
||||
type: Object as PropType<ChiefResponse["commandList"]>,
|
||||
required: true,
|
||||
},
|
||||
|
||||
officer: {
|
||||
type: Object as PropType<ChiefResponse["chiefList"][0]>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const basicModeRowHeight = 30;
|
||||
|
||||
const listReqArgCommand = new Set<string>();
|
||||
for (const commandCategories of props.commandList) {
|
||||
if (!commandCategories.values) {
|
||||
continue;
|
||||
}
|
||||
for (const commandObj of commandCategories.values) {
|
||||
if (!commandObj.reqArg) {
|
||||
continue;
|
||||
}
|
||||
listReqArgCommand.add(commandObj.value);
|
||||
}
|
||||
}
|
||||
|
||||
const selectedCommand = ref(props.commandList[0].values[0]);
|
||||
for (const subCategory of props.commandList) {
|
||||
for (const command of subCategory.values) {
|
||||
if (command.searchText) {
|
||||
continue;
|
||||
}
|
||||
command.searchText = convertSearch초성(command.simpleName).join("|");
|
||||
}
|
||||
}
|
||||
|
||||
const invCommandMap: Record<string, CommandItem> = {};
|
||||
for (const category of props.commandList) {
|
||||
for (const command of category.values) {
|
||||
invCommandMap[command.value] = command;
|
||||
}
|
||||
}
|
||||
|
||||
const rowGridStyle = ref({
|
||||
display: "grid",
|
||||
gridTemplateRows: `repeat(${props.maxTurn}, 30px)`,
|
||||
});
|
||||
|
||||
const updated = ref(false);
|
||||
const isDragSingle = ref(false);
|
||||
const isDragToggle = ref(false);
|
||||
const autorun_limit = ref<number | null>(null);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "raise-reload"): void;
|
||||
(event: "update:selectedTurn", value: Set<number>): void;
|
||||
}>();
|
||||
|
||||
function triggerUpdateCommandList(type?: string) {
|
||||
console.log("try update", type);
|
||||
updated.value = false;
|
||||
setTimeout(() => {
|
||||
updateCommandList();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
function toggleTurn(...reqTurnList: number[] | string[]) {
|
||||
for (let turnIdx of reqTurnList) {
|
||||
if (isString(turnIdx)) {
|
||||
turnIdx = parseInt(turnIdx);
|
||||
}
|
||||
if (selectedTurnList.value.has(turnIdx)) {
|
||||
selectedTurnList.value.delete(turnIdx);
|
||||
} else {
|
||||
selectedTurnList.value.add(turnIdx);
|
||||
}
|
||||
}
|
||||
emit("update:selectedTurn", selectedTurnList.value);
|
||||
}
|
||||
|
||||
function isDropdownChildren(e?: Event): boolean {
|
||||
if (!e) {
|
||||
return false;
|
||||
}
|
||||
if (!e.target) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
(e.target as HTMLElement).classList.contains("dropdown-item") ||
|
||||
(e.target as HTMLElement).classList.contains("dropdown-toggle-split") ||
|
||||
(e.target as HTMLElement).classList.contains("ignoreMe")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function repeatNationCommand(amount: number) {
|
||||
try {
|
||||
await SammoAPI.NationCommand.RepeatCommand({ amount });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
emit("raise-reload");
|
||||
}
|
||||
|
||||
function pushNationCommandSingle(e: Event) {
|
||||
//NOTE: split 구현에 버그가 있어서, 수동으로 구분해야함
|
||||
if (isDropdownChildren(e)) {
|
||||
return;
|
||||
}
|
||||
void pushNationCommand(1);
|
||||
}
|
||||
|
||||
function pullNationCommandSingle(e: Event) {
|
||||
//NOTE: split 구현에 버그가 있어서, 수동으로 구분해야함
|
||||
if (isDropdownChildren(e)) {
|
||||
return;
|
||||
}
|
||||
void pushNationCommand(-1);
|
||||
}
|
||||
|
||||
async function pushNationCommand(amount: number) {
|
||||
try {
|
||||
await SammoAPI.NationCommand.PushCommand({ amount });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
emit("raise-reload");
|
||||
}
|
||||
|
||||
const queryActionHelper = new QueryActionHelper(props.maxTurn);
|
||||
const reservedCommandList = queryActionHelper.reservedCommandList;
|
||||
const prevSelectedTurnList = queryActionHelper.prevSelectedTurnList;
|
||||
const selectedTurnList = queryActionHelper.selectedTurnList;
|
||||
|
||||
async function reserveCommandDirect(args: [number[], TurnObj][], reload = true): Promise<boolean> {
|
||||
const query: {
|
||||
turnList: number[];
|
||||
action: string;
|
||||
arg: Args;
|
||||
}[] = [];
|
||||
for (const [turnList, { action, arg }] of args) {
|
||||
query.push({
|
||||
turnList,
|
||||
action,
|
||||
arg,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await SammoAPI.NationCommand.ReserveBulkCommand(query);
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reload) {
|
||||
emit("raise-reload");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateCommandList() {
|
||||
if (updated.value) {
|
||||
return;
|
||||
}
|
||||
console.log("do update!");
|
||||
const _reservedCommandList: TurnObjWithTime[] = [];
|
||||
let yearMonth = joinYearMonth(props.year, props.month);
|
||||
|
||||
const turnTime = parseTime(props.turnTime);
|
||||
let nextTurnTime = new Date(turnTime);
|
||||
|
||||
const autorunLimitYearMonth = autorun_limit.value ?? yearMonth - 1;
|
||||
const [autorunLimitYear, autorunLimitMonth] = parseYearMonth(autorunLimitYearMonth);
|
||||
|
||||
for (const obj of props.turn) {
|
||||
const [year, month] = parseYearMonth(yearMonth);
|
||||
let tooltip: string[] = [];
|
||||
let style: Record<string, unknown> = {};
|
||||
|
||||
const brief = obj.brief;
|
||||
|
||||
if (yearMonth <= autorunLimitYearMonth) {
|
||||
if (obj.brief == "휴식") {
|
||||
obj.brief = "휴식<small>(자율 행동)</small>";
|
||||
}
|
||||
style.color = "#aaffff";
|
||||
|
||||
tooltip.push(`자율 행동 기간: ${autorunLimitYear}년 ${autorunLimitMonth}월까지`);
|
||||
}
|
||||
|
||||
if (mb_strwidth(brief) > 22) {
|
||||
tooltip.push(brief);
|
||||
}
|
||||
|
||||
_reservedCommandList.push({
|
||||
...obj,
|
||||
year,
|
||||
month,
|
||||
time: formatTime(nextTurnTime, props.turnTerm >= 5 ? "HH:mm" : "mm:ss"),
|
||||
tooltip: tooltip.length == 0 ? undefined : tooltip.join("\n"),
|
||||
style,
|
||||
});
|
||||
|
||||
yearMonth += 1;
|
||||
nextTurnTime = addMinutes(nextTurnTime, props.turnTerm);
|
||||
}
|
||||
reservedCommandList.value = _reservedCommandList;
|
||||
updated.value = true;
|
||||
}
|
||||
|
||||
async function reserveCommand() {
|
||||
const reqTurnList = queryActionHelper.getSelectedTurnList();
|
||||
const commandName = selectedCommand.value.value;
|
||||
|
||||
if (listReqArgCommand.has(commandName)) {
|
||||
document.location.href = stringifyUrl({
|
||||
url: "v_processing.php",
|
||||
query: {
|
||||
command: commandName,
|
||||
turnList: reqTurnList.join("_"),
|
||||
is_chief: true,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await SammoAPI.NationCommand.ReserveCommand({
|
||||
turnList: reqTurnList,
|
||||
action: commandName,
|
||||
});
|
||||
|
||||
storedActionsHelper.pushRecentActions({
|
||||
action: commandName,
|
||||
brief: result.brief,
|
||||
arg: {},
|
||||
});
|
||||
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
emit("raise-reload");
|
||||
}
|
||||
|
||||
function chooseCommand(val?: string) {
|
||||
if (val === undefined) {
|
||||
return;
|
||||
}
|
||||
selectedCommand.value = invCommandMap[val];
|
||||
void reserveCommand();
|
||||
}
|
||||
|
||||
const emptyTurnObj: TurnObj = { action: "휴식", brief: "휴식", arg: {} };
|
||||
|
||||
const storedActionsHelper = inject("storedNationActionsHelper") as StoredActionsHelper;
|
||||
|
||||
const recentActions = storedActionsHelper.recentActions;
|
||||
const storedActions = storedActionsHelper.storedActions;
|
||||
const activatedCategory = storedActionsHelper.activatedCategory;
|
||||
|
||||
async function eraseSelectedTurnList(releaseSelect = true): Promise<boolean> {
|
||||
const result = await reserveCommandDirect([[queryActionHelper.getSelectedTurnList(), emptyTurnObj]]);
|
||||
if (releaseSelect) {
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const clipboard = storedActionsHelper.clipboard;
|
||||
|
||||
async function clipboardCut(releaseSelect = true) {
|
||||
clipboardCopy(false);
|
||||
return eraseSelectedTurnList(releaseSelect);
|
||||
}
|
||||
|
||||
function clipboardCopy(releaseSelect = true) {
|
||||
clipboard.value = queryActionHelper.extractQueryActions();
|
||||
if (releaseSelect) {
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
}
|
||||
}
|
||||
|
||||
async function clipboardPaste(releaseSelect = true) {
|
||||
const rawActions = clipboard.value;
|
||||
if (rawActions === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actions = queryActionHelper.amplifyQueryActions(rawActions, queryActionHelper.getSelectedTurnList());
|
||||
if (actions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await reserveCommandDirect(actions);
|
||||
if (releaseSelect) {
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function subRepeatCommand(releaseSelect = true): Promise<boolean> {
|
||||
const reqTurnList = queryActionHelper.getSelectedTurnList();
|
||||
const selectedMinTurnIdx = reqTurnList[0];
|
||||
const selectedMaxTurnIdx = reqTurnList[reqTurnList.length - 1];
|
||||
const queryLength = selectedMaxTurnIdx - selectedMinTurnIdx + 1;
|
||||
|
||||
const rawActions = queryActionHelper.extractQueryActions();
|
||||
const actions = queryActionHelper.amplifyQueryActions(
|
||||
rawActions,
|
||||
range(selectedMinTurnIdx, props.maxTurn, queryLength)
|
||||
);
|
||||
|
||||
const result = await reserveCommandDirect(actions);
|
||||
if (releaseSelect) {
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function eraseAndPullCommand(releaseSelect = true): Promise<boolean> {
|
||||
const reqTurnList = queryActionHelper.getSelectedTurnList();
|
||||
const selectedMinTurnIdx = reqTurnList[0];
|
||||
const selectedMaxTurnIdx = reqTurnList[reqTurnList.length - 1];
|
||||
const queryLength = selectedMaxTurnIdx - selectedMinTurnIdx + 1;
|
||||
|
||||
if (selectedMinTurnIdx === 0) {
|
||||
await pushNationCommand(-queryLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (selectedMinTurnIdx + queryLength == props.maxTurn) {
|
||||
return eraseSelectedTurnList(releaseSelect);
|
||||
}
|
||||
|
||||
const actions: [number[], TurnObj][] = [];
|
||||
|
||||
const emptyTurnList: number[] = [];
|
||||
|
||||
for (const srcTurnIdx of range(selectedMinTurnIdx + queryLength, props.maxTurn)) {
|
||||
const rawAction = reservedCommandList.value[srcTurnIdx];
|
||||
if (rawAction.action == emptyTurnObj.action) {
|
||||
emptyTurnList.push(srcTurnIdx - queryLength);
|
||||
continue;
|
||||
}
|
||||
actions.push([
|
||||
[srcTurnIdx - queryLength],
|
||||
{
|
||||
action: rawAction.action,
|
||||
arg: rawAction.arg,
|
||||
brief: rawAction.brief,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
emptyTurnList.push(...range(props.maxTurn - queryLength, props.maxTurn));
|
||||
actions.push([emptyTurnList, emptyTurnObj]);
|
||||
|
||||
const result = await reserveCommandDirect(actions);
|
||||
if (releaseSelect) {
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function pushEmptyCommand(releaseSelect = true): Promise<boolean> {
|
||||
const reqTurnList = queryActionHelper.getSelectedTurnList();
|
||||
const selectedMinTurnIdx = reqTurnList[0];
|
||||
const selectedMaxTurnIdx = reqTurnList[reqTurnList.length - 1];
|
||||
const queryLength = selectedMaxTurnIdx - selectedMinTurnIdx + 1;
|
||||
|
||||
if (selectedMinTurnIdx === 0) {
|
||||
await pushNationCommand(queryLength);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (selectedMaxTurnIdx == props.maxTurn) {
|
||||
return eraseSelectedTurnList(releaseSelect);
|
||||
}
|
||||
|
||||
const actions: [number[], TurnObj][] = [];
|
||||
|
||||
const emptyTurnList: number[] = [];
|
||||
|
||||
for (const srcTurnIdx of range(selectedMinTurnIdx, props.maxTurn - queryLength)) {
|
||||
const rawAction = reservedCommandList.value[srcTurnIdx];
|
||||
if (rawAction.action == emptyTurnObj.action) {
|
||||
emptyTurnList.push(srcTurnIdx + queryLength);
|
||||
continue;
|
||||
}
|
||||
actions.push([
|
||||
[srcTurnIdx + queryLength],
|
||||
{
|
||||
action: rawAction.action,
|
||||
arg: rawAction.arg,
|
||||
brief: rawAction.brief,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
emptyTurnList.push(...range(selectedMinTurnIdx, selectedMinTurnIdx + queryLength));
|
||||
actions.push([emptyTurnList, emptyTurnObj]);
|
||||
|
||||
const result = await reserveCommandDirect(actions);
|
||||
if (releaseSelect) {
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function setStoredActions() {
|
||||
const actions = queryActionHelper.extractQueryActions();
|
||||
const turnBrief = new Map<number, string>();
|
||||
for (const [subTurnList, action] of actions) {
|
||||
const actionName = action.action.split("_");
|
||||
const actionShortName = actionName.length == 1 ? actionName[0] : actionName[1];
|
||||
for (const turnIdx of subTurnList) {
|
||||
turnBrief.set(turnIdx, actionShortName[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const turnBriefStr = Array.from(turnBrief.entries())
|
||||
.sort(([turnA], [turnB]) => turnA - turnB)
|
||||
.map(([, action]) => action)
|
||||
.join("");
|
||||
|
||||
const nickName = trim(prompt("선택한 턴들의 별명을 지어주세요", turnBriefStr) ?? "");
|
||||
if (nickName == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
storedActionsHelper.setStoredActions(nickName, actions);
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
}
|
||||
|
||||
function deleteStoredActions(actionKey: string) {
|
||||
storedActionsHelper.deleteStoredActions(actionKey);
|
||||
}
|
||||
|
||||
async function useStoredAction(rawActions: [number[], TurnObj][]) {
|
||||
const reqTurnList = queryActionHelper.getSelectedTurnList();
|
||||
const actions = queryActionHelper.amplifyQueryActions(rawActions, reqTurnList);
|
||||
const result = await reserveCommandDirect(actions);
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
return result;
|
||||
}
|
||||
|
||||
function getQueryActionHelper(): QueryActionHelper {
|
||||
return queryActionHelper;
|
||||
}
|
||||
|
||||
function getStoredActionHeler(): StoredActionsHelper {
|
||||
return storedActionsHelper;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
useStoredAction,
|
||||
deleteStoredActions,
|
||||
clipboardCut,
|
||||
clipboardCopy,
|
||||
clipboardPaste,
|
||||
getQueryActionHelper,
|
||||
getStoredActionHeler,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.date,
|
||||
() => {
|
||||
triggerUpdateCommandList("date");
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => props.year,
|
||||
() => {
|
||||
triggerUpdateCommandList("year");
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => props.month,
|
||||
() => {
|
||||
triggerUpdateCommandList("month");
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => props.turnTime,
|
||||
() => {
|
||||
triggerUpdateCommandList("turnTime");
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => props.commandList,
|
||||
() => {
|
||||
triggerUpdateCommandList("commandList");
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => props.selectedTurn,
|
||||
(val: Set<number>) => {
|
||||
console.log(val);
|
||||
if (val === selectedTurnList.value) {
|
||||
console.log("pass!");
|
||||
return;
|
||||
}
|
||||
selectedTurnList.value.clear();
|
||||
for (const t of val.values()) {
|
||||
selectedTurnList.value.add(t);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(selectedTurnList, () => {
|
||||
console.log(selectedTurnList.value);
|
||||
emit("update:selectedTurn", selectedTurnList.value);
|
||||
});
|
||||
|
||||
const commandQuickReserveForm = ref<InstanceType<typeof CommandSelectForm> | null>(null);
|
||||
const commandSelectForm = ref<InstanceType<typeof CommandSelectForm> | null>(null);
|
||||
|
||||
const currentQuickReserveTarget = ref(-1);
|
||||
function chooseQuickReserveCommand(val?: string) {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
selectedCommand.value = invCommandMap[val];
|
||||
selectedTurnList.value.clear();
|
||||
selectedTurnList.value.add(currentQuickReserveTarget.value);
|
||||
void reserveCommand();
|
||||
}
|
||||
|
||||
function toggleQuickReserveForm(turnIdx: number) {
|
||||
if (turnIdx == currentQuickReserveTarget.value) {
|
||||
commandQuickReserveForm.value?.toggle();
|
||||
return;
|
||||
}
|
||||
currentQuickReserveTarget.value = turnIdx;
|
||||
commandQuickReserveForm.value?.show();
|
||||
}
|
||||
|
||||
const isEditMode = storedActionsHelper.isEditMode;
|
||||
watch(isEditMode, (newEditMode) => {
|
||||
if (newEditMode) {
|
||||
commandQuickReserveForm.value?.close();
|
||||
currentQuickReserveTarget.value = -1;
|
||||
} else {
|
||||
commandSelectForm.value?.close();
|
||||
}
|
||||
});
|
||||
function toggleForm($event: Event): void {
|
||||
$event.preventDefault();
|
||||
|
||||
const form = commandSelectForm.value;
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
form.toggle();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateCommandList();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "@scss/common/break_500px.scss";
|
||||
@import "@scss/common/variables.scss";
|
||||
@import "@scss/common/bootswatch_custom_variables.scss";
|
||||
|
||||
.chiefReservedCommand {
|
||||
background-color: $gray-900;
|
||||
|
||||
.commandTable.editMode {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(39.67px, 1fr) minmax(28px, 1fr) 5fr;
|
||||
//30, 70, 37.65, 160
|
||||
}
|
||||
|
||||
.commandTable.singleMode {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(39.67px, 1fr) 5fr minmax(28px, 1fr);
|
||||
//30, 70, 160, 37.65
|
||||
}
|
||||
|
||||
@include media-1000px {
|
||||
.turn_pad {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.multiselect__content-wrapper {
|
||||
margin-left: calc(-100% / 7 * 2);
|
||||
width: calc(100% / 7 * 12);
|
||||
}
|
||||
|
||||
.multiselect__single {
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-500px {
|
||||
.dropdown-item {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.multiselect__content-wrapper {
|
||||
margin-left: calc(-100% / 7 * 2);
|
||||
width: calc(100% / 7 * 12);
|
||||
}
|
||||
|
||||
.commandPad {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.btn {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.month_pad,
|
||||
.time_pad,
|
||||
.turn_pad {
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.month_pad:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.month_pad,
|
||||
.time_pad,
|
||||
.turn_pad {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.turn_pad {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.turn_pad .turn_text {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<div v-if="showForm" class="my-1">
|
||||
<div class="commandCategory row gx-0 gy-1">
|
||||
<div
|
||||
v-for="[categoryKey, { deco: categoryDeco }] of commandList"
|
||||
:key="categoryKey"
|
||||
class="categoryItem col-4 d-grid"
|
||||
>
|
||||
<BButton variant="success" :active="chosenCategory == categoryKey" @click="chosenCategory = categoryKey">
|
||||
{{ categoryDeco.altName ?? categoryDeco.name }}
|
||||
</BButton>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="commandList my-1"
|
||||
:style="{
|
||||
display: 'grid',
|
||||
alignItems: 'self-start',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="[category, { values }] of commandList"
|
||||
:key="category"
|
||||
class="row gx-1 gy-1"
|
||||
:style="{ visibility: category == chosenCategory ? 'visible' : 'hidden', gridRow: '1', gridColumn: '1' }"
|
||||
>
|
||||
<div
|
||||
v-for="commandItem of values"
|
||||
:key="commandItem.value"
|
||||
class="col-6 d-grid"
|
||||
@click="close(commandItem.value)"
|
||||
>
|
||||
<div class="commandItem">
|
||||
<div class="commandBody">
|
||||
<p :class="['center', 'my-0', commandItem.possible ? '' : 'commandImpossible']">
|
||||
{{ commandItem.simpleName }}
|
||||
<span v-if="commandItem.compensation > 0" class="compensatePositive">▲</span>
|
||||
<span v-else-if="commandItem.compensation < 0" class="compensateNegative">▼</span>
|
||||
</p>
|
||||
<small class="center" :style="{ display: 'block' }">
|
||||
{{
|
||||
commandItem.title.startsWith(commandItem.simpleName)
|
||||
? commandItem.title.substring(commandItem.simpleName.length)
|
||||
: commandItem.title
|
||||
}}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!hideClose" class="commandBottom row mt-1 mb-1">
|
||||
<div class="offset-8 col-4 d-grid">
|
||||
<BButton @click="close()"> 닫기 </BButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { CommandItem } from "@/defs";
|
||||
import { BButton } from "bootstrap-vue-3";
|
||||
import { ref, type PropType, watch, onMounted } from "vue";
|
||||
|
||||
interface CategoryDecoration {
|
||||
name: string;
|
||||
altName?: string;
|
||||
//icon?: string,
|
||||
//color?: string,
|
||||
//backgroundColor?: string,
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
categoryInfo: {
|
||||
type: Object as PropType<Record<string, Omit<CategoryDecoration, "name">>>,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
commandList: {
|
||||
type: Object as PropType<
|
||||
{
|
||||
category: string;
|
||||
values: CommandItem[];
|
||||
}[]
|
||||
>,
|
||||
required: true,
|
||||
},
|
||||
anchor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ".commandSelectFormAnchor",
|
||||
},
|
||||
hideClose: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
activatedCategory: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const chosenCategory = ref<string>("-");
|
||||
const chosenSubList = ref<CommandItem[]>([]);
|
||||
|
||||
const categories = new Set(props.commandList.map(({ category }) => category));
|
||||
|
||||
watch(
|
||||
() => props.activatedCategory,
|
||||
(newValue) => {
|
||||
chosenCategory.value = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
const showForm = ref(false);
|
||||
|
||||
function convCategoryDeco(category: string): CategoryDecoration {
|
||||
const itemInfo = props.categoryInfo?.[category];
|
||||
if (!itemInfo) {
|
||||
return {
|
||||
name: category,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: category,
|
||||
...itemInfo,
|
||||
};
|
||||
}
|
||||
|
||||
const commandList = ref(
|
||||
new Map<
|
||||
string,
|
||||
{
|
||||
deco: CategoryDecoration;
|
||||
values: CommandItem[];
|
||||
}
|
||||
>()
|
||||
);
|
||||
|
||||
function updateCommandList(rawCommandList: typeof props.commandList) {
|
||||
commandList.value.clear();
|
||||
for (const { category, values } of rawCommandList) {
|
||||
commandList.value.set(category, {
|
||||
deco: convCategoryDeco(category),
|
||||
values,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.commandList, updateCommandList);
|
||||
updateCommandList(props.commandList);
|
||||
|
||||
watch(chosenCategory, (category) => {
|
||||
const itemInfo = commandList.value?.get(category);
|
||||
if (itemInfo === undefined) {
|
||||
console.error(`category 없음: ${category}`);
|
||||
return;
|
||||
}
|
||||
chosenSubList.value = itemInfo.values;
|
||||
if (props.activatedCategory !== category) {
|
||||
emits("update:activatedCategory", category);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (!categories.has(props.activatedCategory)) {
|
||||
chosenCategory.value = props.commandList[0].category;
|
||||
} else {
|
||||
chosenCategory.value = props.activatedCategory;
|
||||
}
|
||||
});
|
||||
|
||||
function show(): void {
|
||||
showForm.value = true;
|
||||
}
|
||||
|
||||
function toggle(): void {
|
||||
showForm.value = !showForm.value;
|
||||
if (showForm.value === false) {
|
||||
emits("onClose");
|
||||
}
|
||||
}
|
||||
|
||||
function close(category?: string): void {
|
||||
showForm.value = false;
|
||||
emits("onClose", category);
|
||||
}
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: "onClose", command?: string): void;
|
||||
(event: "update:activatedCategory", category: string): void;
|
||||
}>();
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
close,
|
||||
toggle,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.commandItem {
|
||||
border: gray 1px solid;
|
||||
border-radius: 0.5em;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
padding: 0.1em;
|
||||
margin: 0;
|
||||
min-height: 2.8em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<div
|
||||
ref="container"
|
||||
:style="{
|
||||
position: 'relative',
|
||||
userSelect: disabled ? undefined : 'none',
|
||||
overflow: 'hidden',
|
||||
touchAction: disabled ? undefined : 'none',
|
||||
}"
|
||||
:class="{ disabledDrag: disabled }"
|
||||
>
|
||||
<slot :selected="intersected" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/// https://github.com/andi23rosca/drag-select-vue/blob/master/src/DragSelect.vue
|
||||
import { defineComponent, ref, watch, onMounted, onBeforeUnmount, type PropType } from "vue";
|
||||
import VueTypes from "vue-types";
|
||||
|
||||
function getDimensions(p1: coord, p2: coord): rect {
|
||||
return {
|
||||
width: Math.abs(p1.x - p2.x),
|
||||
height: Math.abs(p1.y - p2.y),
|
||||
};
|
||||
}
|
||||
function collisionCheck(node1: DOMRect, node2: DOMRect): boolean {
|
||||
return (
|
||||
node1.left < node2.left + node2.width &&
|
||||
node1.left + node1.width > node2.left &&
|
||||
node1.top < node2.top + node2.height &&
|
||||
node1.top + node1.height > node2.top
|
||||
);
|
||||
}
|
||||
|
||||
type coord = { x: number; y: number };
|
||||
type rect = { width: number; height: number };
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
attribute: VueTypes.string.isRequired,
|
||||
color: VueTypes.string.def("#4299E1"),
|
||||
opacity: VueTypes.number.def(0.7),
|
||||
modelValue: {
|
||||
type: Object as PropType<Set<string>>,
|
||||
required: false,
|
||||
default: () => ref(new Set()),
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue", "dragDone", "dragStart"],
|
||||
setup(props, { emit }) {
|
||||
const intersected = ref<Set<string>>(props.modelValue);
|
||||
const container = ref<HTMLElement>();
|
||||
|
||||
watch(intersected, (val) => {
|
||||
emit("update:modelValue", val);
|
||||
});
|
||||
watch(props.modelValue, (val) => {
|
||||
if (intersected.value === val) {
|
||||
return;
|
||||
}
|
||||
intersected.value = val;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (!container.value) {
|
||||
console.error(`Container is not referenced.`);
|
||||
return;
|
||||
}
|
||||
const uContainer = container.value;
|
||||
let containerRect = uContainer.getBoundingClientRect();
|
||||
|
||||
function getCoords(e: MouseEvent | Touch): coord {
|
||||
return {
|
||||
x: e.clientX - containerRect.left,
|
||||
y: e.clientY - containerRect.top,
|
||||
};
|
||||
}
|
||||
let children: HTMLCollection;
|
||||
let box = document.createElement("div");
|
||||
box.setAttribute("data-drag-box-component", "");
|
||||
box.style.position = "absolute";
|
||||
box.style.backgroundColor = props.color;
|
||||
box.style.opacity = `${props.opacity}`;
|
||||
let start = { x: 0, y: 0 };
|
||||
let end = { x: 0, y: 0 };
|
||||
function intersection() {
|
||||
const rect = box.getBoundingClientRect();
|
||||
const localIntersected = new Set<string>();
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
if (collisionCheck(rect, children[i].getBoundingClientRect())) {
|
||||
const attr = children[i].getAttribute(props.attribute);
|
||||
if (children[i].hasAttribute(props.attribute)) {
|
||||
localIntersected.add(attr as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let dismatch = false;
|
||||
for (const oldVal of intersected.value) {
|
||||
if (!localIntersected.has(oldVal)) {
|
||||
dismatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!dismatch) {
|
||||
for (const newVal of localIntersected) {
|
||||
if (!intersected.value.has(newVal)) {
|
||||
dismatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dismatch) {
|
||||
intersected.value = localIntersected;
|
||||
}
|
||||
}
|
||||
function touchStart(e: TouchEvent) {
|
||||
e.preventDefault();
|
||||
startDrag(e.touches[0]);
|
||||
}
|
||||
function touchMove(e: TouchEvent) {
|
||||
e.preventDefault();
|
||||
drag(e.touches[0]);
|
||||
}
|
||||
|
||||
let isMine = false;
|
||||
function startDrag(e: MouseEvent | Touch) {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
containerRect = uContainer.getBoundingClientRect();
|
||||
children = uContainer.children;
|
||||
start = getCoords(e);
|
||||
end = start;
|
||||
document.addEventListener("mousemove", drag);
|
||||
document.addEventListener("touchmove", touchMove);
|
||||
box.style.top = start.y + "px";
|
||||
box.style.left = start.x + "px";
|
||||
uContainer.append(box);
|
||||
intersection();
|
||||
isMine = true;
|
||||
emit("dragStart");
|
||||
}
|
||||
function drag(e: MouseEvent | Touch) {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
end = getCoords(e);
|
||||
const dimensions = getDimensions(start, end);
|
||||
if (end.x < start.x) {
|
||||
box.style.left = end.x + "px";
|
||||
}
|
||||
if (end.y < start.y) {
|
||||
box.style.top = end.y + "px";
|
||||
}
|
||||
box.style.width = dimensions.width + "px";
|
||||
box.style.height = dimensions.height + "px";
|
||||
intersection();
|
||||
}
|
||||
function endDrag() {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
start = { x: 0, y: 0 };
|
||||
end = { x: 0, y: 0 };
|
||||
box.style.width = "0";
|
||||
box.style.height = "0";
|
||||
document.removeEventListener("mousemove", drag);
|
||||
document.removeEventListener("touchmove", touchMove);
|
||||
box.remove();
|
||||
if (isMine) {
|
||||
emit("dragDone", intersected.value);
|
||||
}
|
||||
isMine = false;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.disabled,
|
||||
(disabledNext) => {
|
||||
if (disabledNext) {
|
||||
uContainer.removeEventListener("mousedown", startDrag);
|
||||
uContainer.removeEventListener("touchstart", touchStart);
|
||||
document.removeEventListener("mouseup", endDrag);
|
||||
document.removeEventListener("touchend", endDrag);
|
||||
} else {
|
||||
uContainer.addEventListener("mousedown", startDrag);
|
||||
uContainer.addEventListener("touchstart", touchStart);
|
||||
document.addEventListener("mouseup", endDrag);
|
||||
document.addEventListener("touchend", endDrag);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!props.disabled) {
|
||||
uContainer.addEventListener("mousedown", startDrag);
|
||||
uContainer.addEventListener("touchstart", touchStart);
|
||||
document.addEventListener("mouseup", endDrag);
|
||||
document.addEventListener("touchend", endDrag);
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
uContainer.removeEventListener("mousedown", startDrag);
|
||||
uContainer.removeEventListener("touchstart", touchStart);
|
||||
document.removeEventListener("mouseup", endDrag);
|
||||
document.removeEventListener("touchend", endDrag);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
intersected,
|
||||
container,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,266 @@
|
||||
<template>
|
||||
<div class="general-card-basic">
|
||||
<div
|
||||
class="general-icon"
|
||||
:style="{
|
||||
backgroundImage: `url(${iconPath})`,
|
||||
}"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="general-name"
|
||||
:style="{
|
||||
color: isBrightColor(nation.color) ? '#000' : '#fff',
|
||||
backgroundColor: nation.color,
|
||||
}"
|
||||
>
|
||||
{{ general.name }} 【{{ general.officerLevelText }} | {{ generalTypeCall }} |
|
||||
<span :style="{ color: injuryInfo.color }">{{ injuryInfo.text }}</span>
|
||||
】 {{ general.turntime.substring(11, 19) }}
|
||||
</div>
|
||||
|
||||
<div>통솔</div>
|
||||
<div>
|
||||
<div class="row gx-0">
|
||||
<div class="col">
|
||||
<span :style="{ color: injuryInfo.color }">{{ general.leadership }}</span>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-if="general.lbonus > 0" style="color: cyan">+{{ general.lbonus }}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<SammoBar :height="10" :percent="general.leadership_exp / 20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>무력</div>
|
||||
<div>
|
||||
<div class="row gx-0">
|
||||
<div class="col" :style="{ color: injuryInfo.color }">
|
||||
{{ general.strength }}
|
||||
</div>
|
||||
<div class="col">
|
||||
<SammoBar :height="10" :percent="general.strength_exp / 20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>지력</div>
|
||||
<div>
|
||||
<div class="row gx-0">
|
||||
<div class="col" :style="{ color: injuryInfo.color }">
|
||||
{{ general.intel }}
|
||||
</div>
|
||||
<div class="col">
|
||||
<SammoBar :height="10" :percent="general.intel_exp / 20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>명마</div>
|
||||
<div v-b-tooltip.hover :title="horse.info ?? undefined">{{ horse.name }}</div>
|
||||
<div>무기</div>
|
||||
<div v-b-tooltip.hover :title="weapon.info ?? undefined">{{ weapon.name }}</div>
|
||||
<div>서적</div>
|
||||
<div v-b-tooltip.hover :title="book.info ?? undefined">{{ book.name }}</div>
|
||||
|
||||
<div>자금</div>
|
||||
<div>{{ general.gold.toLocaleString() }}</div>
|
||||
<div>군량</div>
|
||||
<div>{{ general.rice.toLocaleString() }}</div>
|
||||
<div>도구</div>
|
||||
<div v-b-tooltip.hover :title="item.info ?? undefined">{{ item.name }}</div>
|
||||
|
||||
<!-- TODO: show_img_level을 고려 -->
|
||||
<div
|
||||
class="general-crew-type-icon"
|
||||
:style="{
|
||||
backgroundImage: `url(${imagePath}/crewtype${general.crewtype}.png)`,
|
||||
}"
|
||||
></div>
|
||||
|
||||
<div>병종</div>
|
||||
<div v-b-tooltip.hover :title="crewtype.info ?? undefined">{{ crewtype.name }}</div>
|
||||
<div>병사</div>
|
||||
<div>{{ general.crew.toLocaleString() }}</div>
|
||||
<div>성격</div>
|
||||
<div v-b-tooltip.hover :title="personal.info ?? undefined">{{ personal.name }}</div>
|
||||
|
||||
<!-- TODO: bonusTrain 같은 개념이 필요 -->
|
||||
<div>훈련</div>
|
||||
<div>{{ general.train }}</div>
|
||||
<div>사기</div>
|
||||
<div>{{ general.atmos }}</div>
|
||||
<div>특기</div>
|
||||
<div>
|
||||
<span v-b-tooltip.hover :title="specialDomestic.info ?? undefined"> {{ specialDomestic.name }}</span> /
|
||||
<span v-b-tooltip.hover :title="specialWar.info ?? undefined"> {{ specialWar.name }}</span>
|
||||
</div>
|
||||
|
||||
<div>Lv</div>
|
||||
<!-- TODO: 경험치 막대가 필요 -->
|
||||
<div class="general-exp-level">
|
||||
{{ general.explevel }}
|
||||
</div>
|
||||
<div class="general-exp-level-bar">{{ nextExpLevelRemain(general.experience, general.explevel) }}</div>
|
||||
<div>연령</div>
|
||||
<div :style="{ color: ageColor }">{{ general.age }}세</div>
|
||||
|
||||
<div>수비</div>
|
||||
<div class="general-defence-train">
|
||||
<span v-if="general.defence_train === 999" style="color: red">수비 안함</span>
|
||||
<span v-else style="color: limegreen">수비 함(훈사{{ general.defence_train }})</span>
|
||||
</div>
|
||||
<div>삭턴</div>
|
||||
<div>{{ general.killturn }} 턴</div>
|
||||
<div>실행</div>
|
||||
<div>{{ nextExecuteMinute }}분 남음</div>
|
||||
|
||||
<div>부대</div>
|
||||
<div v-if="!troopInfo" class="general-troop">-</div>
|
||||
<div v-else class="general-troop">
|
||||
<s v-if="troopInfo.leader.reservedCommand[0]?.action != 'che_집합'" style="color: gray">
|
||||
{{ troopInfo.name }}
|
||||
</s>
|
||||
<span v-else style="color: orange">
|
||||
{{ troopInfo.name }}({{ gameConstStore.cityConst[troopInfo.leader.city].name }})
|
||||
</span>
|
||||
</div>
|
||||
<div>벌점</div>
|
||||
<div class="general-v">
|
||||
{{ formatConnectScore(general.connect) }} {{ general.connect.toLocaleString() }}점({{ general.con }})
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { GeneralListItemP1 } from "@/defs/API/Nation";
|
||||
import { computed, inject, onMounted, ref, toRefs, type Ref } from "vue";
|
||||
import { getIconPath } from "@/util/getIconPath";
|
||||
import { isBrightColor } from "@/util/isBrightColor";
|
||||
import { formatInjury } from "@/utilGame/formatInjury";
|
||||
import type { NationStaticItem } from "@/defs";
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
import type { GameConstStore } from "@/GameConstStore";
|
||||
import { formatGeneralTypeCall } from "@/utilGame/formatGeneralTypeCall";
|
||||
import { nextExpLevelRemain } from "@/utilGame/nextExpLevelRemain";
|
||||
import { formatConnectScore } from "@/utilGame/formatConnectScore";
|
||||
import SammoBar from "@/components/SammoBar.vue";
|
||||
import { parseTime } from "@/util/parseTime";
|
||||
import { clamp } from "lodash";
|
||||
const imagePath = window.pathConfig.gameImage;
|
||||
const gameConstStore = unwrap(inject<Ref<GameConstStore>>("gameConstStore"));
|
||||
const props = defineProps<{
|
||||
general: GeneralListItemP1;
|
||||
troopInfo?: {
|
||||
leader: GeneralListItemP1;
|
||||
name: string;
|
||||
};
|
||||
nation: NationStaticItem;
|
||||
}>();
|
||||
|
||||
const { general, troopInfo, nation } = toRefs(props);
|
||||
const iconPath = computed(() => getIconPath(general.value.imgsvr, general.value.picture));
|
||||
const injuryInfo = computed(() => {
|
||||
const [text, color] = formatInjury(general.value.injury);
|
||||
return {
|
||||
text,
|
||||
color,
|
||||
};
|
||||
});
|
||||
const generalTypeCall = computed(() =>
|
||||
formatGeneralTypeCall(
|
||||
general.value.leadership,
|
||||
general.value.strength,
|
||||
general.value.intel,
|
||||
gameConstStore.value.gameConst
|
||||
)
|
||||
);
|
||||
|
||||
const horse = computed(
|
||||
() => gameConstStore.value.iActionInfo.item[general.value.horse] ?? { value: "None", name: "-" }
|
||||
);
|
||||
const weapon = computed(
|
||||
() => gameConstStore.value.iActionInfo.item[general.value.weapon] ?? { value: "None", name: "-" }
|
||||
);
|
||||
const book = computed(() => gameConstStore.value.iActionInfo.item[general.value.book] ?? { value: "None", name: "-" });
|
||||
const item = computed(() => gameConstStore.value.iActionInfo.item[general.value.item] ?? { value: "None", name: "-" });
|
||||
|
||||
const crewtype = computed(
|
||||
() => gameConstStore.value.iActionInfo.crewtype[general.value.crewtype] ?? { value: "None", name: "-" }
|
||||
);
|
||||
|
||||
const personal = computed(
|
||||
() => gameConstStore.value.iActionInfo.personality[general.value.personal] ?? { value: "None", name: "-" }
|
||||
);
|
||||
const specialDomestic = computed(
|
||||
() =>
|
||||
gameConstStore.value.iActionInfo.specialDomestic[general.value.specialDomestic] ?? {
|
||||
value: "None",
|
||||
name: `${general.value.specage}세`,
|
||||
}
|
||||
);
|
||||
const specialWar = computed(
|
||||
() =>
|
||||
gameConstStore.value.iActionInfo.specialWar[general.value.specialWar] ?? {
|
||||
value: "None",
|
||||
name: `${general.value.specage2}세`,
|
||||
}
|
||||
);
|
||||
|
||||
const ageColor = computed(() => {
|
||||
const age = general.value.age;
|
||||
const retirementYear = gameConstStore.value.gameConst.retirementYear;
|
||||
if (age < retirementYear * 0.75) {
|
||||
return "limegreen";
|
||||
}
|
||||
if (age < retirementYear) {
|
||||
return "yellow";
|
||||
}
|
||||
return "red";
|
||||
});
|
||||
|
||||
const nextExecuteMinute = ref(999);
|
||||
onMounted(() => {
|
||||
const now = new Date();
|
||||
const turnTime = parseTime(general.value.turntime);
|
||||
nextExecuteMinute.value = Math.floor(clamp(turnTime.getSeconds() - now.getSeconds() / 60, 0));
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.general-card-basic {
|
||||
display: grid;
|
||||
grid-template-columns: 64px repeat(3, 2fr 5fr);
|
||||
grid-template-rows: repeat(9, calc(64px / 3));
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
.general-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
grid-row: 1 / 4;
|
||||
}
|
||||
|
||||
.general-name {
|
||||
grid-row: 1 / 2;
|
||||
grid-column: 2 / 8;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.general-crew-type-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
grid-row: 4 / 7;
|
||||
}
|
||||
|
||||
.general-exp-level-bar {
|
||||
grid-column: 3 / 6;
|
||||
}
|
||||
|
||||
.general-defence-train {
|
||||
grid-column: 2 / 4;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,1243 @@
|
||||
<template>
|
||||
<Teleport v-if="toolbarID" :to="`#${toolbarID}`">
|
||||
<BButtonGroup class="d-flex general-list-toolbar">
|
||||
<BDropdown class="w-50" menuClass="view-mode-list" variant="primary" text="보기 모드">
|
||||
<BDropdownItem @click="setDisplaySetting([true, 'normal'], defaultDisplaySetting.normal)">기본</BDropdownItem>
|
||||
<BDropdownItem @click="setDisplaySetting([true, 'war'], defaultDisplaySetting.war)">전투</BDropdownItem>
|
||||
<BDropdownDivider />
|
||||
<BDropdownItem @click="storeDisplaySetting"><i class="bi bi-bookmark-plus-fill" /> 보관하기</BDropdownItem>
|
||||
<BDropdownDivider />
|
||||
<BDropdownItem
|
||||
v-for="[key, setting] of displaySettings.entries()"
|
||||
:key="key"
|
||||
@click="setDisplaySetting([false, key], setting)"
|
||||
><div class="row gx-0">
|
||||
<div class="col-9 text-wrap">
|
||||
<span class="align-middle">{{ key }}</span>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="d-grid"><BButton size="sm" @click="deleteDisplaySetting(key)">삭제</BButton></div>
|
||||
</div>
|
||||
</div></BDropdownItem
|
||||
>
|
||||
</BDropdown>
|
||||
<!-- eslint-disable-next-line vue/max-attributes-per-line -->
|
||||
<BDropdown class="w-50" variant="info" text="열 선택" menuClass="column-menu" right>
|
||||
<template v-for="[colID, col, depth] of getColumnList()" :key="[colID, depth]">
|
||||
<BDropdownItem v-if="col instanceof ProvidedColumnGroup" disabled>
|
||||
<span :style="{ marginLeft: depth ? `${12 * depth}px` : undefined }">
|
||||
{{ col.getColGroupDef()?.headerName }}</span
|
||||
></BDropdownItem
|
||||
>
|
||||
<BDropdownItem v-else>
|
||||
<div :style="{ marginLeft: depth ? `${12 * depth}px` : undefined }" class="form-check" @click.stop="1">
|
||||
<input
|
||||
:id="`column-type-${colID}`"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
:checked="col.isVisible()"
|
||||
@change.stop="toggleColumn(colID, col)"
|
||||
/>
|
||||
<label
|
||||
class="form-check-label"
|
||||
:for="`column-type-${colID}`"
|
||||
:style="{
|
||||
textDecoration: validColumns.has(colID) ? undefined : 'line-through',
|
||||
}"
|
||||
>
|
||||
{{ col.getColDef().headerName }}
|
||||
</label>
|
||||
</div></BDropdownItem
|
||||
>
|
||||
</template>
|
||||
<BDropdownDivider />
|
||||
</BDropdown>
|
||||
</BButtonGroup>
|
||||
</Teleport>
|
||||
<div
|
||||
class="component-general-list"
|
||||
:style="{
|
||||
height: props.height === 'fill' ? '100%' : props.height === 'static' ? undefined : `${props.height}px`,
|
||||
}"
|
||||
>
|
||||
<AgGridVue
|
||||
style="width: 100%; height: 100%"
|
||||
class="ag-theme-balham-dark"
|
||||
:getRowId="getRowId"
|
||||
:getRowHeight="getRowHeight"
|
||||
:columnDefs="columnDefs"
|
||||
:rowData="list"
|
||||
:defaultColDef="defaultColDef"
|
||||
:suppressColumnMoveAnimation="suppressColumnMoveAnimation"
|
||||
@grid-ready="onGridReady"
|
||||
@cell-clicked="onCellClicked"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts"></script>
|
||||
<script lang="ts" setup>
|
||||
import type { GeneralListItem, GeneralListItemP1, GeneralListItemP2, GeneralListResponse } from "@/defs/API/Nation";
|
||||
import { getIconPath } from "@/util/getIconPath";
|
||||
import { inject, ref, watch, type PropType, type Ref, type StyleValue } from "vue";
|
||||
import { AgGridVue } from "ag-grid-vue3";
|
||||
import type {
|
||||
Column,
|
||||
CellClassParams,
|
||||
CellStyle,
|
||||
ColDef,
|
||||
ColGroupDef,
|
||||
ColumnApi,
|
||||
GetRowIdParams,
|
||||
GridApi,
|
||||
GridReadyEvent,
|
||||
RowNode,
|
||||
CellClickedEvent,
|
||||
} from "ag-grid-community";
|
||||
import { ProvidedColumnGroup } from "ag-grid-community";
|
||||
import { getNpcColor } from "@/common_legacy";
|
||||
import type { BaseWithValueColDefParams, ValueGetterParams } from "ag-grid-community/dist/lib/entities/colDef";
|
||||
import type { GameConstStore } from "@/GameConstStore";
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
import SimpleTooltipCell from "@/gridCellRenderer/SimpleTooltipCell.vue";
|
||||
import GridTooltipCell, { type GridCellInfo } from "@/gridCellRenderer/GridTooltipCell.vue";
|
||||
import { formatConnectScore } from "@/utilGame/formatConnectScore";
|
||||
import { convertSearch초성 } from "@/util/convertSearch초성";
|
||||
import { isString } from "lodash";
|
||||
import { formatDefenceTrain } from "@/utilGame/formatDefenceTrain";
|
||||
import { BDropdownItem, BDropdownDivider, BButtonGroup, BDropdown, BButton } from "bootstrap-vue-3";
|
||||
import { unwrap_err } from "@/util/unwrap_err";
|
||||
import { RuntimeError } from "@/util/RuntimeError";
|
||||
import { defaultDisplaySetting, type GridDisplaySetting } from "@/defs/gridDefs";
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Array as PropType<GeneralListItem[]>,
|
||||
required: true,
|
||||
},
|
||||
troops: {
|
||||
type: Object as PropType<Record<number, string>>,
|
||||
required: true,
|
||||
},
|
||||
height: {
|
||||
type: String as PropType<"static" | "fill" | number | `${number}px` | `${number}%`>,
|
||||
required: false,
|
||||
default: "static",
|
||||
},
|
||||
env: {
|
||||
type: Object as PropType<GeneralListResponse["env"]>,
|
||||
required: true,
|
||||
},
|
||||
toolbarID: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "generic",
|
||||
},
|
||||
availableGeneralClick: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "generalClick", generalID: number): void;
|
||||
}>();
|
||||
|
||||
const suppressColumnMoveAnimation = ref(true);
|
||||
const gameConstStore = unwrap(inject<Ref<GameConstStore>>("gameConstStore"));
|
||||
|
||||
const validColumns = ref(new Set<string>());
|
||||
|
||||
watch(
|
||||
() => props.list,
|
||||
(newValue) => {
|
||||
const newValidColumns = new Set<string>(["icon"]);
|
||||
if (newValue.length > 0) {
|
||||
for (const key of Object.keys(newValue[0])) {
|
||||
newValidColumns.add(key);
|
||||
}
|
||||
validColumns.value = newValidColumns;
|
||||
}
|
||||
setTimeout(() => {
|
||||
gridApi.value?.redrawRows();
|
||||
}, 0);
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => props.height,
|
||||
(val) => {
|
||||
if (val === "static") {
|
||||
gridApi.value?.setDomLayout("autoHeight");
|
||||
} else {
|
||||
gridApi.value?.setDomLayout("normal");
|
||||
}
|
||||
}
|
||||
);
|
||||
const generalByID = ref(new Map<number, GeneralListItem>());
|
||||
function refineGeneralList(list: GeneralListItem[]) {
|
||||
const map = new Map<number, GeneralListItem>();
|
||||
for (const general of list) {
|
||||
map.set(general.no, general);
|
||||
}
|
||||
generalByID.value = map;
|
||||
}
|
||||
refineGeneralList(props.list);
|
||||
watch(() => props.list, refineGeneralList);
|
||||
const gridApi = ref<GridApi>();
|
||||
const columnApi = ref<ColumnApi>();
|
||||
const rowHeight = ref(68);
|
||||
function getRowId(params: GetRowIdParams): string {
|
||||
const genID = (params.data as GeneralListItem).no;
|
||||
return `${genID}`;
|
||||
}
|
||||
|
||||
function setDisplaySetting(settingKey: SettingKeyType, setting: GridDisplaySetting) {
|
||||
if (!columnApi.value) {
|
||||
console.error("nyc?");
|
||||
return;
|
||||
}
|
||||
columnApi.value.applyColumnState({ state: setting.column, applyOrder: true });
|
||||
columnApi.value.setColumnGroupState(setting.columnGroup);
|
||||
currentSetting.value = settingKey;
|
||||
}
|
||||
|
||||
const displaySettings = ref(new Map<string, GridDisplaySetting>());
|
||||
const displaySettingVersion = 1; //추가되는 걸로 버전 올리지 말고, 사용할 수 없게 될때만 올리기
|
||||
const displaySettingsKey = "GeneralListDisplaySetting";
|
||||
|
||||
function getLastUsedSettingsKey() {
|
||||
const lastUsedSettingsKey = "LastUsedSettingsKey";
|
||||
return `${lastUsedSettingsKey}_${props.role}`;
|
||||
}
|
||||
|
||||
function loadDisplaySetting() {
|
||||
const rawSettings = localStorage.getItem(displaySettingsKey);
|
||||
if (!rawSettings) {
|
||||
return;
|
||||
}
|
||||
const settings: { version: number; settings: [string, GridDisplaySetting][] } = JSON.parse(rawSettings);
|
||||
if (settings.version != displaySettingVersion) {
|
||||
localStorage.removeItem(displaySettingsKey);
|
||||
return;
|
||||
}
|
||||
|
||||
displaySettings.value = new Map(settings.settings);
|
||||
}
|
||||
loadDisplaySetting();
|
||||
|
||||
type SettingKeyType = [true, keyof typeof defaultDisplaySetting] | [false, string];
|
||||
const currentSetting = ref<SettingKeyType>([true, "normal"]);
|
||||
function loadLastUsedSettings() {
|
||||
const rawLastSettingKey = localStorage.getItem(getLastUsedSettingsKey());
|
||||
if (!rawLastSettingKey) {
|
||||
return;
|
||||
}
|
||||
const settingKey: SettingKeyType = JSON.parse(rawLastSettingKey);
|
||||
const [isDefault, settingKeyName] = settingKey;
|
||||
if (isDefault) {
|
||||
if (!(settingKeyName in defaultDisplaySetting)) {
|
||||
console.error(`${settingKeyName}은 이제 기본 지원 타입이 아닙니다.`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!displaySettings.value.has(settingKeyName)) {
|
||||
console.error(`${settingKeyName}는 저장되어있지 않습니다.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
currentSetting.value = settingKey;
|
||||
return settingKey;
|
||||
}
|
||||
watch(currentSetting, (newTypeKey) => {
|
||||
localStorage.setItem(getLastUsedSettingsKey(), JSON.stringify(newTypeKey));
|
||||
});
|
||||
|
||||
watch(
|
||||
displaySettings,
|
||||
(newSettings) => {
|
||||
const settings = Array.from(newSettings.entries());
|
||||
localStorage.setItem(
|
||||
displaySettingsKey,
|
||||
JSON.stringify({
|
||||
version: displaySettingVersion,
|
||||
settings,
|
||||
})
|
||||
);
|
||||
console.log("저장!", Array.from(newSettings.keys()));
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
function deleteDisplaySetting(key: string) {
|
||||
if (!confirm(`${key} 설정을 지울까요?`)) {
|
||||
return;
|
||||
}
|
||||
displaySettings.value.delete(key);
|
||||
}
|
||||
|
||||
function storeDisplaySetting() {
|
||||
if (!columnApi.value) {
|
||||
console.error("nyc?");
|
||||
return;
|
||||
}
|
||||
|
||||
const nickName = prompt("선택한 설정의 별명을 지어주세요", currentSetting.value[0] ? "" : currentSetting.value[1]);
|
||||
if (!nickName) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (displaySettings.value.has(nickName)) {
|
||||
if (!confirm("이미 있는 이름입니다. 덮어쓸까요?")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const setting: GridDisplaySetting = {
|
||||
column: columnApi.value.getColumnState(),
|
||||
columnGroup: columnApi.value.getColumnGroupState(),
|
||||
};
|
||||
|
||||
displaySettings.value.set(nickName, setting);
|
||||
currentSetting.value = [false, nickName];
|
||||
}
|
||||
|
||||
function onGridReady(params: GridReadyEvent) {
|
||||
gridApi.value = params.api;
|
||||
columnApi.value = params.columnApi;
|
||||
if (props.height === "static") {
|
||||
params.api.setDomLayout("autoHeight");
|
||||
} else {
|
||||
params.api.setDomLayout("normal");
|
||||
}
|
||||
loadLastUsedSettings();
|
||||
if (currentSetting.value[0]) {
|
||||
setDisplaySetting(currentSetting.value, defaultDisplaySetting[currentSetting.value[1]]);
|
||||
} else {
|
||||
setDisplaySetting(currentSetting.value, unwrap(displaySettings.value.get(currentSetting.value[1])));
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
suppressColumnMoveAnimation.value = false;
|
||||
}, 1);
|
||||
}
|
||||
|
||||
function onCellClicked(event: CellClickedEvent) {
|
||||
const colID = event.column.getColId();
|
||||
if (colID === "icon" || colID === "name") {
|
||||
const generalItem = event.data as GeneralListItem;
|
||||
emit("generalClick", generalItem.no);
|
||||
}
|
||||
}
|
||||
|
||||
function getRowHeight(): number {
|
||||
return rowHeight.value;
|
||||
}
|
||||
type headerType =
|
||||
| keyof Omit<GeneralListItemP2, "no" | "imgsvr" | "picture" | "lbonus" | "permission" | "st0" | "st1" | "st2">
|
||||
| "stat"
|
||||
| "icon"
|
||||
| "goldRice"
|
||||
| "expDedLv"
|
||||
| "crewtypeAndCrew"
|
||||
| "trainAtmos"
|
||||
| "specials"
|
||||
| "reservedCommandShort"
|
||||
| "killturnAndConnect"
|
||||
| "years"
|
||||
| "warResults";
|
||||
interface GenValueParams extends BaseWithValueColDefParams {
|
||||
data: GeneralListItem;
|
||||
}
|
||||
interface GenValueGetterParams extends ValueGetterParams {
|
||||
data: GeneralListItem;
|
||||
}
|
||||
interface GenRowNode extends RowNode {
|
||||
data: GeneralListItem;
|
||||
}
|
||||
interface GenColDef extends ColDef {
|
||||
colId: headerType;
|
||||
field?: headerType;
|
||||
headerName: string;
|
||||
valueFormatter?: string | ((params: GenValueParams) => string);
|
||||
filterValueGetter?: string | ((params: GenValueGetterParams) => unknown);
|
||||
valueGetter?: string | ((params: GenValueGetterParams) => unknown);
|
||||
}
|
||||
interface GenColGroupDef extends ColGroupDef {
|
||||
headerName: string;
|
||||
children: GenColDef[]; //1단만 할꺼다!
|
||||
groupId: headerType;
|
||||
}
|
||||
interface GenCellClassParams extends CellClassParams {
|
||||
data: GeneralListItem;
|
||||
}
|
||||
function getColumnList(): [headerType, ProvidedColumnGroup | Column, number?][] {
|
||||
const result: [headerType, ProvidedColumnGroup | Column, number?][] = [];
|
||||
if (!columnApi.value) {
|
||||
return result;
|
||||
}
|
||||
for (const [rawColKey, rawColDef] of Object.entries(columnRawDefs.value)) {
|
||||
if (rawColKey === "name") {
|
||||
continue;
|
||||
}
|
||||
if (!("children" in rawColDef)) {
|
||||
const col = unwrap_err(columnApi.value.getColumn(rawColDef.colId), RuntimeError, `no col: ${rawColDef.colId}`);
|
||||
result.push([rawColDef.colId, col]);
|
||||
continue;
|
||||
}
|
||||
const colGroup = unwrap_err(
|
||||
columnApi.value.getProvidedColumnGroup(rawColDef.groupId),
|
||||
RuntimeError,
|
||||
`no colGroup: ${rawColDef.groupId}`
|
||||
);
|
||||
result.push([rawColDef.groupId, colGroup]);
|
||||
for (const subColDef of rawColDef.children) {
|
||||
const subColId = subColDef.colId;
|
||||
if (rawColDef.groupId == subColId) {
|
||||
continue;
|
||||
}
|
||||
const col = unwrap_err(columnApi.value.getColumn(subColId), RuntimeError, `no subCol: ${subColDef.colId}`);
|
||||
result.push([subColId, col, 1]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function naiveCheClassNameFilter(value: string): string {
|
||||
if (!value) {
|
||||
return "-";
|
||||
}
|
||||
const text = value.split("_").pop() ?? "None";
|
||||
if (text === "None") {
|
||||
return "-";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
function numberFormatter(unit?: string) {
|
||||
if (unit) {
|
||||
return (value: GenValueParams): string => {
|
||||
if (value.value == null) {
|
||||
return "?";
|
||||
}
|
||||
const valueText = (value.value as number).toLocaleString();
|
||||
return `${valueText} ${unit}`;
|
||||
};
|
||||
}
|
||||
return (value: GenValueParams): string => {
|
||||
return (value.value as number).toLocaleString();
|
||||
};
|
||||
}
|
||||
function extractTroopInfo(value: GeneralListItem): [string, GeneralListItemP1] | undefined {
|
||||
if (!value.st1) {
|
||||
return undefined;
|
||||
}
|
||||
const troopID = value.troop;
|
||||
if (!(troopID in props.troops)) {
|
||||
return undefined;
|
||||
}
|
||||
const troopName = props.troops[troopID];
|
||||
const troopLeader = generalByID.value.get(troopID) as GeneralListItemP1 | undefined;
|
||||
if (troopLeader === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return [troopName, troopLeader];
|
||||
}
|
||||
function toggleColumn(colID: headerType, col: Column) {
|
||||
const newState = !col.isVisible();
|
||||
const target: string[] = [colID];
|
||||
const parent = col.getParent();
|
||||
if (newState) {
|
||||
for (const child of (parent.getChildren() ?? []) as Column[]) {
|
||||
if (parent.getGroupId() == child.getColDef().colId) {
|
||||
target.push(child.getColId());
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let stillVisible = false;
|
||||
let header: string | null = null;
|
||||
for (const child of (parent.getChildren() ?? []) as Column[]) {
|
||||
if (child.getColId() == colID) {
|
||||
continue;
|
||||
}
|
||||
if (parent.getGroupId() == child.getColDef().colId) {
|
||||
header = child.getColId();
|
||||
continue;
|
||||
}
|
||||
stillVisible = true;
|
||||
break;
|
||||
}
|
||||
if (!stillVisible && header) {
|
||||
target.push(header);
|
||||
}
|
||||
}
|
||||
columnApi.value?.setColumnsVisible(target, newState);
|
||||
}
|
||||
const defaultCellClass = ["cell-middle"];
|
||||
const centerCellClass = [...defaultCellClass, "cell-center"];
|
||||
const rightAlignClass = [...defaultCellClass, "cell-right"];
|
||||
const sortableNumber: Omit<GenColDef, "colId" | "headerName"> = {
|
||||
sortable: true,
|
||||
comparator: (a, b) => a - b,
|
||||
sortingOrder: ["desc", "asc", null],
|
||||
filter: "number",
|
||||
cellClass: rightAlignClass,
|
||||
};
|
||||
const defaultColDef = ref<ColDef>({
|
||||
resizable: true,
|
||||
headerClass: "default-cell-header",
|
||||
cellClass: centerCellClass,
|
||||
floatingFilter: true,
|
||||
width: 80,
|
||||
});
|
||||
const columnRawDefs = ref<Partial<Record<headerType, GenColDef | GenColGroupDef>>>({
|
||||
icon: {
|
||||
colId: "icon",
|
||||
headerName: "아이콘",
|
||||
width: 64 + 16,
|
||||
suppressSizeToFit: true,
|
||||
resizable: false,
|
||||
cellRenderer: (obj: GenValueParams) => {
|
||||
const { data: gen } = obj;
|
||||
return `<img src="${getIconPath(gen.imgsvr, gen.picture)}" width="64">`;
|
||||
},
|
||||
pinned: "left",
|
||||
cellClass: [props.availableGeneralClick ? "clickable-cell" : "", ...defaultCellClass],
|
||||
lockPosition: true,
|
||||
},
|
||||
name: {
|
||||
headerName: "장수명",
|
||||
colId: "name",
|
||||
field: "name",
|
||||
pinned: "left",
|
||||
sortable: true,
|
||||
width: 120,
|
||||
sortingOrder: ["asc", "desc", null],
|
||||
lockPosition: true,
|
||||
cellStyle: (val: GenCellClassParams) => {
|
||||
const gen = val.data;
|
||||
const style: StyleValue = {
|
||||
color: getNpcColor(gen.npc),
|
||||
};
|
||||
return style as CellStyle;
|
||||
},
|
||||
comparator: (_lhs, _rhs, { data: lhs }: GenRowNode, { data: rhs }: GenRowNode) => {
|
||||
const npcDiff = lhs.npc - rhs.npc;
|
||||
if (npcDiff != 0) {
|
||||
return npcDiff;
|
||||
}
|
||||
return lhs.name.localeCompare(rhs.name);
|
||||
},
|
||||
|
||||
filterValueGetter: ({ data }) => convertSearch초성(data.name),
|
||||
cellClass: [props.availableGeneralClick ? "clickable-cell" : "", ...defaultCellClass],
|
||||
filter: true,
|
||||
hide: false,
|
||||
lockVisible: true,
|
||||
},
|
||||
//npc: { headerName: "NPC", colId: "npc", field: "npc" },
|
||||
stat: {
|
||||
groupId: "stat",
|
||||
openByDefault: false,
|
||||
headerName: "능력치",
|
||||
children: [
|
||||
{
|
||||
colId: "stat",
|
||||
headerName: "통|무|지",
|
||||
width: 88,
|
||||
cellRenderer: (obj: GenValueParams) => {
|
||||
const gen = obj.data;
|
||||
return `${gen.leadership}|${gen.strength}|${gen.intel}`;
|
||||
},
|
||||
columnGroupShow: "closed",
|
||||
},
|
||||
{
|
||||
colId: "leadership",
|
||||
headerName: "통솔",
|
||||
field: "leadership",
|
||||
...sortableNumber,
|
||||
columnGroupShow: "open",
|
||||
width: 60,
|
||||
type: "numericColumn",
|
||||
},
|
||||
{
|
||||
colId: "strength",
|
||||
headerName: "무력",
|
||||
field: "strength",
|
||||
...sortableNumber,
|
||||
columnGroupShow: "open",
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
colId: "intel",
|
||||
headerName: "지력",
|
||||
field: "intel",
|
||||
...sortableNumber,
|
||||
columnGroupShow: "open",
|
||||
width: 60,
|
||||
},
|
||||
],
|
||||
},
|
||||
officerLevel: {
|
||||
headerName: "관직",
|
||||
colId: "officerLevel",
|
||||
field: "officerLevelText",
|
||||
sortable: true,
|
||||
comparator: (a, b, c, d) => c.data.officerLevel - d.data.officerLevel,
|
||||
cellRenderer: ({ data }: GenValueParams) => {
|
||||
if (data.officerLevel >= 5) {
|
||||
return `<span style="color:cyan;">${data.officerLevelText}</span>`;
|
||||
}
|
||||
if (data.st1 && 2 <= data.officerLevel && data.officerLevel <= 4) {
|
||||
const cityName = gameConstStore.value.cityConst[data.officer_city].name;
|
||||
return `${cityName}<br>${data.officerLevelText}`;
|
||||
}
|
||||
return data.officerLevelText;
|
||||
},
|
||||
filterValueGetter: ({ data }) => {
|
||||
if (data.st1 && 2 <= data.officerLevel && data.officerLevel <= 4) {
|
||||
const cityName = gameConstStore.value.cityConst[data.officer_city].name;
|
||||
return convertSearch초성(`${cityName} ${data.officerLevelText}`);
|
||||
}
|
||||
return convertSearch초성(data.officerLevelText);
|
||||
},
|
||||
filter: true,
|
||||
cellClass: centerCellClass,
|
||||
width: 70,
|
||||
},
|
||||
expDedLv: {
|
||||
headerName: "명성/계급",
|
||||
groupId: "expDedLv",
|
||||
width: 70,
|
||||
children: [
|
||||
{
|
||||
colId: "expDedLv",
|
||||
headerName: "",
|
||||
columnGroupShow: "closed",
|
||||
width: 60,
|
||||
cellRenderer: ({ data }: GenValueParams) => {
|
||||
return `Lv ${data.explevel}<br>${data.dedLevelText}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: "explevel",
|
||||
headerName: "명성",
|
||||
field: "explevel",
|
||||
width: 60,
|
||||
cellRenderer: ({ data }: GenValueParams) => {
|
||||
return `Lv ${data.explevel}<br>(${data.honorText})`;
|
||||
},
|
||||
...sortableNumber,
|
||||
cellClass: centerCellClass,
|
||||
columnGroupShow: "open",
|
||||
},
|
||||
{
|
||||
colId: "dedlevel",
|
||||
headerName: "계급",
|
||||
field: "dedLevelText",
|
||||
width: 70,
|
||||
cellRenderer: ({ data }: GenValueParams) => {
|
||||
return `${data.dedLevelText}<br>(${data.bill.toLocaleString()})`;
|
||||
},
|
||||
...sortableNumber,
|
||||
cellClass: centerCellClass,
|
||||
columnGroupShow: "open",
|
||||
},
|
||||
],
|
||||
},
|
||||
goldRice: {
|
||||
headerName: "자금",
|
||||
groupId: "goldRice",
|
||||
children: [
|
||||
{
|
||||
colId: "goldRice",
|
||||
headerName: "금/쌀",
|
||||
cellRenderer: ({ data }: GenValueParams) => {
|
||||
return `${data.gold.toLocaleString()} 금<br>${data.rice.toLocaleString()} 쌀`;
|
||||
},
|
||||
width: 80,
|
||||
cellClass: rightAlignClass,
|
||||
columnGroupShow: "closed",
|
||||
sortable: true,
|
||||
sortingOrder: ["desc", "asc", null],
|
||||
comparator(_a, _b, { data: lhs }: GenRowNode, { data: rhs }: GenRowNode) {
|
||||
const lhsAmount = lhs.gold + lhs.rice;
|
||||
const rhsAmount = rhs.gold + rhs.rice;
|
||||
return lhsAmount - rhsAmount;
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: "gold",
|
||||
headerName: "금",
|
||||
field: "gold",
|
||||
...sortableNumber,
|
||||
valueFormatter: numberFormatter("금"),
|
||||
width: 70,
|
||||
columnGroupShow: "open",
|
||||
},
|
||||
{
|
||||
colId: "rice",
|
||||
headerName: "쌀",
|
||||
field: "rice",
|
||||
...sortableNumber,
|
||||
valueFormatter: numberFormatter("쌀"),
|
||||
width: 70,
|
||||
columnGroupShow: "open",
|
||||
},
|
||||
],
|
||||
},
|
||||
city: {
|
||||
colId: "city",
|
||||
headerName: "도시",
|
||||
field: "city",
|
||||
valueGetter: ({ data }) => {
|
||||
if (!data.st1) {
|
||||
return "?";
|
||||
}
|
||||
return gameConstStore.value.cityConst[data.city].name;
|
||||
},
|
||||
filter: true,
|
||||
sortable: true,
|
||||
width: 60,
|
||||
filterValueGetter: ({ data }) => {
|
||||
if (!data.st1) {
|
||||
return "";
|
||||
}
|
||||
return convertSearch초성(gameConstStore.value.cityConst[data.city].name);
|
||||
},
|
||||
},
|
||||
troop: {
|
||||
colId: "troop",
|
||||
headerName: "부대",
|
||||
field: "troop",
|
||||
valueGetter: ({ data }: GenValueGetterParams) => {
|
||||
if (!data.st1) {
|
||||
return "?";
|
||||
}
|
||||
const troopInfo = extractTroopInfo(data);
|
||||
if (troopInfo === undefined) {
|
||||
return "-";
|
||||
}
|
||||
const [troopName, troopLeader] = troopInfo;
|
||||
const cityName = gameConstStore.value.cityConst[troopLeader.city].name;
|
||||
return [troopName, cityName];
|
||||
},
|
||||
cellRenderer: ({ value }: { value: [string, string] | string }) => {
|
||||
if (isString(value)) {
|
||||
return value;
|
||||
}
|
||||
const [troopName, cityName] = value;
|
||||
return `${troopName}<br>[${cityName}]`;
|
||||
},
|
||||
width: 90,
|
||||
sortable: true,
|
||||
comparator: (valX, valB, { data: lhs }: GenRowNode, { data: rhs }: GenRowNode) => {
|
||||
const troopInfoLhs = extractTroopInfo(lhs);
|
||||
const troopInfoRhs = extractTroopInfo(rhs);
|
||||
console.log(troopInfoLhs, troopInfoRhs);
|
||||
if (troopInfoLhs === troopInfoRhs) {
|
||||
return 0;
|
||||
}
|
||||
if (troopInfoLhs === undefined) {
|
||||
return 1;
|
||||
}
|
||||
if (troopInfoRhs === undefined) {
|
||||
return -1;
|
||||
}
|
||||
return troopInfoLhs[0].localeCompare(troopInfoRhs[0]);
|
||||
},
|
||||
filter: true,
|
||||
filterValueGetter: ({ data }) => {
|
||||
const troopInfo = extractTroopInfo(data);
|
||||
if (troopInfo === undefined) {
|
||||
return "-";
|
||||
}
|
||||
const [troopName, troopLeader] = troopInfo;
|
||||
const cityName = gameConstStore.value.cityConst[troopLeader.city].name;
|
||||
return convertSearch초성(`${troopName}$${cityName}`);
|
||||
},
|
||||
},
|
||||
crewtypeAndCrew: {
|
||||
groupId: "crewtypeAndCrew",
|
||||
headerName: "보유 병력",
|
||||
children: [
|
||||
{
|
||||
colId: "crewtypeAndCrew",
|
||||
headerName: "병종",
|
||||
cellRenderer: GridTooltipCell,
|
||||
cellRendererParams: {
|
||||
cells: ((): GridCellInfo[][] => {
|
||||
return [
|
||||
[{ target: "crewtype", iActionMap: gameConstStore.value.iActionInfo.crewtype }],
|
||||
[{ target: "crew", converter: (value) => [`${value.crew.toLocaleString()}명`, undefined] }],
|
||||
];
|
||||
})(),
|
||||
},
|
||||
columnGroupShow: "closed",
|
||||
},
|
||||
{
|
||||
colId: "crewtype",
|
||||
headerName: "병종",
|
||||
field: "crewtype",
|
||||
cellRenderer: SimpleTooltipCell,
|
||||
cellRendererParams: {
|
||||
iActionMap: gameConstStore.value.iActionInfo.crewtype,
|
||||
},
|
||||
sortable: true,
|
||||
columnGroupShow: "open",
|
||||
filter: true,
|
||||
filterValueGetter: ({ data }) => {
|
||||
if (!data.st1) {
|
||||
return "?";
|
||||
}
|
||||
const name = gameConstStore.value.iActionInfo.crewtype[data.crewtype].name;
|
||||
return convertSearch초성(name);
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: "crew",
|
||||
headerName: "병력",
|
||||
field: "crew",
|
||||
...sortableNumber,
|
||||
valueFormatter: numberFormatter("명"),
|
||||
width: 70,
|
||||
columnGroupShow: "open",
|
||||
},
|
||||
],
|
||||
},
|
||||
trainAtmos: {
|
||||
groupId: "trainAtmos",
|
||||
headerName: "훈/사",
|
||||
children: [
|
||||
{
|
||||
colId: "trainAtmos",
|
||||
headerName: "훈/사",
|
||||
width: 60,
|
||||
cellRenderer: ({ data }: GenValueParams) => {
|
||||
if (!data.st1) {
|
||||
return "?";
|
||||
}
|
||||
return `${data.train}<br>${data.atmos}`;
|
||||
},
|
||||
columnGroupShow: "closed",
|
||||
},
|
||||
{
|
||||
colId: "train",
|
||||
headerName: "훈련",
|
||||
field: "train",
|
||||
...sortableNumber,
|
||||
valueFormatter: numberFormatter(),
|
||||
width: 70,
|
||||
columnGroupShow: "open",
|
||||
},
|
||||
{
|
||||
colId: "atmos",
|
||||
headerName: "사기",
|
||||
field: "atmos",
|
||||
...sortableNumber,
|
||||
valueFormatter: numberFormatter(),
|
||||
width: 70,
|
||||
columnGroupShow: "open",
|
||||
},
|
||||
{
|
||||
colId: "defence_train",
|
||||
headerName: "수비",
|
||||
field: "defence_train",
|
||||
sortable: true,
|
||||
sortingOrder: ["desc", "asc", null],
|
||||
valueFormatter: ({ value }) => formatDefenceTrain(value as number),
|
||||
width: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
specials: {
|
||||
groupId: "specials",
|
||||
headerName: "특성",
|
||||
children: [
|
||||
{
|
||||
colId: "specials",
|
||||
headerName: "요약",
|
||||
cellRenderer: GridTooltipCell,
|
||||
cellRendererParams: {
|
||||
cells: ((): GridCellInfo[][] => {
|
||||
return [
|
||||
[{ target: "personal", iActionMap: gameConstStore.value.iActionInfo.personality }],
|
||||
[
|
||||
{ target: "specialDomestic", iActionMap: gameConstStore.value.iActionInfo.specialDomestic },
|
||||
{ target: "specialWar", iActionMap: gameConstStore.value.iActionInfo.specialWar },
|
||||
],
|
||||
];
|
||||
})(),
|
||||
},
|
||||
width: 80,
|
||||
columnGroupShow: "closed",
|
||||
},
|
||||
{
|
||||
colId: "personal",
|
||||
headerName: "성격",
|
||||
field: "personal",
|
||||
cellRenderer: SimpleTooltipCell,
|
||||
cellRendererParams: {
|
||||
iActionMap: gameConstStore.value.iActionInfo.personality,
|
||||
},
|
||||
width: 60,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
columnGroupShow: "open",
|
||||
filterValueGetter: ({ data }) => {
|
||||
const name = gameConstStore.value.iActionInfo.personality[data.personal].name;
|
||||
return convertSearch초성(name);
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: "specialDomestic",
|
||||
headerName: "내특",
|
||||
field: "specialDomestic",
|
||||
cellRenderer: SimpleTooltipCell,
|
||||
cellRendererParams: {
|
||||
iActionMap: gameConstStore.value.iActionInfo.specialDomestic,
|
||||
},
|
||||
width: 60,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
columnGroupShow: "open",
|
||||
filterValueGetter: ({ data }) => {
|
||||
const name = gameConstStore.value.iActionInfo.specialDomestic[data.specialDomestic].name;
|
||||
return convertSearch초성(name);
|
||||
},
|
||||
},
|
||||
{
|
||||
colId: "specialWar",
|
||||
headerName: "전특",
|
||||
field: "specialWar",
|
||||
cellRenderer: SimpleTooltipCell,
|
||||
cellRendererParams: {
|
||||
iActionMap: gameConstStore.value.iActionInfo.specialWar,
|
||||
},
|
||||
width: 60,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
columnGroupShow: "open",
|
||||
filterValueGetter: ({ data }) => {
|
||||
const name = gameConstStore.value.iActionInfo.specialWar[data.specialWar].name;
|
||||
return convertSearch초성(name);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
reservedCommandShort: {
|
||||
groupId: "reservedCommandShort",
|
||||
headerName: "명령",
|
||||
children: [
|
||||
{
|
||||
colId: "reservedCommandShort",
|
||||
headerName: "단축",
|
||||
width: 70,
|
||||
cellRenderer: ({ data }: GenValueParams) => {
|
||||
if (data.npc >= 2) {
|
||||
return "NPC 장수";
|
||||
}
|
||||
if (!data.reservedCommand) {
|
||||
return "-";
|
||||
}
|
||||
const commandList = data.reservedCommand;
|
||||
if (!commandList) {
|
||||
return "???";
|
||||
}
|
||||
return commandList
|
||||
.map(({ action }) => {
|
||||
if (action !== "휴식" || data.npc >= 2) {
|
||||
return naiveCheClassNameFilter(action);
|
||||
}
|
||||
const limitMinutes = props.env.autorun_user?.limit_minutes ?? 0;
|
||||
if (!limitMinutes) {
|
||||
return naiveCheClassNameFilter(action);
|
||||
}
|
||||
if (data.killturn + limitMinutes > props.env.killturn) {
|
||||
return "자율행동";
|
||||
}
|
||||
return naiveCheClassNameFilter(action);
|
||||
})
|
||||
.join("<br>");
|
||||
},
|
||||
cellStyle: {
|
||||
lineHeight: "1em",
|
||||
fontSize: "0.85em",
|
||||
},
|
||||
columnGroupShow: "closed",
|
||||
},
|
||||
{
|
||||
colId: "reservedCommand",
|
||||
headerName: "전체",
|
||||
width: 120,
|
||||
cellRenderer: ({ data }: GenValueParams) => {
|
||||
if (data.npc >= 2) {
|
||||
return "NPC 장수";
|
||||
}
|
||||
const commandList = data.reservedCommand;
|
||||
if (!commandList) {
|
||||
return "???";
|
||||
}
|
||||
return commandList
|
||||
.map(({ action, brief }) => {
|
||||
if (action !== "휴식" || data.npc >= 2) {
|
||||
return brief;
|
||||
}
|
||||
const limitMinutes = props.env.autorun_user?.limit_minutes ?? 0;
|
||||
if (!limitMinutes) {
|
||||
return brief;
|
||||
}
|
||||
if (data.killturn + limitMinutes > props.env.killturn) {
|
||||
return "자율 행동";
|
||||
}
|
||||
return brief;
|
||||
})
|
||||
.join("<br>");
|
||||
},
|
||||
cellStyle: {
|
||||
lineHeight: "1em",
|
||||
fontSize: "0.85em",
|
||||
},
|
||||
columnGroupShow: "open",
|
||||
},
|
||||
],
|
||||
},
|
||||
turntime: {
|
||||
colId: "turntime",
|
||||
headerName: "턴",
|
||||
field: "turntime",
|
||||
width: 60,
|
||||
valueFormatter: ({ value, data }) => {
|
||||
if (!data.st1) {
|
||||
return "?";
|
||||
}
|
||||
const turntime = value as string;
|
||||
return turntime.substring(14, 19);
|
||||
},
|
||||
sortable: true,
|
||||
cellClass: centerCellClass,
|
||||
},
|
||||
recent_war: {
|
||||
colId: "recent_war",
|
||||
headerName: "최근전투",
|
||||
field: "recent_war",
|
||||
width: 60,
|
||||
valueFormatter: ({ value, data }) => {
|
||||
if (!data.st1) {
|
||||
return "?";
|
||||
}
|
||||
const turntime = value as string;
|
||||
return turntime.substring(14, 19);
|
||||
},
|
||||
sortable: true,
|
||||
cellClass: centerCellClass,
|
||||
},
|
||||
years: {
|
||||
groupId: "years",
|
||||
headerName: "연도",
|
||||
children: [
|
||||
{
|
||||
colId: "years",
|
||||
headerName: "요약",
|
||||
width: 60,
|
||||
cellRenderer: ({ data }: GenValueParams) => {
|
||||
return `${data.age}세<br>${data.belong}년`;
|
||||
},
|
||||
cellClass: centerCellClass,
|
||||
columnGroupShow: "closed",
|
||||
},
|
||||
{
|
||||
colId: "age",
|
||||
headerName: "연령",
|
||||
field: "age",
|
||||
...sortableNumber,
|
||||
valueFormatter: (v: GenValueParams) => `${v.value}세`,
|
||||
width: 60,
|
||||
cellClass: centerCellClass,
|
||||
columnGroupShow: "open",
|
||||
},
|
||||
{
|
||||
colId: "belong",
|
||||
headerName: "사관",
|
||||
field: "belong",
|
||||
...sortableNumber,
|
||||
valueFormatter: (v: GenValueParams) => `${v.value}년`,
|
||||
width: 60,
|
||||
cellClass: centerCellClass,
|
||||
columnGroupShow: "open",
|
||||
},
|
||||
],
|
||||
},
|
||||
killturnAndConnect: {
|
||||
groupId: "killturnAndConnect",
|
||||
headerName: "기타",
|
||||
children: [
|
||||
{
|
||||
colId: "killturnAndConnect",
|
||||
headerName: "삭/벌",
|
||||
cellRenderer: ({ data }: GenValueParams) => {
|
||||
return `${data.killturn.toLocaleString()}턴<br>${data.connect.toLocaleString()}점`;
|
||||
},
|
||||
cellClass: rightAlignClass,
|
||||
columnGroupShow: "closed",
|
||||
width: 70,
|
||||
},
|
||||
{
|
||||
colId: "killturn",
|
||||
headerName: "삭턴",
|
||||
field: "killturn",
|
||||
cellRenderer: ({ data }: GenValueParams) => {
|
||||
return `${data.killturn.toLocaleString()}턴`;
|
||||
},
|
||||
...sortableNumber,
|
||||
width: 70,
|
||||
columnGroupShow: "open",
|
||||
},
|
||||
{
|
||||
colId: "connect",
|
||||
headerName: "벌점",
|
||||
field: "connect",
|
||||
cellRenderer: ({ data }: GenValueParams) => {
|
||||
return `${data.connect.toLocaleString()}점<br>(${formatConnectScore(data.connect)})`;
|
||||
},
|
||||
...sortableNumber,
|
||||
width: 70,
|
||||
columnGroupShow: "open",
|
||||
},
|
||||
],
|
||||
},
|
||||
warResults: {
|
||||
groupId: "warResults",
|
||||
headerName: "전과",
|
||||
children: [
|
||||
{
|
||||
colId: "warResults",
|
||||
headerName: "요약",
|
||||
cellRenderer: ({ data }: GenValueParams) => {
|
||||
if (!data.st1) {
|
||||
return "?";
|
||||
}
|
||||
const killRatePercent = Math.round((data.killcrew / Math.max(1, data.deathcrew)) * 100);
|
||||
return `${data.warnum.toLocaleString()}전 ${data.killnum.toLocaleString()}승<br>살상: ${killRatePercent}%`;
|
||||
},
|
||||
cellClass: centerCellClass,
|
||||
columnGroupShow: "closed",
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
colId: "warnum",
|
||||
headerName: "전투",
|
||||
field: "warnum",
|
||||
...sortableNumber,
|
||||
valueFormatter: numberFormatter("전"),
|
||||
columnGroupShow: "open",
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
colId: "killnum",
|
||||
headerName: "승리",
|
||||
field: "killnum",
|
||||
...sortableNumber,
|
||||
valueFormatter: numberFormatter("승"),
|
||||
columnGroupShow: "open",
|
||||
width: 60,
|
||||
},
|
||||
{
|
||||
colId: "killcrew",
|
||||
headerName: "살상률",
|
||||
field: "killcrew",
|
||||
...sortableNumber,
|
||||
valueGetter: ({ data }) => {
|
||||
if (!data.st1) {
|
||||
return "?";
|
||||
}
|
||||
const killRatePercent = Math.round((data.killcrew / Math.max(1, data.deathcrew)) * 100);
|
||||
return killRatePercent;
|
||||
},
|
||||
valueFormatter: numberFormatter("%"),
|
||||
columnGroupShow: "open",
|
||||
width: 60,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const columnDefs = ref([...Object.values(columnRawDefs.value)]);
|
||||
watch(columnRawDefs, (val) => {
|
||||
columnDefs.value = [...Object.values(val)];
|
||||
gridApi.value?.refreshCells();
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.g-tr {
|
||||
border-bottom: solid gray 1px;
|
||||
}
|
||||
.g-thead-tr {
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
:deep(.view-mode-list) {
|
||||
width: 180px;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.component-general-list {
|
||||
.clickable-cell:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ag-root-wrapper .cell-middle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.ag-root-wrapper {
|
||||
font-family: "Pretendard", "Apple SD Gothic Neo", "Noto Sans KR", "Malgun Gothic";
|
||||
font-size: 14px;
|
||||
overflow: auto;
|
||||
}
|
||||
.ag-root {
|
||||
overflow: auto;
|
||||
}
|
||||
.ag-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.cell-center {
|
||||
justify-content: space-around;
|
||||
text-align: center;
|
||||
}
|
||||
.cell-right {
|
||||
justify-content: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
.cell-sp .col {
|
||||
min-width: 30px;
|
||||
}
|
||||
.ag-header-cell,
|
||||
.ag-header-group-cell,
|
||||
.ag-cell {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
.ag-header-cell-label,
|
||||
.ag-header-group-cell-label {
|
||||
justify-content: center;
|
||||
}
|
||||
.ag-ltr .ag-floating-filter-button {
|
||||
margin-left: 2px;
|
||||
}
|
||||
.ag-rtl .ag-floating-filter-button {
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
.general-list-toolbar {
|
||||
.column-menu {
|
||||
column-count: 3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="general-card-supplement row gx-0">
|
||||
<div class="col-12 general-card-info">
|
||||
<div class="part-title">추가 정보</div>
|
||||
<div>명성</div>
|
||||
<div>{{ formatHonor(general.experience) }} ({{ general.experience.toLocaleString() }})</div>
|
||||
<div>계급</div>
|
||||
<div>{{ general.dedLevelText }} ({{ general.dedication.toLocaleString() }}</div>
|
||||
<div>봉급</div>
|
||||
<div>{{ general.bill.toLocaleString() }}</div>
|
||||
|
||||
<div>전투</div>
|
||||
<div>{{ general.warnum.toLocaleString() }}</div>
|
||||
<div>계략</div>
|
||||
<div>{{ general.firenum.toLocaleString() }}</div>
|
||||
<div>사관</div>
|
||||
<div>{{ general.belong }}년차</div>
|
||||
|
||||
<div>승률</div>
|
||||
<div>{{ ((general.killnum / Math.max(general.warnum, 1)) * 100).toFixed(2) }} %</div>
|
||||
<div>승리</div>
|
||||
<div>{{ general.killnum.toLocaleString() }}</div>
|
||||
<div>패배</div>
|
||||
<div>{{ general.deathnum.toPrecision() }}</div>
|
||||
|
||||
<div>살상률</div>
|
||||
<div>
|
||||
{{
|
||||
((general.killcrew / Math.max(general.deathcrew, 1)) * 100).toLocaleString(undefined, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
}}
|
||||
%
|
||||
</div>
|
||||
<div>사살</div>
|
||||
<div>{{ general.killcrew.toLocaleString() }}</div>
|
||||
<div>피살</div>
|
||||
<div>{{ general.deathcrew.toLocaleString() }}</div>
|
||||
</div>
|
||||
<div class="col-7 general-card-dex">
|
||||
<div class="part-title">숙련도</div>
|
||||
<template v-for="[dexType, dex, dexInfo] of dexList" :key="dexType">
|
||||
<div>{{ dexType }}</div>
|
||||
<div :style="{ color: dexInfo.color }">{{ dexInfo.name }}</div>
|
||||
<div>{{ (dex / 1000).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 }) }}K</div>
|
||||
<div>
|
||||
<SammoBar :height="10" :percent="dex / 1_000_000" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="col-5 general-card-turn">
|
||||
<div class="part-title">예약턴</div>
|
||||
<template v-if="general.reservedCommand">
|
||||
<div v-for="(turn, idx) in general.reservedCommand.slice(0, 5)" :key="idx">
|
||||
{{ turn.brief }}
|
||||
</div>
|
||||
</template>
|
||||
<div v-else style="grid-row: 2 / 7">NPC</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { GeneralListItemP1 } from "@/defs/API/Nation";
|
||||
import { computed } from "vue";
|
||||
import SammoBar from "@/components/SammoBar.vue";
|
||||
import { formatDexLevel, type DexInfo } from "@/utilGame/formatDexLevel";
|
||||
import { formatHonor } from "@/utilGame/formatHonor";
|
||||
const props = defineProps<{
|
||||
general: GeneralListItemP1;
|
||||
}>();
|
||||
|
||||
const dexList = computed((): [string, number, DexInfo][] => {
|
||||
return [
|
||||
["보병", props.general.dex1, formatDexLevel(props.general.dex1)],
|
||||
["궁병", props.general.dex2, formatDexLevel(props.general.dex2)],
|
||||
["기병", props.general.dex3, formatDexLevel(props.general.dex3)],
|
||||
["귀병", props.general.dex4, formatDexLevel(props.general.dex4)],
|
||||
["차병", props.general.dex5, formatDexLevel(props.general.dex5)],
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.general-card-basic {
|
||||
display: grid;
|
||||
grid-template-columns: 64px repeat(3, 2fr 5fr);
|
||||
grid-template-rows: repeat(9, calc(64px / 3));
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
.general-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
grid-row: 1 / 4;
|
||||
}
|
||||
|
||||
.general-name {
|
||||
grid-row: 1 / 2;
|
||||
grid-column: 2 / 8;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.general-crew-type-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
grid-row: 4 / 7;
|
||||
}
|
||||
|
||||
.general-exp-level-bar {
|
||||
grid-column: 3 / 6;
|
||||
}
|
||||
|
||||
.general-defence-train {
|
||||
grid-column: 2 / 4;
|
||||
}
|
||||
|
||||
.general-card-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 64px 1fr);
|
||||
grid-template-rows: repeat(5, calc(64px / 3));
|
||||
|
||||
.part-title {
|
||||
grid-column: 1 / 4;
|
||||
}
|
||||
}
|
||||
|
||||
.general-card-dex {
|
||||
display: grid;
|
||||
grid-template-columns: 64px 40px 60px 1fr;
|
||||
grid-template-rows: repeat(6, calc(64px / 3));
|
||||
|
||||
.part-title {
|
||||
grid-column: 1 / 5;
|
||||
}
|
||||
}
|
||||
|
||||
.general-card-turn {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: repeat(6, calc(64px / 3));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div
|
||||
:class="['city_base', `city_base_${city.id}`, `city_level_${city.level}`]"
|
||||
:style="cityPos"
|
||||
@mouseenter="silent"
|
||||
@mouseleave="silent"
|
||||
>
|
||||
<a
|
||||
class="city_link"
|
||||
:data-text="city.text"
|
||||
:data-nation="city.nation"
|
||||
:data-id="city.id"
|
||||
:href="props.href"
|
||||
:style="{
|
||||
cursor: city.clickable ? 'pointer' : 'default',
|
||||
}"
|
||||
@click="clicked"
|
||||
@touchend="touchend"
|
||||
@mouseenter="mouseenter"
|
||||
@mouseleave="mouseleave"
|
||||
>
|
||||
<div
|
||||
class="city_img"
|
||||
:style="{
|
||||
backgroundColor: city.color,
|
||||
}"
|
||||
>
|
||||
<div :class="['city_filler', props.isMyCity ? 'my_city' : '']"></div>
|
||||
<div v-if="city.state > 0" :class="['city_state', `city_state_${getCityState()}`]"></div>
|
||||
<div v-if="city.nationID && city.nationID > 0" class="city_flag">
|
||||
<div v-if="city.isCapital" class="city_capital"></div>
|
||||
</div>
|
||||
<span class="city_detail_name">{{ city.name }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MapCityParsed } from "@/map";
|
||||
import { ref, toRef, watch, type PropType } from "vue";
|
||||
const emit = defineEmits<{
|
||||
(event: "click", evnet: MouseEvent | TouchEvent): void;
|
||||
(event: "mouseenter", e: MouseEvent): void;
|
||||
(event: "mouseleave", e: MouseEvent): void;
|
||||
}>();
|
||||
const props = defineProps({
|
||||
city: {
|
||||
type: Object as PropType<MapCityParsed>,
|
||||
required: true,
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
isMyCity: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
defeault: false,
|
||||
},
|
||||
isFullWidth: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const city = toRef(props, "city");
|
||||
const cityPos = ref({
|
||||
left: "0px",
|
||||
top: "0px",
|
||||
});
|
||||
watch(
|
||||
() => props.isFullWidth,
|
||||
(isFullWidth) => {
|
||||
const { x, y } = city.value;
|
||||
if (isFullWidth) {
|
||||
cityPos.value = {
|
||||
left: `${x - 20}px`,
|
||||
top: `${y - 15}px`,
|
||||
};
|
||||
} else {
|
||||
cityPos.value = {
|
||||
left: `${(x * 5) / 7 - 20}px`,
|
||||
top: `${(y * 5) / 7 - 18}px`,
|
||||
};
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
function getCityState(): string {
|
||||
const state = city.value.state;
|
||||
if (state < 10) {
|
||||
return "good";
|
||||
}
|
||||
if (state < 40) {
|
||||
return "bad";
|
||||
}
|
||||
if (state < 50) {
|
||||
return "war";
|
||||
}
|
||||
return "wrong";
|
||||
}
|
||||
|
||||
function clicked(event: MouseEvent) {
|
||||
emit("click", event);
|
||||
}
|
||||
|
||||
function mouseenter(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
emit("mouseenter", event);
|
||||
}
|
||||
|
||||
function mouseleave(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
emit("mouseleave", event);
|
||||
}
|
||||
|
||||
function touchend(event: TouchEvent) {
|
||||
event.stopPropagation();
|
||||
emit("click", event);
|
||||
}
|
||||
|
||||
function silent(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
a,
|
||||
div,
|
||||
span {
|
||||
line-height: 1.3;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div
|
||||
:class="['city_base', `city_base_${city.id}`, `city_level_${city.level}`]"
|
||||
:style="cityPos"
|
||||
@mouseenter="silent"
|
||||
@mouseleave="silent"
|
||||
>
|
||||
<div
|
||||
v-if="city.color"
|
||||
class="city_bg"
|
||||
:style="{
|
||||
backgroundImage: `url(${imagePath}/b${city.color.substring(1).toUpperCase()}.png)`,
|
||||
}"
|
||||
></div>
|
||||
|
||||
<a
|
||||
class="city_link"
|
||||
:data-text="city.text"
|
||||
:data-nation="city.nation"
|
||||
:data-id="city.id"
|
||||
:href="props.href"
|
||||
:style="{
|
||||
cursor: city.clickable ? 'pointer' : 'default',
|
||||
}"
|
||||
@click="clicked"
|
||||
@touchend="touchend"
|
||||
@mouseenter="mouseenter"
|
||||
@mouseleave="mouseleave"
|
||||
>
|
||||
<div class="city_img">
|
||||
<img :src="`${imagePath}/cast_${city.level}.gif`" />
|
||||
<div :class="['city_filler', props.isMyCity ? 'my_city' : '']"></div>
|
||||
|
||||
<div v-if="city.nationID && city.nationID > 0" class="city_flag">
|
||||
<img :src="`${imagePath}/${city.supply ? 'f' : 'd'}${city.color.substring(1).toUpperCase()}.gif`" />
|
||||
<div v-if="city.isCapital" class="city_capital">
|
||||
<img :src="`${imagePath}/event51.gif`" />
|
||||
</div>
|
||||
</div>
|
||||
<span class="city_detail_name">{{ city.name }}</span>
|
||||
</div>
|
||||
<div v-if="city.state > 0" class="city_state">
|
||||
<img :src="`${imagePath}/event${city.state}.gif`" />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MapCityParsed } from "@/map";
|
||||
import { ref, toRef, watch, type PropType } from "vue";
|
||||
const emit = defineEmits<{
|
||||
(event: "click", e: MouseEvent | TouchEvent): void;
|
||||
(event: "mouseenter", e: MouseEvent): void;
|
||||
(event: "mouseleave", e: MouseEvent): void;
|
||||
}>();
|
||||
const props = defineProps({
|
||||
city: {
|
||||
type: Object as PropType<MapCityParsed>,
|
||||
required: true,
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
isMyCity: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
defeault: false,
|
||||
},
|
||||
imagePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isFullWidth: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
const city = toRef(props, "city");
|
||||
const cityPos = ref({
|
||||
left: "0px",
|
||||
top: "0px",
|
||||
});
|
||||
watch(
|
||||
() => props.isFullWidth,
|
||||
(isFullWidth) => {
|
||||
const { x, y } = city.value;
|
||||
if (isFullWidth) {
|
||||
cityPos.value = {
|
||||
left: `${x - 20}px`,
|
||||
top: `${y - 15}px`,
|
||||
};
|
||||
} else {
|
||||
cityPos.value = {
|
||||
left: `${(x * 5) / 7 - 20}px`,
|
||||
top: `${(y * 5) / 7 - 18}px`,
|
||||
};
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const imagePath = toRef(props, "imagePath");
|
||||
|
||||
function clicked(event: MouseEvent) {
|
||||
emit("click", event);
|
||||
}
|
||||
|
||||
function mouseenter(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
emit("mouseenter", event);
|
||||
}
|
||||
|
||||
function mouseleave(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
emit("mouseleave", event);
|
||||
}
|
||||
|
||||
function touchend(event: TouchEvent) {
|
||||
event.stopPropagation();
|
||||
emit("click", event);
|
||||
}
|
||||
|
||||
function silent(event: MouseEvent) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.full_width_map{
|
||||
a,
|
||||
div,
|
||||
img,
|
||||
span {
|
||||
line-height: 1.3;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
.small_width_map {
|
||||
a,
|
||||
div,
|
||||
img {
|
||||
line-height: 1.3;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
span {
|
||||
line-height: 1.0;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,493 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="(mapData.version ?? 0) == CURRENT_MAP_VERSION"
|
||||
:id="uuid"
|
||||
:class="[
|
||||
'world_map',
|
||||
`map_theme_${mapTheme}`,
|
||||
drawableMap ? '' : 'draw_required',
|
||||
props.isDetailMap ? 'map_detail' : 'map_basic',
|
||||
hideMapCityName ? 'hide_cityname' : '',
|
||||
isFullWidth ? 'full_width_map' : 'small_width_map',
|
||||
getMapSeasonClassName(),
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-my-tooltip.hover.top="{
|
||||
class: 'map_title_tooltiptext',
|
||||
}"
|
||||
class="map_title"
|
||||
:title="getTitleTooltip()"
|
||||
>
|
||||
<!-- eslint-disable-next-line vue/max-attributes-per-line -->
|
||||
<span class="map_title_text" :style="{ color: getTitleColor() }"
|
||||
>{{ mapData?.year }}年 {{ mapData?.month }}月</span
|
||||
>
|
||||
<span class="tooltiptext" />
|
||||
</div>
|
||||
<div ref="map_area" class="map_body" @click="clickOutside">
|
||||
<div class="map_bglayer1" />
|
||||
<div class="map_bglayer2" />
|
||||
<div class="map_bgroad" />
|
||||
<div class="map_button_stack">
|
||||
<button
|
||||
type="button"
|
||||
:class="['btn btn-primary map_toggle_cityname btn-sm btn-minimum', hideMapCityName ? 'active' : '']"
|
||||
data-bs-toggle="button"
|
||||
:aria-pressed="hideMapCityName"
|
||||
autocomplete="off"
|
||||
@click="hideMapCityName = !hideMapCityName"
|
||||
>
|
||||
도시명 표기</button
|
||||
><br />
|
||||
<button
|
||||
:style="{
|
||||
display: deviceType != 'mouseOnly' ? 'block' : 'none',
|
||||
}"
|
||||
type="button"
|
||||
:class="['btn btn-secondary map_toggle_single_tap btn-sm btn-minimum', toggleSingleTap ? 'active' : '']"
|
||||
data-bs-toggle="button"
|
||||
:aria-pressed="toggleSingleTap"
|
||||
autocomplete="off"
|
||||
@click="toggleSingleTap = !toggleSingleTap"
|
||||
>
|
||||
두번 탭 해 도시 이동
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="drawableMap === undefined"><!--로딩중?--></template>
|
||||
<template v-else-if="props.isDetailMap">
|
||||
<MapCityDetail
|
||||
v-for="city of drawableMap.cityList"
|
||||
:key="city.id"
|
||||
:city="city"
|
||||
:image-path="imagePath"
|
||||
:is-my-city="city.id === drawableMap.myCity"
|
||||
:isFullWidth="isFullWidth"
|
||||
:href="props.genHref?.call(city, city.id)"
|
||||
@click="cityClick(city, $event)"
|
||||
@mouseenter="mouseenter(city, $event)"
|
||||
@mouseleave="mouseleave(city, $event)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else
|
||||
><MapCityBasic
|
||||
v-for="city of drawableMap.cityList"
|
||||
:key="city.id"
|
||||
:city="city"
|
||||
:is-my-city="city.id === drawableMap.myCity"
|
||||
:isFullWidth="isFullWidth"
|
||||
:href="props.genHref?.call(city, city.id)"
|
||||
@click="cityClick(city, $event)"
|
||||
@mouseenter="mouseenter(city, $event)"
|
||||
@mouseleave="mouseleave(city, $event)"
|
||||
/></template>
|
||||
</div>
|
||||
<div
|
||||
ref="tooltipDom"
|
||||
class="city_tooltip"
|
||||
:style="{
|
||||
display: isOutside || !activatedCity ? 'none' : 'block',
|
||||
position: 'absolute',
|
||||
left: `${(() => {
|
||||
if (cursorX + tooltipWidth + 10 > (isFullWidth ? 700 : 500)) {
|
||||
return cursorX - tooltipWidth - 5;
|
||||
}
|
||||
return cursorX + 10;
|
||||
})()}px`,
|
||||
top: `${cursorY + 30}px`,
|
||||
}"
|
||||
>
|
||||
<div class="city_name">{{ activatedCity?.text }}</div>
|
||||
<div class="nation_name">{{ activatedCity?.nation }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="world_map">
|
||||
<span class="map_title_text">
|
||||
맵 버전이 맞지 않습니다.<br />
|
||||
렌더러 버전: {{ CURRENT_MAP_VERSION }}<br />
|
||||
API 버전: {{ mapData.version ?? 0 }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export type MapCityParsedRaw = {
|
||||
id: number;
|
||||
level: number;
|
||||
state: number;
|
||||
nationID?: number;
|
||||
region: number;
|
||||
supply: boolean;
|
||||
};
|
||||
|
||||
type MapCityParsedName = MapCityParsedRaw & {
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
type MapCityParsedNation = MapCityParsedName & {
|
||||
nationID?: number;
|
||||
nation?: string;
|
||||
color?: string;
|
||||
isCapital: boolean;
|
||||
};
|
||||
|
||||
type MapCityParsedClickable = MapCityParsedNation & {
|
||||
clickable: number;
|
||||
};
|
||||
|
||||
type MapCityParsedRegionLevelText = MapCityParsedClickable & {
|
||||
region_str: string;
|
||||
level_str: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type MapCityParsed = MapCityParsedRegionLevelText;
|
||||
|
||||
type MapCityDrawable = {
|
||||
cityList: MapCityParsed[];
|
||||
myCity?: number;
|
||||
};
|
||||
|
||||
type MapNationParsed = {
|
||||
id: number;
|
||||
name: string;
|
||||
color: string;
|
||||
capital: number;
|
||||
};
|
||||
|
||||
export type CityPositionMap = {
|
||||
[cityID: number]: [string, number, number];
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import "@/../scss/map.scss";
|
||||
import { type PropType, toRef, inject, type Ref, ref, watch, type ComponentPublicInstance } from "vue";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { CURRENT_MAP_VERSION, type MapResult } from "@/defs";
|
||||
import { joinYearMonth } from "@/util/joinYearMonth";
|
||||
import { parseYearMonth } from "@/util/parseYearMonth";
|
||||
import vMyTooltip from "@/directives/vMyTooltip";
|
||||
import type { GameConstStore } from "@/GameConstStore";
|
||||
import { unwrap_err } from "@/util/unwrap_err";
|
||||
import { getMaxRelativeTechLevel, TECH_LEVEL_YEAR_GAP } from "@/utilGame/techLevel";
|
||||
import { deviceType } from "detect-it";
|
||||
import MapCityBasic from "./MapCityBasic.vue";
|
||||
import MapCityDetail from "./MapCityDetail.vue";
|
||||
import { convertDictById } from "@/common_legacy";
|
||||
import { useElementSize, useMouse, useMouseInElement } from "@vueuse/core";
|
||||
import { hideMapCityName, toggleSingleTap } from "@/state/mapViewer";
|
||||
import { is1000pxMode } from "@/state/is1000pxMode";
|
||||
const uuid = uuidv4();
|
||||
const gameConstStore = unwrap_err(
|
||||
inject<Ref<GameConstStore>>("gameConstStore"),
|
||||
Error,
|
||||
"gameConstStore가 주입되지 않았습니다."
|
||||
);
|
||||
|
||||
const tooltipDom = ref<ComponentPublicInstance<HTMLDivElement>>();
|
||||
const map_area = ref<ComponentPublicInstance<HTMLDivElement>>();
|
||||
const { elementX: cursorX, elementY: cursorY, isOutside } = useMouseInElement(map_area);
|
||||
|
||||
const tooltipWidth = ref(0);
|
||||
const { width: tooltipCurrWidth } = useElementSize(tooltipDom);
|
||||
watch(
|
||||
tooltipCurrWidth,
|
||||
(newWidth) => {
|
||||
if (newWidth == 0) return;
|
||||
tooltipWidth.value = newWidth;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const { sourceType: cursorType } = useMouse();
|
||||
const emit = defineEmits<{
|
||||
(event: "city-click", city: MapCityParsed, e: MouseEvent | TouchEvent): void;
|
||||
(event: "parsed", drawable: MapCityDrawable): void;
|
||||
(event: "update:modelValue", value: MapCityParsed): void;
|
||||
}>();
|
||||
|
||||
const isFullWidth = ref(true);
|
||||
|
||||
function setWidthMode([widthMode, is1000pxMode]: ["auto" | "full" | "small" | undefined, boolean]): void {
|
||||
if (widthMode == "full") {
|
||||
isFullWidth.value = true;
|
||||
}
|
||||
if (widthMode == "small") {
|
||||
isFullWidth.value = false;
|
||||
}
|
||||
isFullWidth.value = is1000pxMode;
|
||||
}
|
||||
watch([() => props.width, is1000pxMode], setWidthMode, { immediate: true });
|
||||
|
||||
const props = defineProps({
|
||||
width: {
|
||||
type: String as PropType<"full" | "small" | "auto" | undefined>,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
imagePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
mapName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isDetailMap: { type: Boolean, default: undefined, required: false },
|
||||
disallowClick: { type: Boolean, default: undefined, required: false },
|
||||
genHref: {
|
||||
type: Function as PropType<(cityID: number) => string>,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
|
||||
cityPosition: {
|
||||
type: Object as PropType<CityPositionMap>,
|
||||
required: true,
|
||||
},
|
||||
formatCityInfo: {
|
||||
type: Function as PropType<(city: MapCityParsed) => MapCityParsed>,
|
||||
required: true,
|
||||
},
|
||||
|
||||
mapData: {
|
||||
type: Object as PropType<MapResult>,
|
||||
required: true,
|
||||
},
|
||||
|
||||
modelValue: {
|
||||
type: Object as PropType<MapCityParsed>,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const mapData = toRef(props, "mapData");
|
||||
|
||||
const mapTheme = toRef(props, "mapName");
|
||||
function getTitleColor(): string | undefined {
|
||||
const { startYear, year } = mapData.value;
|
||||
|
||||
if (year < startYear + 1) {
|
||||
return "magenta";
|
||||
}
|
||||
if (year < startYear + 2) {
|
||||
return "orange";
|
||||
}
|
||||
if (year < startYear + 3) {
|
||||
return "yellow";
|
||||
}
|
||||
}
|
||||
|
||||
const drawableMap = ref<MapCityDrawable>(convertCityObjs(props.mapData));
|
||||
const activatedCity = ref<MapCityParsed>();
|
||||
|
||||
function getBeginGameLimitTooltip(): string | undefined {
|
||||
const { startYear, year, month } = mapData.value;
|
||||
if (year > startYear + 3) return undefined;
|
||||
|
||||
const [remainYear, remainMonth] = parseYearMonth(joinYearMonth(startYear + 3, 0) - joinYearMonth(year, month));
|
||||
|
||||
return `초반제한 기간 : ${remainYear}년${remainMonth > 0 ? ` ${remainMonth}개월` : ""} (${startYear + 3}년)`;
|
||||
}
|
||||
|
||||
function getTitleTooltip(): string {
|
||||
const result: string[] = [];
|
||||
const beginLimit = getBeginGameLimitTooltip();
|
||||
if (beginLimit) {
|
||||
result.push(beginLimit);
|
||||
}
|
||||
|
||||
const { startYear, year } = mapData.value;
|
||||
|
||||
const maxTechLevel = gameConstStore.value.gameConst.maxTechLevel;
|
||||
const currentTechLimit = getMaxRelativeTechLevel(startYear, year, maxTechLevel);
|
||||
|
||||
if (currentTechLimit == maxTechLevel) {
|
||||
result.push(`기술등급 제한 : ${currentTechLimit}등급 (최종)`);
|
||||
} else {
|
||||
const nextTechLimitYear = currentTechLimit * TECH_LEVEL_YEAR_GAP + startYear;
|
||||
result.push(`기술등급 제한 : ${currentTechLimit}등급 (${nextTechLimitYear}년 해제)`);
|
||||
}
|
||||
|
||||
return result.join("<br>");
|
||||
}
|
||||
|
||||
function getMapSeasonClassName(): string {
|
||||
const { month } = mapData.value;
|
||||
|
||||
if (month <= 3) {
|
||||
return "map_spring";
|
||||
}
|
||||
if (month <= 6) {
|
||||
return "map_summer";
|
||||
}
|
||||
if (month <= 9) {
|
||||
return "map_fall";
|
||||
}
|
||||
return "map_winter";
|
||||
}
|
||||
|
||||
function convertCityObjs(obj: MapResult): MapCityDrawable {
|
||||
//원본 Obj는 굉장히 간소하게 온다, Object 형태로 변환해서 사용한다.
|
||||
|
||||
function toCityObj([id, level, state, nationID, region, supply]: MapResult["cityList"][0]): MapCityParsedRaw {
|
||||
return {
|
||||
id: id,
|
||||
level: level,
|
||||
state: state,
|
||||
nationID: nationID > 0 ? nationID : undefined,
|
||||
region: region,
|
||||
supply: supply != 0,
|
||||
};
|
||||
}
|
||||
|
||||
function toNationObj([id, name, color, capital]: MapResult["nationList"][0]): MapNationParsed {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
color,
|
||||
capital,
|
||||
};
|
||||
}
|
||||
|
||||
const nationList = convertDictById(obj.nationList.map(toNationObj)); //array of object -> dict
|
||||
|
||||
const spyList = obj.spyList;
|
||||
const shownByGeneralList = new Set(obj.shownByGeneralList);
|
||||
|
||||
const myCity = obj.myCity;
|
||||
const myNation = obj.myNation;
|
||||
|
||||
function mergePositionInfo(city: MapCityParsedRaw): MapCityParsedName {
|
||||
const id = city.id;
|
||||
if (!(id in props.cityPosition)) {
|
||||
throw TypeError(`알수 없는 cityID: ${id}`);
|
||||
}
|
||||
const [name, x, y] = props.cityPosition[id];
|
||||
|
||||
return {
|
||||
...city,
|
||||
name,
|
||||
x,
|
||||
y,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeNationInfo(city: MapCityParsedName): MapCityParsedNation {
|
||||
//nationID 값으로 isCapital, color, nation을 통합
|
||||
|
||||
const nationID = city.nationID;
|
||||
if (nationID === undefined || !(nationID in nationList)) {
|
||||
return {
|
||||
...city,
|
||||
isCapital: false,
|
||||
};
|
||||
}
|
||||
|
||||
const nationObj = nationList[nationID];
|
||||
return {
|
||||
...city,
|
||||
nation: nationObj.name,
|
||||
color: nationObj.color,
|
||||
isCapital: nationObj.capital == city.id,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeClickable(city: MapCityParsedNation): MapCityParsedClickable {
|
||||
//clickable = (defaultCity << 4 ) | (remainSpy << 3) | (ourCity << 2) | (shownByGeneral << 1)
|
||||
const id = city.id;
|
||||
const nationID = city.nationID;
|
||||
|
||||
if (props.disallowClick) {
|
||||
return { ...city, clickable: 0 };
|
||||
}
|
||||
|
||||
let clickable = 16;
|
||||
if (id in spyList) {
|
||||
clickable |= spyList[id] << 3;
|
||||
}
|
||||
if (myNation !== null && nationID == myNation) {
|
||||
clickable |= 4;
|
||||
}
|
||||
if (shownByGeneralList.has(id)) {
|
||||
clickable |= 2;
|
||||
}
|
||||
if (myCity !== null && id == myCity) {
|
||||
clickable |= 2;
|
||||
}
|
||||
|
||||
return {
|
||||
...city,
|
||||
clickable,
|
||||
};
|
||||
}
|
||||
|
||||
const cityList = obj.cityList
|
||||
.map(toCityObj)
|
||||
.map(mergePositionInfo)
|
||||
.map(mergeNationInfo)
|
||||
.map(mergeClickable)
|
||||
.map(window.formatCityInfo);
|
||||
|
||||
const result = {
|
||||
cityList: cityList,
|
||||
myCity: myCity,
|
||||
};
|
||||
emit("parsed", result);
|
||||
|
||||
return result;
|
||||
}
|
||||
watch(
|
||||
() => props.mapData,
|
||||
(mapInfo) => {
|
||||
activatedCity.value = undefined;
|
||||
drawableMap.value = convertCityObjs(mapInfo);
|
||||
}
|
||||
);
|
||||
|
||||
const touchState = ref(0);
|
||||
function cityClick(city: MapCityParsed, $event: MouseEvent | TouchEvent): void {
|
||||
if (cursorType.value == "touch") {
|
||||
if (touchState.value == 1 && activatedCity.value?.id !== city.id) {
|
||||
touchState.value = 0;
|
||||
activatedCity.value = undefined;
|
||||
}
|
||||
if (touchState.value == 0) {
|
||||
touchState.value = 1;
|
||||
activatedCity.value = city;
|
||||
$event.preventDefault();
|
||||
if (!toggleSingleTap.value) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
emit("city-click", city, $event);
|
||||
emit("update:modelValue", city);
|
||||
}
|
||||
|
||||
function clickOutside($event: MouseEvent): void {
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
if (touchState.value == 1) {
|
||||
touchState.value = 0;
|
||||
activatedCity.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function mouseenter(city: MapCityParsed, $event: MouseEvent): void {
|
||||
if (cursorType.value == "mouse") {
|
||||
activatedCity.value = city;
|
||||
touchState.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function mouseleave(city: MapCityParsed, $event: MouseEvent): void {
|
||||
if (cursorType.value == "mouse") {
|
||||
activatedCity.value = undefined;
|
||||
touchState.value = 0;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="row form-group number-input-with-info">
|
||||
<label v-if="!right && title" class="col col-form-label">{{ title }}</label>
|
||||
<div class="col">
|
||||
<input
|
||||
ref="input"
|
||||
v-model="rawValue"
|
||||
type="number"
|
||||
:step="step ?? undefined"
|
||||
class="form-control f_tnum"
|
||||
:min="min ?? undefined"
|
||||
:max="max ?? undefined"
|
||||
:style="{ display: editmode ? undefined : 'none' }"
|
||||
@blur="onBlurNumber"
|
||||
@input="updateValue"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control f_tnum"
|
||||
:readonly="readonly"
|
||||
:value="printValue"
|
||||
:style="{ display: !editmode ? undefined : 'none' }"
|
||||
@focus="onFocusText"
|
||||
/>
|
||||
</div>
|
||||
<label v-if="right && title" class="col col-form-label">{{ title }}</label>
|
||||
</div>
|
||||
<div style="text-align: right">
|
||||
<small class="form-text text-muted"><slot /></small>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { clamp } from "lodash";
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "NumberInputWithInfo",
|
||||
props: {
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
int: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
right: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
return {
|
||||
editmode: false,
|
||||
rawValue: this.modelValue,
|
||||
printValue: this.modelValue.toLocaleString(),
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
modelValue: function (newVal: number) {
|
||||
this.rawValue = newVal;
|
||||
this.printValue = newVal.toLocaleString();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateValue() {
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
if (this.int) {
|
||||
this.rawValue = Math.floor(this.rawValue);
|
||||
}
|
||||
this.printValue = this.rawValue.toLocaleString();
|
||||
if (this.min !== undefined || this.max !== undefined) {
|
||||
const clampedValue = clamp(this.rawValue, this.min ?? this.rawValue, this.max ?? this.rawValue);
|
||||
this.$emit("update:modelValue", clampedValue);
|
||||
} else {
|
||||
this.$emit("update:modelValue", this.rawValue);
|
||||
}
|
||||
},
|
||||
onBlurNumber() {
|
||||
this.editmode = false;
|
||||
this.printValue = this.rawValue.toLocaleString();
|
||||
if (this.min !== undefined || this.max !== undefined) {
|
||||
const clampedValue = clamp(this.rawValue, this.min ?? this.rawValue, this.max ?? this.rawValue);
|
||||
if (clampedValue !== this.rawValue) {
|
||||
this.rawValue = clampedValue;
|
||||
this.updateValue();
|
||||
}
|
||||
}
|
||||
},
|
||||
onFocusText() {
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
this.editmode = true;
|
||||
setTimeout(() => {
|
||||
(this.$refs.input as HTMLInputElement).focus();
|
||||
}, 0);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div
|
||||
v-b-tooltip.hover.top
|
||||
:title="`${props.percent}$`"
|
||||
class="sammo-bar"
|
||||
:style="{
|
||||
height: `${props.height}px`,
|
||||
backgroundImage: `url(${imagePath}/pr${props.height - 2}.png)`,
|
||||
backgroundRepeat: 'repeat-x',
|
||||
backgroundPosition: 'center',
|
||||
borderTop: 'solid 1px #888',
|
||||
borderBottom: 'solid 1px #333',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="sammo-bar-in"
|
||||
:style="{
|
||||
height: `${props.height}px`,
|
||||
backgroundImage: `url(${imagePath}/pb${props.height - 2}.png)`,
|
||||
backgroundRepeat: 'repeat-x',
|
||||
backgroundPosition: 'left center',
|
||||
width: `${clamp(props.percent, 0, 100)}%`,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { clamp } from "lodash";
|
||||
const imagePath = window.pathConfig.gameImage;
|
||||
const props = defineProps<{
|
||||
height: 7 | 10;
|
||||
percent: number;
|
||||
}>();
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<span class="time-zone">{{ serverNow }}</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { addMilliseconds } from "date-fns";
|
||||
import { type PropType, ref, onMounted, watch } from "vue";
|
||||
import { formatTime } from "@/util/formatTime";
|
||||
const props = defineProps({
|
||||
serverTime: {
|
||||
type: Object as PropType<Date>,
|
||||
required: false,
|
||||
default: new Date(),
|
||||
},
|
||||
timeFormat: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "HH:mm:ss",
|
||||
},
|
||||
});
|
||||
|
||||
const timeDiff = ref(0);
|
||||
const serverNow = ref("");
|
||||
|
||||
watch(
|
||||
() => props.serverTime,
|
||||
(newValue) => {
|
||||
const clientNow = new Date();
|
||||
timeDiff.value = newValue.getTime() - clientNow.getTime();
|
||||
}
|
||||
);
|
||||
|
||||
function updateNow() {
|
||||
const serverNowObj = addMilliseconds(new Date(), timeDiff.value);
|
||||
serverNow.value = formatTime(serverNowObj, props.timeFormat);
|
||||
setTimeout(() => {
|
||||
updateNow();
|
||||
}, 1000 - serverNowObj.getMilliseconds());
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const clientNow = new Date();
|
||||
timeDiff.value = props.serverTime.getTime() - clientNow.getTime();
|
||||
updateNow();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<table class="simple_nation_list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 44%">국명</th>
|
||||
<th style="width: 23%">국력</th>
|
||||
<th style="width: 15%">장수</th>
|
||||
<th style="width: 15%">속령</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="nation of nations" :key="nation.nation">
|
||||
<td>
|
||||
<span
|
||||
:style="{
|
||||
color: isBrightColor(nation.color) ? '#000' : '#fff',
|
||||
backgroundColor: nation.color,
|
||||
}"
|
||||
>{{ nation.name }}</span
|
||||
>
|
||||
</td>
|
||||
<td style="text-align: right">{{ nation.power.toLocaleString() }}</td>
|
||||
<td style="text-align: right">{{ nation.gennum.toLocaleString() }}</td>
|
||||
<td v-b-tooltip.hover style="text-align: right" :title="(nation.cities ?? []).join(', ')">
|
||||
{{ (nation.cities ?? []).length }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot></tfoot>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SimpleNationObj } from "@/defs";
|
||||
import type { PropType } from "vue";
|
||||
import { isBrightColor } from "@/util/isBrightColor";
|
||||
|
||||
defineProps({
|
||||
nations: {
|
||||
type: Array as PropType<SimpleNationObj[]>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.simple_nation_list {
|
||||
width: 100%;
|
||||
|
||||
thead {
|
||||
background-color: #cccccc;
|
||||
color: black;
|
||||
text-align: center;
|
||||
}
|
||||
th {
|
||||
border: 0;
|
||||
border-left: 1px solid gray;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
td {
|
||||
border: 0;
|
||||
border-left: 1px solid gray;
|
||||
padding: 1px 6px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,516 @@
|
||||
<template>
|
||||
<BButtonToolbar v-if="editable && editor" key-nav class="bg-dark">
|
||||
<BButtonGroup class="mx-1">
|
||||
<BButton v-b-tooltip.hover title="되돌리기" @click="editor?.commands.undo()">
|
||||
<i class="bi bi-arrow-90deg-left" />
|
||||
</BButton>
|
||||
<BButton v-b-tooltip.hover title="재실행" @click="editor?.commands.redo()">
|
||||
<i class="bi bi-arrow-90deg-right" />
|
||||
</BButton>
|
||||
</BButtonGroup>
|
||||
<BButtonGroup class="mx-1">
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{ 'is-active': editor.isActive('bold') }"
|
||||
title="진하게"
|
||||
@click="editor?.chain().focus().toggleBold().run()"
|
||||
>
|
||||
<i class="bi bi-type-bold" />
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{ 'is-active': editor.isActive('italic') }"
|
||||
title="기울이기"
|
||||
@click="editor?.chain().focus().toggleItalic().run()"
|
||||
>
|
||||
<i class="bi bi-type-italic" />
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{ 'is-active': editor.isActive('underline') }"
|
||||
title="밑줄"
|
||||
@click="editor?.chain().focus().toggleUnderline().run()"
|
||||
>
|
||||
<i class="bi bi-type-underline" />
|
||||
</BButton>
|
||||
<!-- 효과 지우기 -->
|
||||
</BButtonGroup>
|
||||
|
||||
<BButtonGroup class="mx-1">
|
||||
<BDropdown>
|
||||
<template #button-content> 크기 </template>
|
||||
<BDropdownItem @click="editor?.chain().focus().unsetFontSize().run()">
|
||||
<span>기본</span>
|
||||
</BDropdownItem>
|
||||
<BDropdownDivider />
|
||||
<BDropdownItem
|
||||
v-for="sizeItem in fontSize"
|
||||
:key="sizeItem"
|
||||
@click="editor?.chain().focus().setFontSize(sizeItem).run()"
|
||||
>
|
||||
<span
|
||||
:style="{
|
||||
'font-size': sizeItem,
|
||||
'text-decoration': editor.isActive('textStyle', {
|
||||
fontSize: sizeItem,
|
||||
})
|
||||
? 'underline'
|
||||
: undefined,
|
||||
}"
|
||||
>{{ sizeItem }}</span
|
||||
>
|
||||
</BDropdownItem>
|
||||
</BDropdown>
|
||||
<!-- 글꼴 -->
|
||||
</BButtonGroup>
|
||||
|
||||
<BButtonGroup class="mx-1">
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{ 'is-active': editor.isActive('strike') }"
|
||||
title="가로선"
|
||||
@click="editor?.chain().focus().toggleStrike().run()"
|
||||
>
|
||||
<i class="bi bi-type-strikethrough" />
|
||||
</BButton>
|
||||
<!-- 윗첨자, 아랫첨자 -->
|
||||
</BButtonGroup>
|
||||
|
||||
<BButtonGroup class="mx-1">
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
title="색상 취소"
|
||||
@click="editor?.chain().focus().unsetColor().unsetBackgroundColor().run()"
|
||||
>
|
||||
<i class="bi bi-droplet" />
|
||||
</BButton>
|
||||
<input
|
||||
v-b-tooltip.hover
|
||||
type="color"
|
||||
class="form-control form-control-color"
|
||||
:value="colorConvert(editor.getAttributes('textStyle').color, '#ffffff')"
|
||||
title="글자색"
|
||||
@input="editor?.chain().focus().setColor(($event.target as HTMLInputElement).value).run()"
|
||||
/>
|
||||
<input
|
||||
v-b-tooltip.hover
|
||||
type="color"
|
||||
class="form-control form-control-color"
|
||||
:value="colorConvert(editor.getAttributes('textStyle').backgroundColor, '#000000')"
|
||||
title="배경색"
|
||||
@input="
|
||||
editor?.chain().focus().setBackgroundColor(($event.target as HTMLInputElement).value).run()
|
||||
"
|
||||
/>
|
||||
</BButtonGroup>
|
||||
|
||||
<BButtonGroup class="mx-1">
|
||||
<BButton v-b-tooltip.hover title="이미지 추가" @click="showImageModal = true">
|
||||
<i class="bi bi-image" />
|
||||
</BButton>
|
||||
<!-- 이미지추가 -->
|
||||
<!-- 링크 -->
|
||||
<!-- 영상링크 -->
|
||||
<!-- 표 -->
|
||||
<!-- 구분선 삽입 -->
|
||||
<BButton v-b-tooltip.hover title="구분선" @click="editor?.chain().focus().setHorizontalRule().run()">
|
||||
<i class="bi bi-hr" />
|
||||
</BButton>
|
||||
</BButtonGroup>
|
||||
|
||||
<BButtonGroup class="mx-1">
|
||||
<!-- 글머리 기호 -->
|
||||
<!-- 번호 매기기 -->
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{ 'is-active': editor.isActive({ textAlign: 'left' }) }"
|
||||
title="왼쪽 정렬"
|
||||
@click="editor?.chain().focus().setTextAlign('left').run()"
|
||||
>
|
||||
<i class="bi bi-text-left" />
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{ 'is-active': editor.isActive({ textAlign: 'center' }) }"
|
||||
title="가운데 정렬"
|
||||
@click="editor?.chain().focus().setTextAlign('center').run()"
|
||||
>
|
||||
<i class="bi bi-text-center" />
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{ 'is-active': editor.isActive({ textAlign: 'right' }) }"
|
||||
title="오른쪽 정렬"
|
||||
@click="editor?.chain().focus().setTextAlign('right').run()"
|
||||
>
|
||||
<i class="bi bi-text-right" />
|
||||
</BButton>
|
||||
<!-- 문단정렬(왼, 가, 오, 양)(내어, 들여) -->
|
||||
</BButtonGroup>
|
||||
|
||||
<BButtonGroup class="mx-1" />
|
||||
|
||||
<BButtonGroup class="mx-1">
|
||||
<!-- 줄간격 (1.0, 1.2, 1.4, 1.5, 1.6, 1.8, 2.0, 3.0) -->
|
||||
</BButtonGroup>
|
||||
|
||||
<BButtonGroup class="mx-1">
|
||||
<!-- 원본 코드 -->
|
||||
</BButtonGroup>
|
||||
</BButtonToolbar>
|
||||
<BubbleMenu
|
||||
v-if="editable && editor"
|
||||
v-show="editor.isActive('custom-image')"
|
||||
:tippyOptions="{ animation: false, maxWidth: 600 }"
|
||||
:editor="editor"
|
||||
>
|
||||
<BButtonToolbar>
|
||||
<BButtonGroup class="mx-1">
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{
|
||||
'is-active': editor.isActive('custom-image', {
|
||||
size: 'small',
|
||||
}),
|
||||
f_frac: true,
|
||||
}"
|
||||
title="1/4 너비로 채우기"
|
||||
@click="editor?.chain().focus().setImageEx({ size: 'small' }).run()"
|
||||
>
|
||||
1/4
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{
|
||||
'is-active': editor.isActive('custom-image', {
|
||||
size: 'medium',
|
||||
}),
|
||||
f_frac: true,
|
||||
}"
|
||||
title="1/2 너비로 채우기"
|
||||
@click="editor?.chain().focus().setImageEx({ size: 'medium' }).run()"
|
||||
>
|
||||
1/2
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{
|
||||
'is-active': editor.isActive('custom-image', {
|
||||
size: 'large',
|
||||
}),
|
||||
f_frac: true,
|
||||
}"
|
||||
title="가득 채우기"
|
||||
@click="editor?.chain().focus().setImageEx({ size: 'large' }).run()"
|
||||
>
|
||||
1
|
||||
</BButton>
|
||||
<BButton
|
||||
:class="{
|
||||
'is-active': editor.isActive('custom-image', {
|
||||
size: 'original',
|
||||
}),
|
||||
}"
|
||||
@click="editor?.chain().focus().setImageEx({ size: 'original' }).run()"
|
||||
>
|
||||
원본
|
||||
</BButton>
|
||||
</BButtonGroup>
|
||||
<BButtonGroup class="mx-1">
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{
|
||||
'is-active': editor.isActive('custom-image', {
|
||||
float: 'float-left',
|
||||
}),
|
||||
}"
|
||||
title="왼쪽으로 붙이기"
|
||||
@click="editor?.chain().focus().setImageEx({ align: 'float-left' }).run()"
|
||||
>
|
||||
<i class="bi bi-chevron-bar-left" />
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{
|
||||
'is-active': editor.isActive('custom-image', {
|
||||
float: 'left',
|
||||
}),
|
||||
}"
|
||||
title="왼쪽으로"
|
||||
@click="editor?.chain().focus().setImageEx({ align: 'left' }).run()"
|
||||
>
|
||||
<i class="bi bi-align-start" />
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{
|
||||
'is-active': editor.isActive('custom-image', {
|
||||
float: 'center',
|
||||
}),
|
||||
}"
|
||||
title="가운데로"
|
||||
@click="editor?.chain().focus().setImageEx({ align: 'center' }).run()"
|
||||
>
|
||||
<i class="bi bi-align-center" />
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{
|
||||
'is-active': editor.isActive('custom-image', {
|
||||
float: 'right',
|
||||
}),
|
||||
}"
|
||||
title="오른쪽으로 붙이기"
|
||||
@click="editor?.chain().focus().setImageEx({ align: 'right' }).run()"
|
||||
>
|
||||
<i class="bi bi-align-end" />
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{
|
||||
'is-active': editor.isActive('custom-image', {
|
||||
float: 'float-right',
|
||||
}),
|
||||
}"
|
||||
title="오른쪽으로 붙이기"
|
||||
@click="editor?.chain().focus().setImageEx({ align: 'float-right' }).run()"
|
||||
>
|
||||
<i class="bi bi-chevron-bar-right" />
|
||||
</BButton>
|
||||
</BButtonGroup>
|
||||
</BButtonToolbar>
|
||||
</BubbleMenu>
|
||||
<EditorContent :editor="editor" class="tiptap-editor" />
|
||||
<BModal
|
||||
v-model="showImageModal"
|
||||
title="이미지 추가"
|
||||
okTitle="추가"
|
||||
cancelTitle="취소"
|
||||
@ok="tryAddImage"
|
||||
@show="resetModal"
|
||||
@hidden="resetModal"
|
||||
>
|
||||
<div class="bg-light text-dark">
|
||||
<BFormGroup
|
||||
label-cols-sm="4"
|
||||
label-cols-lg="3"
|
||||
content-cols-sm
|
||||
content-cols-lg="7"
|
||||
description="업로드할 파일을 선택해주세요. (jpg, png, gif, webp)"
|
||||
label="이미지 업로드"
|
||||
label-align="right"
|
||||
:label-for="`${uuid}_image_upload`"
|
||||
>
|
||||
<input
|
||||
:id="`${uuid}_image_upload`"
|
||||
class="form-control"
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png,.gif,.webp"
|
||||
@change="chooseImage"
|
||||
/>
|
||||
</BFormGroup>
|
||||
<BFormGroup
|
||||
label-cols-sm="4"
|
||||
label-cols-lg="3"
|
||||
content-cols-sm
|
||||
content-cols-lg="7"
|
||||
description="링크할 이미지 주소를 입력해주세요."
|
||||
label="이미지 링크"
|
||||
label-align="right"
|
||||
:label-for="`${uuid}_image_link`"
|
||||
>
|
||||
<BFormInput v-model="imageLink" />
|
||||
</BFormGroup>
|
||||
</div>
|
||||
</BModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//import "@scss/common/bootstrap5.scss";
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { Editor, EditorContent, BubbleMenu } from "@tiptap/vue-3";
|
||||
import { FontSize } from "@/tiptap-ext/FontSize";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import Underline from "@tiptap/extension-underline";
|
||||
import TextStyle from "@tiptap/extension-text-style";
|
||||
import TextAlign from "@tiptap/extension-text-align";
|
||||
import Color from "@tiptap/extension-color";
|
||||
//import Image from "@tiptap/extension-image";
|
||||
import CustomImage from "@/tiptap-ext/CustomImage";
|
||||
import Link from "@tiptap/extension-link";
|
||||
import { BackgroundColor } from "@/tiptap-ext/BackgroundColor";
|
||||
import {
|
||||
BButtonGroup,
|
||||
BButtonToolbar,
|
||||
BButton,
|
||||
BDropdown,
|
||||
BDropdownItem,
|
||||
BDropdownDivider,
|
||||
BModal,
|
||||
BFormGroup,
|
||||
BFormInput,
|
||||
} from "bootstrap-vue-3";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
import { getBase64FromFileObject } from "@/util/getBase64FromFileObject";
|
||||
import { isObject, isString } from "lodash";
|
||||
import type { AxiosError } from "axios";
|
||||
import { SammoAPI } from "@/SammoAPI";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["ready", "update:modelValue"]);
|
||||
|
||||
const uuid = ref(uuidv4());
|
||||
const editor = ref<InstanceType<typeof Editor>>();
|
||||
//const fontList = ref(["Pretendard", "맑은 고딕", "궁서", "돋움"]);
|
||||
const fontSize = ref(["8px", "10px", "12px", "14px", "18px", "22px", "28px", "36px", "48px", "72px"]);
|
||||
const imageUploadFiles = ref(null as FileList | null);
|
||||
const imageLink = ref("");
|
||||
const showImageModal = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value: string) => {
|
||||
const isSame = editor.value?.getHTML() === value;
|
||||
|
||||
if (isSame) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.value?.commands.setContent(value, false);
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.editable,
|
||||
(value: boolean) => {
|
||||
if (!editor.value) {
|
||||
return;
|
||||
}
|
||||
editor.value.options.editable = value;
|
||||
if (value == true) {
|
||||
editor.value.commands.focus();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
const vEditor = new Editor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Underline,
|
||||
FontSize,
|
||||
TextStyle,
|
||||
TextAlign.configure({
|
||||
types: ["heading", "paragraph"],
|
||||
}),
|
||||
Color.configure({
|
||||
types: ["textStyle"],
|
||||
}),
|
||||
BackgroundColor.configure({
|
||||
types: ["textStyle"],
|
||||
}),
|
||||
CustomImage,
|
||||
Link,
|
||||
],
|
||||
editable: props.editable,
|
||||
content: props.modelValue,
|
||||
onUpdate: () => {
|
||||
emit("update:modelValue", editor.value?.getHTML());
|
||||
},
|
||||
onCreate: () => {
|
||||
emit("ready");
|
||||
},
|
||||
});
|
||||
editor.value = vEditor;
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
editor.value?.destroy();
|
||||
});
|
||||
|
||||
function chooseImage(e: Event) {
|
||||
const target = unwrap(e.target) as HTMLInputElement;
|
||||
imageUploadFiles.value = target.files;
|
||||
}
|
||||
|
||||
function colorConvert(val: string | undefined, defaultVal: string) {
|
||||
if (!val) {
|
||||
return defaultVal;
|
||||
}
|
||||
if (val.startsWith("rgb")) {
|
||||
const rgb = val.split("(")[1].split(")")[0].split(",");
|
||||
const vals: string[] = [];
|
||||
for (const subColor of rgb) {
|
||||
const hexSubColor = parseInt(subColor).toString(16);
|
||||
if (hexSubColor.length == 1) {
|
||||
vals.push("0");
|
||||
}
|
||||
vals.push(hexSubColor);
|
||||
}
|
||||
return `#${vals.join("")}`;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
async function tryAddImage(bvModalEvt: Event) {
|
||||
if (imageUploadFiles.value === null || imageUploadFiles.value.length == 0) {
|
||||
addImageLink(bvModalEvt);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetImage = unwrap(unwrap(imageUploadFiles.value).item(0));
|
||||
let imageResult: {
|
||||
result: true;
|
||||
path: string;
|
||||
};
|
||||
try {
|
||||
const base64Binary = await getBase64FromFileObject(targetImage);
|
||||
imageResult = await SammoAPI.Misc.UploadImage({
|
||||
imageData: base64Binary,
|
||||
});
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
alert(e);
|
||||
bvModalEvt.preventDefault();
|
||||
}
|
||||
|
||||
if (isObject(e) && "response" in e) {
|
||||
const axiosErr = e as AxiosError;
|
||||
if (axiosErr.response?.status === 413) {
|
||||
alert("허용 용량을 초과했습니다.");
|
||||
bvModalEvt.preventDefault();
|
||||
}
|
||||
}
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
const imagePath = imageResult.path;
|
||||
editor.value?.chain().focus().setImageEx({ src: imagePath }).run();
|
||||
}
|
||||
|
||||
function addImageLink(bvModalEvt: Event) {
|
||||
if (!imageLink.value) {
|
||||
alert("업로드할 이미지를 선택하거나, 이미지 주소를 입력해주세요.");
|
||||
bvModalEvt.preventDefault();
|
||||
return false;
|
||||
}
|
||||
editor.value?.chain().focus().setImageEx({ src: imageLink.value }).run();
|
||||
}
|
||||
|
||||
function resetModal() {
|
||||
imageLink.value = "";
|
||||
imageUploadFiles.value = null;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div :class="['bg0', 'back_bar', teleportZone ? 'back_bar_teleport' : undefined]">
|
||||
<button type="button" class="btn btn-sammo-base2 back_btn" @click="back">
|
||||
{{ props.type == "close" ? "창 닫기" : "돌아가기" }}</button
|
||||
><button v-if="reloadable" type="button" class="btn btn-sammo-base2 reload_btn" @click="reload">갱신</button>
|
||||
<div v-else />
|
||||
<h2 class="title">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<template v-if="hasSlot">
|
||||
<slot></slot>
|
||||
</template>
|
||||
<div v-else-if="teleportZone" :id="teleportZone" class="teleport-zone"></div>
|
||||
<template v-else>
|
||||
<div> </div>
|
||||
<b-button
|
||||
v-if="toggleSearch !== undefined"
|
||||
class="btn-toggle-zoom"
|
||||
:variant="toggleSearch ? 'info' : 'secondary'"
|
||||
:pressed="toggleSearch"
|
||||
@click="toggleSearch = !toggleSearch"
|
||||
>
|
||||
{{ toggleSearch ? "검색 켜짐" : "검색 꺼짐" }}
|
||||
</b-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import "@scss/game_bg.scss";
|
||||
import { type PropType, ref, watch, useSlots } from "vue";
|
||||
import VueTypes from "vue-types";
|
||||
|
||||
const props = defineProps({
|
||||
title: VueTypes.string.isRequired,
|
||||
type: {
|
||||
type: String as PropType<"normal" | "chief" | "close">,
|
||||
default: "normal",
|
||||
required: false,
|
||||
},
|
||||
searchable: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
reloadable: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
teleportZone: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
const slots = useSlots();
|
||||
console.log(slots);
|
||||
const hasSlot = !!slots['default'];
|
||||
|
||||
const emit = defineEmits(["update:searchable", "reload"]);
|
||||
|
||||
const toggleSearch = ref(props.searchable);
|
||||
|
||||
watch(toggleSearch, (val) => {
|
||||
emit("update:searchable", val);
|
||||
});
|
||||
|
||||
function back() {
|
||||
if (props.type === "normal") {
|
||||
location.href = "./";
|
||||
} else if (props.type == "chief") {
|
||||
location.href = "v_chiefCenter.php";
|
||||
} else {
|
||||
//TODO: window.close하려면 부모창이 있어야함!
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
function reload() {
|
||||
emit("reload");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.back_bar {
|
||||
max-width: 1000px;
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
display: grid;
|
||||
grid-template-columns: 90px 90px 1fr 90px 90px;
|
||||
position: relative;
|
||||
height: 24pt;
|
||||
}
|
||||
|
||||
.back_bar.back_bar_teleport {
|
||||
grid-template-columns: 90px 90px 1fr 180px;
|
||||
}
|
||||
|
||||
.reload_btn {
|
||||
height: 24pt;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.teleport-zone {
|
||||
height: 24pt;
|
||||
}
|
||||
|
||||
.back_btn {
|
||||
height: 24pt;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.btn-toggle-zoom {
|
||||
height: 24pt;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
line-height: 24pt;
|
||||
font-size: 18pt;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,28 @@
|
||||
import $ from 'jquery';
|
||||
import 'bootstrap';
|
||||
import 'select2/dist/js/select2.full.js'
|
||||
|
||||
|
||||
$(function() {
|
||||
$('#citySelector').select2({
|
||||
theme: 'bootstrap4',
|
||||
placeholder: "도시를 선택해 주세요.",
|
||||
allowClear: false,
|
||||
language: "ko",
|
||||
containerCss: {
|
||||
display: "inline-block !important",
|
||||
color: 'white !important'
|
||||
},
|
||||
containerCssClass: 'simple-select2-align-center bg-secondary text-secondary',
|
||||
dropdownCssClass: 'simple-select2-align-center bg-secondary text-secondary',
|
||||
});
|
||||
$('#citySelector').on('select2:select', function(e){
|
||||
const data = e.params.data;
|
||||
if(!data.selected || data.disabled){
|
||||
return;
|
||||
}
|
||||
const $obj = $('#citySelector').parents('form');
|
||||
$obj.trigger('submit');
|
||||
console.log($obj);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { ValidResponse } from "@/util/callSammoAPI";
|
||||
|
||||
export type BasicResourceAuctionBidder = {
|
||||
amount: number;
|
||||
date: string;
|
||||
generalID: number;
|
||||
generalName: string;
|
||||
};
|
||||
|
||||
export type BasicResourceAuctionInfo = {
|
||||
id: number;
|
||||
type: "buyRice" | "sellRice";
|
||||
hostGeneralID: number;
|
||||
hostName: string;
|
||||
openDate: string;
|
||||
closeDate: string;
|
||||
amount: number;
|
||||
startBidAmount: number;
|
||||
finishBidAmount: number;
|
||||
highestBid?: BasicResourceAuctionBidder;
|
||||
};
|
||||
|
||||
export type UniqueItemAuctionBidder = {
|
||||
generalName: string;
|
||||
amount: number;
|
||||
isCallerHighestBidder: boolean;
|
||||
date: string;
|
||||
};
|
||||
|
||||
export type UniqueItemAuctionInfo = {
|
||||
id: number;
|
||||
finished: boolean;
|
||||
title: string;
|
||||
target: string;
|
||||
isCallerHost: boolean;
|
||||
hostName: string;
|
||||
closeDate: string;
|
||||
remainCloseDateExtensionCnt: number;
|
||||
availableLatestBidCloseDate: string;
|
||||
};
|
||||
|
||||
export type ActiveResourceAuctionList = ValidResponse & {
|
||||
buyRice: BasicResourceAuctionInfo[];
|
||||
sellRice: BasicResourceAuctionInfo[];
|
||||
recentLogs: string[];
|
||||
generalID: number;
|
||||
};
|
||||
|
||||
export type UniqueItemAuctionList = ValidResponse & {
|
||||
list: (UniqueItemAuctionInfo & {
|
||||
highestBid: UniqueItemAuctionBidder;
|
||||
})[];
|
||||
obfuscatedName: string;
|
||||
};
|
||||
|
||||
export type UniqueItemAuctionDetail = ValidResponse & {
|
||||
auction: UniqueItemAuctionInfo;
|
||||
bidList: UniqueItemAuctionBidder[];
|
||||
obfuscatedName: string;
|
||||
remainPoint: number;
|
||||
};
|
||||
|
||||
export type OpenAuctionResponse = ValidResponse & {
|
||||
auctionID: number;
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { ValidResponse } from "@/defs";
|
||||
|
||||
export type BettingListResponse = ValidResponse & {
|
||||
bettingList: Record<number, Omit<BettingInfo & { totalAmount: number }, "candidates">>;
|
||||
year: number;
|
||||
month: number;
|
||||
};
|
||||
|
||||
|
||||
export type BettingDetailResponse = ValidResponse & {
|
||||
bettingInfo: BettingInfo;
|
||||
bettingDetail: [string, number][];
|
||||
myBetting: [string, number][];
|
||||
remainPoint: number;
|
||||
year: number;
|
||||
month: number;
|
||||
};
|
||||
|
||||
export type BettingInfo = {
|
||||
id: number;
|
||||
type: 'nationBetting',
|
||||
name: string;
|
||||
finished: boolean;
|
||||
selectCnt: number;
|
||||
isExclusive?: boolean;
|
||||
reqInheritancePoint: boolean;
|
||||
openYearMonth: number;
|
||||
closeYearMonth: number;
|
||||
candidates: Record<string, SelectItem>;
|
||||
winner?: number[];
|
||||
}
|
||||
|
||||
export type SelectItem = {
|
||||
title: string;
|
||||
info?: string;
|
||||
isHtml?: boolean;
|
||||
aux?: Record<string, unknown>;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { TurnObj } from "@/defs";
|
||||
|
||||
export type ReservedCommandResponse = {
|
||||
result: true;
|
||||
turnTime: string;
|
||||
turnTerm: number;
|
||||
year: number;
|
||||
month: number;
|
||||
date: string;
|
||||
turn: TurnObj[];
|
||||
autorun_limit: null | number;
|
||||
};
|
||||
|
||||
export type ReserveCommandResponse = {
|
||||
result: true,
|
||||
brief: string,
|
||||
}
|
||||
|
||||
export type ReserveBulkCommandResponse = {
|
||||
result: true,
|
||||
briefList: string[],
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export type JoinArgs = {
|
||||
name: string;
|
||||
leadership: number;
|
||||
strength: number;
|
||||
intel: number;
|
||||
pic: boolean;
|
||||
character: string;
|
||||
inheritSpecial?: string;
|
||||
inheritTurntime?: number;
|
||||
inheritCity?: number;
|
||||
inheritBonusStat?: [number, number, number];
|
||||
};
|
||||
|
||||
export type GeneralLogType = 'generalHistory'|'generalAction'|'battleResult'|'battleDetail';
|
||||
|
||||
export type GetGeneralLogResponse = {
|
||||
result: true,
|
||||
reqType: GeneralLogType,
|
||||
generalID: number,
|
||||
log: Record<string, string>
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { CityID, CrewTypeID, GameCityDefault, GameConstType, GameUnitType, GameIActionKey, GameIActionCategory, GameIActionInfo } from "@/defs/GameObj";
|
||||
import type { diplomacyState, MapResult, SimpleNationObj } from "@/defs";
|
||||
|
||||
export interface GetConstResponse {
|
||||
result: true;
|
||||
cacheKey: string;
|
||||
data: {
|
||||
gameConst: GameConstType;
|
||||
gameUnitConst: Record<CrewTypeID, GameUnitType>;
|
||||
cityConst: Record<CityID, GameCityDefault>;
|
||||
cityConstMap: {
|
||||
region: Record<number | string, string | number>;
|
||||
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";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type HistoryObj = {
|
||||
server_id: string,
|
||||
year: number;
|
||||
month: number;
|
||||
map: MapResult,
|
||||
global_history: string[],
|
||||
global_action: string[],
|
||||
nations: {
|
||||
capital: number,
|
||||
cities: string[],
|
||||
color: string,
|
||||
gennum: number,
|
||||
level: number,
|
||||
name: string,
|
||||
nation: number,
|
||||
power: number,
|
||||
type: GameIActionKey
|
||||
}[],
|
||||
}
|
||||
|
||||
export type GetHistoryResponse = {
|
||||
result: true;
|
||||
data: HistoryObj & {
|
||||
hash: string;
|
||||
//no: number,
|
||||
};
|
||||
}
|
||||
|
||||
export type GetCurrentHistoryResponse = {
|
||||
result: true;
|
||||
data: HistoryObj;
|
||||
}
|
||||
|
||||
|
||||
export type GetDiplomacyResponse = {
|
||||
result: true;
|
||||
nations: SimpleNationObj[];
|
||||
conflict: [number, Record<number, number>][];
|
||||
diplomacyList: Record<number, Record<number, diplomacyState>>;
|
||||
myNationID: number;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ValidResponse } from "@/util/callSammoAPI";
|
||||
|
||||
export type inheritBuffType =
|
||||
| "warAvoidRatio"
|
||||
| "warCriticalRatio"
|
||||
| "warMagicTrialProb"
|
||||
| "domesticSuccessProb"
|
||||
| "domesticFailProb"
|
||||
| "warAvoidRatioOppose"
|
||||
| "warCriticalRatioOppose"
|
||||
| "warMagicTrialProbOppose";
|
||||
|
||||
export type InheritPointLogItem = {
|
||||
id: number;
|
||||
server_id: string;
|
||||
year: number;
|
||||
month: number;
|
||||
date: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type InheritLogResponse = ValidResponse & {
|
||||
log: InheritPointLogItem[];
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
|
||||
|
||||
export type LoginResponse = {
|
||||
result: true,
|
||||
nextToken: [number, string] | undefined,
|
||||
}
|
||||
|
||||
export type LoginFailed = {
|
||||
result: false,
|
||||
reqOTP: boolean,
|
||||
reason: string,
|
||||
}
|
||||
|
||||
export type LoginResponseWithKakao = LoginResponse | LoginFailed;
|
||||
|
||||
export type OTPResponse = {
|
||||
result: true,
|
||||
validUntil: string,
|
||||
} | {
|
||||
result: false,
|
||||
reset: boolean,
|
||||
reason: string,
|
||||
}
|
||||
|
||||
|
||||
export type AutoLoginNonceResponse = {
|
||||
result: true,
|
||||
loginNonce: string,
|
||||
};
|
||||
|
||||
export type AutoLoginResponse = {
|
||||
result: true,
|
||||
nextToken: [number, string] | undefined,
|
||||
}
|
||||
|
||||
export type AutoLoginFailed = {
|
||||
result: false,
|
||||
silent: boolean,
|
||||
reason: string,
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { ValidResponse } from "@/defs";
|
||||
|
||||
export type UploadImageResponse = ValidResponse & {
|
||||
path: string;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import type { ValuesOf, TurnObj } from "@/defs";
|
||||
import type { GameObjClassKey } from "@/defs/GameObj";
|
||||
import type { ValidResponse } from "@/SammoAPI";
|
||||
|
||||
export type SetBlockWarResponse = ValidResponse & {
|
||||
availableCnt: number;
|
||||
};
|
||||
|
||||
export type GeneralListItemP0 = {
|
||||
no: number,
|
||||
name: string,
|
||||
nation: number,
|
||||
npc: number,
|
||||
injury: number,
|
||||
leadership: number,
|
||||
strength: number,
|
||||
intel: number,
|
||||
explevel: number,
|
||||
dedlevel: number,
|
||||
gold: number,
|
||||
rice: number,
|
||||
killturn: number,
|
||||
picture: string,
|
||||
imgsvr: 0 | 1,
|
||||
age: number,
|
||||
specialDomestic: GameObjClassKey,
|
||||
specialWar: GameObjClassKey,
|
||||
personal: GameObjClassKey,
|
||||
belong: number,
|
||||
connect: number,
|
||||
|
||||
officerLevel: number, //권한에따라 태수,군사,시종 노출 여부가 다름
|
||||
officerLevelText: string,
|
||||
lbonus: number,
|
||||
ownerName: string | null, //NPC 출력용에 따라 결과가 다름
|
||||
honorText: string,
|
||||
dedLevelText: string,
|
||||
bill: number,
|
||||
reservedCommand: TurnObj[] | null,
|
||||
|
||||
autorun_limit: number,
|
||||
}
|
||||
|
||||
export type GeneralListItemP1 = {
|
||||
con: number,
|
||||
specage: number,
|
||||
specage2: number,
|
||||
leadership_exp: number,
|
||||
strength_exp: number,
|
||||
intel_exp: number,
|
||||
|
||||
dex1: number,
|
||||
dex2: number,
|
||||
dex3: number,
|
||||
dex4: number,
|
||||
dex5: number,
|
||||
|
||||
city: number,
|
||||
experience: number,
|
||||
dedication: number,
|
||||
officer_level: number,
|
||||
officer_city: number,
|
||||
defence_train: number,
|
||||
troop: number,
|
||||
crewtype: GameObjClassKey,
|
||||
crew: number,
|
||||
train: number,
|
||||
atmos: number,
|
||||
turntime: string,
|
||||
recent_war: string,
|
||||
horse: GameObjClassKey,
|
||||
weapon: GameObjClassKey,
|
||||
book: GameObjClassKey,
|
||||
item: GameObjClassKey,
|
||||
|
||||
warnum: number,
|
||||
killnum: number,
|
||||
deathnum: number,
|
||||
killcrew: number,
|
||||
deathcrew: number,
|
||||
firenum: number,
|
||||
} & GeneralListItemP0;
|
||||
|
||||
export type GeneralListItemP2 = GeneralListItemP1;
|
||||
|
||||
export type RawGeneralListItem = GeneralListItemP0 | GeneralListItemP1 | GeneralListItemP2;
|
||||
|
||||
export type GeneralListItem =
|
||||
(GeneralListItemP0 & { st0: true, st1: false, st2: false, permission: 0 }) |
|
||||
(GeneralListItemP1 & { st0: true, st1: true, st2: false, permission: 1 }) |
|
||||
(GeneralListItemP2 & { st0: true, st1: true, st2: true, permission: 2 | 3 | 4 });
|
||||
|
||||
type ResponseEnv = {
|
||||
year: number,
|
||||
month: number,
|
||||
turntime: string,
|
||||
turnterm: number,
|
||||
killturn: number,
|
||||
autorun_user?: {
|
||||
limit_minutes: number,
|
||||
options: Record<string, number>,
|
||||
}
|
||||
}
|
||||
|
||||
export type RawGeneralListP0 = ValidResponse & {
|
||||
permission: 0,
|
||||
column: (keyof GeneralListItemP0)[],
|
||||
list: ValuesOf<GeneralListItemP0>[][],
|
||||
troops?: null,
|
||||
env: ResponseEnv,
|
||||
}
|
||||
|
||||
export type RawGeneralListP1 = ValidResponse & {
|
||||
permission: 1,
|
||||
column: (keyof GeneralListItemP1)[],
|
||||
list: ValuesOf<GeneralListItemP1>[][],
|
||||
troops?: null,
|
||||
env: ResponseEnv,
|
||||
}
|
||||
|
||||
export type RawGeneralListP2 = ValidResponse & {
|
||||
permission: 2 | 3 | 4,
|
||||
column: (keyof GeneralListItemP2)[],
|
||||
list: ValuesOf<GeneralListItemP2>[][],
|
||||
troops: [number, string][],
|
||||
env: ResponseEnv,
|
||||
}
|
||||
|
||||
export type GeneralListResponse = RawGeneralListP0 | RawGeneralListP1 | RawGeneralListP2;
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { CommandItem, TurnObj } from "@/defs";
|
||||
|
||||
export type ChiefResponse = {
|
||||
result: true;
|
||||
lastExecute: string;
|
||||
year: number;
|
||||
month: number;
|
||||
turnTerm: number;
|
||||
date: string;
|
||||
chiefList: Record<
|
||||
number,
|
||||
{
|
||||
name: string | undefined;
|
||||
turnTime: string | undefined;
|
||||
officerLevelText: string;
|
||||
officerLevel: number;
|
||||
npcType: number;
|
||||
turn: TurnObj[];
|
||||
}
|
||||
>;
|
||||
isChief: boolean;
|
||||
autorun_limit: number;
|
||||
officerLevel: number;
|
||||
commandList: {
|
||||
category: string;
|
||||
values: CommandItem[];
|
||||
}[];
|
||||
mapName: string,
|
||||
unitSet: string,
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { ValidResponse } from "@/util/callSammoAPI";
|
||||
|
||||
export type VoteInfo = {
|
||||
id: number;
|
||||
title: string;
|
||||
multipleOptions: number;
|
||||
startDate: string;
|
||||
endDate?: string;
|
||||
options: string[];
|
||||
}
|
||||
|
||||
export type VoteComment = {
|
||||
id: number;
|
||||
voteID: number;
|
||||
generalID: number;
|
||||
nationName: string;
|
||||
generalName: string;
|
||||
text: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export type VoteListResult = ValidResponse & {
|
||||
votes: Record<string, VoteInfo>;
|
||||
}
|
||||
|
||||
export type VoteDetailResult = ValidResponse & {
|
||||
voteInfo: VoteInfo,
|
||||
votes: [number[], number][],
|
||||
comments: VoteComment[],
|
||||
myVote: null|number[],
|
||||
userCnt: number,
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import type { CityLevel, ItemTypeKey } from ".";
|
||||
|
||||
type CSSRGBColor = string;
|
||||
|
||||
export type GameObjClassKey = string;
|
||||
export type GameIActionKey = GameObjClassKey;
|
||||
|
||||
type SpecialDomesticKey = GameIActionKey;
|
||||
type SpecialWarKey = GameIActionKey;
|
||||
type GameItemKey = GameIActionKey;
|
||||
type MapTypeKey = string;
|
||||
type UnitSetKey = string;
|
||||
type RawHTMLString = string;
|
||||
type NationTypeKey = GameIActionKey;
|
||||
type GamePersonalityKey = GameIActionKey;
|
||||
type GeneralCommandName = GameObjClassKey;
|
||||
type ChiefCommandName = GameObjClassKey;
|
||||
|
||||
/** GameConst.php */
|
||||
export type GameConstType = {
|
||||
title: string;
|
||||
banner: RawHTMLString;
|
||||
mapName: MapTypeKey;
|
||||
unitSet: UnitSetKey;
|
||||
develrate: number;
|
||||
upgradeLimit: number;
|
||||
dexLimit: number;
|
||||
defaultAtmosLow: number;
|
||||
defaultTrainLow: number;
|
||||
defaultAtmosHigh: number;
|
||||
defaultTrainHigh: number;
|
||||
maxAtmosByCommand: number;
|
||||
maxTrainByCommand: number;
|
||||
maxAtmosByWar: number;
|
||||
maxTrainByWar: number;
|
||||
trainDelta: number;
|
||||
atmosDelta: number;
|
||||
atmosSideEffectByTraining: number;
|
||||
trainSideEffectByAtmosTurn: number;
|
||||
sabotageDefaultProb: number;
|
||||
sabotageProbCoefByStat: number;
|
||||
sabotageDamageMin: number;
|
||||
sabotageDamageMax: number;
|
||||
basecolor: CSSRGBColor;
|
||||
basecolor2: CSSRGBColor;
|
||||
basecolor3: CSSRGBColor;
|
||||
basecolor4: CSSRGBColor;
|
||||
armperphase: number;
|
||||
basegold: number;
|
||||
baserice: number;
|
||||
minNationalGold: number;
|
||||
minNationalRice: number;
|
||||
exchangeFee: number;
|
||||
adultAge: number;
|
||||
minPushHallAge: number;
|
||||
maxDedLevel: number;
|
||||
maxTechLevel: number;
|
||||
maxBetrayCnt: number;
|
||||
|
||||
basePopIncreaseAmount: number;
|
||||
expandCityPopIncreaseAmount: number;
|
||||
expandCityDevelIncreaseAmount: number;
|
||||
expandCityWallIncreaseAmount: number;
|
||||
expandCityDefaultCost: number;
|
||||
expandCityCostCoef: number;
|
||||
minAvailableRecruitPop: number;
|
||||
initialNationGenLimitForRandInit: number;
|
||||
initialNationGenLimit: number;
|
||||
|
||||
defaultMaxGeneral: number;
|
||||
defaultMaxNation: number;
|
||||
defaultMaxGenius: number;
|
||||
defaultStartYear: number;
|
||||
|
||||
joinRuinedNPCProp: number;
|
||||
|
||||
defaultGold: number;
|
||||
defaultRice: number;
|
||||
|
||||
coefAidAmount: number;
|
||||
|
||||
maxResourceActionAmount: number;
|
||||
resourceActionAmountGuide: number[];
|
||||
|
||||
generalMinimumGold: number;
|
||||
generalMinimumRice: number;
|
||||
|
||||
maxTurn: number;
|
||||
maxChiefTurn: number;
|
||||
|
||||
statGradeLevel: number;
|
||||
|
||||
openingPartYear: number;
|
||||
joinActionLimit: number;
|
||||
|
||||
bornMinStatBonus: number;
|
||||
bornMaxStatBonus: number;
|
||||
|
||||
availableNationType: NationTypeKey[];
|
||||
neutralNationType: NationTypeKey;
|
||||
|
||||
defaultSpecialDomestic: SpecialDomesticKey;
|
||||
availableSpecialDomestic: SpecialDomesticKey[];
|
||||
optionalSpecialDomestic: SpecialDomesticKey[];
|
||||
|
||||
defaultSpecialWar: SpecialWarKey;
|
||||
availableSpecialWar: SpecialWarKey[];
|
||||
optionalSpecialWar: SpecialWarKey[];
|
||||
|
||||
neutralPersonality: GamePersonalityKey;
|
||||
availablePersonality: GamePersonalityKey[];
|
||||
optionalPersonality: GamePersonalityKey[];
|
||||
|
||||
maxUniqueItemLimit: [number, number][];
|
||||
|
||||
maxAvailableWarSettingCnt: number;
|
||||
incAvailableWarSettingCnt: number;
|
||||
|
||||
minMonthToAllowInheritItem: number;
|
||||
inheritBornSpecialPoint: number;
|
||||
inheritBornTurntimePoint: number;
|
||||
inheritBornCityPoint: number;
|
||||
inheritBornStatPoint: number;
|
||||
inheritItemUniqueMinPoint: number;
|
||||
inheritItemRandomPoint: number;
|
||||
inheritBuffPoints: number[];
|
||||
inheritSpecificSpecialPoint: number;
|
||||
inheritResetAttrPointBase: number[];
|
||||
|
||||
allItems: Record<ItemTypeKey, Record<GameItemKey, number>>;
|
||||
|
||||
availableGeneralCommand: Record<string, GeneralCommandName[]>;
|
||||
availableChiefCommand: Record<string, ChiefCommandName[]>;
|
||||
|
||||
retirementYear: number;
|
||||
|
||||
targetGeneralPool: GameObjClassKey;
|
||||
generalPoolAllowOption: string[];
|
||||
|
||||
randGenFirstName: string[];
|
||||
randGenMiddleName: string[];
|
||||
randGenLastName: string[];
|
||||
|
||||
npcBanMessageProb: number;
|
||||
npcSeizureMessageProb: number;
|
||||
npcMessageFreqByDay: number;
|
||||
|
||||
/**
|
||||
* Scenario::getGameConf
|
||||
*/
|
||||
|
||||
defaultStatTotal: number;
|
||||
defaultStatMin: number;
|
||||
defaultStatMax: number;
|
||||
defaultStatNPCTotal: number;
|
||||
defaultStatNPCMax: number;
|
||||
defaultStatNPCMin: number;
|
||||
chiefStatMin: number;
|
||||
|
||||
};
|
||||
|
||||
export type CrewTypeID = number;
|
||||
export type ArmTypeID = 0 | 1 | 2 | 3 | 4 | 5;
|
||||
|
||||
export type MapRegionID = number;
|
||||
export type CityID = number;
|
||||
|
||||
export type GameUnitType = {
|
||||
id: CrewTypeID;
|
||||
armType: ArmTypeID;
|
||||
name: string;
|
||||
attack: number;
|
||||
defence: number;
|
||||
speed: number;
|
||||
avoid: number;
|
||||
magicCoef: number;
|
||||
cost: number;
|
||||
rice: number;
|
||||
reqTech: number;
|
||||
reqCities: CityID[] | null;
|
||||
reqRegions: MapRegionID[] | null;
|
||||
reqYear: number;
|
||||
attackCoef: Record<CrewTypeID | ArmTypeID, number> | null | [];
|
||||
defenceCoef: Record<CrewTypeID | ArmTypeID, number> | null | [];
|
||||
info: string | string[];
|
||||
initSkillTrigger: GameObjClassKey[] | null;
|
||||
phaseSkillTrigger: GameObjClassKey[] | null;
|
||||
};
|
||||
|
||||
export type GameCityDefault = {
|
||||
id: CityID;
|
||||
name: string;
|
||||
level: CityLevel;
|
||||
population: number;
|
||||
agriculture: number;
|
||||
commerce: number;
|
||||
security: number;
|
||||
defence: number;
|
||||
wall: number;
|
||||
region: number;
|
||||
posX: number;
|
||||
posY: number;
|
||||
path: Record<CityID, string>;
|
||||
};
|
||||
|
||||
export type GameIActionCategory = "nationType" | "specialDomestic" | "specialWar" | "personality" | "item" | "crewtype";
|
||||
|
||||
export type GameIActionInfo = {
|
||||
value: string;
|
||||
name: string;
|
||||
info?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,635 @@
|
||||
import type { ColumnState } from "ag-grid-community";
|
||||
|
||||
export type GridDisplaySetting = {
|
||||
column: ColumnState[];
|
||||
columnGroup: {
|
||||
groupId: string;
|
||||
open: boolean;
|
||||
}[];
|
||||
};
|
||||
export const defaultDisplaySetting: Record<"war" | "normal", GridDisplaySetting> = {
|
||||
war: {
|
||||
column: [
|
||||
{
|
||||
colId: "icon",
|
||||
width: 80,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "name",
|
||||
width: 126,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "stat_1",
|
||||
width: 88,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "troop",
|
||||
width: 90,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "leadership",
|
||||
width: 60,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "strength",
|
||||
width: 60,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "intel",
|
||||
width: 60,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "officerLevel",
|
||||
width: 70,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "expDedLv_1",
|
||||
width: 60,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "explevel",
|
||||
width: 60,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "dedlevel",
|
||||
width: 70,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "goldRice_1",
|
||||
width: 80,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "gold",
|
||||
width: 70,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "rice",
|
||||
width: 70,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "city",
|
||||
width: 60,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "crewtypeAndCrew_1",
|
||||
width: 80,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "crewtype",
|
||||
width: 80,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "crew",
|
||||
width: 70,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "trainAtmos_1",
|
||||
width: 60,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "train",
|
||||
width: 70,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "atmos",
|
||||
width: 70,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "defence_train",
|
||||
width: 50,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "specials_1",
|
||||
width: 80,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "personal",
|
||||
width: 60,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "specialDomestic",
|
||||
width: 60,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "specialWar",
|
||||
width: 60,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "reservedCommandShort_1",
|
||||
width: 70,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "reservedCommand",
|
||||
width: 120,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "turntime",
|
||||
width: 60,
|
||||
hide: false,
|
||||
sort: "asc",
|
||||
sortIndex: 0,
|
||||
},
|
||||
{
|
||||
colId: "recent_war",
|
||||
width: 60,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "years_1",
|
||||
width: 60,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "age",
|
||||
width: 60,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "belong",
|
||||
width: 60,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "killturnAndConnect_1",
|
||||
width: 70,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "killturn",
|
||||
width: 70,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "connect",
|
||||
width: 70,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "warResults_1",
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "warnum",
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "killnum",
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "killcrew",
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
],
|
||||
columnGroup: [
|
||||
{
|
||||
groupId: "0",
|
||||
open: false,
|
||||
},
|
||||
{
|
||||
groupId: "1",
|
||||
open: false,
|
||||
},
|
||||
{
|
||||
groupId: "stat",
|
||||
open: false,
|
||||
},
|
||||
{
|
||||
groupId: "2",
|
||||
open: false,
|
||||
},
|
||||
{
|
||||
groupId: "expDedLv",
|
||||
open: false,
|
||||
},
|
||||
{
|
||||
groupId: "goldRice",
|
||||
open: true,
|
||||
},
|
||||
{
|
||||
groupId: "3",
|
||||
open: false,
|
||||
},
|
||||
{
|
||||
groupId: "4",
|
||||
open: false,
|
||||
},
|
||||
{
|
||||
groupId: "crewtypeAndCrew",
|
||||
open: false,
|
||||
},
|
||||
{
|
||||
groupId: "trainAtmos",
|
||||
open: false,
|
||||
},
|
||||
{
|
||||
groupId: "specials",
|
||||
open: false,
|
||||
},
|
||||
{
|
||||
groupId: "reservedCommandShort",
|
||||
open: true,
|
||||
},
|
||||
{
|
||||
groupId: "5",
|
||||
open: false,
|
||||
},
|
||||
{
|
||||
groupId: "6",
|
||||
open: false,
|
||||
},
|
||||
{
|
||||
groupId: "years",
|
||||
open: false,
|
||||
},
|
||||
{
|
||||
groupId: "killturnAndConnect",
|
||||
open: true,
|
||||
},
|
||||
{
|
||||
groupId: "warResults",
|
||||
open: false
|
||||
},
|
||||
],
|
||||
},
|
||||
normal: {
|
||||
column: [
|
||||
{
|
||||
colId: "icon",
|
||||
width: 80,
|
||||
hide: false,
|
||||
pinned: "left",
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "name",
|
||||
width: 126,
|
||||
hide: false,
|
||||
pinned: "left",
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "officerLevel",
|
||||
width: 70,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "expDedLv_1",
|
||||
width: 60,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "dedlevel",
|
||||
width: 70,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "explevel",
|
||||
width: 60,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "stat_1",
|
||||
width: 88,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "leadership",
|
||||
width: 60,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "strength",
|
||||
width: 60,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "intel",
|
||||
width: 60,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "troop",
|
||||
width: 90,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "goldRice_1",
|
||||
width: 80,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "gold",
|
||||
width: 70,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "rice",
|
||||
width: 70,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "city",
|
||||
width: 60,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "crewtypeAndCrew_1",
|
||||
width: 80,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "crewtype",
|
||||
width: 80,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "crew",
|
||||
width: 70,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "trainAtmos_1",
|
||||
width: 60,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "train",
|
||||
width: 70,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "atmos",
|
||||
width: 70,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "defence_train",
|
||||
width: 50,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "specials_1",
|
||||
width: 80,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "personal",
|
||||
width: 60,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "specialDomestic",
|
||||
width: 60,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "specialWar",
|
||||
width: 60,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "reservedCommandShort_1",
|
||||
width: 70,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "reservedCommand",
|
||||
width: 120,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "turntime",
|
||||
width: 60,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "recent_war",
|
||||
width: 60,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "years_1",
|
||||
width: 60,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "age",
|
||||
width: 60,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "belong",
|
||||
width: 60,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "killturnAndConnect_1",
|
||||
width: 70,
|
||||
hide: false,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "killturn",
|
||||
width: 70,
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "connect",
|
||||
width: 70,
|
||||
hide: false,
|
||||
sort: "desc",
|
||||
sortIndex: 0,
|
||||
},
|
||||
{
|
||||
colId: "warResults_1",
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "warnum",
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "killnum",
|
||||
hide: true,
|
||||
sort: null,
|
||||
},
|
||||
{
|
||||
colId: "killcrew",
|
||||
hide: true,
|
||||
sort: null,
|
||||
}
|
||||
],
|
||||
columnGroup: [
|
||||
{
|
||||
groupId: "0",
|
||||
open: false
|
||||
},
|
||||
{
|
||||
groupId: "1",
|
||||
open: false
|
||||
},
|
||||
{
|
||||
groupId: "stat",
|
||||
open: true
|
||||
},
|
||||
{
|
||||
groupId: "2",
|
||||
open: false
|
||||
},
|
||||
{
|
||||
groupId: "expDedLv",
|
||||
open: true
|
||||
},
|
||||
{
|
||||
groupId: "goldRice",
|
||||
open: true
|
||||
},
|
||||
{
|
||||
groupId: "3",
|
||||
open: false
|
||||
},
|
||||
{
|
||||
groupId: "4",
|
||||
open: false
|
||||
},
|
||||
{
|
||||
groupId: "crewtypeAndCrew",
|
||||
open: false
|
||||
},
|
||||
{
|
||||
groupId: "trainAtmos",
|
||||
open: false
|
||||
},
|
||||
{
|
||||
groupId: "specials",
|
||||
open: false
|
||||
},
|
||||
{
|
||||
groupId: "reservedCommandShort",
|
||||
open: true
|
||||
},
|
||||
{
|
||||
groupId: "5",
|
||||
open: false
|
||||
},
|
||||
{
|
||||
groupId: "6",
|
||||
open: false,
|
||||
},
|
||||
{
|
||||
groupId: "years",
|
||||
open: false
|
||||
},
|
||||
{
|
||||
groupId: "killturnAndConnect",
|
||||
open: true
|
||||
},
|
||||
{
|
||||
groupId: "warResults",
|
||||
open: false
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,267 @@
|
||||
import type { Args } from "@/processing/args";
|
||||
import type { ValidResponse, InvalidResponse } from "@/util/callSammoAPI";
|
||||
import type { GameIActionKey, GameObjClassKey } from "./GameObj";
|
||||
|
||||
export type { ValidResponse, InvalidResponse };
|
||||
export type BasicGeneralListResponse = {
|
||||
result: true,
|
||||
nationID: number,
|
||||
generalID: number,
|
||||
column: ['no', 'name', 'npc'],
|
||||
list: Record<string, [number, string, number][]>,
|
||||
nation: Record<string, {
|
||||
nation: number,
|
||||
name: string,
|
||||
color: string,
|
||||
}>
|
||||
}
|
||||
|
||||
|
||||
export type PublicGeneralItem = {
|
||||
no: number,
|
||||
picture: string,
|
||||
imgsvr: 0 | 1,
|
||||
npc: number,
|
||||
age: number,
|
||||
nation: string,
|
||||
specialDomestic: GameObjClassKey,
|
||||
specialWar: GameObjClassKey,
|
||||
personal: GameObjClassKey,
|
||||
name: string,
|
||||
ownerName: string | null,
|
||||
injury: number,
|
||||
leadership: number,
|
||||
lbonus: number,
|
||||
strength: number,
|
||||
intel: number,
|
||||
explevel: number,
|
||||
experienceStr: string,
|
||||
dedicationStr: string,
|
||||
officerLevelStr: string,
|
||||
killturn: number,
|
||||
connect: number,
|
||||
}
|
||||
|
||||
|
||||
export type GeneralListResponse = {
|
||||
result: true,
|
||||
list: [
|
||||
PublicGeneralItem['no'],
|
||||
PublicGeneralItem['picture'], PublicGeneralItem['imgsvr'],
|
||||
PublicGeneralItem['npc'], PublicGeneralItem['age'], PublicGeneralItem['nation'],
|
||||
PublicGeneralItem['specialDomestic'], PublicGeneralItem['specialWar'], PublicGeneralItem['personal'],
|
||||
PublicGeneralItem['name'], PublicGeneralItem['ownerName'],
|
||||
PublicGeneralItem['injury'],
|
||||
PublicGeneralItem['leadership'], PublicGeneralItem['lbonus'], PublicGeneralItem['strength'], PublicGeneralItem['intel'],
|
||||
PublicGeneralItem['explevel'],
|
||||
PublicGeneralItem['experienceStr'], PublicGeneralItem['dedicationStr'], PublicGeneralItem['officerLevelStr'],
|
||||
PublicGeneralItem['killturn'], PublicGeneralItem['connect']
|
||||
][],
|
||||
token?: Record<number, number>,
|
||||
}
|
||||
|
||||
export type NationLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||
export const NationLevelText: Record<NationLevel, string> = {
|
||||
0: '방랑군',
|
||||
1: '호족',
|
||||
2: '군벌',
|
||||
3: '주자사',
|
||||
4: '주목',
|
||||
5: '공',
|
||||
6: '왕',
|
||||
7: '황제',
|
||||
}
|
||||
|
||||
export type CityLevel = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
||||
export const CityLevelText: Record<CityLevel, string> = {
|
||||
1: '수',
|
||||
2: '진',
|
||||
3: '관',
|
||||
4: '이',
|
||||
5: '소',
|
||||
6: '중',
|
||||
7: '대',
|
||||
8: '특'
|
||||
}
|
||||
|
||||
export type NationStaticItem = {
|
||||
nation: number,
|
||||
name: string,
|
||||
color: string,
|
||||
type: string,
|
||||
level: NationLevel,
|
||||
capital: number,
|
||||
gennum: number,
|
||||
power: number,
|
||||
}
|
||||
|
||||
export type NPCGeneralActions =
|
||||
'NPC사망대비' |
|
||||
'귀환' |
|
||||
'금쌀구매' |
|
||||
'출병' |
|
||||
'긴급내정' |
|
||||
'전투준비' |
|
||||
'전방워프' |
|
||||
//'NPC증여'|
|
||||
'NPC헌납' |
|
||||
'징병' |
|
||||
'후방워프' |
|
||||
'전쟁내정' |
|
||||
'소집해제' |
|
||||
'일반내정' |
|
||||
'내정워프';
|
||||
|
||||
export type NPCChiefActions =
|
||||
'불가침제의' |
|
||||
'선전포고' |
|
||||
'천도' |
|
||||
'유저장긴급포상' |
|
||||
'부대전방발령' |
|
||||
'유저장구출발령' |
|
||||
'유저장후방발령' |
|
||||
'부대유저장후방발령' |
|
||||
'유저장전방발령' |
|
||||
'유저장포상' |
|
||||
//'유저장몰수'|
|
||||
'부대구출발령' |
|
||||
'부대후방발령' |
|
||||
'NPC긴급포상' |
|
||||
'NPC구출발령' |
|
||||
'NPC후방발령' |
|
||||
'NPC포상' |
|
||||
'NPC전방발령' |
|
||||
'유저장내정발령' |
|
||||
'NPC내정발령' |
|
||||
'NPC몰수';
|
||||
|
||||
export type NationPolicy = {
|
||||
reqNationGold: number,
|
||||
reqNationRice: number,
|
||||
CombatForce: Record<number, number[]>,
|
||||
SupportForce: number[],
|
||||
DevelopForce: number[],
|
||||
reqHumanWarUrgentGold: number,
|
||||
reqHumanWarUrgentRice: number,
|
||||
reqHumanWarRecommandGold: number,
|
||||
reqHumanWarRecommandRice: number,
|
||||
reqHumanDevelGold: number,
|
||||
reqHumanDevelRice: number,
|
||||
reqNPCWarGold: number,
|
||||
reqNPCWarRice: number,
|
||||
reqNPCDevelGold: number,
|
||||
reqNPCDevelRice: number,
|
||||
minimumResourceActionAmount: number,
|
||||
maximumResourceActionAmount: number,
|
||||
minNPCWarLeadership: number,
|
||||
minWarCrew: number,
|
||||
minNPCRecruitCityPopulation: number,
|
||||
safeRecruitCityPopulationRatio: number,
|
||||
properWarTrainAtmos: number,
|
||||
}
|
||||
|
||||
export type ItemTypeKey = 'horse' | 'weapon' | 'book' | 'item';
|
||||
export const ItemTypeNameMap: Record<ItemTypeKey, string> = {
|
||||
horse: '명마',
|
||||
weapon: '무기',
|
||||
book: '서적',
|
||||
item: '도구',
|
||||
}
|
||||
|
||||
export declare type Colors = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'dark' | 'light';
|
||||
|
||||
export type IDItem<T> = {
|
||||
id: T;
|
||||
};
|
||||
|
||||
export type ToastType = {
|
||||
content: {
|
||||
title?: string,
|
||||
body?: string,
|
||||
},
|
||||
options?: {
|
||||
variant?: Colors,
|
||||
delay?: number,
|
||||
}
|
||||
}
|
||||
|
||||
export const keyScreenMode = 'sam.screenMode';
|
||||
export type ScreenModeType = 'auto' | '500px' | '1000px';
|
||||
|
||||
export declare type ValuesOf<T> = T[keyof T];
|
||||
|
||||
export const NoneValue = 'None' as const;
|
||||
|
||||
export type Optional<Type> = {
|
||||
[Property in keyof Type]+?: Type[Property];
|
||||
};
|
||||
|
||||
export type OptionalFull<Type> = {
|
||||
[Property in keyof Type]: Type[Property] | undefined;
|
||||
};
|
||||
|
||||
export type TurnObj = {
|
||||
action: string;
|
||||
brief: string;
|
||||
arg: Args;
|
||||
};
|
||||
|
||||
|
||||
export type CommandItem = {
|
||||
value: string;
|
||||
title: string;
|
||||
info: string,
|
||||
compensation: number;
|
||||
simpleName: string;
|
||||
possible: boolean;
|
||||
reqArg: boolean;
|
||||
searchText?: string;
|
||||
};
|
||||
|
||||
type diplomacyInfo = {
|
||||
name: string,
|
||||
color?: string,
|
||||
}
|
||||
export type diplomacyState = 0 | 1 | 2 | 7;
|
||||
export const diplomacyStateInfo: Record<diplomacyState, diplomacyInfo> = {
|
||||
0: { name: '교전', color: 'red' },
|
||||
1: { name: '선포중', color: 'magenta' },
|
||||
2: { name: '통상' },
|
||||
7: { name: '불가침', color: 'green' },
|
||||
}
|
||||
|
||||
export const CURRENT_MAP_VERSION = 0 as const;
|
||||
|
||||
//Map
|
||||
type MapCityCompact = [number, number, number, number, number, number];
|
||||
type MapNationCompact = [number, string, string, number];
|
||||
export type MapResult = {
|
||||
result: true,
|
||||
version?: typeof CURRENT_MAP_VERSION,
|
||||
startYear: number,
|
||||
year: number,
|
||||
month: number,
|
||||
cityList: MapCityCompact[],
|
||||
nationList: MapNationCompact[],
|
||||
spyList: Record<number, number>,
|
||||
shownByGeneralList: number[],
|
||||
myCity?: number,
|
||||
myNation?: number,
|
||||
}
|
||||
|
||||
export type CachedMapResult = MapResult & {
|
||||
theme?: string,
|
||||
history?: string[],
|
||||
}
|
||||
|
||||
export type SimpleNationObj = {
|
||||
capital: number,
|
||||
cities: string[],
|
||||
color: string,
|
||||
gennum: number,
|
||||
level: number,
|
||||
name: string,
|
||||
nation: number,
|
||||
power: number,
|
||||
type: GameIActionKey
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
import $ from 'jquery';
|
||||
import { unwrap_any } from '@util/unwrap_any';
|
||||
import axios from 'axios';
|
||||
import { isBrightColor } from "@util/isBrightColor";
|
||||
import { setAxiosXMLHttpRequest } from '@util/setAxiosXMLHttpRequest';
|
||||
import { isString } from 'lodash';
|
||||
import { convertFormData } from '@util/convertFormData';
|
||||
import type { InvalidResponse, NationStaticItem } from '@/defs';
|
||||
import { escapeHtml } from '@/legacy/escapeHtml';
|
||||
import { nl2br } from '@util/nl2br';
|
||||
import { unwrap } from '@util/unwrap';
|
||||
import 'bootstrap';
|
||||
import 'select2/dist/js/select2.full.js'
|
||||
import type { LoadingData } from 'select2';
|
||||
|
||||
declare const permissionLevel: number;
|
||||
let myNationID: number | undefined;
|
||||
|
||||
type LetterNationTarget = {
|
||||
nationID: number,
|
||||
nationName: string,
|
||||
nationColor: string,
|
||||
}
|
||||
type LetterFullTarget = LetterNationTarget & {
|
||||
nationID: number,
|
||||
nationName: string,
|
||||
nationColor: string,
|
||||
generalName: string,
|
||||
generalIcon: string,
|
||||
}
|
||||
|
||||
type LetterState = 'proposed' | 'activated' | 'cancelled' | 'replaced';
|
||||
|
||||
type LetterItem = {
|
||||
no: number,
|
||||
|
||||
src: LetterFullTarget,
|
||||
dest: LetterNationTarget | LetterFullTarget,
|
||||
prev_no: number | null,
|
||||
state: LetterState,
|
||||
state_opt: 'try_destroy_src' | 'try_destroy_dest' | null,
|
||||
brief: string,
|
||||
detail: string,
|
||||
date: string,
|
||||
}
|
||||
|
||||
|
||||
|
||||
type LetterResponse = {
|
||||
result: true,
|
||||
nations: Record<number, NationStaticItem>,
|
||||
letters: Record<number, LetterItem>,
|
||||
myNationID: number,
|
||||
}
|
||||
|
||||
const stateText: Record<LetterState, string> = {
|
||||
'proposed': '제안됨',
|
||||
'activated': '승인됨',
|
||||
'cancelled': '거부됨',
|
||||
'replaced': '대체됨',
|
||||
};
|
||||
|
||||
const stateOptionText: Record<NonNullable<LetterItem['state_opt']>, string> = {
|
||||
'try_destroy_src': '송신측의 파기 요청',
|
||||
'try_destroy_dest': '수신측의 파기 요청',
|
||||
}
|
||||
|
||||
async function submitLetter(e: JQuery.Event): Promise<void> {
|
||||
e.preventDefault();
|
||||
|
||||
const $brief = $('#inputBrief');
|
||||
const $detail = $('#inputDetail');
|
||||
const $prevNo = $('#inputPrevNo');
|
||||
const $destNation = $('#inputDestNation');
|
||||
const brief = $.trim(unwrap_any<string>($brief.val()));
|
||||
const detail = $.trim(unwrap_any<string>($detail.val()));
|
||||
let prevNo = $prevNo.val() as number | undefined;
|
||||
const destNation = unwrap_any<number>($destNation.val());
|
||||
|
||||
if (prevNo !== undefined && prevNo < 1) {
|
||||
prevNo = undefined;
|
||||
}
|
||||
|
||||
console.log(brief);
|
||||
if (!brief) {
|
||||
return;
|
||||
}
|
||||
|
||||
$brief.val('');
|
||||
$detail.val('');
|
||||
|
||||
let result: InvalidResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_diplomacy_send_letter.php',
|
||||
responseType: 'json',
|
||||
method: 'post',
|
||||
data: convertFormData({
|
||||
brief: brief,
|
||||
detail: detail,
|
||||
destNation: destNation,
|
||||
prevNo: prevNo ?? null,
|
||||
})
|
||||
});
|
||||
result = response.data;
|
||||
if (!result.result) {
|
||||
throw result.reason;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(`외교 서신을 보내는데 실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
alert('전송했습니다.');
|
||||
location.reload();
|
||||
}
|
||||
|
||||
async function repondLetter(letterNo: number, isAgree: boolean, reason: string | null): Promise<void> {
|
||||
|
||||
let result: InvalidResponse;
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_diplomacy_respond_letter.php',
|
||||
responseType: 'json',
|
||||
method: 'post',
|
||||
data: convertFormData({
|
||||
letterNo: letterNo,
|
||||
isAgree: isAgree,
|
||||
reason: reason,
|
||||
})
|
||||
});
|
||||
result = response.data;
|
||||
if (!result.result) {
|
||||
throw result.reason;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(`응답을 실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAgree) {
|
||||
alert('승인했습니다.');
|
||||
}
|
||||
else {
|
||||
alert('거부했습니다.');
|
||||
}
|
||||
location.reload();
|
||||
}
|
||||
|
||||
async function rollbackLetter(letterNo: number) {
|
||||
let result: InvalidResponse;
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_diplomacy_rollback_letter.php',
|
||||
responseType: 'json',
|
||||
method: 'post',
|
||||
data: convertFormData({
|
||||
letterNo: letterNo,
|
||||
})
|
||||
});
|
||||
result = response.data;
|
||||
if (!result.result) {
|
||||
throw result.reason;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(`회수를 실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
alert('회수 했습니다.');
|
||||
location.reload();
|
||||
}
|
||||
|
||||
async function destroyLetter(letterNo: number) {
|
||||
let result: InvalidResponse | {
|
||||
result: true,
|
||||
state: LetterState
|
||||
};
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_diplomacy_destroy_letter.php',
|
||||
responseType: 'json',
|
||||
method: 'post',
|
||||
data: convertFormData({
|
||||
letterNo: letterNo,
|
||||
})
|
||||
});
|
||||
result = response.data;
|
||||
if (!result.result) {
|
||||
throw result.reason;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(`회수를 실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.state == 'activated') {
|
||||
alert('파기를 요청 했습니다.');
|
||||
}
|
||||
else {
|
||||
alert('파기 되었습니다.');
|
||||
}
|
||||
|
||||
location.reload();
|
||||
}
|
||||
|
||||
function drawLetter(letterObj: LetterItem) {
|
||||
|
||||
if (letterObj.state == 'cancelled') {
|
||||
//TODO: 취소되거나, 대체된 문서도 보여줄 방법을 찾아볼 것
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(letterObj);
|
||||
|
||||
const $letterFrame = $('#letterTemplate > .letterFrame');
|
||||
|
||||
const srcColorFormat = {
|
||||
'background-color': letterObj.src.nationColor,
|
||||
'color': isBrightColor(letterObj.src.nationColor) ? '#000000' : '#ffffff'
|
||||
};
|
||||
|
||||
const destColorFormat = {
|
||||
'background-color': letterObj.dest.nationColor,
|
||||
'color': isBrightColor(letterObj.dest.nationColor) ? '#000000' : '#ffffff'
|
||||
};
|
||||
|
||||
const targetNation = letterObj.src.nationID == myNationID ? letterObj.dest : letterObj.src;
|
||||
const targetColor = letterObj.src.nationID == myNationID ? destColorFormat : srcColorFormat;
|
||||
|
||||
const $letter = $letterFrame.clone();
|
||||
|
||||
if (letterObj.state == 'replaced') {
|
||||
$letter.hide();
|
||||
}
|
||||
|
||||
$letter.addClass('letterObj')
|
||||
.data('no', letterObj.no)
|
||||
.data('brief', letterObj.brief)
|
||||
.data('detail', letterObj.detail)
|
||||
.attr('id', 'letter_' + letterObj.no);
|
||||
|
||||
$letter.find('.letterHeader').css(targetColor);
|
||||
$letter.find('.letterNationName').text(targetNation.nationName);
|
||||
$letter.find('.letterDate').text(letterObj.date);
|
||||
$letter.find('.letterNo').text('#' + letterObj.no);
|
||||
|
||||
$letter.find('.letterStatus').text(stateText[letterObj.state]);
|
||||
|
||||
if (letterObj.state_opt !== null) {
|
||||
$letter.find('.letterStatusOpt').text(`(${unwrap(stateOptionText[letterObj.state_opt])})`);
|
||||
}
|
||||
if (letterObj.prev_no !== null) {
|
||||
const $showPrev = $(`<a href="#">#${letterObj.prev_no}</a>`);
|
||||
$showPrev.click(function () {
|
||||
$('#letter_' + letterObj.prev_no).toggle();
|
||||
})
|
||||
$letter.find('.letterPrevNo').empty().append($showPrev);
|
||||
}
|
||||
else {
|
||||
$letter.find('.letterPrevNo').text('신규');
|
||||
}
|
||||
$letter.find('.letterBrief').html(nl2br(escapeHtml(letterObj.brief)));
|
||||
$letter.find('.letterDetail').html(nl2br(escapeHtml(letterObj.detail)));
|
||||
|
||||
$letter.find('.letterSrc .signerImg img.generalIcon').attr('src', letterObj.src.generalIcon);
|
||||
$letter.find('.letterSrc .signerNation').text(letterObj.src.nationName).css(srcColorFormat);
|
||||
$letter.find('.letterSrc .signerName').text(letterObj.src.generalName).css(srcColorFormat);
|
||||
|
||||
if ('generalName' in letterObj.dest) {
|
||||
$letter.find('.letterDest .signerImg img.generalIcon').attr('src', letterObj.dest.generalIcon);
|
||||
$letter.find('.letterDest .signerNation').text(letterObj.dest.nationName).css(destColorFormat);
|
||||
$letter.find('.letterDest .signerName').text(letterObj.dest.generalName).css(destColorFormat);
|
||||
}
|
||||
|
||||
if (letterObj.state == 'proposed' && letterObj.src.nationID != myNationID) {
|
||||
$letter.find('.btnAgree').show().click(async function (e) {
|
||||
e.preventDefault();
|
||||
if (!confirm('승인하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
await repondLetter(letterObj.no, true, null);
|
||||
});
|
||||
$letter.find('.btnDisagree').show().click(async function (e) {
|
||||
e.preventDefault();
|
||||
let reason = prompt('거부하시겠습니까? (이유 [최대 50자])');
|
||||
if (reason === null) {
|
||||
return;
|
||||
}
|
||||
reason = reason.substr(0, 50);
|
||||
await repondLetter(letterObj.no, false, reason);
|
||||
});
|
||||
}
|
||||
|
||||
if (letterObj.state == 'proposed' && letterObj.src.nationID == myNationID) {
|
||||
$letter.find('.btnRollback').show().click(async function (e) {
|
||||
e.preventDefault();
|
||||
if (!confirm('회수하시겠습니까?')) {
|
||||
return false;
|
||||
}
|
||||
await rollbackLetter(letterObj.no);
|
||||
});
|
||||
}
|
||||
|
||||
if (letterObj.state == 'activated') {
|
||||
const $btnDestroy = $letter.find('.btnDestroy');
|
||||
if ((letterObj.src.nationID == myNationID && letterObj.state_opt == 'try_destroy_src') ||
|
||||
(letterObj.dest.nationID == myNationID && letterObj.state_opt == 'try_destroy_dest')) {
|
||||
$btnDestroy.show().prop('disabled', true);
|
||||
}
|
||||
else {
|
||||
$btnDestroy.show().click(async function (e) {
|
||||
e.preventDefault();
|
||||
if (!confirm('본 문서를 파기하겠습니까? (상호 동의 필요)')) {
|
||||
return false;
|
||||
}
|
||||
await destroyLetter(letterObj.no);
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$letter.find('.btnRenew').click(function () {
|
||||
const $inputPrevNo = $('#inputPrevNo');
|
||||
$inputPrevNo.val(letterObj.no);
|
||||
$inputPrevNo.trigger('change');
|
||||
})
|
||||
|
||||
|
||||
$('#letters').prepend($letter);
|
||||
}
|
||||
|
||||
function initNewLetterForm(lettersObj: LetterResponse) {
|
||||
console.log(lettersObj);
|
||||
const nationList: (NationStaticItem & { id: number, text: string })[] = [];
|
||||
for (const nation of Object.values(lettersObj.nations)) {
|
||||
nationList.push({
|
||||
...nation,
|
||||
id: nation.nation,
|
||||
text: nation.name,
|
||||
});
|
||||
}
|
||||
|
||||
const $destNation = $('#inputDestNation').select2({
|
||||
theme: 'bootstrap4',
|
||||
placeholder: "",
|
||||
language: "ko",
|
||||
width: '300px',
|
||||
containerCss: {
|
||||
display: "inline-block !important;",
|
||||
color: 'white !important'
|
||||
},
|
||||
data: nationList,
|
||||
templateResult: colorNation,
|
||||
containerCssClass: 'simple-select2-align-center bg-secondary text-secondary',
|
||||
dropdownCssClass: 'simple-select2-align-center bg-secondary text-secondary',
|
||||
});
|
||||
|
||||
|
||||
const prevNoList: {
|
||||
id: number,
|
||||
text: string,
|
||||
nation: null | number,
|
||||
}[] = [{
|
||||
id: 0,
|
||||
text: '-새 문서-',
|
||||
nation: null,
|
||||
}];
|
||||
|
||||
for(const letterObj of Object.values(lettersObj.letters)){
|
||||
if (letterObj.state == 'replaced' || letterObj.state == 'cancelled') {
|
||||
continue;
|
||||
}
|
||||
const targetNation = letterObj.src.nationID == myNationID ? letterObj.dest : letterObj.src;
|
||||
prevNoList.push({
|
||||
id: letterObj.no,
|
||||
text: `#${letterObj.no} <${targetNation.nationName}>`,
|
||||
nation: targetNation.nationID
|
||||
});
|
||||
}
|
||||
|
||||
const $inputPrevNo = $('#inputPrevNo').select2({
|
||||
theme: 'bootstrap4',
|
||||
placeholder: "",
|
||||
language: "ko",
|
||||
width: '300px',
|
||||
containerCss: {
|
||||
display: "inline-block !important;",
|
||||
color: 'white !important'
|
||||
},
|
||||
data: prevNoList,
|
||||
containerCssClass: 'simple-select2-align-center bg-secondary text-secondary',
|
||||
dropdownCssClass: 'simple-select2-align-center bg-secondary text-secondary',
|
||||
});
|
||||
$inputPrevNo.on('change', function () {
|
||||
const data = $inputPrevNo.select2('data')[0] as unknown as typeof prevNoList[0];
|
||||
console.log(data);
|
||||
if (data.nation == null) {
|
||||
$destNation.prop("disabled", false);
|
||||
}
|
||||
else {
|
||||
$destNation.val(data.nation).prop("disabled", true);
|
||||
const $targetLetter = $('#letter_' + data.id);
|
||||
resizeTextarea($('#inputBrief').val($targetLetter.data('brief')));
|
||||
resizeTextarea($('#inputDetail').val($targetLetter.data('detail')));
|
||||
}
|
||||
});
|
||||
|
||||
$('#btnSend').click(submitLetter);
|
||||
|
||||
$('#newLetter').show();
|
||||
}
|
||||
|
||||
function drawLetters(lettersObj: LetterResponse) {
|
||||
myNationID = lettersObj.myNationID;
|
||||
|
||||
if (permissionLevel == 4) {
|
||||
initNewLetterForm(lettersObj);
|
||||
$('.letterActionPlate').show();
|
||||
}
|
||||
|
||||
$('.letterObj').detach();//첫 버전이니까 일괄 삭제 일괄 로드
|
||||
for (const letter of Object.values(lettersObj.letters)) {
|
||||
drawLetter(letter);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLetters(): Promise<LetterResponse> {
|
||||
const response = await axios({
|
||||
url: 'j_diplomacy_get_letter.php',
|
||||
responseType: 'json',
|
||||
method: 'post',
|
||||
data: convertFormData({
|
||||
})
|
||||
});
|
||||
const result: LetterResponse|InvalidResponse = response.data;
|
||||
if(!result.result){
|
||||
throw result.reason;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function colorNation(nationInfo: { id: number, text: string, color?: string } | LoadingData) {
|
||||
if (!('color' in nationInfo)) {
|
||||
return nationInfo.text;
|
||||
}
|
||||
if (!nationInfo.color) {
|
||||
return nationInfo.text;
|
||||
}
|
||||
|
||||
const fgColor = isBrightColor(nationInfo.color) ? '#000000' : '#ffffff';
|
||||
const $nationForm = $('<div>' + nationInfo.text + '</div>').css({
|
||||
'color': fgColor,
|
||||
'background-color': nationInfo.color
|
||||
});
|
||||
return $nationForm;
|
||||
}
|
||||
|
||||
function resizeTextarea($obj: JQuery<HTMLElement>) {
|
||||
$obj.height(1).height($obj.prop('scrollHeight') + 12);
|
||||
}
|
||||
|
||||
$(async function () {
|
||||
setAxiosXMLHttpRequest();
|
||||
$('textarea.autosize').on('keydown keyup', function () {
|
||||
resizeTextarea($(this));
|
||||
})
|
||||
|
||||
try {
|
||||
const letters = await loadLetters();
|
||||
drawLetters(letters);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
if (isString(e)) {
|
||||
alert(e);
|
||||
}
|
||||
else {
|
||||
alert(`실패했습니다.`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { Directive, DirectiveBinding } from 'vue'
|
||||
import Tooltip from 'bootstrap/js/dist/tooltip'
|
||||
|
||||
function resolveTrigger(modifiers: DirectiveBinding['modifiers']): Tooltip.Options['trigger'] {
|
||||
if (modifiers.manual) {
|
||||
return 'manual'
|
||||
}
|
||||
|
||||
const trigger: string[] = []
|
||||
|
||||
if (modifiers.click) {
|
||||
trigger.push('click')
|
||||
}
|
||||
|
||||
if (modifiers.hover) {
|
||||
trigger.push('hover')
|
||||
}
|
||||
|
||||
if (modifiers.focus) {
|
||||
trigger.push('focus')
|
||||
}
|
||||
|
||||
if (trigger.length > 0) {
|
||||
return trigger.join(' ') as Tooltip.Options['trigger']
|
||||
}
|
||||
|
||||
return 'hover focus'
|
||||
}
|
||||
|
||||
function resolvePlacement(modifiers: DirectiveBinding['modifiers']): Tooltip.Options['placement'] {
|
||||
if (modifiers.left) {
|
||||
return 'left'
|
||||
}
|
||||
|
||||
if (modifiers.right) {
|
||||
return 'right'
|
||||
}
|
||||
|
||||
if (modifiers.bottom) {
|
||||
return 'bottom'
|
||||
}
|
||||
|
||||
return 'top'
|
||||
}
|
||||
|
||||
function resolveDelay(values: TooltipOptions): Tooltip.Options['delay'] {
|
||||
if (values?.delay) {
|
||||
return values.delay
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
type TooltipOptions = {
|
||||
delay?: number,
|
||||
class?: string
|
||||
} | undefined;
|
||||
|
||||
const vMyTooltip: Directive<HTMLElement, TooltipOptions> = {
|
||||
beforeMount(el, binding) {
|
||||
el.setAttribute('data-bs-toogle', 'tooltip')
|
||||
|
||||
const isHtml = /<("[^"]*"|'[^']*'|[^'">])*>/.test(el.title)
|
||||
const trigger = resolveTrigger(binding.modifiers)
|
||||
const placement = resolvePlacement(binding.modifiers)
|
||||
const delay = resolveDelay(binding.value)
|
||||
|
||||
const options: Partial<Tooltip.Options> = {
|
||||
trigger,
|
||||
placement,
|
||||
delay,
|
||||
html: isHtml,
|
||||
}
|
||||
const customClass = binding.value?.class;
|
||||
if (customClass) {
|
||||
options.customClass = customClass;
|
||||
}
|
||||
|
||||
new Tooltip(el, options)
|
||||
},
|
||||
updated(el) {
|
||||
const title = el.getAttribute('title')
|
||||
|
||||
if (title !== '') {
|
||||
const instance = Tooltip.getInstance(el)
|
||||
instance?.hide()
|
||||
el.setAttribute('data-bs-original-title', title || '')
|
||||
el.setAttribute('title', '')
|
||||
}
|
||||
},
|
||||
unmounted(el) {
|
||||
const instance = Tooltip.getInstance(el)
|
||||
instance?.dispose()
|
||||
},
|
||||
}
|
||||
|
||||
export default vMyTooltip
|
||||
@@ -0,0 +1,677 @@
|
||||
|
||||
import $ from 'jquery';
|
||||
import { unwrap_any } from '@util/unwrap_any';
|
||||
|
||||
type UserItem = {
|
||||
val: string,
|
||||
name: string,
|
||||
city: string,
|
||||
|
||||
|
||||
태수: boolean,
|
||||
군사: boolean,
|
||||
종사: boolean,
|
||||
is수뇌: boolean,
|
||||
$city: CityItem,
|
||||
$user: JQuery<HTMLElement>,
|
||||
}
|
||||
|
||||
type CityLevel = '특' | '대' | '중' | '소' | '이' | '진' | '관' | '수'
|
||||
|
||||
type OfficerItem = {
|
||||
preserved: boolean,
|
||||
$obj: JQuery<HTMLElement>
|
||||
}
|
||||
|
||||
type CityItem = {
|
||||
지역: string,
|
||||
규모: CityLevel,
|
||||
이름: string,
|
||||
|
||||
val: string,
|
||||
users: JQuery<HTMLElement>,
|
||||
userCnt: number,
|
||||
obj: JQuery<HTMLElement>,
|
||||
|
||||
태수: OfficerItem,
|
||||
군사: OfficerItem,
|
||||
종사: OfficerItem,
|
||||
|
||||
warn주민: boolean,
|
||||
warn농업: boolean,
|
||||
warn상업: boolean,
|
||||
warn치안: boolean,
|
||||
warn수비: boolean,
|
||||
warn성벽: boolean,
|
||||
|
||||
$주민: JQuery<HTMLElement>,
|
||||
$인구율: JQuery<HTMLElement>,
|
||||
$농업: JQuery<HTMLElement>,
|
||||
$상업: JQuery<HTMLElement>,
|
||||
$치안: JQuery<HTMLElement>,
|
||||
$수비: JQuery<HTMLElement>,
|
||||
$성벽: JQuery<HTMLElement>,
|
||||
|
||||
주민: number,
|
||||
농업: number,
|
||||
상업: number,
|
||||
치안: number,
|
||||
수비: number,
|
||||
성벽: number,
|
||||
|
||||
max주민: number,
|
||||
max농업: number,
|
||||
max상업: number,
|
||||
max치안: number,
|
||||
max수비: number,
|
||||
max성벽: number,
|
||||
|
||||
remain주민: number,
|
||||
remain농업: number,
|
||||
remain상업: number,
|
||||
remain치안: number,
|
||||
remain수비: number,
|
||||
remain성벽: number,
|
||||
};
|
||||
|
||||
type OfficerSelector = '태수' | '군사' | '종사';
|
||||
|
||||
|
||||
const cityList: Record<string, CityItem> = {};
|
||||
const userList: Record<string, UserItem> = {};
|
||||
|
||||
$(function () {
|
||||
let basicPath = document.location.pathname;
|
||||
basicPath = basicPath.substring(0, basicPath.lastIndexOf('/')) + '/';
|
||||
|
||||
const mergeSort = function <T>(arr: T[], cmpFunc: (lhs: T, rhs: T) => number) {
|
||||
const merge = function (left: T[], right: T[]): T[] {
|
||||
const retVal = [];
|
||||
|
||||
let leftIdx = 0;
|
||||
let rightIdx = 0;
|
||||
|
||||
|
||||
|
||||
while (leftIdx < left.length && rightIdx < right.length) {
|
||||
const cmpVal = cmpFunc(left[leftIdx], right[rightIdx]);
|
||||
|
||||
if (cmpVal <= 0) {
|
||||
retVal.push(left[leftIdx]);
|
||||
leftIdx++;
|
||||
}
|
||||
else {
|
||||
retVal.push(right[rightIdx]);
|
||||
rightIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
return retVal.concat(left.slice(leftIdx)).concat(right.slice(rightIdx));
|
||||
};
|
||||
|
||||
const _mergeSort = function (arr: T[]): T[] {
|
||||
if (arr.length < 2) {
|
||||
return arr;
|
||||
}
|
||||
|
||||
const middle = Math.floor(arr.length / 2);
|
||||
|
||||
const left = arr.slice(0, middle);
|
||||
const right = arr.slice(middle);
|
||||
|
||||
return merge(_mergeSort(left), _mergeSort(right));
|
||||
};
|
||||
|
||||
return _mergeSort(arr);
|
||||
|
||||
};
|
||||
|
||||
const loadDuty = function () {
|
||||
|
||||
try {
|
||||
$('.for_duty').remove();
|
||||
void $.get(basicPath + 'b_myBossInfo.php', function (rawData) {
|
||||
const $html = $(rawData);
|
||||
|
||||
const $tmpTable = $html.filter('#officer_list').eq(0);
|
||||
|
||||
const $selects = $tmpTable.find("select");
|
||||
if ($selects.length == 0) {
|
||||
alert("수뇌가 아닙니다!");
|
||||
return false;
|
||||
}
|
||||
|
||||
const setUserAvailable = function ($userList: JQuery<HTMLElement>, typeName: OfficerSelector) {
|
||||
$userList.each(function () {
|
||||
const $this = $(this);
|
||||
|
||||
const val = unwrap_any<string>($this.val());
|
||||
let name = $.trim($this.text());
|
||||
|
||||
for (let i = name.length - 1; i > 0; i--) {
|
||||
if (name[i] == '【') {
|
||||
name = $.trim(name.substr(0, i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (val == '0') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (userList[name] !== undefined) {
|
||||
userList[name].val = val;
|
||||
userList[name][typeName] = true;
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
const setCityAvailiable = function ($cityList: JQuery<HTMLElement>, typeName: OfficerSelector) {
|
||||
$cityList.each(function () {
|
||||
|
||||
const $this = $(this);
|
||||
|
||||
const val = $.trim(unwrap_any<string>($this.val()));
|
||||
const name = $.trim($this.text());
|
||||
|
||||
cityList[name].val = val;
|
||||
cityList[name][typeName].preserved = true;
|
||||
});
|
||||
}
|
||||
|
||||
for (const cityInfo of Object.values(cityList)) {
|
||||
cityInfo.태수.preserved = false;
|
||||
cityInfo.군사.preserved = false;
|
||||
cityInfo.종사.preserved = false;
|
||||
}
|
||||
|
||||
$.each(userList, function (idx, userInfo) {
|
||||
userInfo.태수 = false;
|
||||
userInfo.군사 = false;
|
||||
userInfo.종사 = false;
|
||||
});
|
||||
|
||||
setUserAvailable($selects.eq(1).find("option"), "태수");
|
||||
setUserAvailable($selects.eq(3).find("option"), "군사");
|
||||
setUserAvailable($selects.eq(5).find("option"), "종사");
|
||||
|
||||
setCityAvailiable($selects.eq(0).find("option"), "태수");
|
||||
setCityAvailiable($selects.eq(2).find("option"), "군사");
|
||||
setCityAvailiable($selects.eq(4).find("option"), "종사");
|
||||
|
||||
|
||||
$.each(cityList, function (idx, cityInfo) {
|
||||
|
||||
//console.log(cityInfo.users.children());
|
||||
|
||||
cityInfo.users.children().each(function () {
|
||||
//console.log(this);
|
||||
const $this = $(this);
|
||||
|
||||
const username = $this.data('username');
|
||||
|
||||
const userInfo = userList[username];
|
||||
if (!userInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (userInfo.val == '-1') {
|
||||
return;
|
||||
}
|
||||
|
||||
const $name = $this.find('.nameplate');
|
||||
|
||||
$name.append('<br class="for_duty">');
|
||||
|
||||
|
||||
const addBtn = function ($name: JQuery<HTMLElement>, cityInfo: CityItem, userInfo: UserItem, level: number, typeName: OfficerSelector) {
|
||||
|
||||
const enabled = cityInfo[typeName] && userInfo[typeName];
|
||||
const cityVal = cityInfo.val;
|
||||
const $btn = $('<button type="button">' + typeName.substr(0, 1) + '</button>');
|
||||
$btn.addClass(`mode_${level}`);
|
||||
$btn.addClass('for_duty');
|
||||
|
||||
if (!enabled) {
|
||||
$btn.prop('disabled', true);
|
||||
$btn.css('background', 'transparent');
|
||||
$btn.css('border', '0');
|
||||
}
|
||||
else {
|
||||
if (userInfo.is수뇌) {
|
||||
$btn.css('color', 'red');
|
||||
}
|
||||
}
|
||||
|
||||
$btn.css('padding', '1px 4px');
|
||||
$btn.css('margin', '0');
|
||||
|
||||
$btn.click(function () {
|
||||
if (userInfo.is수뇌) {
|
||||
if (!confirm('수뇌입니다. 임명할까요?')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void $.post(basicPath + 'j_myBossInfo.php', {
|
||||
destCityID: cityVal,
|
||||
destGeneralID: userInfo.val,
|
||||
officerLevel: level,
|
||||
action: '임명'
|
||||
}, function () {
|
||||
cityInfo[typeName].preserved = false;
|
||||
const $target = cityInfo.users.find(`.mode_${level}`);
|
||||
$target.prop('disabled', true);
|
||||
$target.css('background', 'transparent');
|
||||
$target.css('border', '0');
|
||||
$target.css('color', '');
|
||||
|
||||
cityInfo[typeName].$obj.html(userInfo.name);
|
||||
});
|
||||
});
|
||||
|
||||
//console.log($btn);
|
||||
$name.append($btn);
|
||||
};
|
||||
|
||||
addBtn($name, cityInfo, userInfo, 4, '태수');
|
||||
addBtn($name, cityInfo, userInfo, 3, '군사');
|
||||
addBtn($name, cityInfo, userInfo, 2, '종사');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
catch (a) {
|
||||
console.log(a);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const loadUser = function () {
|
||||
$.each(cityList, function (idx, val) {
|
||||
if (typeof val.users == "undefined") {
|
||||
val.obj.append('<tr><td colspan="10"><table align="center" class="tb_layout cityUser bg0">' +
|
||||
'<thead><tr>' +
|
||||
'<td width="100" align="center" class="bg1">이 름</td><td width="100" align="center" class="bg1">통무지</td><td width="100" align="center" class="bg1">부 대</td><td width="60" align="center" class="bg1">자 금</td>' +
|
||||
'<td width="60" align="center" class="bg1">군 량</td><td width="30" align="center" class="bg1">守</td><td width="60" align="center" class="bg1">병 종</td>' +
|
||||
'<td width="60" align="center" class="bg1">병 사</td><td width="50" align="center" class="bg1">훈련</td><td width="50" align="center" class="bg1">사기</td><td width="150" align="center" class="bg1">명 령</td>' +
|
||||
'<td width="60" align="center" class="bg1">삭턴</td><td width="60" align="center" class="bg1">턴</td>' +
|
||||
'</tr></thead>' +
|
||||
'<tbody class="cityUserBody"></tbody></table></td></tr>');
|
||||
|
||||
val.users = val.obj.find(".cityUserBody");
|
||||
}
|
||||
else {
|
||||
val.users.html("");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
void $.get(basicPath + 'b_genList.php', function (rawData) {
|
||||
const $helper = $('#helper_genlist');
|
||||
$helper.html('').append($.parseHTML(rawData));
|
||||
|
||||
const tmpUsers = $('#general_list tbody tr');
|
||||
|
||||
tmpUsers.each(function () {
|
||||
const $this = $(this);
|
||||
|
||||
const $city = $this.children('.i_city');
|
||||
$city.remove();
|
||||
const cityName = $.trim($city.text());
|
||||
|
||||
const $name = $this.children('.i_name');
|
||||
$name.addClass('nameplate');
|
||||
|
||||
const name = $name.find('.t_name').text();
|
||||
|
||||
const $work = $this.children('.i_action');
|
||||
|
||||
const cityInfo = cityList[cityName];
|
||||
if (typeof cityInfo == 'undefined') {
|
||||
return;
|
||||
}
|
||||
if (cityInfo.warn주민) $work.html($work.html().split('정착 장려').join('<span style="color:yellow;">정착 장려</span>'));
|
||||
if (cityInfo.warn농업) $work.html($work.html().split('농지 개간').join('<span style="color:yellow;">농지 개간</span>'));
|
||||
if (cityInfo.warn상업) $work.html($work.html().split('상업 투자').join('<span style="color:yellow;">상업 투자</span>'));
|
||||
if (cityInfo.warn치안) $work.html($work.html().split('치안 강화').join('<span style="color:yellow;">치안 강화</span>'));
|
||||
if (cityInfo.warn수비) $work.html($work.html().split('수비 강화').join('<span style="color:yellow;">수비 강화</span>'));
|
||||
if (cityInfo.warn성벽) $work.html($work.html().split('성벽 보수').join('<span style="color:yellow;">성벽 보수</span>'));
|
||||
|
||||
const $stat = $this.children('.i_stat');
|
||||
const stat = $stat.text();
|
||||
|
||||
const is수뇌 = stat.indexOf('+') >= 0;
|
||||
|
||||
$this.data('username', name);
|
||||
|
||||
if (cityList[cityName].태수.$obj.text() == name) {
|
||||
cityList[cityName].태수.$obj.html(`<span style="color:lightgreen">${name}</span>`);
|
||||
}
|
||||
if (cityList[cityName].군사.$obj.text() == name) {
|
||||
cityList[cityName].군사.$obj.html(`<span style="color:lightgreen">${name}</span>`);
|
||||
}
|
||||
if (cityList[cityName].종사.$obj.text() == name) {
|
||||
cityList[cityName].종사.$obj.html(`<span style="color:lightgreen">${name}</span>`);
|
||||
}
|
||||
|
||||
userList[name] = {
|
||||
$city: cityInfo,
|
||||
city: cityName,
|
||||
$user: $this,
|
||||
name: name,
|
||||
val: '-1',
|
||||
태수: false,
|
||||
군사: false,
|
||||
종사: false,
|
||||
is수뇌: is수뇌
|
||||
};
|
||||
|
||||
if (cityList[cityName]) {
|
||||
cityList[cityName].users.append($this);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
if ($("#loadDutyBtn").length == 0) {
|
||||
|
||||
const $onBossList = $('<button id="loadDutyBtn">인사부 연동</button>');
|
||||
$onBossList.click(function () {
|
||||
loadDuty();
|
||||
return false;
|
||||
});
|
||||
|
||||
$('form').append($onBossList);
|
||||
}
|
||||
|
||||
$('#by_users').show();
|
||||
};
|
||||
|
||||
const mainFunc = function () {
|
||||
//대상 추출
|
||||
|
||||
$("form").each(function () {
|
||||
const $this = $(this);
|
||||
$this.attr('name', 'p' + $this.attr('name'));
|
||||
});
|
||||
|
||||
$("table").each(function () {
|
||||
const $this = $(this);
|
||||
|
||||
|
||||
if ($this.attr('class') == 'tb_layout bg2') {
|
||||
$this.addClass('cityInfo');
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
const cityInfo: CityItem = {
|
||||
태수: {},
|
||||
군사: {},
|
||||
종사: {},
|
||||
} as CityItem;
|
||||
|
||||
//이름 추출
|
||||
{
|
||||
const titleText = $this.find('tr:eq(0)>td:eq(0)').text();
|
||||
const loc0 = titleText.indexOf("【");
|
||||
const loc1 = titleText.indexOf("|");
|
||||
const loc2 = titleText.indexOf("】");
|
||||
|
||||
const cityLoc = $.trim(titleText.substring(loc0 + 1, loc1));
|
||||
const citySize = $.trim(titleText.substring(loc1 + 1, loc2));
|
||||
let cityName = $.trim(titleText.substring(loc2 + 1));
|
||||
|
||||
cityName = cityName.replace("[", "");
|
||||
cityName = cityName.replace("]", "");
|
||||
|
||||
$this.data('cityname', cityName);
|
||||
|
||||
cityInfo.지역 = cityLoc;
|
||||
cityInfo.규모 = citySize as CityLevel;
|
||||
cityInfo.이름 = cityName;
|
||||
|
||||
cityInfo.val = '-1';
|
||||
|
||||
cityInfo.태수.preserved = false;
|
||||
cityInfo.군사.preserved = false;
|
||||
cityInfo.종사.preserved = false;
|
||||
}
|
||||
|
||||
//주민, 농상치성수
|
||||
|
||||
|
||||
{
|
||||
cityInfo.$주민 = $this.find('.pop-value');
|
||||
cityInfo.$인구율 = $this.find('.pop-prop-value');
|
||||
cityInfo.$농업 = $this.find('.agri-value');
|
||||
cityInfo.$상업 = $this.find('.comm-value');
|
||||
cityInfo.$치안 = $this.find('.secu-value');
|
||||
cityInfo.$수비 = $this.find('.def-value');
|
||||
cityInfo.$성벽 = $this.find('.wall-value');
|
||||
|
||||
let tmpVal;
|
||||
|
||||
tmpVal = cityInfo.$주민.text().split('/');
|
||||
cityInfo.주민 = parseInt(tmpVal[0]);
|
||||
cityInfo.max주민 = parseInt(tmpVal[1]);
|
||||
|
||||
tmpVal = cityInfo.$농업.text().split('/');
|
||||
cityInfo.농업 = parseInt(tmpVal[0]);
|
||||
cityInfo.max농업 = parseInt(tmpVal[1]);
|
||||
|
||||
tmpVal = cityInfo.$상업.text().split('/');
|
||||
cityInfo.상업 = parseInt(tmpVal[0]);
|
||||
cityInfo.max상업 = parseInt(tmpVal[1]);
|
||||
|
||||
tmpVal = cityInfo.$치안.text().split('/');
|
||||
cityInfo.치안 = parseInt(tmpVal[0]);
|
||||
cityInfo.max치안 = parseInt(tmpVal[1]);
|
||||
|
||||
tmpVal = cityInfo.$수비.text().split('/');
|
||||
cityInfo.수비 = parseInt(tmpVal[0]);
|
||||
cityInfo.max수비 = parseInt(tmpVal[1]);
|
||||
|
||||
tmpVal = cityInfo.$성벽.text().split('/');
|
||||
cityInfo.성벽 = parseInt(tmpVal[0]);
|
||||
cityInfo.max성벽 = parseInt(tmpVal[1]);
|
||||
|
||||
if (cityInfo.주민 > cityInfo.max주민 * 0.9) {
|
||||
cityInfo.$주민.css('color', 'lightgreen');
|
||||
cityInfo.$인구율.css('color', 'lightgreen');
|
||||
}
|
||||
else if (cityInfo.주민 > cityInfo.max주민 * 0.7) {
|
||||
cityInfo.$주민.css('color', 'yellow');
|
||||
cityInfo.$인구율.css('color', 'yellow');
|
||||
}
|
||||
else {
|
||||
cityInfo.$주민.css('color', 'orangered');
|
||||
cityInfo.$인구율.css('color', 'orangered');
|
||||
}
|
||||
|
||||
if (cityInfo.농업 > cityInfo.max농업 * 0.8) { cityInfo.$농업.css('color', 'lightgreen'); }
|
||||
else if (cityInfo.농업 > cityInfo.max농업 * 0.4) { cityInfo.$농업.css('color', 'yellow'); }
|
||||
else { cityInfo.$농업.css('color', 'orangered'); }
|
||||
|
||||
if (cityInfo.상업 > cityInfo.max상업 * 0.8) { cityInfo.$상업.css('color', 'lightgreen'); }
|
||||
else if (cityInfo.상업 > cityInfo.max상업 * 0.4) { cityInfo.$상업.css('color', 'yellow'); }
|
||||
else { cityInfo.$상업.css('color', 'orangered'); }
|
||||
|
||||
if (cityInfo.치안 > cityInfo.max치안 * 0.8) { cityInfo.$치안.css('color', 'lightgreen'); }
|
||||
else if (cityInfo.치안 > cityInfo.max치안 * 0.4) { cityInfo.$치안.css('color', 'yellow'); }
|
||||
else { cityInfo.$치안.css('color', 'orangered'); }
|
||||
|
||||
if (cityInfo.수비 > cityInfo.max수비 * 0.6) { cityInfo.$수비.css('color', 'lightgreen'); }
|
||||
else if (cityInfo.수비 > cityInfo.max수비 * 0.3) { cityInfo.$수비.css('color', 'yellow'); }
|
||||
else { cityInfo.$수비.css('color', 'orangered'); }
|
||||
|
||||
if (cityInfo.성벽 > cityInfo.max성벽 * 0.6) { cityInfo.$성벽.css('color', 'lightgreen'); }
|
||||
else if (cityInfo.성벽 > cityInfo.max성벽 * 0.3) { cityInfo.$성벽.css('color', 'yellow'); }
|
||||
else { cityInfo.$성벽.css('color', 'orangered'); }
|
||||
|
||||
|
||||
cityInfo.remain주민 = cityInfo.주민 - cityInfo.max주민;
|
||||
cityInfo.remain농업 = cityInfo.농업 - cityInfo.max농업;
|
||||
cityInfo.remain상업 = cityInfo.상업 - cityInfo.max상업;
|
||||
cityInfo.remain치안 = cityInfo.치안 - cityInfo.max치안;
|
||||
cityInfo.remain수비 = cityInfo.수비 - cityInfo.max수비;
|
||||
cityInfo.remain성벽 = cityInfo.성벽 - cityInfo.max성벽;
|
||||
|
||||
cityInfo.warn주민 = false;
|
||||
cityInfo.warn농업 = false;
|
||||
cityInfo.warn상업 = false;
|
||||
cityInfo.warn치안 = false;
|
||||
cityInfo.warn수비 = false;
|
||||
cityInfo.warn성벽 = false;
|
||||
|
||||
if (cityInfo.remain주민 > -10 * 2000) cityInfo.warn주민 = true;
|
||||
if (cityInfo.주민 > 0.92 * cityInfo.max주민) cityInfo.warn주민 = true;
|
||||
if (cityInfo.remain농업 > -10 * 100) cityInfo.warn농업 = true;
|
||||
if (cityInfo.remain상업 > -10 * 100) cityInfo.warn상업 = true;
|
||||
if (cityInfo.remain치안 > -10 * 100) cityInfo.warn치안 = true;
|
||||
if (cityInfo.remain수비 > -10 * 70) cityInfo.warn수비 = true;
|
||||
if (cityInfo.remain성벽 > -10 * 70) cityInfo.warn성벽 = true;
|
||||
|
||||
if (cityInfo.warn농업) cityInfo.$농업.append('<span class="remain" style="color:yellow;">[' + cityInfo.remain농업 + ']</span>');
|
||||
if (cityInfo.warn상업) cityInfo.$상업.append('<span class="remain" style="color:yellow;">[' + cityInfo.remain상업 + ']</span>');
|
||||
if (cityInfo.warn치안) cityInfo.$치안.append('<span class="remain" style="color:yellow;">[' + cityInfo.remain치안 + ']</span>');
|
||||
if (cityInfo.warn수비) cityInfo.$수비.append('<span class="remain" style="color:yellow;">[' + cityInfo.remain수비 + ']</span>');
|
||||
if (cityInfo.warn성벽) cityInfo.$성벽.append('<span class="remain" style="color:yellow;">[' + cityInfo.remain성벽 + ']</span>');
|
||||
|
||||
}
|
||||
|
||||
//태수,군사,종사
|
||||
{
|
||||
cityInfo.태수.$obj = $this.find('.officer-4-value');
|
||||
cityInfo.군사.$obj = $this.find('.officer-3-value');
|
||||
cityInfo.종사.$obj = $this.find('.officer-2-valuet');
|
||||
}
|
||||
|
||||
//기타
|
||||
{
|
||||
|
||||
cityInfo.userCnt = $this.find('.city-generals').text().split(',').length - 1;
|
||||
}
|
||||
|
||||
cityInfo.obj = $this;
|
||||
cityList[cityInfo.이름] = cityInfo as CityItem;
|
||||
});
|
||||
|
||||
|
||||
const $onGenList = $('<button type="button">암행부 연동</button>');
|
||||
$onGenList.click(function () {
|
||||
loadUser();
|
||||
return false;
|
||||
});
|
||||
$('form').append($onGenList);
|
||||
|
||||
|
||||
$('table:eq(0) tr:last').after('<tr><td id="sort_more"></td></tr>');
|
||||
|
||||
|
||||
const $sort_more = $('#sort_more');
|
||||
$sort_more.html('재 정렬 순서 :');
|
||||
|
||||
const sortIt = function (callback: (lhs: CityItem, rhs: CityItem) => number) {
|
||||
let arCity: CityItem[] = [];
|
||||
$('.cityInfo').each(function () {
|
||||
const $this = $(this);
|
||||
const cityName = $this.data('cityname');
|
||||
|
||||
const cityInfo = cityList[cityName];
|
||||
arCity.push(cityInfo);
|
||||
});
|
||||
|
||||
arCity = mergeSort(arCity, callback);
|
||||
//console.log(arCity);
|
||||
|
||||
const $anchor = $('.anchor');
|
||||
//console.log($anchor);
|
||||
|
||||
$('body > br').remove();
|
||||
|
||||
$('.cityInfo').detach();
|
||||
|
||||
$.each(arCity, function (idx, val) {
|
||||
$anchor.before('<br>');
|
||||
$anchor.before(val.obj);
|
||||
});
|
||||
$anchor.before('<br>');
|
||||
|
||||
};
|
||||
|
||||
let $btn: JQuery<HTMLElement>;
|
||||
|
||||
$btn = $('<button type="button">도시명</button>').click(function () {
|
||||
sortIt(function (a, b) {
|
||||
return a.이름.localeCompare(b.이름);
|
||||
});
|
||||
});
|
||||
$sort_more.append($btn);
|
||||
|
||||
$btn = $('<button type="button">인구율</button>').click(function () {
|
||||
sortIt(function (a, b) {
|
||||
return 1.0 * a.주민 / a.max주민 - 1.0 * b.주민 / b.max주민;
|
||||
});
|
||||
});
|
||||
$sort_more.append($btn);
|
||||
|
||||
$btn = $('<button type="button">남은 주민</button>').click(function () {
|
||||
sortIt(function (a, b) {
|
||||
return a.remain주민 - b.remain주민;
|
||||
});
|
||||
});
|
||||
$sort_more.append($btn);
|
||||
|
||||
$btn = $('<button type="button">남은 농업</button>').click(function () {
|
||||
sortIt(function (a, b) {
|
||||
return a.remain농업 - b.remain농업;
|
||||
});
|
||||
});
|
||||
$sort_more.append($btn);
|
||||
|
||||
$btn = $('<button type="button">남은 상업</button>').click(function () {
|
||||
sortIt(function (a, b) {
|
||||
return a.remain상업 - b.remain상업;
|
||||
});
|
||||
});
|
||||
$sort_more.append($btn);
|
||||
|
||||
$btn = $('<button type="button">남은 치안</button>').click(function () {
|
||||
sortIt(function (a, b) {
|
||||
return a.remain치안 - b.remain치안;
|
||||
});
|
||||
});
|
||||
$sort_more.append($btn);
|
||||
|
||||
$btn = $('<button type="button">남은 수비</button>').click(function () {
|
||||
sortIt(function (a, b) {
|
||||
return a.remain수비 - b.remain수비;
|
||||
});
|
||||
});
|
||||
$sort_more.append($btn);
|
||||
|
||||
$btn = $('<button type="button">남은 성벽</button>').click(function () {
|
||||
sortIt(function (a, b) {
|
||||
return a.remain성벽 - b.remain성벽;
|
||||
});
|
||||
});
|
||||
$sort_more.append($btn);
|
||||
|
||||
$btn = $('<button type="button">배치 장수 수</button>').click(function () {
|
||||
sortIt(function (a, b) {
|
||||
return b.userCnt - a.userCnt;
|
||||
});
|
||||
});
|
||||
$sort_more.append($btn);
|
||||
};
|
||||
|
||||
mainFunc();
|
||||
});
|
||||
@@ -0,0 +1,294 @@
|
||||
import $ from 'jquery';
|
||||
import { unwrap } from '@util/unwrap';
|
||||
import axios from 'axios';
|
||||
import { setAxiosXMLHttpRequest } from '@util/setAxiosXMLHttpRequest';
|
||||
import { getNpcColor } from './common_legacy';
|
||||
|
||||
declare const killturn: number;
|
||||
declare const autorun_user: undefined|null|{
|
||||
limit_minutes: number;
|
||||
options: Record<string, number>;
|
||||
};
|
||||
declare const turnterm: number;
|
||||
|
||||
type KingdomGeneral = {
|
||||
html: JQuery<HTMLElement>,
|
||||
장수명: string
|
||||
국가: string
|
||||
벌점: number,
|
||||
통: number,
|
||||
무: number,
|
||||
지: number,
|
||||
삭턴: number,
|
||||
종류: UserType,
|
||||
NPC: number,
|
||||
}
|
||||
|
||||
type UserType = "통" | "무" | "지" | "만능" | "평범" | "무능" | "무지";
|
||||
|
||||
type NationInfo = {
|
||||
[v in UserType]: KingdomGeneral[]
|
||||
}
|
||||
|
||||
$(function () {
|
||||
setAxiosXMLHttpRequest();
|
||||
|
||||
const $userFrame: JQuery<HTMLElement> = $('<div id="on_mover" style="position:absolute;">' +
|
||||
'<table class="tb_layout bg0" style="width:100%;"><thead><tr>' +
|
||||
'<td width="64" align="center" class="bg1">얼 굴</td>' +
|
||||
'<td width="100" align="center" class="bg1">이 름</td>' +
|
||||
'<td width="50" align="center" class="bg1">연령</td>' +
|
||||
'<td width="50" align="center" class="bg1">성격</td>' +
|
||||
'<td width="90" align="center" class="bg1">특기</td>' +
|
||||
'<td width="50" align="center" class="bg1">레 벨</td>' +
|
||||
'<td width="100" align="center" class="bg1">국 가</td>' +
|
||||
'<td width="60" align="center" class="bg1">명 성</td>' +
|
||||
'<td width="60" align="center" class="bg1">계 급</td>' +
|
||||
'<td width="80" align="center" class="bg1">관 직</td>' +
|
||||
'<td width="45" align="center" class="bg1">통솔</td>' +
|
||||
'<td width="45" align="center" class="bg1">무력</td>' +
|
||||
'<td width="45" align="center" class="bg1">지력</td>' +
|
||||
'<td width="45" align="center" class="bg1">삭턴</td>' +
|
||||
'<td width="84" align="center" class="bg1">벌점</td>' +
|
||||
'</tr></thead><tbody class="content"></tbody></table></div>');
|
||||
$userFrame.find('thead td');
|
||||
$userFrame.css('width', '1000px').css('margin', '0').css('padding', '0').css('left', '50%').css('margin-left', '-500px');
|
||||
$userFrame.css('box-shadow', '0px 0px 7px 3px rgba(255,255,255,50)');
|
||||
$userFrame.hide();
|
||||
|
||||
const 국가테이블 = $('table:gt(0):lt(-2)');
|
||||
|
||||
const getUserType = function (통: number, 무: number, 지: number): UserType {
|
||||
//const 총 = 통 + 무 + 지;
|
||||
if (통 < 40) {
|
||||
if (무 + 지 < 40) {
|
||||
return "무능";
|
||||
}
|
||||
return "무지";
|
||||
}
|
||||
|
||||
const 최대능력치 = Math.max(통, 무, 지);
|
||||
const 능력치2합 = Math.min(통 + 무, 무 + 지, 지 + 통);
|
||||
if (최대능력치 >= 70 && 능력치2합 >= 최대능력치 * 1.7) {
|
||||
return "만능";
|
||||
}
|
||||
|
||||
if (무 >= 60 && 지 < 무 * 0.8) {
|
||||
return "무";
|
||||
}
|
||||
if (지 >= 60 && 무 < 지 * 0.8) {
|
||||
return "지";
|
||||
}
|
||||
if (통 >= 60 && 무 + 지 < 통) {
|
||||
return "통";
|
||||
}
|
||||
|
||||
return "평범";
|
||||
};
|
||||
|
||||
function formatScore(x: number) {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
const runAnalysis = async function () {
|
||||
let realKillturn = killturn;
|
||||
if(autorun_user && autorun_user.limit_minutes){
|
||||
realKillturn -= autorun_user.limit_minutes / turnterm;
|
||||
}
|
||||
const $content = $('#on_mover .content');
|
||||
try {
|
||||
const response = await axios({ url: 'a_genList.php', method: 'get', responseType: 'text' });
|
||||
const rawData = response.data; const $html = $(rawData);
|
||||
|
||||
let $장수일람: JQuery<HTMLElement> | undefined;
|
||||
|
||||
const 국가별: Record<string, NationInfo> = {};
|
||||
let cnt = 0;
|
||||
$html.each(function () {
|
||||
if (this.tagName == "TABLE") {
|
||||
cnt += 1;
|
||||
if (cnt == 2) {
|
||||
$장수일람 = $(this);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
if ($장수일람 !== undefined) {
|
||||
$장수일람.find('tr:gt(0)').each(function () {
|
||||
const 장수 = {} as KingdomGeneral;
|
||||
const $this = $(this);
|
||||
const $tds = $this.find('td');
|
||||
|
||||
const 장수명 = $.trim($tds.eq(1).text());
|
||||
const 국가 = $.trim($tds.eq(6).text());
|
||||
|
||||
//const 부상 = $this.data('general-wounded');
|
||||
|
||||
장수.html = $this.clone();
|
||||
장수.장수명 = 장수명;
|
||||
장수.국가 = 국가;
|
||||
장수.벌점 = parseInt($tds.eq(-1).text());
|
||||
장수.통 = parseInt($this.data('general-leadership'));
|
||||
장수.무 = parseInt($this.data('general-strength'));
|
||||
장수.지 = parseInt($this.data('general-intel'));
|
||||
장수.삭턴 = parseInt($tds.eq(-2).text());
|
||||
장수.종류 = getUserType(장수.통, 장수.무, 장수.지);
|
||||
장수.NPC = $this.data('npc-type');
|
||||
|
||||
if (!(국가 in 국가별)) {
|
||||
국가별[국가] = {
|
||||
만능: [],
|
||||
통: [],
|
||||
무: [],
|
||||
지: [],
|
||||
평범: [],
|
||||
무지: [],
|
||||
무능: [],
|
||||
//NPC:[],
|
||||
};
|
||||
}
|
||||
|
||||
//if(장수.NPC) 국가별[국가].NPC.push(장수);
|
||||
국가별[국가][장수.종류].push(장수);
|
||||
|
||||
장수.html.hide();
|
||||
$content.append(장수.html);
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
국가테이블.each(function () {
|
||||
const $this = $(this);
|
||||
const $tbl = $this;
|
||||
const $td = $this.find('td:last');
|
||||
let name = $.trim($this.find('td:first').text());
|
||||
name = name.substr(2, name.length - 4);
|
||||
|
||||
const 국가정보 = 국가별[name];
|
||||
|
||||
let total = 0;
|
||||
let 전투유저장수 = 0;
|
||||
let 통솔합 = 0;
|
||||
let 삭턴장수 = 0;
|
||||
|
||||
let 전투N장수 = 0;
|
||||
let N장통솔합 = 0;
|
||||
$td.html('<p class="sum" style="margin:0;font-weight:bold;color:yellow;text-align:center"></p>');
|
||||
$td.css('text-indent', '-5.8em').css('padding-left', '5.8em');
|
||||
for (const [종류명, 테이블] of Object.entries(국가정보)) {
|
||||
|
||||
const $p = $("<p></p>").css('margin', '0');
|
||||
|
||||
if (테이블.length == 0) continue;
|
||||
|
||||
테이블.sort(function (좌, 우) {
|
||||
if (우.벌점 == 좌.벌점) {
|
||||
return 좌.장수명 > 우.장수명 ? 1 : 0;
|
||||
}
|
||||
return 우.벌점 - 좌.벌점
|
||||
});
|
||||
|
||||
|
||||
let text = " " + 종류명;
|
||||
text = text.substr(text.length - 2);
|
||||
$p.append(text + '장(');
|
||||
|
||||
text = "" + 테이블.length;
|
||||
|
||||
$p.append(text + ')');
|
||||
if (text.length < 3) {
|
||||
$p.append("<span style='display:inline-block;width:" + (3 - text.length) / 2 + "em;'> </span>");
|
||||
}
|
||||
$p.append(': ');
|
||||
|
||||
total += 테이블.length;
|
||||
|
||||
$.each(테이블, function (idx, val) {
|
||||
//const 종능 = val.통 + val.무 + val.지;
|
||||
console.log(val);
|
||||
if (종류명 != '무능' && 종류명 != '무지') {
|
||||
if (val.삭턴 >= realKillturn && val.NPC < 2) {
|
||||
전투유저장수 += 1;
|
||||
|
||||
통솔합 += val.통;
|
||||
}
|
||||
|
||||
if (val.삭턴 > 5 && val.NPC >= 2) {
|
||||
전투N장수 += 1;
|
||||
|
||||
N장통솔합 += val.통;
|
||||
}
|
||||
}
|
||||
|
||||
const $obj = $('<span></span>');
|
||||
const $obj2 = $('<span></span>');
|
||||
$obj.html(val.장수명);
|
||||
|
||||
if (val.NPC < 2 && val.삭턴 < realKillturn) {
|
||||
$obj.css('text-decoration', 'line-through');
|
||||
삭턴장수 += 1;
|
||||
}
|
||||
const colorNPC = getNpcColor(val.NPC);
|
||||
if (colorNPC !== undefined) {
|
||||
$obj.css('color', colorNPC);
|
||||
}
|
||||
if (val.벌점 >= 1500) $obj.css('color', 'yellow');
|
||||
else if (val.벌점 >= 200) $obj.css('color', 'lightgreen');
|
||||
|
||||
$obj2.append($obj);
|
||||
if (idx < 테이블.length - 1) {
|
||||
$obj2.append(', ');
|
||||
}
|
||||
$p.append($obj2);
|
||||
$obj2.hover(function () {
|
||||
const top = unwrap($tbl.offset()).top + unwrap($tbl.outerHeight()) + 3;
|
||||
$userFrame.css('top', top);
|
||||
val.html.show();
|
||||
$userFrame.show();
|
||||
console.log('올림!' + val.장수명);
|
||||
}, function () {
|
||||
$userFrame.hide();
|
||||
val.html.hide();
|
||||
console.log('내림!' + val.장수명);
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
$td.append($p);
|
||||
}
|
||||
|
||||
const result = "* 총(" + total + "), 전투장(" + 전투유저장수 + ", 약 " + formatScore(통솔합 * 100) + "명), 전투N장(" + 전투N장수 + ", 약 " + formatScore(N장통솔합 * 100) + "명), 삭턴장(" + 삭턴장수 + ") *";
|
||||
$tbl.find('.sum').html(result);
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
$('body').append($userFrame);
|
||||
|
||||
const $frame = $('table:eq(0) td:eq(0)');
|
||||
$frame.find('br:last').remove();
|
||||
|
||||
const $btn = $('<input type="button" value="장수 일람 연동">');
|
||||
$btn.on('click', function () {
|
||||
void runAnalysis();
|
||||
$btn.prop("disabled", true);
|
||||
const $tr0 = $('table:eq(0) tr:eq(0)');
|
||||
$tr0.append('<td><strong>*벌점 순 정렬*</strong><br><span style="color:yellow">벌점 1500점 이상</span>, <span style="color:lightgreen">벌점 200점 이상</span>, ' +
|
||||
'<span style="text-decoration:line-through">삭턴장</span>, <span style="color:cyan">ⓝ장</span>' + '<br><strong>전투장 :</strong> 만능장 + 무장 + 지장 + 평범장 - 삭턴자(만능, 무, 지, 평범) </td>');
|
||||
});
|
||||
|
||||
|
||||
|
||||
$frame.append($btn);
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import axios from "axios";
|
||||
import { unwrap } from "@util/unwrap";
|
||||
import { RuntimeError } from "@util/RuntimeError";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
userList: Record<number, JQuery<HTMLElement>>;
|
||||
}
|
||||
}
|
||||
|
||||
export function launchTroopPlugin($: JQueryStatic): void {
|
||||
|
||||
let userList: Record<number, JQuery<HTMLElement>> = {};
|
||||
const basicPath = (() => {
|
||||
const path = document.location.pathname;
|
||||
return path.substring(0, path.lastIndexOf('/'));
|
||||
})();
|
||||
|
||||
const $userFrame: JQuery<HTMLElement> = $("<div id='on_mover' style='position:absolute;'>" +
|
||||
"<table class='tb_layout bg0' style='width:100%;'><thead>" +
|
||||
"<tr>" +
|
||||
"<td width=98 class='bg1 center'>이 름</td>" +
|
||||
"<td width=98 class='bg1 center'>통무지</td>" +
|
||||
"<td width=53 class='bg1 center'>자 금</td>" +
|
||||
"<td width=53 class='bg1 center'>군 량</td>" +
|
||||
"<td width=48 class='bg1 center'>도시</td>" +
|
||||
"<td width=28 class='bg1 center'>守</td>" +
|
||||
"<td width=58 class='bg1 center'>병 종</td>" +
|
||||
"<td width=63 class='bg1 center'>병 사</td>" +
|
||||
"<td width=38 class='bg1 center'>훈련</td>" +
|
||||
"<td width=38 class='bg1 center'>사기</td>" +
|
||||
"<td width=213 class='bg1 center'>명 령</td>" +
|
||||
"<td width=38 class='bg1 center'>삭턴</td>" +
|
||||
"<td width=48 class='bg1 center'>턴</td>" +
|
||||
"</tr>" +
|
||||
"</thead></thead><tbody class='content'></tbody></table></div>");
|
||||
$userFrame.find('thead td');
|
||||
$userFrame.css('width', '960px').css('margin', '0').css('padding', '0').css('left', '50%').css('margin-left', '-480px');
|
||||
$userFrame.hide();
|
||||
|
||||
|
||||
const runAnalysis = async function () {
|
||||
userList = {};
|
||||
const $content = $('#on_mover .content');
|
||||
$content.html('');
|
||||
const response = await axios.get(`${basicPath}/b_genList.php`, {responseType: 'text'});
|
||||
const rawData = response.data;
|
||||
|
||||
try {
|
||||
const $html = $(rawData);
|
||||
|
||||
const tmpUsers: JQuery<HTMLElement> = (() => {
|
||||
let tmpUsers = undefined;
|
||||
$html.each(function () {
|
||||
const $this = $(this);
|
||||
if ($this.attr('id') == "general_list") {
|
||||
tmpUsers = $(this);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (tmpUsers === undefined) {
|
||||
throw new RuntimeError();
|
||||
}
|
||||
return tmpUsers;
|
||||
})()
|
||||
|
||||
|
||||
tmpUsers.find("tbody > tr").each(function () {
|
||||
const $this = $(this);
|
||||
const $부대 = $this.children('.i_troop');
|
||||
|
||||
const 부대 = $.trim($부대.text());
|
||||
|
||||
if (부대 == '-') {
|
||||
//부대 안탔음!
|
||||
return;
|
||||
}
|
||||
$부대.remove();
|
||||
|
||||
const generalID = parseInt($this.data('general-id'));
|
||||
userList[generalID] = $this;
|
||||
$this.hide();
|
||||
$content.append($this);
|
||||
});
|
||||
|
||||
$('.troopUser').hover(function () {
|
||||
const $this = $(this);
|
||||
const parent = $this.closest('tr');
|
||||
const generalID = parseInt($this.data('general-id'));
|
||||
console.log(generalID);
|
||||
const top = unwrap(parent.offset()).top + unwrap(parent.outerHeight());
|
||||
$userFrame.css('top', top);
|
||||
userList[generalID].show();
|
||||
$userFrame.show();
|
||||
}, function () {
|
||||
const $this = $(this);
|
||||
const generalID = parseInt($this.data('general-id'));
|
||||
userList[generalID].hide();
|
||||
$userFrame.hide();
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
const $frame = $('table:eq(0) td:eq(0)');
|
||||
$frame.find('br:last').remove();
|
||||
|
||||
const $btn = $('<input type="button" value="암행부 연동">');
|
||||
$btn.click(async function () {
|
||||
await runAnalysis();
|
||||
});
|
||||
|
||||
$frame.append($btn);
|
||||
|
||||
|
||||
window.userList = userList;
|
||||
|
||||
$('body').append($userFrame);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import axios from 'axios';
|
||||
import $ from 'jquery';
|
||||
import { isNumber } from 'lodash';
|
||||
import { TemplateEngine } from '@util/TemplateEngine';
|
||||
import type { InvalidResponse } from '@/defs';
|
||||
import { setAxiosXMLHttpRequest } from '@util/setAxiosXMLHttpRequest';
|
||||
import { unwrap_any } from '@util/unwrap_any';
|
||||
import { convertFormData } from '@util/convertFormData';
|
||||
import { exportWindow } from '@util/exportWindow';
|
||||
import '@/gateway/common';
|
||||
|
||||
type UserEntry = {
|
||||
userID: string,
|
||||
userName: string,
|
||||
email: string,
|
||||
authType: string | null,
|
||||
userGrade: number,
|
||||
blockUntil: string | null,
|
||||
nickname: string,
|
||||
icon: string,
|
||||
joinDate: string,
|
||||
loginDate: string,
|
||||
deleteAfter: string | null,
|
||||
}
|
||||
|
||||
type UserListResponse = {
|
||||
result: true,
|
||||
users: UserEntry[],
|
||||
servers: string[],
|
||||
allowJoin: boolean,
|
||||
allowLogin: boolean,
|
||||
}
|
||||
|
||||
const userFrame = '\
|
||||
<tr id="userinfo_<%userID%>" data-username="<%userName%>" data-id="<%userID%>">\
|
||||
<th scope="row"><%userID%></th>\
|
||||
<td><%userName%></td>\
|
||||
<td class="small"><%emailFunc(email)%><br>(<%authType%>)</td>\
|
||||
<td><%userGradeText%><p class="small hide_text user_grade_<%userGrade%>" style="margin:0;"><%shortDate(blockUntil)%></p></td>\
|
||||
<td><%nickname%></td>\
|
||||
<td><img class="generalIcon" src="<%icon%>" width="64" height="64"></td>\
|
||||
<td class="small"><%slotGeneralList%></td>\
|
||||
<td class="small"><%shortDate(joinDate)%></td>\
|
||||
<td class="small"><%shortDate(loginDate)%></td>\
|
||||
<td class="small"><%shortDate(deleteAfter)%></td>\
|
||||
<td>\
|
||||
<div class="btn-group" role="group">\
|
||||
<button type="button" onclick="changeUserStatus(\'delete\', this);" class="btn btn-danger btn-sm">강제<br>탈퇴</button>\
|
||||
<button type="button" onclick="changeUserStatus(\'reset_pw\', this);" class="btn btn-info btn-sm">암호<br>변경</button>\
|
||||
<button type="button" onclick="changeUserStatus(\'block\', this);" class="btn btn-warning btn-sm">유저<br>차단</button>\
|
||||
<button type="button" onclick="changeUserStatus(\'unblock\', this);" class="btn btn-secondary btn-sm">차단<br>해제</button>\
|
||||
<button type="button" onclick="changeUserStatus(\'set_userlevel\', this);" class="btn btn-primary btn-sm">별도<br>권한</button>\
|
||||
</div>\
|
||||
</td>\
|
||||
</tr>';
|
||||
|
||||
function convUserGrade(grade: number): string {
|
||||
const userGradeMap = {
|
||||
0: '차단',
|
||||
1: '일반',
|
||||
4: '특별',
|
||||
5: '부운영자',
|
||||
6: '운영자'
|
||||
};
|
||||
|
||||
if (grade in userGradeMap) {
|
||||
return userGradeMap[grade as (keyof typeof userGradeMap)];
|
||||
}
|
||||
return grade.toString();
|
||||
}
|
||||
|
||||
function fillAllowJoinLogin(result: UserListResponse) {
|
||||
if (result.allowJoin) {
|
||||
$('#allow_join_y').trigger('click');
|
||||
}
|
||||
else {
|
||||
$('#allow_join_n').trigger('click');
|
||||
}
|
||||
|
||||
if (result.allowLogin) {
|
||||
$('#allow_login_y').trigger('click');
|
||||
}
|
||||
else {
|
||||
$('#allow_login_n').trigger('click');
|
||||
}
|
||||
}
|
||||
|
||||
function fillUserList(result: UserListResponse) {
|
||||
const $user_list = $('#user_list');
|
||||
|
||||
|
||||
$user_list.empty();
|
||||
|
||||
const slotGeneralList = $.map(result.servers, function (value) {
|
||||
return `<span class="server_generalName_${value}"></span>`;
|
||||
}).join('<br>');
|
||||
|
||||
const emailFunc = function (text: string) {
|
||||
return String(text).replace('@', '@<br>');
|
||||
}
|
||||
const brFunc = function (text: string) {
|
||||
return String(text).split(' ').join('<br>');
|
||||
};
|
||||
|
||||
const shortDateFunc = function (date: string | null) {
|
||||
if (!date) {
|
||||
return '-';
|
||||
}
|
||||
return brFunc(date.substr(2));
|
||||
}
|
||||
|
||||
$.each(result.users, function (idx, user) {
|
||||
const templateItem = {
|
||||
...user,
|
||||
email: user.email ?? '-',
|
||||
br: brFunc,
|
||||
shortDate: shortDateFunc,
|
||||
emailFunc: emailFunc,
|
||||
slotGeneralList: slotGeneralList,
|
||||
userGradeText: convUserGrade(user.userGrade),
|
||||
}
|
||||
|
||||
$user_list.append(
|
||||
TemplateEngine(userFrame, templateItem)
|
||||
)
|
||||
});
|
||||
|
||||
//TODO: slotGeneralList에 값을 채워야함. ajax로 받아올 필요 있음
|
||||
}
|
||||
|
||||
async function changeSystem(action: string, param?: string): Promise<void> {
|
||||
const text = `${action}${param ? (', ' + param) : ''}을 진행합니다.`;
|
||||
if (!confirm(text)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let result: InvalidResponse | {
|
||||
result: true,
|
||||
affected?: number,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_set_userlist.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
'action': action,
|
||||
'param': param ?? null
|
||||
})
|
||||
})
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(result.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.affected) {
|
||||
alert(`${result.affected}건이 처리되었습니다.`);
|
||||
await refreshAll();
|
||||
}
|
||||
else {
|
||||
alert('완료되었습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
async function changeUserStatus(action: string, userID: Element | number, param?: number): Promise<void> {
|
||||
if (userID instanceof Element) {
|
||||
userID = parseInt($(userID).parents('tr').data('id'));
|
||||
}
|
||||
if (!isNumber(userID)) {
|
||||
alert('userID가 올바르게 지정되지 않았습니다!');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (action == 'set_userlevel') {
|
||||
if (!isNumber(param)) {
|
||||
param = parseInt(prompt('원하는 등급을 입력해주세요.(1:일반, 4:특별, 5:부운영자, 6:운영자)', '1') ?? '0');
|
||||
}
|
||||
|
||||
if (param < 1 || param > 6) {
|
||||
alert('올바르지 않습니다.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (action == 'block') {
|
||||
if (!isNumber(param)) {
|
||||
param = parseInt(prompt('블록 기간을 입력해주세요. <= 0은 반영구(50년)입니다.', '7') ?? '7');
|
||||
}
|
||||
}
|
||||
|
||||
const userName = unwrap_any<string>($('#userinfo_' + userID).data('username'));
|
||||
|
||||
const text = `${userName}에 대해서 ${action}${param ? (', ' + param) : ''}을 진행합니다.`;
|
||||
if (!confirm(text)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let result: InvalidResponse | {
|
||||
result: true,
|
||||
detail?: string,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_set_userlist.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
'action': action,
|
||||
'user_id': userID,
|
||||
'param': param ?? null
|
||||
})
|
||||
})
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(result.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.detail) {
|
||||
alert(`완료되었습니다: ${result.detail}`);
|
||||
}
|
||||
else {
|
||||
alert('완료되었습니다.');
|
||||
}
|
||||
|
||||
await refreshAll();
|
||||
}
|
||||
|
||||
async function refreshAll() {
|
||||
let result: InvalidResponse | UserListResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_get_userlist.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
});
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(result.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
fillAllowJoinLogin(result);
|
||||
fillUserList(result);
|
||||
}
|
||||
|
||||
$(async function () {
|
||||
setAxiosXMLHttpRequest();
|
||||
await refreshAll();
|
||||
|
||||
$('input[name=allow_join], input[name=allow_login]').on('change', async function () {
|
||||
const $this = $(this);
|
||||
await changeSystem(unwrap_any<string>($this.attr('name')), unwrap_any<string>($this.val()));
|
||||
})
|
||||
});
|
||||
|
||||
exportWindow(changeSystem, 'changeSystem');
|
||||
exportWindow(changeUserStatus, 'changeUserStatus');
|
||||
@@ -0,0 +1,320 @@
|
||||
import { exportWindow } from '@util/exportWindow';
|
||||
import axios from 'axios';
|
||||
import { setAxiosXMLHttpRequest } from '@util/setAxiosXMLHttpRequest';
|
||||
import type { InvalidResponse } from '@/defs';
|
||||
import { convertFormData } from '@util/convertFormData';
|
||||
import { TemplateEngine } from "@util/TemplateEngine";
|
||||
import { unwrap_any } from '@util/unwrap_any';
|
||||
import { unwrap } from '@util/unwrap';
|
||||
import '@/gateway/common';
|
||||
|
||||
type ServerUpdateResponse = {
|
||||
result: true,
|
||||
version: string,
|
||||
server: string,
|
||||
imgResult: false,
|
||||
}
|
||||
|
||||
type ServerRootUpdateResponse = {
|
||||
result: true,
|
||||
version: string,
|
||||
server: string,
|
||||
imgResult: true,
|
||||
imgDetail: string,
|
||||
}
|
||||
|
||||
type ServerStateItem = {
|
||||
name: string,
|
||||
korName: string,
|
||||
color: string,
|
||||
isRoot: boolean,
|
||||
lastGitPath: string,
|
||||
valid: boolean,
|
||||
run: boolean,
|
||||
installed: boolean,
|
||||
version: string,
|
||||
reason: string,
|
||||
status?: string,
|
||||
}
|
||||
|
||||
type ServerStateResponse = {
|
||||
acl: Record<string, string[]>,
|
||||
server: ServerStateItem[],
|
||||
grade: number,
|
||||
}
|
||||
|
||||
type ServerChangeResponse = {
|
||||
result: true,
|
||||
installURL?: string,
|
||||
}
|
||||
|
||||
const serverAdminTemplate = '\
|
||||
<tr class="bg0 server_admin_<%name%>" data-server_name="<%name%>" data-is_root="<%isRoot%>" data-git-path="<%lastGitPath%>">\
|
||||
<th style="color:<%color%>;"><%korName%>(<%name%>)</th>\
|
||||
<td><%status%></td>\
|
||||
<td><%version%></td>\
|
||||
<td><button type="button" class="serv_act_close with_skin valid_if_set with_border obj_fill" onclick="modifyServerStatus(this, \'close\');">폐쇄</button></td>\
|
||||
<td><button type="button" class="serv_act_open with_skin valid_if_set with_border obj_fill" onclick="modifyServerStatus(this, \'open\');">오픈</button></td>\
|
||||
<td><a class="just_link" href="../<%name%>/install.php"><button type="button" class="serv_act_reset with_skin valid_if_set with_border obj_fill">리셋</button></a></td>\
|
||||
<td><a class="just_link" href="../<%name%>/install_db.php"><button type="button" class="serv_act_hard_reset with_skin valid_if_installed only_admin with_border obj_fill">하드리셋</button></a></td>\
|
||||
<td><button type="button" class="serv_act_119 with_skin valid_if_set with_border obj_fill" onclick="Entrance_AdminOpen119(this);">서버119</button></td>\
|
||||
<td><button type="button" class="serv_act_update with_skin with_border obj_fill" onclick="serverUpdate(this);">업데이트</button></td>\
|
||||
</tr>\
|
||||
';//TODO: npm install 관련 기능 추가, js/css output 경로 변경
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
adminGrade: number;
|
||||
aclList: Record<string, string[]>;
|
||||
}
|
||||
}
|
||||
|
||||
async function serverUpdate(caller: HTMLElement) {
|
||||
const $caller = $(caller);
|
||||
const $tr = $caller.parents('tr');
|
||||
const server = unwrap_any<string>($tr.data('server_name'));
|
||||
let isRoot: string | boolean = unwrap_any<string>($tr.data('is_root'));
|
||||
|
||||
let target = $tr.data('gitPath');
|
||||
|
||||
if (typeof isRoot !== 'boolean') {
|
||||
isRoot = (isRoot != 'false');
|
||||
}
|
||||
|
||||
let allowFullUpdate = (server in window.aclList && window.aclList[server].indexOf('fullUpdate') >= 0);
|
||||
allowFullUpdate = allowFullUpdate || window.adminGrade > 5;
|
||||
|
||||
let allowUpdate = (server in window.aclList && window.aclList[server].indexOf('update') >= 0);
|
||||
allowUpdate = allowUpdate || window.adminGrade >= 5;
|
||||
allowUpdate = allowUpdate || allowFullUpdate;
|
||||
|
||||
if (!allowUpdate) {
|
||||
alert('권한이 없습니다!');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (allowFullUpdate) {
|
||||
target = prompt('가져올 git tree-ish 명을 입력해주세요.', target)
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (isRoot) {
|
||||
if (!confirm('서버 라이브러리, 루트 서버에 대해 git pull을 실행합니다.')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (!confirm('다음 git tree-ish 주소로 업데이트를 시도합니다 : ' + target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let result: InvalidResponse | ServerUpdateResponse | ServerRootUpdateResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: '../j_updateServer.php',
|
||||
responseType: 'json',
|
||||
method: 'post',
|
||||
data: convertFormData({
|
||||
server: server,
|
||||
target: target
|
||||
})
|
||||
});
|
||||
result = response.data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`에러: ${e}`);
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(`실패했습니다: ${result.reason}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let aux = '';
|
||||
if (result.imgResult) {
|
||||
aux = ` (이미지 서버 갱신:${result.imgResult}, ${result.imgDetail})`;
|
||||
}
|
||||
alert(`${result.server} 서버가 ${result.version} 버전으로 업데이트 되었습니다.${aux}`);
|
||||
}
|
||||
|
||||
function drawServerAdminList(serverList: ServerStateResponse) {
|
||||
const $table = $('#server_admin_list');
|
||||
const $showErrorLog = $('#showErrorLog');
|
||||
|
||||
if (serverList.grade >= 5) {
|
||||
$showErrorLog.show();
|
||||
}
|
||||
$.each(serverList.server, function (idx, server) {
|
||||
console.log(server);
|
||||
let status: string;
|
||||
if (!server.valid) {
|
||||
status = `에러, ${server.reason}`;
|
||||
}
|
||||
else if (!server.run) {
|
||||
status = '폐쇄됨';
|
||||
}
|
||||
else {
|
||||
status = '운영 중';
|
||||
}
|
||||
server.status = status;
|
||||
|
||||
const $tr = $(TemplateEngine(serverAdminTemplate, server));
|
||||
$table.append($tr);
|
||||
if (serverList.grade < 4) {
|
||||
$tr.find('button').prop('disabled', true);
|
||||
}
|
||||
if (!server.valid) {
|
||||
$tr.find('.valid_if_set').prop('disabled', true);
|
||||
}
|
||||
if (!server.installed) {
|
||||
$tr.find('.valid_if_installed').prop('disabled', true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const aclByServer = serverList.acl[server.name];
|
||||
|
||||
$.each(aclByServer, function (idx, action) {
|
||||
console.log(action);
|
||||
if (action == 'update' || action == 'fullUpdate') {
|
||||
if (!server.installed) {
|
||||
return true;
|
||||
}
|
||||
$tr.find('.serv_act_update').prop('disabled', false);
|
||||
$showErrorLog.show();
|
||||
}
|
||||
else if (action == 'openClose') {
|
||||
if (!server.valid) {
|
||||
return true;
|
||||
}
|
||||
$tr.find('.serv_act_open, .serv_act_close').prop('disabled', false);
|
||||
}
|
||||
else if (action == 'reset') {
|
||||
if (!server.installed) {
|
||||
return true;
|
||||
}
|
||||
$tr.find('.serv_act_reset, .serv_act_close').prop('disabled', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
window.adminGrade = serverList.grade;
|
||||
window.aclList = serverList.acl;
|
||||
if (serverList.grade <= 5) {
|
||||
$table.find('.only_admin').prop('disabled', true);
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadPlugin(): Promise<void> {
|
||||
setAxiosXMLHttpRequest();
|
||||
|
||||
Entrance_AdminInit();
|
||||
|
||||
const response = await axios({
|
||||
url: 'j_server_get_admin_status.php',
|
||||
method: 'post',
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
drawServerAdminList(response.data);
|
||||
}
|
||||
|
||||
function Entrance_AdminInit() {
|
||||
console.log('adminInit');
|
||||
$("#Entrance_000202").on('click', Entrance_Member);
|
||||
$("#notice_change_btn").on('click', Entrance_AdminChangeNotice);
|
||||
}
|
||||
|
||||
function Entrance_Member(e: JQuery.Event) {
|
||||
e.preventDefault();
|
||||
$("#Entrance_00").hide();
|
||||
$("#EntranceMember_00").show();
|
||||
}
|
||||
|
||||
async function Entrance_AdminChangeNotice(e: JQuery.Event) {
|
||||
e.preventDefault();
|
||||
const notice = unwrap_any<string>($("#notice_edit").val());
|
||||
|
||||
if (!confirm('정말 실행하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let result: InvalidResponse | ServerChangeResponse;
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_server_change_status.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
action: 'notice',
|
||||
notice: notice
|
||||
})
|
||||
});
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(`실패했습니다: ${result.reason}`);
|
||||
return;
|
||||
}
|
||||
|
||||
location.reload();
|
||||
}
|
||||
|
||||
async function modifyServerStatus(caller: HTMLElement, action: string) {
|
||||
const $caller = $(caller);
|
||||
const server = unwrap_any<string>($caller.parents('tr').data('server_name'));
|
||||
|
||||
if (!confirm('정말 실행하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let result: InvalidResponse | ServerChangeResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_server_change_status.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
server: server,
|
||||
action: action
|
||||
})
|
||||
});
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(`실패했습니다: ${result.reason}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == 'reset') {
|
||||
location.href = unwrap(result.installURL);
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
function Entrance_AdminOpen119(caller: HTMLElement) {
|
||||
const $caller = $(caller);
|
||||
const serverDir = $caller.parents('tr').data('server_name');
|
||||
location.href = `../${serverDir}/_119.php`;
|
||||
}
|
||||
|
||||
exportWindow(modifyServerStatus, 'modifyServerStatus');
|
||||
exportWindow(Entrance_AdminOpen119, 'Entrance_AdminOpen119');
|
||||
exportWindow(serverUpdate, 'serverUpdate');
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
import 'bootstrap';
|
||||
import "@scss/common/bootstrap5.scss";
|
||||
@@ -0,0 +1,297 @@
|
||||
import { exportWindow } from '@util/exportWindow';
|
||||
import $ from 'jquery';
|
||||
exportWindow($, '$');
|
||||
import axios from 'axios';
|
||||
import { initTooltip } from "@/legacy/initTooltip";
|
||||
import { TemplateEngine } from '@util/TemplateEngine';
|
||||
import type { InvalidResponse } from '@/defs';
|
||||
import { getDateTimeNow } from '@util/getDateTimeNow';
|
||||
import { setAxiosXMLHttpRequest } from '@util/setAxiosXMLHttpRequest';
|
||||
import { loadPlugin as loadAdminPlugin } from '@/gateway/admin_server';
|
||||
import '@/gateway/common';
|
||||
|
||||
declare const isAdmin: boolean;
|
||||
|
||||
const serverListTemplate = "\
|
||||
<tr class='server_item bg0 server_name_<%name%>' data-server='<%name%>'>\
|
||||
<td class='server_name obj_tooltip' data-bs-toggle='tooltip' data-bs-placement='bottom'>\
|
||||
<span style='font-weight:bold;font-size:1.4em;color:<%color%>'><%korName%>섭</span><br>\
|
||||
<span class='n_country'></span>\
|
||||
<span class='tooltiptext server_date'></span>\
|
||||
</td>\
|
||||
<td colspan='4' class='server_down'>- 폐 쇄 중 -</td>\
|
||||
</tr>\
|
||||
";
|
||||
|
||||
const serverTextInfo = "\
|
||||
<td>\
|
||||
서기 <%year%>년 <%month%>월 (<span style='color:orange;'><%scenario%></span>)<br>\
|
||||
유저 : <%userCnt%> / <%maxUserCnt%>명 <span style='color:cyan;'>NPC : <%npcCnt%>명</span> (<span style='color:limegreen;'><%turnTerm%>분 턴 서버</span>)<br>\
|
||||
(상성 설정:<%fictionMode%>), (기타 설정:<%otherTextInfo%>)\
|
||||
</td>\
|
||||
";
|
||||
|
||||
const serverProvisionalInfo = "\
|
||||
<td>\
|
||||
- 오픈 일시 : <%opentime%> -<br>\
|
||||
서기 <%year%>년 <%month%>월 (<span style='color:orange;'><%scenario%></span>)<br>\
|
||||
유저 : <%userCnt%> / <%maxUserCnt%>명 <span style='color:cyan;'>NPC : <%npcCnt%>명</span> (<span style='color:limegreen;'><%turnTerm%>분 턴 서버</span>)<br>\
|
||||
(상성 설정:<%fictionMode%>), (기타 설정:<%otherTextInfo%>)\
|
||||
</td>\
|
||||
";
|
||||
|
||||
const serverFullTemplate = "\
|
||||
<td colspan='4' class='server_full'>- 장수 등록 마감 -</td>\
|
||||
";
|
||||
|
||||
const serverCreateTemplate = "\
|
||||
<td colspan='2' class='not_registered'>- 미 등 록 -\
|
||||
<td class='ignore_border'>\
|
||||
<div class='d-grid'>\
|
||||
<%if(canCreate) {%>\
|
||||
<a class='btn btn-secondary server-action' href='<%serverPath%>/v_join.php'>장수생성</a>\
|
||||
<%}%>\
|
||||
<%if(canSelectNPC) {%>\
|
||||
<a class='btn btn-secondary server-action' href='<%serverPath%>/select_npc.php'>장수빙의</a>\
|
||||
<%}%>\
|
||||
<%if(canSelectPool) {%>\
|
||||
<a class='btn btn-secondary server-action' href='<%serverPath%>/select_general_from_pool.php'>장수선택</a>\
|
||||
<%}%>\
|
||||
</div></td>";
|
||||
|
||||
const serverLoginTemplate = "\
|
||||
<td style='background:url(\"<%picture%>\");background-size: 64px 64px;background-repeat: no-repeat;background-position: center center;'></td>\
|
||||
<td><%name%></td>\
|
||||
<td class='ignore_border'>\
|
||||
<div class='d-grid'>\
|
||||
<a href='<%serverPath%>/' class='btn btn-secondary server-action'>입장</a>\
|
||||
</div></td>\
|
||||
";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const serverReservedTemplate = "\
|
||||
<td colspan='4' class='server_reserved'>\
|
||||
<%openDatetime==starttime?'':'- 가오픈 일시 : '+openDatetime+ '- <br>'%>\
|
||||
- 오픈 일시 : <%starttime%> - <br>\
|
||||
<span style='color:orange;'><%scenarioName%></span> <span style='color:limegreen;'><%turnterm%>분 턴 서버</span><br>\
|
||||
(상성 설정:<%fictionMode%>), (빙의 여부:<%npcMode%>), (최대 스탯:<%gameConf.defaultStatTotal%>), (기타 설정:<%otherTextInfo%>)\
|
||||
</td>\
|
||||
";
|
||||
|
||||
type ServerResponseItem = {
|
||||
color: string,
|
||||
korName: string,
|
||||
name: string,
|
||||
exists: boolean,
|
||||
enable: boolean,
|
||||
};
|
||||
|
||||
type ServerResponse = {
|
||||
result: true;
|
||||
reason: 'success';
|
||||
server: ServerResponseItem[];
|
||||
}
|
||||
|
||||
type RawServerResponse = InvalidResponse | ServerResponse;
|
||||
|
||||
type ReservedGameInfo = {
|
||||
scenarioName: string,
|
||||
turnterm: number,
|
||||
fictionMode: '가상' | '사실',
|
||||
block_general_create: boolean,
|
||||
npcMode: '불가' | '가능' | '선택 생성',
|
||||
openDatetime: string,
|
||||
starttime: string,
|
||||
gameConf: Record<string, string | number>,
|
||||
otherTextInfo: string,
|
||||
}
|
||||
|
||||
type GameInfo = {
|
||||
isUnited: number,
|
||||
npcMode: '불가' | '가능' | '선택 생성',
|
||||
year: number,
|
||||
month: number,
|
||||
scenario: string,
|
||||
maxUserCnt: number,
|
||||
turnTerm: number,
|
||||
opentime: string,
|
||||
starttime: string,
|
||||
turntime: string,
|
||||
join_mode: string,
|
||||
fictionMode: '가상' | '사실',
|
||||
block_general_create: boolean,
|
||||
autorun_user: string,
|
||||
userCnt: number,
|
||||
npcCnt: number,
|
||||
nationCnt: number,
|
||||
otherTextInfo: string,
|
||||
defaultStatTotal: number,
|
||||
}
|
||||
|
||||
type MyInfo = {
|
||||
name: string,
|
||||
picture: string,
|
||||
serverPath?: string,
|
||||
}
|
||||
|
||||
type ServerDetailResponse = {
|
||||
reserved?: ReservedGameInfo;
|
||||
game: GameInfo;
|
||||
me: MyInfo | null | undefined;
|
||||
}
|
||||
|
||||
$(function ($) {
|
||||
setAxiosXMLHttpRequest();
|
||||
$("#btn_logout").on('click', Entrance_Logout);
|
||||
void Entrance_UpdateServer();
|
||||
if(isAdmin){
|
||||
void loadAdminPlugin();
|
||||
}
|
||||
});
|
||||
|
||||
async function Entrance_UpdateServer() {
|
||||
let data: RawServerResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_server_get_status.php',
|
||||
responseType: 'json',
|
||||
method: 'post'
|
||||
});
|
||||
data = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
alert(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.result) {
|
||||
alert(data.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
await Entrance_drawServerList(data.server);
|
||||
}
|
||||
|
||||
async function Entrance_drawServerList(serverInfos: ServerResponseItem[]) {
|
||||
const $serverList = $('#server_list');
|
||||
const now = getDateTimeNow();
|
||||
|
||||
const serverDetailInfoP: Record<string, Promise<ServerDetailResponse>> = {};
|
||||
|
||||
for (const serverInfo of serverInfos) {
|
||||
if(!serverInfo.exists){
|
||||
continue;
|
||||
}
|
||||
const responseP = axios({
|
||||
url: `../${serverInfo.name}/j_server_basic_info.php`,
|
||||
method: 'get',
|
||||
responseType: 'json'
|
||||
}).then(v=>{
|
||||
return v.data as ServerDetailResponse;
|
||||
});
|
||||
serverDetailInfoP[serverInfo.name] = responseP;
|
||||
}
|
||||
for (const serverInfo of serverInfos) {
|
||||
const $serverHtml = $(TemplateEngine(serverListTemplate, serverInfo));
|
||||
$serverList.append($serverHtml);
|
||||
if (!serverInfo.exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const serverPath = `../${serverInfo.name}`;
|
||||
|
||||
let response: ServerDetailResponse;
|
||||
try{
|
||||
if(!(serverInfo.name in serverDetailInfoP)){
|
||||
continue;
|
||||
}
|
||||
response = await serverDetailInfoP[serverInfo.name];
|
||||
}
|
||||
catch(e){
|
||||
console.error(e);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(response.reserved){
|
||||
$serverHtml.find('.server_down').detach();
|
||||
$serverHtml.append(
|
||||
TemplateEngine(serverReservedTemplate, response.reserved)
|
||||
);
|
||||
initTooltip($serverHtml);
|
||||
}
|
||||
|
||||
if(!response.game){
|
||||
continue;
|
||||
}
|
||||
|
||||
const game = response.game;
|
||||
|
||||
//TODO: 서버 폐쇄 방식을 새롭게 변경
|
||||
$serverHtml.find('.server_down').detach();
|
||||
|
||||
if (game.isUnited == 3) {
|
||||
$serverHtml.find('.n_country').html('§이벤트 종료§');
|
||||
$serverHtml.find('.server_date').html(`${game.starttime} <br>~ ${game.turntime}`);
|
||||
} else if (game.isUnited == 1) {
|
||||
$serverHtml.find('.n_country').html('§이벤트 진행중§');
|
||||
$serverHtml.find('.server_date').html(`${game.starttime} ~`);
|
||||
} else if (game.isUnited == 2) {
|
||||
$serverHtml.find('.n_country').html('§천하통일§');
|
||||
$serverHtml.find('.server_date').html(`${game.starttime} <br>~ ${game.turntime}`);
|
||||
} else if (game.opentime <= now) {
|
||||
$serverHtml.find('.n_country').html(`<${game.nationCnt}국 경쟁중>`);
|
||||
$serverHtml.find('.server_date').html(`${game.starttime} ~`);
|
||||
} else {
|
||||
$serverHtml.find('.n_country').html('-가오픈 중-');
|
||||
$serverHtml.find('.server_date').html(`${game.starttime} ~`);
|
||||
}
|
||||
|
||||
if (game.opentime <= now) {
|
||||
$serverHtml.append(
|
||||
TemplateEngine(serverTextInfo, game)
|
||||
);
|
||||
} else {
|
||||
$serverHtml.append(
|
||||
TemplateEngine(serverProvisionalInfo, game)
|
||||
);
|
||||
}
|
||||
|
||||
if (response.me && response.me.name) {
|
||||
const me = response.me;
|
||||
me.serverPath = serverPath;
|
||||
$serverHtml.append(
|
||||
TemplateEngine(serverLoginTemplate, me)
|
||||
);
|
||||
} else if (game.userCnt >= game.maxUserCnt) {
|
||||
$serverHtml.append(
|
||||
TemplateEngine(serverFullTemplate, {})
|
||||
);
|
||||
} else {
|
||||
$serverHtml.append(
|
||||
TemplateEngine(serverCreateTemplate, {
|
||||
serverPath: serverPath,
|
||||
canCreate: !game.block_general_create,
|
||||
canSelectNPC: game.npcMode == '가능',
|
||||
canSelectPool: game.npcMode == '선택 생성'
|
||||
})
|
||||
)
|
||||
}
|
||||
initTooltip($serverHtml);
|
||||
}
|
||||
}
|
||||
|
||||
async function Entrance_Logout() {
|
||||
const response = await axios({
|
||||
url: 'j_logout.php',
|
||||
method: 'post',
|
||||
responseType: 'json'
|
||||
});
|
||||
|
||||
const data: InvalidResponse = response.data;
|
||||
if(!data.result){
|
||||
alert(`로그아웃 실패: ${data.reason}`);
|
||||
return;
|
||||
}
|
||||
location.href = "../";
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
|
||||
import $ from 'jquery';
|
||||
import axios from 'axios';
|
||||
import { setAxiosXMLHttpRequest } from '@util/setAxiosXMLHttpRequest';
|
||||
import type { InvalidResponse } from '@/defs';
|
||||
import { JQValidateForm, type NamedRules } from '@util/jqValidateForm';
|
||||
import { convertFormData } from '@util/convertFormData';
|
||||
import { unwrap_any } from '@util/unwrap_any';
|
||||
import { mb_strwidth } from '@util/mb_strwidth';
|
||||
import { isString } from 'lodash';
|
||||
import { sha512 } from 'js-sha512';
|
||||
import '@/gateway/common';
|
||||
|
||||
async function changeInstallMode() {
|
||||
let result: {
|
||||
step: string,
|
||||
globalSalt: string,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_install_status.php',
|
||||
method: 'post',
|
||||
responseType: 'json'
|
||||
});
|
||||
result = response.data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`에러: ${e}`);
|
||||
return;
|
||||
}
|
||||
if (result.step == 'config') {
|
||||
$('#db_form_card').show();
|
||||
$('#admin_form_card').hide();
|
||||
return;
|
||||
}
|
||||
if (result.step == 'admin') {
|
||||
$('#db_form_card').hide();
|
||||
$('#admin_form_card').show();
|
||||
$('#global_salt').val(result.globalSalt);
|
||||
return;
|
||||
}
|
||||
if (result.step == 'done') {
|
||||
alert('설치가 완료되었습니다.');
|
||||
window.location.href = "..";
|
||||
return;
|
||||
}
|
||||
if (result.step == 'conn_fail') {
|
||||
$('#db_form_card').hide();
|
||||
$('#admin_form_card').hide();
|
||||
alert('설치 이후 DB 설정이 변경된 것 같습니다. RootDB.php 파일의 설정을 확인해주십시오.');
|
||||
return;
|
||||
}
|
||||
if (result.step == 'sql_fail') {
|
||||
$('#db_form_card').hide();
|
||||
$('#admin_form_card').hide();
|
||||
alert('DB가 제대로 설정되지 않았거나, 훼손된 것 같습니다. DB를 복구하거나 RootDB.php 파일을 삭제 후 재설치를 진행해 주십시오.');
|
||||
return;
|
||||
}
|
||||
|
||||
alert(`알 수 없는 오류: ${result}`);
|
||||
}
|
||||
|
||||
function setupDBForm() {
|
||||
const parentPathname = location.pathname.split('/').slice(0, -2).join('/');
|
||||
$('#serv_host').val(
|
||||
[location.protocol, '//', location.host, parentPathname].join('')
|
||||
);
|
||||
|
||||
$('#btn_random_generate_key').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
let token = '';
|
||||
while (token.length < 24) {
|
||||
token += (Math.random() + 1).toString(36).substring(7);
|
||||
}
|
||||
token = token.substr(0, 24);
|
||||
$('#image_request_key').val(token);
|
||||
});
|
||||
|
||||
type DBFormType = {
|
||||
db_host: string,
|
||||
db_port: string,
|
||||
db_id: string,
|
||||
db_pw: string,
|
||||
db_name: string,
|
||||
serv_host: string,
|
||||
shared_icon_path: string,
|
||||
game_image_path: string,
|
||||
image_request_key: string,
|
||||
kakao_rest_key: string,
|
||||
kakao_admin_key: string,
|
||||
}
|
||||
const descriptor: NamedRules<DBFormType> = {
|
||||
db_host: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
},
|
||||
db_port: {
|
||||
required: true,
|
||||
type: 'number',
|
||||
transform: parseInt,
|
||||
},
|
||||
db_id: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
},
|
||||
db_pw: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
},
|
||||
db_name: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
},
|
||||
serv_host: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
},
|
||||
shared_icon_path: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
},
|
||||
game_image_path: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
},
|
||||
image_request_key: {
|
||||
required: false,
|
||||
type: 'string',
|
||||
min: 16,
|
||||
},
|
||||
kakao_rest_key: {
|
||||
required: false,
|
||||
type: 'string',
|
||||
},
|
||||
kakao_admin_key: {
|
||||
required: false,
|
||||
type: 'string',
|
||||
}
|
||||
}
|
||||
const validator = new JQValidateForm($('#db_form'), descriptor);
|
||||
validator.installChangeHandler();
|
||||
|
||||
$('#db_form').on('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
const values = await validator.validate();
|
||||
if (values === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let result: InvalidResponse;
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_setup_db.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
db_host: values.db_host,
|
||||
db_port: values.db_port,
|
||||
db_id: values.db_id,
|
||||
db_pw: values.db_pw,
|
||||
db_name: values.db_name,
|
||||
serv_host: values.serv_host,
|
||||
shared_icon_path: values.shared_icon_path,
|
||||
game_image_path: values.game_image_path,
|
||||
image_request_key: values.image_request_key,
|
||||
kakao_rest_key: values.kakao_rest_key,
|
||||
kakao_admin_key: values.kakao_admin_key,
|
||||
})
|
||||
});
|
||||
result = response.data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`에러: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(result.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
alert('RootDB.php가 생성되었습니다. 관리자 계정 생성을 진행합니다.');
|
||||
await changeInstallMode();
|
||||
});
|
||||
}
|
||||
|
||||
function maxStrWidth(maxWidth: number) {
|
||||
return (rule: unknown, value: string) => {
|
||||
if (!value || !isString(value) || mb_strwidth(value) > maxWidth) {
|
||||
return new Error(`글자 너비가 알파벳 ${maxWidth} 자보다 길지 않아야합니다`);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
function setupAdminForm() {
|
||||
type AdminValues = {
|
||||
username: string,
|
||||
password: string,
|
||||
confirm_password: string,
|
||||
nickname: string,
|
||||
}
|
||||
const descriptor: NamedRules<AdminValues> = {
|
||||
username: {
|
||||
required: true,
|
||||
min: 4,
|
||||
max: 64,
|
||||
type: 'string',
|
||||
options: {//FIXME: options.messages가 동작하지 않는다?
|
||||
messages: {
|
||||
required: "유저명을 입력해주세요",
|
||||
string: {
|
||||
min: (v, l) => `${l}글자 이상 입력하셔야 합니다`,
|
||||
max: (v, l) => `${l}자를 넘을 수 없습니다`,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
password: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
min: 6,
|
||||
options: {
|
||||
messages: {
|
||||
required: "비밀번호를 입력해주세요",
|
||||
string: {
|
||||
min: (v, l) => `비밀번호는 적어도 ${l}글자 이상이어야 합니다`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirm_password: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
min: 6,
|
||||
validator: (rule, value: string, _callback, source) => {
|
||||
console.log(value);
|
||||
if (value != (source.password ?? '')) {
|
||||
return new Error('비밀번호가 일치하지 않습니다');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
options: {
|
||||
messages: {
|
||||
required: "비밀번호를 입력해주세요",
|
||||
string: {
|
||||
min: (v, l) => `비밀번호는 적어도 ${l}글자 이상이어야 합니다`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
nickname: {
|
||||
required: true,
|
||||
validator: maxStrWidth(18),
|
||||
options: {
|
||||
messages: {
|
||||
required: "유저명을 입력해주세요",
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
const validator = new JQValidateForm($('#admin_form'), descriptor);
|
||||
validator.installChangeHandler();
|
||||
|
||||
$('#admin_form').on('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
const values = await validator.validate();
|
||||
if (values === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const raw_password = values.password;
|
||||
const salt = unwrap_any<string>($('#global_salt').val());
|
||||
console.log(salt + raw_password + salt);
|
||||
const hash_pw = sha512(salt + raw_password + salt);
|
||||
|
||||
let result: InvalidResponse;
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_create_admin.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
username: values.username,
|
||||
password: hash_pw,
|
||||
nickname: values.nickname,
|
||||
})
|
||||
});
|
||||
result = response.data;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`에러: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(result.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
alert('관리자 계정이 생성되었습니다.');
|
||||
await changeInstallMode();
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
setAxiosXMLHttpRequest();
|
||||
|
||||
void changeInstallMode();
|
||||
setupDBForm();
|
||||
setupAdminForm();
|
||||
|
||||
|
||||
|
||||
});
|
||||
@@ -0,0 +1,191 @@
|
||||
import $ from 'jquery';
|
||||
import axios from 'axios';
|
||||
import { JQValidateForm, type NamedRules } from '@util/jqValidateForm';
|
||||
import { convertFormData } from '@util/convertFormData';
|
||||
import type { InvalidResponse } from '@/defs';
|
||||
import { setAxiosXMLHttpRequest } from '@util/setAxiosXMLHttpRequest';
|
||||
import { unwrap_any } from '@util/unwrap_any';
|
||||
import { sha512 } from 'js-sha512';
|
||||
import { isString } from 'lodash';
|
||||
import { mb_strwidth } from '@util/mb_strwidth';
|
||||
import '@/gateway/common';
|
||||
|
||||
|
||||
$(async function () {
|
||||
setAxiosXMLHttpRequest();
|
||||
|
||||
const terms1P = axios('../terms.1.html');
|
||||
const terms2P = axios('../terms.2.html');
|
||||
|
||||
type JoinFormType = {
|
||||
secret_agree: boolean,
|
||||
secret_agree2: boolean,
|
||||
third_use: boolean,
|
||||
username: string,
|
||||
password: string,
|
||||
confirm_password: string,
|
||||
nickname: string,
|
||||
}
|
||||
const descriptor: NamedRules<JoinFormType> = {
|
||||
username: {
|
||||
required: true,
|
||||
min: 4,
|
||||
max: 64,
|
||||
type: 'string',
|
||||
asyncValidator: async (rule, value: string) => {
|
||||
const response = await axios({
|
||||
url: 'j_check_dup.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
type: 'username',
|
||||
value: value,
|
||||
})
|
||||
});
|
||||
const isValid: boolean|string = response.data;
|
||||
if (isString(isValid)) {
|
||||
throw new Error(isValid);
|
||||
}
|
||||
},
|
||||
options: {//FIXME: options.messages가 동작하지 않는다?
|
||||
messages: {
|
||||
required: "유저명을 입력해주세요",
|
||||
string: {
|
||||
min: (v, l) => `${l}글자 이상 입력하셔야 합니다`,
|
||||
max: (v, l) => `${l}자를 넘을 수 없습니다`,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
password: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
min: 6,
|
||||
options: {
|
||||
messages: {
|
||||
required: "비밀번호를 입력해주세요",
|
||||
string: {
|
||||
min: (v, l) => `비밀번호는 적어도 ${l}글자 이상이어야 합니다`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirm_password: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
min: 6,
|
||||
validator: (rule, value: string, _callback, source) => {
|
||||
console.log(value);
|
||||
if (value != (source.password ?? '')) {
|
||||
return new Error('비밀번호가 일치하지 않습니다');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
options: {
|
||||
messages: {
|
||||
required: "비밀번호를 입력해주세요",
|
||||
string: {
|
||||
min: (v, l) => `비밀번호는 적어도 ${l}글자 이상이어야 합니다`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
nickname: {
|
||||
required: true,
|
||||
asyncValidator: async (rule, value: string) => {
|
||||
if (!value || !isString(value) || mb_strwidth(value) > 18) {
|
||||
throw new Error(`글자 너비가 알파벳 ${18}자보다 길지 않아야합니다`);
|
||||
}
|
||||
const response = await axios({
|
||||
url: 'j_check_dup.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
type: 'nickname',
|
||||
value: value,
|
||||
})
|
||||
});
|
||||
const isValid: boolean|string = response.data;
|
||||
if (isString(isValid)) {
|
||||
throw new Error(isValid);
|
||||
}
|
||||
},
|
||||
options: {
|
||||
messages: {
|
||||
required: "유저명을 입력해주세요",
|
||||
}
|
||||
}
|
||||
},
|
||||
secret_agree: {
|
||||
required: true,
|
||||
transform: (v)=>v=='on',
|
||||
type: 'enum',
|
||||
enum: [true],
|
||||
message: '동의해야만 가입하실 수 있습니다.',
|
||||
},
|
||||
secret_agree2: {
|
||||
required: true,
|
||||
transform: (v)=>v=='on',
|
||||
type: 'enum',
|
||||
enum: [true],
|
||||
message: '동의해야만 가입하실 수 있습니다.',
|
||||
},
|
||||
third_use: {
|
||||
required: true,
|
||||
transform: (v)=>v=='on',
|
||||
type: 'boolean'
|
||||
}
|
||||
}
|
||||
const validator = new JQValidateForm($('#main_form'), descriptor);
|
||||
validator.installChangeHandler();
|
||||
|
||||
$("#main_form").on('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const values = await validator.validate();
|
||||
if (values === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const raw_password = values.password;
|
||||
const salt = unwrap_any<string>($('#global_salt').val());
|
||||
console.log(salt + raw_password + salt);
|
||||
const hash_pw = sha512(salt + raw_password + salt);
|
||||
|
||||
let result: InvalidResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_join_process.php',
|
||||
responseType: 'json',
|
||||
method: 'post',
|
||||
data: convertFormData({
|
||||
secret_agree: values.secret_agree,
|
||||
secret_agree2: values.secret_agree2,
|
||||
third_use: values.third_use,
|
||||
username: values.username,
|
||||
password: hash_pw,
|
||||
nickname: values.nickname,
|
||||
})
|
||||
});
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(result.reason);
|
||||
window.location.href = "../";
|
||||
return;
|
||||
}
|
||||
|
||||
alert('회원 등록되었습니다.\n첫 로그인 과정에서 인증 코드를 입력하는 것으로 계정이 활성화됩니다.');
|
||||
window.location.href = "../";
|
||||
});
|
||||
|
||||
$('#terms1').html((await terms1P).data);
|
||||
$('#terms2').html((await terms2P).data);
|
||||
});
|
||||
@@ -0,0 +1,381 @@
|
||||
import '@scss/gateway/login.scss';
|
||||
|
||||
import $ from 'jquery';
|
||||
import { JQValidateForm, type NamedRules } from '@util/jqValidateForm';
|
||||
import axios from 'axios';
|
||||
import { convertFormData } from '@util/convertFormData';
|
||||
import { setAxiosXMLHttpRequest } from '@util/setAxiosXMLHttpRequest';
|
||||
import { unwrap_any } from '@util/unwrap_any';
|
||||
import { sha512 } from 'js-sha512';
|
||||
import { unwrap } from '@util/unwrap';
|
||||
import { delay } from '@util/delay';
|
||||
import { Modal } from 'bootstrap';
|
||||
import '@/gateway/common';
|
||||
import { isString } from 'lodash';
|
||||
import { SammoRootAPI, type InvalidResponse } from '@/SammoRootAPI';
|
||||
import type { LoginFailed, LoginResponse, LoginResponseWithKakao, OTPResponse } from '@/defs/API/Login';
|
||||
declare global {
|
||||
interface Window {
|
||||
getOAuthToken: (mode: string, scope_list: string[]) => void;
|
||||
postOAuthResult: (mode: string) => void;
|
||||
}
|
||||
}
|
||||
|
||||
declare const kakao_oauth_client_id: string;
|
||||
declare const kakao_oauth_redirect_uri: string;
|
||||
|
||||
let modalOTP: Modal | undefined = undefined;
|
||||
|
||||
let oauthMode: string | null = null;
|
||||
|
||||
const TOKEN_VERSION = 1;
|
||||
const LOGIN_TOKEN_KEY = 'sammo_login_token';
|
||||
function regNextToken(tokenInfo: [number, string]) {
|
||||
localStorage.setItem(LOGIN_TOKEN_KEY, JSON.stringify([TOKEN_VERSION, tokenInfo, Date.now()]));
|
||||
}
|
||||
|
||||
function getToken(): [number, string] | undefined {
|
||||
const trialToken = localStorage.getItem(LOGIN_TOKEN_KEY);
|
||||
if (!trialToken) {
|
||||
return;
|
||||
}
|
||||
const tokenItems = JSON.parse(trialToken) as [number, [number, string], string];
|
||||
if (tokenItems[0] != TOKEN_VERSION) {
|
||||
console.log(tokenItems);
|
||||
resetToken();
|
||||
return;
|
||||
}
|
||||
const [, token,] = tokenItems;
|
||||
return token;
|
||||
}
|
||||
|
||||
function resetToken() {
|
||||
localStorage.removeItem(LOGIN_TOKEN_KEY);
|
||||
}
|
||||
|
||||
async function tryAutoLogin() {
|
||||
try {
|
||||
const tokenInfo = getToken();
|
||||
if (!tokenInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [tokenID, token] = tokenInfo;
|
||||
|
||||
const result = await SammoRootAPI.Login.ReqNonce(undefined, true);
|
||||
|
||||
if (!result) {
|
||||
//api 에러.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
resetToken();
|
||||
return;
|
||||
}
|
||||
|
||||
const nonce = result.loginNonce;
|
||||
|
||||
const hashedToken = sha512(token + nonce);
|
||||
const loginResult = await SammoRootAPI.Login.LoginByToken({
|
||||
'hashedToken': hashedToken,
|
||||
'token_id': tokenID,
|
||||
}, true);
|
||||
|
||||
if (!loginResult.result) {
|
||||
if (!loginResult.silent) {
|
||||
alert(loginResult.reason);
|
||||
}
|
||||
console.error(loginResult.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
if (loginResult.nextToken) {
|
||||
regNextToken(loginResult.nextToken);
|
||||
}
|
||||
window.location.href = "./";
|
||||
|
||||
}
|
||||
catch (e) {
|
||||
if (isString(e)) {
|
||||
alert(e);
|
||||
}
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
function getOAuthToken(mode: string, scope_list?: string[] | string) {
|
||||
if (mode === undefined) {
|
||||
mode = 'login';
|
||||
}
|
||||
oauthMode = mode;
|
||||
let url = `https://kauth.kakao.com/oauth/authorize?client_id=${kakao_oauth_client_id}&redirect_uri=${kakao_oauth_redirect_uri}&response_type=code`;
|
||||
if (Array.isArray(scope_list)) {
|
||||
url += `&scope=${scope_list.join(',')}`;
|
||||
} else if (typeof scope_list === 'string') {
|
||||
url += `&scope=${scope_list}`;
|
||||
}
|
||||
|
||||
window.open(url, "KakaoAccountLogin", "width=600,height=450,resizable=yes,scrollbars=yes");
|
||||
}
|
||||
window.getOAuthToken = getOAuthToken;
|
||||
|
||||
async function sendTempPasswordToKakaoTalk(): Promise<void> {
|
||||
|
||||
let result: InvalidResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'oauth_kakao/j_login_oauth.php',
|
||||
responseType: 'json',
|
||||
method: 'post',
|
||||
});
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다_login: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(result.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'oauth_kakao/j_change_pw.php',
|
||||
responseType: 'json',
|
||||
method: 'post',
|
||||
});
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다_pw: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
alert('임시 비밀번호가 카카오톡으로 전송되었습니다.');
|
||||
}
|
||||
|
||||
async function doLoginUsingOAuth() {
|
||||
let result: LoginResponseWithKakao;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'oauth_kakao/j_login_oauth.php',
|
||||
responseType: 'json',
|
||||
method: 'post',
|
||||
});
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다_login: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.result) {
|
||||
if (result.nextToken) {
|
||||
regNextToken(result.nextToken);
|
||||
}
|
||||
window.location.href = "./";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.reqOTP) {
|
||||
alert(result.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
const modalEl = unwrap(document.querySelector('#modalOTP'))
|
||||
if (!modalOTP) {
|
||||
modalOTP = new Modal(modalEl);
|
||||
modalEl.addEventListener('shown.bs.modal', function () {
|
||||
unwrap(document.querySelector<HTMLElement>('#otp_code')).focus();
|
||||
});
|
||||
}
|
||||
modalOTP.show();
|
||||
}
|
||||
|
||||
function postOAuthResult(mode: string) {
|
||||
if (mode == 'join') {
|
||||
window.location.href = 'oauth_kakao/join.php';
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == 'req_email') {
|
||||
alert('이메일 정보 공유를 허가해 주셔야 합니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode == 'login') {
|
||||
console.log('로그인모드');
|
||||
if (oauthMode == 'change_pw') {
|
||||
void sendTempPasswordToKakaoTalk();
|
||||
} else {
|
||||
void doLoginUsingOAuth();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
alert('예외 발생!');
|
||||
}
|
||||
window.postOAuthResult = postOAuthResult;
|
||||
|
||||
|
||||
$(async function ($) {
|
||||
setAxiosXMLHttpRequest();
|
||||
|
||||
//로그인 먼저 해볼 것
|
||||
if (getToken()) {
|
||||
void tryAutoLogin();
|
||||
await delay(100);
|
||||
}
|
||||
|
||||
|
||||
type LoginFormType = {
|
||||
username: string,
|
||||
password: string,
|
||||
};
|
||||
const descriptor: NamedRules<LoginFormType> = {
|
||||
username: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '유저명을 입력해주세요',
|
||||
},
|
||||
password: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '비밀번호를 입력해주세요'
|
||||
}
|
||||
};
|
||||
|
||||
const validator = new JQValidateForm($('#main_form'), descriptor);
|
||||
validator.installChangeHandler();
|
||||
|
||||
$("#main_form").on('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const values = await validator.validate();
|
||||
if (values === undefined) {
|
||||
return;
|
||||
}
|
||||
const raw_password = values.password;
|
||||
const salt = unwrap_any<string>($('#global_salt').val());
|
||||
const hash_pw = sha512(salt + raw_password + salt);
|
||||
|
||||
let result: LoginResponse | LoginFailed;
|
||||
|
||||
try {
|
||||
result = await SammoRootAPI.Login.LoginByID({
|
||||
username: values.username,
|
||||
password: hash_pw,
|
||||
|
||||
}, true);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (result.result) {
|
||||
if (result.nextToken) {
|
||||
regNextToken(result.nextToken);
|
||||
}
|
||||
window.location.href = "./";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.reqOTP) {
|
||||
alert(result.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
const modalEl = unwrap(document.querySelector('#modalOTP'))
|
||||
if (!modalOTP) {
|
||||
modalOTP = new Modal(modalEl);
|
||||
modalEl.addEventListener('shown.bs.modal', function () {
|
||||
unwrap(document.querySelector<HTMLElement>('#otp_code')).focus();
|
||||
});
|
||||
}
|
||||
modalOTP.show();
|
||||
});
|
||||
|
||||
$('#otp_form').on('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
let result: OTPResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'oauth_kakao/j_check_OTP.php',
|
||||
responseType: 'json',
|
||||
method: 'post',
|
||||
data: convertFormData({
|
||||
otp: unwrap_any<string>($('#otp_code').val()),
|
||||
})
|
||||
});
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.result) {
|
||||
alert(`로그인되었습니다. ${result.validUntil}까지 유효합니다.`);
|
||||
window.location.href = "./";
|
||||
return;
|
||||
}
|
||||
|
||||
alert(result.reason);
|
||||
|
||||
if (result.reset) {
|
||||
if (modalOTP) {
|
||||
modalOTP.hide();
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
$('#oauth_change_pw').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
getOAuthToken('change_pw', 'talk_message');
|
||||
});
|
||||
|
||||
$('#btn_kakao_login').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
getOAuthToken('login', ['account_email', 'talk_message']);
|
||||
})
|
||||
|
||||
//TODO: 모바일에서 크기 비례 지도 다시 띄우기?
|
||||
/*
|
||||
if (document.body.clientWidth < 700) {
|
||||
const targetWidth = document.body.clientWidth * 0.9;
|
||||
const scale = targetWidth / 700;
|
||||
const $map = $('#running_map');
|
||||
$map.find('.col').css('max-width', targetWidth);
|
||||
$map.find('.card').css('width', targetWidth);
|
||||
$map.find('.map-container').css({
|
||||
'transform-origin': 'top left',
|
||||
'transform': `scale(${scale}, ${scale})`,
|
||||
'height': 500 * scale,
|
||||
});
|
||||
$map.find('.map_body').data('scale', scale);
|
||||
}*/
|
||||
});
|
||||
|
||||
window.fitIframe = function () {
|
||||
const iframe = unwrap(document.querySelector<HTMLIFrameElement>('#running_map'));//TODO: 근황 여러개 볼 수 있도록?
|
||||
const scrollHeight = unwrap(iframe.contentWindow).document.body.scrollHeight;
|
||||
iframe.style.height = `${scrollHeight}px`;
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
import { setAxiosXMLHttpRequest } from '@util/setAxiosXMLHttpRequest';
|
||||
import $ from 'jquery';
|
||||
import axios from 'axios';
|
||||
import { subDays } from 'date-fns';
|
||||
import { getDateTimeNow } from '@util/getDateTimeNow';
|
||||
import { sha512 } from "js-sha512";
|
||||
import { convertFormData } from '@util/convertFormData';
|
||||
import type { InvalidResponse } from '@/defs';
|
||||
import { unwrap } from '@util/unwrap';
|
||||
import { parseTime } from '@util/parseTime';
|
||||
import { formatTime } from '@util/formatTime';
|
||||
import '@/gateway/common';
|
||||
import { Modal } from 'bootstrap';
|
||||
|
||||
type ResultUserInfo = {
|
||||
result: true,
|
||||
id: number,
|
||||
name: string,
|
||||
grade: string,
|
||||
picture: string,
|
||||
global_salt: string,
|
||||
join_date: string,
|
||||
third_use: boolean,
|
||||
acl: string,
|
||||
oauth_type: 'NONE' | 'KAKAO',
|
||||
token_valid_until?: string
|
||||
}
|
||||
|
||||
function fillUserInfo(result: ResultUserInfo | InvalidResponse) {
|
||||
if (!result.result) {
|
||||
alert(result.reason);
|
||||
location.href = 'entrance.php';
|
||||
return;
|
||||
}
|
||||
|
||||
$('#slot_id').html(result.id.toString());
|
||||
$('#slot_nickname').html(result.name);
|
||||
$('#slot_grade').html(result.grade);
|
||||
$('#slot_acl').html(result.acl);
|
||||
$('#slot_icon').attr('src', result.picture);
|
||||
$('#global_salt').val(result.global_salt);
|
||||
$('#slot_join_date').html(result.join_date);
|
||||
$('#slot_third_use').html(result.third_use ? '○' : '×');
|
||||
if (result.third_use) {
|
||||
$('#third_use_disallow').show();
|
||||
}
|
||||
$('#slot_oauth_type').text(result.oauth_type);
|
||||
if (result.oauth_type != 'NONE') {
|
||||
$('#slot_token_valid_until').text(unwrap(result.token_valid_until));
|
||||
}
|
||||
else {
|
||||
$('#slot_token_valid_until').parent().html('');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
function changeIconPreview(this: HTMLFormElement) {
|
||||
const $preview = $(this) as JQuery<HTMLFormElement>;
|
||||
console.log($preview);
|
||||
|
||||
const filename = $preview[0].files[0].name;
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
$('#slot_new_icon').attr('src', unwrap(unwrap(e.target).result).toString()).css('visibility', 'visible');
|
||||
}
|
||||
|
||||
reader.readAsDataURL($preview[0].files[0]);
|
||||
|
||||
$('#image_upload_filename').val(filename);
|
||||
}
|
||||
|
||||
type IconResponse = {
|
||||
result: true,
|
||||
servers: [string, string][]
|
||||
};
|
||||
|
||||
async function deleteIcon() {
|
||||
let result: InvalidResponse | IconResponse;
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_icon_delete.php',
|
||||
responseType: 'json',
|
||||
method: 'post'
|
||||
});
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(`아이콘 삭제를 실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(result.reason);
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
showAdjustServerModal(result.servers);
|
||||
}
|
||||
|
||||
async function disallowThirdUse() {
|
||||
try {
|
||||
await axios({
|
||||
url: 'j_disallow_third_use.php',
|
||||
method: 'post',
|
||||
responseType: 'json'
|
||||
});
|
||||
alert('철회했습니다.');
|
||||
}
|
||||
catch (e) {
|
||||
alert('알 수 없는 이유로 철회를 실패했습니다.');
|
||||
}
|
||||
location.reload();
|
||||
}
|
||||
|
||||
function showAdjustServerModal(serverList: [string, string][]) {
|
||||
|
||||
const $form = $('#chooseServerForm');
|
||||
$form.empty();
|
||||
|
||||
for (const [serverKey, serverKorName] of serverList) {
|
||||
const $item = $(`<div style="display:inline-block;margin-right:7px;" class="custom-control custom-checkbox">\
|
||||
<input type="checkbox" checked class="custom-control-input" name="${serverKey}" id="switch_${serverKey}">\
|
||||
<label class="custom-control-label" for="switch_${serverKey}">${serverKorName}</label>\
|
||||
</div>`);
|
||||
$form.append($item);
|
||||
}
|
||||
|
||||
const modalEl = unwrap(document.querySelector('#chooseServer'));
|
||||
const modal = new Modal(modalEl, {
|
||||
backdrop: 'static'
|
||||
});
|
||||
|
||||
modalEl.addEventListener('hidden.bs.modal', function () {
|
||||
location.reload();
|
||||
return;
|
||||
});
|
||||
modal.show();
|
||||
|
||||
$("#modal-apply").off("click").on("click", async function () {
|
||||
const events: Promise<unknown>[] = [];
|
||||
$form.find('input:checked').each(function () {
|
||||
const $input = $(this);
|
||||
const server = $input.attr('name');
|
||||
|
||||
console.log(server);
|
||||
|
||||
events.push(axios({
|
||||
url: `../${server}/j_adjust_icon.php`,
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
}));
|
||||
})
|
||||
|
||||
for (const p of events) {
|
||||
try {
|
||||
await p;
|
||||
}
|
||||
catch (e) {
|
||||
//서버 폐쇄등으로 접근하지 못할 수도 있음.
|
||||
console.error(e, p);
|
||||
}
|
||||
}
|
||||
|
||||
alert('적용되었습니다.');
|
||||
location.reload();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async function changeIcon(this: HTMLFormElement) {
|
||||
const $icon = $('#image_upload') as JQuery<HTMLFormElement>;
|
||||
|
||||
if ($icon[0].files.length == 0) {
|
||||
alert('파일을 선택해주세요');
|
||||
return false;
|
||||
}
|
||||
|
||||
let result: InvalidResponse | IconResponse;
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_icon_change.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: new FormData(this)
|
||||
});
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
alert('알 수 없는 이유로 아이콘 업로드를 실패했습니다.');
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(result.reason);
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
showAdjustServerModal(result.servers);
|
||||
}
|
||||
|
||||
async function changePassword() {
|
||||
const old_pw = $('#current_pw').val() as string;
|
||||
const new_pw = $('#new_pw').val() as string;
|
||||
const new_pw_confirm = $('#new_pw_confirm').val() as string;
|
||||
|
||||
if (!old_pw) {
|
||||
alert('이전 비밀번호를 입력해야 합니다.');
|
||||
return;
|
||||
}
|
||||
if (new_pw.length < 6) {
|
||||
alert('비밀번호 길이는 6글자 이상이어야 합니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (new_pw != new_pw_confirm) {
|
||||
alert('입력 값이 일치하지 않습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const global_salt = $('#global_salt').val();
|
||||
|
||||
const old_password = sha512(global_salt + old_pw + global_salt);
|
||||
const new_password = sha512(global_salt + new_pw + global_salt);
|
||||
|
||||
let result: InvalidResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_change_password.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
old_pw: old_password,
|
||||
new_pw: new_password
|
||||
})
|
||||
});
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(`알 수 없는 이유로 비밀번호를 바꾸지 못했습니다.: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(result.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
alert('비밀번호를 바꾸었습니다');
|
||||
location.reload();
|
||||
}
|
||||
|
||||
async function deleteMe() {
|
||||
const pw = $('#delete_pw').val() as string;
|
||||
|
||||
if (!pw) {
|
||||
alert('비밀번호를 입력해야 합니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const global_salt = $('#global_salt').val();
|
||||
|
||||
const password = sha512(global_salt + pw + global_salt);
|
||||
|
||||
let data: InvalidResponse;
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_delete_me.php',
|
||||
responseType: 'json',
|
||||
method: 'post',
|
||||
data: convertFormData({
|
||||
pw: password
|
||||
})
|
||||
});
|
||||
data = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(`회원 탈퇴에 실패했습니다.: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.result) {
|
||||
alert(data.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
alert('탈퇴 처리되었습니다.');
|
||||
location.href = '../';
|
||||
}
|
||||
|
||||
async function extendAuth() {
|
||||
const validUntil = $('#slot_token_valid_until').html();
|
||||
const availableAt = formatTime(subDays(parseTime(validUntil), 5));
|
||||
const now = getDateTimeNow();
|
||||
|
||||
if (now < availableAt) {
|
||||
alert(`${availableAt}부터 초기화할 수 있습니다.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!confirm('로그아웃됩니다. 진행할까요?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let result: InvalidResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: '../oauth_kakao/j_reset_token.php',
|
||||
method: 'post',
|
||||
responseType: 'json'
|
||||
})
|
||||
result = response.data;
|
||||
} catch (e) {
|
||||
alert(`알 수 없는 이유로 로그인 토큰 연장에 실패했습니다. : ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(result.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
alert('초기화했습니다. 다시 로그인해 주십시오.');
|
||||
location.href = '../';
|
||||
}
|
||||
|
||||
$(function () {
|
||||
setAxiosXMLHttpRequest();
|
||||
$('#slot_icon, #slot_new_icon').attr('src', window.pathConfig.sharedIcon + '/default.jpg');
|
||||
|
||||
axios({
|
||||
url: 'j_get_user_info.php',
|
||||
method: 'post',
|
||||
responseType: 'json'
|
||||
}).then(result => {
|
||||
fillUserInfo(result.data);
|
||||
}, () => {
|
||||
alert('알 수 없는 이유로, 회원 정보를 불러오지 못했습니다.');
|
||||
location.href = 'entrance.php';
|
||||
})
|
||||
|
||||
$('#image_upload').on('change', changeIconPreview);
|
||||
|
||||
$('#btn_remove_icon').on('click', function () {
|
||||
if (confirm('아이콘을 제거할까요?')) {
|
||||
void deleteIcon();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#third_use_disallow').on('click', function () {
|
||||
if (confirm('개인정보 3자 제공 동의를 철회할까요?')) {
|
||||
void disallowThirdUse();
|
||||
}
|
||||
});
|
||||
|
||||
$('#change_pw_form').on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
void changePassword();
|
||||
});
|
||||
|
||||
$('#change_icon_form').on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
void changeIcon.apply(this as HTMLFormElement);
|
||||
});
|
||||
|
||||
$('#delete_me_form').on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
if (confirm('한 달 동안 재 가입할 수 없습니다. 정말로 탈퇴할까요?')) {
|
||||
void deleteMe();
|
||||
}
|
||||
});
|
||||
|
||||
$('#expand_login_token').on('click', extendAuth);
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="(rowValue, rowIdx) in props.params.cells" :key="rowIdx" class="row gx-1 m-0 cell-sp">
|
||||
<template v-for="colValue of rowValue" :key="colValue.target">
|
||||
<div v-if="params.data[colValue.target] == null">?</div>
|
||||
<div
|
||||
v-else-if="colValue.iActionMap"
|
||||
v-b-tooltip.hover
|
||||
class="col"
|
||||
:title="colValue.iActionMap[params.data[colValue.target] as string ].info??''"
|
||||
>
|
||||
{{ colValue.iActionMap[params.data[colValue.target] as string ].name }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="colValue.converter"
|
||||
v-b-tooltip.hover
|
||||
class="col"
|
||||
:title="colValue.converter(params.data)[1] ?? ''"
|
||||
>
|
||||
{{ colValue.converter(params.data)[0] }}
|
||||
</div>
|
||||
<div v-else v-b-tooltip.hover class="col" :title="colValue.info ?? ''">
|
||||
{{params.data[colValue.target] as string}}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { GeneralListItemP2 } from "@/defs/API/Nation";
|
||||
import type { GameIActionInfo } from "@/defs/GameObj";
|
||||
import type { CellClassParams } from "ag-grid-community";
|
||||
|
||||
//제일 큰 타입 기준
|
||||
export type GridCellInfo = {
|
||||
iActionMap?: Record<string, GameIActionInfo>;
|
||||
info?: string;
|
||||
converter?: (value: GeneralListItemP2) => [string, string?];
|
||||
target: keyof GeneralListItemP2;
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from "vue";
|
||||
interface GenCellClassParams extends CellClassParams {
|
||||
data: GeneralListItemP2;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
params: {
|
||||
type: Object as PropType<
|
||||
GenCellClassParams & {
|
||||
cells: GridCellInfo[][];
|
||||
}
|
||||
>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<span v-if="props.params.value == null">?</span>
|
||||
<span v-else-if="params.iActionMap" v-b-tooltip.hover :title="params.iActionMap[props.params.value].info ?? ''">{{
|
||||
params.iActionMap[props.params.value].name
|
||||
}}</span>
|
||||
<span v-else-if="params.info" v-b-tooltip.hover :title="params.info">{{ displayValue }}</span>
|
||||
<span v-else>{{ displayValue }}</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { GameIActionInfo } from "@/defs/GameObj";
|
||||
import type { ValueFormatterParams } from "ag-grid-community";
|
||||
import { isNumber, isString } from "lodash";
|
||||
import { ref, watch, type PropType } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
params: {
|
||||
type: Object as PropType<
|
||||
ValueFormatterParams & {
|
||||
info?: string;
|
||||
iActionMap?: Record<string, GameIActionInfo>;
|
||||
}
|
||||
>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
function convertValue(value: unknown): string {
|
||||
if (isString(value)) {
|
||||
return value;
|
||||
}
|
||||
if (isNumber(value)) {
|
||||
return value.toLocaleString();
|
||||
}
|
||||
return `${value}`;
|
||||
}
|
||||
|
||||
const displayValue = ref<string>(convertValue(props.params.value));
|
||||
watch(
|
||||
() => props.params,
|
||||
(newParams) => {
|
||||
displayValue.value = convertValue(newParams.value);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,29 @@
|
||||
import "@scss/hallOfFame.scss";
|
||||
|
||||
import $ from 'jquery';
|
||||
import { stringifyUrl } from 'query-string';
|
||||
import { initTooltip } from "@/legacy/initTooltip";
|
||||
import { auto500px } from './util/auto500px';
|
||||
import { htmlReady } from './util/htmlReady';
|
||||
import { insertCustomCSS } from './util/customCSS';
|
||||
|
||||
auto500px();
|
||||
|
||||
htmlReady(() => {
|
||||
$('#by_scenario').on('change', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const $this = $(this);
|
||||
const scenarioIdx = $this.val();
|
||||
const seasonIdx = $(this).find('option:selected').data('season');
|
||||
|
||||
document.location.href = stringifyUrl({
|
||||
url: 'a_hallOfFame.php',
|
||||
query: {
|
||||
scenarioIdx: scenarioIdx, seasonIdx: seasonIdx
|
||||
}
|
||||
});
|
||||
})
|
||||
initTooltip();
|
||||
insertCustomCSS();
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { NPCChiefActions, NPCGeneralActions } from "@/defs";
|
||||
|
||||
export const NPCPriorityBtnHelpMessage: {
|
||||
[v in NPCChiefActions | NPCGeneralActions]: string;
|
||||
} &
|
||||
Record<string, string> = {
|
||||
불가침제의:
|
||||
"군주가 NPC이고, 타국에서 원조를 받았을 때,<br>세입(금/쌀) 대비 원조량에 따라 불가침제의를 합니다.",
|
||||
선전포고:
|
||||
"군주가 NPC이고, 전쟁중이 아닐 때,<br>주변국중 하나를 골라 선포합니다.<br><br>선포 시점은 다음을 참고합니다.<br>- 인구율<br>- 도시내정률<br>- NPC전투장권장 금 충족률<br>- NPC전투장권장 쌀 충족률<br><br>국력이 낮은 국가를 조금 더 선호합니다.",
|
||||
천도: "인구가 많은 곳을 찾아 천도를 시도합니다.<br>영토의 가운데를 선호합니다.<br><br>도시 인구가 충분하다면, 굳이 천도하지는 않습니다.",
|
||||
유저장긴급포상:
|
||||
"금/쌀이 부족한 유저전투장에게 긴급하게 포상합니다.<br>국고가 권장량보다 적어지더라도 시도합니다.",
|
||||
부대전방발령:
|
||||
"(작동하지 않음)<br>전투 부대를 접경으로 발령합니다.<br>수도->시작점->도착점 경로를 따릅니다.",
|
||||
유저장구출발령:
|
||||
"아군 영토에 있지 않은 유저장을 아군 영토로 발령합니다.<br>곧 집합하는 부대에 탑승한 경우는 제외합니다.",
|
||||
유저장후방발령:
|
||||
"유저전투장 중에<br>- 병력이 충분하지 않고,<br>- 도시의 인구가 제자리 징병할 수 있을 정도로 충분하지 않고,<br>- 부대에 탑승하지 않았다면,<br>인구가 충분한 후방도시로 발령합니다.",
|
||||
부대유저장후방발령:
|
||||
"접경에 위치한 부대에 탑승한 유저전투장 중에,<br>- 병력이 충분하지 않고,<br>- 첫 턴이 징병턴이며,<br>- 부대장 집합 턴 사이라면,<br>인구가 충분한 후방도시로 발령합니다.<br><br>부대장의 위치와 유저장의 위치가 다르다면 발령하지 않습니다.",
|
||||
유저장전방발령:
|
||||
"후방에 있는 유저장이<br>- 병력을 가지고 있으며,<br>- 곧 훈련/사기진작이 완료될 것 같으면,<br>전방으로 발령합니다.<br><br>도시 관직이 많이 임명된 도시를 선호합니다.",
|
||||
유저장포상:
|
||||
"금/쌀이 부족한 유저장에게 포상합니다.<br>유저전투장과 유저내정장은 각각 기준을 따릅니다.<br>국고 권장량을 가급적 지킵니다.",
|
||||
부대후방발령:
|
||||
"(작동하지 않음)<br>후방 부대가 위치한 도시의 인구가 충분하지 않을 경우,<br>인구가 충분한 도시로 발령합니다.",
|
||||
부대구출발령:
|
||||
"전투 부대, 후방 부대가 아닌 부대가 아군 영토에 있지 않을 때,<br>전방 도시 중 하나를 골라 발령합니다.",
|
||||
NPC긴급포상:
|
||||
"금/쌀이 부족한 NPC전투장에게 긴급하게 포상합니다.<br>국고가 권장량보다 '약간' 적어지더라도 시도합니다.",
|
||||
NPC구출발령: "아군 영토에 있지 않은 NPC장을 아군 영토로 발령합니다.",
|
||||
NPC후방발령:
|
||||
"NPC전투장 중에<br>- 병력이 충분하지 않고,<br>- 도시의 인구가 제자리 징병할 수 있을 정도로 충분하지 않고,<br>- 부대에 탑승하지 않았다면,<br>인구가 충분한 후방도시로 발령합니다.",
|
||||
NPC포상:
|
||||
"금/쌀이 부족한 NPC에게 포상합니다.<br>NPC전투장과 NPC내정장은 각각 기준을 따릅니다.<br>국고 권장량을 가급적 지킵니다.",
|
||||
NPC전방발령:
|
||||
"후방에 있는 유저장이<br>- 병력을 가지고 있으며,<br>- 곧 훈련/사기진작이 완료될 것 같으면,<br>전방으로 발령합니다.<br><br>도시 관직이 많이 임명된 도시를 선호합니다.",
|
||||
유저장내정발령:
|
||||
"내정중인 유저장이 위치한 도시의 내정률이 95% 이상이면<br>개발되지 않은 도시로 발령합니다.",
|
||||
NPC내정발령:
|
||||
"내정중인 NPC장이 위치한 도시의 내정률이 95% 이상이면<br>개발되지 않은 도시로 발령합니다.",
|
||||
NPC몰수:
|
||||
"국고가 부족하다면 NPC에게서 몰수합니다. 내정NPC장은 국고가 부족하지 않아도 몰수합니다.",
|
||||
|
||||
NPC사망대비:
|
||||
"NPC의 사망까지 5턴 이내인 경우, 헌납합니다.<br>헌납할 금쌀이 없다면 물자조달을 수행합니다.",
|
||||
귀환: "아국 도시에 있지 않다면 귀환합니다.",
|
||||
금쌀구매:
|
||||
"전쟁 중에 금쌀의 비율이 크게 차이난다면 금쌀을 거래하여 비슷하게 맞춥니다.<br>금쌀 비율이 적절하는지 판단하는데 살상률을 포함합니다.<br>NPC는 상인이 없어도 금쌀을 구매할 수 있습니다.<br><br>또는 금쌀 한쪽이 지나치게 적은 경우에는 내정 중에도 금쌀을 거래합니다.",
|
||||
출병: "충분한 병력과 충분한 훈련/사기를 가지고 있는 경우 출병합니다.<br>접경이 여럿인 경우 무작위로 선택합니다.<br><br>타국과 전쟁중인 경우 공백지로는 출병하지 않습니다.",
|
||||
긴급내정:
|
||||
"전쟁중에 민심이 70 미만이거나,<br>인구가 제자리 징병이 가능하지 않을 정도로 적을 경우,<br>일정확률로 주민선정과 정착장려를 수행합니다.<br><br>통솔이 높을 수록 수행할 확률이 높습니다.",
|
||||
전투준비:
|
||||
"충분한 병력을 가지고 있지만 훈련과 사기가 부족한 경우 훈련과 사기진작을 수행합니다.",
|
||||
전방워프: "전투장이 충분한 병력을 가지고 있다면 전방으로 이동합니다.",
|
||||
NPC헌납:
|
||||
"국고가 부족한데 NPC전쟁장이 충분한 금쌀(권장량 대비 1.5배)을 가지고 있다면 일부를 헌납합니다. <br>NPC내정장은 국고가 넉넉하더라도 충분한 금쌀을 가지고 있다면 권장량만 유지하고 헌납합니다.",
|
||||
징병: "전쟁 중 병력을 소진하였다면 재 징병합니다.<br><br>기존에 사용한 병종군 중에서 사용가능한 병종을 랜덤하게 선택합니다.<br>고급 병종을 선택할 확률이 조금 더 높습니다.<br><br>NPC의 경우 도시의 인구가 충분하지 않다면 징병을 할 확률이 감소합니다.<br><br>유저장은 최대한 고급병종을 유지하며,<br>유저장 모병이 허용되는 경우 모병을 3회할 수 있다면 모병합니다.",
|
||||
후방워프:
|
||||
"전쟁 중 병력을 소진하였는데 도시의 인구가 충분하지 않다면,<br>인구가 많은 도시로 이동합니다.",
|
||||
전쟁내정:
|
||||
"전쟁 중 수행하는 내정입니다.<br>정착장려, 기술연구의 확률이 좀 더 높고,<br>치안강화, 농지개간, 상업투자의 확률이 낮습니다.<br><br>내정이 가능하다 하더라도 전시임을 고려해,<br>30% 확률로 다른 턴을 수행합니다.",
|
||||
소집해제:
|
||||
"전쟁 중이 아닌 데 병력이 남아있는 경우,<br>3/4 확률로 소집해제합니다.",
|
||||
일반내정:
|
||||
"도시에서 내정을 수행합니다. 낮은 내정일 수록 수행할 확률이 높습니다.<br>기술 연구는 1등급 이상 뒤쳐지지 않도록 노력합니다.",
|
||||
내정워프:
|
||||
"도시에서 더이상 내정을 수행할 수 없는 경우,<br>일정확률로 내정이 부족한 다른 도시로 이동합니다.",
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import $ from 'jquery';
|
||||
import '@/map';
|
||||
import { joinYearMonth } from './util/joinYearMonth';
|
||||
import { parseYearMonth } from './util/parseYearMonth';
|
||||
|
||||
declare const staticValues: {
|
||||
startYear:number;
|
||||
startMonth:number;
|
||||
|
||||
lastYear: number;
|
||||
lastMonth: number;
|
||||
|
||||
selectYear: number;
|
||||
selectMonth: number;
|
||||
}
|
||||
|
||||
|
||||
|
||||
$(function ($) {
|
||||
let currYear = staticValues.startYear;
|
||||
let currMonth = staticValues.startMonth;
|
||||
const selectDate = joinYearMonth(staticValues.selectYear, staticValues.selectMonth);
|
||||
|
||||
const $yearMonth = $('#yearmonth');
|
||||
let $elements = $();
|
||||
|
||||
const endDate = joinYearMonth(staticValues.lastYear, staticValues.lastMonth) + 1;//연감 마지막 + 1(현재)
|
||||
let currDate = joinYearMonth(currYear, currMonth);
|
||||
while (currDate <= endDate) {
|
||||
let sel = '';
|
||||
if (currDate == selectDate) {
|
||||
sel = 'selected="selected"';
|
||||
}
|
||||
|
||||
const more = currDate == endDate? ' (현재)':'';
|
||||
const option = $(`<option value="${currDate}" ${sel} >${currYear}년 ${currMonth}월${more}</option>`);
|
||||
$elements = $elements.add(option);
|
||||
|
||||
currDate += 1;
|
||||
[currYear, currMonth] = parseYearMonth(currDate);
|
||||
}
|
||||
$yearMonth.empty();
|
||||
$yearMonth.append($elements);
|
||||
});
|
||||
@@ -0,0 +1,269 @@
|
||||
import $ from "jquery";
|
||||
import 'bootstrap';
|
||||
import { setAxiosXMLHttpRequest } from "@util/setAxiosXMLHttpRequest";
|
||||
import axios from "axios";
|
||||
import type { InvalidResponse } from "@/defs";
|
||||
import { JQValidateForm, type NamedRules } from "@util/jqValidateForm";
|
||||
import { convertFormData } from "@util/convertFormData";
|
||||
import { exportWindow } from "@util/exportWindow";
|
||||
|
||||
type ResponseScenarioItem = {
|
||||
year?: number,
|
||||
title: string,
|
||||
npc_cnt: number,
|
||||
npcEx_cnt: number,
|
||||
npcNeutral_cnt: number,
|
||||
nation: Record<number, {
|
||||
id: number,
|
||||
name: string,
|
||||
color: string,
|
||||
gold: number,
|
||||
rice: number,
|
||||
infoText: string,
|
||||
tech: number,
|
||||
type: string,
|
||||
nationLevel: number,
|
||||
cities: string[],
|
||||
generals: number,
|
||||
generalsEx: number,
|
||||
generalsNeutral: number
|
||||
}>
|
||||
}
|
||||
|
||||
type ExpandedScenarioItem = ResponseScenarioItem & {
|
||||
category: string,
|
||||
idx: string,
|
||||
year: number,
|
||||
}
|
||||
|
||||
type ResponseScenario = {
|
||||
result: true,
|
||||
scenario: Record<number, ResponseScenarioItem>
|
||||
}
|
||||
|
||||
async function loadScenarios() {
|
||||
|
||||
let result: InvalidResponse | ResponseScenario;
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_load_scenarios.php',
|
||||
method: 'post',
|
||||
responseType: 'json'
|
||||
});
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
alert(`시나리오를 불러오는데 실패했습니다.: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(result.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
const list: Record<string, Record<string, ExpandedScenarioItem>> = {};
|
||||
const pat = /【(.*?)[0-9\-_.a-zA-Z]*】/;
|
||||
|
||||
for (const [idx, value] of Object.entries(result.scenario)) {
|
||||
const title = value.title || "-";
|
||||
const categoryRes = pat.exec(title);
|
||||
const category = categoryRes ? categoryRes[1] : '-';
|
||||
|
||||
if (!(category in list)) {
|
||||
list[category] = {};
|
||||
}
|
||||
|
||||
list[category][idx] = {
|
||||
...value,
|
||||
title: title,
|
||||
category: category,
|
||||
idx: idx,
|
||||
year: value.year ?? 180
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const $select = $('#scenario_sel');
|
||||
for (const [category, items] of Object.entries(list)) {
|
||||
const $optgroup = $('<optgroup>').attr('label', category);
|
||||
for (const [idx, scenario] of Object.entries(items)) {
|
||||
const $option = $('<option>')
|
||||
.data('scenario', scenario)
|
||||
.val(idx)
|
||||
.html(scenario.title);
|
||||
$optgroup.append($option);
|
||||
}
|
||||
$select.append($optgroup);
|
||||
|
||||
}
|
||||
|
||||
$select.val(0);
|
||||
$select.trigger('change');
|
||||
}
|
||||
|
||||
function scenarioPreview() {
|
||||
const $select = $('#scenario_sel');
|
||||
const $option = $select.find('option:selected');
|
||||
|
||||
const $year = $('#scenario_begin');
|
||||
const $npc = $('#scenario_npc');
|
||||
const $npcEx = $('#scenario_npc_extend');
|
||||
const $nation = $('#scenario_nation');
|
||||
|
||||
const scenario = $option.data('scenario') as ExpandedScenarioItem;
|
||||
console.log(scenario.idx, scenario.title);
|
||||
|
||||
$year.html(`${scenario.year}년`);
|
||||
$npc.html(`${scenario.npc_cnt}명`);
|
||||
if (scenario.npcEx_cnt == 0) {
|
||||
$npcEx.html('');
|
||||
} else {
|
||||
$npcEx.html(`+${scenario.npcEx_cnt}명`);
|
||||
}
|
||||
|
||||
$nation.html('');
|
||||
$.each(scenario.nation, function (idx, nation) {
|
||||
$nation.append(`<span style="color:${nation.color}">${nation.name}</span> ${nation.generals}명. ${nation.cities.join(', ')}<br>`);
|
||||
});
|
||||
}
|
||||
|
||||
type InstallFormType = {
|
||||
turnterm: number,
|
||||
sync: 1|0,
|
||||
scenario: number,
|
||||
fiction: number,
|
||||
extend: number,
|
||||
npcmode: number,
|
||||
show_img_level: number,
|
||||
tournament_trig: number,
|
||||
join_mode: number,
|
||||
autorun_user: string[],
|
||||
autorun_user_minutes: number,
|
||||
reserve_open?: string,
|
||||
pre_reserve_open?: string,
|
||||
}
|
||||
const descriptor: NamedRules<InstallFormType> = {
|
||||
turnterm: {
|
||||
required: true,
|
||||
type: "enum",
|
||||
enum: [1, 2, 5, 10, 20, 30, 60, 120],
|
||||
transform: parseInt,
|
||||
},
|
||||
sync: {
|
||||
required: true,
|
||||
type: "enum",
|
||||
enum: [1, 0],
|
||||
transform: parseInt,
|
||||
},
|
||||
scenario: {
|
||||
required: true,
|
||||
},
|
||||
fiction: {
|
||||
required: true,
|
||||
type: "integer",
|
||||
transform: parseInt,
|
||||
},
|
||||
extend: {
|
||||
required: true,
|
||||
type: "integer",
|
||||
transform: parseInt,
|
||||
},
|
||||
npcmode: {
|
||||
required: true,
|
||||
type: "integer",
|
||||
transform: parseInt,
|
||||
},
|
||||
show_img_level: {
|
||||
required: true,
|
||||
type: "integer",
|
||||
transform: parseInt,
|
||||
},
|
||||
tournament_trig: {
|
||||
required: true,
|
||||
type: "integer",
|
||||
transform: parseInt,
|
||||
},
|
||||
join_mode: {
|
||||
required: true,
|
||||
type: "enum",
|
||||
enum: ["full", "onlyRandom"]
|
||||
},
|
||||
autorun_user: {
|
||||
type: 'array',
|
||||
required: false,
|
||||
defaultField: {
|
||||
type: 'enum',
|
||||
enum: ['develop', 'warp', 'recruit', 'recruit_high', 'train', 'battle', 'chief']
|
||||
},
|
||||
},
|
||||
autorun_user_minutes: {
|
||||
type: 'integer',
|
||||
required: true,
|
||||
transform: parseInt,
|
||||
min: 0,
|
||||
validator: (rule, value, _callback, source) => {
|
||||
if(value == 0 && !source.autorun_user?.length){
|
||||
return true;
|
||||
}
|
||||
if(value != 0 && source.autorun_user?.length){
|
||||
return true;
|
||||
}
|
||||
return new Error('유효 시간과 옵션은 동시에 설정해야합니다.');
|
||||
}
|
||||
},
|
||||
reserve_open: {
|
||||
type: 'pattern',
|
||||
pattern: new RegExp(/\d{4,4}-\d{2,2}-\d{2,2} \d{2,2}:\d{2,2}/)
|
||||
},
|
||||
pre_reserve_open: {
|
||||
type: 'pattern',
|
||||
pattern: new RegExp(/\d{4,4}-\d{2,2}-\d{2,2} \d{2,2}:\d{2,2}/)
|
||||
}
|
||||
};
|
||||
|
||||
function formSetup() {
|
||||
const validator = new JQValidateForm($('#game_form'), descriptor);
|
||||
validator.installChangeHandler();
|
||||
|
||||
$('#game_form').on('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
const values = await validator.validate();
|
||||
if (values === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
let result: InvalidResponse;
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_install.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData(values)
|
||||
});
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
alert(`알수 없는 에러: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(result.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
alert('게임이 리셋되었습니다.');
|
||||
location.href = '..';
|
||||
});
|
||||
}
|
||||
|
||||
$(async function () {
|
||||
setAxiosXMLHttpRequest();
|
||||
void loadScenarios();
|
||||
$('#scenario_sel').on('change', scenarioPreview);
|
||||
formSetup();
|
||||
|
||||
})
|
||||
|
||||
exportWindow($, '$');
|
||||
@@ -0,0 +1,84 @@
|
||||
|
||||
import axios from 'axios';
|
||||
import jQuery from 'jquery';
|
||||
import { JQValidateForm, type NamedRules } from '@util/jqValidateForm';
|
||||
import { convertFormData } from '@util/convertFormData';
|
||||
import type { InvalidResponse } from '@/defs';
|
||||
import { setAxiosXMLHttpRequest } from '@util/setAxiosXMLHttpRequest';
|
||||
|
||||
jQuery(async function ($) {
|
||||
setAxiosXMLHttpRequest();
|
||||
|
||||
type FormValue = {
|
||||
full_reset: string,
|
||||
db_host: string,
|
||||
db_port: number,
|
||||
db_id: string,
|
||||
db_pw: string,
|
||||
db_name: string,
|
||||
}
|
||||
const descriptor: NamedRules<FormValue> = {
|
||||
full_reset: {
|
||||
required: true,
|
||||
},
|
||||
db_host: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
db_port: {
|
||||
type: 'integer',
|
||||
transform: parseInt,
|
||||
validator: (rule, value: number) => {
|
||||
if (value <= 0 || value >= 65535) {
|
||||
return new Error('올바른 포트 범위가 아닙니다.');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
db_id: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
db_pw: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
db_name: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
}
|
||||
};
|
||||
const validator = new JQValidateForm($('#db_form'), descriptor);
|
||||
validator.installChangeHandler();
|
||||
$('#db_form').on('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const items = await validator.validate();
|
||||
if(items === undefined){
|
||||
return;
|
||||
}
|
||||
|
||||
let data: InvalidResponse;
|
||||
|
||||
try{
|
||||
const response = await axios({
|
||||
url: 'j_install_db.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData(items)
|
||||
})
|
||||
data = response.data as InvalidResponse;
|
||||
}
|
||||
catch(e){
|
||||
alert(e);
|
||||
return false;
|
||||
}
|
||||
if(!data.result){
|
||||
alert(`에러: ${data.reason}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
alert('DB.php가 생성되었습니다.');
|
||||
location.href = 'install.php';
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import $ from "jquery";
|
||||
|
||||
export function activateFlip($obj?: JQuery<HTMLElement>): void {
|
||||
let $result: JQuery<HTMLElement>;
|
||||
if ($obj === undefined) {
|
||||
$result = $('img[data-flip]');
|
||||
} else {
|
||||
$result = $obj.find('img[data-flip]');
|
||||
}
|
||||
|
||||
$result.each(function () {
|
||||
activeFlipItem($(this));
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
export function activeFlipItem($img: JQuery<HTMLElement>): void {
|
||||
const imageList = [];
|
||||
imageList.push($img.attr('src'));
|
||||
$.each($img.data('flip').split(','), function (idx, value) {
|
||||
value = $.trim(value);
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
imageList.push(value);
|
||||
});
|
||||
if (imageList.length <= 1) {
|
||||
return;
|
||||
}
|
||||
$img.data('computed_flip_array', imageList);
|
||||
$img.data('computed_flip_idx', 0);
|
||||
|
||||
$img.click(function () {
|
||||
const arr = $img.data('computed_flip_array');
|
||||
let idx = $img.data('computed_flip_idx');
|
||||
idx = (idx + 1) % (arr.length);
|
||||
$img.attr('src', arr[idx]);
|
||||
$img.data('computed_flip_idx', idx);
|
||||
});
|
||||
$img.css('cursor', 'pointer');
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* <>& 등을 html에서도 그대로 보이도록 escape주는 함수
|
||||
* @see https://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
|
||||
*/
|
||||
const entityMap: { [v: string]: string } = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
'`': '`',
|
||||
'=': '='
|
||||
};
|
||||
export function escapeHtml(string: string): string{
|
||||
return String(string).replace(/[&<>"'`=/]/g, function (s: string) {
|
||||
return entityMap[s];
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import Tooltip from "bootstrap/js/dist/tooltip";
|
||||
//HACK: 이유는 잘 모르겠지만 bootstrap-vue3에서 bootstrap 호출하는 것과 충돌하여 우회 중
|
||||
import $ from "jquery";
|
||||
import { trim } from "lodash";
|
||||
|
||||
export function initTooltip($obj?: JQuery<HTMLElement>): void {
|
||||
if ($obj === undefined) {
|
||||
$obj = $('.obj_tooltip');
|
||||
} else if (!$obj.hasClass('obj_tooltip')) {
|
||||
$obj = $obj.find('.obj_tooltip');
|
||||
}
|
||||
|
||||
$obj.each(function () {
|
||||
const $target = $(this);
|
||||
|
||||
if ($target.data('installHandler')) {
|
||||
return;
|
||||
}
|
||||
$target.data('installHandler', true);
|
||||
|
||||
$target.on('mouseover', function () {
|
||||
const $objTooltip = $(this);
|
||||
if ($objTooltip.data('setObjTooltip')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tooltipClassText = $objTooltip.data('tooltip-class');
|
||||
if (!tooltipClassText) {
|
||||
tooltipClassText = '';
|
||||
}
|
||||
const template = `<div class="tooltip ${tooltipClassText}" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>`;
|
||||
|
||||
const oTooltip = new Tooltip(this, {
|
||||
title: function () {
|
||||
return trim(this.querySelector('.tooltiptext')?.innerHTML);
|
||||
},
|
||||
template: template,
|
||||
html: true
|
||||
});
|
||||
oTooltip.show();
|
||||
|
||||
$objTooltip.data('setObjTooltip', true);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import jQuery from 'jquery';
|
||||
import { activateFlip } from "@/legacy/activateFlip";
|
||||
import { unwrap } from '@util/unwrap';
|
||||
import { htmlReady } from '@util/htmlReady';
|
||||
import { initTooltip } from './initTooltip';
|
||||
import { exportWindow } from '@/util/exportWindow';
|
||||
import { reloadWorldMap } from '@/map';
|
||||
import { refreshMsg } from '@/msg';
|
||||
|
||||
exportWindow(jQuery, '$');
|
||||
|
||||
|
||||
htmlReady(() => {
|
||||
for(const refreshBtn of document.querySelectorAll('.refreshPage')){
|
||||
refreshBtn.addEventListener('click', function () {
|
||||
document.location.reload();
|
||||
return false;
|
||||
})
|
||||
}
|
||||
|
||||
for(const openWindowBtn of document.querySelectorAll('.open-window')){
|
||||
openWindowBtn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
console.log(e);
|
||||
let target: HTMLElement | null = e.target as HTMLElement;
|
||||
while (target !== null) {
|
||||
if(target.tagName != 'a' && (target as HTMLAnchorElement).href){
|
||||
break;
|
||||
}
|
||||
target = target.parentElement;
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.open((target as HTMLAnchorElement).href);
|
||||
});
|
||||
}
|
||||
|
||||
activateFlip();
|
||||
initTooltip();
|
||||
|
||||
void reloadWorldMap({
|
||||
hrefTemplate: 'b_currentCity.php?citylist={0}',
|
||||
useCachedMap: true
|
||||
});
|
||||
|
||||
setInterval(function() {
|
||||
void refreshMsg();
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
(() => {
|
||||
|
||||
let finInit = false;
|
||||
|
||||
let nationMsgBox!: HTMLElement;
|
||||
let nationMsg!: HTMLElement;
|
||||
let nationMsgHeight: number | undefined = undefined;
|
||||
|
||||
function init() {
|
||||
if (finInit) {
|
||||
return false;
|
||||
}
|
||||
const _nationMsgBox = document.getElementById('nation-msg-box');
|
||||
if (!_nationMsgBox) {
|
||||
return false;
|
||||
}
|
||||
finInit = true;
|
||||
|
||||
nationMsgBox = _nationMsgBox;
|
||||
nationMsg = unwrap(document.getElementById('nation-msg'));
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
if (!finInit && !init()) return;
|
||||
|
||||
if (nationMsgBox.offsetWidth < nationMsg.offsetWidth) {
|
||||
if (nationMsgHeight === undefined) {
|
||||
nationMsgHeight = nationMsgBox.offsetHeight;
|
||||
nationMsgBox.style.height = `${nationMsgHeight / 2}px`;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (nationMsgBox.style.height) {
|
||||
nationMsgHeight = undefined;
|
||||
nationMsgBox.style.height = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
htmlReady(() => {
|
||||
init();
|
||||
onScroll();
|
||||
window.addEventListener('scroll', onScroll, true);
|
||||
window.addEventListener('orientationchange', onScroll, true);
|
||||
});
|
||||
})();
|
||||
+790
@@ -0,0 +1,790 @@
|
||||
import axios from 'axios';
|
||||
import $ from 'jquery';
|
||||
import { isNumber, merge } from 'lodash';
|
||||
import { convColorValue, convertDictById, stringFormat } from '@/common_legacy';
|
||||
import type { InvalidResponse, MapResult } from '@/defs';
|
||||
import { unwrap } from "@util/unwrap";
|
||||
import { convertFormData } from '@util/convertFormData';
|
||||
import { exportWindow } from '@util/exportWindow';
|
||||
import { htmlReady } from './util/htmlReady';
|
||||
import { initTooltip } from './legacy/initTooltip';
|
||||
|
||||
declare const staticValues: {
|
||||
serverNick: string;
|
||||
serverID: string;
|
||||
}
|
||||
|
||||
const { serverNick, serverID } = staticValues;
|
||||
|
||||
type CityPositionMap = {
|
||||
[cityID: number]: [string, number, number];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
sam_toggleSingleTap?: boolean,
|
||||
getCityPosition: () => CityPositionMap;
|
||||
formatCityInfo: (city: MapCityParsedRaw) => MapCityParsedRegionLevelText
|
||||
}
|
||||
}
|
||||
|
||||
type MapCityParsedRaw = {
|
||||
id: number,
|
||||
level: number,
|
||||
state: number,
|
||||
nationID?: number,
|
||||
region: number,
|
||||
supply: boolean
|
||||
};
|
||||
|
||||
type MapCityParsedName = MapCityParsedRaw & {
|
||||
name: string,
|
||||
x: number,
|
||||
y: number,
|
||||
};
|
||||
|
||||
type MapCityParsedNation = MapCityParsedName & {
|
||||
nationID?: number;
|
||||
nation?: string;
|
||||
color?: string;
|
||||
isCapital: boolean
|
||||
}
|
||||
|
||||
type MapCityParsedClickable = MapCityParsedNation & {
|
||||
clickable: number
|
||||
}
|
||||
|
||||
type MapCityParsedRegionLevelText = MapCityParsedClickable & {
|
||||
region_str: string,
|
||||
level_str: string,
|
||||
text: string,
|
||||
}
|
||||
|
||||
export type MapCityParsed = MapCityParsedRegionLevelText;
|
||||
|
||||
type MapCityDrawable = {
|
||||
cityList: MapCityParsed[],
|
||||
myCity?: number
|
||||
};
|
||||
|
||||
type MapNationParsed = {
|
||||
id: number,
|
||||
name: string,
|
||||
color: string,
|
||||
capital: number
|
||||
}
|
||||
|
||||
type MapRawResult = InvalidResponse | MapResult;
|
||||
|
||||
function is_touch_device(): boolean {
|
||||
const prefixes = ' -webkit- -moz- -o- -ms- '.split(' ');
|
||||
const mq = (query: string) => {
|
||||
return window.matchMedia(query).matches;
|
||||
}
|
||||
|
||||
type TouchWindow = { DocumentTouch?: () => void | Document };
|
||||
const tWindow = window as unknown as TouchWindow;
|
||||
if (('ontouchstart' in window) || (tWindow.DocumentTouch && (document instanceof tWindow.DocumentTouch))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// include the 'heartz' as a way to have a non matching MQ to help terminate the join
|
||||
// https://git.io/vznFH
|
||||
const query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join('');
|
||||
return mq(query);
|
||||
}
|
||||
|
||||
export type loadMapOption = {
|
||||
isDetailMap?: boolean, //복잡 지도, 단순 지도
|
||||
clickableAll?: boolean, //어떤 경우든 클릭을 가능하게 함. 해당 동작의 동작 가능성 여부와는 별도.
|
||||
selectCallback?: (city: MapCityParsed) => void, //callback을 지정시 clickable과 관계 없이 해당 함수를 실행.
|
||||
hrefTemplate?: string, //도시가 클릭가능할 경우 지정할 href값. {0}은 도시 id로 변환됨
|
||||
useCachedMap?: boolean, //맵 캐시를 사용
|
||||
|
||||
//아래부터는 post query에 들어갈 녀석
|
||||
year?: number, //year값, 연감등에 사용
|
||||
month?: number, //month값, 연감등에 사용
|
||||
aux?: Record<number | string, unknown>, //기타 넣고 싶은 값을 입력
|
||||
neutralView?: boolean, //clickable, 소속 국가, 첩보 여부 등을 반환여부를 설정
|
||||
showMe?: boolean, //반환 값에 본인이 위치한 도시 값을 반환하도록 설정. neutralView와 별개
|
||||
|
||||
targetJson?: string,
|
||||
reqType?: 'get' | 'post',
|
||||
dynamicMapTheme?: boolean,
|
||||
callback?: (a: MapCityDrawable, rawObejct: MapRawResult) => void,
|
||||
|
||||
//기타 보조 값
|
||||
startYear?: number,
|
||||
}
|
||||
|
||||
export async function reloadWorldMap(option: loadMapOption, drawTarget = '.world_map'): Promise<void> {
|
||||
const $world_map = $(drawTarget);
|
||||
|
||||
if ($world_map.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultOption: loadMapOption = {
|
||||
isDetailMap: true, //복잡 지도, 단순 지도
|
||||
clickableAll: false, //어떤 경우든 클릭을 가능하게 함. 해당 동작의 동작 가능성 여부와는 별도.
|
||||
selectCallback: undefined, //callback을 지정시 clickable과 관계 없이 해당 함수를 실행.
|
||||
hrefTemplate: '#', //도시가 클릭가능할 경우 지정할 href값. {0}은 도시 id로 변환됨
|
||||
useCachedMap: false, //맵 캐시를 사용
|
||||
|
||||
//아래부터는 post query에 들어갈 녀석
|
||||
year: undefined, //year값, 연감등에 사용
|
||||
month: undefined, //month값, 연감등에 사용
|
||||
aux: undefined, //기타 넣고 싶은 값을 입력
|
||||
neutralView: false, //clickable, 소속 국가, 첩보 여부 등을 반환여부를 설정
|
||||
showMe: true, //반환 값에 본인이 위치한 도시 값을 반환하도록 설정. neutralView와 별개
|
||||
|
||||
targetJson: 'j_map.php',
|
||||
reqType: 'post',
|
||||
dynamicMapTheme: false,
|
||||
callback: undefined,
|
||||
|
||||
//기타 보조 값
|
||||
startYear: undefined,
|
||||
};
|
||||
|
||||
option = merge({}, defaultOption, option);
|
||||
|
||||
const useCachedMap = option.useCachedMap;
|
||||
const isDetailMap = option.isDetailMap;
|
||||
const clickableAll = option.clickableAll;
|
||||
const selectCallback = option.selectCallback;
|
||||
const hrefTemplate = unwrap(option.hrefTemplate);
|
||||
|
||||
const cityPosition = window.getCityPosition();
|
||||
|
||||
const storedOldMapKey = `sam.${serverNick}.map`;
|
||||
const storedStartYear = `sam.${serverNick}.startYear`;
|
||||
//OBJ : startYear, year, month, cityList, nationList, spyList, shownByGeneralList, myCity
|
||||
|
||||
async function checkReturnObject(obj: MapRawResult): Promise<MapResult> {
|
||||
if (!obj.result) {
|
||||
throw `fail: ${obj.reason}`;
|
||||
}
|
||||
|
||||
if (!isNumber(obj.startYear) ||
|
||||
!isNumber(obj.year) ||
|
||||
!isNumber(obj.month)
|
||||
) {
|
||||
throw 'fail: date type';
|
||||
}
|
||||
|
||||
if (useCachedMap) {
|
||||
localStorage.setItem(storedOldMapKey, JSON.stringify([serverID, obj]));
|
||||
localStorage.setItem(storedStartYear, JSON.stringify(obj.startYear));
|
||||
}
|
||||
|
||||
$world_map.removeClass('draw_required');
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function setMapBackground(obj: MapResult): Promise<MapResult> {
|
||||
function setTheme() {
|
||||
const oldTheme = $world_map.data('currentTheme');
|
||||
const newTheme = obj.theme;
|
||||
if (oldTheme === newTheme) {
|
||||
return;
|
||||
}
|
||||
if (oldTheme) {
|
||||
$world_map.removeClass('map_theme_' + oldTheme);
|
||||
}
|
||||
$world_map.addClass('map_theme_' + newTheme);
|
||||
$world_map.data('currentTheme', newTheme ?? '_current');//FIXME: 뭔가 틀렸음. 전송시에 theme가 있어야하나?
|
||||
}
|
||||
if (option.dynamicMapTheme) {
|
||||
setTheme();
|
||||
}
|
||||
const startYear = obj.startYear;
|
||||
const year = obj.year;
|
||||
const month = obj.month;
|
||||
|
||||
if (isDetailMap) {
|
||||
$world_map.addClass('map_detail').removeClass('map_basic');
|
||||
} else {
|
||||
$world_map.addClass('map_basic').removeClass('map_detail');
|
||||
}
|
||||
|
||||
const $map_title = $('.map_title_text');
|
||||
if (year < startYear + 1) {
|
||||
$map_title.css('color', 'magenta');
|
||||
} else if (year < startYear + 2) {
|
||||
$map_title.css('color', 'orange');
|
||||
} else if (year < startYear + 3) {
|
||||
$map_title.css('color', 'yellow');
|
||||
}
|
||||
|
||||
const $map_title_tooltip = $('.map_title .tooltiptext');
|
||||
$map_title_tooltip.empty();
|
||||
|
||||
const tooltipTexts = [];
|
||||
if (year < startYear + 3) {
|
||||
const startYearText = [];
|
||||
let remainYear = startYear + 3 - year;
|
||||
const remainMonth = 12 - month + 1;
|
||||
if (remainMonth > 0) {
|
||||
remainYear -= 1;
|
||||
}
|
||||
if (remainYear) {
|
||||
startYearText.push(`${remainYear}년`);
|
||||
}
|
||||
if (remainMonth) {
|
||||
startYearText.push(`${remainMonth}개월`);
|
||||
}
|
||||
|
||||
tooltipTexts.push(`초반제한 기간 : ${startYearText.join(' ')} (${startYear + 3}년)`);
|
||||
}
|
||||
|
||||
const currentTechLimit = Math.floor(Math.max(0, year - startYear) / 5) + 1;
|
||||
const nextTechLimitYear = currentTechLimit * 5 + startYear;
|
||||
|
||||
tooltipTexts.push(`기술등급 제한 : ${currentTechLimit}등급 (${nextTechLimitYear}년 해제)`);
|
||||
$map_title_tooltip.html(tooltipTexts.join('<br>'));
|
||||
|
||||
$world_map.removeClass('map_string map_summer map_fall map_winter');
|
||||
if (month <= 3) {
|
||||
$world_map.addClass('map_spring');
|
||||
} else if (month <= 6) {
|
||||
$world_map.addClass('map_summer');
|
||||
} else if (month <= 9) {
|
||||
$world_map.addClass('map_fall');
|
||||
} else {
|
||||
$world_map.addClass('map_winter');
|
||||
}
|
||||
|
||||
$map_title.html(`${year}年 ${month}月`);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
async function convertCityObjs(obj: MapResult): Promise<MapCityDrawable> {
|
||||
//원본 Obj는 굉장히 간소하게 온다, Object 형태로 변환해서 사용한다.
|
||||
|
||||
function toCityObj([id, level, state, nationID, region, supply]: MapResult['cityList'][0]): MapCityParsedRaw {
|
||||
return {
|
||||
id: id,
|
||||
level: level,
|
||||
state: state,
|
||||
nationID: nationID > 0 ? nationID : undefined,
|
||||
region: region,
|
||||
supply: supply != 0
|
||||
};
|
||||
}
|
||||
|
||||
function toNationObj([id, name, color, capital]: MapResult['nationList'][0]): MapNationParsed {
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
color,
|
||||
capital
|
||||
};
|
||||
}
|
||||
|
||||
const nationList = convertDictById(obj.nationList.map(toNationObj)); //array of object -> dict
|
||||
|
||||
const spyList = obj.spyList;
|
||||
const shownByGeneralList = new Set(obj.shownByGeneralList);
|
||||
|
||||
const myCity = obj.myCity;
|
||||
const myNation = obj.myNation;
|
||||
|
||||
|
||||
function mergePositionInfo(city: MapCityParsedRaw): MapCityParsedName {
|
||||
const id = city.id;
|
||||
if (!(id in cityPosition)) {
|
||||
throw TypeError(`알수 없는 cityID: ${id}`);
|
||||
}
|
||||
const [name, x, y] = cityPosition[id];
|
||||
|
||||
return {
|
||||
...city,
|
||||
name,
|
||||
x,
|
||||
y,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeNationInfo(city: MapCityParsedName): MapCityParsedNation {
|
||||
//nationID 값으로 isCapital, color, nation을 통합
|
||||
|
||||
const nationID = city.nationID;
|
||||
if (nationID === undefined || !(nationID in nationList)) {
|
||||
return {
|
||||
...city,
|
||||
isCapital: false,
|
||||
};
|
||||
}
|
||||
|
||||
const nationObj = nationList[nationID];
|
||||
return {
|
||||
...city,
|
||||
nation: nationObj.name,
|
||||
color: nationObj.color,
|
||||
isCapital: nationObj.capital == city.id
|
||||
};
|
||||
}
|
||||
|
||||
function mergeClickable(city: MapCityParsedNation): MapCityParsedClickable {
|
||||
//clickable = (defaultCity << 4 ) | (remainSpy << 3) | (ourCity << 2) | (shownByGeneral << 1) | clickableAll
|
||||
const id = city.id;
|
||||
const nationID = city.nationID;
|
||||
let clickable = 16;
|
||||
if (id in spyList) {
|
||||
clickable |= spyList[id] << 3;
|
||||
}
|
||||
if (myNation !== null && nationID == myNation) {
|
||||
clickable |= 4;
|
||||
}
|
||||
if (shownByGeneralList.has(id)) {
|
||||
clickable |= 2;
|
||||
}
|
||||
if (myCity !== null && id == myCity) {
|
||||
clickable |= 2;
|
||||
}
|
||||
if (clickableAll) {
|
||||
clickable |= 1;
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
...city,
|
||||
clickable
|
||||
};
|
||||
}
|
||||
|
||||
const cityList = obj.cityList.map(toCityObj)
|
||||
.map(mergePositionInfo)
|
||||
.map(mergeNationInfo)
|
||||
.map(mergeClickable)
|
||||
.map(window.formatCityInfo);
|
||||
|
||||
return {
|
||||
'cityList': cityList,
|
||||
'myCity': myCity
|
||||
};
|
||||
}
|
||||
|
||||
function drawDetailWorldMap(obj: MapCityDrawable) {
|
||||
|
||||
const $map_body = $(drawTarget + ' .map_body');
|
||||
|
||||
const cityList = obj.cityList;
|
||||
const myCity = obj.myCity;
|
||||
|
||||
cityList.forEach(function (city) {
|
||||
const id = city.id;
|
||||
$(`.city_base_${id}`).detach();
|
||||
//이전 도시는 지운다.
|
||||
|
||||
const $cityObj = $(`<div class="city_base city_base_${id}"></div>`);
|
||||
$cityObj.addClass(`city_level_${city.level}`);
|
||||
$cityObj.data('obj', city).css({ 'left': city.x - 20, 'top': city.y - 15 });
|
||||
|
||||
if (city.color !== undefined) {
|
||||
const $bgObj = $('<div class="city_bg"></div>');
|
||||
$cityObj.append($bgObj);
|
||||
$bgObj.css({ 'background-image': `url(${window.pathConfig.gameImage}/b${convColorValue(city.color)}.png)` });
|
||||
}
|
||||
|
||||
const $linkObj = $('<a class="city_link"></a>');
|
||||
$linkObj.data({ 'text': city.text, 'nation': city.nation, 'id': city.id });
|
||||
$cityObj.append($linkObj);
|
||||
|
||||
const $imgObj = $(`<div class="city_img"><img src="${window.pathConfig.gameImage}/cast_${city.level}.gif"><div class="city_filler"></div></div>`);
|
||||
$linkObj.append($imgObj);
|
||||
|
||||
if (city.state > 0) {
|
||||
const $stateObj = $(`<div class="city_state"><img src="${window.pathConfig.gameImage}/event${city.state}.gif"></div>`);
|
||||
$linkObj.append($stateObj);
|
||||
}
|
||||
|
||||
if (city.nationID && city.nationID > 0) {
|
||||
const flagType = city.supply ? 'f' : 'd';
|
||||
const $flagObj = $(`<div class="city_flag"><img src="${window.pathConfig.gameImage}/${flagType}${convColorValue(unwrap(city.color))}.gif"></div>`);
|
||||
if (city.isCapital) {
|
||||
const $capitalObj = $(`<div class="city_capital"><img src="${window.pathConfig.gameImage}/event51.gif"></div>`);
|
||||
$flagObj.append($capitalObj);
|
||||
}
|
||||
$imgObj.append($flagObj);
|
||||
}
|
||||
|
||||
const $nameObj = $(`<span class="city_detail_name">${city.name}</span>`);
|
||||
$imgObj.append($nameObj);
|
||||
|
||||
|
||||
$map_body.append($cityObj);
|
||||
|
||||
|
||||
});
|
||||
|
||||
if (myCity) {
|
||||
$world_map.find(`.city_base_${myCity} .city_filler`).addClass('my_city');
|
||||
}
|
||||
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function drawBasicWorldMap(obj: MapCityDrawable) {
|
||||
|
||||
const $map_body = $(`${drawTarget} .map_body`);
|
||||
|
||||
const cityList = obj.cityList;
|
||||
const myCity = obj.myCity;
|
||||
|
||||
cityList.forEach(function (city) {
|
||||
const id = city.id;
|
||||
$(`.city_base_${id}`).detach();
|
||||
//이전 도시는 지운다.
|
||||
|
||||
const $cityObj = $(`<div class="city_base city_base_${id}"></div>`);
|
||||
$cityObj.addClass(`city_level_${city.level}`);
|
||||
$cityObj.data('obj', city).css({ 'left': city.x - 20, 'top': city.y - 15 });
|
||||
|
||||
const $linkObj = $('<a class="city_link"></a>');
|
||||
$linkObj.data({ 'text': city.text, 'nation': city.nation, 'id': city.id });
|
||||
$cityObj.append($linkObj);
|
||||
|
||||
const $imgObj = $('<div class="city_img"><div class="city_filler"></div></div>');
|
||||
if (city.color !== undefined) {
|
||||
$imgObj.css({ 'background-color': city.color });
|
||||
}
|
||||
$linkObj.append($imgObj);
|
||||
|
||||
if (city.state > 0) {
|
||||
let state_text = 'wrong';
|
||||
if (city.state < 10) {
|
||||
state_text = 'good';
|
||||
} else if (city.state < 40) {
|
||||
state_text = 'bad';
|
||||
} else if (city.state < 50) {
|
||||
state_text = 'war';
|
||||
}
|
||||
|
||||
const $stateObj = $(`<div class="city_state city_state_${state_text}"></div>`);
|
||||
$imgObj.append($stateObj);
|
||||
}
|
||||
|
||||
//단순 표기에서는 깃발 여부가 없음
|
||||
if (city.isCapital) {
|
||||
const $capitalObj = $('<div class="city_capital"></div>');
|
||||
$imgObj.append($capitalObj);
|
||||
}
|
||||
|
||||
const $nameObj = $(`<span class="city_detail_name">${city.name}</span>`);
|
||||
$imgObj.append($nameObj);
|
||||
|
||||
$map_body.append($cityObj);
|
||||
});
|
||||
|
||||
if (myCity) {
|
||||
$world_map.find(`.city_base_${myCity} .city_filler`).addClass('my_city');
|
||||
}
|
||||
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function setMouseWork(obj: MapCityDrawable) {
|
||||
|
||||
initTooltip($(drawTarget));
|
||||
|
||||
const $tooltip = $(drawTarget + ' .city_tooltip');
|
||||
const $tooltip_city = $tooltip.find('.city_name');
|
||||
const $tooltip_nation = $tooltip.find('.nation_name');
|
||||
|
||||
const $objs = $(drawTarget + ' .city_link');
|
||||
|
||||
const $map_body = $(drawTarget + ' .map_body');
|
||||
|
||||
//터치스크린 탭
|
||||
|
||||
if (!option.neutralView && is_touch_device()) {
|
||||
$objs.on('touchstart', function () {
|
||||
if (window.sam_toggleSingleTap) {
|
||||
return true;
|
||||
}
|
||||
const $this = $(this);
|
||||
|
||||
const touchMode = $this.data('touchMode') as number;
|
||||
if ($tooltip_city.data('target') != $this.data('id')) {
|
||||
$this.data('touchMode', 1);
|
||||
} else if (touchMode === undefined) {
|
||||
$this.data('touchMode', 1);
|
||||
} else {
|
||||
$this.data('touchMode', touchMode + 1);
|
||||
}
|
||||
$map_body.data('touchMode', 1);
|
||||
|
||||
$tooltip_city.data('target', $this.data('id'));
|
||||
|
||||
|
||||
});
|
||||
|
||||
$objs.on('touchend', function () {
|
||||
if (window.sam_toggleSingleTap) {
|
||||
return true;
|
||||
}
|
||||
const $this = $(this);
|
||||
const position = $this.parent().position();
|
||||
$tooltip_city.html($this.data('text') as string);
|
||||
|
||||
const nation_text = $this.data('nation') as string;
|
||||
if (nation_text) {
|
||||
$tooltip_nation.html(nation_text).show();
|
||||
} else {
|
||||
$tooltip_nation.html('').hide();
|
||||
}
|
||||
|
||||
const left = position.left;
|
||||
const top = position.top;
|
||||
|
||||
const tooltipWidth = unwrap($tooltip.width());
|
||||
|
||||
if (left + tooltipWidth + 35 > window.innerWidth) {
|
||||
$tooltip.css({ 'top': top + 45, 'left': left - tooltipWidth - 10 }).show();
|
||||
}
|
||||
else {
|
||||
$tooltip.css({ 'top': top + 45, 'left': left + 35 }).show();
|
||||
}
|
||||
|
||||
const touchMode = $this.data('touchMode') as number;
|
||||
if (touchMode <= 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//xxx: touchend 다음 click 이벤트가 갈 수도 있고, 안 갈 수도 있다.
|
||||
$this.data('touchMode', 0);
|
||||
});
|
||||
|
||||
$map_body.on('touchend', function () {
|
||||
if (window.sam_toggleSingleTap) {
|
||||
return true;
|
||||
}
|
||||
//위의 touchend bind에 해당하지 않는 경우 -> 빈 지도 터치
|
||||
$tooltip.hide();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
//Mouse over 모드 작동
|
||||
|
||||
$map_body.on('mousemove', function (e) {
|
||||
if ($(this).data('touchMode')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const rect = this.getBoundingClientRect();
|
||||
const left = (e.clientX - rect.left - this.clientLeft + this.scrollLeft);
|
||||
const top = (e.clientY - rect.top - this.clientTop + this.scrollTop);
|
||||
|
||||
const tooltipWidth = unwrap($tooltip.width());
|
||||
|
||||
if (e.clientX + rect.left + tooltipWidth + 10 > window.innerWidth) {
|
||||
$tooltip.css({ 'top': top + 30, 'left': left - tooltipWidth - 10 });
|
||||
}
|
||||
else {
|
||||
$tooltip.css({ 'top': top + 30, 'left': left + 10 });
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
$objs.on('mouseenter', function () {
|
||||
if ($map_body.data('touchMode')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const $this = $(this);
|
||||
|
||||
$tooltip_city.data('target', $this.data('id'));
|
||||
$tooltip_city.html($this.data('text'));
|
||||
const nation_text = $this.data('nation');
|
||||
if (nation_text) {
|
||||
$tooltip_nation.html(nation_text).show();
|
||||
} else {
|
||||
$tooltip_nation.html('').hide();
|
||||
}
|
||||
|
||||
$tooltip.show();
|
||||
});
|
||||
|
||||
$objs.on('mouseleave', function () {
|
||||
$tooltip.hide();
|
||||
});
|
||||
|
||||
$objs.on('click', function () {
|
||||
//xxx: touchend 다음 click 이벤트가 갈 수도 있고, 안 갈 수도 있다.
|
||||
const touchMode = $(this).data('touchMode') as number | undefined;
|
||||
if (touchMode === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (touchMode === 1) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function setCityClickable(obj: MapCityDrawable) {
|
||||
|
||||
obj.cityList.forEach(function (city) {
|
||||
const $cityLink = $world_map.find(`.city_base_${city.id} .city_link`);
|
||||
|
||||
if ('clickable' in city && city.clickable > 0) {
|
||||
$cityLink.attr('href', stringFormat(hrefTemplate, city.id));
|
||||
}
|
||||
|
||||
if (selectCallback) {
|
||||
$cityLink.on('click', function (e) {
|
||||
e.preventDefault();
|
||||
return selectCallback(city);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function saveCityInfo(obj: MapCityDrawable) {
|
||||
$world_map.data('cityInfo', obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
const $hideCityNameBtn = $world_map.find('.map_toggle_cityname');
|
||||
if (localStorage.getItem('sam.hideMapCityName') == 'yes') {
|
||||
$world_map.addClass('hide_cityname');
|
||||
$hideCityNameBtn.addClass('active').attr('aria-pressed', 'true');
|
||||
}
|
||||
|
||||
$hideCityNameBtn.on('click', function () {
|
||||
//이전 상태 확인
|
||||
const state = localStorage.getItem('sam.hideMapCityName') == 'no';
|
||||
if (state) {
|
||||
$world_map.addClass('hide_cityname');
|
||||
localStorage.setItem('sam.hideMapCityName', 'yes');
|
||||
} else {
|
||||
$world_map.removeClass('hide_cityname');
|
||||
localStorage.setItem('sam.hideMapCityName', 'no');
|
||||
}
|
||||
});
|
||||
|
||||
const $toggleSingleTapBtn = $world_map.find('.map_toggle_single_tap');
|
||||
if (localStorage.getItem('sam.toggleSingleTap') == 'yes') {
|
||||
window.sam_toggleSingleTap = true;
|
||||
$toggleSingleTapBtn.addClass('active').attr('aria-pressed', 'true');
|
||||
} else {
|
||||
window.sam_toggleSingleTap = false;
|
||||
}
|
||||
|
||||
const $map_body = $(drawTarget + ' .map_body');
|
||||
|
||||
$toggleSingleTapBtn.on('click', function () {
|
||||
//이전 상태 확인
|
||||
const state = localStorage.getItem('sam.toggleSingleTap') == 'no';
|
||||
if (state) {
|
||||
$map_body.removeData('touchMode');
|
||||
localStorage.setItem('sam.toggleSingleTap', 'yes');
|
||||
window.sam_toggleSingleTap = true;
|
||||
} else {
|
||||
localStorage.setItem('sam.toggleSingleTap', 'no');
|
||||
window.sam_toggleSingleTap = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (isDetailMap) {
|
||||
$world_map.addClass('map_detail');
|
||||
} else {
|
||||
$world_map.removeClass('map_datail');
|
||||
}
|
||||
|
||||
const responseP = axios({
|
||||
url: unwrap(option.targetJson),
|
||||
method: unwrap(option.reqType),
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
data: JSON.stringify({
|
||||
neutralView: option.neutralView,
|
||||
year: option.year,
|
||||
month: option.month,
|
||||
showMe: option.showMe,
|
||||
aux: option.aux
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
const response = await responseP;
|
||||
|
||||
const rawObject: MapRawResult = response.data;
|
||||
|
||||
const computedResult = await checkReturnObject(rawObject)
|
||||
.then(setMapBackground)
|
||||
.then(convertCityObjs)
|
||||
.then(isDetailMap ? drawDetailWorldMap : drawBasicWorldMap)
|
||||
.then(setMouseWork)
|
||||
.then(setCityClickable)
|
||||
.then(saveCityInfo);
|
||||
|
||||
if (option.callback) {
|
||||
option.callback(computedResult, rawObject);
|
||||
}
|
||||
|
||||
if ($world_map.hasClass('draw_required')) {
|
||||
if (useCachedMap) {
|
||||
//일단 불러옴
|
||||
await (async () => {
|
||||
const rawStoredMap = localStorage.getItem(storedOldMapKey);
|
||||
if (!rawStoredMap) {
|
||||
return;
|
||||
}
|
||||
const [storedServerID, storedMap] = JSON.parse(rawStoredMap) as [string, MapResult];
|
||||
if (storedServerID != serverID) {
|
||||
return;
|
||||
}
|
||||
|
||||
await setMapBackground(storedMap)
|
||||
.then(convertCityObjs)
|
||||
.then(isDetailMap ? drawDetailWorldMap : drawBasicWorldMap)
|
||||
.then(setMouseWork)
|
||||
.then(setCityClickable)
|
||||
.then(saveCityInfo);
|
||||
})();
|
||||
} else if (option.year && option.month) {
|
||||
const rawStartYear = localStorage.getItem(storedStartYear) as string | undefined;
|
||||
let startYear: number;
|
||||
if (rawStartYear) {
|
||||
startYear = JSON.parse(rawStartYear);
|
||||
} else {
|
||||
startYear = option.year;
|
||||
}
|
||||
await setMapBackground({
|
||||
year: option.year,
|
||||
month: option.month,
|
||||
startYear: startYear
|
||||
} as unknown as MapResult);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
exportWindow(reloadWorldMap, 'reloadWorldMap');
|
||||
htmlReady(function(){
|
||||
if( is_touch_device()){
|
||||
const target = document.querySelector('.map_body .map_toggle_single_tap') as HTMLElement | null;
|
||||
if(!target){
|
||||
return;
|
||||
}
|
||||
target.style.display = 'block';
|
||||
}
|
||||
});
|
||||
+702
@@ -0,0 +1,702 @@
|
||||
|
||||
import $ from 'jquery';
|
||||
import type { InvalidResponse } from '@/defs';
|
||||
import { getDateTimeNow } from '@util/getDateTimeNow';
|
||||
import axios from 'axios';
|
||||
import { convertFormData } from '@util/convertFormData';
|
||||
import { isBrightColor } from "@util/isBrightColor";
|
||||
import { unwrap } from '@util/unwrap';
|
||||
import _ from 'lodash';
|
||||
import { addMinutes } from 'date-fns';
|
||||
import { parseTime } from '@util/parseTime';
|
||||
import { formatTime } from '@util/formatTime';
|
||||
import { TemplateEngine } from '@util/TemplateEngine';
|
||||
import { isNotNull } from '@util/isNotNull';
|
||||
import { unwrap_any } from '@util/unwrap_any';
|
||||
import { setAxiosXMLHttpRequest } from '@util/setAxiosXMLHttpRequest';
|
||||
import { exportWindow } from '@util/exportWindow';
|
||||
|
||||
const messageTemplate = `<div
|
||||
class="msg_plate msg_plate_<%msgType%> msg_plate_<%nationType%>"
|
||||
id="msg_<%id%>"
|
||||
data-id="<%id%>"
|
||||
>
|
||||
<div class="msg_icon">
|
||||
<%if(src.icon){ %>
|
||||
<img class='generalIcon' width='64' height='64' src="<%encodeURI(src.icon)%>">
|
||||
<%} else {%>
|
||||
<img class='generalIcon' width='64' height='64' src="<%encodeURI(defaultIcon)%>">
|
||||
<%}%>
|
||||
</div>
|
||||
<div class="msg_body">
|
||||
<div class="msg_header">
|
||||
<%if(!this.option.action && src.id == myGeneralID && now <= last5min && invalidType == 'msg_valid' && !deletable){%>
|
||||
<button type="button" data-erase_until="<%last5min%>" class="btn btn btn-outline-warning btn-sm btn-delete-msg" style='float:right'>❌</button>
|
||||
<%}%>
|
||||
<%if(msgType == 'private') {%>
|
||||
<%if(src.name == generalName){%>
|
||||
<span class="msg_target msg_<%src.colorType%>" style="background-color:<%src.color%>;">나</span
|
||||
><span class="msg_from_to">▶</span
|
||||
><span class="msg_target msg_<%dest.colorType%>" style="background-color:<%dest.color%>;"><%e(dest.name)%>:<%dest.nation%></span>
|
||||
<%}else{%>
|
||||
<span class="msg_target msg_<%src.colorType%>" style="background-color:<%src.color%>;"><%e(src.name)%>:<%src.nation%></span
|
||||
><span class="msg_from_to">▶</span
|
||||
><span class="msg_target msg_<%dest.colorType%>" style="background-color:<%dest.color%>;">나</span>
|
||||
<%}%>
|
||||
<%} else if(msgType == 'national' && src.nation_id == dest.nation_id){%>
|
||||
<span class="msg_target msg_<%src.colorType%>" style="background-color:<%src.color%>;"><%e(src.name)%></span>
|
||||
<%} else if(msgType == 'national' || msgType == 'diplomacy'){%>
|
||||
<%if(src.nation_id == nationID){%>
|
||||
<span class="msg_target msg_<%src.colorType%>" style="background-color:<%src.color%>;"><%e(src.name)%></span
|
||||
><span class="msg_from_to">▶</span
|
||||
><span class="msg_target msg_<%dest.colorType%>" style="background-color:<%dest.color%>;"><%dest.nation%></span>
|
||||
<%}else{%>
|
||||
<span class="msg_target msg_<%src.colorType%>" style="background-color:<%src.color%>;"><%e(src.name)%>:<%src.nation%></span
|
||||
><span class="msg_from_to"></span>
|
||||
<%}%>
|
||||
<%} else {%>
|
||||
<span class="msg_target msg_<%src.colorType%>" style="background-color:<%src.color%>;"><%e(src.name)%>:<%src.nation%></span>
|
||||
<%} %>
|
||||
<span class="msg_time"><<%e(time)%>></span>
|
||||
</div>
|
||||
<div class="msg_content <%invalidType%>"><%linkifyStr(text)%></div>
|
||||
<%if(this.option && this.option.action) {%>
|
||||
<div class="msg_prompt">
|
||||
<button type="button" class="prompt_yes btn_prompt" <%allowButton?'':'disabled="disabled"'%>>수락</button> <button type="button" class="prompt_no btn_prompt" <%allowButton?'':'disabled="disabled"'%>>거절</button>
|
||||
</div><%} %></div></div>`;
|
||||
let myGeneralID: number | undefined;
|
||||
//let isChief: boolean | undefined;
|
||||
let lastSequence: number | undefined;
|
||||
let myNation: {
|
||||
id: number,
|
||||
mailbox: number,
|
||||
color: string,
|
||||
nation: string,
|
||||
} | undefined;
|
||||
//let myOfficerLevel: number | undefined;
|
||||
let permissionLevel: number | undefined;
|
||||
|
||||
type MsgType = 'private' | 'public' | 'national' | 'diplomacy';
|
||||
const minMsgSeq: Record<MsgType, number> = {
|
||||
'private': 0x7fffffff,
|
||||
'public': 0x7fffffff,
|
||||
'national': 0x7fffffff,
|
||||
'diplomacy': 0x7fffffff,
|
||||
}
|
||||
|
||||
type MsgTarget = {
|
||||
id: number,
|
||||
name: string,
|
||||
nation_id: number, //XXX: 왜 이 값은 nationID가 아니고 nation_id인가?
|
||||
nation: string;
|
||||
color: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
type MsgItem = {
|
||||
id: number,
|
||||
msgType: MsgType;
|
||||
src: MsgTarget;
|
||||
dest?: MsgTarget;
|
||||
text: string,
|
||||
option: Record<string, string | number>;
|
||||
time: string;
|
||||
}
|
||||
|
||||
type MsgPrintItem = MsgItem & {
|
||||
generalName: string;
|
||||
nationID: number;
|
||||
nationType: 'local' | 'src' | 'dest';
|
||||
myGeneralID: number;
|
||||
allowButton: boolean;
|
||||
last5min: string;
|
||||
now: string;
|
||||
invalidType: 'msg_invalid' | 'msg_valid';
|
||||
deletable: boolean;
|
||||
src: MsgTarget & { colorType: 'bright' | 'dark' },
|
||||
dest: MsgTarget & { colorType: 'bright' | 'dark' },
|
||||
defaultIcon: string,
|
||||
}
|
||||
|
||||
type MsgResponse = {
|
||||
[v in MsgType]: MsgItem[];
|
||||
} & {
|
||||
result: true;
|
||||
keepRecent: boolean;
|
||||
nationID: number;
|
||||
generalName: string;
|
||||
sequence: number;
|
||||
};
|
||||
|
||||
type BasicGeneralTarget = {
|
||||
id: number,
|
||||
textName: string,
|
||||
name?: string,
|
||||
isRuler?: boolean,
|
||||
nation?: number,
|
||||
color?: string,
|
||||
}
|
||||
|
||||
type MailboxItem = {
|
||||
id: number,
|
||||
mailbox: number,
|
||||
color: string,
|
||||
name: string,
|
||||
nationID: number,
|
||||
//nation: string,
|
||||
general: [number, string, number][]
|
||||
}
|
||||
|
||||
type MabilboxListResponse = {
|
||||
result: true,
|
||||
nation: MailboxItem[]
|
||||
}
|
||||
|
||||
type BasicInfoResponse = {
|
||||
generalID: number,
|
||||
myNationID: number,
|
||||
isChief: boolean,
|
||||
officerLevel: number,
|
||||
permission: number,
|
||||
}
|
||||
|
||||
|
||||
let generalList: Record<number, BasicGeneralTarget> = {};
|
||||
|
||||
async function responseMessage(msgID: number, responseAct: boolean): Promise<void> {
|
||||
const response = await axios({
|
||||
url: 'j_msg_decide_opt.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
data: JSON.stringify({
|
||||
msgID: msgID,
|
||||
response: responseAct
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
const result: InvalidResponse = response.data;
|
||||
if (!result.result) {
|
||||
alert(result.reason);
|
||||
}
|
||||
location.reload();
|
||||
}
|
||||
|
||||
async function deleteMessage(msgID: number): Promise<MsgResponse> {
|
||||
const response = await axios({
|
||||
url: 'j_msg_delete.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
msgID: msgID,
|
||||
})
|
||||
});
|
||||
const result: InvalidResponse | MsgResponse = response.data;
|
||||
if (!result.result) {
|
||||
throw result.reason;
|
||||
}
|
||||
return await refreshMsg();
|
||||
}
|
||||
|
||||
export async function refreshMsg(): Promise<MsgResponse> {
|
||||
const value = await fetchRecentMsg();
|
||||
return redrawMsg(value, true);
|
||||
}
|
||||
exportWindow(refreshMsg, 'refreshMsg');
|
||||
|
||||
async function fetchRecentMsg(): Promise<MsgResponse> {
|
||||
const response = await axios({
|
||||
url: 'j_msg_get_recent.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
sequence: lastSequence ?? 0
|
||||
})
|
||||
});
|
||||
const result: InvalidResponse | MsgResponse = response.data;
|
||||
if (!result.result) {
|
||||
throw result.reason;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function showOldMsg(msgType: MsgType): Promise<MsgResponse> {
|
||||
const response = await axios({
|
||||
url: 'j_msg_get_old.php',
|
||||
responseType: 'json',
|
||||
method: 'post',
|
||||
data: convertFormData({
|
||||
to: minMsgSeq[msgType],
|
||||
type: msgType,
|
||||
})
|
||||
});
|
||||
const result: InvalidResponse | MsgResponse = response.data;
|
||||
if (!result.result) {
|
||||
throw result.reason;
|
||||
}
|
||||
|
||||
return redrawMsg(result, false);
|
||||
}
|
||||
|
||||
function redrawMsg(msgResponse: MsgResponse, addFront: boolean): MsgResponse {
|
||||
function checkErasable(obj: MsgResponse) {
|
||||
|
||||
const now = getDateTimeNow();
|
||||
$('.btn-delete-msg').each(function () {
|
||||
const $btn = $(this);
|
||||
const eraseUntil = $btn.data('erase_until');
|
||||
if (eraseUntil < now) {
|
||||
$btn.detach();
|
||||
}
|
||||
})
|
||||
return obj;
|
||||
}
|
||||
function checkClear(obj: MsgResponse): MsgResponse {
|
||||
if (!obj.keepRecent) {
|
||||
$('.msg_plate').detach();
|
||||
lastSequence = undefined;
|
||||
console.log('refresh!');
|
||||
void fetchRecentMsg().then(async (data) => {
|
||||
redrawMsg(data, true);
|
||||
})
|
||||
throw true;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
function registerSequence(obj: MsgResponse): MsgResponse {
|
||||
lastSequence = Math.max(lastSequence ?? 0, obj.sequence);
|
||||
for (const msgType of ['public', 'private', 'national', 'diplomacy'] as MsgType[]) {
|
||||
const msgList = obj[msgType];
|
||||
if (!msgList || msgList.length == 0) {
|
||||
continue;
|
||||
}
|
||||
const lastMsg = unwrap(_.last(msgList));
|
||||
minMsgSeq[msgType] = Math.min(minMsgSeq[msgType], lastMsg.id);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
function printTemplate(obj: MsgResponse) {
|
||||
const printList: [MsgItem[], JQuery<HTMLElement>, MsgType][] = [
|
||||
[obj.public, $('#message_board .public_message'), 'public'],
|
||||
[obj.private, $('#message_board .private_message'), 'private'],
|
||||
[obj.diplomacy, $('#message_board .diplomacy_message'), 'diplomacy'],
|
||||
[obj.national, $('#message_board .national_message'), 'national'],
|
||||
];
|
||||
|
||||
for (const [msgSource, $msgBoard, msgType] of printList) {
|
||||
if (!msgSource || $msgBoard.length == 0) {
|
||||
console.log('No Items', msgSource, $msgBoard);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
let needRefreshLastContact = (msgType == 'private');
|
||||
|
||||
const now = getDateTimeNow();
|
||||
//list의 맨 앞이 가장 최신 메시지임.
|
||||
const $msgs: JQuery<HTMLElement>[] = msgSource.map(function (msg) {
|
||||
|
||||
const contactTarget = (msg.src.id != myGeneralID || msg.msgType === 'public') ? msg.src.id : unwrap(msg.dest).id;
|
||||
if (needRefreshLastContact && contactTarget != myGeneralID && contactTarget in generalList) {
|
||||
needRefreshLastContact = false;
|
||||
$('#last_contact').val(contactTarget).html(generalList[contactTarget].textName).show();
|
||||
}
|
||||
|
||||
const nationID = obj.nationID;
|
||||
const generalName = obj.generalName;
|
||||
let nationType: MsgPrintItem["nationType"];
|
||||
|
||||
const src = {
|
||||
...msg.src,
|
||||
colorType: isBrightColor(msg.src.color) ? 'bright' as const : 'dark' as const,
|
||||
};
|
||||
|
||||
if (!src.nation) {
|
||||
src.nation = '재야';
|
||||
src.color = '#000000';
|
||||
}
|
||||
|
||||
const dest = {
|
||||
...(msg.dest ?? msg.src),
|
||||
colorType: isBrightColor((msg.dest ?? msg.src).color) ? 'bright' as const : 'dark' as const,
|
||||
};
|
||||
|
||||
if (!dest.nation) {
|
||||
dest.nation = '재야';
|
||||
dest.color = '#000000';
|
||||
}
|
||||
|
||||
if (src.nation_id == dest.nation_id) {
|
||||
nationType = 'local';
|
||||
}
|
||||
else if (nationID == src.nation_id) {
|
||||
nationType = 'src';
|
||||
}
|
||||
else {
|
||||
nationType = 'dest';
|
||||
}
|
||||
|
||||
|
||||
const defaultIcon = `${window.pathConfig.sharedIcon}/default.jpg`;
|
||||
let allowButton: boolean;
|
||||
if (msgType == 'diplomacy') {
|
||||
allowButton = unwrap(permissionLevel) >= 4;
|
||||
}
|
||||
else {
|
||||
allowButton = true;
|
||||
}
|
||||
|
||||
const last5min = formatTime(addMinutes(parseTime(msg.time), 5));
|
||||
let invalidType: MsgPrintItem['invalidType'];
|
||||
if (msg.option && msg.option.invalid) {
|
||||
invalidType = 'msg_invalid';
|
||||
}
|
||||
else {
|
||||
invalidType = 'msg_valid';
|
||||
}
|
||||
|
||||
let deletable: boolean;
|
||||
if (msg.option && !msg.option.deletable) {
|
||||
deletable = false;
|
||||
}
|
||||
else {
|
||||
deletable = true;
|
||||
}
|
||||
|
||||
const printMsg: MsgPrintItem = {
|
||||
...msg,
|
||||
generalName,
|
||||
nationID,
|
||||
nationType,
|
||||
myGeneralID: unwrap(myGeneralID),
|
||||
src,
|
||||
dest,
|
||||
now,
|
||||
allowButton,
|
||||
last5min,
|
||||
invalidType,
|
||||
deletable,
|
||||
defaultIcon,
|
||||
msgType,
|
||||
};
|
||||
|
||||
const msgHtml = TemplateEngine(unwrap(messageTemplate), printMsg);
|
||||
|
||||
|
||||
//만약 이전 메시지와 같은 id가 온 경우 덮어씌운다.
|
||||
//NOTE:현 프로세스 상에서는 같은 id가 와선 안된다.
|
||||
const $existMsg = $(`#msg_${msg.id}`);
|
||||
let $msg = $(msgHtml);
|
||||
if ($existMsg.length) {
|
||||
console.log('메시지 충돌', $msg, $existMsg);
|
||||
$existMsg.html($msg.html());
|
||||
$msg = $existMsg;
|
||||
}
|
||||
|
||||
let hideMsg = false;
|
||||
if (msg.option) {
|
||||
if (msg.option.delete !== undefined) {
|
||||
//delete는 삭제.
|
||||
$(`#msg_${msg.option.delete}`).detach();
|
||||
}
|
||||
if (msg.option.overwrite !== undefined) {
|
||||
//overwrite는 숨기기.
|
||||
$.map(msg.option.overwrite, function (overwriteID) {
|
||||
const $msg = $(`#msg_${overwriteID}`);
|
||||
$msg.find('.btn-delete-msg').detach();
|
||||
$msg.find('.msg_content').html('삭제된 메시지입니다.').removeClass('msg_valid').addClass('msg_invalid');
|
||||
});
|
||||
|
||||
}
|
||||
if (msg.option.hide) {
|
||||
hideMsg = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hideMsg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$msg.find('.btn-delete-msg').click(function () {
|
||||
if (!confirm("삭제하시겠습니까?")) {
|
||||
return false;
|
||||
}
|
||||
void deleteMessage(msg.id);
|
||||
});
|
||||
|
||||
$msg.find('button.prompt_yes').click(function () {
|
||||
if (!confirm("수락하시겠습니까?")) {
|
||||
return false;
|
||||
}
|
||||
void responseMessage(msg.id, true);
|
||||
|
||||
});
|
||||
|
||||
$msg.find('button.prompt_no').click(function () {
|
||||
if (!confirm("거절하시겠습니까?")) {
|
||||
return false;
|
||||
}
|
||||
void responseMessage(msg.id, false);
|
||||
});
|
||||
|
||||
if ($existMsg.length) {
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return $msg;
|
||||
}
|
||||
|
||||
}).filter(isNotNull);
|
||||
|
||||
if (addFront) {
|
||||
$msgBoard.prepend($msgs);
|
||||
}
|
||||
else {
|
||||
$msgBoard.find('.load_old_message').before($msgs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
msgResponse = checkErasable(msgResponse);
|
||||
msgResponse = checkClear(msgResponse);
|
||||
msgResponse = registerSequence(msgResponse);
|
||||
printTemplate(msgResponse);
|
||||
return msgResponse;
|
||||
}
|
||||
|
||||
function refreshMailboxList(obj: MabilboxListResponse) {
|
||||
const $mailboxList = $('#mailbox_list');
|
||||
|
||||
$mailboxList.change(function () {
|
||||
console.log($(this).val());
|
||||
})
|
||||
|
||||
const oldSelected = $mailboxList.val();
|
||||
|
||||
$mailboxList.empty();
|
||||
|
||||
let $lastContact = $('#last_contact');
|
||||
let lastContact: BasicGeneralTarget | undefined;
|
||||
if ($lastContact.length > 0 && parseInt(unwrap_any<string>($lastContact.val())) >= 0) {
|
||||
lastContact = {
|
||||
id: parseInt(unwrap_any<string>($lastContact.val())),
|
||||
textName: $lastContact.html()
|
||||
};
|
||||
//$lastContact = undefined;
|
||||
}
|
||||
|
||||
generalList = {};
|
||||
|
||||
|
||||
obj.nation.sort(function (lhs, rhs) {
|
||||
if (lhs.mailbox == unwrap(myNation).mailbox) {
|
||||
return -1;
|
||||
}
|
||||
if (rhs.mailbox == unwrap(myNation).mailbox) {
|
||||
return 1;
|
||||
}
|
||||
return lhs.mailbox - rhs.mailbox;
|
||||
})
|
||||
|
||||
for (const nation of obj.nation) {
|
||||
//console.log(nation);
|
||||
const $optgroup = $(`<optgroup label="${nation.name}"></optgroup>`);
|
||||
$optgroup.css('background-color', nation.color);
|
||||
|
||||
if (unwrap(myNation).mailbox == nation.mailbox) {
|
||||
unwrap(myNation).color = nation.color;
|
||||
}
|
||||
|
||||
if (isBrightColor(nation.color)) {
|
||||
$optgroup.css('color', 'black');
|
||||
}
|
||||
else {
|
||||
$optgroup.css('color', 'white');
|
||||
}
|
||||
|
||||
nation.general.sort(function (lhs, rhs) {
|
||||
if (lhs[1] < rhs[1]) {
|
||||
return -1;
|
||||
}
|
||||
if (lhs[1] > rhs[1]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
$.each(nation.general, function () {
|
||||
const generalID = this[0];
|
||||
const generalName = this[1];
|
||||
//const isNPC = !!(this[2] & 0x2);
|
||||
const isRuler = !!(this[2] & 0x1);
|
||||
const isAmbassador = !!(this[2] & 0x4);
|
||||
|
||||
|
||||
|
||||
if (generalID == myGeneralID) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let textName = generalName;
|
||||
if (isRuler) {
|
||||
textName = `*${textName}*`;
|
||||
}
|
||||
else if (isAmbassador) {
|
||||
textName = `#${textName}#`;
|
||||
}
|
||||
|
||||
generalList[generalID] = {
|
||||
id: generalID,
|
||||
name: generalName,
|
||||
textName: textName,
|
||||
isRuler: isRuler,
|
||||
nation: nation.nationID,
|
||||
color: nation.color
|
||||
};
|
||||
|
||||
const $item = $(`<option value="${generalID}">${textName}</option>`);
|
||||
|
||||
if (unwrap(permissionLevel) == 4 && isAmbassador && unwrap(myNation).mailbox != nation.mailbox) {
|
||||
$item.prop('disabled', true);
|
||||
}
|
||||
$optgroup.append($item);
|
||||
});
|
||||
|
||||
$mailboxList.append($optgroup);
|
||||
}
|
||||
|
||||
const $favorite = $('<optgroup label="즐겨찾기"></optgroup>');
|
||||
|
||||
//아국메시지, 전체메시지
|
||||
const $ourCountry = $(`<option value="${unwrap(myNation).mailbox}">【 아국 메세지 】</option>`)
|
||||
.css({ 'background-color': unwrap(myNation).color, 'color': isBrightColor(unwrap(myNation).color) ? 'black' : 'white' });
|
||||
const $toPublic = $('<option value="9999">【 전체 메세지 】</option>');
|
||||
$favorite.append($ourCountry);
|
||||
$favorite.append($toPublic);
|
||||
|
||||
$lastContact = $('<option id="last_contact" value="-1"></option>').hide();
|
||||
if (lastContact) {
|
||||
$lastContact.show().val(lastContact.id).html(lastContact.textName);
|
||||
}
|
||||
$favorite.append($lastContact);
|
||||
//TODO:운영자를 추가하는 코드도 넣을 것.
|
||||
|
||||
if (unwrap(permissionLevel) >= 4) {
|
||||
for (const nation of obj.nation) {
|
||||
//console.log(nation);
|
||||
const $nation = $(`<option value="${nation.mailbox}">${nation.name}</option>`);
|
||||
$nation.css('background-color', nation.color);
|
||||
|
||||
if (isBrightColor(nation.color)) {
|
||||
$nation.css('color', 'black');
|
||||
}
|
||||
else {
|
||||
$nation.css('color', 'white');
|
||||
}
|
||||
$favorite.append($nation);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
$mailboxList.prepend($favorite);
|
||||
|
||||
if (!oldSelected) {
|
||||
$mailboxList.val(unwrap(myNation).mailbox);
|
||||
}
|
||||
else {
|
||||
$mailboxList.val(oldSelected);
|
||||
}
|
||||
}
|
||||
|
||||
function registerGlobal(basicInfo: BasicInfoResponse) {
|
||||
|
||||
myNation = {
|
||||
'id': basicInfo.myNationID,
|
||||
'mailbox': basicInfo.myNationID + 9000,
|
||||
'color': '#000000',
|
||||
'nation': '재야'
|
||||
};
|
||||
myGeneralID = basicInfo.generalID;
|
||||
//isChief = basicInfo.isChief;
|
||||
//myOfficerLevel = basicInfo.officerLevel;
|
||||
permissionLevel = basicInfo.permission;
|
||||
}
|
||||
|
||||
function activateMessageForm() {
|
||||
const $msgInput = $('#msg_input');
|
||||
const $msgSubmit = $('#msg_submit');
|
||||
const $mailboxList = $('#mailbox_list');
|
||||
|
||||
$msgInput.keypress(function (e) {
|
||||
const code = (e.keyCode ? e.keyCode : e.which);
|
||||
if (code == 13) {
|
||||
$msgSubmit.trigger('click');
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
$msgSubmit.click(async function () {
|
||||
|
||||
const text = $.trim(unwrap_any<string>($msgInput.val()));
|
||||
$msgInput.val('').focus();
|
||||
|
||||
const targetMailbox = unwrap_any<string>($mailboxList.val());
|
||||
console.log(targetMailbox, text);
|
||||
|
||||
const response = await axios({
|
||||
url: 'j_msg_submit.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
data: JSON.stringify({
|
||||
mailbox: parseInt(targetMailbox),
|
||||
text: text
|
||||
})
|
||||
})
|
||||
})
|
||||
const result: InvalidResponse = response.data;
|
||||
if (!result.result) {
|
||||
alert(result.reason);
|
||||
}
|
||||
await refreshMsg();
|
||||
});
|
||||
}
|
||||
|
||||
$(async function ($) {
|
||||
setAxiosXMLHttpRequest();
|
||||
|
||||
//basic_info.json은 세션값에 따라 동적으로 바뀌는 데이터임.
|
||||
const basicInfoP = axios({
|
||||
url: 'j_basic_info.php',
|
||||
method: 'post',
|
||||
responseType: 'json'
|
||||
}).then((v) => registerGlobal(v.data));
|
||||
|
||||
//sender_list.json 은 서버측에선 캐시 가능한 데이터임.
|
||||
const senderListP = axios({
|
||||
url: 'j_msg_contact_list.php',
|
||||
method: 'post',
|
||||
responseType: 'json'
|
||||
}).then(v => v.data);
|
||||
|
||||
const messageListP = fetchRecentMsg();
|
||||
|
||||
await basicInfoP
|
||||
const senderList = await senderListP;
|
||||
refreshMailboxList(senderList);
|
||||
activateMessageForm();
|
||||
|
||||
const messageList = await messageListP;
|
||||
redrawMsg(messageList, true);
|
||||
$('.load_old_message').click(function () {
|
||||
const $this = $(this);
|
||||
const msgType = $this.data('msg_type');
|
||||
void showOldMsg(msgType);
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,277 @@
|
||||
import "@scss/myPage.scss";
|
||||
|
||||
import axios from 'axios';
|
||||
import $ from 'jquery';
|
||||
import { type InvalidResponse, keyScreenMode, type ScreenModeType, type ItemTypeKey } from '@/defs';
|
||||
import { convertFormData } from '@util/convertFormData';
|
||||
import { setAxiosXMLHttpRequest } from '@util/setAxiosXMLHttpRequest';
|
||||
import { unwrap } from '@util/unwrap';
|
||||
import { unwrap_any } from '@util/unwrap_any';
|
||||
import { auto500px } from './util/auto500px';
|
||||
import { initTooltip } from "./legacy/initTooltip";
|
||||
import { insertCustomCSS } from "./util/customCSS";
|
||||
import * as JosaUtil from '@util/JosaUtil';
|
||||
import { SammoAPI } from "./SammoAPI";
|
||||
|
||||
type LogResponse = {
|
||||
result: true;
|
||||
log: Record<string, string>;
|
||||
};
|
||||
|
||||
declare const staticValues: {
|
||||
items: Record<ItemTypeKey, {
|
||||
name: string;
|
||||
rawName: string;
|
||||
className: string;
|
||||
cost: number;
|
||||
isBuyable: boolean;
|
||||
}>
|
||||
}
|
||||
|
||||
$(function ($) {
|
||||
setAxiosXMLHttpRequest();
|
||||
|
||||
const initCustomCSSForm = function () {
|
||||
let lastTimeOut: NodeJS.Timeout | undefined = undefined;
|
||||
const $obj = $('#custom_css');
|
||||
const key = 'sam_customCSS';
|
||||
|
||||
let text = localStorage.getItem(key);
|
||||
if (text) {
|
||||
$obj.val(text);
|
||||
console.log(text);
|
||||
}
|
||||
|
||||
$obj.on('change keyup paste', function () {
|
||||
const newText = $obj.val();
|
||||
if (text == newText) {
|
||||
return;
|
||||
}
|
||||
if (lastTimeOut) {
|
||||
clearTimeout(unwrap(lastTimeOut));
|
||||
}
|
||||
$obj.css('background-color', '#222222');
|
||||
lastTimeOut = setTimeout(function () {
|
||||
text = unwrap_any<string>($obj.val());
|
||||
localStorage.setItem(key, text);
|
||||
$obj.css('background-color', 'black');
|
||||
}, 500);
|
||||
})
|
||||
;
|
||||
};
|
||||
|
||||
$('.load_old_log').on('click', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const $thisBtn = $(this);
|
||||
const logType = $thisBtn.data('log_type');
|
||||
const $last = $(`.log_${logType}:last`);
|
||||
let reqTo: number | null = null;
|
||||
if ($last.length) {
|
||||
reqTo = $last.data('seq');
|
||||
}
|
||||
|
||||
let result: InvalidResponse | LogResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_general_log_old.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
to: reqTo,
|
||||
type: logType
|
||||
})
|
||||
});
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(`로그를 받아오지 못했습니다. : ${result.reason}`);
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
const keys: string[] = Object.keys(result.log);
|
||||
if (keys.length > 1 && parseInt(keys[0]) < parseInt(keys[1])) {
|
||||
keys.reverse();
|
||||
}
|
||||
|
||||
if (keys.length == 0) {
|
||||
$thisBtn.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
const html: string[] = [];
|
||||
for (const key of keys) {
|
||||
if ($(`#log_${logType}_${key}`).length) {
|
||||
return true;
|
||||
}
|
||||
const item = result.log[key];
|
||||
html.push(`<div class='log_${logType}' id='log_${logType}_${key}' data-seq='${key}'>${item}</div>`);
|
||||
}
|
||||
$(`#${logType}Plate`).append(html.join(''));
|
||||
})
|
||||
|
||||
initCustomCSSForm();
|
||||
|
||||
|
||||
const $screenModeRadios = $('input:radio[name=screenMode]');
|
||||
$screenModeRadios.prop('checked', false).filter(`[value="${localStorage.getItem(keyScreenMode) ?? 'auto'}"]`).prop('checked', true);
|
||||
$screenModeRadios.on('click', function (e) {
|
||||
const mode = (e.target as HTMLInputElement).value as ScreenModeType;
|
||||
localStorage.setItem(keyScreenMode, mode);
|
||||
document.dispatchEvent(new CustomEvent('tryChangeScreenMode'));
|
||||
});
|
||||
|
||||
|
||||
$('#dieOnPrestart').on('click', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!confirm('정말로 삭제하시겠습니까?')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await SammoAPI.General.DieOnPrestart();
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
location.replace('..');
|
||||
});
|
||||
|
||||
$('#buildNationCandidate').on('click', async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if(!confirm('거병 이후 장수를 삭제할 수 없게됩니다. 거병하시겠습니까?')){
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await SammoAPI.General.BuildNationCandidate();
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
location.reload();
|
||||
});
|
||||
|
||||
$('#vacation').on('click', async function (e) {
|
||||
e.preventDefault();
|
||||
if (!confirm('휴가 기능을 신청할까요?')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let result: InvalidResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_vacation.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
});
|
||||
result = response.data;
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.result) {
|
||||
alert(`실패했습니다: ${result.reason}`);
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
location.reload();
|
||||
});
|
||||
|
||||
$('#set_my_setting').on('click', async function (e) {
|
||||
e.preventDefault();
|
||||
let result: InvalidResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: 'j_set_my_setting.php',
|
||||
method: 'post',
|
||||
responseType: 'json',
|
||||
data: convertFormData({
|
||||
tnmt: unwrap_any<string>($('.tnmt:checked').val()),
|
||||
defence_train: parseInt(unwrap_any<string>($('#defence_train').val())),
|
||||
use_treatment: unwrap_any<string>($('#use_treatment').val()),
|
||||
use_auto_nation_turn: unwrap_any<string>($('#use_auto_nation_turn').val()),
|
||||
})
|
||||
});
|
||||
result = response.data;
|
||||
if (!result.result) {
|
||||
throw result.reason;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
location.reload();
|
||||
|
||||
});
|
||||
|
||||
$('.drop-item-btn').on('click', async function (e) {
|
||||
e.preventDefault();
|
||||
const $this = $(this);
|
||||
const type = $this.data('item-type') as ItemTypeKey | undefined;
|
||||
if (!type) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${type} 판매 시도`);
|
||||
const item = staticValues.items[type];
|
||||
console.log(item);
|
||||
|
||||
const josaUl = JosaUtil.pick(item.rawName, '을');
|
||||
if (!confirm(`${item.name}${josaUl} 버리시겠습니까? (판매시 가치: ${item.cost / 2})`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.isBuyable && !confirm(`이 아이템은 유니크 아이템입니다. 진짜로 ${item.name}${josaUl} 버리시겠습니까?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
await SammoAPI.General.DropItem({
|
||||
itemType: type,
|
||||
});
|
||||
alert(`${item.name}${josaUl} 버렸습니다.`);
|
||||
location.reload();
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
alert(e);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
initTooltip();
|
||||
insertCustomCSS();
|
||||
});
|
||||
|
||||
auto500px();
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div v-for="(cityList, distance) in distanceList" :key="distance">
|
||||
{{ distance }}칸 떨어진 도시:
|
||||
<template v-for="(cityID, key) in cityList" :key="key">
|
||||
<template v-if="key !== 0"> , </template>
|
||||
<a
|
||||
:style="{ color: colorMap[distance as keyof typeof colorMap] ?? undefined, textDecoration:'underline' }"
|
||||
@click="$emit('selected', cityID)"
|
||||
>{{ citiesMap.get(cityID)?.name }}</a
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
distanceList: {
|
||||
type: Object as PropType<Record<number, number[]>>,
|
||||
required: true,
|
||||
},
|
||||
citiesMap: {
|
||||
type: Object as PropType<Map<number, { name: string }>>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["selected"],
|
||||
data() {
|
||||
return {
|
||||
colorMap: {
|
||||
1: "magenta",
|
||||
2: "orange",
|
||||
3: "yellow",
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="crewTypeItem crewTypeSubGrid text-center s-border-b">
|
||||
<div
|
||||
class="crewTypeImg"
|
||||
:style="{
|
||||
background: '#222222 no-repeat center',
|
||||
backgroundImage: `url('${crewType.img}')`,
|
||||
backgroundSize: '64px',
|
||||
outline: 'solid 1px gray',
|
||||
}"
|
||||
/>
|
||||
<div
|
||||
:style="{
|
||||
backgroundColor: crewType.notAvailable ? 'red' : crewType.reqTech == 0 ? 'green' : 'limegreen',
|
||||
height: '100%',
|
||||
}"
|
||||
class="d-grid crewTypeName"
|
||||
>
|
||||
<div style="margin: auto">{{ crewType.name }}</div>
|
||||
</div>
|
||||
<div>{{ crewType.attack }}</div>
|
||||
<div>{{ crewType.defence }}</div>
|
||||
<div>{{ crewType.speed }}</div>
|
||||
<div>{{ crewType.avoid }}</div>
|
||||
<div>{{ crewType.baseCost.toFixed(1) }}</div>
|
||||
<div>{{ crewType.baseRice.toFixed(1) }}</div>
|
||||
<div class="crewTypePanel">
|
||||
<b-button-group>
|
||||
<b-button class="py-1" variant="dark" @click="beHalf">절반</b-button>
|
||||
<b-button class="py-1" variant="dark" @click="beFilled">채우기</b-button>
|
||||
<b-button class="py-1" variant="dark" @click="beFull">가득</b-button>
|
||||
</b-button-group>
|
||||
<div class="row">
|
||||
<div class="col mx-2">
|
||||
<div class="input-group my-0">
|
||||
<span class="input-group-text py-1">병력</span>
|
||||
<input v-model.number="amount" type="number" class="form-control py-1 f_tnum px-0 text-end" min="1" />
|
||||
<span class="input-group-text py-1 f_tnum">00명</span>
|
||||
<span
|
||||
class="input-group-text py-1 f_tnum"
|
||||
style="text-align: right; min-width: 10ch; color: #303030; background-color: #ddd"
|
||||
>
|
||||
<div style="margin-left: auto">
|
||||
{{ Math.ceil(amount * crewType.baseCost * goldCoeff).toLocaleString() }}금
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="crewTypeBtn d-grid">
|
||||
<b-button variant="primary" @click="doSubmit">{{ commandName }}</b-button>
|
||||
</div>
|
||||
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div class="crewTypeInfo text-start" v-html="crewType.info.join('<br>')" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from "vue";
|
||||
import VueTypes from "vue-types";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
crewType: VueTypes.object.isRequired,
|
||||
leadership: VueTypes.number.isRequired,
|
||||
commandName: VueTypes.string.isRequired,
|
||||
currentCrewType: VueTypes.number.def(-1),
|
||||
crew: VueTypes.number.def(0),
|
||||
goldCoeff: VueTypes.number.isRequired,
|
||||
},
|
||||
emits: ["submitOutput", "update:amount"],
|
||||
setup(props, { emit }) {
|
||||
const amount = ref(0);
|
||||
|
||||
function beHalf() {
|
||||
amount.value = Math.ceil(props.leadership * 0.5);
|
||||
}
|
||||
|
||||
function beFilled() {
|
||||
if (props.crewType.id == props.currentCrewType) {
|
||||
amount.value = Math.max(1, props.leadership - Math.floor(props.crew / 100));
|
||||
} else {
|
||||
amount.value = props.leadership;
|
||||
}
|
||||
}
|
||||
|
||||
function beFull() {
|
||||
amount.value = Math.floor(props.leadership * 1.2);
|
||||
}
|
||||
|
||||
function doSubmit(e: Event) {
|
||||
emit("submitOutput", e, amount.value, props.crewType.id);
|
||||
}
|
||||
|
||||
beFilled();
|
||||
|
||||
return {
|
||||
amount,
|
||||
beHalf,
|
||||
beFilled,
|
||||
beFull,
|
||||
doSubmit,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
amount(val: number) {
|
||||
this.$emit("update:amount", val);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<TopBackBar :title="commandName" />
|
||||
<div v-if="!available건국" class="bg0">더 이상 건국은 불가능합니다.</div>
|
||||
<div v-else class="bg0">
|
||||
<div>현재 도시에서 나라를 세웁니다. 중, 소도시에서만 가능합니다.</div>
|
||||
<ul>
|
||||
<li v-for="nationType in nationTypes" :key="nationType.type" class="row">
|
||||
<div class="col-2 col-md-1">- {{ nationType.name }}</div>
|
||||
<div class="col-4 col-md-2">
|
||||
: <span style="color: cyan">{{ nationType.pros }}</span
|
||||
>,
|
||||
</div>
|
||||
<div class="col-4 col-md-2">
|
||||
<span style="color: magenta">{{ nationType.cons }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="row">
|
||||
<div class="col-4 col-md-2">국명 : <b-form-input v-model="destNationName" maxlength="18" /></div>
|
||||
<div class="col-3 col-md-2">색상 : <ColorSelect v-model="selectedColorID" :colors="colors" /></div>
|
||||
<div class="col-3 col-md-2">
|
||||
<label>성향 :</label>
|
||||
<b-form-select v-model="selectedNationType" :options="nationTypesOption" />
|
||||
</div>
|
||||
|
||||
<div class="col-2 col-md-2 d-grid">
|
||||
<b-button @click="submit">
|
||||
{{ commandName }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BottomBar :title="commandName" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import ColorSelect from "@/processing/SelectColor.vue";
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
import type { Args } from "@/processing/args";
|
||||
import TopBackBar from "@/components/TopBackBar.vue";
|
||||
import BottomBar from "@/components/BottomBar.vue";
|
||||
import type { procNationTypeList } from "../processingRes";
|
||||
|
||||
declare const commandName: string;
|
||||
|
||||
declare const procRes: {
|
||||
available건국: boolean;
|
||||
colors: string[];
|
||||
nationTypes: procNationTypeList;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
ColorSelect,
|
||||
TopBackBar,
|
||||
BottomBar,
|
||||
},
|
||||
setup() {
|
||||
const destNationName = ref("");
|
||||
const selectedColorID = ref(0);
|
||||
|
||||
const selectedNationType = ref(Object.values(procRes.nationTypes)[0].type);
|
||||
|
||||
async function submit(e: Event) {
|
||||
const event = new CustomEvent<Args>("customSubmit", {
|
||||
detail: {
|
||||
colorType: selectedColorID.value,
|
||||
nationName: destNationName.value,
|
||||
nationType: selectedNationType.value,
|
||||
},
|
||||
});
|
||||
unwrap(e.target).dispatchEvent(event);
|
||||
}
|
||||
|
||||
const nationTypesOption: { html: string; value: string }[] = [];
|
||||
for (const nationType of Object.values(procRes.nationTypes)) {
|
||||
nationTypesOption.push({
|
||||
html: nationType.name,
|
||||
value: nationType.type,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
available건국: procRes.available건국,
|
||||
selectedColorID,
|
||||
selectedNationType,
|
||||
colors: procRes.colors,
|
||||
nationTypes: procRes.nationTypes,
|
||||
nationTypesOption,
|
||||
destNationName,
|
||||
commandName,
|
||||
submit,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<TopBackBar :title="commandName" />
|
||||
<div class="bg0">
|
||||
<div>자신의 군량을 사거나 팝니다.<br /></div>
|
||||
<div class="row">
|
||||
<div class="col-2 col-md-1">
|
||||
군량을 :
|
||||
<b-button-group>
|
||||
<b-button :pressed="buyRice" @click="buyRice = true"> 삼 </b-button>
|
||||
<b-button :pressed="!buyRice" @click="buyRice = false"> 팜 </b-button>
|
||||
</b-button-group>
|
||||
</div>
|
||||
<div class="col-7 col-md-4">
|
||||
금액 :
|
||||
<SelectAmount v-model="amount" :amountGuide="amountGuide" :maxAmount="maxAmount" :minAmount="minAmount" />
|
||||
</div>
|
||||
<div class="col-3 col-md-2 d-grid">
|
||||
<b-button variant="primary" @click="submit">
|
||||
{{ commandName }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BottomBar :title="commandName" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import SelectAmount from "@/processing/SelectAmount.vue";
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
import type { Args } from "@/processing/args";
|
||||
import TopBackBar from "@/components/TopBackBar.vue";
|
||||
import BottomBar from "@/components/BottomBar.vue";
|
||||
declare const commandName: string;
|
||||
|
||||
declare const procRes: {
|
||||
minAmount: number;
|
||||
maxAmount: number;
|
||||
amountGuide: number[];
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
SelectAmount,
|
||||
TopBackBar,
|
||||
BottomBar,
|
||||
},
|
||||
setup() {
|
||||
const amount = ref(1000);
|
||||
const buyRice = ref(true);
|
||||
|
||||
async function submit(e: Event) {
|
||||
const event = new CustomEvent<Args>("customSubmit", {
|
||||
detail: {
|
||||
amount: amount.value,
|
||||
buyRice: buyRice.value,
|
||||
},
|
||||
});
|
||||
unwrap(e.target).dispatchEvent(event);
|
||||
}
|
||||
|
||||
return {
|
||||
buyRice,
|
||||
amount,
|
||||
minAmount: ref(procRes.minAmount),
|
||||
maxAmount: ref(procRes.maxAmount),
|
||||
amountGuide: procRes.amountGuide,
|
||||
commandName,
|
||||
submit,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<TopBackBar v-model:searchable="searchable" :title="commandName" />
|
||||
<div class="bg0">
|
||||
<div>
|
||||
재야나 타국의 장수를 등용합니다.<br />
|
||||
서신은 개인 메세지로 전달됩니다.<br />
|
||||
등용할 장수를 목록에서 선택하세요.<br />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
장수 :
|
||||
<SelectGeneral
|
||||
v-model="selectedGeneralID"
|
||||
:generals="generalList"
|
||||
:groupByNation="nationList"
|
||||
:textHelper="textHelpGeneral"
|
||||
:searchable="searchable"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-4 col-md-2 d-grid">
|
||||
<b-button variant="primary" @click="submit">
|
||||
{{ commandName }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BottomBar :title="commandName" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import SelectGeneral from "@/processing/SelectGeneral.vue";
|
||||
import { defineComponent, ref } from "vue";
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
import type { Args } from "@/processing/args";
|
||||
import TopBackBar from "@/components/TopBackBar.vue";
|
||||
import BottomBar from "@/components/BottomBar.vue";
|
||||
import {
|
||||
convertGeneralList,
|
||||
getProcSearchable,
|
||||
type procGeneralItem,
|
||||
type procGeneralKey,
|
||||
type procGeneralRawItemList,
|
||||
type procNationItem,
|
||||
type procNationList,
|
||||
} from "../processingRes";
|
||||
import { getNpcColor } from "@/common_legacy";
|
||||
declare const commandName: string;
|
||||
|
||||
declare const procRes: {
|
||||
generals: procGeneralRawItemList;
|
||||
generalsKey: procGeneralKey[];
|
||||
nationList: procNationList;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
SelectGeneral,
|
||||
TopBackBar,
|
||||
BottomBar,
|
||||
},
|
||||
setup() {
|
||||
const generalList = convertGeneralList(procRes.generalsKey, procRes.generals);
|
||||
|
||||
const selectedGeneralID = ref(generalList[0].no);
|
||||
|
||||
function textHelpGeneral(gen: procGeneralItem): string {
|
||||
const nameColor = getNpcColor(gen.npc);
|
||||
const name = nameColor ? `<span style="color:${nameColor}">${gen.name}</span>` : gen.name;
|
||||
return name;
|
||||
}
|
||||
|
||||
async function submit(e: Event) {
|
||||
const event = new CustomEvent<Args>("customSubmit", {
|
||||
detail: {
|
||||
destGeneralID: selectedGeneralID.value,
|
||||
},
|
||||
});
|
||||
unwrap(e.target).dispatchEvent(event);
|
||||
}
|
||||
|
||||
const nationList = new Map<number, procNationItem>();
|
||||
for (const nationItem of procRes.nationList) {
|
||||
nationList.set(nationItem.id, nationItem);
|
||||
}
|
||||
|
||||
return {
|
||||
searchable: getProcSearchable(),
|
||||
selectedGeneralID,
|
||||
generalList,
|
||||
nationList,
|
||||
commandName,
|
||||
textHelpGeneral,
|
||||
submit,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user