|
|
|
|
import { TableNames } from '../../game/ConfigTableData';
|
|
|
|
|
import { heroCharacterIds, isAttackGlyph } from '../config/BattleHeroTable';
|
|
|
|
|
import { formatIdList, formatWeapon2Brief, logBagRefreshDebug } from '../debug/BagRefreshDebugLog';
|
|
|
|
|
import type { BagRollEntry } from './BagWeaponPool';
|
|
|
|
|
import type { PlayerBagTextPool } from './PlayerBagTextPool';
|
|
|
|
|
|
|
|
|
|
/** 新号首局保底神将(配表 Hero.name) */
|
|
|
|
|
const FIRST_BATTLE_HERO_NAMES = ['哪吒', '杨戬', '悟空'];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 新号第一局前两次刷新:先给哪吒/杨戬/悟空六字之一,再给能合成的另一半。
|
|
|
|
|
* 与是否拖到地图无关。仅玩家;GM 不走。
|
|
|
|
|
*/
|
|
|
|
|
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 或非两字则告警跳过该将 */
|
|
|
|
|
private _heroPairs(): number[] {
|
|
|
|
|
if (this._pairs) return this._pairs;
|
|
|
|
|
const out: number[] = [];
|
|
|
|
|
const list = gg.data.table.getTableList<ITableHero>(TableNames.Hero) ?? [];
|
|
|
|
|
for (let n = 0; n < FIRST_BATTLE_HERO_NAMES.length; n++) {
|
|
|
|
|
const name = FIRST_BATTLE_HERO_NAMES[n];
|
|
|
|
|
let found: ITableHero | null = null;
|
|
|
|
|
for (let i = 0; i < list.length; i++) {
|
|
|
|
|
if (String(list[i].name ?? '') !== name) continue;
|
|
|
|
|
found = list[i];
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (!found) {
|
|
|
|
|
console.log('[FirstBattleHeroPity] Hero 缺失', name);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const chars = heroCharacterIds(found);
|
|
|
|
|
if (chars.length !== 2) {
|
|
|
|
|
console.log('[FirstBattleHeroPity] Hero.text 不是两字', name, chars.join(','));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const a = Math.floor(Number(chars[0]) || 0);
|
|
|
|
|
const b = Math.floor(Number(chars[1]) || 0);
|
|
|
|
|
if (!(a > 0) || !(b > 0)) {
|
|
|
|
|
console.log('[FirstBattleHeroPity] Hero.text 非法', name, chars.join(','));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
out.push(a, b);
|
|
|
|
|
}
|
|
|
|
|
this._pairs = out;
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 本批是否已含任一对的两个字 */
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|