Files
pixelheros/docs/superpowers/specs/2026-08-21-hero-equip-design.md
pan e537170812 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.
2026-08-21 10:43:49 +08:00

282 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 英雄个人装备系统设计
- 日期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; // 实例唯一 idequip_next_id 自增)
base_id: number;
affixes: { attr: Attrs; value: number }[]; // quality = affixes.length不单独存储
}
/** 装备箱子配置(段位 8901-89992 档) */
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 | 概率型 |
| getEffectiveSkillCdspeed 词条 → 攻速) | 概率型 |
## 5. 局外数据持久化SingletonModuleComp.data
```typescript
data: any = {
// ...现有字段
/** 玩家装备库key=inst_id局外资产随云存档持久化 */
equips: {} as Record<number, HeroEquipInstance>,
/** 实例 id 自增计数器(云端同步,防 id 冲突) */
equip_next_id: 1,
/** 英雄穿戴表key=英雄模板 uuidvalue=inst_id0=未穿戴) */
hero_equips: {} as Record<number, number>,
/** 装备箱子库存key=箱子 uuidvalue=数量 */
equip_boxes: {} as Record<number, number>,
/** 装备抽卡券数量 */
equip_tickets: 0,
}
```
`GameDate` 接口同步补充上述可选字段。
**数据量评估**:每件装备实例 JSON 约 60~80 字节(只存 `inst_id/base_id/affixes`,不冗余存 name/icon500 件 ≈ 40KB远低于微信云存储 1MB 单记录上限。`quality` 不存储(= `affixes.length`)。
### 数据操作方法(封装在 SingletonModuleCompUI 不直改数据)
```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洗练