diff --git a/.gitignore b/.gitignore index a547bf3..7ffa1b2 100644 --- a/.gitignore +++ b/.gitignore @@ -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/* diff --git a/hwe/ts/.htaccess b/hwe/ts/.htaccess new file mode 100644 index 0000000..698196a --- /dev/null +++ b/hwe/ts/.htaccess @@ -0,0 +1 @@ +Deny from all \ No newline at end of file diff --git a/hwe/ts/@types/types__react/index.d.ts b/hwe/ts/@types/types__react/index.d.ts new file mode 100644 index 0000000..693da49 --- /dev/null +++ b/hwe/ts/@types/types__react/index.d.ts @@ -0,0 +1 @@ +export {} \ No newline at end of file diff --git a/hwe/ts/@types/vue-multiselect.d.ts b/hwe/ts/@types/vue-multiselect.d.ts new file mode 100644 index 0000000..82271a5 --- /dev/null +++ b/hwe/ts/@types/vue-multiselect.d.ts @@ -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 }; +} diff --git a/hwe/ts/ChiefCenter/BottomItem.vue b/hwe/ts/ChiefCenter/BottomItem.vue new file mode 100644 index 0000000..b1cb7ea --- /dev/null +++ b/hwe/ts/ChiefCenter/BottomItem.vue @@ -0,0 +1,57 @@ + + diff --git a/hwe/ts/ChiefCenter/TopItem.vue b/hwe/ts/ChiefCenter/TopItem.vue new file mode 100644 index 0000000..f4fa210 --- /dev/null +++ b/hwe/ts/ChiefCenter/TopItem.vue @@ -0,0 +1,178 @@ + + diff --git a/hwe/ts/GameConstStore.ts b/hwe/ts/GameConstStore.ts new file mode 100644 index 0000000..140fb1a --- /dev/null +++ b/hwe/ts/GameConstStore.ts @@ -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; + public readonly cityConst: Record; + public readonly cityConstMap: { + region: Record; + level: Record; //defs.CityLevelText + }; + public readonly iActionInfo: Record< + GameIActionCategory, + Record< + GameIActionKey, + GameIActionInfo + > + >; + public readonly iActionKeyMap: Record; + + 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 { + //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; +} \ No newline at end of file diff --git a/hwe/ts/PageAuction.vue b/hwe/ts/PageAuction.vue new file mode 100644 index 0000000..75a6544 --- /dev/null +++ b/hwe/ts/PageAuction.vue @@ -0,0 +1,49 @@ + + + diff --git a/hwe/ts/PageBoard.vue b/hwe/ts/PageBoard.vue new file mode 100644 index 0000000..6247da2 --- /dev/null +++ b/hwe/ts/PageBoard.vue @@ -0,0 +1,192 @@ + + + + + + + + + + diff --git a/hwe/ts/PartialReservedCommand.vue b/hwe/ts/PartialReservedCommand.vue new file mode 100644 index 0000000..97f90d2 --- /dev/null +++ b/hwe/ts/PartialReservedCommand.vue @@ -0,0 +1,895 @@ + + + + + + diff --git a/hwe/ts/SammoAPI.ts b/hwe/ts/SammoAPI.ts new file mode 100644 index 0000000..bf6e3a4 --- /dev/null +++ b/hwe/ts/SammoAPI.ts @@ -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, + 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, + 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), + GetBettingList: GET as APICallT< + { + req?: "bettingNation" | "tournament"; + }, + BettingListResponse + >, + }, + Command: { + GetReservedCommand: GET as APICallT, + 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, + GetGeneralLog: GET as APICallT< + { + reqType: GeneralLogType; + reqTo?: number; + }, + GetGeneralLogResponse + >, + DropItem: PUT as APICallT<{ + itemType: ItemTypeKey; + }>, + DieOnPrestart: POST as APICallT, + BuildNationCandidate: POST as APICallT, + }, + Global: { + GeneralList: GET as APICallT, + GeneralListWithToken: GET as APICallT, + GetConst: GET as APICallT, + GetHistory: StrVar("serverID")(NumVar("year", NumVar("month", GET as APICallT))), + GetCurrentHistory: GET as APICallT, + GetMap: GET as APICallT< + { + neutralView?: 0 | 1; + showMe?: 0 | 1; + }, + MapResult + >, + GetCachedMap: GET as APICallT, + GetDiplomacy: GET as APICallT, + ExecuteEngine: POST as APICallT, + }, + InheritAction: { + BuyHiddenBuff: PUT as APICallT<{ + type: inheritBuffType; + level: number; + }>, + BuyRandomUnique: PUT as APICallT, + ResetSpecialWar: PUT as APICallT, + ResetTurnTime: PUT as APICallT, + 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, + 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, + 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, + 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); + }; +}); diff --git a/hwe/ts/SammoRootAPI.ts b/hwe/ts/SammoRootAPI.ts new file mode 100644 index 0000000..4bcf5b7 --- /dev/null +++ b/hwe/ts/SammoRootAPI.ts @@ -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 + }, +} 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); + }; +}); \ No newline at end of file diff --git a/hwe/ts/battleCenter.ts b/hwe/ts/battleCenter.ts new file mode 100644 index 0000000..51fbf11 --- /dev/null +++ b/hwe/ts/battleCenter.ts @@ -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(); +}); \ No newline at end of file diff --git a/hwe/ts/battle_simulator.ts b/hwe/ts/battle_simulator.ts new file mode 100644 index 0000000..db21ac1 --- /dev/null +++ b/hwe/ts/battle_simulator.ts @@ -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, + defendersSkills: Record[], +} + +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> = {}; + + 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; + 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(); + }) + + $('.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(); + }) + + $('.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, 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): GeneralInfo { + const getInt = function (query: string): number { + return parseInt(unwrap_any($general.find(query).val())); + } + + const getVal = function (query: string): string { + return unwrap_any($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(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) { + 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(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) { + const $card = $btn.closest('.general_form'); + return $card; + } + + const getGeneralDetail = function ($btn: JQuery) { + const $card = getGeneralFrame($btn); + const $general = $card.find('.general_detail'); + return $general; + } + + const getGeneralNo = function ($btn: JQuery) { + return parseInt(getGeneralFrame($btn).data('general_no')); + } + + const setGeneralNo = function ($btn: JQuery, 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) { + const $card = getGeneralFrame($btn); + $card.removeData('general_no'); + const generalNo = getGeneralNo($card); + delete defenderNoList[generalNo]; + } + + const addDefender = function ($cardAfter?: JQuery) { + const $newCard = $('
'); + + 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) { + deleteGeneralNo($card); + $card.detach(); + } + + const copyDefender = function ($card: JQuery) { + 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($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($defenderNation.find('.form_nation_type').val()), + tech: parseInt(unwrap_any($defenderNation.find('.form_tech').val())) * 1000, + level: parseInt(unwrap_any($defenderNation.find('.form_nation_level').val())), + capital: $defenderNation.find('.form_is_capital:checked').val() == '1' ? 3 : 4, + } + + const year = parseInt(unwrap_any($('#year').val())); + const month = parseInt(unwrap_any($('#month').val())); + const repeatCnt = parseInt(unwrap_any($('#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) { + 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 = $(`수비자${parseInt(idx) + 1} 스킬`); + $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.css('background-color', color); + $optGroup.css('color', isBrightColor(color) ? 'black' : 'white'); + + for (const general of generalList) { + const $item = $(``); + 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(); + }) +}); \ No newline at end of file diff --git a/hwe/ts/bestGeneral.ts b/hwe/ts/bestGeneral.ts new file mode 100644 index 0000000..2cae158 --- /dev/null +++ b/hwe/ts/bestGeneral.ts @@ -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(); +}); \ No newline at end of file diff --git a/hwe/ts/betting.ts b/hwe/ts/betting.ts new file mode 100644 index 0000000..b624681 --- /dev/null +++ b/hwe/ts/betting.ts @@ -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($(`#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; + }); +}); \ No newline at end of file diff --git a/hwe/ts/bossInfo.ts b/hwe/ts/bossInfo.ts new file mode 100644 index 0000000..588ebc7 --- /dev/null +++ b/hwe/ts/bossInfo.ts @@ -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($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($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(); + }) +}) \ No newline at end of file diff --git a/hwe/ts/build_exports.json b/hwe/ts/build_exports.json new file mode 100644 index 0000000..20065ec --- /dev/null +++ b/hwe/ts/build_exports.json @@ -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" + } +} \ No newline at end of file diff --git a/hwe/ts/common_deprecated.ts b/hwe/ts/common_deprecated.ts new file mode 100644 index 0000000..3aa4e77 --- /dev/null +++ b/hwe/ts/common_deprecated.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'); \ No newline at end of file diff --git a/hwe/ts/common_legacy.ts b/hwe/ts/common_legacy.ts new file mode 100644 index 0000000..7514090 --- /dev/null +++ b/hwe/ts/common_legacy.ts @@ -0,0 +1,99 @@ + +/** + * object의 array를 id를 key로 삼는 object로 재 변환 + */ +export function convertDictById(arr: ArrayLike): Record { + const result: Record = {}; + for (const v of Object.values(arr)) { + result[v.id] = v; + } + return result; +} + +/** + * array를 set 형태의 object로 변환 + */ +export function convertSet(arr: ArrayLike): Record { + const result: Record = {}; + 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; + } +} +export function combineObject(item: V[], columnList: K[]): Record { + const newItem: Record = {}; + 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; +} diff --git a/hwe/ts/common_path.d.ts b/hwe/ts/common_path.d.ts new file mode 100644 index 0000000..05260e4 --- /dev/null +++ b/hwe/ts/common_path.d.ts @@ -0,0 +1,7 @@ +interface Window { + pathConfig: { + root: string, + sharedIcon: string, + gameImage: string, + } +} \ No newline at end of file diff --git a/hwe/ts/components/AuctionResource.vue b/hwe/ts/components/AuctionResource.vue new file mode 100644 index 0000000..c4bb078 --- /dev/null +++ b/hwe/ts/components/AuctionResource.vue @@ -0,0 +1,393 @@ + + + + + diff --git a/hwe/ts/components/AuctionUniqueItem.vue b/hwe/ts/components/AuctionUniqueItem.vue new file mode 100644 index 0000000..65abd10 --- /dev/null +++ b/hwe/ts/components/AuctionUniqueItem.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/hwe/ts/components/BettingDetail.vue b/hwe/ts/components/BettingDetail.vue new file mode 100644 index 0000000..50f1d46 --- /dev/null +++ b/hwe/ts/components/BettingDetail.vue @@ -0,0 +1,460 @@ + + + diff --git a/hwe/ts/components/BoardArticle.vue b/hwe/ts/components/BoardArticle.vue new file mode 100644 index 0000000..311b96c --- /dev/null +++ b/hwe/ts/components/BoardArticle.vue @@ -0,0 +1,105 @@ + + + + diff --git a/hwe/ts/components/BoardComment.vue b/hwe/ts/components/BoardComment.vue new file mode 100644 index 0000000..f49ade5 --- /dev/null +++ b/hwe/ts/components/BoardComment.vue @@ -0,0 +1,31 @@ + + diff --git a/hwe/ts/components/BottomBar.vue b/hwe/ts/components/BottomBar.vue new file mode 100644 index 0000000..afbb361 --- /dev/null +++ b/hwe/ts/components/BottomBar.vue @@ -0,0 +1,32 @@ + + + diff --git a/hwe/ts/components/ChiefReservedCommand.vue b/hwe/ts/components/ChiefReservedCommand.vue new file mode 100644 index 0000000..65546e7 --- /dev/null +++ b/hwe/ts/components/ChiefReservedCommand.vue @@ -0,0 +1,969 @@ + + + + diff --git a/hwe/ts/components/CommandSelectForm.vue b/hwe/ts/components/CommandSelectForm.vue new file mode 100644 index 0000000..c9440b2 --- /dev/null +++ b/hwe/ts/components/CommandSelectForm.vue @@ -0,0 +1,218 @@ + + + + diff --git a/hwe/ts/components/DragSelect.vue b/hwe/ts/components/DragSelect.vue new file mode 100644 index 0000000..0e81976 --- /dev/null +++ b/hwe/ts/components/DragSelect.vue @@ -0,0 +1,221 @@ + + + diff --git a/hwe/ts/components/GeneralBasicCard.vue b/hwe/ts/components/GeneralBasicCard.vue new file mode 100644 index 0000000..64f8ded --- /dev/null +++ b/hwe/ts/components/GeneralBasicCard.vue @@ -0,0 +1,266 @@ + + + + + diff --git a/hwe/ts/components/GeneralList.vue b/hwe/ts/components/GeneralList.vue new file mode 100644 index 0000000..c57cbd8 --- /dev/null +++ b/hwe/ts/components/GeneralList.vue @@ -0,0 +1,1243 @@ + + + + + diff --git a/hwe/ts/components/GeneralSupplementCard.vue b/hwe/ts/components/GeneralSupplementCard.vue new file mode 100644 index 0000000..44220ff --- /dev/null +++ b/hwe/ts/components/GeneralSupplementCard.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/hwe/ts/components/MapCityBasic.vue b/hwe/ts/components/MapCityBasic.vue new file mode 100644 index 0000000..e718238 --- /dev/null +++ b/hwe/ts/components/MapCityBasic.vue @@ -0,0 +1,137 @@ + + + + + diff --git a/hwe/ts/components/MapCityDetail.vue b/hwe/ts/components/MapCityDetail.vue new file mode 100644 index 0000000..4283643 --- /dev/null +++ b/hwe/ts/components/MapCityDetail.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/hwe/ts/components/MapViewer.vue b/hwe/ts/components/MapViewer.vue new file mode 100644 index 0000000..742f112 --- /dev/null +++ b/hwe/ts/components/MapViewer.vue @@ -0,0 +1,493 @@ + + + + diff --git a/hwe/ts/components/NumberInputWithInfo.vue b/hwe/ts/components/NumberInputWithInfo.vue new file mode 100644 index 0000000..a95755e --- /dev/null +++ b/hwe/ts/components/NumberInputWithInfo.vue @@ -0,0 +1,131 @@ + + diff --git a/hwe/ts/components/SammoBar.vue b/hwe/ts/components/SammoBar.vue new file mode 100644 index 0000000..8358494 --- /dev/null +++ b/hwe/ts/components/SammoBar.vue @@ -0,0 +1,35 @@ + + + diff --git a/hwe/ts/components/SimpleClock.vue b/hwe/ts/components/SimpleClock.vue new file mode 100644 index 0000000..23c97ff --- /dev/null +++ b/hwe/ts/components/SimpleClock.vue @@ -0,0 +1,46 @@ + + + diff --git a/hwe/ts/components/SimpleNationList.vue b/hwe/ts/components/SimpleNationList.vue new file mode 100644 index 0000000..e559965 --- /dev/null +++ b/hwe/ts/components/SimpleNationList.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/hwe/ts/components/TipTap.vue b/hwe/ts/components/TipTap.vue new file mode 100644 index 0000000..70c42af --- /dev/null +++ b/hwe/ts/components/TipTap.vue @@ -0,0 +1,516 @@ + + + diff --git a/hwe/ts/components/TopBackBar.vue b/hwe/ts/components/TopBackBar.vue new file mode 100644 index 0000000..ce7cbd0 --- /dev/null +++ b/hwe/ts/components/TopBackBar.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/hwe/ts/currentCity.ts b/hwe/ts/currentCity.ts new file mode 100644 index 0000000..d5dbdc1 --- /dev/null +++ b/hwe/ts/currentCity.ts @@ -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); + }); +}); \ No newline at end of file diff --git a/hwe/ts/defs/API/Auction.ts b/hwe/ts/defs/API/Auction.ts new file mode 100644 index 0000000..a84ca72 --- /dev/null +++ b/hwe/ts/defs/API/Auction.ts @@ -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; +}; diff --git a/hwe/ts/defs/API/Betting.ts b/hwe/ts/defs/API/Betting.ts new file mode 100644 index 0000000..7170067 --- /dev/null +++ b/hwe/ts/defs/API/Betting.ts @@ -0,0 +1,38 @@ +import type { ValidResponse } from "@/defs"; + +export type BettingListResponse = ValidResponse & { + bettingList: Record>; + 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; + winner?: number[]; +} + +export type SelectItem = { + title: string; + info?: string; + isHtml?: boolean; + aux?: Record; +} diff --git a/hwe/ts/defs/API/Command.ts b/hwe/ts/defs/API/Command.ts new file mode 100644 index 0000000..fe3f6e1 --- /dev/null +++ b/hwe/ts/defs/API/Command.ts @@ -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[], +} diff --git a/hwe/ts/defs/API/General.ts b/hwe/ts/defs/API/General.ts new file mode 100644 index 0000000..9441355 --- /dev/null +++ b/hwe/ts/defs/API/General.ts @@ -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 +} \ No newline at end of file diff --git a/hwe/ts/defs/API/Global.ts b/hwe/ts/defs/API/Global.ts new file mode 100644 index 0000000..681567b --- /dev/null +++ b/hwe/ts/defs/API/Global.ts @@ -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; + cityConst: Record; + cityConstMap: { + region: Record; + level: Record; //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][]; + diplomacyList: Record>; + myNationID: number; +} \ No newline at end of file diff --git a/hwe/ts/defs/API/InheritAction.ts b/hwe/ts/defs/API/InheritAction.ts new file mode 100644 index 0000000..2cc3688 --- /dev/null +++ b/hwe/ts/defs/API/InheritAction.ts @@ -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[]; +}; diff --git a/hwe/ts/defs/API/Login.ts b/hwe/ts/defs/API/Login.ts new file mode 100644 index 0000000..5bf0ca2 --- /dev/null +++ b/hwe/ts/defs/API/Login.ts @@ -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, +} \ No newline at end of file diff --git a/hwe/ts/defs/API/Misc.ts b/hwe/ts/defs/API/Misc.ts new file mode 100644 index 0000000..68d9177 --- /dev/null +++ b/hwe/ts/defs/API/Misc.ts @@ -0,0 +1,5 @@ +import type { ValidResponse } from "@/defs"; + +export type UploadImageResponse = ValidResponse & { + path: string; +} \ No newline at end of file diff --git a/hwe/ts/defs/API/Nation.ts b/hwe/ts/defs/API/Nation.ts new file mode 100644 index 0000000..a1f64fe --- /dev/null +++ b/hwe/ts/defs/API/Nation.ts @@ -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, + } +} + +export type RawGeneralListP0 = ValidResponse & { + permission: 0, + column: (keyof GeneralListItemP0)[], + list: ValuesOf[][], + troops?: null, + env: ResponseEnv, +} + +export type RawGeneralListP1 = ValidResponse & { + permission: 1, + column: (keyof GeneralListItemP1)[], + list: ValuesOf[][], + troops?: null, + env: ResponseEnv, +} + +export type RawGeneralListP2 = ValidResponse & { + permission: 2 | 3 | 4, + column: (keyof GeneralListItemP2)[], + list: ValuesOf[][], + troops: [number, string][], + env: ResponseEnv, +} + +export type GeneralListResponse = RawGeneralListP0 | RawGeneralListP1 | RawGeneralListP2; diff --git a/hwe/ts/defs/API/NationCommand.ts b/hwe/ts/defs/API/NationCommand.ts new file mode 100644 index 0000000..65c5e17 --- /dev/null +++ b/hwe/ts/defs/API/NationCommand.ts @@ -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, +}; \ No newline at end of file diff --git a/hwe/ts/defs/API/Vote.ts b/hwe/ts/defs/API/Vote.ts new file mode 100644 index 0000000..54a7712 --- /dev/null +++ b/hwe/ts/defs/API/Vote.ts @@ -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; +} + +export type VoteDetailResult = ValidResponse & { + voteInfo: VoteInfo, + votes: [number[], number][], + comments: VoteComment[], + myVote: null|number[], + userCnt: number, +} \ No newline at end of file diff --git a/hwe/ts/defs/GameObj.ts b/hwe/ts/defs/GameObj.ts new file mode 100644 index 0000000..ad21e2c --- /dev/null +++ b/hwe/ts/defs/GameObj.ts @@ -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>; + + availableGeneralCommand: Record; + availableChiefCommand: Record; + + 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 | null | []; + defenceCoef: Record | 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; +}; + +export type GameIActionCategory = "nationType" | "specialDomestic" | "specialWar" | "personality" | "item" | "crewtype"; + +export type GameIActionInfo = { + value: string; + name: string; + info?: string | null; +} \ No newline at end of file diff --git a/hwe/ts/defs/gridDefs.ts b/hwe/ts/defs/gridDefs.ts new file mode 100644 index 0000000..1eca66a --- /dev/null +++ b/hwe/ts/defs/gridDefs.ts @@ -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 + }, + ], + }, +}; \ No newline at end of file diff --git a/hwe/ts/defs/index.ts b/hwe/ts/defs/index.ts new file mode 100644 index 0000000..8c4a907 --- /dev/null +++ b/hwe/ts/defs/index.ts @@ -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, + nation: Record +} + + +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, +} + +export type NationLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; +export const NationLevelText: Record = { + 0: '방랑군', + 1: '호족', + 2: '군벌', + 3: '주자사', + 4: '주목', + 5: '공', + 6: '왕', + 7: '황제', +} + +export type CityLevel = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8; +export const CityLevelText: Record = { + 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, + 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 = { + horse: '명마', + weapon: '무기', + book: '서적', + item: '도구', +} + +export declare type Colors = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'dark' | 'light'; + +export type IDItem = { + 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[keyof T]; + +export const NoneValue = 'None' as const; + +export type Optional = { + [Property in keyof Type]+?: Type[Property]; +}; + +export type OptionalFull = { + [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 = { + 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, + 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 +} \ No newline at end of file diff --git a/hwe/ts/diplomacy.ts b/hwe/ts/diplomacy.ts new file mode 100644 index 0000000..b3bcc7e --- /dev/null +++ b/hwe/ts/diplomacy.ts @@ -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, + letters: Record, + myNationID: number, +} + +const stateText: Record = { + 'proposed': '제안됨', + 'activated': '승인됨', + 'cancelled': '거부됨', + 'replaced': '대체됨', +}; + +const stateOptionText: Record, string> = { + 'try_destroy_src': '송신측의 파기 요청', + 'try_destroy_dest': '수신측의 파기 요청', +} + +async function submitLetter(e: JQuery.Event): Promise { + e.preventDefault(); + + const $brief = $('#inputBrief'); + const $detail = $('#inputDetail'); + const $prevNo = $('#inputPrevNo'); + const $destNation = $('#inputDestNation'); + const brief = $.trim(unwrap_any($brief.val())); + const detail = $.trim(unwrap_any($detail.val())); + let prevNo = $prevNo.val() as number | undefined; + const destNation = unwrap_any($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 { + + 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 = $(`#${letterObj.prev_no}`); + $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 { + 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 = $('
' + nationInfo.text + '
').css({ + 'color': fgColor, + 'background-color': nationInfo.color + }); + return $nationForm; +} + +function resizeTextarea($obj: JQuery) { + $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; + } +}); \ No newline at end of file diff --git a/hwe/ts/directives/vMyTooltip.ts b/hwe/ts/directives/vMyTooltip.ts new file mode 100644 index 0000000..6032a0d --- /dev/null +++ b/hwe/ts/directives/vMyTooltip.ts @@ -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 = { + 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 = { + 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 diff --git a/hwe/ts/extExpandCity.ts b/hwe/ts/extExpandCity.ts new file mode 100644 index 0000000..b442b54 --- /dev/null +++ b/hwe/ts/extExpandCity.ts @@ -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, +} + +type CityLevel = '특' | '대' | '중' | '소' | '이' | '진' | '관' | '수' + +type OfficerItem = { + preserved: boolean, + $obj: JQuery +} + +type CityItem = { + 지역: string, + 규모: CityLevel, + 이름: string, + + val: string, + users: JQuery, + userCnt: number, + obj: JQuery, + + 태수: OfficerItem, + 군사: OfficerItem, + 종사: OfficerItem, + + warn주민: boolean, + warn농업: boolean, + warn상업: boolean, + warn치안: boolean, + warn수비: boolean, + warn성벽: boolean, + + $주민: JQuery, + $인구율: JQuery, + $농업: JQuery, + $상업: JQuery, + $치안: JQuery, + $수비: JQuery, + $성벽: JQuery, + + 주민: 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 = {}; +const userList: Record = {}; + +$(function () { + let basicPath = document.location.pathname; + basicPath = basicPath.substring(0, basicPath.lastIndexOf('/')) + '/'; + + const mergeSort = function (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, typeName: OfficerSelector) { + $userList.each(function () { + const $this = $(this); + + const val = unwrap_any($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, typeName: OfficerSelector) { + $cityList.each(function () { + + const $this = $(this); + + const val = $.trim(unwrap_any($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('
'); + + + const addBtn = function ($name: JQuery, cityInfo: CityItem, userInfo: UserItem, level: number, typeName: OfficerSelector) { + + const enabled = cityInfo[typeName] && userInfo[typeName]; + const cityVal = cityInfo.val; + const $btn = $(''); + $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('' + + '' + + '' + + '' + + '' + + '' + + '' + + '
이 름통무지부 대자 금군 량병 종병 사훈련사기명 령삭턴
'); + + 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('정착 장려')); + if (cityInfo.warn농업) $work.html($work.html().split('농지 개간').join('농지 개간')); + if (cityInfo.warn상업) $work.html($work.html().split('상업 투자').join('상업 투자')); + if (cityInfo.warn치안) $work.html($work.html().split('치안 강화').join('치안 강화')); + if (cityInfo.warn수비) $work.html($work.html().split('수비 강화').join('수비 강화')); + if (cityInfo.warn성벽) $work.html($work.html().split('성벽 보수').join('성벽 보수')); + + 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(`${name}`); + } + if (cityList[cityName].군사.$obj.text() == name) { + cityList[cityName].군사.$obj.html(`${name}`); + } + if (cityList[cityName].종사.$obj.text() == name) { + cityList[cityName].종사.$obj.html(`${name}`); + } + + 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 = $(''); + $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('[' + cityInfo.remain농업 + ']'); + if (cityInfo.warn상업) cityInfo.$상업.append('[' + cityInfo.remain상업 + ']'); + if (cityInfo.warn치안) cityInfo.$치안.append('[' + cityInfo.remain치안 + ']'); + if (cityInfo.warn수비) cityInfo.$수비.append('[' + cityInfo.remain수비 + ']'); + if (cityInfo.warn성벽) cityInfo.$성벽.append('[' + cityInfo.remain성벽 + ']'); + + } + + //태수,군사,종사 + { + 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 = $(''); + $onGenList.click(function () { + loadUser(); + return false; + }); + $('form').append($onGenList); + + + $('table:eq(0) tr:last').after(''); + + + 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('
'); + $anchor.before(val.obj); + }); + $anchor.before('
'); + + }; + + let $btn: JQuery; + + $btn = $('').click(function () { + sortIt(function (a, b) { + return a.이름.localeCompare(b.이름); + }); + }); + $sort_more.append($btn); + + $btn = $('').click(function () { + sortIt(function (a, b) { + return 1.0 * a.주민 / a.max주민 - 1.0 * b.주민 / b.max주민; + }); + }); + $sort_more.append($btn); + + $btn = $('').click(function () { + sortIt(function (a, b) { + return a.remain주민 - b.remain주민; + }); + }); + $sort_more.append($btn); + + $btn = $('').click(function () { + sortIt(function (a, b) { + return a.remain농업 - b.remain농업; + }); + }); + $sort_more.append($btn); + + $btn = $('').click(function () { + sortIt(function (a, b) { + return a.remain상업 - b.remain상업; + }); + }); + $sort_more.append($btn); + + $btn = $('').click(function () { + sortIt(function (a, b) { + return a.remain치안 - b.remain치안; + }); + }); + $sort_more.append($btn); + + $btn = $('').click(function () { + sortIt(function (a, b) { + return a.remain수비 - b.remain수비; + }); + }); + $sort_more.append($btn); + + $btn = $('').click(function () { + sortIt(function (a, b) { + return a.remain성벽 - b.remain성벽; + }); + }); + $sort_more.append($btn); + + $btn = $('').click(function () { + sortIt(function (a, b) { + return b.userCnt - a.userCnt; + }); + }); + $sort_more.append($btn); + }; + + mainFunc(); +}); \ No newline at end of file diff --git a/hwe/ts/extKingdoms.ts b/hwe/ts/extKingdoms.ts new file mode 100644 index 0000000..86b484b --- /dev/null +++ b/hwe/ts/extKingdoms.ts @@ -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; +}; +declare const turnterm: number; + +type KingdomGeneral = { + html: JQuery, + 장수명: string + 국가: string + 벌점: number, + 통: number, + 무: number, + 지: number, + 삭턴: number, + 종류: UserType, + NPC: number, +} + +type UserType = "통" | "무" | "지" | "만능" | "평범" | "무능" | "무지"; + +type NationInfo = { + [v in UserType]: KingdomGeneral[] +} + +$(function () { + setAxiosXMLHttpRequest(); + + const $userFrame: JQuery = $('
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
얼 굴이 름연령성격특기레 벨국 가명 성계 급관 직통솔무력지력삭턴벌점
'); + $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 | undefined; + + const 국가별: Record = {}; + 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('

'); + $td.css('text-indent', '-5.8em').css('padding-left', '5.8em'); + for (const [종류명, 테이블] of Object.entries(국가정보)) { + + const $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(" "); + } + $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 = $(''); + const $obj2 = $(''); + $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 = $(''); + $btn.on('click', function () { + void runAnalysis(); + $btn.prop("disabled", true); + const $tr0 = $('table:eq(0) tr:eq(0)'); + $tr0.append('*벌점 순 정렬*
벌점 1500점 이상, 벌점 200점 이상, ' + + '삭턴장, ⓝ장' + '
전투장 : 만능장 + 무장 + 지장 + 평범장 - 삭턴자(만능, 무, 지, 평범) '); + }); + + + + $frame.append($btn); +}); \ No newline at end of file diff --git a/hwe/ts/extPluginTroop.ts b/hwe/ts/extPluginTroop.ts new file mode 100644 index 0000000..2acd0a4 --- /dev/null +++ b/hwe/ts/extPluginTroop.ts @@ -0,0 +1,124 @@ +import axios from "axios"; +import { unwrap } from "@util/unwrap"; +import { RuntimeError } from "@util/RuntimeError"; + +declare global { + interface Window { + userList: Record>; + } +} + +export function launchTroopPlugin($: JQueryStatic): void { + + let userList: Record> = {}; + const basicPath = (() => { + const path = document.location.pathname; + return path.substring(0, path.lastIndexOf('/')); + })(); + + const $userFrame: JQuery = $("
" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
이 름통무지자 금군 량도시병 종병 사훈련사기명 령삭턴
"); + $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 = (() => { + 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 = $(''); + $btn.click(async function () { + await runAnalysis(); + }); + + $frame.append($btn); + + + window.userList = userList; + + $('body').append($userFrame); + +} \ No newline at end of file diff --git a/hwe/ts/gateway/admin_member.ts b/hwe/ts/gateway/admin_member.ts new file mode 100644 index 0000000..36040bd --- /dev/null +++ b/hwe/ts/gateway/admin_member.ts @@ -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 = '\ +\ + <%userID%>\ + <%userName%>\ + <%emailFunc(email)%>
(<%authType%>)\ + <%userGradeText%>

<%shortDate(blockUntil)%>

\ + <%nickname%>\ + \ + <%slotGeneralList%>\ + <%shortDate(joinDate)%>\ + <%shortDate(loginDate)%>\ + <%shortDate(deleteAfter)%>\ + \ +
\ + \ + \ + \ + \ + \ +
\ + \ +'; + +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 ``; + }).join('
'); + + const emailFunc = function (text: string) { + return String(text).replace('@', '@
'); + } + const brFunc = function (text: string) { + return String(text).split(' ').join('
'); + }; + + 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 { + 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 { + 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($('#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($this.attr('name')), unwrap_any($this.val())); + }) +}); + +exportWindow(changeSystem, 'changeSystem'); +exportWindow(changeUserStatus, 'changeUserStatus'); \ No newline at end of file diff --git a/hwe/ts/gateway/admin_server.ts b/hwe/ts/gateway/admin_server.ts new file mode 100644 index 0000000..ac0e6ce --- /dev/null +++ b/hwe/ts/gateway/admin_server.ts @@ -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, + server: ServerStateItem[], + grade: number, +} + +type ServerChangeResponse = { + result: true, + installURL?: string, +} + +const serverAdminTemplate = '\ +\ + <%korName%>(<%name%>)\ + <%status%>\ + <%version%>\ + \ + \ + \ + \ + \ + \ +\ +';//TODO: npm install 관련 기능 추가, js/css output 경로 변경 + +declare global { + interface Window { + adminGrade: number; + aclList: Record; + } +} + +async function serverUpdate(caller: HTMLElement) { + const $caller = $(caller); + const $tr = $caller.parents('tr'); + const server = unwrap_any($tr.data('server_name')); + let isRoot: string | boolean = unwrap_any($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 { + 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($("#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($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'); \ No newline at end of file diff --git a/hwe/ts/gateway/common.ts b/hwe/ts/gateway/common.ts new file mode 100644 index 0000000..1333a49 --- /dev/null +++ b/hwe/ts/gateway/common.ts @@ -0,0 +1,3 @@ + +import 'bootstrap'; +import "@scss/common/bootstrap5.scss"; \ No newline at end of file diff --git a/hwe/ts/gateway/entrance.ts b/hwe/ts/gateway/entrance.ts new file mode 100644 index 0000000..a6a81a0 --- /dev/null +++ b/hwe/ts/gateway/entrance.ts @@ -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 = "\ +\ + \ + <%korName%>섭
\ + \ + \ + \ + - 폐 쇄 중 -\ +\ +"; + +const serverTextInfo = "\ +\ +서기 <%year%>년 <%month%>월 (<%scenario%>)
\ +유저 : <%userCnt%> / <%maxUserCnt%>명 NPC : <%npcCnt%>명 (<%turnTerm%>분 턴 서버)
\ +(상성 설정:<%fictionMode%>), (기타 설정:<%otherTextInfo%>)\ +\ +"; + +const serverProvisionalInfo = "\ +\ +- 오픈 일시 : <%opentime%> -
\ +서기 <%year%>년 <%month%>월 (<%scenario%>)
\ +유저 : <%userCnt%> / <%maxUserCnt%>명 NPC : <%npcCnt%>명 (<%turnTerm%>분 턴 서버)
\ +(상성 설정:<%fictionMode%>), (기타 설정:<%otherTextInfo%>)\ +\ +"; + +const serverFullTemplate = "\ +- 장수 등록 마감 -\ +"; + +const serverCreateTemplate = "\ +- 미 등 록 -\ +\ +
\ +<%if(canCreate) {%>\ +장수생성\ +<%}%>\ +<%if(canSelectNPC) {%>\ +장수빙의\ +<%}%>\ +<%if(canSelectPool) {%>\ +장수선택\ +<%}%>\ +
"; + +const serverLoginTemplate = "\ +\ +<%name%>\ +\ +
\ +입장\ +
\ +"; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const serverReservedTemplate = "\ +\ +<%openDatetime==starttime?'':'- 가오픈 일시 : '+openDatetime+ '-
'%>\ +- 오픈 일시 : <%starttime%> -
\ +<%scenarioName%> <%turnterm%>분 턴 서버
\ +(상성 설정:<%fictionMode%>), (빙의 여부:<%npcMode%>), (최대 스탯:<%gameConf.defaultStatTotal%>), (기타 설정:<%otherTextInfo%>)\ +\ +"; + +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, + 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> = {}; + + 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}
~ ${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}
~ ${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 = "../"; +} \ No newline at end of file diff --git a/hwe/ts/gateway/install.ts b/hwe/ts/gateway/install.ts new file mode 100644 index 0000000..5175ffd --- /dev/null +++ b/hwe/ts/gateway/install.ts @@ -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 = { + 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 = { + 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($('#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(); + + + +}); \ No newline at end of file diff --git a/hwe/ts/gateway/join.ts b/hwe/ts/gateway/join.ts new file mode 100644 index 0000000..5b44cac --- /dev/null +++ b/hwe/ts/gateway/join.ts @@ -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 = { + 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($('#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); +}); \ No newline at end of file diff --git a/hwe/ts/gateway/login.ts b/hwe/ts/gateway/login.ts new file mode 100644 index 0000000..dfeb676 --- /dev/null +++ b/hwe/ts/gateway/login.ts @@ -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 { + + 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('#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 = { + 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($('#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('#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($('#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('#running_map'));//TODO: 근황 여러개 볼 수 있도록? + const scrollHeight = unwrap(iframe.contentWindow).document.body.scrollHeight; + iframe.style.height = `${scrollHeight}px`; +} \ No newline at end of file diff --git a/hwe/ts/gateway/user_info.ts b/hwe/ts/gateway/user_info.ts new file mode 100644 index 0000000..7805512 --- /dev/null +++ b/hwe/ts/gateway/user_info.ts @@ -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; + 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 = $(`
\ + \ + \ +
`); + $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[] = []; + $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; + + 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); +}) diff --git a/hwe/ts/gridCellRenderer/GridTooltipCell.vue b/hwe/ts/gridCellRenderer/GridTooltipCell.vue new file mode 100644 index 0000000..b31ea92 --- /dev/null +++ b/hwe/ts/gridCellRenderer/GridTooltipCell.vue @@ -0,0 +1,59 @@ + + + + diff --git a/hwe/ts/gridCellRenderer/SimpleTooltipCell.vue b/hwe/ts/gridCellRenderer/SimpleTooltipCell.vue new file mode 100644 index 0000000..c3e6d4d --- /dev/null +++ b/hwe/ts/gridCellRenderer/SimpleTooltipCell.vue @@ -0,0 +1,45 @@ + + + diff --git a/hwe/ts/hallOfFame.ts b/hwe/ts/hallOfFame.ts new file mode 100644 index 0000000..b6a4f0e --- /dev/null +++ b/hwe/ts/hallOfFame.ts @@ -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(); +}); \ No newline at end of file diff --git a/hwe/ts/helpTexts.ts b/hwe/ts/helpTexts.ts new file mode 100644 index 0000000..290434d --- /dev/null +++ b/hwe/ts/helpTexts.ts @@ -0,0 +1,70 @@ +import type { NPCChiefActions, NPCGeneralActions } from "@/defs"; + +export const NPCPriorityBtnHelpMessage: { + [v in NPCChiefActions | NPCGeneralActions]: string; +} & + Record = { + 불가침제의: + "군주가 NPC이고, 타국에서 원조를 받았을 때,
세입(금/쌀) 대비 원조량에 따라 불가침제의를 합니다.", + 선전포고: + "군주가 NPC이고, 전쟁중이 아닐 때,
주변국중 하나를 골라 선포합니다.

선포 시점은 다음을 참고합니다.
- 인구율
- 도시내정률
- NPC전투장권장 금 충족률
- NPC전투장권장 쌀 충족률

국력이 낮은 국가를 조금 더 선호합니다.", + 천도: "인구가 많은 곳을 찾아 천도를 시도합니다.
영토의 가운데를 선호합니다.

도시 인구가 충분하다면, 굳이 천도하지는 않습니다.", + 유저장긴급포상: + "금/쌀이 부족한 유저전투장에게 긴급하게 포상합니다.
국고가 권장량보다 적어지더라도 시도합니다.", + 부대전방발령: + "(작동하지 않음)
전투 부대를 접경으로 발령합니다.
수도->시작점->도착점 경로를 따릅니다.", + 유저장구출발령: + "아군 영토에 있지 않은 유저장을 아군 영토로 발령합니다.
곧 집합하는 부대에 탑승한 경우는 제외합니다.", + 유저장후방발령: + "유저전투장 중에
- 병력이 충분하지 않고,
- 도시의 인구가 제자리 징병할 수 있을 정도로 충분하지 않고,
- 부대에 탑승하지 않았다면,
인구가 충분한 후방도시로 발령합니다.", + 부대유저장후방발령: + "접경에 위치한 부대에 탑승한 유저전투장 중에,
- 병력이 충분하지 않고,
- 첫 턴이 징병턴이며,
- 부대장 집합 턴 사이라면,
인구가 충분한 후방도시로 발령합니다.

부대장의 위치와 유저장의 위치가 다르다면 발령하지 않습니다.", + 유저장전방발령: + "후방에 있는 유저장이
- 병력을 가지고 있으며,
- 곧 훈련/사기진작이 완료될 것 같으면,
전방으로 발령합니다.

도시 관직이 많이 임명된 도시를 선호합니다.", + 유저장포상: + "금/쌀이 부족한 유저장에게 포상합니다.
유저전투장과 유저내정장은 각각 기준을 따릅니다.
국고 권장량을 가급적 지킵니다.", + 부대후방발령: + "(작동하지 않음)
후방 부대가 위치한 도시의 인구가 충분하지 않을 경우,
인구가 충분한 도시로 발령합니다.", + 부대구출발령: + "전투 부대, 후방 부대가 아닌 부대가 아군 영토에 있지 않을 때,
전방 도시 중 하나를 골라 발령합니다.", + NPC긴급포상: + "금/쌀이 부족한 NPC전투장에게 긴급하게 포상합니다.
국고가 권장량보다 '약간' 적어지더라도 시도합니다.", + NPC구출발령: "아군 영토에 있지 않은 NPC장을 아군 영토로 발령합니다.", + NPC후방발령: + "NPC전투장 중에
- 병력이 충분하지 않고,
- 도시의 인구가 제자리 징병할 수 있을 정도로 충분하지 않고,
- 부대에 탑승하지 않았다면,
인구가 충분한 후방도시로 발령합니다.", + NPC포상: + "금/쌀이 부족한 NPC에게 포상합니다.
NPC전투장과 NPC내정장은 각각 기준을 따릅니다.
국고 권장량을 가급적 지킵니다.", + NPC전방발령: + "후방에 있는 유저장이
- 병력을 가지고 있으며,
- 곧 훈련/사기진작이 완료될 것 같으면,
전방으로 발령합니다.

도시 관직이 많이 임명된 도시를 선호합니다.", + 유저장내정발령: + "내정중인 유저장이 위치한 도시의 내정률이 95% 이상이면
개발되지 않은 도시로 발령합니다.", + NPC내정발령: + "내정중인 NPC장이 위치한 도시의 내정률이 95% 이상이면
개발되지 않은 도시로 발령합니다.", + NPC몰수: + "국고가 부족하다면 NPC에게서 몰수합니다. 내정NPC장은 국고가 부족하지 않아도 몰수합니다.", + + NPC사망대비: + "NPC의 사망까지 5턴 이내인 경우, 헌납합니다.
헌납할 금쌀이 없다면 물자조달을 수행합니다.", + 귀환: "아국 도시에 있지 않다면 귀환합니다.", + 금쌀구매: + "전쟁 중에 금쌀의 비율이 크게 차이난다면 금쌀을 거래하여 비슷하게 맞춥니다.
금쌀 비율이 적절하는지 판단하는데 살상률을 포함합니다.
NPC는 상인이 없어도 금쌀을 구매할 수 있습니다.

또는 금쌀 한쪽이 지나치게 적은 경우에는 내정 중에도 금쌀을 거래합니다.", + 출병: "충분한 병력과 충분한 훈련/사기를 가지고 있는 경우 출병합니다.
접경이 여럿인 경우 무작위로 선택합니다.

타국과 전쟁중인 경우 공백지로는 출병하지 않습니다.", + 긴급내정: + "전쟁중에 민심이 70 미만이거나,
인구가 제자리 징병이 가능하지 않을 정도로 적을 경우,
일정확률로 주민선정과 정착장려를 수행합니다.

통솔이 높을 수록 수행할 확률이 높습니다.", + 전투준비: + "충분한 병력을 가지고 있지만 훈련과 사기가 부족한 경우 훈련과 사기진작을 수행합니다.", + 전방워프: "전투장이 충분한 병력을 가지고 있다면 전방으로 이동합니다.", + NPC헌납: + "국고가 부족한데 NPC전쟁장이 충분한 금쌀(권장량 대비 1.5배)을 가지고 있다면 일부를 헌납합니다.
NPC내정장은 국고가 넉넉하더라도 충분한 금쌀을 가지고 있다면 권장량만 유지하고 헌납합니다.", + 징병: "전쟁 중 병력을 소진하였다면 재 징병합니다.

기존에 사용한 병종군 중에서 사용가능한 병종을 랜덤하게 선택합니다.
고급 병종을 선택할 확률이 조금 더 높습니다.

NPC의 경우 도시의 인구가 충분하지 않다면 징병을 할 확률이 감소합니다.

유저장은 최대한 고급병종을 유지하며,
유저장 모병이 허용되는 경우 모병을 3회할 수 있다면 모병합니다.", + 후방워프: + "전쟁 중 병력을 소진하였는데 도시의 인구가 충분하지 않다면,
인구가 많은 도시로 이동합니다.", + 전쟁내정: + "전쟁 중 수행하는 내정입니다.
정착장려, 기술연구의 확률이 좀 더 높고,
치안강화, 농지개간, 상업투자의 확률이 낮습니다.

내정이 가능하다 하더라도 전시임을 고려해,
30% 확률로 다른 턴을 수행합니다.", + 소집해제: + "전쟁 중이 아닌 데 병력이 남아있는 경우,
3/4 확률로 소집해제합니다.", + 일반내정: + "도시에서 내정을 수행합니다. 낮은 내정일 수록 수행할 확률이 높습니다.
기술 연구는 1등급 이상 뒤쳐지지 않도록 노력합니다.", + 내정워프: + "도시에서 더이상 내정을 수행할 수 없는 경우,
일정확률로 내정이 부족한 다른 도시로 이동합니다.", +}; diff --git a/hwe/ts/history.ts b/hwe/ts/history.ts new file mode 100644 index 0000000..2e2a84a --- /dev/null +++ b/hwe/ts/history.ts @@ -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 = $(``); + $elements = $elements.add(option); + + currDate += 1; + [currYear, currMonth] = parseYearMonth(currDate); + } + $yearMonth.empty(); + $yearMonth.append($elements); +}); \ No newline at end of file diff --git a/hwe/ts/install.ts b/hwe/ts/install.ts new file mode 100644 index 0000000..c5a1617 --- /dev/null +++ b/hwe/ts/install.ts @@ -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 +} + +type ExpandedScenarioItem = ResponseScenarioItem & { + category: string, + idx: string, + year: number, +} + +type ResponseScenario = { + result: true, + scenario: Record +} + +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> = {}; + 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 = $('').attr('label', category); + for (const [idx, scenario] of Object.entries(items)) { + const $option = $('