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.
246 lines
8.5 KiB
246 lines
8.5 KiB
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 });
|
|
}
|
|
}
|
|
}
|
|
|