merge: 설문 기본 유니크 풀을 복원한다
This commit is contained in:
@@ -30,7 +30,6 @@ import {
|
|||||||
resolveTroopSecretPermission,
|
resolveTroopSecretPermission,
|
||||||
resolveMessageTargetIcon,
|
resolveMessageTargetIcon,
|
||||||
type GeneralActionModule,
|
type GeneralActionModule,
|
||||||
resolveUniqueConfig,
|
|
||||||
rollUniqueLottery,
|
rollUniqueLottery,
|
||||||
type ItemModule,
|
type ItemModule,
|
||||||
type LogEntryDraft,
|
type LogEntryDraft,
|
||||||
@@ -39,6 +38,7 @@ import {
|
|||||||
type TriggerValue,
|
type TriggerValue,
|
||||||
type TurnCommandProfile,
|
type TurnCommandProfile,
|
||||||
} from '@sammo-ts/logic';
|
} from '@sammo-ts/logic';
|
||||||
|
import { resolveLegacyCompatibleUniqueConfig } from '@sammo-ts/logic/rewards/legacyUniqueItemPool.js';
|
||||||
import { round as roundLegacyInteger, simpleSerialize } from '@sammo-ts/logic/war/utils.js';
|
import { round as roundLegacyInteger, simpleSerialize } from '@sammo-ts/logic/war/utils.js';
|
||||||
import {
|
import {
|
||||||
cloneItemInventory,
|
cloneItemInventory,
|
||||||
@@ -2909,7 +2909,11 @@ async function handleVoteReward(
|
|||||||
) * 5;
|
) * 5;
|
||||||
|
|
||||||
const itemRegistry = await getItemRegistry();
|
const itemRegistry = await getItemRegistry();
|
||||||
const uniqueConfig = resolveUniqueConfig(configConst);
|
// Ref always has GameConst::$allItems even when a scenario omits an
|
||||||
|
// allItems override. Keep survey rewards on the same compatibility path
|
||||||
|
// as rankings, inheritance, and auctions instead of treating that omission
|
||||||
|
// as an empty lottery pool.
|
||||||
|
const uniqueConfig = await resolveLegacyCompatibleUniqueConfig(configConst);
|
||||||
const generals = world.listGenerals();
|
const generals = world.listGenerals();
|
||||||
const occupiedUniqueCounts = countOccupiedUniqueItems(
|
const occupiedUniqueCounts = countOccupiedUniqueItems(
|
||||||
generals.map((entry) => entry.role.items),
|
generals.map((entry) => entry.role.items),
|
||||||
|
|||||||
@@ -53,6 +53,33 @@ const actorBindingDb = (userId = 'user-1') => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const buildDefaultUniquePoolSnapshot = (general: TurnGeneral): TurnWorldSnapshot => ({
|
||||||
|
generals: [general] as any,
|
||||||
|
cities: [] as any,
|
||||||
|
nations: [] as any,
|
||||||
|
troops: [],
|
||||||
|
diplomacy: [],
|
||||||
|
events: [],
|
||||||
|
initialEvents: [],
|
||||||
|
map: {
|
||||||
|
id: 'test_map',
|
||||||
|
name: 'TestMap',
|
||||||
|
cities: [],
|
||||||
|
defaults: { trust: 50, trade: 100, supplyState: 1, frontState: 0 },
|
||||||
|
} as any,
|
||||||
|
scenarioConfig: {
|
||||||
|
stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 70 },
|
||||||
|
iconPath: '',
|
||||||
|
map: {},
|
||||||
|
// Ordinary Ref scenarios inherit GameConst::$allItems without writing
|
||||||
|
// an allItems override into the scenario JSON.
|
||||||
|
const: { develCost: 100, uniqueTrialCoef: 1 },
|
||||||
|
environment: { mapName: 'test_map', unitSet: 'default' },
|
||||||
|
},
|
||||||
|
scenarioMeta: { startYear: 180 } as any,
|
||||||
|
unitSet: {} as any,
|
||||||
|
});
|
||||||
|
|
||||||
describe('voteReward command', () => {
|
describe('voteReward command', () => {
|
||||||
it('keeps the wall-time fallback open at exact deadline equality', () => {
|
it('keeps the wall-time fallback open at exact deadline equality', () => {
|
||||||
const deadline = new Date('0180-01-01T00:00:00.000Z');
|
const deadline = new Date('0180-01-01T00:00:00.000Z');
|
||||||
@@ -416,6 +443,87 @@ describe('voteReward command', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['PREOPEN', new Date('2026-08-27T10:30:00.000Z'), 1, 'preopen-seed'],
|
||||||
|
['OPEN', new Date('2026-08-27T11:00:05.000Z'), 2, 'open-seed'],
|
||||||
|
] as const)(
|
||||||
|
'awards a survey unique from the Ref default pool at coefficient 1 during %s',
|
||||||
|
async (_phase, wallNow, voteId, hiddenSeed) => {
|
||||||
|
const general = buildGeneral(1);
|
||||||
|
const state: TurnWorldState = {
|
||||||
|
id: 1,
|
||||||
|
currentYear: 180,
|
||||||
|
currentMonth: 1,
|
||||||
|
tickSeconds: 600,
|
||||||
|
lastTurnTime: new Date('0180-01-01T10:00:00.000Z'),
|
||||||
|
clockBaseTime: new Date('0180-01-01T10:00:00.000Z'),
|
||||||
|
clockTick: 36_000_000,
|
||||||
|
clockMode: 'realtime',
|
||||||
|
clockWallAnchor: new Date('2026-08-27T11:00:00.000Z'),
|
||||||
|
meta: {
|
||||||
|
hiddenSeed,
|
||||||
|
scenarioId: 0,
|
||||||
|
initYear: 180,
|
||||||
|
initMonth: 1,
|
||||||
|
scenarioMeta: { startYear: 180 },
|
||||||
|
preopenAt: '2026-08-27 19:30:00',
|
||||||
|
opentime: '2026-08-27 20:00:00',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const world = new InMemoryTurnWorld(state, buildDefaultUniquePoolSnapshot(general), {
|
||||||
|
schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] },
|
||||||
|
});
|
||||||
|
const acceptedGameTick = world.dateToGameTick(world.getGameNow(wallNow));
|
||||||
|
expect(acceptedGameTick).toBe(_phase === 'PREOPEN' ? 36_000_000 : 36_300_000);
|
||||||
|
|
||||||
|
const commandDb = {
|
||||||
|
...actorBindingDb(),
|
||||||
|
auction: { findMany: async () => [] },
|
||||||
|
$queryRaw: async (query: { strings: readonly string[] }) =>
|
||||||
|
query.strings.join(' ').includes('SELECT options')
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
options: ['찬성'],
|
||||||
|
multipleOptions: 1,
|
||||||
|
endAt: null,
|
||||||
|
endTick: null,
|
||||||
|
closedAt: null,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [{ id: voteId }],
|
||||||
|
};
|
||||||
|
const handler = createTurnDaemonCommandHandler({ world });
|
||||||
|
const result = await handler.handle(
|
||||||
|
{
|
||||||
|
type: 'voteReward',
|
||||||
|
requestId: `vote-reward-${_phase.toLowerCase()}`,
|
||||||
|
userId: 'user-1',
|
||||||
|
voteId,
|
||||||
|
generalId: 1,
|
||||||
|
selection: [0],
|
||||||
|
acceptedGameTick,
|
||||||
|
},
|
||||||
|
{ db: commandDb as any }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
type: 'voteReward',
|
||||||
|
ok: true,
|
||||||
|
awardedUnique: true,
|
||||||
|
});
|
||||||
|
if (!result || result.type !== 'voteReward' || !result.ok || !result.itemKey) {
|
||||||
|
throw new Error(`${_phase} survey unique reward missing`);
|
||||||
|
}
|
||||||
|
const updated = world.getGeneralById(1);
|
||||||
|
expect(updated?.gold).toBe(1500);
|
||||||
|
expect(Object.values(updated?.role.items ?? {})).toContain(result.itemKey);
|
||||||
|
expect(updated?.meta.voteRewards).toMatchObject({
|
||||||
|
[voteId]: { awarded: true, itemKey: result.itemKey },
|
||||||
|
});
|
||||||
|
expect(world.consumeDirtyState().logs.some((entry) => entry.text.includes('【설문조사】'))).toBe(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
it('treats an active unique auction as occupied when revalidating the lottery', async () => {
|
it('treats an active unique auction as occupied when revalidating the lottery', async () => {
|
||||||
const general = buildGeneral(1);
|
const general = buildGeneral(1);
|
||||||
const snapshot: TurnWorldSnapshot = {
|
const snapshot: TurnWorldSnapshot = {
|
||||||
|
|||||||
Reference in New Issue
Block a user