feat: add troop model and integrate troop handling in game logic
This commit is contained in:
@@ -121,6 +121,7 @@ export const seedScenarioToDatabase = async (
|
||||
await prisma.event.deleteMany();
|
||||
await prisma.diplomacy.deleteMany();
|
||||
await prisma.general.deleteMany();
|
||||
await prisma.troop.deleteMany();
|
||||
await prisma.city.deleteMany();
|
||||
await prisma.nation.deleteMany();
|
||||
await prisma.worldState.deleteMany();
|
||||
@@ -239,6 +240,16 @@ export const seedScenarioToDatabase = async (
|
||||
});
|
||||
}
|
||||
|
||||
if (seed.troops.length > 0) {
|
||||
await prisma.troop.createMany({
|
||||
data: seed.troops.map((troop) => ({
|
||||
troopLeaderId: troop.id,
|
||||
nationId: troop.nationId,
|
||||
name: troop.name,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
if (seed.diplomacy.length > 0) {
|
||||
await prisma.diplomacy.createMany({
|
||||
data: seed.diplomacy.map((row) => ({
|
||||
|
||||
@@ -121,6 +121,21 @@ const buildNationUpdate = (
|
||||
meta: asJson(nation.meta),
|
||||
});
|
||||
|
||||
const buildTroopUpdate = (
|
||||
troop: ReturnType<InMemoryTurnWorld['consumeDirtyState']>['troops'][number]
|
||||
): Prisma.TroopUpdateInput => ({
|
||||
nationId: troop.nationId,
|
||||
name: troop.name,
|
||||
});
|
||||
|
||||
const buildTroopCreate = (
|
||||
troop: ReturnType<InMemoryTurnWorld['consumeDirtyState']>['troops'][number]
|
||||
): Prisma.TroopCreateManyInput => ({
|
||||
troopLeaderId: troop.id,
|
||||
nationId: troop.nationId,
|
||||
name: troop.name,
|
||||
});
|
||||
|
||||
const buildLogCreateData = (
|
||||
entry: LogEntryDraft,
|
||||
context: { year: number; month: number; at: Date }
|
||||
@@ -161,8 +176,15 @@ export const createDatabaseTurnHooks = async (
|
||||
const hooks: TurnDaemonHooks = {
|
||||
flushChanges: async () => {
|
||||
const state = world.getState();
|
||||
const { generals, cities, nations, logs, createdGenerals } =
|
||||
world.consumeDirtyState();
|
||||
const {
|
||||
generals,
|
||||
cities,
|
||||
nations,
|
||||
troops,
|
||||
logs,
|
||||
createdGenerals,
|
||||
createdTroops,
|
||||
} = world.consumeDirtyState();
|
||||
|
||||
await connector.prisma.worldState.update({
|
||||
where: { id: state.id },
|
||||
@@ -177,12 +199,20 @@ export const createDatabaseTurnHooks = async (
|
||||
const createdIds = new Set(
|
||||
createdGenerals.map((general) => general.id)
|
||||
);
|
||||
const createdTroopIds = new Set(
|
||||
createdTroops.map((troop) => troop.id)
|
||||
);
|
||||
|
||||
if (createdGenerals.length > 0) {
|
||||
await connector.prisma.general.createMany({
|
||||
data: createdGenerals.map(buildGeneralCreate),
|
||||
});
|
||||
}
|
||||
if (createdTroops.length > 0) {
|
||||
await connector.prisma.troop.createMany({
|
||||
data: createdTroops.map(buildTroopCreate),
|
||||
});
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
...generals
|
||||
@@ -205,6 +235,14 @@ export const createDatabaseTurnHooks = async (
|
||||
data: buildNationUpdate(nation),
|
||||
})
|
||||
),
|
||||
...troops
|
||||
.filter((troop) => !createdTroopIds.has(troop.id))
|
||||
.map((troop) =>
|
||||
connector.prisma.troop.update({
|
||||
where: { troopLeaderId: troop.id },
|
||||
data: buildTroopUpdate(troop),
|
||||
})
|
||||
),
|
||||
]);
|
||||
|
||||
if (logs.length > 0) {
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { City, LogEntryDraft, Nation, TurnSchedule } from '@sammo-ts/logic';
|
||||
import type {
|
||||
City,
|
||||
LogEntryDraft,
|
||||
Nation,
|
||||
Troop,
|
||||
TurnSchedule,
|
||||
} from '@sammo-ts/logic';
|
||||
import { getNextTurnAt } from '@sammo-ts/logic';
|
||||
|
||||
import type { TurnCheckpoint } from '../lifecycle/types.js';
|
||||
@@ -22,9 +28,11 @@ export interface GeneralTurnResult {
|
||||
generals: Array<{ id: number; patch: Partial<TurnGeneral> }>;
|
||||
cities: Array<{ id: number; patch: Partial<City> }>;
|
||||
nations: Array<{ id: number; patch: Partial<Nation> }>;
|
||||
troops: Array<{ id: number; patch: Partial<Troop> }>;
|
||||
};
|
||||
created?: {
|
||||
generals: TurnGeneral[];
|
||||
troops?: Troop[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -141,6 +149,11 @@ const applyNationPatch = (base: Nation, patch: Partial<Nation>): Nation => ({
|
||||
meta: patch.meta ? { ...base.meta, ...patch.meta } : base.meta,
|
||||
});
|
||||
|
||||
const applyTroopPatch = (base: Troop, patch: Partial<Troop>): Troop => ({
|
||||
...base,
|
||||
...patch,
|
||||
});
|
||||
|
||||
export class InMemoryTurnWorld {
|
||||
// DB에서 읽어온 월드 상태를 메모리에 고정해 턴 처리를 담당한다.
|
||||
private readonly schedule: TurnSchedule;
|
||||
@@ -149,10 +162,13 @@ export class InMemoryTurnWorld {
|
||||
private readonly generals = new Map<number, TurnGeneral>();
|
||||
private readonly cities = new Map<number, City>();
|
||||
private readonly nations = new Map<number, Nation>();
|
||||
private readonly troops = new Map<number, Troop>();
|
||||
private readonly dirtyGeneralIds = new Set<number>();
|
||||
private readonly dirtyCityIds = new Set<number>();
|
||||
private readonly dirtyNationIds = new Set<number>();
|
||||
private readonly dirtyTroopIds = new Set<number>();
|
||||
private readonly createdGeneralIds = new Set<number>();
|
||||
private readonly createdTroopIds = new Set<number>();
|
||||
private readonly logs: LogEntryDraft[] = [];
|
||||
private checkpoint?: TurnCheckpoint;
|
||||
private state: TurnWorldState;
|
||||
@@ -180,6 +196,9 @@ export class InMemoryTurnWorld {
|
||||
for (const nation of snapshot.nations) {
|
||||
this.nations.set(nation.id, { ...nation });
|
||||
}
|
||||
for (const troop of snapshot.troops) {
|
||||
this.troops.set(troop.id, { ...troop });
|
||||
}
|
||||
}
|
||||
|
||||
getState(): TurnWorldState {
|
||||
@@ -198,6 +217,10 @@ export class InMemoryTurnWorld {
|
||||
return this.nations.get(id) ?? null;
|
||||
}
|
||||
|
||||
getTroopById(id: number): Troop | null {
|
||||
return this.troops.get(id) ?? null;
|
||||
}
|
||||
|
||||
listGenerals(): TurnGeneral[] {
|
||||
return Array.from(this.generals.values()).map((general) => ({
|
||||
...general,
|
||||
@@ -214,6 +237,12 @@ export class InMemoryTurnWorld {
|
||||
}));
|
||||
}
|
||||
|
||||
listTroops(): Troop[] {
|
||||
return Array.from(this.troops.values()).map((troop) => ({
|
||||
...troop,
|
||||
}));
|
||||
}
|
||||
|
||||
setLastTurnTime(turnTime: Date): void {
|
||||
const meta = {
|
||||
...this.state.meta,
|
||||
@@ -326,6 +355,14 @@ export class InMemoryTurnWorld {
|
||||
);
|
||||
this.dirtyNationIds.add(patch.id);
|
||||
}
|
||||
for (const patch of result.patches.troops) {
|
||||
const target = this.troops.get(patch.id);
|
||||
if (!target) {
|
||||
continue;
|
||||
}
|
||||
this.troops.set(patch.id, applyTroopPatch(target, patch.patch));
|
||||
this.dirtyTroopIds.add(patch.id);
|
||||
}
|
||||
}
|
||||
if (result.created) {
|
||||
for (const createdGeneral of result.created.generals) {
|
||||
@@ -336,6 +373,16 @@ export class InMemoryTurnWorld {
|
||||
this.dirtyGeneralIds.add(createdGeneral.id);
|
||||
this.createdGeneralIds.add(createdGeneral.id);
|
||||
}
|
||||
if (result.created.troops) {
|
||||
for (const createdTroop of result.created.troops) {
|
||||
if (this.troops.has(createdTroop.id)) {
|
||||
continue;
|
||||
}
|
||||
this.troops.set(createdTroop.id, { ...createdTroop });
|
||||
this.dirtyTroopIds.add(createdTroop.id);
|
||||
this.createdTroopIds.add(createdTroop.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nextTurnAt;
|
||||
@@ -380,8 +427,10 @@ export class InMemoryTurnWorld {
|
||||
generals: TurnGeneral[];
|
||||
cities: City[];
|
||||
nations: Nation[];
|
||||
troops: Troop[];
|
||||
logs: LogEntryDraft[];
|
||||
createdGenerals: TurnGeneral[];
|
||||
createdTroops: Troop[];
|
||||
} {
|
||||
const generals = Array.from(this.dirtyGeneralIds)
|
||||
.map((id) => this.generals.get(id))
|
||||
@@ -395,13 +444,29 @@ export class InMemoryTurnWorld {
|
||||
const nations = Array.from(this.dirtyNationIds)
|
||||
.map((id) => this.nations.get(id))
|
||||
.filter((nation): nation is Nation => Boolean(nation));
|
||||
const troops = Array.from(this.dirtyTroopIds)
|
||||
.map((id) => this.troops.get(id))
|
||||
.filter((troop): troop is Troop => Boolean(troop));
|
||||
const createdTroops = Array.from(this.createdTroopIds)
|
||||
.map((id) => this.troops.get(id))
|
||||
.filter((troop): troop is Troop => Boolean(troop));
|
||||
const logs = this.logs.splice(0, this.logs.length);
|
||||
|
||||
this.dirtyGeneralIds.clear();
|
||||
this.dirtyCityIds.clear();
|
||||
this.dirtyNationIds.clear();
|
||||
this.dirtyTroopIds.clear();
|
||||
this.createdGeneralIds.clear();
|
||||
this.createdTroopIds.clear();
|
||||
|
||||
return { generals, cities, nations, logs, createdGenerals };
|
||||
return {
|
||||
generals,
|
||||
cities,
|
||||
nations,
|
||||
troops,
|
||||
logs,
|
||||
createdGenerals,
|
||||
createdTroops,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
ScenarioConfig,
|
||||
ScenarioDiplomacy,
|
||||
ScenarioMeta,
|
||||
Troop,
|
||||
} from '@sammo-ts/logic';
|
||||
import {
|
||||
AssignmentActionDefinition,
|
||||
@@ -605,6 +606,7 @@ export const createReservedTurnHandler = (options: {
|
||||
generals: [] as Array<{ id: number; patch: Partial<TurnGeneral> }>,
|
||||
cities: [] as Array<{ id: number; patch: Partial<City> }>,
|
||||
nations: [] as Array<{ id: number; patch: Partial<Nation> }>,
|
||||
troops: [] as Array<{ id: number; patch: Partial<Troop> }>,
|
||||
};
|
||||
const created: TurnGeneral[] = [];
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
ScenarioConfig,
|
||||
ScenarioDiplomacy,
|
||||
ScenarioMeta,
|
||||
Troop,
|
||||
UnitSetDefinition,
|
||||
WorldSnapshot,
|
||||
} from '@sammo-ts/logic';
|
||||
@@ -25,7 +26,10 @@ export interface TurnGeneral extends General {
|
||||
}
|
||||
|
||||
export interface TurnWorldSnapshot
|
||||
extends Omit<WorldSnapshot, 'generals' | 'cities' | 'nations'> {
|
||||
extends Omit<
|
||||
WorldSnapshot,
|
||||
'generals' | 'cities' | 'nations' | 'troops'
|
||||
> {
|
||||
scenarioConfig: ScenarioConfig;
|
||||
scenarioMeta?: ScenarioMeta;
|
||||
map: MapDefinition;
|
||||
@@ -36,6 +40,7 @@ export interface TurnWorldSnapshot
|
||||
generals: TurnGeneral[];
|
||||
cities: City[];
|
||||
nations: Nation[];
|
||||
troops: Troop[];
|
||||
}
|
||||
|
||||
export interface TurnWorldLoadResult {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
General as PrismaGeneral,
|
||||
Nation as PrismaNation,
|
||||
Prisma,
|
||||
Troop as PrismaTroop,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { createPostgresConnector } from '@sammo-ts/infra';
|
||||
@@ -13,6 +14,7 @@ import type {
|
||||
ScenarioConfig,
|
||||
ScenarioDiplomacy,
|
||||
ScenarioMeta,
|
||||
Troop,
|
||||
TriggerValue,
|
||||
} from '@sammo-ts/logic';
|
||||
import { z } from 'zod';
|
||||
@@ -216,6 +218,12 @@ const mapDiplomacyRow = (row: PrismaDiplomacy): ScenarioDiplomacy => ({
|
||||
durationMonths: row.term,
|
||||
});
|
||||
|
||||
const mapTroopRow = (row: PrismaTroop): Troop => ({
|
||||
id: row.troopLeaderId,
|
||||
nationId: row.nationId,
|
||||
name: row.name,
|
||||
});
|
||||
|
||||
export const loadTurnWorldFromDatabase = async (
|
||||
options: TurnWorldLoaderOptions
|
||||
): Promise<TurnWorldLoadResult> => {
|
||||
@@ -233,12 +241,14 @@ export const loadTurnWorldFromDatabase = async (
|
||||
cityRows,
|
||||
nationRows,
|
||||
diplomacyRows,
|
||||
troopRows,
|
||||
eventRows,
|
||||
] = await Promise.all([
|
||||
prisma.general.findMany(),
|
||||
prisma.city.findMany(),
|
||||
prisma.nation.findMany(),
|
||||
prisma.diplomacy.findMany(),
|
||||
prisma.troop.findMany(),
|
||||
prisma.event.findMany({
|
||||
orderBy: [{ priority: 'desc' }, { id: 'asc' }],
|
||||
}),
|
||||
@@ -248,6 +258,7 @@ export const loadTurnWorldFromDatabase = async (
|
||||
const cities = cityRows.map(mapCityRow);
|
||||
const nations = nationRows.map(mapNationRow);
|
||||
const diplomacy = diplomacyRows.map(mapDiplomacyRow);
|
||||
const troops = troopRows.map(mapTroopRow);
|
||||
|
||||
const scenarioConfig = mapScenarioConfig(worldState.config);
|
||||
const mapName = scenarioConfig.environment?.mapName ?? 'che';
|
||||
@@ -305,6 +316,7 @@ export const loadTurnWorldFromDatabase = async (
|
||||
nations,
|
||||
cities,
|
||||
generals,
|
||||
troops,
|
||||
diplomacy,
|
||||
events,
|
||||
initialEvents,
|
||||
|
||||
@@ -147,6 +147,14 @@ model General {
|
||||
@@map("general")
|
||||
}
|
||||
|
||||
model Troop {
|
||||
troopLeaderId Int @id @map("troop_leader")
|
||||
nationId Int @map("nation")
|
||||
name String
|
||||
|
||||
@@map("troop")
|
||||
}
|
||||
|
||||
model GeneralTurn {
|
||||
id Int @id @default(autoincrement())
|
||||
generalId Int @map("general_id")
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
export type GeneralId = number;
|
||||
export type CityId = number;
|
||||
export type NationId = number;
|
||||
export type TroopId = number;
|
||||
|
||||
export type ColorCode = string;
|
||||
|
||||
@@ -95,3 +96,9 @@ export interface Nation {
|
||||
typeCode: string;
|
||||
meta: Record<string, TriggerValue>;
|
||||
}
|
||||
|
||||
export interface Troop {
|
||||
id: TroopId;
|
||||
nationId: NationId;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { City, General, Nation } from '../domain/entities.js';
|
||||
import type { City, General, Nation, Troop } from '../domain/entities.js';
|
||||
import type { ScenarioConfig } from '../scenario/types.js';
|
||||
import type { ScenarioMeta } from '../world/types.js';
|
||||
|
||||
@@ -6,11 +6,13 @@ import type { ScenarioMeta } from '../world/types.js';
|
||||
export interface WorldStateSnapshotSource<
|
||||
GeneralType extends General = General,
|
||||
CityType extends City = City,
|
||||
NationType extends Nation = Nation
|
||||
NationType extends Nation = Nation,
|
||||
TroopType extends Troop = Troop
|
||||
> {
|
||||
listGenerals(): Promise<GeneralType[]>;
|
||||
listCities(): Promise<CityType[]>;
|
||||
listNations(): Promise<NationType[]>;
|
||||
listTroops(): Promise<TroopType[]>;
|
||||
}
|
||||
|
||||
export interface ScenarioConfigSource {
|
||||
|
||||
@@ -676,6 +676,7 @@ export const buildScenarioBootstrap = (
|
||||
nations: seedNations,
|
||||
cities: seedCities,
|
||||
generals: allGeneralSeeds,
|
||||
troops: [],
|
||||
diplomacy: scenario.diplomacy,
|
||||
events: scenario.events,
|
||||
initialEvents: scenario.initialEvents,
|
||||
@@ -689,6 +690,7 @@ export const buildScenarioBootstrap = (
|
||||
nations: domainNations,
|
||||
cities: domainCities,
|
||||
generals: allGenerals,
|
||||
troops: [],
|
||||
diplomacy: scenario.diplomacy,
|
||||
events: scenario.events,
|
||||
initialEvents: scenario.initialEvents,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { City, General, Nation } from '../domain/entities.js';
|
||||
import type { City, General, Nation, Troop } from '../domain/entities.js';
|
||||
import type { ScenarioConfig, ScenarioDiplomacy } from '../scenario/types.js';
|
||||
import type { ScenarioConfigSource, WorldStateSnapshotSource } from '../ports/worldSnapshot.js';
|
||||
import type {
|
||||
@@ -11,9 +11,15 @@ import type {
|
||||
export interface WorldSnapshotLoadInput<
|
||||
GeneralType extends General = General,
|
||||
CityType extends City = City,
|
||||
NationType extends Nation = Nation
|
||||
NationType extends Nation = Nation,
|
||||
TroopType extends Troop = Troop
|
||||
> {
|
||||
worldSource: WorldStateSnapshotSource<GeneralType, CityType, NationType>;
|
||||
worldSource: WorldStateSnapshotSource<
|
||||
GeneralType,
|
||||
CityType,
|
||||
NationType,
|
||||
TroopType
|
||||
>;
|
||||
scenarioConfig?: ScenarioConfig;
|
||||
scenarioMeta?: ScenarioMeta;
|
||||
scenarioSource?: ScenarioConfigSource;
|
||||
@@ -28,9 +34,15 @@ export interface WorldSnapshotLoadInput<
|
||||
export const loadWorldSnapshot = async <
|
||||
GeneralType extends General,
|
||||
CityType extends City,
|
||||
NationType extends Nation
|
||||
NationType extends Nation,
|
||||
TroopType extends Troop
|
||||
>(
|
||||
input: WorldSnapshotLoadInput<GeneralType, CityType, NationType>
|
||||
input: WorldSnapshotLoadInput<
|
||||
GeneralType,
|
||||
CityType,
|
||||
NationType,
|
||||
TroopType
|
||||
>
|
||||
): Promise<WorldSnapshot> => {
|
||||
const { worldSource, scenarioSource } = input;
|
||||
|
||||
@@ -47,10 +59,11 @@ export const loadWorldSnapshot = async <
|
||||
? await scenarioSource.loadScenarioMeta()
|
||||
: undefined);
|
||||
|
||||
const [generals, cities, nations] = await Promise.all([
|
||||
const [generals, cities, nations, troops] = await Promise.all([
|
||||
worldSource.listGenerals(),
|
||||
worldSource.listCities(),
|
||||
worldSource.listNations(),
|
||||
worldSource.listTroops(),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -61,6 +74,7 @@ export const loadWorldSnapshot = async <
|
||||
nations,
|
||||
cities,
|
||||
generals,
|
||||
troops,
|
||||
diplomacy: input.diplomacy ?? [],
|
||||
events: input.events ?? [],
|
||||
initialEvents: input.initialEvents ?? [],
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import type { City, General, Nation, StatBlock } from '../domain/entities.js';
|
||||
import type {
|
||||
City,
|
||||
General,
|
||||
Nation,
|
||||
StatBlock,
|
||||
Troop,
|
||||
} from '../domain/entities.js';
|
||||
import type {
|
||||
ScenarioConfig,
|
||||
ScenarioDiplomacy,
|
||||
@@ -136,6 +142,7 @@ export interface WorldSeedPayload {
|
||||
nations: NationSeed[];
|
||||
cities: CitySeed[];
|
||||
generals: GeneralSeed[];
|
||||
troops: Troop[];
|
||||
diplomacy: ScenarioDiplomacy[];
|
||||
events: unknown[];
|
||||
initialEvents: unknown[];
|
||||
@@ -149,6 +156,7 @@ export interface WorldSnapshot {
|
||||
nations: Nation[];
|
||||
cities: City[];
|
||||
generals: General[];
|
||||
troops: Troop[];
|
||||
diplomacy: ScenarioDiplomacy[];
|
||||
events: unknown[];
|
||||
initialEvents: unknown[];
|
||||
|
||||
Reference in New Issue
Block a user