humingxing 1 week ago
parent 2f8f9305a8
commit 1388366f39
  1. 161
      assets/script/battle/combat/WeaponDropService.ts
  2. 2
      assets/script/battle/runtime/BattleRuntime.ts
  3. 36
      assets/script/ui/BattleGame/BattleWeaponDropPresenter.ts
  4. 31
      docs/需求-装备掉落.md

@ -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 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 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 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;
}
/**

@ -217,7 +217,7 @@ export class BattleRuntime implements IBattleRuntimeHandle {
readonly MonsterSpawner = new MonsterSpawnerBridge();
/** 本局理论秒伤(UI / 三选1 危险系数共用) */
readonly dpsMeter = new BattleDpsMeter();
/** 局内武器碎片掉落 */
/** 局内宝石掉落(选品 Level.expdrop) */
readonly weaponDrop = new WeaponDropService();
/** 当前理论秒伤;由 UIBattleGame 刷新 */
currentDps = 0;

@ -107,28 +107,28 @@ export class WeaponDropView extends Component {
}
/**
* 按武器信息刷新底框与图标(掉落 / 暂停列表共用)。
* @param node 武器预制体根节点
* 按宝石信息刷新底框与图标(掉落 / 暂停列表共用)。
* @param node 「武器」预制体根节点(仍复用该预制)
* @param record 掉落记录
*/
static applyVisual(node: Node, record: WeaponDropRecord): void {
WeaponDropView.clearFragmentCount(node);
const weapon = gg.equip.getEquip(record.weaponId);
if (!weapon) {
console.log('[WeaponDrop] 武器表缺失', record.weaponId);
console.warn('[WeaponDrop] 宝石表缺失', record.weaponId);
return;
}
const bgIndex = WeaponDropView._qualityBgIndex(record.quality, record.weaponId);
WeaponDropView._setSprite(node, `weapon/武器底${bgIndex}`);
const bgIndex = WeaponDropView._gemFrameIndex(record.quality, record.weaponId);
WeaponDropView._setSprite(node, `weapon/宝石框大${bgIndex}`);
const bgSprite = node.getComponent(Sprite);
if (bgSprite) bgSprite.type = Sprite.Type.SLICED;
const iconName = String(weapon.icon ?? '').trim();
if (!iconName) {
console.log('[WeaponDrop] icon 缺失', record.weaponId);
console.warn('[WeaponDrop] icon 缺失', record.weaponId);
}
const icon = node.getChildByName(ICON_NODE);
if (!icon?.isValid) {
console.log('[WeaponDrop] 缺少图标节点', record.weaponId);
console.warn('[WeaponDrop] 缺少图标节点', record.weaponId);
return;
}
WeaponDropView._setSprite(icon, iconName ? `weapon/${iconName}` : '', true);
@ -143,20 +143,20 @@ export class WeaponDropView extends Component {
WeaponDropView.clearFragmentCount(node);
const weapon = gg.equip.getEquip(record.weaponId);
if (!weapon) {
console.log('[WeaponDrop] 武器表缺失', record.weaponId);
console.warn('[WeaponDrop] 宝石表缺失', record.weaponId);
return;
}
const bgIndex = WeaponDropView._qualityBgIndex(record.quality, record.weaponId);
WeaponDropView._setSprite(node, `weapon/武器底${bgIndex}`);
const bgIndex = WeaponDropView._gemFrameIndex(record.quality, record.weaponId);
WeaponDropView._setSprite(node, `weapon/宝石框大${bgIndex}`);
const bgSprite = node.getComponent(Sprite);
if (bgSprite) bgSprite.type = Sprite.Type.SLICED;
const iconName = String(weapon.icon ?? '').trim();
if (!iconName) {
console.log('[WeaponDrop] icon 缺失', record.weaponId);
console.warn('[WeaponDrop] icon 缺失', record.weaponId);
}
const icon = node.getChildByName(SETTLE_ICON_NODE);
if (!icon?.isValid) {
console.log('[WeaponDrop] 缺少 name 节点', record.weaponId);
console.warn('[WeaponDrop] 缺少 name 节点', record.weaponId);
return;
}
WeaponDropView._setSprite(icon, iconName ? `weapon/${iconName}` : '', true);
@ -225,11 +225,15 @@ export class WeaponDropView extends Component {
if (lb) lb.string = '';
}
/** Weapon.quality 0~4 → 武器底 0~4(白/绿/蓝/紫/橙) */
private static _qualityBgIndex(quality: number, weaponId: number): number {
/**
* Weapon.quality → 宝石框大资源下标(现有资源 1~5)。
* @param quality 品质
* @param weaponId 宝石 id(仅日志)
*/
private static _gemFrameIndex(quality: number, weaponId: number): number {
const q = Number(quality) | 0;
if (q < 0 || q > 4) {
console.log('[WeaponDrop] 品质无效', weaponId, q);
if (q < 1 || q > 5) {
console.warn('[WeaponDrop] 宝石品质无对应宝石框大', weaponId, q);
}
return q;
}

@ -1,16 +1,23 @@
# 需求:击杀怪物掉落装备碎片
# 需求:击杀怪物掉落宝石
> 状态:**已确认(局内实现)**
> 日期:2026-09-04
> 状态:**已实现(局内)**
> 日期:2026-10-07
> 说明:老项目掉落武器碎片;新产品 `Weapon` 表即为宝石,流程复用,选品改为关卡 `expdrop`。
## 规则
1. 击杀怪物后按 `Monster.weapondrop + Rank.weapondrop`(万分比)掷骰,成功则掉落 **1 个武器碎片**。
2. 段位取本局玩家 `playerRankId`(需求中的「当前段位」)。
3. 品质按 `Rank.weaponweight` 四档权重随机:绿 / 蓝 / 紫 / 橙,对应 `Weapon.quality` 1 / 2 / 3 / 4。
4. 品质确定后,从该品质且 `itemid > 0` 的武器中均匀随机一把,入账为 `Weapon.itemid` 碎片。
5. 己方击杀:掉落在死亡点,点击后飞向左上角暂停按钮并渐隐。掉落弹跳全程须完整留在屏幕内。
6. AI 击杀:掉落后自动飞向暂停按钮并渐隐。
7. 暂停页随时展示本局已掷出的己方 / 敌方碎片(「武器」预制体)。
8. 胜利领取时获得双方全部碎片;失败 / 投降不获得。
9. 还魂丹暂缓击杀不掉落(与击杀掉桃一致)。
1. 击杀怪物后按 `Monster.weapondrop + Rank.weapondrop`(万分比)掷骰,成功则掉落 **1 颗宝石**(入账为 `Weapon.itemid` 对应 `Item`,`Item.type=3`)。
2. 段位取本局玩家 `playerRankId`。
3. **选品**:读本关 `Level.expdrop`,格式 `宝石id,权重|宝石id,权重...`,按权重随机一颗(**不再**使用 `Rank.weaponweight` 品质池从全表抽取)。
4. 己方击杀:掉落在死亡点,点击后飞向左上角暂停按钮并渐隐。掉落弹跳全程须完整留在屏幕内。
5. AI 击杀:掉落后自动飞向暂停按钮并渐隐。
6. 暂停页随时展示本局已掷出的己方 / 敌方掉落。
7. 胜利领取时获得双方全部掉落;失败 / 投降不获得。
8. 还魂丹暂缓击杀不掉落(与击杀掉桃一致)。
## 实现
- 逻辑:`assets/script/battle/combat/WeaponDropService.ts`
- 概率:`Monster.weapondrop + Rank.weapondrop`(段位加值,与老项目一致)
- 选品:本关 `Level.expdrop`(`宝石id,权重|...`)
- 表现:仍复用战场「武器」预制体;底框改 `icons/weapon/宝石框大{quality}`(1~5),图标读 `Weapon.icon`。

Loading…
Cancel
Save