merge: 유산 포인트 적립 조건 정합화를 main에 반영한다
This commit is contained in:
@@ -271,12 +271,27 @@ export const inheritRouter = router({
|
|||||||
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: '장수가 존재하지 않습니다.' });
|
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: '장수가 존재하지 않습니다.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rankRows = await ctx.db.rankData.findMany({
|
||||||
|
where: {
|
||||||
|
generalId: general.id,
|
||||||
|
type: { in: ['warnum', 'firenum', 'betwin', 'betgold', 'betwingold'] },
|
||||||
|
},
|
||||||
|
select: { type: true, value: true },
|
||||||
|
});
|
||||||
|
const calculationMeta = {
|
||||||
|
...asRecord(general.meta),
|
||||||
|
...Object.fromEntries(rankRows.map((row) => [row.type, row.value])),
|
||||||
|
...Object.fromEntries(rankRows.map((row) => [`rank_${row.type}`, row.value])),
|
||||||
|
};
|
||||||
|
|
||||||
const meta = asRecord(worldState.meta);
|
const meta = asRecord(worldState.meta);
|
||||||
const isUnited = typeof meta.isUnited === 'number' && meta.isUnited !== 0;
|
const isUnited =
|
||||||
|
(typeof meta.isUnited === 'number' && meta.isUnited !== 0) ||
|
||||||
|
(typeof meta.isunited === 'number' && meta.isunited !== 0);
|
||||||
const items = await computeInheritanceItems({
|
const items = await computeInheritanceItems({
|
||||||
db: ctx.db,
|
db: ctx.db,
|
||||||
userId,
|
userId,
|
||||||
generalMeta: asRecord(general.meta),
|
generalMeta: calculationMeta,
|
||||||
isUnited,
|
isUnited,
|
||||||
});
|
});
|
||||||
const totalPoint = sumInheritanceItems(items);
|
const totalPoint = sumInheritanceItems(items);
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { asNumber, asRecord } from '@sammo-ts/common';
|
import { asNumber, asRecord } from '@sammo-ts/common';
|
||||||
import { GamePrisma } from '@sammo-ts/infra';
|
import { GamePrisma } from '@sammo-ts/infra';
|
||||||
|
import {
|
||||||
|
ALL_MERGED_INHERITANCE_KEYS,
|
||||||
|
computeActiveInheritancePoint,
|
||||||
|
} from '@sammo-ts/logic/inheritance/pointCalculation.js';
|
||||||
import type { DatabaseClient, WorldStateRow, InputJsonValue } from '../context.js';
|
import type { DatabaseClient, WorldStateRow, InputJsonValue } from '../context.js';
|
||||||
|
|
||||||
export type InheritPointKey =
|
export type InheritPointKey =
|
||||||
@@ -163,73 +167,34 @@ export const appendInheritanceLog = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const readUserMetaValue = (meta: Record<string, unknown>, key: string): number => {
|
|
||||||
const value = meta[key];
|
|
||||||
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const computeDexPoint = (meta: Record<string, unknown>): number => {
|
|
||||||
let total = 0;
|
|
||||||
for (const [key, value] of Object.entries(meta)) {
|
|
||||||
if (!key.startsWith('dex')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
||||||
total += value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return total * 0.001;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const computeInheritanceItems = async (options: {
|
export const computeInheritanceItems = async (options: {
|
||||||
db: DatabaseClient;
|
db: DatabaseClient;
|
||||||
userId: string;
|
userId: string;
|
||||||
generalMeta: Record<string, unknown> | null;
|
generalMeta: Record<string, unknown> | null;
|
||||||
isUnited: boolean;
|
isUnited: boolean;
|
||||||
}): Promise<Record<InheritPointKey, number>> => {
|
}): Promise<Record<InheritPointKey, number>> => {
|
||||||
const previous = await readInheritancePoint(options.db, options.userId, 'previous');
|
const pointRows = await options.db.inheritancePoint.findMany({
|
||||||
const unifier = await readInheritancePoint(options.db, options.userId, 'unifier');
|
where: { userId: options.userId },
|
||||||
|
select: { key: true, value: true },
|
||||||
|
});
|
||||||
|
const inheritancePoints = Object.fromEntries(pointRows.map((row) => [row.key, row.value]));
|
||||||
|
const previous = inheritancePoints.previous ?? 0;
|
||||||
|
const general = {
|
||||||
|
meta: options.generalMeta ?? {},
|
||||||
|
inheritancePoints,
|
||||||
|
};
|
||||||
|
|
||||||
if (options.isUnited) {
|
if (options.isUnited) {
|
||||||
return {
|
return Object.fromEntries([
|
||||||
previous,
|
['previous', previous],
|
||||||
lived_month: 0,
|
...ALL_MERGED_INHERITANCE_KEYS.map((key) => [key, inheritancePoints[key] ?? 0] as const),
|
||||||
max_domestic_critical: 0,
|
]) as Record<InheritPointKey, number>;
|
||||||
active_action: 0,
|
|
||||||
combat: 0,
|
|
||||||
sabotage: 0,
|
|
||||||
dex: 0,
|
|
||||||
unifier,
|
|
||||||
tournament: 0,
|
|
||||||
betting: 0,
|
|
||||||
max_belong: 0,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const meta = options.generalMeta ?? {};
|
return Object.fromEntries([
|
||||||
const livedMonth = readUserMetaValue(meta, 'inherit_lived_month');
|
['previous', previous],
|
||||||
const maxDomestic = readUserMetaValue(meta, 'max_domestic_critical');
|
...ALL_MERGED_INHERITANCE_KEYS.map((key) => [key, computeActiveInheritancePoint(general, key)] as const),
|
||||||
const activeAction = readUserMetaValue(meta, 'inherit_active_action');
|
]) as Record<InheritPointKey, number>;
|
||||||
const combat = readUserMetaValue(meta, 'rank_warnum') * 5;
|
|
||||||
const sabotage = readUserMetaValue(meta, 'firenum') * 20;
|
|
||||||
const dex = computeDexPoint(meta);
|
|
||||||
|
|
||||||
return {
|
|
||||||
previous,
|
|
||||||
lived_month: livedMonth,
|
|
||||||
max_domestic_critical: maxDomestic,
|
|
||||||
active_action: activeAction,
|
|
||||||
combat,
|
|
||||||
sabotage,
|
|
||||||
dex,
|
|
||||||
unifier,
|
|
||||||
tournament: 0,
|
|
||||||
betting: 0,
|
|
||||||
max_belong: 0,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sumInheritanceItems = (items: Record<InheritPointKey, number>): number => {
|
export const sumInheritanceItems = (items: Record<InheritPointKey, number>): number => {
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ const buildContext = (options: {
|
|||||||
general?: GeneralRow | null;
|
general?: GeneralRow | null;
|
||||||
target?: GeneralRow | null;
|
target?: GeneralRow | null;
|
||||||
inheritancePoint?: number;
|
inheritancePoint?: number;
|
||||||
|
inheritanceRows?: Array<{ key: string; value: number }>;
|
||||||
|
rankRows?: Array<{ type: string; value: number }>;
|
||||||
inheritanceLogs?: Array<{ id: number; year: number; month: number; text: string; createdAt: Date }>;
|
inheritanceLogs?: Array<{ id: number; year: number; month: number; text: string; createdAt: Date }>;
|
||||||
configConst?: Record<string, unknown>;
|
configConst?: Record<string, unknown>;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -176,6 +178,12 @@ const buildContext = (options: {
|
|||||||
},
|
},
|
||||||
inheritancePoint: {
|
inheritancePoint: {
|
||||||
upsert: pointUpsert,
|
upsert: pointUpsert,
|
||||||
|
findMany: vi.fn(
|
||||||
|
async () => options.inheritanceRows ?? [{ key: 'previous', value: options.inheritancePoint ?? 10_000 }]
|
||||||
|
),
|
||||||
|
},
|
||||||
|
rankData: {
|
||||||
|
findMany: vi.fn(async () => options.rankRows ?? []),
|
||||||
},
|
},
|
||||||
inheritanceLog: {
|
inheritanceLog: {
|
||||||
create: logCreate,
|
create: logCreate,
|
||||||
@@ -254,6 +262,58 @@ describe('inherit router actor and permission boundaries', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('projects every Ref inheritance source with its own coefficient and stored/calculated boundary', async () => {
|
||||||
|
const fixture = buildContext({
|
||||||
|
general: buildGeneral({
|
||||||
|
meta: {
|
||||||
|
inherit_lived_month: 12,
|
||||||
|
max_domestic_critical: 20,
|
||||||
|
inherit_active_action: 0.5,
|
||||||
|
belong: 7,
|
||||||
|
max_belong: 9,
|
||||||
|
rank_warnum: 300,
|
||||||
|
firenum: 200,
|
||||||
|
dex1: 1_275_978,
|
||||||
|
dex2: 100,
|
||||||
|
event100_allstar: { granted: { dex2: 40 } },
|
||||||
|
betwin: 200,
|
||||||
|
betgold: 200_000,
|
||||||
|
betwingold: 100_000,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
rankRows: [
|
||||||
|
{ type: 'warnum', value: 3 },
|
||||||
|
{ type: 'firenum', value: 2 },
|
||||||
|
{ type: 'betwin', value: 2 },
|
||||||
|
{ type: 'betgold', value: 2_000 },
|
||||||
|
{ type: 'betwingold', value: 1_000 },
|
||||||
|
],
|
||||||
|
inheritanceRows: [
|
||||||
|
{ key: 'previous', value: 100 },
|
||||||
|
{ key: 'max_domestic_critical', value: 80 },
|
||||||
|
{ key: 'unifier', value: 250 },
|
||||||
|
{ key: 'tournament', value: 50 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = await appRouter.createCaller(fixture.context).inherit.getStatus();
|
||||||
|
|
||||||
|
expect(status.items).toEqual({
|
||||||
|
previous: 100,
|
||||||
|
lived_month: 12,
|
||||||
|
max_domestic_critical: 80,
|
||||||
|
active_action: 1.5,
|
||||||
|
unifier: 250,
|
||||||
|
tournament: 50,
|
||||||
|
max_belong: 90,
|
||||||
|
combat: 15,
|
||||||
|
sabotage: 40,
|
||||||
|
dex: 1_276.036,
|
||||||
|
betting: 5,
|
||||||
|
});
|
||||||
|
expect(status.totalPoint).toBeCloseTo(1_919.536, 8);
|
||||||
|
});
|
||||||
|
|
||||||
it.each([{}, { allItems: '{}' }])(
|
it.each([{}, { allItems: '{}' }])(
|
||||||
'restores selectable Ref default uniques for a legacy scenario config: %j',
|
'restores selectable Ref default uniques for a legacy scenario config: %j',
|
||||||
async (configConst) => {
|
async (configConst) => {
|
||||||
|
|||||||
@@ -142,12 +142,12 @@ export const createTournamentRewardFinalizer = async (options: {
|
|||||||
const nameMap = new Map<number, string>();
|
const nameMap = new Map<number, string>();
|
||||||
const generals = await db.general.findMany({
|
const generals = await db.general.findMany({
|
||||||
where: { id: { in: Array.from(rewardMap.keys()) } },
|
where: { id: { in: Array.from(rewardMap.keys()) } },
|
||||||
select: { id: true, userId: true, name: true },
|
select: { id: true, userId: true, name: true, npcState: true },
|
||||||
});
|
});
|
||||||
const userMap = new Map<number, string>();
|
const userMap = new Map<number, string>();
|
||||||
for (const general of generals) {
|
for (const general of generals) {
|
||||||
nameMap.set(general.id, general.name);
|
nameMap.set(general.id, general.name);
|
||||||
if (general.userId) {
|
if (general.userId && general.npcState < 2) {
|
||||||
userMap.set(general.id, general.userId);
|
userMap.set(general.id, general.userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,6 +261,15 @@ export const createTournamentRewardFinalizer = async (options: {
|
|||||||
update: { value: { increment: entry.value } },
|
update: { value: { increment: entry.value } },
|
||||||
create: { userId: entry.userId!, key: 'tournament', value: entry.value },
|
create: { userId: entry.userId!, key: 'tournament', value: entry.value },
|
||||||
});
|
});
|
||||||
|
const general = world.getGeneralById(entry.generalId);
|
||||||
|
if (general) {
|
||||||
|
world.updateGeneral(entry.generalId, {
|
||||||
|
inheritancePoints: {
|
||||||
|
...general.inheritancePoints,
|
||||||
|
tournament: Number(general.inheritancePoints?.tournament ?? 0) + entry.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1241,21 +1241,6 @@ export const createDatabaseTurnHooks = async (
|
|||||||
const meta = asRecord(state.meta);
|
const meta = asRecord(state.meta);
|
||||||
const serverId =
|
const serverId =
|
||||||
typeof meta.serverId === 'string' && meta.serverId.trim() ? meta.serverId.trim() : 'default';
|
typeof meta.serverId === 'string' && meta.serverId.trim() ? meta.serverId.trim() : 'default';
|
||||||
await persistGeneralLifecycleEvents(
|
|
||||||
prisma,
|
|
||||||
lifecycleEvents,
|
|
||||||
meta,
|
|
||||||
asRecord(world.getScenarioConfig().const),
|
|
||||||
world.gameTickToDate(state.clockTick ?? state.lastTurnTick ?? 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (accessScoreResetGeneralIds.length > 0) {
|
|
||||||
await prisma.generalAccessLog.updateMany({
|
|
||||||
where: { generalId: { in: accessScoreResetGeneralIds } },
|
|
||||||
data: { refreshScore: 0 },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inheritancePointAdjustments.length > 0) {
|
if (inheritancePointAdjustments.length > 0) {
|
||||||
const grouped = new Map<string, { userId: string; key: string; amount: number }>();
|
const grouped = new Map<string, { userId: string; key: string; amount: number }>();
|
||||||
for (const entry of inheritancePointAdjustments) {
|
for (const entry of inheritancePointAdjustments) {
|
||||||
@@ -1275,6 +1260,20 @@ export const createDatabaseTurnHooks = async (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await persistGeneralLifecycleEvents(
|
||||||
|
prisma,
|
||||||
|
lifecycleEvents,
|
||||||
|
meta,
|
||||||
|
asRecord(world.getScenarioConfig().const),
|
||||||
|
world.gameTickToDate(state.clockTick ?? state.lastTurnTick ?? 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (accessScoreResetGeneralIds.length > 0) {
|
||||||
|
await prisma.generalAccessLog.updateMany({
|
||||||
|
where: { generalId: { in: accessScoreResetGeneralIds } },
|
||||||
|
data: { refreshScore: 0 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (deletedNationSnapshots.length > 0) {
|
if (deletedNationSnapshots.length > 0) {
|
||||||
const nationIds = deletedNationSnapshots.map((snapshot) => snapshot.nation.id);
|
const nationIds = deletedNationSnapshots.map((snapshot) => snapshot.nation.id);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { asRecord, HALL_OF_FAME_TYPES, resolveLegacyTextColor, type HallOfFameType } from '@sammo-ts/common';
|
import { asRecord, HALL_OF_FAME_TYPES, resolveLegacyTextColor, type HallOfFameType } from '@sammo-ts/common';
|
||||||
import type { GamePrisma, InputJsonValue } from '@sammo-ts/infra';
|
import type { GamePrisma, InputJsonValue } from '@sammo-ts/infra';
|
||||||
import { LogCategory, LogScope } from '@sammo-ts/logic';
|
import { LogCategory, LogScope } from '@sammo-ts/logic';
|
||||||
|
import { computeInheritanceSettlementBreakdown } from '@sammo-ts/logic/inheritance/pointCalculation.js';
|
||||||
|
|
||||||
import type { GeneralLifecycleEvent } from './inMemoryWorld.js';
|
import type { GeneralLifecycleEvent } from './inMemoryWorld.js';
|
||||||
|
|
||||||
@@ -23,14 +24,6 @@ const readWorldNumber = (record: Record<string, unknown>, key: string, fallback:
|
|||||||
return value === 0 && record[key] === undefined ? fallback : Math.floor(value);
|
return value === 0 && record[key] === undefined ? fallback : Math.floor(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const computeDexPoint = (meta: Record<string, unknown>): number => {
|
|
||||||
let total = 0;
|
|
||||||
for (let dex = 1; dex <= 5; dex += 1) {
|
|
||||||
total += readNumber(meta, `dex${dex}`);
|
|
||||||
}
|
|
||||||
return total * 0.001;
|
|
||||||
};
|
|
||||||
|
|
||||||
const settleInheritance = async (
|
const settleInheritance = async (
|
||||||
prisma: GamePrisma.TransactionClient,
|
prisma: GamePrisma.TransactionClient,
|
||||||
event: GeneralLifecycleEvent,
|
event: GeneralLifecycleEvent,
|
||||||
@@ -71,8 +64,6 @@ const settleInheritance = async (
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
const points = new Map(rows.map((row) => [row.key, row.value]));
|
const points = new Map(rows.map((row) => [row.key, row.value]));
|
||||||
const ranks = new Map(rankRows.map((row) => [row.type, row.value]));
|
|
||||||
const rank = (key: string): number => ranks.get(key) ?? readNumber(meta, `rank_${key}`);
|
|
||||||
const previous = points.get('previous') ?? 0;
|
const previous = points.get('previous') ?? 0;
|
||||||
const randomUniqueRefund = meta.inheritRandomUnique
|
const randomUniqueRefund = meta.inheritRandomUnique
|
||||||
? readWorldNumber(configConst, 'inheritItemRandomPoint', 3000)
|
? readWorldNumber(configConst, 'inheritItemRandomPoint', 3000)
|
||||||
@@ -81,30 +72,45 @@ const settleInheritance = async (
|
|||||||
? readWorldNumber(configConst, 'inheritSpecificSpecialPoint', 4000)
|
? readWorldNumber(configConst, 'inheritSpecificSpecialPoint', 4000)
|
||||||
: 0;
|
: 0;
|
||||||
const refund = randomUniqueRefund + specificSpecialRefund;
|
const refund = randomUniqueRefund + specificSpecialRefund;
|
||||||
const lived = readNumber(meta, 'inherit_lived_month');
|
const calculationMeta = {
|
||||||
const maxBelong = readNumber(meta, 'inherit_max_belong') * 10;
|
...meta,
|
||||||
const maxDomestic = readNumber(meta, 'max_domestic_critical');
|
...Object.fromEntries(rankRows.map((row) => [row.type, row.value])),
|
||||||
const active = readNumber(meta, 'inherit_active_action') * 3;
|
...Object.fromEntries(rankRows.map((row) => [`rank_${row.type}`, row.value])),
|
||||||
const combat = rank('warnum') * 5;
|
};
|
||||||
const sabotage = (ranks.get('firenum') ?? readNumber(meta, 'firenum')) * 20;
|
const settlement = computeInheritanceSettlementBreakdown(
|
||||||
const dex = computeDexPoint(meta);
|
{
|
||||||
const unifier = points.get('unifier') ?? 0;
|
meta: calculationMeta,
|
||||||
const earned = isRebirth
|
inheritancePoints: Object.fromEntries(points),
|
||||||
? lived + active + combat + sabotage + dex * 0.5
|
},
|
||||||
: lived + maxBelong + maxDomestic + active + combat + sabotage + dex + unifier;
|
isRebirth
|
||||||
const total = Math.trunc(previous + refund + earned);
|
);
|
||||||
|
const total = Math.trunc(previous + refund + settlement.totalEarned);
|
||||||
|
|
||||||
await prisma.inheritancePoint.upsert({
|
await prisma.inheritancePoint.upsert({
|
||||||
where: { userId_key: { userId, key: 'previous' } },
|
where: { userId_key: { userId, key: 'previous' } },
|
||||||
update: { value: total },
|
update: { value: total },
|
||||||
create: { userId, key: 'previous', value: total },
|
create: { userId, key: 'previous', value: total },
|
||||||
});
|
});
|
||||||
|
if (isRebirth) {
|
||||||
|
const retainedEntries = Object.entries(settlement.retained).filter(
|
||||||
|
([key, value]) => key === 'max_belong' || points.has(key) || value !== 0
|
||||||
|
);
|
||||||
|
for (const [key, value] of retainedEntries) {
|
||||||
|
await prisma.inheritancePoint.upsert({
|
||||||
|
where: { userId_key: { userId, key } },
|
||||||
|
update: { value },
|
||||||
|
create: { userId, key, value },
|
||||||
|
});
|
||||||
|
}
|
||||||
await prisma.inheritancePoint.deleteMany({
|
await prisma.inheritancePoint.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
userId,
|
userId,
|
||||||
key: isRebirth ? { notIn: ['previous', 'unifier'] } : { not: 'previous' },
|
key: { notIn: ['previous', ...retainedEntries.map(([key]) => key)] },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
await prisma.inheritancePoint.deleteMany({ where: { userId, key: { not: 'previous' } } });
|
||||||
|
}
|
||||||
const serverId =
|
const serverId =
|
||||||
typeof worldMeta.serverId === 'string' && worldMeta.serverId.trim() ? worldMeta.serverId.trim() : 'default';
|
typeof worldMeta.serverId === 'string' && worldMeta.serverId.trim() ? worldMeta.serverId.trim() : 'default';
|
||||||
await prisma.inheritanceResult.create({
|
await prisma.inheritanceResult.create({
|
||||||
@@ -117,15 +123,10 @@ const settleInheritance = async (
|
|||||||
value: asJson({
|
value: asJson({
|
||||||
previous,
|
previous,
|
||||||
refund,
|
refund,
|
||||||
lived_month: lived,
|
...settlement.earned,
|
||||||
max_belong: maxBelong,
|
...(isRebirth ? { retained: settlement.retained } : {}),
|
||||||
max_domestic_critical: maxDomestic,
|
|
||||||
active_action: active,
|
|
||||||
combat,
|
|
||||||
sabotage,
|
|
||||||
dex: isRebirth ? dex * 0.5 : dex,
|
|
||||||
unifier: isRebirth ? 0 : unifier,
|
|
||||||
rebirth: isRebirth,
|
rebirth: isRebirth,
|
||||||
|
total,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,97 +1,12 @@
|
|||||||
const DEX_LIMIT = 1_275_975;
|
export {
|
||||||
|
ALL_MERGED_INHERITANCE_KEYS,
|
||||||
interface InheritancePointGeneral {
|
computeActiveInheritancePoint,
|
||||||
meta: Record<string, unknown>;
|
computeBettingInheritancePoint,
|
||||||
inheritancePoints?: Record<string, number>;
|
computeDexInheritancePoint,
|
||||||
}
|
computeInheritanceSettlementBreakdown,
|
||||||
|
LEGACY_DEX_INHERITANCE_LIMIT,
|
||||||
const STORED_INHERITANCE_KEYS = [
|
REBIRTH_INHERITANCE_COEFFICIENTS,
|
||||||
'lived_month',
|
type InheritancePointGeneral,
|
||||||
'max_domestic_critical',
|
type InheritanceSettlementBreakdown,
|
||||||
'active_action',
|
type MergedInheritanceKey,
|
||||||
'unifier',
|
} from '@sammo-ts/logic/inheritance/pointCalculation.js';
|
||||||
'tournament',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const ALL_MERGED_INHERITANCE_KEYS = [
|
|
||||||
...STORED_INHERITANCE_KEYS,
|
|
||||||
'max_belong',
|
|
||||||
'combat',
|
|
||||||
'sabotage',
|
|
||||||
'dex',
|
|
||||||
'betting',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export type MergedInheritanceKey = (typeof ALL_MERGED_INHERITANCE_KEYS)[number];
|
|
||||||
|
|
||||||
const readNumber = (source: Record<string, unknown>, key: string): number => {
|
|
||||||
const value = source[key];
|
|
||||||
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
const parsed = Number(value);
|
|
||||||
if (Number.isFinite(parsed)) return parsed;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const computeDexPoint = (general: InheritancePointGeneral): number => {
|
|
||||||
let totalDexterity = 0;
|
|
||||||
for (let index = 1; index <= 5; index += 1) {
|
|
||||||
let dexterity = readNumber(general.meta, `dex${index}`);
|
|
||||||
if (dexterity > DEX_LIMIT) {
|
|
||||||
totalDexterity += (dexterity - DEX_LIMIT) / 3;
|
|
||||||
dexterity = DEX_LIMIT;
|
|
||||||
}
|
|
||||||
totalDexterity += dexterity;
|
|
||||||
}
|
|
||||||
return totalDexterity * 0.001;
|
|
||||||
};
|
|
||||||
|
|
||||||
const computeBettingPoint = (general: InheritancePointGeneral): number => {
|
|
||||||
const wins = readNumber(general.meta, 'betwin');
|
|
||||||
const gold = readNumber(general.meta, 'betgold');
|
|
||||||
const wonGold = readNumber(general.meta, 'betwingold');
|
|
||||||
const winRate = wonGold / Math.max(1000, gold);
|
|
||||||
return wins * 10 * winRate ** 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const computeActiveInheritancePoint = (
|
|
||||||
general: InheritancePointGeneral,
|
|
||||||
key: MergedInheritanceKey,
|
|
||||||
storedOverride?: number
|
|
||||||
): number => {
|
|
||||||
const stored = storedOverride ?? general.inheritancePoints?.[key] ?? 0;
|
|
||||||
switch (key) {
|
|
||||||
case 'lived_month': {
|
|
||||||
const value = readNumber(general.meta, 'inherit_lived_month');
|
|
||||||
return value !== 0 ? value : stored;
|
|
||||||
}
|
|
||||||
case 'max_domestic_critical': {
|
|
||||||
const value = readNumber(general.meta, 'max_domestic_critical');
|
|
||||||
return value !== 0 ? value : stored;
|
|
||||||
}
|
|
||||||
case 'active_action': {
|
|
||||||
const value = readNumber(general.meta, 'inherit_active_action');
|
|
||||||
return value !== 0 ? value * 3 : stored;
|
|
||||||
}
|
|
||||||
case 'unifier':
|
|
||||||
case 'tournament':
|
|
||||||
return stored;
|
|
||||||
case 'max_belong':
|
|
||||||
return (
|
|
||||||
Math.max(
|
|
||||||
readNumber(general.meta, 'belong'),
|
|
||||||
readNumber(general.meta, 'max_belong'),
|
|
||||||
readNumber(general.meta, 'inherit_max_belong')
|
|
||||||
) * 10
|
|
||||||
);
|
|
||||||
case 'combat':
|
|
||||||
return readNumber(general.meta, 'rank_warnum') * 5;
|
|
||||||
case 'sabotage':
|
|
||||||
return readNumber(general.meta, 'firenum') * 20;
|
|
||||||
case 'dex':
|
|
||||||
return computeDexPoint(general);
|
|
||||||
case 'betting':
|
|
||||||
return computeBettingPoint(general);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -375,8 +375,15 @@ export const createUpdateNationLevelHandler = (options: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const isUnited = readNumber(state.meta.isunited ?? state.meta.isUnited);
|
const isUnited = readNumber(state.meta.isunited ?? state.meta.isUnited);
|
||||||
if (chief?.userId && isUnited === 0) {
|
if (chief?.userId && chief.npcState < 2 && isUnited === 0) {
|
||||||
world.queueInheritancePointAdjustment(chief.userId, 'unifier', 250 * levelDiff);
|
const amount = 250 * levelDiff;
|
||||||
|
world.queueInheritancePointAdjustment(chief.userId, 'unifier', amount);
|
||||||
|
world.updateGeneral(chief.id, {
|
||||||
|
inheritancePoints: {
|
||||||
|
...chief.inheritancePoints,
|
||||||
|
unifier: readNumber(chief.inheritancePoints?.unifier) + amount,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ const readConfigNumber = (config: ScenarioConfig, key: string, fallback: number)
|
|||||||
|
|
||||||
const cloneTurnGeneral = (general: TurnGeneral): TurnGeneral => ({
|
const cloneTurnGeneral = (general: TurnGeneral): TurnGeneral => ({
|
||||||
...general,
|
...general,
|
||||||
|
...(general.inheritancePoints ? { inheritancePoints: { ...general.inheritancePoints } } : {}),
|
||||||
stats: { ...general.stats },
|
stats: { ...general.stats },
|
||||||
role: {
|
role: {
|
||||||
...general.role,
|
...general.role,
|
||||||
@@ -372,6 +373,25 @@ const readMetaNumber = (meta: Record<string, unknown>, key: string, fallback: nu
|
|||||||
return fallback;
|
return fallback;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const readInheritanceNumber = (value: unknown): number => {
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed : 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const canAccumulateInheritance = (
|
||||||
|
general: Pick<TurnGeneral, 'userId' | 'npcState'>,
|
||||||
|
worldMeta: Record<string, unknown>
|
||||||
|
): general is Pick<TurnGeneral, 'userId' | 'npcState'> & { userId: string } =>
|
||||||
|
Boolean(general.userId) &&
|
||||||
|
general.npcState < 2 &&
|
||||||
|
readMetaNumber(worldMeta, 'isunited', readMetaNumber(worldMeta, 'isUnited', 0)) === 0;
|
||||||
|
|
||||||
const readMetaBool = (meta: Record<string, unknown>, key: string, fallback = false): boolean => {
|
const readMetaBool = (meta: Record<string, unknown>, key: string, fallback = false): boolean => {
|
||||||
const value = meta[key];
|
const value = meta[key];
|
||||||
if (typeof value === 'boolean') {
|
if (typeof value === 'boolean') {
|
||||||
@@ -1214,6 +1234,34 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
currentGeneral = resolution.general as TurnGeneral;
|
currentGeneral = resolution.general as TurnGeneral;
|
||||||
currentCity = resolution.city ?? currentCity;
|
currentCity = resolution.city ?? currentCity;
|
||||||
currentNation = resolution.nation ?? currentNation;
|
currentNation = resolution.nation ?? currentNation;
|
||||||
|
const inheritanceEnabled = canAccumulateInheritance(currentGeneral, asRecord(context.world.meta));
|
||||||
|
const inheritanceUserId = inheritanceEnabled ? currentGeneral.userId : null;
|
||||||
|
if (actionKey === 'che_인재탐색') {
|
||||||
|
const previousActive = readMetaNumber(
|
||||||
|
asRecord(generalBeforeExecution.meta),
|
||||||
|
'inherit_active_action',
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const nextActive = readMetaNumber(asRecord(currentGeneral.meta), 'inherit_active_action', 0);
|
||||||
|
if (!inheritanceUserId) {
|
||||||
|
currentGeneral = {
|
||||||
|
...currentGeneral,
|
||||||
|
meta: { ...currentGeneral.meta, inherit_active_action: previousActive },
|
||||||
|
};
|
||||||
|
} else if (nextActive > previousActive) {
|
||||||
|
const pointAmount = (nextActive - previousActive) * 3;
|
||||||
|
worldRef?.queueInheritancePointAdjustment(inheritanceUserId, 'active_action', pointAmount);
|
||||||
|
currentGeneral = {
|
||||||
|
...currentGeneral,
|
||||||
|
inheritancePoints: {
|
||||||
|
...currentGeneral.inheritancePoints,
|
||||||
|
active_action:
|
||||||
|
readInheritanceNumber(currentGeneral.inheritancePoints?.active_action) +
|
||||||
|
pointAmount,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!resolution.alternative && !usedFallback && resolution.completed) {
|
if (!resolution.alternative && !usedFallback && resolution.completed) {
|
||||||
currentGeneral = applyLegacyGeneralProgression(
|
currentGeneral = applyLegacyGeneralProgression(
|
||||||
currentGeneral,
|
currentGeneral,
|
||||||
@@ -1229,13 +1277,20 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
!usedFallback &&
|
!usedFallback &&
|
||||||
resolution.completed &&
|
resolution.completed &&
|
||||||
definition.countsAsInheritanceActiveAction &&
|
definition.countsAsInheritanceActiveAction &&
|
||||||
Boolean(currentGeneral.userId) &&
|
inheritanceUserId
|
||||||
currentGeneral.npcState < 2
|
|
||||||
) {
|
) {
|
||||||
const meta = { ...currentGeneral.meta };
|
const meta = { ...currentGeneral.meta };
|
||||||
const active = typeof meta.inherit_active_action === 'number' ? meta.inherit_active_action : 0;
|
const active = typeof meta.inherit_active_action === 'number' ? meta.inherit_active_action : 0;
|
||||||
meta.inherit_active_action = active + 1;
|
meta.inherit_active_action = active + 1;
|
||||||
currentGeneral = { ...currentGeneral, meta };
|
worldRef?.queueInheritancePointAdjustment(inheritanceUserId, 'active_action', 3);
|
||||||
|
currentGeneral = {
|
||||||
|
...currentGeneral,
|
||||||
|
meta,
|
||||||
|
inheritancePoints: {
|
||||||
|
...currentGeneral.inheritancePoints,
|
||||||
|
active_action: readInheritanceNumber(currentGeneral.inheritancePoints?.active_action) + 3,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!resolution.alternative &&
|
!resolution.alternative &&
|
||||||
@@ -1243,17 +1298,53 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
!usedFallback &&
|
!usedFallback &&
|
||||||
resolution.completed &&
|
resolution.completed &&
|
||||||
executionDefinition.getInheritanceActiveActionAmount &&
|
executionDefinition.getInheritanceActiveActionAmount &&
|
||||||
Boolean(currentGeneral.userId) &&
|
inheritanceEnabled
|
||||||
currentGeneral.npcState < 2
|
|
||||||
) {
|
) {
|
||||||
const amount = executionDefinition.getInheritanceActiveActionAmount(actionContext, actionArgs);
|
const amount = executionDefinition.getInheritanceActiveActionAmount(actionContext, actionArgs);
|
||||||
if (Number.isFinite(amount) && amount !== 0) {
|
if (Number.isFinite(amount) && amount !== 0) {
|
||||||
const meta = { ...currentGeneral.meta };
|
const meta = { ...currentGeneral.meta };
|
||||||
const active = typeof meta.inherit_active_action === 'number' ? meta.inherit_active_action : 0;
|
const active = typeof meta.inherit_active_action === 'number' ? meta.inherit_active_action : 0;
|
||||||
meta.inherit_active_action = active + amount;
|
meta.inherit_active_action = active + amount;
|
||||||
currentGeneral = { ...currentGeneral, meta };
|
const pointAmount = amount * 3;
|
||||||
|
worldRef?.queueInheritancePointAdjustment(inheritanceUserId!, 'active_action', pointAmount);
|
||||||
|
currentGeneral = {
|
||||||
|
...currentGeneral,
|
||||||
|
meta,
|
||||||
|
inheritancePoints: {
|
||||||
|
...currentGeneral.inheritancePoints,
|
||||||
|
active_action:
|
||||||
|
readInheritanceNumber(currentGeneral.inheritancePoints?.active_action) +
|
||||||
|
pointAmount,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
!resolution.alternative &&
|
||||||
|
kind === 'general' &&
|
||||||
|
!usedFallback &&
|
||||||
|
resolution.completed &&
|
||||||
|
inheritanceUserId
|
||||||
|
) {
|
||||||
|
const inheritancePoints = { ...currentGeneral.inheritancePoints };
|
||||||
|
const storedDomesticMaximum = readInheritanceNumber(inheritancePoints.max_domestic_critical);
|
||||||
|
const currentDomesticStreak = readInheritanceNumber(
|
||||||
|
asRecord(currentGeneral.meta).max_domestic_critical
|
||||||
|
);
|
||||||
|
if (currentDomesticStreak > storedDomesticMaximum) {
|
||||||
|
worldRef?.queueInheritancePointAdjustment(
|
||||||
|
inheritanceUserId,
|
||||||
|
'max_domestic_critical',
|
||||||
|
currentDomesticStreak - storedDomesticMaximum
|
||||||
|
);
|
||||||
|
inheritancePoints.max_domestic_critical = currentDomesticStreak;
|
||||||
|
}
|
||||||
|
if (actionKey === 'che_건국') {
|
||||||
|
worldRef?.queueInheritancePointAdjustment(inheritanceUserId, 'unifier', 250);
|
||||||
|
inheritancePoints.unifier = readInheritanceNumber(inheritancePoints.unifier) + 250;
|
||||||
|
}
|
||||||
|
currentGeneral = { ...currentGeneral, inheritancePoints };
|
||||||
|
}
|
||||||
|
|
||||||
if (!currentNation && resolution.created?.nations) {
|
if (!currentNation && resolution.created?.nations) {
|
||||||
currentNation =
|
currentNation =
|
||||||
@@ -1551,9 +1642,14 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
|
|
||||||
const lifecycleBefore = cloneTurnGeneral(currentGeneral);
|
const lifecycleBefore = cloneTurnGeneral(currentGeneral);
|
||||||
currentGeneral = cloneTurnGeneral(currentGeneral);
|
currentGeneral = cloneTurnGeneral(currentGeneral);
|
||||||
if (currentGeneral.npcState < 2) {
|
if (canAccumulateInheritance(currentGeneral, asRecord(context.world.meta))) {
|
||||||
currentGeneral.meta.inherit_lived_month =
|
currentGeneral.meta.inherit_lived_month =
|
||||||
readMetaNumber(currentGeneral.meta, 'inherit_lived_month', 0) + 1;
|
readMetaNumber(currentGeneral.meta, 'inherit_lived_month', 0) + 1;
|
||||||
|
worldRef?.queueInheritancePointAdjustment(currentGeneral.userId, 'lived_month', 1);
|
||||||
|
currentGeneral.inheritancePoints = {
|
||||||
|
...currentGeneral.inheritancePoints,
|
||||||
|
lived_month: readInheritanceNumber(currentGeneral.inheritancePoints?.lived_month) + 1,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const preprocessRng = new RandUtil(
|
const preprocessRng = new RandUtil(
|
||||||
new LiteHashDRBG(
|
new LiteHashDRBG(
|
||||||
@@ -1626,10 +1722,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
currentGeneral.crew = 0;
|
currentGeneral.crew = 0;
|
||||||
currentGeneral.rice = 0;
|
currentGeneral.rice = 0;
|
||||||
logs.push(
|
logs.push(
|
||||||
createGeneralActionLog(
|
createGeneralActionLog(currentGeneral.id, '군량이 모자라 병사들이 <R>소집해제</>되었습니다!')
|
||||||
currentGeneral.id,
|
|
||||||
'군량이 모자라 병사들이 <R>소집해제</>되었습니다!'
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
preTurnContext.skill.activate('pre.소집해제');
|
preTurnContext.skill.activate('pre.소집해제');
|
||||||
}
|
}
|
||||||
@@ -2125,10 +2218,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
currentGeneral = resetRetiredGeneral(currentGeneral);
|
currentGeneral = resetRetiredGeneral(currentGeneral);
|
||||||
lifecycleOutcome = 'retired';
|
lifecycleOutcome = 'retired';
|
||||||
logs.push(
|
logs.push(
|
||||||
createGeneralActionLog(
|
createGeneralActionLog(currentGeneral.id, '나이가 들어 <R>은퇴</>하고 자손에게 자리를 물려줍니다.')
|
||||||
currentGeneral.id,
|
|
||||||
'나이가 들어 <R>은퇴</>하고 자손에게 자리를 물려줍니다.'
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2384,11 +2474,17 @@ export const createImmediateGeneralActionExecutor = async (options: {
|
|||||||
if (
|
if (
|
||||||
Number.isFinite(activeActionAmount) &&
|
Number.isFinite(activeActionAmount) &&
|
||||||
activeActionAmount !== 0 &&
|
activeActionAmount !== 0 &&
|
||||||
nextGeneral.userId &&
|
canAccumulateInheritance(nextGeneral, asRecord(state.meta))
|
||||||
nextGeneral.npcState < 2
|
|
||||||
) {
|
) {
|
||||||
|
const pointAmount = activeActionAmount * 3;
|
||||||
|
options.world.queueInheritancePointAdjustment(nextGeneral.userId, 'active_action', pointAmount);
|
||||||
nextGeneral = {
|
nextGeneral = {
|
||||||
...nextGeneral,
|
...nextGeneral,
|
||||||
|
inheritancePoints: {
|
||||||
|
...nextGeneral.inheritancePoints,
|
||||||
|
active_action:
|
||||||
|
readInheritanceNumber(nextGeneral.inheritancePoints?.active_action) + pointAmount,
|
||||||
|
},
|
||||||
meta: {
|
meta: {
|
||||||
...nextGeneral.meta,
|
...nextGeneral.meta,
|
||||||
inherit_active_action:
|
inherit_active_action:
|
||||||
|
|||||||
@@ -215,16 +215,16 @@ integration('game cancellation transaction', () => {
|
|||||||
[userId]: {
|
[userId]: {
|
||||||
openingPoint: 10_000,
|
openingPoint: 10_000,
|
||||||
currentPoint: 7_000,
|
currentPoint: 7_000,
|
||||||
earnedPoint: 1_750.005,
|
earnedPoint: 1_790.005,
|
||||||
retainedEarnedPoint: 700,
|
retainedEarnedPoint: 716,
|
||||||
finalPoint: 10_700,
|
finalPoint: 10_716,
|
||||||
baselineSource: 'OPENING',
|
baselineSource: 'OPENING',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await expect(
|
await expect(
|
||||||
db.inheritancePoint.findMany({ where: { userId }, orderBy: { key: 'asc' } })
|
db.inheritancePoint.findMany({ where: { userId }, orderBy: { key: 'asc' } })
|
||||||
).resolves.toMatchObject([{ key: 'previous', value: 10_700 }]);
|
).resolves.toMatchObject([{ key: 'previous', value: 10_716 }]);
|
||||||
await expect(db.gameHistory.findUniqueOrThrow({ where: { serverId } })).resolves.toMatchObject({
|
await expect(db.gameHistory.findUniqueOrThrow({ where: { serverId } })).resolves.toMatchObject({
|
||||||
status: 'ABANDONED',
|
status: 'ABANDONED',
|
||||||
winnerNation: null,
|
winnerNation: null,
|
||||||
|
|||||||
@@ -391,7 +391,9 @@ describe('legacy general-turn execution contract', () => {
|
|||||||
expect(updated.injury).toBe(10);
|
expect(updated.injury).toBe(10);
|
||||||
expect(updated.experience).toBe(0);
|
expect(updated.experience).toBe(0);
|
||||||
expect(updated.meta.killturn).toBe(4);
|
expect(updated.meta.killturn).toBe(4);
|
||||||
expect(updated.meta.inherit_lived_month).toBe(1);
|
// Ref InheritancePointManager ignores every source when the general
|
||||||
|
// has no owner, including the per-turn lived_month source.
|
||||||
|
expect(updated.meta.inherit_lived_month).toBeUndefined();
|
||||||
expect(updated.meta.myset).toBe(3);
|
expect(updated.meta.myset).toBe(3);
|
||||||
expect(harness.reservedTurnStore.getGeneralTurn(1, 0).action).toBe('휴식');
|
expect(harness.reservedTurnStore.getGeneralTurn(1, 0).action).toBe('휴식');
|
||||||
expect(harness.getCollectedLogs().some((log) => log.text.includes('악성유저'))).toBe(true);
|
expect(harness.getCollectedLogs().some((log) => log.text.includes('악성유저'))).toBe(true);
|
||||||
|
|||||||
@@ -96,19 +96,28 @@ integration('general turn lifecycle persistence', () => {
|
|||||||
killturn: 0,
|
killturn: 0,
|
||||||
inherit_lived_month: 10,
|
inherit_lived_month: 10,
|
||||||
inherit_active_action: 2,
|
inherit_active_action: 2,
|
||||||
|
max_belong: 9,
|
||||||
inheritRandomUnique: true,
|
inheritRandomUnique: true,
|
||||||
dex1: 1_000,
|
dex1: 1_000,
|
||||||
dex2: 1,
|
dex2: 1,
|
||||||
dex3: 1,
|
dex3: 1,
|
||||||
dex4: 1,
|
dex4: 1,
|
||||||
dex5: 1,
|
dex5: 1,
|
||||||
|
betwin: 2,
|
||||||
|
betgold: 2_000,
|
||||||
|
betwingold: 1_000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await db.generalAccessLog.create({
|
await db.generalAccessLog.create({
|
||||||
data: { generalId: general.id, userId: general.userId, refreshScore: 99 },
|
data: { generalId: general.id, userId: general.userId, refreshScore: 99 },
|
||||||
});
|
});
|
||||||
await db.inheritancePoint.create({
|
await db.inheritancePoint.createMany({
|
||||||
data: { userId: general.userId!, key: 'previous', value: 100 },
|
data: [
|
||||||
|
{ userId: general.userId!, key: 'previous', value: 100 },
|
||||||
|
{ userId: general.userId!, key: 'max_domestic_critical', value: 80 },
|
||||||
|
{ userId: general.userId!, key: 'unifier', value: 250 },
|
||||||
|
{ userId: general.userId!, key: 'tournament', value: 50 },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
await db.rankData.createMany({
|
await db.rankData.createMany({
|
||||||
data: [
|
data: [
|
||||||
@@ -202,7 +211,7 @@ integration('general turn lifecycle persistence', () => {
|
|||||||
await db.inheritancePoint.findUnique({
|
await db.inheritancePoint.findUnique({
|
||||||
where: { userId_key: { userId: general.userId!, key: 'previous' } },
|
where: { userId_key: { userId: general.userId!, key: 'previous' } },
|
||||||
})
|
})
|
||||||
).toMatchObject({ value: 3_147 });
|
).toMatchObject({ value: 3_622 });
|
||||||
expect(
|
expect(
|
||||||
(
|
(
|
||||||
await db.inheritanceLog.findMany({
|
await db.inheritanceLog.findMany({
|
||||||
@@ -211,16 +220,33 @@ integration('general turn lifecycle persistence', () => {
|
|||||||
select: { text: true },
|
select: { text: true },
|
||||||
})
|
})
|
||||||
).map(({ text }) => text)
|
).map(({ text }) => text)
|
||||||
).toEqual(['사망으로 랜덤 유니크 구입 3000 포인트 반환', '사망 정산: 3,147 포인트']);
|
).toEqual(['사망으로 랜덤 유니크 구입 3000 포인트 반환', '사망 정산: 3,622 포인트']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resets access/ranks and records pre-rebirth hall and inheritance values', async () => {
|
it('resets access/ranks and records pre-rebirth hall and inheritance values', async () => {
|
||||||
const general = makeGeneral(generalIds[1]!, userIds[1]!);
|
const general = makeGeneral(generalIds[1]!, userIds[1]!, {
|
||||||
|
meta: {
|
||||||
|
killturn: 0,
|
||||||
|
inherit_lived_month: 10,
|
||||||
|
inherit_active_action: 2,
|
||||||
|
max_domestic_critical: 20,
|
||||||
|
max_belong: 7,
|
||||||
|
dex1: 1_000,
|
||||||
|
betwin: 2,
|
||||||
|
betgold: 2_000,
|
||||||
|
betwingold: 1_000,
|
||||||
|
},
|
||||||
|
});
|
||||||
await db.generalAccessLog.create({
|
await db.generalAccessLog.create({
|
||||||
data: { generalId: general.id, userId: general.userId, refreshScore: 77 },
|
data: { generalId: general.id, userId: general.userId, refreshScore: 77 },
|
||||||
});
|
});
|
||||||
await db.inheritancePoint.create({
|
await db.inheritancePoint.createMany({
|
||||||
data: { userId: general.userId!, key: 'previous', value: 50 },
|
data: [
|
||||||
|
{ userId: general.userId!, key: 'previous', value: 50 },
|
||||||
|
{ userId: general.userId!, key: 'max_domestic_critical', value: 80 },
|
||||||
|
{ userId: general.userId!, key: 'unifier', value: 250 },
|
||||||
|
{ userId: general.userId!, key: 'tournament', value: 50 },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
await db.rankData.create({
|
await db.rankData.create({
|
||||||
data: { generalId: general.id, nationId: 0, type: 'warnum', value: 10 },
|
data: { generalId: general.id, nationId: 0, type: 'warnum', value: 10 },
|
||||||
@@ -256,7 +282,19 @@ integration('general turn lifecycle persistence', () => {
|
|||||||
await db.inheritancePoint.findUnique({
|
await db.inheritancePoint.findUnique({
|
||||||
where: { userId_key: { userId: general.userId!, key: 'previous' } },
|
where: { userId_key: { userId: general.userId!, key: 'previous' } },
|
||||||
})
|
})
|
||||||
).toMatchObject({ value: 116 });
|
).toMatchObject({ value: 171 });
|
||||||
|
expect(
|
||||||
|
await db.inheritancePoint.findMany({
|
||||||
|
where: { userId: general.userId! },
|
||||||
|
orderBy: { key: 'asc' },
|
||||||
|
select: { key: true, value: true },
|
||||||
|
})
|
||||||
|
).toEqual([
|
||||||
|
{ key: 'max_belong', value: 70 },
|
||||||
|
{ key: 'max_domestic_critical', value: 80 },
|
||||||
|
{ key: 'previous', value: 171 },
|
||||||
|
{ key: 'unifier', value: 250 },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not settle a possessed NPC before the legacy minimum possession period', async () => {
|
it('does not settle a possessed NPC before the legacy minimum possession period', async () => {
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { DEFAULT_TURN_COMMAND_PROFILE, type ScenarioConfig } from '@sammo-ts/logic';
|
||||||
|
|
||||||
|
import { buildCommandEnv, buildReservedTurnDefinitions } from '../src/turn/reservedTurnCommands.js';
|
||||||
|
|
||||||
|
const scenarioConfig: ScenarioConfig = {
|
||||||
|
stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMin: 10, npcMax: 70, chiefMin: 70 },
|
||||||
|
iconPath: '',
|
||||||
|
map: {},
|
||||||
|
const: {},
|
||||||
|
environment: { mapName: 'inheritance-active-action', unitSet: 'default' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const generalCommands = [
|
||||||
|
'che_거병',
|
||||||
|
'che_건국',
|
||||||
|
'che_등용수락',
|
||||||
|
'che_랜덤임관',
|
||||||
|
'che_모반시도',
|
||||||
|
'che_무작위건국',
|
||||||
|
'che_방랑',
|
||||||
|
'che_선양',
|
||||||
|
'che_인재탐색',
|
||||||
|
'che_임관',
|
||||||
|
'che_장수대상임관',
|
||||||
|
'che_첩보',
|
||||||
|
'che_출병',
|
||||||
|
'che_하야',
|
||||||
|
'cr_건국',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const nationCommands = [
|
||||||
|
'che_감축',
|
||||||
|
'che_국기변경',
|
||||||
|
'che_국호변경',
|
||||||
|
'che_무작위수도이전',
|
||||||
|
'che_증축',
|
||||||
|
'che_천도',
|
||||||
|
'che_초토화',
|
||||||
|
'event_극병연구',
|
||||||
|
'event_대검병연구',
|
||||||
|
'event_무희연구',
|
||||||
|
'event_산저병연구',
|
||||||
|
'event_상병연구',
|
||||||
|
'event_원융노병연구',
|
||||||
|
'event_음귀병연구',
|
||||||
|
'event_화륜차연구',
|
||||||
|
'event_화시병연구',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
describe('Ref active-action inheritance inventory', () => {
|
||||||
|
it('marks every Ref command call site, including trend-producing strategic and research actions', async () => {
|
||||||
|
const { general, nation } = await buildReservedTurnDefinitions({
|
||||||
|
env: buildCommandEnv(scenarioConfig),
|
||||||
|
commandProfile: DEFAULT_TURN_COMMAND_PROFILE,
|
||||||
|
defaultActionKey: '휴식',
|
||||||
|
});
|
||||||
|
|
||||||
|
const generalWithFixedOrContextAmount = [...general.entries()]
|
||||||
|
.filter(([, definition]) => typeof definition.getInheritanceActiveActionAmount === 'function')
|
||||||
|
.map(([key]) => key)
|
||||||
|
.sort();
|
||||||
|
expect(generalWithFixedOrContextAmount).toEqual(generalCommands.filter((key) => key !== 'che_인재탐색').sort());
|
||||||
|
// 인재탐색은 발견확률을 실제 resolve 안에서 계산해 sqrt(1/p)를
|
||||||
|
// 기록한다. 별도 차등 fixture가 이 가중 경로를 검증한다.
|
||||||
|
expect(general.get('che_인재탐색')).toBeDefined();
|
||||||
|
|
||||||
|
const nationWithPoint = [...nation.entries()]
|
||||||
|
.filter(([, definition]) => definition.countsAsInheritanceActiveAction)
|
||||||
|
.map(([key]) => key)
|
||||||
|
.sort();
|
||||||
|
expect(nationWithPoint).toEqual([...nationCommands].sort());
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -216,6 +216,7 @@ describe('UpdateNationLevel monthly action', () => {
|
|||||||
expect(world.peekDirtyState().inheritancePointAdjustments).toEqual([
|
expect(world.peekDirtyState().inheritancePointAdjustments).toEqual([
|
||||||
{ userId: 'user-1', key: 'unifier', amount: 500 },
|
{ userId: 'user-1', key: 'unifier', amount: 500 },
|
||||||
]);
|
]);
|
||||||
|
expect(world.getGeneralById(1)?.inheritancePoints?.unifier).toBe(500);
|
||||||
expect(world.peekDirtyState().logs).toEqual(
|
expect(world.peekDirtyState().logs).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
{
|
{
|
||||||
@@ -292,6 +293,7 @@ describe('UpdateNationLevel monthly action', () => {
|
|||||||
expect(world.peekDirtyState().inheritancePointAdjustments).toEqual([
|
expect(world.peekDirtyState().inheritancePointAdjustments).toEqual([
|
||||||
{ userId: 'user-1', key: 'unifier', amount: 250 },
|
{ userId: 'user-1', key: 'unifier', amount: 250 },
|
||||||
]);
|
]);
|
||||||
|
expect(world.getGeneralById(1)?.inheritancePoints?.unifier).toBe(250);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not duplicate a unique item reserved by an unfinished auction', async () => {
|
it('does not duplicate a unique item reserved by an unfinished auction', async () => {
|
||||||
@@ -306,6 +308,7 @@ describe('UpdateNationLevel monthly action', () => {
|
|||||||
expect(world.peekDirtyState().inheritancePointAdjustments).toEqual([
|
expect(world.peekDirtyState().inheritancePointAdjustments).toEqual([
|
||||||
{ userId: 'user-1', key: 'unifier', amount: 250 },
|
{ userId: 'user-1', key: 'unifier', amount: 250 },
|
||||||
]);
|
]);
|
||||||
|
expect(world.getGeneralById(1)?.inheritancePoints?.unifier).toBe(250);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -495,9 +495,7 @@ describe('Reserved Turn Execution Integration', () => {
|
|||||||
category: 'ACTION',
|
category: 'ACTION',
|
||||||
generalId: 1,
|
generalId: 1,
|
||||||
});
|
});
|
||||||
const personalActionLogs = dirty.logs.filter(
|
const personalActionLogs = dirty.logs.filter((log) => log.scope === 'GENERAL' && log.category === 'ACTION');
|
||||||
(log) => log.scope === 'GENERAL' && log.category === 'ACTION'
|
|
||||||
);
|
|
||||||
expect(personalActionLogs.length).toBeGreaterThan(0);
|
expect(personalActionLogs.length).toBeGreaterThan(0);
|
||||||
expect(personalActionLogs.every((log) => log.generalId === 1)).toBe(true);
|
expect(personalActionLogs.every((log) => log.generalId === 1)).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
@@ -679,6 +677,7 @@ describe('Reserved Turn Execution Integration', () => {
|
|||||||
const generals: TurnGeneral[] = [
|
const generals: TurnGeneral[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
|
userId: 'founder-user',
|
||||||
name: 'General_Leader',
|
name: 'General_Leader',
|
||||||
nationId: 0,
|
nationId: 0,
|
||||||
cityId: 1,
|
cityId: 1,
|
||||||
@@ -708,6 +707,7 @@ describe('Reserved Turn Execution Integration', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
|
userId: 'domestic-user',
|
||||||
name: 'General_Sub',
|
name: 'General_Sub',
|
||||||
nationId: 0,
|
nationId: 0,
|
||||||
cityId: 1,
|
cityId: 1,
|
||||||
@@ -973,6 +973,14 @@ describe('Reserved Turn Execution Integration', () => {
|
|||||||
expect(world.getCityById(1)!.agriculture).toBe(100);
|
expect(world.getCityById(1)!.agriculture).toBe(100);
|
||||||
expect(world.getCityById(1)!.nationId).toBe(0); // City 1 is still unowned
|
expect(world.getCityById(1)!.nationId).toBe(0); // City 1 is still unowned
|
||||||
expect(world.getCityById(2)!.agriculture).toBeGreaterThan(100); // City 2 agric increased
|
expect(world.getCityById(2)!.agriculture).toBeGreaterThan(100); // City 2 agric increased
|
||||||
|
expect(gen1ReallyFinal.inheritancePoints?.unifier).toBe(250);
|
||||||
|
expect(world.peekDirtyState().inheritancePointAdjustments).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{ userId: 'founder-user', key: 'active_action', amount: 3 },
|
||||||
|
{ userId: 'founder-user', key: 'lived_month', amount: 1 },
|
||||||
|
{ userId: 'founder-user', key: 'unifier', amount: 250 },
|
||||||
|
])
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail founding with specific constraints', async () => {
|
it('should fail founding with specific constraints', async () => {
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ export class ActionResolver<
|
|||||||
|
|
||||||
// Penalty
|
// Penalty
|
||||||
const betrayal = typeof general.meta.betray === 'number' ? general.meta.betray : 0;
|
const betrayal = typeof general.meta.betray === 'number' ? general.meta.betray : 0;
|
||||||
|
const belong = typeof general.meta.belong === 'number' ? general.meta.belong : 0;
|
||||||
|
const maxBelong = typeof general.meta.max_belong === 'number' ? general.meta.max_belong : 0;
|
||||||
const penaltyRatio = betrayal * 0.1;
|
const penaltyRatio = betrayal * 0.1;
|
||||||
const nextExp = Math.round(general.experience * (1 - penaltyRatio));
|
const nextExp = Math.round(general.experience * (1 - penaltyRatio));
|
||||||
const nextDed = Math.round(general.dedication * (1 - penaltyRatio));
|
const nextDed = Math.round(general.dedication * (1 - penaltyRatio));
|
||||||
@@ -99,6 +101,7 @@ export class ActionResolver<
|
|||||||
...general.meta,
|
...general.meta,
|
||||||
betray: Math.min(9, betrayal + 1),
|
betray: Math.min(9, betrayal + 1),
|
||||||
belong: 0,
|
belong: 0,
|
||||||
|
...(general.npcState < 2 ? { max_belong: Math.max(belong, maxBelong) } : {}),
|
||||||
makelimit: 12,
|
makelimit: 12,
|
||||||
officer_city: 0,
|
officer_city: 0,
|
||||||
permission: 'normal',
|
permission: 'normal',
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
export const LEGACY_DEX_INHERITANCE_LIMIT = 1_275_975;
|
||||||
|
|
||||||
|
export const ALL_MERGED_INHERITANCE_KEYS = [
|
||||||
|
'lived_month',
|
||||||
|
'max_domestic_critical',
|
||||||
|
'active_action',
|
||||||
|
'unifier',
|
||||||
|
'tournament',
|
||||||
|
'max_belong',
|
||||||
|
'combat',
|
||||||
|
'sabotage',
|
||||||
|
'dex',
|
||||||
|
'betting',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type MergedInheritanceKey = (typeof ALL_MERGED_INHERITANCE_KEYS)[number];
|
||||||
|
|
||||||
|
export interface InheritancePointGeneral {
|
||||||
|
meta: Record<string, unknown>;
|
||||||
|
inheritancePoints?: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ref InheritancePointType::rebirthStoreCoeff. A null coefficient means that
|
||||||
|
* the point is not paid on rebirth and remains reserved for the final death or
|
||||||
|
* unification settlement.
|
||||||
|
*/
|
||||||
|
export const REBIRTH_INHERITANCE_COEFFICIENTS: Readonly<Record<MergedInheritanceKey, number | null>> = {
|
||||||
|
lived_month: 1,
|
||||||
|
max_domestic_critical: null,
|
||||||
|
active_action: 1,
|
||||||
|
unifier: null,
|
||||||
|
tournament: 1,
|
||||||
|
max_belong: null,
|
||||||
|
combat: 1,
|
||||||
|
sabotage: 1,
|
||||||
|
dex: 0.5,
|
||||||
|
betting: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const asRecord = (value: unknown): Record<string, unknown> =>
|
||||||
|
typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
||||||
|
|
||||||
|
const readNumber = (source: Record<string, unknown>, ...keys: string[]): number => {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = source[key];
|
||||||
|
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (Number.isFinite(parsed)) return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const readStoredPoint = (
|
||||||
|
general: InheritancePointGeneral,
|
||||||
|
key: MergedInheritanceKey,
|
||||||
|
storedOverride?: number
|
||||||
|
): number => storedOverride ?? general.inheritancePoints?.[key] ?? 0;
|
||||||
|
|
||||||
|
const readRecordableDexterity = (general: InheritancePointGeneral, key: string): number => {
|
||||||
|
const value = readNumber(general.meta, key);
|
||||||
|
const allStar = asRecord(general.meta.event100_allstar);
|
||||||
|
const granted = readNumber(asRecord(allStar.granted), key);
|
||||||
|
return Math.max(0, value - Math.min(Math.max(0, value), Math.max(0, granted)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const computeDexInheritancePoint = (general: InheritancePointGeneral): number => {
|
||||||
|
let totalDexterity = 0;
|
||||||
|
for (let index = 1; index <= 5; index += 1) {
|
||||||
|
let dexterity = readRecordableDexterity(general, `dex${index}`);
|
||||||
|
if (dexterity > LEGACY_DEX_INHERITANCE_LIMIT) {
|
||||||
|
totalDexterity += (dexterity - LEGACY_DEX_INHERITANCE_LIMIT) / 3;
|
||||||
|
dexterity = LEGACY_DEX_INHERITANCE_LIMIT;
|
||||||
|
}
|
||||||
|
totalDexterity += dexterity;
|
||||||
|
}
|
||||||
|
return totalDexterity * 0.001;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const computeBettingInheritancePoint = (general: InheritancePointGeneral): number => {
|
||||||
|
const wins = readNumber(general.meta, 'betwin', 'rank_betwin');
|
||||||
|
const gold = readNumber(general.meta, 'betgold', 'rank_betgold');
|
||||||
|
const wonGold = readNumber(general.meta, 'betwingold', 'rank_betwingold');
|
||||||
|
const winRate = wonGold / Math.max(1000, gold);
|
||||||
|
return wins * 10 * winRate ** 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const computeActiveInheritancePoint = (
|
||||||
|
general: InheritancePointGeneral,
|
||||||
|
key: MergedInheritanceKey,
|
||||||
|
storedOverride?: number
|
||||||
|
): number => {
|
||||||
|
const stored = readStoredPoint(general, key, storedOverride);
|
||||||
|
switch (key) {
|
||||||
|
case 'lived_month': {
|
||||||
|
const value = readNumber(general.meta, 'inherit_lived_month');
|
||||||
|
return value !== 0 ? value : stored;
|
||||||
|
}
|
||||||
|
case 'max_domestic_critical':
|
||||||
|
// Ref keeps the current streak in general.aux and the lifetime max
|
||||||
|
// in inheritance storage. Math.max also upgrades pre-fix live
|
||||||
|
// snapshots whose current streak has not yet been copied there.
|
||||||
|
return Math.max(
|
||||||
|
stored,
|
||||||
|
readNumber(general.meta, 'max_domestic_critical'),
|
||||||
|
readNumber(general.meta, 'inherit_max_domestic_critical')
|
||||||
|
);
|
||||||
|
case 'active_action': {
|
||||||
|
const value = readNumber(general.meta, 'inherit_active_action');
|
||||||
|
return value !== 0 ? value * 3 : stored;
|
||||||
|
}
|
||||||
|
case 'unifier':
|
||||||
|
case 'tournament':
|
||||||
|
return stored;
|
||||||
|
case 'max_belong':
|
||||||
|
return (
|
||||||
|
Math.max(
|
||||||
|
readNumber(general.meta, 'belong'),
|
||||||
|
readNumber(general.meta, 'max_belong'),
|
||||||
|
readNumber(general.meta, 'inherit_max_belong')
|
||||||
|
) * 10
|
||||||
|
);
|
||||||
|
case 'combat':
|
||||||
|
return readNumber(general.meta, 'rank_warnum', 'warnum') * 5;
|
||||||
|
case 'sabotage':
|
||||||
|
return readNumber(general.meta, 'firenum', 'rank_firenum') * 20;
|
||||||
|
case 'dex':
|
||||||
|
return computeDexInheritancePoint(general);
|
||||||
|
case 'betting':
|
||||||
|
return computeBettingInheritancePoint(general);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface InheritanceSettlementBreakdown {
|
||||||
|
earned: Record<MergedInheritanceKey, number>;
|
||||||
|
retained: Partial<Record<MergedInheritanceKey, number>>;
|
||||||
|
totalEarned: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const computeInheritanceSettlementBreakdown = (
|
||||||
|
general: InheritancePointGeneral,
|
||||||
|
isRebirth: boolean
|
||||||
|
): InheritanceSettlementBreakdown => {
|
||||||
|
const earned = {} as Record<MergedInheritanceKey, number>;
|
||||||
|
const retained: Partial<Record<MergedInheritanceKey, number>> = {};
|
||||||
|
let totalEarned = 0;
|
||||||
|
|
||||||
|
for (const key of ALL_MERGED_INHERITANCE_KEYS) {
|
||||||
|
const value = computeActiveInheritancePoint(general, key);
|
||||||
|
const rebirthCoefficient = REBIRTH_INHERITANCE_COEFFICIENTS[key];
|
||||||
|
if (isRebirth && rebirthCoefficient === null) {
|
||||||
|
earned[key] = 0;
|
||||||
|
retained[key] = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const settledValue = value * (isRebirth ? (rebirthCoefficient ?? 0) : 1);
|
||||||
|
earned[key] = settledValue;
|
||||||
|
totalEarned += settledValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { earned, retained, totalEarned };
|
||||||
|
};
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ALL_MERGED_INHERITANCE_KEYS,
|
||||||
|
computeActiveInheritancePoint,
|
||||||
|
computeInheritanceSettlementBreakdown,
|
||||||
|
} from '../src/inheritance/pointCalculation.js';
|
||||||
|
|
||||||
|
describe('Ref inheritance point calculation', () => {
|
||||||
|
const general = {
|
||||||
|
meta: {
|
||||||
|
inherit_lived_month: 12,
|
||||||
|
max_domestic_critical: 20,
|
||||||
|
inherit_active_action: 0.5,
|
||||||
|
belong: 7,
|
||||||
|
max_belong: 9,
|
||||||
|
rank_warnum: 3,
|
||||||
|
firenum: 2,
|
||||||
|
dex1: 1_275_978,
|
||||||
|
dex2: 100,
|
||||||
|
event100_allstar: { granted: { dex2: 40 } },
|
||||||
|
betwin: 2,
|
||||||
|
betgold: 2_000,
|
||||||
|
betwingold: 1_000,
|
||||||
|
},
|
||||||
|
inheritancePoints: {
|
||||||
|
max_domestic_critical: 80,
|
||||||
|
unifier: 250,
|
||||||
|
tournament: 50,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('keeps all ten Ref sources distinct', () => {
|
||||||
|
expect(ALL_MERGED_INHERITANCE_KEYS).toEqual([
|
||||||
|
'lived_month',
|
||||||
|
'max_domestic_critical',
|
||||||
|
'active_action',
|
||||||
|
'unifier',
|
||||||
|
'tournament',
|
||||||
|
'max_belong',
|
||||||
|
'combat',
|
||||||
|
'sabotage',
|
||||||
|
'dex',
|
||||||
|
'betting',
|
||||||
|
]);
|
||||||
|
expect(
|
||||||
|
Object.fromEntries(
|
||||||
|
ALL_MERGED_INHERITANCE_KEYS.map((key) => [key, computeActiveInheritancePoint(general, key)])
|
||||||
|
)
|
||||||
|
).toEqual({
|
||||||
|
lived_month: 12,
|
||||||
|
max_domestic_critical: 80,
|
||||||
|
active_action: 1.5,
|
||||||
|
unifier: 250,
|
||||||
|
tournament: 50,
|
||||||
|
max_belong: 90,
|
||||||
|
combat: 15,
|
||||||
|
sabotage: 40,
|
||||||
|
dex: 1_276.036,
|
||||||
|
betting: 5,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pays only Ref rebirth-enabled sources and retains the three delayed sources', () => {
|
||||||
|
const settlement = computeInheritanceSettlementBreakdown(general, true);
|
||||||
|
|
||||||
|
expect(settlement.earned).toEqual({
|
||||||
|
lived_month: 12,
|
||||||
|
max_domestic_critical: 0,
|
||||||
|
active_action: 1.5,
|
||||||
|
unifier: 0,
|
||||||
|
tournament: 50,
|
||||||
|
max_belong: 0,
|
||||||
|
combat: 15,
|
||||||
|
sabotage: 40,
|
||||||
|
dex: 638.018,
|
||||||
|
betting: 5,
|
||||||
|
});
|
||||||
|
expect(settlement.retained).toEqual({
|
||||||
|
max_domestic_critical: 80,
|
||||||
|
unifier: 250,
|
||||||
|
max_belong: 90,
|
||||||
|
});
|
||||||
|
expect(settlement.totalEarned).toBeCloseTo(761.518, 8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the current domestic streak only as a live upgrade candidate for the stored maximum', () => {
|
||||||
|
expect(
|
||||||
|
computeActiveInheritancePoint(
|
||||||
|
{
|
||||||
|
meta: { max_domestic_critical: 120 },
|
||||||
|
inheritancePoints: { max_domestic_critical: 80 },
|
||||||
|
},
|
||||||
|
'max_domestic_critical'
|
||||||
|
)
|
||||||
|
).toBe(120);
|
||||||
|
expect(
|
||||||
|
computeActiveInheritancePoint(
|
||||||
|
{
|
||||||
|
meta: { max_domestic_critical: 0 },
|
||||||
|
inheritancePoints: { max_domestic_critical: 80 },
|
||||||
|
},
|
||||||
|
'max_domestic_critical'
|
||||||
|
)
|
||||||
|
).toBe(80);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -251,7 +251,7 @@ describe('General Commands New Scenario', () => {
|
|||||||
items: { horse: null, weapon: null, book: null, item: null },
|
items: { horse: null, weapon: null, book: null, item: null },
|
||||||
},
|
},
|
||||||
triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
|
triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
|
||||||
meta: { killturn: 24 },
|
meta: { killturn: 24, belong: 18, max_belong: 12 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const snapshot: WorldSnapshot = {
|
const snapshot: WorldSnapshot = {
|
||||||
@@ -390,6 +390,7 @@ describe('General Commands New Scenario', () => {
|
|||||||
|
|
||||||
const g1_after_resign = world.getGeneral(1)!;
|
const g1_after_resign = world.getGeneral(1)!;
|
||||||
expect(g1_after_resign.nationId).toBe(0);
|
expect(g1_after_resign.nationId).toBe(0);
|
||||||
|
expect(g1_after_resign.meta.max_belong).toBe(18);
|
||||||
|
|
||||||
// 6. Retire (Needs age >= 60)
|
// 6. Retire (Needs age >= 60)
|
||||||
// Manually set age
|
// Manually set age
|
||||||
|
|||||||
Reference in New Issue
Block a user