253 lines
9.7 KiB
TypeScript
253 lines
9.7 KiB
TypeScript
import type { General } from '@sammo-ts/logic/domain/entities.js';
|
|
import type { ScenarioConfig } from '@sammo-ts/logic/scenario/types.js';
|
|
import type { ScenarioMeta } from '@sammo-ts/logic/world/types.js';
|
|
import type { WarAftermathConfig, WarEngineConfig, WarTimeContext } from '@sammo-ts/logic/war/types.js';
|
|
import type { UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
|
import type { ActionContextWorldRef, ActionContextWorldState } from './actionContext.js';
|
|
import { asRecord } from '@sammo-ts/common';
|
|
|
|
export interface WorldSummary {
|
|
totalGeneralCount: number;
|
|
totalNpcCount: number;
|
|
averageStats?: General['stats'];
|
|
}
|
|
|
|
export interface NationSummary {
|
|
averageStats?: General['stats'];
|
|
averageExperience?: number;
|
|
averageDedication?: number;
|
|
averageDex?: [number, number, number, number, number];
|
|
}
|
|
|
|
export const buildWorldSummary = (world: ActionContextWorldRef | null): WorldSummary => {
|
|
if (!world) {
|
|
return { totalGeneralCount: 0, totalNpcCount: 0 };
|
|
}
|
|
const generals = world.listGenerals();
|
|
if (generals.length === 0) {
|
|
return { totalGeneralCount: 0, totalNpcCount: 0 };
|
|
}
|
|
const countedGenerals = generals.filter((general) => general.npcState < 4);
|
|
const total = countedGenerals.length;
|
|
const totalGeneralCount = generals.filter((general) => general.npcState <= 2).length;
|
|
const totalNpcCount = generals.filter((general) => general.npcState >= 3 && general.npcState <= 4).length;
|
|
const statSum = countedGenerals.reduce(
|
|
(acc, general) => ({
|
|
leadership: acc.leadership + general.stats.leadership,
|
|
strength: acc.strength + general.stats.strength,
|
|
intelligence: acc.intelligence + general.stats.intelligence,
|
|
}),
|
|
{ leadership: 0, strength: 0, intelligence: 0 }
|
|
);
|
|
return {
|
|
totalGeneralCount,
|
|
totalNpcCount,
|
|
averageStats: {
|
|
leadership: statSum.leadership / total,
|
|
strength: statSum.strength / total,
|
|
intelligence: statSum.intelligence / total,
|
|
},
|
|
};
|
|
};
|
|
|
|
export const buildNationSummary = (world: ActionContextWorldRef | null, nationId: number): NationSummary => {
|
|
if (!world || nationId <= 0) {
|
|
return {};
|
|
}
|
|
const generals = world.listGenerals().filter((general) => general.nationId === nationId);
|
|
if (generals.length === 0) {
|
|
return {};
|
|
}
|
|
const total = generals.length;
|
|
const statSum = generals.reduce(
|
|
(acc, general) => ({
|
|
leadership: acc.leadership + general.stats.leadership,
|
|
strength: acc.strength + general.stats.strength,
|
|
intelligence: acc.intelligence + general.stats.intelligence,
|
|
}),
|
|
{ leadership: 0, strength: 0, intelligence: 0 }
|
|
);
|
|
const expSum = generals.reduce((acc, general) => acc + general.experience, 0);
|
|
const dedSum = generals.reduce((acc, general) => acc + general.dedication, 0);
|
|
const dexSum = generals.reduce<[number, number, number, number, number]>(
|
|
(acc, general) => [
|
|
acc[0] + Number(general.meta.dex1 ?? 0),
|
|
acc[1] + Number(general.meta.dex2 ?? 0),
|
|
acc[2] + Number(general.meta.dex3 ?? 0),
|
|
acc[3] + Number(general.meta.dex4 ?? 0),
|
|
acc[4] + Number(general.meta.dex5 ?? 0),
|
|
],
|
|
[0, 0, 0, 0, 0] as [number, number, number, number, number]
|
|
);
|
|
return {
|
|
averageStats: {
|
|
leadership: statSum.leadership / total,
|
|
strength: statSum.strength / total,
|
|
intelligence: statSum.intelligence / total,
|
|
},
|
|
averageExperience: expSum / total,
|
|
averageDedication: dedSum / total,
|
|
averageDex: [dexSum[0] / total, dexSum[1] / total, dexSum[2] / total, dexSum[3] / total, dexSum[4] / total],
|
|
};
|
|
};
|
|
|
|
export const buildAverageNationGeneralCount = (world: ActionContextWorldRef | null): number => {
|
|
if (!world) {
|
|
return 0;
|
|
}
|
|
const generals = world.listGenerals();
|
|
const nations = world.listNations().filter((nation) => nation.level > 0);
|
|
if (nations.length === 0) {
|
|
return generals.length;
|
|
}
|
|
return (
|
|
nations.reduce((sum, nation) => {
|
|
const storedCount = nation.meta.gennum;
|
|
if (typeof storedCount === 'number' && Number.isFinite(storedCount)) {
|
|
return sum + storedCount;
|
|
}
|
|
return sum + generals.filter((general) => general.nationId === nation.id).length;
|
|
}, 0) / nations.length
|
|
);
|
|
};
|
|
|
|
export const resolveStartYear = (world: ActionContextWorldState, scenarioMeta?: ScenarioMeta): number => {
|
|
if (typeof scenarioMeta?.startYear === 'number') {
|
|
return scenarioMeta.startYear;
|
|
}
|
|
return world.currentYear;
|
|
};
|
|
|
|
const readFiniteInteger = (value: unknown, fallback: number): number => {
|
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
return Math.floor(value);
|
|
}
|
|
if (typeof value === 'string') {
|
|
const parsed = Number(value);
|
|
if (Number.isFinite(parsed)) {
|
|
return Math.floor(parsed);
|
|
}
|
|
}
|
|
return fallback;
|
|
};
|
|
|
|
export const resolveInitYearMonth = (
|
|
world: ActionContextWorldState,
|
|
scenarioMeta?: ScenarioMeta
|
|
): { year: number; month: number } => {
|
|
const meta = asRecord(world.meta);
|
|
const startYear = resolveStartYear(world, scenarioMeta);
|
|
return {
|
|
year: readFiniteInteger(meta.initYear, startYear),
|
|
month: readFiniteInteger(meta.initMonth, 1),
|
|
};
|
|
};
|
|
|
|
export const resolveTurnTermMinutes = (world: ActionContextWorldState): number =>
|
|
Math.max(1, Math.round(world.tickSeconds / 60));
|
|
|
|
const DEFAULT_WAR_CONFIG = {
|
|
armPerPhase: 500,
|
|
maxTrainByCommand: 100,
|
|
maxAtmosByCommand: 100,
|
|
maxTrainByWar: 110,
|
|
maxAtmosByWar: 150,
|
|
};
|
|
|
|
const DEFAULT_AFTER_CONFIG = {
|
|
techLevelIncYear: 5,
|
|
initialAllowedTechLevel: 1,
|
|
defaultCityWall: 1000,
|
|
baseGold: 0,
|
|
baseRice: 2000,
|
|
};
|
|
|
|
const resolveNumber = (record: Record<string, unknown>, keys: string[], fallback: number): number => {
|
|
for (const key of keys) {
|
|
const value = record[key];
|
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
return value;
|
|
}
|
|
}
|
|
return fallback;
|
|
};
|
|
|
|
// 성벽 병종은 이름/요구조건을 우선해 찾고, 없으면 기본값을 사용한다.
|
|
const resolveCastleCrewTypeId = (unitSet: UnitSetDefinition, fallback: number): number => {
|
|
const crewTypes = unitSet.crewTypes ?? [];
|
|
const byName = crewTypes.find((crewType) => crewType.name.includes('성벽'));
|
|
if (byName) {
|
|
return byName.id;
|
|
}
|
|
const byRequirement = crewTypes.find((crewType) =>
|
|
crewType.requirements.some((requirement) => requirement.type === 'Impossible')
|
|
);
|
|
if (byRequirement) {
|
|
return byRequirement.id;
|
|
}
|
|
if (typeof unitSet.defaultCrewTypeId === 'number') {
|
|
return unitSet.defaultCrewTypeId;
|
|
}
|
|
return crewTypes[0]?.id ?? fallback;
|
|
};
|
|
|
|
const resolveCastleArmType = (unitSet: UnitSetDefinition, castleCrewTypeId: number): number => {
|
|
const crewTypes = unitSet.crewTypes ?? [];
|
|
return crewTypes.find((crewType) => crewType.id === castleCrewTypeId)?.armType ?? 0;
|
|
};
|
|
|
|
export const buildWarConfig = (scenarioConfig: ScenarioConfig, unitSet: UnitSetDefinition): WarEngineConfig => {
|
|
const constValues = asRecord(scenarioConfig.const);
|
|
const castleCrewTypeId = resolveNumber(constValues, ['castleCrewTypeId'], resolveCastleCrewTypeId(unitSet, 0));
|
|
const castleArmType = resolveCastleArmType(unitSet, castleCrewTypeId);
|
|
|
|
return {
|
|
armPerPhase: resolveNumber(constValues, ['armPerPhase', 'armperphase'], DEFAULT_WAR_CONFIG.armPerPhase),
|
|
maxTrainByCommand: resolveNumber(constValues, ['maxTrainByCommand'], DEFAULT_WAR_CONFIG.maxTrainByCommand),
|
|
maxAtmosByCommand: resolveNumber(constValues, ['maxAtmosByCommand'], DEFAULT_WAR_CONFIG.maxAtmosByCommand),
|
|
maxTrainByWar: resolveNumber(constValues, ['maxTrainByWar'], DEFAULT_WAR_CONFIG.maxTrainByWar),
|
|
maxAtmosByWar: resolveNumber(constValues, ['maxAtmosByWar'], DEFAULT_WAR_CONFIG.maxAtmosByWar),
|
|
maxGeneralStat: resolveNumber(constValues, ['maxLevel'], 255),
|
|
statUpgradeLimit: resolveNumber(constValues, ['upgradeLimit'], 30),
|
|
castleCrewTypeId,
|
|
armTypes: {
|
|
footman: 1,
|
|
archer: 2,
|
|
cavalry: 3,
|
|
wizard: 4,
|
|
siege: 5,
|
|
misc: 6,
|
|
castle: castleArmType,
|
|
},
|
|
};
|
|
};
|
|
|
|
export const buildWarAftermathConfig = (
|
|
scenarioConfig: ScenarioConfig,
|
|
castleCrewTypeId: number
|
|
): WarAftermathConfig => {
|
|
const constValues = asRecord(scenarioConfig.const);
|
|
return {
|
|
initialNationGenLimit: resolveNumber(constValues, ['initialNationGenLimit'], 0),
|
|
techLevelIncYear: resolveNumber(constValues, ['techLevelIncYear'], DEFAULT_AFTER_CONFIG.techLevelIncYear),
|
|
initialAllowedTechLevel: resolveNumber(
|
|
constValues,
|
|
['initialAllowedTechLevel'],
|
|
DEFAULT_AFTER_CONFIG.initialAllowedTechLevel
|
|
),
|
|
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], 0),
|
|
defaultCityWall: resolveNumber(constValues, ['defaultCityWall'], DEFAULT_AFTER_CONFIG.defaultCityWall),
|
|
baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], DEFAULT_AFTER_CONFIG.baseGold),
|
|
baseRice: resolveNumber(constValues, ['baseRice', 'baserice'], DEFAULT_AFTER_CONFIG.baseRice),
|
|
castleCrewTypeId,
|
|
joinMode: 'full',
|
|
joinRuinedNpcProbability: resolveNumber(constValues, ['joinRuinedNPCProp'], 0.1),
|
|
};
|
|
};
|
|
|
|
export const buildWarTime = (world: ActionContextWorldState, scenarioMeta?: ScenarioMeta): WarTimeContext => ({
|
|
year: world.currentYear,
|
|
month: world.currentMonth,
|
|
startYear: resolveStartYear(world, scenarioMeta),
|
|
});
|