From c5d113b35f7551f681c9f5dfc07f50dd087198c1 Mon Sep 17 00:00:00 2001 From: SepComet <2428390463@qq.com> Date: Tue, 17 Mar 2026 13:21:49 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=20convert.py=20=E8=BD=AC?= =?UTF-8?q?=E6=8D=A2=E9=80=BB=E8=BE=91=20+=20=E8=A7=84=E5=88=92=E5=90=8E?= =?UTF-8?q?=E7=BB=AD=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/GameMain/DataTables/Weapon.txt | 8 +- Assets/GameMain/Scripts/DataTable/DRWeapon.cs | 52 ++-- .../Entity/EntityData/Weapon/WeaponData.cs | 25 +- .../EntityData/Weapon/WeaponHandgunData.cs | 9 + .../EntityData/Weapon/WeaponKnifeData.cs | 12 +- .../EntityData/Weapon/WeaponLightningData.cs | 11 + .../EntityData/Weapon/WeaponSlashData.cs | 12 +- .../Weapon/WeaponKnife/WeaponKnife.cs | 12 +- .../Weapon/WeaponLightning/WeaponLightning.cs | 22 +- .../Weapon/WeaponSlash/WeaponSlash.cs | 13 +- docs/CodeX-TODO.md | 255 ++++++++++++++++++ skills/weapon-development/SKILL.md | 42 ++- .../references/WeaponDevelopmentSkill.md | 192 ++++++++++--- 数据表/Entity/Weapon.xlsx | Bin 11293 -> 11316 bytes 数据表/convert.py | 30 +-- 15 files changed, 572 insertions(+), 123 deletions(-) create mode 100644 docs/CodeX-TODO.md diff --git a/Assets/GameMain/DataTables/Weapon.txt b/Assets/GameMain/DataTables/Weapon.txt index 6b69ff2..9c72174 100644 --- a/Assets/GameMain/DataTables/Weapon.txt +++ b/Assets/GameMain/DataTables/Weapon.txt @@ -2,7 +2,7 @@ # Id EntityTypeId Title IconAssetName Rarity Price PriceRandomPercent Attack Cooldown AttackRange AttackSoundId Pramas Modifiers # int int string string RarityType int float int float float int string[] StatModifier[] # 武器编号 策划备注 武器实体编号 武器名 图标资源名 道具品质 武器价格 价格浮动 伤害 冷却 范围 攻击音效编号 额外参数 额外属性 - 1 玩家武器 201 小刀 Almighty_Icon White 120 0.05 100 1.5 5 10000 [hitRadius:2] [] - 2 202 手枪 Almighty_Icon White 130 0.05 120 1 15 10000 [] [] - 3 203 斧头 Almighty_Icon White 100 0.1 150 2 5 10000 [SectorAngle:120] [] - 4 204 闪电 Almighty_Icon White 150 0.08 80 3 12 10000 [hitRadius:3;HoverHeight:10] [] + 1 玩家武器 201 小刀 Almighty_Icon White 120 0.05 100 1.5 5 10000 {"hitRadius":2} [] + 2 202 手枪 Almighty_Icon White 130 0.05 120 1 15 10000 {} [] + 3 203 斧头 Almighty_Icon White 100 0.1 150 2 5 10000 {"SectorAngle":120} [] + 4 204 闪电 Almighty_Icon White 150 0.08 80 3 12 10000 {"hitRadius":3} [] diff --git a/Assets/GameMain/Scripts/DataTable/DRWeapon.cs b/Assets/GameMain/Scripts/DataTable/DRWeapon.cs index 01bba93..edf6b37 100644 --- a/Assets/GameMain/Scripts/DataTable/DRWeapon.cs +++ b/Assets/GameMain/Scripts/DataTable/DRWeapon.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; -using System.Globalization; using Definition.DataStruct; using Definition.Enum; using GameFramework; using CustomUtility; +using Newtonsoft.Json.Linq; using UnityEngine; using UnityGameFramework.Runtime; @@ -74,6 +74,11 @@ namespace DataTable /// public Dictionary Pramas { get; private set; } + /// + /// 获取武器额外参数 Json。 + /// + public string ParamsJson { get; private set; } + /// /// 获取武器额外属性。 /// @@ -97,7 +102,8 @@ namespace DataTable Cooldown = float.Parse(columnStrings[index++]); AttackRange = float.Parse(columnStrings[index++]); AttackSoundId = int.Parse(columnStrings[index++]); - Pramas = DeserializeParams(columnStrings[index++]); + ParamsJson = columnStrings[index++]; + Pramas = DeserializeParams(ParamsJson); Modifiers = Utility.Json.ToObject(columnStrings[index++]); GeneratePropertyArray(); @@ -114,30 +120,38 @@ namespace DataTable /// /// /// - /// private Dictionary DeserializeParams(string rawParams) { - if (!rawParams.StartsWith('[') || !rawParams.EndsWith(']')) - { - throw new ArgumentException("Input must be enclosed in square brackets."); - } - var dict = new Dictionary(); - - if (string.IsNullOrEmpty(rawParams)) return dict; - - string[] items = rawParams.Substring(1, rawParams.Length - 2).Split(";"); - foreach (var item in items) + if (string.IsNullOrWhiteSpace(rawParams)) { - string entry = item.Trim(); - if (string.IsNullOrEmpty(entry)) continue; + return dict; + } - string[] pair = entry.Split(':' , StringSplitOptions.RemoveEmptyEntries); - if (pair.Length != 2) continue; - dict.Add(pair[0].ToLower(), pair[1]); + try + { + JObject paramObject = Utility.Json.ToObject(rawParams); + if (paramObject == null) + { + return dict; + } + + foreach (var pair in paramObject) + { + if (string.IsNullOrWhiteSpace(pair.Key) || pair.Value == null) + { + continue; + } + + dict[pair.Key.ToLower()] = pair.Value.ToString(); + } + } + catch (Exception exception) + { + Log.Warning("Failed to parse weapon params json '{0}'. Error: {1}", rawParams, exception.Message); } return dict; } } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponData.cs b/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponData.cs index 2ac3551..b47958a 100644 --- a/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponData.cs +++ b/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponData.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using DataTable; using Definition.DataStruct; using Definition.Enum; +using GameFramework; namespace Entity.EntityData { @@ -39,6 +40,26 @@ namespace Entity.EntityData return Params.TryGetValue(key.ToLower(), out value); } + protected TParams ParseParams() where TParams : new() + { + if (string.IsNullOrWhiteSpace(_drWeapon.ParamsJson)) + { + return new TParams(); + } + + try + { + TParams parsed = Utility.Json.ToObject(_drWeapon.ParamsJson); + return parsed ?? new TParams(); + } + catch (Exception exception) + { + throw new Exception( + $"Failed to parse weapon params, WeaponType='{WeaponType}', Json='{_drWeapon.ParamsJson}'.", + exception); + } + } + /// /// 攻击力。 /// @@ -80,9 +101,11 @@ namespace Entity.EntityData /// public Dictionary Params => _drWeapon.Pramas; + public string ParamsJson => _drWeapon.ParamsJson; + /// /// 额外属性。 /// public StatModifier[] Modifiers => _drWeapon.Modifiers; } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponHandgunData.cs b/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponHandgunData.cs index b9b1914..c47c6ee 100644 --- a/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponHandgunData.cs +++ b/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponHandgunData.cs @@ -1,12 +1,21 @@ +using System; using Definition.Enum; namespace Entity.EntityData { + [Serializable] + public sealed class WeaponHandgunParamsData + { + } + public class WeaponHandgunData : WeaponData { + public WeaponHandgunParamsData ParamsData { get; } + public WeaponHandgunData(int entityId, int ownerId, CampType ownerCamp) : base(entityId, WeaponType.WeaponHandgun, ownerId, ownerCamp) { + ParamsData = ParseParams(); } } } diff --git a/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponKnifeData.cs b/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponKnifeData.cs index 2bbe261..4fcf4bf 100644 --- a/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponKnifeData.cs +++ b/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponKnifeData.cs @@ -1,12 +1,22 @@ +using System; using Definition.Enum; namespace Entity.EntityData { + [Serializable] + public sealed class WeaponKnifeParamsData + { + public float HitRadius { get; set; } + } + public class WeaponKnifeData : WeaponData { + public WeaponKnifeParamsData ParamsData { get; } + public WeaponKnifeData(int entityId, int ownerId, CampType ownerCamp) : base(entityId, WeaponType.WeaponKnife, ownerId, ownerCamp) { + ParamsData = ParseParams(); } } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponLightningData.cs b/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponLightningData.cs index 5d5ed5e..abe5a03 100644 --- a/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponLightningData.cs +++ b/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponLightningData.cs @@ -1,12 +1,23 @@ +using System; using Definition.Enum; namespace Entity.EntityData { + [Serializable] + public sealed class WeaponLightningParamsData + { + public float HitRadius { get; set; } + public float HoverHeight { get; set; } + } + public class WeaponLightningData : WeaponData { + public WeaponLightningParamsData ParamsData { get; } + public WeaponLightningData(int entityId, int ownerId, CampType ownerCamp) : base(entityId, WeaponType.WeaponLightning, ownerId, ownerCamp) { + ParamsData = ParseParams(); } } } diff --git a/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponSlashData.cs b/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponSlashData.cs index 99a7381..f500788 100644 --- a/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponSlashData.cs +++ b/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponSlashData.cs @@ -1,12 +1,22 @@ +using System; using Definition.Enum; namespace Entity.EntityData { + [Serializable] + public sealed class WeaponSlashParamsData + { + public float SectorAngle { get; set; } + } + public class WeaponSlashData : WeaponData { + public WeaponSlashParamsData ParamsData { get; } + public WeaponSlashData(int entityId, int ownerId, CampType ownerCamp) : base(entityId, WeaponType.WeaponSlash, ownerId, ownerCamp) { + ParamsData = ParseParams(); } } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponKnife/WeaponKnife.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponKnife/WeaponKnife.cs index 038c5ce..6458605 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponKnife/WeaponKnife.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponKnife/WeaponKnife.cs @@ -11,8 +11,6 @@ namespace Entity.Weapon { public partial class WeaponKnife : WeaponBase { - private const string HitRadiusParamKey = "HitRadius"; - private WeaponKnifeData _weaponData; private Quaternion _cachedRotation; @@ -157,14 +155,8 @@ namespace Entity.Weapon _sqrRange = _weaponData.AttackRange * _weaponData.AttackRange; _cachedRotation = CachedTransform.rotation; - if (_weaponData.TryGetParam(HitRadiusParamKey, out string hitRadiusRaw)) - { - _hitRadius = Mathf.Max(0.1f, float.Parse(hitRadiusRaw)); - } - else - { - _hitRadius = _weaponData.AttackRange; - } + float configuredHitRadius = _weaponData.ParamsData != null ? _weaponData.ParamsData.HitRadius : 0f; + _hitRadius = configuredHitRadius > 0f ? Mathf.Max(0.1f, configuredHitRadius) : _weaponData.AttackRange; _hitRadiusSqr = _hitRadius * _hitRadius; _attackEffect = new KnifeRangeAttackEffect(); diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponLightning/WeaponLightning.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponLightning/WeaponLightning.cs index 83de6b9..746a343 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponLightning/WeaponLightning.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponLightning/WeaponLightning.cs @@ -11,8 +11,6 @@ namespace Entity.Weapon { public partial class WeaponLightning : WeaponBase { - private const string HitRadiusParamKey = "HitRadius"; - private WeaponLightningData _weaponData; private Quaternion _cachedRotation; @@ -178,8 +176,14 @@ namespace Entity.Weapon _sqrRange = _weaponData.AttackRange * _weaponData.AttackRange; _cachedRotation = CachedTransform.rotation; - _hitRadius = ReadPositiveParam(HitRadiusParamKey, _weaponData.AttackRange); + float configuredHitRadius = _weaponData.ParamsData != null ? _weaponData.ParamsData.HitRadius : 0f; + _hitRadius = configuredHitRadius > 0f ? Mathf.Max(0.1f, configuredHitRadius) : _weaponData.AttackRange; _hitRadiusSqr = _hitRadius * _hitRadius; + float configuredHoverHeight = _weaponData.ParamsData != null ? _weaponData.ParamsData.HoverHeight : 0f; + if (configuredHoverHeight > 0f) + { + _hoverHeight = Mathf.Max(0.1f, configuredHoverHeight); + } int colliderCapacity = Mathf.Max(1, _maxHitColliders); if (_hitResults == null || _hitResults.Length != colliderCapacity) @@ -228,18 +232,6 @@ namespace Entity.Weapon } } - private float ReadPositiveParam(string paramName, float defaultValue) - { - if (_weaponData.Params != null && - _weaponData.Params.TryGetValue(paramName.ToLower(), out string rawValue) && - float.TryParse(rawValue, out float parsedValue)) - { - return Mathf.Max(0.1f, parsedValue); - } - - return Mathf.Max(0.1f, defaultValue); - } - private void StopAttackTween(bool resetTransform) { if (_attackSequence != null) diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponSlash/WeaponSlash.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponSlash/WeaponSlash.cs index 3067dc9..53c0d35 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponSlash/WeaponSlash.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponSlash/WeaponSlash.cs @@ -13,8 +13,6 @@ namespace Entity.Weapon { #region Property - private const string SectorAngleParamKey = "SectorAngle"; - private WeaponSlashData _weaponData; private Quaternion _cachedRotation; @@ -184,15 +182,8 @@ namespace Entity.Weapon _attackRadius = Mathf.Max(0.1f, _weaponData.AttackRange); _attackRadiusSqr = _attackRadius * _attackRadius; - _sectorAngle = 90f; - if (_weaponData.Params != null && - _weaponData.Params.TryGetValue(SectorAngleParamKey.ToLower(), out string rawAngle)) - { - if (float.TryParse(rawAngle, out float parsedAngle)) - { - _sectorAngle = Mathf.Clamp(parsedAngle, 1f, 360f); - } - } + float configuredSectorAngle = _weaponData.ParamsData != null ? _weaponData.ParamsData.SectorAngle : 0f; + _sectorAngle = configuredSectorAngle > 0f ? Mathf.Clamp(configuredSectorAngle, 1f, 360f) : 90f; int capacity = Mathf.Max(1, _maxHitColliders); if (_hitResults == null || _hitResults.Length != capacity) diff --git a/docs/CodeX-TODO.md b/docs/CodeX-TODO.md new file mode 100644 index 0000000..4ef1548 --- /dev/null +++ b/docs/CodeX-TODO.md @@ -0,0 +1,255 @@ +# CodeX TODO + +## Weapon 现状梳理 + +### 1. 数据层结构 + +- `Assets/GameMain/DataTables/Weapon.txt` + - 武器基础数据表。 + - 当前 `Params` 列已经切换为标准 JSON 对象。 + - 约束: + - 空参数统一写 `{}` + - 不再兼容 `[]` + - key 名应与对应 `ParamsData` 属性名一致 + +- `Assets/GameMain/Scripts/DataTable/DRWeapon.cs` + - 负责解析武器表基础字段。 + - 保留两份参数视图: + - `ParamsJson` + - 原始 JSON 字符串,供 `WeaponData` 强类型反序列化使用。 + - `Pramas` + - 由 `ParamsJson` 转成的 `Dictionary`。 + - 仅用于描述/UI 等弱类型读取场景。 + +- `Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponData.cs` + - 保留所有武器共用字段: + - `Attack` + - `Cooldown` + - `AttackRange` + - `Price` + - `Rarity` + - `Modifiers` + - `ParamsJson` + - `Params` + - 提供 `ParseParams()` 作为武器子类的强类型参数解析入口。 + +- `Assets/GameMain/Scripts/Entity/EntityData/Weapon/*` + - 每个具体武器子类持有自己的 `ParamsData`: + - `WeaponKnifeData -> WeaponKnifeParamsData` + - `WeaponHandgunData -> WeaponHandgunParamsData` + - `WeaponSlashData -> WeaponSlashParamsData` + - `WeaponLightningData -> WeaponLightningParamsData` + +### 2. 逻辑层结构 + +- `Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponBase.cs` + - 武器统一基类。 + - 负责: + - 生命周期 + - 状态机切换 + - 选敌 + - Simulation area/sector query 接入 + - 绑定玩家攻击属性 + +- 当前已有四种武器实现模板: + - `WeaponKnife` + - 近身前刺 + 圆形范围命中 + - `WeaponHandgun` + - 单次 Raycast 瞬发命中 + - `WeaponSlash` + - 扇形范围命中 + - `WeaponLightning` + - 锁定目标点 + 落点范围打击 + +- 参数读取方式 + - 已改为在具体武器中直接读取对应 `ParamsData` + - 不再在武器逻辑中手动解析字符串参数 + +### 3. 当前已接通的参数 + +- `WeaponKnifeParamsData` + - `HitRadius` + +- `WeaponHandgunParamsData` + - 暂无字段,待扩展 + +- `WeaponSlashParamsData` + - `SectorAngle` + +- `WeaponLightningParamsData` + - `HitRadius` + - `HoverHeight` + +### 4. 当前结构的优点 + +- 公共字段仍集中在 `WeaponData`,不会把通用逻辑拆散 +- 武器专属参数已经强类型化,初始化更稳定 +- 数据表可读性比旧的 KV 字符串格式更高 +- 新武器扩展时,可以复用: + - 表 + - `DRWeapon` + - `WeaponData` + - `WeaponBase` + - AttackEffect + - Simulation area/sector query + +### 5. 当前结构的限制 + +- 仍然不是“纯配置驱动行为” + - 行为差异主要还在具体武器类里 +- `Handgun` 参数化程度还不够 + - 目前还不适合直接高效派生霰弹枪、狙击枪、 burst 枪 +- `Pramas` 命名拼写仍保留旧名字 + - 目前为了兼容存量调用,暂不改 + +## Weapon 扩展计划 + +### P0: 稳定当前数据链路 + +- 检查 `Weapon.txt` 中全部行: + - `Params` 必须都是 JSON 对象 + - 空参数统一为 `{}` +- 检查后续新增字段时: + - 数据表 key 与 `ParamsData` 属性名保持一致 +- 补一轮基础验证: + - 商店描述 + - 玩家初始武器生成 + - 商店购买武器生成 + +### P1: 先把 Handgun 参数化做完整 + +目标: +- 把 `WeaponHandgun` 做成远程枪械母版,而不是只有一把“手枪” + +建议新增参数: +- `PelletCount` +- `SpreadAngle` +- `PenetrationCount` +- `FireOriginOffsetX` +- `FireOriginOffsetY` +- `FireOriginOffsetZ` +- `HitMarkerSize` +- `HitMarkerYOffset` +- `HitMarkerDuration` + +落地后可直接派生: +- 手枪 +- 霰弹枪 +- 狙击枪 +- 三连发手炮 + +### P2: 优先做低成本高收益的新武器 + +优先顺序建议: + +1. 长枪 / 刺剑 +- 基于 `WeaponKnife` +- 重点调: + - 前刺距离 + - 命中半径 + - 冷却 + +2. 大剑 / 半月斩 +- 基于 `WeaponSlash` +- 重点调: + - `SectorAngle` + - 攻击范围 + - 动画时长 + +3. 战锤 / 震地锤 +- 基于 `WeaponLightning` 或 `WeaponKnife` +- 重点调: + - 落点半径 + - 前摇 + - 低频高伤 + +4. 霰弹枪 +- 基于参数化后的 `WeaponHandgun` +- 重点调: + - 散射 + - 多 pellet + - 近距离爆发 + +5. 狙击枪 +- 基于参数化后的 `WeaponHandgun` +- 重点调: + - 单发高伤 + - 超远射程 + - 慢冷却 + +6. 陨石杖 / 圣光柱 +- 基于 `WeaponLightning` +- 重点调: + - `HoverHeight` + - 爆炸半径 + - 冷却 + +### P3: 中成本扩展 + +1. 链式闪电 +- 在首目标命中后,继续寻找附近目标 +- 需要新增: + - 连锁次数 + - 连锁半径 + - 每跳衰减 + +2. 穿透弹 / 火球 +- 复用现有 projectile/simulation 基础 +- 需要明确: + - 穿透次数 + - 命中后是否爆炸 + +3. 地雷 / 陷阱 +- 本质是延时触发 area hit +- 需要新增: + - 布置后触发时机 + - 持续时间 + - 触发半径 + +4. 回旋镖 +- 需要双阶段投射物状态 +- 成本高于普通枪械/范围武器 + +### P4: 暂缓项 + +以下方向暂不建议优先投入: + +- 持续激光 +- 喷火器 +- 环绕飞剑 +- 常驻法球 +- 冰冻/中毒/击退等状态驱动武器流派 + +原因: +- 当前武器框架核心仍是“单次攻击结算” +- 持续伤害与异常状态还没有形成统一挂点 + +## 新武器接入步骤模板 + +1. 在 `Weapon.txt` 新增一行 +- 配好基础字段 +- `Params` 写 JSON 对象 + +2. 新增 `WeaponType` +- 在 `Assets/GameMain/Scripts/Definition/Enum/WeaponType.cs` + +3. 新增武器数据子类 +- 新建 `WeaponXXXData` +- 新建 `WeaponXXXParamsData` +- 在构造里调用 `ParseParams()` + +4. 新增武器逻辑类 +- 继承 `WeaponBase` +- 接入状态机 +- 读取 `ParamsData` + +5. 接入生成入口 +- 玩家初始武器 +- 商店购买武器 +- 其他掉落/奖励入口 + +6. 验证点 +- 武器生成正确 +- 参数生效正确 +- 描述文本正确 +- Simulation 模式和非 Simulation 模式都能命中 diff --git a/skills/weapon-development/SKILL.md b/skills/weapon-development/SKILL.md index 4ec3047..d0bdc7b 100644 --- a/skills/weapon-development/SKILL.md +++ b/skills/weapon-development/SKILL.md @@ -13,20 +13,30 @@ description: Develop and extend the VampireLike weapon system. Use when creating - state flow (`Idle`, `Check_OutRange`, `Check_InRange`, `Attack`) - target selector (`ITargetSelector`, `TargetSelectorType`) - effect layer (`IWeaponAttackEffect`) - - data contract (`DRWeapon`, `WeaponData`) -3. Keep behavior compatibility with current gameplay loop and UI/event chain. + - data contract (`DRWeapon`, `WeaponData`, `ParamsData`) +3. Keep behavior compatibility with current gameplay loop, shop flow, inventory flow, and UI/event chain. ## Source Map -- Weapon base: `../../Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponBase.cs` +- Weapon base: + - `../../Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponBase.cs` - Existing weapons: - `../../Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponKnife/` - `../../Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponHandgun/` - - `../../Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponSlash.cs` + - `../../Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponSlash/` + - `../../Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponLightning/` - Selectors: - `../../Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/TargetSelector/` +- Attack effects: + - `../../Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/AttackEffects/` - Weapon data: - `../../Assets/GameMain/Scripts/Entity/EntityData/Weapon/` +- Weapon table: + - `../../Assets/GameMain/DataTables/Weapon.txt` +- Data row: + - `../../Assets/GameMain/Scripts/DataTable/DRWeapon.cs` +- Shop integration: + - `../../Assets/GameMain/Scripts/UI/GameScene/UseCase/ShopFormUseCase.cs` - Entity show flow: - `../../Assets/GameMain/Scripts/Entity/EntityExtension.cs` @@ -36,7 +46,8 @@ description: Develop and extend the VampireLike weapon system. Use when creating - Keep state transitions explicit and non-blocking. - Keep cooldown accumulation valid even when no target is found. - Keep attack logic and visual effect logic decoupled. -- Use safe parsing (`TryParse` + defaults) for runtime weapon parameters. +- Parse weapon-specific parameters into strong-typed `ParamsData` at data initialization time. +- Treat `Weapon.txt` `Params` as a JSON object column; empty params must use `{}`. - Preserve compatibility with shop/inventory/UI refresh flow. ## Change Recipes @@ -44,17 +55,18 @@ description: Develop and extend the VampireLike weapon system. Use when creating ### Add a New Weapon 1. Extend `WeaponType` without reordering existing enum values. -2. Add `WeaponXxxData : WeaponData` for strong-typed fields. -3. Add `WeaponXxx : WeaponBase` and implement only weapon-specific behavior. -4. Build state files under `Weapon/WeaponXxx/` with partial class layout. -5. Register display/data mapping path so `ShowWeapon` can instantiate correctly. +2. Add `WeaponXxxData : WeaponData` and `WeaponXxxParamsData` for weapon-specific parameters. +3. Parse `ParamsJson` through `ParseParams()` inside the weapon data constructor. +4. Add `WeaponXxx : WeaponBase` and implement only weapon-specific behavior. +5. Build state files under `Weapon/WeaponXxx/` with partial class layout. +6. Register display/data mapping path so `ShowWeapon` and shop purchase can instantiate correctly. ### Add or Update a Target Selector 1. Implement `ITargetSelector`. 2. Update `TargetSelectorType`. 3. Register creation in `WeaponBase.CreateSelector`. -4. Validate target semantics with current-health rules where applicable. +4. Validate target semantics with current-health/runtime-health rules where applicable. ### Add or Update Attack Effect @@ -62,11 +74,19 @@ description: Develop and extend the VampireLike weapon system. Use when creating 2. Trigger effect from weapon attack state only. 3. Keep damage resolution outside effect code. +### Add or Update Weapon Parameters + +1. Update `Weapon.txt` `Params` JSON object. +2. Add or update the corresponding `WeaponXxxParamsData` fields. +3. Keep key names aligned with `ParamsData` property names. +4. Read values from `ParamsData` in weapon initialization, not by manual string parsing in runtime logic. + ## Validation Checklist - Weapon can be shown, attached, and updated without exceptions. - State machine does not stall across target loss/reacquire. - Cooldown and range checks match design expectation. - Damage path and effect path remain decoupled. +- `ParamsData` matches table content and default fallback behavior. - UI/shop/inventory interactions stay stable after the change. -- Update `./references/WeaponDevelopmentSkill.md` if contracts changed. +- Update `./references/WeaponDevelopmentSkill.md` if contracts or recommended patterns changed. diff --git a/skills/weapon-development/references/WeaponDevelopmentSkill.md b/skills/weapon-development/references/WeaponDevelopmentSkill.md index 875a989..b8f75c1 100644 --- a/skills/weapon-development/references/WeaponDevelopmentSkill.md +++ b/skills/weapon-development/references/WeaponDevelopmentSkill.md @@ -1,16 +1,17 @@ -# Weapon Development Skill(VampireLike) +# Weapon Development Skill(VampireLike) ## 目标 本文件是 `Entity.Weapon` 体系的开发规范与速查手册。 -后续新增武器或扩展机制时,优先按本文档执行,避免重复通读历史上下文。 +后续新增武器、扩展参数、调整状态机或联动商店/背包时,优先按本文档执行,避免重复通读历史上下文。 ## 当前架构总览 - 武器运行时入口:`WeaponBase`(`Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponBase.cs`) -- 武器具体实现:`WeaponKnife`、`WeaponHandgun`、`WeaponSlash` +- 武器具体实现:`WeaponKnife`、`WeaponHandgun`、`WeaponSlash`、`WeaponLightning` - 目标选择策略:`ITargetSelector` + `TargetSelectorType` - 攻击可视化:`IWeaponAttackEffect` - 数据入口:`DRWeapon` -> `WeaponData`(及其子类) - 实体生成:`EntityExtension.ShowWeapon` +- 商店接入:`ShopFormUseCase.CreateWeaponData` ## WeaponBase 统一职责(已上收) `WeaponBase` 负责以下通用逻辑,子类不要重复实现: @@ -20,8 +21,34 @@ - 启用门控:`OnUpdate` 中统一 `if (!_isEnabled) return` - 目标选择入口:`SelectTarget`、`SetTargetSelector`、`CreateSelector` - 距离判定:`IsInRange`(基于 XZ 平面距离) +- Simulation 命中请求:`TryQueueAreaCollisionQuery`、`TryQueueSectorCollisionQuery` - 玩家攻击属性订阅:`BindAttackStatFromOwner` / `ReleaseAttackStatSubscription` +约束: +- 不要在子类里重复实现 `WeaponBase` 已有能力。 +- 行为差异优先收敛在:`BuildStates`、`Check`、`Attack`、少量专属辅助方法。 + +## 当前武器模板分类 +### 1. `WeaponKnife` +- 模式:近身前刺 + 圆形范围命中 +- 典型参数:`HitRadius` +- 适合派生:长枪、刺剑、短矛、震地锤近身版 + +### 2. `WeaponHandgun` +- 模式:单次 `Raycast` 瞬发命中 +- 当前参数化程度较低,仍适合作为远程枪械母版继续扩展 +- 适合派生:手枪、霰弹枪、狙击枪、三连发枪 + +### 3. `WeaponSlash` +- 模式:扇形范围命中 +- 典型参数:`SectorAngle` +- 适合派生:大剑、斧头、半月斩、横扫类武器 + +### 4. `WeaponLightning` +- 模式:锁定目标点 + 落点范围打击 +- 典型参数:`HitRadius`、`HoverHeight` +- 适合派生:闪电、陨石杖、圣光柱、空袭类武器 + ## 状态机约定 统一状态枚举: - `Idle` @@ -39,12 +66,14 @@ 关键规则: - 即使没有目标,也允许蓄力(计时器持续走)。 - 一旦进入 `Check_InRange` 且冷却已满,应立即触发攻击。 +- 攻击过程中若需要多帧动画,使用 `_isAttacking` 控制状态退出时机。 ## 3D 场景下的距离/朝向原则 - 射程与目标筛选:优先使用 XZ 平面距离(忽略 Y),调用 `AIUtility.GetSqrMagnitudeXZ`。 - 视觉朝向与弹道:可按武器设计决定是否使用完整 3D 向量。 - `WeaponHandgun`:允许俯仰瞄准,逻辑上用射线命中对象判定伤害。 - 近战地面范围类(Knife/Slash):伤害检测建议投影到地面(XZ)再判定。 + - 落点类(Lightning):锁点可取目标当前位置,但范围判定仍建议按地面距离收口。 ## 目标选择策略规范 接口:`ITargetSelector.SelectTarget(WeaponBase weapon, IEnumerable candidates, float maxSqrRange)` @@ -55,14 +84,15 @@ - `LowestHealthTargetSelector` 语义约定: -- `HighestHealth` / `LowestHealth` 必须按“当前血量”筛选。 -- 当前实现读取 `HealthComponent.CurrentHealth`。 +- `NearestTargetSelector` 在 Simulation 启用时优先走空间索引。 +- `HighestHealth` / `LowestHealth` 当前基于运行时生命值选择目标。 +- 若新增新策略,必须明确“按当前值”还是“按最大值”筛选,避免语义漂移。 扩展策略步骤: 1. 新建 selector 类并实现 `ITargetSelector`。 2. 更新 `TargetSelectorType` 枚举。 3. 在 `WeaponBase.CreateSelector` 中注册。 -4. 武器在 `OnWeaponShow` 或构造阶段选择策略。 +4. 武器在 `OnWeaponShow` 或初始化阶段选择策略。 ## 攻击可视化效果规范 接口:`IWeaponAttackEffect.Play(WeaponBase weapon, Vector3 position, EntityBase target, float radius)` @@ -71,25 +101,68 @@ - 可视化逻辑与伤害逻辑解耦。 - 武器类只负责触发 `Play`,不把可视化细节塞回武器核心逻辑。 - 当前阶段允许临时对象创建;后续若有性能压力再统一对象池化。 +- 命中特效、范围预警、扇形描边都属于 effect 层,不属于伤害层。 ## 数据层规范(DRWeapon / WeaponData) -`DRWeapon` 提供通用字段: -- `Attack`、`Cooldown`、`AttackRange`、`AttackSoundId` -- `Pramas`(字典,Key 建议统一转小写) +### `DRWeapon` +提供通用字段: +- `Attack` +- `Cooldown` +- `AttackRange` +- `AttackSoundId` +- `ParamsJson` +- `Pramas` - `Modifiers` 约定: -- 参数解析尽量在数据层/初始化阶段完成。 -- 武器逻辑层读取 `WeaponData` 的强类型结果,不要散落 `Parse`。 -- 解析时必须容错:优先 `TryParse` + 默认值,避免运行时异常。 +- `Params` 列现在使用标准 JSON 对象。 +- 空参数统一写 `{}`。 +- 不再兼容 `[]`。 +- `Pramas` 保留为描述/UI 的兼容字典视图,不再作为武器逻辑主读取入口。 + +### `WeaponData` +提供公共武器字段与通用解析入口: +- 公共战斗字段:`Attack`、`Cooldown`、`AttackRange` +- 公共展示字段:`Title`、`IconAssetName`、`Rarity`、`Price` +- 参数入口:`ParamsJson`、`Params` +- 通用强类型解析:`ParseParams()` + +约定: +- 参数解析优先在数据层完成。 +- 武器逻辑层读取强类型 `ParamsData`,不要散落字符串 `Parse`。 +- 只有描述/UI 等弱类型场景才继续读取 `Params` 字典。 + +### 具体武器数据子类 +每种武器数据子类都应持有自己的 `ParamsData`: +- `WeaponKnifeData -> WeaponKnifeParamsData` +- `WeaponHandgunData -> WeaponHandgunParamsData` +- `WeaponSlashData -> WeaponSlashParamsData` +- `WeaponLightningData -> WeaponLightningParamsData` + +当前已接通字段: +- `WeaponKnifeParamsData` + - `HitRadius` +- `WeaponHandgunParamsData` + - 暂无字段 +- `WeaponSlashParamsData` + - `SectorAngle` +- `WeaponLightningParamsData` + - `HitRadius` + - `HoverHeight` + +JSON 约束: +- key 名应与 `ParamsData` 属性名一致。 +- 统一使用 JSON 对象,不要再使用自定义 KV 串。 +- 不建议在 `ParamsJson` 中使用制表符和跨行内容,避免 txt 表分列出错。 ## 新增武器标准流程 1. 定义枚举 - - 更新 `WeaponType`(保持递增值,避免重排已有值)。 + - 更新 `WeaponType`,保持递增值,避免重排已有值。 2. 建立数据类 - 新建 `WeaponXxxData : WeaponData`。 - - 如果有独有参数,提供强类型字段或统一初始化逻辑。 + - 新建 `WeaponXxxParamsData`。 + - 在构造阶段调用 `ParseParams()`,把参数初始化为强类型字段。 3. 建立行为类 - 新建 `WeaponXxx : WeaponBase`。 @@ -106,29 +179,84 @@ 5. 建立可视化(可选) - 新建 `WeaponXxxAttackEffect : IWeaponAttackEffect`,在武器中组合调用。 -6. 若为远程武器,建立子弹实体 - - `BulletXxx : Bullet` - - `BulletXxxData : BulletData` - - 通过武器赋予伤害/阵营参数。 - - 自动销毁应走对象池回收。 - -7. 接入实体展示与数据表 +6. 接入实体展示与数据表 - 确保 `DRWeapon` / `DREntity` 配置齐全。 - `EntityExtension.ShowWeapon` 可正确映射到 `Entity.Weapon.WeaponXxx`。 + - 商店/背包创建入口能正确构造 `WeaponXxxData`。 -8. 联动系统 +7. 联动系统 - 背包、商店购买/出售、UI 展示、事件流刷新。 +8. 验证点 + - 武器能正确生成、附着、更新。 + - `ParamsData` 与数据表一致。 + - 描述文本仍正确展示。 + - Simulation 模式和非 Simulation 模式都能命中。 + +## 扩展优先级建议 +### 第一批:低成本高收益 +- 长枪 / 刺剑 + - 基于 `WeaponKnife` +- 大剑 / 半月斩 + - 基于 `WeaponSlash` +- 战锤 / 震地锤 + - 基于 `WeaponLightning` 或 `WeaponKnife` +- 霰弹枪 + - 基于参数化后的 `WeaponHandgun` +- 狙击枪 + - 基于参数化后的 `WeaponHandgun` +- 陨石杖 / 圣光柱 + - 基于 `WeaponLightning` + +### 第二批:中成本扩展 +- 链式闪电 +- 穿透弹 / 火球 +- 地雷 / 陷阱 +- 回旋镖 + +### 暂缓项 +- 持续激光 +- 喷火器 +- 环绕飞剑 +- 常驻法球 +- 状态异常驱动的复杂武器流派 + +原因: +- 当前武器主流程仍是单次攻击结算。 +- 持续伤害、异常状态和常驻 orbit 行为尚未形成统一挂点。 + +## `WeaponHandgun` 后续建议 +当前 `WeaponHandgun` 参数化仍偏弱,建议作为下一阶段母版优先扩展。 + +建议新增字段: +- `PelletCount` +- `SpreadAngle` +- `PenetrationCount` +- `FireOriginOffsetX` +- `FireOriginOffsetY` +- `FireOriginOffsetZ` +- `HitMarkerSize` +- `HitMarkerYOffset` +- `HitMarkerDuration` + +完成后可快速派生: +- 手枪 +- 霰弹枪 +- 狙击枪 +- 三连发枪 + ## 代码检查清单(提交前) - - 是否重复实现了 `WeaponBase` 已有通用逻辑。 - - 状态流转是否会卡死或漏转场。 - - 冷却计时是否在无目标时仍正常累积。 - - 目标失效(null/Unavailable/死亡)是否安全处理。 - - 命中检测是否符合武器设计(地面范围/射线/扇形)。 - - 数据参数是否全部容错解析。 - - 可视化是否与伤害逻辑解耦。 - - 是否正确订阅/解绑攻击属性(或复用基类方法)。 +- 是否重复实现了 `WeaponBase` 已有通用逻辑。 +- 状态流转是否会卡死或漏转场。 +- 冷却计时是否在无目标时仍正常累积。 +- 目标失效(null/Unavailable/死亡)是否安全处理。 +- 命中检测是否符合武器设计(地面范围/射线/扇形/落点)。 +- 数据参数是否全部收敛到 `ParamsData`。 +- 可视化是否与伤害逻辑解耦。 +- 是否正确订阅/解绑攻击属性(或复用基类方法)。 +- 商店、背包、武器描述是否仍保持兼容。 ## 已知注意点 - - 目录中存在 `Pramas` 命名拼写历史包袱,保持兼容即可。 - - 文档中的“规范”优先级高于历史实现;历史实现若偏离,按本规范逐步收敛。 +- 目录中存在 `Pramas` 命名拼写历史包袱,当前保持兼容即可。 +- 文档中的“规范”优先级高于历史实现;历史实现若偏离,按本规范逐步收敛。 +- 当前更推荐“公共 `WeaponData` + 专属 `ParamsData`”结构,不建议把每种武器做成一套完全独立的数据总线。 diff --git a/数据表/Entity/Weapon.xlsx b/数据表/Entity/Weapon.xlsx index ed04d94d5edf42b70dfd5128fbef56c4f034ae77..15e9b26454551c8e7cb34ad238513fda5c4450e3 100644 GIT binary patch delta 3082 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 YyelsPnGBO*D;1LhEENWeCjbBd0OgFIrT_o{ delta 3031 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 {output_file}") count += 1