import type { GeneralActionDefinition, GeneralTurnCommandKey, NationTurnCommandKey, ScenarioConfig, TurnCommandEnv, TurnCommandProfile, UnitSetDefinition, } from '@sammo-ts/logic'; import { LEGACY_RANDOM_GENERAL_FIRST_NAMES, LEGACY_RANDOM_GENERAL_LAST_NAMES, loadGeneralTurnCommandSpecs, loadNationTurnCommandSpecs, loadActionModuleBundle, } from '@sammo-ts/logic'; import { asRecord } from '@sammo-ts/common'; // legacy GameConstBase 기본값 const DEFAULT_GENERAL_GOLD = 1000; const DEFAULT_GENERAL_RICE = 1000; const DEFAULT_CREW_TYPE_ID = 1100; const DEFAULT_TRAIN_DELTA = 30; const DEFAULT_ATMOS_DELTA = 30; const DEFAULT_MAX_TRAIN_BY_COMMAND = 100; const DEFAULT_MAX_ATMOS_BY_COMMAND = 100; const DEFAULT_SABOTAGE_PROB = 0.35; const DEFAULT_SABOTAGE_PROB_COEF = 300; const DEFAULT_SABOTAGE_DEFENCE_COEF = 0.04; const DEFAULT_SABOTAGE_DAMAGE_MIN = 100; const DEFAULT_SABOTAGE_DAMAGE_MAX = 800; const DEFAULT_OPENING_PART_YEAR = 3; const DEFAULT_MAX_GENERAL = 500; const DEFAULT_INITIAL_NATION_GEN_LIMIT = 10; const DEFAULT_MAX_TECH_LEVEL = 12; const DEFAULT_BASE_GOLD = 0; const DEFAULT_BASE_RICE = 2000; const DEFAULT_GENERAL_MINIMUM_GOLD = 0; const DEFAULT_GENERAL_MINIMUM_RICE = 500; const DEFAULT_NPC_SEIZURE_MESSAGE_PROB = 0.01; const DEFAULT_MAX_RESOURCE_ACTION_AMOUNT = 10000; const normalizeCode = (value: string | null | undefined): string | null => { if (!value || value === 'None') { return null; } return value; }; const resolveNumber = (source: Record, keys: string[], fallback: number): number => { for (const key of keys) { const value = source[key]; if (typeof value === 'number' && Number.isFinite(value)) { return value; } } return fallback; }; const resolveOptionalString = (source: Record, keys: string[]): string | null => { for (const key of keys) { const value = source[key]; if (typeof value === 'string') { return normalizeCode(value); } } return null; }; const resolveStringList = (source: Record, key: string, fallback: readonly string[]): string[] => { const value = source[key]; if (!Array.isArray(value)) { return [...fallback]; } const result = value.filter((entry): entry is string => typeof entry === 'string'); return result.length > 0 ? result : [...fallback]; }; export const buildCommandEnv = (config: ScenarioConfig, unitSet?: UnitSetDefinition): TurnCommandEnv => { const constValues = asRecord(config.const); return { ...(unitSet ? { unitSet } : {}), develCost: resolveNumber(constValues, ['develCost', 'develcost', 'develrate'], 0), minAvailableRecruitPop: resolveNumber(constValues, ['minAvailableRecruitPop'], 30000), trainDelta: resolveNumber(constValues, ['trainDelta'], DEFAULT_TRAIN_DELTA), atmosDelta: resolveNumber(constValues, ['atmosDelta'], DEFAULT_ATMOS_DELTA), trainSideEffectByAtmosTurn: resolveNumber(constValues, ['trainSideEffectByAtmosTurn'], 1), maxTrainByCommand: resolveNumber(constValues, ['maxTrainByCommand'], DEFAULT_MAX_TRAIN_BY_COMMAND), maxAtmosByCommand: resolveNumber(constValues, ['maxAtmosByCommand'], DEFAULT_MAX_ATMOS_BY_COMMAND), sabotageDefaultProb: resolveNumber(constValues, ['sabotageDefaultProb'], DEFAULT_SABOTAGE_PROB), sabotageProbCoefByStat: resolveNumber(constValues, ['sabotageProbCoefByStat'], DEFAULT_SABOTAGE_PROB_COEF), sabotageDefenceCoefByGeneralCount: resolveNumber( constValues, ['sabotageDefenceCoefByGeneralCount'], DEFAULT_SABOTAGE_DEFENCE_COEF ), sabotageDamageMin: resolveNumber(constValues, ['sabotageDamageMin'], DEFAULT_SABOTAGE_DAMAGE_MIN), sabotageDamageMax: resolveNumber(constValues, ['sabotageDamageMax'], DEFAULT_SABOTAGE_DAMAGE_MAX), openingPartYear: resolveNumber(constValues, ['openingPartYear'], DEFAULT_OPENING_PART_YEAR), maxGeneral: resolveNumber(constValues, ['defaultMaxGeneral', 'maxGeneral'], DEFAULT_MAX_GENERAL), defaultNpcGold: resolveNumber(constValues, ['defaultNpcGold', 'defaultGold'], DEFAULT_GENERAL_GOLD), defaultNpcRice: resolveNumber(constValues, ['defaultNpcRice', 'defaultRice'], DEFAULT_GENERAL_RICE), defaultCrewTypeId: resolveNumber( constValues, ['defaultCrewTypeId'], unitSet?.defaultCrewTypeId ?? DEFAULT_CREW_TYPE_ID ), defaultSpecialDomestic: resolveOptionalString(constValues, ['defaultSpecialDomestic']), defaultSpecialWar: resolveOptionalString(constValues, ['defaultSpecialWar']), npcStatTotal: resolveNumber(constValues, ['defaultStatNPCTotal', 'npcStatTotal'], config.stat.npcTotal), npcStatMin: resolveNumber(constValues, ['defaultStatNPCMin', 'npcStatMin'], config.stat.npcMin), npcStatMax: resolveNumber(constValues, ['defaultStatNPCMax', 'npcStatMax'], config.stat.npcMax), randomGeneralFirstNames: resolveStringList(constValues, 'randGenFirstName', LEGACY_RANDOM_GENERAL_FIRST_NAMES), randomGeneralMiddleNames: resolveStringList(constValues, 'randGenMiddleName', ['']), randomGeneralLastNames: resolveStringList(constValues, 'randGenLastName', LEGACY_RANDOM_GENERAL_LAST_NAMES), availablePersonalities: resolveStringList(constValues, 'availablePersonality', [ 'che_안전', 'che_유지', 'che_재간', 'che_출세', 'che_할거', 'che_정복', 'che_패권', 'che_의협', 'che_대의', 'che_왕좌', ]), initialNationGenLimit: resolveNumber(constValues, ['initialNationGenLimit'], DEFAULT_INITIAL_NATION_GEN_LIMIT), maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], DEFAULT_MAX_TECH_LEVEL), maxStatLevel: resolveNumber(constValues, ['maxLevel'], 255), maxDedicationLevel: resolveNumber(constValues, ['maxDedLevel'], 30), statUpgradeLimit: resolveNumber(constValues, ['upgradeLimit'], 30), techLevelIncYear: resolveNumber(constValues, ['techLevelIncYear'], 5), initialAllowedTechLevel: resolveNumber(constValues, ['initialAllowedTechLevel'], 1), baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], DEFAULT_BASE_GOLD), baseRice: resolveNumber(constValues, ['baseRice', 'baserice'], DEFAULT_BASE_RICE), generalMinimumGold: resolveNumber(constValues, ['generalMinimumGold'], DEFAULT_GENERAL_MINIMUM_GOLD), generalMinimumRice: resolveNumber(constValues, ['generalMinimumRice'], DEFAULT_GENERAL_MINIMUM_RICE), npcSeizureMessageProb: resolveNumber(constValues, ['npcSeizureMessageProb'], DEFAULT_NPC_SEIZURE_MESSAGE_PROB), maxResourceActionAmount: resolveNumber( constValues, ['maxResourceActionAmount'], DEFAULT_MAX_RESOURCE_ACTION_AMOUNT ), }; }; const ensureGeneralFallback = async ( definitions: Map, fallbackKey: GeneralTurnCommandKey, env: TurnCommandEnv ): Promise => { if (definitions.has(fallbackKey)) { return; } const [spec] = await loadGeneralTurnCommandSpecs([fallbackKey]); if (!spec) { return; } definitions.set(fallbackKey, spec.createDefinition(env)); }; export const buildReservedTurnDefinitions = async (options: { env: TurnCommandEnv; commandProfile: TurnCommandProfile; defaultActionKey: GeneralTurnCommandKey & NationTurnCommandKey; }): Promise<{ general: Map; nation: Map; }> => { const moduleBundle = await loadActionModuleBundle(options.env.unitSet); const itemModules = moduleBundle.itemModules; options.env.itemCatalog = Object.fromEntries( itemModules.map((item) => [ item.key, { slot: item.slot, name: item.name, rawName: item.rawName, cost: item.cost, reqSecu: item.reqSecu, buyable: item.buyable, unique: item.unique, ...(item.initialCharges === undefined ? {} : { initialCharges: item.initialCharges }), }, ]) ); options.env.generalActionModules = [...(options.env.generalActionModules ?? []), ...moduleBundle.general]; options.env.warActionModules = [...(options.env.warActionModules ?? []), ...moduleBundle.war]; options.env.nationTraitModules = moduleBundle.nationTraitModules; const generalSpecs = await loadGeneralTurnCommandSpecs(options.commandProfile.general); const nationSpecs = await loadNationTurnCommandSpecs(options.commandProfile.nation); const general = new Map(generalSpecs.map((spec) => [spec.key, spec.createDefinition(options.env)])); const nation = new Map(nationSpecs.map((spec) => [spec.key, spec.createDefinition(options.env)])); await ensureGeneralFallback(general, options.defaultActionKey, options.env); if (!nation.has(options.defaultActionKey)) { const [spec] = await loadNationTurnCommandSpecs([options.defaultActionKey]); if (spec) { nation.set(spec.key, spec.createDefinition(options.env)); } } return { general, nation }; };