fix: 은퇴 명예 기록과 유산 수명주기 정합성을 복원한다

은퇴·사망·통일의 저장 순서와 명예의 전당 및 명장일람 판정을 Ref 흐름에 맞춘다.

유산 행동을 인증된 daemon transaction으로 통합하고 중복 지급·고유 아이템·로그·오류 경계를 회귀 테스트한다.
This commit is contained in:
2026-08-24 03:38:12 +00:00
parent 95cf68dffd
commit 4fc20de8d4
33 changed files with 4160 additions and 1166 deletions
+18 -7
View File
@@ -44,6 +44,12 @@ export type UniqueLotteryInput = {
inheritRandomUnique?: boolean;
};
export type UniqueLotteryOutcome =
| { status: 'NO_SLOT' }
| { status: 'ROLL_FAILED' }
| { status: 'NO_SUPPLY' }
| { status: 'ACQUIRED'; itemKey: string };
const DEFAULT_MAX_UNIQUE_ITEM_LIMIT: Array<[number, number]> = [
[-1, 1],
[3, 2],
@@ -183,7 +189,7 @@ export const buildGenericUniqueSeed = (
export const buildVoteUniqueSeed = (hiddenSeed: string | number, voteId: number, generalId: number): string =>
serializeSeed(hiddenSeed, 'voteUnique', voteId, generalId);
export const rollUniqueLottery = (input: UniqueLotteryInput): string | null => {
export const rollUniqueLotteryDetailed = (input: UniqueLotteryInput): UniqueLotteryOutcome => {
const {
rng,
config,
@@ -203,13 +209,13 @@ export const rollUniqueLottery = (input: UniqueLotteryInput): string | null => {
const resolvedAcquireType = acquireType ?? '아이템';
if (userCount <= 0) {
return null;
return { status: 'ROLL_FAILED' };
}
const itemTypes = Object.keys(config.allItems);
const itemTypeCnt = itemTypes.length;
if (itemTypeCnt <= 0) {
return null;
return { status: 'NO_SLOT' };
}
const relYear = currentYear - startYear;
@@ -246,7 +252,7 @@ export const rollUniqueLottery = (input: UniqueLotteryInput): string | null => {
}
if (trialCnt <= 0 || maxCnt <= 0) {
return null;
return { status: 'NO_SLOT' };
}
const relMonthByInit = joinYearMonth(currentYear, currentMonth) - joinYearMonth(initYear, initMonth);
@@ -289,7 +295,7 @@ export const rollUniqueLottery = (input: UniqueLotteryInput): string | null => {
}
if (!success) {
return null;
return { status: 'ROLL_FAILED' };
}
const availableUnique: Array<[string, number]> = [];
@@ -315,10 +321,15 @@ export const rollUniqueLottery = (input: UniqueLotteryInput): string | null => {
}
if (availableUnique.length === 0) {
return null;
return { status: 'NO_SUPPLY' };
}
return rng.choiceUsingWeightPair(availableUnique);
return { status: 'ACQUIRED', itemKey: rng.choiceUsingWeightPair(availableUnique) };
};
export const rollUniqueLottery = (input: UniqueLotteryInput): string | null => {
const outcome = rollUniqueLotteryDetailed(input);
return outcome.status === 'ACQUIRED' ? outcome.itemKey : null;
};
const applyUniqueItemGain = <TriggerState extends GeneralTriggerState = GeneralTriggerState>(