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.
377 lines
13 KiB
377 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 { isComboHero } from '../config/BattleHeroTable';
|
|
|
|
/** 通关时场上一个完整英雄单位(同 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 (!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 };
|
|
}
|
|
|