添加武器长枪 + bug fix

- WeaponLance:Entity/Goods/Weapon 表,状态机
- Battle -> Shop/LevelUp:隐藏子弹
This commit is contained in:
SepComet 2026-03-19 17:53:24 +08:00
parent f31babdb85
commit 8d21d53c4d
35 changed files with 945 additions and 79 deletions

View File

@ -10,5 +10,6 @@
202 武器手枪 WeaponHandgun 202 武器手枪 WeaponHandgun
203 武器斧头 WeaponSlash 203 武器斧头 WeaponSlash
204 武器闪电 WeaponLightning 204 武器闪电 WeaponLightning
205 武器长枪 WeaponLance
10001 金币实体 CoinEntity 10001 金币实体 CoinEntity
10002 经验实体 ExpEntity 10002 经验实体 ExpEntity

View File

@ -1,5 +1,4 @@
# 商品表 # Id 列1 GoodsType GoodsTypeId
# Id GoodsType GoodsTypeId
# int GoodsType int # int GoodsType int
# 商品编号 策划备注 商品类型 商品对应物品Id # 商品编号 策划备注 商品类型 商品对应物品Id
101 道具:药 Prop 101 101 道具:药 Prop 101
@ -26,3 +25,4 @@
122 Prop 120 122 Prop 120
123 Weapon 3 123 Weapon 3
124 Weapon 4 124 Weapon 4
125 Weapon 5

View File

@ -1,8 +1,8 @@
# 武器表 # Id 列1 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} [] 4 204 闪电 Almighty_Icon White 150 0.08 80 3 12 10000 {"hitRadius":3} []
5 205 长枪 Almighty_Icon White 100 0.1 100 1.5 7 10000 {"hitRadius":0.3,"thrustDistance":1.2,"pierceLength":0.3}

View File

@ -0,0 +1,225 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &6354441506395502586
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8872382261416578947}
- component: {fileID: 1092941560137749238}
- component: {fileID: 2293075059394330032}
m_Layer: 11
m_Name: Cylinder
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &8872382261416578947
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6354441506395502586}
serializedVersion: 2
m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071068}
m_LocalPosition: {x: 0, y: 0, z: 0.4}
m_LocalScale: {x: 0.5, y: 0.3, z: 0.5}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5097192555115739519}
m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0}
--- !u!33 &1092941560137749238
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6354441506395502586}
m_Mesh: {fileID: 10206, guid: 0000000000000000e000000000000000, type: 0}
--- !u!23 &2293075059394330032
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6354441506395502586}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: c4f37184fcb9306428d7d002f7dca96d, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!1 &6722279723536450523
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4238244161129684256}
- component: {fileID: 8136283925019532162}
- component: {fileID: 452598937405325984}
- component: {fileID: 6200741578935482964}
m_Layer: 11
m_Name: Cylinder 1
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &4238244161129684256
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6722279723536450523}
serializedVersion: 2
m_LocalRotation: {x: 0.7071068, y: 0, z: 0, w: 0.7071068}
m_LocalPosition: {x: 0, y: 0, z: 0.7}
m_LocalScale: {x: 0.2, y: 0.3, z: 0.2}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 5097192555115739519}
m_LocalEulerAnglesHint: {x: 90, y: 0, z: 0}
--- !u!33 &8136283925019532162
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6722279723536450523}
m_Mesh: {fileID: 10206, guid: 0000000000000000e000000000000000, type: 0}
--- !u!23 &452598937405325984
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6722279723536450523}
m_Enabled: 1
m_CastShadows: 1
m_ReceiveShadows: 1
m_DynamicOccludee: 1
m_StaticShadowCaster: 0
m_MotionVectors: 1
m_LightProbeUsage: 1
m_ReflectionProbeUsage: 1
m_RayTracingMode: 2
m_RayTraceProcedural: 0
m_RenderingLayerMask: 1
m_RendererPriority: 0
m_Materials:
- {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
m_StaticBatchInfo:
firstSubMesh: 0
subMeshCount: 0
m_StaticBatchRoot: {fileID: 0}
m_ProbeAnchor: {fileID: 0}
m_LightProbeVolumeOverride: {fileID: 0}
m_ScaleInLightmap: 1
m_ReceiveGI: 1
m_PreserveUVs: 0
m_IgnoreNormalsForChartDetection: 0
m_ImportantGI: 0
m_StitchLightmapSeams: 1
m_SelectedEditorRenderState: 3
m_MinimumChartSize: 4
m_AutoUVMaxDistance: 0.5
m_AutoUVMaxAngle: 89
m_LightmapParameters: {fileID: 0}
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
m_AdditionalVertexStreams: {fileID: 0}
--- !u!136 &6200741578935482964
CapsuleCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6722279723536450523}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 2
m_Radius: 0.5000001
m_Height: 2
m_Direction: 1
m_Center: {x: 0.000000059604645, y: 0, z: -0.00000008940697}
--- !u!1 &7825103691467368365
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 5097192555115739519}
m_Layer: 11
m_Name: WeaponLance
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &5097192555115739519
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7825103691467368365}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8872382261416578947}
- {fileID: 4238244161129684256}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 11143001bcbdc864b8d8fe2083142e5a
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -122,7 +122,7 @@ namespace DataTable
/// <returns></returns> /// <returns></returns>
private Dictionary<string, string> DeserializeParams(string rawParams) private Dictionary<string, string> DeserializeParams(string rawParams)
{ {
var dict = new Dictionary<string, string>(); var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(rawParams)) if (string.IsNullOrWhiteSpace(rawParams))
{ {
return dict; return dict;
@ -143,7 +143,7 @@ namespace DataTable
continue; continue;
} }
dict[pair.Key.ToLower()] = pair.Value.ToString(); dict[pair.Key] = pair.Value.ToString();
} }
} }
catch (Exception exception) catch (Exception exception)

View File

@ -7,5 +7,6 @@ namespace Definition.Enum
WeaponHandgun = 2, WeaponHandgun = 2,
WeaponSlash = 3, WeaponSlash = 3,
WeaponLightning = 4, WeaponLightning = 4,
WeaponLance = 5,
} }
} }

View File

@ -59,7 +59,7 @@ namespace Entity.EntityData
return false; return false;
} }
return _drEnemy.Params.TryGetValue(key.ToLower(), out value); return _drEnemy.Params.TryGetValue(key, out value);
} }
} }
} }

View File

@ -37,7 +37,7 @@ namespace Entity.EntityData
return false; return false;
} }
return Params.TryGetValue(key.ToLower(), out value); return Params.TryGetValue(key, out value);
} }
protected TParams ParseParams<TParams>() where TParams : new() protected TParams ParseParams<TParams>() where TParams : new()

View File

@ -0,0 +1,56 @@
using System;
using Definition.Enum;
namespace Entity.EntityData
{
[Serializable]
public sealed class WeaponLanceParamsData
{
/// <summary>
/// 枪尖命中半径。
/// </summary>
public float HitRadius { get; set; }
/// <summary>
/// 武器模型前刺的位移距离。
/// </summary>
public float ThrustDistance { get; set; }
/// <summary>
/// 实际判定的前刺长度。
/// </summary>
public float PierceLength { get; set; }
/// <summary>
/// 判定起点相对武器当前位置的前置偏移。
/// </summary>
public float ForwardOffset { get; set; }
/// <summary>
/// 追踪目标时的转向速度。
/// </summary>
public float RotateSpeed { get; set; }
/// <summary>
/// 向前突刺阶段耗时。
/// </summary>
public float AttackDuration { get; set; }
/// <summary>
/// 收枪返回阶段耗时。
/// </summary>
public float ReturnDuration { get; set; }
}
[Serializable]
public class WeaponLanceData : WeaponData
{
public WeaponLanceParamsData ParamsData { get; }
public WeaponLanceData(int entityId, int ownerId, CampType ownerCamp)
: base(entityId, WeaponType.WeaponLance, ownerId, ownerCamp)
{
ParamsData = ParseParams<WeaponLanceParamsData>();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4d1a821e8ee1a9a4b912b70b0a1616eb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -304,11 +304,4 @@ namespace Entity.Weapon
public abstract void OnLeave(); public abstract void OnLeave();
public override string ToString() => State.ToString(); public override string ToString() => State.ToString();
} }
} }

View File

@ -46,23 +46,16 @@ namespace Entity.Weapon
{ {
FaceTargetImmediately(); FaceTargetImmediately();
Vector3 fireOrigin = CachedTransform.TransformPoint(_fireOriginOffset); if (!TryResolveAttackTarget(out TargetableObject targetable, out Vector3 hitPosition))
Vector3 fireDirection = CachedTransform.forward; {
float maxDistance = Mathf.Max(0.1f, _weaponData.AttackRange); return;
}
if (Physics.Raycast(fireOrigin, fireDirection, out RaycastHit hit, maxDistance, _hitMask, _attackEffect?.Play(this, hitPosition, targetable, 0f);
QueryTriggerInteraction.Collide))
{
TargetableObject targetable = hit.collider.GetComponentInParent<TargetableObject>();
if (targetable != null && targetable.Available && !targetable.IsDead)
{
_attackEffect?.Play(this, hit.point, targetable, 0f);
_isAttacking = true; _isAttacking = true;
AIUtility.PerformCollision(targetable, this); AIUtility.PerformCollision(targetable, this);
_isAttacking = false; _isAttacking = false;
} }
}
}
protected override void Check() protected override void Check()
{ {
@ -95,6 +88,40 @@ namespace Entity.Weapon
CachedTransform.rotation = Quaternion.LookRotation(directionToTarget.normalized, Vector3.up); CachedTransform.rotation = Quaternion.LookRotation(directionToTarget.normalized, Vector3.up);
} }
private bool TryResolveAttackTarget(out TargetableObject targetable, out Vector3 hitPosition)
{
targetable = _target as TargetableObject;
hitPosition = CachedTransform.position;
if (targetable == null || !targetable.Available || targetable.IsDead)
{
return false;
}
Transform targetTransform = targetable.CachedTransform;
if (targetTransform == null)
{
return false;
}
hitPosition = targetTransform.position;
Vector3 fireOrigin = CachedTransform.TransformPoint(_fireOriginOffset);
Vector3 directionToTarget = targetTransform.position - fireOrigin;
float maxDistance = Mathf.Max(0.1f, _weaponData.AttackRange);
if (directionToTarget.sqrMagnitude > Mathf.Epsilon &&
Physics.Raycast(fireOrigin, directionToTarget.normalized, out RaycastHit hit, maxDistance, _hitMask,
QueryTriggerInteraction.Collide))
{
TargetableObject raycastTarget = hit.collider.GetComponentInParent<TargetableObject>();
if (raycastTarget == targetable)
{
hitPosition = hit.point;
}
}
return true;
}
#region Lifecycle #region Lifecycle
protected override bool OnWeaponShow(object userData) protected override bool OnWeaponShow(object userData)

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: aee5d5037e73e894cac11712e321b930
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,31 @@
namespace Entity.Weapon
{
public partial class WeaponLance
{
private class AttackState : WeaponStateBase
{
private WeaponLance _weapon;
public override WeaponStateType State => WeaponStateType.Attack;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLance;
public override void OnEnter()
{
_weapon._currAttackTimer = 0f;
_weapon.Attack();
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (!_weapon._isAttacking)
{
_weapon.TransitionTo(WeaponStateType.Check_InRange);
}
}
public override void OnLeave()
{
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d9991ee189b733b4e9d40395357f5ef2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,49 @@
namespace Entity.Weapon
{
public partial class WeaponLance
{
private class CheckInRangeState : WeaponStateBase
{
private WeaponLance _weapon;
public override WeaponStateType State => WeaponStateType.Check_InRange;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLance;
public override void OnEnter()
{
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
{
_weapon.TransitionTo(WeaponStateType.Attack);
}
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.Check();
_weapon.RotateToTarget(elapseSeconds);
_weapon._currAttackTimer += elapseSeconds;
if (_weapon._target == null || !_weapon._target.Available)
{
_weapon.TransitionTo(WeaponStateType.Idle);
return;
}
if (!_weapon.IsInRange(_weapon._target, _weapon._sqrRange))
{
_weapon.TransitionTo(WeaponStateType.Check_OutRange);
return;
}
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
{
_weapon.TransitionTo(WeaponStateType.Attack);
}
}
public override void OnLeave()
{
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 01edc8bb0ff46a14d8028e03af932b52
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,39 @@
namespace Entity.Weapon
{
public partial class WeaponLance
{
private class CheckOutRangeState : WeaponStateBase
{
private WeaponLance _weapon;
public override WeaponStateType State => WeaponStateType.Check_OutRange;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLance;
public override void OnEnter()
{
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.Check();
_weapon.RotateToTarget(elapseSeconds);
_weapon._currAttackTimer += elapseSeconds;
if (_weapon._target == null || !_weapon._target.Available)
{
_weapon.TransitionTo(WeaponStateType.Idle);
return;
}
if (_weapon.IsInRange(_weapon._target, _weapon._sqrRange))
{
_weapon.TransitionTo(WeaponStateType.Check_InRange);
}
}
public override void OnLeave()
{
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8009ad2c332a956438e3357ed5e30eb6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,33 @@
namespace Entity.Weapon
{
public partial class WeaponLance
{
public class IdleState : WeaponStateBase
{
private WeaponLance _weapon;
public override WeaponStateType State => WeaponStateType.Idle;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLance;
public override void OnEnter()
{
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.Check();
_weapon.RotateToOrigin(elapseSeconds);
_weapon._currAttackTimer += elapseSeconds;
if (_weapon._target != null && _weapon._target.Available)
{
_weapon.TransitionTo(WeaponStateType.Check_OutRange);
}
}
public override void OnLeave()
{
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4c9e98f5381e579469dcdac7bf3e1e25
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,325 @@
using System.Collections.Generic;
using CustomUtility;
using Definition.DataStruct;
using Definition.Enum;
using DG.Tweening;
using Entity.EntityData;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace Entity.Weapon
{
public partial class WeaponLance : WeaponBase
{
// 长枪专用数据,包含强类型 ParamsData。
[SerializeField] private WeaponLanceData _weaponData;
private Quaternion _cachedRotation;
// 朝向目标时的旋转速度,可被 ParamsData.RotateSpeed 覆盖。
[SerializeField] private float _rotateSpeed = 5f;
private Sequence _attackSequence;
private Transform _attackParent;
// 前刺动画耗时。
[SerializeField] private float _attackDuration = 0.12f;
// 收枪返回耗时。
[SerializeField] private float _returnDuration = 0.18f;
[SerializeField] private LayerMask _hitMask = ~0;
[SerializeField] private int _maxHitColliders = 32;
private IWeaponAttackEffect _attackEffect;
private Collider[] _hitResults;
private readonly HashSet<int> _hitEntityIds = new();
// 枪尖判定半径。
private float _hitRadius;
// 从判定起点向前延伸的有效刺击长度。
private float _pierceLength;
// 判定起点相对武器当前位置的前移量。
private float _forwardOffset;
// 武器模型本身向前突刺的位移长度。
private float _thrustDistance;
public override ImpactData GetImpactData()
{
return new ImpactData(_weaponData.OwnerCamp, _weaponData.Attack, AttackStat);
}
protected override void BuildStates()
{
RegisterState(new IdleState());
RegisterState(new CheckInRangeState());
RegisterState(new CheckOutRangeState());
RegisterState(new AttackState());
}
protected override void Attack()
{
StopAttackTween(false);
FaceTargetImmediately();
_isAttacking = true;
_attackParent = CachedTransform.parent;
CachedTransform.SetParent(null);
Vector3 targetPos = CachedTransform.position + CachedTransform.forward * _thrustDistance;
_attackSequence = DOTween.Sequence();
_attackSequence.Append(CachedTransform.DOMove(targetPos, _attackDuration).SetEase(Ease.OutQuad));
_attackSequence.AppendCallback(() =>
{
Vector3 strikeCenter = GetStrikeCenter();
_attackEffect?.Play(this, strikeCenter, _target, _hitRadius);
ApplyPierceDamage();
if (_attackParent != null)
{
CachedTransform.SetParent(_attackParent);
}
});
_attackSequence.Append(CachedTransform.DOLocalMove(Vector3.zero, _returnDuration).SetEase(Ease.InQuad));
_attackSequence.AppendCallback(() =>
{
_isAttacking = false;
_attackSequence = null;
_attackParent = null;
});
}
protected override void Check()
{
_target = SelectTarget(_sqrRange);
}
private void RotateToTarget(float elapseSeconds)
{
if (_target == null || !_target.Available) return;
Vector3 directionToTarget = _target.CachedTransform.position - CachedTransform.position;
directionToTarget.y = 0f;
if (directionToTarget.sqrMagnitude <= Mathf.Epsilon) return;
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget.normalized, Vector3.up);
CachedTransform.rotation =
Quaternion.Slerp(CachedTransform.rotation, targetRotation, _rotateSpeed * elapseSeconds);
}
private void RotateToOrigin(float elapseSeconds)
{
CachedTransform.rotation =
Quaternion.Slerp(CachedTransform.rotation, _cachedRotation, _rotateSpeed * elapseSeconds);
}
private void FaceTargetImmediately()
{
if (_target == null || !_target.Available) return;
Vector3 directionToTarget = _target.CachedTransform.position - CachedTransform.position;
directionToTarget.y = 0f;
if (directionToTarget.sqrMagnitude <= Mathf.Epsilon) return;
CachedTransform.rotation = Quaternion.LookRotation(directionToTarget.normalized, Vector3.up);
}
private Vector3 GetStrikeStart()
{
Vector3 forward = CachedTransform.forward;
forward.y = 0f;
if (forward.sqrMagnitude <= Mathf.Epsilon)
{
forward = Vector3.forward;
}
forward.Normalize();
return CachedTransform.position + forward * _forwardOffset;
}
private Vector3 GetStrikeEnd()
{
Vector3 start = GetStrikeStart();
Vector3 forward = CachedTransform.forward;
forward.y = 0f;
if (forward.sqrMagnitude <= Mathf.Epsilon)
{
forward = Vector3.forward;
}
forward.Normalize();
return start + forward * _pierceLength;
}
private Vector3 GetStrikeCenter()
{
return Vector3.Lerp(GetStrikeStart(), GetStrikeEnd(), 0.5f);
}
private void ApplyPierceDamage()
{
if (_hitRadius <= 0f || _pierceLength <= 0f) return;
Vector3 strikeStart = GetStrikeStart();
Vector3 strikeEnd = GetStrikeEnd();
Vector3 strikeDirection = strikeEnd - strikeStart;
strikeDirection.y = 0f;
if (strikeDirection.sqrMagnitude <= Mathf.Epsilon) return;
strikeDirection.Normalize();
float halfAngle = Mathf.Rad2Deg * Mathf.Atan2(_hitRadius, Mathf.Max(0.01f, _pierceLength));
if (TryQueueSectorCollisionQuery(strikeStart, _pierceLength, in strikeDirection, halfAngle,
Mathf.Max(1, _maxHitColliders)))
{
_hitEntityIds.Clear();
return;
}
int capacity = Mathf.Max(1, _maxHitColliders);
float broadPhaseRadius = _pierceLength * 0.5f + _hitRadius;
Vector3 broadPhaseCenter = Vector3.Lerp(strikeStart, strikeEnd, 0.5f);
if (_hitResults == null || _hitResults.Length != capacity)
{
_hitResults = new Collider[capacity];
}
int hitCount = Physics.OverlapSphereNonAlloc(broadPhaseCenter, broadPhaseRadius, _hitResults, _hitMask,
QueryTriggerInteraction.Collide);
_hitEntityIds.Clear();
for (int i = 0; i < hitCount; i++)
{
Collider collider = _hitResults[i];
if (collider == null) continue;
TargetableObject targetable = collider.GetComponentInParent<TargetableObject>();
if (targetable == null || !targetable.Available || targetable.IsDead) continue;
if (!_hitEntityIds.Add(targetable.Id)) continue;
if (!IsTargetInsidePierce(targetable, strikeStart, strikeDirection)) continue;
AIUtility.PerformCollision(targetable, this);
}
}
private bool IsTargetInsidePierce(TargetableObject targetable, Vector3 strikeStart, Vector3 strikeDirection)
{
Vector3 toTarget = targetable.CachedTransform.position - strikeStart;
toTarget.y = 0f;
float projection = Vector3.Dot(toTarget, strikeDirection);
if (projection < 0f || projection > _pierceLength) return false;
Vector3 closestPoint = strikeStart + strikeDirection * projection;
Vector3 delta = targetable.CachedTransform.position - closestPoint;
delta.y = 0f;
return delta.sqrMagnitude <= _hitRadius * _hitRadius;
}
protected override bool OnWeaponShow(object userData)
{
_weaponData = RequireWeaponData<WeaponLanceData>(userData);
if (_weaponData == null) return false;
WeaponData = _weaponData;
_currAttackTimer = 0f;
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
_cachedRotation = CachedTransform.rotation;
WeaponLanceParamsData paramsData = _weaponData.ParamsData;
_hitRadius = paramsData != null && paramsData.HitRadius > 0f
? Mathf.Max(0.1f, paramsData.HitRadius)
: 0.45f;
_thrustDistance = paramsData != null && paramsData.ThrustDistance > 0f
? paramsData.ThrustDistance
: _weaponData.AttackRange;
_pierceLength = paramsData != null && paramsData.PierceLength > 0f
? paramsData.PierceLength
: Mathf.Max(_weaponData.AttackRange, _thrustDistance);
_forwardOffset = paramsData != null && paramsData.ForwardOffset > 0f
? paramsData.ForwardOffset
: 0f;
if (paramsData != null && paramsData.RotateSpeed > 0f)
{
_rotateSpeed = paramsData.RotateSpeed;
}
if (paramsData != null && paramsData.AttackDuration > 0f)
{
_attackDuration = paramsData.AttackDuration;
}
if (paramsData != null && paramsData.ReturnDuration > 0f)
{
_returnDuration = paramsData.ReturnDuration;
}
int colliderCapacity = Mathf.Max(1, _maxHitColliders);
if (_hitResults == null || _hitResults.Length != colliderCapacity)
{
_hitResults = new Collider[colliderCapacity];
}
_attackEffect = new KnifeRangeAttackEffect();
if (_weaponData.OwnerCamp == CampType.Player)
{
gameObject.layer = LayerMask.NameToLayer("PlayerWeapon");
_hitMask = LayerMask.GetMask("Enemy");
}
else if (_weaponData.OwnerCamp == CampType.Enemy)
{
gameObject.layer = LayerMask.NameToLayer("EnemyWeapon");
_hitMask = LayerMask.GetMask("Player");
}
return true;
}
protected override void OnWeaponHide(object userData)
{
StopAttackTween(true);
_attackEffect = null;
}
protected override void OnWeaponAttach(EntityLogic parentEntity, Transform parentTransform, object userData)
{
BindAttackStatFromOwner(parentEntity);
}
protected override void OnWeaponDetach(EntityLogic parentEntity, object userData)
{
StopAttackTween(true);
ReleaseAttackStatSubscription();
}
protected override void OnEnabledChanged(bool enabled)
{
if (!enabled)
{
StopAttackTween(true);
}
}
private void StopAttackTween(bool resetTransform)
{
if (_attackSequence != null)
{
_attackSequence.Kill();
_attackSequence = null;
}
_isAttacking = false;
if (resetTransform && _attackParent != null)
{
CachedTransform.SetParent(_attackParent);
CachedTransform.localPosition = Vector3.zero;
CachedTransform.rotation = _cachedRotation;
}
_attackParent = null;
_hitEntityIds.Clear();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f90612be7695ee549b39473c9b706122
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -92,14 +92,9 @@ namespace Procedure
GameEntry.Entity.HideEntity(entity.Id); GameEntry.Entity.HideEntity(entity.Id);
} }
var enemyProjectiles = GameEntry.Entity.GetEntityGroup("EnemyProjectile")?.GetAllEntities(); HideEntityGroup("Bullet");
if (enemyProjectiles != null) HideEntityGroup("Projectile");
{ HideEntityGroup("EnemyProjectile");
foreach (var projectile in enemyProjectiles)
{
GameEntry.Entity.HideEntity(projectile.Id);
}
}
} }
public override void OnDestroy(IFsm<IProcedureManager> procedureOwner) public override void OnDestroy(IFsm<IProcedureManager> procedureOwner)
@ -108,6 +103,21 @@ namespace Procedure
_procedureGame = null; _procedureGame = null;
} }
private static void HideEntityGroup(string groupName)
{
var entityGroup = GameEntry.Entity.GetEntityGroup(groupName);
var entities = entityGroup?.GetAllEntities();
if (entities == null)
{
return;
}
foreach (var entity in entities)
{
GameEntry.Entity.HideEntity(entity.Id);
}
}
#endregion #endregion
} }
} }

View File

@ -104,18 +104,8 @@ namespace Simulation
private void PlayHitMarker(ProjectileHitPresentationEventArgs args) private void PlayHitMarker(ProjectileHitPresentationEventArgs args)
{ {
EntityBase targetEntity = TryGetEntityById(args.TargetEntityId); // Projectile hit markers were reusing the handgun marker effect and were
if (targetEntity == null || !targetEntity.Available) // visually indistinguishable from handgun lock/hit feedback.
{
return;
}
_projectileHitMarkerEffect ??= new HandgunHitMarkerAttackEffect(
Mathf.Max(0.01f, _world._projectileHitMarkerSize),
_world._projectileHitMarkerYOffset,
Mathf.Max(0.01f, _world._projectileHitMarkerDuration),
_world._projectileHitMarkerColor);
_projectileHitMarkerEffect.Play(null, args.HitPosition, targetEntity, 0f);
} }
private void PlayHitEffect(ProjectileHitPresentationEventArgs args) private void PlayHitEffect(ProjectileHitPresentationEventArgs args)

View File

@ -140,18 +140,6 @@ namespace UI
return; return;
} }
if (Context == null)
{
Log.Error("DisplayItemInfoFormController.DisplayItemInfoHide() Context is null.");
return;
}
if (Form == null)
{
Log.Error("DisplayItemInfoFormController.DisplayItemInfoHide() Form is null.");
return;
}
if (args.Force) if (args.Force)
{ {
GameEntry.UIRouter.CloseUI(UIFormType.DisplayItemInfoForm); GameEntry.UIRouter.CloseUI(UIFormType.DisplayItemInfoForm);
@ -159,10 +147,7 @@ namespace UI
return; return;
} }
if (_locked && !IsCurrentFormSender(sender) && sender is not DisplayItem) if (_locked && !args.Force) return;
{
return;
}
GameEntry.UIRouter.CloseUI(UIFormType.DisplayItemInfoForm); GameEntry.UIRouter.CloseUI(UIFormType.DisplayItemInfoForm);
_locked = false; _locked = false;

View File

@ -199,6 +199,12 @@ namespace UI
#region UI Methods #region UI Methods
public override void CloseUI()
{
base.CloseUI();
GameEntry.Event.Fire(this, DisplayItemInfoHideEventArgs.Create(true));
}
public int? OpenUI(ShopFormRawData rawData) public int? OpenUI(ShopFormRawData rawData)
{ {
ShopFormContext context = BuildContext(rawData); ShopFormContext context = BuildContext(rawData);

View File

@ -397,6 +397,8 @@ namespace UI
return new WeaponSlashData(entityId, ownerId, ownerCamp); return new WeaponSlashData(entityId, ownerId, ownerCamp);
case WeaponType.WeaponLightning: case WeaponType.WeaponLightning:
return new WeaponLightningData(entityId, ownerId, ownerCamp); return new WeaponLightningData(entityId, ownerId, ownerCamp);
case WeaponType.WeaponLance:
return new WeaponLanceData(entityId, ownerId, ownerCamp);
default: default:
return null; return null;
} }

View File

@ -199,7 +199,7 @@ namespace UI
public void OnCancelButtonClick() public void OnCancelButtonClick()
{ {
GameEntry.Event.Fire(this, DisplayItemInfoHideEventArgs.Create()); GameEntry.Event.Fire(this, DisplayItemInfoHideEventArgs.Create(true));
} }
} }
} }

View File

@ -4,16 +4,23 @@ using DataTable;
using Definition.DataStruct; using Definition.DataStruct;
using Entity.EntityData; using Entity.EntityData;
using Entity.Weapon; using Entity.Weapon;
using System;
using UnityGameFramework.Runtime; using UnityGameFramework.Runtime;
namespace CustomUtility namespace CustomUtility
{ {
public static class ItemDescUtility public static class ItemDescUtility
{ {
private static readonly Dictionary<string, string> _paramsDict = new() private static readonly Dictionary<string, string> _paramsDict = new(StringComparer.OrdinalIgnoreCase)
{ {
{"hitradius", "伤害范围"}, {"hitRadius", "伤害范围"},
{"sectorangle", "攻击角度"} {"sectorAngle", "攻击角度"},
{"pierceLength", "前戳距离"},
{"thrustDistance", "枪尖长度"},
{"forwardOffset", "前置偏移"},
{"rotateSpeed", "转向速度"},
{"attackDuration", "突刺时长"},
{"returnDuration", "收枪时长"}
}; };
public static string CreatePropDescription(StatModifier[] modifiers) public static string CreatePropDescription(StatModifier[] modifiers)
@ -89,6 +96,11 @@ namespace CustomUtility
sb.Append(modifiersDesc); sb.Append(modifiersDesc);
} }
if (@params == null || @params.Count == 0)
{
return sb.ToString();
}
foreach (var kvp in @params) foreach (var kvp in @params)
{ {
if (!_paramsDict.TryGetValue(kvp.Key, out string value)) if (!_paramsDict.TryGetValue(kvp.Key, out string value))

View File

@ -731,7 +731,7 @@ PrefabInstance:
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 11499388, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3} - target: {fileID: 11499388, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
propertyPath: m_EditorResourceMode propertyPath: m_EditorResourceMode
value: 0 value: 1
objectReference: {fileID: 0} objectReference: {fileID: 0}
- target: {fileID: 11499388, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3} - target: {fileID: 11499388, guid: adb3eb1c35fcff14f89fba7b05c9d71c, type: 3}
propertyPath: m_JsonHelperTypeName propertyPath: m_JsonHelperTypeName

Binary file not shown.

Binary file not shown.

Binary file not shown.