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/combat/WeaponDropService.ts

274 lines
9.4 KiB

3 weeks ago
import { RankDataManager } from '../../manager/RankDataManager';
import { TableNames } from '../../game/ConfigTableData';
import { BattleBuffCarryStore } from '../../manager/battleBuff/BattleBuffCarryStore';
import { resolveCarriedGemDropBonusWan } from '../../manager/battleBuff/BattleBuffPassiveSkillUtil';
3 weeks ago
import type { EntityId } from 'db://assets/mx/module/ecs';
import type { ProjectBattleHandle } from '../runtime/BattleRuntime';
/** 万分比分母 */
const WAN = 10000;
2 weeks ago
/** Level.expdrop 一条加权宝石 */
interface IExpDropEntry {
/** Weapon.id(宝石) */
gemId: number;
/** 权重 */
weight: number;
}
/** 局内一次宝石掉落 */
3 weeks ago
export interface WeaponDropRecord {
2 weeks ago
/** Weapon.id(宝石) */
3 weeks ago
weaponId: number;
2 weeks ago
/** 道具 Item.id(Weapon.itemid) */
3 weeks ago
itemId: number;
/** Weapon.quality */
quality: number;
/** 是否己方击杀 */
isSelf: boolean;
}
2 weeks ago
/** 结算用掉落堆叠项 */
3 weeks ago
export interface WeaponDropRewardItem {
2 weeks ago
/** Item.id */
3 weeks ago
itemId: number;
/** 数量 */
num: number;
}
/** 结算 UI:同 weaponId 合并后的展示项 */
export interface WeaponDropAggregatedEntry {
/** 代表记录(首条,用于外观) */
record: WeaponDropRecord;
2 weeks ago
/** 数量 */
3 weeks ago
count: number;
}
/**
2 weeks ago
* 击杀掉落宝石:概率沿用 Monster/Rank.weapondrop,选品读本关 Level.expdrop。
3 weeks ago
*/
export class WeaponDropService {
private readonly _self: WeaponDropRecord[] = [];
private readonly _other: WeaponDropRecord[] = [];
/** 按击杀顺序的全部掉落(暂停列表用) */
private readonly _all: WeaponDropRecord[] = [];
private readonly _pendingVisual: WeaponDropRecord[] = [];
2 weeks ago
/** 本关 expdrop 解析缓存 */
private _expPool: IExpDropEntry[] | null = null;
/** 缓存对应的 Level.id */
private _expLevelId = 0;
3 weeks ago
2 weeks ago
/** 己方已掉落 */
3 weeks ago
get selfDrops(): readonly WeaponDropRecord[] {
return this._self;
}
2 weeks ago
/** 敌方已掉落 */
3 weeks ago
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 {
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;
}
2 weeks ago
/** 胜利结算:双方掉落按 itemId 堆叠 */
3 weeks ago
collectRewardItems(): WeaponDropRewardItem[] {
const out: WeaponDropRewardItem[] = [];
this._stackInto(out, this._self);
this._stackInto(out, this._other);
return out;
}
/**
2 weeks ago
* 按 weaponId 合并掉落(结算列表:同宝石共用一个预制体并显示数量)。
3 weeks ago
* @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;
2 weeks ago
this._expPool = null;
this._expLevelId = 0;
3 weeks ago
}
/**
2 weeks ago
* 概率成功后,按本关 Level.expdrop 权重抽一颗宝石。
3 weeks ago
* @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) {
2 weeks ago
console.warn('[WeaponDrop] Rank 缺失', battle.playerRankId);
3 weeks ago
return null;
}
const monsterDrop = Math.floor(Number(monster.weapondrop) || 0);
const rankDrop = Math.floor(Number(rank.weapondrop) || 0);
if (monsterDrop < 0 || rankDrop < 0) {
2 weeks ago
console.warn('[WeaponDrop] weapondrop 非法', monster.id, monster.weapondrop, rank.weapondrop);
3 weeks ago
return null;
}
let chance = monsterDrop + rankDrop;
3 weeks ago
if (chance <= 0) return null;
const ownerId = battle.resolveKillPeachOwnerPlayerId(killer, victim);
const gemBonus = resolveCarriedGemDropBonusWan(BattleBuffCarryStore.getPassives(ownerId));
if (gemBonus.active && gemBonus.bonusWan > 0) {
chance = Math.floor(chance * (1 + gemBonus.bonusWan / WAN));
}
3 weeks ago
if (Math.random() * WAN >= chance) return null;
2 weeks ago
const gem = this._pickGemFromExpDrop(battle.levelConfigId);
if (!gem) return null;
const itemId = Math.floor(Number(gem.itemid) || 0);
3 weeks ago
if (itemId <= 0) {
2 weeks ago
console.warn('[WeaponDrop] 宝石无道具 id', gem.id);
3 weeks ago
return null;
}
return {
2 weeks ago
weaponId: Math.floor(Number(gem.id) || 0),
3 weeks ago
itemId,
2 weeks ago
quality: Math.floor(Number(gem.quality) || 0),
3 weeks ago
isSelf: ownerId === battle.localPlayerId,
};
}
/**
2 weeks ago
* 按 Level.expdrop「宝石id,权重|...」加权抽取。
* @param levelId Level.id
3 weeks ago
*/
2 weeks ago
private _pickGemFromExpDrop(levelId: number): ITableWeapon | null {
const pool = this._ensureExpPool(levelId);
if (!pool.length) return null;
3 weeks ago
let total = 0;
2 weeks ago
for (let i = 0; i < pool.length; i++) total += pool[i].weight;
3 weeks ago
if (total <= 0) {
2 weeks ago
console.warn('[WeaponDrop] expdrop 权重全 0', levelId);
return null;
3 weeks ago
}
let roll = Math.random() * total;
2 weeks ago
let gemId = 0;
for (let i = 0; i < pool.length; i++) {
roll -= pool[i].weight;
if (roll < 0) {
gemId = pool[i].gemId;
break;
}
3 weeks ago
}
2 weeks ago
if (gemId <= 0) gemId = pool[pool.length - 1].gemId;
const gem = gg.data.table.getTableData<ITableWeapon>(TableNames.Weapon, gemId);
if (!gem) {
console.warn('[WeaponDrop] expdrop 宝石表缺失', gemId, levelId);
return null;
}
return gem;
3 weeks ago
}
/**
2 weeks ago
* 解析并缓存本关 expdrop。
* @param levelId Level.id
3 weeks ago
*/
2 weeks ago
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;
3 weeks ago
}
2 weeks ago
const level = gg.data.table.getTableData<ITableLevel>(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);
3 weeks ago
continue;
}
2 weeks ago
this._expPool.push({ gemId, weight });
3 weeks ago
}
2 weeks ago
if (!this._expPool.length) console.warn('[WeaponDrop] expdrop 无有效条目', id, raw);
return this._expPool;
3 weeks ago
}
/**
* 把掉落按 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 });
}
}
}