feat: 통합 세력 장수/암행부 페이지 (#213)

- ag-grid 기반
- 컬럼 사용자 정의 기능 제공
  - 컬럼 재정렬, 고정, 정렬 기능
  - 컬럼 상태 저장, 불러오기
  - 마지막 컬럼 재 사용
- 아이콘, 장수명 클릭 시 특수 기능 제공
  - 기본 세력 장수/암행부에서는 '감찰부'
- 추가 컬럼
  - 최근 전투
  - 전투 수
  - 승리 수
  - 살상률
- API/Nation/GeneralList 추가

Co-authored-by: hide_d <hided62@gmail.com>
Reviewed-on: https://storage.hided.net/gitea/devsam/core/pulls/213
This commit is contained in:
2022-04-08 02:28:58 +09:00
parent c96f6d9381
commit a95b26551c
32 changed files with 3137 additions and 381 deletions
+121
View File
@@ -0,0 +1,121 @@
<template>
<BContainer id="container" :toast="{ root: true }" class="pageNationGeneral bg0">
<TopBackBar :title="title" :reloadable="true" @reload="reload" :teleport-zone="toolbarID" />
<!-- 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>
+6 -1
View File
@@ -9,10 +9,11 @@ import type { BettingDetailResponse, BettingListResponse } from "./defs/API/Bett
import type { ReserveBulkCommandResponse, ReserveCommandResponse, ReservedCommandResponse } from "./defs/API/Command";
import type { ChiefResponse } from "./defs/API/NationCommand";
import type { inheritBuffType } from "./defs/API/InheritAction";
import type { SetBlockWarResponse } from "./defs/API/Nation";
import type { SetBlockWarResponse, GeneralListResponse as NationGeneralListResponse } from "./defs/API/Nation";
import type { UploadImageResponse } from "./defs/API/Misc";
import type { JoinArgs } from "./defs/API/General";
import type { GetConstResponse } from "./defs/API/Global";
import type { GeneralListResponse } from "./defs";
const apiRealPath = {
Betting: {
@@ -49,6 +50,9 @@ const apiRealPath = {
Join: POST as APICallT<JoinArgs>,
},
Global: {
GeneralList: GET as APICallT<{
with_token?: boolean
}, GeneralListResponse>,
GetConst: GET as APICallT<undefined, GetConstResponse>,
},
InheritAction: {
@@ -92,6 +96,7 @@ const apiRealPath = {
}[], ReserveBulkCommandResponse>,
},
Nation: {
GeneralList: GET as APICallT<undefined, NationGeneralListResponse>,
SetNotice: PUT as APICallT<{
msg: string,
}>,
+2 -1
View File
@@ -28,6 +28,7 @@
"v_main": "v_main.ts",
"v_nationStratFinan": "v_nationStratFinan.ts",
"v_processing": "v_processing.ts",
"v_nationBetting": "v_nationBetting.ts"
"v_nationBetting": "v_nationBetting.ts",
"v_nationGeneral": "v_nationGeneral.ts"
}
}
+1234
View File
@@ -0,0 +1,1234 @@
<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" />&nbsp;보관하기</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>
<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, 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, GeneralListItemP2] | undefined {
if (!value.st2) {
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 GeneralListItemP2 | 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;
},
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.st2 && 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.st2 && 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.st2) {
return "?";
}
return gameConstStore.value.cityConst[data.city].name;
},
filter: true,
sortable: true,
width: 60,
filterValueGetter: ({ data }) => {
if (!data.st2) {
return "";
}
return convertSearch초성(gameConstStore.value.cityConst[data.city].name);
},
},
troop: {
colId: "troop",
headerName: "부대",
field: "troop",
valueGetter: ({ data }: GenValueGetterParams) => {
if (!data.st2) {
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.st2) {
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.st2) {
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.st2) {
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.st2) {
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.st2) {
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.st2) {
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>
+29 -12
View File
@@ -1,21 +1,24 @@
<template>
<div class="bg0 back_bar">
<div :class="['bg0', 'back_bar', teleportZone?'back_bar_teleport':undefined]">
<button type="button" class="btn btn-sammo-base2 back_btn" @click="back">돌아가기</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>
<div>&nbsp;</div>
<b-button
v-if="toggleSearch !== undefined"
class="btn-toggle-zoom"
:variant="toggleSearch ? 'info' : 'secondary'"
:pressed="toggleSearch"
@click="toggleSearch = !toggleSearch"
>
{{ toggleSearch ? "검색 켜짐" : "검색 꺼짐" }}
</b-button>
<div v-if="teleportZone" :id="teleportZone" class="teleport-zone"></div>
<template v-else>
<div>&nbsp;</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>
@@ -41,6 +44,11 @@ const props = defineProps({
default: undefined,
required: false,
},
teleportZone: {
type: String,
default: undefined,
required: false,
},
});
const emit = defineEmits(["update:searchable", "reload"]);
@@ -72,16 +80,24 @@ function reload() {
width: 100%;
margin: auto;
display: grid;
grid-template-columns: 80px 80px 1fr 80px 80px;
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;
@@ -89,6 +105,7 @@ function reload() {
.btn-toggle-zoom {
height: 24pt;
position: relative;
}
.title {
+115 -3
View File
@@ -1,5 +1,117 @@
import type { ValidResponse } from "@/defs";
import type { ValuesOf, TurnObj } from "@/defs";
import type { GameObjClassKey } from "@/defs/GameObj";
import type { ValidResponse } from "@/SammoAPI";
export type SetBlockWarResponse = ValidResponse & {
availableCnt: number;
};
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 = {
city: number,
experience: number,
dedication: number,
} & GeneralListItemP0;
export type GeneralListItemP2 = GeneralListItemP1 & {
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,
}
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;
+1 -1
View File
@@ -203,7 +203,7 @@ export type GameCityDefault = {
path: Record<CityID, string>;
};
export type GameIActionCategory = "nationType" | "specialDomestic" | "specialWar" | "personality" | "item";
export type GameIActionCategory = "nationType" | "specialDomestic" | "specialWar" | "personality" | "item" | "crewtype";
export type GameIActionInfo = {
value: string;
+635
View File
@@ -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
},
],
},
};
+40 -12
View File
@@ -1,5 +1,6 @@
import type { Args } from "@/processing/args";
import type { ValidResponse, InvalidResponse } from "@/util/callSammoAPI";
import type { GameObjClassKey } from "./GameObj";
export type { ValidResponse, InvalidResponse };
export type BasicGeneralListResponse = {
@@ -15,21 +16,48 @@ export type BasicGeneralListResponse = {
}>
}
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: [
number,
string, 0 | 1,
number, number, string,
string, string, string,
string, string | null,
number,
number, number, number, number,
number,
string, string, string,
number, number
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>,
token?: Record<number, number>,
}
export type NationLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
@@ -200,4 +228,4 @@ export const diplomacyStateInfo: Record<diplomacyState, diplomacyInfo> = {
1: { name: '선포중', color: 'magenta' },
2: { name: '통상' },
7: { name: '불가침', color: 'green' },
}
}
@@ -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>
+28 -51
View File
@@ -3,12 +3,14 @@ import { errUnknown } from '@/common_legacy';
import { getIconPath } from "@util/getIconPath";
import { TemplateEngine } from "@util/TemplateEngine";
import { getNpcColor } from '@/common_legacy';
import type { GeneralListResponse, InvalidResponse } from '@/defs';
import type { GeneralListResponse, InvalidResponse, PublicGeneralItem } from '@/defs';
import { convertFormData } from '@util/convertFormData';
import { unwrap_any } from '@util/unwrap_any';
import { setAxiosXMLHttpRequest } from '@util/setAxiosXMLHttpRequest';
import { Tooltip } from 'bootstrap';
import { trim } from 'lodash';
import { SammoAPI } from './SammoAPI';
import { unwrap } from './util/unwrap';
setAxiosXMLHttpRequest();
@@ -40,29 +42,7 @@ type NPCPick = {
personalText?: string,
}
type NPCPickPrintableR = {
no: number,
picture: string,
imgsvr: 0 | 1,
npc: number,
age: number,
nation: string,
special: string,
special2: string,
personal: string,
name: string,
ownerName: string | null,
injury: number,
leadership: number,
lbonus: number,
strength: number,
intel: number,
explevel: number,
experience: string,
dedication: string,
officerLevel: string,
killturn: number,
connect: number,
type NPCPickPrintableR = PublicGeneralItem & {
reserved: number,
userCSS?: string,
@@ -269,8 +249,8 @@ function printGenerals(value: NPCToken) {
function printGeneralList(value: GeneralListResponse) {
console.log(value);
const tokenList = value.token;
const generalList:NPCPickPrintable[] = value.list.map((rawGeneral) => {
const tokenList = unwrap(value.token);
const generalList: NPCPickPrintable[] = value.list.map((rawGeneral) => {
const general: NPCPickPrintableR = {
no: rawGeneral[0],
picture: rawGeneral[1],
@@ -278,8 +258,8 @@ function printGeneralList(value: GeneralListResponse) {
npc: rawGeneral[3],
age: rawGeneral[4],
nation: rawGeneral[5],
special: rawGeneral[6],
special2: rawGeneral[7],
specialDomestic: rawGeneral[6],
specialWar: rawGeneral[7],
personal: rawGeneral[8],
name: rawGeneral[9],
ownerName: rawGeneral[10],
@@ -289,9 +269,9 @@ function printGeneralList(value: GeneralListResponse) {
strength: rawGeneral[14],
intel: rawGeneral[15],
explevel: rawGeneral[16],
experience: rawGeneral[17],
dedication: rawGeneral[18],
officerLevel: rawGeneral[19],
experienceStr: rawGeneral[17],
dedicationStr: rawGeneral[18],
officerLevelStr: rawGeneral[19],
killturn: rawGeneral[20],
connect: rawGeneral[21],
reserved: 0
@@ -317,7 +297,7 @@ function printGeneralList(value: GeneralListResponse) {
}
if (general.reserved == 1) {
general.nameAux += `<br><small>(${tokenList[general.no]}회)</small>`;
general.nameAux += `<br><small>(${tokenList[general.no]}회)</small>`;
}
@@ -325,13 +305,13 @@ function printGeneralList(value: GeneralListResponse) {
general.iconPath = getIconPath(general.imgsvr, general.picture);
general.specialDomesticWithTooltip = TemplateEngine(templateSpecial, {
text: general.special,
info: specialInfo[general.special]
text: general.specialDomestic,
info: specialInfo[general.specialDomestic]
});
general.specialWarWithTooltip = TemplateEngine(templateSpecial, {
text: general.special2,
info: specialInfo[general.special2]
text: general.specialWar,
info: specialInfo[general.specialWar]
});
general.personalWithTooltip = TemplateEngine(templateSpecial, {
@@ -369,7 +349,7 @@ function printGeneralList(value: GeneralListResponse) {
_printGeneralList(true);
}
function _printGeneralList(clear?:boolean) {
function _printGeneralList(clear?: boolean) {
const $generalTable = $('#general_list');
if (clear) {
$generalTable.empty();
@@ -438,23 +418,20 @@ $(function ($) {
}, errUnknown);
});
$('#btn_load_general_list').click(function () {
$.post({
url: 'j_get_general_list.php',
dataType: 'json',
data: {
with_token: true
}
}).then(function (result) {
if (!result.result) {
alert(result.reason);
return false;
}
$('#btn_load_general_list').on('click', async function () {
try {
const result = await SammoAPI.Global.GeneralList({
with_token: true,
});
printGeneralList(result);
}, errUnknown);
} catch (e) {
console.error(e);
alert(`실패했습니다: ${e}`);
return;
}
});
$('#btn_print_more').click(function () {
$('#btn_print_more').on('click', function () {
_printGeneralList();
})
+20
View File
@@ -0,0 +1,20 @@
import type { ValuesOf } from "@/defs";
import { zip } from "lodash";
export function merge2DArrToObjectArr<T extends Record<string, unknown>>(column: (keyof T)[], list: ValuesOf<T>[][]): T[]{
const result: T[] = [];
for(const rawItem of list){
const item: Record<string, unknown> = {};
if(column.length != rawItem.length){
throw `column과 item의 길이가 같지 않습니다: ${column.length} != ${list.length}, ${JSON.stringify(item)}`;
}
for(const [key, value] of zip(column, rawItem)){
item[key as string] = value;
}
result.push(item as T);
}
return result;
}
+22
View File
@@ -0,0 +1,22 @@
import bs from 'binary-search';
const connectMap: [number, string][] = [
[0, '안함'],
[50, '무관심'],
[100, '보통'],
[400, '가끔'],
[200, '자주'],
[800, '열심'],
[1600, '중독'],
[3200, '폐인'],
[6400, '경고'],
[12800, '헐...'],
]
export function formatConnectScore(connect: number) {
const idx = bs(connectMap, connect, ([key], needle) => key - needle);
if (idx >= 0) {
return connectMap[idx][1] ?? '?';
}
return connectMap[-(idx + 1)][1] ?? '?';
}
+17
View File
@@ -0,0 +1,17 @@
import bs from 'binary-search';
const defenceMap: [number,string][] = [
[0, "△"],
[60, "○"],
[80, "◎"],
[90, "☆"],
[999, "×"],
]
export function formatDefenceTrain(defenceTrain: number): string {
const idx = bs(defenceMap, defenceTrain, ([defenceKey], needle) => defenceKey - needle);
if(idx >= 0){
return defenceMap[idx][1]??'?';
}
return defenceMap[-(idx + 1)][1]??'?';
}
+50
View File
@@ -0,0 +1,50 @@
import bs from 'binary-search';
export const DexLevelMap: [number, string, string][] = [
[0, 'navy', 'F-'],
[350, 'navy', 'F'],
[1375, 'navy', 'F+'],
[3500, 'skyblue', 'E-'],
[7125, 'skyblue', 'E'],
[12650, 'skyblue', 'E+'],
[20475, 'seagreen', 'D-'],
[31000, 'seagreen', 'D'],
[44625, 'seagreen', 'D+'],
[61750, 'teal', 'C-'],
[82775, 'teal', 'C'],
[108100, 'teal', 'C+'],
[138125, 'limegreen', 'B-'],
[173250, 'limegreen', 'B'],
[213875, 'limegreen', 'B+'],
[260400, 'darkorange', 'A-'],
[313225, 'darkorange', 'A'],
[372750, 'darkorange', 'A+'],
[439375, 'tomato', 'S-'],
[513500, 'tomato', 'S'],
[595525, 'tomato', 'S+'],
[685850, 'darkviolet', 'Z-'],
[784875, 'darkviolet', 'Z'],
[893000, 'darkviolet', 'Z+'],
[1010625, 'gold', 'EX-'],
[1138150, 'gold', 'EX'],
[1275975, 'white', 'EX+'],
];
export type DexInfo = {
level: number,
name: string,
color: string,
}
export function formatDexLevel(dex: number): DexInfo {
const rawIdx = bs(DexLevelMap, dex, ([dexKey], needle) => dexKey - needle);
const level = rawIdx >= 0 ? rawIdx : -(rawIdx + 1);
const [, color, name] = DexLevelMap[level];
return {
level,
name,
color
};
}
+26
View File
@@ -0,0 +1,26 @@
import bs from 'binary-search';
const hornorMap: [number, string][] = [
[0, '전무'],
[640, '무명'],
[2560, '신동'],
[5760, '약간'],
[10240, '평범'],
[16000, '지역적'],
[23040, '전국적'],
[31360, '세계적'],
[40960, '유명'],
[45000, '명사'],
[51840, '호걸'],
[55000, '효웅'],
[64000, '영웅'],
[77440, '구세주'],
]
export function formatHonor(experience: number): string {
const idx = bs(hornorMap, experience, ([experienceKey], needle) => experienceKey - needle);
if (idx >= 0) {
return hornorMap[idx][1] ?? '?';
}
return hornorMap[-(idx + 1)][1] ?? '?';
}
+92
View File
@@ -0,0 +1,92 @@
export const OfficerLevelMapDefault: Record<number, string> = {
12: '군주',
11: '참모',
10: '제1장군',
9: '제1모사',
8: '제2장군',
7: '제2모사',
6: '제3장군',
5: '제3모사',
4: '태수',
3: '군사',
2: '종사',
1: '일반',
0: '재야',
}
export const OfficerLevelMapByNationLevel: Record<number, Record<number, string>> = {
7: {
12: '황제',
11: '승상',
10: '표기장군',
9: '사공',
8: '거기장군',
7: '태위',
6: '위장군',
5: '사도'
},
6: {
12: '왕',
11: '광록훈',
10: '좌장군',
9: '상서령',
8: '우장군',
7: '중서령',
6: '전장군',
5: '비서령'
},
5: {
12: '공',
11: '광록대부',
10: '안국장군',
9: '집금오',
8: '파로장군',
7: '소부'
},
4: {
12: '주목',
11: '태사령',
10: '아문장군',
9: '낭중',
8: '호군',
7: '종사중랑'
},
3: {
12: '주자사',
11: '주부',
10: '편장군',
9: '간의대부'
},
2: {
12: '군벌',
11: '참모',
10: '비장군',
9: '부참모'
},
1: {
12: '영주',
11: '참모'
},
0: {
12: '두목',
11: '부두목'
},
}
export function formatOfficerLevelText(officerLevel: number, nationLevel?: number) {
if (officerLevel < 5) {
return OfficerLevelMapDefault[officerLevel] ?? '???';
}
const nationMap = nationLevel === undefined
? OfficerLevelMapDefault
: (OfficerLevelMapByNationLevel[nationLevel] ?? OfficerLevelMapDefault);
return nationMap[officerLevel] ?? (OfficerLevelMapDefault[officerLevel] ?? '???');
}
+14
View File
@@ -0,0 +1,14 @@
import "@scss/nationGeneral.scss";
import "ag-grid-community/dist/styles/ag-grid.css";
import "ag-grid-community/dist/styles/ag-theme-balham-dark.css";
import { createApp } from 'vue'
import PageNationGeneral from '@/PageNationGeneral.vue';
import { BootstrapVue3, BToastPlugin } from 'bootstrap-vue-3';
import { auto500px } from './util/auto500px';
auto500px();
createApp(PageNationGeneral).use(BootstrapVue3).use(BToastPlugin).mount('#app');