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; /** Rank.weaponweight 下标 → Weapon.quality:绿 / 蓝 / 紫 / 橙 */ const WEIGHT_QUALITY = [1, 2, 3, 4]; /** 掷骰失败哨兵(0 为白色,不可与失败混用) */ const ROLL_FAIL = -1; /** 局内一次武器碎片掉落 */ 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; } /** * 击杀掉落武器碎片:概率、品质、武器抽取与本局记录。 */ export class WeaponDropService { private readonly _self: WeaponDropRecord[] = []; private readonly _other: WeaponDropRecord[] = []; /** 按击杀顺序的全部掉落(暂停列表用) */ private readonly _all: WeaponDropRecord[] = []; private readonly _pendingVisual: WeaponDropRecord[] = []; private _poolByQuality: ITableWeapon[][] | null = null; /** 己方已掉落碎片 */ 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; } /** * 按品质权重与武器表掷出一条掉落。 * @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.log('[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.log('[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 quality = this._rollQuality(rank.weaponweight); if (quality < 0) return null; const weapon = this._pickWeapon(quality); if (!weapon) return null; const itemId = Math.floor(Number(weapon.itemid) || 0); if (itemId <= 0) { console.log('[WeaponDrop] 武器无碎片 id', weapon.id); return null; } const ownerId = battle.resolveKillPeachOwnerPlayerId(killer, victim); return { weaponId: Math.floor(Number(weapon.id) || 0), itemId, quality, isSelf: ownerId === battle.localPlayerId, }; } /** * 按绿蓝紫橙四档权重随机品质。 * @param weights Rank.weaponweight */ private _rollQuality(weights: readonly number[] | null | undefined): number { if (!weights || weights.length < WEIGHT_QUALITY.length) { console.log('[WeaponDrop] weaponweight 长度非法'); return ROLL_FAIL; } let total = 0; for (let i = 0; i < WEIGHT_QUALITY.length; i++) { const w = Math.max(0, Math.floor(Number(weights[i]) || 0)); total += w; } if (total <= 0) { console.log('[WeaponDrop] weaponweight 全 0'); return ROLL_FAIL; } let roll = Math.random() * total; for (let i = 0; i < WEIGHT_QUALITY.length; i++) { roll -= Math.max(0, Math.floor(Number(weights[i]) || 0)); if (roll < 0) return WEIGHT_QUALITY[i]; } return WEIGHT_QUALITY[WEIGHT_QUALITY.length - 1]; } /** * 从该品质武器池均匀随机一把。 * @param quality Weapon.quality */ private _pickWeapon(quality: number): ITableWeapon | null { this._ensurePools(); const pool = this._poolByQuality?.[quality]; if (!pool || pool.length <= 0) { console.log('[WeaponDrop] 该品质无可用武器', quality); return null; } const idx = Math.floor(Math.random() * pool.length); return pool[idx] ?? null; } /** 按品质缓存 itemid>0 的武器 */ private _ensurePools(): void { if (this._poolByQuality) return; const buckets: ITableWeapon[][] = [[], [], [], [], []]; const list = gg.data.table.getTableList(TableNames.Weapon) ?? []; for (let i = 0; i < list.length; i++) { const row = list[i]; if (!(Math.floor(Number(row.itemid) || 0) > 0)) continue; const q = Math.floor(Number(row.quality) || 0); if (q < 0 || q > 4) { console.log('[WeaponDrop] 武器品质越界', row.id, q); continue; } buckets[q].push(row); } this._poolByQuality = buckets; } /** * 把掉落按 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 }); } } }