fix: Ref 유산 포인트 적립 조건을 전면 정합화한다

능동 행동 31개 호출 지점과 소유자·NPC·통일 경계를 고정하고 사용자 저장값을 함께 갱신한다.\n\n최대 내정·임관, 천통 기여, 토너먼트, 숙련·베팅·랭크 계산과 환생 정산을 공통 계산기로 통합한다.
This commit is contained in:
2026-08-23 12:06:59 +00:00
parent 2f793a9518
commit 18e0bed30a
19 changed files with 712 additions and 245 deletions
@@ -215,16 +215,16 @@ integration('game cancellation transaction', () => {
[userId]: {
openingPoint: 10_000,
currentPoint: 7_000,
earnedPoint: 1_750.005,
retainedEarnedPoint: 700,
finalPoint: 10_700,
earnedPoint: 1_790.005,
retainedEarnedPoint: 716,
finalPoint: 10_716,
baselineSource: 'OPENING',
},
},
});
await expect(
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({
status: 'ABANDONED',
winnerNation: null,
@@ -391,7 +391,9 @@ describe('legacy general-turn execution contract', () => {
expect(updated.injury).toBe(10);
expect(updated.experience).toBe(0);
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(harness.reservedTurnStore.getGeneralTurn(1, 0).action).toBe('휴식');
expect(harness.getCollectedLogs().some((log) => log.text.includes('악성유저'))).toBe(true);
@@ -96,19 +96,28 @@ integration('general turn lifecycle persistence', () => {
killturn: 0,
inherit_lived_month: 10,
inherit_active_action: 2,
max_belong: 9,
inheritRandomUnique: true,
dex1: 1_000,
dex2: 1,
dex3: 1,
dex4: 1,
dex5: 1,
betwin: 2,
betgold: 2_000,
betwingold: 1_000,
},
});
await db.generalAccessLog.create({
data: { generalId: general.id, userId: general.userId, refreshScore: 99 },
});
await db.inheritancePoint.create({
data: { userId: general.userId!, key: 'previous', value: 100 },
await db.inheritancePoint.createMany({
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({
data: [
@@ -202,7 +211,7 @@ integration('general turn lifecycle persistence', () => {
await db.inheritancePoint.findUnique({
where: { userId_key: { userId: general.userId!, key: 'previous' } },
})
).toMatchObject({ value: 3_147 });
).toMatchObject({ value: 3_622 });
expect(
(
await db.inheritanceLog.findMany({
@@ -211,16 +220,33 @@ integration('general turn lifecycle persistence', () => {
select: { text: true },
})
).map(({ text }) => text)
).toEqual(['사망으로 랜덤 유니크 구입 3000 포인트 반환', '사망 정산: 3,147 포인트']);
).toEqual(['사망으로 랜덤 유니크 구입 3000 포인트 반환', '사망 정산: 3,622 포인트']);
});
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({
data: { generalId: general.id, userId: general.userId, refreshScore: 77 },
});
await db.inheritancePoint.create({
data: { userId: general.userId!, key: 'previous', value: 50 },
await db.inheritancePoint.createMany({
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({
data: { generalId: general.id, nationId: 0, type: 'warnum', value: 10 },
@@ -256,7 +282,19 @@ integration('general turn lifecycle persistence', () => {
await db.inheritancePoint.findUnique({
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 () => {
@@ -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([
{ userId: 'user-1', key: 'unifier', amount: 500 },
]);
expect(world.getGeneralById(1)?.inheritancePoints?.unifier).toBe(500);
expect(world.peekDirtyState().logs).toEqual(
expect.arrayContaining([
{
@@ -292,6 +293,7 @@ describe('UpdateNationLevel monthly action', () => {
expect(world.peekDirtyState().inheritancePointAdjustments).toEqual([
{ 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 () => {
@@ -306,6 +308,7 @@ describe('UpdateNationLevel monthly action', () => {
expect(world.peekDirtyState().inheritancePointAdjustments).toEqual([
{ 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',
generalId: 1,
});
const personalActionLogs = dirty.logs.filter(
(log) => log.scope === 'GENERAL' && log.category === 'ACTION'
);
const personalActionLogs = dirty.logs.filter((log) => log.scope === 'GENERAL' && log.category === 'ACTION');
expect(personalActionLogs.length).toBeGreaterThan(0);
expect(personalActionLogs.every((log) => log.generalId === 1)).toBe(true);
expect(
@@ -679,6 +677,7 @@ describe('Reserved Turn Execution Integration', () => {
const generals: TurnGeneral[] = [
{
id: 1,
userId: 'founder-user',
name: 'General_Leader',
nationId: 0,
cityId: 1,
@@ -708,6 +707,7 @@ describe('Reserved Turn Execution Integration', () => {
},
{
id: 2,
userId: 'domestic-user',
name: 'General_Sub',
nationId: 0,
cityId: 1,
@@ -973,6 +973,14 @@ describe('Reserved Turn Execution Integration', () => {
expect(world.getCityById(1)!.agriculture).toBe(100);
expect(world.getCityById(1)!.nationId).toBe(0); // City 1 is still unowned
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 () => {