Command 초안
- PHP 버전 거의 그대로 이식
This commit is contained in:
@@ -17,7 +17,8 @@
|
|||||||
"@strpc/express": "workspace:^",
|
"@strpc/express": "workspace:^",
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^3.3.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"mongoose": "^8.2.1"
|
"mongoose": "^8.2.1",
|
||||||
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.24"
|
"@types/node": "^20.11.24"
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
import type { City } from "@/City.js";
|
||||||
|
import { testContraints, type IConstraint, type TestInput } from "@/Constraint/index.js";
|
||||||
|
import type { GameEnv } from "@/GameEnv.js";
|
||||||
|
import type { General } from "@/General.js";
|
||||||
|
import type { Nation } from "@/Nation.js";
|
||||||
|
import type { IntYearMonth, ReservedTurn } from "@/defs.js";
|
||||||
|
import type { RandUtil } from "@sammo/crypto";
|
||||||
|
import { InvalidArgument, must } from "@sammo/util";
|
||||||
|
import zod, { z } from "zod";
|
||||||
|
|
||||||
|
export const enum CommandType {
|
||||||
|
General = 0,
|
||||||
|
Nation = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const enum CompensateType {
|
||||||
|
Neutral = 0,
|
||||||
|
Positive = 1,
|
||||||
|
Negative = -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class BaseCommand<ArgType extends Record<string, unknown>>{
|
||||||
|
abstract readonly actionName: string;
|
||||||
|
abstract readonly className: string;
|
||||||
|
abstract readonly type: CommandType;
|
||||||
|
|
||||||
|
public abstract readonly compensateType: CompensateType;
|
||||||
|
|
||||||
|
readonly isLazyCalcReqTurn: boolean = false;
|
||||||
|
|
||||||
|
protected cachedPermissionToReserve = false;
|
||||||
|
protected cachedMinConditionMet = false;
|
||||||
|
protected cachedFullConditionMet = false;
|
||||||
|
|
||||||
|
protected reasonNotFullConditionMet: string | undefined = undefined;
|
||||||
|
protected reasonNotMinConditionMet: string | undefined = undefined;
|
||||||
|
protected reasonNotPermissionToReserve: string | undefined = undefined;
|
||||||
|
|
||||||
|
protected _destNation?: Nation;
|
||||||
|
protected _destCity?: City;
|
||||||
|
protected _destGeneral?: General;
|
||||||
|
|
||||||
|
public get destNation(): Readonly<Nation> | undefined {
|
||||||
|
return this._destNation;
|
||||||
|
}
|
||||||
|
public get destCity(): Readonly<City> | undefined {
|
||||||
|
return this._destCity;
|
||||||
|
}
|
||||||
|
public get destGeneral(): Readonly<General> | undefined {
|
||||||
|
return this._destGeneral;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract alternative: BaseCommand<any> | undefined;
|
||||||
|
|
||||||
|
protected abstract permissionConstraints: IConstraint<any>[] | undefined;
|
||||||
|
protected abstract minConditionConstraints: IConstraint<any>[] | undefined;
|
||||||
|
protected abstract fullConditionConstraints: IConstraint<any>[] | undefined;
|
||||||
|
protected abstract errorInvoker: string | undefined;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
public readonly general: General,
|
||||||
|
public readonly env: GameEnv,
|
||||||
|
public readonly arg: ArgType
|
||||||
|
) {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract init(): void;
|
||||||
|
protected initWithArg(): void {
|
||||||
|
if (this.arg === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new InvalidArgument("initWithArg must be overridden if arg is not undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
public get commandBrief(): string {
|
||||||
|
return this.actionName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//TODO: ExecuteKey의 타입
|
||||||
|
abstract getNextExecuteKey(): string;
|
||||||
|
abstract getNextAvailableTurn(): IntYearMonth | undefined;
|
||||||
|
abstract setNextAvailableTurn(turn?: IntYearMonth): void;
|
||||||
|
|
||||||
|
abstract getCost(): [gold: number, rice: number];
|
||||||
|
abstract getPreReqTurn(): number;
|
||||||
|
abstract getPostReqTurn(): number;
|
||||||
|
|
||||||
|
protected testPostReqTurn(): ["testPostReqTurn", string] | undefined {
|
||||||
|
if (this.getPostReqTurn() === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextAvailableTurn = this.getNextAvailableTurn();
|
||||||
|
if (nextAvailableTurn === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearMonth = this.env.raw.yearMonth;
|
||||||
|
const remainTurn = nextAvailableTurn - yearMonth;
|
||||||
|
if (remainTurn <= 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ["testPostReqTurn", `${remainTurn}턴 더 기다려야 합니다`];
|
||||||
|
}
|
||||||
|
|
||||||
|
testPermissionToReserve(): string | undefined {
|
||||||
|
if (this.cachedPermissionToReserve) {
|
||||||
|
return this.reasonNotPermissionToReserve;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.reasonNotPermissionToReserve) {
|
||||||
|
return this.reasonNotPermissionToReserve;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testResult = testContraints(this.permissionConstraints ?? [], this);
|
||||||
|
if (testResult !== undefined) {
|
||||||
|
[this.errorInvoker, this.reasonNotPermissionToReserve] = testResult;
|
||||||
|
}
|
||||||
|
this.cachedPermissionToReserve = true;
|
||||||
|
return this.reasonNotPermissionToReserve;
|
||||||
|
}
|
||||||
|
|
||||||
|
public canDisplay(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public testMinConditionMet(): string | undefined {
|
||||||
|
if (this.minConditionConstraints === undefined) {
|
||||||
|
return this.testFullConditionMet();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cachedMinConditionMet) {
|
||||||
|
return this.reasonNotMinConditionMet;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testResult = testContraints(this.minConditionConstraints, this);
|
||||||
|
if (testResult !== undefined) {
|
||||||
|
[this.errorInvoker, this.reasonNotMinConditionMet] = testResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.reasonNotMinConditionMet === undefined && !this.isLazyCalcReqTurn) {
|
||||||
|
const result = this.testPostReqTurn();
|
||||||
|
if (result !== undefined) {
|
||||||
|
[this.errorInvoker, this.reasonNotMinConditionMet] = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cachedMinConditionMet = true;
|
||||||
|
return this.reasonNotMinConditionMet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public testFullConditionMet(): string | undefined {
|
||||||
|
if (this.fullConditionConstraints === undefined) {
|
||||||
|
throw new InvalidArgument('fullConditionConstraints가 제대로 설정되지 않았습니다');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cachedFullConditionMet) {
|
||||||
|
return this.reasonNotFullConditionMet;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testResult = testContraints(this.fullConditionConstraints, this);
|
||||||
|
if (testResult !== undefined) {
|
||||||
|
[this.errorInvoker, this.reasonNotFullConditionMet] = testResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.reasonNotFullConditionMet === undefined) {
|
||||||
|
const result = this.testPostReqTurn();
|
||||||
|
if (result !== undefined) {
|
||||||
|
[this.errorInvoker, this.reasonNotFullConditionMet] = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cachedFullConditionMet = true;
|
||||||
|
return this.reasonNotFullConditionMet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get termString(): string {
|
||||||
|
const commandName = this.commandBrief;
|
||||||
|
const term = this.getNextAvailableTurn();
|
||||||
|
const termMax = this.getPreReqTurn() + 1;
|
||||||
|
return `${commandName} 수행중... (${term}/${termMax})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public addTermStack(): boolean {
|
||||||
|
if (this.getPreReqTurn() === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastTurn = this.general.raw.aux.lastTurn;
|
||||||
|
|
||||||
|
const nextTurn: Exclude<typeof lastTurn, undefined> = {
|
||||||
|
action: this.className,
|
||||||
|
arg: this.arg,
|
||||||
|
brief: this.commandBrief,
|
||||||
|
term: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lastTurn === undefined) {
|
||||||
|
this.general.update((raw) => {
|
||||||
|
raw.aux.lastTurn = nextTurn;
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastTurn.action !== this.className || lastTurn.arg !== this.arg) {
|
||||||
|
this.general.update((raw) => {
|
||||||
|
raw.aux.lastTurn = nextTurn;
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastTurn.term < this.getPreReqTurn()) {
|
||||||
|
nextTurn.term = lastTurn.term + 1;
|
||||||
|
this.general.update((raw) => {
|
||||||
|
raw.aux.lastTurn = nextTurn;
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get hasPermissionToReserve(): boolean {
|
||||||
|
return this.testPermissionToReserve() === undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get hasMinConditionMet(): boolean {
|
||||||
|
return this.testMinConditionMet() === undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get hasFullConditionMet(): boolean {
|
||||||
|
return this.testFullConditionMet() === undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get failString(): string {
|
||||||
|
const commandName = this.commandBrief;
|
||||||
|
const failReason = this.testFullConditionMet();
|
||||||
|
if (failReason === undefined) {
|
||||||
|
throw new Error('실행 가능한 커맨드에 대해 실패 이유를 수집');
|
||||||
|
}
|
||||||
|
return `${failReason} ${commandName} 실패.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get alternativeCommand(): BaseCommand<any> {
|
||||||
|
return must(this.alternative);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract run(rng: RandUtil): boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import type { City } from "@/City.js"
|
||||||
|
import type { GameEnv } from "@/GameEnv.js"
|
||||||
|
import type { General } from "@/General.js"
|
||||||
|
import type { Nation } from "@/Nation.js"
|
||||||
|
|
||||||
|
export const enum RequiredDestLevel {
|
||||||
|
None = 0,
|
||||||
|
Nation = 1,
|
||||||
|
City = 2,
|
||||||
|
General = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IConstraint<ArgType extends Record<string, unknown> = {}> {
|
||||||
|
readonly reason?: string;
|
||||||
|
readonly requiredDestLevel: RequiredDestLevel;
|
||||||
|
readonly arg: ArgType;
|
||||||
|
test(input: TestInput): undefined | [testName: string, err: string];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const constraints: {
|
||||||
|
[key: string]: IConstraint
|
||||||
|
} = {
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestInput {
|
||||||
|
general: Readonly<General>,
|
||||||
|
destNation?: Readonly<Nation>,
|
||||||
|
destCity?: Readonly<City>,
|
||||||
|
destGeneral?: Readonly<General>,
|
||||||
|
env: Readonly<GameEnv>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function testContraints(testList: IConstraint[], input: TestInput): undefined | [testName: string, err: string]{
|
||||||
|
for(const test of testList){
|
||||||
|
if(test.requiredDestLevel >= RequiredDestLevel.Nation && input.destNation === undefined){
|
||||||
|
return [test.constructor.name, "require destNation"];
|
||||||
|
}
|
||||||
|
else if(test.requiredDestLevel >= RequiredDestLevel.City && input.destCity === undefined){
|
||||||
|
return [test.constructor.name, "require destCity"];
|
||||||
|
}
|
||||||
|
else if(test.requiredDestLevel >= RequiredDestLevel.General && input.destGeneral === undefined){
|
||||||
|
return [test.constructor.name, "require destGeneral"];
|
||||||
|
}
|
||||||
|
const result = test.test(input);
|
||||||
|
if(result !== undefined){
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CityID, DiplomaticPermission, FractionalRange, GeneralID, GeneralName, IntMonth, IntYear, IntYearMonth, ItemID, ItemKeyType, NationID, NpcType, OfficerLevel, PersonalityType, SpecialityDomesticType, SpecialityWarType, SquadID, URILike, UserID, UserName } from "../defs.js";
|
import type { CityID, DiplomaticPermission, FractionalRange, GeneralID, GeneralName, IntMonth, IntYear, IntYearMonth, ItemID, ItemKeyType, NationID, NpcType, OfficerLevel, PersonalityType, ReservedTurn, SpecialityDomesticType, SpecialityWarType, SquadID, URILike, UserID, UserName } from "../defs.js";
|
||||||
|
|
||||||
export interface IGeneralEntity {
|
export interface IGeneralEntity {
|
||||||
id: GeneralID;
|
id: GeneralID;
|
||||||
@@ -73,5 +73,6 @@ export interface IGeneralEntity {
|
|||||||
aux: {
|
aux: {
|
||||||
lastBattleDate?: Date;
|
lastBattleDate?: Date;
|
||||||
lastBattleYearMonth?: IntYearMonth;
|
lastBattleYearMonth?: IntYearMonth;
|
||||||
|
lastTurn?: ReservedTurn & { term: number };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+3
@@ -230,6 +230,9 @@ importers:
|
|||||||
mongoose:
|
mongoose:
|
||||||
specifier: ^8.2.1
|
specifier: ^8.2.1
|
||||||
version: 8.2.1
|
version: 8.2.1
|
||||||
|
zod:
|
||||||
|
specifier: ^3.22.4
|
||||||
|
version: 3.22.4
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.11.24
|
specifier: ^20.11.24
|
||||||
|
|||||||
Reference in New Issue
Block a user