Command 초안
- PHP 버전 거의 그대로 이식
This commit is contained in:
@@ -17,7 +17,8 @@
|
||||
"@strpc/express": "workspace:^",
|
||||
"date-fns": "^3.3.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"mongoose": "^8.2.1"
|
||||
"mongoose": "^8.2.1",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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 {
|
||||
id: GeneralID;
|
||||
@@ -73,5 +73,6 @@ export interface IGeneralEntity {
|
||||
aux: {
|
||||
lastBattleDate?: Date;
|
||||
lastBattleYearMonth?: IntYearMonth;
|
||||
lastTurn?: ReservedTurn & { term: number };
|
||||
}
|
||||
}
|
||||
Generated
+3
@@ -230,6 +230,9 @@ importers:
|
||||
mongoose:
|
||||
specifier: ^8.2.1
|
||||
version: 8.2.1
|
||||
zod:
|
||||
specifier: ^3.22.4
|
||||
version: 3.22.4
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^20.11.24
|
||||
|
||||
Reference in New Issue
Block a user