feat: 군대 관련 기능 추가 및 명령어 정의 업데이트
This commit is contained in:
@@ -106,13 +106,20 @@ export interface NationRow {
|
|||||||
meta: unknown;
|
meta: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TroopRow {
|
||||||
|
troopLeaderId: number;
|
||||||
|
nationId: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type DatabaseClient = InfraDatabaseClient<
|
export type DatabaseClient = InfraDatabaseClient<
|
||||||
WorldStateRow,
|
WorldStateRow,
|
||||||
GeneralRow,
|
GeneralRow,
|
||||||
CityRow,
|
CityRow,
|
||||||
NationRow,
|
NationRow,
|
||||||
GeneralTurnRow,
|
GeneralTurnRow,
|
||||||
NationTurnRow
|
NationTurnRow,
|
||||||
|
TroopRow
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export interface GameApiContext {
|
export interface GameApiContext {
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export const appRouter = router({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const [city, nation] = await Promise.all([
|
const [city, nation, nationGenerals] = await Promise.all([
|
||||||
general.cityId > 0
|
general.cityId > 0
|
||||||
? ctx.db.city.findUnique({
|
? ctx.db.city.findUnique({
|
||||||
where: { id: general.cityId },
|
where: { id: general.cityId },
|
||||||
@@ -207,6 +207,11 @@ export const appRouter = router({
|
|||||||
where: { id: general.nationId },
|
where: { id: general.nationId },
|
||||||
})
|
})
|
||||||
: null,
|
: null,
|
||||||
|
general.nationId > 0
|
||||||
|
? ctx.db.general.findMany({
|
||||||
|
where: { nationId: general.nationId },
|
||||||
|
})
|
||||||
|
: Promise.resolve(null),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return buildTurnCommandTable({
|
return buildTurnCommandTable({
|
||||||
@@ -214,6 +219,7 @@ export const appRouter = router({
|
|||||||
general,
|
general,
|
||||||
city,
|
city,
|
||||||
nation,
|
nation,
|
||||||
|
nationGenerals,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
reserved: router({
|
reserved: router({
|
||||||
@@ -619,6 +625,98 @@ export const appRouter = router({
|
|||||||
return { msgType, msgId: result.receiverId };
|
return { msgType, msgId: result.receiverId };
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
troop: router({
|
||||||
|
join: authedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
generalId: z.number().int().positive(),
|
||||||
|
troopId: z.number().int().positive(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const general = await ctx.db.general.findUnique({
|
||||||
|
where: { id: input.generalId },
|
||||||
|
});
|
||||||
|
if (!general) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'General not found.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (general.troopId !== 0) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'PRECONDITION_FAILED',
|
||||||
|
message: 'Already in a troop.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (general.nationId <= 0) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'PRECONDITION_FAILED',
|
||||||
|
message: 'General is not part of a nation.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const troop = await ctx.db.troop.findUnique({
|
||||||
|
where: { troopLeaderId: input.troopId },
|
||||||
|
});
|
||||||
|
if (!troop || troop.nationId !== general.nationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'PRECONDITION_FAILED',
|
||||||
|
message: 'Troop is invalid.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.db.general.update({
|
||||||
|
where: { id: general.id },
|
||||||
|
data: { troopId: input.troopId },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
}),
|
||||||
|
exit: authedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
generalId: z.number().int().positive(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const general = await ctx.db.general.findUnique({
|
||||||
|
where: { id: input.generalId },
|
||||||
|
});
|
||||||
|
if (!general) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'General not found.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (general.troopId === 0) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'PRECONDITION_FAILED',
|
||||||
|
message: 'Not in a troop.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (general.troopId !== general.id) {
|
||||||
|
await ctx.db.general.update({
|
||||||
|
where: { id: general.id },
|
||||||
|
data: { troopId: 0 },
|
||||||
|
});
|
||||||
|
return { ok: true, wasLeader: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.db.$transaction([
|
||||||
|
ctx.db.general.updateMany({
|
||||||
|
where: { troopId: general.troopId },
|
||||||
|
data: { troopId: 0 },
|
||||||
|
}),
|
||||||
|
ctx.db.troop.deleteMany({
|
||||||
|
where: { troopLeaderId: general.troopId },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { ok: true, wasLeader: true };
|
||||||
|
}),
|
||||||
|
}),
|
||||||
turnDaemon: router({
|
turnDaemon: router({
|
||||||
run: procedure
|
run: procedure
|
||||||
.input(
|
.input(
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ class MemoryStateView implements StateView {
|
|||||||
switch (req.kind) {
|
switch (req.kind) {
|
||||||
case 'general':
|
case 'general':
|
||||||
return `general:${req.id}`;
|
return `general:${req.id}`;
|
||||||
|
case 'generalList':
|
||||||
|
return 'general:list';
|
||||||
case 'city':
|
case 'city':
|
||||||
return `city:${req.id}`;
|
return `city:${req.id}`;
|
||||||
case 'nation':
|
case 'nation':
|
||||||
@@ -382,7 +384,8 @@ const mapNationRow = (row: NationRow): Nation => ({
|
|||||||
const buildStateView = (
|
const buildStateView = (
|
||||||
general: General,
|
general: General,
|
||||||
city: City | null,
|
city: City | null,
|
||||||
nation: Nation | null
|
nation: Nation | null,
|
||||||
|
generalList: General[] | null
|
||||||
): StateView => {
|
): StateView => {
|
||||||
const view = new MemoryStateView();
|
const view = new MemoryStateView();
|
||||||
view.set({ kind: 'general', id: general.id }, general);
|
view.set({ kind: 'general', id: general.id }, general);
|
||||||
@@ -392,6 +395,9 @@ const buildStateView = (
|
|||||||
if (nation) {
|
if (nation) {
|
||||||
view.set({ kind: 'nation', id: nation.id }, nation);
|
view.set({ kind: 'nation', id: nation.id }, nation);
|
||||||
}
|
}
|
||||||
|
if (generalList) {
|
||||||
|
view.set({ kind: 'generalList' }, generalList);
|
||||||
|
}
|
||||||
return view;
|
return view;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -538,12 +544,16 @@ export const buildTurnCommandTable = async (options: {
|
|||||||
general: GeneralRow;
|
general: GeneralRow;
|
||||||
city: CityRow | null;
|
city: CityRow | null;
|
||||||
nation: NationRow | null;
|
nation: NationRow | null;
|
||||||
|
nationGenerals: GeneralRow[] | null;
|
||||||
}): Promise<TurnCommandTable> => {
|
}): Promise<TurnCommandTable> => {
|
||||||
// 턴 입력 화면에서 쓰는 사전 판단이므로 최소 정보로 가능/불가만 계산한다.
|
// 턴 입력 화면에서 쓰는 사전 판단이므로 최소 정보로 가능/불가만 계산한다.
|
||||||
const general = mapGeneralRow(options.general);
|
const general = mapGeneralRow(options.general);
|
||||||
const city = options.city ? mapCityRow(options.city) : null;
|
const city = options.city ? mapCityRow(options.city) : null;
|
||||||
const nation = options.nation ? mapNationRow(options.nation) : null;
|
const nation = options.nation ? mapNationRow(options.nation) : null;
|
||||||
const view = buildStateView(general, city, nation);
|
const generalList = options.nationGenerals
|
||||||
|
? options.nationGenerals.map(mapGeneralRow)
|
||||||
|
: null;
|
||||||
|
const view = buildStateView(general, city, nation, generalList);
|
||||||
|
|
||||||
const ctx: ConstraintContext = {
|
const ctx: ConstraintContext = {
|
||||||
actorId: general.id,
|
actorId: general.id,
|
||||||
|
|||||||
@@ -3,7 +3,16 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"composite": true
|
"composite": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@sammo-ts/common": ["../../packages/common/src/index.ts"],
|
||||||
|
"@sammo-ts/common/*": ["../../packages/common/src/*"],
|
||||||
|
"@sammo-ts/infra": ["../../packages/infra/src/index.ts"],
|
||||||
|
"@sammo-ts/infra/*": ["../../packages/infra/src/*"],
|
||||||
|
"@sammo-ts/logic": ["../../packages/logic/src/index.ts"],
|
||||||
|
"@sammo-ts/logic/*": ["../../packages/logic/src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"references": [
|
"references": [
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ type WorldView = {
|
|||||||
getGeneralById(id: number): TurnGeneral | null;
|
getGeneralById(id: number): TurnGeneral | null;
|
||||||
getCityById(id: number): City | null;
|
getCityById(id: number): City | null;
|
||||||
getNationById(id: number): Nation | null;
|
getNationById(id: number): Nation | null;
|
||||||
|
getTroopById(id: number): Troop | null;
|
||||||
getDiplomacyEntry(
|
getDiplomacyEntry(
|
||||||
srcNationId: number,
|
srcNationId: number,
|
||||||
destNationId: number
|
destNationId: number
|
||||||
@@ -127,6 +128,7 @@ type WorldView = {
|
|||||||
listGenerals(): TurnGeneral[];
|
listGenerals(): TurnGeneral[];
|
||||||
listCities(): City[];
|
listCities(): City[];
|
||||||
listNations(): Nation[];
|
listNations(): Nation[];
|
||||||
|
listTroops(): Troop[];
|
||||||
listDiplomacy(): TurnDiplomacy[];
|
listDiplomacy(): TurnDiplomacy[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -230,6 +232,7 @@ const createWorldOverlay = (world: InMemoryTurnWorld) => {
|
|||||||
getCityById: (id) => cityOverrides.get(id) ?? world.getCityById(id),
|
getCityById: (id) => cityOverrides.get(id) ?? world.getCityById(id),
|
||||||
getNationById: (id) =>
|
getNationById: (id) =>
|
||||||
nationOverrides.get(id) ?? world.getNationById(id),
|
nationOverrides.get(id) ?? world.getNationById(id),
|
||||||
|
getTroopById: (id) => world.getTroopById(id),
|
||||||
getDiplomacyEntry: (srcNationId, destNationId) =>
|
getDiplomacyEntry: (srcNationId, destNationId) =>
|
||||||
diplomacyOverrides.get(
|
diplomacyOverrides.get(
|
||||||
buildDiplomacyKey(srcNationId, destNationId)
|
buildDiplomacyKey(srcNationId, destNationId)
|
||||||
@@ -246,6 +249,7 @@ const createWorldOverlay = (world: InMemoryTurnWorld) => {
|
|||||||
mergeList(world.listNations(), nationOverrides).map((nation) => ({
|
mergeList(world.listNations(), nationOverrides).map((nation) => ({
|
||||||
...nation,
|
...nation,
|
||||||
})),
|
})),
|
||||||
|
listTroops: () => world.listTroops().map((troop) => ({ ...troop })),
|
||||||
listDiplomacy: () =>
|
listDiplomacy: () =>
|
||||||
mergeDiplomacyList(
|
mergeDiplomacyList(
|
||||||
world.listDiplomacy(),
|
world.listDiplomacy(),
|
||||||
@@ -330,6 +334,8 @@ class WorldStateView implements StateView {
|
|||||||
return this.overrides.general;
|
return this.overrides.general;
|
||||||
}
|
}
|
||||||
return this.world.getGeneralById(req.id);
|
return this.world.getGeneralById(req.id);
|
||||||
|
case 'generalList':
|
||||||
|
return this.world.listGenerals();
|
||||||
case 'destGeneral':
|
case 'destGeneral':
|
||||||
return this.world.getGeneralById(req.id);
|
return this.world.getGeneralById(req.id);
|
||||||
case 'city':
|
case 'city':
|
||||||
|
|||||||
@@ -23,7 +23,13 @@
|
|||||||
|
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"],
|
||||||
|
"@sammo-ts/common": ["../../packages/common/src/index.ts"],
|
||||||
|
"@sammo-ts/common/*": ["../../packages/common/src/*"],
|
||||||
|
"@sammo-ts/infra": ["../../packages/infra/src/index.ts"],
|
||||||
|
"@sammo-ts/infra/*": ["../../packages/infra/src/*"],
|
||||||
|
"@sammo-ts/logic": ["../../packages/logic/src/index.ts"],
|
||||||
|
"@sammo-ts/logic/*": ["../../packages/logic/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||||
|
|||||||
@@ -4,8 +4,12 @@ export interface DatabaseClient<
|
|||||||
CityRow = unknown,
|
CityRow = unknown,
|
||||||
NationRow = unknown,
|
NationRow = unknown,
|
||||||
GeneralTurnRow = unknown,
|
GeneralTurnRow = unknown,
|
||||||
NationTurnRow = unknown
|
NationTurnRow = unknown,
|
||||||
|
TroopRow = unknown
|
||||||
> {
|
> {
|
||||||
|
$transaction<T = unknown>(
|
||||||
|
queries: Array<Promise<T>>
|
||||||
|
): Promise<T[]>;
|
||||||
$queryRaw<T = unknown>(
|
$queryRaw<T = unknown>(
|
||||||
query: TemplateStringsArray,
|
query: TemplateStringsArray,
|
||||||
...values: unknown[]
|
...values: unknown[]
|
||||||
@@ -19,9 +23,20 @@ export interface DatabaseClient<
|
|||||||
where: { userId?: string; npcState?: number | { gt: number } };
|
where: { userId?: string; npcState?: number | { gt: number } };
|
||||||
select?: { name?: boolean; picture?: boolean };
|
select?: { name?: boolean; picture?: boolean };
|
||||||
}): Promise<GeneralRow | null>;
|
}): Promise<GeneralRow | null>;
|
||||||
|
findMany(args: {
|
||||||
|
where?: { nationId?: number };
|
||||||
|
}): Promise<GeneralRow[]>;
|
||||||
count(args?: {
|
count(args?: {
|
||||||
where?: { npcState?: number | { gt: number } };
|
where?: { npcState?: number | { gt: number } };
|
||||||
}): Promise<number>;
|
}): Promise<number>;
|
||||||
|
update(args: {
|
||||||
|
where: { id: number };
|
||||||
|
data: Partial<GeneralRow>;
|
||||||
|
}): Promise<GeneralRow>;
|
||||||
|
updateMany(args: {
|
||||||
|
where: { troopId?: number };
|
||||||
|
data: Partial<GeneralRow>;
|
||||||
|
}): Promise<unknown>;
|
||||||
};
|
};
|
||||||
city: {
|
city: {
|
||||||
findUnique(args: { where: { id: number } }): Promise<CityRow | null>;
|
findUnique(args: { where: { id: number } }): Promise<CityRow | null>;
|
||||||
@@ -63,4 +78,12 @@ export interface DatabaseClient<
|
|||||||
}>;
|
}>;
|
||||||
}): Promise<unknown>;
|
}): Promise<unknown>;
|
||||||
};
|
};
|
||||||
|
troop: {
|
||||||
|
findUnique(args: {
|
||||||
|
where: { troopLeaderId: number };
|
||||||
|
}): Promise<TroopRow | null>;
|
||||||
|
deleteMany(args: {
|
||||||
|
where: { troopLeaderId: number };
|
||||||
|
}): Promise<unknown>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import type { City, General, Nation } 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 { ScenarioConfig } from '@sammo-ts/logic/scenario/types.js';
|
||||||
import type { ScenarioMeta } from '@sammo-ts/logic/world/types.js';
|
import type { ScenarioMeta } from '@sammo-ts/logic/world/types.js';
|
||||||
import type { MapDefinition, UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
import type { MapDefinition, UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
||||||
@@ -32,6 +37,7 @@ export interface ActionContextWorldRef {
|
|||||||
listGenerals(): ActionContextGeneral[];
|
listGenerals(): ActionContextGeneral[];
|
||||||
listCities(): City[];
|
listCities(): City[];
|
||||||
listNations(): Nation[];
|
listNations(): Nation[];
|
||||||
|
listTroops(): Troop[];
|
||||||
listDiplomacy(): Array<{
|
listDiplomacy(): Array<{
|
||||||
fromNationId: number;
|
fromNationId: number;
|
||||||
toNationId: number;
|
toNationId: number;
|
||||||
@@ -48,6 +54,7 @@ export interface ActionContextWorldRef {
|
|||||||
getGeneralById(id: number): ActionContextGeneral | null;
|
getGeneralById(id: number): ActionContextGeneral | null;
|
||||||
getCityById(id: number): City | null;
|
getCityById(id: number): City | null;
|
||||||
getNationById(id: number): Nation | null;
|
getNationById(id: number): Nation | null;
|
||||||
|
getTroopById(id: number): Troop | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionContextOptions {
|
export interface ActionContextOptions {
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import type { General, GeneralTriggerState, Troop } from '@sammo-ts/logic/domain/entities.js';
|
||||||
|
import type {
|
||||||
|
Constraint,
|
||||||
|
ConstraintContext,
|
||||||
|
} from '@sammo-ts/logic/constraints/types.js';
|
||||||
|
import {
|
||||||
|
mustBeTroopLeader,
|
||||||
|
notBeNeutral,
|
||||||
|
occupiedCity,
|
||||||
|
reqTroopMembers,
|
||||||
|
suppliedCity,
|
||||||
|
} from '@sammo-ts/logic/constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
|
import type {
|
||||||
|
GeneralActionEffect,
|
||||||
|
GeneralActionOutcome,
|
||||||
|
GeneralActionResolveContext,
|
||||||
|
} 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 { GeneralTurnCommandSpec } from './index.js';
|
||||||
|
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||||
|
import { JosaUtil } from '@sammo-ts/common';
|
||||||
|
import { increaseMetaNumber } from '@sammo-ts/logic/war/utils.js';
|
||||||
|
|
||||||
|
export interface AssemblyArgs {}
|
||||||
|
|
||||||
|
export interface AssemblyResolveContext<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> extends GeneralActionResolveContext<TriggerState> {
|
||||||
|
troop: Troop | null;
|
||||||
|
troopMembers: Array<General<TriggerState>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_NAME = '집합';
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> implements GeneralActionDefinition<
|
||||||
|
TriggerState,
|
||||||
|
AssemblyArgs,
|
||||||
|
AssemblyResolveContext<TriggerState>
|
||||||
|
> {
|
||||||
|
public readonly key = 'che_집합';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
|
parseArgs(_raw: unknown): AssemblyArgs | null {
|
||||||
|
void _raw;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(
|
||||||
|
_ctx: ConstraintContext,
|
||||||
|
_args: AssemblyArgs
|
||||||
|
): Constraint[] {
|
||||||
|
return [
|
||||||
|
notBeNeutral(),
|
||||||
|
occupiedCity(),
|
||||||
|
suppliedCity(),
|
||||||
|
mustBeTroopLeader(),
|
||||||
|
reqTroopMembers(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
context: AssemblyResolveContext<TriggerState>,
|
||||||
|
_args: AssemblyArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
const city = context.city;
|
||||||
|
if (!city) {
|
||||||
|
context.addLog('도시 정보가 없어 집합을 진행할 수 없습니다.');
|
||||||
|
return { effects: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const general = context.general;
|
||||||
|
const troopName = context.troop?.name ?? '부대';
|
||||||
|
const cityName = city.name;
|
||||||
|
const josaRo = JosaUtil.pick(cityName, '로');
|
||||||
|
|
||||||
|
context.addLog(`<G><b>${cityName}</b></>에서 집합을 실시했습니다.`);
|
||||||
|
|
||||||
|
const effects: Array<GeneralActionEffect<TriggerState>> = [];
|
||||||
|
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(
|
||||||
|
createLogEffect(
|
||||||
|
`${troopName} 부대원들은 <G><b>${cityName}</b></>${josaRo} 집합되었습니다.`,
|
||||||
|
{
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.PLAIN,
|
||||||
|
generalId: member.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
general.experience += 70;
|
||||||
|
general.dedication += 100;
|
||||||
|
increaseMetaNumber(general.meta, 'leadership_exp', 1);
|
||||||
|
|
||||||
|
return { effects };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
) ?? [];
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
troop,
|
||||||
|
troopMembers,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const commandSpec: GeneralTurnCommandSpec = {
|
||||||
|
key: 'che_집합',
|
||||||
|
category: '군사',
|
||||||
|
reqArg: false,
|
||||||
|
args: {},
|
||||||
|
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||||
|
};
|
||||||
@@ -21,6 +21,7 @@ export const GENERAL_TURN_COMMAND_KEYS = [
|
|||||||
'che_수비강화',
|
'che_수비강화',
|
||||||
'che_성벽보수',
|
'che_성벽보수',
|
||||||
'che_화계',
|
'che_화계',
|
||||||
|
'che_집합',
|
||||||
'che_인재탐색',
|
'che_인재탐색',
|
||||||
'che_징병',
|
'che_징병',
|
||||||
'휴식',
|
'휴식',
|
||||||
@@ -61,6 +62,7 @@ const defaultImporters: Record<
|
|||||||
che_수비강화: async () => import('./che_수비강화.js'),
|
che_수비강화: async () => import('./che_수비강화.js'),
|
||||||
che_성벽보수: async () => import('./che_성벽보수.js'),
|
che_성벽보수: async () => import('./che_성벽보수.js'),
|
||||||
che_화계: async () => import('./che_화계.js'),
|
che_화계: async () => import('./che_화계.js'),
|
||||||
|
che_집합: async () => import('./che_집합.js'),
|
||||||
che_인재탐색: async () => import('./che_인재탐색.js'),
|
che_인재탐색: async () => import('./che_인재탐색.js'),
|
||||||
che_징병: async () => import('./che_징병.js'),
|
che_징병: async () => import('./che_징병.js'),
|
||||||
휴식: async () => import('./휴식.js'),
|
휴식: async () => import('./휴식.js'),
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
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,
|
||||||
|
existsDestGeneral,
|
||||||
|
friendlyDestGeneral,
|
||||||
|
notBeNeutral,
|
||||||
|
} from '@sammo-ts/logic/constraints/presets.js';
|
||||||
|
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||||
|
import type {
|
||||||
|
GeneralActionEffect,
|
||||||
|
GeneralActionOutcome,
|
||||||
|
GeneralActionResolveContext,
|
||||||
|
} 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';
|
||||||
|
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||||
|
import { JosaUtil } from '@sammo-ts/common';
|
||||||
|
|
||||||
|
export interface TroopKickArgs {
|
||||||
|
destGeneralId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TroopKickResolveContext<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> extends GeneralActionResolveContext<TriggerState> {
|
||||||
|
destGeneral: General<TriggerState>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTION_NAME = '부대 탈퇴 지시';
|
||||||
|
|
||||||
|
export class ActionDefinition<
|
||||||
|
TriggerState extends GeneralTriggerState = GeneralTriggerState
|
||||||
|
> implements GeneralActionDefinition<
|
||||||
|
TriggerState,
|
||||||
|
TroopKickArgs,
|
||||||
|
TroopKickResolveContext<TriggerState>
|
||||||
|
> {
|
||||||
|
public readonly key = 'che_부대탈퇴지시';
|
||||||
|
public readonly name = ACTION_NAME;
|
||||||
|
|
||||||
|
parseArgs(raw: unknown): TroopKickArgs | null {
|
||||||
|
if (!raw || typeof raw !== 'object') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const data = raw as { destGeneralId?: unknown };
|
||||||
|
if (typeof data.destGeneralId !== 'number') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (data.destGeneralId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { destGeneralId: data.destGeneralId };
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConstraints(
|
||||||
|
ctx: ConstraintContext,
|
||||||
|
_args: TroopKickArgs
|
||||||
|
): Constraint[] {
|
||||||
|
if (ctx.destGeneralId !== undefined && ctx.destGeneralId === ctx.actorId) {
|
||||||
|
return [alwaysFail('본인입니다')];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
notBeNeutral(),
|
||||||
|
beChief(),
|
||||||
|
existsDestGeneral(),
|
||||||
|
friendlyDestGeneral(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(
|
||||||
|
context: TroopKickResolveContext<TriggerState>,
|
||||||
|
_args: TroopKickArgs
|
||||||
|
): GeneralActionOutcome<TriggerState> {
|
||||||
|
const general = context.general;
|
||||||
|
const destGeneral = context.destGeneral;
|
||||||
|
const destGeneralName = destGeneral.name;
|
||||||
|
const josaUn = JosaUtil.pick(destGeneralName, '은');
|
||||||
|
const effects: Array<GeneralActionEffect<TriggerState>> = [];
|
||||||
|
|
||||||
|
if (destGeneral.troopId === 0) {
|
||||||
|
context.addLog(
|
||||||
|
`<Y>${destGeneralName}</>${josaUn} 부대원이 아닙니다.`
|
||||||
|
);
|
||||||
|
return { effects };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destGeneral.troopId === destGeneral.id) {
|
||||||
|
context.addLog(
|
||||||
|
`<Y>${destGeneralName}</>${josaUn} 부대장입니다.`
|
||||||
|
);
|
||||||
|
return { effects };
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
effects.push(
|
||||||
|
createLogEffect(
|
||||||
|
`<Y>${general.name}</>에게 부대 탈퇴를 지시 받았습니다.`,
|
||||||
|
{
|
||||||
|
scope: LogScope.GENERAL,
|
||||||
|
category: LogCategory.ACTION,
|
||||||
|
format: LogFormat.PLAIN,
|
||||||
|
generalId: destGeneral.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { effects };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||||
|
const destGeneralId = options.actionArgs.destGeneralId;
|
||||||
|
if (typeof destGeneralId !== 'number') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const destGeneral = options.worldRef?.getGeneralById(destGeneralId);
|
||||||
|
if (!destGeneral) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
destGeneral,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const commandSpec: NationTurnCommandSpec = {
|
||||||
|
key: 'che_부대탈퇴지시',
|
||||||
|
category: '인사',
|
||||||
|
reqArg: true,
|
||||||
|
args: { destGeneralId: 0 },
|
||||||
|
createDefinition: (_env: TurnCommandEnv) => new ActionDefinition(),
|
||||||
|
};
|
||||||
@@ -3,6 +3,7 @@ import type { TurnCommandModule, TurnCommandSpecBase } from '@sammo-ts/logic/act
|
|||||||
export const NATION_TURN_COMMAND_KEYS = [
|
export const NATION_TURN_COMMAND_KEYS = [
|
||||||
'휴식',
|
'휴식',
|
||||||
'che_포상',
|
'che_포상',
|
||||||
|
'che_부대탈퇴지시',
|
||||||
'che_발령',
|
'che_발령',
|
||||||
'che_선전포고',
|
'che_선전포고',
|
||||||
'che_불가침제의',
|
'che_불가침제의',
|
||||||
@@ -33,6 +34,7 @@ const defaultImporters: Record<
|
|||||||
> = {
|
> = {
|
||||||
휴식: async () => import('./휴식.js'),
|
휴식: async () => import('./휴식.js'),
|
||||||
che_포상: async () => import('./che_포상.js'),
|
che_포상: async () => import('./che_포상.js'),
|
||||||
|
che_부대탈퇴지시: async () => import('./che_부대탈퇴지시.js'),
|
||||||
che_발령: async () => import('./che_발령.js'),
|
che_발령: async () => import('./che_발령.js'),
|
||||||
che_선전포고: async () => import('./che_선전포고.js'),
|
che_선전포고: async () => import('./che_선전포고.js'),
|
||||||
che_불가침제의: async () => import('./che_불가침제의.js'),
|
che_불가침제의: async () => import('./che_불가침제의.js'),
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ export * from './general.js';
|
|||||||
export * from './helpers.js';
|
export * from './helpers.js';
|
||||||
export * from './misc.js';
|
export * from './misc.js';
|
||||||
export * from './nation.js';
|
export * from './nation.js';
|
||||||
|
export * from './troop.js';
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type { General } from '@sammo-ts/logic/domain/entities.js';
|
||||||
|
import { allow, unknownOrDeny } from './helpers.js';
|
||||||
|
import type { Constraint, RequirementKey } from './types.js';
|
||||||
|
|
||||||
|
export const mustBeTroopLeader = (): Constraint => ({
|
||||||
|
name: 'MustBeTroopLeader',
|
||||||
|
requires: (ctx) => [{ kind: 'general', id: ctx.actorId }],
|
||||||
|
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.id === general.troopId) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: '부대장이 아닙니다.' };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const reqTroopMembers = (): Constraint => ({
|
||||||
|
name: 'ReqTroopMembers',
|
||||||
|
requires: (ctx) => [
|
||||||
|
{ kind: 'general', id: ctx.actorId },
|
||||||
|
{ kind: 'generalList' },
|
||||||
|
],
|
||||||
|
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 listReq: RequirementKey = { kind: 'generalList' };
|
||||||
|
if (!view.has(listReq)) {
|
||||||
|
return unknownOrDeny(ctx, [listReq], '장수 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const generals = view.get(listReq) as General[] | null;
|
||||||
|
if (!generals) {
|
||||||
|
return unknownOrDeny(ctx, [listReq], '장수 정보가 없습니다.');
|
||||||
|
}
|
||||||
|
const hasMember = generals.some(
|
||||||
|
(entry) =>
|
||||||
|
entry.troopId === general.troopId && entry.id !== general.id
|
||||||
|
);
|
||||||
|
if (hasMember) {
|
||||||
|
return allow();
|
||||||
|
}
|
||||||
|
return { kind: 'deny', reason: '집합 가능한 부대원이 없습니다.' };
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -5,6 +5,7 @@ export type ConstraintResult =
|
|||||||
|
|
||||||
export type RequirementKey =
|
export type RequirementKey =
|
||||||
| { kind: 'general'; id: number }
|
| { kind: 'general'; id: number }
|
||||||
|
| { kind: 'generalList' }
|
||||||
| { kind: 'city'; id: number }
|
| { kind: 'city'; id: number }
|
||||||
| { kind: 'nation'; id: number }
|
| { kind: 'nation'; id: number }
|
||||||
| { kind: 'destGeneral'; id: number }
|
| { kind: 'destGeneral'; id: number }
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"che_수비강화",
|
"che_수비강화",
|
||||||
"che_성벽보수",
|
"che_성벽보수",
|
||||||
"che_화계",
|
"che_화계",
|
||||||
|
"che_집합",
|
||||||
"che_인재탐색",
|
"che_인재탐색",
|
||||||
"che_징병",
|
"che_징병",
|
||||||
"휴식"
|
"휴식"
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"nation": [
|
"nation": [
|
||||||
"휴식",
|
"휴식",
|
||||||
"che_포상",
|
"che_포상",
|
||||||
|
"che_부대탈퇴지시",
|
||||||
"che_발령",
|
"che_발령",
|
||||||
"che_선전포고",
|
"che_선전포고",
|
||||||
"che_불가침제의",
|
"che_불가침제의",
|
||||||
|
|||||||
Reference in New Issue
Block a user