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(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); } }