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

378 lines
13 KiB

import { TableNames } from '../../game/ConfigTableData';
import type { ComposedUnit, BagSlotRecord } from 'db://assets/mx/module/merge';
import { membershipAdd, membershipFromArray } from '../../../mx/tools/MembershipSet';
import { OtherDataType } from '../../game/GameDataEnums';
import type { BattleBagSession } from './BattleBagSession';
import { WeaponComposeConfigProvider } from './WeaponComposeConfigProvider';
import { TextListCellState } from '../view/TextListLandHost';
/** 通关时场上一个完整英雄单位(同 weapon1 不同局内等级视为不同单位) */
export interface KeepHeroClearUnit {
weapon1Id: number;
level: number;
}
/** 抽奖阶段:0 未抽 / 1 已抽出待开局生效(多次抽覆盖) */
export enum KeepHeroLotteryPhase {
None = 0,
Pending = 1,
}
export interface KeepHeroLotteryState {
drawCount: number;
drawn: KeepHeroClearUnit | null;
phase: KeepHeroLotteryPhase;
}
/** 测试:弹窗/抽奖池用假数据;正式环境改为 false */
export const KEEP_HERO_USE_FAKE_UNITS = false;
const FAKE_UNITS: KeepHeroClearUnit[] = [
{ weapon1Id: 1, level: 1 },
{ weapon1Id: 1, level: 2 },
{ weapon1Id: 2, level: 3 },
{ weapon1Id: 3, level: 1 },
{ weapon1Id: 5, level: 1 },
{ weapon1Id: 8, level: 2 },
];
const DEFAULT_KEEP_HERO_LEVEL = 1;
const KEEP_HERO_LEVEL_SLOTS = [1, 2, 3, 4];
/**
* 从 Merge 合成态收集通关完整英雄。
* 每个 compositions 条目算 1 个单位(刀/斧/悟空等;Lv 不同分开记)。
*/
export function collectCompleteHeroUnits(
compositions: readonly ComposedUnit[],
): KeepHeroClearUnit[] {
const units: KeepHeroClearUnit[] = [];
for (const c of compositions) {
if (c.weapon1Id <= 0 || c.level <= 0) continue;
units.push({ weapon1Id: c.weapon1Id, level: c.level });
}
units.sort((a, b) => a.weapon1Id - b.weapon1Id || a.level - b.level);
return units;
}
/** 序列化:`weapon1Id,level;weapon1Id,level;...`(避免与 keyKeyValue 的 | # 冲突) */
export function serializeKeepHeroClearUnits(units: readonly KeepHeroClearUnit[]): string {
if (units.length === 0) return '';
return units.map((u) => `${u.weapon1Id},${u.level}`).join(';');
}
export function parseKeepHeroClearUnits(raw: string): KeepHeroClearUnit[] {
if (!raw) return [];
const out: KeepHeroClearUnit[] = [];
const parts = raw.includes(';') ? raw.split(';') : raw.split('|');
for (const part of parts) {
if (!part) continue;
const [idStr, lvStr] = part.split(',');
const weapon1Id = Number(idStr);
const level = Number(lvStr);
if (!(weapon1Id > 0) || !(level > 0)) continue;
out.push({ weapon1Id, level });
}
return out;
}
function resolveLevelId(levelId?: number): number {
return levelId
?? gg.game.CurentBattle?.LevelConfigId
?? gg.game.CurentSelectLevelId
?? DEFAULT_KEEP_HERO_LEVEL;
}
function writeClearUnits(levelId: number, units: readonly KeepHeroClearUnit[]): void {
gg.data.setkeyKeyValue(
OtherDataType.KeepHeroClearUnits,
levelId,
serializeKeepHeroClearUnits(units),
);
}
/** 通关结算:按战场覆盖写入完整英雄列表,并重置该战场抽奖态 */
export function saveKeepHeroClearUnits(
units: readonly KeepHeroClearUnit[],
levelId?: number,
): void {
const lv = resolveLevelId(levelId);
writeClearUnits(lv, units);
saveLotteryState(lv, { drawCount: 0, drawn: null, phase: KeepHeroLotteryPhase.None });
}
export function loadKeepHeroClearUnits(levelId?: number): KeepHeroClearUnit[] {
const lv = resolveLevelId(levelId);
const raw = gg.data.getkeyKeyValue(OtherDataType.KeepHeroClearUnits, lv) || '';
return parseKeepHeroClearUnits(raw);
}
/** 弹窗展示 / 抽奖池(可走假数据) */
export function getKeepHeroDisplayUnits(levelId?: number): KeepHeroClearUnit[] {
if (KEEP_HERO_USE_FAKE_UNITS) return FAKE_UNITS.slice();
return loadKeepHeroClearUnits(levelId);
}
/** 清空该战场结算池(不改抽奖态) */
export function clearKeepHeroClearUnits(levelId?: number): void {
writeClearUnits(resolveLevelId(levelId), []);
}
/** 本局发放的单位是否仍在背包(已部署到地图则 false) */
export function isKeepHeroGrantInBag(
bag: BattleBagSession,
unit: KeepHeroClearUnit,
): boolean {
WeaponComposeConfigProvider.instance.load();
const group = WeaponComposeConfigProvider.instance.getWeapon1Group(unit.weapon1Id);
if (group.length === 0) return false;
const level = Math.max(1, unit.level);
const slots = bag.merge.bag;
for (let i = 0; i < slots.slotCount; i++) {
const slot = slots.getSlot(i);
if (!slot || slot.level !== level) continue;
if (group.includes(slot.weapon2Id)) return true;
}
return false;
}
/** 生效后清空该战场全部保留文字数据(结算池 + 抽奖态) */
export function clearKeepHeroLevel(levelId: number): void {
writeClearUnits(levelId, []);
saveLotteryState(levelId, { drawCount: 0, drawn: null, phase: KeepHeroLotteryPhase.None });
}
/** 保留文字抽奖池最少条数(不足时用局外已上阵基础单字 1 级补足) */
const MIN_KEEP_HERO_POOL_SIZE = 3;
function keepHeroUnitKey(u: KeepHeroClearUnit): string {
return `${u.weapon1Id}_${u.level}`;
}
function shuffleInPlace<T>(arr: T[]): void {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
/** 局外已上阵且 herotype === 1 的基础武器 id */
function collectEquippedBasicWeapon1Ids(): number[] {
return gg.data.project.getEquippedBasicWeaponDataList().map((w) => w.id);
}
/** 场上完整英雄不足 {@link MIN_KEEP_HERO_POOL_SIZE} 时,随机补局外上阵基础单字 1 级 */
function padKeepHeroUnitsWithEquippedBasics(
units: readonly KeepHeroClearUnit[],
): KeepHeroClearUnit[] {
if (units.length >= MIN_KEEP_HERO_POOL_SIZE) return units.slice();
const existingKeys = membershipFromArray(units.map(keepHeroUnitKey));
const candidates = collectEquippedBasicWeapon1Ids()
.filter((weapon1Id) => !existingKeys.has(`${weapon1Id}_1`));
shuffleInPlace(candidates);
const out = units.slice();
let need = MIN_KEEP_HERO_POOL_SIZE - out.length;
for (const weapon1Id of candidates) {
if (need <= 0) break;
const filler: KeepHeroClearUnit = { weapon1Id, level: 1 };
out.push(filler);
membershipAdd(existingKeys, keepHeroUnitKey(filler));
need--;
}
out.sort((a, b) => a.weapon1Id - b.weapon1Id || a.level - b.level);
return out;
}
/** 从当前对局合成态记录通关英雄(仅常规关;按本局战场分存) */
export function recordKeepHeroClearFromBattle(): KeepHeroClearUnit[] {
const battle = gg.game.CurentBattle;
if (!battle) return [];
const compositions = battle.bagSession?.compositions
?? gg.game.localPlayer?.compositions
?? [];
const units = padKeepHeroUnitsWithEquippedBasics(
collectCompleteHeroUnits(compositions),
);
saveKeepHeroClearUnits(units, battle.LevelConfigId);
return units;
}
/** 是否有可抽奖的结算池(用于弹窗) */
export function hasPendingKeepHeroLottery(levelId?: number): boolean {
if (levelId != null) {
return getKeepHeroDisplayUnits(levelId).length > 0;
}
return findPendingKeepHeroLevel() != null;
}
/** 各战场中第一个「有结算池且尚未抽出」的 levelId */
export function findPendingKeepHeroLevel(): number | null {
if (KEEP_HERO_USE_FAKE_UNITS) {
return resolveLevelId();
}
for (const lv of KEEP_HERO_LEVEL_SLOTS) {
if (loadKeepHeroClearUnits(lv).length === 0) continue;
const state = loadLotteryState(lv);
if (state.phase === KeepHeroLotteryPhase.Pending && state.drawn) continue;
return lv;
}
return null;
}
/** @deprecated 使用 findPendingKeepHeroLevel */
export function findPendingKeepHeroDifficulty(): number | null {
return findPendingKeepHeroLevel();
}
function serializeLotteryState(state: KeepHeroLotteryState): string {
const unit = state.drawn ? `${state.drawn.weapon1Id},${state.drawn.level}` : '0,0';
return `${state.drawCount};${unit};${state.phase}`;
}
function parseLotteryState(raw: string): KeepHeroLotteryState {
if (!raw) {
return { drawCount: 0, drawn: null, phase: KeepHeroLotteryPhase.None };
}
const [countStr, unitStr, phaseStr] = raw.split(';');
const drawCount = Math.max(0, Number(countStr) || 0);
const rawPhase = Number(phaseStr) || KeepHeroLotteryPhase.None;
const phase = rawPhase > 0 ? KeepHeroLotteryPhase.Pending : KeepHeroLotteryPhase.None;
let drawn: KeepHeroClearUnit | null = null;
if (unitStr) {
const [idStr, lvStr] = unitStr.split(',');
const weapon1Id = Number(idStr);
const level = Number(lvStr);
if (weapon1Id > 0 && level > 0) drawn = { weapon1Id, level };
}
return { drawCount, drawn, phase: drawn ? phase : KeepHeroLotteryPhase.None };
}
export function loadLotteryState(levelId?: number): KeepHeroLotteryState {
const lv = resolveLevelId(levelId);
const raw = gg.data.getkeyKeyValue(OtherDataType.KeepHeroLotteryState, lv) || '';
return parseLotteryState(raw);
}
export function saveLotteryState(
levelId: number,
state: KeepHeroLotteryState,
): void {
gg.data.setkeyKeyValue(
OtherDataType.KeepHeroLotteryState,
levelId,
serializeLotteryState(state),
);
}
/** keep_hero:按 herotype 与等级取 type,再按抽奖次数取 weight */
export function resolveKeepHeroWeight(
_weapon1Id: number,
_level: number,
_drawIndex: number,
): number {
return 0;
}
/** 按当前抽奖次数做加权随机;drawIndex = 已抽次数(第 1 次用 weight[0]) */
export function rollKeepHeroUnit(
units: readonly KeepHeroClearUnit[],
drawIndex: number,
): KeepHeroClearUnit | null {
if (units.length === 0) return null;
let total = 0;
const weights: number[] = [];
for (const u of units) {
const w = Math.max(0, resolveKeepHeroWeight(u.weapon1Id, u.level, drawIndex));
weights.push(w);
total += w;
}
if (total <= 0) {
return units[Math.floor(Math.random() * units.length)] ?? null;
}
let r = Math.random() * total;
for (let i = 0; i < units.length; i++) {
r -= weights[i];
if (r <= 0) return units[i];
}
return units[units.length - 1] ?? null;
}
/**
* 免费/广告抽一次:抽出即存为待开局单位(覆盖上次)。
* 结算池仍保留,供本面板内广告再抽;点「领取」后再清池结束本次。
*/
export function performKeepHeroDraw(
levelId?: number,
pool?: readonly KeepHeroClearUnit[],
): KeepHeroClearUnit | null {
const lv = resolveLevelId(levelId);
const units = pool?.length ? pool : getKeepHeroDisplayUnits(lv);
const state = loadLotteryState(lv);
const drawn = rollKeepHeroUnit(units, state.drawCount);
if (!drawn) return null;
saveLotteryState(lv, {
drawCount: state.drawCount + 1,
drawn,
phase: KeepHeroLotteryPhase.Pending,
});
return drawn;
}
/** 点领取关闭:本次保留文字弹窗流程结束(清结算池,不再回主页弹) */
export function finishKeepHeroLotteryUi(levelId?: number): void {
clearKeepHeroClearUnits(resolveLevelId(levelId));
}
export interface KeepHeroBagGrant {
records: BagSlotRecord[];
unit: KeepHeroClearUnit;
}
export function tryGrantPendingKeepHeroToBag(
bag: BattleBagSession,
levelId: number,
): KeepHeroBagGrant | null {
const state = loadLotteryState(levelId);
if (state.phase !== KeepHeroLotteryPhase.Pending || !state.drawn) return null;
const provider = WeaponComposeConfigProvider.instance;
provider.load();
const group = provider.getWeapon1Group(state.drawn.weapon1Id);
if (group.length === 0) {
console.error('[KeepHero] 无法解析 Weapon1.characters', state.drawn.weapon1Id);
return null;
}
const drawn = state.drawn;
const isSingle = group.length === 1;
const level = Math.max(1, drawn.level);
const records: BagSlotRecord[] = group.map((weapon2Id) => (
isSingle
? { weapon2Id, weapon1Id: drawn.weapon1Id, level }
: { weapon2Id, level }
));
const slots = bag.merge.bag;
const emptyIndices: number[] = [];
for (let i = 0; i < slots.slotCount; i++) {
if (bag.landStateOf?.(i) === TextListCellState.Blank) continue;
if (!slots.getSlot(i)) emptyIndices.push(i);
}
if (emptyIndices.length < records.length) {
console.log(
'[KeepHero] 背包空位不足,保留待赠送',
`需要${records.length}`,
`空位${emptyIndices.length}`,
);
return null;
}
for (let i = 0; i < records.length; i++) {
slots.setSlot(emptyIndices[i], records[i]);
}
clearKeepHeroLevel(levelId);
return { records, unit: drawn };
}