import { RankDataManager } from '../../manager/RankDataManager'; import { TableNames } from '../../game/ConfigTableData'; import { getBattleBuffUnitReviveHost } from './BattleBuffUnitReviveHost'; import type { EntityId } from 'db://assets/mx/module/ecs'; import type { ProjectBattleHandle } from '../runtime/BattleRuntime'; /** 万分比分母 */ const WAN = 10000; /** Level.expdrop 一条加权宝石 */ interface IExpDropEntry { /** Weapon.id(宝石) */ gemId: number; /** 权重 */ weight: number; } /** 局内一次宝石掉落 */ export interface WeaponDropRecord { /** Weapon.id(宝石) */ weaponId: number; /** 道具 Item.id(Weapon.itemid) */ itemId: number; /** Weapon.quality */ quality: number; /** 是否己方击杀 */ isSelf: boolean; } /** 结算用掉落堆叠项 */ export interface WeaponDropRewardItem { /** Item.id */ itemId: number; /** 数量 */ num: number; } /** 结算 UI:同 weaponId 合并后的展示项 */ export interface WeaponDropAggregatedEntry { /** 代表记录(首条,用于外观) */ record: WeaponDropRecord; /** 数量 */ count: number; } /** * 击杀掉落宝石:概率沿用 Monster/Rank.weapondrop,选品读本关 Level.expdrop。 */ export class WeaponDropService { private readonly _self: WeaponDropRecord[] = []; private readonly _other: WeaponDropRecord[] = []; /** 按击杀顺序的全部掉落(暂停列表用) */ private readonly _all: WeaponDropRecord[] = []; private readonly _pendingVisual: WeaponDropRecord[] = []; /** 本关 expdrop 解析缓存 */ private _expPool: IExpDropEntry[] | null = null; /** 缓存对应的 Level.id */ private _expLevelId = 0; /** 己方已掉落 */ get selfDrops(): readonly WeaponDropRecord[] { return this._self; } /** 敌方已掉落 */ get otherDrops(): readonly WeaponDropRecord[] { return this._other; } /** 全部掉落(击杀顺序) */ get allDrops(): readonly WeaponDropRecord[] { return this._all; } /** * 击杀时掷骰;成功则记入列表并排队表现。 * @param battle 当前战斗 * @param killer 击杀者 * @param victim 死亡实体 */ tryOnKill(battle: ProjectBattleHandle, killer: EntityId, victim: EntityId): WeaponDropRecord | null { if (getBattleBuffUnitReviveHost().isPassiveRevivedEntity(victim)) return null; const rec = this._roll(battle, killer, victim); if (!rec) return null; this._all.push(rec); if (rec.isSelf) this._self.push(rec); else this._other.push(rec); this._pendingVisual.push(rec); return rec; } /** 取出下一条待播表现的掉落(与 EntityKilled 一对一) */ shiftPendingVisual(): WeaponDropRecord | null { if (this._pendingVisual.length <= 0) return null; return this._pendingVisual.shift() ?? null; } /** 胜利结算:双方掉落按 itemId 堆叠 */ collectRewardItems(): WeaponDropRewardItem[] { const out: WeaponDropRewardItem[] = []; this._stackInto(out, this._self); this._stackInto(out, this._other); return out; } /** * 按 weaponId 合并掉落(结算列表:同宝石共用一个预制体并显示数量)。 * @param drops 本局全部掉落 */ static aggregateByWeaponId(drops: readonly WeaponDropRecord[]): WeaponDropAggregatedEntry[] { const out: WeaponDropAggregatedEntry[] = []; for (let i = 0; i < drops.length; i++) { const rec = drops[i]; let found = false; for (let j = 0; j < out.length; j++) { if (out[j].record.weaponId !== rec.weaponId) continue; out[j].count += 1; found = true; break; } if (!found) out.push({ record: rec, count: 1 }); } return out; } /** 清空本局记录 */ clear(): void { this._self.length = 0; this._other.length = 0; this._all.length = 0; this._pendingVisual.length = 0; this._expPool = null; this._expLevelId = 0; } /** * 概率成功后,按本关 Level.expdrop 权重抽一颗宝石。 * @param battle 当前战斗 * @param killer 击杀者 * @param victim 死亡实体 */ private _roll(battle: ProjectBattleHandle, killer: EntityId, victim: EntityId): WeaponDropRecord | null { const monster = battle.resolveMonsterRow(victim); if (!monster) return null; const rank = RankDataManager.getRankConfig(battle.playerRankId); if (!rank) { console.warn('[WeaponDrop] Rank 缺失', battle.playerRankId); return null; } const monsterDrop = Math.floor(Number(monster.weapondrop) || 0); const rankDrop = Math.floor(Number(rank.weapondrop) || 0); if (monsterDrop < 0 || rankDrop < 0) { console.warn('[WeaponDrop] weapondrop 非法', monster.id, monster.weapondrop, rank.weapondrop); return null; } const chance = monsterDrop + rankDrop; if (chance <= 0) return null; if (Math.random() * WAN >= chance) return null; const gem = this._pickGemFromExpDrop(battle.levelConfigId); if (!gem) return null; const itemId = Math.floor(Number(gem.itemid) || 0); if (itemId <= 0) { console.warn('[WeaponDrop] 宝石无道具 id', gem.id); return null; } const ownerId = battle.resolveKillPeachOwnerPlayerId(killer, victim); return { weaponId: Math.floor(Number(gem.id) || 0), itemId, quality: Math.floor(Number(gem.quality) || 0), isSelf: ownerId === battle.localPlayerId, }; } /** * 按 Level.expdrop「宝石id,权重|...」加权抽取。 * @param levelId Level.id */ private _pickGemFromExpDrop(levelId: number): ITableWeapon | null { const pool = this._ensureExpPool(levelId); if (!pool.length) return null; let total = 0; for (let i = 0; i < pool.length; i++) total += pool[i].weight; if (total <= 0) { console.warn('[WeaponDrop] expdrop 权重全 0', levelId); return null; } let roll = Math.random() * total; let gemId = 0; for (let i = 0; i < pool.length; i++) { roll -= pool[i].weight; if (roll < 0) { gemId = pool[i].gemId; break; } } if (gemId <= 0) gemId = pool[pool.length - 1].gemId; const gem = gg.data.table.getTableData(TableNames.Weapon, gemId); if (!gem) { console.warn('[WeaponDrop] expdrop 宝石表缺失', gemId, levelId); return null; } return gem; } /** * 解析并缓存本关 expdrop。 * @param levelId Level.id */ private _ensureExpPool(levelId: number): IExpDropEntry[] { const id = Math.floor(Number(levelId) || 0); if (this._expPool && this._expLevelId === id) return this._expPool; this._expLevelId = id; this._expPool = []; if (id <= 0) { console.warn('[WeaponDrop] levelId 无效'); return this._expPool; } const level = gg.data.table.getTableData(TableNames.Level, id); if (!level) { console.warn('[WeaponDrop] Level 缺失', id); return this._expPool; } const raw = String(level.expdrop ?? '').trim(); if (!raw) { console.warn('[WeaponDrop] expdrop 为空', id); return this._expPool; } const parts = raw.split('|'); for (let i = 0; i < parts.length; i++) { const part = parts[i].trim(); if (!part) continue; const segs = part.split(','); if (segs.length < 2) { console.warn('[WeaponDrop] expdrop 段格式非法', id, part); continue; } const gemId = Math.floor(Number(segs[0]) || 0); const weight = Math.floor(Number(segs[1]) || 0); if (gemId <= 0 || weight <= 0) { console.warn('[WeaponDrop] expdrop 条目非法', id, part); continue; } this._expPool.push({ gemId, weight }); } if (!this._expPool.length) console.warn('[WeaponDrop] expdrop 无有效条目', id, raw); return this._expPool; } /** * 把掉落按 itemId 堆入结算数组。 * @param out 结算数组 * @param drops 掉落列表 */ private _stackInto(out: WeaponDropRewardItem[], drops: readonly WeaponDropRecord[]): void { for (let i = 0; i < drops.length; i++) { const itemId = drops[i].itemId; let found = false; for (let j = 0; j < out.length; j++) { if (out[j].itemId !== itemId) continue; out[j].num += 1; found = true; break; } if (!found) out.push({ itemId, num: 1 }); } } }