내무부, 금/쌀수입, 감찰부 구현
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import type { TurnCalendarHandler } from './inMemoryWorld.js';
|
||||
|
||||
export const composeCalendarHandlers = (
|
||||
...handlers: Array<TurnCalendarHandler | null | undefined>
|
||||
): TurnCalendarHandler | null => {
|
||||
const resolved = handlers.filter(Boolean) as TurnCalendarHandler[];
|
||||
if (resolved.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
onMonthChanged: (context) => {
|
||||
for (const handler of resolved) {
|
||||
handler.onMonthChanged?.(context);
|
||||
}
|
||||
},
|
||||
onYearChanged: (context) => {
|
||||
for (const handler of resolved) {
|
||||
handler.onYearChanged?.(context);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,281 @@
|
||||
import { asNumber, asRecord } from '@sammo-ts/common';
|
||||
import {
|
||||
ActionLogger,
|
||||
LogFormat,
|
||||
createIncomeActionContext,
|
||||
getBill,
|
||||
getGoldIncome,
|
||||
getOutcome,
|
||||
getRiceIncome,
|
||||
getWallIncome,
|
||||
type CityIncomeSource,
|
||||
type Nation,
|
||||
type NationIncomeContext,
|
||||
type NationTraitModule,
|
||||
type TriggerNationalIncomeType,
|
||||
} from '@sammo-ts/logic';
|
||||
|
||||
import type { ScenarioConfig } from '@sammo-ts/logic';
|
||||
|
||||
import type { InMemoryTurnWorld, TurnCalendarHandler, TurnCalendarContext } from './inMemoryWorld.js';
|
||||
import type { TurnGeneral } from './types.js';
|
||||
|
||||
const resolveNumber = (source: Record<string, unknown>, 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 resolveNationRate = (nation: Nation): number => asNumber(nation.meta.rate, 20);
|
||||
|
||||
const resolveNationBill = (nation: Nation): number => asNumber(nation.meta.bill, 100);
|
||||
|
||||
const resolveOfficerCity = (meta: Record<string, unknown>): number => {
|
||||
const camel = asNumber(meta.officerCity, 0);
|
||||
if (camel > 0) {
|
||||
return camel;
|
||||
}
|
||||
return asNumber(meta.officer_city, 0);
|
||||
};
|
||||
|
||||
const resolveCityTrust = (meta: Record<string, unknown>): number => {
|
||||
const trust = asNumber(meta.trust, 50);
|
||||
return trust;
|
||||
};
|
||||
|
||||
const toIncomeCity = (city: ReturnType<InMemoryTurnWorld['listCities']>[number]): CityIncomeSource => ({
|
||||
id: city.id,
|
||||
population: city.population,
|
||||
populationMax: city.populationMax,
|
||||
agriculture: city.agriculture,
|
||||
agricultureMax: city.agricultureMax,
|
||||
commerce: city.commerce,
|
||||
commerceMax: city.commerceMax,
|
||||
security: city.security,
|
||||
securityMax: city.securityMax,
|
||||
trust: resolveCityTrust(asRecord(city.meta)),
|
||||
supplyState: city.supplyState,
|
||||
defence: city.defence,
|
||||
defenceMax: city.defenceMax,
|
||||
wall: city.wall,
|
||||
wallMax: city.wallMax,
|
||||
meta: asRecord(city.meta),
|
||||
});
|
||||
|
||||
const buildNationIncomeContext = (nation: Nation, trait: NationTraitModule | null): NationIncomeContext => {
|
||||
const actionContext = createIncomeActionContext(nation);
|
||||
const modifyIncome = trait?.onCalcNationalIncome
|
||||
? (type: TriggerNationalIncomeType, amount: number) => trait.onCalcNationalIncome!(actionContext, type, amount)
|
||||
: undefined;
|
||||
return {
|
||||
rate: resolveNationRate(nation),
|
||||
modifyIncome,
|
||||
};
|
||||
};
|
||||
|
||||
const buildOfficerCountMap = (generals: TurnGeneral[]): Map<number, number> => {
|
||||
const officerCntByCity = new Map<number, number>();
|
||||
for (const general of generals) {
|
||||
if (general.officerLevel < 2 || general.officerLevel > 4) {
|
||||
continue;
|
||||
}
|
||||
const officerCity = resolveOfficerCity(asRecord(general.meta));
|
||||
if (!officerCity || general.cityId !== officerCity) {
|
||||
continue;
|
||||
}
|
||||
officerCntByCity.set(officerCity, (officerCntByCity.get(officerCity) ?? 0) + 1);
|
||||
}
|
||||
return officerCntByCity;
|
||||
};
|
||||
|
||||
const pushLogs = (world: InMemoryTurnWorld, logs: ReturnType<ActionLogger['flush']>): void => {
|
||||
for (const log of logs) {
|
||||
world.pushLog(log);
|
||||
}
|
||||
};
|
||||
|
||||
const roundResource = (value: number): number => Math.round(value);
|
||||
|
||||
const applyIncomeOutcome = (
|
||||
current: number,
|
||||
income: number,
|
||||
outcome: number,
|
||||
baseResource: number,
|
||||
originOutcome: number
|
||||
): { next: number; ratio: number; realOutcome: number } => {
|
||||
let next = current + income;
|
||||
let realOutcome = 0;
|
||||
if (next < baseResource) {
|
||||
realOutcome = 0;
|
||||
next = baseResource;
|
||||
} else if (next - baseResource < outcome) {
|
||||
realOutcome = next - baseResource;
|
||||
next = baseResource;
|
||||
} else {
|
||||
realOutcome = outcome;
|
||||
next -= realOutcome;
|
||||
}
|
||||
|
||||
const ratio = originOutcome > 0 ? realOutcome / originOutcome : 0;
|
||||
return { next: Math.max(next, baseResource), ratio, realOutcome };
|
||||
};
|
||||
|
||||
const processIncomeForNation = (
|
||||
world: InMemoryTurnWorld,
|
||||
nation: Nation,
|
||||
generals: TurnGeneral[],
|
||||
cities: ReturnType<InMemoryTurnWorld['listCities']>,
|
||||
officerCounts: Map<number, number>,
|
||||
traitMap: Map<string, NationTraitModule>,
|
||||
type: 'gold' | 'rice',
|
||||
baseResource: number
|
||||
): void => {
|
||||
const nationCities = cities.filter((city) => city.nationId === nation.id).map(toIncomeCity);
|
||||
const nationGenerals = generals.filter((general) => general.nationId === nation.id && general.npcState !== 5);
|
||||
const trait = traitMap.get(nation.typeCode) ?? null;
|
||||
const incomeContext = buildNationIncomeContext(nation, trait);
|
||||
|
||||
let income = 0;
|
||||
if (type === 'gold') {
|
||||
income = getGoldIncome(
|
||||
incomeContext,
|
||||
nationCities,
|
||||
officerCounts,
|
||||
nation.capitalCityId ?? 0,
|
||||
nation.level
|
||||
);
|
||||
} else {
|
||||
income =
|
||||
getRiceIncome(
|
||||
incomeContext,
|
||||
nationCities,
|
||||
officerCounts,
|
||||
nation.capitalCityId ?? 0,
|
||||
nation.level
|
||||
) +
|
||||
getWallIncome(
|
||||
incomeContext,
|
||||
nationCities,
|
||||
officerCounts,
|
||||
nation.capitalCityId ?? 0,
|
||||
nation.level
|
||||
);
|
||||
}
|
||||
|
||||
const incomeValue = roundResource(income);
|
||||
const originOutcome = getOutcome(100, nationGenerals);
|
||||
const bill = resolveNationBill(nation);
|
||||
const outcome = Math.round((bill / 100) * originOutcome);
|
||||
const current = type === 'gold' ? nation.gold : nation.rice;
|
||||
|
||||
const { next, ratio } = applyIncomeOutcome(current, incomeValue, outcome, baseResource, originOutcome);
|
||||
const nextMeta = {
|
||||
...nation.meta,
|
||||
[`prev_income_${type}`]: incomeValue,
|
||||
};
|
||||
|
||||
if (type === 'gold') {
|
||||
world.updateNation(nation.id, { gold: next, meta: nextMeta });
|
||||
} else {
|
||||
world.updateNation(nation.id, { rice: next, meta: nextMeta });
|
||||
}
|
||||
|
||||
const incomeText = incomeValue.toLocaleString();
|
||||
const incomeLog = type === 'gold' ? `이번 수입은 금 <C>${incomeText}</>입니다.` : `이번 수입은 쌀 <C>${incomeText}</>입니다.`;
|
||||
for (const general of nationGenerals) {
|
||||
const pay = Math.round(getBill(general.dedication) * ratio);
|
||||
if (type === 'gold') {
|
||||
world.updateGeneral(general.id, { gold: general.gold + pay });
|
||||
} else {
|
||||
world.updateGeneral(general.id, { rice: general.rice + pay });
|
||||
}
|
||||
|
||||
const logger = new ActionLogger({ generalId: general.id, nationId: nation.id });
|
||||
if (general.officerLevel > 4) {
|
||||
logger.pushGeneralActionLog(incomeLog, LogFormat.PLAIN);
|
||||
}
|
||||
const payText = pay.toLocaleString();
|
||||
const payLog =
|
||||
type === 'gold'
|
||||
? `봉급으로 금 <C>${payText}</>을 받았습니다.`
|
||||
: `봉급으로 쌀 <C>${payText}</>을 받았습니다.`;
|
||||
logger.pushGeneralActionLog(payLog, LogFormat.PLAIN);
|
||||
pushLogs(world, logger.flush());
|
||||
}
|
||||
};
|
||||
|
||||
export const createIncomeHandler = (options: {
|
||||
getWorld: () => InMemoryTurnWorld | null;
|
||||
scenarioConfig: ScenarioConfig;
|
||||
nationTraits: Map<string, NationTraitModule>;
|
||||
}): TurnCalendarHandler => {
|
||||
const constValues = asRecord(options.scenarioConfig.const);
|
||||
const baseGold = resolveNumber(constValues, ['baseGold', 'basegold'], 0);
|
||||
const baseRice = resolveNumber(constValues, ['baseRice', 'baserice'], 0);
|
||||
|
||||
const handler: TurnCalendarHandler = {
|
||||
onMonthChanged: (context: TurnCalendarContext) => {
|
||||
const world = options.getWorld();
|
||||
if (!world) {
|
||||
return;
|
||||
}
|
||||
const month = context.currentMonth;
|
||||
if (month !== 1 && month !== 7) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nations = world.listNations();
|
||||
const generals = world.listGenerals();
|
||||
const cities = world.listCities();
|
||||
|
||||
const byNation: Map<number, TurnGeneral[]> = new Map();
|
||||
for (const general of generals) {
|
||||
const bucket = byNation.get(general.nationId) ?? [];
|
||||
bucket.push(general);
|
||||
byNation.set(general.nationId, bucket);
|
||||
}
|
||||
|
||||
for (const nation of nations) {
|
||||
const nationGenerals = byNation.get(nation.id) ?? [];
|
||||
const officerCounts = buildOfficerCountMap(nationGenerals);
|
||||
if (month === 1) {
|
||||
processIncomeForNation(
|
||||
world,
|
||||
nation,
|
||||
nationGenerals,
|
||||
cities,
|
||||
officerCounts,
|
||||
options.nationTraits,
|
||||
'gold',
|
||||
baseGold
|
||||
);
|
||||
} else if (month === 7) {
|
||||
processIncomeForNation(
|
||||
world,
|
||||
nation,
|
||||
nationGenerals,
|
||||
cities,
|
||||
officerCounts,
|
||||
options.nationTraits,
|
||||
'rice',
|
||||
baseRice
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new ActionLogger();
|
||||
if (month === 1) {
|
||||
logger.pushGlobalHistoryLog('<W><b>【지급】</b></>봄이 되어 봉록에 따라 자금이 지급됩니다.');
|
||||
} else if (month === 7) {
|
||||
logger.pushGlobalHistoryLog('<W><b>【지급】</b></>가을이 되어 봉록에 따라 군량이 지급됩니다.');
|
||||
}
|
||||
pushLogs(world, logger.flush());
|
||||
},
|
||||
};
|
||||
|
||||
return handler;
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TurnCommandProfile, TurnSchedule } from '@sammo-ts/logic';
|
||||
import { buildGameEventChannel, type RealtimeEvent } from '@sammo-ts/common';
|
||||
import { createRedisConnector, resolveRedisConfigFromEnv } from '@sammo-ts/infra';
|
||||
import { NATION_TRAIT_KEYS, NationTraitLoader, loadNationTraitModules } from '@sammo-ts/logic';
|
||||
|
||||
import { SystemClock } from '../lifecycle/clock.js';
|
||||
import { getNextTickTime } from '../lifecycle/getNextTickTime.js';
|
||||
@@ -16,6 +17,8 @@ import { InMemoryTurnProcessor } from './inMemoryTurnProcessor.js';
|
||||
import { InMemoryTurnStateStore } from './inMemoryStateStore.js';
|
||||
import { createGatewayAdminActionConsumer } from './gatewayAdminActions.js';
|
||||
import { createGatewayProfileGate } from './gatewayProfileGate.js';
|
||||
import { composeCalendarHandlers } from './calendarHandlers.js';
|
||||
import { createIncomeHandler } from './incomeHandler.js';
|
||||
import { createReservedTurnHandler } from './reservedTurnHandler.js';
|
||||
import { createReservedTurnStore } from './reservedTurnStore.js';
|
||||
import { createTurnDaemonCommandHandler } from './worldCommandHandler.js';
|
||||
@@ -100,6 +103,8 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
||||
})
|
||||
: await loadTurnCommandProfile());
|
||||
let worldRef: InMemoryTurnWorld | null = null;
|
||||
const nationTraits = await loadNationTraitModules([...NATION_TRAIT_KEYS], new NationTraitLoader());
|
||||
const nationTraitMap = new Map(nationTraits.map((module) => [module.key, module]));
|
||||
const unification = options.calendarHandler
|
||||
? null
|
||||
: createUnificationHandler({
|
||||
@@ -107,6 +112,12 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
||||
profileName: options.profileName ?? options.profile,
|
||||
getWorld: () => worldRef,
|
||||
});
|
||||
const incomeHandler = createIncomeHandler({
|
||||
getWorld: () => worldRef,
|
||||
scenarioConfig: snapshot.scenarioConfig,
|
||||
nationTraits: nationTraitMap,
|
||||
});
|
||||
const calendarHandler = composeCalendarHandlers(options.calendarHandler ?? unification?.handler, incomeHandler);
|
||||
const worldOptions: InMemoryTurnWorldOptions = {
|
||||
schedule,
|
||||
generalTurnHandler:
|
||||
@@ -120,7 +131,7 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
||||
getWorld: () => worldRef,
|
||||
commandProfile,
|
||||
})),
|
||||
calendarHandler: options.calendarHandler ?? unification?.handler,
|
||||
calendarHandler: calendarHandler ?? undefined,
|
||||
};
|
||||
const world = new InMemoryTurnWorld(resolvedState, snapshot, worldOptions);
|
||||
worldRef = world;
|
||||
|
||||
Reference in New Issue
Block a user