调整 convert.py 转换逻辑 + 规划后续开发

This commit is contained in:
SepComet 2026-03-17 13:21:49 +08:00
parent 59f1d02a18
commit c5d113b35f
15 changed files with 572 additions and 123 deletions

View File

@ -2,7 +2,7 @@
# Id EntityTypeId Title IconAssetName Rarity Price PriceRandomPercent Attack Cooldown AttackRange AttackSoundId Pramas Modifiers # 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[] # 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] [] 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 [] [] 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] [] 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] [] 4 204 闪电 Almighty_Icon White 150 0.08 80 3 12 10000 {"hitRadius":3} []

View File

@ -1,10 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using Definition.DataStruct; using Definition.DataStruct;
using Definition.Enum; using Definition.Enum;
using GameFramework; using GameFramework;
using CustomUtility; using CustomUtility;
using Newtonsoft.Json.Linq;
using UnityEngine; using UnityEngine;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
@ -74,6 +74,11 @@ namespace DataTable
/// </summary> /// </summary>
public Dictionary<string, string> Pramas { get; private set; } public Dictionary<string, string> Pramas { get; private set; }
/// <summary>
/// 获取武器额外参数 Json。
/// </summary>
public string ParamsJson { get; private set; }
/// <summary> /// <summary>
/// 获取武器额外属性。 /// 获取武器额外属性。
/// </summary> /// </summary>
@ -97,7 +102,8 @@ namespace DataTable
Cooldown = float.Parse(columnStrings[index++]); Cooldown = float.Parse(columnStrings[index++]);
AttackRange = float.Parse(columnStrings[index++]); AttackRange = float.Parse(columnStrings[index++]);
AttackSoundId = int.Parse(columnStrings[index++]); AttackSoundId = int.Parse(columnStrings[index++]);
Pramas = DeserializeParams(columnStrings[index++]); ParamsJson = columnStrings[index++];
Pramas = DeserializeParams(ParamsJson);
Modifiers = Utility.Json.ToObject<StatModifier[]>(columnStrings[index++]); Modifiers = Utility.Json.ToObject<StatModifier[]>(columnStrings[index++]);
GeneratePropertyArray(); GeneratePropertyArray();
@ -114,30 +120,38 @@ namespace DataTable
/// </summary> /// </summary>
/// <param name="rawParams"></param> /// <param name="rawParams"></param>
/// <returns></returns> /// <returns></returns>
/// <exception cref="ArgumentException"></exception>
private Dictionary<string, string> DeserializeParams(string rawParams) private Dictionary<string, string> DeserializeParams(string rawParams)
{ {
if (!rawParams.StartsWith('[') || !rawParams.EndsWith(']'))
{
throw new ArgumentException("Input must be enclosed in square brackets.");
}
var dict = new Dictionary<string, string>(); var dict = new Dictionary<string, string>();
if (string.IsNullOrWhiteSpace(rawParams))
if (string.IsNullOrEmpty(rawParams)) return dict;
string[] items = rawParams.Substring(1, rawParams.Length - 2).Split(";");
foreach (var item in items)
{ {
string entry = item.Trim(); return dict;
if (string.IsNullOrEmpty(entry)) continue; }
string[] pair = entry.Split(':' , StringSplitOptions.RemoveEmptyEntries); try
if (pair.Length != 2) continue; {
dict.Add(pair[0].ToLower(), pair[1]); JObject paramObject = Utility.Json.ToObject<JObject>(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; return dict;
} }
} }
} }

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using DataTable; using DataTable;
using Definition.DataStruct; using Definition.DataStruct;
using Definition.Enum; using Definition.Enum;
using GameFramework;
namespace Entity.EntityData namespace Entity.EntityData
{ {
@ -39,6 +40,26 @@ namespace Entity.EntityData
return Params.TryGetValue(key.ToLower(), out value); return Params.TryGetValue(key.ToLower(), out value);
} }
protected TParams ParseParams<TParams>() where TParams : new()
{
if (string.IsNullOrWhiteSpace(_drWeapon.ParamsJson))
{
return new TParams();
}
try
{
TParams parsed = Utility.Json.ToObject<TParams>(_drWeapon.ParamsJson);
return parsed ?? new TParams();
}
catch (Exception exception)
{
throw new Exception(
$"Failed to parse weapon params, WeaponType='{WeaponType}', Json='{_drWeapon.ParamsJson}'.",
exception);
}
}
/// <summary> /// <summary>
/// 攻击力。 /// 攻击力。
/// </summary> /// </summary>
@ -80,9 +101,11 @@ namespace Entity.EntityData
/// </summary> /// </summary>
public Dictionary<string, string> Params => _drWeapon.Pramas; public Dictionary<string, string> Params => _drWeapon.Pramas;
public string ParamsJson => _drWeapon.ParamsJson;
/// <summary> /// <summary>
/// 额外属性。 /// 额外属性。
/// </summary> /// </summary>
public StatModifier[] Modifiers => _drWeapon.Modifiers; public StatModifier[] Modifiers => _drWeapon.Modifiers;
} }
} }

View File

@ -1,12 +1,21 @@
using System;
using Definition.Enum; using Definition.Enum;
namespace Entity.EntityData namespace Entity.EntityData
{ {
[Serializable]
public sealed class WeaponHandgunParamsData
{
}
public class WeaponHandgunData : WeaponData public class WeaponHandgunData : WeaponData
{ {
public WeaponHandgunParamsData ParamsData { get; }
public WeaponHandgunData(int entityId, int ownerId, CampType ownerCamp) public WeaponHandgunData(int entityId, int ownerId, CampType ownerCamp)
: base(entityId, WeaponType.WeaponHandgun, ownerId, ownerCamp) : base(entityId, WeaponType.WeaponHandgun, ownerId, ownerCamp)
{ {
ParamsData = ParseParams<WeaponHandgunParamsData>();
} }
} }
} }

View File

@ -1,12 +1,22 @@
using System;
using Definition.Enum; using Definition.Enum;
namespace Entity.EntityData namespace Entity.EntityData
{ {
[Serializable]
public sealed class WeaponKnifeParamsData
{
public float HitRadius { get; set; }
}
public class WeaponKnifeData : WeaponData public class WeaponKnifeData : WeaponData
{ {
public WeaponKnifeParamsData ParamsData { get; }
public WeaponKnifeData(int entityId, int ownerId, CampType ownerCamp) : base(entityId, WeaponType.WeaponKnife, public WeaponKnifeData(int entityId, int ownerId, CampType ownerCamp) : base(entityId, WeaponType.WeaponKnife,
ownerId, ownerCamp) ownerId, ownerCamp)
{ {
ParamsData = ParseParams<WeaponKnifeParamsData>();
} }
} }
} }

View File

@ -1,12 +1,23 @@
using System;
using Definition.Enum; using Definition.Enum;
namespace Entity.EntityData namespace Entity.EntityData
{ {
[Serializable]
public sealed class WeaponLightningParamsData
{
public float HitRadius { get; set; }
public float HoverHeight { get; set; }
}
public class WeaponLightningData : WeaponData public class WeaponLightningData : WeaponData
{ {
public WeaponLightningParamsData ParamsData { get; }
public WeaponLightningData(int entityId, int ownerId, CampType ownerCamp) public WeaponLightningData(int entityId, int ownerId, CampType ownerCamp)
: base(entityId, WeaponType.WeaponLightning, ownerId, ownerCamp) : base(entityId, WeaponType.WeaponLightning, ownerId, ownerCamp)
{ {
ParamsData = ParseParams<WeaponLightningParamsData>();
} }
} }
} }

View File

@ -1,12 +1,22 @@
using System;
using Definition.Enum; using Definition.Enum;
namespace Entity.EntityData namespace Entity.EntityData
{ {
[Serializable]
public sealed class WeaponSlashParamsData
{
public float SectorAngle { get; set; }
}
public class WeaponSlashData : WeaponData public class WeaponSlashData : WeaponData
{ {
public WeaponSlashParamsData ParamsData { get; }
public WeaponSlashData(int entityId, int ownerId, CampType ownerCamp) public WeaponSlashData(int entityId, int ownerId, CampType ownerCamp)
: base(entityId, WeaponType.WeaponSlash, ownerId, ownerCamp) : base(entityId, WeaponType.WeaponSlash, ownerId, ownerCamp)
{ {
ParamsData = ParseParams<WeaponSlashParamsData>();
} }
} }
} }

View File

@ -11,8 +11,6 @@ namespace Entity.Weapon
{ {
public partial class WeaponKnife : WeaponBase public partial class WeaponKnife : WeaponBase
{ {
private const string HitRadiusParamKey = "HitRadius";
private WeaponKnifeData _weaponData; private WeaponKnifeData _weaponData;
private Quaternion _cachedRotation; private Quaternion _cachedRotation;
@ -157,14 +155,8 @@ namespace Entity.Weapon
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange; _sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
_cachedRotation = CachedTransform.rotation; _cachedRotation = CachedTransform.rotation;
if (_weaponData.TryGetParam(HitRadiusParamKey, out string hitRadiusRaw)) float configuredHitRadius = _weaponData.ParamsData != null ? _weaponData.ParamsData.HitRadius : 0f;
{ _hitRadius = configuredHitRadius > 0f ? Mathf.Max(0.1f, configuredHitRadius) : _weaponData.AttackRange;
_hitRadius = Mathf.Max(0.1f, float.Parse(hitRadiusRaw));
}
else
{
_hitRadius = _weaponData.AttackRange;
}
_hitRadiusSqr = _hitRadius * _hitRadius; _hitRadiusSqr = _hitRadius * _hitRadius;
_attackEffect = new KnifeRangeAttackEffect(); _attackEffect = new KnifeRangeAttackEffect();

View File

@ -11,8 +11,6 @@ namespace Entity.Weapon
{ {
public partial class WeaponLightning : WeaponBase public partial class WeaponLightning : WeaponBase
{ {
private const string HitRadiusParamKey = "HitRadius";
private WeaponLightningData _weaponData; private WeaponLightningData _weaponData;
private Quaternion _cachedRotation; private Quaternion _cachedRotation;
@ -178,8 +176,14 @@ namespace Entity.Weapon
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange; _sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
_cachedRotation = CachedTransform.rotation; _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; _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); int colliderCapacity = Mathf.Max(1, _maxHitColliders);
if (_hitResults == null || _hitResults.Length != colliderCapacity) 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) private void StopAttackTween(bool resetTransform)
{ {
if (_attackSequence != null) if (_attackSequence != null)

View File

@ -13,8 +13,6 @@ namespace Entity.Weapon
{ {
#region Property #region Property
private const string SectorAngleParamKey = "SectorAngle";
private WeaponSlashData _weaponData; private WeaponSlashData _weaponData;
private Quaternion _cachedRotation; private Quaternion _cachedRotation;
@ -184,15 +182,8 @@ namespace Entity.Weapon
_attackRadius = Mathf.Max(0.1f, _weaponData.AttackRange); _attackRadius = Mathf.Max(0.1f, _weaponData.AttackRange);
_attackRadiusSqr = _attackRadius * _attackRadius; _attackRadiusSqr = _attackRadius * _attackRadius;
_sectorAngle = 90f; float configuredSectorAngle = _weaponData.ParamsData != null ? _weaponData.ParamsData.SectorAngle : 0f;
if (_weaponData.Params != null && _sectorAngle = configuredSectorAngle > 0f ? Mathf.Clamp(configuredSectorAngle, 1f, 360f) : 90f;
_weaponData.Params.TryGetValue(SectorAngleParamKey.ToLower(), out string rawAngle))
{
if (float.TryParse(rawAngle, out float parsedAngle))
{
_sectorAngle = Mathf.Clamp(parsedAngle, 1f, 360f);
}
}
int capacity = Mathf.Max(1, _maxHitColliders); int capacity = Mathf.Max(1, _maxHitColliders);
if (_hitResults == null || _hitResults.Length != capacity) if (_hitResults == null || _hitResults.Length != capacity)

255
docs/CodeX-TODO.md Normal file
View File

@ -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<string, string>`
- 仅用于描述/UI 等弱类型读取场景。
- `Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponData.cs`
- 保留所有武器共用字段:
- `Attack`
- `Cooldown`
- `AttackRange`
- `Price`
- `Rarity`
- `Modifiers`
- `ParamsJson`
- `Params`
- 提供 `ParseParams<TParams>()` 作为武器子类的强类型参数解析入口。
- `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<TParams>()`
4. 新增武器逻辑类
- 继承 `WeaponBase`
- 接入状态机
- 读取 `ParamsData`
5. 接入生成入口
- 玩家初始武器
- 商店购买武器
- 其他掉落/奖励入口
6. 验证点
- 武器生成正确
- 参数生效正确
- 描述文本正确
- Simulation 模式和非 Simulation 模式都能命中

View File

@ -13,20 +13,30 @@ description: Develop and extend the VampireLike weapon system. Use when creating
- state flow (`Idle`, `Check_OutRange`, `Check_InRange`, `Attack`) - state flow (`Idle`, `Check_OutRange`, `Check_InRange`, `Attack`)
- target selector (`ITargetSelector`, `TargetSelectorType`) - target selector (`ITargetSelector`, `TargetSelectorType`)
- effect layer (`IWeaponAttackEffect`) - effect layer (`IWeaponAttackEffect`)
- data contract (`DRWeapon`, `WeaponData`) - data contract (`DRWeapon`, `WeaponData`, `ParamsData`)
3. Keep behavior compatibility with current gameplay loop and UI/event chain. 3. Keep behavior compatibility with current gameplay loop, shop flow, inventory flow, and UI/event chain.
## Source Map ## Source Map
- Weapon base: `../../Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponBase.cs` - Weapon base:
- `../../Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponBase.cs`
- Existing weapons: - Existing weapons:
- `../../Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponKnife/` - `../../Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponKnife/`
- `../../Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponHandgun/` - `../../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: - Selectors:
- `../../Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/TargetSelector/` - `../../Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/TargetSelector/`
- Attack effects:
- `../../Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/AttackEffects/`
- Weapon data: - Weapon data:
- `../../Assets/GameMain/Scripts/Entity/EntityData/Weapon/` - `../../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: - Entity show flow:
- `../../Assets/GameMain/Scripts/Entity/EntityExtension.cs` - `../../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 state transitions explicit and non-blocking.
- Keep cooldown accumulation valid even when no target is found. - Keep cooldown accumulation valid even when no target is found.
- Keep attack logic and visual effect logic decoupled. - 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. - Preserve compatibility with shop/inventory/UI refresh flow.
## Change Recipes ## Change Recipes
@ -44,17 +55,18 @@ description: Develop and extend the VampireLike weapon system. Use when creating
### Add a New Weapon ### Add a New Weapon
1. Extend `WeaponType` without reordering existing enum values. 1. Extend `WeaponType` without reordering existing enum values.
2. Add `WeaponXxxData : WeaponData` for strong-typed fields. 2. Add `WeaponXxxData : WeaponData` and `WeaponXxxParamsData` for weapon-specific parameters.
3. Add `WeaponXxx : WeaponBase` and implement only weapon-specific behavior. 3. Parse `ParamsJson` through `ParseParams<TParams>()` inside the weapon data constructor.
4. Build state files under `Weapon/WeaponXxx/` with partial class layout. 4. Add `WeaponXxx : WeaponBase` and implement only weapon-specific behavior.
5. Register display/data mapping path so `ShowWeapon` can instantiate correctly. 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 ### Add or Update a Target Selector
1. Implement `ITargetSelector`. 1. Implement `ITargetSelector`.
2. Update `TargetSelectorType`. 2. Update `TargetSelectorType`.
3. Register creation in `WeaponBase.CreateSelector`. 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 ### 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. 2. Trigger effect from weapon attack state only.
3. Keep damage resolution outside effect code. 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 ## Validation Checklist
- Weapon can be shown, attached, and updated without exceptions. - Weapon can be shown, attached, and updated without exceptions.
- State machine does not stall across target loss/reacquire. - State machine does not stall across target loss/reacquire.
- Cooldown and range checks match design expectation. - Cooldown and range checks match design expectation.
- Damage path and effect path remain decoupled. - Damage path and effect path remain decoupled.
- `ParamsData` matches table content and default fallback behavior.
- UI/shop/inventory interactions stay stable after the change. - 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.

View File

@ -1,16 +1,17 @@
# Weapon Development SkillVampireLike # Weapon Development SkillVampireLike
## 目标 ## 目标
本文件是 `Entity.Weapon` 体系的开发规范与速查手册。 本文件是 `Entity.Weapon` 体系的开发规范与速查手册。
后续新增武器或扩展机制时,优先按本文档执行,避免重复通读历史上下文。 后续新增武器、扩展参数、调整状态机或联动商店/背包时,优先按本文档执行,避免重复通读历史上下文。
## 当前架构总览 ## 当前架构总览
- 武器运行时入口:`WeaponBase``Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponBase.cs` - 武器运行时入口:`WeaponBase``Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponBase.cs`
- 武器具体实现:`WeaponKnife`、`WeaponHandgun`、`WeaponSlash` - 武器具体实现:`WeaponKnife`、`WeaponHandgun`、`WeaponSlash`、`WeaponLightning`
- 目标选择策略:`ITargetSelector` + `TargetSelectorType` - 目标选择策略:`ITargetSelector` + `TargetSelectorType`
- 攻击可视化:`IWeaponAttackEffect` - 攻击可视化:`IWeaponAttackEffect`
- 数据入口:`DRWeapon` -> `WeaponData`(及其子类) - 数据入口:`DRWeapon` -> `WeaponData`(及其子类)
- 实体生成:`EntityExtension.ShowWeapon` - 实体生成:`EntityExtension.ShowWeapon`
- 商店接入:`ShopFormUseCase.CreateWeaponData`
## WeaponBase 统一职责(已上收) ## WeaponBase 统一职责(已上收)
`WeaponBase` 负责以下通用逻辑,子类不要重复实现: `WeaponBase` 负责以下通用逻辑,子类不要重复实现:
@ -20,8 +21,34 @@
- 启用门控:`OnUpdate` 中统一 `if (!_isEnabled) return` - 启用门控:`OnUpdate` 中统一 `if (!_isEnabled) return`
- 目标选择入口:`SelectTarget`、`SetTargetSelector`、`CreateSelector` - 目标选择入口:`SelectTarget`、`SetTargetSelector`、`CreateSelector`
- 距离判定:`IsInRange`(基于 XZ 平面距离) - 距离判定:`IsInRange`(基于 XZ 平面距离)
- Simulation 命中请求:`TryQueueAreaCollisionQuery`、`TryQueueSectorCollisionQuery`
- 玩家攻击属性订阅:`BindAttackStatFromOwner` / `ReleaseAttackStatSubscription` - 玩家攻击属性订阅:`BindAttackStatFromOwner` / `ReleaseAttackStatSubscription`
约束:
- 不要在子类里重复实现 `WeaponBase` 已有能力。
- 行为差异优先收敛在:`BuildStates`、`Check`、`Attack`、少量专属辅助方法。
## 当前武器模板分类
### 1. `WeaponKnife`
- 模式:近身前刺 + 圆形范围命中
- 典型参数:`HitRadius`
- 适合派生:长枪、刺剑、短矛、震地锤近身版
### 2. `WeaponHandgun`
- 模式:单次 `Raycast` 瞬发命中
- 当前参数化程度较低,仍适合作为远程枪械母版继续扩展
- 适合派生:手枪、霰弹枪、狙击枪、三连发枪
### 3. `WeaponSlash`
- 模式:扇形范围命中
- 典型参数:`SectorAngle`
- 适合派生:大剑、斧头、半月斩、横扫类武器
### 4. `WeaponLightning`
- 模式:锁定目标点 + 落点范围打击
- 典型参数:`HitRadius`、`HoverHeight`
- 适合派生:闪电、陨石杖、圣光柱、空袭类武器
## 状态机约定 ## 状态机约定
统一状态枚举: 统一状态枚举:
- `Idle` - `Idle`
@ -39,12 +66,14 @@
关键规则: 关键规则:
- 即使没有目标,也允许蓄力(计时器持续走)。 - 即使没有目标,也允许蓄力(计时器持续走)。
- 一旦进入 `Check_InRange` 且冷却已满,应立即触发攻击。 - 一旦进入 `Check_InRange` 且冷却已满,应立即触发攻击。
- 攻击过程中若需要多帧动画,使用 `_isAttacking` 控制状态退出时机。
## 3D 场景下的距离/朝向原则 ## 3D 场景下的距离/朝向原则
- 射程与目标筛选:优先使用 XZ 平面距离(忽略 Y调用 `AIUtility.GetSqrMagnitudeXZ` - 射程与目标筛选:优先使用 XZ 平面距离(忽略 Y调用 `AIUtility.GetSqrMagnitudeXZ`
- 视觉朝向与弹道:可按武器设计决定是否使用完整 3D 向量。 - 视觉朝向与弹道:可按武器设计决定是否使用完整 3D 向量。
- `WeaponHandgun`:允许俯仰瞄准,逻辑上用射线命中对象判定伤害。 - `WeaponHandgun`:允许俯仰瞄准,逻辑上用射线命中对象判定伤害。
- 近战地面范围类Knife/Slash伤害检测建议投影到地面XZ再判定。 - 近战地面范围类Knife/Slash伤害检测建议投影到地面XZ再判定。
- 落点类Lightning锁点可取目标当前位置但范围判定仍建议按地面距离收口。
## 目标选择策略规范 ## 目标选择策略规范
接口:`ITargetSelector.SelectTarget(WeaponBase weapon, IEnumerable<EntityBase> candidates, float maxSqrRange)` 接口:`ITargetSelector.SelectTarget(WeaponBase weapon, IEnumerable<EntityBase> candidates, float maxSqrRange)`
@ -55,14 +84,15 @@
- `LowestHealthTargetSelector` - `LowestHealthTargetSelector`
语义约定: 语义约定:
- `HighestHealth` / `LowestHealth` 必须按“当前血量”筛选。 - `NearestTargetSelector` 在 Simulation 启用时优先走空间索引。
- 当前实现读取 `HealthComponent.CurrentHealth` - `HighestHealth` / `LowestHealth` 当前基于运行时生命值选择目标。
- 若新增新策略,必须明确“按当前值”还是“按最大值”筛选,避免语义漂移。
扩展策略步骤: 扩展策略步骤:
1. 新建 selector 类并实现 `ITargetSelector` 1. 新建 selector 类并实现 `ITargetSelector`
2. 更新 `TargetSelectorType` 枚举。 2. 更新 `TargetSelectorType` 枚举。
3. 在 `WeaponBase.CreateSelector` 中注册。 3. 在 `WeaponBase.CreateSelector` 中注册。
4. 武器在 `OnWeaponShow`构造阶段选择策略。 4. 武器在 `OnWeaponShow`初始化阶段选择策略。
## 攻击可视化效果规范 ## 攻击可视化效果规范
接口:`IWeaponAttackEffect.Play(WeaponBase weapon, Vector3 position, EntityBase target, float radius)` 接口:`IWeaponAttackEffect.Play(WeaponBase weapon, Vector3 position, EntityBase target, float radius)`
@ -71,25 +101,68 @@
- 可视化逻辑与伤害逻辑解耦。 - 可视化逻辑与伤害逻辑解耦。
- 武器类只负责触发 `Play`,不把可视化细节塞回武器核心逻辑。 - 武器类只负责触发 `Play`,不把可视化细节塞回武器核心逻辑。
- 当前阶段允许临时对象创建;后续若有性能压力再统一对象池化。 - 当前阶段允许临时对象创建;后续若有性能压力再统一对象池化。
- 命中特效、范围预警、扇形描边都属于 effect 层,不属于伤害层。
## 数据层规范DRWeapon / WeaponData ## 数据层规范DRWeapon / WeaponData
`DRWeapon` 提供通用字段: ### `DRWeapon`
- `Attack`、`Cooldown`、`AttackRange`、`AttackSoundId` 提供通用字段:
- `Pramas`字典Key 建议统一转小写) - `Attack`
- `Cooldown`
- `AttackRange`
- `AttackSoundId`
- `ParamsJson`
- `Pramas`
- `Modifiers` - `Modifiers`
约定: 约定:
- 参数解析尽量在数据层/初始化阶段完成。 - `Params` 列现在使用标准 JSON 对象。
- 武器逻辑层读取 `WeaponData` 的强类型结果,不要散落 `Parse` - 空参数统一写 `{}`
- 解析时必须容错:优先 `TryParse` + 默认值,避免运行时异常。 - 不再兼容 `[]`
- `Pramas` 保留为描述/UI 的兼容字典视图,不再作为武器逻辑主读取入口。
### `WeaponData`
提供公共武器字段与通用解析入口:
- 公共战斗字段:`Attack`、`Cooldown`、`AttackRange`
- 公共展示字段:`Title`、`IconAssetName`、`Rarity`、`Price`
- 参数入口:`ParamsJson`、`Params`
- 通用强类型解析:`ParseParams<TParams>()`
约定:
- 参数解析优先在数据层完成。
- 武器逻辑层读取强类型 `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. 定义枚举 1. 定义枚举
- 更新 `WeaponType`(保持递增值,避免重排已有值)。 - 更新 `WeaponType`,保持递增值,避免重排已有值
2. 建立数据类 2. 建立数据类
- 新建 `WeaponXxxData : WeaponData` - 新建 `WeaponXxxData : WeaponData`
- 如果有独有参数,提供强类型字段或统一初始化逻辑。 - 新建 `WeaponXxxParamsData`
- 在构造阶段调用 `ParseParams<TParams>()`,把参数初始化为强类型字段。
3. 建立行为类 3. 建立行为类
- 新建 `WeaponXxx : WeaponBase` - 新建 `WeaponXxx : WeaponBase`
@ -106,29 +179,84 @@
5. 建立可视化(可选) 5. 建立可视化(可选)
- 新建 `WeaponXxxAttackEffect : IWeaponAttackEffect`,在武器中组合调用。 - 新建 `WeaponXxxAttackEffect : IWeaponAttackEffect`,在武器中组合调用。
6. 若为远程武器,建立子弹实体 6. 接入实体展示与数据表
- `BulletXxx : Bullet`
- `BulletXxxData : BulletData`
- 通过武器赋予伤害/阵营参数。
- 自动销毁应走对象池回收。
7. 接入实体展示与数据表
- 确保 `DRWeapon` / `DREntity` 配置齐全。 - 确保 `DRWeapon` / `DREntity` 配置齐全。
- `EntityExtension.ShowWeapon` 可正确映射到 `Entity.Weapon.WeaponXxx` - `EntityExtension.ShowWeapon` 可正确映射到 `Entity.Weapon.WeaponXxx`
- 商店/背包创建入口能正确构造 `WeaponXxxData`
8. 联动系统 7. 联动系统
- 背包、商店购买/出售、UI 展示、事件流刷新。 - 背包、商店购买/出售、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` 已有通用逻辑。 - 是否重复实现了 `WeaponBase` 已有通用逻辑。
- 状态流转是否会卡死或漏转场。 - 状态流转是否会卡死或漏转场。
- 冷却计时是否在无目标时仍正常累积。 - 冷却计时是否在无目标时仍正常累积。
- 目标失效null/Unavailable/死亡)是否安全处理。 - 目标失效null/Unavailable/死亡)是否安全处理。
- 命中检测是否符合武器设计(地面范围/射线/扇形)。 - 命中检测是否符合武器设计(地面范围/射线/扇形/落点)。
- 数据参数是否全部容错解析。 - 数据参数是否全部收敛到 `ParamsData`
- 可视化是否与伤害逻辑解耦。 - 可视化是否与伤害逻辑解耦。
- 是否正确订阅/解绑攻击属性(或复用基类方法)。 - 是否正确订阅/解绑攻击属性(或复用基类方法)。
- 商店、背包、武器描述是否仍保持兼容。
## 已知注意点 ## 已知注意点
- 目录中存在 `Pramas` 命名拼写历史包袱,保持兼容即可。 - 目录中存在 `Pramas` 命名拼写历史包袱,当前保持兼容即可。
- 文档中的“规范”优先级高于历史实现;历史实现若偏离,按本规范逐步收敛。 - 文档中的“规范”优先级高于历史实现;历史实现若偏离,按本规范逐步收敛。
- 当前更推荐“公共 `WeaponData` + 专属 `ParamsData`”结构,不建议把每种武器做成一套完全独立的数据总线。

Binary file not shown.

View File

@ -1,6 +1,9 @@
import pandas as pd import pandas as pd
import os import os
import csv def format_cell(value):
if value is None:
return ''
return str(value)
def convert_excel_to_txt(folder_path='.'): def convert_excel_to_txt(folder_path='.'):
# 计数器,用于最后汇总 # 计数器,用于最后汇总
@ -28,24 +31,15 @@ def convert_excel_to_txt(folder_path='.'):
try: try:
# 读取 Excel # 读取 Excel
df = pd.read_excel(file_path, header=None) df = pd.read_excel(
file_path,
# 预处理:将 NaN 替换为空字符串,否则导出会变成 "nan" header=None,
df = df.fillna('') keep_default_na=False,
# 导出设置:
# 1. sep='\t' : 使用制表符分隔
# 2. quoting=csv.QUOTE_NONE : 不使用引号包裹字段,也不会把 " 变成 ""
# 3. escapechar='\\' : 如果单元格内恰好有 Tab 键,会用反斜杠转义,防止数据列错位
df.to_csv(
output_file,
sep='\t',
index=False,
header=False,
encoding='utf-8',
quoting=csv.QUOTE_NONE,
escapechar='\\'
) )
with open(output_file, 'w', encoding='utf-8', newline='') as f:
for row in df.itertuples(index=False, name=None):
f.write('\t'.join(format_cell(cell) for cell in row) + '\n')
print(f"成功转换 -> {output_file}") print(f"成功转换 -> {output_file}")
count += 1 count += 1