|
|
|
|
import { isBagToolTextType, isElementGlyphType } from 'db://assets/mx/module/merge';
|
|
|
|
|
import { TableNames } from '../../game/ConfigTableData';
|
|
|
|
|
import { RankDataManager } from '../../manager/RankDataManager';
|
|
|
|
|
import { isAttackGlyph } from '../config/BattleHeroTable';
|
|
|
|
|
import { logBagRefreshDebug, logBagRefreshSection } from '../debug/BagRefreshDebugLog';
|
|
|
|
|
import type { BagRollEntry } from './BagWeaponPool';
|
|
|
|
|
|
|
|
|
|
/** Text.type 可入池:1–5 五行 + 10–13 功能符(tolockland / glyphsOnly 仍只抽五行) */
|
|
|
|
|
const POOL_TYPES = [1, 2, 3, 4, 5, 10, 11, 12, 13];
|
|
|
|
|
/** number 档限量规则已废弃,剩余量统一用该值表示不限量(需兼容 |0 截断,取安全量级) */
|
|
|
|
|
const UNLIMITED_REMAINING = 999999;
|
|
|
|
|
/** 局内字池建池参数(召神令档位规则已随 Text.numberN 删除,保留形参兼容调用方) */
|
|
|
|
|
export type PlayerBagTextPoolBeginOptions = {
|
|
|
|
|
/** @deprecated 已忽略 */
|
|
|
|
|
heroSummonRateActive?: boolean;
|
|
|
|
|
/** @deprecated 已忽略 */
|
|
|
|
|
heroSummonRateOffset?: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 局内字池快照 */
|
|
|
|
|
export interface PlayerBagTextPoolState {
|
|
|
|
|
/** @deprecated 档位已删,恒 0 */
|
|
|
|
|
numberN: number;
|
|
|
|
|
/** @deprecated 恒 0 */
|
|
|
|
|
baseNumberN: number;
|
|
|
|
|
/** @deprecated 恒 0 */
|
|
|
|
|
heroSummonOffset: number;
|
|
|
|
|
/** 候选 Text id */
|
|
|
|
|
textIds: number[];
|
|
|
|
|
/** 与 textIds 对齐的 Text.type */
|
|
|
|
|
types: number[];
|
|
|
|
|
/** 与 textIds 对齐的输出 weapon2Id(Text.id;旧快照缺失时用 textIds) */
|
|
|
|
|
heroIds?: number[];
|
|
|
|
|
/** 与 textIds 对齐的显示等级(Text.level + 段位加值;旧快照缺失时按 Text.level) */
|
|
|
|
|
levels?: number[];
|
|
|
|
|
/** 基础权重 */
|
|
|
|
|
weights: number[];
|
|
|
|
|
/** 剩余数量 */
|
|
|
|
|
remaining: number[];
|
|
|
|
|
/** 是否已建池 */
|
|
|
|
|
ready: boolean;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* 玩家局内背包字池。
|
|
|
|
|
* 按 Text.weight 加权随机,POOL_TYPES 内 weight>0 的行全部入池、不限量。
|
|
|
|
|
*/
|
|
|
|
|
export class PlayerBagTextPool {
|
|
|
|
|
private _textIds: number[] = [];
|
|
|
|
|
private _types: number[] = [];
|
|
|
|
|
private _heroIds: number[] = [];
|
|
|
|
|
private _levels: number[] = [];
|
|
|
|
|
private _weights: number[] = [];
|
|
|
|
|
private _remaining: number[] = [];
|
|
|
|
|
private _ready = false;
|
|
|
|
|
|
|
|
|
|
/** 是否已开局建池 */
|
|
|
|
|
get ready(): boolean {
|
|
|
|
|
return this._ready;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @deprecated 档位已删,恒 0 */
|
|
|
|
|
get numberN(): number {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 开战建池:Text.type∈五行或功能符、weight>0;五行按段位 levelvalue 映射 Hero,功能符直接用 Text.id。
|
|
|
|
|
* @param rankId 玩家当前段位 id
|
|
|
|
|
* @param _options 兼容旧召神令参数(已忽略)
|
|
|
|
|
*/
|
|
|
|
|
beginBattle(rankId?: number, _options?: PlayerBagTextPoolBeginOptions): void {
|
|
|
|
|
const rid = rankId ?? RankDataManager.getPlayerRankId();
|
|
|
|
|
const rank = RankDataManager.getRankConfig(rid);
|
|
|
|
|
if (!rank) {
|
|
|
|
|
console.log(`[PlayerBagTextPool] Rank 缺失 rankId=${rid}`);
|
|
|
|
|
this._ready = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const list = gg.data.table.getTableList<ITableText>(TableNames.Text) ?? [];
|
|
|
|
|
this._textIds = [];
|
|
|
|
|
this._types = [];
|
|
|
|
|
this._heroIds = [];
|
|
|
|
|
this._levels = [];
|
|
|
|
|
this._weights = [];
|
|
|
|
|
this._remaining = [];
|
|
|
|
|
|
|
|
|
|
// 段位等级加值:Text.level + levelvalue = 显示等级;须有 Hero(herotype=type, level) 行才有编号可显示
|
|
|
|
|
const levelValue = Math.max(0, Math.floor(Number(rank.levelvalue) || 0));
|
|
|
|
|
const heroes = gg.data.table.getTableList<ITableHero>(TableNames.Hero) ?? [];
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
|
|
|
const row = list[i];
|
|
|
|
|
const type = Number(row.type);
|
|
|
|
|
if (POOL_TYPES.indexOf(type) < 0) continue;
|
|
|
|
|
const weight = Number(row.weight);
|
|
|
|
|
if (!(weight > 0)) continue;
|
|
|
|
|
const textId = Number(row.id);
|
|
|
|
|
if (isBagToolTextType(type)) {
|
|
|
|
|
this._textIds.push(textId);
|
|
|
|
|
this._types.push(type);
|
|
|
|
|
this._heroIds.push(textId);
|
|
|
|
|
this._levels.push(0);
|
|
|
|
|
this._weights.push(weight);
|
|
|
|
|
this._remaining.push(UNLIMITED_REMAINING);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const level = Math.floor(Number(row.level)) + levelValue;
|
|
|
|
|
const hero = this._heroAt(heroes, type, level);
|
|
|
|
|
if (!hero) continue;
|
|
|
|
|
this._textIds.push(textId);
|
|
|
|
|
this._types.push(type);
|
|
|
|
|
this._heroIds.push(hero.id);
|
|
|
|
|
this._levels.push(level);
|
|
|
|
|
this._weights.push(weight);
|
|
|
|
|
this._remaining.push(UNLIMITED_REMAINING);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this._textIds.length === 0) {
|
|
|
|
|
console.log(`[PlayerBagTextPool] 候选为空 rankId=${rid} levelValue=${levelValue}`);
|
|
|
|
|
this._ready = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._ready = true;
|
|
|
|
|
logBagRefreshSection(`字池建池 rankId=${rid} 候选=${this._textIds.length} levelValue=${levelValue}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 按 herotype=Text.type、level=Text.level+段位加值 查 Hero 行 */
|
|
|
|
|
private _heroAt(heroes: readonly ITableHero[], herotype: number, level: number): ITableHero | null {
|
|
|
|
|
for (let i = 0; i < heroes.length; i++) {
|
|
|
|
|
const h = heroes[i];
|
|
|
|
|
if (Math.floor(Number(h.herotype)) !== herotype) continue;
|
|
|
|
|
if (Math.floor(Number(h.level)) !== level) continue;
|
|
|
|
|
return h;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 导出局内池状态(快照) */
|
|
|
|
|
exportState(): PlayerBagTextPoolState {
|
|
|
|
|
return {
|
|
|
|
|
numberN: 0,
|
|
|
|
|
baseNumberN: 0,
|
|
|
|
|
heroSummonOffset: 0,
|
|
|
|
|
textIds: this._textIds.slice(),
|
|
|
|
|
types: this._types.slice(),
|
|
|
|
|
heroIds: this._heroIds.slice(),
|
|
|
|
|
levels: this._levels.slice(),
|
|
|
|
|
weights: this._weights.slice(),
|
|
|
|
|
remaining: this._remaining.slice(),
|
|
|
|
|
ready: this._ready,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 从快照恢复局内池 */
|
|
|
|
|
importState(state: PlayerBagTextPoolState): void {
|
|
|
|
|
this._textIds = state.textIds.slice();
|
|
|
|
|
this._types = state.types?.length === state.textIds.length
|
|
|
|
|
? state.types.slice()
|
|
|
|
|
: this._typesFromTextIds(state.textIds);
|
|
|
|
|
this._heroIds = state.heroIds?.length === state.textIds.length
|
|
|
|
|
? state.heroIds.slice()
|
|
|
|
|
: state.textIds.slice();
|
|
|
|
|
this._levels = state.levels?.length === state.textIds.length
|
|
|
|
|
? state.levels.slice()
|
|
|
|
|
: this._levelsFromTextIds(state.textIds);
|
|
|
|
|
this._weights = state.weights.slice();
|
|
|
|
|
this._remaining = state.remaining.slice();
|
|
|
|
|
this._ready = state.ready;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 兜底:按 Text 表行补 levels */
|
|
|
|
|
private _levelsFromTextIds(textIds: readonly number[]): number[] {
|
|
|
|
|
const out: number[] = [];
|
|
|
|
|
for (let i = 0; i < textIds.length; i++) {
|
|
|
|
|
const row = gg.data.table.getTableData<ITableText>(TableNames.Text, textIds[i]);
|
|
|
|
|
out.push(row ? Math.floor(Number(row.level)) : 1);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 文昌笔:按召唤字池加权抽取 1 个五行字(扣减 remaining)。
|
|
|
|
|
* @param excludeWeapon2Id 尽量排除该字;池内无其它候选时返回 null
|
|
|
|
|
*/
|
|
|
|
|
rollSingleGlyph(excludeWeapon2Id?: number): BagRollEntry | null {
|
|
|
|
|
if (!this._ready) {
|
|
|
|
|
console.log('[PlayerBagTextPool] 未建池不可抽取');
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const idx = this._pickGlyphIndexExcluding(excludeWeapon2Id);
|
|
|
|
|
if (idx == null) return null;
|
|
|
|
|
this._remaining[idx] -= 1;
|
|
|
|
|
const entry: BagRollEntry = { weapon2Id: this._textIds[idx], level: this._levels[idx] };
|
|
|
|
|
logBagRefreshDebug('字池单抽', String(entry.weapon2Id));
|
|
|
|
|
return entry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 是否还能抽到与 exclude 不同的五行字(不扣减 remaining) */
|
|
|
|
|
canRollSingleGlyph(excludeWeapon2Id?: number): boolean {
|
|
|
|
|
if (!this._ready) return false;
|
|
|
|
|
return this._pickGlyphIndexExcluding(excludeWeapon2Id) != null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 属性卡/随机牌:保留显示等级,随机换成其它五行 Text.id(不扣减字池)。
|
|
|
|
|
* 不按字池 Text.level 过滤——高等级由合成抬升,只要求目标属性在该显示等级有 Hero。
|
|
|
|
|
* @param currentWeapon2Id 当前字
|
|
|
|
|
* @param level 显示等级
|
|
|
|
|
*/
|
|
|
|
|
pickAttrSwap(currentWeapon2Id: number, level: number): BagRollEntry | null {
|
|
|
|
|
const curId = Math.floor(Number(currentWeapon2Id) || 0);
|
|
|
|
|
const lv = Math.floor(Number(level) || 0);
|
|
|
|
|
if (curId <= 0 || lv <= 0) return null;
|
|
|
|
|
const curType = this._resolveElementType(curId);
|
|
|
|
|
if (curType < 1 || curType > 5) {
|
|
|
|
|
console.log('[PlayerBagTextPool] 属性卡无法解析当前 type', curId);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const heroes = gg.data.table.getTableList<ITableHero>(TableNames.Hero) ?? [];
|
|
|
|
|
const candidates: number[] = [];
|
|
|
|
|
for (let type = 1; type <= 5; type++) {
|
|
|
|
|
if (type === curType) continue;
|
|
|
|
|
if (!this._heroAt(heroes, type, lv)) continue;
|
|
|
|
|
const textId = this._anyElementTextId(type);
|
|
|
|
|
if (!(textId > 0)) continue;
|
|
|
|
|
candidates.push(textId);
|
|
|
|
|
}
|
|
|
|
|
if (candidates.length <= 0) {
|
|
|
|
|
console.log('[PlayerBagTextPool] 属性卡无同级异属性候选', lv, curType);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const pick = candidates[Math.floor(Math.random() * candidates.length)];
|
|
|
|
|
return { weapon2Id: pick, level: lv };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 解析五行 Text.type(池内命中或查 Text 表)。
|
|
|
|
|
* @param weapon2Id Text.id / 旧池 Hero.id
|
|
|
|
|
*/
|
|
|
|
|
private _resolveElementType(weapon2Id: number): number {
|
|
|
|
|
const id = Math.floor(Number(weapon2Id) || 0);
|
|
|
|
|
if (!(id > 0)) return 0;
|
|
|
|
|
for (let i = 0; i < this._textIds.length; i++) {
|
|
|
|
|
if (this._textIds[i] !== id && this._heroIds[i] !== id) continue;
|
|
|
|
|
const t = this._types[i];
|
|
|
|
|
if (t >= 1 && t <= 5) return t;
|
|
|
|
|
}
|
|
|
|
|
const list = gg.data.table.getTableList<ITableText>(TableNames.Text) ?? [];
|
|
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
|
|
|
if (Number(list[i].id) !== id) continue;
|
|
|
|
|
return Math.floor(Number(list[i].type) || 0);
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 取任一该五行的 Text.id(池优先,否则 Text 表)。
|
|
|
|
|
* @param type Text.type 1–5
|
|
|
|
|
*/
|
|
|
|
|
private _anyElementTextId(type: number): number {
|
|
|
|
|
const t = Math.floor(Number(type) || 0);
|
|
|
|
|
if (t < 1 || t > 5) return 0;
|
|
|
|
|
for (let i = 0; i < this._textIds.length; i++) {
|
|
|
|
|
if (this._types[i] !== t) continue;
|
|
|
|
|
const id = this._textIds[i];
|
|
|
|
|
if (id > 0) return id;
|
|
|
|
|
}
|
|
|
|
|
const list = gg.data.table.getTableList<ITableText>(TableNames.Text) ?? [];
|
|
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
|
|
|
if (Math.floor(Number(list[i].type) || 0) !== t) continue;
|
|
|
|
|
const id = Math.floor(Number(list[i].id) || 0);
|
|
|
|
|
if (id > 0) return id;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 按 id 扣减 1 次 remaining(GM/预设刷新等非 rollSlots 路径)。
|
|
|
|
|
* @param weapon2Id Text.id 或池输出的 Hero.id
|
|
|
|
|
* @returns 是否命中字池并成功扣减
|
|
|
|
|
*/
|
|
|
|
|
tryConsumeWeapon2Id(weapon2Id: number): boolean {
|
|
|
|
|
if (!this._ready) return false;
|
|
|
|
|
const id = Math.floor(Number(weapon2Id) || 0);
|
|
|
|
|
if (id <= 0) return false;
|
|
|
|
|
for (let i = 0; i < this._textIds.length; i++) {
|
|
|
|
|
if (this._textIds[i] !== id && this._heroIds[i] !== id) continue;
|
|
|
|
|
if (this._remaining[i] <= 0) return false;
|
|
|
|
|
this._remaining[i] -= 1;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 按 id 归还 1 次 remaining(保底替换退还原抽中字)。
|
|
|
|
|
* @param weapon2Id Text.id 或池输出的 Hero.id
|
|
|
|
|
* @returns 是否命中字池并成功归还
|
|
|
|
|
*/
|
|
|
|
|
tryRefundWeapon2Id(weapon2Id: number): boolean {
|
|
|
|
|
if (!this._ready) return false;
|
|
|
|
|
const id = Math.floor(Number(weapon2Id) || 0);
|
|
|
|
|
if (id <= 0) return false;
|
|
|
|
|
for (let i = 0; i < this._textIds.length; i++) {
|
|
|
|
|
if (this._textIds[i] !== id && this._heroIds[i] !== id) continue;
|
|
|
|
|
this._remaining[i] += 1;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 打乱抽取结果(首次刷新保底攻击字后打乱槽位)。
|
|
|
|
|
* @param list 待打乱条目
|
|
|
|
|
*/
|
|
|
|
|
shuffleEntries(list: BagRollEntry[]): void {
|
|
|
|
|
this._shuffleEntries(list);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 是否还能抽满 count 个字(不扣减) */
|
|
|
|
|
canRollSlots(count: number): boolean {
|
|
|
|
|
if (!this._ready || count <= 0) return false;
|
|
|
|
|
let avail = 0;
|
|
|
|
|
for (let i = 0; i < this._remaining.length; i++) {
|
|
|
|
|
avail += Math.max(0, this._remaining[i] | 0);
|
|
|
|
|
if (avail >= count) return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 加权抽取 count 个字并扣减剩余量。
|
|
|
|
|
* @param count 槽数
|
|
|
|
|
* @param ensureAttacker 为 true 时至少抽 1 个可攻击单字
|
|
|
|
|
* @param shuffle 首次攻击字保底后是否打乱;连败保底需按抽取顺序处理时传 false
|
|
|
|
|
* @returns 成功条目;候选不足时不扣减并返回 null
|
|
|
|
|
*/
|
|
|
|
|
rollSlots(count: number, ensureAttacker = false, shuffle = true): BagRollEntry[] | null {
|
|
|
|
|
if (!this._ready) {
|
|
|
|
|
console.log('[PlayerBagTextPool] 未建池不可抽取');
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const pickedIdx: number[] = [];
|
|
|
|
|
const snapshot = this._remaining.slice();
|
|
|
|
|
if (ensureAttacker) {
|
|
|
|
|
const atk = this._pickAttackerIndex();
|
|
|
|
|
if (atk == null) {
|
|
|
|
|
console.log('[PlayerBagTextPool] 字池无攻击字,首次刷新无法保底');
|
|
|
|
|
} else {
|
|
|
|
|
this._remaining[atk] -= 1;
|
|
|
|
|
pickedIdx.push(atk);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
while (pickedIdx.length < count) {
|
|
|
|
|
const idx = this._pickIndex();
|
|
|
|
|
if (idx == null) {
|
|
|
|
|
this._remaining = snapshot;
|
|
|
|
|
console.log(`[PlayerBagTextPool] 候选耗尽,需要 ${count} 实际不可抽满`);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
this._remaining[idx] -= 1;
|
|
|
|
|
pickedIdx.push(idx);
|
|
|
|
|
}
|
|
|
|
|
const result: BagRollEntry[] = [];
|
|
|
|
|
for (let i = 0; i < pickedIdx.length; i++) {
|
|
|
|
|
result.push({ weapon2Id: this._textIds[pickedIdx[i]], level: this._levels[pickedIdx[i]] });
|
|
|
|
|
}
|
|
|
|
|
if (shuffle && ensureAttacker) this._shuffleEntries(result);
|
|
|
|
|
logBagRefreshDebug('字池抽取', result.map((e) => e.weapon2Id).join(','));
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 按当前 remaining 加权选下标;无候选返回 null(不改状态) */
|
|
|
|
|
private _pickIndex(): number | null {
|
|
|
|
|
return this._pickIndexExcluding(undefined);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 开格随机:normalland 用全池,tolockland 只收 Text.type 1-5。
|
|
|
|
|
* @param gate true 为 tolockland 候选
|
|
|
|
|
* @returns weapon2Id(Text.id)与显示等级;无候选返回 null
|
|
|
|
|
*/
|
|
|
|
|
rollOneForLand(gate: boolean): BagRollEntry | null {
|
|
|
|
|
if (!this._ready) return null;
|
|
|
|
|
const idx = this._pickIndexFiltered(undefined, false, gate ? 5 : 0);
|
|
|
|
|
if (idx == null) return null;
|
|
|
|
|
this._remaining[idx] -= 1;
|
|
|
|
|
return { weapon2Id: this._textIds[idx], level: this._levels[idx] };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 只从可攻击单字里加权选 */
|
|
|
|
|
private _pickAttackerIndex(): number | null {
|
|
|
|
|
let total = 0;
|
|
|
|
|
for (let i = 0; i < this._textIds.length; i++) {
|
|
|
|
|
const rem = this._remaining[i];
|
|
|
|
|
if (rem <= 0 || !isAttackGlyph(this._textIds[i])) continue;
|
|
|
|
|
total += this._weights[i] * rem;
|
|
|
|
|
}
|
|
|
|
|
if (!(total > 0)) return null;
|
|
|
|
|
let r = Math.random() * total;
|
|
|
|
|
for (let i = 0; i < this._textIds.length; i++) {
|
|
|
|
|
const rem = this._remaining[i];
|
|
|
|
|
if (rem <= 0 || !isAttackGlyph(this._textIds[i])) continue;
|
|
|
|
|
r -= this._weights[i] * rem;
|
|
|
|
|
if (r < 0) return i;
|
|
|
|
|
}
|
|
|
|
|
for (let i = this._textIds.length - 1; i >= 0; i--) {
|
|
|
|
|
if (this._remaining[i] <= 0 || !isAttackGlyph(this._textIds[i])) continue;
|
|
|
|
|
return i;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 打乱抽取结果 */
|
|
|
|
|
private _shuffleEntries(list: BagRollEntry[]): void {
|
|
|
|
|
for (let i = list.length - 1; i > 0; i--) {
|
|
|
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
|
|
|
const tmp = list[i];
|
|
|
|
|
list[i] = list[j];
|
|
|
|
|
list[j] = tmp;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 加权选下标,可排除指定 weapon2Id */
|
|
|
|
|
private _pickIndexExcluding(excludeWeapon2Id?: number): number | null {
|
|
|
|
|
return this._pickIndexFiltered(excludeWeapon2Id, false, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 文昌笔:仅五行字加权选下标 */
|
|
|
|
|
private _pickGlyphIndexExcluding(excludeWeapon2Id?: number): number | null {
|
|
|
|
|
return this._pickIndexFiltered(excludeWeapon2Id, true, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 加权选下标。
|
|
|
|
|
* @param excludeWeapon2Id 排除 id(匹配 Text.id 或输出 Hero.id)
|
|
|
|
|
* @param glyphsOnly true 时跳过非改字类型
|
|
|
|
|
* @param maxType Text.type 上限;0 不限(tolockland 候选传 5)
|
|
|
|
|
*/
|
|
|
|
|
private _pickIndexFiltered(excludeWeapon2Id: number | undefined, glyphsOnly: boolean, maxType: number): number | null {
|
|
|
|
|
let total = 0;
|
|
|
|
|
for (let i = 0; i < this._textIds.length; i++) {
|
|
|
|
|
if (!this._isPickable(i, excludeWeapon2Id, glyphsOnly, maxType)) continue;
|
|
|
|
|
total += this._weights[i] * this._remaining[i];
|
|
|
|
|
}
|
|
|
|
|
if (!(total > 0)) return null;
|
|
|
|
|
|
|
|
|
|
let r = Math.random() * total;
|
|
|
|
|
for (let i = 0; i < this._textIds.length; i++) {
|
|
|
|
|
if (!this._isPickable(i, excludeWeapon2Id, glyphsOnly, maxType)) continue;
|
|
|
|
|
r -= this._weights[i] * this._remaining[i];
|
|
|
|
|
if (r < 0) return i;
|
|
|
|
|
}
|
|
|
|
|
for (let i = this._textIds.length - 1; i >= 0; i--) {
|
|
|
|
|
if (!this._isPickable(i, excludeWeapon2Id, glyphsOnly, maxType)) continue;
|
|
|
|
|
return i;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 候选是否可参与本次加权抽取 */
|
|
|
|
|
private _isPickable(index: number, excludeWeapon2Id: number | undefined, glyphsOnly: boolean, maxType: number): boolean {
|
|
|
|
|
if (this._remaining[index] <= 0) return false;
|
|
|
|
|
if (excludeWeapon2Id != null && this._textIds[index] === excludeWeapon2Id) return false;
|
|
|
|
|
if (excludeWeapon2Id != null && this._heroIds[index] === excludeWeapon2Id) return false;
|
|
|
|
|
if (glyphsOnly && !isElementGlyphType(this._types[index])) return false;
|
|
|
|
|
if (maxType > 0 && this._types[index] > maxType) return false;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 旧快照无 types 时按 Text 表回填 */
|
|
|
|
|
private _typesFromTextIds(textIds: readonly number[]): number[] {
|
|
|
|
|
const out: number[] = [];
|
|
|
|
|
for (let i = 0; i < textIds.length; i++) {
|
|
|
|
|
const row = gg.data.table.getTableData<ITableText>(TableNames.Text, textIds[i]);
|
|
|
|
|
out.push(row ? Number(row.type) : 0);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
}
|