164 lines
6.5 KiB
TypeScript
164 lines
6.5 KiB
TypeScript
import { JosaUtil } from '@sammo-ts/common';
|
|
import { LogCategory, LogFormat, LogScope, type MapDefinition } from '@sammo-ts/logic';
|
|
|
|
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
|
import type { MonthlyEventActionHandler } from './monthlyEventHandler.js';
|
|
|
|
// REF-COMPAT:BEGIN ref-int-column-write-rounding
|
|
const roundLegacyIntegerColumn = (value: number): number => Math.round(value);
|
|
// REF-COMPAT:END ref-int-column-write-rounding
|
|
|
|
const resolveOfficerCity = (meta: Record<string, unknown>): number => {
|
|
const camel = meta.officerCity;
|
|
if (typeof camel === 'number' && Number.isFinite(camel)) {
|
|
return Math.floor(camel);
|
|
}
|
|
const snake = meta.officer_city;
|
|
return typeof snake === 'number' && Number.isFinite(snake) ? Math.floor(snake) : 0;
|
|
};
|
|
|
|
export const createUpdateCitySupplyHandler = (options: {
|
|
getWorld: () => InMemoryTurnWorld | null;
|
|
map: MapDefinition;
|
|
}): MonthlyEventActionHandler => {
|
|
const mapCityById = new Map(options.map.cities.map((city) => [city.id, city]));
|
|
|
|
return (_args, environment) => {
|
|
const world = options.getWorld();
|
|
if (!world) {
|
|
return;
|
|
}
|
|
|
|
const cities = world.listCities().sort((left, right) => left.id - right.id);
|
|
const ownedCityById = new Map(
|
|
cities.filter((city) => city.nationId !== 0).map((city) => [city.id, { ...city, supplied: false }])
|
|
);
|
|
const queue: Array<{ id: number; nationId: number }> = [];
|
|
|
|
for (const nation of world
|
|
.listNations()
|
|
.filter((candidate) => candidate.level > 0)
|
|
.sort((left, right) => left.id - right.id)) {
|
|
if (nation.capitalCityId === null) {
|
|
continue;
|
|
}
|
|
const capital = ownedCityById.get(nation.capitalCityId);
|
|
if (!capital || capital.nationId !== nation.id) {
|
|
continue;
|
|
}
|
|
capital.supplied = true;
|
|
queue.push({ id: capital.id, nationId: nation.id });
|
|
}
|
|
|
|
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
|
const current = queue[cursor]!;
|
|
const mapCity = mapCityById.get(current.id);
|
|
if (!mapCity) {
|
|
throw new Error(`UpdateCitySupply map city is missing (cityId=${current.id})`);
|
|
}
|
|
for (const connectedCityId of mapCity.connections) {
|
|
const connected = ownedCityById.get(connectedCityId);
|
|
if (!connected || connected.nationId !== current.nationId || connected.supplied) {
|
|
continue;
|
|
}
|
|
connected.supplied = true;
|
|
queue.push({ id: connected.id, nationId: current.nationId });
|
|
}
|
|
}
|
|
|
|
const unsuppliedCities = [];
|
|
for (const city of cities) {
|
|
const supplied = city.nationId === 0 || ownedCityById.get(city.id)?.supplied === true;
|
|
if (supplied) {
|
|
world.updateCity(city.id, { supplyState: 1 });
|
|
continue;
|
|
}
|
|
const trust = typeof city.meta.trust === 'number' ? city.meta.trust : 0;
|
|
const damaged = world.updateCity(city.id, {
|
|
supplyState: 0,
|
|
population: roundLegacyIntegerColumn(city.population * 0.9),
|
|
agriculture: roundLegacyIntegerColumn(city.agriculture * 0.9),
|
|
commerce: roundLegacyIntegerColumn(city.commerce * 0.9),
|
|
security: roundLegacyIntegerColumn(city.security * 0.9),
|
|
defence: roundLegacyIntegerColumn(city.defence * 0.9),
|
|
wall: roundLegacyIntegerColumn(city.wall * 0.9),
|
|
meta: {
|
|
...city.meta,
|
|
// REF-COMPAT:BEGIN ref-mariadb-float-boundary
|
|
// 레거시 FLOAT column에 SQL 곱셈 결과가 먼저 저장된 뒤
|
|
// 민심 30 threshold를 판정하므로 float32 양자화를 보존한다.
|
|
trust: Math.fround(trust * 0.9),
|
|
// REF-COMPAT:END ref-mariadb-float-boundary
|
|
},
|
|
});
|
|
if (damaged) {
|
|
unsuppliedCities.push(damaged);
|
|
}
|
|
}
|
|
|
|
const generals = world.listGenerals().sort((left, right) => left.id - right.id);
|
|
const unsuppliedNationByCityId = new Map(unsuppliedCities.map((city) => [city.id, city.nationId]));
|
|
for (const general of generals) {
|
|
if (unsuppliedNationByCityId.get(general.cityId) !== general.nationId) {
|
|
continue;
|
|
}
|
|
world.updateGeneral(general.id, {
|
|
crew: roundLegacyIntegerColumn(general.crew * 0.95),
|
|
atmos: roundLegacyIntegerColumn(general.atmos * 0.95),
|
|
train: roundLegacyIntegerColumn(general.train * 0.95),
|
|
});
|
|
}
|
|
|
|
const lostCities = unsuppliedCities.filter((city) => {
|
|
const trust = city.meta.trust;
|
|
return typeof trust === 'number' && trust < 30;
|
|
});
|
|
if (lostCities.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const lostCityIds = new Set(lostCities.map((city) => city.id));
|
|
for (const lostCity of lostCities) {
|
|
const josaYi = JosaUtil.pick(lostCity.name, '이');
|
|
world.pushLog({
|
|
scope: LogScope.SYSTEM,
|
|
category: LogCategory.HISTORY,
|
|
text: `<R><b>【고립】</b></><G><b>${lostCity.name}</b></>${josaYi} 보급이 끊겨 <R>미지배</> 도시가 되었습니다.`,
|
|
format: LogFormat.YEAR_MONTH,
|
|
year: environment.year,
|
|
month: environment.month,
|
|
});
|
|
}
|
|
|
|
for (const general of generals) {
|
|
if (!lostCityIds.has(resolveOfficerCity(general.meta))) {
|
|
continue;
|
|
}
|
|
const current = world.getGeneralById(general.id);
|
|
if (!current) {
|
|
continue;
|
|
}
|
|
world.updateGeneral(general.id, {
|
|
officerLevel: 1,
|
|
meta: {
|
|
...current.meta,
|
|
officerCity: 0,
|
|
officer_city: 0,
|
|
},
|
|
});
|
|
}
|
|
for (const lostCity of lostCities) {
|
|
world.updateCity(lostCity.id, {
|
|
nationId: 0,
|
|
frontState: 0,
|
|
conflict: {},
|
|
meta: {
|
|
...lostCity.meta,
|
|
officer_set: 0,
|
|
term: 0,
|
|
},
|
|
});
|
|
}
|
|
};
|
|
};
|