Checkpoint 5 & Checkpoint 6

This commit is contained in:
SepComet
2026-02-22 22:00:43 +08:00
parent 5fb7ea499f
commit f0abf78128
65 changed files with 3875 additions and 137 deletions
@@ -0,0 +1,81 @@
using UnityEngine;
namespace Entity.Weapon
{
public sealed class LightningStrikeAttackEffect : IWeaponAttackEffect
{
private readonly float _duration = 0.22f;
private readonly float _yOffset = 0.2f;
private readonly float _ringLineWidth = 0.045f;
private readonly float _boltLineWidth = 0.08f;
private readonly int _ringSegments = 32;
private readonly float _boltHeight = 3f;
private readonly Color _ringColor = new(0.35f, 0.82f, 1f, 0.92f);
private readonly Color _boltColor = new(0.85f, 0.96f, 1f, 0.97f);
public void Play(WeaponBase weapon, Vector3 position, EntityBase target, float radius)
{
float safeRadius = Mathf.Max(0.1f, radius);
Vector3 center = new Vector3(position.x, position.y + _yOffset, position.z);
GameObject root = new GameObject("LightningStrikeEffect");
root.transform.position = center;
Shader shader = Shader.Find("Sprites/Default");
if (shader == null)
{
shader = Shader.Find("Unlit/Color");
}
Material ringMaterial = new Material(shader);
ringMaterial.color = _ringColor;
LineRenderer ring = root.AddComponent<LineRenderer>();
ring.loop = true;
ring.useWorldSpace = true;
ring.positionCount = _ringSegments;
ring.startWidth = _ringLineWidth;
ring.endWidth = _ringLineWidth;
ring.startColor = _ringColor;
ring.endColor = _ringColor;
ring.material = ringMaterial;
float step = Mathf.PI * 2f / _ringSegments;
for (int i = 0; i < _ringSegments; i++)
{
float angle = i * step;
Vector3 offset = new Vector3(Mathf.Cos(angle) * safeRadius, 0f, Mathf.Sin(angle) * safeRadius);
ring.SetPosition(i, center + offset);
}
GameObject bolt = new GameObject("Bolt");
bolt.transform.SetParent(root.transform, false);
Material boltMaterial = new Material(shader);
boltMaterial.color = _boltColor;
LineRenderer boltLine = bolt.AddComponent<LineRenderer>();
boltLine.loop = false;
boltLine.useWorldSpace = true;
boltLine.positionCount = 4;
boltLine.startWidth = _boltLineWidth;
boltLine.endWidth = _boltLineWidth * 0.55f;
boltLine.startColor = _boltColor;
boltLine.endColor = _boltColor;
boltLine.material = boltMaterial;
Vector3 top = center + Vector3.up * _boltHeight;
Vector3 middleA = center + Vector3.up * (_boltHeight * 0.66f) +
new Vector3(Random.Range(-0.18f, 0.18f), 0f, Random.Range(-0.18f, 0.18f));
Vector3 middleB = center + Vector3.up * (_boltHeight * 0.3f) +
new Vector3(Random.Range(-0.12f, 0.12f), 0f, Random.Range(-0.12f, 0.12f));
boltLine.SetPosition(0, top);
boltLine.SetPosition(1, middleA);
boltLine.SetPosition(2, middleB);
boltLine.SetPosition(3, center);
Object.Destroy(root, Mathf.Max(0.01f, _duration));
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 81cbdf8961ad419c91b989d56ca782d0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -220,6 +220,32 @@ namespace Entity.Weapon
return AIUtility.GetSqrMagnitudeXZ(this, target) < sqrRange;
}
protected bool TryQueueAreaCollisionQuery(in Vector3 center, float radius, int maxTargets = 16)
{
var simulationWorld = GameEntry.SimulationWorld;
if (simulationWorld == null)
{
return false;
}
int ownerEntityId = WeaponData != null ? WeaponData.OwnerId : Id;
return simulationWorld.TryEnqueueAreaCollisionQuery(Id, ownerEntityId, in center, radius, maxTargets);
}
protected bool TryQueueSectorCollisionQuery(in Vector3 center, float radius, in Vector3 direction,
float halfAngleDeg, int maxTargets = 16)
{
var simulationWorld = GameEntry.SimulationWorld;
if (simulationWorld == null)
{
return false;
}
int ownerEntityId = WeaponData != null ? WeaponData.OwnerId : Id;
return simulationWorld.TryEnqueueSectorCollisionQuery(Id, ownerEntityId, in center, radius, in direction,
halfAngleDeg, maxTargets);
}
protected void SetTargetSelector(TargetSelectorType selectorType)
{
TargetSelector = CreateSelector(selectorType);
@@ -286,4 +312,3 @@ namespace Entity.Weapon
}
@@ -27,7 +27,7 @@ namespace Entity.Weapon
[SerializeField] private LayerMask _hitMask = ~0;
[SerializeField] private int _maxHitColliders = 32;
private IWeaponAttackEffect _attackEffect;
private Collider[] _hitResults;
private readonly HashSet<int> _hitEntityIds = new();
@@ -116,7 +116,15 @@ namespace Entity.Weapon
private void ApplyGroundAreaDamage()
{
if (_hitRadius <= 0f || _hitResults == null || _hitResults.Length == 0) return;
if (_hitRadius <= 0f) return;
if (TryQueueAreaCollisionQuery(_attackCenter, _hitRadius, Mathf.Max(1, _maxHitColliders)))
{
_hitEntityIds.Clear();
return;
}
if (_hitResults == null || _hitResults.Length == 0) return;
int hitCount = Physics.OverlapSphereNonAlloc(_attackCenter, _hitRadius, _hitResults, _hitMask,
QueryTriggerInteraction.Collide);
@@ -149,12 +157,14 @@ namespace Entity.Weapon
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
_cachedRotation = CachedTransform.rotation;
string hitRadiusRaw = _weaponData.GetParamsString(HitRadiusParamKey);
if (!float.TryParse(hitRadiusRaw, out _hitRadius))
if (_weaponData.TryGetParam(HitRadiusParamKey, out string hitRadiusRaw))
{
_hitRadius = Mathf.Max(0.1f, float.Parse(hitRadiusRaw));
}
else
{
_hitRadius = _weaponData.AttackRange;
}
_hitRadius = Mathf.Max(0.1f, _hitRadius);
_hitRadiusSqr = _hitRadius * _hitRadius;
_attackEffect = new KnifeRangeAttackEffect();
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5fded69986744ded97edb5f2ac06304f
timeCreated: 1771578597
@@ -0,0 +1,31 @@
namespace Entity.Weapon
{
public partial class WeaponLightning
{
private class AttackState : WeaponStateBase
{
private WeaponLightning _weapon;
public override WeaponStateType State => WeaponStateType.Attack;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLightning;
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()
{
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 25854832a7d3416dacab67bcfef2a2fa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,49 @@
namespace Entity.Weapon
{
public partial class WeaponLightning
{
private class CheckInRangeState : WeaponStateBase
{
private WeaponLightning _weapon;
public override WeaponStateType State => WeaponStateType.Check_InRange;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLightning;
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()
{
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 25404ad172434ba38609e797b6d96919
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,39 @@
namespace Entity.Weapon
{
public partial class WeaponLightning
{
private class CheckOutRangeState : WeaponStateBase
{
private WeaponLightning _weapon;
public override WeaponStateType State => WeaponStateType.Check_OutRange;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLightning;
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()
{
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f3aa479abb79491d83e8de76806faf7a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,33 @@
namespace Entity.Weapon
{
public partial class WeaponLightning
{
private class IdleState : WeaponStateBase
{
private WeaponLightning _weapon;
public override WeaponStateType State => WeaponStateType.Idle;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponLightning;
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()
{
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3464037c29774c3ea326dc93fe33551e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,272 @@
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 WeaponLightning : WeaponBase
{
private const string HitRadiusParamKey = "HitRadius";
private WeaponLightningData _weaponData;
private Quaternion _cachedRotation;
[SerializeField] private float _rotateSpeed = 5f;
[SerializeField] private float _takeOffDuration = 0.3f;
[SerializeField] private float _flyToTargetDuration = 0.2f;
[SerializeField] private float _strikeDuration = 0.1f;
[SerializeField] private float _returnDuration = 0.22f;
[SerializeField] private float _hoverHeight = 15f;
[SerializeField] private LayerMask _hitMask = ~0;
[SerializeField] private int _maxHitColliders = 32;
private Sequence _attackSequence;
private Transform _attackParent;
private Vector3 _lockedStrikePoint;
private Collider[] _hitResults;
private readonly HashSet<int> _hitEntityIds = new();
private float _hitRadius;
private float _hitRadiusSqr;
private IWeaponAttackEffect _attackEffect;
public override ImpactData GetImpactData()
{
return new ImpactData(_weaponData.OwnerCamp, _weaponData.Attack, AttackStat);
}
protected override void BuildStates()
{
RegisterState(new IdleState());
RegisterState(new CheckOutRangeState());
RegisterState(new CheckInRangeState());
RegisterState(new AttackState());
}
protected override void Attack()
{
StopAttackTween(false);
FaceTargetImmediately();
_isAttacking = true;
_lockedStrikePoint = ResolveStrikePoint();
_attackParent = CachedTransform.parent;
CachedTransform.SetParent(null);
Vector3 takeOffPosition = CachedTransform.position;
Vector3 hoverPosition = _lockedStrikePoint + Vector3.up * Mathf.Max(0.1f, _hoverHeight);
_attackSequence = DOTween.Sequence();
_attackSequence.Append(CachedTransform.DOMove(takeOffPosition, _takeOffDuration).SetEase(Ease.OutQuad));
_attackSequence.Append(CachedTransform.DOMove(hoverPosition, _flyToTargetDuration).SetEase(Ease.OutSine));
_attackSequence.AppendCallback(() => CachedTransform.LookAt(_lockedStrikePoint));
_attackSequence.Append(CachedTransform.DOMove(_lockedStrikePoint, _strikeDuration).SetEase(Ease.InQuad));
_attackSequence.AppendCallback(() =>
{
_attackEffect?.Play(this, _lockedStrikePoint, _target, _hitRadius);
ApplyGroundAreaDamage();
if (_attackParent != null)
{
CachedTransform.SetParent(_attackParent);
}
});
_attackSequence.Append(CachedTransform.DOLocalMove(Vector3.zero, _returnDuration).SetEase(Ease.OutSine));
_attackSequence.AppendCallback(() =>
{
_isAttacking = false;
_attackSequence = null;
_attackParent = null;
});
}
protected override void Check()
{
_target = SelectTarget(_sqrRange);
}
private Vector3 ResolveStrikePoint()
{
if (_target != null && _target.Available)
{
return _target.CachedTransform.position;
}
return CachedTransform.position;
}
private void ApplyGroundAreaDamage()
{
if (_hitRadius <= 0f) return;
if (TryQueueAreaCollisionQuery(_lockedStrikePoint, _hitRadius, Mathf.Max(1, _maxHitColliders)))
{
_hitEntityIds.Clear();
return;
}
if (_hitResults == null || _hitResults.Length == 0) return;
int hitCount = Physics.OverlapSphereNonAlloc(_lockedStrikePoint, _hitRadius, _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;
Vector3 delta = targetable.CachedTransform.position - _lockedStrikePoint;
delta.y = 0f;
if (delta.sqrMagnitude > _hitRadiusSqr) continue;
AIUtility.PerformCollision(targetable, this);
}
}
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);
}
protected override bool OnWeaponShow(object userData)
{
_weaponData = RequireWeaponData<WeaponLightningData>(userData);
if (_weaponData == null) return false;
WeaponData = _weaponData;
_currAttackTimer = 0f;
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
_cachedRotation = CachedTransform.rotation;
_hitRadius = ReadPositiveParam(HitRadiusParamKey, _weaponData.AttackRange);
_hitRadiusSqr = _hitRadius * _hitRadius;
int colliderCapacity = Mathf.Max(1, _maxHitColliders);
if (_hitResults == null || _hitResults.Length != colliderCapacity)
{
_hitResults = new Collider[colliderCapacity];
}
_attackEffect = new LightningStrikeAttackEffect();
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 float ReadPositiveParam(string paramName, float defaultValue)
{
if (_weaponData.Params != null &&
_weaponData.Params.TryGetValue(paramName.ToLower(), out string rawValue) &&
float.TryParse(rawValue, out float parsedValue))
{
return Mathf.Max(0.1f, parsedValue);
}
return Mathf.Max(0.1f, defaultValue);
}
private void StopAttackTween(bool resetTransform)
{
if (_attackSequence != null)
{
_attackSequence.Kill();
_attackSequence = null;
}
_isAttacking = false;
if (resetTransform)
{
if (_attackParent != null)
{
CachedTransform.SetParent(_attackParent);
CachedTransform.localPosition = Vector3.zero;
}
else if (CachedTransform.parent != null)
{
CachedTransform.localPosition = Vector3.zero;
}
CachedTransform.rotation = _cachedRotation;
}
_attackParent = null;
_hitEntityIds.Clear();
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a8315d9e60c4434ebde9b23c853f27d0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -93,12 +93,8 @@ namespace Entity.Weapon
private void ApplySectorDamage()
{
if (_attackRadius <= 0f || _hitResults == null || _hitResults.Length == 0) return;
if (_attackRadius <= 0f) return;
int hitCount = Physics.OverlapSphereNonAlloc(_attackCenter, _attackRadius, _hitResults, _hitMask,
QueryTriggerInteraction.Collide);
_hitEntityIds.Clear();
Vector3 forward = CachedTransform.forward;
forward.y = 0f;
if (forward.sqrMagnitude <= Mathf.Epsilon)
@@ -107,8 +103,20 @@ namespace Entity.Weapon
}
forward.Normalize();
float halfAngle = _sectorAngle * 0.5f;
if (TryQueueSectorCollisionQuery(_attackCenter, _attackRadius, in forward, halfAngle,
Mathf.Max(1, _maxHitColliders)))
{
_hitEntityIds.Clear();
return;
}
if (_hitResults == null || _hitResults.Length == 0) return;
int hitCount = Physics.OverlapSphereNonAlloc(_attackCenter, _attackRadius, _hitResults, _hitMask,
QueryTriggerInteraction.Collide);
_hitEntityIds.Clear();
for (int i = 0; i < hitCount; i++)
{
Collider collider = _hitResults[i];