feat: add new general actions for border return, gifting, disbanding, founding nations, and fierce training

- Implemented '접경귀환' action for returning to a home city within 3 spaces.
- Added '증여' action for transferring resources between generals with constraints.
- Created '해산' action to disband a faction, resetting generals and cities.
- Developed '건국' action for founding a new nation with specified attributes.
- Introduced '맹훈련' action for training generals, enhancing their stats.
- Added tests for the new actions to ensure functionality and constraints are respected.
This commit is contained in:
2026-02-07 06:10:28 +00:00
parent ded90dfb08
commit 952c0d28c6
20 changed files with 2552 additions and 0 deletions
+37
View File
@@ -51,6 +51,41 @@ export const occupiedCity = (options: { allowNeutral?: boolean } = {}): Constrai
},
});
export const notOccupiedCity = (): Constraint => ({
name: 'notOccupiedCity',
requires: (ctx) => {
const reqs: RequirementKey[] = [{ kind: 'general', id: ctx.actorId }];
if (ctx.cityId !== undefined) {
reqs.push({ kind: 'city', id: ctx.cityId });
}
return reqs;
},
test: (ctx, view) => {
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
if (!view.has(generalReq)) {
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
}
const general = view.get(generalReq) as General | null;
if (!general) {
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
}
const cityId = ctx.cityId ?? general.cityId;
const cityReq: RequirementKey = { kind: 'city', id: cityId };
if (!view.has(cityReq)) {
return unknownOrDeny(ctx, [cityReq], '도시 정보가 없습니다.');
}
const city = view.get(cityReq) as City | null;
if (!city) {
return unknownOrDeny(ctx, [cityReq], '도시 정보가 없습니다.');
}
if (city.nationId !== general.nationId) {
return allow();
}
return { kind: 'deny', reason: '아국입니다.' };
},
});
export const occupiedDestCity = (): Constraint => ({
name: 'occupiedDestCity',
requires: (ctx) => {
@@ -499,6 +534,8 @@ export const beNeutralCity = (): Constraint => ({
},
});
export const neutralCity = (): Constraint => beNeutralCity();
export const constructableCity = (): Constraint => ({
name: 'constructableCity',
requires: (ctx) => (ctx.cityId !== undefined ? [{ kind: 'city', id: ctx.cityId }] : []),
+68
View File
@@ -286,6 +286,74 @@ export const noPenalty = (penaltyKey: string): Constraint => ({
},
});
export const allowRebellion = (): Constraint => ({
name: 'allowRebellion',
requires: (ctx) => [
{ kind: 'general', id: ctx.actorId },
{ kind: 'generalList' },
{ kind: 'env', key: 'killturn' },
],
test: (ctx, view) => {
const generalReq: RequirementKey = { kind: 'general', id: ctx.actorId };
if (!view.has(generalReq)) {
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
}
const general = view.get(generalReq) as General | null;
if (!general) {
return unknownOrDeny(ctx, [generalReq], '장수 정보가 없습니다.');
}
if (general.nationId === 0) {
return { kind: 'deny', reason: '재야입니다.' };
}
const generalListReq: RequirementKey = { kind: 'generalList' };
if (!view.has(generalListReq)) {
return unknownOrDeny(ctx, [generalListReq], '장수 정보가 없습니다.');
}
const generals = view.get(generalListReq) as General[] | null;
if (!generals) {
return unknownOrDeny(ctx, [generalListReq], '장수 정보가 없습니다.');
}
const lord = generals.find((entry) => entry.nationId === general.nationId && entry.officerLevel === 12);
if (!lord) {
return unknownOrDeny(ctx, [generalListReq], '군주 정보가 없습니다.');
}
if (lord.id === general.id) {
return { kind: 'deny', reason: '이미 군주입니다.' };
}
const envReq: RequirementKey = { kind: 'env', key: 'killturn' };
if (!view.has(envReq)) {
return unknownOrDeny(ctx, [envReq], '턴 정보가 없습니다.');
}
const rawKillturn = view.get(envReq);
const worldKillturn =
typeof rawKillturn === 'number'
? rawKillturn
: typeof rawKillturn === 'string'
? Number(rawKillturn)
: NaN;
if (!Number.isFinite(worldKillturn)) {
return unknownOrDeny(ctx, [envReq], '턴 정보가 없습니다.');
}
const lordKillturn = typeof lord.meta.killturn === 'number' ? lord.meta.killturn : NaN;
if (!Number.isFinite(lordKillturn)) {
return unknownOrDeny(ctx, [generalListReq], '군주 정보가 없습니다.');
}
if (lordKillturn >= worldKillturn) {
return { kind: 'deny', reason: '군주가 활동중입니다.' };
}
if ([2, 3, 6, 9].includes(lord.npcState)) {
return { kind: 'deny', reason: '군주가 NPC입니다.' };
}
return allow();
},
});
export const reqGeneralValue = (
key: string,
keyNick: string,