231 lines
6.9 KiB
TypeScript
231 lines
6.9 KiB
TypeScript
jest.mock(
|
||
'@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache',
|
||
() => ({
|
||
__esModule: true,
|
||
default: {},
|
||
}),
|
||
);
|
||
|
||
import {
|
||
calculateCnEventEstimateStartAt,
|
||
getBangDreamOccupiedDays,
|
||
type BangDreamEventSchedule,
|
||
} from '@/modules/qqbot/plugins/bangdream/src/domain/policy/cn-event-estimate.policy';
|
||
import {
|
||
getCutoffDateByServerTimezone,
|
||
getCutoffDayIndex,
|
||
getCutoffEventStatus,
|
||
getCutoffPredictionWindow,
|
||
getCutoffTierList,
|
||
isCutoffDailyCheckpoint,
|
||
isCutoffTierSupported,
|
||
selectRecentCutoffEventIds,
|
||
} from '@/modules/qqbot/plugins/bangdream/src/domain/policy/cutoff.policy';
|
||
import {
|
||
getBangDreamServerUtcOffset,
|
||
normalizeBangDreamTimestamp,
|
||
} from '@/modules/qqbot/plugins/bangdream/src/domain/policy/server.policy';
|
||
import {
|
||
applyGachaGuaranteedRarity,
|
||
BANGDREAM_GACHA_MAX_SPIN_COUNT,
|
||
isBirthdayGachaType,
|
||
isFreeGachaType,
|
||
isGachaSpinCountTooLarge,
|
||
isPermanentJapaneseGachaPeriod,
|
||
pickGachaCardIdByWeight,
|
||
pickGachaRarityByRate,
|
||
} from '@/modules/qqbot/plugins/bangdream/src/domain/policy/gacha.policy';
|
||
import {
|
||
BangDreamEventStatus,
|
||
BangDreamGachaType,
|
||
BangDreamServerId as Server,
|
||
} from '@/modules/qqbot/plugins/bangdream/src/domain/common/bangdream-protocol';
|
||
|
||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||
|
||
describe('BangDream server policy', () => {
|
||
it('normalizes timestamps and applies server UTC offsets', () => {
|
||
expect(normalizeBangDreamTimestamp(1_700_000_000)).toBe(1_700_000_000_000);
|
||
expect(normalizeBangDreamTimestamp(1_700_000_000_000)).toBe(
|
||
1_700_000_000_000,
|
||
);
|
||
expect(getBangDreamServerUtcOffset(Server.cn)).toBe(8);
|
||
expect(getBangDreamServerUtcOffset(Server.jp)).toBe(9);
|
||
expect(getBangDreamServerUtcOffset(Server.en)).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('BangDream CN event estimate policy', () => {
|
||
it('keeps occupied day calculation inclusive', () => {
|
||
expect(
|
||
getBangDreamOccupiedDays(Date.UTC(2026, 0, 1), Date.UTC(2026, 0, 1)),
|
||
).toBe(1);
|
||
expect(
|
||
getBangDreamOccupiedDays(Date.UTC(2026, 0, 1), Date.UTC(2026, 0, 3)),
|
||
).toBe(3);
|
||
});
|
||
|
||
it('calculates the next missing CN event start from pure schedules', () => {
|
||
const schedules: Record<number, BangDreamEventSchedule> = {
|
||
100: {
|
||
endAt: [Date.UTC(2026, 0, 5), null, null, Date.UTC(2026, 0, 5), null],
|
||
startAt: [Date.UTC(2026, 0, 1), null, null, Date.UTC(2026, 0, 1), null],
|
||
},
|
||
101: {
|
||
endAt: [Date.UTC(2026, 0, 10), null, null, null, null],
|
||
startAt: [Date.UTC(2026, 0, 6), null, null, null, null],
|
||
},
|
||
};
|
||
|
||
const result = calculateCnEventEstimateStartAt({
|
||
currentEvent: { eventId: 100, ...schedules[100] },
|
||
eventId: 101,
|
||
/**
|
||
* 读取 BangDream回调数据。
|
||
* @param eventId - 活动 ID;定位本次读取、更新、删除或关联的活动。
|
||
*/
|
||
getSchedule: (eventId) => schedules[eventId] || null,
|
||
options: {
|
||
defaultNoBangDays: 1,
|
||
estimateStartEventId: 100,
|
||
},
|
||
presentJpEventId: 101,
|
||
});
|
||
|
||
expect(result).toBe(Date.UTC(2026, 0, 6));
|
||
});
|
||
});
|
||
|
||
describe('BangDream cutoff policy', () => {
|
||
it('checks tier support by server', () => {
|
||
expect(getCutoffTierList(Server.cn)).toContain(1500);
|
||
expect(isCutoffTierSupported(Server.cn, 1500)).toBe(true);
|
||
expect(isCutoffTierSupported(Server.jp, 1500)).toBe(false);
|
||
});
|
||
|
||
it('resolves event status and prediction windows', () => {
|
||
const startAt = Date.UTC(2026, 0, 1);
|
||
const endAt = Date.UTC(2026, 0, 5);
|
||
|
||
expect(getCutoffEventStatus(startAt, endAt, startAt - 1)).toBe(
|
||
BangDreamEventStatus.notStart,
|
||
);
|
||
expect(getCutoffEventStatus(startAt, endAt, startAt + 1)).toBe(
|
||
BangDreamEventStatus.inProgress,
|
||
);
|
||
expect(getCutoffEventStatus(startAt, endAt, endAt + 1)).toBe(
|
||
BangDreamEventStatus.ended,
|
||
);
|
||
expect(getCutoffPredictionWindow(startAt, endAt)).toEqual({
|
||
endTs: Math.floor(endAt / 1000),
|
||
startTs: Math.floor(startAt / 1000),
|
||
});
|
||
});
|
||
|
||
it('calculates daily checkpoints and event day index', () => {
|
||
const checkpoint = getCutoffDateByServerTimezone(
|
||
Date.UTC(2026, 0, 1, 18, 45),
|
||
Server.jp,
|
||
);
|
||
expect(isCutoffDailyCheckpoint(Server.jp, checkpoint)).toBe(true);
|
||
expect(isCutoffDailyCheckpoint(Server.en, checkpoint)).toBe(false);
|
||
|
||
const eventStartAt = Date.UTC(2026, 0, 1, 12);
|
||
expect(getCutoffDayIndex(Server.jp, eventStartAt, eventStartAt + 1)).toBe(
|
||
0,
|
||
);
|
||
expect(
|
||
getCutoffDayIndex(Server.jp, eventStartAt, eventStartAt + DAY_MS),
|
||
).toBe(1);
|
||
});
|
||
|
||
it('selects recent comparable events by type and server start time', () => {
|
||
const candidates = [
|
||
eventCandidate(1, 'story', [Date.UTC(2026, 0, 1)]),
|
||
eventCandidate(2, 'festival', [Date.UTC(2026, 0, 5)]),
|
||
eventCandidate(3, 'story', [Date.UTC(2026, 0, 10)]),
|
||
eventCandidate(4, 'story', [Date.UTC(2026, 0, 15)]),
|
||
];
|
||
|
||
expect(
|
||
selectRecentCutoffEventIds({
|
||
candidates,
|
||
count: 2,
|
||
event: candidates[3],
|
||
sameType: true,
|
||
server: Server.jp,
|
||
}),
|
||
).toEqual([3, 4]);
|
||
});
|
||
});
|
||
|
||
describe('BangDream gacha policy', () => {
|
||
it('classifies gacha types and spin count limits', () => {
|
||
expect(isBirthdayGachaType(BangDreamGachaType.birthday)).toBe(true);
|
||
expect(isBirthdayGachaType(BangDreamGachaType.limited)).toBe(false);
|
||
expect(isFreeGachaType(BangDreamGachaType.free)).toBe(true);
|
||
expect(isPermanentJapaneseGachaPeriod('期限なし')).toBe(true);
|
||
expect(isGachaSpinCountTooLarge(BANGDREAM_GACHA_MAX_SPIN_COUNT)).toBe(
|
||
false,
|
||
);
|
||
expect(isGachaSpinCountTooLarge(BANGDREAM_GACHA_MAX_SPIN_COUNT + 1)).toBe(
|
||
true,
|
||
);
|
||
});
|
||
|
||
it('applies ten-pull guaranteed rarity without changing other pulls', () => {
|
||
expect(applyGachaGuaranteedRarity(8, 2)).toBe(2);
|
||
expect(applyGachaGuaranteedRarity(9, 2)).toBe(3);
|
||
expect(applyGachaGuaranteedRarity(9, 4)).toBe(4);
|
||
});
|
||
|
||
it('picks rarity by rate and cards by rarity weight', () => {
|
||
expect(
|
||
pickGachaRarityByRate(
|
||
{
|
||
2: { rate: 97, weightTotal: 100 },
|
||
3: { rate: 3, weightTotal: 50 },
|
||
},
|
||
() => 0.98,
|
||
),
|
||
).toBe('3');
|
||
|
||
expect(
|
||
pickGachaCardIdByWeight(
|
||
3,
|
||
50,
|
||
{
|
||
100: { rarityIndex: 2, weight: 100 },
|
||
200: { rarityIndex: 3, weight: 10 },
|
||
201: { rarityIndex: 3, weight: 40 },
|
||
},
|
||
() => 0.5,
|
||
),
|
||
).toBe('201');
|
||
});
|
||
});
|
||
|
||
/**
|
||
* 执行 BangDream 插件流程。
|
||
* @param eventId - 活动 ID;定位本次读取、更新、删除或关联的活动。
|
||
* @param eventType - eventType 输入;影响 eventCandidate 的返回值。
|
||
* @param jpStartAt - jpStartAt 输入;影响 eventCandidate 的返回值。
|
||
*/
|
||
function eventCandidate(
|
||
eventId: number,
|
||
eventType: string,
|
||
jpStartAt: number[],
|
||
) {
|
||
return {
|
||
eventId,
|
||
eventType,
|
||
startAt: [
|
||
jpStartAt[0],
|
||
null,
|
||
null,
|
||
jpStartAt[Server.cn] ?? jpStartAt[0],
|
||
null,
|
||
],
|
||
};
|
||
}
|