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.
AdLike2048/assets/script/battle/bag/PlayerBagTextPool.ts

483 lines
19 KiB

40 minutes ago
import { isBagToolTextType, isElementGlyphType, isShovelTextType } from 'db://assets/mx/module/merge';
3 weeks ago
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';
40 minutes ago
/** Text.type 可入池:1–5 五行 + 10–13 功能符 + 14 铲子(tolockland / glyphsOnly 仍只抽五行) */
const POOL_TYPES = [1, 2, 3, 4, 5, 10, 11, 12, 13, 14];
2 weeks ago
/** number 档限量规则已废弃,剩余量统一用该值表示不限量(需兼容 |0 截断,取安全量级) */
const UNLIMITED_REMAINING = 999999;
/** 局内字池建池参数(召神令档位规则已随 Text.numberN 删除,保留形参兼容调用方) */
3 weeks ago
export type PlayerBagTextPoolBeginOptions = {
/** @deprecated 已忽略 */
3 weeks ago
heroSummonRateActive?: boolean;
/** @deprecated 已忽略 */
3 weeks ago
heroSummonRateOffset?: number;
};
/** 局内字池快照 */
export interface PlayerBagTextPoolState {
/** @deprecated 档位已删,恒 0 */
3 weeks ago
numberN: number;
/** @deprecated 恒 0 */
3 weeks ago
baseNumberN: number;
/** @deprecated 恒 0 */
3 weeks ago
heroSummonOffset: number;
/** 候选 Text id */
textIds: number[];
/** 与 textIds 对齐的 Text.type */
types: number[];
2 weeks ago
/** 与 textIds 对齐的输出 weapon2Id(Text.id;旧快照缺失时用 textIds) */
heroIds?: number[];
/** 与 textIds 对齐的显示等级(Text.level + 段位加值;旧快照缺失时按 Text.level) */
levels?: number[];
3 weeks ago
/** 基础权重 */
weights: number[];
/** 剩余数量 */
remaining: number[];
/** 是否已建池 */
ready: boolean;
}
/**
* 玩家局内背包字池。
* 按 Text.weight 加权随机,POOL_TYPES 内 weight>0 的行全部入池、不限量。
3 weeks ago
*/
export class PlayerBagTextPool {
private _textIds: number[] = [];
private _types: number[] = [];
2 weeks ago
private _heroIds: number[] = [];
private _levels: number[] = [];
3 weeks ago
private _weights: number[] = [];
private _remaining: number[] = [];
private _ready = false;
/** 是否已开局建池 */
get ready(): boolean {
return this._ready;
}
/** @deprecated 档位已删,恒 0 */
3 weeks ago
get numberN(): number {
return 0;
3 weeks ago
}
/**
* 开战建池:Text.type∈五行或功能符、weight>0;五行按段位 levelvalue 映射 Hero,功能符直接用 Text.id。
3 weeks ago
* @param rankId 玩家当前段位 id
* @param _options 兼容旧召神令参数(已忽略)
3 weeks ago
*/
beginBattle(rankId?: number, _options?: PlayerBagTextPoolBeginOptions): void {
3 weeks ago
const rid = rankId ?? RankDataManager.getPlayerRankId();
const rank = RankDataManager.getRankConfig(rid);
if (!rank) {
console.log(`[PlayerBagTextPool] Rank 缺失 rankId=${rid}`);
this._ready = false;
return;
}
3 weeks ago
const list = gg.data.table.getTableList<ITableText>(TableNames.Text) ?? [];
this._textIds = [];
this._types = [];
2 weeks ago
this._heroIds = [];
this._levels = [];
3 weeks ago
this._weights = [];
this._remaining = [];
2 weeks ago
// 段位等级加值:Text.level + levelvalue = 显示等级;须有 Hero(herotype=type, level) 行才有编号可显示
const levelValue = Math.max(0, Math.floor(Number(rank.levelvalue) || 0));
2 weeks ago
const heroes = gg.data.table.getTableList<ITableHero>(TableNames.Hero) ?? [];
3 weeks ago
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);
40 minutes ago
if (isBagToolTextType(type) || isShovelTextType(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;
}
2 weeks ago
const level = Math.floor(Number(row.level)) + levelValue;
const hero = this._heroAt(heroes, type, level);
if (!hero) continue;
this._textIds.push(textId);
3 weeks ago
this._types.push(type);
2 weeks ago
this._heroIds.push(hero.id);
this._levels.push(level);
3 weeks ago
this._weights.push(weight);
2 weeks ago
this._remaining.push(UNLIMITED_REMAINING);
3 weeks ago
}
if (this._textIds.length === 0) {
console.log(`[PlayerBagTextPool] 候选为空 rankId=${rid} levelValue=${levelValue}`);
3 weeks ago
this._ready = false;
return;
}
this._ready = true;
logBagRefreshSection(`字池建池 rankId=${rid} 候选=${this._textIds.length} levelValue=${levelValue}`);
2 weeks ago
}
/** 按 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;
3 weeks ago
}
/** 导出局内池状态(快照) */
exportState(): PlayerBagTextPoolState {
return {
numberN: 0,
baseNumberN: 0,
heroSummonOffset: 0,
3 weeks ago
textIds: this._textIds.slice(),
types: this._types.slice(),
2 weeks ago
heroIds: this._heroIds.slice(),
levels: this._levels.slice(),
3 weeks ago
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);
2 weeks ago
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);
3 weeks ago
this._weights = state.weights.slice();
this._remaining = state.remaining.slice();
this._ready = state.ready;
}
2 weeks ago
/** 兜底:按 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;
}
3 weeks ago
/**
2 weeks ago
* 文昌笔:按召唤字池加权抽取 1 个五行字(扣减 remaining)。
3 weeks ago
* @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;
2 weeks ago
const entry: BagRollEntry = { weapon2Id: this._textIds[idx], level: this._levels[idx] };
3 weeks ago
logBagRefreshDebug('字池单抽', String(entry.weapon2Id));
return entry;
}
2 weeks ago
/** 是否还能抽到与 exclude 不同的五行字(不扣减 remaining) */
3 weeks ago
canRollSingleGlyph(excludeWeapon2Id?: number): boolean {
if (!this._ready) return false;
return this._pickGlyphIndexExcluding(excludeWeapon2Id) != null;
}
/**
20 hours ago
* 属性卡/随机牌:保留显示等级,随机换成其它五行 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;
20 hours ago
const curType = this._resolveElementType(curId);
if (curType < 1 || curType > 5) {
console.log('[PlayerBagTextPool] 属性卡无法解析当前 type', curId);
return null;
}
20 hours ago
const heroes = gg.data.table.getTableList<ITableHero>(TableNames.Hero) ?? [];
const candidates: number[] = [];
20 hours ago
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 };
}
20 hours ago
/**
* 解析五行 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;
}
3 weeks ago
/**
2 weeks ago
* 按 id 扣减 1 次 remaining(GM/预设刷新等非 rollSlots 路径)。
* @param weapon2Id Text.id 或池输出的 Hero.id
3 weeks ago
* @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++) {
2 weeks ago
if (this._textIds[i] !== id && this._heroIds[i] !== id) continue;
3 weeks ago
if (this._remaining[i] <= 0) return false;
this._remaining[i] -= 1;
return true;
}
return false;
}
/**
2 weeks ago
* 按 id 归还 1 次 remaining(保底替换退还原抽中字)。
* @param weapon2Id Text.id 或池输出的 Hero.id
3 weeks ago
* @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++) {
2 weeks ago
if (this._textIds[i] !== id && this._heroIds[i] !== id) continue;
3 weeks ago
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++) {
2 weeks ago
result.push({ weapon2Id: this._textIds[pickedIdx[i]], level: this._levels[pickedIdx[i]] });
3 weeks ago
}
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);
}
2 weeks ago
/**
* 开格随机: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] };
}
3 weeks ago
/** 只从可攻击单字里加权选 */
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 {
2 weeks ago
return this._pickIndexFiltered(excludeWeapon2Id, false, 0);
3 weeks ago
}
2 weeks ago
/** 文昌笔:仅五行字加权选下标 */
3 weeks ago
private _pickGlyphIndexExcluding(excludeWeapon2Id?: number): number | null {
2 weeks ago
return this._pickIndexFiltered(excludeWeapon2Id, true, 0);
3 weeks ago
}
/**
* 加权选下标。
2 weeks ago
* @param excludeWeapon2Id 排除 id(匹配 Text.id 或输出 Hero.id)
2 weeks ago
* @param glyphsOnly true 时跳过非改字类型
2 weeks ago
* @param maxType Text.type 上限;0 不限(tolockland 候选传 5)
3 weeks ago
*/
2 weeks ago
private _pickIndexFiltered(excludeWeapon2Id: number | undefined, glyphsOnly: boolean, maxType: number): number | null {
3 weeks ago
let total = 0;
for (let i = 0; i < this._textIds.length; i++) {
2 weeks ago
if (!this._isPickable(i, excludeWeapon2Id, glyphsOnly, maxType)) continue;
3 weeks ago
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++) {
2 weeks ago
if (!this._isPickable(i, excludeWeapon2Id, glyphsOnly, maxType)) continue;
3 weeks ago
r -= this._weights[i] * this._remaining[i];
if (r < 0) return i;
}
for (let i = this._textIds.length - 1; i >= 0; i--) {
2 weeks ago
if (!this._isPickable(i, excludeWeapon2Id, glyphsOnly, maxType)) continue;
3 weeks ago
return i;
}
return null;
}
/** 候选是否可参与本次加权抽取 */
2 weeks ago
private _isPickable(index: number, excludeWeapon2Id: number | undefined, glyphsOnly: boolean, maxType: number): boolean {
3 weeks ago
if (this._remaining[index] <= 0) return false;
if (excludeWeapon2Id != null && this._textIds[index] === excludeWeapon2Id) return false;
2 weeks ago
if (excludeWeapon2Id != null && this._heroIds[index] === excludeWeapon2Id) return false;
2 weeks ago
if (glyphsOnly && !isElementGlyphType(this._types[index])) return false;
2 weeks ago
if (maxType > 0 && this._types[index] > maxType) return false;
3 weeks ago
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;
}
}