docs: add hero personal equip system design
Spec for per-hero equipment with random affixes: equip bases, affix pools, quality by affix count, box/gacha acquisition, out-of-run persistence, and in-run snapshot into HeroAttrsComp.
This commit is contained in:
281
docs/superpowers/specs/2026-08-21-hero-equip-design.md
Normal file
281
docs/superpowers/specs/2026-08-21-hero-equip-design.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# 英雄个人装备系统设计
|
||||
|
||||
- 日期:2026-08-21
|
||||
- 状态:已评审(brainstorming 阶段用户逐节确认)
|
||||
- 涉及文件:`HeroEquipSet.ts`(新增)、`heroSet.ts`、`HeroAttrs.ts`、`HeroAttrsComp.ts`、`SingletonModuleComp.ts`、`GameEvent.ts`、`EquipSet.ts`(改名 TalentSet)
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
现有"装备"(`EquipSet.ts`,8701~8820 段)是**全体驻场光环卡**:购买后全队加属性,经 `EquipBoxComp → FieldSkillHelper` 全局聚合。
|
||||
|
||||
本设计新增**英雄个人装备**:每个英雄可装备**一件**装备,词条只作用于该英雄本身;与现有光环卡**并存**,光环卡在表述上改名为**战队天赋**。
|
||||
|
||||
核心特性:
|
||||
- 装备在**玩家获得时**(开箱子 / 抽卡)随机生成词条并固定(暗黑式:从词条池随机抽 N 条、数值在区间内随机)。
|
||||
- **词条数量与数值高低决定装备品质**。
|
||||
- 装备是**局外资产**,随云存档持久化;局内只有战斗,不产生、不修改装备,开局快照生效。
|
||||
|
||||
## 2. 数据结构(HeroEquipSet.ts,新增)
|
||||
|
||||
位置:`assets/script/game/common/config/HeroEquipSet.ts`,与 `EquipSet.ts`(将改名 `TalentSet.ts`)平级。
|
||||
|
||||
```typescript
|
||||
import { Attrs } from "./HeroAttrs";
|
||||
|
||||
/** 装备品质(由词条数推导:1=普通 2=稀有 3=史诗 4=传说) */
|
||||
export enum EquipQuality {
|
||||
Normal = 1,
|
||||
Rare = 2,
|
||||
Epic = 3,
|
||||
Legend = 4,
|
||||
}
|
||||
|
||||
/** 词条池条目:attr 指向 Attrs 枚举,roll 时 value 在 [min, max] 内随机 */
|
||||
export interface AffixPoolEntry {
|
||||
attr: Attrs;
|
||||
min: number; // 数值型=整数下限;概率型=百分点下限
|
||||
max: number;
|
||||
weight: number; // 抽取权重
|
||||
}
|
||||
|
||||
/** 装备底座:只定义随机规则,不含固定词条(段位 9001-9099) */
|
||||
export interface HeroEquipBase {
|
||||
base_id: number;
|
||||
name: string;
|
||||
icon: string;
|
||||
affix_count: [number, number]; // 词条数区间,如 [1,2]
|
||||
affix_pool: AffixPoolEntry[];
|
||||
}
|
||||
|
||||
/** 装备实例(roll 生成后固定;不冗余存 name/icon,查询走底座表) */
|
||||
export interface HeroEquipInstance {
|
||||
inst_id: number; // 实例唯一 id(equip_next_id 自增)
|
||||
base_id: number;
|
||||
affixes: { attr: Attrs; value: number }[]; // quality = affixes.length,不单独存储
|
||||
}
|
||||
|
||||
/** 装备箱子配置(段位 8901-8999,2 档) */
|
||||
export interface EquipBoxConfig {
|
||||
uuid: number;
|
||||
name: string;
|
||||
icon: string;
|
||||
pool: { base_id: number; weight: number }[]; // 底座加权池
|
||||
}
|
||||
```
|
||||
|
||||
### 词条口径(与现有百分比口径一致)
|
||||
|
||||
- **数值型**(仅 `ap` / `hp_max` / `defense`):Flat 数值,roll 取**整数**。
|
||||
- **概率型**(其余全部):百分点,roll 取 **1 位小数**。
|
||||
- 词条池仅允许出现第 4.4 节白名单中的 attr,配置时校验。
|
||||
|
||||
### 品质规则
|
||||
|
||||
- `quality = 词条数`(1~4 对应普通~传说),初版从简;数值高低不参与品质,后续可扩展档位评分。
|
||||
|
||||
## 3. Roll 生成与获取途径
|
||||
|
||||
### 3.1 核心纯函数(HeroEquipSet.ts)
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 获得装备时调用:按底座规则 roll 出一件词条固定的装备实例
|
||||
* @param base_id 装备底座 id
|
||||
* @param instId 实例 id(由 SingletonModuleComp 分配)
|
||||
*/
|
||||
export function rollHeroEquip(base_id: number, instId: number): HeroEquipInstance | null
|
||||
```
|
||||
|
||||
Roll 流程:
|
||||
1. `affix_count: [min, max]` 内均匀随机词条数 N。
|
||||
2. 从 `affix_pool` 按 weight 加权**不放回**抽 N 条不同 attr(同装备不重复属性)。
|
||||
3. 每条词条在 `[min, max]` 内随机(数值型取整,概率型 1 位小数)。
|
||||
4. 品质 = N。
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 按底座池加权抽取并 roll 出 count 件装备(箱子/抽卡统一入口)
|
||||
* @param pool 底座加权池
|
||||
* @param count 抽取件数
|
||||
* @param allocInstId 实例 id 分配器(每次调用返回新 id)
|
||||
* @param guaranteeMinQuality N 连保底品质(0=不保底)
|
||||
*/
|
||||
export function rollEquipPool(
|
||||
pool: { base_id: number; weight: number }[],
|
||||
count: number,
|
||||
allocInstId: () => number,
|
||||
guaranteeMinQuality: EquipQuality = 0,
|
||||
): HeroEquipInstance[]
|
||||
```
|
||||
|
||||
保底逻辑:当 `count >= GACHA_COUNT` 且全部 < `guaranteeMinQuality` 时,最后一件重 roll 至 ≥ 保底品质。
|
||||
|
||||
### 3.2 可调参数(GameSet.ts)
|
||||
|
||||
```typescript
|
||||
GACHA_COUNT = 10, // N 连连抽数(后期可调 20/30)
|
||||
GACHA_MAX_DRAW = 10, // 单次最多一起抽的张数
|
||||
GACHA_GUARANTEE_QUALITY = 2, // N 连保底品质(稀有)
|
||||
```
|
||||
|
||||
### 3.3 双获取通道
|
||||
|
||||
| 途径 | 流程 | 产出 |
|
||||
|------|------|------|
|
||||
| **装备箱子**(2 档:普通箱/高级箱) | 商店购买/奖励 → `equip_boxes` 库存 → 背包界面点击开箱 | 固定 1 件,走 `rollEquipPool(box.pool, 1)` |
|
||||
| **抽卡**(消耗装备抽卡券) | 抽卡界面选 1 抽 / N 连(默认 10,≤ GACHA_MAX_DRAW)→ 消耗 `equip_tickets` | N 件,走 `rollEquipPool(gachaPool, N, 保底)` |
|
||||
|
||||
- 高级箱与普通箱的差异仅为 `pool` 中底座权重(高词条数底座权重更高)。
|
||||
- 单局结算奖励可发放:箱子(入 `equip_boxes`)和/或装备抽卡券(入 `equip_tickets`)。
|
||||
|
||||
## 4. 局内集成(HeroAttrsComp)
|
||||
|
||||
**原则:局内快照,只读生效。** 开战创建英雄实体时快照装备词条,局内不参与装备增删改。
|
||||
|
||||
### 4.1 快照字段
|
||||
|
||||
```typescript
|
||||
// HeroAttrsComp 新增
|
||||
/** 装备词条加成缓存(局外穿戴装备在开局时快照;数值型=flat,概率型=百分点) */
|
||||
equip_mods: Partial<Record<Attrs, number>> = {};
|
||||
```
|
||||
|
||||
- 不存 `HeroEquipInstance`,只存"每条属性加了多少"的聚合结果(同 attr 多词条快照时已累加)。
|
||||
- `reset()` 中清空。
|
||||
- 局内 UI 若要显示装备名,从 `hero_equips` 反查,不影响战斗数据。
|
||||
|
||||
### 4.2 快照时机
|
||||
|
||||
英雄实体初始化(读 `HeroInfo` 填充属性的同一入口处)追加:
|
||||
|
||||
```typescript
|
||||
const instId = smc.data.hero_equips[hero_uuid];
|
||||
const inst = instId ? smc.data.equips[instId] : null;
|
||||
attrsComp.equip_mods = aggregateAffixes(inst); // HeroEquipSet.ts 纯函数,UI 属性预览可复用
|
||||
```
|
||||
|
||||
### 4.3 结算接入(最小侵入,每个 getFinal 加一项)
|
||||
|
||||
**数值型**:装备 flat **先进基础值,再吃驻场光环百分比**(乘算放大,装备更值钱):
|
||||
|
||||
```typescript
|
||||
public getFinalAp(): number {
|
||||
const equipFlat = this.equip_mods[Attrs.ap] ?? 0;
|
||||
const runtimeAp = this.getRuntimeAp(this.ap + equipFlat);
|
||||
const mods = this.aggregateTimedMods(Attrs.ap);
|
||||
return (runtimeAp + mods.flatSum) * (1 + mods.pctSum / 100);
|
||||
}
|
||||
```
|
||||
|
||||
顺序:装备 flat → 驻场光环 % → 计时 buff。同理 `getFinalHpMax`、`getFinalDefense`。
|
||||
|
||||
**概率型**:百分点直接累加(`base + equip + timed`):
|
||||
|
||||
```typescript
|
||||
public getFinalCritical(): number {
|
||||
const base = this.getRuntimeCritical() + (this.equip_mods[Attrs.critical] ?? 0);
|
||||
const mods = this.aggregateTimedMods(Attrs.critical);
|
||||
return base + mods.flatSum + mods.pctSum;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 接入清单(词条池 attr 白名单)
|
||||
|
||||
| getFinal 方法 | 词条类型 |
|
||||
|---|---|
|
||||
| getFinalAp / getFinalHpMax / getFinalDefense | 数值型 |
|
||||
| getFinalCritical / getFinalCritDamage | 概率型 |
|
||||
| getFinalFreezeChance / getFinalStunChance / getFinalParalyzeChance | 概率型 |
|
||||
| getFinalPunctureChance / getFinalPunctureDmgBonus / getFinalWindFury | 概率型 |
|
||||
| getFinalCriticalRes / getFinalFreezeRes / getFinalStunRes | 概率型 |
|
||||
| getFinalKnockbackChance / getFinalKnockbackDistance / getFinalKnockbackRes | 概率型 |
|
||||
| getFinalVulnerableRes / getFinalParalyzePower | 概率型 |
|
||||
| getEffectiveSkillCd(speed 词条 → 攻速) | 概率型 |
|
||||
|
||||
## 5. 局外数据持久化(SingletonModuleComp.data)
|
||||
|
||||
```typescript
|
||||
data: any = {
|
||||
// ...现有字段
|
||||
/** 玩家装备库:key=inst_id;局外资产随云存档持久化 */
|
||||
equips: {} as Record<number, HeroEquipInstance>,
|
||||
/** 实例 id 自增计数器(云端同步,防 id 冲突) */
|
||||
equip_next_id: 1,
|
||||
/** 英雄穿戴表:key=英雄模板 uuid,value=inst_id(0=未穿戴) */
|
||||
hero_equips: {} as Record<number, number>,
|
||||
/** 装备箱子库存:key=箱子 uuid,value=数量 */
|
||||
equip_boxes: {} as Record<number, number>,
|
||||
/** 装备抽卡券数量 */
|
||||
equip_tickets: 0,
|
||||
}
|
||||
```
|
||||
|
||||
`GameDate` 接口同步补充上述可选字段。
|
||||
|
||||
**数据量评估**:每件装备实例 JSON 约 60~80 字节(只存 `inst_id/base_id/affixes`,不冗余存 name/icon);500 件 ≈ 40KB,远低于微信云存储 1MB 单记录上限。`quality` 不存储(= `affixes.length`)。
|
||||
|
||||
### 数据操作方法(封装在 SingletonModuleComp,UI 不直改数据)
|
||||
|
||||
```typescript
|
||||
addEquipInstances(insts: HeroEquipInstance[]): void // 入库 + markDataDirty + Equip_Update
|
||||
wearEquip(heroUuid: number, instId: number): boolean // 穿戴/替换,旧装自动回库
|
||||
unwearEquip(heroUuid: number): void
|
||||
consumeEquipBox(boxUuid: number): HeroEquipInstance | null // 开箱:扣库存→roll→入库→返回
|
||||
drawEquips(count: number): HeroEquipInstance[] | null // 抽卡:扣券→roll(保底)→入库
|
||||
addEquipTickets(n: number): void
|
||||
addEquipBox(boxUuid: number, n: number): void
|
||||
```
|
||||
|
||||
每个方法内部 `gameDataSync.markDataDirty()` + `oops.message.dispatchEvent` 对应事件。
|
||||
|
||||
### 新增事件(GameEvent.ts)
|
||||
|
||||
```typescript
|
||||
static Equip_Update = "Equip_Update"; // 装备库/穿戴表变更(背包/英雄界面刷新)
|
||||
static EquipBox_Update = "EquipBox_Update"; // 箱子数量变更
|
||||
static EquipTicket_Update = "EquipTicket_Update"; // 抽卡券数量变更
|
||||
```
|
||||
|
||||
监听方必须在 `onDestroy`/`onDisable` 注销(oops 规范)。
|
||||
|
||||
## 6. UI 与交互
|
||||
|
||||
### 6.1 英雄界面(唯一穿戴入口)
|
||||
|
||||
- 英雄详情面板新增**装备槽**(属性区附近):空槽显示 `+`,已穿戴显示装备图标+品质边框。
|
||||
- **穿戴/换装**:点击装备槽 → 弹出装备选择层,仅列**未被任何英雄穿戴**的装备(按品质排序)→ 点击即穿戴/替换(旧装回库)。
|
||||
- **卸下**:已穿戴时选择层提供【卸下】按钮。
|
||||
- 已被其他英雄穿戴的装备在选择层中**不显示**。
|
||||
|
||||
### 6.2 装备背包界面(资产总览 + 开箱)
|
||||
|
||||
- 入口:主界面"装备"按钮(复用 `tip_done.equip` 引导标记)。
|
||||
- 布局:装备网格列表(按品质:传说→史诗→稀有→普通;已穿戴的标记穿戴英雄头像角标)+ 点击看词条详情;顶部**箱子区**(普通箱/高级箱图标+数量),点击开箱 → 展示战利品 → 入库。
|
||||
|
||||
### 6.3 抽卡界面
|
||||
|
||||
- 入口:商店界面新增"装备抽卡"页签。
|
||||
- 【抽 1 次】= 1 张券;【抽 N 次】= N 张券(N 显示当前 `GACHA_COUNT`);券不足按钮置灰。
|
||||
- 结果浮层:网格展示 N 件装备(图标+品质色),点击看词条;保底装备高亮"保底"标记。
|
||||
|
||||
## 7. 现有光环卡改名"战队天赋"
|
||||
|
||||
- `EquipSet.ts` → `TalentSet.ts`(文件改名);`EquipPoolList` → `TalentPoolList`;`drawEquipCards` → `drawTalentCards`;`findEquipByUuid` → `findTalentByUuid`。
|
||||
- UI 文案"装备"→"战队天赋";`tip_done.equip` 字段名不变(避免存档兼容问题),语义指向天赋面板。
|
||||
- 局内组件 `EquipBoxComp` / `MissEquipComp` / `EquipListComp` 等**逻辑不变**,仅引用路径与显示文案调整。
|
||||
|
||||
## 8. 错误处理与边界
|
||||
|
||||
- `rollHeroEquip` 对未知 `base_id` 返回 `null` 并 `mLogger.warn`;`rollEquipPool` 空池返回空数组。
|
||||
- 穿戴时 `instId` 不存在(云端数据异常)→ `wearEquip` 返回 false,不崩溃。
|
||||
- 英雄被穿戴的装备在库中不存在 → 快照时按无装备处理(`equip_mods = {}`)。
|
||||
- 词条池 attr 不在白名单 → 配置加载时 `mLogger.warn` 并跳过该词条。
|
||||
- 旧存档无 `equips` 等新字段 → `Object.assign` 合并后由访问方兜底初始化(参照 `getHeroGrowth` 的懒初始化模式)。
|
||||
|
||||
## 9. 不做的事(YAGNI)
|
||||
|
||||
- 跨局账号级装备强化/分解/合成系统。
|
||||
- 装备部位(武器/盔甲等多槽位)——当前每英雄仅 1 槽。
|
||||
- 数值档位评分影响品质(初版品质=词条数)。
|
||||
- 局内掉落/换装。
|
||||
- 词条重 roll(洗练)。
|
||||
Reference in New Issue
Block a user