feat: add atmos property to General entity and update related logic
- Added `atmos` property to the `General` interface in `entities.ts`. - Initialized `atmos` to 0 in the `buildGeneralSeeds` function in `bootstrap.ts`. - Updated exports in `index.ts` to include `unitSet.js`. - Introduced new types for crew types and their requirements in `types.ts`. - Implemented `unitSetLoader.ts` for loading unit set definitions from JSON files. - Created `che_징병.ts` action for recruiting crew types with cost and training calculations. - Developed `unitSet.ts` for handling crew type availability and requirements. - Added tests for crew type availability and unit set parsing in `crewType.test.ts`.
This commit is contained in:
@@ -44,6 +44,7 @@ export interface GeneralRow {
|
||||
crew: number;
|
||||
crewTypeId: number;
|
||||
train: number;
|
||||
atmos: number;
|
||||
age: number;
|
||||
npcState: number;
|
||||
meta: unknown;
|
||||
@@ -79,12 +80,15 @@ export interface CityRow {
|
||||
commerceMax: number;
|
||||
security: number;
|
||||
securityMax: number;
|
||||
trust: number;
|
||||
trade: number;
|
||||
supplyState: number;
|
||||
frontState: number;
|
||||
defence: number;
|
||||
defenceMax: number;
|
||||
wall: number;
|
||||
wallMax: number;
|
||||
region: number;
|
||||
meta: unknown;
|
||||
}
|
||||
|
||||
@@ -95,6 +99,7 @@ export interface NationRow {
|
||||
capitalCityId: number | null;
|
||||
gold: number;
|
||||
rice: number;
|
||||
tech: number;
|
||||
level: number;
|
||||
typeCode: string;
|
||||
meta: unknown;
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
evaluateConstraints,
|
||||
FireAttackActionDefinition,
|
||||
NationRestActionDefinition,
|
||||
RecruitActionDefinition,
|
||||
RestActionDefinition,
|
||||
TalentScoutActionDefinition,
|
||||
VolunteerRecruitActionDefinition,
|
||||
@@ -324,6 +325,7 @@ const mapGeneralRow = (row: GeneralRow): General => ({
|
||||
crew: row.crew,
|
||||
crewTypeId: row.crewTypeId,
|
||||
train: row.train,
|
||||
atmos: row.atmos,
|
||||
age: row.age,
|
||||
npcState: row.npcState,
|
||||
triggerState: {
|
||||
@@ -354,7 +356,12 @@ const mapCityRow = (row: CityRow): City => ({
|
||||
defenceMax: row.defenceMax,
|
||||
wall: row.wall,
|
||||
wallMax: row.wallMax,
|
||||
meta: asTriggerRecord(row.meta),
|
||||
meta: {
|
||||
...asTriggerRecord(row.meta),
|
||||
trust: row.trust,
|
||||
trade: row.trade,
|
||||
region: row.region,
|
||||
},
|
||||
});
|
||||
|
||||
const mapNationRow = (row: NationRow): Nation => ({
|
||||
@@ -368,7 +375,10 @@ const mapNationRow = (row: NationRow): Nation => ({
|
||||
power: 0,
|
||||
level: row.level,
|
||||
typeCode: row.typeCode,
|
||||
meta: asTriggerRecord(row.meta),
|
||||
meta: {
|
||||
...asTriggerRecord(row.meta),
|
||||
tech: row.tech,
|
||||
},
|
||||
});
|
||||
|
||||
const buildStateView = (
|
||||
@@ -465,6 +475,12 @@ const buildEntries = (env: CommandEnv): {
|
||||
reqArg: false,
|
||||
args: {},
|
||||
},
|
||||
{
|
||||
category: '내정',
|
||||
definition: new RecruitActionDefinition(emptyModules, {}),
|
||||
reqArg: true,
|
||||
args: {},
|
||||
},
|
||||
{
|
||||
category: '계략',
|
||||
definition: new FireAttackActionDefinition(emptyModules, {
|
||||
|
||||
@@ -11,6 +11,8 @@ import type { MapLoaderOptions } from './mapLoader.js';
|
||||
import { loadMapDefinitionByName } from './mapLoader.js';
|
||||
import type { ScenarioLoaderOptions } from './scenarioLoader.js';
|
||||
import { loadScenarioDefinitionById } from './scenarioLoader.js';
|
||||
import type { UnitSetLoaderOptions } from './unitSetLoader.js';
|
||||
import { loadUnitSetDefinitionByName } from './unitSetLoader.js';
|
||||
|
||||
const DEFAULT_TICK_SECONDS = 120 * 60;
|
||||
const DEFAULT_GENERAL_GOLD = 1000;
|
||||
@@ -21,6 +23,7 @@ export interface ScenarioSeedOptions {
|
||||
databaseUrl: string;
|
||||
scenarioOptions?: ScenarioLoaderOptions;
|
||||
mapOptions?: MapLoaderOptions;
|
||||
unitSetOptions?: UnitSetLoaderOptions;
|
||||
resetTables?: boolean;
|
||||
now?: Date;
|
||||
tickSeconds?: number;
|
||||
@@ -97,10 +100,15 @@ export const seedScenarioToDatabase = async (
|
||||
scenario.config.environment.mapName,
|
||||
options.mapOptions
|
||||
);
|
||||
const unitSet = await loadUnitSetDefinitionByName(
|
||||
scenario.config.environment.unitSet,
|
||||
options.unitSetOptions
|
||||
);
|
||||
|
||||
const { seed, warnings } = buildScenarioBootstrap({
|
||||
scenario,
|
||||
map,
|
||||
unitSet,
|
||||
options: {
|
||||
includeNeutralNationInSeed:
|
||||
options.includeNeutralNationInSeed ?? true,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { parseUnitSetDefinition, type UnitSetDefinition } from '@sammo-ts/logic';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const DEFAULT_UNIT_SET_ROOT = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'resources',
|
||||
'unitset'
|
||||
);
|
||||
|
||||
export interface UnitSetLoaderOptions {
|
||||
unitSetRoot?: string;
|
||||
filePrefix?: string;
|
||||
}
|
||||
|
||||
const readJsonFile = async (filePath: string): Promise<unknown> => {
|
||||
const raw = await fs.readFile(filePath, 'utf8');
|
||||
return JSON.parse(raw) as unknown;
|
||||
};
|
||||
|
||||
const resolveUnitSetRoot = (options?: UnitSetLoaderOptions): string =>
|
||||
options?.unitSetRoot ?? DEFAULT_UNIT_SET_ROOT;
|
||||
|
||||
export const resolveUnitSetDefinitionPath = (
|
||||
unitSetName: string,
|
||||
options?: UnitSetLoaderOptions
|
||||
): string => {
|
||||
const prefix = options?.filePrefix ?? 'unitset_';
|
||||
return path.resolve(resolveUnitSetRoot(options), `${prefix}${unitSetName}.json`);
|
||||
};
|
||||
|
||||
export const loadUnitSetDefinition = async (
|
||||
unitSetPath: string
|
||||
): Promise<UnitSetDefinition> => {
|
||||
const raw = await readJsonFile(unitSetPath);
|
||||
return parseUnitSetDefinition(raw);
|
||||
};
|
||||
|
||||
export const loadUnitSetDefinitionByName = async (
|
||||
unitSetName: string,
|
||||
options?: UnitSetLoaderOptions
|
||||
): Promise<UnitSetDefinition> => {
|
||||
const unitSetPath = resolveUnitSetDefinitionPath(unitSetName, options);
|
||||
return loadUnitSetDefinition(unitSetPath);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,14 @@ const asJson = (value: unknown): Prisma.InputJsonValue =>
|
||||
const toCode = (value: string | null | undefined): string =>
|
||||
value && value !== 'None' ? value : 'None';
|
||||
|
||||
const readMetaNumber = (
|
||||
meta: Record<string, unknown>,
|
||||
key: string
|
||||
): number | null => {
|
||||
const value = meta[key];
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
||||
};
|
||||
|
||||
const buildGeneralUpdate = (
|
||||
general: ReturnType<InMemoryTurnWorld['consumeDirtyState']>['generals'][number]
|
||||
): Prisma.GeneralUpdateInput => ({
|
||||
@@ -37,6 +45,7 @@ const buildGeneralUpdate = (
|
||||
crew: general.crew,
|
||||
crewTypeId: general.crewTypeId,
|
||||
train: general.train,
|
||||
atmos: general.atmos,
|
||||
age: general.age,
|
||||
npcState: general.npcState,
|
||||
horseCode: toCode(general.role.items.horse),
|
||||
@@ -72,6 +81,7 @@ const buildGeneralCreate = (
|
||||
crew: general.crew,
|
||||
crewTypeId: general.crewTypeId,
|
||||
train: general.train,
|
||||
atmos: general.atmos,
|
||||
age: general.age,
|
||||
horseCode: toCode(general.role.items.horse),
|
||||
weaponCode: toCode(general.role.items.weapon),
|
||||
@@ -87,26 +97,45 @@ const buildGeneralCreate = (
|
||||
|
||||
const buildCityUpdate = (
|
||||
city: ReturnType<InMemoryTurnWorld['consumeDirtyState']>['cities'][number]
|
||||
): Prisma.CityUpdateInput => ({
|
||||
name: city.name,
|
||||
nationId: city.nationId,
|
||||
level: city.level,
|
||||
population: city.population,
|
||||
populationMax: city.populationMax,
|
||||
agriculture: city.agriculture,
|
||||
agricultureMax: city.agricultureMax,
|
||||
commerce: city.commerce,
|
||||
commerceMax: city.commerceMax,
|
||||
security: city.security,
|
||||
securityMax: city.securityMax,
|
||||
supplyState: city.supplyState,
|
||||
frontState: city.frontState,
|
||||
defence: city.defence,
|
||||
defenceMax: city.defenceMax,
|
||||
wall: city.wall,
|
||||
wallMax: city.wallMax,
|
||||
meta: asJson(city.meta),
|
||||
});
|
||||
): Prisma.CityUpdateInput => {
|
||||
const meta = city.meta as Record<string, unknown>;
|
||||
const trust = readMetaNumber(meta, 'trust');
|
||||
const trade = readMetaNumber(meta, 'trade');
|
||||
const region = readMetaNumber(meta, 'region');
|
||||
|
||||
const data: Prisma.CityUpdateInput = {
|
||||
name: city.name,
|
||||
nationId: city.nationId,
|
||||
level: city.level,
|
||||
population: city.population,
|
||||
populationMax: city.populationMax,
|
||||
agriculture: city.agriculture,
|
||||
agricultureMax: city.agricultureMax,
|
||||
commerce: city.commerce,
|
||||
commerceMax: city.commerceMax,
|
||||
security: city.security,
|
||||
securityMax: city.securityMax,
|
||||
supplyState: city.supplyState,
|
||||
frontState: city.frontState,
|
||||
defence: city.defence,
|
||||
defenceMax: city.defenceMax,
|
||||
wall: city.wall,
|
||||
wallMax: city.wallMax,
|
||||
meta: asJson(city.meta),
|
||||
};
|
||||
|
||||
if (trust !== null) {
|
||||
data.trust = trust;
|
||||
}
|
||||
if (trade !== null) {
|
||||
data.trade = trade;
|
||||
}
|
||||
if (region !== null) {
|
||||
data.region = region;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const buildNationUpdate = (
|
||||
nation: ReturnType<InMemoryTurnWorld['consumeDirtyState']>['nations'][number]
|
||||
|
||||
@@ -4,11 +4,13 @@ import type {
|
||||
GeneralActionDefinition,
|
||||
GeneralActionResolveContext,
|
||||
LogEntryDraft,
|
||||
MapDefinition,
|
||||
Nation,
|
||||
ScenarioConfig,
|
||||
ScenarioDiplomacy,
|
||||
ScenarioMeta,
|
||||
Troop,
|
||||
UnitSetDefinition,
|
||||
} from '@sammo-ts/logic';
|
||||
import {
|
||||
AssignmentActionDefinition,
|
||||
@@ -17,6 +19,7 @@ import {
|
||||
evaluateConstraints,
|
||||
FireAttackActionDefinition,
|
||||
NationRestActionDefinition,
|
||||
RecruitActionDefinition,
|
||||
resolveGeneralAction,
|
||||
RestActionDefinition,
|
||||
TalentScoutActionDefinition,
|
||||
@@ -110,7 +113,10 @@ const resolveOptionalString = (
|
||||
return null;
|
||||
};
|
||||
|
||||
const buildCommandEnv = (config: ScenarioConfig): CommandEnv => {
|
||||
const buildCommandEnv = (
|
||||
config: ScenarioConfig,
|
||||
unitSet?: UnitSetDefinition
|
||||
): CommandEnv => {
|
||||
const constValues = asRecord(config.const);
|
||||
|
||||
return {
|
||||
@@ -167,7 +173,7 @@ const buildCommandEnv = (config: ScenarioConfig): CommandEnv => {
|
||||
defaultCrewTypeId: resolveNumber(
|
||||
constValues,
|
||||
['defaultCrewTypeId'],
|
||||
DEFAULT_CREW_TYPE_ID
|
||||
unitSet?.defaultCrewTypeId ?? DEFAULT_CREW_TYPE_ID
|
||||
),
|
||||
defaultSpecialDomestic: resolveOptionalString(
|
||||
constValues,
|
||||
@@ -422,6 +428,7 @@ const buildGeneralDefinitions = (
|
||||
definitions.set('che_화계', new FireAttackActionDefinition([], env));
|
||||
definitions.set('che_인재탐색', new TalentScoutActionDefinition([], env));
|
||||
definitions.set('che_의병모집', new VolunteerRecruitActionDefinition([], env));
|
||||
definitions.set('che_징병', new RecruitActionDefinition([], {}));
|
||||
definitions.set('휴식', new RestActionDefinition());
|
||||
return definitions;
|
||||
};
|
||||
@@ -468,6 +475,8 @@ const buildActionContext = (
|
||||
options: {
|
||||
world: TurnWorldState;
|
||||
scenarioMeta?: ScenarioMeta;
|
||||
map?: MapDefinition;
|
||||
unitSet?: UnitSetDefinition;
|
||||
worldRef: InMemoryTurnWorld | null;
|
||||
actionArgs: Record<string, unknown>;
|
||||
createGeneralId: () => number;
|
||||
@@ -535,6 +544,18 @@ const buildActionContext = (
|
||||
destGeneralTurnTime: destGeneral.turnTime,
|
||||
};
|
||||
}
|
||||
case 'che_징병':
|
||||
if (!options.map || !options.unitSet) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
map: options.map,
|
||||
unitSet: options.unitSet,
|
||||
cities: options.worldRef?.listCities() ?? [],
|
||||
currentYear: options.world.currentYear,
|
||||
startYear: resolveStartYear(options.world, options.scenarioMeta),
|
||||
};
|
||||
default:
|
||||
return base;
|
||||
}
|
||||
@@ -573,9 +594,11 @@ export const createReservedTurnHandler = (options: {
|
||||
scenarioConfig: ScenarioConfig;
|
||||
scenarioMeta?: ScenarioMeta;
|
||||
diplomacy: ScenarioDiplomacy[];
|
||||
map?: MapDefinition;
|
||||
unitSet?: UnitSetDefinition;
|
||||
getWorld: () => InMemoryTurnWorld | null;
|
||||
}): GeneralTurnHandler => {
|
||||
const env = buildCommandEnv(options.scenarioConfig);
|
||||
const env = buildCommandEnv(options.scenarioConfig, options.unitSet);
|
||||
const generalDefinitions = buildGeneralDefinitions(env);
|
||||
const nationDefinitions = buildNationDefinitions(env);
|
||||
const generalFallback = generalDefinitions.get(DEFAULT_ACTION)!;
|
||||
@@ -597,10 +620,12 @@ export const createReservedTurnHandler = (options: {
|
||||
return {
|
||||
execute(context): GeneralTurnResult {
|
||||
const worldRef = options.getWorld();
|
||||
const constraintEnv = resolveConstraintEnv(
|
||||
context.world,
|
||||
options.scenarioMeta
|
||||
);
|
||||
const constraintEnv = {
|
||||
...resolveConstraintEnv(context.world, options.scenarioMeta),
|
||||
...(options.map ? { map: options.map } : {}),
|
||||
...(options.unitSet ? { unitSet: options.unitSet } : {}),
|
||||
cities: worldRef?.listCities() ?? [],
|
||||
};
|
||||
const logs: LogEntryDraft[] = [];
|
||||
const patches = {
|
||||
generals: [] as Array<{ id: number; patch: Partial<TurnGeneral> }>,
|
||||
@@ -694,6 +719,8 @@ export const createReservedTurnHandler = (options: {
|
||||
let specificContext = buildActionContext(actionKey, baseContext, {
|
||||
world: context.world,
|
||||
scenarioMeta: options.scenarioMeta,
|
||||
map: options.map,
|
||||
unitSet: options.unitSet,
|
||||
worldRef,
|
||||
actionArgs: actionArgsRecord,
|
||||
createGeneralId,
|
||||
|
||||
@@ -88,6 +88,8 @@ export const createTurnDaemonRuntime = async (
|
||||
scenarioConfig: snapshot.scenarioConfig,
|
||||
scenarioMeta: snapshot.scenarioMeta,
|
||||
diplomacy: snapshot.diplomacy,
|
||||
map: snapshot.map,
|
||||
unitSet: snapshot.unitSet,
|
||||
getWorld: () => worldRef,
|
||||
}),
|
||||
calendarHandler: options.calendarHandler,
|
||||
|
||||
@@ -22,11 +22,14 @@ import { z } from 'zod';
|
||||
import { getNextTickTime } from '../lifecycle/getNextTickTime.js';
|
||||
import type { MapLoaderOptions } from '../scenario/mapLoader.js';
|
||||
import { loadMapDefinitionByName } from '../scenario/mapLoader.js';
|
||||
import type { UnitSetLoaderOptions } from '../scenario/unitSetLoader.js';
|
||||
import { loadUnitSetDefinitionByName } from '../scenario/unitSetLoader.js';
|
||||
import type { TurnGeneral, TurnWorldLoadResult } from './types.js';
|
||||
|
||||
interface TurnWorldLoaderOptions {
|
||||
databaseUrl: string;
|
||||
mapOptions?: MapLoaderOptions;
|
||||
unitSetOptions?: UnitSetLoaderOptions;
|
||||
}
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
@@ -162,6 +165,7 @@ const mapGeneralRow = (row: PrismaGeneral): TurnGeneral => ({
|
||||
crew: row.crew,
|
||||
crewTypeId: row.crewTypeId,
|
||||
train: row.train,
|
||||
atmos: row.atmos,
|
||||
age: row.age,
|
||||
npcState: row.npcState,
|
||||
triggerState: {
|
||||
@@ -194,7 +198,12 @@ const mapCityRow = (row: PrismaCity): City => ({
|
||||
defenceMax: row.defenceMax,
|
||||
wall: row.wall,
|
||||
wallMax: row.wallMax,
|
||||
meta: asTriggerRecord(row.meta),
|
||||
meta: {
|
||||
...asTriggerRecord(row.meta),
|
||||
trust: row.trust,
|
||||
trade: row.trade,
|
||||
region: row.region,
|
||||
},
|
||||
});
|
||||
|
||||
const mapNationRow = (row: PrismaNation): Nation => ({
|
||||
@@ -208,7 +217,10 @@ const mapNationRow = (row: PrismaNation): Nation => ({
|
||||
power: 0,
|
||||
level: row.level,
|
||||
typeCode: row.typeCode,
|
||||
meta: asTriggerRecord(row.meta),
|
||||
meta: {
|
||||
...asTriggerRecord(row.meta),
|
||||
tech: row.tech,
|
||||
},
|
||||
});
|
||||
|
||||
const mapDiplomacyRow = (row: PrismaDiplomacy): ScenarioDiplomacy => ({
|
||||
@@ -263,6 +275,11 @@ export const loadTurnWorldFromDatabase = async (
|
||||
const scenarioConfig = mapScenarioConfig(worldState.config);
|
||||
const mapName = scenarioConfig.environment?.mapName ?? 'che';
|
||||
const map = await loadMapDefinitionByName(mapName, options.mapOptions);
|
||||
const unitSetName = scenarioConfig.environment?.unitSet ?? 'che';
|
||||
const unitSet = await loadUnitSetDefinitionByName(
|
||||
unitSetName,
|
||||
options.unitSetOptions
|
||||
);
|
||||
|
||||
const meta = asRecord(worldState.meta);
|
||||
const scenarioMeta = parseScenarioMeta(meta);
|
||||
@@ -313,6 +330,7 @@ export const loadTurnWorldFromDatabase = async (
|
||||
scenarioConfig,
|
||||
...(scenarioMeta ? { scenarioMeta } : {}),
|
||||
map,
|
||||
unitSet,
|
||||
nations,
|
||||
cities,
|
||||
generals,
|
||||
|
||||
Reference in New Issue
Block a user