From 0d7df18324d73b881bf8799a977719bcfe113dc4 Mon Sep 17 00:00:00 2001
From: SepComet <2428390463@qq.com>
Date: Fri, 20 Mar 2026 09:55:52 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20WeaponLance=20=E7=9A=84?=
=?UTF-8?q?=E6=94=BB=E5=87=BB=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Assets/GameMain/DataTables/Weapon.txt | 2 +-
.../EntityData/Weapon/WeaponLanceData.cs | 23 ++-
.../AttackEffects/LanceThrustAttackEffect.cs | 82 +++++++++
.../LanceThrustAttackEffect.cs.meta | 11 ++
.../Entity/EntityLogic/Weapon/WeaponBase.cs | 16 +-
.../Weapon/WeaponLance/WeaponLance.cs | 158 +++++++++++-------
.../SimulationWorld.CollisionTransient.cs | 10 +-
.../SimulationWorld.JobDataChannel.cs | 1 +
.../JobStruct/AreaCollisionRequestData.cs | 4 +-
.../JobStruct/CollisionQueryData.cs | 4 +-
.../QueryCollisionCandidatesBurstJob.cs | 22 ++-
.../SimulationWorld.CollisionBroadPhase.cs | 3 +-
.../Jobs/SimulationWorld.CollisionRequests.cs | 21 ++-
.../Jobs/SimulationWorld.CollisionResolve.cs | 46 ++++-
.../Scripts/Utility/ItemDescUtility.cs | 7 +-
数据表/Entity/Weapon.xlsx | Bin 12936 -> 12931 bytes
16 files changed, 321 insertions(+), 89 deletions(-)
create mode 100644 Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/AttackEffects/LanceThrustAttackEffect.cs
create mode 100644 Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/AttackEffects/LanceThrustAttackEffect.cs.meta
diff --git a/Assets/GameMain/DataTables/Weapon.txt b/Assets/GameMain/DataTables/Weapon.txt
index 5075f13..921e7c9 100644
--- a/Assets/GameMain/DataTables/Weapon.txt
+++ b/Assets/GameMain/DataTables/Weapon.txt
@@ -5,4 +5,4 @@
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} []
- 5 205 长枪 Almighty_Icon White 100 0.1 100 1.5 7 10000 {"hitRadius":0.3,"thrustDistance":1.2,"pierceLength":0.3}
+ 5 205 长枪 Almighty_Icon White 100 0.1 100 1.5 5 10000 {"hitHalfWidth":0.7,"pierceLength":4.5,"hitHeight":0.5,"hitCenterYOffset":0}
diff --git a/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponLanceData.cs b/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponLanceData.cs
index c3c3cce..b3183c1 100644
--- a/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponLanceData.cs
+++ b/Assets/GameMain/Scripts/Entity/EntityData/Weapon/WeaponLanceData.cs
@@ -7,20 +7,35 @@ namespace Entity.EntityData
public sealed class WeaponLanceParamsData
{
///
- /// 枪尖命中半径。
+ /// 横向半宽,表示前戳矩形判定的一半宽度。
+ ///
+ public float HitHalfWidth { get; set; }
+
+ ///
+ /// 旧字段兼容,未配置 HitHalfWidth 时回退使用。
///
public float HitRadius { get; set; }
///
- /// 武器模型前刺的位移距离。
+ /// 前戳判定盒体的总高度。
///
- public float ThrustDistance { get; set; }
+ public float HitHeight { get; set; }
///
- /// 实际判定的前刺长度。
+ /// 判定盒体中心相对战斗平面的高度偏移。
+ ///
+ public float HitCenterYOffset { get; set; }
+
+ ///
+ /// 前刺距离,同时驱动武器位移和命中长度。
///
public float PierceLength { get; set; }
+ ///
+ /// 旧字段兼容,未配置 PierceLength 时回退使用。
+ ///
+ public float ThrustDistance { get; set; }
+
///
/// 判定起点相对武器当前位置的前置偏移。
///
diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/AttackEffects/LanceThrustAttackEffect.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/AttackEffects/LanceThrustAttackEffect.cs
new file mode 100644
index 0000000..b358215
--- /dev/null
+++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/AttackEffects/LanceThrustAttackEffect.cs
@@ -0,0 +1,82 @@
+using UnityEngine;
+
+namespace Entity.Weapon
+{
+ public sealed class LanceThrustAttackEffect : IWeaponAttackEffect
+ {
+ private readonly float _duration = 0.18f;
+ private readonly float _yOffset = 0.06f;
+ private readonly float _lineWidth = 0.05f;
+ private readonly Color _color = new(0.2f, 0.95f, 0.7f, 0.92f);
+
+ public LanceThrustAttackEffect()
+ {
+ }
+
+ public LanceThrustAttackEffect(float duration, float yOffset, float lineWidth, Color color)
+ {
+ _duration = duration;
+ _yOffset = yOffset;
+ _lineWidth = lineWidth;
+ _color = color;
+ }
+
+ public void Play(WeaponBase weapon, Vector3 position, EntityBase target, float radius)
+ {
+ if (weapon is not WeaponLance lance) return;
+
+ Vector3 forward = lance.StrikeDirection;
+ forward.y = 0f;
+ if (forward.sqrMagnitude <= Mathf.Epsilon)
+ {
+ forward = Vector3.forward;
+ }
+
+ forward.Normalize();
+ Vector3 right = Vector3.Cross(Vector3.up, forward);
+ float halfWidth = Mathf.Max(0.1f, lance.HitHalfWidth);
+ float halfLength = Mathf.Max(0.1f, lance.PierceLength * 0.5f);
+ Vector3 center = position + Vector3.up * _yOffset;
+
+ Vector3 frontCenter = center + forward * halfLength;
+ Vector3 backCenter = center - forward * halfLength;
+
+ Vector3[] corners =
+ {
+ frontCenter - right * halfWidth,
+ frontCenter + right * halfWidth,
+ backCenter + right * halfWidth,
+ backCenter - right * halfWidth,
+ };
+
+ GameObject indicator = new GameObject("LanceThrustIndicator");
+ indicator.transform.position = center;
+
+ LineRenderer line = indicator.AddComponent();
+ line.loop = true;
+ line.useWorldSpace = true;
+ line.positionCount = corners.Length;
+ line.startWidth = _lineWidth;
+ line.endWidth = _lineWidth;
+ line.startColor = _color;
+ line.endColor = _color;
+
+ Shader shader = Shader.Find("Sprites/Default");
+ if (shader == null)
+ {
+ shader = Shader.Find("Unlit/Color");
+ }
+
+ Material material = new Material(shader);
+ material.color = _color;
+ line.material = material;
+
+ for (int i = 0; i < corners.Length; i++)
+ {
+ line.SetPosition(i, corners[i]);
+ }
+
+ Object.Destroy(indicator, Mathf.Max(0.01f, _duration));
+ }
+ }
+}
diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/AttackEffects/LanceThrustAttackEffect.cs.meta b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/AttackEffects/LanceThrustAttackEffect.cs.meta
new file mode 100644
index 0000000..ebbae42
--- /dev/null
+++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/AttackEffects/LanceThrustAttackEffect.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8c77c590c7233e544a9295ed41ff8827
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponBase.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponBase.cs
index 3a4fd7c..0f67bd6 100644
--- a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponBase.cs
+++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponBase.cs
@@ -246,6 +246,20 @@ namespace Entity.Weapon
halfAngleDeg, maxTargets);
}
+ protected bool TryQueueRectangleCollisionQuery(in Vector3 center, float halfWidth, float halfLength,
+ in Vector3 direction, int maxTargets = 16)
+ {
+ var simulationWorld = GameEntry.SimulationWorld;
+ if (simulationWorld == null)
+ {
+ return false;
+ }
+
+ int ownerEntityId = WeaponData != null ? WeaponData.OwnerId : Id;
+ return simulationWorld.TryRequestRectangleCollision(Id, ownerEntityId, in center, halfWidth, halfLength,
+ in direction, maxTargets);
+ }
+
protected void SetTargetSelector(TargetSelectorType selectorType)
{
TargetSelector = CreateSelector(selectorType);
@@ -304,4 +318,4 @@ namespace Entity.Weapon
public abstract void OnLeave();
public override string ToString() => State.ToString();
}
-}
\ No newline at end of file
+}
diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponLance/WeaponLance.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponLance/WeaponLance.cs
index 2a94a49..ddf076f 100644
--- a/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponLance/WeaponLance.cs
+++ b/Assets/GameMain/Scripts/Entity/EntityLogic/Weapon/WeaponLance/WeaponLance.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using Components;
using CustomUtility;
using Definition.DataStruct;
using Definition.Enum;
@@ -33,14 +34,20 @@ namespace Entity.Weapon
private Collider[] _hitResults;
private readonly HashSet _hitEntityIds = new();
- // 枪尖判定半径。
- private float _hitRadius;
+ // 前戳矩形判定的横向半宽。
+ private float _hitHalfWidth;
+ // 前戳矩形判定盒体的总高度。
+ private float _hitHeight;
+ // 盒体中心相对战斗平面的高度偏移。
+ private float _hitCenterYOffset;
// 从判定起点向前延伸的有效刺击长度。
private float _pierceLength;
// 判定起点相对武器当前位置的前移量。
private float _forwardOffset;
- // 武器模型本身向前突刺的位移长度。
- private float _thrustDistance;
+ // 本次攻击锁定的前戳方向,避免受位移动画中的武器位置影响。
+ private Vector3 _strikeDirection = Vector3.forward;
+ // 本次攻击锁定的矩形判定中心。
+ private Vector3 _strikeCenter;
public override ImpactData GetImpactData()
{
@@ -59,18 +66,18 @@ namespace Entity.Weapon
{
StopAttackTween(false);
FaceTargetImmediately();
+ CacheStrikeSnapshot();
_isAttacking = true;
_attackParent = CachedTransform.parent;
CachedTransform.SetParent(null);
- Vector3 targetPos = CachedTransform.position + CachedTransform.forward * _thrustDistance;
+ Vector3 targetPos = CachedTransform.position + _strikeDirection * _pierceLength;
_attackSequence = DOTween.Sequence();
_attackSequence.Append(CachedTransform.DOMove(targetPos, _attackDuration).SetEase(Ease.OutQuad));
_attackSequence.AppendCallback(() =>
{
- Vector3 strikeCenter = GetStrikeCenter();
- _attackEffect?.Play(this, strikeCenter, _target, _hitRadius);
+ _attackEffect?.Play(this, _strikeCenter, _target, _hitHalfWidth);
ApplyPierceDamage();
if (_attackParent != null)
@@ -122,7 +129,17 @@ namespace Entity.Weapon
CachedTransform.rotation = Quaternion.LookRotation(directionToTarget.normalized, Vector3.up);
}
- private Vector3 GetStrikeStart()
+ private void CacheStrikeSnapshot()
+ {
+ _strikeDirection = ResolvePlanarForward();
+ Vector3 strikeStart = CachedTransform.position;
+ strikeStart.y = ResolveStrikePlaneY();
+ strikeStart += _strikeDirection * _forwardOffset;
+ _strikeCenter = strikeStart + _strikeDirection * (_pierceLength * 0.5f);
+ _strikeCenter.y += _hitCenterYOffset;
+ }
+
+ private Vector3 ResolvePlanarForward()
{
Vector3 forward = CachedTransform.forward;
forward.y = 0f;
@@ -132,59 +149,35 @@ namespace Entity.Weapon
}
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);
+ return forward;
}
private void ApplyPierceDamage()
{
- if (_hitRadius <= 0f || _pierceLength <= 0f) return;
+ if (_hitHalfWidth <= 0f || _pierceLength <= 0f) return;
- Vector3 strikeStart = GetStrikeStart();
- Vector3 strikeEnd = GetStrikeEnd();
- Vector3 strikeDirection = strikeEnd - strikeStart;
- strikeDirection.y = 0f;
+ Vector3 strikeDirection = _strikeDirection;
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)))
+ Vector3 broadPhaseCenter = _strikeCenter;
+ Quaternion broadPhaseRotation = Quaternion.LookRotation(strikeDirection, Vector3.up);
+
+ if (TryQueueRectangleCollisionQuery(broadPhaseCenter, _hitHalfWidth, _pierceLength * 0.5f,
+ strikeDirection, 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);
-
+ Vector3 halfExtents = new(_hitHalfWidth, _hitHeight * 0.5f, _pierceLength * 0.5f);
+ int hitCount = Physics.OverlapBoxNonAlloc(broadPhaseCenter, halfExtents, _hitResults, broadPhaseRotation,
+ _hitMask, QueryTriggerInteraction.Collide);
_hitEntityIds.Clear();
for (int i = 0; i < hitCount; i++)
{
@@ -195,24 +188,55 @@ namespace Entity.Weapon
if (targetable == null || !targetable.Available || targetable.IsDead) continue;
if (!_hitEntityIds.Add(targetable.Id)) continue;
- if (!IsTargetInsidePierce(targetable, strikeStart, strikeDirection)) continue;
+ if (!IsTargetInsidePierce(targetable, broadPhaseCenter, strikeDirection)) continue;
AIUtility.PerformCollision(targetable, this);
}
}
- private bool IsTargetInsidePierce(TargetableObject targetable, Vector3 strikeStart, Vector3 strikeDirection)
+ private bool IsTargetInsidePierce(TargetableObject targetable, Vector3 strikeCenter, 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;
+ Vector3 delta = targetable.CachedTransform.position - strikeCenter;
delta.y = 0f;
- return delta.sqrMagnitude <= _hitRadius * _hitRadius;
+
+ Vector3 right = Vector3.Cross(Vector3.up, strikeDirection);
+ float forwardDistance = Vector3.Dot(delta, strikeDirection);
+ float lateralDistance = Vector3.Dot(delta, right);
+ float targetRadius = ResolveTargetCollisionRadius(targetable);
+
+ return Mathf.Abs(forwardDistance) <= _pierceLength * 0.5f + targetRadius &&
+ Mathf.Abs(lateralDistance) <= _hitHalfWidth + targetRadius;
+ }
+
+ private static float ResolveTargetCollisionRadius(TargetableObject targetable)
+ {
+ if (targetable == null)
+ {
+ return 0f;
+ }
+
+ MovementComponent movementComponent = targetable.GetComponent();
+ return movementComponent != null ? Mathf.Max(0f, movementComponent.EnemyBodyRadius) : 0f;
+ }
+
+ private float ResolveStrikePlaneY()
+ {
+ if (_target != null && _target.Available)
+ {
+ return _target.CachedTransform.position.y;
+ }
+
+ if (_attackParent != null)
+ {
+ return _attackParent.position.y;
+ }
+
+ if (CachedTransform.parent != null)
+ {
+ return CachedTransform.parent.position.y;
+ }
+
+ return CachedTransform.position.y;
}
protected override bool OnWeaponShow(object userData)
@@ -226,15 +250,21 @@ namespace Entity.Weapon
_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;
+ float configuredHalfWidth = paramsData != null && paramsData.HitHalfWidth > 0f
+ ? paramsData.HitHalfWidth
+ : paramsData != null && paramsData.HitRadius > 0f
+ ? paramsData.HitRadius
+ : 0.45f;
+ _hitHalfWidth = Mathf.Max(0.1f, configuredHalfWidth);
+ _hitHeight = paramsData != null && paramsData.HitHeight > 0f
+ ? Mathf.Max(0.2f, paramsData.HitHeight)
+ : 4f;
+ _hitCenterYOffset = paramsData != null ? paramsData.HitCenterYOffset : 0f;
_pierceLength = paramsData != null && paramsData.PierceLength > 0f
? paramsData.PierceLength
- : Mathf.Max(_weaponData.AttackRange, _thrustDistance);
+ : paramsData != null && paramsData.ThrustDistance > 0f
+ ? paramsData.ThrustDistance
+ : _weaponData.AttackRange;
_forwardOffset = paramsData != null && paramsData.ForwardOffset > 0f
? paramsData.ForwardOffset
: 0f;
@@ -260,7 +290,7 @@ namespace Entity.Weapon
_hitResults = new Collider[colliderCapacity];
}
- _attackEffect = new KnifeRangeAttackEffect();
+ _attackEffect = new LanceThrustAttackEffect();
if (_weaponData.OwnerCamp == CampType.Player)
{
@@ -321,5 +351,9 @@ namespace Entity.Weapon
_attackParent = null;
_hitEntityIds.Clear();
}
+
+ public float HitHalfWidth => _hitHalfWidth;
+ public float PierceLength => _pierceLength;
+ public Vector3 StrikeDirection => _strikeDirection;
}
}
diff --git a/Assets/GameMain/Scripts/Simulation/DataChannel/SimulationWorld.CollisionTransient.cs b/Assets/GameMain/Scripts/Simulation/DataChannel/SimulationWorld.CollisionTransient.cs
index ec2f324..a18242b 100644
--- a/Assets/GameMain/Scripts/Simulation/DataChannel/SimulationWorld.CollisionTransient.cs
+++ b/Assets/GameMain/Scripts/Simulation/DataChannel/SimulationWorld.CollisionTransient.cs
@@ -65,13 +65,15 @@ namespace Simulation
MaxTargets = math.max(1, maxTargets),
ShapeType = CollisionShapeCircle,
Direction = new float3(0f, 0f, 1f),
- HalfAngleDeg = 180f
+ HalfAngleDeg = 180f,
+ HalfWidth = 0f,
+ HalfLength = 0f
});
}
private void AddAreaCollisionQuery(int queryId, int sourceEntityId, int sourceOwnerEntityId,
bool sourceWasActiveAtQueryTime, in Vector3 center, float radius, int maxTargets, int shapeType,
- in Vector3 direction, float halfAngleDeg)
+ in Vector3 direction, float halfAngleDeg, float halfWidth, float halfLength)
{
if (!_collisionQueryInputs.IsCreated || radius <= 0f)
{
@@ -101,7 +103,9 @@ namespace Simulation
MaxTargets = math.max(1, maxTargets),
ShapeType = shapeType,
Direction = new float3(normalizedDirection.x, normalizedDirection.y, normalizedDirection.z),
- HalfAngleDeg = Mathf.Clamp(halfAngleDeg, 0f, 180f)
+ HalfAngleDeg = Mathf.Clamp(halfAngleDeg, 0f, 180f),
+ HalfWidth = Mathf.Max(0f, halfWidth),
+ HalfLength = Mathf.Max(0f, halfLength)
});
}
diff --git a/Assets/GameMain/Scripts/Simulation/DataChannel/SimulationWorld.JobDataChannel.cs b/Assets/GameMain/Scripts/Simulation/DataChannel/SimulationWorld.JobDataChannel.cs
index a4211b5..5dd7ac6 100644
--- a/Assets/GameMain/Scripts/Simulation/DataChannel/SimulationWorld.JobDataChannel.cs
+++ b/Assets/GameMain/Scripts/Simulation/DataChannel/SimulationWorld.JobDataChannel.cs
@@ -10,6 +10,7 @@ namespace Simulation
private const int CollisionSourceTypeArea = 2;
private const int CollisionShapeCircle = 0;
private const int CollisionShapeSector = 1;
+ private const int CollisionShapeRectangle = 2;
// Kept as top-level fields because current regression tests reflect them directly.
private NativeList _collisionQueryInputs;
diff --git a/Assets/GameMain/Scripts/Simulation/JobStruct/AreaCollisionRequestData.cs b/Assets/GameMain/Scripts/Simulation/JobStruct/AreaCollisionRequestData.cs
index 27df2b4..0e51176 100644
--- a/Assets/GameMain/Scripts/Simulation/JobStruct/AreaCollisionRequestData.cs
+++ b/Assets/GameMain/Scripts/Simulation/JobStruct/AreaCollisionRequestData.cs
@@ -15,6 +15,8 @@ namespace Simulation
public int ShapeType;
public Vector3 Direction;
public float HalfAngleDeg;
+ public float HalfWidth;
+ public float HalfLength;
}
}
-}
\ No newline at end of file
+}
diff --git a/Assets/GameMain/Scripts/Simulation/JobStruct/CollisionQueryData.cs b/Assets/GameMain/Scripts/Simulation/JobStruct/CollisionQueryData.cs
index cdf7c23..75d6cd9 100644
--- a/Assets/GameMain/Scripts/Simulation/JobStruct/CollisionQueryData.cs
+++ b/Assets/GameMain/Scripts/Simulation/JobStruct/CollisionQueryData.cs
@@ -17,6 +17,8 @@ namespace Simulation
public int ShapeType;
public float3 Direction;
public float HalfAngleDeg;
+ public float HalfWidth;
+ public float HalfLength;
}
}
-}
\ No newline at end of file
+}
diff --git a/Assets/GameMain/Scripts/Simulation/JobStruct/QueryCollisionCandidatesBurstJob.cs b/Assets/GameMain/Scripts/Simulation/JobStruct/QueryCollisionCandidatesBurstJob.cs
index 3765232..b654f41 100644
--- a/Assets/GameMain/Scripts/Simulation/JobStruct/QueryCollisionCandidatesBurstJob.cs
+++ b/Assets/GameMain/Scripts/Simulation/JobStruct/QueryCollisionCandidatesBurstJob.cs
@@ -23,7 +23,6 @@ namespace Simulation
public void Execute(int index)
{
CollisionQueryData query = Queries[index];
- float radiusSqr = query.Radius * query.Radius;
int centerCellX = (int)math.floor(query.Position.x / CellSize);
int centerCellZ = (int)math.floor(query.Position.z / CellSize);
int queryRange = math.max(1, (int)math.ceil(query.Radius / CellSize));
@@ -34,10 +33,14 @@ namespace Simulation
query.SourceOwnerEntityId != PlayerTargetEntityId)
{
float3 playerPosition = PlayerPosition;
- playerPosition.y = query.Position.y;
+ if (query.SourceType == CollisionSourceTypeArea)
+ {
+ playerPosition.y = query.Position.y;
+ }
float3 playerDelta = playerPosition - query.Position;
float playerSqrDistance = math.lengthsq(playerDelta);
- if (playerSqrDistance <= radiusSqr)
+ float playerRadiusSqr = query.Radius * query.Radius;
+ if (playerSqrDistance <= playerRadiusSqr)
{
Candidates.AddNoResize(new CollisionCandidateData
{
@@ -86,12 +89,19 @@ namespace Simulation
continue;
}
+ float deltaY = query.SourceType == CollisionSourceTypeArea
+ ? 0f
+ : enemy.Position.y - query.Position.y;
float3 delta = new float3(
enemy.Position.x - query.Position.x,
- enemy.Position.y - query.Position.y,
+ deltaY,
enemy.Position.z - query.Position.z);
float sqrDistance = math.lengthsq(delta);
- if (sqrDistance > radiusSqr)
+ float targetRadius = query.SourceType == CollisionSourceTypeArea
+ ? math.max(0f, enemy.EnemyBodyRadius)
+ : 0f;
+ float effectiveRadius = query.Radius + targetRadius;
+ if (sqrDistance > effectiveRadius * effectiveRadius)
{
continue;
}
@@ -119,4 +129,4 @@ namespace Simulation
}
}
}
-}
\ No newline at end of file
+}
diff --git a/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionBroadPhase.cs b/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionBroadPhase.cs
index d7a3c90..9803c85 100644
--- a/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionBroadPhase.cs
+++ b/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionBroadPhase.cs
@@ -58,7 +58,8 @@ namespace Simulation
AreaCollisionRequestData request = _areaCollisionRequests[i];
AddAreaCollisionQuery(queryId, request.SourceEntityId, request.SourceOwnerEntityId,
request.SourceWasActiveAtQueryTime, in request.Center, request.Radius, request.MaxTargets,
- request.ShapeType, in request.Direction, request.HalfAngleDeg);
+ request.ShapeType, in request.Direction, request.HalfAngleDeg, request.HalfWidth,
+ request.HalfLength);
queryId++;
builtAreaQueryCount++;
if (request.Radius > maxQueryRadius)
diff --git a/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionRequests.cs b/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionRequests.cs
index 84e8ebb..4b790c6 100644
--- a/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionRequests.cs
+++ b/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionRequests.cs
@@ -12,18 +12,29 @@ namespace Simulation
float radius, int maxTargets = 16)
{
return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, radius,
- maxTargets, CollisionShapeCircle, Vector3.forward, 180f);
+ maxTargets, CollisionShapeCircle, Vector3.forward, 180f, 0f, 0f);
}
public bool TryRequestSectorCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
float radius, in Vector3 direction, float halfAngleDeg, int maxTargets = 16)
{
return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, radius,
- maxTargets, CollisionShapeSector, direction, halfAngleDeg);
+ maxTargets, CollisionShapeSector, direction, halfAngleDeg, 0f, 0f);
+ }
+
+ public bool TryRequestRectangleCollision(int sourceEntityId, int sourceOwnerEntityId, in Vector3 center,
+ float halfWidth, float halfLength, in Vector3 direction, int maxTargets = 16)
+ {
+ float safeHalfWidth = Mathf.Max(0.01f, halfWidth);
+ float safeHalfLength = Mathf.Max(0.01f, halfLength);
+ float boundingRadius = Mathf.Sqrt(safeHalfWidth * safeHalfWidth + safeHalfLength * safeHalfLength);
+ return TryRequestAreaCollisionInternal(sourceEntityId, sourceOwnerEntityId, in center, boundingRadius,
+ maxTargets, CollisionShapeRectangle, direction, 0f, safeHalfWidth, safeHalfLength);
}
private bool TryRequestAreaCollisionInternal(int sourceEntityId, int sourceOwnerEntityId,
- in Vector3 center, float radius, int maxTargets, int shapeType, in Vector3 direction, float halfAngleDeg)
+ in Vector3 center, float radius, int maxTargets, int shapeType, in Vector3 direction, float halfAngleDeg,
+ float halfWidth, float halfLength)
{
if (!_useSimulationMovement)
{
@@ -58,7 +69,9 @@ namespace Simulation
MaxTargets = Mathf.Max(1, maxTargets),
ShapeType = shapeType,
Direction = normalizedDirection,
- HalfAngleDeg = Mathf.Clamp(halfAngleDeg, 0f, 180f)
+ HalfAngleDeg = Mathf.Clamp(halfAngleDeg, 0f, 180f),
+ HalfWidth = Mathf.Max(0f, halfWidth),
+ HalfLength = Mathf.Max(0f, halfLength)
});
return true;
diff --git a/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionResolve.cs b/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionResolve.cs
index 5caeb77..065d9e8 100644
--- a/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionResolve.cs
+++ b/Assets/GameMain/Scripts/Simulation/Jobs/SimulationWorld.CollisionResolve.cs
@@ -1,3 +1,4 @@
+using Components;
using CustomDebugger;
using CustomUtility;
using Definition.DataStruct;
@@ -188,7 +189,8 @@ namespace Simulation
continue;
}
- if (!IsAreaTargetInsidePreciseShape(in query, target))
+ float targetRadius = ResolveAreaTargetRadius(target);
+ if (!IsAreaTargetInsidePreciseShape(in query, target, targetRadius))
{
continue;
}
@@ -230,7 +232,24 @@ namespace Simulation
return false;
}
- private static bool IsAreaTargetInsidePreciseShape(in CollisionQueryData query, TargetableObject target)
+ private float ResolveAreaTargetRadius(TargetableObject target)
+ {
+ if (target == null)
+ {
+ return 0f;
+ }
+
+ if (target is EnemyBase && TryGetEnemyData(target.Id, out EnemySimData enemyData))
+ {
+ return Mathf.Max(0f, enemyData.EnemyBodyRadius);
+ }
+
+ MovementComponent movementComponent = target.GetComponent();
+ return movementComponent != null ? Mathf.Max(0f, movementComponent.EnemyBodyRadius) : 0f;
+ }
+
+ private static bool IsAreaTargetInsidePreciseShape(in CollisionQueryData query, TargetableObject target,
+ float targetRadius)
{
if (target == null || target.CachedTransform == null)
{
@@ -241,7 +260,7 @@ namespace Simulation
Vector3 toTarget = target.CachedTransform.position - center;
toTarget.y = 0f;
- float radius = Mathf.Max(0.01f, query.Radius);
+ float radius = Mathf.Max(0.01f, query.Radius + Mathf.Max(0f, targetRadius));
float radiusSqr = radius * radius;
float sqrDistance = toTarget.sqrMagnitude;
if (sqrDistance > radiusSqr)
@@ -249,6 +268,27 @@ namespace Simulation
return false;
}
+ if (query.ShapeType == CollisionShapeRectangle)
+ {
+ Vector3 forwardRect = new Vector3(query.Direction.x, query.Direction.y, query.Direction.z);
+ forwardRect.y = 0f;
+ if (forwardRect.sqrMagnitude <= Mathf.Epsilon)
+ {
+ forwardRect = Vector3.forward;
+ }
+ else
+ {
+ forwardRect.Normalize();
+ }
+
+ Vector3 rightRect = Vector3.Cross(Vector3.up, forwardRect);
+ float halfWidth = Mathf.Max(0.01f, query.HalfWidth + Mathf.Max(0f, targetRadius));
+ float halfLength = Mathf.Max(0.01f, query.HalfLength + Mathf.Max(0f, targetRadius));
+ float forwardDistance = Vector3.Dot(toTarget, forwardRect);
+ float lateralDistance = Vector3.Dot(toTarget, rightRect);
+ return Mathf.Abs(forwardDistance) <= halfLength && Mathf.Abs(lateralDistance) <= halfWidth;
+ }
+
if (query.ShapeType != CollisionShapeSector)
{
return true;
diff --git a/Assets/GameMain/Scripts/Utility/ItemDescUtility.cs b/Assets/GameMain/Scripts/Utility/ItemDescUtility.cs
index 35f582d..3a416ea 100644
--- a/Assets/GameMain/Scripts/Utility/ItemDescUtility.cs
+++ b/Assets/GameMain/Scripts/Utility/ItemDescUtility.cs
@@ -13,10 +13,13 @@ namespace CustomUtility
{
private static readonly Dictionary _paramsDict = new(StringComparer.OrdinalIgnoreCase)
{
- {"hitRadius", "伤害范围"},
+ {"hitHalfWidth", "横向半宽"},
+ {"hitRadius", "攻击半宽"},
+ {"hitHeight", "判定高度"},
+ {"hitCenterYOffset", "判定高度偏移"},
{"sectorAngle", "攻击角度"},
{"pierceLength", "前戳距离"},
- {"thrustDistance", "枪尖长度"},
+ {"thrustDistance", "前戳距离(旧)"},
{"forwardOffset", "前置偏移"},
{"rotateSpeed", "转向速度"},
{"attackDuration", "突刺时长"},
diff --git a/数据表/Entity/Weapon.xlsx b/数据表/Entity/Weapon.xlsx
index e3c59558c149db9baba48560715e40257c31e693..28a267a4ce5be1a17df58dc176dab62edfe05980 100644
GIT binary patch
delta 6036
zcmZ8lbx_o8wBBXu?(U9dmyi;qq(Qn{I;9a5_yNKa62cNLp`>&PNJvRa2!e#Ll!SzI
zr(C}8-a9wWJMS}d>Y4M$JM*4%VjW*O);5AMFB661kS$;q;F|_Sm@@i2a$m?n4(aeg
zko<#z?$%J`_xKeUchl}NCq=3Yx_vK{@*EaIudviTaxy^O=eQ?z={w4GXS2%}FrJpLiX#gK2y}u3W9b75m+j>yQ#Ec!7X=OnOqCQW
zI%bo9>OuY<cbED5f0o132vOj&+`bv
z&|PwFltFXvMViZfe<(AEH?+&+S)?3~11e*=5U9=MD$(blLPXPg##(UHXBb9s
z!KbnQiB~T!^l1Km>G@&6t>W3$zlM<>U)}ol2&C
zm(bfyp9Te>hP-8Ke&Fl>&k7ii{Zg@&&?7AY^;&|7npHhsRH(0KAUtutduC7NAjP=-O=!Sfz4CEx$N|T
zgO%!@4Ov(|f(~ai$b3wCqyY{q)>{RI?RjJn4i|uahL$7^wXwZZ`2I4W9{E!tr{I*!
z$8Ml65Iz>KHK53)Cot~w*{1XHM8j7$G=DT$^EYFX({Dy%$HlfgbLvF_*SQ>H3OQo-
zB@QE%rr=YN!@DcL!)I$R1vM)?&A-QM-FOw9E-n52i~g1(ZZr3YZ3IGMBz<_EFg5e^
zr_)P7|AVKzyMBbDYPN{uL47bDx|MCqSd0I)P+Y-qt=92SCUbSRtnB8qOlJp``p4@0
z+_?{m)aZJb_KsVP7CWB3yhMv{k@*@Oy0DEFt?$5@%@5Oqr@v?HnZ6kpNz0!#OX{gh
zaYmUXG4}EQ_&uB-hLhy3wkHMmJ$q7Afx%u09Q(;Izp`JzHVcSX6wnQ!A{T$ukdRTI
zR{VE2(RSH@VsX*Kuav!H(f#YQ6(F`of`g?WYh^!&(PDSee(Ts-|4l-QpjzKKZIy|`
zz=tAU?!Qcg-cMurXruXyes^Jt#%P)K>2fiE>T+1u9
zGZJYhEtfN%CO!?y!Z#5MpjBGkgYccig8@6L!7d}IZHAt8SANT>$rQ8kcfyvrj9*UI
zc-14nIG#s9UvfbnX>meKsKzC~$CF1OUPxi&cw^4M*b{#}wTW-JiRK9_w2Q|OYyzj%
zm}niEb$J(BTHTOu)unn@s_$=S@>2o
zw01qbq8N{Jh#|d=Z5lymBbXok)V^3LGw7J@n;|89Kqh+?L824FOZVU%wTg{bVfF!4
zt7)KONCR12+}pX>UZoL_-Y%sP&))A!P>j@#5=}%q2rS>*&0eGeS`DDp$Z=b%6A+qm~V#RURcDw!~wkfLRtK3=61uWk?li
zK$eV{8VPg!DO55MG#%epmNj22rgVyyWL^42xhT8T%XdeRC#ehwI(|T>;+ky?l}9JC
z5-`~L@UzLS83vOzEN=^@L!KpK6qV!H?
z_mOYiOfbR+o-Fn9E{`P?l#KdYkhA@P
ztw1>#O0bJn`vVpUo7M~}s;fqURpKrdbCRr9LDvZe4MwG_q#g_;x^HiZ=JKnqenJogd
zl5d*Ujn?3pRPPH~&_<#RG3B|pC7(*Zjhq4YK)&Pl46}AI-c4aVMvx9#Z)lPvsB9%n
zjv}zM{b7~*vD6*ODEL=fxVTh~+-v)dIGY~`G1t+HUlIPFNUfK7a!}N326MI3!<+e$
z%@@?6%%@p@LZlC~*NtAOhn6Hq(a=)}*Wj!yB$b{LDL?22pDkCp7`70BrcZ7CFP
zMMpl}(rAA!PF=YA+V&vV?|SrdbZew+@w_)64Nt8!QO`0_xu6^)B~Fl`cy)VWJYsmp
zFj^!Wx@?QN>c*_!I3@{bm!$D^(~LJ~J%yqxTql7Md7AIIql%ml__Qp$dQ*zTvbac1
zA3)grn5sDFcc(3FIgEsTi0hi?77F>L9FjL|JK>w#dPKQ%h|vUs)_y{(3YL&BZ>Z7^
zy4|3>pCjIWJ1V?{5~7pmMj{kq*eD7yi{C6@KU0$;jE$xCS0_gkqQs+mB87Lj*@W5b
z6q*1``>qhmv@`o1(+gqzp-^`+A*RS9BaTQB0ZWHe(0HP4gGE6jpT&uEX>CuizDh4G
zCQ&H=b}zOVAI>Y?y0ACbB$53bgtbVANOQYQPCnU914j%=JE(oR(`CLU#DBdL^C&-g9D5pQRf>pk+$aThc7v!-GS2flQK!>QXd?+awe1n+=U$FT8uX15
zYP{&RBJydfyep14-s=D*M@JKjg=&2$Qv+9NsHk+YcdJMpc$7)gv*rmToz7)-(Ao@li-lF
zkzVb5^-TSOg2l}lQQ-t-H0y(fcUwW#Q`9<@@@=i>>+r_NgWkGo77JruP!vN$->MMf
zu7>{iN1qT*wX2Cxn(C8BfD7Vaibd_q*QOEc@F+x5kzqxEtIJ$9E!z(9=U{onhr8cF
zhFdRsfE2np)lc>@JhYmWh=qk2oa&8iRpPe~4^J`ah-va-0%y)KYFPF_X=|s~fivc{
zy49K;0mCZe<9+QJ(#-TdLZq#WKZ(!<8Slso1ZvKgi=g-^KbUH8oaQMzxGgWQ(gmin
zyWKZ0UQ%75O#q>@?iv1ZoQ_on2`oJg!|~=JK;{|e$)z(%A;kP!H<$UJ?!HYW7{_EV
zhh3HQl+@i)|_wpd>nnEj!x*HK_qSzY5ngNv5Xb=$i1})KFmZIO4I1lMtP!q0p)?Ofo>z
zw2Z!dytHtc+6cp}x=%&{f9fbmu}{PpaIth}G$ar_qGG1Mut{OFO6~j>{JiWPw{sla
zhfu}Yx6aj~Kx8uYX;SfK2i0?ukYgodZ`E`i5Vm$8iJ9;wuv}+3
zVXJUGzQx+EdWePIHKQQFJ`a!yL-M{lrq3*^_%VJ;w53mYfljD)7dvU9v&y~7AD5a>
zfZ3|p`?V6EpMDIc%@q3`5a+Kmg`1zqoLeDHWHpoR+dk{DoyG5E4G^9DaWkk9uvKaF
zD2H9uL|&;>O;--K77RK{fb0GOL@j0%P1#Nwcg7Q1TT2>Qhic_FZq;5=-_rh86`>|N
z(;rA^>>aPD6xOXjFt}<;{WmhfIb*2C>Fw5%+tR;De76?r#|Q+dmiOb-e@A7x_LHCv
zOkb{DJ~CCHSTSO2(51|sbC)ufi`r0IVrDEH`_7PG0%*VqpANd#Ox1sj*vKMg)QD3$
zisO8`PDDLBXBR~H+F@|b_vTG&O`hB&SZ#CKR8#gG8LC1VsDpXRZOeuWw`WG6?=K
zE+c=cexPUa(w~+ony|0#0hKGtWm8?*-DL`4Y_P32a@?AE@kNsjRX3KcsKIW7&li!y
zSHu~A0HlnFZ=9y2e%;{UdhjG(cJX
z=#&;4`uo=);4)XY*mTkd`>4n0nx9~zZ-BfZVvKNi|s
zIxGka!#*C+F0%_HCkgB8YeeuW1IElu_&)_Ka{36pQ_=@|o4*d(hrh0r;ignJW`Cu61PrFdpi>Bt(C
z!`kZ}ZY^(o6fKzukE1VoV*0GBZUgQ0rD~G#$*38F?)#C*aOypKfO5XOpRGji@75b#?sSs_i;ki7;J;>~-KL9%NG8yJO%{mh7cEd$N+DgpujL|-KHGuZ|D*gT-NF8Zm&z8X~BH>j>
zI!{kfcM~2KPjlsbUh>=^@GJCD-q;AK)Gg;!w+IGyqfAP@T)X7UH}SPO;lKD*9nOC3
z&a$v*@81T#I0r^|ZgmXIGHCE6^vKp}9L7n*H9mG%ZOzJQJ2%}SOMC47Q&(Ipj%9w8
zki7qJUSEaiF77>9tv?CNAi~ciqGua_9BL?qTo}vgUP>gR!
zN{av?QtXFQ^i2V4N?5++rxKg#?(rV3q&-s5fqQ9oJOYIddI`U_wibfsy!Adq_~uyrR;x9e-&v!DGKEfu#oGEga7kmR{7
zi5q(T?IJWZWL2{D?Hk7__uxZ!Zw9U(0x|itcQvAIM8@qC>Vf`;&IC4McNMME@Q`7p
zIiEd$K_?JBc9$)ulALl>6U_&TM4_L%W}$vNQ4$lcJyRF~iv1u&z6%gC)3jq@9m_Vy
zE)@pZ<&9CNa9OiWJx&CPhxe2So$Y?QDD1gKgn7I0raJ|6nvEM)7>~PO%(
zAxI;gkN5q&Yd!D&u-7{4I{WwOtaHwFMcKcwuW10|)pTvUp_{?1K(`)_7`5st_f7BH
z%R(Uzyj-NlR89IPyu^MBH*4xo30yH8br6KNnF}wnYHak$|GebJpKoe=qczG@_h6
z2yg4qketGek~)I6WaKy`ysEUgDVF_Lb5R_23|e8gXI&S}2x^Tv
zF(iT0k9H)zOf9Gnun>^)p>QlQS%@vpv+eDZE5LnzXQB@xo4w$T0Nd7c6p^9<-v-E;t=+W21%mZ5FB^#S9(bP48cO`cwtJmpn-Ie0!*7!*4~;^^
zJ6=witRNn@QiazUL%0P!tma&C`lDKKU;S)<*3ca(qd+!nmcMpSe`a=Oe)8Al@aI1+
z@jya^8J$zjy<&fGo2*n_37pZ(fXzd0
zO`S`l30oePtfSKx=8C4j)+RMP-d?@k7p7(v&787uiHb#m2OO6imp4Q+b>=w
z^^W(c!xxTq2y*odZE4gm8e+%oX#;*HgD`ed8>f4)K9oa*21XDn@i^mpY|QE?k0@I+
z>>`A+E>&+PvAQMRaDKO$Ekx
zE~`~{+tMqr8=gO7ESIHMUrt!~h2%5=*UbSBz4CIoK3k+ua^y5EY3y`#US5Bk7k}R|
zMF@L|OPhTBNe&P-T|5iRyQ`5lbysMRGF9$8;6#B48e4Px66UclL#TwjODjUw>w0vKzAR
zidgazLt^ETlSZRXV%lau7r#UBm$y6ItiMVkSg_@tv5c@E;@MrqXT6_DZeDQO&kCe%mwAAEkE;Xt
zJ7XO$c9emym;DhaOtF4uJ;&v0Mm^u??V#VbsORVB;wOZ13aQ&`U0to;TD@3;Yy$l_
zT@JOpKs>vt3cMwc%uxDe0|BG0dTAjvGxZ+=){xFm2A>3JGM5p)jffUqvl(f{7+J+|jZ
zyej9AE0znTw-Ig3;SI*emD9n)o#`*GE#u@0XZ&qX6wpI~kV&CNt#nYu
zjys~b!Y0qC1GHRmHLOGmdK$xjl#2NtYZt}p694%+`;zg-u(YR?>U^}%SoS0)?QoaL
z+6YJ}G)#0#I+6T3jB^}ds=WMao6{>K{upW36IGpGBoJh4m?kMah}&BUB%wr|AOxjCj8plHt$@c=sY
zTEtc&AzEyTK^rYL!H~jjTPbeJ&7q=cVGi)}zGH&ommF&vzNwr06w6y}4lU8HByada
zOXQxGh4Z6VL6`zf$!=eoPL&`OoBe(Jm-jj?B;EVKd#D{1$qCfJQ*X7Q_}M5_WM!ow
zwVnO7_fS7fi99Hs`n@nb>A9}f)K`ot-8hr0Z+9$!pyTF%Ny<0C;YnkH)W$2`0JP+t
z=a+R_T8P%%U*7MXZkSTvhY#@7{uEk>p`*PaL7p^wQ3EQnuC*2p_bcF3eV+jy+MR}N
z*1a=&w?JXE0|mJ|B2R;f!K_-CEePu?a0h9jbtx6DDkNew+0g0zdrxpd3f?n|O@ty13CxT&
z&kHf>UX(N<{Uz~FRWfT5$1pE4V}93FN|LzncQ=}n=yH)2Sa`v28UflNe+krfo2Jq7
z$K0Z<`$o0AiW`Wc-1!%EqI@x>={N;$`?zgzt~GWjcG0lN@SscCjdDNSZmR&Ettg1Wm<23E!`v&U-kAs^(*)Zl0;Ctei{`$hG2K`l*0C%zDa
zilj05PE^aUO@lIt2e=g~_A$db#>V&tl2s;xDqijq;}Zc~Rci!p+JBFNw?3MhjChfg
zPvM>$qllqhsSV{~+4?^)3tyxmiKYl8}
zeO3Mm$<^4-v)hv%Ie16;oV;Dfa)7(svd(sA)q|HfHU+rrWI4E^=t=%L(}tSrMu&kp
z_Q4XF2fywztI2Y=ez9ChJxsfk5>Uq@oZ=G$q4w|KAt;k_6pKCjg)*x_8nExY!IPLH&fbBM$x>&lmFe#LJwx
zxR;ao*;Ss4sc@E3au#-j9#Bpx^tx_#dd$6d^4sG0Q#bf}-XH6a#j5TBtHDEfMI&5N
z#idl5CQ9Gf8x#I89G`3~uL)H2bbV^vq)fc;Of1&0Bn9X>l2HFhc%tSDf`sPw3j&1a
z+}f!)Juq=KjC-WnL|Z?EI_H5m1RduQP7evhTy&(b&GxY6mo!FYM4IKyp+|5?3J-f
zLdz<~rBBDa2iZ-3;Qit$k@A!f6tI3uUNixQ$-a{PWDw{igtKl7FLYLcJA6(UF);Gs
zVB9Hn+IcB(!D7;MAj<$3BQWSqjpwd47=5WeVpfYxZ9OgO=dkW6uq%4%ydl9Ozg>GD
z8Dt}9RN=5edq8BONRsoiH!GTqilmUZ8hiKF%r)l`H;sjBu%GWk`=cybRs6x;dfh+44aZ4yXn?BX-8(l2$;lq2*;xK
zpa_01+n}^g%t7BKI`o`HUcM5aNu4T?3>+234+z!ItrSD5cQq@~os|d8p>g3n?)e9j
zIpN(CYz2>8Ll&S}!xJB00R3g;ZS0Ld%eoJ%RMO{btY)Ihb>nJ|KUxJGn>Pt*MX$B~
zX_KWP#N*gHf4&$oalJmuBXJfy*s{_@A+yXQl1xX8ps7Gk`XvY=PD%+Sgb%svV+^
zcl5sMs-;WU_sW^gu4aqwfUQS2IfC
zK)A@>lp-J?ke&|ufSCqxai5=NxRJNMjj*l>rFS-J+gN{!wVB0(_;iKab%nbTImOk*
z8NO9B%vmT>Y?SbwVZ)7a9X6#ln_(Z87hIesbs3l3EQq7NoV@8-^d=khPWIvbLdjR6
zYICoMw__|gc(K=;a>E+sw0}0t-ikjf9;?S8v=^_a&u_
zdz7rrzf%w&Td2M>cGg<*lFhm2dMp#DcwxHV?@{yR;1XBBi;Q_DE?F!~@L0yYnk6XA
znx)D5H?I@-fQJ+=@ZDcJg7u=^>CPMK&KYm%lSGuWF7Y-KS-hUn;_OLGXgw>+@H1iq
zq-UzoB)rpIp(?x$4J=@;adrr^X~14-=&jy|6+B>{Zh4Y(2&CjMkBeC4@4XdEQm_?F
zwv3ejnvJ(GAB3^d{3&dyp-0eyd7FZdPKuKG
z?gzWc$}WXT6+i$aA-8?W+_99e9;^X#tYoX5rx33pi%u>%5&U-q0#z}M*EmL&B;k=DK8hL=1Vg_d{q
zUz+tpMg@-BwF*@`x|HwS3fzC|efUv&PuQny@X1ea<|tq>=c()5I{~$^~l1m56N`8mQDbjOYvjclzRbqv{lK~6?$9s
zUOz;MNmT=$IW2QL4&JIQ5_fJ)i?_6BuHUOYZf$gLJbhdYeQ98oS9Sf}VOBPAnDI}G
zN=upZ+W{rzUqcS`(4DGm{(oOCH5O2SHjSrqhgY=A%Xj`9Lu|e!nvA=?R}uGE=DXe3|&zimfS-F7PYX
z9Dp-x8>MB%MX2$seaB2CRhV*!jcr-}jO9i7^vkajA!C0u+vj;(2Wedy_+2Ux7G!E&A`(8UwUTJWN0X7IWArfqX;UdHm=7V-t>
zw@{lx9`7ZDt_v49sx2(QR=(F-obo$60N=FDJ#Zy|6yL2sBb#R*?-6{uq#i43z`&Zm
zo*Obd+lcrzzBPYW@N`aT)o0wsldxmX4&^`fDpGRaD~Px$fT2AA(d3^+1rW5^7X^5e
z{?idLq#DWwuGZF+B3j}0P0$#l-HNW$1h4E`+S>PcE=bxE5;OXSY%sJ~g-viyzD~sz
zX>3R!SYc-{G+s19HSN%CniXZj@Duh7{3
zXs8w8q-uJX3Gz+A{B<9*M_T6RzNUepmGFd0Sz+qG3P6akTk|aBtvmd8gcN1@ciHvt
z$aNsUK9Waaz?euJ8hPPFSAdtM)*j&eiIHSsMPM+IXpX%TVU@SITvjJuG7%le_*OU9
zy(^7X%Dty#gjpB+gb7|S5*beWLlh$XV<)6nuIWwZv_M5i7G<%|Y2oKbrPF*&|$0SJ20UbLisl=YM=A
zH+FM|m!Wf$rM${+R6Lw7ilVIBxGKG!TF^>xE=F5Zz4(j2k1gIA-5mPxtA@833u
zhMf+s&Ya$n!zjb-2fRg%ko3UG=ImznuT#B((0%Oe2if2L@W3__`S;$3)t&hTZ3Vn4
z*TnoFTw|pU?A;stt5>HyoeDW2XCGC_eCg;P9b$SiO3*2KScMy9_6WkxMBs(!l_R-4
zc!m2R>;ByMDphe6|0O|~hRxbY@RPR+O{oxVq_!LhG&WF#WkK8NnoXjCe4Xbv4-I0~tt9&FdM_k?d_H9@
zoB8uThWqWOv--)SpnTHYRMKA$frFzjy~feMBDq(4&JxDPZakq<@?imcS1aUWVds1$
zLQ>r*r2**sL!oENxzJv3{FWb)f{Y2sfSV22%@l3#{_9BL#xBMP8Q__Pvuy2h93fcI
z!Pt;qma#CJZ49b+hg+PVI>tT42Wg(gKm);5Ct2|oV{vRwe;`XiK&Ch_HiCiE=hwHS
z*5?=$ipqNXvU!Ua!;k^%jIeb`Q+i{i^w^Vt_$zkAU(peXZwx~JrSyu^O_$?~<``%q
z?~%AxkRrEuUjUHR=mq!j-XoG(5;jNi=D!RqBMj@HhU(cLk>0O@yE#&T90Ca|B*M
z;DX5z#aht+$*=ZqXFBa
Z`^8kjk!X_ps$h4t;e7#u3BiB({{f%2ERg^J