내무부, 금/쌀수입, 감찰부 구현

This commit is contained in:
2026-01-18 16:40:12 +00:00
parent bb618a9ade
commit 5b51aad6d0
15 changed files with 2705 additions and 143 deletions
+2
View File
@@ -7,9 +7,11 @@ export interface DatabaseClient {
general: GamePrisma.GeneralDelegate;
city: GamePrisma.CityDelegate;
nation: GamePrisma.NationDelegate;
diplomacy: GamePrisma.DiplomacyDelegate;
generalTurn: GamePrisma.GeneralTurnDelegate;
nationTurn: GamePrisma.NationTurnDelegate;
troop: GamePrisma.TroopDelegate;
logEntry: GamePrisma.LogEntryDelegate;
inheritancePoint: GamePrisma.InheritancePointDelegate;
inheritanceLog: GamePrisma.InheritanceLogDelegate;
inheritanceResult: GamePrisma.InheritanceResultDelegate;
+1
View File
@@ -0,0 +1 @@
export * from './nationIncome.js';
+275
View File
@@ -0,0 +1,275 @@
import type { General, Nation } from '../domain/entities.js';
import type { GeneralActionContext } from '../triggers/general.js';
import type { TriggerNationalIncomeType } from '../triggers/types.js';
export type NationIncomeModifier = (type: TriggerNationalIncomeType, amount: number) => number;
export interface NationIncomeContext {
rate: number;
modifyIncome?: NationIncomeModifier;
}
export interface CityIncomeSource {
id: number;
population: number;
populationMax: number;
agriculture: number;
agricultureMax: number;
commerce: number;
commerceMax: number;
security: number;
securityMax: number;
trust: number;
supplyState: number;
defence: number;
defenceMax: number;
wall: number;
wallMax: number;
meta: Record<string, unknown>;
}
const MAX_DED_LEVEL = 30;
const readMetaNumber = (meta: Record<string, unknown>, key: string, fallback = 0): number => {
const value = meta[key];
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
return fallback;
};
const applyNationIncome = (context: NationIncomeContext, type: TriggerNationalIncomeType, amount: number): number => {
if (!context.modifyIncome) {
return amount;
}
return context.modifyIncome(type, amount);
};
export const createIncomeActionContext = (nation: Nation): GeneralActionContext => {
const general: General = {
id: 0,
name: 'SYSTEM',
nationId: nation.id,
cityId: 0,
troopId: 0,
stats: { leadership: 0, strength: 0, intelligence: 0 },
experience: 0,
dedication: 0,
officerLevel: 0,
role: {
personality: null,
specialDomestic: null,
specialWar: null,
items: {
horse: null,
weapon: null,
book: null,
item: null,
},
},
injury: 0,
gold: 0,
rice: 0,
crew: 0,
crewTypeId: 0,
train: 0,
atmos: 0,
age: 0,
npcState: 0,
triggerState: {
flags: {},
counters: {},
modifiers: {},
meta: {},
},
meta: {},
};
return { general, nation };
};
export const calcCityGoldIncomeBase = (
context: NationIncomeContext,
city: CityIncomeSource,
officerCnt: number,
isCapital: boolean,
nationLevel: number
): number => {
if (city.supplyState === 0) {
return 0;
}
const trustRatio = city.trust / 200 + 0.5;
const commMax = Math.max(1, city.commerceMax);
const secuMax = Math.max(1, city.securityMax);
let income = (city.population * city.commerce * trustRatio) / commMax / 30;
income *= 1 + city.security / secuMax / 10;
income *= Math.pow(1.05, officerCnt);
if (isCapital && nationLevel > 0) {
income *= 1 + 1 / (3 * nationLevel);
}
const adjusted = applyNationIncome(context, 'gold', income);
return Math.round(adjusted);
};
export const calcCityRiceIncomeBase = (
context: NationIncomeContext,
city: CityIncomeSource,
officerCnt: number,
isCapital: boolean,
nationLevel: number
): number => {
if (city.supplyState === 0) {
return 0;
}
const trustRatio = city.trust / 200 + 0.5;
const agriMax = Math.max(1, city.agricultureMax);
const secuMax = Math.max(1, city.securityMax);
let income = (city.population * city.agriculture * trustRatio) / agriMax / 30;
income *= 1 + city.security / secuMax / 10;
income *= Math.pow(1.05, officerCnt);
if (isCapital && nationLevel > 0) {
income *= 1 + 1 / (3 * nationLevel);
}
const adjusted = applyNationIncome(context, 'rice', income);
return Math.round(adjusted);
};
export const calcCityWallIncomeBase = (
context: NationIncomeContext,
city: CityIncomeSource,
officerCnt: number,
isCapital: boolean,
nationLevel: number
): number => {
if (city.supplyState === 0) {
return 0;
}
const wallMax = Math.max(1, city.wallMax);
const secuMax = Math.max(1, city.securityMax);
let income = (city.defence * city.wall) / wallMax / 3;
income *= 1 + city.security / secuMax / 10;
income *= Math.pow(1.05, officerCnt);
if (isCapital && nationLevel > 0) {
income *= 1 + 1 / (3 * nationLevel);
}
const adjusted = applyNationIncome(context, 'rice', income);
return Math.round(adjusted);
};
export const calcCityWarGoldIncome = (context: NationIncomeContext, city: CityIncomeSource): number => {
if (city.supplyState === 0) {
return 0;
}
const dead = readMetaNumber(city.meta as Record<string, unknown>, 'dead', 0);
const income = dead / 10;
const adjusted = applyNationIncome(context, 'gold', income);
return Math.round(adjusted);
};
export const calcCityGoldIncome = (
context: NationIncomeContext,
city: CityIncomeSource,
officerCnt: number,
isCapital: boolean,
nationLevel: number
): number => {
const base = calcCityGoldIncomeBase(context, city, officerCnt, isCapital, nationLevel);
return Math.round(base * (context.rate / 20));
};
export const calcCityRiceIncome = (
context: NationIncomeContext,
city: CityIncomeSource,
officerCnt: number,
isCapital: boolean,
nationLevel: number
): number => {
const base = calcCityRiceIncomeBase(context, city, officerCnt, isCapital, nationLevel);
return Math.round(base * (context.rate / 20));
};
export const calcCityWallIncome = (
context: NationIncomeContext,
city: CityIncomeSource,
officerCnt: number,
isCapital: boolean,
nationLevel: number
): number => {
const base = calcCityWallIncomeBase(context, city, officerCnt, isCapital, nationLevel);
return Math.round(base * (context.rate / 20));
};
export const getGoldIncome = (
context: NationIncomeContext,
cities: CityIncomeSource[],
officerCounts: Map<number, number>,
capitalCityId: number | null,
nationLevel: number
): number => {
let total = 0;
for (const city of cities) {
const officerCnt = officerCounts.get(city.id) ?? 0;
total += calcCityGoldIncomeBase(context, city, officerCnt, capitalCityId === city.id, nationLevel);
}
return total * (context.rate / 20);
};
export const getRiceIncome = (
context: NationIncomeContext,
cities: CityIncomeSource[],
officerCounts: Map<number, number>,
capitalCityId: number | null,
nationLevel: number
): number => {
let total = 0;
for (const city of cities) {
const officerCnt = officerCounts.get(city.id) ?? 0;
total += calcCityRiceIncomeBase(context, city, officerCnt, capitalCityId === city.id, nationLevel);
}
return total * (context.rate / 20);
};
export const getWallIncome = (
context: NationIncomeContext,
cities: CityIncomeSource[],
officerCounts: Map<number, number>,
capitalCityId: number | null,
nationLevel: number
): number => {
let total = 0;
for (const city of cities) {
const officerCnt = officerCounts.get(city.id) ?? 0;
total += calcCityWallIncomeBase(context, city, officerCnt, capitalCityId === city.id, nationLevel);
}
return total * (context.rate / 20);
};
export const getWarGoldIncome = (context: NationIncomeContext, cities: CityIncomeSource[]): number => {
let total = 0;
for (const city of cities) {
total += calcCityWarGoldIncome(context, city);
}
return total;
};
export const resolveDedLevel = (dedication: number): number => {
const level = Math.ceil(Math.sqrt(Math.max(0, dedication)) / 10);
return Math.max(0, Math.min(MAX_DED_LEVEL, level));
};
export const getBillByLevel = (dedicationLevel: number): number => dedicationLevel * 200 + 400;
export const getBill = (dedication: number): number => getBillByLevel(resolveDedLevel(dedication));
export const getOutcome = (billRate: number, generals: Array<{ dedication: number }>): number => {
let total = 0;
for (const general of generals) {
total += getBill(general.dedication);
}
return Math.round(total * (billRate / 100));
};
+1
View File
@@ -3,6 +3,7 @@ export type { RandomGenerator } from '@sammo-ts/common';
export * from './actions/index.js';
export * from './constraints/index.js';
export * from './diplomacy/index.js';
export * from './economy/index.js';
export * from './logging/index.js';
export * from './messages/index.js';
export * from './items/index.js';
+1
View File
@@ -1,4 +1,5 @@
export * from './core.js';
export * from './general.js';
export * from './general-action.js';
export * from './types.js';
export * from './special/index.js';