refactor: 收口 BangDream 档线策略
This commit is contained in:
parent
25e39ae7ca
commit
47a48fb311
@ -364,6 +364,14 @@ export interface TsuguHook {
|
|||||||
- `models/cutoff.ts` 的请求、预测、展示规则分层。
|
- `models/cutoff.ts` 的请求、预测、展示规则分层。
|
||||||
- policy 均有纯函数测试。
|
- policy 均有纯函数测试。
|
||||||
|
|
||||||
|
当前进度:
|
||||||
|
|
||||||
|
- 已新增 `models/server-policy.ts`,收口服务器 UTC 偏移、服务器时区 Date 转换、时间戳规范化和档线日增 checkpoint 服务器规则;`render-blocks/list-time.ts` 保留兼容入口但不再维护时区 switch。
|
||||||
|
- 已新增 `models/cn-event-estimate-policy.ts`,把国服预估起始活动、跳过活动和无邦日规则从 `list-time.ts` 移出,提供 `calculateCnEventEstimateStartAt` 纯函数测试入口;`list-time.ts` 和 `event.ts` 改走 policy,不再让 Event 排序依赖渲染层预估实现。
|
||||||
|
- 已新增 `models/cutoff-policy.ts`,收口档位列表、档位支持判断、国服缺失活动时间预估、档线活动状态、预测窗口、日增天数和最近同类型活动选择规则;`models/cutoff.ts` 和 `cutoff-all.ts` 已接入。
|
||||||
|
- 已新增 `test/qqbot/plugins/bangDream/tsugu/policy.spec.ts`,覆盖服务器时区、国服预估纯计算、档位判断、状态/预测窗口、日增 checkpoint、活动天数和最近活动选择;policy 单测 mock `main-data-store`,避免纯测试启动主数据定时器。
|
||||||
|
- 已生成 `phase6-policy-event-50.jpg`、`phase6-policy-cutoff-detail-100-50-cn.jpg`、`phase6-policy-cutoff-recent-100-50-cn.jpg`,验证查活动、单档线和历史档线图片输出非空。
|
||||||
|
|
||||||
### Phase 7:Facade 和 hook 接入
|
### Phase 7:Facade 和 hook 接入
|
||||||
|
|
||||||
目标:Nest 边界稳定,Tsugu 内部可观测。
|
目标:Nest 边界稳定,Tsugu 内部可观测。
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
import { Event } from '@/qqbot/plugins/bangDream/tsugu/models/event';
|
import { Event } from '@/qqbot/plugins/bangDream/tsugu/models/event';
|
||||||
import { drawList, line } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-frame';
|
import {
|
||||||
|
drawList,
|
||||||
|
line,
|
||||||
|
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-frame';
|
||||||
import { drawDataBlock } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/data-block';
|
import { drawDataBlock } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/data-block';
|
||||||
import { Image, Canvas } from 'skia-canvas';
|
import { Image, Canvas } from 'skia-canvas';
|
||||||
import { formatTime } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-time';
|
import { formatTime } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-time';
|
||||||
@ -8,13 +11,11 @@ import { drawTitle } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/title';
|
|||||||
import { outputEasyImages } from '@/qqbot/plugins/bangDream/tsugu/canvas/output';
|
import { outputEasyImages } from '@/qqbot/plugins/bangDream/tsugu/canvas/output';
|
||||||
import { Cutoff } from '@/qqbot/plugins/bangDream/tsugu/models/cutoff';
|
import { Cutoff } from '@/qqbot/plugins/bangDream/tsugu/models/cutoff';
|
||||||
import { drawCutoffChart } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/cutoff-chart';
|
import { drawCutoffChart } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/cutoff-chart';
|
||||||
import {
|
import { serverNameFullList } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
||||||
serverNameFullList,
|
|
||||||
tierListOfServer,
|
|
||||||
} from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
|
||||||
import { drawEventDataBlock } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/detail-blocks';
|
import { drawEventDataBlock } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/detail-blocks';
|
||||||
import { statusName } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
import { statusName } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
||||||
import { BangDreamEventStatus } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
|
import { BangDreamEventStatus } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
|
||||||
|
import { getCutoffTierList } from '@/qqbot/plugins/bangDream/tsugu/models/cutoff-policy';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在QQBot 图片视图层中绘制档线全部。
|
* 在QQBot 图片视图层中绘制档线全部。
|
||||||
@ -43,7 +44,7 @@ export async function drawCutoffAll(
|
|||||||
const list: Array<Image | Canvas> = [];
|
const list: Array<Image | Canvas> = [];
|
||||||
|
|
||||||
//初始化档线列表
|
//初始化档线列表
|
||||||
const tierList = tierListOfServer[Server[mainServer]];
|
const tierList = getCutoffTierList(mainServer);
|
||||||
const cutoffList: Array<Cutoff> = [];
|
const cutoffList: Array<Cutoff> = [];
|
||||||
for (const i in tierList) {
|
for (const i in tierList) {
|
||||||
const tempCutoff = new Cutoff(eventId, mainServer, tierList[i]);
|
const tempCutoff = new Cutoff(eventId, mainServer, tierList[i]);
|
||||||
|
|||||||
@ -0,0 +1,269 @@
|
|||||||
|
import { bangDreamMainDataRepository } from '@/qqbot/plugins/bangDream/tsugu/models/main-data-repository';
|
||||||
|
import { stringToNumberArray } from '@/qqbot/plugins/bangDream/tsugu/models/model-utils';
|
||||||
|
import { BangDreamServerId as Server } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-protocol';
|
||||||
|
import {
|
||||||
|
BANGDREAM_CN_BLOCKED_EVENT_IDS,
|
||||||
|
BANGDREAM_CN_ESTIMATE_START_EVENT_ID,
|
||||||
|
BANGDREAM_DEFAULT_NO_BANG_DAYS,
|
||||||
|
} from '@/qqbot/plugins/bangDream/tsugu/runtime/runtime-options';
|
||||||
|
|
||||||
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||||
|
const ACTIVE_EVENT_LOOKBACK_MS = DAY_MS;
|
||||||
|
|
||||||
|
export interface BangDreamEventSchedule {
|
||||||
|
endAt: Array<number | null>;
|
||||||
|
startAt: Array<number | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BangDreamEventTimeLike extends BangDreamEventSchedule {
|
||||||
|
eventId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CnEventEstimatePolicyOptions {
|
||||||
|
blockedEventIds?: readonly number[];
|
||||||
|
defaultNoBangDays?: number;
|
||||||
|
estimateStartEventId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CnEventEstimateCalculationContext {
|
||||||
|
currentEvent: BangDreamEventTimeLike;
|
||||||
|
eventId: number;
|
||||||
|
getSchedule: (eventId: number) => BangDreamEventSchedule | null;
|
||||||
|
options?: CnEventEstimatePolicyOptions;
|
||||||
|
presentJpEventId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取活动时间表,避免国服预估逻辑依赖 Event class。
|
||||||
|
*
|
||||||
|
* @param eventId - 活动 ID。
|
||||||
|
* @returns 活动时间表。
|
||||||
|
*/
|
||||||
|
export function getBangDreamEventSchedule(
|
||||||
|
eventId: number,
|
||||||
|
): BangDreamEventSchedule | null {
|
||||||
|
const eventData = bangDreamMainDataRepository.getEntity<Record<string, any>>(
|
||||||
|
'events',
|
||||||
|
eventId,
|
||||||
|
);
|
||||||
|
if (!eventData) return null;
|
||||||
|
return {
|
||||||
|
endAt: stringToNumberArray(eventData.endAt),
|
||||||
|
startAt: stringToNumberArray(eventData.startAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取某服务器当前或最近结束的活动 ID。
|
||||||
|
*
|
||||||
|
* @param server - 目标服务器。
|
||||||
|
* @param time - 查询时间,默认当前时间。
|
||||||
|
* @returns 活动 ID。
|
||||||
|
*/
|
||||||
|
export function getPresentBangDreamEventId(
|
||||||
|
server: number,
|
||||||
|
time = Date.now(),
|
||||||
|
): number | null {
|
||||||
|
const eventIds = bangDreamMainDataRepository.getNumericIds('events');
|
||||||
|
const activeEventIds: number[] = [];
|
||||||
|
let latestEndedEventId: number | null = null;
|
||||||
|
let latestEndedAt = 0;
|
||||||
|
|
||||||
|
for (const eventId of eventIds) {
|
||||||
|
const schedule = getBangDreamEventSchedule(eventId);
|
||||||
|
const startAt = schedule?.startAt[server];
|
||||||
|
const endAt = schedule?.endAt[server];
|
||||||
|
if (startAt == null || endAt == null) continue;
|
||||||
|
|
||||||
|
if (startAt - ACTIVE_EVENT_LOOKBACK_MS <= time && endAt >= time) {
|
||||||
|
activeEventIds.push(eventId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (endAt <= time && endAt > latestEndedAt) {
|
||||||
|
latestEndedAt = endAt;
|
||||||
|
latestEndedEventId = eventId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeEventIds.length > 0) {
|
||||||
|
return activeEventIds[activeEventIds.length - 1];
|
||||||
|
}
|
||||||
|
return latestEndedEventId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算活动跨越的自然日数量,首尾日都计入。
|
||||||
|
*
|
||||||
|
* @param startTs - 开始时间。
|
||||||
|
* @param endTs - 结束时间。
|
||||||
|
* @returns 自然日数量。
|
||||||
|
*/
|
||||||
|
export function getBangDreamOccupiedDays(
|
||||||
|
startTs: number,
|
||||||
|
endTs: number,
|
||||||
|
): number {
|
||||||
|
const start = new Date(startTs);
|
||||||
|
const end = new Date(endTs);
|
||||||
|
const startDay = new Date(
|
||||||
|
start.getFullYear(),
|
||||||
|
start.getMonth(),
|
||||||
|
start.getDate(),
|
||||||
|
);
|
||||||
|
const endDay = new Date(end.getFullYear(), end.getMonth(), end.getDate());
|
||||||
|
return Math.floor((endDay.getTime() - startDay.getTime()) / DAY_MS) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 纯函数计算国服预估开放时间。
|
||||||
|
*
|
||||||
|
* @param context - 预估所需活动上下文。
|
||||||
|
* @returns 预估开始时间。
|
||||||
|
*/
|
||||||
|
export function calculateCnEventEstimateStartAt({
|
||||||
|
currentEvent,
|
||||||
|
eventId,
|
||||||
|
getSchedule,
|
||||||
|
options,
|
||||||
|
presentJpEventId,
|
||||||
|
}: CnEventEstimateCalculationContext): number | null {
|
||||||
|
const policyOptions = {
|
||||||
|
blockedEventIds: BANGDREAM_CN_BLOCKED_EVENT_IDS,
|
||||||
|
defaultNoBangDays: BANGDREAM_DEFAULT_NO_BANG_DAYS,
|
||||||
|
estimateStartEventId: BANGDREAM_CN_ESTIMATE_START_EVENT_ID,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
const currentStartCn = currentEvent.startAt[Server.cn];
|
||||||
|
const currentEndCn = currentEvent.endAt[Server.cn];
|
||||||
|
const currentStartJp = currentEvent.startAt[Server.jp];
|
||||||
|
const currentEndJp = currentEvent.endAt[Server.jp];
|
||||||
|
if (
|
||||||
|
currentStartCn == null ||
|
||||||
|
currentEndCn == null ||
|
||||||
|
currentStartJp == null ||
|
||||||
|
currentEndJp == null
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventLenOffset =
|
||||||
|
getBangDreamOccupiedDays(currentStartCn, currentEndCn) -
|
||||||
|
getBangDreamOccupiedDays(currentStartJp, currentEndJp);
|
||||||
|
const currentEventOffset = calculateCurrentEventOffset({
|
||||||
|
currentEvent,
|
||||||
|
eventLenOffset,
|
||||||
|
getSchedule,
|
||||||
|
policyOptions,
|
||||||
|
presentJpEventId,
|
||||||
|
});
|
||||||
|
if (currentEventOffset == null) return null;
|
||||||
|
|
||||||
|
let probableTimeOffset = currentStartCn;
|
||||||
|
for (let i = policyOptions.estimateStartEventId; i < presentJpEventId; i++) {
|
||||||
|
const schedule = getSchedule(i);
|
||||||
|
if (!schedule) continue;
|
||||||
|
if (!schedule.startAt[Server.cn]) {
|
||||||
|
const nextSchedule = getSchedule(i + 1);
|
||||||
|
if (!nextSchedule) continue;
|
||||||
|
probableTimeOffset +=
|
||||||
|
(getBangDreamOccupiedDays(
|
||||||
|
schedule.startAt[Server.jp],
|
||||||
|
nextSchedule.startAt[Server.jp],
|
||||||
|
) -
|
||||||
|
1) *
|
||||||
|
DAY_MS;
|
||||||
|
}
|
||||||
|
if (i + 1 === eventId) return probableTimeOffset + currentEventOffset;
|
||||||
|
if (policyOptions.blockedEventIds.includes(i + 1)) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const presentJpSchedule = getSchedule(presentJpEventId);
|
||||||
|
if (
|
||||||
|
!presentJpSchedule ||
|
||||||
|
presentJpSchedule.startAt[Server.jp] == null ||
|
||||||
|
presentJpSchedule.endAt[Server.jp] == null
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const presentJpEventLength =
|
||||||
|
(getBangDreamOccupiedDays(
|
||||||
|
presentJpSchedule.startAt[Server.jp],
|
||||||
|
presentJpSchedule.endAt[Server.jp],
|
||||||
|
) +
|
||||||
|
policyOptions.defaultNoBangDays) *
|
||||||
|
DAY_MS;
|
||||||
|
return probableTimeOffset + currentEventOffset + presentJpEventLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按当前主数据计算国服预估开放时间。
|
||||||
|
*
|
||||||
|
* @param eventId - 目标活动 ID。
|
||||||
|
* @param currentEvent - 国服当前活动。
|
||||||
|
* @returns 预估开始时间。
|
||||||
|
*/
|
||||||
|
export function estimateCnEventStartAt(
|
||||||
|
eventId: number,
|
||||||
|
currentEvent: BangDreamEventTimeLike | null,
|
||||||
|
): number | null {
|
||||||
|
if (!currentEvent) return null;
|
||||||
|
const presentJpEventId = getPresentBangDreamEventId(Server.jp);
|
||||||
|
if (presentJpEventId == null) return null;
|
||||||
|
return calculateCnEventEstimateStartAt({
|
||||||
|
currentEvent,
|
||||||
|
eventId,
|
||||||
|
getSchedule: getBangDreamEventSchedule,
|
||||||
|
presentJpEventId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateCurrentEventOffset({
|
||||||
|
currentEvent,
|
||||||
|
eventLenOffset,
|
||||||
|
getSchedule,
|
||||||
|
policyOptions,
|
||||||
|
presentJpEventId,
|
||||||
|
}: {
|
||||||
|
currentEvent: BangDreamEventTimeLike;
|
||||||
|
eventLenOffset: number;
|
||||||
|
getSchedule: (eventId: number) => BangDreamEventSchedule | null;
|
||||||
|
policyOptions: Required<CnEventEstimatePolicyOptions>;
|
||||||
|
presentJpEventId: number;
|
||||||
|
}) {
|
||||||
|
const currentEventId = currentEvent.eventId;
|
||||||
|
if (currentEventId < presentJpEventId) {
|
||||||
|
const nextSchedule = getSchedule(currentEventId + 1);
|
||||||
|
if (!nextSchedule || nextSchedule.startAt[Server.jp] == null) return null;
|
||||||
|
return (
|
||||||
|
(getBangDreamOccupiedDays(
|
||||||
|
currentEvent.startAt[Server.jp],
|
||||||
|
nextSchedule.startAt[Server.jp],
|
||||||
|
) -
|
||||||
|
1 +
|
||||||
|
eventLenOffset) *
|
||||||
|
DAY_MS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (currentEventId === presentJpEventId) {
|
||||||
|
const previousSchedule = getSchedule(currentEventId - 1);
|
||||||
|
if (!previousSchedule || previousSchedule.endAt[Server.jp] == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
(getBangDreamOccupiedDays(
|
||||||
|
previousSchedule.endAt[Server.jp],
|
||||||
|
currentEvent.endAt[Server.jp],
|
||||||
|
) -
|
||||||
|
1 +
|
||||||
|
eventLenOffset) *
|
||||||
|
DAY_MS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
(getBangDreamOccupiedDays(
|
||||||
|
currentEvent.startAt[Server.cn],
|
||||||
|
currentEvent.endAt[Server.cn],
|
||||||
|
) +
|
||||||
|
policyOptions.defaultNoBangDays) *
|
||||||
|
DAY_MS
|
||||||
|
);
|
||||||
|
}
|
||||||
212
src/qqbot/plugins/bangDream/tsugu/models/cutoff-policy.ts
Normal file
212
src/qqbot/plugins/bangDream/tsugu/models/cutoff-policy.ts
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
import {
|
||||||
|
BangDreamEventStatus,
|
||||||
|
BangDreamServerId as Server,
|
||||||
|
} from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-protocol';
|
||||||
|
import {
|
||||||
|
estimateCnEventStartAt,
|
||||||
|
type BangDreamEventTimeLike,
|
||||||
|
} from '@/qqbot/plugins/bangDream/tsugu/models/cn-event-estimate-policy';
|
||||||
|
import {
|
||||||
|
getBangDreamDateByServerTimezone,
|
||||||
|
getBangDreamServerUtcOffset,
|
||||||
|
isBangDreamDailyCheckpoint,
|
||||||
|
normalizeBangDreamTimestamp,
|
||||||
|
} from '@/qqbot/plugins/bangDream/tsugu/models/server-policy';
|
||||||
|
import { BANGDREAM_TIER_LIST_BY_SERVER } from '@/qqbot/plugins/bangDream/tsugu/runtime/runtime-options';
|
||||||
|
|
||||||
|
const FIRST_DAY_END_HOUR = 4;
|
||||||
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
export interface CutoffEventScheduleInput {
|
||||||
|
currentEvent?: BangDreamEventTimeLike | null;
|
||||||
|
endAt: Array<number | null>;
|
||||||
|
eventId: number;
|
||||||
|
server: number;
|
||||||
|
startAt: Array<number | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CutoffRecentEventCandidate {
|
||||||
|
eventId: number;
|
||||||
|
eventType: string;
|
||||||
|
startAt: Array<number | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CutoffRecentEventSelectionOptions {
|
||||||
|
candidates: CutoffRecentEventCandidate[];
|
||||||
|
count: number;
|
||||||
|
event: CutoffRecentEventCandidate;
|
||||||
|
sameType?: boolean;
|
||||||
|
server: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定服务器支持的档位列表。
|
||||||
|
*
|
||||||
|
* @param server - 目标服务器。
|
||||||
|
* @returns 档位列表。
|
||||||
|
*/
|
||||||
|
export function getCutoffTierList(server: number): readonly number[] {
|
||||||
|
return BANGDREAM_TIER_LIST_BY_SERVER[Server[server]] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断服务器是否支持指定档位。
|
||||||
|
*
|
||||||
|
* @param server - 目标服务器。
|
||||||
|
* @param tier - 目标档位。
|
||||||
|
* @returns 是否支持。
|
||||||
|
*/
|
||||||
|
export function isCutoffTierSupported(server: number, tier: number): boolean {
|
||||||
|
return getCutoffTierList(server).includes(tier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析档线活动时间,国服缺失时使用预估开始时间。
|
||||||
|
*
|
||||||
|
* @param input - 活动时间输入。
|
||||||
|
* @returns 档线可用的开始与结束时间。
|
||||||
|
*/
|
||||||
|
export function resolveCutoffEventSchedule({
|
||||||
|
currentEvent,
|
||||||
|
endAt,
|
||||||
|
eventId,
|
||||||
|
server,
|
||||||
|
startAt,
|
||||||
|
}: CutoffEventScheduleInput): { endAt: number | null; startAt: number | null } {
|
||||||
|
const directStartAt = startAt[server];
|
||||||
|
const directEndAt = endAt[server];
|
||||||
|
if (directStartAt || server !== Server.cn) {
|
||||||
|
return { endAt: directEndAt, startAt: directStartAt };
|
||||||
|
}
|
||||||
|
|
||||||
|
const estimatedStartAt = estimateCnEventStartAt(eventId, currentEvent);
|
||||||
|
const jpStartAt = startAt[Server.jp];
|
||||||
|
const jpEndAt = endAt[Server.jp];
|
||||||
|
if (estimatedStartAt == null || jpStartAt == null || jpEndAt == null) {
|
||||||
|
return { endAt: directEndAt, startAt: directStartAt };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
endAt: estimatedStartAt + (jpEndAt - jpStartAt),
|
||||||
|
startAt: estimatedStartAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算活动档线状态。
|
||||||
|
*
|
||||||
|
* @param startAt - 开始时间。
|
||||||
|
* @param endAt - 结束时间。
|
||||||
|
* @param now - 当前时间。
|
||||||
|
* @returns 活动状态。
|
||||||
|
*/
|
||||||
|
export function getCutoffEventStatus(
|
||||||
|
startAt: number | null,
|
||||||
|
endAt: number | null,
|
||||||
|
now = Date.now(),
|
||||||
|
): BangDreamEventStatus {
|
||||||
|
if (startAt == null || endAt == null) return BangDreamEventStatus.ended;
|
||||||
|
if (now < startAt) return BangDreamEventStatus.notStart;
|
||||||
|
if (now > endAt) return BangDreamEventStatus.ended;
|
||||||
|
return BangDreamEventStatus.inProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取预测窗口,统一使用档线对象已解析的时间。
|
||||||
|
*
|
||||||
|
* @param startAt - 开始时间。
|
||||||
|
* @param endAt - 结束时间。
|
||||||
|
* @returns 秒级预测窗口。
|
||||||
|
*/
|
||||||
|
export function getCutoffPredictionWindow(startAt: number, endAt: number) {
|
||||||
|
return {
|
||||||
|
endTs: Math.floor(endAt / 1000),
|
||||||
|
startTs: Math.floor(startAt / 1000),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算时间点位于活动第几天。
|
||||||
|
*
|
||||||
|
* @param server - 目标服务器。
|
||||||
|
* @param eventStartAt - 活动开始时间。
|
||||||
|
* @param time - 目标时间。
|
||||||
|
* @returns 活动天数索引。
|
||||||
|
*/
|
||||||
|
export function getCutoffDayIndex(
|
||||||
|
server: number,
|
||||||
|
eventStartAt: number,
|
||||||
|
time: number | string,
|
||||||
|
) {
|
||||||
|
if (!eventStartAt) return 0;
|
||||||
|
const offsetMs = getBangDreamServerUtcOffset(server) * 60 * 60 * 1000;
|
||||||
|
const eventStartAtTime = normalizeBangDreamTimestamp(eventStartAt);
|
||||||
|
const timestamp = normalizeBangDreamTimestamp(time);
|
||||||
|
const serverStartTime = eventStartAtTime + offsetMs;
|
||||||
|
const startDate = new Date(serverStartTime);
|
||||||
|
const firstDayEndServerTime =
|
||||||
|
serverStartTime +
|
||||||
|
(DAY_MS +
|
||||||
|
FIRST_DAY_END_HOUR * 60 * 60 * 1000 -
|
||||||
|
startDate.getUTCHours() * 60 * 60 * 1000 -
|
||||||
|
startDate.getUTCMinutes() * 60 * 1000 -
|
||||||
|
startDate.getUTCSeconds() * 1000 -
|
||||||
|
startDate.getUTCMilliseconds());
|
||||||
|
const firstDayEndTime = firstDayEndServerTime - offsetMs;
|
||||||
|
if (timestamp < firstDayEndTime) return 0;
|
||||||
|
return Math.ceil((timestamp - firstDayEndTime) / DAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断 Date 是否是档线日增 checkpoint。
|
||||||
|
*
|
||||||
|
* @param server - 目标服务器。
|
||||||
|
* @param date - 已按服务器时区转换后的 Date。
|
||||||
|
* @returns 是否为 checkpoint。
|
||||||
|
*/
|
||||||
|
export function isCutoffDailyCheckpoint(server: number, date: Date): boolean {
|
||||||
|
return isBangDreamDailyCheckpoint(server, date);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将时间戳转换为服务器时区 Date。
|
||||||
|
*
|
||||||
|
* @param time - 秒级或毫秒级时间戳。
|
||||||
|
* @param server - 目标服务器。
|
||||||
|
* @returns 服务器时区 Date。
|
||||||
|
*/
|
||||||
|
export function getCutoffDateByServerTimezone(
|
||||||
|
time: number | string,
|
||||||
|
server: number,
|
||||||
|
): Date {
|
||||||
|
return getBangDreamDateByServerTimezone(time, server);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择用于档线对比的最近活动 ID。
|
||||||
|
*
|
||||||
|
* @param options - 活动候选和过滤条件。
|
||||||
|
* @returns 最近活动 ID 列表。
|
||||||
|
*/
|
||||||
|
export function selectRecentCutoffEventIds({
|
||||||
|
candidates,
|
||||||
|
count,
|
||||||
|
event,
|
||||||
|
sameType = false,
|
||||||
|
server,
|
||||||
|
}: CutoffRecentEventSelectionOptions): number[] {
|
||||||
|
const eventStartAt = event.startAt[server];
|
||||||
|
if (eventStartAt == null) return [];
|
||||||
|
|
||||||
|
const matched = [...candidates]
|
||||||
|
.filter((candidate) => candidate.startAt[server] != null)
|
||||||
|
.sort((a, b) => b.startAt[server] - a.startAt[server])
|
||||||
|
.filter((candidate) => {
|
||||||
|
if (sameType && candidate.eventType !== event.eventType) return false;
|
||||||
|
return candidate.startAt[server] <= eventStartAt;
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.startAt[server] - b.startAt[server]);
|
||||||
|
|
||||||
|
return matched
|
||||||
|
.slice(Math.max(matched.length - count, 0))
|
||||||
|
.map((item) => item.eventId);
|
||||||
|
}
|
||||||
@ -6,7 +6,6 @@ import {
|
|||||||
preferHhwxSource,
|
preferHhwxSource,
|
||||||
reportDataSourceProblem,
|
reportDataSourceProblem,
|
||||||
clearDataSourceProblem,
|
clearDataSourceProblem,
|
||||||
tierListOfServer,
|
|
||||||
} from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
} from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
||||||
import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server';
|
import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server';
|
||||||
import {
|
import {
|
||||||
@ -16,11 +15,14 @@ import {
|
|||||||
import { predict } from '@/qqbot/plugins/bangDream/tsugu/calculations/cutoff-predictor';
|
import { predict } from '@/qqbot/plugins/bangDream/tsugu/calculations/cutoff-predictor';
|
||||||
import { logger } from '@/qqbot/plugins/bangDream/tsugu/runtime/logger';
|
import { logger } from '@/qqbot/plugins/bangDream/tsugu/runtime/logger';
|
||||||
import {
|
import {
|
||||||
normalizeTimestamp,
|
getCutoffDateByServerTimezone,
|
||||||
getDateByServerTimezone,
|
getCutoffDayIndex,
|
||||||
getServerUtcOffset,
|
getCutoffEventStatus,
|
||||||
getProbableTimeDifference,
|
getCutoffPredictionWindow,
|
||||||
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-time';
|
isCutoffDailyCheckpoint,
|
||||||
|
isCutoffTierSupported,
|
||||||
|
resolveCutoffEventSchedule,
|
||||||
|
} from '@/qqbot/plugins/bangDream/tsugu/models/cutoff-policy';
|
||||||
import { BangDreamEventStatus } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
|
import { BangDreamEventStatus } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
|
||||||
|
|
||||||
export class Cutoff {
|
export class Cutoff {
|
||||||
@ -58,7 +60,7 @@ export class Cutoff {
|
|||||||
this.eventId = eventId;
|
this.eventId = eventId;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
//如果该档线不在该服的档线列表中,直接返回
|
//如果该档线不在该服的档线列表中,直接返回
|
||||||
if (!tierListOfServer[Server[server]].includes(tier)) {
|
if (!isCutoffTierSupported(server, tier)) {
|
||||||
this.isExist = false;
|
this.isExist = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -67,29 +69,17 @@ export class Cutoff {
|
|||||||
// this.startAt = event.startAt[server]
|
// this.startAt = event.startAt[server]
|
||||||
// this.endAt = event.endAt[server]
|
// this.endAt = event.endAt[server]
|
||||||
// 当该活动在服务器上尚未存在时,使用预测的时间去推断startAt以及endAt,以解决部分情况下卡死及档线状态不对的问题
|
// 当该活动在服务器上尚未存在时,使用预测的时间去推断startAt以及endAt,以解决部分情况下卡死及档线状态不对的问题
|
||||||
this.startAt =
|
const schedule = resolveCutoffEventSchedule({
|
||||||
event.startAt[server] || server != Server.cn
|
currentEvent: getPresentEvent(this.server),
|
||||||
? event.startAt[server]
|
endAt: event.endAt,
|
||||||
: getProbableTimeDifference(this.eventId, getPresentEvent(this.server));
|
eventId: this.eventId,
|
||||||
this.endAt =
|
server,
|
||||||
event.endAt[server] || server != Server.cn
|
startAt: event.startAt,
|
||||||
? event.endAt[server]
|
});
|
||||||
: getProbableTimeDifference(
|
this.startAt = schedule.startAt;
|
||||||
this.eventId,
|
this.endAt = schedule.endAt;
|
||||||
getPresentEvent(this.server),
|
|
||||||
) +
|
|
||||||
(event.endAt[Server.jp] - event.startAt[Server.jp]);
|
|
||||||
const tempEvent = new Event(this.eventId);
|
|
||||||
this.currentGetDataTime = new Date().getTime();
|
this.currentGetDataTime = new Date().getTime();
|
||||||
//状态
|
this.status = getCutoffEventStatus(this.startAt, this.endAt);
|
||||||
const time = new Date().getTime();
|
|
||||||
if (time < tempEvent.startAt[this.server]) {
|
|
||||||
this.status = BangDreamEventStatus.notStart;
|
|
||||||
} else if (time > tempEvent.endAt[this.server]) {
|
|
||||||
this.status = BangDreamEventStatus.ended;
|
|
||||||
} else {
|
|
||||||
this.status = BangDreamEventStatus.inProgress;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 在 Cutoff 模型中获取最终 Tracker 数据源。
|
* 在 Cutoff 模型中获取最终 Tracker 数据源。
|
||||||
@ -245,9 +235,10 @@ export class Cutoff {
|
|||||||
this.latestCutoff = this.cutoffs[this.cutoffs.length - 1];
|
this.latestCutoff = this.cutoffs[this.cutoffs.length - 1];
|
||||||
}
|
}
|
||||||
//rate
|
//rate
|
||||||
const rateDataList = bangDreamMainDataRepository.getValue<
|
const rateDataList =
|
||||||
Array<{ server: number; type: string; tier: number; rate: number }>
|
bangDreamMainDataRepository.getValue<
|
||||||
>('rates');
|
Array<{ server: number; type: string; tier: number; rate: number }>
|
||||||
|
>('rates');
|
||||||
const rateData = rateDataList.find((element) => {
|
const rateData = rateDataList.find((element) => {
|
||||||
return (
|
return (
|
||||||
element.server == this.server &&
|
element.server == this.server &&
|
||||||
@ -294,11 +285,7 @@ export class Cutoff {
|
|||||||
* @returns 计算后的数值。
|
* @returns 计算后的数值。
|
||||||
*/
|
*/
|
||||||
getPredictionWindow(): { startTs: number; endTs: number } {
|
getPredictionWindow(): { startTs: number; endTs: number } {
|
||||||
const event = new Event(this.eventId);
|
return getCutoffPredictionWindow(this.startAt, this.endAt);
|
||||||
return {
|
|
||||||
startTs: Math.floor(event.startAt[this.server] / 1000),
|
|
||||||
endTs: Math.floor(event.endAt[this.server] / 1000),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 在 Cutoff 模型中获取档线列表InSeconds。
|
* 在 Cutoff 模型中获取档线列表InSeconds。
|
||||||
@ -342,36 +329,7 @@ export class Cutoff {
|
|||||||
* @param ts - ts参数。
|
* @param ts - ts参数。
|
||||||
*/
|
*/
|
||||||
getDaysOfEvent(ts: number) {
|
getDaysOfEvent(ts: number) {
|
||||||
if (!this.startAt) return 0;
|
return getCutoffDayIndex(this.server, this.startAt, ts);
|
||||||
const offsetMs = getServerUtcOffset(this.server) * 60 * 60 * 1000;
|
|
||||||
const eventStartAtTime = normalizeTimestamp(this.startAt);
|
|
||||||
const timestamp = normalizeTimestamp(ts);
|
|
||||||
|
|
||||||
const serverStartTime = eventStartAtTime + offsetMs;
|
|
||||||
|
|
||||||
const startDate = new Date(serverStartTime);
|
|
||||||
|
|
||||||
const hour = startDate.getUTCHours();
|
|
||||||
const minute = startDate.getUTCMinutes();
|
|
||||||
const second = startDate.getUTCSeconds();
|
|
||||||
const millisecond = startDate.getUTCMilliseconds();
|
|
||||||
|
|
||||||
const firstDayEndServerTime =
|
|
||||||
serverStartTime +
|
|
||||||
(86400000 +
|
|
||||||
4 * 60 * 60 * 1000 -
|
|
||||||
hour * 60 * 60 * 1000 -
|
|
||||||
minute * 60 * 1000 -
|
|
||||||
second * 1000 -
|
|
||||||
millisecond);
|
|
||||||
|
|
||||||
const firstDayEndTime = firstDayEndServerTime - offsetMs;
|
|
||||||
|
|
||||||
if (timestamp < firstDayEndTime) {
|
|
||||||
return 0;
|
|
||||||
} else {
|
|
||||||
return Math.ceil((timestamp - firstDayEndTime) / 86400000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 在 Cutoff 模型中判断日增Checkpoint。
|
* 在 Cutoff 模型中判断日增Checkpoint。
|
||||||
@ -380,13 +338,7 @@ export class Cutoff {
|
|||||||
* @returns 判断结果。
|
* @returns 判断结果。
|
||||||
*/
|
*/
|
||||||
isDailyCheckpoint(date: Date): boolean {
|
isDailyCheckpoint(date: Date): boolean {
|
||||||
return (
|
return isCutoffDailyCheckpoint(this.server, date);
|
||||||
(this.server == Server.cn ||
|
|
||||||
this.server == Server.tw ||
|
|
||||||
this.server == Server.jp) &&
|
|
||||||
date.getUTCHours() === 3 &&
|
|
||||||
date.getUTCMinutes() === 45
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 在 Cutoff 模型中获取日增CheckpointSeries。
|
* 在 Cutoff 模型中获取日增CheckpointSeries。
|
||||||
@ -397,8 +349,8 @@ export class Cutoff {
|
|||||||
const score: number[] = [];
|
const score: number[] = [];
|
||||||
const time: number[] = [];
|
const time: number[] = [];
|
||||||
for (const c of this.cutoffs) {
|
for (const c of this.cutoffs) {
|
||||||
const timestamp = normalizeTimestamp(c.time);
|
const timestamp = c.time;
|
||||||
const date = getDateByServerTimezone(timestamp, this.server);
|
const date = getCutoffDateByServerTimezone(timestamp, this.server);
|
||||||
if (this.isDailyCheckpoint(date)) {
|
if (this.isDailyCheckpoint(date)) {
|
||||||
score.push(c.ep);
|
score.push(c.ep);
|
||||||
time.push(timestamp);
|
time.push(timestamp);
|
||||||
@ -609,11 +561,11 @@ export class Cutoff {
|
|||||||
let lastCutoffTime = this.cutoffs[this.cutoffs.length - 1].time;
|
let lastCutoffTime = this.cutoffs[this.cutoffs.length - 1].time;
|
||||||
// HHWX数据源会在快要结活的时候改为每15分钟抓取一次,因此需要主动规避
|
// HHWX数据源会在快要结活的时候改为每15分钟抓取一次,因此需要主动规避
|
||||||
let usePrevPoint = false;
|
let usePrevPoint = false;
|
||||||
const UTCMin = getDateByServerTimezone(
|
const UTCMin = getCutoffDateByServerTimezone(
|
||||||
lastCutoffTime,
|
lastCutoffTime,
|
||||||
this.server,
|
this.server,
|
||||||
).getUTCMinutes();
|
).getUTCMinutes();
|
||||||
const UTCHour = getDateByServerTimezone(
|
const UTCHour = getCutoffDateByServerTimezone(
|
||||||
lastCutoffTime,
|
lastCutoffTime,
|
||||||
this.server,
|
this.server,
|
||||||
).getUTCHours();
|
).getUTCHours();
|
||||||
@ -630,13 +582,13 @@ export class Cutoff {
|
|||||||
const time: number[] = [];
|
const time: number[] = [];
|
||||||
const scoreCur: number[] = [];
|
const scoreCur: number[] = [];
|
||||||
const timeCur: number[] = [];
|
const timeCur: number[] = [];
|
||||||
const dateNow = getDateByServerTimezone(lastCutoffTime, this.server);
|
const dateNow = getCutoffDateByServerTimezone(lastCutoffTime, this.server);
|
||||||
const lastestUtcHour = dateNow.getUTCHours();
|
const lastestUtcHour = dateNow.getUTCHours();
|
||||||
const lastestUtcMinutes = dateNow.getUTCMinutes();
|
const lastestUtcMinutes = dateNow.getUTCMinutes();
|
||||||
|
|
||||||
for (const c of this.cutoffs) {
|
for (const c of this.cutoffs) {
|
||||||
let allowPushFlag = false;
|
let allowPushFlag = false;
|
||||||
const timestamp = normalizeTimestamp(c.time);
|
const timestamp = c.time;
|
||||||
const d = this.getDaysOfEvent(timestamp);
|
const d = this.getDaysOfEvent(timestamp);
|
||||||
if (d < curEventDays - 2) {
|
if (d < curEventDays - 2) {
|
||||||
continue;
|
continue;
|
||||||
@ -644,7 +596,7 @@ export class Cutoff {
|
|||||||
if (d > curEventDays - 2) {
|
if (d > curEventDays - 2) {
|
||||||
allowPushFlag = true;
|
allowPushFlag = true;
|
||||||
}
|
}
|
||||||
const date = getDateByServerTimezone(timestamp, this.server);
|
const date = getCutoffDateByServerTimezone(timestamp, this.server);
|
||||||
if (
|
if (
|
||||||
(this.server == Server.cn ||
|
(this.server == Server.cn ||
|
||||||
this.server == Server.tw ||
|
this.server == Server.tw ||
|
||||||
|
|||||||
@ -3,13 +3,15 @@ import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server';
|
|||||||
import { bangDreamMainDataRepository } from '@/qqbot/plugins/bangDream/tsugu/models/main-data-repository';
|
import { bangDreamMainDataRepository } from '@/qqbot/plugins/bangDream/tsugu/models/main-data-repository';
|
||||||
import { Attribute } from '@/qqbot/plugins/bangDream/tsugu/models/attribute';
|
import { Attribute } from '@/qqbot/plugins/bangDream/tsugu/models/attribute';
|
||||||
import { Character } from '@/qqbot/plugins/bangDream/tsugu/models/character';
|
import { Character } from '@/qqbot/plugins/bangDream/tsugu/models/character';
|
||||||
import {
|
import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
||||||
globalDefaultServer,
|
|
||||||
} from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
|
||||||
import { stringToNumberArray } from '@/qqbot/plugins/bangDream/tsugu/models/model-utils';
|
import { stringToNumberArray } from '@/qqbot/plugins/bangDream/tsugu/models/model-utils';
|
||||||
import { getProbableTimeDifference } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-time';
|
|
||||||
import { BANGDREAM_EVENT_TYPE_NAME } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
|
import { BANGDREAM_EVENT_TYPE_NAME } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
|
||||||
import { eventDataRepository } from '@/qqbot/plugins/bangDream/tsugu/models/event-data-repository';
|
import { eventDataRepository } from '@/qqbot/plugins/bangDream/tsugu/models/event-data-repository';
|
||||||
|
import { estimateCnEventStartAt } from '@/qqbot/plugins/bangDream/tsugu/models/cn-event-estimate-policy';
|
||||||
|
import {
|
||||||
|
selectRecentCutoffEventIds,
|
||||||
|
type CutoffRecentEventCandidate,
|
||||||
|
} from '@/qqbot/plugins/bangDream/tsugu/models/cutoff-policy';
|
||||||
|
|
||||||
const typeName: Record<string, string> = BANGDREAM_EVENT_TYPE_NAME;
|
const typeName: Record<string, string> = BANGDREAM_EVENT_TYPE_NAME;
|
||||||
|
|
||||||
@ -93,10 +95,9 @@ export class Event {
|
|||||||
*/
|
*/
|
||||||
constructor(eventId: number) {
|
constructor(eventId: number) {
|
||||||
this.eventId = eventId;
|
this.eventId = eventId;
|
||||||
const eventData = bangDreamMainDataRepository.getEntity<Record<string, any>>(
|
const eventData = bangDreamMainDataRepository.getEntity<
|
||||||
'events',
|
Record<string, any>
|
||||||
eventId,
|
>('events', eventId);
|
||||||
);
|
|
||||||
if (eventData == undefined) {
|
if (eventData == undefined) {
|
||||||
this.isExist = false;
|
this.isExist = false;
|
||||||
return;
|
return;
|
||||||
@ -387,17 +388,17 @@ export function sortEventList(
|
|||||||
let prvEvent = null;
|
let prvEvent = null;
|
||||||
let nxtEvent = null;
|
let nxtEvent = null;
|
||||||
if (a.startAt[server] == null) {
|
if (a.startAt[server] == null) {
|
||||||
prvEvent = getProbableTimeDifference(a.eventId, presentEventCN);
|
prvEvent = estimateCnEventStartAt(a.eventId, presentEventCN);
|
||||||
} else {
|
} else {
|
||||||
prvEvent = a.startAt[server];
|
prvEvent = a.startAt[server];
|
||||||
}
|
}
|
||||||
if (b.startAt[server] == null) {
|
if (b.startAt[server] == null) {
|
||||||
nxtEvent = getProbableTimeDifference(b.eventId, presentEventCN);
|
nxtEvent = estimateCnEventStartAt(b.eventId, presentEventCN);
|
||||||
} else {
|
} else {
|
||||||
nxtEvent = b.startAt[server];
|
nxtEvent = b.startAt[server];
|
||||||
}
|
}
|
||||||
if (prvEvent != null || nxtEvent != null) {
|
if (prvEvent != null || nxtEvent != null) {
|
||||||
return prvEvent - nxtEvent;
|
return (prvEvent ?? 0) - (nxtEvent ?? 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@ -425,31 +426,18 @@ export function getRecentEventListByEventAndServer(
|
|||||||
sameType: boolean = false,
|
sameType: boolean = false,
|
||||||
) {
|
) {
|
||||||
const eventIdList = bangDreamMainDataRepository.getNumericIds('events');
|
const eventIdList = bangDreamMainDataRepository.getNumericIds('events');
|
||||||
//对活动列表进行排序,从新到旧
|
const candidates: CutoffRecentEventCandidate[] = eventIdList
|
||||||
eventIdList.sort((a, b) => {
|
.map((eventId) => new Event(eventId))
|
||||||
const eventA = new Event(a);
|
.map((candidate) => ({
|
||||||
const eventB = new Event(b);
|
eventId: candidate.eventId,
|
||||||
if (eventA.startAt[server] == null || eventB.startAt[server] == null) {
|
eventType: candidate.eventType,
|
||||||
return 0;
|
startAt: candidate.startAt,
|
||||||
}
|
}));
|
||||||
return eventB.startAt[server] - eventA.startAt[server];
|
return selectRecentCutoffEventIds({
|
||||||
});
|
candidates,
|
||||||
const tempEventList: Array<Event> = [];
|
count,
|
||||||
for (let i = 0; i < eventIdList.length; i++) {
|
event,
|
||||||
const tempEvent = new Event(eventIdList[i]);
|
sameType,
|
||||||
if (tempEvent.startAt[server] != null) {
|
server,
|
||||||
if (sameType && tempEvent.eventType != event.eventType) {
|
}).map((eventId) => new Event(eventId));
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (tempEvent.startAt[server] > event.startAt[server]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
tempEventList.push(tempEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sortEventList(tempEventList, [server]);
|
|
||||||
return tempEventList.slice(
|
|
||||||
tempEventList.length - count,
|
|
||||||
tempEventList.length,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
80
src/qqbot/plugins/bangDream/tsugu/models/server-policy.ts
Normal file
80
src/qqbot/plugins/bangDream/tsugu/models/server-policy.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { BangDreamServerId as Server } from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-protocol';
|
||||||
|
|
||||||
|
const SERVER_UTC_OFFSET_BY_SERVER: Record<number, number> = {
|
||||||
|
[Server.cn]: 8,
|
||||||
|
[Server.en]: 0,
|
||||||
|
[Server.jp]: 9,
|
||||||
|
[Server.kr]: 9,
|
||||||
|
[Server.tw]: 8,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DAILY_CHECKPOINT_SERVERS = new Set<number>([
|
||||||
|
Server.cn,
|
||||||
|
Server.jp,
|
||||||
|
Server.tw,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化上游时间戳为毫秒。
|
||||||
|
*
|
||||||
|
* @param time - 秒级或毫秒级时间戳。
|
||||||
|
* @returns 毫秒级时间戳。
|
||||||
|
*/
|
||||||
|
export function normalizeBangDreamTimestamp(time: number | string): number {
|
||||||
|
const timestamp = Number(time);
|
||||||
|
return timestamp < 1e12 ? timestamp * 1000 : timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取服务器对应的 UTC 偏移小时数。
|
||||||
|
*
|
||||||
|
* @param server - 目标服务器。
|
||||||
|
* @returns UTC 偏移小时数。
|
||||||
|
*/
|
||||||
|
export function getBangDreamServerUtcOffset(server: number): number {
|
||||||
|
return SERVER_UTC_OFFSET_BY_SERVER[server] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将时间戳转换为目标服务器时区下的 Date。
|
||||||
|
*
|
||||||
|
* @param time - 秒级或毫秒级时间戳。
|
||||||
|
* @param server - 目标服务器。
|
||||||
|
* @returns 服务器时区 Date。
|
||||||
|
*/
|
||||||
|
export function getBangDreamDateByServerTimezone(
|
||||||
|
time: number | string,
|
||||||
|
server: number,
|
||||||
|
): Date {
|
||||||
|
const timestamp = normalizeBangDreamTimestamp(time);
|
||||||
|
const offset = getBangDreamServerUtcOffset(server);
|
||||||
|
return new Date(timestamp + offset * 60 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断服务器是否存在每日档线 checkpoint。
|
||||||
|
*
|
||||||
|
* @param server - 目标服务器。
|
||||||
|
* @returns 是否启用每日 checkpoint。
|
||||||
|
*/
|
||||||
|
export function hasBangDreamDailyCheckpoint(server: number): boolean {
|
||||||
|
return DAILY_CHECKPOINT_SERVERS.has(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断时间是否命中档线日增 checkpoint。
|
||||||
|
*
|
||||||
|
* @param server - 目标服务器。
|
||||||
|
* @param date - 已按服务器时区转换后的 Date。
|
||||||
|
* @returns 是否命中 checkpoint。
|
||||||
|
*/
|
||||||
|
export function isBangDreamDailyCheckpoint(
|
||||||
|
server: number,
|
||||||
|
date: Date,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
hasBangDreamDailyCheckpoint(server) &&
|
||||||
|
date.getUTCHours() === 3 &&
|
||||||
|
date.getUTCMinutes() === 45
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -6,13 +6,15 @@ import {
|
|||||||
} from '@/qqbot/plugins/bangDream/tsugu/models/server';
|
} from '@/qqbot/plugins/bangDream/tsugu/models/server';
|
||||||
import { drawListByServerList } from './list-frame';
|
import { drawListByServerList } from './list-frame';
|
||||||
import { Canvas } from 'skia-canvas';
|
import { Canvas } from 'skia-canvas';
|
||||||
import { Event } from '@/qqbot/plugins/bangDream/tsugu/models/event';
|
|
||||||
import mainAPI from '@/qqbot/plugins/bangDream/tsugu/models/main-data-store';
|
|
||||||
import {
|
import {
|
||||||
BANGDREAM_CN_BLOCKED_EVENT_IDS,
|
estimateCnEventStartAt,
|
||||||
BANGDREAM_CN_ESTIMATE_START_EVENT_ID,
|
type BangDreamEventTimeLike,
|
||||||
BANGDREAM_DEFAULT_NO_BANG_DAYS,
|
} from '@/qqbot/plugins/bangDream/tsugu/models/cn-event-estimate-policy';
|
||||||
} from '@/qqbot/plugins/bangDream/tsugu/models/bangdream-constants';
|
import {
|
||||||
|
getBangDreamDateByServerTimezone,
|
||||||
|
getBangDreamServerUtcOffset,
|
||||||
|
normalizeBangDreamTimestamp,
|
||||||
|
} from '@/qqbot/plugins/bangDream/tsugu/models/server-policy';
|
||||||
|
|
||||||
interface TimeInListOptions {
|
interface TimeInListOptions {
|
||||||
key?: string;
|
key?: string;
|
||||||
@ -35,13 +37,16 @@ export async function drawTimeInList(
|
|||||||
for (let i = 0; i < content.length; i++) {
|
for (let i = 0; i < content.length; i++) {
|
||||||
const element = content[i];
|
const element = content[i];
|
||||||
if (element == null) {
|
if (element == null) {
|
||||||
if (i == 3 && estimateCNTime) {
|
if (i == 3 && estimateCNTime && eventId != null) {
|
||||||
const currentEvent = getPresentEvent(getServerByName('cn'));
|
const currentEvent = getPresentEvent(getServerByName('cn'));
|
||||||
const currentEventId = currentEvent.eventId;
|
const currentEventId = currentEvent?.eventId;
|
||||||
if (eventId > currentEventId) {
|
const estimatedStartAt = getProbableTimeDifference(
|
||||||
|
eventId,
|
||||||
|
currentEvent,
|
||||||
|
);
|
||||||
|
if (currentEventId != null && eventId > currentEventId) {
|
||||||
formattedTimeList.push(
|
formattedTimeList.push(
|
||||||
formatTime(getProbableTimeDifference(eventId, currentEvent)) +
|
formatTime(estimatedStartAt) + ' (预计开放时间)',
|
||||||
' (预计开放时间)',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,91 +73,9 @@ export async function drawTimeInList(
|
|||||||
*/
|
*/
|
||||||
export function getProbableTimeDifference(
|
export function getProbableTimeDifference(
|
||||||
eventId: number,
|
eventId: number,
|
||||||
currentEvent: Event,
|
currentEvent: BangDreamEventTimeLike | null,
|
||||||
): number {
|
): number | null {
|
||||||
const eventsData = mainAPI['events'];
|
return estimateCnEventStartAt(eventId, currentEvent);
|
||||||
const presentEventJP = getPresentEvent(Server.jp).eventId; // 取得日服最新一期的活动
|
|
||||||
let currentEventWithNoBanGDaysTotalOffset = 0;
|
|
||||||
const currentEventId = currentEvent.eventId;
|
|
||||||
// 计算当前活动日服与国服的实际持续天数偏移
|
|
||||||
const eventLenOffset =
|
|
||||||
occupiedDays(
|
|
||||||
new Event(currentEventId).startAt[Server.cn],
|
|
||||||
new Event(currentEventId).endAt[Server.cn],
|
|
||||||
) -
|
|
||||||
occupiedDays(
|
|
||||||
new Event(currentEventId).startAt[Server.jp],
|
|
||||||
new Event(currentEventId).endAt[Server.jp],
|
|
||||||
);
|
|
||||||
// 计算当前正在进行的活动含无邦日当天一共有多少天
|
|
||||||
if (currentEventId < presentEventJP) {
|
|
||||||
currentEventWithNoBanGDaysTotalOffset =
|
|
||||||
(occupiedDays(
|
|
||||||
new Event(currentEventId).startAt[Server.jp],
|
|
||||||
new Event(currentEventId + 1).startAt[Server.jp],
|
|
||||||
) -
|
|
||||||
1 +
|
|
||||||
eventLenOffset) *
|
|
||||||
24 *
|
|
||||||
3600 *
|
|
||||||
1000;
|
|
||||||
} else if (currentEventId == presentEventJP) {
|
|
||||||
// 如果当前活动与日服并行,则计算无邦日就从上一个endAt到这一个endAt
|
|
||||||
currentEventWithNoBanGDaysTotalOffset =
|
|
||||||
(occupiedDays(
|
|
||||||
new Event(currentEventId - 1).endAt[Server.jp],
|
|
||||||
new Event(currentEventId).endAt[Server.jp],
|
|
||||||
) -
|
|
||||||
1 +
|
|
||||||
eventLenOffset) *
|
|
||||||
24 *
|
|
||||||
3600 *
|
|
||||||
1000;
|
|
||||||
} else {
|
|
||||||
// 预防国服可能出现与台服一样存在自有活动的情形,如台服5001。
|
|
||||||
currentEventWithNoBanGDaysTotalOffset =
|
|
||||||
(occupiedDays(
|
|
||||||
new Event(currentEventId).startAt[Server.cn],
|
|
||||||
new Event(currentEventId).endAt[Server.cn],
|
|
||||||
) +
|
|
||||||
BANGDREAM_DEFAULT_NO_BANG_DAYS) *
|
|
||||||
24 *
|
|
||||||
3600 *
|
|
||||||
1000;
|
|
||||||
}
|
|
||||||
let probableTimeOffset = currentEvent.startAt[Server.cn]; // 等于正在举办活动的StartAt
|
|
||||||
for (let i = BANGDREAM_CN_ESTIMATE_START_EVENT_ID; i < presentEventJP; i++) {
|
|
||||||
if (!eventsData[i.toString()]['startAt'][Server.cn]) {
|
|
||||||
// 对于国服来说当前活动未举办
|
|
||||||
// 计算活动相对于日服(含无邦日)的时长。
|
|
||||||
probableTimeOffset +=
|
|
||||||
(occupiedDays(
|
|
||||||
new Event(i).startAt[Server.jp],
|
|
||||||
new Event(i + 1).startAt[Server.jp],
|
|
||||||
) -
|
|
||||||
1) *
|
|
||||||
24 *
|
|
||||||
3600 *
|
|
||||||
1000;
|
|
||||||
}
|
|
||||||
if (i + 1 == eventId)
|
|
||||||
return probableTimeOffset + currentEventWithNoBanGDaysTotalOffset; //如果下一个循环是要获取的时间
|
|
||||||
if (BANGDREAM_CN_BLOCKED_EVENT_IDS.includes(i + 1)) continue; // 对于国服而言不会举办的活动,跳过
|
|
||||||
}
|
|
||||||
const presentEventJPLen =
|
|
||||||
(occupiedDays(
|
|
||||||
new Event(presentEventJP).startAt[Server.jp],
|
|
||||||
new Event(presentEventJP).endAt[Server.jp],
|
|
||||||
) +
|
|
||||||
BANGDREAM_DEFAULT_NO_BANG_DAYS) *
|
|
||||||
24 *
|
|
||||||
3600 *
|
|
||||||
1000;
|
|
||||||
return (
|
|
||||||
probableTimeOffset +
|
|
||||||
currentEventWithNoBanGDaysTotalOffset +
|
|
||||||
presentEventJPLen
|
|
||||||
); // 日服321,预测322+
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -311,24 +234,6 @@ export function formatSeconds(value: number) {
|
|||||||
* @param endTs - endTs参数。
|
* @param endTs - endTs参数。
|
||||||
* @returns 计算后的数值。
|
* @returns 计算后的数值。
|
||||||
*/
|
*/
|
||||||
function occupiedDays(startTs: number, endTs: number): number {
|
|
||||||
const start = new Date(startTs);
|
|
||||||
const end = new Date(endTs);
|
|
||||||
|
|
||||||
// 取年月日,忽略时分秒
|
|
||||||
const startDay = new Date(
|
|
||||||
start.getFullYear(),
|
|
||||||
start.getMonth(),
|
|
||||||
start.getDate(),
|
|
||||||
);
|
|
||||||
const endDay = new Date(end.getFullYear(), end.getMonth(), end.getDate());
|
|
||||||
|
|
||||||
const msPerDay = 1000 * 60 * 60 * 24;
|
|
||||||
|
|
||||||
// 计算跨越的天数,再加1包含第一天
|
|
||||||
return Math.floor((endDay.getTime() - startDay.getTime()) / msPerDay) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在图片布局层中规范化Timestamp。
|
* 在图片布局层中规范化Timestamp。
|
||||||
*
|
*
|
||||||
@ -336,8 +241,7 @@ function occupiedDays(startTs: number, endTs: number): number {
|
|||||||
* @returns 计算后的数值。
|
* @returns 计算后的数值。
|
||||||
*/
|
*/
|
||||||
export function normalizeTimestamp(time: number | string): number {
|
export function normalizeTimestamp(time: number | string): number {
|
||||||
const t = Number(time);
|
return normalizeBangDreamTimestamp(time);
|
||||||
return t < 1e12 ? t * 1000 : t;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -347,19 +251,7 @@ export function normalizeTimestamp(time: number | string): number {
|
|||||||
* @returns 计算后的数值。
|
* @returns 计算后的数值。
|
||||||
*/
|
*/
|
||||||
export function getServerUtcOffset(server: Server): number {
|
export function getServerUtcOffset(server: Server): number {
|
||||||
switch (server) {
|
return getBangDreamServerUtcOffset(server);
|
||||||
case Server.cn:
|
|
||||||
case Server.tw:
|
|
||||||
return 8;
|
|
||||||
|
|
||||||
case Server.jp:
|
|
||||||
case Server.kr:
|
|
||||||
return 9;
|
|
||||||
|
|
||||||
case Server.en:
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -373,7 +265,5 @@ export function getDateByServerTimezone(
|
|||||||
time: number | string,
|
time: number | string,
|
||||||
server: Server,
|
server: Server,
|
||||||
): Date {
|
): Date {
|
||||||
const timestamp = normalizeTimestamp(time);
|
return getBangDreamDateByServerTimezone(time, server);
|
||||||
const offset = getServerUtcOffset(server);
|
|
||||||
return new Date(timestamp + offset * 60 * 60 * 1000);
|
|
||||||
}
|
}
|
||||||
|
|||||||
160
test/qqbot/plugins/bangDream/tsugu/policy.spec.ts
Normal file
160
test/qqbot/plugins/bangDream/tsugu/policy.spec.ts
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
jest.mock('@/qqbot/plugins/bangDream/tsugu/models/main-data-store', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import {
|
||||||
|
calculateCnEventEstimateStartAt,
|
||||||
|
getBangDreamOccupiedDays,
|
||||||
|
type BangDreamEventSchedule,
|
||||||
|
} from '@/qqbot/plugins/bangDream/tsugu/models/cn-event-estimate-policy';
|
||||||
|
import {
|
||||||
|
getCutoffDateByServerTimezone,
|
||||||
|
getCutoffDayIndex,
|
||||||
|
getCutoffEventStatus,
|
||||||
|
getCutoffPredictionWindow,
|
||||||
|
getCutoffTierList,
|
||||||
|
isCutoffDailyCheckpoint,
|
||||||
|
isCutoffTierSupported,
|
||||||
|
selectRecentCutoffEventIds,
|
||||||
|
} from '@/qqbot/plugins/bangDream/tsugu/models/cutoff-policy';
|
||||||
|
import {
|
||||||
|
getBangDreamServerUtcOffset,
|
||||||
|
normalizeBangDreamTimestamp,
|
||||||
|
} from '@/qqbot/plugins/bangDream/tsugu/models/server-policy';
|
||||||
|
import {
|
||||||
|
BangDreamEventStatus,
|
||||||
|
BangDreamServerId as Server,
|
||||||
|
} from '@/qqbot/plugins/bangDream/tsugu/models/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,
|
||||||
|
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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function eventCandidate(
|
||||||
|
eventId: number,
|
||||||
|
eventType: string,
|
||||||
|
jpStartAt: number[],
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
eventId,
|
||||||
|
eventType,
|
||||||
|
startAt: [
|
||||||
|
jpStartAt[0],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
jpStartAt[Server.cn] ?? jpStartAt[0],
|
||||||
|
null,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user