|
|
|
|
@ -6,16 +6,20 @@ 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; |
|
|
|
|
|
|
|
|
|
/** 局内一次武器碎片掉落 */ |
|
|
|
|
/** Level.expdrop 一条加权宝石 */ |
|
|
|
|
interface IExpDropEntry { |
|
|
|
|
/** Weapon.id(宝石) */ |
|
|
|
|
gemId: number; |
|
|
|
|
/** 权重 */ |
|
|
|
|
weight: number; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** 局内一次宝石掉落 */ |
|
|
|
|
export interface WeaponDropRecord { |
|
|
|
|
/** Weapon.id */ |
|
|
|
|
/** Weapon.id(宝石) */ |
|
|
|
|
weaponId: number; |
|
|
|
|
/** 碎片 Item.id(Weapon.itemid) */ |
|
|
|
|
/** 道具 Item.id(Weapon.itemid) */ |
|
|
|
|
itemId: number; |
|
|
|
|
/** Weapon.quality */ |
|
|
|
|
quality: number; |
|
|
|
|
@ -23,9 +27,9 @@ export interface WeaponDropRecord { |
|
|
|
|
isSelf: boolean; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** 结算用碎片堆叠项 */ |
|
|
|
|
/** 结算用掉落堆叠项 */ |
|
|
|
|
export interface WeaponDropRewardItem { |
|
|
|
|
/** 碎片 Item.id */ |
|
|
|
|
/** Item.id */ |
|
|
|
|
itemId: number; |
|
|
|
|
/** 数量 */ |
|
|
|
|
num: number; |
|
|
|
|
@ -35,12 +39,12 @@ export interface WeaponDropRewardItem { |
|
|
|
|
export interface WeaponDropAggregatedEntry { |
|
|
|
|
/** 代表记录(首条,用于外观) */ |
|
|
|
|
record: WeaponDropRecord; |
|
|
|
|
/** 碎片数量 */ |
|
|
|
|
/** 数量 */ |
|
|
|
|
count: number; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 击杀掉落武器碎片:概率、品质、武器抽取与本局记录。 |
|
|
|
|
* 击杀掉落宝石:概率沿用 Monster/Rank.weapondrop,选品读本关 Level.expdrop。 |
|
|
|
|
*/ |
|
|
|
|
export class WeaponDropService { |
|
|
|
|
private readonly _self: WeaponDropRecord[] = []; |
|
|
|
|
@ -48,14 +52,17 @@ export class WeaponDropService { |
|
|
|
|
/** 按击杀顺序的全部掉落(暂停列表用) */ |
|
|
|
|
private readonly _all: WeaponDropRecord[] = []; |
|
|
|
|
private readonly _pendingVisual: WeaponDropRecord[] = []; |
|
|
|
|
private _poolByQuality: ITableWeapon[][] | null = null; |
|
|
|
|
/** 本关 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; |
|
|
|
|
} |
|
|
|
|
@ -88,7 +95,7 @@ export class WeaponDropService { |
|
|
|
|
return this._pendingVisual.shift() ?? null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** 胜利结算:双方碎片按 itemId 堆叠 */ |
|
|
|
|
/** 胜利结算:双方掉落按 itemId 堆叠 */ |
|
|
|
|
collectRewardItems(): WeaponDropRewardItem[] { |
|
|
|
|
const out: WeaponDropRewardItem[] = []; |
|
|
|
|
this._stackInto(out, this._self); |
|
|
|
|
@ -97,7 +104,7 @@ export class WeaponDropService { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 按 weaponId 合并掉落(结算列表:同武器共用一个预制体并显示数量)。 |
|
|
|
|
* 按 weaponId 合并掉落(结算列表:同宝石共用一个预制体并显示数量)。 |
|
|
|
|
* @param drops 本局全部掉落 |
|
|
|
|
*/ |
|
|
|
|
static aggregateByWeaponId(drops: readonly WeaponDropRecord[]): WeaponDropAggregatedEntry[] { |
|
|
|
|
@ -122,10 +129,12 @@ export class WeaponDropService { |
|
|
|
|
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 死亡实体 |
|
|
|
|
@ -135,94 +144,108 @@ export class WeaponDropService { |
|
|
|
|
if (!monster) return null; |
|
|
|
|
const rank = RankDataManager.getRankConfig(battle.playerRankId); |
|
|
|
|
if (!rank) { |
|
|
|
|
console.log('[WeaponDrop] Rank 缺失', battle.playerRankId); |
|
|
|
|
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.log('[WeaponDrop] weapondrop 非法', monster.id, monster.weapondrop, rank.weapondrop); |
|
|
|
|
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 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); |
|
|
|
|
const gem = this._pickGemFromExpDrop(battle.levelConfigId); |
|
|
|
|
if (!gem) return null; |
|
|
|
|
const itemId = Math.floor(Number(gem.itemid) || 0); |
|
|
|
|
if (itemId <= 0) { |
|
|
|
|
console.log('[WeaponDrop] 武器无碎片 id', weapon.id); |
|
|
|
|
console.warn('[WeaponDrop] 宝石无道具 id', gem.id); |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
const ownerId = battle.resolveKillPeachOwnerPlayerId(killer, victim); |
|
|
|
|
return { |
|
|
|
|
weaponId: Math.floor(Number(weapon.id) || 0), |
|
|
|
|
weaponId: Math.floor(Number(gem.id) || 0), |
|
|
|
|
itemId, |
|
|
|
|
quality, |
|
|
|
|
quality: Math.floor(Number(gem.quality) || 0), |
|
|
|
|
isSelf: ownerId === battle.localPlayerId, |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 按绿蓝紫橙四档权重随机品质。 |
|
|
|
|
* @param weights Rank.weaponweight |
|
|
|
|
* 按 Level.expdrop「宝石id,权重|...」加权抽取。 |
|
|
|
|
* @param levelId Level.id |
|
|
|
|
*/ |
|
|
|
|
private _rollQuality(weights: readonly number[] | null | undefined): number { |
|
|
|
|
if (!weights || weights.length < WEIGHT_QUALITY.length) { |
|
|
|
|
console.log('[WeaponDrop] weaponweight 长度非法'); |
|
|
|
|
return ROLL_FAIL; |
|
|
|
|
} |
|
|
|
|
private _pickGemFromExpDrop(levelId: number): ITableWeapon | null { |
|
|
|
|
const pool = this._ensureExpPool(levelId); |
|
|
|
|
if (!pool.length) return null; |
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
for (let i = 0; i < pool.length; i++) total += pool[i].weight; |
|
|
|
|
if (total <= 0) { |
|
|
|
|
console.log('[WeaponDrop] weaponweight 全 0'); |
|
|
|
|
return ROLL_FAIL; |
|
|
|
|
console.warn('[WeaponDrop] expdrop 权重全 0', levelId); |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
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]; |
|
|
|
|
let gemId = 0; |
|
|
|
|
for (let i = 0; i < pool.length; i++) { |
|
|
|
|
roll -= pool[i].weight; |
|
|
|
|
if (roll < 0) { |
|
|
|
|
gemId = pool[i].gemId; |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return WEIGHT_QUALITY[WEIGHT_QUALITY.length - 1]; |
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 从该品质武器池均匀随机一把。 |
|
|
|
|
* @param quality Weapon.quality |
|
|
|
|
* 解析并缓存本关 expdrop。 |
|
|
|
|
* @param levelId Level.id |
|
|
|
|
*/ |
|
|
|
|
private _pickWeapon(quality: number): ITableWeapon | null { |
|
|
|
|
this._ensurePools(); |
|
|
|
|
const pool = this._poolByQuality?.[quality]; |
|
|
|
|
if (!pool || pool.length <= 0) { |
|
|
|
|
console.log('[WeaponDrop] 该品质无可用武器', quality); |
|
|
|
|
return null; |
|
|
|
|
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 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); |
|
|
|
|
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); |
|
|
|
|
continue; |
|
|
|
|
} |
|
|
|
|
buckets[q].push(row); |
|
|
|
|
this._expPool.push({ gemId, weight }); |
|
|
|
|
} |
|
|
|
|
this._poolByQuality = buckets; |
|
|
|
|
if (!this._expPool.length) console.warn('[WeaponDrop] expdrop 无有效条目', id, raw); |
|
|
|
|
return this._expPool; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|