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

247 lines
8.5 KiB

3 weeks ago
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<ITableWeapon>(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 });
}
}
}