From 1388366f39e48fa7b0a7e582ce846333de57d890 Mon Sep 17 00:00:00 2001 From: humingxing <930621236@qq.com> Date: Wed, 7 Oct 2026 18:05:47 +0800 Subject: [PATCH] 11 --- .../script/battle/combat/WeaponDropService.ts | 161 ++++++++++-------- assets/script/battle/runtime/BattleRuntime.ts | 2 +- .../BattleGame/BattleWeaponDropPresenter.ts | 36 ++-- docs/需求-装备掉落.md | 31 ++-- 4 files changed, 132 insertions(+), 98 deletions(-) diff --git a/assets/script/battle/combat/WeaponDropService.ts b/assets/script/battle/combat/WeaponDropService.ts index 3178e4d..d98c945 100644 --- a/assets/script/battle/combat/WeaponDropService.ts +++ b/assets/script/battle/combat/WeaponDropService.ts @@ -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(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(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(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; } /** diff --git a/assets/script/battle/runtime/BattleRuntime.ts b/assets/script/battle/runtime/BattleRuntime.ts index b5efa8f..34a731d 100644 --- a/assets/script/battle/runtime/BattleRuntime.ts +++ b/assets/script/battle/runtime/BattleRuntime.ts @@ -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; diff --git a/assets/script/ui/BattleGame/BattleWeaponDropPresenter.ts b/assets/script/ui/BattleGame/BattleWeaponDropPresenter.ts index c96053d..f29b203 100644 --- a/assets/script/ui/BattleGame/BattleWeaponDropPresenter.ts +++ b/assets/script/ui/BattleGame/BattleWeaponDropPresenter.ts @@ -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; } diff --git a/docs/需求-装备掉落.md b/docs/需求-装备掉落.md index 9aac0fc..c96d378 100644 --- a/docs/需求-装备掉落.md +++ b/docs/需求-装备掉落.md @@ -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`。