feat: eslint 적용 및 관련 코드 일괄 수정

This commit is contained in:
2026-01-05 15:46:47 +00:00
parent cb312f02b3
commit c965b1120f
387 changed files with 39808 additions and 38800 deletions
+2 -1
View File
@@ -8,7 +8,8 @@
"scripts": {
"build": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/common",
"dev": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/common --watch",
"lint": "node -e \"console.log('lint not configured')\"",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "vitest run --config vitest.config.ts",
"typecheck": "tsc -b"
},
+4 -13
View File
@@ -39,13 +39,9 @@ export interface GameSessionTokenPayload {
const toBase64Url = (data: Buffer): string => data.toString('base64url');
const fromBase64Url = (value: string): Buffer => Buffer.from(value, 'base64url');
const buildKey = (secret: string): Buffer =>
createHash('sha256').update(secret).digest();
const buildKey = (secret: string): Buffer => createHash('sha256').update(secret).digest();
export const encryptGameSessionToken = (
payload: GameSessionTokenPayload,
secret: string
): string => {
export const encryptGameSessionToken = (payload: GameSessionTokenPayload, secret: string): string => {
const iv = randomBytes(12);
const key = buildKey(secret);
const cipher = createCipheriv('aes-256-gcm', key, iv);
@@ -90,10 +86,7 @@ const parsePayload = (value: unknown): GameSessionTokenPayload | null => {
return payload as GameSessionTokenPayload;
};
export const decryptGameSessionToken = (
token: string,
secret: string
): GameSessionTokenPayload | null => {
export const decryptGameSessionToken = (token: string, secret: string): GameSessionTokenPayload | null => {
const parts = token.split('.');
if (parts.length !== 3) {
return null;
@@ -106,9 +99,7 @@ export const decryptGameSessionToken = (
const key = buildKey(secret);
const decipher = createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(tag);
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString(
'utf8'
);
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
return parsePayload(JSON.parse(plaintext));
} catch {
return null;
+1 -6
View File
@@ -1,9 +1,4 @@
export type TurnDaemonState =
| 'idle'
| 'running'
| 'flushing'
| 'paused'
| 'stopping';
export type TurnDaemonState = 'idle' | 'running' | 'flushing' | 'paused' | 'stopping';
export type RunReason = 'schedule' | 'manual' | 'poke';
+4 -19
View File
@@ -1,12 +1,4 @@
export type JosaKey =
| '은'
| '이'
| '과'
| '이나'
| '을'
| '으로'
| '이라'
| '이랑';
export type JosaKey = '은' | '이' | '과' | '이나' | '을' | '으로' | '이라' | '이랑';
const DEFAULT_POSTPOSITION: Record<JosaKey, string> = {
: '는',
@@ -40,10 +32,7 @@ const KO_FINISH_CODE = 0xd7a3;
const JONGSUNG_RIEUL = 8;
const getLastChar = (text: string): string => {
const cleaned = text
.replace(REG_INVALID_CHAR, ' ')
.replace(REG_TARGET_CHAR, '$1')
.trim();
const cleaned = text.replace(REG_INVALID_CHAR, ' ').replace(REG_TARGET_CHAR, '$1').trim();
if (!cleaned) {
return '';
}
@@ -51,9 +40,7 @@ const getLastChar = (text: string): string => {
return chars[chars.length - 1] ?? '';
};
const getDigitJongsung = (
digit: number
): { has: boolean; rieul: boolean } => {
const getDigitJongsung = (digit: number): { has: boolean; rieul: boolean } => {
switch (digit) {
case 0:
case 3:
@@ -121,9 +108,7 @@ export class JosaUtil {
}
const isRo = withJongsung === '으로';
return hasJongsung(normalizedText, isRo)
? withJongsung
: withoutJongsung;
return hasJongsung(normalizedText, isRo) ? withJongsung : withoutJongsung;
}
static put(text: string, wJongsung: string, woJongsung = ''): string {
+5 -3
View File
@@ -79,13 +79,16 @@ function calcBitMask(n: bigint): bigint {
// SHA-512 기반 DRBG 구현
export class LiteHashDRBG implements RNG {
protected buffer!: ArrayBuffer;
protected bufferIdx!: number;
protected hq: DataView;
protected hqIdxPos: number;
public constructor(protected seed: BytesLike, protected stateIdx = 0, bufferIdx = 0) {
public constructor(
protected seed: BytesLike,
protected stateIdx = 0,
bufferIdx = 0
) {
if (bufferIdx < 0) {
throw new Error(`bufferIdx ${bufferIdx} < 0`);
}
@@ -215,7 +218,6 @@ export class LiteHashDRBG implements RNG {
}
public nextFloat1(): number {
// eslint-disable-next-line no-constant-condition
while (true) {
const nInt = this._nextInt(maxRngSupportBit + 1);
if (nInt < maxIntMore1) {
-1
View File
@@ -1,5 +1,4 @@
export interface RNG {
/**
* nextInt()가 반환 가능한 최댓값
*/
+2 -4
View File
@@ -2,9 +2,7 @@ import type { RNG } from './RNG.js';
// RNG 유틸리티 모음
export class RandUtil {
constructor(protected rng: RNG) {
}
constructor(protected rng: RNG) {}
public nextFloat1(): number {
return this.rng.nextFloat1();
@@ -12,7 +10,7 @@ export class RandUtil {
public nextRange(min: number, max: number): number {
const range = max - min;
return this.nextFloat1() * (range) + min;
return this.nextFloat1() * range + min;
}
public nextRangeInt(min: number, max: number): number {
@@ -5,11 +5,7 @@ export function convertBytesLikeToArrayBuffer(data: BytesLike, encodeUTF8 = true
return data;
}
if (data instanceof Uint8Array) {
if (
data.byteOffset === 0
&& data.byteLength === data.buffer.byteLength
&& data.buffer instanceof ArrayBuffer
) {
if (data.byteOffset === 0 && data.byteLength === data.buffer.byteLength && data.buffer instanceof ArrayBuffer) {
return data.buffer;
}
return data.slice().buffer;
@@ -18,9 +14,9 @@ export function convertBytesLikeToArrayBuffer(data: BytesLike, encodeUTF8 = true
const view = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
return view.slice().buffer;
}
if (typeof (data) === 'string') {
if (typeof data === 'string') {
if (encodeUTF8) {
return (new TextEncoder()).encode(data).buffer;
return new TextEncoder().encode(data).buffer;
}
return new Uint8Array(data.split('').map((s) => s.codePointAt(0) as number)).buffer;
}
@@ -1,15 +1,8 @@
import type { BytesLike } from './BytesLike.js';
export function convertBytesLikeToUint8Array(
data: BytesLike,
encodeUTF8 = true
): Uint8Array<ArrayBuffer> {
export function convertBytesLikeToUint8Array(data: BytesLike, encodeUTF8 = true): Uint8Array<ArrayBuffer> {
if (data instanceof Uint8Array) {
if (
data.buffer instanceof ArrayBuffer
&& data.byteOffset === 0
&& data.byteLength === data.buffer.byteLength
) {
if (data.buffer instanceof ArrayBuffer && data.byteOffset === 0 && data.byteLength === data.buffer.byteLength) {
return data;
}
return new Uint8Array(data) as Uint8Array<ArrayBuffer>;
@@ -20,9 +13,9 @@ export function convertBytesLikeToUint8Array(
if (data instanceof DataView) {
return new Uint8Array<ArrayBuffer>(data.buffer, data.byteOffset, data.byteLength);
}
if (typeof (data) === 'string') {
if (typeof data === 'string') {
if (encodeUTF8) {
return (new TextEncoder()).encode(data);
return new TextEncoder().encode(data);
}
return new Uint8Array(data.split('').map((s) => s.codePointAt(0) as number));
}
+3 -8
View File
@@ -10,24 +10,19 @@ type NodeHash = {
type NodeCreateHash = (algorithm: 'sha512') => NodeHash;
const isNode = typeof process !== 'undefined'
&& typeof process.versions?.node === 'string';
const isNode = typeof process !== 'undefined' && typeof process.versions?.node === 'string';
const nodeCryptoSpecifier = 'node:crypto';
let nodeCreateHash: NodeCreateHash | null = null;
if (isNode) {
const nodeCrypto = await import(nodeCryptoSpecifier) as typeof import('node:crypto');
const nodeCrypto = (await import(nodeCryptoSpecifier)) as typeof import('node:crypto');
nodeCreateHash = nodeCrypto.createHash as NodeCreateHash;
}
function normalizeUint8Array(bytes: Uint8Array): Uint8Array<ArrayBuffer> {
if (
bytes.buffer instanceof ArrayBuffer
&& bytes.byteOffset === 0
&& bytes.byteLength === bytes.buffer.byteLength
) {
if (bytes.buffer instanceof ArrayBuffer && bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) {
return bytes as Uint8Array<ArrayBuffer>;
}
const out = new Uint8Array(bytes.byteLength);
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=clock.test.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"clock.test.d.ts","sourceRoot":"","sources":["clock.test.ts"],"names":[],"mappings":""}
+44
View File
@@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest';
import { ManualClock, StepClock } from '../src/time/Clock.js';
describe('ManualClock', () => {
it('returns current time without advancing', () => {
const clock = new ManualClock(1000);
expect(clock.nowMs()).toBe(1000);
expect(clock.nowMs()).toBe(1000);
});
it('advances with sleep and manual advance', async () => {
const clock = new ManualClock(0);
await clock.sleepMs(250);
expect(clock.nowMs()).toBe(250);
clock.advanceMs(750);
expect(clock.nowMs()).toBe(1000);
});
it('can set time explicitly', () => {
const clock = new ManualClock(10);
clock.setMs(5000);
expect(clock.nowMs()).toBe(5000);
});
});
describe('StepClock', () => {
it('advances on each nowMs call', () => {
const clock = new StepClock(100, 0);
expect(clock.nowMs()).toBe(100);
expect(clock.nowMs()).toBe(200);
expect(clock.nowMs()).toBe(300);
});
it('advances with sleep', async () => {
const clock = new StepClock(50, 1000);
await clock.sleepMs(200);
expect(clock.nowMs()).toBe(1250);
});
it('advances manually', () => {
const clock = new StepClock(10, 0);
clock.advanceMs(50);
expect(clock.nowMs()).toBe(60);
});
it('rejects non-positive step', () => {
expect(() => new StepClock(0)).toThrow('stepMs must be positive');
expect(() => new StepClock(-5)).toThrow('stepMs must be positive');
});
});
//# sourceMappingURL=clock.test.js.map
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=rng.test.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"rng.test.d.ts","sourceRoot":"","sources":["rng.test.ts"],"names":[],"mappings":""}
+317
View File
@@ -0,0 +1,317 @@
import { describe, expect, it } from 'vitest';
import {
bufferByteSize,
convertBytesLikeToArrayBuffer,
convertBytesLikeToUint8Array as toBytes,
LiteHashDRBG,
RandUtil,
} from '../src/index.js';
const range = (count) => Array.from({ length: count }, (_, idx) => idx);
const expectBytes = (actual, expected) => {
expect(Array.from(actual)).toEqual(Array.from(expected));
};
function fillBlock(body, filler = '\0', length = bufferByteSize) {
const u8Body = toBytes(body);
const u8Filler = toBytes(filler, false);
if (u8Filler.byteLength < 1) {
throw new Error('filler must have length');
}
const buffer = new Uint8Array(length);
buffer.set(u8Body, 0);
let bufferIdx = u8Body.byteLength;
while (bufferIdx + u8Filler.byteLength < length) {
buffer.set(u8Filler, bufferIdx);
bufferIdx += u8Filler.byteLength;
}
if (bufferIdx < length) {
const slice = new Uint8Array(u8Filler.buffer, u8Filler.byteOffset, length - bufferIdx);
buffer.set(slice, bufferIdx);
}
return buffer;
}
class DummyBlockRNG extends LiteHashDRBG {
repeatBlockCnt;
repeatBlock;
constructor(repeatBlock, stateIdx = 0) {
super('x');
this.repeatBlock = [];
for (const rawBlock of repeatBlock) {
const block = convertBytesLikeToArrayBuffer(rawBlock);
if (block.byteLength !== bufferByteSize) {
throw new Error('invalid block size');
}
this.repeatBlock.push(block);
}
this.repeatBlockCnt = this.repeatBlock.length;
this.stateIdx = stateIdx;
this.bufferIdx = 0;
this.genNextBlock();
}
genNextBlock() {
if (!this.repeatBlock) {
return;
}
this.buffer = this.repeatBlock[this.stateIdx];
this.bufferIdx = 0;
this.stateIdx = (this.stateIdx + 1) % this.repeatBlockCnt;
}
}
const fixedKey = 'HelloWorld';
describe('RNGtestDummy', () => {
const rng = new DummyBlockRNG([fillBlock('', '\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff')]);
it('BasicConvert', () => {
expect(toBytes('\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff', false).length).toBe(16);
});
it('SimpleByte', () => {
expectBytes(toBytes('\x00', false), rng.nextBytes(1));
expectBytes(toBytes('\x11\x22', false), rng.nextBytes(2));
expectBytes(toBytes('\x33\x44\x55', false), rng.nextBytes(3));
expectBytes(toBytes('\x66\x77\x88\x99', false), rng.nextBytes(4));
});
it('OverflowBlock', () => {
for (let idx = 0; idx < 16; idx += 1) {
expectBytes(
toBytes('\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99', false),
rng.nextBytes(16)
);
}
});
it('MultiBlock', () => {
expectBytes(
fillBlock('', '\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99', bufferByteSize * 2),
rng.nextBytes(bufferByteSize * 2)
);
});
it('bitTest', () => {
expectBytes(toBytes('\x00', false), rng.nextBits(1)); //aa
expectBytes(toBytes('\x01', false), rng.nextBits(1)); //bb
expectBytes(toBytes('\xcc', false), rng.nextBits(8)); //cc
expectBytes(toBytes('\xdd\x02', false), rng.nextBits(10)); //ddee
expectBytes(toBytes('\x7f', false), rng.nextBits(7)); //ff
expectBytes(toBytes('\x00\x11\x22\x33\x44\x55\x06', false), rng.nextBits(53));
});
it('int', () => {
expect(rng.nextInt(0xff)).toBe(0x77);
expect(rng.nextInt((1 << 16) - 1)).toBe(0x9988);
expect(rng.nextInt(0xffffffff)).toBe(0xddccbbaa);
expect(rng.nextInt()).toBe(0x0433221100ffee);
expect(rng.nextInt(0x0f)).toBe(0x05); //55
expect(rng.nextInt(0x12)).toBe(0x06); //66
expect(rng.nextInt(99)).toBe(0x08); //77(119 -> 7bit) -> 88(136 -> 8bit -> 8)
expect(rng.nextInt(0x99)).toBe(0x99); //99
expect(rng.nextInt(0xaa)).toBe(0xaa); //aa (fit Max)
});
it('float', () => {
const floatMax = 2 ** 53;
const fa = rng.nextFloat1();
expect(fa).toBe(0x1100ffeeddccbb / floatMax);
expect(0.5313720384 > fa).toBe(true);
expect(0.5313720383 < fa).toBe(true);
const fb = rng.nextFloat1();
expect(fb).toBe(0x08776655443322 / floatMax);
});
});
describe('RandUtilDummy', () => {
it('shuffle', () => {
const rng = new DummyBlockRNG([fillBlock('', '\x17\x16\x15\x14\x13\x12\x11\x10')]);
const randUtil = new RandUtil(rng);
/**
* 7, [7,1,2,3,4,5,6,0]
* 6, [7,0,2,3,4,5,6,1]
* 5, [7,0,1,3,4,5,6,2]
* 4, [7,0,1,2,4,5,6,3]
* 3, [7,0,1,2,3,5,6,4]
* 2, [7,0,1,2,3,4,6,5]
* 1, [7,0,1,2,3,4,5,6]
*/
expect(randUtil.shuffle(range(8))).toEqual([7, 0, 1, 2, 3, 4, 5, 6]);
/**
* 0, [0,1,2,3,4,5,6,7,8,9]
* 7, [0,8,2,3,4,5,6,7,1,9]
* 6, [0,8,1,3,4,5,6,7,2,9]
* 5, [0,8,1,2,4,5,6,7,3,9]
* 4, [0,8,1,2,3,5,6,7,4,9]
* 3, [0,8,1,2,3,4,6,7,5,9]
* 2, [0,8,1,2,3,4,5,7,6,9]
* 1, [0,8,1,2,3,4,5,6,7,9]
* 0, [0,8,1,2,3,4,5,6,7,9]
*/
expect(randUtil.shuffle(range(10))).toEqual([0, 8, 1, 2, 3, 4, 5, 6, 7, 9]);
});
const rng = new DummyBlockRNG([fillBlock('', '\x17\x16\x15\x14\x13\x12\x11\x10')]);
const randUtil = new RandUtil(rng);
it('choice', () => {
//0x17(7), 0x16(6)
expect(randUtil.choice([0, 1, 2, 3, 4, 5])).toBe(5);
//0x15(5), Set 순서 유지
expect(randUtil.choice(new Set([5, 3, 1, 2, 8, 0]))).toBe(8);
//0x14(4), 정렬 순서상 숫자(소-대) > 문자열(삽입순) > 심볼 순서
expect(randUtil.choice({ c: 'c', a: 'a', b: 'b', 4: 'x', 2: 't', 3: 'q' })).toBe('c');
});
it('choiceUsingWeight', () => {
//0.6275740099377194 * 38.1 = 23.91
expect(
randUtil.choiceUsingWeight({
a: 0.1,
b: 10,
tt: 2,
x: -1,
c: 20,
d: 0,
e: 6,
})
).toBe('c');
//0.658946544056166
expect(randUtil.choiceUsingWeightPair([['xx', 10]])).toBe('xx');
//0.6903152783785083 * 27.3 = 18.84560709973328
expect(
randUtil.choiceUsingWeightPair([
['e', 10],
['d', 4],
['c', 0.1],
['baba', 0.2],
['q', 9],
['xt', 4],
])
).toBe('q');
});
});
describe('RNGexpectedError', () => {
const rng = new LiteHashDRBG(fixedKey);
it('nextBits0', () => {
expect(() => rng.nextBits(0)).toThrow();
});
it('nextBits-1', () => {
expect(() => rng.nextBits(-1)).toThrow();
});
it('nextBytes0', () => {
expect(() => rng.nextBytes(0)).toThrow();
});
it('nextBytes-1', () => {
expect(() => rng.nextBytes(-1)).toThrow();
});
const randUtil = new RandUtil(rng);
it('utilEmptyChoice', () => {
expect(() => randUtil.choice([])).toThrow();
});
it('utilEmptyChoiceUsingWeight', () => {
expect(() => randUtil.choiceUsingWeight({})).toThrow();
});
it('utilEmptyChoiceUsingWeightPair', () => {
expect(() => randUtil.choiceUsingWeightPair([])).toThrow();
});
});
describe('RNGAcceptable', () => {
const rng = new LiteHashDRBG(fixedKey);
it('RNG', () => {
rng.nextInt(0);
rng.nextInt(2 ** 53 - 1);
rng.nextBytes(65);
rng.nextBits(512);
});
const randUtil = new RandUtil(rng);
it('RandUtil', () => {
randUtil.choice([0, 0, 0]);
randUtil.choiceUsingWeight({
0: 0,
1: -1,
});
randUtil.choiceUsingWeightPair([
[0, 0],
[1, 0],
[2, -2],
]);
randUtil.nextBool(1.1);
randUtil.nextBool(-0.1);
randUtil.shuffle([]);
randUtil.shuffle([1]);
randUtil.nextRange(0, 0);
randUtil.nextRangeInt(0, 0);
randUtil.nextRange(1, -1);
randUtil.nextRangeInt(1, -1);
});
it('RNGLong', () => {
const longKey = fixedKey;
for (let idx = 0; idx < 8; idx += 1) {
longKey.concat(longKey);
}
const rngLong = new LiteHashDRBG(longKey);
for (let idx = 0; idx < 10; idx += 1) {
rngLong.nextBytes(16);
}
});
});
/* Python TestVector
import hashlib
import struct
fixedKey = 'HelloWorld'.encode('utf-8')
def hash(key, idx):
idxV = struct.pack("<I", idx)
return hashlib.sha512(key + idxV).digest()
for idx in range(5):
print(hash(fixedKey, idx).hex())
*/
describe('RNG', () => {
//JS - PHP 일치 확인 정도로.
const testVector = Buffer.from(
[
'24d9ccd648556255fd0ee9f5b29918de90617341958b3b354d572167e4dee02b757816a2bbe0b502c52413ffd384381a9d7b4e193df6f4345d6a95e111d661c4',
'2e9264512f6f4b080cf1376b74fab6878ecf4a6e185942d2e5b22cf923885b9952d40601a414225d6901417fd4ce9368ac77e4a63d3fc9b58ab952bb8c33f165',
'8e2ebf5af6283a1b18f4c044c86c20d02be3890613c4cc8b7c6b7b35581263b972a82630df69a9289988422d7c3a9be5edf78d5de16fabd01e5dd4e458068d8a',
'398596047ba547bfe371ec863a3e019ab0dbc4bb3b27e9077685aae4283ff6bbccfd981d92f9358f7efffbb72a940414802d98466d132e2ad0a16a12946d5f47',
'b3606fe9b18c4aa7315e78bb9e47cb51cc4e203fcc2e631f0405c1b872c8e1cb5b6415ea74bbb77fffaaadb002b47cb4f4628dc0709634365b187667f5c708cb',
].join(''),
'hex'
);
it('bytes', () => {
const rng = new LiteHashDRBG(fixedKey);
let offset = 0;
expectBytes(rng.nextBytes(10), testVector.slice(offset, offset + 10));
offset += 10;
expectBytes(rng.nextBytes(32), testVector.slice(offset, offset + 32));
offset += 32;
expectBytes(rng.nextBytes(1), testVector.slice(offset, offset + 1));
offset += 1;
expectBytes(rng.nextBytes(64), testVector.slice(offset, offset + 64));
offset += 64;
expectBytes(rng.nextBytes(5), testVector.slice(offset, offset + 5));
offset += 5;
const lastA = rng.nextBytes(16, 18);
const lastB = new Uint8Array(18);
lastB.set(testVector.slice(offset, offset + 16));
expectBytes(lastA, lastB);
});
it('bits', () => {
const rng = new LiteHashDRBG(fixedKey);
let offset = 0;
const testBits = [10, 4, 15, 32, 7, 99, 512, 1, 2, 3];
for (const bits of testBits) {
const bytes = Math.ceil(bits / 8);
const A = rng.nextBits(bits);
const B = new Uint8Array(testVector.slice(offset, offset + bytes));
offset += bytes;
if (bits % 8 !== 0) {
const bitMask = 0xff >> (8 - (bits % 8));
B[bytes - 1] &= bitMask;
}
expectBytes(A, B);
}
});
it('float', () => {
const rng = new LiteHashDRBG(fixedKey);
const rng2 = new DummyBlockRNG([
new Uint8Array(testVector.slice(bufferByteSize * 0, bufferByteSize * 1)),
new Uint8Array(testVector.slice(bufferByteSize * 1, bufferByteSize * 2)),
new Uint8Array(testVector.slice(bufferByteSize * 2, bufferByteSize * 3)),
new Uint8Array(testVector.slice(bufferByteSize * 3, bufferByteSize * 4)),
new Uint8Array(testVector.slice(bufferByteSize * 4, bufferByteSize * 5)),
]);
for (let i = 0; i < 18; i++) {
expect(rng.nextFloat1()).toBe(rng2.nextFloat1());
}
});
});
//# sourceMappingURL=rng.test.js.map
File diff suppressed because one or more lines are too long
+53 -63
View File
@@ -17,11 +17,7 @@ const expectBytes = (actual: Uint8Array, expected: Uint8Array): void => {
expect(Array.from(actual)).toEqual(Array.from(expected));
};
function fillBlock(
body: MaybeBytes,
filler: MaybeBytes = '\0',
length = bufferByteSize
): Uint8Array<ArrayBuffer> {
function fillBlock(body: MaybeBytes, filler: MaybeBytes = '\0', length = bufferByteSize): Uint8Array<ArrayBuffer> {
const u8Body = toBytes(body);
const u8Filler = toBytes(filler, false);
@@ -80,26 +76,23 @@ class DummyBlockRNG extends LiteHashDRBG {
const fixedKey = 'HelloWorld';
describe('RNGtestDummy', () => {
const rng = new DummyBlockRNG([
fillBlock('', "\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff")
]);
const rng = new DummyBlockRNG([fillBlock('', '\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff')]);
it('BasicConvert', () => {
expect(toBytes("\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff", false).length)
.toBe(16);
expect(toBytes('\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff', false).length).toBe(16);
});
it('SimpleByte', () => {
expectBytes(toBytes("\x00", false), rng.nextBytes(1));
expectBytes(toBytes("\x11\x22", false), rng.nextBytes(2));
expectBytes(toBytes("\x33\x44\x55", false), rng.nextBytes(3));
expectBytes(toBytes("\x66\x77\x88\x99", false), rng.nextBytes(4));
expectBytes(toBytes('\x00', false), rng.nextBytes(1));
expectBytes(toBytes('\x11\x22', false), rng.nextBytes(2));
expectBytes(toBytes('\x33\x44\x55', false), rng.nextBytes(3));
expectBytes(toBytes('\x66\x77\x88\x99', false), rng.nextBytes(4));
});
it('OverflowBlock', () => {
for (let idx = 0; idx < 16; idx += 1) {
expectBytes(
toBytes("\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99", false),
toBytes('\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99', false),
rng.nextBytes(16)
);
}
@@ -107,18 +100,18 @@ describe('RNGtestDummy', () => {
it('MultiBlock', () => {
expectBytes(
fillBlock('', "\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99", bufferByteSize * 2),
fillBlock('', '\xaa\xbb\xcc\xdd\xee\xff\x00\x11\x22\x33\x44\x55\x66\x77\x88\x99', bufferByteSize * 2),
rng.nextBytes(bufferByteSize * 2)
);
});
it('bitTest', () => {
expectBytes(toBytes("\x00", false), rng.nextBits(1)); //aa
expectBytes(toBytes("\x01", false), rng.nextBits(1)); //bb
expectBytes(toBytes("\xcc", false), rng.nextBits(8)); //cc
expectBytes(toBytes("\xdd\x02", false), rng.nextBits(10)); //ddee
expectBytes(toBytes("\x7f", false), rng.nextBits(7)); //ff
expectBytes(toBytes("\x00\x11\x22\x33\x44\x55\x06", false), rng.nextBits(53));
expectBytes(toBytes('\x00', false), rng.nextBits(1)); //aa
expectBytes(toBytes('\x01', false), rng.nextBits(1)); //bb
expectBytes(toBytes('\xcc', false), rng.nextBits(8)); //cc
expectBytes(toBytes('\xdd\x02', false), rng.nextBits(10)); //ddee
expectBytes(toBytes('\x7f', false), rng.nextBits(7)); //ff
expectBytes(toBytes('\x00\x11\x22\x33\x44\x55\x06', false), rng.nextBits(53));
});
it('int', () => {
@@ -157,9 +150,7 @@ describe('RandUtilDummy', () => {
* 2, [7,0,1,2,3,4,6,5]
* 1, [7,0,1,2,3,4,5,6]
*/
expect(randUtil.shuffle(range(8))).toEqual(
[7, 0, 1, 2, 3, 4, 5, 6]
);
expect(randUtil.shuffle(range(8))).toEqual([7, 0, 1, 2, 3, 4, 5, 6]);
/**
* 0, [0,1,2,3,4,5,6,7,8,9]
@@ -172,9 +163,7 @@ describe('RandUtilDummy', () => {
* 1, [0,8,1,2,3,4,5,6,7,9]
* 0, [0,8,1,2,3,4,5,6,7,9]
*/
expect(randUtil.shuffle(range(10))).toEqual(
[0, 8, 1, 2, 3, 4, 5, 6, 7, 9]
);
expect(randUtil.shuffle(range(10))).toEqual([0, 8, 1, 2, 3, 4, 5, 6, 7, 9]);
});
const rng = new DummyBlockRNG([fillBlock('', '\x17\x16\x15\x14\x13\x12\x11\x10')]);
@@ -187,37 +176,37 @@ describe('RandUtilDummy', () => {
expect(randUtil.choice(new Set([5, 3, 1, 2, 8, 0]))).toBe(8);
//0x14(4), 정렬 순서상 숫자(소-대) > 문자열(삽입순) > 심볼 순서
expect(randUtil.choice({ c: 'c', a: 'a', b: 'b', 4: 'x', 2: 't', '3': 'q' }))
.toBe('c');
expect(randUtil.choice({ c: 'c', a: 'a', b: 'b', 4: 'x', 2: 't', '3': 'q' })).toBe('c');
});
it('choiceUsingWeight', () => {
//0.6275740099377194 * 38.1 = 23.91
expect(randUtil.choiceUsingWeight({
a: 0.1,
b: 10,
tt: 2,
x: -1,
c: 20,
d: 0,
e: 6
})).toBe('c');
expect(
randUtil.choiceUsingWeight({
a: 0.1,
b: 10,
tt: 2,
x: -1,
c: 20,
d: 0,
e: 6,
})
).toBe('c');
//0.658946544056166
expect(randUtil.choiceUsingWeightPair([
['xx', 10],
])).toBe('xx');
expect(randUtil.choiceUsingWeightPair([['xx', 10]])).toBe('xx');
//0.6903152783785083 * 27.3 = 18.84560709973328
expect(randUtil.choiceUsingWeightPair([
['e', 10],
['d', 4],
['c', 0.1],
['baba', 0.2],
['q', 9],
['xt', 4]
])).toBe('q');
expect(
randUtil.choiceUsingWeightPair([
['e', 10],
['d', 4],
['c', 0.1],
['baba', 0.2],
['q', 9],
['xt', 4],
])
).toBe('q');
});
});
@@ -263,12 +252,12 @@ describe('RNGAcceptable', () => {
randUtil.choice([0, 0, 0]);
randUtil.choiceUsingWeight({
0: 0,
1: -1
1: -1,
});
randUtil.choiceUsingWeightPair([
[0, 0],
[1, 0],
[2, -2]
[2, -2],
]);
randUtil.nextBool(1.1);
randUtil.nextBool(-0.1);
@@ -306,16 +295,18 @@ for idx in range(5):
print(hash(fixedKey, idx).hex())
*/
describe('RNG', () => {
//JS - PHP 일치 확인 정도로.
const testVector = Buffer.from([
'24d9ccd648556255fd0ee9f5b29918de90617341958b3b354d572167e4dee02b757816a2bbe0b502c52413ffd384381a9d7b4e193df6f4345d6a95e111d661c4',
'2e9264512f6f4b080cf1376b74fab6878ecf4a6e185942d2e5b22cf923885b9952d40601a414225d6901417fd4ce9368ac77e4a63d3fc9b58ab952bb8c33f165',
'8e2ebf5af6283a1b18f4c044c86c20d02be3890613c4cc8b7c6b7b35581263b972a82630df69a9289988422d7c3a9be5edf78d5de16fabd01e5dd4e458068d8a',
'398596047ba547bfe371ec863a3e019ab0dbc4bb3b27e9077685aae4283ff6bbccfd981d92f9358f7efffbb72a940414802d98466d132e2ad0a16a12946d5f47',
'b3606fe9b18c4aa7315e78bb9e47cb51cc4e203fcc2e631f0405c1b872c8e1cb5b6415ea74bbb77fffaaadb002b47cb4f4628dc0709634365b187667f5c708cb',
].join(''), 'hex');
const testVector = Buffer.from(
[
'24d9ccd648556255fd0ee9f5b29918de90617341958b3b354d572167e4dee02b757816a2bbe0b502c52413ffd384381a9d7b4e193df6f4345d6a95e111d661c4',
'2e9264512f6f4b080cf1376b74fab6878ecf4a6e185942d2e5b22cf923885b9952d40601a414225d6901417fd4ce9368ac77e4a63d3fc9b58ab952bb8c33f165',
'8e2ebf5af6283a1b18f4c044c86c20d02be3890613c4cc8b7c6b7b35581263b972a82630df69a9289988422d7c3a9be5edf78d5de16fabd01e5dd4e458068d8a',
'398596047ba547bfe371ec863a3e019ab0dbc4bb3b27e9077685aae4283ff6bbccfd981d92f9358f7efffbb72a940414802d98466d132e2ad0a16a12946d5f47',
'b3606fe9b18c4aa7315e78bb9e47cb51cc4e203fcc2e631f0405c1b872c8e1cb5b6415ea74bbb77fffaaadb002b47cb4f4628dc0709634365b187667f5c708cb',
].join(''),
'hex'
);
it('bytes', () => {
const rng = new LiteHashDRBG(fixedKey);
@@ -369,9 +360,8 @@ describe('RNG', () => {
new Uint8Array(testVector.slice(bufferByteSize * 4, bufferByteSize * 5)),
]);
for (const idx of range(18)) {
for (const _ of range(18)) {
expect(rng.nextFloat1()).toBe(rng2.nextFloat1());
}
});
});
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=test-rngs.test.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"test-rngs.test.d.ts","sourceRoot":"","sources":["test-rngs.test.ts"],"names":[],"mappings":""}
+65
View File
@@ -0,0 +1,65 @@
import { describe, expect, it } from 'vitest';
import { ConstantRNG, MidpointRNG, SequenceRNG, SineRNG } from '../src/index.js';
const toArray = (bytes) => Array.from(bytes);
describe('TestRNG:Constant', () => {
it('returns fixed 0', () => {
const rng = new ConstantRNG(0);
expect(rng.nextFloat1()).toBe(0);
expect(rng.nextInt(10)).toBe(0);
expect(toArray(rng.nextBytes(3))).toEqual([0, 0, 0]);
expect(toArray(rng.nextBits(3))).toEqual([0]);
});
it('returns fixed 1', () => {
const rng = new ConstantRNG(1);
expect(rng.nextFloat1()).toBe(1);
expect(rng.nextInt(10)).toBe(10);
expect(toArray(rng.nextBytes(2))).toEqual([255, 255]);
expect(toArray(rng.nextBits(3))).toEqual([7]);
});
});
describe('TestRNG:Midpoint', () => {
it('returns midpoint for int/float', () => {
const rng = new MidpointRNG();
expect(rng.nextFloat1()).toBe(0.5);
expect(rng.nextInt(9)).toBe(4);
expect(rng.nextInt(10)).toBe(5);
});
it('alternates bits', () => {
const rng = new MidpointRNG();
expect(toArray(rng.nextBits(4))).toEqual([10]);
expect(toArray(rng.nextBits(4))).toEqual([10]);
});
});
describe('TestRNG:Sine', () => {
it('follows sine wave with period/amplitude', () => {
const rng = new SineRNG(4, 0.5, 0);
expect(rng.nextFloat1()).toBeCloseTo(0.5, 8);
expect(rng.nextFloat1()).toBeCloseTo(1, 8);
expect(rng.nextFloat1()).toBeCloseTo(0.5, 8);
expect(rng.nextFloat1()).toBeCloseTo(0, 8);
});
it('maps float to int range', () => {
const rng = new SineRNG(4, 0.5, 0);
expect(rng.nextInt(9)).toBe(5);
expect(rng.nextInt(9)).toBe(9);
});
});
describe('TestRNG:Sequence', () => {
it('cycles fixed sequence', () => {
const rng = new SequenceRNG([0, 0.25, 0.5, 0.75, 1]);
expect(rng.nextFloat1()).toBe(0);
expect(rng.nextFloat1()).toBe(0.25);
expect(rng.nextFloat1()).toBe(0.5);
expect(rng.nextFloat1()).toBe(0.75);
expect(rng.nextFloat1()).toBe(1);
expect(rng.nextFloat1()).toBe(0);
});
it('converts sequence to bytes and ints', () => {
const rng = new SequenceRNG([0, 0.5, 1]);
expect(toArray(rng.nextBytes(3))).toEqual([0, 128, 255]);
expect(rng.nextInt(8)).toBe(0);
expect(rng.nextInt(8)).toBe(4);
expect(rng.nextInt(8)).toBe(8);
});
});
//# sourceMappingURL=test-rngs.test.js.map
File diff suppressed because one or more lines are too long
+1 -2
View File
@@ -2,8 +2,7 @@
"extends": "../../tsconfig.paths.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"composite": true
},
"include": ["src"]
"include": ["src", "test", "*.ts", "**/*.d.ts"]
}
+3
View File
@@ -0,0 +1,3 @@
declare const _default: import('vite').UserConfig;
export default _default;
//# sourceMappingURL=vitest.config.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"vitest.config.d.ts","sourceRoot":"","sources":["vitest.config.ts"],"names":[],"mappings":";AAEA,wBAMG"}
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
globals: true,
include: ['test/**/*.test.ts'],
},
});
//# sourceMappingURL=vitest.config.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"vitest.config.js","sourceRoot":"","sources":["vitest.config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,eAAe,YAAY,CAAC;IACxB,IAAI,EAAE;QACF,WAAW,EAAE,MAAM;QACnB,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,CAAC,mBAAmB,CAAC;KACjC;CACJ,CAAC,CAAC"}
+2 -1
View File
@@ -8,7 +8,8 @@
"scripts": {
"build": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/infra",
"dev": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/infra --watch",
"lint": "node -e \"console.log('lint not configured')\"",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "node -e \"console.log('test not configured')\"",
"prisma:generate": "pnpm prisma:generate:game && pnpm prisma:generate:gateway",
"typecheck": "tsc -b",
+2 -5
View File
@@ -16,14 +16,11 @@ const buildDatabaseUrlFromEnv = (): string => {
const user = process.env.POSTGRES_USER ?? 'sammo';
const password = process.env.POSTGRES_PASSWORD ?? '';
const dbName = process.env.POSTGRES_DB ?? 'sammo';
const schema = resolveSchemaName(
process.env.POSTGRES_SCHEMA ?? process.env.DATABASE_SCHEMA
);
const schema = resolveSchemaName(process.env.POSTGRES_SCHEMA ?? process.env.DATABASE_SCHEMA);
return `postgresql://${user}:${password}@${host}:${port}/${dbName}?schema=${schema}`;
};
const databaseUrl =
process.env.DATABASE_URL ?? buildDatabaseUrlFromEnv();
const databaseUrl = process.env.DATABASE_URL ?? buildDatabaseUrlFromEnv();
const schemaPath = process.env.PRISMA_SCHEMA ?? 'prisma/game.prisma';
+3 -12
View File
@@ -1,18 +1,9 @@
import { PrismaClient as GamePrismaClient } from '../prisma/generated/game/index.js';
export {
LogCategory,
LogScope,
Prisma as GamePrisma,
} from '../prisma/generated/game/index.js';
export { LogCategory, LogScope, Prisma as GamePrisma } from '../prisma/generated/game/index.js';
export type { PrismaClient as GamePrismaClient } from '../prisma/generated/game/index.js';
import type { PostgresConfig, PostgresConnector } from './postgres.js';
import { createPostgresConnector } from './postgres.js';
export const createGamePostgresConnector = (
config: PostgresConfig
): PostgresConnector<GamePrismaClient> =>
createPostgresConnector(
config,
(options) => new GamePrismaClient(options)
);
export const createGamePostgresConnector = (config: PostgresConfig): PostgresConnector<GamePrismaClient> =>
createPostgresConnector(config, (options) => new GamePrismaClient(options));
+2 -7
View File
@@ -10,10 +10,5 @@ export type { PrismaClient as GatewayPrismaClient } from '../prisma/generated/ga
import type { PostgresConfig, PostgresConnector } from './postgres.js';
import { createPostgresConnector } from './postgres.js';
export const createGatewayPostgresConnector = (
config: PostgresConfig
): PostgresConnector<GatewayPrismaClient> =>
createPostgresConnector(
config,
(options) => new GatewayPrismaClient(options)
);
export const createGatewayPostgresConnector = (config: PostgresConfig): PostgresConnector<GatewayPrismaClient> =>
createPostgresConnector(config, (options) => new GatewayPrismaClient(options));
+1 -6
View File
@@ -1,10 +1,5 @@
export * from './postgres.js';
export {
createGamePostgresConnector,
GamePrisma,
LogCategory,
LogScope,
} from './gamePrisma.js';
export { createGamePostgresConnector, GamePrisma, LogCategory, LogScope } from './gamePrisma.js';
export type { GamePrismaClient } from './gamePrisma.js';
export {
createGatewayPostgresConnector,
+2 -8
View File
@@ -45,10 +45,7 @@ export class LogRepository {
constructor(private readonly prisma: GamePrismaClient) {}
// 전역(시스템) 로그 조회
async listSystemLogs(
category: LogCategory,
options: LogQueryOptions = {}
): Promise<LogEntryView[]> {
async listSystemLogs(category: LogCategory, options: LogQueryOptions = {}): Promise<LogEntryView[]> {
return this.prisma.logEntry.findMany(
buildFindArgs(
{
@@ -97,10 +94,7 @@ export class LogRepository {
}
// 유저 로그 조회
async listUserLogs(
userId: number,
options: LogQueryOptions & { subType?: string } = {}
): Promise<LogEntryView[]> {
async listUserLogs(userId: number, options: LogQueryOptions & { subType?: string } = {}): Promise<LogEntryView[]> {
return this.prisma.logEntry.findMany(
buildFindArgs(
{
+8 -27
View File
@@ -26,13 +26,9 @@ export interface PrismaClientFactoryOptions {
log?: PostgresLogOption[];
}
export type PrismaClientFactory<TClient> = (
options: PrismaClientFactoryOptions
) => TClient;
export type PrismaClientFactory<TClient> = (options: PrismaClientFactoryOptions) => TClient;
const resolveSchemaName = (
value: string | undefined
): string => {
const resolveSchemaName = (value: string | undefined): string => {
if (!value) {
return 'public';
}
@@ -40,10 +36,7 @@ const resolveSchemaName = (
return trimmed ? trimmed : 'public';
};
const applySchemaToDatabaseUrl = (
url: string,
schema: string | undefined
): string => {
const applySchemaToDatabaseUrl = (url: string, schema: string | undefined): string => {
if (!schema) {
return url;
}
@@ -56,9 +49,7 @@ const applySchemaToDatabaseUrl = (
}
};
const extractSchemaFromDatabaseUrl = (
url: string
): string | undefined => {
const extractSchemaFromDatabaseUrl = (url: string): string | undefined => {
try {
const parsed = new URL(url);
const schema = parsed.searchParams.get('schema');
@@ -68,18 +59,13 @@ const extractSchemaFromDatabaseUrl = (
}
};
const buildDatabaseUrlFromEnv = (
env: NodeJS.ProcessEnv,
schemaOverride?: string
): string => {
const buildDatabaseUrlFromEnv = (env: NodeJS.ProcessEnv, schemaOverride?: string): string => {
const host = env.POSTGRES_HOST ?? '127.0.0.1';
const port = env.POSTGRES_PORT ?? '15432';
const user = env.POSTGRES_USER ?? 'sammo';
const password = env.POSTGRES_PASSWORD ?? '';
const dbName = env.POSTGRES_DB ?? 'sammo';
const schema = resolveSchemaName(
schemaOverride ?? env.POSTGRES_SCHEMA ?? env.DATABASE_SCHEMA
);
const schema = resolveSchemaName(schemaOverride ?? env.POSTGRES_SCHEMA ?? env.DATABASE_SCHEMA);
return `postgresql://${user}:${password}@${host}:${port}/${dbName}?schema=${schema}`;
};
@@ -102,17 +88,12 @@ export const createPostgresConnector = <TClient>(
createClient: PrismaClientFactory<TClient>
): PostgresConnector<TClient> => {
const schema =
extractSchemaFromDatabaseUrl(config.url) ??
process.env.POSTGRES_SCHEMA ??
process.env.DATABASE_SCHEMA;
extractSchemaFromDatabaseUrl(config.url) ?? process.env.POSTGRES_SCHEMA ?? process.env.DATABASE_SCHEMA;
const pool = new Pool({
connectionString: config.url,
...(schema ? { options: `-c search_path=${schema}` } : {}),
});
const adapter = new PrismaPg(
pool,
schema ? { schema } : undefined
);
const adapter = new PrismaPg(pool, schema ? { schema } : undefined);
const prisma = createClient({
adapter,
log: config.log,
+1 -3
View File
@@ -12,9 +12,7 @@ export interface RedisConnector {
disconnect(): Promise<void>;
}
export const resolveRedisConfigFromEnv = (
env: NodeJS.ProcessEnv = process.env
): RedisConfig => {
export const resolveRedisConfigFromEnv = (env: NodeJS.ProcessEnv = process.env): RedisConfig => {
const url = env.REDIS_URL ?? '';
if (!url) {
throw new Error('REDIS_URL is required to create a Redis client.');
+13 -44
View File
@@ -334,53 +334,31 @@ export interface TurnEngineLogEntryCreateManyInput {
export interface TurnEngineDatabaseClient {
worldState: {
findFirst(args?: unknown): Promise<TurnEngineWorldStateRow | null>;
update(args: {
where: { id: number };
data: TurnEngineWorldStateUpdateInput;
}): Promise<unknown>;
create(args: {
data: TurnEngineWorldStateCreateInput;
}): Promise<unknown>;
update(args: { where: { id: number }; data: TurnEngineWorldStateUpdateInput }): Promise<unknown>;
create(args: { data: TurnEngineWorldStateCreateInput }): Promise<unknown>;
deleteMany(args?: unknown): Promise<unknown>;
};
general: {
findMany(args?: unknown): Promise<TurnEngineGeneralRow[]>;
createMany(args: {
data: TurnEngineGeneralCreateManyInput[];
}): Promise<unknown>;
update(args: {
where: { id: number };
data: TurnEngineGeneralUpdateInput;
}): Promise<unknown>;
createMany(args: { data: TurnEngineGeneralCreateManyInput[] }): Promise<unknown>;
update(args: { where: { id: number }; data: TurnEngineGeneralUpdateInput }): Promise<unknown>;
deleteMany(args?: unknown): Promise<unknown>;
};
city: {
findMany(args?: unknown): Promise<TurnEngineCityRow[]>;
createMany(args: {
data: TurnEngineCityCreateManyInput[];
}): Promise<unknown>;
update(args: {
where: { id: number };
data: TurnEngineCityUpdateInput;
}): Promise<unknown>;
createMany(args: { data: TurnEngineCityCreateManyInput[] }): Promise<unknown>;
update(args: { where: { id: number }; data: TurnEngineCityUpdateInput }): Promise<unknown>;
deleteMany(args?: unknown): Promise<unknown>;
};
nation: {
findMany(args?: unknown): Promise<TurnEngineNationRow[]>;
createMany(args: {
data: TurnEngineNationCreateManyInput[];
}): Promise<unknown>;
update(args: {
where: { id: number };
data: TurnEngineNationUpdateInput;
}): Promise<unknown>;
createMany(args: { data: TurnEngineNationCreateManyInput[] }): Promise<unknown>;
update(args: { where: { id: number }; data: TurnEngineNationUpdateInput }): Promise<unknown>;
deleteMany(args?: unknown): Promise<unknown>;
};
diplomacy: {
findMany(args?: unknown): Promise<TurnEngineDiplomacyRow[]>;
createMany(args: {
data: TurnEngineDiplomacyCreateManyInput[];
}): Promise<unknown>;
createMany(args: { data: TurnEngineDiplomacyCreateManyInput[] }): Promise<unknown>;
update(args: {
where: {
srcNationId_destNationId: {
@@ -394,26 +372,17 @@ export interface TurnEngineDatabaseClient {
};
troop: {
findMany(args?: unknown): Promise<TurnEngineTroopRow[]>;
createMany(args: {
data: TurnEngineTroopCreateManyInput[];
}): Promise<unknown>;
update(args: {
where: { troopLeaderId: number };
data: TurnEngineTroopUpdateInput;
}): Promise<unknown>;
createMany(args: { data: TurnEngineTroopCreateManyInput[] }): Promise<unknown>;
update(args: { where: { troopLeaderId: number }; data: TurnEngineTroopUpdateInput }): Promise<unknown>;
deleteMany(args?: unknown): Promise<unknown>;
};
event: {
findMany(args?: unknown): Promise<TurnEngineEventRow[]>;
createMany(args: {
data: TurnEngineEventCreateManyInput[];
}): Promise<unknown>;
createMany(args: { data: TurnEngineEventCreateManyInput[] }): Promise<unknown>;
deleteMany(args?: unknown): Promise<unknown>;
};
logEntry: {
createMany(args: {
data: TurnEngineLogEntryCreateManyInput[];
}): Promise<unknown>;
createMany(args: { data: TurnEngineLogEntryCreateManyInput[] }): Promise<unknown>;
};
generalTurn: {
findMany(args?: unknown): Promise<TurnEngineGeneralTurnRow[]>;
+1 -2
View File
@@ -2,8 +2,7 @@
"extends": "../../tsconfig.paths.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"composite": true
},
"include": ["src"]
"include": ["src", "test", "*.ts"]
}
+2 -1
View File
@@ -14,7 +14,8 @@
"scripts": {
"build": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/logic",
"dev": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/logic --watch",
"lint": "node -e \"console.log('lint not configured')\"",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "vitest run --config vitest.config.ts",
"typecheck": "tsc -b"
},
+2 -5
View File
@@ -1,14 +1,11 @@
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from './engine.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from './engine.js';
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
export interface GeneralActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState,
Args = unknown,
Context extends GeneralActionResolveContext<TriggerState> = GeneralActionResolveContext<TriggerState>
Context extends GeneralActionResolveContext<TriggerState> = GeneralActionResolveContext<TriggerState>,
> {
key: string;
name: string;
+26 -80
View File
@@ -11,55 +11,42 @@ import type {
} from '@sammo-ts/logic/domain/entities.js';
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import { getNextTurnAt, type TurnSchedule } from '@sammo-ts/logic/turn/calendar.js';
import {
LogCategory,
type LogEntryDraft,
LogFormat,
LogScope,
} from '@sammo-ts/logic/logging/types.js';
import { LogCategory, type LogEntryDraft, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
enablePatches();
export interface WorldState<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
export interface WorldState<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
general: General<TriggerState>;
city?: City;
nation?: Nation | null;
}
export interface GeneralActionResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionContext<TriggerState> {
rng: RandomGenerator;
city?: City;
nation?: Nation | null;
addLog(
message: string,
options?: Partial<Omit<LogEntryDraft, 'text'>>
): void;
addLog(message: string, options?: Partial<Omit<LogEntryDraft, 'text'>>): void;
}
export type GeneralActionResolveInputContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> = Omit<GeneralActionResolveContext<TriggerState>, 'addLog'>;
export type GeneralActionResolveInputContext<TriggerState extends GeneralTriggerState = GeneralTriggerState> = Omit<
GeneralActionResolveContext<TriggerState>,
'addLog'
>;
export interface TurnScheduleContext {
now: Date;
schedule: TurnSchedule;
}
export interface GeneralPatchEffect<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
export interface GeneralPatchEffect<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
type: 'general:patch';
patch: Partial<General<TriggerState>>;
targetId?: GeneralId;
}
export interface GeneralAddEffect<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
export interface GeneralAddEffect<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
type: 'general:add';
general: General<TriggerState>;
}
@@ -99,9 +86,7 @@ export interface NextTurnOverrideEffect {
nextTurnAt: Date;
}
export type GeneralActionEffect<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> =
export type GeneralActionEffect<TriggerState extends GeneralTriggerState = GeneralTriggerState> =
| GeneralPatchEffect<TriggerState>
| GeneralAddEffect<TriggerState>
| CityPatchEffect
@@ -110,21 +95,13 @@ export type GeneralActionEffect<
| LogEffect
| NextTurnOverrideEffect;
export interface GeneralActionOutcome<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
export interface GeneralActionOutcome<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
effects: GeneralActionEffect<TriggerState>[];
}
export interface GeneralActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState,
Args = unknown
> {
export interface GeneralActionResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState, Args = unknown> {
key: string;
resolve(
context: GeneralActionResolveContext<TriggerState>,
args: Args
): GeneralActionOutcome<TriggerState>;
resolve(context: GeneralActionResolveContext<TriggerState>, args: Args): GeneralActionOutcome<TriggerState>;
}
export interface GeneralActionResolution {
@@ -152,9 +129,7 @@ export interface GeneralActionResolution {
};
}
export const createGeneralPatchEffect = <
TriggerState extends GeneralTriggerState = GeneralTriggerState
>(
export const createGeneralPatchEffect = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
patch: Partial<General<TriggerState>>,
targetId?: GeneralId
): GeneralPatchEffect<TriggerState> => ({
@@ -163,28 +138,20 @@ export const createGeneralPatchEffect = <
...(targetId !== undefined ? { targetId } : {}),
});
export const createGeneralAddEffect = <
TriggerState extends GeneralTriggerState = GeneralTriggerState
>(
export const createGeneralAddEffect = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
general: General<TriggerState>
): GeneralAddEffect<TriggerState> => ({
type: 'general:add',
general,
});
export const createCityPatchEffect = (
patch: Partial<City>,
targetId?: CityId
): CityPatchEffect => ({
export const createCityPatchEffect = (patch: Partial<City>, targetId?: CityId): CityPatchEffect => ({
type: 'city:patch',
patch,
...(targetId !== undefined ? { targetId } : {}),
});
export const createNationPatchEffect = (
patch: Partial<Nation>,
targetId?: NationId
): NationPatchEffect => ({
export const createNationPatchEffect = (patch: Partial<Nation>, targetId?: NationId): NationPatchEffect => ({
type: 'nation:patch',
patch,
...(targetId !== undefined ? { targetId } : {}),
@@ -201,21 +168,14 @@ export const createDiplomacyPatchEffect = (
patch,
});
export const createLogEffect = (
message: string,
options: Partial<Omit<LogEntryDraft, 'text'>> = {}
): LogEffect => ({
export const createLogEffect = (message: string, options: Partial<Omit<LogEntryDraft, 'text'>> = {}): LogEffect => ({
type: 'log',
entry: {
scope: options.scope ?? LogScope.GENERAL,
category: options.category ?? LogCategory.ACTION,
text: message,
...(options.generalId !== undefined
? { generalId: options.generalId }
: {}),
...(options.nationId !== undefined
? { nationId: options.nationId }
: {}),
...(options.generalId !== undefined ? { generalId: options.generalId } : {}),
...(options.nationId !== undefined ? { nationId: options.nationId } : {}),
...(options.userId !== undefined ? { userId: options.userId } : {}),
...(options.subType !== undefined ? { subType: options.subType } : {}),
...(options.meta !== undefined ? { meta: options.meta } : {}),
@@ -223,18 +183,13 @@ export const createLogEffect = (
},
});
export const createNextTurnOverrideEffect = (
nextTurnAt: Date
): NextTurnOverrideEffect => ({
export const createNextTurnOverrideEffect = (nextTurnAt: Date): NextTurnOverrideEffect => ({
type: 'schedule:override',
nextTurnAt,
});
// 행동 결과를 Effect로 모아 상태/턴 계산을 수행한다.
export const resolveGeneralAction = <
TriggerState extends GeneralTriggerState = GeneralTriggerState,
Args = unknown
>(
export const resolveGeneralAction = <TriggerState extends GeneralTriggerState = GeneralTriggerState, Args = unknown>(
resolver: GeneralActionResolver<TriggerState, Args>,
context: GeneralActionResolveInputContext<TriggerState>,
scheduleContext: TurnScheduleContext,
@@ -257,10 +212,7 @@ export const resolveGeneralAction = <
nation: context.nation,
} as WorldState<TriggerState>,
(draft) => {
const addLog = (
message: string,
options: Partial<Omit<LogEntryDraft, 'text'>> = {}
) => {
const addLog = (message: string, options: Partial<Omit<LogEntryDraft, 'text'>> = {}) => {
const entry: LogEntryDraft = {
scope: options.scope ?? LogScope.GENERAL,
category: options.category ?? LogCategory.ACTION,
@@ -363,9 +315,7 @@ export const resolveGeneralAction = <
}
);
const nextTurnAt =
nextTurnAtOverride ??
getNextTurnAt(scheduleContext.now, scheduleContext.schedule);
const nextTurnAt = nextTurnAtOverride ?? getNextTurnAt(scheduleContext.now, scheduleContext.schedule);
const dirty: NonNullable<GeneralActionResolution['dirty']> = {
general: false,
@@ -396,11 +346,7 @@ export const resolveGeneralAction = <
if (dirty.general || dirty.city || dirty.nation) {
resolution.dirty = dirty;
}
if (
patches.generals.length > 0 ||
patches.cities.length > 0 ||
patches.nations.length > 0
) {
if (patches.generals.length > 0 || patches.cities.length > 0 || patches.nations.length > 0) {
resolution.patches = patches;
}
if (createdGenerals.length > 0) {
@@ -12,10 +12,7 @@ import {
} from '@sammo-ts/logic/constraints/presets.js';
import { allow, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import { createDiplomacyPatchEffect, createLogEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
@@ -27,7 +24,7 @@ export interface NonAggressionAcceptArgs {
}
export interface NonAggressionAcceptContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
currentYear: number;
currentMonth: number;
@@ -65,8 +62,7 @@ const parseMonth = (raw: unknown): number | null => {
return month >= 1 && month <= 12 ? month : null;
};
const resolveMonthIndex = (year: number, month: number): number =>
year * 12 + month - 1;
const resolveMonthIndex = (year: number, month: number): number => year * 12 + month - 1;
const requireFutureTerm = (): Constraint => ({
name: 'RequireNonAggressionFutureTerm',
@@ -78,11 +74,9 @@ const requireFutureTerm = (): Constraint => ({
],
test: (ctx) => {
const yearValue = typeof ctx.args.year === 'number' ? ctx.args.year : null;
const monthValue =
typeof ctx.args.month === 'number' ? ctx.args.month : null;
const monthValue = typeof ctx.args.month === 'number' ? ctx.args.month : null;
const envYearValue = typeof ctx.env.year === 'number' ? ctx.env.year : null;
const envMonthValue =
typeof ctx.env.month === 'number' ? ctx.env.month : null;
const envMonthValue = typeof ctx.env.month === 'number' ? ctx.env.month : null;
const missing = [];
if (yearValue === null) {
@@ -126,11 +120,7 @@ const notSameDestGeneral = (): Constraint => ({
test: (ctx) => {
const destGeneralId = ctx.args.destGeneralId;
if (typeof destGeneralId !== 'number') {
return unknownOrDeny(
ctx,
[{ kind: 'arg', key: 'destGeneralId' }],
'장수 정보가 없습니다.'
);
return unknownOrDeny(ctx, [{ kind: 'arg', key: 'destGeneralId' }], '장수 정보가 없습니다.');
}
if (destGeneralId === ctx.actorId) {
return { kind: 'deny', reason: '대상이 올바르지 않습니다.' };
@@ -141,12 +131,8 @@ const notSameDestGeneral = (): Constraint => ({
// 불가침 수락은 메시지와 연결되는 즉시 국가 커맨드로 사용한다.
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<
TriggerState,
NonAggressionAcceptArgs,
NonAggressionAcceptContext<TriggerState>
> {
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, NonAggressionAcceptArgs, NonAggressionAcceptContext<TriggerState>> {
public readonly key = 'che_불가침수락';
public readonly name = ACTION_NAME;
@@ -161,21 +147,13 @@ export class ActionDefinition<
const destGeneralId = parseGeneralId(data?.destGeneralId);
const year = parseYear(data?.year);
const month = parseMonth(data?.month);
if (
destNationId === null ||
destGeneralId === null ||
year === null ||
month === null
) {
if (destNationId === null || destGeneralId === null || year === null || month === null) {
return null;
}
return { destNationId, destGeneralId, year, month };
}
buildConstraints(
_ctx: ConstraintContext,
_args: NonAggressionAcceptArgs
): Constraint[] {
buildConstraints(_ctx: ConstraintContext, _args: NonAggressionAcceptArgs): Constraint[] {
return [
beChief(),
notBeNeutral(),
@@ -201,22 +179,16 @@ export class ActionDefinition<
if (nationId === undefined || nationId <= 0) {
return {
effects: [
createLogEffect(
`${ACTION_NAME}을 준비했지만 국가 정보가 없습니다.`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}
),
createLogEffect(`${ACTION_NAME}을 준비했지만 국가 정보가 없습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
}
const currentMonth = resolveMonthIndex(
context.currentYear,
context.currentMonth
);
const currentMonth = resolveMonthIndex(context.currentYear, context.currentMonth);
const targetMonth = args.year * 12 + args.month;
const term = Math.max(0, targetMonth - currentMonth);
@@ -230,14 +202,11 @@ export class ActionDefinition<
state: DIPLOMACY_NON_AGGRESSION,
term,
}),
createLogEffect(
`${ACTION_NAME}을 실행했습니다. (국가 ${args.destNationId})`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}
),
createLogEffect(`${ACTION_NAME}을 실행했습니다. (국가 ${args.destNationId})`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
}
@@ -10,10 +10,7 @@ import {
} from '@sammo-ts/logic/constraints/presets.js';
import { allow, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import { createDiplomacyPatchEffect, createLogEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
@@ -46,11 +43,7 @@ const notSameDestGeneral = (): Constraint => ({
test: (ctx) => {
const destGeneralId = ctx.args.destGeneralId;
if (typeof destGeneralId !== 'number') {
return unknownOrDeny(
ctx,
[{ kind: 'arg', key: 'destGeneralId' }],
'장수 정보가 없습니다.'
);
return unknownOrDeny(ctx, [{ kind: 'arg', key: 'destGeneralId' }], '장수 정보가 없습니다.');
}
if (destGeneralId === ctx.actorId) {
return { kind: 'deny', reason: '대상이 올바르지 않습니다.' };
@@ -61,10 +54,8 @@ const notSameDestGeneral = (): Constraint => ({
// 불가침 파기 수락은 메시지와 연결되는 즉시 국가 커맨드로 사용한다.
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements
GeneralActionDefinition<TriggerState, NonAggressionCancelAcceptArgs>
{
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, NonAggressionCancelAcceptArgs> {
public readonly key = 'che_불가침파기수락';
public readonly name = ACTION_NAME;
@@ -78,10 +69,7 @@ export class ActionDefinition<
return { destNationId, destGeneralId };
}
buildConstraints(
_ctx: ConstraintContext,
_args: NonAggressionCancelAcceptArgs
): Constraint[] {
buildConstraints(_ctx: ConstraintContext, _args: NonAggressionCancelAcceptArgs): Constraint[] {
return [
beChief(),
notBeNeutral(),
@@ -89,10 +77,7 @@ export class ActionDefinition<
existsDestGeneral(),
destGeneralInDestNation(),
notSameDestGeneral(),
allowDiplomacyBetweenStatus(
[DIPLOMACY_NON_AGGRESSION],
'불가침 중인 상대국에게만 가능합니다.'
),
allowDiplomacyBetweenStatus([DIPLOMACY_NON_AGGRESSION], '불가침 중인 상대국에게만 가능합니다.'),
];
}
@@ -104,14 +89,11 @@ export class ActionDefinition<
if (nationId === undefined || nationId <= 0) {
return {
effects: [
createLogEffect(
`${ACTION_NAME}을 준비했지만 국가 정보가 없습니다.`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}
),
createLogEffect(`${ACTION_NAME}을 준비했지만 국가 정보가 없습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
}
@@ -126,14 +108,11 @@ export class ActionDefinition<
state: DIPLOMACY_NEUTRAL,
term: 0,
}),
createLogEffect(
`${ACTION_NAME}을 실행했습니다. (국가 ${args.destNationId})`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}
),
createLogEffect(`${ACTION_NAME}을 실행했습니다. (국가 ${args.destNationId})`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
}
@@ -1,9 +1,4 @@
import type {
City,
General,
Nation,
Troop,
} from '@sammo-ts/logic/domain/entities.js';
import type { City, General, Nation, Troop } from '@sammo-ts/logic/domain/entities.js';
import type { ScenarioConfig } from '@sammo-ts/logic/scenario/types.js';
import type { ScenarioMeta } from '@sammo-ts/logic/world/types.js';
import type { MapDefinition, UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
@@ -43,7 +38,10 @@ export interface ActionContextWorldRef {
toNationId: number;
state: number;
}>;
getDiplomacyEntry(fromNationId: number, toNationId: number): {
getDiplomacyEntry(
fromNationId: number,
toNationId: number
): {
fromNationId: number;
toNationId: number;
state: number;
@@ -3,10 +3,7 @@ import type { ScenarioConfig } from '@sammo-ts/logic/scenario/types.js';
import type { ScenarioMeta } from '@sammo-ts/logic/world/types.js';
import type { WarAftermathConfig, WarEngineConfig, WarTimeContext } from '@sammo-ts/logic/war/types.js';
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
import type {
ActionContextWorldRef,
ActionContextWorldState,
} from './actionContext.js';
import type { ActionContextWorldRef, ActionContextWorldState } from './actionContext.js';
export interface WorldSummary {
totalGeneralCount: number;
@@ -20,9 +17,7 @@ export interface NationSummary {
averageDedication?: number;
}
export const buildWorldSummary = (
world: ActionContextWorldRef | null
): WorldSummary => {
export const buildWorldSummary = (world: ActionContextWorldRef | null): WorldSummary => {
if (!world) {
return { totalGeneralCount: 0, totalNpcCount: 0 };
}
@@ -51,16 +46,11 @@ export const buildWorldSummary = (
};
};
export const buildNationSummary = (
world: ActionContextWorldRef | null,
nationId: number
): NationSummary => {
export const buildNationSummary = (world: ActionContextWorldRef | null, nationId: number): NationSummary => {
if (!world || nationId <= 0) {
return {};
}
const generals = world.listGenerals().filter(
(general) => general.nationId === nationId
);
const generals = world.listGenerals().filter((general) => general.nationId === nationId);
if (generals.length === 0) {
return {};
}
@@ -86,9 +76,7 @@ export const buildNationSummary = (
};
};
export const buildAverageNationGeneralCount = (
world: ActionContextWorldRef | null
): number => {
export const buildAverageNationGeneralCount = (world: ActionContextWorldRef | null): number => {
if (!world) {
return 0;
}
@@ -100,10 +88,7 @@ export const buildAverageNationGeneralCount = (
return generals.length / nations.length;
};
export const resolveStartYear = (
world: ActionContextWorldState,
scenarioMeta?: ScenarioMeta
): number => {
export const resolveStartYear = (world: ActionContextWorldState, scenarioMeta?: ScenarioMeta): number => {
if (typeof scenarioMeta?.startYear === 'number') {
return scenarioMeta.startYear;
}
@@ -128,15 +113,9 @@ const DEFAULT_AFTER_CONFIG = {
};
const asRecord = (value: unknown): Record<string, unknown> =>
value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
const resolveNumber = (
record: Record<string, unknown>,
keys: string[],
fallback: number
): number => {
const resolveNumber = (record: Record<string, unknown>, keys: string[], fallback: number): number => {
for (const key of keys) {
const value = record[key];
if (typeof value === 'number' && Number.isFinite(value)) {
@@ -147,19 +126,14 @@ const resolveNumber = (
};
// 성벽 병종은 이름/요구조건을 우선해 찾고, 없으면 기본값을 사용한다.
const resolveCastleCrewTypeId = (
unitSet: UnitSetDefinition,
fallback: number
): number => {
const resolveCastleCrewTypeId = (unitSet: UnitSetDefinition, fallback: number): number => {
const crewTypes = unitSet.crewTypes ?? [];
const byName = crewTypes.find((crewType) => crewType.name.includes('성벽'));
if (byName) {
return byName.id;
}
const byRequirement = crewTypes.find((crewType) =>
crewType.requirements.some(
(requirement) => requirement.type === 'Impossible'
)
crewType.requirements.some((requirement) => requirement.type === 'Impossible')
);
if (byRequirement) {
return byRequirement.id;
@@ -170,55 +144,22 @@ const resolveCastleCrewTypeId = (
return crewTypes[0]?.id ?? fallback;
};
const resolveCastleArmType = (
unitSet: UnitSetDefinition,
castleCrewTypeId: number
): number => {
const resolveCastleArmType = (unitSet: UnitSetDefinition, castleCrewTypeId: number): number => {
const crewTypes = unitSet.crewTypes ?? [];
return (
crewTypes.find((crewType) => crewType.id === castleCrewTypeId)?.armType ??
0
);
return crewTypes.find((crewType) => crewType.id === castleCrewTypeId)?.armType ?? 0;
};
export const buildWarConfig = (
scenarioConfig: ScenarioConfig,
unitSet: UnitSetDefinition
): WarEngineConfig => {
export const buildWarConfig = (scenarioConfig: ScenarioConfig, unitSet: UnitSetDefinition): WarEngineConfig => {
const constValues = asRecord(scenarioConfig.const);
const castleCrewTypeId = resolveNumber(
constValues,
['castleCrewTypeId'],
resolveCastleCrewTypeId(unitSet, 0)
);
const castleCrewTypeId = resolveNumber(constValues, ['castleCrewTypeId'], resolveCastleCrewTypeId(unitSet, 0));
const castleArmType = resolveCastleArmType(unitSet, castleCrewTypeId);
return {
armPerPhase: resolveNumber(
constValues,
['armPerPhase', 'armperphase'],
DEFAULT_WAR_CONFIG.armPerPhase
),
maxTrainByCommand: resolveNumber(
constValues,
['maxTrainByCommand'],
DEFAULT_WAR_CONFIG.maxTrainByCommand
),
maxAtmosByCommand: resolveNumber(
constValues,
['maxAtmosByCommand'],
DEFAULT_WAR_CONFIG.maxAtmosByCommand
),
maxTrainByWar: resolveNumber(
constValues,
['maxTrainByWar'],
DEFAULT_WAR_CONFIG.maxTrainByWar
),
maxAtmosByWar: resolveNumber(
constValues,
['maxAtmosByWar'],
DEFAULT_WAR_CONFIG.maxAtmosByWar
),
armPerPhase: resolveNumber(constValues, ['armPerPhase', 'armperphase'], DEFAULT_WAR_CONFIG.armPerPhase),
maxTrainByCommand: resolveNumber(constValues, ['maxTrainByCommand'], DEFAULT_WAR_CONFIG.maxTrainByCommand),
maxAtmosByCommand: resolveNumber(constValues, ['maxAtmosByCommand'], DEFAULT_WAR_CONFIG.maxAtmosByCommand),
maxTrainByWar: resolveNumber(constValues, ['maxTrainByWar'], DEFAULT_WAR_CONFIG.maxTrainByWar),
maxAtmosByWar: resolveNumber(constValues, ['maxAtmosByWar'], DEFAULT_WAR_CONFIG.maxAtmosByWar),
castleCrewTypeId,
armTypes: {
footman: 1,
@@ -238,37 +179,22 @@ export const buildWarAftermathConfig = (
): WarAftermathConfig => {
const constValues = asRecord(scenarioConfig.const);
return {
initialNationGenLimit: resolveNumber(
constValues,
['initialNationGenLimit'],
0
),
techLevelIncYear: resolveNumber(
constValues,
['techLevelIncYear'],
DEFAULT_AFTER_CONFIG.techLevelIncYear
),
initialNationGenLimit: resolveNumber(constValues, ['initialNationGenLimit'], 0),
techLevelIncYear: resolveNumber(constValues, ['techLevelIncYear'], DEFAULT_AFTER_CONFIG.techLevelIncYear),
initialAllowedTechLevel: resolveNumber(
constValues,
['initialAllowedTechLevel'],
DEFAULT_AFTER_CONFIG.initialAllowedTechLevel
),
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], 0),
defaultCityWall: resolveNumber(
constValues,
['defaultCityWall'],
DEFAULT_AFTER_CONFIG.defaultCityWall
),
defaultCityWall: resolveNumber(constValues, ['defaultCityWall'], DEFAULT_AFTER_CONFIG.defaultCityWall),
baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], 0),
baseRice: resolveNumber(constValues, ['baseRice', 'baserice'], 0),
castleCrewTypeId,
};
};
export const buildWarTime = (
world: ActionContextWorldState,
scenarioMeta?: ScenarioMeta
): WarTimeContext => ({
export const buildWarTime = (world: ActionContextWorldState, scenarioMeta?: ScenarioMeta): WarTimeContext => ({
year: world.currentYear,
month: world.currentMonth,
startYear: resolveStartYear(world, scenarioMeta),
@@ -1,13 +1,5 @@
import {
GENERAL_TURN_COMMAND_KEYS,
isGeneralTurnCommandKey,
type GeneralTurnCommandKey,
} from './general/index.js';
import {
NATION_TURN_COMMAND_KEYS,
isNationTurnCommandKey,
type NationTurnCommandKey,
} from './nation/index.js';
import { GENERAL_TURN_COMMAND_KEYS, isGeneralTurnCommandKey, type GeneralTurnCommandKey } from './general/index.js';
import { NATION_TURN_COMMAND_KEYS, isNationTurnCommandKey, type NationTurnCommandKey } from './nation/index.js';
export interface TurnCommandProfile {
general: GeneralTurnCommandKey[];
@@ -25,9 +17,7 @@ const asStringArray = (value: unknown): string[] | null => {
return list.length > 0 ? list : null;
};
const parseKeyList = <
T extends string
>(options: {
const parseKeyList = <T extends string>(options: {
raw: unknown;
defaults: T[];
isKey: (value: string) => value is T;
@@ -40,9 +30,7 @@ const parseKeyList = <
const parsed: T[] = [];
for (const value of rawList) {
if (!options.isKey(value)) {
throw new Error(
`Unknown ${options.label} command key: ${value}`
);
throw new Error(`Unknown ${options.label} command key: ${value}`);
}
parsed.push(value);
}
@@ -2,10 +2,7 @@ import type { GeneralTriggerState, TriggerValue } from '@sammo-ts/logic/domain/e
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import { beNeutral } from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat } from '@sammo-ts/logic/logging/types.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
@@ -16,7 +13,7 @@ export interface UprisingArgs {}
const ACTION_NAME = '거병';
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, UprisingArgs> {
public readonly key = 'che_거병';
public readonly name = ACTION_NAME;
@@ -38,7 +35,7 @@ export class ActionDefinition<
// 직접 수정 (Immer Draft)
general.meta = {
...general.meta as object,
...(general.meta as object),
uprising: true as TriggerValue,
};
@@ -2,10 +2,7 @@ import type { GeneralTriggerState, TriggerValue } from '@sammo-ts/logic/domain/e
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import { beNeutral } from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat } from '@sammo-ts/logic/logging/types.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
@@ -16,7 +13,7 @@ export interface FoundingArgs {}
const ACTION_NAME = '건국';
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, FoundingArgs> {
public readonly key = 'che_건국';
public readonly name = ACTION_NAME;
@@ -38,7 +35,7 @@ export class ActionDefinition<
// 직접 수정 (Immer Draft)
general.meta = {
...general.meta as object,
...(general.meta as object),
founding: true as TriggerValue,
};
@@ -1,15 +1,7 @@
import type {
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
} from '@sammo-ts/logic/constraints/types.js';
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -47,34 +39,22 @@ const SIGHTSEEING_MESSAGES: Array<{
},
{
flags: IncHeavyExp,
texts: [
'주점에서 사람들과 어울려 술을 마셨습니다.',
'위기에 빠진 사람을 구해주었습니다.',
],
texts: ['주점에서 사람들과 어울려 술을 마셨습니다.', '위기에 빠진 사람을 구해주었습니다.'],
weight: 1,
},
{
flags: IncHeavyExp | IncLeadership,
texts: [
'백성들에게 현인의 가르침을 설파했습니다.',
'어느 집의 도망친 가축을 되찾아 주었습니다.',
],
texts: ['백성들에게 현인의 가르침을 설파했습니다.', '어느 집의 도망친 가축을 되찾아 주었습니다.'],
weight: 2,
},
{
flags: IncHeavyExp | IncStrength,
texts: [
'동네 장사와 힘겨루기를 하여 멋지게 이겼습니다.',
'어느 집의 무너진 울타리를 고쳐주었습니다.',
],
texts: ['동네 장사와 힘겨루기를 하여 멋지게 이겼습니다.', '어느 집의 무너진 울타리를 고쳐주었습니다.'],
weight: 2,
},
{
flags: IncHeavyExp | IncIntel,
texts: [
'어느 명사와 설전을 벌여 멋지게 이겼습니다.',
'거리에서 글 모르는 아이들을 모아 글을 가르쳤습니다.',
],
texts: ['어느 명사와 설전을 벌여 멋지게 이겼습니다.', '거리에서 글 모르는 아이들을 모아 글을 가르쳤습니다.'],
weight: 2,
},
{
@@ -89,10 +69,7 @@ const SIGHTSEEING_MESSAGES: Array<{
},
{
flags: IncExp | DecGold,
texts: [
'산적을 만나 금 :goldAmount:을 빼앗겼습니다.',
'돈을 :goldAmount: 빌려주었다가 떼어먹혔습니다.',
],
texts: ['산적을 만나 금 :goldAmount:을 빼앗겼습니다.', '돈을 :goldAmount: 빌려주었다가 떼어먹혔습니다.'],
weight: 1,
},
{
@@ -127,10 +104,7 @@ const SIGHTSEEING_MESSAGES: Array<{
},
{
flags: IncHeavyExp | IncStrength | IncRice,
texts: [
'호랑이를 잡아 고기 :riceAmount:을 얻었습니다.',
'곰을 잡아 고기 :riceAmount:을 얻었습니다.',
],
texts: ['호랑이를 잡아 고기 :riceAmount:을 얻었습니다.', '곰을 잡아 고기 :riceAmount:을 얻었습니다.'],
weight: 1,
},
{
@@ -145,16 +119,11 @@ const SIGHTSEEING_MESSAGES: Array<{
},
];
const pickByWeight = (
rng: GeneralActionResolveContext['rng']
): { flags: number; text: string } => {
const pickByWeight = (rng: GeneralActionResolveContext['rng']): { flags: number; text: string } => {
if (SIGHTSEEING_MESSAGES.length === 0) {
return { flags: 0, text: '' };
}
const total = SIGHTSEEING_MESSAGES.reduce(
(sum, entry) => sum + Math.max(entry.weight, 0),
0
);
const total = SIGHTSEEING_MESSAGES.reduce((sum, entry) => sum + Math.max(entry.weight, 0), 0);
const base = SIGHTSEEING_MESSAGES[0];
if (!base) {
return { flags: 0, text: '' };
@@ -183,7 +152,7 @@ const pickByWeight = (
};
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, SightseeingArgs> {
public readonly key = 'che_견문';
public readonly name = ACTION_NAME;
@@ -193,10 +162,7 @@ export class ActionDefinition<
return {};
}
buildConstraints(
_ctx: ConstraintContext,
_args: SightseeingArgs
): Constraint[] {
buildConstraints(_ctx: ConstraintContext, _args: SightseeingArgs): Constraint[] {
return [];
}
@@ -1,12 +1,5 @@
import type {
GeneralTriggerState,
Nation,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
StateView,
} from '@sammo-ts/logic/constraints/types.js';
import type { GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
import {
notBeNeutral,
notWanderingNation,
@@ -15,10 +8,7 @@ import {
suppliedCity,
} from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -43,7 +33,7 @@ const readTech = (nation: Nation | null | undefined): number => {
};
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, TechResearchArgs> {
public readonly key = 'che_기술연구';
public readonly name = ACTION_NAME;
@@ -58,19 +48,9 @@ export class ActionDefinition<
return {};
}
buildConstraints(
_ctx: ConstraintContext,
_args: TechResearchArgs
): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
this.env.costGold ?? 0;
return [
notBeNeutral(),
notWanderingNation(),
occupiedCity(),
suppliedCity(),
reqGeneralGold(getRequiredGold),
];
buildConstraints(_ctx: ConstraintContext, _args: TechResearchArgs): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.costGold ?? 0;
return [notBeNeutral(), notWanderingNation(), occupiedCity(), suppliedCity(), reqGeneralGold(getRequiredGold)];
}
resolve(
@@ -87,8 +67,7 @@ export class ActionDefinition<
const delta = this.env.techDelta ?? DEFAULT_TECH_DELTA;
const currentTech = readTech(nation);
const maxTech =
typeof this.env.maxTechLevel === 'number' &&
this.env.maxTechLevel > 0
typeof this.env.maxTechLevel === 'number' && this.env.maxTechLevel > 0
? this.env.maxTechLevel
: currentTech + delta;
const nextTech = Math.min(currentTech + delta, maxTech);
@@ -1,16 +1,8 @@
import type {
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
} from '@sammo-ts/logic/constraints/types.js';
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import { allow, unknownOrDeny, readGeneral } from '@sammo-ts/logic/constraints/helpers.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -40,7 +32,7 @@ const reqDomesticSpecial = (): Constraint => ({
});
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, ResetSpecialDomesticArgs> {
public readonly key = 'che_내정특기초기화';
public readonly name = ACTION_NAME;
@@ -50,10 +42,7 @@ export class ActionDefinition<
return {};
}
buildConstraints(
_ctx: ConstraintContext,
_args: ResetSpecialDomesticArgs
): Constraint[] {
buildConstraints(_ctx: ConstraintContext, _args: ResetSpecialDomesticArgs): Constraint[] {
return [reqDomesticSpecial()];
}
@@ -5,7 +5,7 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
import type { GeneralTurnCommandSpec } from './index.js';
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends CityDevelopmentActionDefinition<TriggerState> {
constructor(env: { develCost?: number; amount?: number } = {}) {
super(
@@ -30,6 +30,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
category: '내정',
reqArg: false,
args: {},
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition({ develCost: env.develCost }),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition({ develCost: env.develCost }),
};
@@ -1,26 +1,10 @@
import type {
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
StateView,
} from '@sammo-ts/logic/constraints/types.js';
import {
notBeNeutral,
reqGeneralCrew,
reqGeneralGold,
reqGeneralRice,
} from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
import { notBeNeutral, reqGeneralCrew, reqGeneralGold, reqGeneralRice } from '@sammo-ts/logic/constraints/presets.js';
import { allow, unknownOrDeny, readGeneral } from '@sammo-ts/logic/constraints/helpers.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type {
ActionContextBuilder,
} from '@sammo-ts/logic/actions/turn/actionContext.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
@@ -30,7 +14,7 @@ import { getMetaNumber, setMetaNumber, increaseMetaNumber } from '@sammo-ts/logi
export interface DrillArgs {}
export interface DrillContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
unitSet?: UnitSetDefinition | null;
}
@@ -45,16 +29,10 @@ type DrillPick = 'success' | 'normal' | 'fail';
const ACTION_NAME = '단련';
const resolveArmTypeName = (
unitSet: UnitSetDefinition,
armType: number
): string =>
const resolveArmTypeName = (unitSet: UnitSetDefinition, armType: number): string =>
unitSet.armTypes?.[String(armType)] ?? `병종${armType}`;
const pickByWeight = <T extends string>(
rng: DrillContext['rng'],
weights: Record<T, number>
): T => {
const pickByWeight = <T extends string>(rng: DrillContext['rng'], weights: Record<T, number>): T => {
const entries = Object.entries(weights) as Array<[T, number]>;
const first = entries[0];
if (!first) {
@@ -83,11 +61,7 @@ const pickByWeight = <T extends string>(
return last ? last[0] : first[0];
};
const reqGeneralStat = (
key: 'train' | 'atmos',
label: string,
minValue: number
): Constraint => ({
const reqGeneralStat = (key: 'train' | 'atmos', label: string, minValue: number): Constraint => ({
name: `ReqGeneral${label}`,
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
test: (ctx, view) => {
@@ -105,12 +79,8 @@ const reqGeneralStat = (
});
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<
TriggerState,
DrillArgs,
DrillContext<TriggerState>
> {
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, DrillArgs, DrillContext<TriggerState>> {
public readonly key = 'che_단련';
public readonly name = ACTION_NAME;
private readonly env: DrillEnvironment;
@@ -124,16 +94,11 @@ export class ActionDefinition<
return {};
}
buildConstraints(
_ctx: ConstraintContext,
_args: DrillArgs
): Constraint[] {
buildConstraints(_ctx: ConstraintContext, _args: DrillArgs): Constraint[] {
const trainLow = this.env.defaultTrainLow ?? 40;
const atmosLow = this.env.defaultAtmosLow ?? 40;
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
this.env.develCost ?? 0;
const getRequiredRice = (_context: ConstraintContext, _view: StateView): number =>
this.env.develCost ?? 0;
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.develCost ?? 0;
const getRequiredRice = (_context: ConstraintContext, _view: StateView): number => this.env.develCost ?? 0;
return [
notBeNeutral(),
reqGeneralCrew(),
@@ -144,19 +109,14 @@ export class ActionDefinition<
];
}
resolve(
context: DrillContext<TriggerState>,
_args: DrillArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: DrillContext<TriggerState>, _args: DrillArgs): GeneralActionOutcome<TriggerState> {
if (!context.unitSet) {
context.addLog('병종 정보를 확인할 수 없어 단련을 진행할 수 없습니다.');
return { effects: [] };
}
const general = context.general;
const crewType = context.unitSet.crewTypes?.find(
(entry) => entry.id === general.crewTypeId
);
const crewType = context.unitSet.crewTypes?.find((entry) => entry.id === general.crewTypeId);
if (!crewType) {
context.addLog('병종 정보를 확인할 수 없어 단련을 진행할 수 없습니다.');
return { effects: [] };
@@ -169,20 +129,10 @@ export class ActionDefinition<
});
const multiplier = pick === 'success' ? 3 : pick === 'normal' ? 2 : 1;
const baseScore = Math.round(
(general.crew * general.train * general.atmos) / 20 / 10000
);
const baseScore = Math.round((general.crew * general.train * general.atmos) / 20 / 10000);
const score = baseScore * multiplier;
const armTypeName = resolveArmTypeName(
context.unitSet,
crewType.armType
);
const logPrefix =
pick === 'success'
? '단련이 일취월장하여'
: pick === 'fail'
? '단련이 지지부진하여'
: '';
const armTypeName = resolveArmTypeName(context.unitSet, crewType.armType);
const logPrefix = pick === 'success' ? '단련이 일취월장하여' : pick === 'fail' ? '단련이 지지부진하여' : '';
const logText = logPrefix
? `${logPrefix} ${armTypeName} 숙련도가 ${score} 향상되었습니다.`
: `${armTypeName} 숙련도가 ${score} 향상되었습니다.`;
@@ -1,17 +1,8 @@
import type {
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
StateView,
} from '@sammo-ts/logic/constraints/types.js';
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
import { notBeNeutral, reqGeneralGold } from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -30,7 +21,7 @@ const DEFAULT_ATMOS_DELTA = 5;
const DEFAULT_MAX_ATMOS = 100;
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, BoostMoraleArgs> {
public readonly key = 'che_사기진작';
public readonly name = ACTION_NAME;
@@ -45,12 +36,8 @@ export class ActionDefinition<
return {};
}
buildConstraints(
_ctx: ConstraintContext,
_args: BoostMoraleArgs
): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
this.env.costGold ?? 0;
buildConstraints(_ctx: ConstraintContext, _args: BoostMoraleArgs): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.costGold ?? 0;
return [notBeNeutral(), reqGeneralGold(getRequiredGold)];
}
@@ -63,10 +50,7 @@ export class ActionDefinition<
this.env.maxAtmosByCommand && this.env.maxAtmosByCommand > 0
? this.env.maxAtmosByCommand
: DEFAULT_MAX_ATMOS;
const delta =
this.env.atmosDelta && this.env.atmosDelta > 0
? this.env.atmosDelta
: DEFAULT_ATMOS_DELTA;
const delta = this.env.atmosDelta && this.env.atmosDelta > 0 ? this.env.atmosDelta : DEFAULT_ATMOS_DELTA;
const nextAtmos = clamp(general.atmos + delta, 0, maxAtmos);
const applied = nextAtmos - general.atmos;
const costGold = this.env.costGold ?? 0;
@@ -1,16 +1,6 @@
import type { RandomGenerator } from '@sammo-ts/common';
import type {
City,
General,
GeneralTriggerState,
Nation,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
RequirementKey,
StateView,
} from '@sammo-ts/logic/constraints/types.js';
import type { City, General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext, RequirementKey, StateView } from '@sammo-ts/logic/constraints/types.js';
import {
notBeNeutral,
notWanderingNation,
@@ -21,10 +11,7 @@ import {
suppliedCity,
} from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import {
GeneralActionPipeline,
type GeneralActionModule,
} from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
@@ -39,7 +26,7 @@ import { clamp } from 'es-toolkit';
export type DomesticCriticalPick = 'fail' | 'normal' | 'success';
export interface DomesticActionContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionContext<TriggerState> {
general: General<TriggerState>;
city: City;
@@ -52,14 +39,8 @@ export interface InvestmentEnvironment {
frontDebuff?: number;
frontStatesWithDebuff?: number[];
getDomesticExpLevelBonus?: (expLevel: number) => number;
getCriticalRatio?: (
context: DomesticActionContext,
statKey: string
) => { success: number; fail: number };
getCriticalScoreMultiplier?: (
rng: RandomGenerator,
pick: DomesticCriticalPick
) => number;
getCriticalRatio?: (context: DomesticActionContext, statKey: string) => { success: number; fail: number };
getCriticalScoreMultiplier?: (rng: RandomGenerator, pick: DomesticCriticalPick) => number;
adjustFrontDebuff?: (context: DomesticActionContext, debuff: number) => number;
}
@@ -82,23 +63,15 @@ const ACTION_NAME = '상업 투자';
const CITY_KEY = 'commerce';
const STAT_EXP_KEY = 'intel_exp';
const getMetaNumber = (
meta: Record<string, unknown>,
key: string
): number | null => {
const getMetaNumber = (meta: Record<string, unknown>, key: string): number | null => {
const raw = meta[key];
return typeof raw === 'number' ? raw : null;
};
const randomRange = (rng: RandomGenerator, min: number, max: number): number =>
min + (max - min) * rng.nextFloat();
const randomRange = (rng: RandomGenerator, min: number, max: number): number => min + (max - min) * rng.nextFloat();
const pickByWeight = (
rng: RandomGenerator,
weights: Record<DomesticCriticalPick, number>
): DomesticCriticalPick => {
const total =
weights.fail + weights.normal + weights.success;
const pickByWeight = (rng: RandomGenerator, weights: Record<DomesticCriticalPick, number>): DomesticCriticalPick => {
const total = weights.fail + weights.normal + weights.success;
if (total <= 0) {
return 'normal';
}
@@ -112,18 +85,12 @@ const pickByWeight = (
return 'normal';
};
const addMetaNumber = (
meta: Record<string, unknown>,
key: string,
delta: number
): Record<string, unknown> => {
const addMetaNumber = (meta: Record<string, unknown>, key: string, delta: number): Record<string, unknown> => {
const current = getMetaNumber(meta, key) ?? 0;
return { ...meta, [key]: current + delta };
};
const buildDomesticContextFromView = <
TriggerState extends GeneralTriggerState = GeneralTriggerState
>(
const buildDomesticContextFromView = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
ctx: ConstraintContext,
view: StateView
): DomesticActionContext<TriggerState> | null => {
@@ -141,10 +108,7 @@ const buildDomesticContextFromView = <
}
const nationId = ctx.nationId ?? general.nationId;
const nation =
nationId !== undefined
? ((view.get({ kind: 'nation', id: nationId }) as Nation | null) ??
null)
: null;
nationId !== undefined ? ((view.get({ kind: 'nation', id: nationId }) as Nation | null) ?? null) : null;
return {
general,
@@ -154,18 +118,13 @@ const buildDomesticContextFromView = <
};
// 상업 투자 결과치를 계산하는 경로를 제공한다.
export class CommandResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
private readonly pipeline: GeneralActionPipeline<TriggerState>;
private readonly env: InvestmentEnvironment;
private readonly actionKey = '상업';
private readonly statKey = 'intelligence';
constructor(
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
env: InvestmentEnvironment
) {
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: InvestmentEnvironment) {
this.pipeline = new GeneralActionPipeline(modules);
this.env = env;
}
@@ -175,84 +134,42 @@ export class CommandResolver<
rice: number;
} {
const baseGold = this.env.develCost;
const gold = Math.round(
this.pipeline.onCalcDomestic(
context,
this.actionKey,
'cost',
baseGold
)
);
const gold = Math.round(this.pipeline.onCalcDomestic(context, this.actionKey, 'cost', baseGold));
return { gold, rice: 0 };
}
calcBaseScore(
context: DomesticActionContext<TriggerState>,
rng: RandomGenerator
): number {
const trust =
getMetaNumber(context.city.meta, 'trust') ??
this.env.defaultTrust ??
DEFAULT_TRUST;
calcBaseScore(context: DomesticActionContext<TriggerState>, rng: RandomGenerator): number {
const trust = getMetaNumber(context.city.meta, 'trust') ?? this.env.defaultTrust ?? DEFAULT_TRUST;
let score = this.pipeline.onCalcStat(
context,
this.statKey,
context.general.stats.intelligence
);
let score = this.pipeline.onCalcStat(context, this.statKey, context.general.stats.intelligence);
const expLevel =
getMetaNumber(context.general.meta, 'explevel') ??
getMetaNumber(context.general.meta, 'expLevel') ??
0;
const expBonus =
this.env.getDomesticExpLevelBonus?.(expLevel) ?? 1;
getMetaNumber(context.general.meta, 'explevel') ?? getMetaNumber(context.general.meta, 'expLevel') ?? 0;
const expBonus = this.env.getDomesticExpLevelBonus?.(expLevel) ?? 1;
score *= trust / 100;
score *= expBonus;
score *= randomRange(rng, 0.8, 1.2);
return this.pipeline.onCalcDomestic(
context,
this.actionKey,
'score',
score
);
return this.pipeline.onCalcDomestic(context, this.actionKey, 'score', score);
}
resolve(
context: DomesticActionContext<TriggerState>,
rng: RandomGenerator
): CommerceInvestmentResult {
resolve(context: DomesticActionContext<TriggerState>, rng: RandomGenerator): CommerceInvestmentResult {
const { gold: costGold, rice: costRice } = this.getCost(context);
const trust =
getMetaNumber(context.city.meta, 'trust') ??
this.env.defaultTrust ??
DEFAULT_TRUST;
const trust = getMetaNumber(context.city.meta, 'trust') ?? this.env.defaultTrust ?? DEFAULT_TRUST;
let score = clamp(this.calcBaseScore(context, rng), 1, Number.MAX_SAFE_INTEGER);
const ratio =
this.env.getCriticalRatio?.(context, this.statKey) ?? {
success: 0,
fail: 0,
};
const ratio = this.env.getCriticalRatio?.(context, this.statKey) ?? {
success: 0,
fail: 0,
};
let successRatio = ratio.success;
let failRatio = ratio.fail;
if (trust < 80) {
successRatio *= trust / 80;
}
successRatio = this.pipeline.onCalcDomestic(
context,
this.actionKey,
'success',
successRatio
);
failRatio = this.pipeline.onCalcDomestic(
context,
this.actionKey,
'fail',
failRatio
);
successRatio = this.pipeline.onCalcDomestic(context, this.actionKey, 'success', successRatio);
failRatio = this.pipeline.onCalcDomestic(context, this.actionKey, 'fail', failRatio);
successRatio = clamp(successRatio, 0, 1);
failRatio = clamp(failRatio, 0, 1 - successRatio);
@@ -264,18 +181,14 @@ export class CommandResolver<
normal: normalRatio,
});
const criticalMultiplier =
this.env.getCriticalScoreMultiplier?.(rng, pick) ?? 1;
const criticalMultiplier = this.env.getCriticalScoreMultiplier?.(rng, pick) ?? 1;
score = Math.round(score * criticalMultiplier);
const frontStates =
this.env.frontStatesWithDebuff ?? DEFAULT_FRONT_STATES;
const frontStates = this.env.frontStatesWithDebuff ?? DEFAULT_FRONT_STATES;
let appliedFrontDebuff = false;
if (frontStates.includes(context.city.frontState)) {
const baseDebuff =
this.env.frontDebuff ?? DEFAULT_FRONT_DEBUFF;
const adjustedDebuff =
this.env.adjustFrontDebuff?.(context, baseDebuff) ?? baseDebuff;
const baseDebuff = this.env.frontDebuff ?? DEFAULT_FRONT_DEBUFF;
const adjustedDebuff = this.env.adjustFrontDebuff?.(context, baseDebuff) ?? baseDebuff;
score *= adjustedDebuff;
appliedFrontDebuff = true;
}
@@ -296,15 +209,12 @@ export class CommandResolver<
}
export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionResolver<TriggerState, CommerceInvestmentArgs> {
readonly key = 'che_상업투자';
private readonly command: CommandResolver<TriggerState>;
constructor(
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
env: InvestmentEnvironment
) {
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: InvestmentEnvironment) {
this.command = new CommandResolver(modules, env);
}
@@ -329,11 +239,7 @@ export class ActionResolver<
);
// 직접 수정 (Immer Draft)
city.commerce = clamp(
city.commerce + result.score,
0,
city.commerceMax
);
city.commerce = clamp(city.commerce + result.score, 0, city.commerceMax);
general.gold = Math.max(0, general.gold - result.costGold);
general.rice = Math.max(0, general.rice - result.costRice);
@@ -346,12 +252,7 @@ export class ActionResolver<
? { ...metaWithStatExp, max_domestic_critical: result.score }
: { ...metaWithStatExp, max_domestic_critical: 0 };
const pickLabel =
result.pick === 'success'
? '성공'
: result.pick === 'fail'
? '실패'
: '완료';
const pickLabel = result.pick === 'success' ? '성공' : result.pick === 'fail' ? '실패' : '완료';
const logMessage = `${ACTION_NAME} ${pickLabel}: +${Math.round(result.score)}`;
context.addLog(logMessage);
@@ -360,17 +261,14 @@ export class ActionResolver<
}
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, CommerceInvestmentArgs> {
public readonly key = 'che_상업투자';
public readonly name = ACTION_NAME;
private readonly command: CommandResolver<TriggerState>;
private readonly resolver: ActionResolver<TriggerState>;
constructor(
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
env: InvestmentEnvironment
) {
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: InvestmentEnvironment) {
this.command = new CommandResolver(modules, env);
this.resolver = new ActionResolver(modules, env);
}
@@ -380,10 +278,7 @@ export class ActionDefinition<
return {};
}
buildConstraints(
ctx: ConstraintContext,
_args: CommerceInvestmentArgs
): Constraint[] {
buildConstraints(ctx: ConstraintContext, _args: CommerceInvestmentArgs): Constraint[] {
void _args;
const requirements: RequirementKey[] = [];
if (ctx.cityId !== undefined) {
@@ -394,8 +289,7 @@ export class ActionDefinition<
}
const getCost = (context: ConstraintContext, view: StateView): number => {
const domesticContext =
buildDomesticContextFromView<TriggerState>(context, view);
const domesticContext = buildDomesticContextFromView<TriggerState>(context, view);
if (!domesticContext) {
return 0;
}
@@ -429,6 +323,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
category: '내정',
reqArg: false,
args: {},
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition(env.generalActionModules ?? [], env),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? [], env),
};
@@ -5,7 +5,7 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
import type { GeneralTurnCommandSpec } from './index.js';
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends CityDevelopmentActionDefinition<TriggerState> {
constructor(env: { develCost?: number; amount?: number } = {}) {
super(
@@ -30,6 +30,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
category: '내정',
reqArg: false,
args: {},
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition({ develCost: env.develCost }),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition({ develCost: env.develCost }),
};
@@ -5,7 +5,7 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
import type { GeneralTurnCommandSpec } from './index.js';
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends CityDevelopmentActionDefinition<TriggerState> {
constructor(env: { develCost?: number; amount?: number } = {}) {
super(
@@ -30,6 +30,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
category: '내정',
reqArg: false,
args: {},
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition({ develCost: env.develCost }),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition({ develCost: env.develCost }),
};
@@ -1,25 +1,9 @@
import type {
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
StateView,
} from '@sammo-ts/logic/constraints/types.js';
import {
notBeNeutral,
occupiedCity,
reqGeneralGold,
reqGeneralRice,
} from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
import { notBeNeutral, occupiedCity, reqGeneralGold, reqGeneralRice } from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type {
ActionContextBuilder,
} from '@sammo-ts/logic/actions/turn/actionContext.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
@@ -32,7 +16,7 @@ export interface DexTransferArgs {
}
export interface DexTransferContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
unitSet?: UnitSetDefinition | null;
}
@@ -55,19 +39,12 @@ const resolveArmType = (value: unknown): number | null => {
return value > 0 ? value : null;
};
const resolveArmTypeName = (
unitSet: UnitSetDefinition | null | undefined,
armType: number
): string =>
const resolveArmTypeName = (unitSet: UnitSetDefinition | null | undefined, armType: number): string =>
unitSet?.armTypes?.[String(armType)] ?? `병종${armType}`;
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<
TriggerState,
DexTransferArgs,
DexTransferContext<TriggerState>
> {
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, DexTransferArgs, DexTransferContext<TriggerState>> {
public readonly key = 'che_숙련전환';
public readonly name = ACTION_NAME;
private readonly env: DexTransferEnvironment;
@@ -89,26 +66,13 @@ export class ActionDefinition<
return { srcArmType, destArmType };
}
buildConstraints(
_ctx: ConstraintContext,
_args: DexTransferArgs
): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
this.env.develCost ?? 0;
const getRequiredRice = (_context: ConstraintContext, _view: StateView): number =>
this.env.develCost ?? 0;
return [
notBeNeutral(),
occupiedCity(),
reqGeneralGold(getRequiredGold),
reqGeneralRice(getRequiredRice),
];
buildConstraints(_ctx: ConstraintContext, _args: DexTransferArgs): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.develCost ?? 0;
const getRequiredRice = (_context: ConstraintContext, _view: StateView): number => this.env.develCost ?? 0;
return [notBeNeutral(), occupiedCity(), reqGeneralGold(getRequiredGold), reqGeneralRice(getRequiredRice)];
}
resolve(
context: DexTransferContext<TriggerState>,
args: DexTransferArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: DexTransferContext<TriggerState>, args: DexTransferArgs): GeneralActionOutcome<TriggerState> {
const general = context.general;
const srcKey = `dex${args.srcArmType}`;
const destKey = `dex${args.destArmType}`;
@@ -117,20 +81,10 @@ export class ActionDefinition<
const addDex = Math.trunc(cutDex * CONVERT_COEFF);
setMetaNumber(general.meta, srcKey, srcDex - cutDex);
setMetaNumber(
general.meta,
destKey,
getMetaNumber(general.meta, destKey, 0) + addDex
);
setMetaNumber(general.meta, destKey, getMetaNumber(general.meta, destKey, 0) + addDex);
const srcName = resolveArmTypeName(
context.unitSet,
args.srcArmType
);
const destName = resolveArmTypeName(
context.unitSet,
args.destArmType
);
const srcName = resolveArmTypeName(context.unitSet, args.srcArmType);
const destName = resolveArmTypeName(context.unitSet, args.destArmType);
const cutJosa = JosaUtil.pick(String(cutDex), '을');
const addJosa = JosaUtil.pick(String(addDex), '으로');
@@ -140,9 +94,7 @@ export class ActionDefinition<
general.experience += 10;
increaseMetaNumber(general.meta, 'leadership_exp', 2);
context.addLog(
`${srcName} 숙련 ${cutDex}${cutJosa} ${destName} 숙련 ${addDex}${addJosa} 전환했습니다.`
);
context.addLog(`${srcName} 숙련 ${cutDex}${cutJosa} ${destName} 숙련 ${addDex}${addJosa} 전환했습니다.`);
return { effects: [] };
}
@@ -158,6 +110,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
category: '군사',
reqArg: true,
args: { srcArmType: 0, destArmType: 0 },
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition({ develCost: env.develCost }),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition({ develCost: env.develCost }),
};
@@ -1,17 +1,8 @@
import type {
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
StateView,
} from '@sammo-ts/logic/constraints/types.js';
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
import { notBeNeutral, reqGeneralGold } from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -27,7 +18,7 @@ const ACTION_NAME = '요양';
const DEFAULT_INJURY_DELTA = 10;
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, RecoveryArgs> {
public readonly key = 'che_요양';
public readonly name = ACTION_NAME;
@@ -42,12 +33,8 @@ export class ActionDefinition<
return {};
}
buildConstraints(
_ctx: ConstraintContext,
_args: RecoveryArgs
): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
this.env.costGold ?? 0;
buildConstraints(_ctx: ConstraintContext, _args: RecoveryArgs): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.costGold ?? 0;
return [notBeNeutral(), reqGeneralGold(getRequiredGold)];
}
@@ -1,32 +1,15 @@
import type { RandomGenerator } from '@sammo-ts/common';
import type {
City,
General,
GeneralTriggerState,
StatBlock,
TriggerValue,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
} from '@sammo-ts/logic/constraints/types.js';
import {
reqGeneralGold,
reqGeneralRice,
} from '@sammo-ts/logic/constraints/presets.js';
import {
GeneralActionPipeline,
type GeneralActionModule,
} from '@sammo-ts/logic/triggers/general-action.js';
import type { City, General, GeneralTriggerState, StatBlock, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import { reqGeneralGold, reqGeneralRice } from '@sammo-ts/logic/constraints/presets.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
GeneralActionResolver,
} from '@sammo-ts/logic/actions/engine.js';
import {
createGeneralAddEffect,
} from '@sammo-ts/logic/actions/engine.js';
import { createGeneralAddEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
import { buildRecruitmentGeneral } from './recruitment.js';
import { JosaUtil } from '@sammo-ts/common';
@@ -55,7 +38,7 @@ export interface TalentScoutWorldSummary {
}
export interface TalentScoutResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
currentYear: number;
worldSummary: TalentScoutWorldSummary;
@@ -77,14 +60,8 @@ export interface TalentScoutEnvironment {
minDeathYears?: number;
maxDeathYears?: number;
decorateName?: (name: string, npcState: number) => string;
pickCandidate?: (
context: TalentScoutResolveContext,
rng: RandomGenerator
) => TalentScoutCandidate | null;
pickSpawnCityId?: (
context: TalentScoutResolveContext,
rng: RandomGenerator
) => number | null;
pickCandidate?: (context: TalentScoutResolveContext, rng: RandomGenerator) => TalentScoutCandidate | null;
pickSpawnCityId?: (context: TalentScoutResolveContext, rng: RandomGenerator) => number | null;
buildStats?: (
context: TalentScoutResolveContext,
rng: RandomGenerator,
@@ -118,15 +95,11 @@ const addMetaNumber = (
key: StatExpKey,
delta: number
): Record<string, TriggerValue> => {
const current =
typeof meta[key] === 'number' ? (meta[key] as number) : 0;
const current = typeof meta[key] === 'number' ? (meta[key] as number) : 0;
return { ...meta, [key]: current + delta };
};
const pickByWeight = <T extends string>(
rng: RandomGenerator,
weights: Record<T, number>
): T => {
const pickByWeight = <T extends string>(rng: RandomGenerator, weights: Record<T, number>): T => {
const entries = Object.entries(weights) as Array<[T, number]>;
const first = entries[0];
if (!first) {
@@ -155,26 +128,18 @@ const pickByWeight = <T extends string>(
return last ? last[0] : first[0];
};
const pickStatExpKey = (
rng: RandomGenerator,
general: General
): StatExpKey =>
const pickStatExpKey = (rng: RandomGenerator, general: General): StatExpKey =>
pickByWeight(rng, {
leadership_exp: general.stats.leadership,
strength_exp: general.stats.strength,
intel_exp: general.stats.intelligence,
});
const calcFoundProp = (
maxGeneral: number,
totalGeneralCount: number,
totalNpcCount: number
): number => {
const calcFoundProp = (maxGeneral: number, totalGeneralCount: number, totalNpcCount: number): number => {
if (maxGeneral <= 0) {
return 0;
}
const current =
totalGeneralCount + totalNpcCount / 2;
const current = totalGeneralCount + totalNpcCount / 2;
const remainSlot = Math.max(maxGeneral - current, 0);
const main = Math.pow(remainSlot / maxGeneral, 6);
const small = 1 / (totalNpcCount / 3 + 1);
@@ -185,11 +150,7 @@ const calcFoundProp = (
return Math.max(main, big);
};
const randomRangeInt = (
rng: RandomGenerator,
min: number,
max: number
): number => rng.nextInt(min, max + 1);
const randomRangeInt = (rng: RandomGenerator, min: number, max: number): number => rng.nextInt(min, max + 1);
const resolveCandidate = (
context: TalentScoutResolveContext,
@@ -235,8 +196,7 @@ const resolveStats = (
if (env.buildStats) {
return env.buildStats(context, rng, candidate);
}
const fallback =
context.worldSummary.averageStats ?? context.general.stats;
const fallback = context.worldSummary.averageStats ?? context.general.stats;
return {
leadership: candidate.stats?.leadership ?? fallback.leadership,
strength: candidate.stats?.strength ?? fallback.strength,
@@ -245,16 +205,11 @@ const resolveStats = (
};
// 인재탐색 확률과 비용을 계산한다.
export class CommandResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
private readonly pipeline: GeneralActionPipeline<TriggerState>;
private readonly env: TalentScoutEnvironment;
constructor(
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
env: TalentScoutEnvironment
) {
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: TalentScoutEnvironment) {
this.pipeline = new GeneralActionPipeline(modules);
this.env = env;
}
@@ -272,27 +227,19 @@ export class CommandResolver<
context.worldSummary.totalGeneralCount,
context.worldSummary.totalNpcCount
);
return this.pipeline.onCalcDomestic(
context,
ACTION_KEY,
'probability',
base
);
return this.pipeline.onCalcDomestic(context, ACTION_KEY, 'probability', base);
}
}
// 인재탐색 실행 결과를 계산한다.
export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionResolver<TriggerState, TalentScoutArgs> {
readonly key = 'che_인재탐색';
private readonly env: TalentScoutEnvironment;
private readonly command: CommandResolver<TriggerState>;
constructor(
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
env: TalentScoutEnvironment
) {
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: TalentScoutEnvironment) {
this.env = env;
this.command = new CommandResolver(modules, env);
}
@@ -308,10 +255,7 @@ export class ActionResolver<
const found = context.rng.nextBool(prop);
const statKey = pickStatExpKey(context.rng, general);
const metaAfter =
found
? addMetaNumber(general.meta, statKey, 3)
: addMetaNumber(general.meta, statKey, 1);
const metaAfter = found ? addMetaNumber(general.meta, statKey, 3) : addMetaNumber(general.meta, statKey, 1);
const nextGold = Math.max(0, general.gold - reqGold);
const nextRice = Math.max(0, general.rice - reqRice);
@@ -335,8 +279,7 @@ export class ActionResolver<
const candidate = resolveCandidate(context, context.rng, this.env);
const newGeneralId = context.createGeneralId();
const resolvedCandidate: TalentScoutCandidate =
candidate ?? { name: `NPC_${newGeneralId}` };
const resolvedCandidate: TalentScoutCandidate = candidate ?? { name: `NPC_${newGeneralId}` };
const age = randomRangeInt(
context.rng,
@@ -351,12 +294,7 @@ export class ActionResolver<
this.env.minDeathYears ?? DEFAULT_DEATH_MIN,
this.env.maxDeathYears ?? DEFAULT_DEATH_MAX
);
const stats = resolveStats(
context,
context.rng,
this.env,
resolvedCandidate
);
const stats = resolveStats(context, context.rng, this.env, resolvedCandidate);
const name = this.env.decorateName
? this.env.decorateName(resolvedCandidate.name, NPC_TYPE)
: resolvedCandidate.name;
@@ -415,21 +353,14 @@ export class ActionResolver<
}
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<
TriggerState,
TalentScoutArgs,
TalentScoutResolveContext<TriggerState>
> {
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, TalentScoutArgs, TalentScoutResolveContext<TriggerState>> {
public readonly key = 'che_인재탐색';
public readonly name = ACTION_NAME;
private readonly command: CommandResolver<TriggerState>;
private readonly resolver: ActionResolver<TriggerState>;
constructor(
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
env: TalentScoutEnvironment
) {
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: TalentScoutEnvironment) {
this.command = new CommandResolver(modules, env);
this.resolver = new ActionResolver(modules, env);
}
@@ -439,17 +370,11 @@ export class ActionDefinition<
return {};
}
buildConstraints(
_ctx: ConstraintContext,
_args: TalentScoutArgs
): Constraint[] {
buildConstraints(_ctx: ConstraintContext, _args: TalentScoutArgs): Constraint[] {
void _ctx;
void _args;
const { gold, rice } = this.command.getCost();
return [
reqGeneralGold(() => gold),
reqGeneralRice(() => rice),
];
return [reqGeneralGold(() => gold), reqGeneralRice(() => rice)];
}
resolve(
@@ -473,6 +398,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
category: '인사',
reqArg: false,
args: {},
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition(env.generalActionModules ?? [], env),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? [], env),
};
@@ -1,14 +1,8 @@
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
} from '@sammo-ts/logic/constraints/types.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import { beNeutral, existsDestNation } from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat } from '@sammo-ts/logic/logging/types.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
@@ -28,7 +22,7 @@ const parseNationId = (raw: unknown): number | null => {
};
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, AppointmentArgs> {
public readonly key = 'che_임관';
public readonly name = ACTION_NAME;
@@ -50,13 +44,10 @@ export class ActionDefinition<
context: GeneralActionResolveContext<TriggerState>,
args: AppointmentArgs
): GeneralActionOutcome<TriggerState> {
context.addLog(
`${ACTION_NAME}을 신청했습니다. (국가 ${args.destNationId})`,
{
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}
);
context.addLog(`${ACTION_NAME}을 신청했습니다. (국가 ${args.destNationId})`, {
category: LogCategory.ACTION,
format: LogFormat.MONTH,
});
return { effects: [] };
}
}
@@ -1,16 +1,8 @@
import type {
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
} from '@sammo-ts/logic/constraints/types.js';
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import { allow, unknownOrDeny, readGeneral } from '@sammo-ts/logic/constraints/helpers.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -40,7 +32,7 @@ const reqWarSpecial = (): Constraint => ({
});
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, ResetSpecialWarArgs> {
public readonly key = 'che_전투특기초기화';
public readonly name = ACTION_NAME;
@@ -50,10 +42,7 @@ export class ActionDefinition<
return {};
}
buildConstraints(
_ctx: ConstraintContext,
_args: ResetSpecialWarArgs
): Constraint[] {
buildConstraints(_ctx: ConstraintContext, _args: ResetSpecialWarArgs): Constraint[] {
return [reqWarSpecial()];
}
@@ -5,7 +5,7 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
import type { GeneralTurnCommandSpec } from './index.js';
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends CityDevelopmentActionDefinition<TriggerState> {
constructor(env: { develCost?: number; amount?: number } = {}) {
super(
@@ -30,6 +30,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
category: '내정',
reqArg: false,
args: {},
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition({ develCost: env.develCost }),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition({ develCost: env.develCost }),
};
@@ -1,8 +1,5 @@
import type { General, GeneralTriggerState, Troop } from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
} from '@sammo-ts/logic/constraints/types.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import {
mustBeTroopLeader,
notBeNeutral,
@@ -27,7 +24,7 @@ import { increaseMetaNumber } from '@sammo-ts/logic/war/utils.js';
export interface AssemblyArgs {}
export interface AssemblyResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
troop: Troop | null;
troopMembers: Array<General<TriggerState>>;
@@ -36,12 +33,8 @@ export interface AssemblyResolveContext<
const ACTION_NAME = '집합';
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<
TriggerState,
AssemblyArgs,
AssemblyResolveContext<TriggerState>
> {
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, AssemblyArgs, AssemblyResolveContext<TriggerState>> {
public readonly key = 'che_집합';
public readonly name = ACTION_NAME;
@@ -50,23 +43,11 @@ export class ActionDefinition<
return {};
}
buildConstraints(
_ctx: ConstraintContext,
_args: AssemblyArgs
): Constraint[] {
return [
notBeNeutral(),
occupiedCity(),
suppliedCity(),
mustBeTroopLeader(),
reqTroopMembers(),
];
buildConstraints(_ctx: ConstraintContext, _args: AssemblyArgs): Constraint[] {
return [notBeNeutral(), occupiedCity(), suppliedCity(), mustBeTroopLeader(), reqTroopMembers()];
}
resolve(
context: AssemblyResolveContext<TriggerState>,
_args: AssemblyArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: AssemblyResolveContext<TriggerState>, _args: AssemblyArgs): GeneralActionOutcome<TriggerState> {
const city = context.city;
if (!city) {
context.addLog('도시 정보가 없어 집합을 진행할 수 없습니다.');
@@ -81,26 +62,16 @@ export class ActionDefinition<
context.addLog(`<G><b>${cityName}</b></>에서 집합을 실시했습니다.`);
const effects: Array<GeneralActionEffect<TriggerState>> = [];
const targets = context.troopMembers.filter(
(member) => member.cityId !== city.id
);
const targets = context.troopMembers.filter((member) => member.cityId !== city.id);
for (const member of targets) {
effects.push(createGeneralPatchEffect({ cityId: city.id } as Partial<General<TriggerState>>, member.id));
effects.push(
createGeneralPatchEffect(
{ cityId: city.id } as Partial<General<TriggerState>>,
member.id
)
);
effects.push(
createLogEffect(
`${troopName} 부대원들은 <G><b>${cityName}</b></>${josaRo} 집합되었습니다.`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
generalId: member.id,
}
)
createLogEffect(`${troopName} 부대원들은 <G><b>${cityName}</b></>${josaRo} 집합되었습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
generalId: member.id,
})
);
}
@@ -116,9 +87,9 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
const troopId = base.general.troopId;
const troop = options.worldRef?.getTroopById(troopId) ?? null;
const troopMembers =
options.worldRef?.listGenerals().filter(
(member) => member.troopId === troopId && member.id !== base.general.id
) ?? [];
options.worldRef
?.listGenerals()
.filter((member) => member.troopId === troopId && member.id !== base.general.id) ?? [];
return {
...base,
troop,
@@ -1,16 +1,5 @@
import type {
City,
General,
GeneralTriggerState,
Nation,
TriggerValue,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
RequirementKey,
StateView,
} from '@sammo-ts/logic/constraints/types.js';
import type { City, General, GeneralTriggerState, Nation, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext, RequirementKey, StateView } from '@sammo-ts/logic/constraints/types.js';
import {
notBeNeutral,
occupiedCity,
@@ -20,10 +9,7 @@ import {
reqGeneralGold,
reqGeneralRice,
} from '@sammo-ts/logic/constraints/presets.js';
import {
GeneralActionPipeline,
type GeneralActionModule,
} from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
@@ -56,7 +42,7 @@ export interface RecruitEnvironment {
}
export interface RecruitResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
map: MapDefinition;
unitSet: UnitSetDefinition;
@@ -97,10 +83,7 @@ const readNationTech = (nation: Nation | null | undefined): number => {
return typeof tech === 'number' ? tech : 0;
};
const readCityTrust = (
city: City,
fallback: number
): number => {
const readCityTrust = (city: City, fallback: number): number => {
const meta = city.meta as Record<string, unknown>;
const trust = meta?.trust;
return typeof trust === 'number' ? trust : fallback;
@@ -136,10 +119,7 @@ const resolveCrewAmount = (args: Record<string, unknown>): number | null => {
return value;
};
const buildCrewTypeContext = (
ctx: ConstraintContext,
view: StateView
): CrewTypeAvailabilityContext | null => {
const buildCrewTypeContext = (ctx: ConstraintContext, view: StateView): CrewTypeAvailabilityContext | null => {
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
const general = view.get(generalReq) as General | null;
if (!general) {
@@ -147,8 +127,7 @@ const buildCrewTypeContext = (
}
const nationId = ctx.nationId ?? general.nationId;
const nationReq: RequirementKey = { kind: 'nation', id: nationId };
const nation =
nationId > 0 ? ((view.get(nationReq) as Nation | null) ?? null) : null;
const nation = nationId > 0 ? ((view.get(nationReq) as Nation | null) ?? null) : null;
const map = ctx.env.map;
const cities = ctx.env.cities;
if (!map || !cities || !Array.isArray(cities)) {
@@ -158,10 +137,9 @@ const buildCrewTypeContext = (
typeof ctx.env.currentYear === 'number'
? ctx.env.currentYear
: typeof ctx.env.year === 'number'
? ctx.env.year
: undefined;
const startYear =
typeof ctx.env.startYear === 'number' ? ctx.env.startYear : undefined;
? ctx.env.year
: undefined;
const startYear = typeof ctx.env.startYear === 'number' ? ctx.env.startYear : undefined;
const result: CrewTypeAvailabilityContext = {
general,
nation,
@@ -177,9 +155,7 @@ const buildCrewTypeContext = (
return result;
};
type RecruitCalcContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> = {
type RecruitCalcContext<TriggerState extends GeneralTriggerState = GeneralTriggerState> = {
general: General<TriggerState>;
city?: City;
nation?: Nation | null;
@@ -196,12 +172,10 @@ const buildCalcContext = <TriggerState extends GeneralTriggerState>(
}
const nationId = ctx.nationId ?? general.nationId;
const nationReq: RequirementKey = { kind: 'nation', id: nationId };
const nation =
nationId > 0 ? ((view.get(nationReq) as Nation | null) ?? null) : null;
const nation = nationId > 0 ? ((view.get(nationReq) as Nation | null) ?? null) : null;
const city =
ctx.cityId !== undefined
? ((view.get({ kind: 'city', id: ctx.cityId }) as City | null) ??
undefined)
? ((view.get({ kind: 'city', id: ctx.cityId }) as City | null) ?? undefined)
: undefined;
const result: RecruitCalcContext<TriggerState> = { general };
if (city) {
@@ -213,26 +187,19 @@ const buildCalcContext = <TriggerState extends GeneralTriggerState>(
return result;
};
export class CommandResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
// 징병 명령의 비용/훈련/사기 계산을 담당한다.
private readonly pipeline: GeneralActionPipeline<TriggerState>;
private readonly env: RecruitEnvironment;
constructor(
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
env: RecruitEnvironment
) {
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: RecruitEnvironment) {
this.pipeline = new GeneralActionPipeline(modules);
this.env = env;
}
resolveLeadership(context: RecruitCalcContext<TriggerState>): number {
const base = context.general.stats.leadership;
return Math.round(
this.pipeline.onCalcStat(context, 'leadership', base)
);
return Math.round(this.pipeline.onCalcStat(context, 'leadership', base));
}
resolveCrewPlan(
@@ -259,9 +226,7 @@ export class CommandResolver<
const plan = this.resolveCrewPlan(context, crewTypeId, amount);
const tech = readNationTech(context.nation ?? null);
const costOffset = this.env.costOffset ?? DEFAULT_COST_OFFSET;
const baseGold = crewType
? crewType.cost * getTechCost(tech) * plan.applied / 100
: 0;
const baseGold = crewType ? (crewType.cost * getTechCost(tech) * plan.applied) / 100 : 0;
const adjustedGold = this.pipeline.onCalcDomestic(
context,
ACTION_NAME,
@@ -285,10 +250,7 @@ export class CommandResolver<
};
}
getTrain(
context: RecruitCalcContext<TriggerState>,
crewType?: { armType: number }
): number {
getTrain(context: RecruitCalcContext<TriggerState>, crewType?: { armType: number }): number {
const base = this.env.defaultTrain ?? DEFAULT_TRAIN;
return this.pipeline.onCalcDomestic(
context,
@@ -299,10 +261,7 @@ export class CommandResolver<
);
}
getAtmos(
context: RecruitCalcContext<TriggerState>,
crewType?: { armType: number }
): number {
getAtmos(context: RecruitCalcContext<TriggerState>, crewType?: { armType: number }): number {
const base = this.env.defaultAtmos ?? DEFAULT_ATMOS;
return this.pipeline.onCalcDomestic(
context,
@@ -313,40 +272,26 @@ export class CommandResolver<
);
}
getRecruitPopulation(
context: RecruitCalcContext<TriggerState>,
amount: number
): number {
const base = this.pipeline.onCalcDomestic(
context,
'징집인구',
'score',
amount
);
getRecruitPopulation(context: RecruitCalcContext<TriggerState>, amount: number): number {
const base = this.pipeline.onCalcDomestic(context, '징집인구', 'score', amount);
return Math.round(base);
}
}
export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionResolver<TriggerState, RecruitArgs> {
readonly key = 'che_징병';
// 징병 실행 결과를 계산하고 효과로 변환한다.
private readonly env: RecruitEnvironment;
private readonly command: CommandResolver<TriggerState>;
constructor(
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
env: RecruitEnvironment
) {
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: RecruitEnvironment) {
this.env = env;
this.command = new CommandResolver(modules, env);
}
resolve(
context: RecruitResolveContext<TriggerState>,
args: RecruitArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: RecruitResolveContext<TriggerState>, args: RecruitArgs): GeneralActionOutcome<TriggerState> {
const { general, city } = context;
if (!city) {
context.addLog('도시 정보가 없습니다.');
@@ -376,27 +321,16 @@ export class ActionResolver<
return { effects: [] };
}
const plan = this.command.getCost(
context,
crewType.id,
args.amount,
crewType
);
const plan = this.command.getCost(context, crewType.id, args.amount, crewType);
const setTrain = this.command.getTrain(context, crewType);
const setAtmos = this.command.getAtmos(context, crewType);
const appliedCrew = plan.applied;
const costOffset = this.env.costOffset ?? DEFAULT_COST_OFFSET;
const recruitPop = this.command.getRecruitPopulation(
context,
appliedCrew
);
const recruitPop = this.command.getRecruitPopulation(context, appliedCrew);
const nextPopulation = Math.max(city.population - recruitPop, 0);
const baseTrust = readCityTrust(city, this.env.defaultTrust ?? DEFAULT_TRUST);
const trustLoss =
city.population > 0
? (recruitPop / city.population) / costOffset * 100
: 0;
const trustLoss = city.population > 0 ? (recruitPop / city.population / costOffset) * 100 : 0;
const nextTrust = Math.max(baseTrust - trustLoss, 0);
let nextCrewTypeId = general.crewTypeId;
@@ -409,12 +343,10 @@ export class ActionResolver<
if (crewType.id === general.crewTypeId && general.crew > 0) {
nextCrew = general.crew + appliedCrew;
nextTrain = Math.round(
(general.crew * general.train + appliedCrew * setTrain) /
(general.crew + appliedCrew)
(general.crew * general.train + appliedCrew * setTrain) / (general.crew + appliedCrew)
);
nextAtmos = Math.round(
(general.crew * general.atmos + appliedCrew * setAtmos) /
(general.crew + appliedCrew)
(general.crew * general.atmos + appliedCrew * setAtmos) / (general.crew + appliedCrew)
);
logMessage = `${crewLabel} 추가 ${ACTION_NAME}했습니다.`;
} else {
@@ -433,7 +365,7 @@ export class ActionResolver<
// 직접 수정 (Immer Draft)
city.population = nextPopulation;
city.meta = {
...city.meta as object,
...(city.meta as object),
trust: nextTrust,
};
@@ -454,7 +386,7 @@ export class ActionResolver<
}
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, RecruitArgs, RecruitResolveContext<TriggerState>> {
public readonly key = 'che_징병';
public readonly name = ACTION_NAME;
@@ -462,10 +394,7 @@ export class ActionDefinition<
private readonly resolver: ActionResolver<TriggerState>;
private readonly env: RecruitEnvironment;
constructor(
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
env: RecruitEnvironment
) {
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: RecruitEnvironment) {
this.command = new CommandResolver(modules, env);
this.resolver = new ActionResolver(modules, env);
this.env = env;
@@ -481,10 +410,7 @@ export class ActionDefinition<
return { crewType: crewTypeId, amount };
}
buildConstraints(
ctx: ConstraintContext,
_args: RecruitArgs
): Constraint[] {
buildConstraints(ctx: ConstraintContext, _args: RecruitArgs): Constraint[] {
const requirements: RequirementKey[] = [
{ kind: 'arg', key: 'crewType' },
{ kind: 'arg', key: 'amount' },
@@ -553,9 +479,7 @@ export class ActionDefinition<
if (!availabilityContext) {
return { kind: 'deny', reason: '병종 정보가 없습니다.' };
}
if (
isCrewTypeAvailable(unitSet, crewTypeId, availabilityContext)
) {
if (isCrewTypeAvailable(unitSet, crewTypeId, availabilityContext)) {
return { kind: 'allow' };
}
return { kind: 'deny', reason: '현재 선택할 수 없는 병종입니다.' };
@@ -565,18 +489,11 @@ export class ActionDefinition<
const constraints: Constraint[] = [
notBeNeutral(),
occupiedCity(),
reqCityCapacity(
'population',
'주민',
minPopBase + resolveRequestedCrew(ctx)
),
reqCityCapacity('population', '주민', minPopBase + resolveRequestedCrew(ctx)),
reqCityTrust(20),
reqGeneralGold(getCost, requirements),
reqGeneralRice(getRice, requirements),
reqGeneralCrewMargin(
(context) => resolveCrewTypeId(context.args),
requirements
),
reqGeneralCrewMargin((context) => resolveCrewTypeId(context.args), requirements),
];
if (ctx.mode === 'full') {
@@ -586,10 +503,7 @@ export class ActionDefinition<
return constraints;
}
resolve(
context: RecruitResolveContext<TriggerState>,
args: RecruitArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: RecruitResolveContext<TriggerState>, args: RecruitArgs): GeneralActionOutcome<TriggerState> {
return this.resolver.resolve(context, args);
}
}
@@ -614,6 +528,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
category: '내정',
reqArg: true,
args: {},
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition(env.generalActionModules ?? [], {}),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? [], {}),
};
@@ -1,14 +1,5 @@
import type {
City,
General,
GeneralTriggerState,
Nation,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
StateView,
} from '@sammo-ts/logic/constraints/types.js';
import type { City, General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
import {
existsDestCity,
hasRouteWithEnemy,
@@ -37,22 +28,12 @@ import {
import { JosaUtil, LiteHashDRBG } from '@sammo-ts/common';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import type { GeneralTurnCommandSpec } from './index.js';
import type {
WarAftermathConfig,
WarEngineConfig,
WarTimeContext,
} from '@sammo-ts/logic/war/types.js';
import type { WarAftermathConfig, WarEngineConfig, WarTimeContext } from '@sammo-ts/logic/war/types.js';
import { resolveWarAftermath } from '@sammo-ts/logic/war/aftermath.js';
import { resolveWarBattle } from '@sammo-ts/logic/war/engine.js';
import type { WarActionModule } from '@sammo-ts/logic/war/actions.js';
import {
increaseMetaNumber,
simpleSerialize,
} from '@sammo-ts/logic/war/utils.js';
import type {
MapDefinition,
UnitSetDefinition,
} from '@sammo-ts/logic/world/types.js';
import { increaseMetaNumber, simpleSerialize } from '@sammo-ts/logic/war/utils.js';
import type { MapDefinition, UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
import {
buildWarAftermathConfig,
@@ -65,7 +46,7 @@ export interface DispatchArgs {
}
export interface DispatchResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
destCity: City;
destNation?: Nation | null;
@@ -197,9 +178,7 @@ const pickCandidateCity = (
return { cityId, isEnemy: true, minDist };
}
const fallback = distanceList.get(minDist) ?? [];
const friendly = fallback.filter(
([, nationId]) => nationId === attackerNationId
);
const friendly = fallback.filter(([, nationId]) => nationId === attackerNationId);
if (friendly.length === 0) {
return null;
}
@@ -216,10 +195,7 @@ const getRequiredRice = (ctx: ConstraintContext, view: StateView): number => {
return Math.round(general.crew / 100);
};
const resolveCrewTypeArm = (
unitSet: UnitSetDefinition,
crewTypeId: number
): number | null => {
const resolveCrewTypeArm = (unitSet: UnitSetDefinition, crewTypeId: number): number | null => {
const crewTypes = unitSet.crewTypes ?? [];
const crewType = crewTypes.find((entry) => entry.id === crewTypeId);
if (!crewType) {
@@ -260,13 +236,8 @@ const cloneNation = (nation: Nation): Nation => ({
});
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements
GeneralActionDefinition<
TriggerState,
DispatchArgs,
DispatchResolveContext<TriggerState>
> {
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, DispatchArgs, DispatchResolveContext<TriggerState>> {
public readonly key = 'che_출병';
public readonly name = ACTION_NAME;
private readonly warModules: Array<WarActionModule<TriggerState>>;
@@ -286,10 +257,7 @@ export class ActionDefinition<
buildConstraints(_ctx: ConstraintContext, _args: DispatchArgs): Constraint[] {
const relYear = typeof _ctx.env.relYear === 'number' ? _ctx.env.relYear : 0;
const openingPartYear =
typeof _ctx.env.openingPartYear === 'number'
? _ctx.env.openingPartYear
: 0;
const openingPartYear = typeof _ctx.env.openingPartYear === 'number' ? _ctx.env.openingPartYear : 0;
return [
notOpeningPart(relYear, openingPartYear),
notSameDestCity(),
@@ -304,10 +272,7 @@ export class ActionDefinition<
];
}
resolve(
context: DispatchResolveContext<TriggerState>,
args: DispatchArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: DispatchResolveContext<TriggerState>, args: DispatchArgs): GeneralActionOutcome<TriggerState> {
void args;
const attackerCity = context.city;
if (!attackerCity) {
@@ -322,10 +287,7 @@ export class ActionDefinition<
const unitSet = context.unitSet;
const time = context.time;
const diplomacy = context.diplomacy ?? [];
const allowedNationIds = buildAllowedNationIds(
attackerNation.id,
diplomacy
);
const allowedNationIds = buildAllowedNationIds(attackerNation.id, diplomacy);
const mapIndex = context.map ? buildMapIndex(context.map) : null;
let defenderCityId = finalTargetCity.id;
@@ -345,11 +307,7 @@ export class ActionDefinition<
mapIndex,
allowedCityIds
);
const picked = pickCandidateCity(
context.rng,
distanceList,
attackerNation.id
);
const picked = pickCandidateCity(context.rng, distanceList, attackerNation.id);
if (!picked) {
context.addLog('경로에 도달할 방법이 없습니다.');
return { effects: [] };
@@ -362,15 +320,12 @@ export class ActionDefinition<
const destCity =
defenderCityId === finalTargetCity.id
? finalTargetCity
: context.cities.find((city) => city.id === defenderCityId) ??
finalTargetCity;
: (context.cities.find((city) => city.id === defenderCityId) ?? finalTargetCity);
if (!isEnemyTarget && destCity.nationId === attackerNation.id) {
const josaRo = JosaUtil.pick(destCity.name, '로');
if (finalTargetCity.id === destCity.id) {
context.addLog(
`본국입니다. <G><b>${destCity.name}</b></>${josaRo} 이동합니다.`
);
context.addLog(`본국입니다. <G><b>${destCity.name}</b></>${josaRo} 이동합니다.`);
} else {
const targetName = finalTargetCity.name;
const josaRoTarget = JosaUtil.pick(targetName, '로');
@@ -410,11 +365,7 @@ export class ActionDefinition<
const armType = resolveCrewTypeArm(unitSet, context.general.crewTypeId);
if (armType !== null) {
increaseMetaNumber(
context.general.meta,
`dex${armType}`,
context.general.crew / 100
);
increaseMetaNumber(context.general.meta, `dex${armType}`, context.general.crew / 100);
}
const cities = context.cities.map(cloneCity);
@@ -425,15 +376,10 @@ export class ActionDefinition<
const nationMap = new Map(nations.map((nation) => [nation.id, nation]));
const defenderCity = cityMap.get(destCity.id) ?? cloneCity(destCity);
const defenderNation =
defenderCity.nationId > 0
? nationMap.get(defenderCity.nationId) ?? null
: null;
const defenderNation = defenderCity.nationId > 0 ? (nationMap.get(defenderCity.nationId) ?? null) : null;
const defenderGenerals = generals.filter(
(general) =>
general.cityId === defenderCity.id &&
general.nationId === defenderCity.nationId
(general) => general.cityId === defenderCity.id && general.nationId === defenderCity.nationId
);
const battle = resolveWarBattle({
@@ -556,16 +502,10 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
if (!destCity) {
return null;
}
const destNation =
destCity.nationId > 0
? options.worldRef.getNationById(destCity.nationId)
: null;
const destNation = destCity.nationId > 0 ? options.worldRef.getNationById(destCity.nationId) : null;
const diplomacy = options.worldRef.listDiplomacy();
const warConfig = buildWarConfig(options.scenarioConfig, options.unitSet);
const aftermathConfig = buildWarAftermathConfig(
options.scenarioConfig,
warConfig.castleCrewTypeId
);
const aftermathConfig = buildWarAftermathConfig(options.scenarioConfig, warConfig.castleCrewTypeId);
return {
...base,
destCity,
@@ -588,6 +528,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
category: '군사',
reqArg: true,
args: { destCityId: 0 },
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition(env.warActionModules ?? []),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.warActionModules ?? []),
};
@@ -5,7 +5,7 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
import type { GeneralTurnCommandSpec } from './index.js';
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends CityDevelopmentActionDefinition<TriggerState> {
constructor(env: { develCost?: number; amount?: number } = {}) {
super(
@@ -30,6 +30,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
category: '내정',
reqArg: false,
args: {},
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition({ develCost: env.develCost }),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition({ develCost: env.develCost }),
};
@@ -1,11 +1,5 @@
import type { RandomGenerator } from '@sammo-ts/common';
import type {
City,
General,
GeneralTriggerState,
Nation,
TriggerValue,
} from '@sammo-ts/logic/domain/entities.js';
import type { City, General, GeneralTriggerState, Nation, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import {
disallowDiplomacyBetweenStatus,
@@ -19,10 +13,7 @@ import {
suppliedCity,
} from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import {
GeneralActionPipeline,
type GeneralActionModule,
} from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionEffect,
@@ -30,10 +21,7 @@ import type {
GeneralActionResolveContext,
GeneralActionResolver,
} from '@sammo-ts/logic/actions/engine.js';
import {
createCityPatchEffect,
createGeneralPatchEffect,
} from '@sammo-ts/logic/actions/engine.js';
import { createCityPatchEffect, createGeneralPatchEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
@@ -54,18 +42,12 @@ export interface FireAttackEnvironment {
maxSuccessProbability?: number;
statKey?: 'leadership' | 'strength' | 'intelligence';
getDistance?: (sourceCityId: number, destCityId: number) => number | null;
getDefenceCorrection?: (
context: FireAttackContext,
defender: General
) => number;
getInjuryProbability?: (
context: FireAttackContext,
defender: General
) => number;
getDefenceCorrection?: (context: FireAttackContext, defender: General) => number;
getInjuryProbability?: (context: FireAttackContext, defender: General) => number;
}
export interface FireAttackContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionContext<TriggerState> {
general: General<TriggerState>;
city: City;
@@ -76,16 +58,14 @@ export interface FireAttackContext<
}
export interface FireAttackResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
destCity: City;
destNation?: Nation | null;
destGenerals: General<TriggerState>[];
}
export interface FireAttackResult<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
export interface FireAttackResult<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
success: boolean;
probability: number;
distance: number;
@@ -109,16 +89,9 @@ const DEFAULT_MAX_PROB = 0.5;
const INJURY_MAX = 80;
const CITY_STATE_BURNING = 32;
const randomRangeInt = (
rng: RandomGenerator,
min: number,
max: number
): number => rng.nextInt(min, max + 1);
const randomRangeInt = (rng: RandomGenerator, min: number, max: number): number => rng.nextInt(min, max + 1);
const getStatValue = (
general: General,
statKey: 'leadership' | 'strength' | 'intelligence'
): number => {
const getStatValue = (general: General, statKey: 'leadership' | 'strength' | 'intelligence'): number => {
if (statKey === 'leadership') {
return general.stats.leadership;
}
@@ -138,17 +111,12 @@ const addMetaNumber = (
};
// 화계 성공/실패 및 피해량 계산을 담당한다.
export class CommandResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
private readonly pipeline: GeneralActionPipeline<TriggerState>;
private readonly env: FireAttackEnvironment;
private readonly statKey: 'leadership' | 'strength' | 'intelligence';
constructor(
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
env: FireAttackEnvironment
) {
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: FireAttackEnvironment) {
this.pipeline = new GeneralActionPipeline(modules);
this.env = env;
this.statKey = env.statKey ?? 'intelligence';
@@ -159,22 +127,13 @@ export class CommandResolver<
return { gold: cost, rice: cost };
}
private calcAttackProb(
context: FireAttackContext<TriggerState>
): number {
private calcAttackProb(context: FireAttackContext<TriggerState>): number {
const stat = getStatValue(context.general, this.statKey);
let prob = stat / this.env.sabotageProbCoefByStat;
return this.pipeline.onCalcDomestic(
context,
ACTION_KEY,
'success',
prob
);
const prob = stat / this.env.sabotageProbCoefByStat;
return this.pipeline.onCalcDomestic(context, ACTION_KEY, 'success', prob);
}
private calcDefenceProb(
context: FireAttackContext<TriggerState>
): number {
private calcDefenceProb(context: FireAttackContext<TriggerState>): number {
const destNationId = context.destCity.nationId;
let maxStat = 0;
let probCorrection = 0;
@@ -185,19 +144,13 @@ export class CommandResolver<
continue;
}
affectCount += 1;
maxStat = Math.max(
maxStat,
getStatValue(defender, this.statKey)
);
probCorrection +=
this.env.getDefenceCorrection?.(context, defender) ?? 0;
maxStat = Math.max(maxStat, getStatValue(defender, this.statKey));
probCorrection += this.env.getDefenceCorrection?.(context, defender) ?? 0;
}
let prob = maxStat / this.env.sabotageProbCoefByStat;
prob += probCorrection;
prob +=
(Math.log2(affectCount + 1) - 1.25) *
this.env.sabotageDefenceCoefByGeneralCount;
prob += (Math.log2(affectCount + 1) - 1.25) * this.env.sabotageDefenceCoefByGeneralCount;
prob += context.destCity.security / context.destCity.securityMax / 5;
prob += context.destCity.supplyState ? 0.1 : 0;
@@ -205,25 +158,15 @@ export class CommandResolver<
return prob;
}
resolve(
context: FireAttackContext<TriggerState>,
rng: RandomGenerator
): FireAttackResult<TriggerState> {
resolve(context: FireAttackContext<TriggerState>, rng: RandomGenerator): FireAttackResult<TriggerState> {
const { gold: costGold, rice: costRice } = this.getCost();
const distance =
this.env.getDistance?.(context.general.cityId, context.destCity.id) ??
99;
const distance = this.env.getDistance?.(context.general.cityId, context.destCity.id) ?? 99;
const attackProb = this.calcAttackProb(context);
const defenceProb = this.calcDefenceProb(context);
let probability =
this.env.sabotageDefaultProb + attackProb - defenceProb;
let probability = this.env.sabotageDefaultProb + attackProb - defenceProb;
probability /= distance;
probability = clamp(
probability,
0,
this.env.maxSuccessProbability ?? DEFAULT_MAX_PROB
);
probability = clamp(probability, 0, this.env.maxSuccessProbability ?? DEFAULT_MAX_PROB);
const success = rng.nextBool(probability);
const expRange: [number, number] = success ? [201, 300] : [1, 100];
@@ -248,20 +191,12 @@ export class CommandResolver<
}
const agriDamage = clamp(
randomRangeInt(
rng,
this.env.sabotageDamageMin,
this.env.sabotageDamageMax
),
randomRangeInt(rng, this.env.sabotageDamageMin, this.env.sabotageDamageMax),
0,
context.destCity.agriculture
);
const commDamage = clamp(
randomRangeInt(
rng,
this.env.sabotageDamageMin,
this.env.sabotageDamageMax
),
randomRangeInt(rng, this.env.sabotageDamageMin, this.env.sabotageDamageMax),
0,
context.destCity.commerce
);
@@ -275,9 +210,7 @@ export class CommandResolver<
if (defender.nationId !== context.destCity.nationId) {
continue;
}
const injuryProb =
this.env.getInjuryProbability?.(context, defender) ??
injuryProbDefault;
const injuryProb = this.env.getInjuryProbability?.(context, defender) ?? injuryProbDefault;
if (!rng.nextBool(injuryProb)) {
continue;
}
@@ -285,11 +218,7 @@ export class CommandResolver<
injuredGenerals.push({
id: defender.id,
patch: {
injury: clamp(
defender.injury + injuryAmount,
0,
INJURY_MAX
),
injury: clamp(defender.injury + injuryAmount, 0, INJURY_MAX),
crew: Math.floor(defender.crew * 0.98),
train: Math.floor(defender.train * 0.98),
},
@@ -313,15 +242,12 @@ export class CommandResolver<
}
export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionResolver<TriggerState, FireAttackArgs> {
readonly key = 'che_화계';
private readonly command: CommandResolver<TriggerState>;
constructor(
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
env: FireAttackEnvironment
) {
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: FireAttackEnvironment) {
this.command = new CommandResolver(modules, env);
}
@@ -355,14 +281,8 @@ export class ActionResolver<
const nextExperience = general.experience + result.exp;
const nextDedication = general.dedication + result.dedication;
const metaWithStatExp = addMetaNumber(
general.meta,
STAT_EXP_KEY,
1
);
const metaUpdated = result.success
? addMetaNumber(metaWithStatExp, 'firenum', 1)
: metaWithStatExp;
const metaWithStatExp = addMetaNumber(general.meta, STAT_EXP_KEY, 1);
const metaUpdated = result.success ? addMetaNumber(metaWithStatExp, 'firenum', 1) : metaWithStatExp;
// 직접 수정 (Immer Draft)
general.gold = nextGold;
@@ -372,12 +292,9 @@ export class ActionResolver<
general.meta = metaUpdated;
if (!result.success) {
context.addLog(
`<G><b>${context.destCity.name}</b></>에 ${ACTION_NAME} 실패했습니다.`,
{
format: LogFormat.MONTH,
}
);
context.addLog(`<G><b>${context.destCity.name}</b></>에 ${ACTION_NAME} 실패했습니다.`, {
format: LogFormat.MONTH,
});
return { effects: [] };
}
@@ -398,20 +315,14 @@ export class ActionResolver<
)
);
context.addLog(
`<G><b>${context.destCity.name}</b></>이 불타고 있습니다.`,
{
scope: LogScope.SYSTEM,
category: LogCategory.SUMMARY,
format: LogFormat.MONTH,
}
);
context.addLog(
`<G><b>${context.destCity.name}</b></>에 ${ACTION_NAME} 성공했습니다.`,
{
format: LogFormat.MONTH,
}
);
context.addLog(`<G><b>${context.destCity.name}</b></>이 불타고 있습니다.`, {
scope: LogScope.SYSTEM,
category: LogCategory.SUMMARY,
format: LogFormat.MONTH,
});
context.addLog(`<G><b>${context.destCity.name}</b></>에 ${ACTION_NAME} 성공했습니다.`, {
format: LogFormat.MONTH,
});
context.addLog(
`도시의 농업이 <C>${result.agriDamage}</>, 상업이 <C>${result.commDamage}</>만큼 감소하고, 장수 <C>${result.injuryCount}</>명이 부상 당했습니다.`,
{
@@ -421,16 +332,11 @@ export class ActionResolver<
for (const injured of result.injuredGenerals) {
// 타겟 장수는 Draft가 아니므로 Effect 반환
effects.push(
createGeneralPatchEffect(injured.patch, injured.id)
);
context.addLog(
`<M>${ACTION_KEY}</>로 인해 <R>부상</>을 당했습니다.`,
{
generalId: injured.id,
format: LogFormat.MONTH,
}
);
effects.push(createGeneralPatchEffect(injured.patch, injured.id));
context.addLog(`<M>${ACTION_KEY}</>로 인해 <R>부상</>을 당했습니다.`, {
generalId: injured.id,
format: LogFormat.MONTH,
});
}
return { effects };
@@ -438,17 +344,14 @@ export class ActionResolver<
}
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, FireAttackArgs, FireAttackResolveContext<TriggerState>> {
public readonly key = 'che_화계';
public readonly name = ACTION_NAME;
private readonly command: CommandResolver<TriggerState>;
private readonly resolver: ActionResolver<TriggerState>;
constructor(
modules: Array<GeneralActionModule<TriggerState> | null | undefined>,
env: FireAttackEnvironment
) {
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>, env: FireAttackEnvironment) {
this.command = new CommandResolver(modules, env);
this.resolver = new ActionResolver(modules, env);
}
@@ -464,10 +367,7 @@ export class ActionDefinition<
return { destCityId };
}
buildConstraints(
_ctx: ConstraintContext,
_args: FireAttackArgs
): Constraint[] {
buildConstraints(_ctx: ConstraintContext, _args: FireAttackArgs): Constraint[] {
void _ctx;
void _args;
const { gold, rice } = this.command.getCost();
@@ -486,10 +386,7 @@ export class ActionDefinition<
];
}
resolve(
context: FireAttackResolveContext<TriggerState>,
args: FireAttackArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: FireAttackResolveContext<TriggerState>, args: FireAttackArgs): GeneralActionOutcome<TriggerState> {
return this.resolver.resolve(context, args);
}
}
@@ -502,6 +399,5 @@ export const commandSpec: GeneralTurnCommandSpec = {
category: '계략',
reqArg: true,
args: { destCityId: 0 },
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition(env.generalActionModules ?? [], env),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? [], env),
};
@@ -1,17 +1,8 @@
import type {
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
StateView,
} from '@sammo-ts/logic/constraints/types.js';
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
import { notBeNeutral, reqGeneralGold } from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
import type { GeneralTurnCommandSpec } from './index.js';
@@ -30,7 +21,7 @@ const DEFAULT_TRAIN_DELTA = 5;
const DEFAULT_MAX_TRAIN = 100;
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, TrainingArgs> {
public readonly key = 'che_훈련';
public readonly name = ACTION_NAME;
@@ -45,12 +36,8 @@ export class ActionDefinition<
return {};
}
buildConstraints(
_ctx: ConstraintContext,
_args: TrainingArgs
): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
this.env.costGold ?? 0;
buildConstraints(_ctx: ConstraintContext, _args: TrainingArgs): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.costGold ?? 0;
return [notBeNeutral(), reqGeneralGold(getRequiredGold)];
}
@@ -63,10 +50,7 @@ export class ActionDefinition<
this.env.maxTrainByCommand && this.env.maxTrainByCommand > 0
? this.env.maxTrainByCommand
: DEFAULT_MAX_TRAIN;
const delta =
this.env.trainDelta && this.env.trainDelta > 0
? this.env.trainDelta
: DEFAULT_TRAIN_DELTA;
const delta = this.env.trainDelta && this.env.trainDelta > 0 ? this.env.trainDelta : DEFAULT_TRAIN_DELTA;
const nextTrain = clamp(general.train + delta, 0, maxTrain);
const applied = nextTrain - general.train;
const costGold = this.env.costGold ?? 0;
@@ -1,12 +1,5 @@
import type {
City,
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
StateView,
} from '@sammo-ts/logic/constraints/types.js';
import type { City, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
import {
notBeNeutral,
notWanderingNation,
@@ -16,10 +9,7 @@ import {
suppliedCity,
} from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import { clamp } from 'es-toolkit';
export interface CityDevelopmentArgs {}
@@ -44,7 +34,7 @@ const readNumber = (value: unknown): number | null =>
typeof value === 'number' && Number.isFinite(value) ? value : null;
export class CityDevelopmentActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, CityDevelopmentArgs> {
public readonly key: string;
public readonly name: string;
@@ -63,23 +53,15 @@ export class CityDevelopmentActionDefinition<
return {};
}
buildConstraints(
_ctx: ConstraintContext,
_args: CityDevelopmentArgs
): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number =>
this.env.develCost ?? 0;
buildConstraints(_ctx: ConstraintContext, _args: CityDevelopmentArgs): Constraint[] {
const getRequiredGold = (_context: ConstraintContext, _view: StateView): number => this.env.develCost ?? 0;
return [
notBeNeutral(),
notWanderingNation(),
occupiedCity(),
suppliedCity(),
remainCityCapacityByMax(
this.config.statKey,
this.config.maxKey,
this.config.label
),
remainCityCapacityByMax(this.config.statKey, this.config.maxKey, this.config.label),
reqGeneralGold(getRequiredGold),
];
}
@@ -27,21 +27,15 @@ export const GENERAL_TURN_COMMAND_KEYS = [
'휴식',
] as const;
export type GeneralTurnCommandKey =
(typeof GENERAL_TURN_COMMAND_KEYS)[number];
export type GeneralTurnCommandKey = (typeof GENERAL_TURN_COMMAND_KEYS)[number];
export type GeneralTurnCommandSpec =
TurnCommandSpecBase<GeneralTurnCommandKey>;
export type GeneralTurnCommandSpec = TurnCommandSpecBase<GeneralTurnCommandKey>;
export type GeneralTurnCommandModule =
TurnCommandModule<GeneralTurnCommandSpec>;
export type GeneralTurnCommandModule = TurnCommandModule<GeneralTurnCommandSpec>;
export type GeneralTurnCommandImporter = () => Promise<GeneralTurnCommandModule>;
const defaultImporters: Record<
GeneralTurnCommandKey,
GeneralTurnCommandImporter
> = {
const defaultImporters: Record<GeneralTurnCommandKey, GeneralTurnCommandImporter> = {
che_거병: async () => import('./che_거병.js'),
che_임관: async () => import('./che_임관.js'),
che_건국: async () => import('./che_건국.js'),
@@ -68,28 +62,17 @@ const defaultImporters: Record<
휴식: async () => import('./휴식.js'),
};
export const isGeneralTurnCommandKey = (
value: string
): value is GeneralTurnCommandKey =>
export const isGeneralTurnCommandKey = (value: string): value is GeneralTurnCommandKey =>
GENERAL_TURN_COMMAND_KEYS.includes(value as GeneralTurnCommandKey);
export class GeneralTurnCommandLoader {
private readonly cache = new Map<
GeneralTurnCommandKey,
Promise<GeneralTurnCommandModule>
>();
private readonly cache = new Map<GeneralTurnCommandKey, Promise<GeneralTurnCommandModule>>();
constructor(
private readonly importers: Record<
GeneralTurnCommandKey,
GeneralTurnCommandImporter
> = defaultImporters
) { }
private readonly importers: Record<GeneralTurnCommandKey, GeneralTurnCommandImporter> = defaultImporters
) {}
async load(
key: GeneralTurnCommandKey
): Promise<GeneralTurnCommandModule> {
async load(key: GeneralTurnCommandKey): Promise<GeneralTurnCommandModule> {
const cached = this.cache.get(key);
if (cached) {
return cached;
@@ -6,9 +6,7 @@ import type {
TriggerValue,
} from '@sammo-ts/logic/domain/entities.js';
export interface GeneralRecruitmentInput<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
export interface GeneralRecruitmentInput<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
id: number;
name: string;
nationId: number;
@@ -52,9 +50,7 @@ const createEmptyRole = (): GeneralRole => ({
});
// 모집/탐색 등으로 생성되는 장수의 기본 모델을 구성한다.
export const buildRecruitmentGeneral = <
TriggerState extends GeneralTriggerState = GeneralTriggerState
>(
export const buildRecruitmentGeneral = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(
input: GeneralRecruitmentInput<TriggerState>
): General<TriggerState> => ({
id: input.id,
@@ -79,8 +75,6 @@ export const buildRecruitmentGeneral = <
atmos: input.atmos ?? 0,
age: input.age,
npcState: input.npcState,
triggerState:
input.triggerState ??
(createEmptyTriggerState() as TriggerState),
triggerState: input.triggerState ?? (createEmptyTriggerState() as TriggerState),
meta: input.meta ?? {},
});
@@ -1,10 +1,5 @@
import type {
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
} from '@sammo-ts/logic/constraints/types.js';
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
@@ -21,14 +16,11 @@ export interface RestArgs {}
const ACTION_NAME = '휴식';
export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionResolver<TriggerState, RestArgs> {
readonly key = '휴식';
resolve(
context: GeneralActionResolveContext<TriggerState>,
_args: RestArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: GeneralActionResolveContext<TriggerState>, _args: RestArgs): GeneralActionOutcome<TriggerState> {
context.addLog('아무것도 실행하지 않았습니다.', {
category: LogCategory.ACTION,
format: LogFormat.MONTH,
@@ -38,7 +30,7 @@ export class ActionResolver<
}
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, RestArgs> {
public readonly key = '휴식';
public readonly name = ACTION_NAME;
@@ -49,19 +41,13 @@ export class ActionDefinition<
return {};
}
buildConstraints(
_ctx: ConstraintContext,
_args: RestArgs
): Constraint[] {
buildConstraints(_ctx: ConstraintContext, _args: RestArgs): Constraint[] {
void _ctx;
void _args;
return [];
}
resolve(
context: GeneralActionResolveContext<TriggerState>,
args: RestArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: GeneralActionResolveContext<TriggerState>, args: RestArgs): GeneralActionOutcome<TriggerState> {
return this.resolver.resolve(context, args);
}
}
@@ -1,8 +1,4 @@
import type {
General,
GeneralTriggerState,
Nation,
} from '@sammo-ts/logic/domain/entities.js';
import type { General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import {
allowDiplomacyWithTerm,
@@ -11,10 +7,7 @@ import {
existsDestNation,
occupiedCity,
} from '@sammo-ts/logic/constraints/presets.js';
import {
GeneralActionPipeline,
type GeneralActionModule,
} from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionEffect,
@@ -22,10 +15,7 @@ import type {
GeneralActionResolveContext,
GeneralActionResolver,
} from '@sammo-ts/logic/actions/engine.js';
import {
createDiplomacyPatchEffect,
createLogEffect,
} from '@sammo-ts/logic/actions/engine.js';
import { createDiplomacyPatchEffect, createLogEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
@@ -38,7 +28,7 @@ export interface RaidArgs {
}
export interface RaidResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
destNation: Nation;
diplomacy: { state: number; term: number };
@@ -62,9 +52,7 @@ const parseNationId = (raw: unknown): number | null => {
};
// 급습 쿨타임 계산을 담당한다.
export class CommandResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
private readonly pipeline: GeneralActionPipeline<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
@@ -72,20 +60,13 @@ export class CommandResolver<
}
getGlobalDelay(context: RaidResolveContext<TriggerState>): number {
return Math.round(
this.pipeline.onCalcStrategic(
context,
ACTION_NAME,
'globalDelay',
DEFAULT_GLOBAL_DELAY
)
);
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
}
}
// 급습 실행 결과를 계산한다.
export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionResolver<TriggerState, RaidArgs> {
readonly key = 'che_급습';
private readonly command: CommandResolver<TriggerState>;
@@ -94,10 +75,7 @@ export class ActionResolver<
this.command = new CommandResolver(modules);
}
resolve(
context: RaidResolveContext<TriggerState>,
_args: RaidArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: RaidResolveContext<TriggerState>, _args: RaidArgs): GeneralActionOutcome<TriggerState> {
void _args;
const { general, nation } = context;
const generalName = general.name;
@@ -111,29 +89,18 @@ export class ActionResolver<
general.dedication += EXP_DED_GAIN;
context.addLog(`${ACTION_NAME} 발동!`, { format: LogFormat.MONTH });
context.addLog(
`<D><b>${destNationName}</b></>에 <M>${ACTION_NAME}</>${actionJosa} 발동`,
{
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
}
);
context.addLog(`<D><b>${destNationName}</b></>에 <M>${ACTION_NAME}</>${actionJosa} 발동`, {
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
});
const effects: Array<GeneralActionEffect<TriggerState>> = [
createDiplomacyPatchEffect(
general.nationId,
context.destNation.id,
{
term: context.diplomacy.term - TERM_REDUCE,
}
),
createDiplomacyPatchEffect(
context.destNation.id,
general.nationId,
{
term: context.reverseDiplomacy.term - TERM_REDUCE,
}
),
createDiplomacyPatchEffect(general.nationId, context.destNation.id, {
term: context.diplomacy.term - TERM_REDUCE,
}),
createDiplomacyPatchEffect(context.destNation.id, general.nationId, {
term: context.reverseDiplomacy.term - TERM_REDUCE,
}),
];
for (const target of context.friendlyGenerals) {
@@ -195,12 +162,8 @@ export class ActionResolver<
// 급습 실행을 위한 정의/제약을 구성한다.
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<
TriggerState,
RaidArgs,
RaidResolveContext<TriggerState>
> {
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, RaidArgs, RaidResolveContext<TriggerState>> {
public readonly key = 'che_급습';
public readonly name = ACTION_NAME;
private readonly resolver: ActionResolver<TriggerState>;
@@ -225,19 +188,12 @@ export class ActionDefinition<
occupiedCity(),
beChief(),
existsDestNation(),
allowDiplomacyWithTerm(
1,
12,
'선포 12개월 이상인 상대국에만 가능합니다.'
),
allowDiplomacyWithTerm(1, 12, '선포 12개월 이상인 상대국에만 가능합니다.'),
availableStrategicCommand(),
];
}
resolve(
context: RaidResolveContext<TriggerState>,
args: RaidArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: RaidResolveContext<TriggerState>, args: RaidArgs): GeneralActionOutcome<TriggerState> {
return this.resolver.resolve(context, args);
}
}
@@ -263,12 +219,8 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
worldRef.getDiplomacyEntry(destNationId, base.general.nationId) ??
buildDefaultDiplomacy(destNationId, base.general.nationId);
const generals = worldRef.listGenerals();
const friendlyGenerals = generals.filter(
(general) => general.nationId === base.general.nationId
);
const destNationGenerals = generals.filter(
(general) => general.nationId === destNationId
);
const friendlyGenerals = generals.filter((general) => general.nationId === base.general.nationId);
const destNationGenerals = generals.filter((general) => general.nationId === destNationId);
return {
...base,
destNation,
@@ -284,6 +236,5 @@ export const commandSpec: NationTurnCommandSpec = {
category: '외교',
reqArg: true,
args: { destNationId: 0 },
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition(env.generalActionModules ?? []),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
};
@@ -1,13 +1,5 @@
import type {
City,
General,
GeneralTriggerState,
TriggerValue,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
} from '@sammo-ts/logic/constraints/types.js';
import type { City, General, GeneralTriggerState, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import {
alwaysFail,
beChief,
@@ -40,7 +32,7 @@ export interface AssignmentArgs {
}
export interface AssignmentResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
destGeneral: General<TriggerState>;
destCity: City;
@@ -57,17 +49,14 @@ export interface AssignmentEnvironment {
const ACTION_NAME = '발령';
const joinYearMonth = (year: number, month: number): number =>
year * 12 + month - 1;
const joinYearMonth = (year: number, month: number): number => year * 12 + month - 1;
const cutTurn = (time: Date, turnTermMinutes: number): number => {
const turnMs = turnTermMinutes * 60 * 1000;
return Math.floor(time.getTime() / turnMs);
};
const resolveLastAssignment = (
context: AssignmentResolveContext
): number => {
const resolveLastAssignment = (context: AssignmentResolveContext): number => {
let yearMonth = joinYearMonth(context.currentYear, context.currentMonth);
const term = context.turnTermMinutes;
const srcTime = context.generalTurnTime;
@@ -91,7 +80,7 @@ const addMetaValue = (
// 발령 결과를 계산한다.
export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionResolver<TriggerState, AssignmentArgs> {
readonly key = 'che_발령';
private readonly env: AssignmentEnvironment;
@@ -107,9 +96,7 @@ export class ActionResolver<
void _args;
const destGeneral = context.destGeneral;
const destCity = context.destCity;
const cityName = this.env.formatCityName
? this.env.formatCityName(destCity)
: destCity.name;
const cityName = this.env.formatCityName ? this.env.formatCityName(destCity) : destCity.name;
const cityJosa = JosaUtil.pick(cityName, '로');
const generalJosa = JosaUtil.pick(destGeneral.name, '을');
const yearMonth = resolveLastAssignment(context);
@@ -125,15 +112,12 @@ export class ActionResolver<
];
effects.push(
createLogEffect(
`<Y>${context.general.name}</>에 의해 <G><b>${cityName}</b></>${cityJosa} 발령됐습니다.`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
generalId: destGeneral.id,
format: LogFormat.MONTH,
}
)
createLogEffect(`<Y>${context.general.name}</>에 의해 <G><b>${cityName}</b></>${cityJosa} 발령됐습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
generalId: destGeneral.id,
format: LogFormat.MONTH,
})
);
effects.push(
createLogEffect(
@@ -151,12 +135,8 @@ export class ActionResolver<
}
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<
TriggerState,
AssignmentArgs,
AssignmentResolveContext<TriggerState>
> {
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, AssignmentArgs, AssignmentResolveContext<TriggerState>> {
public readonly key = 'che_발령';
public readonly name = ACTION_NAME;
private readonly resolver: ActionResolver<TriggerState>;
@@ -185,10 +165,7 @@ export class ActionDefinition<
};
}
buildConstraints(
ctx: ConstraintContext,
_args: AssignmentArgs
): Constraint[] {
buildConstraints(ctx: ConstraintContext, _args: AssignmentArgs): Constraint[] {
void _args;
if (ctx.destGeneralId === ctx.actorId) {
return [alwaysFail('본인입니다')];
@@ -205,10 +182,7 @@ export class ActionDefinition<
];
}
resolve(
context: AssignmentResolveContext<TriggerState>,
args: AssignmentArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: AssignmentResolveContext<TriggerState>, args: AssignmentArgs): GeneralActionOutcome<TriggerState> {
return this.resolver.resolve(context, args);
}
}
@@ -1,8 +1,4 @@
import type {
City,
General,
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type { City, General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import {
availableStrategicCommand,
@@ -10,10 +6,7 @@ import {
occupiedCity,
occupiedDestCity,
} from '@sammo-ts/logic/constraints/presets.js';
import {
GeneralActionPipeline,
type GeneralActionModule,
} from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionEffect,
@@ -33,7 +26,7 @@ export interface MobilizePeopleArgs {
}
export interface MobilizePeopleResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
destCity: City;
friendlyGenerals: Array<General<TriggerState>>;
@@ -54,9 +47,7 @@ const parseCityId = (raw: unknown): number | null => {
};
// 백성동원 쿨타임 계산을 담당한다.
export class CommandResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
private readonly pipeline: GeneralActionPipeline<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
@@ -64,20 +55,13 @@ export class CommandResolver<
}
getGlobalDelay(context: MobilizePeopleResolveContext<TriggerState>): number {
return Math.round(
this.pipeline.onCalcStrategic(
context,
ACTION_NAME,
'globalDelay',
DEFAULT_GLOBAL_DELAY
)
);
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
}
}
// 백성동원 실행 결과를 계산한다.
export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionResolver<TriggerState, MobilizePeopleArgs> {
readonly key = 'che_백성동원';
private readonly command: CommandResolver<TriggerState>;
@@ -101,13 +85,10 @@ export class ActionResolver<
general.dedication += EXP_DED_GAIN;
context.addLog(`${ACTION_NAME} 발동!`, { format: LogFormat.MONTH });
context.addLog(
`<G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>을 발동`,
{
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
}
);
context.addLog(`<G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>을 발동`, {
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
});
const effects: Array<GeneralActionEffect<TriggerState>> = [];
@@ -125,14 +106,8 @@ export class ActionResolver<
);
}
const nextDefence = Math.max(
context.destCity.defence,
context.destCity.defenceMax * DEFENCE_RATE
);
const nextWall = Math.max(
context.destCity.wall,
context.destCity.wallMax * DEFENCE_RATE
);
const nextDefence = Math.max(context.destCity.defence, context.destCity.defenceMax * DEFENCE_RATE);
const nextWall = Math.max(context.destCity.wall, context.destCity.wallMax * DEFENCE_RATE);
effects.push(
createCityPatchEffect(
{
@@ -165,12 +140,8 @@ export class ActionResolver<
// 백성동원 실행을 위한 정의/제약을 구성한다.
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<
TriggerState,
MobilizePeopleArgs,
MobilizePeopleResolveContext<TriggerState>
> {
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, MobilizePeopleArgs, MobilizePeopleResolveContext<TriggerState>> {
public readonly key = 'che_백성동원';
public readonly name = ACTION_NAME;
private readonly resolver: ActionResolver<TriggerState>;
@@ -188,18 +159,10 @@ export class ActionDefinition<
return { destCityId };
}
buildConstraints(
_ctx: ConstraintContext,
_args: MobilizePeopleArgs
): Constraint[] {
buildConstraints(_ctx: ConstraintContext, _args: MobilizePeopleArgs): Constraint[] {
void _ctx;
void _args;
return [
occupiedCity(),
beChief(),
occupiedDestCity(),
availableStrategicCommand(),
];
return [occupiedCity(), beChief(), occupiedDestCity(), availableStrategicCommand()];
}
resolve(
@@ -224,9 +187,7 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
if (!destCity) {
return null;
}
const friendlyGenerals = worldRef
.listGenerals()
.filter((general) => general.nationId === base.general.nationId);
const friendlyGenerals = worldRef.listGenerals().filter((general) => general.nationId === base.general.nationId);
return {
...base,
destCity,
@@ -239,6 +200,5 @@ export const commandSpec: NationTurnCommandSpec = {
category: '전략',
reqArg: true,
args: { destCityId: 0 },
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition(env.generalActionModules ?? []),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
};
@@ -1,11 +1,5 @@
import type {
General,
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
} from '@sammo-ts/logic/constraints/types.js';
import type { General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import {
alwaysFail,
beChief,
@@ -19,10 +13,7 @@ import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import {
createGeneralPatchEffect,
createLogEffect,
} from '@sammo-ts/logic/actions/engine.js';
import { createGeneralPatchEffect, createLogEffect } from '@sammo-ts/logic/actions/engine.js';
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import type { NationTurnCommandSpec } from './index.js';
@@ -34,7 +25,7 @@ export interface TroopKickArgs {
}
export interface TroopKickResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
destGeneral: General<TriggerState>;
}
@@ -42,12 +33,8 @@ export interface TroopKickResolveContext<
const ACTION_NAME = '부대 탈퇴 지시';
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<
TriggerState,
TroopKickArgs,
TroopKickResolveContext<TriggerState>
> {
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, TroopKickArgs, TroopKickResolveContext<TriggerState>> {
public readonly key = 'che_부대탈퇴지시';
public readonly name = ACTION_NAME;
@@ -65,25 +52,14 @@ export class ActionDefinition<
return { destGeneralId: data.destGeneralId };
}
buildConstraints(
ctx: ConstraintContext,
_args: TroopKickArgs
): Constraint[] {
buildConstraints(ctx: ConstraintContext, _args: TroopKickArgs): Constraint[] {
if (ctx.destGeneralId !== undefined && ctx.destGeneralId === ctx.actorId) {
return [alwaysFail('본인입니다')];
}
return [
notBeNeutral(),
beChief(),
existsDestGeneral(),
friendlyDestGeneral(),
];
return [notBeNeutral(), beChief(), existsDestGeneral(), friendlyDestGeneral()];
}
resolve(
context: TroopKickResolveContext<TriggerState>,
_args: TroopKickArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: TroopKickResolveContext<TriggerState>, _args: TroopKickArgs): GeneralActionOutcome<TriggerState> {
const general = context.general;
const destGeneral = context.destGeneral;
const destGeneralName = destGeneral.name;
@@ -91,46 +67,31 @@ export class ActionDefinition<
const effects: Array<GeneralActionEffect<TriggerState>> = [];
if (destGeneral.troopId === 0) {
context.addLog(
`<Y>${destGeneralName}</>${josaUn} 부대원이 아닙니다.`
);
context.addLog(`<Y>${destGeneralName}</>${josaUn} 부대원이 아닙니다.`);
return { effects };
}
if (destGeneral.troopId === destGeneral.id) {
context.addLog(
`<Y>${destGeneralName}</>${josaUn} 부대장입니다.`
);
context.addLog(`<Y>${destGeneralName}</>${josaUn} 부대장입니다.`);
return { effects };
}
effects.push(
createGeneralPatchEffect(
{ troopId: 0 } as Partial<General<TriggerState>>,
destGeneral.id
)
);
effects.push(createGeneralPatchEffect({ troopId: 0 } as Partial<General<TriggerState>>, destGeneral.id));
effects.push(
createLogEffect(
`<Y>${destGeneralName}</>에게 부대 탈퇴를 지시했습니다.`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}
)
createLogEffect(`<Y>${destGeneralName}</>에게 부대 탈퇴를 지시했습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
})
);
effects.push(
createLogEffect(
`<Y>${general.name}</>에게 부대 탈퇴를 지시 받았습니다.`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
generalId: destGeneral.id,
}
)
createLogEffect(`<Y>${general.name}</>에게 부대 탈퇴를 지시 받았습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.PLAIN,
generalId: destGeneral.id,
})
);
return { effects };
@@ -9,10 +9,7 @@ import {
} from '@sammo-ts/logic/constraints/presets.js';
import { allow, unknownOrDeny } from '@sammo-ts/logic/constraints/helpers.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import { createLogEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
@@ -50,8 +47,7 @@ const parseMonth = (raw: unknown): number | null => {
return month >= 1 && month <= 12 ? month : null;
};
const resolveMonthIndex = (year: number, month: number): number =>
year * 12 + month - 1;
const resolveMonthIndex = (year: number, month: number): number => year * 12 + month - 1;
const requireMinimumTerm = (minMonths: number): Constraint => ({
name: 'RequireNonAggressionMinimumTerm',
@@ -65,8 +61,7 @@ const requireMinimumTerm = (minMonths: number): Constraint => ({
const yearValue = typeof ctx.args.year === 'number' ? ctx.args.year : null;
const monthValue = typeof ctx.args.month === 'number' ? ctx.args.month : null;
const envYearValue = typeof ctx.env.year === 'number' ? ctx.env.year : null;
const envMonthValue =
typeof ctx.env.month === 'number' ? ctx.env.month : null;
const envMonthValue = typeof ctx.env.month === 'number' ? ctx.env.month : null;
const missing = [];
if (yearValue === null) {
@@ -106,7 +101,7 @@ const requireMinimumTerm = (minMonths: number): Constraint => ({
// 불가침 제의를 처리하는 국가 커맨드.
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, NonAggressionProposalArgs> {
public readonly key = 'che_불가침제의';
public readonly name = ACTION_NAME;
@@ -126,10 +121,7 @@ export class ActionDefinition<
return { destNationId, year, month };
}
buildConstraints(
_ctx: ConstraintContext,
_args: NonAggressionProposalArgs
): Constraint[] {
buildConstraints(_ctx: ConstraintContext, _args: NonAggressionProposalArgs): Constraint[] {
return [
beChief(),
notBeNeutral(),
@@ -149,14 +141,11 @@ export class ActionDefinition<
): GeneralActionOutcome<TriggerState> {
return {
effects: [
createLogEffect(
`${ACTION_NAME}을 준비했습니다. (국가 ${args.destNationId})`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}
),
createLogEffect(`${ACTION_NAME}을 준비했습니다. (국가 ${args.destNationId})`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
}
@@ -9,10 +9,7 @@ import {
suppliedCity,
} from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import { createLogEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
@@ -35,10 +32,8 @@ const parseNationId = (raw: unknown): number | null => {
// 불가침 파기 제의를 처리하는 국가 커맨드.
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements
GeneralActionDefinition<TriggerState, NonAggressionCancelProposalArgs>
{
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, NonAggressionCancelProposalArgs> {
public readonly key = 'che_불가침파기제의';
public readonly name = ACTION_NAME;
@@ -51,20 +46,14 @@ export class ActionDefinition<
return { destNationId };
}
buildConstraints(
_ctx: ConstraintContext,
_args: NonAggressionCancelProposalArgs
): Constraint[] {
buildConstraints(_ctx: ConstraintContext, _args: NonAggressionCancelProposalArgs): Constraint[] {
return [
beChief(),
notBeNeutral(),
occupiedCity(),
suppliedCity(),
existsDestNation(),
allowDiplomacyBetweenStatus(
[DIPLOMACY_NON_AGGRESSION],
'불가침 중인 상대국에게만 가능합니다.'
),
allowDiplomacyBetweenStatus([DIPLOMACY_NON_AGGRESSION], '불가침 중인 상대국에게만 가능합니다.'),
];
}
@@ -74,14 +63,11 @@ export class ActionDefinition<
): GeneralActionOutcome<TriggerState> {
return {
effects: [
createLogEffect(
`${ACTION_NAME}을 준비했습니다. (국가 ${args.destNationId})`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}
),
createLogEffect(`${ACTION_NAME}을 준비했습니다. (국가 ${args.destNationId})`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
}
@@ -9,10 +9,7 @@ import {
suppliedCity,
} from '@sammo-ts/logic/constraints/presets.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
GeneralActionResolveContext,
} from '@sammo-ts/logic/actions/engine.js';
import type { GeneralActionOutcome, GeneralActionResolveContext } from '@sammo-ts/logic/actions/engine.js';
import { createDiplomacyPatchEffect, createLogEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
@@ -36,7 +33,7 @@ const parseNationId = (raw: unknown): number | null => {
};
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, DeclareWarArgs> {
public readonly key = 'che_선전포고';
public readonly name = ACTION_NAME;
@@ -73,14 +70,11 @@ export class ActionDefinition<
if (nationId === undefined || nationId <= 0) {
return {
effects: [
createLogEffect(
`${ACTION_NAME}을 준비했지만 국가 정보가 없습니다.`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}
),
createLogEffect(`${ACTION_NAME}을 준비했지만 국가 정보가 없습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
}
@@ -94,14 +88,11 @@ export class ActionDefinition<
state: DIPLOMACY_DECLARE,
term: DECLARE_TERM,
}),
createLogEffect(
`${ACTION_NAME}을 실행했습니다. (국가 ${args.destNationId})`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}
),
createLogEffect(`${ACTION_NAME}을 실행했습니다. (국가 ${args.destNationId})`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}),
],
};
}
@@ -1,9 +1,4 @@
import type {
City,
General,
GeneralTriggerState,
Nation,
} from '@sammo-ts/logic/domain/entities.js';
import type { City, General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import {
allowDiplomacyBetweenStatus,
@@ -13,10 +8,7 @@ import {
notOccupiedDestCity,
occupiedCity,
} from '@sammo-ts/logic/constraints/presets.js';
import {
GeneralActionPipeline,
type GeneralActionModule,
} from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionEffect,
@@ -36,7 +28,7 @@ export interface FloodArgs {
}
export interface FloodResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
destCity: City;
destNation: Nation | null;
@@ -59,9 +51,7 @@ const parseCityId = (raw: unknown): number | null => {
};
// 수몰 쿨타임 계산을 담당한다.
export class CommandResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
private readonly pipeline: GeneralActionPipeline<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
@@ -69,20 +59,13 @@ export class CommandResolver<
}
getGlobalDelay(context: FloodResolveContext<TriggerState>): number {
return Math.round(
this.pipeline.onCalcStrategic(
context,
ACTION_NAME,
'globalDelay',
DEFAULT_GLOBAL_DELAY
)
);
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
}
}
// 수몰 실행 결과를 계산한다.
export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionResolver<TriggerState, FloodArgs> {
readonly key = 'che_수몰';
private readonly command: CommandResolver<TriggerState>;
@@ -91,10 +74,7 @@ export class ActionResolver<
this.command = new CommandResolver(modules);
}
resolve(
context: FloodResolveContext<TriggerState>,
_args: FloodArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: FloodResolveContext<TriggerState>, _args: FloodArgs): GeneralActionOutcome<TriggerState> {
void _args;
const { general, nation } = context;
const generalName = general.name;
@@ -107,13 +87,10 @@ export class ActionResolver<
general.dedication += EXP_DED_GAIN;
context.addLog(`${ACTION_NAME} 발동!`, { format: LogFormat.MONTH });
context.addLog(
`<G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>을 발동`,
{
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
}
);
context.addLog(`<G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>을 발동`, {
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
});
const effects: Array<GeneralActionEffect<TriggerState>> = [];
@@ -188,12 +165,8 @@ export class ActionResolver<
// 수몰 실행을 위한 정의/제약을 구성한다.
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<
TriggerState,
FloodArgs,
FloodResolveContext<TriggerState>
> {
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, FloodArgs, FloodResolveContext<TriggerState>> {
public readonly key = 'che_수몰';
public readonly name = ACTION_NAME;
private readonly resolver: ActionResolver<TriggerState>;
@@ -211,10 +184,7 @@ export class ActionDefinition<
return { destCityId };
}
buildConstraints(
_ctx: ConstraintContext,
_args: FloodArgs
): Constraint[] {
buildConstraints(_ctx: ConstraintContext, _args: FloodArgs): Constraint[] {
void _ctx;
void _args;
return [
@@ -227,10 +197,7 @@ export class ActionDefinition<
];
}
resolve(
context: FloodResolveContext<TriggerState>,
args: FloodArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: FloodResolveContext<TriggerState>, args: FloodArgs): GeneralActionOutcome<TriggerState> {
return this.resolver.resolve(context, args);
}
}
@@ -251,12 +218,8 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
}
const destNation = worldRef.getNationById(destCity.nationId);
const generals = worldRef.listGenerals();
const friendlyGenerals = generals.filter(
(general) => general.nationId === base.general.nationId
);
const destNationGenerals = generals.filter(
(general) => general.nationId === destCity.nationId
);
const friendlyGenerals = generals.filter((general) => general.nationId === base.general.nationId);
const destNationGenerals = generals.filter((general) => general.nationId === destCity.nationId);
return {
...base,
destCity,
@@ -271,6 +234,5 @@ export const commandSpec: NationTurnCommandSpec = {
category: '전략',
reqArg: true,
args: { destCityId: 0 },
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition(env.generalActionModules ?? []),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
};
@@ -1,13 +1,6 @@
import type { RandomGenerator } from '@sammo-ts/common';
import type {
GeneralTriggerState,
StatBlock,
TriggerValue,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
} from '@sammo-ts/logic/constraints/types.js';
import type { GeneralTriggerState, StatBlock, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import {
availableStrategicCommand,
beChief,
@@ -15,10 +8,7 @@ import {
notOpeningPart,
occupiedCity,
} from '@sammo-ts/logic/constraints/presets.js';
import {
GeneralActionPipeline,
type GeneralActionModule,
} from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionEffect,
@@ -26,9 +16,7 @@ import type {
GeneralActionResolveContext,
GeneralActionResolver,
} from '@sammo-ts/logic/actions/engine.js';
import {
createGeneralAddEffect,
} from '@sammo-ts/logic/actions/engine.js';
import { createGeneralAddEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
import { buildRecruitmentGeneral } from '@sammo-ts/logic/actions/turn/general/recruitment.js';
import { JosaUtil } from '@sammo-ts/common';
@@ -55,7 +43,7 @@ export interface VolunteerRecruitCandidate {
}
export interface VolunteerRecruitResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
currentYear: number;
startYear: number;
@@ -83,10 +71,7 @@ export interface VolunteerRecruitEnvironment {
killTurnMin?: number;
killTurnMax?: number;
decorateName?: (name: string, npcState: number) => string;
pickCandidate?: (
context: VolunteerRecruitResolveContext,
rng: RandomGenerator
) => VolunteerRecruitCandidate | null;
pickCandidate?: (context: VolunteerRecruitResolveContext, rng: RandomGenerator) => VolunteerRecruitCandidate | null;
buildStats?: (
context: VolunteerRecruitResolveContext,
rng: RandomGenerator,
@@ -117,19 +102,12 @@ const addMetaValue = (
meta[key] = value;
};
const readMetaNumber = (
meta: Record<string, TriggerValue>,
key: string
): number | null => {
const readMetaNumber = (meta: Record<string, TriggerValue>, key: string): number | null => {
const value = meta[key];
return typeof value === 'number' ? value : null;
};
const randomRangeInt = (
rng: RandomGenerator,
min: number,
max: number
): number => rng.nextInt(min, max + 1);
const randomRangeInt = (rng: RandomGenerator, min: number, max: number): number => rng.nextInt(min, max + 1);
const resolveRelYear = (ctx: ConstraintContext): number => {
const relYear = ctx.env.relYear;
@@ -173,8 +151,7 @@ const resolveStats = (
if (env.buildStats) {
return env.buildStats(context, rng, candidate);
}
const fallback =
context.nationAverageStats ?? context.general.stats;
const fallback = context.nationAverageStats ?? context.general.stats;
return {
leadership: candidate.stats?.leadership ?? fallback.leadership,
strength: candidate.stats?.strength ?? fallback.strength,
@@ -183,9 +160,7 @@ const resolveStats = (
};
// 의병모집 쿨타임/인원 계산을 제공한다.
export class CommandResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
private readonly pipeline: GeneralActionPipeline<TriggerState>;
private readonly env: VolunteerRecruitEnvironment;
@@ -197,34 +172,15 @@ export class CommandResolver<
this.env = env;
}
getPostDelay(
context: VolunteerRecruitResolveContext<TriggerState>,
gennum: number
): number {
getPostDelay(context: VolunteerRecruitResolveContext<TriggerState>, gennum: number): number {
const fitted = Math.max(gennum, this.env.initialNationGenLimit);
const base = Math.round(Math.sqrt(fitted * 10) * 10);
return Math.round(
this.pipeline.onCalcStrategic(
context,
ACTION_NAME,
'delay',
base
)
);
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'delay', base));
}
getGlobalDelay(
context: VolunteerRecruitResolveContext<TriggerState>
): number {
getGlobalDelay(context: VolunteerRecruitResolveContext<TriggerState>): number {
const base = this.env.globalDelayBase ?? DEFAULT_GLOBAL_DELAY;
return Math.round(
this.pipeline.onCalcStrategic(
context,
ACTION_NAME,
'globalDelay',
base
)
);
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', base));
}
getCreateCount(avgNationGenCount: number): number {
@@ -236,7 +192,7 @@ export class CommandResolver<
// 의병모집 실행 결과를 계산한다.
export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionResolver<TriggerState, VolunteerRecruitArgs> {
readonly key = 'che_의병모집';
private readonly env: VolunteerRecruitEnvironment;
@@ -275,35 +231,24 @@ export class ActionResolver<
const generalName = general.name;
const generalJosa = JosaUtil.pick(generalName, '이');
const actionJosa = JosaUtil.pick(ACTION_NAME, '을');
context.addLog(
`<Y>${generalName}</>${generalJosa} <M>${ACTION_NAME}</>${actionJosa} 발동했습니다.`,
{
scope: LogScope.NATION,
category: LogCategory.HISTORY,
nationId: nation.id,
format: LogFormat.YEAR_MONTH,
}
);
context.addLog(`<Y>${generalName}</>${generalJosa} <M>${ACTION_NAME}</>${actionJosa} 발동했습니다.`, {
scope: LogScope.NATION,
category: LogCategory.HISTORY,
nationId: nation.id,
format: LogFormat.YEAR_MONTH,
});
}
const avgNationGen =
Number.isFinite(context.averageNationGeneralCount)
? context.averageNationGeneralCount
: 0;
const createCount = Math.max(
0,
this.command.getCreateCount(avgNationGen)
);
const gennumValue = nation
? readMetaNumber(nation.meta, 'gennum')
: null;
const avgNationGen = Number.isFinite(context.averageNationGeneralCount) ? context.averageNationGeneralCount : 0;
const createCount = Math.max(0, this.command.getCreateCount(avgNationGen));
const gennumValue = nation ? readMetaNumber(nation.meta, 'gennum') : null;
const currentGennum = gennumValue ?? 0;
const nextGennum = currentGennum + createCount;
const globalDelay = this.command.getGlobalDelay(context);
if (nation) {
nation.meta = {
...nation.meta as object,
...(nation.meta as object),
gennum: nextGennum,
strategic_cmd_limit: globalDelay,
};
@@ -318,20 +263,11 @@ export class ActionResolver<
for (let idx = 0; idx < createCount; idx += 1) {
const newGeneralId = context.createGeneralId();
const candidate =
resolveCandidate(context, context.rng, this.env) ??
{ name: `NPC_${newGeneralId}` };
const name = this.env.decorateName
? this.env.decorateName(candidate.name, NPC_TYPE)
: candidate.name;
const candidate = resolveCandidate(context, context.rng, this.env) ?? { name: `NPC_${newGeneralId}` };
const name = this.env.decorateName ? this.env.decorateName(candidate.name, NPC_TYPE) : candidate.name;
const birthYear = context.currentYear - baseAge;
const deathYear = context.currentYear + deathYears;
const stats = resolveStats(
context,
context.rng,
this.env,
candidate
);
const stats = resolveStats(context, context.rng, this.env, candidate);
const meta: Record<string, TriggerValue> = {
npcType: NPC_TYPE,
crewTypeId: this.env.defaultCrewTypeId,
@@ -342,11 +278,7 @@ export class ActionResolver<
addMetaValue(meta, 'deathYear', deathYear);
addMetaValue(meta, 'specAge', DEFAULT_SPEC_AGE);
addMetaValue(meta, 'specAge2', DEFAULT_SPEC_AGE);
addMetaValue(
meta,
'killturn',
randomRangeInt(context.rng, killTurnMin, killTurnMax)
);
addMetaValue(meta, 'killturn', randomRangeInt(context.rng, killTurnMin, killTurnMax));
addMetaValue(meta, 'text', candidate.text ?? null);
const newGeneral = buildRecruitmentGeneral<TriggerState>({
@@ -378,12 +310,8 @@ export class ActionResolver<
}
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<
TriggerState,
VolunteerRecruitArgs,
VolunteerRecruitResolveContext<TriggerState>
> {
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, VolunteerRecruitArgs, VolunteerRecruitResolveContext<TriggerState>> {
public readonly key = 'che_의병모집';
public readonly name = ACTION_NAME;
private readonly resolver: ActionResolver<TriggerState>;
@@ -402,10 +330,7 @@ export class ActionDefinition<
return {};
}
buildConstraints(
ctx: ConstraintContext,
_args: VolunteerRecruitArgs
): Constraint[] {
buildConstraints(ctx: ConstraintContext, _args: VolunteerRecruitArgs): Constraint[] {
void _args;
const relYear = resolveRelYear(ctx);
return [
@@ -427,17 +352,12 @@ export class ActionDefinition<
// 예약 턴 실행에 필요한 국가 평균 정보를 구성한다.
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
const nationSummary = buildNationSummary(
options.worldRef,
base.general.nationId
);
const nationSummary = buildNationSummary(options.worldRef, base.general.nationId);
return {
...base,
currentYear: options.world.currentYear,
startYear: resolveStartYear(options.world, options.scenarioMeta),
averageNationGeneralCount: buildAverageNationGeneralCount(
options.worldRef
),
averageNationGeneralCount: buildAverageNationGeneralCount(options.worldRef),
nationAverageStats: nationSummary.averageStats,
nationAverageExperience: nationSummary.averageExperience,
nationAverageDedication: nationSummary.averageDedication,
@@ -450,6 +370,5 @@ export const commandSpec: NationTurnCommandSpec = {
category: '전략',
reqArg: false,
args: {},
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition(env.generalActionModules ?? [], env),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? [], env),
};
@@ -1,8 +1,4 @@
import type {
General,
GeneralTriggerState,
Nation,
} from '@sammo-ts/logic/domain/entities.js';
import type { General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import {
allowDiplomacyBetweenStatus,
@@ -11,10 +7,7 @@ import {
existsDestNation,
occupiedCity,
} from '@sammo-ts/logic/constraints/presets.js';
import {
GeneralActionPipeline,
type GeneralActionModule,
} from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionEffect,
@@ -22,17 +15,11 @@ import type {
GeneralActionResolveContext,
GeneralActionResolver,
} from '@sammo-ts/logic/actions/engine.js';
import {
createDiplomacyPatchEffect,
createLogEffect,
} from '@sammo-ts/logic/actions/engine.js';
import { createDiplomacyPatchEffect, createLogEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import {
buildDefaultDiplomacy,
DIPLOMACY_STATE,
} from '../../../diplomacy/index.js';
import { buildDefaultDiplomacy, DIPLOMACY_STATE } from '../../../diplomacy/index.js';
import { JosaUtil } from '@sammo-ts/common';
import type { NationTurnCommandSpec } from './index.js';
@@ -41,7 +28,7 @@ export interface DegradeRelationsArgs {
}
export interface DegradeRelationsResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
destNation: Nation;
diplomacy: { state: number; term: number };
@@ -63,13 +50,10 @@ const parseNationId = (raw: unknown): number | null => {
return value > 0 ? value : null;
};
const resolveNextTerm = (state: number, term: number): number =>
state === DIPLOMACY_STATE.WAR ? 3 : term + 3;
const resolveNextTerm = (state: number, term: number): number => (state === DIPLOMACY_STATE.WAR ? 3 : term + 3);
// 이호경식 쿨타임 계산을 담당한다.
export class CommandResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
private readonly pipeline: GeneralActionPipeline<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
@@ -77,20 +61,13 @@ export class CommandResolver<
}
getGlobalDelay(context: DegradeRelationsResolveContext<TriggerState>): number {
return Math.round(
this.pipeline.onCalcStrategic(
context,
ACTION_NAME,
'globalDelay',
DEFAULT_GLOBAL_DELAY
)
);
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
}
}
// 이호경식 실행 결과를 계산한다.
export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionResolver<TriggerState, DegradeRelationsArgs> {
readonly key = 'che_이호경식';
private readonly command: CommandResolver<TriggerState>;
@@ -117,37 +94,20 @@ export class ActionResolver<
general.dedication += EXP_DED_GAIN;
context.addLog(`${ACTION_NAME} 발동!`, { format: LogFormat.MONTH });
context.addLog(
`<D><b>${destNationName}</b></>에 <M>${ACTION_NAME}</>${actionJosa} 발동`,
{
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
}
);
context.addLog(`<D><b>${destNationName}</b></>에 <M>${ACTION_NAME}</>${actionJosa} 발동`, {
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
});
const effects: Array<GeneralActionEffect<TriggerState>> = [
createDiplomacyPatchEffect(
general.nationId,
context.destNation.id,
{
state: DIPLOMACY_STATE.DECLARATION,
term: resolveNextTerm(
context.diplomacy.state,
context.diplomacy.term
),
}
),
createDiplomacyPatchEffect(
context.destNation.id,
general.nationId,
{
state: DIPLOMACY_STATE.DECLARATION,
term: resolveNextTerm(
context.reverseDiplomacy.state,
context.reverseDiplomacy.term
),
}
),
createDiplomacyPatchEffect(general.nationId, context.destNation.id, {
state: DIPLOMACY_STATE.DECLARATION,
term: resolveNextTerm(context.diplomacy.state, context.diplomacy.term),
}),
createDiplomacyPatchEffect(context.destNation.id, general.nationId, {
state: DIPLOMACY_STATE.DECLARATION,
term: resolveNextTerm(context.reverseDiplomacy.state, context.reverseDiplomacy.term),
}),
];
for (const target of context.friendlyGenerals) {
@@ -209,12 +169,8 @@ export class ActionResolver<
// 이호경식 실행을 위한 정의/제약을 구성한다.
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<
TriggerState,
DegradeRelationsArgs,
DegradeRelationsResolveContext<TriggerState>
> {
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, DegradeRelationsArgs, DegradeRelationsResolveContext<TriggerState>> {
public readonly key = 'che_이호경식';
public readonly name = ACTION_NAME;
private readonly resolver: ActionResolver<TriggerState>;
@@ -232,20 +188,14 @@ export class ActionDefinition<
return { destNationId };
}
buildConstraints(
_ctx: ConstraintContext,
_args: DegradeRelationsArgs
): Constraint[] {
buildConstraints(_ctx: ConstraintContext, _args: DegradeRelationsArgs): Constraint[] {
void _ctx;
void _args;
return [
occupiedCity(),
beChief(),
existsDestNation(),
allowDiplomacyBetweenStatus(
[0, 1],
'선포, 전쟁중인 상대국에게만 가능합니다.'
),
allowDiplomacyBetweenStatus([0, 1], '선포, 전쟁중인 상대국에게만 가능합니다.'),
availableStrategicCommand(),
];
}
@@ -279,12 +229,8 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
worldRef.getDiplomacyEntry(destNationId, base.general.nationId) ??
buildDefaultDiplomacy(destNationId, base.general.nationId);
const generals = worldRef.listGenerals();
const friendlyGenerals = generals.filter(
(general) => general.nationId === base.general.nationId
);
const destNationGenerals = generals.filter(
(general) => general.nationId === destNationId
);
const friendlyGenerals = generals.filter((general) => general.nationId === base.general.nationId);
const destNationGenerals = generals.filter((general) => general.nationId === destNationId);
return {
...base,
destNation,
@@ -300,6 +246,5 @@ export const commandSpec: NationTurnCommandSpec = {
category: '외교',
reqArg: true,
args: { destNationId: 0 },
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition(env.generalActionModules ?? []),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
};
@@ -1,14 +1,5 @@
import type {
General,
GeneralTriggerState,
Nation,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
RequirementKey,
StateView,
} from '@sammo-ts/logic/constraints/types.js';
import type { General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext, RequirementKey, StateView } from '@sammo-ts/logic/constraints/types.js';
import {
alwaysFail,
beChief,
@@ -27,11 +18,7 @@ import type {
GeneralActionResolveContext,
GeneralActionResolver,
} from '@sammo-ts/logic/actions/engine.js';
import {
createGeneralPatchEffect,
createLogEffect,
createNationPatchEffect,
} from '@sammo-ts/logic/actions/engine.js';
import { createGeneralPatchEffect, createLogEffect, createNationPatchEffect } from '@sammo-ts/logic/actions/engine.js';
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
import type { NationTurnCommandSpec } from './index.js';
@@ -46,7 +33,7 @@ export interface AwardArgs {
}
export interface AwardResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
destGeneral: General<TriggerState>;
}
@@ -63,16 +50,11 @@ const ACTION_NAME = '포상';
const DEFAULT_MIN_AMOUNT = 100;
const DEFAULT_AMOUNT_UNIT = 100;
const roundToUnit = (value: number, unit: number): number =>
Math.round(value / unit) * unit;
const roundToUnit = (value: number, unit: number): number => Math.round(value / unit) * unit;
const formatNumber = (value: number): string =>
value.toLocaleString('en-US');
const formatNumber = (value: number): string => value.toLocaleString('en-US');
const normalizeAmount = (
amount: number,
env: AwardEnvironment
): number => {
const normalizeAmount = (amount: number, env: AwardEnvironment): number => {
const unit = env.amountUnit ?? DEFAULT_AMOUNT_UNIT;
const min = env.minAmount ?? DEFAULT_MIN_AMOUNT;
const max = env.maxAmount;
@@ -108,7 +90,7 @@ export class CommandResolver {
// 포상 결과를 계산한다.
export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionResolver<TriggerState, AwardArgs> {
readonly key = 'che_포상';
private readonly env: AwardEnvironment;
@@ -119,10 +101,7 @@ export class ActionResolver<
this.command = new CommandResolver(env);
}
resolve(
context: AwardResolveContext<TriggerState>,
args: AwardArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: AwardResolveContext<TriggerState>, args: AwardArgs): GeneralActionOutcome<TriggerState> {
const nation = context.nation;
if (!nation) {
return { effects: [] };
@@ -130,11 +109,7 @@ export class ActionResolver<
const { key, label } = resolveNationResource(nation, args.isGold);
const base = args.isGold ? this.env.baseGold : this.env.baseRice;
const available = Math.max(nation[key] - base, 0);
const amount = clamp(
this.command.normalizeAmount(args.amount),
0,
available
);
const amount = clamp(this.command.normalizeAmount(args.amount), 0, available);
if (amount <= 0) {
return { effects: [] };
}
@@ -142,37 +117,32 @@ export class ActionResolver<
const amountText = formatNumber(amount);
const effects: Array<GeneralActionEffect<TriggerState>> = [
createGeneralPatchEffect(
{ [key]: context.destGeneral[key] + amount } as Partial<
General<TriggerState>
>,
{ [key]: context.destGeneral[key] + amount } as Partial<General<TriggerState>>,
context.destGeneral.id
),
createNationPatchEffect({
[key]: nation[key] - amount,
} as Partial<Nation>, nation.id),
createNationPatchEffect(
{
[key]: nation[key] - amount,
} as Partial<Nation>,
nation.id
),
];
const amountJosa = JosaUtil.pick(amountText, '을');
effects.push(
createLogEffect(
`${label} ${amountText}${amountJosa} 포상으로 받았습니다.`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
generalId: context.destGeneral.id,
format: LogFormat.PLAIN,
}
)
createLogEffect(`${label} ${amountText}${amountJosa} 포상으로 받았습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
generalId: context.destGeneral.id,
format: LogFormat.PLAIN,
})
);
effects.push(
createLogEffect(
`<Y>${context.destGeneral.name}</>에게 ${label} ${amountText}${amountJosa} 수여했습니다.`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
}
)
createLogEffect(`<Y>${context.destGeneral.name}</>에게 ${label} ${amountText}${amountJosa} 수여했습니다.`, {
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
format: LogFormat.MONTH,
})
);
return { effects };
@@ -180,12 +150,8 @@ export class ActionResolver<
}
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<
TriggerState,
AwardArgs,
AwardResolveContext<TriggerState>
> {
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, AwardArgs, AwardResolveContext<TriggerState>> {
public readonly key = 'che_포상';
public readonly name = ACTION_NAME;
private readonly command: CommandResolver;
@@ -225,10 +191,7 @@ export class ActionDefinition<
};
}
buildConstraints(
ctx: ConstraintContext,
args: AwardArgs
): Constraint[] {
buildConstraints(ctx: ConstraintContext, args: AwardArgs): Constraint[] {
const requirements: RequirementKey[] = [];
if (ctx.cityId !== undefined) {
requirements.push({ kind: 'city', id: ctx.cityId });
@@ -262,10 +225,7 @@ export class ActionDefinition<
];
}
resolve(
context: AwardResolveContext<TriggerState>,
args: AwardArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: AwardResolveContext<TriggerState>, args: AwardArgs): GeneralActionOutcome<TriggerState> {
return this.resolver.resolve(context, args);
}
}
@@ -293,9 +253,7 @@ export const commandSpec: NationTurnCommandSpec = {
args: { isGold: true, amount: 1, destGeneralId: 0 },
createDefinition: (env: TurnCommandEnv) => {
const maxAmount =
env.maxResourceActionAmount > 0
? env.maxResourceActionAmount
: Math.max(env.baseGold, env.baseRice, 1000);
env.maxResourceActionAmount > 0 ? env.maxResourceActionAmount : Math.max(env.baseGold, env.baseRice, 1000);
return new ActionDefinition({
baseGold: env.baseGold,
baseRice: env.baseRice,
@@ -1,7 +1,4 @@
import type {
General,
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type { General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import {
allowDiplomacyStatus,
@@ -9,10 +6,7 @@ import {
beChief,
occupiedCity,
} from '@sammo-ts/logic/constraints/presets.js';
import {
GeneralActionPipeline,
type GeneralActionModule,
} from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionEffect,
@@ -30,7 +24,7 @@ import type { NationTurnCommandSpec } from './index.js';
export interface DesperateFightArgs {}
export interface DesperateFightResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
nationGenerals: Array<General<TriggerState>>;
}
@@ -43,9 +37,7 @@ const TRAIN_CAP = 100;
const ATMOS_CAP = 100;
// 필사즉생 쿨타임 계산을 담당한다.
export class CommandResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
private readonly pipeline: GeneralActionPipeline<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
@@ -53,20 +45,13 @@ export class CommandResolver<
}
getGlobalDelay(context: DesperateFightResolveContext<TriggerState>): number {
return Math.round(
this.pipeline.onCalcStrategic(
context,
ACTION_NAME,
'globalDelay',
DEFAULT_GLOBAL_DELAY
)
);
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
}
}
// 필사즉생 실행 결과를 계산한다.
export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionResolver<TriggerState, DesperateFightArgs> {
readonly key = 'che_필사즉생';
private readonly command: CommandResolver<TriggerState>;
@@ -96,9 +81,7 @@ export class ActionResolver<
const effects: Array<GeneralActionEffect<TriggerState>> = [];
const updateTrainAtmos = (
target: General<TriggerState>
): { train: number; atmos: number } | null => {
const updateTrainAtmos = (target: General<TriggerState>): { train: number; atmos: number } | null => {
const nextTrain = Math.max(target.train, TRAIN_CAP);
const nextAtmos = Math.max(target.atmos, ATMOS_CAP);
if (nextTrain === target.train && nextAtmos === target.atmos) {
@@ -119,9 +102,7 @@ export class ActionResolver<
}
const patch = updateTrainAtmos(target);
if (patch) {
effects.push(
createGeneralPatchEffect(patch, target.id)
);
effects.push(createGeneralPatchEffect(patch, target.id));
}
effects.push(
createLogEffect(broadcastMessage, {
@@ -155,12 +136,8 @@ export class ActionResolver<
// 필사즉생 실행을 위한 정의/제약을 구성한다.
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> implements GeneralActionDefinition<
TriggerState,
DesperateFightArgs,
DesperateFightResolveContext<TriggerState>
> {
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, DesperateFightArgs, DesperateFightResolveContext<TriggerState>> {
public readonly key = 'che_필사즉생';
public readonly name = ACTION_NAME;
private readonly resolver: ActionResolver<TriggerState>;
@@ -174,10 +151,7 @@ export class ActionDefinition<
return {};
}
buildConstraints(
_ctx: ConstraintContext,
_args: DesperateFightArgs
): Constraint[] {
buildConstraints(_ctx: ConstraintContext, _args: DesperateFightArgs): Constraint[] {
void _ctx;
void _args;
return [
@@ -202,9 +176,7 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
if (!worldRef) {
return null;
}
const nationGenerals = worldRef
.listGenerals()
.filter((entry) => entry.nationId === base.general.nationId);
const nationGenerals = worldRef.listGenerals().filter((entry) => entry.nationId === base.general.nationId);
return {
...base,
nationGenerals,
@@ -216,6 +188,5 @@ export const commandSpec: NationTurnCommandSpec = {
category: '전략',
reqArg: false,
args: {},
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition(env.generalActionModules ?? []),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
};
@@ -1,9 +1,4 @@
import type {
City,
General,
GeneralTriggerState,
Nation,
} from '@sammo-ts/logic/domain/entities.js';
import type { City, General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import {
allowDiplomacyBetweenStatus,
@@ -13,10 +8,7 @@ import {
notOccupiedDestCity,
occupiedCity,
} from '@sammo-ts/logic/constraints/presets.js';
import {
GeneralActionPipeline,
type GeneralActionModule,
} from '@sammo-ts/logic/triggers/general-action.js';
import { GeneralActionPipeline, type GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionEffect,
@@ -36,7 +28,7 @@ export interface DeceptionArgs {
}
export interface DeceptionResolveContext<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> extends GeneralActionResolveContext<TriggerState> {
destCity: City;
destNation: Nation | null;
@@ -58,11 +50,7 @@ const parseCityId = (raw: unknown): number | null => {
return value > 0 ? value : null;
};
const pickMoveCityId = (
rng: GeneralActionResolveContext['rng'],
destCityId: number,
candidates: City[]
): number => {
const pickMoveCityId = (rng: GeneralActionResolveContext['rng'], destCityId: number, candidates: City[]): number => {
if (candidates.length === 0) {
return destCityId;
}
@@ -76,9 +64,7 @@ const pickMoveCityId = (
};
// 허보 쿨타임 계산을 담당한다.
export class CommandResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
> {
export class CommandResolver<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
private readonly pipeline: GeneralActionPipeline<TriggerState>;
constructor(modules: Array<GeneralActionModule<TriggerState> | null | undefined>) {
@@ -86,20 +72,13 @@ export class CommandResolver<
}
getGlobalDelay(context: DeceptionResolveContext<TriggerState>): number {
return Math.round(
this.pipeline.onCalcStrategic(
context,
ACTION_NAME,
'globalDelay',
DEFAULT_GLOBAL_DELAY
)
);
return Math.round(this.pipeline.onCalcStrategic(context, ACTION_NAME, 'globalDelay', DEFAULT_GLOBAL_DELAY));
}
}
// 허보 실행 결과를 계산한다.
export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionResolver<TriggerState, DeceptionArgs> {
readonly key = 'che_허보';
private readonly command: CommandResolver<TriggerState>;
@@ -108,10 +87,7 @@ export class ActionResolver<
this.command = new CommandResolver(modules);
}
resolve(
context: DeceptionResolveContext<TriggerState>,
_args: DeceptionArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: DeceptionResolveContext<TriggerState>, _args: DeceptionArgs): GeneralActionOutcome<TriggerState> {
void _args;
const { general, nation } = context;
const generalName = general.name;
@@ -124,13 +100,10 @@ export class ActionResolver<
general.dedication += EXP_DED_GAIN;
context.addLog(`${ACTION_NAME} 발동!`, { format: LogFormat.MONTH });
context.addLog(
`<G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>를 발동`,
{
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
}
);
context.addLog(`<G><b>${cityName}</b></>에 <M>${ACTION_NAME}</>를 발동`, {
category: LogCategory.HISTORY,
format: LogFormat.YEAR_MONTH,
});
const effects: Array<GeneralActionEffect<TriggerState>> = [];
@@ -149,11 +122,7 @@ export class ActionResolver<
}
for (const target of context.destCityGenerals) {
const moveCityId = pickMoveCityId(
context.rng,
context.destCity.id,
context.destNationSupplyCities
);
const moveCityId = pickMoveCityId(context.rng, context.destCity.id, context.destNationSupplyCities);
effects.push(
createLogEffect(destBroadcastMessage, {
scope: LogScope.GENERAL,
@@ -163,12 +132,7 @@ export class ActionResolver<
})
);
if (moveCityId !== target.cityId) {
effects.push(
createGeneralPatchEffect(
{ cityId: moveCityId },
target.id
)
);
effects.push(createGeneralPatchEffect({ cityId: moveCityId }, target.id));
}
}
@@ -208,7 +172,7 @@ export class ActionResolver<
// 허보 실행을 위한 정의/제약을 구성한다.
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, DeceptionArgs, DeceptionResolveContext<TriggerState>> {
public readonly key = 'che_허보';
public readonly name = ACTION_NAME;
@@ -227,10 +191,7 @@ export class ActionDefinition<
return { destCityId };
}
buildConstraints(
_ctx: ConstraintContext,
_args: DeceptionArgs
): Constraint[] {
buildConstraints(_ctx: ConstraintContext, _args: DeceptionArgs): Constraint[] {
void _ctx;
void _args;
return [
@@ -238,18 +199,12 @@ export class ActionDefinition<
beChief(),
notNeutralDestCity(),
notOccupiedDestCity(),
allowDiplomacyBetweenStatus(
[0, 1],
'선포, 전쟁중인 상대국에게만 가능합니다.'
),
allowDiplomacyBetweenStatus([0, 1], '선포, 전쟁중인 상대국에게만 가능합니다.'),
availableStrategicCommand(),
];
}
resolve(
context: DeceptionResolveContext<TriggerState>,
args: DeceptionArgs
): GeneralActionOutcome<TriggerState> {
resolve(context: DeceptionResolveContext<TriggerState>, args: DeceptionArgs): GeneralActionOutcome<TriggerState> {
return this.resolver.resolve(context, args);
}
}
@@ -271,19 +226,12 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
const destNation = worldRef.getNationById(destCity.nationId);
const generals = worldRef.listGenerals();
const destCityGenerals = generals.filter(
(general) =>
general.nationId === destCity.nationId &&
general.cityId === destCity.id
);
const friendlyGenerals = generals.filter(
(general) => general.nationId === base.general.nationId
(general) => general.nationId === destCity.nationId && general.cityId === destCity.id
);
const friendlyGenerals = generals.filter((general) => general.nationId === base.general.nationId);
const destNationSupplyCities = worldRef
.listCities()
.filter(
(city) =>
city.nationId === destCity.nationId && city.supplyState > 0
);
.filter((city) => city.nationId === destCity.nationId && city.supplyState > 0);
return {
...base,
destCity,
@@ -299,6 +247,5 @@ export const commandSpec: NationTurnCommandSpec = {
category: '전략',
reqArg: true,
args: { destCityId: 0 },
createDefinition: (env: TurnCommandEnv) =>
new ActionDefinition(env.generalActionModules ?? []),
createDefinition: (env: TurnCommandEnv) => new ActionDefinition(env.generalActionModules ?? []),
};
@@ -17,21 +17,15 @@ export const NATION_TURN_COMMAND_KEYS = [
'che_급습',
] as const;
export type NationTurnCommandKey =
(typeof NATION_TURN_COMMAND_KEYS)[number];
export type NationTurnCommandKey = (typeof NATION_TURN_COMMAND_KEYS)[number];
export type NationTurnCommandSpec =
TurnCommandSpecBase<NationTurnCommandKey>;
export type NationTurnCommandSpec = TurnCommandSpecBase<NationTurnCommandKey>;
export type NationTurnCommandModule =
TurnCommandModule<NationTurnCommandSpec>;
export type NationTurnCommandModule = TurnCommandModule<NationTurnCommandSpec>;
export type NationTurnCommandImporter = () => Promise<NationTurnCommandModule>;
const defaultImporters: Record<
NationTurnCommandKey,
NationTurnCommandImporter
> = {
const defaultImporters: Record<NationTurnCommandKey, NationTurnCommandImporter> = {
휴식: async () => import('./휴식.js'),
che_포상: async () => import('./che_포상.js'),
che_부대탈퇴지시: async () => import('./che_부대탈퇴지시.js'),
@@ -48,29 +42,17 @@ const defaultImporters: Record<
che_급습: async () => import('./che_급습.js'),
};
export const isNationTurnCommandKey = (
value: string
): value is NationTurnCommandKey =>
export const isNationTurnCommandKey = (value: string): value is NationTurnCommandKey =>
NATION_TURN_COMMAND_KEYS.includes(value as NationTurnCommandKey);
export class NationTurnCommandLoader {
private readonly cache = new Map<
NationTurnCommandKey,
Promise<NationTurnCommandModule>
>();
private readonly cache = new Map<NationTurnCommandKey, Promise<NationTurnCommandModule>>();
constructor(
private readonly importers: Record<
NationTurnCommandKey,
NationTurnCommandImporter
> = defaultImporters
) { }
private readonly importers: Record<NationTurnCommandKey, NationTurnCommandImporter> = defaultImporters
) {}
async load(
key: NationTurnCommandKey
): Promise<NationTurnCommandModule> {
async load(key: NationTurnCommandKey): Promise<NationTurnCommandModule> {
const cached = this.cache.get(key);
if (cached) {
return cached;
@@ -1,10 +1,5 @@
import type {
GeneralTriggerState,
} from '@sammo-ts/logic/domain/entities.js';
import type {
Constraint,
ConstraintContext,
} from '@sammo-ts/logic/constraints/types.js';
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
import type {
GeneralActionOutcome,
@@ -20,7 +15,7 @@ export interface NationRestArgs {}
const ACTION_NAME = '휴식';
export class ActionResolver<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionResolver<TriggerState, NationRestArgs> {
readonly key = '휴식';
@@ -35,7 +30,7 @@ export class ActionResolver<
}
export class ActionDefinition<
TriggerState extends GeneralTriggerState = GeneralTriggerState
TriggerState extends GeneralTriggerState = GeneralTriggerState,
> implements GeneralActionDefinition<TriggerState, NationRestArgs> {
public readonly key = '휴식';
public readonly name = ACTION_NAME;
@@ -46,10 +41,7 @@ export class ActionDefinition<
return {};
}
buildConstraints(
_ctx: ConstraintContext,
_args: NationRestArgs
): Constraint[] {
buildConstraints(_ctx: ConstraintContext, _args: NationRestArgs): Constraint[] {
void _ctx;
void _args;
return [];
+13 -43
View File
@@ -13,9 +13,7 @@ import {
} from './helpers.js';
import type { Constraint, RequirementKey } from './types.js';
export const occupiedCity = (
options: { allowNeutral?: boolean } = {}
): Constraint => ({
export const occupiedCity = (options: { allowNeutral?: boolean } = {}): Constraint => ({
name: 'OccupiedCity',
requires: (ctx) => {
const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }];
@@ -93,8 +91,7 @@ export const occupiedDestCity = (): Constraint => ({
export const suppliedCity = (): Constraint => ({
name: 'SuppliedCity',
requires: (ctx) =>
ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : [],
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
test: (ctx, view) => {
const city = readCity(view, ctx.cityId);
if (!city) {
@@ -142,13 +139,9 @@ export const suppliedDestCity = (): Constraint => ({
},
});
export const remainCityCapacity = (
key: keyof City,
label: string
): Constraint => ({
export const remainCityCapacity = (key: keyof City, label: string): Constraint => ({
name: 'RemainCityCapacity',
requires: (ctx) =>
ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : [],
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
test: (ctx, view) => {
const city = readCity(view, ctx.cityId);
if (!city) {
@@ -171,14 +164,9 @@ export const remainCityCapacity = (
},
});
export const remainCityCapacityByMax = (
key: keyof City,
maxKey: keyof City,
label: string
): Constraint => ({
export const remainCityCapacityByMax = (key: keyof City, maxKey: keyof City, label: string): Constraint => ({
name: 'RemainCityCapacityByMax',
requires: (ctx) =>
ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : [],
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
test: (ctx, view) => {
const city = readCity(view, ctx.cityId);
if (!city) {
@@ -200,14 +188,9 @@ export const remainCityCapacityByMax = (
},
});
export const reqCityCapacity = (
key: keyof City,
label: string,
required: number | string
): Constraint => ({
export const reqCityCapacity = (key: keyof City, label: string, required: number | string): Constraint => ({
name: 'ReqCityCapacity',
requires: (ctx) =>
ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : [],
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
test: (ctx, view) => {
const city = readCity(view, ctx.cityId);
if (!city) {
@@ -240,8 +223,7 @@ export const reqCityCapacity = (
export const reqCityTrust = (minTrust: number): Constraint => ({
name: 'ReqCityTrust',
requires: (ctx) =>
ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : [],
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
test: (ctx, view) => {
const city = readCity(view, ctx.cityId);
if (!city) {
@@ -251,11 +233,7 @@ export const reqCityTrust = (minTrust: number): Constraint => ({
const req: RequirementKey = { kind: 'city', id: ctx.cityId };
return unknownOrDeny(ctx, [req], '도시 정보가 없습니다.');
}
const trust =
readMetaNumberFromUnknown(
city.meta,
'trust'
) ?? null;
const trust = readMetaNumberFromUnknown(city.meta, 'trust') ?? null;
if (trust === null) {
return unknownOrDeny(ctx, [], '민심 정보가 없습니다.');
}
@@ -440,9 +418,7 @@ const hasRouteToDest = (
export const hasRouteWithEnemy = (): Constraint => ({
name: 'HasRouteWithEnemy',
requires: (ctx) => {
const reqs: RequirementKey[] = [
{ kind: 'general', id: ctx.actorId },
];
const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }];
const destCityId = resolveDestCityId(ctx);
if (destCityId !== undefined) {
reqs.push({ kind: 'destCity', id: destCityId });
@@ -469,9 +445,7 @@ export const hasRouteWithEnemy = (): Constraint => ({
}
const map = view.get({ kind: 'env', key: 'map' }) as MapDefinition | null;
const cities = view.get({ kind: 'env', key: 'cities' }) as City[] | null;
const nations = view.get({ kind: 'env', key: 'nations' }) as
| Array<{ id: number }>
| null;
const nations = view.get({ kind: 'env', key: 'nations' }) as Array<{ id: number }> | null;
if (!map || !cities || !nations) {
return unknownOrDeny(ctx, [], '경로 정보가 없습니다.');
}
@@ -480,11 +454,7 @@ export const hasRouteWithEnemy = (): Constraint => ({
allowedNationIds.add(general.nationId);
allowedNationIds.add(0);
for (const nation of nations) {
const state = readDiplomacyState(
view,
general.nationId,
nation.id
);
const state = readDiplomacyState(view, general.nationId, nation.id);
if (state === 0) {
allowedNationIds.add(nation.id);
}
+9 -24
View File
@@ -32,17 +32,15 @@ const readDiplomacyEntry = (
typeof record.state === 'number'
? record.state
: typeof record.stateCode === 'number'
? record.stateCode
: null;
? record.stateCode
: null;
const term = typeof record.term === 'number' ? record.term : null;
return { state, term };
}
return null;
};
export const disallowDiplomacyBetweenStatus = (
disallowList: Record<number, string>
): Constraint => ({
export const disallowDiplomacyBetweenStatus = (disallowList: Record<number, string>): Constraint => ({
name: 'DisallowDiplomacyBetweenStatus',
requires: (ctx) => {
const reqs: RequirementKey[] = [];
@@ -73,8 +71,7 @@ export const disallowDiplomacyBetweenStatus = (
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
}
const destCity = readDestCity(ctx, view);
const destNationId =
resolveDestNationId(ctx) ?? destCity?.nationId;
const destNationId = resolveDestNationId(ctx) ?? destCity?.nationId;
if (destNationId === undefined) {
return unknownOrDeny(ctx, [], '상대 국가 정보가 없습니다.');
}
@@ -95,10 +92,7 @@ export const disallowDiplomacyBetweenStatus = (
},
});
export const allowDiplomacyBetweenStatus = (
allowList: number[],
reason: string
): Constraint => ({
export const allowDiplomacyBetweenStatus = (allowList: number[], reason: string): Constraint => ({
name: 'AllowDiplomacyBetweenStatus',
requires: (ctx) => {
const reqs: RequirementKey[] = [];
@@ -129,8 +123,7 @@ export const allowDiplomacyBetweenStatus = (
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
}
const destCity = readDestCity(ctx, view);
const destNationId =
resolveDestNationId(ctx) ?? destCity?.nationId;
const destNationId = resolveDestNationId(ctx) ?? destCity?.nationId;
if (destNationId === undefined) {
return unknownOrDeny(ctx, [], '상대 국가 정보가 없습니다.');
}
@@ -150,11 +143,7 @@ export const allowDiplomacyBetweenStatus = (
},
});
export const allowDiplomacyWithTerm = (
requiredState: number,
minTerm: number,
reason: string
): Constraint => ({
export const allowDiplomacyWithTerm = (requiredState: number, minTerm: number, reason: string): Constraint => ({
name: 'AllowDiplomacyWithTerm',
requires: (ctx) => {
const reqs: RequirementKey[] = [];
@@ -185,8 +174,7 @@ export const allowDiplomacyWithTerm = (
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
}
const destCity = readDestCity(ctx, view);
const destNationId =
resolveDestNationId(ctx) ?? destCity?.nationId;
const destNationId = resolveDestNationId(ctx) ?? destCity?.nationId;
if (destNationId === undefined) {
return unknownOrDeny(ctx, [], '상대 국가 정보가 없습니다.');
}
@@ -206,10 +194,7 @@ export const allowDiplomacyWithTerm = (
},
});
export const allowDiplomacyStatus = (
allowList: number[],
reason: string
): Constraint => ({
export const allowDiplomacyStatus = (allowList: number[], reason: string): Constraint => ({
name: 'AllowDiplomacyStatus',
requires: (ctx) => {
const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }];
+3 -14
View File
@@ -1,10 +1,4 @@
import type {
Constraint,
ConstraintContext,
ConstraintResult,
RequirementKey,
StateView,
} from './types.js';
import type { Constraint, ConstraintContext, ConstraintResult, RequirementKey, StateView } from './types.js';
export const evaluateConstraints = (
constraints: Constraint[],
@@ -12,9 +6,7 @@ export const evaluateConstraints = (
view: StateView
): ConstraintResult => {
for (const constraint of constraints) {
const missing = constraint
.requires(ctx)
.filter((req) => !view.has(req));
const missing = constraint.requires(ctx).filter((req) => !view.has(req));
if (missing.length > 0 && ctx.mode === 'precheck') {
return { kind: 'unknown', missing };
}
@@ -26,10 +18,7 @@ export const evaluateConstraints = (
return { kind: 'allow' };
};
export const collectRequirements = (
constraints: Constraint[],
ctx: ConstraintContext
): RequirementKey[] => {
export const collectRequirements = (constraints: Constraint[], ctx: ConstraintContext): RequirementKey[] => {
const keys: RequirementKey[] = [];
for (const constraint of constraints) {
keys.push(...constraint.requires(ctx));
+4 -16
View File
@@ -1,11 +1,5 @@
import type { General } from '@sammo-ts/logic/domain/entities.js';
import {
allow,
readDestGeneral,
resolveDestGeneralId,
resolveDestNationId,
unknownOrDeny,
} from './helpers.js';
import { allow, readDestGeneral, resolveDestGeneralId, resolveDestNationId, unknownOrDeny } from './helpers.js';
import type { Constraint, ConstraintContext, RequirementKey, StateView } from './types.js';
export const notBeNeutral = (): Constraint => ({
@@ -73,9 +67,7 @@ export const reqGeneralGold = (
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }, ...requirements],
test: (ctx, view) => {
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
const missing = [generalReq, ...requirements].filter(
(req) => !view.has(req)
);
const missing = [generalReq, ...requirements].filter((req) => !view.has(req));
if (missing.length > 0) {
return unknownOrDeny(ctx, missing, '장수 정보가 없습니다.');
}
@@ -99,9 +91,7 @@ export const reqGeneralRice = (
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }, ...requirements],
test: (ctx, view) => {
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
const missing = [generalReq, ...requirements].filter(
(req) => !view.has(req)
);
const missing = [generalReq, ...requirements].filter((req) => !view.has(req));
if (missing.length > 0) {
return unknownOrDeny(ctx, missing, '장수 정보가 없습니다.');
}
@@ -144,9 +134,7 @@ export const reqGeneralCrewMargin = (
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }, ...requirements],
test: (ctx, view) => {
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
const missing = [generalReq, ...requirements].filter(
(req) => !view.has(req)
);
const missing = [generalReq, ...requirements].filter((req) => !view.has(req));
if (missing.length > 0) {
return unknownOrDeny(ctx, missing, '장수 정보가 없습니다.');
}
+10 -43
View File
@@ -1,26 +1,12 @@
import type { City, General, Nation, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
import type {
ConstraintContext,
ConstraintResult,
RequirementKey,
StateView,
} from './types.js';
import type { ConstraintContext, ConstraintResult, RequirementKey, StateView } from './types.js';
export const allow = (): ConstraintResult => ({ kind: 'allow' });
export const unknownOrDeny = (
ctx: ConstraintContext,
missing: RequirementKey[],
reason: string
): ConstraintResult =>
ctx.mode === 'precheck'
? { kind: 'unknown', missing }
: { kind: 'deny', reason };
export const unknownOrDeny = (ctx: ConstraintContext, missing: RequirementKey[], reason: string): ConstraintResult =>
ctx.mode === 'precheck' ? { kind: 'unknown', missing } : { kind: 'deny', reason };
export const readGeneral = (
ctx: ConstraintContext,
view: StateView
): General | null => {
export const readGeneral = (ctx: ConstraintContext, view: StateView): General | null => {
const req: RequirementKey = { kind: 'general', id: ctx.actorId };
if (!view.has(req)) {
return null;
@@ -47,10 +33,7 @@ export const resolveDestGeneralId = (ctx: ConstraintContext): number | undefined
return typeof raw === 'number' ? raw : undefined;
};
export const readDestGeneral = (
ctx: ConstraintContext,
view: StateView
): General | null => {
export const readDestGeneral = (ctx: ConstraintContext, view: StateView): General | null => {
const destGeneralId = resolveDestGeneralId(ctx);
if (destGeneralId === undefined) {
return null;
@@ -78,18 +61,12 @@ export const resolveDestNationId = (ctx: ConstraintContext): number | undefined
return typeof raw === 'number' ? raw : undefined;
};
export const readDestCity = (
ctx: ConstraintContext,
view: StateView
): City | null => {
export const readDestCity = (ctx: ConstraintContext, view: StateView): City | null => {
const destCityId = resolveDestCityId(ctx);
return readCity(view, destCityId);
};
export const readNation = (
view: StateView,
id?: number
): Nation | null => {
export const readNation = (view: StateView, id?: number): Nation | null => {
if (id === undefined) {
return null;
}
@@ -100,19 +77,12 @@ export const readNation = (
return view.get(req) as Nation | null;
};
export const readMetaNumber = (
meta: Record<string, TriggerValue>,
key: string
): number | null => {
export const readMetaNumber = (meta: Record<string, TriggerValue>, key: string): number | null => {
const value = meta[key];
return typeof value === 'number' ? value : null;
};
export const readDiplomacyState = (
view: StateView,
srcNationId: number,
destNationId: number
): number | null => {
export const readDiplomacyState = (view: StateView, srcNationId: number, destNationId: number): number | null => {
const req: RequirementKey = {
kind: 'diplomacy',
srcNationId,
@@ -138,10 +108,7 @@ export const readDiplomacyState = (
return null;
};
export const readMetaNumberFromUnknown = (
meta: Record<string, unknown>,
key: string
): number | null => {
export const readMetaNumberFromUnknown = (meta: Record<string, unknown>, key: string): number | null => {
const value = meta[key];
return typeof value === 'number' ? value : null;
};
+1 -4
View File
@@ -7,10 +7,7 @@ export const alwaysFail = (reason: string): Constraint => ({
test: () => ({ kind: 'deny', reason }),
});
export const notOpeningPart = (
relYear: number,
openingPartYear: number
): Constraint => ({
export const notOpeningPart = (relYear: number, openingPartYear: number): Constraint => ({
name: 'NotOpeningPart',
requires: () => [],
test: (_ctx) => {
+5 -21
View File
@@ -1,20 +1,10 @@
import type { General, Nation } from '@sammo-ts/logic/domain/entities.js';
import {
allow,
readGeneral,
readMetaNumber,
readNation,
resolveDestNationId,
unknownOrDeny,
} from './helpers.js';
import { allow, readGeneral, readMetaNumber, readNation, resolveDestNationId, unknownOrDeny } from './helpers.js';
import type { Constraint, ConstraintContext, RequirementKey, StateView } from './types.js';
export const notWanderingNation = (): Constraint => ({
name: 'NotWanderingNation',
requires: (ctx) =>
ctx.nationId !== undefined
? [{ kind: 'nation', id: ctx.nationId }]
: [],
requires: (ctx) => (ctx.nationId !== undefined ? [{ kind: 'nation', id: ctx.nationId }] : []),
test: (ctx, view) => {
const nation = readNation(view, ctx.nationId);
if (!nation) {
@@ -31,9 +21,7 @@ export const notWanderingNation = (): Constraint => ({
},
});
export const availableStrategicCommand = (
allowTurnCnt = 0
): Constraint => ({
export const availableStrategicCommand = (allowTurnCnt = 0): Constraint => ({
name: 'AvailableStrategicCommand',
requires: (ctx) => {
const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }];
@@ -92,9 +80,7 @@ export const reqNationGold = (
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
}
const nationReq: RequirementKey = { kind: 'nation', id: nationId };
const missing = [nationReq, ...requirements].filter(
(req) => !view.has(req)
);
const missing = [nationReq, ...requirements].filter((req) => !view.has(req));
if (missing.length > 0) {
return unknownOrDeny(ctx, missing, '국가 정보가 없습니다.');
}
@@ -128,9 +114,7 @@ export const reqNationRice = (
return unknownOrDeny(ctx, [], '국가 정보가 없습니다.');
}
const nationReq: RequirementKey = { kind: 'nation', id: nationId };
const missing = [nationReq, ...requirements].filter(
(req) => !view.has(req)
);
const missing = [nationReq, ...requirements].filter((req) => !view.has(req));
if (missing.length > 0) {
return unknownOrDeny(ctx, missing, '국가 정보가 없습니다.');
}
+2 -8
View File
@@ -23,10 +23,7 @@ export const mustBeTroopLeader = (): Constraint => ({
export const reqTroopMembers = (): Constraint => ({
name: 'ReqTroopMembers',
requires: (ctx) => [
{ kind: 'general', id: ctx.actorId },
{ kind: 'generalList' },
],
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }, { kind: 'generalList' }],
test: (ctx, view) => {
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
if (!view.has(generalReq)) {
@@ -44,10 +41,7 @@ export const reqTroopMembers = (): Constraint => ({
if (!generals) {
return unknownOrDeny(ctx, [listReq], '장수 정보가 없습니다.');
}
const hasMember = generals.some(
(entry) =>
entry.troopId === general.troopId && entry.id !== general.id
);
const hasMember = generals.some((entry) => entry.troopId === general.troopId && entry.id !== general.id);
if (hasMember) {
return allow();
}
+13 -41
View File
@@ -31,15 +31,10 @@ export interface DiplomacyPatch {
meta?: Record<string, unknown>;
}
export const buildDiplomacyKey = (
srcNationId: number,
destNationId: number
): string => `${srcNationId}:${destNationId}`;
export const buildDiplomacyKey = (srcNationId: number, destNationId: number): string =>
`${srcNationId}:${destNationId}`;
export const buildDefaultDiplomacy = (
srcNationId: number,
destNationId: number
): DiplomacyEntry => ({
export const buildDefaultDiplomacy = (srcNationId: number, destNationId: number): DiplomacyEntry => ({
fromNationId: srcNationId,
toNationId: destNationId,
state: DIPLOMACY_STATE.TRADE,
@@ -48,16 +43,13 @@ export const buildDefaultDiplomacy = (
meta: {},
});
export const applyDiplomacyPatch = (
entry: DiplomacyEntry,
patch: DiplomacyPatch
): DiplomacyEntry => {
export const applyDiplomacyPatch = (entry: DiplomacyEntry, patch: DiplomacyPatch): DiplomacyEntry => {
const nextDead =
typeof patch.dead === 'number'
? patch.dead
: typeof patch.deadDelta === 'number'
? entry.dead + patch.deadDelta
: entry.dead;
? entry.dead + patch.deadDelta
: entry.dead;
return {
...entry,
state: patch.state ?? entry.state,
@@ -67,9 +59,7 @@ export const applyDiplomacyPatch = (
};
};
export const readDiplomacyMeta = (
meta: Record<string, unknown>
): { meta: Record<string, unknown>; dead: number } => {
export const readDiplomacyMeta = (meta: Record<string, unknown>): { meta: Record<string, unknown>; dead: number } => {
const rawDead = meta.dead;
const dead = typeof rawDead === 'number' ? rawDead : 0;
const cleaned = { ...meta };
@@ -77,14 +67,11 @@ export const readDiplomacyMeta = (
return { meta: cleaned, dead };
};
export const buildDiplomacyMeta = (
entry: DiplomacyEntry
): Record<string, unknown> => ({
export const buildDiplomacyMeta = (entry: DiplomacyEntry): Record<string, unknown> => ({
...entry.meta,
dead: entry.dead,
});
export const processDiplomacyMonth = (
diplomacy: DiplomacyEntry[],
generalCounts: Map<number, number>
@@ -94,10 +81,7 @@ export const processDiplomacyMonth = (
meta: { ...entry.meta },
}));
const byKey = new Map<string, DiplomacyEntry>(
next.map((entry) => [
buildDiplomacyKey(entry.fromNationId, entry.toNationId),
entry,
])
next.map((entry) => [buildDiplomacyKey(entry.fromNationId, entry.toNationId), entry])
);
// 전쟁 기간 갱신: 사상자에 따라 term 증가, 잔여 사상자 유지.
@@ -127,14 +111,8 @@ export const processDiplomacyMonth = (
if (processedPairs.has(pairKey)) {
continue;
}
const opposite = byKey.get(
buildDiplomacyKey(entry.toNationId, entry.fromNationId)
);
if (
opposite &&
opposite.state === DIPLOMACY_STATE.WAR &&
opposite.term <= 1
) {
const opposite = byKey.get(buildDiplomacyKey(entry.toNationId, entry.fromNationId));
if (opposite && opposite.state === DIPLOMACY_STATE.WAR && opposite.term <= 1) {
entry.state = DIPLOMACY_STATE.TRADE;
entry.term = 0;
opposite.state = DIPLOMACY_STATE.TRADE;
@@ -153,15 +131,9 @@ export const processDiplomacyMonth = (
// 불가침/선전포고 만료 처리.
for (const entry of next) {
if (
entry.state === DIPLOMACY_STATE.NON_AGGRESSION &&
entry.term === 0
) {
if (entry.state === DIPLOMACY_STATE.NON_AGGRESSION && entry.term === 0) {
entry.state = DIPLOMACY_STATE.TRADE;
} else if (
entry.state === DIPLOMACY_STATE.DECLARATION &&
entry.term === 0
) {
} else if (entry.state === DIPLOMACY_STATE.DECLARATION && entry.term === 0) {
entry.state = DIPLOMACY_STATE.WAR;
entry.term = DEFAULT_WAR_TERM;
}
+1 -6
View File
@@ -6,12 +6,7 @@ export * from './diplomacy/index.js';
export * from './logging/index.js';
export * from './messages/index.js';
export * from './items/index.js';
export {
ITEM_KEYS,
createItemActionModules,
createItemModuleRegistry,
loadItemModules,
} from './items/index.js';
export { ITEM_KEYS, createItemActionModules, createItemModuleRegistry, loadItemModules } from './items/index.js';
export * from './ports/world.js';
export * from './ports/worldSnapshot.js';
export * from './scenario/index.js';
+1 -3
View File
@@ -29,9 +29,7 @@ export interface StatItemOptions {
extraInfo?: string;
}
export const createStatItemModule = (
options: StatItemOptions
): ItemModule => {
export const createStatItemModule = (options: StatItemOptions): ItemModule => {
const statLabel = resolveStatLabel(options.statName);
const name = `${options.rawName}(+${options.statValue})`;
const baseInfo = `${statLabel} +${options.statValue}`;
@@ -1,14 +1,9 @@
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import {
CheSnipingActivateTrigger,
CheSnipingAttemptTrigger,
} from '@sammo-ts/logic/war/triggers/che_저격.js';
import { CheSnipingActivateTrigger, CheSnipingAttemptTrigger } from '@sammo-ts/logic/war/triggers/che_저격.js';
import { createStatItemModule } from './base.js';
import type { ItemModule } from './types.js';
const raiseType =
BaseWarUnitTrigger.TYPE_ITEM +
BaseWarUnitTrigger.TYPE_DEDUP_TYPE_BASE * 102;
const raiseType = BaseWarUnitTrigger.TYPE_ITEM + BaseWarUnitTrigger.TYPE_DEDUP_TYPE_BASE * 102;
const baseModule = createStatItemModule({
key: 'che_무기_02_단궁',
@@ -29,13 +24,7 @@ export const itemModule: ItemModule = {
return null;
}
return new WarTriggerCaller(
new CheSnipingAttemptTrigger(
context.unit,
raiseType,
0.01,
10,
30
),
new CheSnipingAttemptTrigger(context.unit, raiseType, 0.01, 10, 30),
new CheSnipingActivateTrigger(context.unit, raiseType)
);
},

Some files were not shown because too many files have changed in this diff Show More