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/BagHeroRefreshAlgorithm.ts

219 lines
7.8 KiB

import { isAttackGlyph } from '../config/BattleHeroTable';
3 weeks ago
import { formatIdList, formatWeapon2Brief, logBagRefreshDebug } from '../debug/BagRefreshDebugLog';
import type { BagRollEntry } from './BagWeaponPool';
import type { PlayerBagTextPool } from './PlayerBagTextPool';
/**
* 新号第一局神将字保底(Hero.text 已删:字对恒空,实际为 no-op)。
3 weeks ago
*/
export class FirstBattleHeroPity {
/** 本局是否启用 */
private readonly _enabled: boolean;
/** 第二次要给的另一半 Text.id;0=无需再给 */
private _pending = 0;
/** 三对神将字,扁平 [a,b, a,b, a,b] */
private _pairs: number[] | null = null;
/**
* @param enabled 开战时是否新号第一局
*/
constructor(enabled: boolean) {
this._enabled = enabled;
}
/** 第二次待给的另一半 */
get pendingPartner(): number {
return this._pending;
}
/**
* 快照恢复待给另一半。
* @param id Text.id;0 表示无需再给
*/
setPendingPartner(id: number): void {
this._pending = Math.floor(Number(id) || 0);
}
/**
* 按本局第几次刷新就地改 slots。
* @param slots 本次抽出的槽
* @param refreshUsed 本局已成功普通刷新次数(本次尚未 +1)
* @param pool 字池
*/
applyToSlots(slots: BagRollEntry[], refreshUsed: number, pool: PlayerBagTextPool): void {
if (!this._enabled) return;
if (refreshUsed === 0) {
this._ensureFirst(slots, pool);
return;
}
if (refreshUsed === 1) this._ensureSecond(slots, pool);
}
/** 第一次:保证六字之一并记下另一半 */
private _ensureFirst(slots: BagRollEntry[], pool: PlayerBagTextPool): void {
const pairs = this._heroPairs();
if (pairs.length < 2) {
console.log('[FirstBattleHeroPity] 神将字对为空,跳过首次保底');
return;
}
if (this._hasCompletePair(slots, pairs)) {
this._pending = 0;
logBagRefreshDebug('首局神将: 首次已成对,第二次不补', formatIdList(this._slotIds(slots)));
return;
}
const present = this._presentGlyphs(slots, pairs);
if (present.length > 0) {
const pick = present[Math.floor(Math.random() * present.length)];
this._pending = this._partnerOf(pick, pairs);
logBagRefreshDebug(
'首局神将: 首次已有六字',
formatWeapon2Brief(pick),
`下一半=${formatWeapon2Brief(this._pending)}`,
);
return;
}
const glyphs = this._allGlyphs(pairs);
const pick = glyphs[Math.floor(Math.random() * glyphs.length)];
if (!this._replaceOne(slots, pick, pool, true)) return;
this._pending = this._partnerOf(pick, pairs);
logBagRefreshDebug(
'首局神将: 首次替换',
formatWeapon2Brief(pick),
`下一半=${formatWeapon2Brief(this._pending)}`,
);
}
/** 第二次:保证给记下的另一半 */
private _ensureSecond(slots: BagRollEntry[], pool: PlayerBagTextPool): void {
if (this._pending <= 0) return;
const want = this._pending;
this._pending = 0;
if (this._contains(slots, want)) {
logBagRefreshDebug('首局神将: 第二次已抽出', formatWeapon2Brief(want));
return;
}
if (!this._replaceOne(slots, want, pool, false)) return;
logBagRefreshDebug('首局神将: 第二次替换', formatWeapon2Brief(want));
}
/**
* 替换一槽为指定字:扣新字 remaining、归还原字。remaining 不足仍强制给字。
* @param keepAttacker true 时不替换本批唯一攻击字
*/
private _replaceOne(slots: BagRollEntry[], newId: number, pool: PlayerBagTextPool, keepAttacker: boolean): boolean {
const idx = this._pickReplaceIndex(slots, keepAttacker);
if (idx < 0) {
console.log('[FirstBattleHeroPity] 无槽可替换', newId);
return false;
}
const oldId = slots[idx].weapon2Id;
if (oldId === newId) return true;
if (!pool.tryConsumeWeapon2Id(newId)) {
console.log('[FirstBattleHeroPity] 字池 remaining 不足仍强制给字', newId);
}
if (!pool.tryRefundWeapon2Id(oldId)) {
console.log('[FirstBattleHeroPity] 归还原抽字 remaining 失败', oldId);
}
slots[idx] = { weapon2Id: newId };
return true;
}
/**
* 随机选替换下标。
* @param keepAttacker true 时跳过本批唯一攻击字
*/
private _pickReplaceIndex(slots: BagRollEntry[], keepAttacker: boolean): number {
let atkCount = 0;
for (let i = 0; i < slots.length; i++) {
if (isAttackGlyph(slots[i].weapon2Id)) atkCount++;
}
const cand: number[] = [];
for (let i = 0; i < slots.length; i++) {
if (keepAttacker && atkCount <= 1 && isAttackGlyph(slots[i].weapon2Id)) continue;
cand.push(i);
}
if (cand.length <= 0) {
console.log('[FirstBattleHeroPity] 无可替换槽', `atk=${atkCount}`, `n=${slots.length}`);
return -1;
}
return cand[Math.floor(Math.random() * cand.length)];
}
/** Hero.text 已删:首局神将字保底无字对可配,恒空 */
3 weeks ago
private _heroPairs(): number[] {
if (this._pairs) return this._pairs;
this._pairs = [];
return this._pairs;
3 weeks ago
}
/** 本批是否已含任一对的两个字 */
private _hasCompletePair(slots: readonly BagRollEntry[], pairs: readonly number[]): boolean {
for (let i = 0; i + 1 < pairs.length; i += 2) {
if (this._contains(slots, pairs[i]) && this._contains(slots, pairs[i + 1])) return true;
}
return false;
}
/** 本批已出现的六字(去重) */
private _presentGlyphs(slots: readonly BagRollEntry[], pairs: readonly number[]): number[] {
const out: number[] = [];
for (let i = 0; i < slots.length; i++) {
const id = slots[i].weapon2Id;
if (this._isGlyph(id, pairs)) this._addUnique(out, id);
}
return out;
}
/** 六字全集 */
private _allGlyphs(pairs: readonly number[]): number[] {
const out: number[] = [];
for (let i = 0; i < pairs.length; i++) this._addUnique(out, pairs[i]);
return out;
}
/** 同一对里的另一半;找不到返回 0 */
private _partnerOf(id: number, pairs: readonly number[]): number {
for (let i = 0; i + 1 < pairs.length; i += 2) {
if (pairs[i] === id) return pairs[i + 1];
if (pairs[i + 1] === id) return pairs[i];
}
console.log('[FirstBattleHeroPity] 无配对另一半', id);
return 0;
}
/** id 是否属于保底六字 */
private _isGlyph(id: number, pairs: readonly number[]): boolean {
for (let i = 0; i < pairs.length; i++) {
if (pairs[i] === id) return true;
}
return false;
}
/** 本批是否含该 Text.id */
private _contains(slots: readonly BagRollEntry[], id: number): boolean {
for (let i = 0; i < slots.length; i++) {
if (slots[i].weapon2Id === id) return true;
}
return false;
}
/** 槽位 Text.id 列表(调试) */
private _slotIds(slots: readonly BagRollEntry[]): number[] {
const ids: number[] = [];
for (let i = 0; i < slots.length; i++) ids.push(slots[i].weapon2Id);
return ids;
}
/**
* 去重追加。
* @param ids 目标数组
* @param id 待加入 id
*/
private _addUnique(ids: number[], id: number): void {
for (let i = 0; i < ids.length; i++) {
if (ids[i] === id) return;
}
ids.push(id);
}
}