You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
302 lines
11 KiB
302 lines
11 KiB
import { ConstNumType } from '../../game/GameDataEnums';
|
|
import { TableNames } from '../../game/ConfigTableData';
|
|
import { WeightedPick } from '../../tools/WeightedPick';
|
|
import type { DailyShopParsed } from '../DailyShopTypes';
|
|
import { BattleBuffCarryStore } from './BattleBuffCarryStore';
|
|
|
|
/** 品质:1绿 2蓝 3紫 4橙 */
|
|
const QUALITY_COUNT = 4;
|
|
/** 每次三选一选项数 */
|
|
const OPTION_COUNT = 3;
|
|
|
|
/** 单次抽取结果的选项 */
|
|
export interface BattleBuffSelectOption {
|
|
/** Battlebuff.id */
|
|
id: number;
|
|
/** 品质 */
|
|
quality: number;
|
|
/** 表行 */
|
|
row: ITableBattlebuff;
|
|
}
|
|
|
|
/** 玩家本局三选一会话状态 */
|
|
interface BattleBuffSelectSession {
|
|
/** playerId */
|
|
playerId: number;
|
|
/** 本局已弹出次数(含当前进行中) */
|
|
popupOrdinal: number;
|
|
/** 免费刷新剩余 */
|
|
freeLeft: number;
|
|
/** 广告刷新剩余 */
|
|
adLeft: number;
|
|
/** 当前选项 */
|
|
options: BattleBuffSelectOption[];
|
|
}
|
|
|
|
/**
|
|
* 局内 Battlebuff 三选一:品质权重 → 子池权重 → 选项。
|
|
* 每玩家独立会话;开战需 reset。
|
|
*/
|
|
export class BattleBuffSelectService {
|
|
private static readonly _sessions: BattleBuffSelectSession[] = [];
|
|
|
|
/** 开战清空全部会话 */
|
|
static resetAll(): void {
|
|
this._sessions.length = 0;
|
|
}
|
|
|
|
/**
|
|
* 开战初始化某玩家刷新次数。
|
|
* @param playerId 槽位
|
|
*/
|
|
static ensurePlayer(playerId: number): void {
|
|
const pid = Math.max(0, Math.floor(Number(playerId) || 0));
|
|
if (this._find(pid)) return;
|
|
this._sessions.push({
|
|
playerId: pid,
|
|
popupOrdinal: 0,
|
|
freeLeft: Math.max(0, Math.floor(gg.data.table.getConst(ConstNumType.BattleBuffFreeRefreshCount) || 0)),
|
|
adLeft: Math.max(0, Math.floor(gg.data.table.getConst(ConstNumType.BattleBuffAdRefreshCount) || 0)),
|
|
options: [],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 开始一次弹窗(自动刷新权重),生成 3 选项。
|
|
* @param playerId 槽位
|
|
*/
|
|
static beginPopup(playerId: number): BattleBuffSelectOption[] {
|
|
this.ensurePlayer(playerId);
|
|
const session = this._find(Math.max(0, Math.floor(Number(playerId) || 0)));
|
|
if (!session) {
|
|
console.warn('[BattleBuffSelect] beginPopup 无会话', playerId);
|
|
return [];
|
|
}
|
|
session.popupOrdinal += 1;
|
|
session.options = this._rollOptions(session.playerId, session.popupOrdinal, false);
|
|
return session.options.slice();
|
|
}
|
|
|
|
/**
|
|
* 主动刷新:消耗免费或广告次数后重抽(广告权重)。
|
|
* @param playerId 槽位
|
|
* @param useAd 是否按广告次数扣减(false=免费)
|
|
*/
|
|
static refreshOptions(playerId: number, useAd: boolean): BattleBuffSelectOption[] | null {
|
|
const session = this._find(Math.max(0, Math.floor(Number(playerId) || 0)));
|
|
if (!session) {
|
|
console.warn('[BattleBuffSelect] refresh 无会话', playerId);
|
|
return null;
|
|
}
|
|
if (useAd) {
|
|
if (session.adLeft <= 0) {
|
|
console.warn('[BattleBuffSelect] 广告刷新次数已尽', playerId);
|
|
return null;
|
|
}
|
|
session.adLeft -= 1;
|
|
} else {
|
|
if (session.freeLeft <= 0) {
|
|
console.warn('[BattleBuffSelect] 免费刷新次数已尽', playerId);
|
|
return null;
|
|
}
|
|
session.freeLeft -= 1;
|
|
}
|
|
session.options = this._rollOptions(session.playerId, session.popupOrdinal, true);
|
|
return session.options.slice();
|
|
}
|
|
|
|
/**
|
|
* 当前是否应优先免费刷新(免费剩余>0)。
|
|
* @param playerId 槽位
|
|
*/
|
|
static shouldUseFreeRefresh(playerId: number): boolean {
|
|
const session = this._find(Math.max(0, Math.floor(Number(playerId) || 0)));
|
|
return !!session && session.freeLeft > 0;
|
|
}
|
|
|
|
/**
|
|
* 是否还能主动刷新。
|
|
* @param playerId 槽位
|
|
*/
|
|
static canRefresh(playerId: number): boolean {
|
|
const session = this._find(Math.max(0, Math.floor(Number(playerId) || 0)));
|
|
if (!session) return false;
|
|
return session.freeLeft > 0 || session.adLeft > 0;
|
|
}
|
|
|
|
/**
|
|
* 剩余刷新次数合计(展示用)。
|
|
* @param playerId 槽位
|
|
*/
|
|
static remainRefreshTotal(playerId: number): number {
|
|
const session = this._find(Math.max(0, Math.floor(Number(playerId) || 0)));
|
|
if (!session) return 0;
|
|
return session.freeLeft + session.adLeft;
|
|
}
|
|
|
|
/** 当前弹窗选项 */
|
|
static currentOptions(playerId: number): BattleBuffSelectOption[] {
|
|
const session = this._find(Math.max(0, Math.floor(Number(playerId) || 0)));
|
|
return session ? session.options.slice() : [];
|
|
}
|
|
|
|
/**
|
|
* 确认选择某一选项 id。
|
|
* @param playerId 槽位
|
|
* @param buffId 选项 id
|
|
*/
|
|
static confirmPick(playerId: number, buffId: number): DailyShopParsed | null {
|
|
const session = this._find(Math.max(0, Math.floor(Number(playerId) || 0)));
|
|
if (!session) {
|
|
console.warn('[BattleBuffSelect] confirm 无会话', playerId);
|
|
return null;
|
|
}
|
|
let hit = false;
|
|
for (let i = 0; i < session.options.length; i++) {
|
|
if (session.options[i].id === buffId) {
|
|
hit = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!hit) {
|
|
console.warn('[BattleBuffSelect] confirm 非当前选项', playerId, buffId);
|
|
return null;
|
|
}
|
|
const parsed = BattleBuffCarryStore.appendBuffId(playerId, buffId);
|
|
session.options = [];
|
|
return parsed;
|
|
}
|
|
|
|
/**
|
|
* AI:开始弹窗并自动选中第一项(若有)。
|
|
* @param playerId AI playerId
|
|
*/
|
|
static autoPickOne(playerId: number): DailyShopParsed | null {
|
|
const options = this.beginPopup(playerId);
|
|
if (!options.length) {
|
|
console.warn('[BattleBuffSelect] AI 无可选项', playerId);
|
|
return null;
|
|
}
|
|
return this.confirmPick(playerId, options[0].id);
|
|
}
|
|
|
|
/**
|
|
* @deprecated 原 Const27 解锁次数门闸;开挖宝箱每次都弹三选一,勿再调用。
|
|
* @param unlockCount 忽略
|
|
*/
|
|
static isTriggerUnlockCount(unlockCount: number): boolean {
|
|
console.warn('[BattleBuffSelect] isTriggerUnlockCount 已废弃,开挖宝箱不再读 Const27', unlockCount);
|
|
return false;
|
|
}
|
|
|
|
private static _find(playerId: number): BattleBuffSelectSession | null {
|
|
for (let i = 0; i < this._sessions.length; i++) {
|
|
if (this._sessions[i].playerId === playerId) return this._sessions[i];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param playerId 槽位
|
|
* @param popupOrdinal 弹窗序号
|
|
* @param activeRefresh true=主动刷新权重
|
|
*/
|
|
private static _rollOptions(playerId: number, popupOrdinal: number, activeRefresh: boolean): BattleBuffSelectOption[] {
|
|
const qualities = this._rollQualities(popupOrdinal, activeRefresh);
|
|
const pickedIds: number[] = [];
|
|
const out: BattleBuffSelectOption[] = [];
|
|
for (let i = 0; i < qualities.length; i++) {
|
|
const q = qualities[i];
|
|
const row = this._pickRowByQuality(playerId, q, pickedIds);
|
|
if (!row) {
|
|
console.warn('[BattleBuffSelect] 品质池为空', q, 'player', playerId);
|
|
continue;
|
|
}
|
|
pickedIds.push(row.id);
|
|
out.push({ id: row.id, quality: q, row });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
private static _rollQualities(popupOrdinal: number, activeRefresh: boolean): number[] {
|
|
const weights = this._qualityWeights(popupOrdinal, activeRefresh);
|
|
const out: number[] = [];
|
|
for (let i = 0; i < OPTION_COUNT; i++) {
|
|
const entries: { quality: number; weight: number }[] = [];
|
|
for (let q = 1; q <= QUALITY_COUNT; q++) {
|
|
entries.push({ quality: q, weight: weights[q - 1] });
|
|
}
|
|
const hit = WeightedPick.pick(entries);
|
|
if (!hit) {
|
|
console.warn('[BattleBuffSelect] 品质权重全 0');
|
|
out.push(1);
|
|
continue;
|
|
}
|
|
out.push(hit.quality);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
private static _qualityWeights(popupOrdinal: number, activeRefresh: boolean): number[] {
|
|
const baseId = activeRefresh ? ConstNumType.BattleBuffAdWeight : ConstNumType.BattleBuffFreeWeight;
|
|
const purpleId = activeRefresh ? ConstNumType.BattleBuffAdPurpleBonus : ConstNumType.BattleBuffFreePurpleBonus;
|
|
const orangeId = activeRefresh ? ConstNumType.BattleBuffAdOrangeBonus : ConstNumType.BattleBuffFreeOrangeBonus;
|
|
const base = gg.data.table.getConstArray(baseId);
|
|
const weights = [0, 0, 0, 0];
|
|
for (let i = 0; i < QUALITY_COUNT; i++) {
|
|
weights[i] = Math.max(0, Math.floor(Number(base[i]) || 0));
|
|
}
|
|
if (base.length < QUALITY_COUNT) {
|
|
console.warn('[BattleBuffSelect] 品质基础权重不足 4 项', baseId, base);
|
|
}
|
|
this._applyQualityBonus(weights, 2, purpleId, popupOrdinal);
|
|
this._applyQualityBonus(weights, 3, orangeId, popupOrdinal);
|
|
return weights;
|
|
}
|
|
|
|
/**
|
|
* @param weights 绿蓝紫橙
|
|
* @param index 0~3
|
|
* @param bonusConstId 次数,加值
|
|
* @param popupOrdinal 弹窗序号
|
|
*/
|
|
private static _applyQualityBonus(weights: number[], index: number, bonusConstId: number, popupOrdinal: number): void {
|
|
const arr = gg.data.table.getConstArray(bonusConstId);
|
|
if (arr.length < 2) {
|
|
console.warn('[BattleBuffSelect] 品质加值配置非法', bonusConstId, arr);
|
|
return;
|
|
}
|
|
const need = Math.floor(arr[0]);
|
|
const add = Math.floor(arr[1]);
|
|
if (need <= 0) return;
|
|
if (popupOrdinal >= need) {
|
|
weights[index] = Math.max(0, weights[index] + add);
|
|
}
|
|
}
|
|
|
|
private static _pickRowByQuality(playerId: number, quality: number, excludeIds: readonly number[]): ITableBattlebuff | null {
|
|
const list = gg.data.table.getTableList<ITableBattlebuff>(TableNames.Battlebuff) ?? [];
|
|
const pool: ITableBattlebuff[] = [];
|
|
for (let i = 0; i < list.length; i++) {
|
|
const row = list[i];
|
|
if (Math.floor(Number(row.quality) || 0) !== quality) continue;
|
|
const id = Math.floor(Number(row.id) || 0);
|
|
if (id <= 0) continue;
|
|
let excluded = false;
|
|
for (let e = 0; e < excludeIds.length; e++) {
|
|
if (excludeIds[e] === id) {
|
|
excluded = true;
|
|
break;
|
|
}
|
|
}
|
|
if (excluded) continue;
|
|
const limit = Math.floor(Number(row.limit) || 0);
|
|
if (limit > 0 && BattleBuffCarryStore.getObtainCount(playerId, id) >= limit) continue;
|
|
const w = Number(row.weight) || 0;
|
|
if (w <= 0) continue;
|
|
pool.push(row);
|
|
}
|
|
if (!pool.length) return null;
|
|
return WeightedPick.pick(pool);
|
|
}
|
|
}
|
|
|