Merge branch 'main' into feature/nation-seizure-npc-message-parity
This commit is contained in:
@@ -711,7 +711,7 @@ export class GeneralAI {
|
||||
const leadership = this.general.stats.leadership;
|
||||
const strength = Math.max(this.general.stats.strength, 1);
|
||||
const intel = Math.max(this.general.stats.intelligence, 1);
|
||||
let genType = 0;
|
||||
let genType: number;
|
||||
|
||||
if (strength >= intel) {
|
||||
genType = t무장;
|
||||
|
||||
@@ -38,7 +38,7 @@ export const do부대전방발령 = (ai: GeneralAI) => {
|
||||
const force = ai.nationPolicy.combatForce[leader.id];
|
||||
let [fromCityId, toCityId] = force;
|
||||
|
||||
let targetCityId: number | null = null;
|
||||
let targetCityId: number | null;
|
||||
if (!ai.warRoute || !ai.warRoute[fromCityId] || ai.warRoute[fromCityId][toCityId] === undefined) {
|
||||
targetCityId = pickRandomCityId(ai, ai.frontCities);
|
||||
} else {
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
type LogEntryDraft,
|
||||
type MessageRecordDraft,
|
||||
} from '@sammo-ts/logic';
|
||||
import { asRecord, type RankDataType } from '@sammo-ts/common';
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
|
||||
import type { TurnDaemonCommandResult, TurnDaemonHooks } from '../lifecycle/types.js';
|
||||
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
||||
@@ -33,6 +33,7 @@ import { persistGeneralLifecycleEvents } from './generalTurnLifecyclePersistence
|
||||
import type { DatabaseTurnDaemonLease } from '../lifecycle/databaseTurnDaemonLease.js';
|
||||
import { calculateNationBettingRewards } from '../betting/nationBettingSettlement.js';
|
||||
import type { NationBettingCandidate, PendingNationBettingFinish, PendingNationBettingOpen } from './types.js';
|
||||
import { buildPersistedRankRows } from './rankData.js';
|
||||
|
||||
export interface DatabaseTurnHooks {
|
||||
hooks: TurnDaemonHooks;
|
||||
@@ -270,20 +271,6 @@ const toLegacyDatabaseInt = (value: number): number => {
|
||||
return value >= 0 ? Math.floor(value + 0.5) : Math.ceil(value - 0.5);
|
||||
};
|
||||
|
||||
const readRankMetaNumber = (meta: Record<string, unknown>, key: string): number => {
|
||||
const value = meta[key];
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return toLegacyDatabaseInt(value);
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return toLegacyDatabaseInt(parsed);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const LEGACY_INTEGER_GENERAL_META_KEYS = [
|
||||
'leadership_exp',
|
||||
'strength_exp',
|
||||
@@ -312,72 +299,10 @@ const buildPersistedGeneralMeta = (
|
||||
return asJson(meta);
|
||||
};
|
||||
|
||||
const buildRankRows = (
|
||||
general: ReturnType<InMemoryTurnWorld['consumeDirtyState']>['generals'][number]
|
||||
): Array<{ generalId: number; nationId: number; type: string; value: number }> => {
|
||||
const meta = asRecord(general.meta);
|
||||
const readMeta = (key: string) => readRankMetaNumber(meta, key);
|
||||
const readRank = (key: string) => readRankMetaNumber(meta, `rank_${key}`);
|
||||
|
||||
const entries: Array<[RankDataType, number]> = [
|
||||
['experience', toLegacyDatabaseInt(general.experience)],
|
||||
['dedication', toLegacyDatabaseInt(general.dedication)],
|
||||
['firenum', readMeta('firenum')],
|
||||
['warnum', readRank('warnum')],
|
||||
['killnum', readRank('killnum')],
|
||||
['deathnum', readRank('deathnum')],
|
||||
['occupied', readRank('occupied')],
|
||||
['killcrew', readRank('killcrew')],
|
||||
['deathcrew', readRank('deathcrew')],
|
||||
['killcrew_person', readRank('killcrew_person')],
|
||||
['deathcrew_person', readRank('deathcrew_person')],
|
||||
['dex1', readMeta('dex1')],
|
||||
['dex2', readMeta('dex2')],
|
||||
['dex3', readMeta('dex3')],
|
||||
['dex4', readMeta('dex4')],
|
||||
['dex5', readMeta('dex5')],
|
||||
['ttw', readMeta('ttw')],
|
||||
['ttd', readMeta('ttd')],
|
||||
['ttl', readMeta('ttl')],
|
||||
['ttg', readMeta('ttg')],
|
||||
['ttp', readMeta('ttp')],
|
||||
['tlw', readMeta('tlw')],
|
||||
['tld', readMeta('tld')],
|
||||
['tll', readMeta('tll')],
|
||||
['tlg', readMeta('tlg')],
|
||||
['tlp', readMeta('tlp')],
|
||||
['tsw', readMeta('tsw')],
|
||||
['tsd', readMeta('tsd')],
|
||||
['tsl', readMeta('tsl')],
|
||||
['tsg', readMeta('tsg')],
|
||||
['tsp', readMeta('tsp')],
|
||||
['tiw', readMeta('tiw')],
|
||||
['tid', readMeta('tid')],
|
||||
['til', readMeta('til')],
|
||||
['tig', readMeta('tig')],
|
||||
['tip', readMeta('tip')],
|
||||
['betgold', readMeta('betgold')],
|
||||
['betwin', readMeta('betwin')],
|
||||
['betwingold', readMeta('betwingold')],
|
||||
['inherit_earned', readMeta('inherit_earned')],
|
||||
['inherit_spent', readMeta('inherit_spent')],
|
||||
['inherit_earned_dyn', readMeta('inherit_earned_dyn')],
|
||||
['inherit_earned_act', readMeta('inherit_earned_act')],
|
||||
['inherit_spent_dyn', readMeta('inherit_spent_dyn')],
|
||||
];
|
||||
|
||||
return entries.map(([type, value]) => ({
|
||||
generalId: general.id,
|
||||
nationId: general.nationId,
|
||||
type,
|
||||
value,
|
||||
}));
|
||||
};
|
||||
|
||||
const buildInitialRankRows = (
|
||||
general: ReturnType<InMemoryTurnWorld['consumeDirtyState']>['generals'][number]
|
||||
): Array<{ generalId: number; nationId: number; type: string; value: number }> =>
|
||||
buildRankRows(general).map((row) => ({ ...row, nationId: 0, value: 0 }));
|
||||
buildPersistedRankRows(general).map((row) => ({ ...row, nationId: 0, value: 0 }));
|
||||
|
||||
const buildGeneralUpdate = (
|
||||
general: ReturnType<InMemoryTurnWorld['consumeDirtyState']>['generals'][number]
|
||||
@@ -953,7 +878,7 @@ export const createDatabaseTurnHooks = async (
|
||||
if (createdGenerals.length > 0 || rankTargets.length > 0) {
|
||||
const rankRows = [
|
||||
...createdGenerals.flatMap(buildInitialRankRows),
|
||||
...rankTargets.flatMap(buildRankRows),
|
||||
...rankTargets.flatMap(buildPersistedRankRows),
|
||||
];
|
||||
await Promise.all(
|
||||
rankRows.map((row) =>
|
||||
|
||||
@@ -108,7 +108,7 @@ const applyIncomeOutcome = (
|
||||
originOutcome: number
|
||||
): { next: number; ratio: number; realOutcome: number } => {
|
||||
let next = current + income;
|
||||
let realOutcome = 0;
|
||||
let realOutcome: number;
|
||||
if (next < baseResource) {
|
||||
realOutcome = 0;
|
||||
next = baseResource;
|
||||
@@ -139,14 +139,11 @@ const processIncomeForNation = (
|
||||
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 income =
|
||||
type === 'gold'
|
||||
? getGoldIncome(incomeContext, nationCities, officerCounts, nation.capitalCityId ?? 0, nation.level)
|
||||
: 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);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
LEGACY_RANK_DATA_TYPES,
|
||||
RANK_DATA_TYPES,
|
||||
rankDataMetaKey,
|
||||
type LegacyRankDataType,
|
||||
type RankDataType,
|
||||
} from '@sammo-ts/common';
|
||||
|
||||
export interface RankedGeneralState {
|
||||
id: number;
|
||||
nationId: number;
|
||||
experience: number;
|
||||
dedication: number;
|
||||
meta: unknown;
|
||||
}
|
||||
|
||||
export interface PersistedRankRow {
|
||||
generalId: number;
|
||||
nationId: number;
|
||||
type: RankDataType;
|
||||
value: number;
|
||||
}
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
? { ...(value as Record<string, unknown>) }
|
||||
: {};
|
||||
const toLegacyDatabaseInt = (value: number): number => {
|
||||
if (!Number.isFinite(value)) {
|
||||
return 0;
|
||||
}
|
||||
return value >= 0 ? Math.floor(value + 0.5) : Math.ceil(value - 0.5);
|
||||
};
|
||||
|
||||
const readMetaNumber = (meta: Record<string, unknown>, key: string): number => {
|
||||
const value = meta[key];
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return toLegacyDatabaseInt(value);
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? toLegacyDatabaseInt(parsed) : 0;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const rankMetaKey = rankDataMetaKey;
|
||||
|
||||
export const buildPersistedRankRows = (general: RankedGeneralState): PersistedRankRow[] => {
|
||||
const meta = asRecord(general.meta);
|
||||
return RANK_DATA_TYPES.map((type) => {
|
||||
const value =
|
||||
type === 'experience'
|
||||
? toLegacyDatabaseInt(general.experience)
|
||||
: type === 'dedication'
|
||||
? toLegacyDatabaseInt(general.dedication)
|
||||
: readMetaNumber(meta, rankMetaKey(type));
|
||||
return {
|
||||
generalId: general.id,
|
||||
nationId: general.nationId,
|
||||
type,
|
||||
value,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const buildLegacyComparableRankRows = (
|
||||
general: RankedGeneralState
|
||||
): Array<PersistedRankRow & { type: LegacyRankDataType }> => {
|
||||
const legacyTypes = new Set<RankDataType>(LEGACY_RANK_DATA_TYPES);
|
||||
return buildPersistedRankRows(general).filter(
|
||||
(row): row is PersistedRankRow & { type: LegacyRankDataType } => legacyTypes.has(row.type)
|
||||
);
|
||||
};
|
||||
|
||||
export const applyPersistedRankRowsToMeta = (
|
||||
rawMeta: Record<string, unknown>,
|
||||
rows: ReadonlyArray<{ type: string; value: number }>
|
||||
): void => {
|
||||
const supportedTypes = new Set<string>(RANK_DATA_TYPES);
|
||||
for (const row of rows) {
|
||||
if (row.type === 'experience' || row.type === 'dedication' || !supportedTypes.has(row.type)) {
|
||||
continue;
|
||||
}
|
||||
rawMeta[rankMetaKey(row.type as RankDataType)] = row.value;
|
||||
}
|
||||
};
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
} from '@sammo-ts/logic';
|
||||
import { buildLegacyDefaultUniqueItemPool } from '@sammo-ts/logic/rewards/legacyUniqueItemPool.js';
|
||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic';
|
||||
import { asRecord, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
|
||||
import { asRecord, LEGACY_RANK_DATA_TYPES, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
|
||||
|
||||
import type { ConstraintContext, StateView } from '@sammo-ts/logic';
|
||||
|
||||
@@ -57,6 +57,7 @@ import { buildFrontStatePatches } from './frontStateHandler.js';
|
||||
import { buildActionContext } from './reservedTurnActionContext.js';
|
||||
import { GeneralAI, shouldUseAi } from './ai/generalAi.js';
|
||||
import type { AiReservedTurnProvider } from './ai/types.js';
|
||||
import { rankMetaKey } from './rankData.js';
|
||||
|
||||
const DEFAULT_ACTION = '휴식';
|
||||
|
||||
@@ -227,44 +228,11 @@ const cloneTurnGeneral = (general: TurnGeneral): TurnGeneral => ({
|
||||
|
||||
const resetRetiredGeneral = (general: TurnGeneral): TurnGeneral => {
|
||||
const meta = { ...general.meta };
|
||||
for (const key of [
|
||||
'firenum',
|
||||
'rank_warnum',
|
||||
'rank_killnum',
|
||||
'rank_deathnum',
|
||||
'rank_occupied',
|
||||
'rank_killcrew',
|
||||
'rank_deathcrew',
|
||||
'rank_killcrew_person',
|
||||
'rank_deathcrew_person',
|
||||
'rank_ttw',
|
||||
'rank_ttd',
|
||||
'rank_ttl',
|
||||
'rank_ttg',
|
||||
'rank_ttp',
|
||||
'rank_tlw',
|
||||
'rank_tld',
|
||||
'rank_tll',
|
||||
'rank_tlg',
|
||||
'rank_tlp',
|
||||
'rank_tsw',
|
||||
'rank_tsd',
|
||||
'rank_tsl',
|
||||
'rank_tsg',
|
||||
'rank_tsp',
|
||||
'rank_tiw',
|
||||
'rank_tid',
|
||||
'rank_til',
|
||||
'rank_tig',
|
||||
'rank_tip',
|
||||
'rank_betgold',
|
||||
'rank_betwin',
|
||||
'rank_betwingold',
|
||||
'specage',
|
||||
'specage2',
|
||||
]) {
|
||||
meta[key] = 0;
|
||||
for (const type of LEGACY_RANK_DATA_TYPES) {
|
||||
meta[rankMetaKey(type)] = 0;
|
||||
}
|
||||
meta.specage = 0;
|
||||
meta.specage2 = 0;
|
||||
for (let dex = 1; dex <= 5; dex += 1) {
|
||||
const key = `dex${dex}`;
|
||||
meta[key] = Math.round(readMetaNumber(meta, key, 0) * 0.5);
|
||||
|
||||
@@ -167,7 +167,8 @@ export const createUnificationHandler = (options: {
|
||||
sabotage,
|
||||
dex,
|
||||
unifier,
|
||||
unifierAward: general.nationId === winnerNationId && general.officerLevel > 4 ? UNIFIER_POINT : 0,
|
||||
unifierAward:
|
||||
general.nationId === winnerNationId && general.officerLevel > 4 ? UNIFIER_POINT : 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -194,15 +195,11 @@ export const createUnificationHandler = (options: {
|
||||
const meta = asRecord(state.meta);
|
||||
|
||||
const serverId =
|
||||
typeof meta.serverId === 'string' && meta.serverId.trim()
|
||||
? meta.serverId.trim()
|
||||
: options.profileName;
|
||||
typeof meta.serverId === 'string' && meta.serverId.trim() ? meta.serverId.trim() : options.profileName;
|
||||
const season = readMetaNumberOrNull(meta, 'season') ?? 1;
|
||||
const scenario = readMetaNumberOrNull(meta, 'scenarioId') ?? 0;
|
||||
const scenarioName =
|
||||
typeof asRecord(meta.scenarioMeta).title === 'string'
|
||||
? String(asRecord(meta.scenarioMeta).title)
|
||||
: '';
|
||||
typeof asRecord(meta.scenarioMeta).title === 'string' ? String(asRecord(meta.scenarioMeta).title) : '';
|
||||
const startTime = typeof meta.starttime === 'string' ? meta.starttime : null;
|
||||
const unitedTime = new Date().toISOString();
|
||||
|
||||
@@ -307,14 +304,16 @@ export const createUnificationHandler = (options: {
|
||||
};
|
||||
|
||||
for (const [typeName, valueType] of hallTypes) {
|
||||
let value = 0;
|
||||
if (valueType === 'natural') {
|
||||
value = typeName === 'experience' ? general.experience : typeName === 'dedication' ? general.dedication : ranks[typeName] ?? 0;
|
||||
} else if (valueType === 'rank') {
|
||||
value = ranks[typeName] ?? 0;
|
||||
} else {
|
||||
value = calcValues[typeName] ?? 0;
|
||||
}
|
||||
const value =
|
||||
valueType === 'natural'
|
||||
? typeName === 'experience'
|
||||
? general.experience
|
||||
: typeName === 'dedication'
|
||||
? general.dedication
|
||||
: (ranks[typeName] ?? 0)
|
||||
: valueType === 'rank'
|
||||
? (ranks[typeName] ?? 0)
|
||||
: (calcValues[typeName] ?? 0);
|
||||
|
||||
if ((typeName === 'winrate' || typeName === 'killrate') && warnum < 10) {
|
||||
continue;
|
||||
@@ -391,9 +390,7 @@ export const createUnificationHandler = (options: {
|
||||
const meta = asRecord(state.meta);
|
||||
|
||||
const serverId =
|
||||
typeof meta.serverId === 'string' && meta.serverId.trim()
|
||||
? meta.serverId.trim()
|
||||
: options.profileName;
|
||||
typeof meta.serverId === 'string' && meta.serverId.trim() ? meta.serverId.trim() : options.profileName;
|
||||
const serverName =
|
||||
typeof meta.serverName === 'string' && meta.serverName.trim()
|
||||
? meta.serverName.trim()
|
||||
@@ -653,30 +650,30 @@ export const createUnificationHandler = (options: {
|
||||
await Promise.all(
|
||||
oldGeneralTargets.map((general) =>
|
||||
((snapshot) =>
|
||||
prisma.oldGeneral.upsert({
|
||||
where: {
|
||||
by_no: {
|
||||
prisma.oldGeneral.upsert({
|
||||
where: {
|
||||
by_no: {
|
||||
serverId,
|
||||
generalNo: general.id,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
owner: general.userId ?? null,
|
||||
name: general.name,
|
||||
lastYearMonth: state.currentYear * 100 + state.currentMonth,
|
||||
turnTime: general.turnTime,
|
||||
data: snapshot,
|
||||
},
|
||||
create: {
|
||||
serverId,
|
||||
generalNo: general.id,
|
||||
owner: general.userId ?? null,
|
||||
name: general.name,
|
||||
lastYearMonth: state.currentYear * 100 + state.currentMonth,
|
||||
turnTime: general.turnTime,
|
||||
data: snapshot,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
owner: general.userId ?? null,
|
||||
name: general.name,
|
||||
lastYearMonth: state.currentYear * 100 + state.currentMonth,
|
||||
turnTime: general.turnTime,
|
||||
data: snapshot,
|
||||
},
|
||||
create: {
|
||||
serverId,
|
||||
generalNo: general.id,
|
||||
owner: general.userId ?? null,
|
||||
name: general.name,
|
||||
lastYearMonth: state.currentYear * 100 + state.currentMonth,
|
||||
turnTime: general.turnTime,
|
||||
data: snapshot,
|
||||
},
|
||||
}))( {
|
||||
}))({
|
||||
...general,
|
||||
turnTime: general.turnTime.toISOString(),
|
||||
})
|
||||
|
||||
@@ -315,8 +315,8 @@ async function handleTournamentMatchResult(
|
||||
const attackerG = getRankNumber(attacker, rankKey('g'));
|
||||
const defenderG = getRankNumber(defender, rankKey('g'));
|
||||
|
||||
let attackerGDelta = 0;
|
||||
let defenderGDelta = 0;
|
||||
let attackerGDelta: number;
|
||||
let defenderGDelta: number;
|
||||
let attackerW = 0;
|
||||
let attackerD = 0;
|
||||
let attackerL = 0;
|
||||
|
||||
@@ -32,6 +32,7 @@ import type { UnitSetLoaderOptions } from '../scenario/unitSetLoader.js';
|
||||
import { loadUnitSetDefinitionByName } from '../scenario/unitSetLoader.js';
|
||||
import type { TurnDiplomacy, TurnEvent, TurnGeneral, TurnWorldLoadResult } from './types.js';
|
||||
import { readDiplomacyMeta } from '@sammo-ts/logic';
|
||||
import { applyPersistedRankRowsToMeta } from './rankData.js';
|
||||
|
||||
interface TurnWorldLoaderOptions {
|
||||
databaseUrl: string;
|
||||
@@ -157,17 +158,6 @@ const mapScenarioConfig = (raw: JsonValue): ScenarioConfig => {
|
||||
return parsed.data;
|
||||
};
|
||||
|
||||
const GENERAL_RANK_META_PREFIX_TYPES = new Set([
|
||||
'warnum',
|
||||
'killnum',
|
||||
'deathnum',
|
||||
'occupied',
|
||||
'killcrew',
|
||||
'deathcrew',
|
||||
'killcrew_person',
|
||||
'deathcrew_person',
|
||||
]);
|
||||
|
||||
const mapGeneralRow = (
|
||||
row: TurnEngineGeneralRow,
|
||||
rankRows: readonly TurnEngineRankDataRow[],
|
||||
@@ -181,12 +171,7 @@ const mapGeneralRow = (
|
||||
item: normalizeCode(row.itemCode),
|
||||
};
|
||||
const rawMeta = { ...(asTriggerRecord(row.meta) as Record<string, unknown>) };
|
||||
for (const rank of rankRows) {
|
||||
if (rank.type === 'experience' || rank.type === 'dedication') {
|
||||
continue;
|
||||
}
|
||||
rawMeta[GENERAL_RANK_META_PREFIX_TYPES.has(rank.type) ? `rank_${rank.type}` : rank.type] = rank.value;
|
||||
}
|
||||
applyPersistedRankRowsToMeta(rawMeta, rankRows);
|
||||
const inheritancePoints = Object.fromEntries(inheritanceRows.map((entry) => [entry.key, entry.value]));
|
||||
const itemInventory = readItemInventoryFromMeta(rawMeta, legacySlots);
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user